authorgravatar for clembs@clembs.comClément Voisin <clembs@clembs.com> 2024-09-21 09:21:48+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2024-09-21 00:21:48-07:00
log5f480e3bc33c572ac26aefc905d9426cad259954
treed2a2feee03a169ca6c485879f9974e90a511a0b7
parent5556f630ad43a64e22a760df66534eb135dd9671
signature Signed by PGP key B5690EEEBB952194

feat: new language manual design


18 files changed, 2034 insertions(+), 962 deletions(-)

CHANGELOG.md+1
......@@ -1,5 +1,6 @@
11## 0.1.7
22
3- chore: rename the language to "ix"
34- feat: allow any expression as a function body
45- fix: coerce `break` operands and implicit breaks to result type
56- fix: coerce results when `if` expression branches merge
README.md+21-8
......@@ -1,21 +1,34 @@
1# New Language Project
1# Ix
22
3This is a high-level, general purpose programming language. It is a mix of Rust,
4Zig, and JavaScript's great points.
3Ix (**I**nferable E**x**pression) is a high-level, general purpose programming
4language. It features a mix of Rust, Zig, and JavaScript's great points.
55
6For a guide on how to use the language, please see the Language Manual included
7in the binary distribution. (TODO: host a rendered version)
6For a guide on how to use Ix, see the manual included in the binary
7distribution. Additionally, the latest commit of the manual is hosted online at
8[language-manual.pages.dev](https://language-manual.pages.dev).
89
910## Building
1011
11The primary dependency of the language SDK is a Zig compiler. If one is not
12The primary dependency of the Ix compiler is a Zig compiler. If one is not
1213available, the `./build` and `.\build.cmd` scripts can substitute for running
1314`zig build` on POSIX and Windows systems, respectively. With a Zig compiler, a
1415release build can be produced in a single command:
1516
1617 zig build --release
1718
18To run the standard packaging procedure, which builds the SDK for all officially
19supported targets:
19To run the standard packaging procedure, which builds the compiler for all
20officially supported targets:
2021
2122 zig build package
23
24## Development
25
26Continuous integration is provided via a `./ci` (script for POSIX), which is run
27by [self hosted Buildkite runners][ci]. This script uses the [`typos`][typos]
28CLI to check spelling mistakes and [`dprint`][dprint] for code formatting. It is
29recommended to setup editor integration with these two tools to prevent pushing
30broken commits.
31
32[ci]: https://buildkite.com/paperdave/compiler
33[typos]: https://github.com/crate-ci/typos
34[dprint]: https://dprint.dev
build.zig+15-24
......@@ -18,8 +18,8 @@ pub const Options = struct {
1818 config: BuildConfig,
1919};
2020
21/// `createArtifactGroup` multiple times in order to have different
22/// targets and configurations of the SDK at the same time.
21/// `createArtifactGroup` is called multiple times in order to have different
22/// targets and configurations of Ix at the same time.
2323const ArtifactGroup = struct {
2424 build: *Build,
2525 opts: Options,
......@@ -446,10 +446,10 @@ pub fn build(b: *Build) !void {
446446 test_all.dependOn(run_step);
447447 }
448448
449 // Language Reference Generator
449 // Language Manual Generator
450450 const manual_generator = b.addExecutable(.{
451 .name = "generate-man",
452 .root_source_file = b.path("src/manual/generate-man.zig"),
451 .name = "generate_manual",
452 .root_source_file = b.path("src/manual/generate_manual.zig"),
453453 .target = b.graph.host,
454454 .optimize = .Debug,
455455 .link_libc = true,
......@@ -533,30 +533,21 @@ pub fn build(b: *Build) !void {
533533 .version = version,
534534 .have_embedded_quickjs = have_embedded_quickjs,
535535 },
536 .optimize = optimize,
536 .optimize = .ReleaseSafe,
537537 .target = cross_target,
538538 .no_llvm = no_llvm,
539539 });
540540
541 inline for (.{
542 .{ artifacts.cli, "compiler" },
543 }) |entry| {
544 const artifact, const bin_name = entry;
545 package.dependOn(&b.addInstallFile(
546 artifact.getEmittedBin(),
547 b.pathJoin(&.{
548 "package",
549 name,
550 b.fmt("{s}{s}", .{
551 bin_name,
552 cross_target.result.exeFileExt(),
553 }),
554 }),
555 ).step);
556 }
557
541 package.dependOn(&b.addInstallFile(
542 artifacts.cli.getEmittedBin(),
543 b.pathJoin(&.{
544 "package",
545 name,
546 b.fmt("ix{s}", .{cross_target.result.exeFileExt()}),
547 }),
548 ).step);
558549 package.dependOn(&b.addInstallFile(b.path("CHANGELOG.md"), "package/CHANGELOG.md").step);
559 package.dependOn(&b.addInstallFile(manual_out, "package/README.html").step);
550 package.dependOn(&b.addInstallFile(manual_out, "package/ix.html").step);
560551 }
561552
562553 {
ci+10-5
......@@ -11,9 +11,11 @@ commit_short=$(echo "$commit" | cut -c -9)
1111./build --version
1212typos
1313dprint check
14./build all --summary new
1514
1615if [ -n "$BUILDKITE" ]; then
16 # perform the entire build
17 ./build all --summary all
18
1719 pushd zig-out/package
1820 zip -r compiler-$commit.zip *
1921 buildkite-agent artifact upload compiler-$commit.zip
......@@ -21,12 +23,15 @@ if [ -n "$BUILDKITE" ]; then
2123
2224 rm -fr zig-out/site
2325 mkdir zig-out/site
24 cp zig-out/package/README.html zig-out/site/index.html
25 branch=$BUILDKITE_BRANCH
26 cp zig-out/package/ix.html zig-out/site/index.html
27 branch="$BUILDKITE_BRANCH"
2628 if [ "$branch" = "dev" ]; then
2729 branch=main
2830 fi
29 CLOUDFLARE_API_TOKEN=$(buildkite-agent secret get CLOUDFLARE_API_TOKEN) \
30 CLOUDFLARE_ACCOUNT_ID=$(buildkite-agent secret get CLOUDFLARE_ACCOUNT_ID) \
31 CLOUDFLARE_API_TOKEN="$(buildkite-agent secret get CLOUDFLARE_API_TOKEN)" \
32 CLOUDFLARE_ACCOUNT_ID="$(buildkite-agent secret get CLOUDFLARE_ACCOUNT_ID)" \
3133 wrangler pages deploy zig-out/site --branch "$branch" --commit-hash "$commit" --no-bundle --project-name language-manual
34else
35 # perform only the tests, with a shorter summary
36 ./build test --summary new
3237fi
ci.cmd created+14
......@@ -0,0 +1,14 @@
1@ECHO OFF
2@REM Continuous Integration Script
3@REM This is a local-only version of ./ci as none of the
4@REM self-hosted CI runners use Windows. This is just
5@REM for local verification of builds.
6
7typos
8if %errorlevel% neq 0 exit /b %errorlevel%
9
10dprint check
11if %errorlevel% neq 0 exit /b %errorlevel%
12
13build.cmd test
14if %errorlevel% neq 0 exit /b %errorlevel%
doc/language-manual.md+196-59
......@@ -1,54 +1,65 @@
1# New Language Project
1# Ix Programming Language
22
3This is a high-level, general purpose programming language.
3Ix (**I**nferable E**x**pression) is a high-level programming language for
4developing web applications and games.
45
56Key goals of this project are:
67
7- **Ease of Use**: Use of type inference keeps code concise and easy to read and
8 write.
9- **Fast Iteration**: The compiler is designed for quick incremental compilation
10 and hot module reloading. (note: incremental is not yet available)
11- TODO: some more
8- **Ease of Use**: Use of type inference and simple syntax makes code easy to
9 read and write.
10- **Fast Iteration**: Incremental compilation keeps development productivity
11 high.
12- **Safety**: Compiler errors catch most mistakes at compile-time, and
13 additional tooling to catch mistakes at runtime.
14- **Universal**: One codebase can seamlessly target different web browsers and
15 native targets.
1216
1317It primarily focuses on merging some great points of Rust, Zig, and JavaScript's
1418language designs:
1519
16- **Rust** - Expressive control flow that is easy to write.
17- **Zig** - Execute any code at compile time; everything is an expression.
18- **JavaScript** - Garbage collected values with a versatile type system.
20- [**Rust**](https://www.rust-lang.org) - Expressive and concise control flow
21 that is easy to write.
22- [**Zig**](https://ziglang.org) - Execute any code at compile time; everything
23 is an expression.
24- [**JavaScript**](https://developer.mozilla.org/en-US/docs/Web/JavaScript) -
25 Garbage collected values with a versatile type system.
1926
20## Standard Library
21
22Currently, there is no standard library. One will be introduced later.
27This manual is a standalone HTML document. Code samples are compiled and tested
28as a part of the release cycle.
2329
2430## Hello World
2531
2632```lang:run
2733let print = @extern("console").log;
28
2934fn main {
3035 print("Hello, world!");
3136}
3237```
3338
3439The `print` function is implemented by getting an external reference to the
35`console` JavaScript global and then retrieving the `log` property from it.
40`console` JavaScript global, retrieving the `log` property from it, and then
41calling it when the program starts.
42
43> In the future, common APIs will be available in the standard library, such as
44> `std.log.info()`. For now, no such library exists.
3645
3746## Primitive Data Types
3847
39| name | ts equivalent | description |
40| -------- | --------------------- | ---------------------------------------------- |
41| `int` | `number` | a number in signed 32-bit range |
42| `float` | `number` | a 64-bit floating point number |
43| `bool` | `boolean` | either `true` or `false` |
44| `string` | `string` | a sequence of text characters |
45| `void` | `undefined` or `null` | detectable lack of a value |
46| `opaque` | `unknown` or `any` | could be any value |
47| `never` | `never` | control flow prevents this value from existing |
48| name | ts equivalent | description |
49| ---------- | ----------------------------- | ---------------------------------------------- |
50| `bool` | `boolean` | either `true` or `false` |
51| `float` | `number` | a 64-bit floating point number |
52| `int` | `number` | an integer number in signed 32-bit range |
53| `bigint` | `bigint` | an integer number with no range limit |
54| `string` | `string` | a sequence of text characters |
55| `void` | `undefined` / `null` / `void` | detectable lack of a value |
56| `opaque` | `unknown` / `any` | could be any value |
57| `noreturn` | `never` | control flow prevents this value from existing |
4858
49> **note**: `bigint` is planned but not added
59> **Note**: Unlike JavaScript, the values `undefined` and `null` are merged into
60> one concept: [void](#void)
5061
51## Built-in Constants
62### Built-in Constants
5263
5364In addition to the above types being built-in constants, the following are
5465available.
......@@ -62,9 +73,48 @@ available.
6273These constants are reserved names, and it is an error to re-declare them.
6374
6475```lang:build
65let true = 1 == 0;
76fn main {
77 let true = 1 == 0;
78}
6679```
6780
81### Numbers
82
83Numbers have the following rules:
84
85- May start with a `.`, but cannot end with `.`
86- May contain leading zeros, but transform pass will warn.
87- May contain a `_` separator, cannot have two in a row.
88- May contain `e` or `E` for exponents.
89- May use 0x, 0b, 0o prefixes for hex, binary, octal numbers.
90
91### Strings
92
93Numbers
94
95### Multi-line string literals
96
97```lang
98let bwaa =
99 \\hello
100 \\ world
101 \\end
102;
103```
104
105### Void
106
107Void is the lack a value. For example, a function that does nothing returns
108`void`.
109
110```lang
111pub fn hello() {
112 // do nothing
113}
114```
115
116<!-- TODO: change this section to "Optionals" -->
117
68118## Variables
69119
70120There are two types of variables, `let` and `var`.
......@@ -74,24 +124,30 @@ expression within. Referring to these identifiers is equivalent to inlining it
74124where it was used, aside from side effect duplication.
75125
76126```lang:run
77let hello: int = 1;
78let world = hello + 2;
79let hello = hello * world;
127let print = @extern("console").log;
80128
81@extern("console").log(hello);
129fn main {
130 let hello: int = 1;
131 let world = hello + 2;
132 let hello = hello * world;
133
134 print(hello);
135}
82136```
83137
84138`var` bindings create mutable memory locations at runtime. This means they can
85139be re-assigned to:
86140
87141```lang:run
88var message = "first message";
142fn main {
143 var message = "first message";
89144
90if @extern("Math").random() > 0.5 {
91 message = "second message";
92}
145 if @extern("Math").random() > 0.5 {
146 message = "second message";
147 }
93148
94@extern("console").log(message);
149 @extern("console").log(message);
150}
95151```
96152
97153Using `let` is preferred whenever possible, as it will lead to more readable
......@@ -99,13 +155,15 @@ code, and the compiler is able to optimize it better. The above can be rewritten
99155as:
100156
101157```lang:run
102let message = if @extern("Math").random() > 0.5 {
103 "second message"
104} else {
105 "first message"
106};
158fn main {
159 let message = if @extern("Math").random() > 0.5 {
160 "second message"
161 } else {
162 "first message"
163 };
107164
108@extern("console").log(message);
165 @extern("console").log(message);
166}
109167```
110168
111169It should be noted that numeric literals are a comptime-only data type, meaning
......@@ -114,7 +172,7 @@ it cannot be assigned to a `var` without an explicit type annotation. Since
114172float, but a compiler warning will be emitted.
115173
116174```lang
117var number = 0; // `number` is a `float`, but not explicit
175var number = 0; // `number` is a `float`, but not explicit
118176var number: int = 0; // `number` is `int`
119177var number: float = 0; // `number` is `float`
120178```
......@@ -125,12 +183,15 @@ Functions are declared with the `fn` keyword. Parameter types must be annotated.
125183The function body is a block, which may use implicit return to return.
126184
127185```lang:run
186let print = @extern("console").log;
187
128188fn add(a: int, b: int) -> int {
129189 a + b
130190}
131191
132let print = @extern("console").log;
133print("Sum of 2 and 7:", add(2, 7));
192fn main {
193 print("Sum of 2 and 7:", add(2, 7));
194}
134195```
135196
136197> **Bug**: Return types are currently required. Omitting the `-> type` syntax
......@@ -140,6 +201,8 @@ print("Sum of 2 and 7:", add(2, 7));
140201The `return` expression can be used within functions to return early.
141202
142203```lang:run
204let print = @extern("console").log;
205
143206fn something(a: int) -> bool {
144207 if a == 0 {
145208 return false;
......@@ -148,8 +211,9 @@ fn something(a: int) -> bool {
148211 a > 100
149212}
150213
151let print = @extern("console").log;
152print(something(0xFF), something(0));
214fn main {
215 print(something(0xFF), something(0));
216}
153217```
154218
155219## Number Literals
......@@ -178,7 +242,9 @@ fn currentYear() -> int {
178242 @jsNew(@extern("Date")).getFullYear()
179243}
180244
181print(currentYear());
245fn main {
246 print(currentYear());
247}
182248```
183249
184250A field access on an external handle is allowed to be performed at compile time,
......@@ -197,19 +263,21 @@ struct Position {
197263 y: int,
198264}
199265
200let center = Position{ x: 0, y: -2 };
201print(center.x, center.y);
266fn main {
267 let center = Position{ x: 0, y: -2 };
268 print(center.x, center.y);
202269
203fn addPositions(a: Position, b: Position) -> Position {
204270 // To allow less verbose code, the struct type can be inferred with `.`
205 .{ x: a.x + b.x, y: a.y + b.y }
271 let added = addPositions(
272 .{ x: 1, y: -4 },
273 .{ x: 5, y: 8 },
274 );
275 print("Add result:", added.x, added.y);
206276}
207277
208let added = addPositions(
209 .{ x: 1, y: -4 },
210 .{ x: 5, y: 8 },
211);
212print("Add result:", added.x, added.y);
278fn addPositions(a: Position, b: Position) -> Position {
279 .{ x: a.x + b.x, y: a.y + b.y }
280}
213281```
214282
215283To allow optimizations, `struct`s by default do not have a defined
......@@ -217,11 +285,80 @@ representation in JavaScript. This means passing the struct to an opaque
217285function is not allowed.
218286
219287```lang:build
288let print = @extern("console").log;
289
220290struct Person {
221291 name: string,
222292 age: int,
223293}
224294
225let print = @extern("console").log;
226print(Person{ name: "hello", age: 42 });
295fn main {
296 print(Person{ name: "hello", age: 42 });
297}
227298```
299
300## A
301
302### B
303
304## C
305
306### a
307
308### somewhat long header name but still is within dave's reasonable-ish constraints
309
310#### Number Literals 2
311
312TODO
313
314#### String Literals 2
315
316TODO
317
318#### String WAdsafdsa
319
320TODO
321
322### hewwo
323
324cat
325
326### cat
327
328#### yeah
329
330## what
331
332## omg
333
334## Lorem ipsum dolor sit
335
336## amet, consectetur adipiscing
337
338## elit. Proin vulputate ligula
339
340## ac risus blandit, eu accumsan
341
342## rna feugiat. Phasellus quis
343
344## metus felis. Semper mattis
345
346## augue eu elementum. Sed nec odio
347
348## cursus neque fringilla rutrum.
349
350## Cras vestibulum bibendum hendrerit.
351
352## Duis porta, leo vel
353
354## sollicitudin rhoncus, arcu nulla
355
356## lobortis est, vitae interdum
357
358## quam ante eu ante. Integer luctus
359
360## justo a odio cursus auctor. Phasellus
361
362## blandit urna vitae quam dapibus
363
364## efficitur. Proin sagittis urna non
dprint.jsonc+1
......@@ -2,6 +2,7 @@
22 "plugins": [
33 // sort-lines: start
44 "https://plugins.dprint.dev/exec-0.5.0.json@8d9972eee71fa1590e04873540421f3eda7674d0f1aae3d7c788615e7b7413d0",
5 "https://plugins.dprint.dev/g-plane/markup_fmt-v0.12.0.wasm",
56 "https://plugins.dprint.dev/json-0.19.3.wasm",
67 "https://plugins.dprint.dev/markdown-0.17.2.wasm",
78 // sort-lines: end
src/core/Comp.zig+1-1
......@@ -666,7 +666,7 @@ pub fn indexTegModule(comp: *Comp, file: *File, module_index: Teg.Inst.Index) !N
666666
667667const Options = struct {
668668 dump_ir: bool = false,
669 verbose_sema: bool = true,
669 verbose_sema: bool = false,
670670};
671671
672672/// The compiler backend is stored separately from the compilation, and
src/core/InternPool.zig+1-1
......@@ -2,7 +2,7 @@
22//! https://github.com/ziglang/zig/blob/0.12.0/src/InternPool.zig
33//!
44//! All interned objects have both a Value and a Type. Type is a subset of
5//! Value, since the language allows operating on types the same as values.
5//! Value, since Ix allows operating on types the same as values.
66//! This data structure is intended to be serializable for incremental
77//! compilation efforts.
88//!
src/core/Sema.zig+21-19
......@@ -693,6 +693,27 @@ fn stepInner(s: *Sema, comptime tag: Teg.Tag) Error!InstructionKind.fromTag(tag)
693693 const lhs_type = s.typeOfRef(lhs);
694694
695695 if (lhs_type == .@"opaque") {
696 if (lhs.value()) |value| {
697 if (value.valueTag(s.ip) == .external_value) {
698 const rhs = try s.getCoercedValue(s.lazyNode(.rhs), field.rhs, .string);
699 const field_key = rhs.asString(s.ip).tryToNullTerminatedString(s.ip, s.gpa) catch {
700 @panic("TODO: support null in these strings");
701 };
702
703 const previous_key = value.toKey(s.ip).external_value.key;
704
705 const new_key = try std.mem.concat(s.arena(), NullTerminatedString, &.{
706 previous_key,
707 &.{field_key},
708 });
709 defer s.arena().free(new_key);
710
711 return s.makeValueAir(.{ .external_value = .{
712 .key = new_key,
713 } });
714 }
715 }
716
696717 try s.ensureAtRuntime(s.lazyNode(.lhs));
697718 return s.addRef(.field_opaque, .{
698719 .lhs = lhs,
......@@ -704,25 +725,6 @@ fn stepInner(s: *Sema, comptime tag: Teg.Tag) Error!InstructionKind.fromTag(tag)
704725 return try s.fieldAccessStruct(lhs, lhs_type, struct_type, field.rhs);
705726 } else if (lhs_type.toTupleType(s.ip)) |tuple_type| {
706727 return try s.fieldAccessTuple(lhs, lhs_type, tuple_type, field.rhs);
707 } else if (lhs.value()) |value| {
708 if (value.valueTag(s.ip) == .external_value) {
709 const rhs = try s.getCoercedValue(s.lazyNode(.rhs), field.rhs, .string);
710 const field_key = rhs.asString(s.ip).tryToNullTerminatedString(s.ip, s.gpa) catch {
711 @panic("TODO: support null in these strings");
712 };
713
714 const previous_key = value.toKey(s.ip).external_value.key;
715
716 const new_key = try std.mem.concat(s.arena(), NullTerminatedString, &.{
717 previous_key,
718 &.{field_key},
719 });
720 defer s.arena().free(new_key);
721
722 return s.makeValueAir(.{ .external_value = .{
723 .key = new_key,
724 } });
725 }
726728 }
727729
728730 return s.fail(.field_invalid_type, .{
src/core/Teg.zig+1-1
......@@ -1,4 +1,4 @@
1//! TODO: rename this to something else. Teg is not a graph
1//! TODO: rename this to EIR (expression intermediate representation)
22
33insts: Inst.List.Slice,
44string_bytes: []const u8,
src/core/js/Link.zig+7-5
......@@ -76,7 +76,7 @@ pub fn linkJsCode(link: *Link, entry_decl: Decl.Index, writer: Writer) !void {
7676 }
7777
7878 try writer.writeAll(std.fmt.comptimePrint(
79 "// Language Compiler v{}. Alpha quality software.\n",
79 "// ix compiler v{}. alpha quality software.\n",
8080 .{@import("config").version},
8181 ));
8282
......@@ -514,16 +514,18 @@ fn printExprLevel(
514514
515515 try writer.writeAll(")");
516516 },
517
517518 .js_new => {
518519 const call: Tree.Call = tree.nodeData(.js_new, node);
519520 const args = call.args.get(tree);
520521
521 if (Level.new.needsParens(level))
522 const wrap = Level.new.needsParens(level) and level != .member;
523 if (wrap)
522524 try writer.writeAll("(");
523525 try writer.writeAll("new ");
524526 try link.printExpr(tree, call.base, writer);
525527
526 if (args.len > 0) {
528 if (args.len > 0 or level == .member) {
527529 try writer.writeAll("(");
528530
529531 for (args.slice(tree), 0..) |arg, i| {
......@@ -533,14 +535,14 @@ fn printExprLevel(
533535
534536 try writer.writeAll(")");
535537 }
536 if (Level.new.needsParens(level))
538 if (wrap)
537539 try writer.writeAll(")");
538540 },
539541
540542 .field => {
541543 const field: Tree.Field = tree.nodeData(.field, node);
542544
543 try link.printExpr(tree, field.base, writer);
545 try link.printExprLevel(tree, field.base, .member, writer);
544546 try writer.writeAll(".");
545547 try writer.writeAll(field.field.toSlice(&link.comp.intern_pool));
546548 },
src/core/mod.zig+1-1
......@@ -1,4 +1,4 @@
1//! Core language compiler module.
1//! Core module for the Ix compiler
22
33// sort-lines: start
44
src/main.zig+1-1
......@@ -1,7 +1,7 @@
11//! This is the user-facing command line, combining all features into
22//! different subcommands, each with their own option set.
33
4const default_program_name = "wlang"; // fallback when argv[0] is missing
4const default_program_name = "ix"; // fallback when argv[0] is missing
55
66const Command = struct {
77 name: [:0]const u8,
src/manual/NaiveHtmlMinifier.zig created+231
......@@ -0,0 +1,231 @@
1//! Removes whitespace between HTML/CSS markup.
2//! This function is exclusively intended for `template.html`,
3//! and doesn't handle many edge cases.
4input: []const u8,
5output: std.ArrayList(u8),
6
7const ws = " \t\r\n";
8
9pub fn minify(input: []const u8, allocator: Allocator) ![]u8 {
10 var m: NaiveHtmlMinifier = .{
11 .input = input,
12 .output = std.ArrayList(u8).init(allocator),
13 };
14 errdefer m.output.deinit();
15
16 while (m.input.len > 0) {
17 m.skipWhitespace();
18 try m.processHtml();
19 }
20
21 return try m.output.toOwnedSlice();
22}
23
24pub inline fn minifyComptime(input: []const u8) []const u8 {
25 comptime {
26 @setEvalBranchQuota(99999999);
27 var buf: [input.len * 2]u8 = undefined;
28 var fba = std.heap.FixedBufferAllocator.init(&buf);
29 const result_slice = minify(input, fba.allocator()) catch unreachable; // should not increase size
30 const result = result_slice[0..].*;
31 return &result;
32 }
33}
34
35fn skipWhitespace(m: *NaiveHtmlMinifier) void {
36 var i: usize = 0;
37 while (m.input.len > i) switch (m.input[i]) {
38 ' ', '\t', '\n', '\r' => i += 1,
39 else => break,
40 };
41 m.input = m.input[i..];
42}
43
44fn processHtml(m: *NaiveHtmlMinifier) !void {
45 switch (m.input[0]) {
46 ' ', '\t', '\n', '\r' => unreachable, // did not skip whitespace
47
48 '<' => switch (m.input[1]) {
49 '!' => try m.moveTokenWith(">"),
50 '/' => try m.moveTokenWith(">"),
51 else => {
52 const first_tok = getToken(m, ws);
53 try m.moveToken(ws);
54 if (first_tok[first_tok.len - 1] == '>') {
55 try m.maybeProcessBody(first_tok[1 .. first_tok.len - 1]);
56 return;
57 }
58 const tag_name = first_tok[1..];
59 var need_space_before_attr = true;
60 while (true) switch (m.input[0]) {
61 ' ', '\t', '\n', '\r' => unreachable, // did not skip whitespace
62 '>' => {
63 m.input = m.input[1..];
64 try m.output.appendSlice(">");
65 try m.maybeProcessBody(tag_name);
66 return;
67 },
68 '/' => {
69 const tok = m.getToken(">");
70 m.input = m.input[tok.len + 1 ..];
71 try m.output.appendSlice("/>");
72 try m.maybeProcessBody(tag_name);
73 return;
74 },
75 else => {
76 if (need_space_before_attr)
77 try m.output.appendSlice(" ");
78
79 try m.moveToken("=>" ++ ws);
80 if (m.input[0] == '=') {
81 m.input = m.input[1..];
82 m.skipWhitespace();
83 try m.output.appendSlice("=");
84
85 // move attr value
86 const end = switch (m.input[0]) {
87 '"' => brk: {
88 need_space_before_attr = false;
89 try m.output.append('"');
90 m.input = m.input[1..];
91 break :brk "\"";
92 },
93 '\'' => brk: {
94 need_space_before_attr = false;
95 try m.output.append('\'');
96 m.input = m.input[1..];
97 break :brk "'";
98 },
99 else => brk: {
100 need_space_before_attr = true;
101 break :brk ws ++ ">";
102 },
103 };
104 try m.moveTokenWith(end);
105
106 m.skipWhitespace();
107 }
108 },
109 };
110 },
111 },
112
113 else => {
114 const text_block = m.getToken("<");
115 m.input = m.input[text_block.len..];
116 var it = mem.tokenizeAny(u8, text_block, ws);
117 try m.output.appendSlice(it.next().?);
118 while (it.next()) |token| {
119 try m.output.append(' ');
120 try m.output.appendSlice(token);
121 }
122 },
123 }
124}
125
126fn maybeProcessBody(m: *NaiveHtmlMinifier, tag_name: []const u8) !void {
127 inline for (.{ .style, .script, .pre }) |tag| {
128 if (mem.eql(u8, tag_name, @tagName(tag))) {
129 const end = mem.indexOf(u8, m.input, "</" ++ @tagName(tag) ++ ">") orelse @panic("no end");
130 const tok = m.input[0 .. end + @tagName(tag).len + 3];
131
132 if (tag == .style) {
133 try m.processCss(m.input[0..end]);
134 } else {
135 try m.output.appendSlice(tok);
136 }
137
138 m.input = m.input[end..];
139 }
140 }
141}
142
143fn processCss(m: *NaiveHtmlMinifier, input_css: []const u8) !void {
144 var without_comments = try std.ArrayList(u8).initCapacity(m.output.allocator, input_css.len);
145 defer without_comments.deinit();
146
147 {
148 var i: usize = 0;
149 while (mem.indexOfScalarPos(u8, input_css, i, '/')) |j| {
150 if (j + 1 < input_css.len and input_css[j + 1] == '*') {
151 try without_comments.appendSlice(input_css[i..j]);
152 i = mem.indexOfPos(u8, input_css, j + 2, "*/") orelse
153 std.debug.panic("no end comment in {s}", .{input_css[j..]});
154 } else {
155 i = j + 1;
156 }
157 }
158 try without_comments.appendSlice(input_css[i..]);
159 }
160
161 var cm: NaiveHtmlMinifier = .{ .input = without_comments.items, .output = undefined };
162 cm.skipWhitespace();
163
164 while (cm.input.len > 0) {
165 const part = cm.getToken("{};");
166 const last = cm.input[part.len];
167 cm.input = cm.input[part.len + 1 ..];
168
169 switch (last) {
170 '{' => {
171 try appendWithoutCssComment(part, &m.output);
172 },
173 ';', '}' => rule: {
174 if (part.len == 0) {
175 if (m.output.items[m.output.items.len - 1] == ';')
176 m.output.items.len -= 1;
177 break :rule;
178 }
179
180 const i = mem.indexOfScalar(u8, part, ':') orelse
181 std.debug.panic("missing colon in {s}", .{part});
182 const key = part[0..i];
183 const value = part[i + 1 ..];
184
185 try appendWithoutCssComment(key, &m.output);
186 try m.output.append(':');
187 try appendWithoutCssComment(value, &m.output);
188 },
189 else => @panic("no end"),
190 }
191
192 try m.output.append(last);
193 cm.skipWhitespace();
194 }
195}
196
197fn getToken(m: *const NaiveHtmlMinifier, ends: []const u8) []const u8 {
198 return m.input[0 .. mem.indexOfAny(u8, m.input, ends) orelse return m.input];
199}
200
201fn moveToken(m: *NaiveHtmlMinifier, ends: []const u8) !void {
202 const tok = m.getToken(ends);
203 m.input = m.input[tok.len..];
204 m.skipWhitespace();
205 try m.output.appendSlice(tok);
206}
207
208fn moveTokenWith(m: *NaiveHtmlMinifier, ends: []const u8) !void {
209 const tok = m.getToken(ends);
210 try m.output.appendSlice(m.input[0 .. tok.len + 1]);
211 m.input = m.input[tok.len + 1 ..];
212 m.skipWhitespace();
213}
214
215fn appendWithoutCssComment(text: []const u8, output: *std.ArrayList(u8)) !void {
216 var it = mem.tokenizeAny(u8, text, ws);
217 var needs_space = false;
218 while (it.next()) |tok| {
219 if (needs_space) try output.append(' ');
220 try output.appendSlice(tok);
221 needs_space = switch (tok[tok.len - 1]) {
222 ',' => false,
223 else => true,
224 };
225 }
226}
227
228const NaiveHtmlMinifier = @This();
229const std = @import("std");
230const mem = std.mem;
231const Allocator = mem.Allocator;
src/manual/generate-man.zig deleted-705
......@@ -1,705 +0,0 @@
1//! Language reference compiler. Converts markdown into HTML, using
2//! the Tokenizer to highlight code, and the compiler and node.js to
3//! show code results. Only ever built in Debug mode. We are not
4//! smart about memory. Initially written at 3am on a Thursday. Enjoy
5pub const std = @import("std");
6pub const core = @import("core");
7
8const mem = std.mem;
9const Allocator = mem.Allocator;
10const ArrayList = std.ArrayList;
11const ArrayListUnmanaged = std.ArrayListUnmanaged;
12const assert = std.debug.assert;
13
14comptime {
15 assert(core.runtime_safety);
16}
17
18pub fn main() !void {
19 comptime std.debug.assert(@import("builtin").mode == .Debug);
20 var gpa_state: std.heap.GeneralPurposeAllocator(.{}) = .{};
21 const gpa = gpa_state.allocator();
22
23 const progress = std.Progress.start(.{
24 .root_name = "Build manual",
25 .estimated_total_items = 0,
26 });
27
28 var args = try std.process.argsWithAllocator(gpa);
29 _ = args.next().?;
30
31 const compiler_path = args.next().?;
32 const runtime_path = args.next().?;
33 const markdown_src = try std.fs.cwd().readFileAlloc(gpa, args.next().?, std.math.maxInt(usize));
34 const template_src = try std.fs.cwd().readFileAlloc(gpa, args.next().?, std.math.maxInt(usize));
35 const out_path = args.next().?;
36
37 assert(args.next() == null);
38
39 var contents = ArrayList(u8).init(gpa);
40 var header: HeaderWriter = .{};
41
42 try processMarkdown(
43 gpa,
44 progress,
45 markdown_src,
46 compiler_path,
47 runtime_path,
48 &header,
49 contents.writer(),
50 );
51
52 try header.end(gpa);
53
54 const applied_template = try replaceInTemplate(gpa, template_src, .{
55 .contents = contents.items,
56 .navigation = header.data.items,
57 .nav_h1 = header.h1.?,
58 .repository = "https://github.com/paperdave/wl",
59 });
60
61 const out_file = try std.fs.createFileAbsolute(out_path, .{});
62 defer out_file.close();
63 try out_file.writeAll(applied_template);
64
65 progress.end();
66
67 std.debug.lockStdErr();
68 std.debug.unlockStdErr();
69
70 std.process.exit(0);
71}
72
73const BlockType = enum {
74 para,
75 h1,
76 h2,
77 h3,
78 h4,
79 h5,
80 h6,
81 code_run,
82 code_build,
83 code,
84 code_generic,
85 ul,
86 table,
87 blockquote,
88};
89
90const InlineType = enum {
91 bold,
92 code,
93 link,
94};
95
96const MarkdownTokenizer = struct {
97 /// Remaining text
98 str: []const u8,
99
100 fn skipWhitespace(t: *MarkdownTokenizer) void {
101 t.str = mem.trimLeft(u8, t.str, " \r\n");
102 }
103
104 fn advance(t: *MarkdownTokenizer, n: usize) []const u8 {
105 defer {
106 t.str = t.str[n..];
107 }
108 return t.str[0..n];
109 }
110
111 fn advanceLine(t: *MarkdownTokenizer) []const u8 {
112 return mem.trim(u8, t.advanceLineRaw(), " \r\n");
113 }
114
115 fn advanceLineRaw(t: *MarkdownTokenizer) []const u8 {
116 const n = mem.indexOfScalar(u8, t.str, '\n');
117 const line = t.advance(if (n) |x| x + 1 else t.str.len);
118 return line;
119 }
120
121 fn blockType(t: *MarkdownTokenizer) BlockType {
122 switch (t.str[0]) {
123 '#' => {
124 defer {
125 while (t.str[0] == '#')
126 _ = t.advance(1);
127 }
128
129 if (t.str[1] != '#') return .h1;
130 if (t.str[2] != '#') return .h2;
131 if (t.str[3] != '#') return .h3;
132 if (t.str[4] != '#') return .h4;
133 if (t.str[5] != '#') return .h5;
134 if (t.str[6] != '#') return .h6;
135 @panic("headers nested too deep");
136 },
137 '>' => {
138 _ = t.advance(1);
139 return .blockquote;
140 },
141 '|' => {
142 return .table;
143 },
144 '`' => {
145 if (mem.startsWith(u8, t.str, "```")) {
146 defer _ = t.advanceLine();
147 if (mem.startsWith(u8, t.str, "```lang:run"))
148 return .code_run;
149 if (mem.startsWith(u8, t.str, "```lang:build"))
150 return .code_build;
151 if (mem.startsWith(u8, t.str, "```lang"))
152 return .code;
153 return .code_generic;
154 }
155 return .para;
156 },
157 '-' => {
158 return .ul;
159 },
160 else => {
161 return .para;
162 },
163 }
164 }
165};
166
167const HeaderWriter = struct {
168 data: std.ArrayListUnmanaged(u8) = .{},
169 level: u8 = 0,
170 h1: ?[]const u8 = null,
171
172 pub fn add(header: *HeaderWriter, alloc: Allocator, name: []const u8, id: []const u8, level: u8) !void {
173 const w = header.data.writer(alloc);
174
175 while (header.level > level) {
176 header.level -= 1;
177 try w.writeAll("</ul>");
178 }
179
180 while (header.level < level) {
181 if (header.level == 0) {
182 try w.writeAll("<ul id=\"toc\">");
183 } else {
184 try w.writeAll("<li><ul>");
185 }
186 header.level += 1;
187 }
188
189 try w.print("<li><a class=\"header-link\" href=\"#{s}\">{s}</a></li>", .{ id, name });
190 }
191
192 pub fn end(header: *HeaderWriter, alloc: Allocator) !void {
193 const w = header.data.writer(alloc);
194 while (header.level > 1) {
195 header.level -= 1;
196 try w.writeAll("</ul></li>");
197 }
198 if (header.level == 1) {
199 header.level -= 1;
200 try w.writeAll("</ul>");
201 }
202 }
203};
204
205fn processMarkdown(
206 gpa: Allocator,
207 progress: std.Progress.Node,
208 src: []const u8,
209 compiler_path: []const u8,
210 runtime_path: []const u8,
211 header: *HeaderWriter,
212 writer: anytype,
213) !void {
214 var t = MarkdownTokenizer{ .str = src };
215
216 t.skipWhitespace();
217 while (t.str.len > 0) : (t.skipWhitespace()) {
218 switch (t.blockType()) {
219 .para,
220 => {
221 try writer.writeAll("<p>\n");
222
223 var para = ArrayList(u8).init(gpa);
224 try para.appendSlice(t.advanceLine());
225
226 var next_line = t.advanceLine();
227 while (t.str.len > 0 and next_line.len > 0) {
228 try para.append(' ');
229 try para.appendSlice(next_line);
230 next_line = t.advanceLine();
231 }
232
233 try processInlineMarkdown(gpa, para.items, writer);
234
235 try writer.writeAll("\n</p>\n");
236 },
237 .h1,
238 .h2,
239 .h3,
240 .h4,
241 .h5,
242 .h6,
243 => |tag| {
244 const html_tag = @tagName(tag);
245 const name = t.advanceLine();
246 const id = try nameToHeaderId(gpa, name);
247
248 if (tag != .h1) {
249 try writer.print("<{0s} id=\"{2s}\"><a href=\"#{2s}\">{1s}</a></{0s}>\n", .{
250 html_tag,
251 name,
252 id,
253 });
254
255 if (tag != .h1)
256 try header.add(gpa, name, id, switch (tag) {
257 .h2 => 1,
258 .h3 => 2,
259 .h4 => 3,
260 .h5 => 4,
261 .h6 => 5,
262 else => unreachable,
263 });
264 } else {
265 assert(header.h1 == null);
266
267 header.h1 = try std.fmt.allocPrint(
268 gpa,
269 "<a href=\"#top\">{s}</a>",
270 .{name},
271 );
272
273 try writer.print("<{0s}><a href=\"#top\">{1s}</a></{0s}>\n", .{
274 html_tag,
275 name,
276 });
277 }
278 },
279 .code_run,
280 .code_build,
281 .code,
282 .code_generic,
283 => |tag| {
284 var code = ArrayList(u8).init(gpa);
285
286 var next_line = t.advanceLineRaw();
287 while (t.str.len > 0 and !mem.startsWith(u8, next_line, "```")) {
288 try code.appendSlice(next_line);
289 next_line = t.advanceLineRaw();
290 }
291
292 if (tag == .code_run or tag == .code_build or tag == .code) {
293 try writer.writeAll("<figure><figcaption><cite>hello.wl</cite></figcaption>");
294 } else {
295 try writer.writeAll("<figure>");
296 }
297
298 try writer.writeAll("<pre><code>");
299 if (tag == .code_generic) {
300 try writeHtmlEscapedSlice(code.items, writer, false);
301 } else {
302 try writeTokenizedCode(gpa, code.items, writer);
303 }
304 try writer.writeAll("</code></pre>");
305
306 try code.append('\n');
307
308 if (tag == .code_run or tag == .code_build) {
309 var prog = progress.start("Code Snippet", if (tag == .code_run) 2 else 1);
310 defer prog.end();
311
312 var prog_step = prog.start("Compile", 1);
313 var build = std.process.Child.init(&.{ compiler_path, "build", "/dev/stdin" }, gpa);
314 build.stdin_behavior = .Pipe;
315 build.stdout_behavior = .Pipe;
316 build.stderr_behavior = .Pipe;
317 var env_map = try std.process.getEnvMap(gpa);
318 defer env_map.deinit();
319 try env_map.put("YES_COLOR", "1");
320 build.env_map = &env_map;
321 try build.spawn();
322 try build.stdin.?.writeAll(code.items);
323 var out = ArrayList(u8).init(gpa);
324 var err = ArrayList(u8).init(gpa);
325 try build.collectOutput(&out, &err, std.math.maxInt(usize));
326 const result = try build.wait();
327 if (result != .Exited or result.Exited != 0) {
328 try err.writer().print("[Process {s} with {d}]", .{
329 switch (result) {
330 .Exited => "exited",
331 .Stopped => "stopped",
332 .Signal => "signaled",
333 .Unknown => "hit unknown state",
334 },
335 switch (result) {
336 inline else => |d| @as(u32, d),
337 },
338 });
339 }
340 if (mem.trim(u8, err.items, " \n\r").len > 0) {
341 try writer.writeAll("<pre class='term'><code>");
342 try writeAnsi(gpa, err.items, writer);
343 try writer.writeAll("</code></pre>");
344 }
345 const trimmed_out = mem.trim(u8, out.items, " \n\r");
346 if (trimmed_out.len > 0) {
347 try writer.writeAll("<pre><code>");
348 try writer.writeAll("// Compiled JavaScript Code");
349 try writer.writeAll(trimmed_out[mem.indexOfScalar(u8, trimmed_out, '\n').?..]);
350 try writer.writeAll("</code></pre>");
351 }
352
353 prog.completeOne();
354 prog_step.end();
355
356 if (tag == .code_run and runtime_path.len > 0) {
357 const run_step = prog.start("Execute", 0);
358 defer run_step.end();
359
360 var temp_dir = std.testing.tmpDir(.{});
361 defer temp_dir.cleanup();
362
363 const input_sub_path = "result.js";
364 try temp_dir.dir.writeFile(.{
365 .sub_path = input_sub_path,
366 .data = trimmed_out,
367 });
368
369 var env_map_run = try std.process.getEnvMap(gpa);
370 defer env_map_run.deinit();
371 try env_map_run.put("NO_COLOR", "1");
372 try env_map_run.put("TERM", "dumb");
373
374 var run_out = ArrayList(u8).init(gpa);
375 var run_err = ArrayList(u8).init(gpa);
376
377 // Different JavaScript runtimes have different quirks with
378 // regards to CLI parsing and standard input streams.
379 const run_sub_command = std.mem.indexOf(u8, runtime_path, "deno") != null or
380 std.mem.indexOf(u8, runtime_path, "porf") != null;
381
382 const cmd: []const []const u8 = if (run_sub_command)
383 &.{ runtime_path, "run", input_sub_path }
384 else
385 &.{ runtime_path, input_sub_path };
386
387 var run = std.process.Child.init(cmd, gpa);
388
389 run.cwd_dir = temp_dir.dir;
390 var path_buf: [std.fs.max_path_bytes]u8 = undefined;
391 if (@import("builtin").os.tag == .windows)
392 run.cwd = try std.os.getFdPath(temp_dir.dir.fd, &path_buf);
393
394 run.stdin_behavior = .Ignore;
395 run.stdout_behavior = .Pipe;
396 run.stderr_behavior = .Pipe;
397
398 run.env_map = &env_map_run;
399 try run.spawn();
400
401 try run.collectOutput(&run_out, &run_err, std.math.maxInt(usize));
402 const run_result = run.wait() catch |e| {
403 if (e == error.FileNotFound) {
404 std.debug.panic(
405 \\Missing JavaScript evaluator: {s}
406 \\Specify another one using -Djs_runtime=... or set to "" to skip evaluation.
407 \\
408 , .{runtime_path});
409 }
410 return e;
411 };
412
413 if (run_result != .Exited or run_result.Exited != 0) {
414 try run_err.writer().print("[Process {s} with {d}]", .{
415 switch (run_result) {
416 .Exited => "exited",
417 .Stopped => "stopped",
418 .Signal => "signaled",
419 .Unknown => "hit unknown state",
420 },
421 switch (run_result) {
422 inline else => |d| @as(u32, d),
423 },
424 });
425 }
426
427 try writer.writeAll("<pre><code>");
428 try writer.writeAll(run_out.items);
429 try writer.writeAll(run_err.items);
430 try writer.writeAll("</code></pre>");
431 }
432 }
433
434 try writer.writeAll("</figure>");
435 },
436 .ul,
437 => {
438 try writer.writeAll("<ul>");
439 while (t.str.len > 0 and (t.str[0] == '-')) {
440 _ = t.advance(1);
441 const line = t.advanceLine();
442 try writer.writeAll("<li>");
443 try processInlineMarkdown(gpa, line, writer);
444 try writer.writeAll("</li>");
445 }
446 try writer.writeAll("</ul>");
447 },
448 .table,
449 => {
450 try writer.writeAll("<table><tbody>");
451 {
452 try writer.writeAll("<tr>");
453 var it = mem.tokenizeScalar(u8, t.advanceLine()[1..], '|');
454 while (it.next()) |tok| {
455 try writer.writeAll("<th>");
456 try processInlineMarkdown(gpa, tok, writer);
457 try writer.writeAll("</th>");
458 }
459 try writer.writeAll("</tr>");
460 }
461 _ = t.advanceLine();
462 t.skipWhitespace();
463 while (t.str.len > 0 and t.str[0] == '|') {
464 try writer.writeAll("<tr>");
465 var it = mem.tokenizeScalar(u8, t.advanceLine()[1..], '|');
466 while (it.next()) |tok| {
467 try writer.writeAll("<td>");
468 try processInlineMarkdown(gpa, tok, writer);
469 try writer.writeAll("</td>");
470 }
471 try writer.writeAll("</tr>");
472 }
473 try writer.writeAll("</tbody></table>");
474 },
475 .blockquote,
476 => {
477 try writer.writeAll("<blockquote>\n");
478
479 var para = ArrayList(u8).init(gpa);
480 try para.appendSlice(t.advanceLine());
481
482 var next_line = t.advanceLine();
483 while (t.str.len > 0 and next_line.len > 0) {
484 try para.append(' ');
485 try para.appendSlice(next_line);
486 next_line = t.advanceLine();
487 }
488
489 try processInlineMarkdown(gpa, para.items, writer);
490
491 try writer.writeAll("\n</blockquote>\n");
492 },
493 }
494 }
495}
496
497pub fn nextInlineToken(src: []const u8) ?struct { InlineType, usize } {
498 var pos: usize = 0;
499 while (true) : (pos += 1) {
500 pos = mem.indexOfAnyPos(u8, src, pos, "*`[") orelse return null;
501
502 switch (src[pos]) {
503 '*' => switch (src[pos + 1]) {
504 '*' => return .{ .bold, pos },
505 else => continue,
506 },
507 '`' => return .{ .code, pos },
508 '[' => return .{ .link, pos },
509 else => unreachable,
510 }
511 }
512}
513
514fn processInlineMarkdown(gpa: Allocator, src: []const u8, writer: anytype) !void {
515 var bold = false;
516 var remain = src;
517
518 while (nextInlineToken(remain)) |tok| {
519 try writer.writeAll(remain[0..tok[1]]);
520 remain = remain[tok[1]..];
521 switch (tok[0]) {
522 .bold => {
523 bold = !bold;
524 try writer.writeAll(switch (bold) {
525 true => "<strong>",
526 false => "</strong>",
527 });
528 remain = remain[2..];
529 },
530 .code => {
531 const end = mem.indexOfScalarPos(u8, remain, 1, '`') orelse @panic("invalid code block");
532 const code = remain[1..end];
533 try writer.writeAll("<code>");
534 try writeTokenizedCode(gpa, code, writer);
535 try writer.writeAll("</code>");
536 remain = remain[end + 1 ..];
537 },
538 .link => {
539 const label_end = mem.indexOfScalarPos(u8, remain, 1, ']') orelse @panic("invalid link block");
540 assert(remain[label_end + 1] == '('); // invalid link. '[' syntax is not implemented
541
542 const href_end = mem.indexOfScalarPos(u8, remain, label_end + 2, ')') orelse @panic("invalid link block");
543
544 const label = remain[1..label_end];
545 const href = remain[label_end + 2 .. href_end];
546
547 try writer.writeAll("<a href=\"");
548 try writeHtmlEscapedSlice(href, writer, true);
549 if (!mem.startsWith(u8, href, "#")) {
550 try writer.writeAll("\" class=\"external\" target=\"_blank\" ref=\"noopener noreferrer");
551 }
552 try writer.writeAll("\">");
553 try processInlineMarkdown(gpa, label, writer);
554 try writer.writeAll("</a>");
555 remain = remain[href_end + 1 ..];
556 },
557 }
558 }
559 try writer.writeAll(remain);
560
561 if (bold) {
562 std.debug.panic("Unterminated Bold in '{s}'", .{src});
563 }
564}
565
566// TODO: Change this to use Tree-sitter, which would also make it very simple
567// to highlight JavaScript code in addition to the language.
568fn writeTokenizedCode(gpa: Allocator, code: []const u8, writer: anytype) !void {
569 const code_z = try gpa.dupeZ(u8, code);
570 defer gpa.free(code_z);
571
572 var t = core.Tokenizer{ .code = code_z };
573
574 t.skipWhitespace();
575 var last: usize = 0;
576 while (true) {
577 t.skipWhitespace();
578 const tok = t.next() orelse break;
579 try writeHtmlEscapedSlice(t.code[last..tok.offset], writer, false);
580 try writer.print("<span class='tok-{s}{s}'>{s}</span>", .{
581 @tagName(tok.tag),
582 if (tok.tag.isKeyword()) " kw" else "",
583 t.code[tok.offset..t.pos],
584 });
585 last = t.pos;
586 }
587 try writeHtmlEscapedSlice(t.code[last..], writer, false);
588}
589
590fn writeAnsi(gpa: Allocator, code: []const u8, writer: anytype) !void {
591 var remain = code;
592 var count: usize = 0;
593 while (mem.indexOfScalar(u8, remain, '\x1b')) |esc_i| {
594 try writeHtmlEscapedSlice(remain[0..esc_i], writer, false);
595 switch (remain[esc_i + 1]) {
596 '[' => switch (remain[esc_i + 2]) {
597 '0'...'9' => {
598 const end = mem.indexOfScalarPos(u8, remain, esc_i + 2, 'm').?;
599 const int = std.fmt.parseInt(u8, remain[esc_i + 2 .. end], 10) catch unreachable;
600 switch (int) {
601 else => {
602 try writer.print("<span class='ansi-{d}'>", .{int});
603 count += 1;
604 },
605 0 => {
606 for (0..count) |_| {
607 try writer.writeAll("</span>");
608 }
609 count = 0;
610 },
611 }
612 remain = remain[end + 1 ..];
613 },
614 else => |x| std.debug.panic("Unknown esc + 2: {d}", .{x}),
615 },
616 else => |x| std.debug.panic("Unknown esc + 1: {d}", .{x}),
617 }
618 }
619 _ = gpa;
620}
621
622fn nameToHeaderId(alloc: Allocator, input: []const u8) ![]const u8 {
623 var out = try ArrayList(u8).initCapacity(alloc, input.len);
624
625 var is_split = false;
626 for (input) |char| {
627 switch (char) {
628 'a'...'z', 'A'...'Z', '0'...'9' => {
629 if (is_split) {
630 out.appendAssumeCapacity('-');
631 is_split = false;
632 }
633 out.appendAssumeCapacity(std.ascii.toLower(char));
634 },
635 else => {
636 is_split = true;
637 },
638 }
639 }
640
641 return try out.toOwnedSlice();
642}
643
644fn replaceInTemplate(alloc: Allocator, template: []const u8, replacements: anytype) ![]const u8 {
645 const Replacements = @TypeOf(replacements);
646 const Map = std.StringArrayHashMapUnmanaged([]const u8);
647
648 var map: Map = .{};
649 inline for (@typeInfo(Replacements).@"struct".fields) |field| {
650 try map.put(alloc, field.name, @field(replacements, field.name));
651 }
652
653 var out: ArrayListUnmanaged(u8) = .{};
654 const w = out.writer(alloc);
655
656 var i: usize = 0;
657 while (mem.indexOfScalarPos(u8, template, i, '{')) |open_at| {
658 const end = mem.indexOfScalarPos(u8, template, open_at + 1, '}') orelse break;
659 const inner = template[open_at + 1 .. end];
660
661 if (map.get(inner)) |replacement| {
662 // write previous
663 try w.writeAll(template[i..open_at]);
664 // write template
665 try w.writeAll(replacement);
666 // move one past the ending '}'
667 i = end + 1;
668 } else {
669 // write up to and including the {
670 try w.writeAll(template[i .. open_at + 1]);
671 // move one past it
672 i = open_at + 1;
673 }
674 }
675
676 // write ending
677 try w.writeAll(template[i..]);
678
679 return out.items;
680}
681
682fn writeHtmlEscapedSlice(text: []const u8, writer: anytype, escape_quotes: bool) !void {
683 var i: usize = 0;
684 while (mem.indexOfAnyPos(
685 u8,
686 text,
687 i,
688 if (escape_quotes)
689 &.{ '&', '<', '>', '"' }
690 else
691 &.{ '&', '<', '>' },
692 )) |at| {
693 const replace = switch (text[at]) {
694 '&' => "&amp;",
695 '<' => "&lt;",
696 '>' => "&gt;",
697 '"' => "&quot;",
698 else => unreachable,
699 };
700 try writer.writeAll(text[i..at]);
701 try writer.writeAll(replace);
702 i = at + 1;
703 }
704 try writer.writeAll(text[i..]);
705}
src/manual/generate_manual.zig created+825
......@@ -0,0 +1,825 @@
1//! Language reference compiler. Converts markdown into HTML, using
2//! the Tokenizer to highlight code, and the compiler and QJS to
3//! show code results. Only ever built in Debug mode. We are not
4//! smart about memory. Initially written at 3am on a Thursday. Enjoy
5pub const std = @import("std");
6pub const core = @import("core");
7const NaiveHtmlMinifier = @import("NaiveHtmlMinifier.zig");
8
9const mem = std.mem;
10const Allocator = mem.Allocator;
11const ArrayList = std.ArrayList;
12const ArrayListUnmanaged = std.ArrayListUnmanaged;
13const assert = std.debug.assert;
14
15comptime {
16 assert(@import("builtin").mode == .Debug);
17}
18
19const cli_name = "wlang";
20const build_cli = cli_name ++ " build";
21const test_cli = cli_name ++ " test";
22const run_cli = cli_name ++ " run";
23
24pub fn main() !void {
25 comptime std.debug.assert(@import("builtin").mode == .Debug);
26 const gpa = std.heap.c_allocator;
27
28 const progress = std.Progress.start(.{
29 .root_name = "Build manual",
30 .estimated_total_items = 0,
31 });
32
33 var args = try std.process.argsWithAllocator(gpa);
34 _ = args.next().?;
35
36 const compiler_path = args.next().?;
37 const runtime_path = args.next().?;
38 const markdown_src = try std.fs.cwd().readFileAlloc(gpa, args.next().?, std.math.maxInt(usize));
39 const template_src = try std.fs.cwd().readFileAlloc(gpa, args.next().?, std.math.maxInt(usize));
40 const out_path = args.next().?;
41
42 assert(args.next() == null);
43
44 var contents = ArrayList(u8).init(gpa);
45 var header: HeaderWriter = .{};
46
47 const minify = false;
48
49 try processMarkdown(
50 gpa,
51 progress,
52 markdown_src,
53 compiler_path,
54 runtime_path,
55 &header,
56 contents.writer(),
57 );
58
59 try header.end(gpa);
60
61 const minified_template = if (minify)
62 try NaiveHtmlMinifier.minify(template_src, gpa)
63 else
64 template_src;
65
66 const applied_template = try replaceInTemplate(gpa, minified_template, .{
67 .contents = contents.items,
68 .navigation = header.data.items,
69 .nav_h1 = "Ix Language",
70 .repository = "https://github.com/paperdave/wl",
71 });
72
73 const out_file = try std.fs.createFileAbsolute(out_path, .{});
74 defer out_file.close();
75 try out_file.writeAll(applied_template);
76
77 progress.end();
78
79 std.debug.lockStdErr();
80 std.debug.unlockStdErr();
81
82 std.process.exit(0);
83}
84
85const BlockType = enum {
86 para,
87 h1,
88 h2,
89 h3,
90 h4,
91 h5,
92 h6,
93 code_run,
94 code_build,
95 code,
96 code_generic,
97 ul,
98 table,
99 blockquote,
100};
101
102const InlineType = enum {
103 bold,
104 code,
105 link,
106};
107
108const MarkdownTokenizer = struct {
109 /// Remaining text
110 str: []const u8,
111
112 fn skipWhitespace(t: *MarkdownTokenizer) void {
113 t.str = mem.trimLeft(u8, t.str, " \r\n");
114 }
115
116 fn advance(t: *MarkdownTokenizer, n: usize) []const u8 {
117 defer {
118 t.str = t.str[n..];
119 }
120 return t.str[0..n];
121 }
122
123 fn advanceLine(t: *MarkdownTokenizer) []const u8 {
124 return mem.trim(u8, t.advanceLineRaw(), " \r\n");
125 }
126
127 fn advanceLineRaw(t: *MarkdownTokenizer) []const u8 {
128 const n = mem.indexOfScalar(u8, t.str, '\n');
129 const line = t.advance(if (n) |x| x + 1 else t.str.len);
130 return line;
131 }
132
133 fn blockType(t: *MarkdownTokenizer) BlockType {
134 switch (t.str[0]) {
135 '#' => {
136 defer {
137 while (t.str[0] == '#')
138 _ = t.advance(1);
139 }
140
141 if (t.str[1] != '#') return .h1;
142 if (t.str[2] != '#') return .h2;
143 if (t.str[3] != '#') return .h3;
144 if (t.str[4] != '#') return .h4;
145 if (t.str[5] != '#') return .h5;
146 if (t.str[6] != '#') return .h6;
147 @panic("headers nested too deep");
148 },
149 '>' => {
150 _ = t.advance(1);
151 return .blockquote;
152 },
153 '|' => {
154 return .table;
155 },
156 '`' => {
157 if (mem.startsWith(u8, t.str, "```")) {
158 defer _ = t.advanceLine();
159 if (mem.startsWith(u8, t.str, "```lang:run"))
160 return .code_run;
161 if (mem.startsWith(u8, t.str, "```lang:build"))
162 return .code_build;
163 if (mem.startsWith(u8, t.str, "```lang"))
164 return .code;
165 return .code_generic;
166 }
167 return .para;
168 },
169 '-' => {
170 return .ul;
171 },
172 else => {
173 return .para;
174 },
175 }
176 }
177};
178
179const HeaderWriter = struct {
180 data: std.ArrayListUnmanaged(u8) = .{},
181 level: u8 = 0,
182 h1: ?[]const u8 = null,
183
184 numbers: [5]u32 = .{ 0, 0, 0, 0, 0 },
185
186 has_closed_li: bool = true,
187
188 fn add(header: *HeaderWriter, alloc: Allocator, name: []const u8, id: []const u8, level: u8) !void {
189 const w = header.data.writer(alloc);
190
191 if (header.level == level) {
192 try w.writeAll("</li>");
193 }
194
195 while (header.level > level) {
196 header.level -= 1;
197 header.numbers[header.level] = 0;
198 try w.writeAll("</ul></li>");
199 }
200
201 while (header.level < level) {
202 if (header.level == 0) {
203 try w.writeAll("<ul id=\"toc\">");
204 } else {
205 try w.writeAll("<ul>");
206 }
207 header.level += 1;
208 }
209
210 header.numbers[level - 1] += 1;
211
212 try w.print("<li><a class=\"header-link\" href=\"#{s}\"><span class=\"number\">", .{id});
213
214 for (0..level) |i| {
215 if (i != 0) try w.print(".", .{});
216 try w.print("{d}", .{header.numbers[i]});
217 }
218
219 try w.print("</span>. {s}</a>", .{name});
220 }
221
222 fn end(header: *HeaderWriter, alloc: Allocator) !void {
223 const w = header.data.writer(alloc);
224 while (header.level > 1) {
225 header.level -= 1;
226 try w.writeAll("</ul></li>");
227 }
228 if (header.level == 1) {
229 header.level -= 1;
230 try w.writeAll("</ul>");
231 }
232 }
233};
234
235const CodeExample = struct {
236 filename: []const u8,
237 source: union(enum) { tokenized: []const u8, raw: []const u8 },
238 terminal: ?[]const u8 = null,
239 javascript: ?[]const u8 = null,
240
241 const copy_button = NaiveHtmlMinifier.minifyComptime(
242 \\<button class="copy-button">
243 \\ <svg
244 \\ xmlns="http://www.w3.org/2000/svg"
245 \\ height="20px"
246 \\ viewBox="0 -960 960 960"
247 \\ width="20px"
248 \\ fill="currentColor"
249 \\ >
250 \\ <path
251 \\ d="M355-240q-30.94 0-52.97-22.03Q280-284.06 280-315v-480q0-30.94 22.03-52.97Q324.06-870 355-870h360q30.94 0 52.97 22.03Q790-825.94 790-795v480q0 30.94-22.03 52.97Q745.94-240 715-240H355Zm0-75h360v-480H355v480ZM205-90q-30.94 0-52.97-22.03Q130-134.06 130-165v-555h75v555h435v75H205Zm150-225v-480 480Z"
252 \\ />
253 \\ </svg>
254 \\ <svg
255 \\ xmlns="http://www.w3.org/2000/svg"
256 \\ height="20px"
257 \\ viewBox="0 -960 960 960"
258 \\ width="20px"
259 \\ fill="currentColor"
260 \\ class="hidden"
261 \\ >
262 \\ <path
263 \\ d="M383-246 162-467l53.5-53.5L383-353l361.5-361.5L798-661 383-246Z"
264 \\ />
265 \\ </svg>
266 \\</button>
267 );
268
269 pub fn write(code: CodeExample, w: std.io.AnyWriter) !void {
270 try w.writeAll("<figure class=\"file\">");
271 try w.print("<figcaption>{}</figcaption>", .{fmtHtmlEscape(code.filename, false)});
272 try w.writeAll("<pre>");
273 try w.writeAll(copy_button);
274 try w.writeAll("<code>");
275 switch (code.source) {
276 .tokenized => |str| try w.writeAll(str),
277 .raw => |str| try writeHtmlEscapedSlice(str, w, false),
278 }
279 try w.writeAll("</code></pre>");
280
281 if (code.terminal) |terminal| {
282 try w.writeAll("<section class=\"tabs\">");
283 if (code.javascript != null) {
284 try w.writeAll("<div role=\"tablist\">");
285 try w.writeAll("<label id=\"output-tab\" role=\"tab\" for=\"output\">Output</label>");
286 try w.writeAll("<label id=\"js-tab\" role=\"tab\" for=\"js\">Compiled JS</label>");
287 try w.writeAll("</div>");
288 } else {
289 // ???
290 }
291
292 try w.writeAll("<div class=\"bottom output\" aria-labelledby=\"output-tab\">");
293 try w.writeAll("<pre class=\"terminal\"><code>");
294 try writeAnsi(terminal, w);
295 try w.writeAll("</code></pre>");
296 try w.writeAll("</div>");
297
298 if (code.javascript) |js| {
299 try w.writeAll("<div class=\"bottom js\" aria-labelledby=\"js-tab\">");
300 try w.print("<pre><code>{}</code></pre>", .{fmtHtmlEscape(js, false)});
301 try w.writeAll("</div>");
302 }
303
304 try w.writeAll("</section>");
305 } else {
306 assert(code.javascript == null);
307 }
308 try w.writeAll("</figure>");
309 }
310};
311
312fn processMarkdown(
313 gpa: Allocator,
314 progress: std.Progress.Node,
315 src: []const u8,
316 compiler_path: []const u8,
317 runtime_path: []const u8,
318 header: *HeaderWriter,
319 writer: anytype,
320) !void {
321 var t = MarkdownTokenizer{ .str = src };
322
323 t.skipWhitespace();
324 while (t.str.len > 0) : (t.skipWhitespace()) {
325 switch (t.blockType()) {
326 .para => {
327 try writer.writeAll("<p>");
328
329 var para = ArrayList(u8).init(gpa);
330 try para.appendSlice(t.advanceLine());
331
332 var next_line = t.advanceLine();
333 while (t.str.len > 0 and next_line.len > 0) {
334 try para.append(' ');
335 try para.appendSlice(next_line);
336 next_line = t.advanceLine();
337 }
338
339 try processInlineMarkdown(gpa, para.items, writer);
340
341 try writer.writeAll("</p>");
342 },
343 .h1,
344 .h2,
345 .h3,
346 .h4,
347 .h5,
348 .h6,
349 => |tag| {
350 const html_tag = @tagName(tag);
351 const name = t.advanceLine();
352 const id = try nameToHeaderId(gpa, name);
353
354 if (tag != .h1) {
355 try writer.print("<{0s} id=\"{2s}\"><a class=\"heading\" href=\"#{2s}\">{1s}</a></{0s}>", .{
356 html_tag,
357 name,
358 id,
359 });
360
361 if (tag != .h1)
362 try header.add(gpa, name, id, switch (tag) {
363 .h2 => 1,
364 .h3 => 2,
365 .h4 => 3,
366 .h5 => 4,
367 .h6 => 5,
368 else => unreachable,
369 });
370 } else {
371 assert(header.h1 == null);
372
373 header.h1 = try std.fmt.allocPrint(
374 gpa,
375 "<a href=\"#top\">{s}</a>",
376 .{name},
377 );
378
379 try writer.print("<{0s}><a class=\"heading\" href=\"#top\">{1s}</a></{0s}>", .{
380 html_tag,
381 name,
382 });
383 }
384 },
385 .code_run,
386 .code_build,
387 .code,
388 .code_generic,
389 => |tag| {
390 var code = ArrayList(u8).init(gpa);
391
392 var next_line = t.advanceLineRaw();
393 while (t.str.len > 0 and !mem.startsWith(u8, next_line, "```")) {
394 try code.appendSlice(next_line);
395 next_line = t.advanceLineRaw();
396 }
397
398 var example: CodeExample = .{
399 .filename = "example.wl",
400 .source = .{ .raw = code.items },
401 };
402
403 if (tag != .code_generic) {
404 var tmp = ArrayList(u8).init(gpa);
405 try writeTokenizedCode(gpa, code.items, tmp.writer());
406 example.source = .{ .tokenized = tmp.items };
407 }
408
409 try code.append('\n');
410
411 if (tag == .code_run or tag == .code_build) {
412 var prog = progress.start("Code Snippet", if (tag == .code_run) 2 else 1);
413 defer prog.end();
414
415 var prog_step = prog.start("Compile", 1);
416 var build = std.process.Child.init(&.{ compiler_path, "build", "/dev/stdin" }, gpa);
417 build.stdin_behavior = .Pipe;
418 build.stdout_behavior = .Pipe;
419 build.stderr_behavior = .Pipe;
420 var env_map = try std.process.getEnvMap(gpa);
421 defer env_map.deinit();
422 try env_map.put("YES_COLOR", "1");
423 build.env_map = &env_map;
424 try build.spawn();
425 try build.stdin.?.writeAll(code.items);
426 var out = ArrayList(u8).init(gpa);
427 var err = ArrayList(u8).init(gpa);
428
429 try build.collectOutput(&out, &err, std.math.maxInt(usize));
430 const result = try build.wait();
431 if (result != .Exited or result.Exited != 0) {
432 try err.writer().print("[Process {s} with {d}]", .{
433 switch (result) {
434 .Exited => "exited",
435 .Stopped => "stopped",
436 .Signal => "signaled",
437 .Unknown => "hit unknown state",
438 },
439 switch (result) {
440 inline else => |d| @as(u32, d),
441 },
442 });
443 }
444 example.terminal = try mem.concat(gpa, u8, &.{
445 "$ ",
446 switch (tag) {
447 .code_run => run_cli,
448 .code_build => build_cli,
449 else => unreachable,
450 },
451 " ",
452 example.filename,
453 "\n",
454 mem.trim(u8, err.items, " \n\r"),
455 });
456 const trimmed_out = mem.trim(u8, out.items, " \n\r");
457 if (trimmed_out.len > 0) {
458 example.javascript = try std.mem.concat(gpa, u8, &.{
459 "// Compiled JavaScript Code",
460 trimmed_out[mem.indexOfScalar(u8, trimmed_out, '\n').?..],
461 });
462 }
463
464 prog.completeOne();
465 prog_step.end();
466
467 if (tag == .code_run and runtime_path.len > 0 and trimmed_out.len > 0) {
468 const run_step = prog.start("Execute", 0);
469 defer run_step.end();
470
471 var temp_dir = std.testing.tmpDir(.{});
472 defer temp_dir.cleanup();
473
474 const input_sub_path = "result.js";
475 try temp_dir.dir.writeFile(.{
476 .sub_path = input_sub_path,
477 .data = example.javascript.?,
478 });
479
480 var env_map_run = try std.process.getEnvMap(gpa);
481 defer env_map_run.deinit();
482 try env_map_run.put("NO_COLOR", "1");
483 try env_map_run.put("TERM", "dumb");
484
485 var run_out = ArrayList(u8).init(gpa);
486 var run_err = ArrayList(u8).init(gpa);
487
488 const run_sub_command = std.mem.indexOf(u8, runtime_path, "deno") != null or
489 std.mem.indexOf(u8, runtime_path, "porf") != null;
490
491 const cmd: []const []const u8 = if (run_sub_command)
492 &.{ runtime_path, "run", input_sub_path }
493 else
494 &.{ runtime_path, input_sub_path };
495
496 var run = std.process.Child.init(cmd, gpa);
497
498 run.cwd_dir = temp_dir.dir;
499 var path_buf: [std.fs.max_path_bytes]u8 = undefined;
500 if (@import("builtin").os.tag == .windows)
501 run.cwd = try std.os.getFdPath(temp_dir.dir.fd, &path_buf);
502
503 run.stdin_behavior = .Ignore;
504 run.stdout_behavior = .Pipe;
505 run.stderr_behavior = .Pipe;
506
507 run.env_map = &env_map_run;
508 try run.spawn();
509
510 try run.collectOutput(&run_out, &run_err, std.math.maxInt(usize));
511 const run_result = run.wait() catch |e| {
512 if (e == error.FileNotFound) {
513 std.debug.panic(
514 \\Missing JavaScript evaluator: {s}
515 \\Specify another one using -Djs_runtime=... or set to "" to skip evaluation.
516 \\
517 , .{runtime_path});
518 }
519 return e;
520 };
521
522 if (run_result != .Exited or run_result.Exited != 0) {
523 try run_err.writer().print("[Process {s} with {d}]", .{
524 switch (run_result) {
525 .Exited => "exited",
526 .Stopped => "stopped",
527 .Signal => "signaled",
528 .Unknown => "hit unknown state",
529 },
530 switch (run_result) {
531 inline else => |d| @as(u32, d),
532 },
533 });
534 }
535
536 example.terminal = try std.mem.concat(gpa, u8, &.{
537 example.terminal.?,
538 run_out.items,
539 run_err.items,
540 });
541 }
542 }
543
544 try example.write(writer.any());
545 },
546 .ul => {
547 try writer.writeAll("<ul>");
548 while (t.str.len > 0 and (t.str[0] == '-')) {
549 _ = t.advance(1);
550 const line = t.advanceLine();
551 try writer.writeAll("<li>");
552 try processInlineMarkdown(gpa, line, writer);
553 while (t.str.len > 0 and (t.str[0] != '-' and t.str[0] != '\n')) {
554 const next_line = t.advanceLine();
555 try writer.writeAll(" ");
556 try processInlineMarkdown(gpa, next_line, writer);
557 }
558 try writer.writeAll("</li>");
559 }
560 try writer.writeAll("</ul>");
561 },
562 .table => {
563 try writer.writeAll("<table><tbody>");
564 {
565 try writer.writeAll("<tr>");
566 var it = mem.tokenizeScalar(u8, t.advanceLine()[1..], '|');
567 while (it.next()) |tok| {
568 try writer.writeAll("<th>");
569 try processInlineMarkdown(gpa, tok, writer);
570 try writer.writeAll("</th>");
571 }
572 try writer.writeAll("</tr>");
573 }
574 _ = t.advanceLine();
575 t.skipWhitespace();
576 while (t.str.len > 0 and t.str[0] == '|') {
577 try writer.writeAll("<tr>");
578 var it = mem.tokenizeScalar(u8, t.advanceLine()[1..], '|');
579 while (it.next()) |tok| {
580 try writer.writeAll("<td>");
581 try processInlineMarkdown(gpa, tok, writer);
582 try writer.writeAll("</td>");
583 }
584 try writer.writeAll("</tr>");
585 }
586 try writer.writeAll("</tbody></table>");
587 },
588 .blockquote => {
589 try writer.writeAll("<blockquote>");
590
591 var para = ArrayList(u8).init(gpa);
592 try para.appendSlice(t.advanceLine());
593
594 var next_line = t.advanceLine();
595 while (t.str.len > 0 and next_line.len > 0) {
596 try para.append(' ');
597 try para.appendSlice(next_line);
598 next_line = t.advanceLine();
599 }
600
601 try processInlineMarkdown(gpa, para.items, writer);
602
603 try writer.writeAll("</blockquote>");
604 },
605 }
606 }
607}
608
609pub fn nextInlineToken(src: []const u8) ?struct { InlineType, usize } {
610 var pos: usize = 0;
611 while (true) : (pos += 1) {
612 pos = mem.indexOfAnyPos(u8, src, pos, "*`[") orelse return null;
613
614 switch (src[pos]) {
615 '*' => switch (src[pos + 1]) {
616 '*' => return .{ .bold, pos },
617 else => continue,
618 },
619 '`' => return .{ .code, pos },
620 '[' => return .{ .link, pos },
621 else => unreachable,
622 }
623 }
624}
625
626fn processInlineMarkdown(gpa: Allocator, src: []const u8, writer: anytype) !void {
627 var bold = false;
628 var remain = src;
629
630 while (nextInlineToken(remain)) |tok| {
631 try writer.writeAll(remain[0..tok[1]]);
632 remain = remain[tok[1]..];
633 switch (tok[0]) {
634 .bold => {
635 bold = !bold;
636 try writer.writeAll(switch (bold) {
637 true => "<strong>",
638 false => "</strong>",
639 });
640 remain = remain[2..];
641 },
642 .code => {
643 const end = mem.indexOfScalarPos(u8, remain, 1, '`') orelse @panic("invalid code block");
644 const code = remain[1..end];
645 try writer.writeAll("<code>");
646 try writeTokenizedCode(gpa, code, writer);
647 try writer.writeAll("</code>");
648 remain = remain[end + 1 ..];
649 },
650 .link => {
651 const label_end = mem.indexOfScalarPos(u8, remain, 1, ']') orelse @panic("invalid link block");
652 assert(remain[label_end + 1] == '('); // invalid link. '[' syntax is not implemented
653
654 const href_end = mem.indexOfScalarPos(u8, remain, label_end + 2, ')') orelse @panic("invalid link block");
655
656 const label = remain[1..label_end];
657 const href = remain[label_end + 2 .. href_end];
658
659 try writer.writeAll("<a href=\"");
660 try writeHtmlEscapedSlice(href, writer, true);
661 if (!mem.startsWith(u8, href, "#")) {
662 try writer.writeAll("\" class=\"external\" target=\"_blank\" ref=\"noopener noreferrer");
663 }
664 try writer.writeAll("\">");
665 try processInlineMarkdown(gpa, label, writer);
666 try writer.writeAll("</a>");
667 remain = remain[href_end + 1 ..];
668 },
669 }
670 }
671 try writer.writeAll(remain);
672
673 if (bold) {
674 std.debug.panic("Unterminated Bold in '{s}'", .{src});
675 }
676}
677
678// TODO: Change this to use Tree-sitter, which would also make it very simple
679// to highlight JavaScript code in addition to the language.
680fn writeTokenizedCode(gpa: Allocator, code: []const u8, writer: anytype) !void {
681 const code_z = try gpa.dupeZ(u8, code);
682 defer gpa.free(code_z);
683
684 var t = core.Tokenizer{ .code = code_z };
685
686 t.skipWhitespace();
687 var last: usize = 0;
688 while (true) {
689 t.skipWhitespace();
690 const tok = t.next() orelse break;
691 try writeHtmlEscapedSlice(t.code[last..tok.offset], writer, false);
692 try writer.print("<span class='tok-{s}{s}'>{s}</span>", .{
693 @tagName(tok.tag),
694 if (tok.tag.isKeyword()) " kw" else "",
695 t.code[tok.offset..t.pos],
696 });
697 last = t.pos;
698 }
699 try writeHtmlEscapedSlice(t.code[last..], writer, false);
700}
701
702fn writeAnsi(code: []const u8, writer: anytype) !void {
703 var remain = code;
704 var count: usize = 0;
705 while (mem.indexOfScalar(u8, remain, '\x1b')) |esc_i| {
706 try writeHtmlEscapedSlice(remain[0..esc_i], writer, false);
707 switch (remain[esc_i + 1]) {
708 '[' => switch (remain[esc_i + 2]) {
709 '0'...'9' => {
710 const end = mem.indexOfScalarPos(u8, remain, esc_i + 2, 'm').?;
711 const int = std.fmt.parseInt(u8, remain[esc_i + 2 .. end], 10) catch unreachable;
712 switch (int) {
713 else => {
714 try writer.print("<span class='ansi-{d}'>", .{int});
715 count += 1;
716 },
717 0 => {
718 for (0..count) |_| {
719 try writer.writeAll("</span>");
720 }
721 count = 0;
722 },
723 }
724 remain = remain[end + 1 ..];
725 },
726 else => |x| std.debug.panic("Unknown esc + 2: {d}", .{x}),
727 },
728 else => |x| std.debug.panic("Unknown esc + 1: {d}", .{x}),
729 }
730 }
731 try writeHtmlEscapedSlice(remain, writer, false);
732}
733
734fn nameToHeaderId(alloc: Allocator, input: []const u8) ![]const u8 {
735 var out = try ArrayList(u8).initCapacity(alloc, input.len);
736
737 var is_split = false;
738 for (input) |char| {
739 switch (char) {
740 'a'...'z', 'A'...'Z', '0'...'9' => {
741 if (is_split) {
742 out.appendAssumeCapacity('-');
743 is_split = false;
744 }
745 out.appendAssumeCapacity(std.ascii.toLower(char));
746 },
747 else => {
748 is_split = true;
749 },
750 }
751 }
752
753 return try out.toOwnedSlice();
754}
755
756fn replaceInTemplate(alloc: Allocator, template: []const u8, replacements: anytype) ![]const u8 {
757 const Replacements = @TypeOf(replacements);
758 const Map = std.StringArrayHashMapUnmanaged([]const u8);
759
760 var map: Map = .{};
761 inline for (@typeInfo(Replacements).@"struct".fields) |field| {
762 try map.put(alloc, field.name, @field(replacements, field.name));
763 }
764
765 var out: ArrayListUnmanaged(u8) = .{};
766 const w = out.writer(alloc);
767
768 var i: usize = 0;
769 while (mem.indexOfScalarPos(u8, template, i, '{')) |open_at| {
770 const end = mem.indexOfScalarPos(u8, template, open_at + 1, '}') orelse break;
771 const inner = template[open_at + 1 .. end];
772
773 if (map.get(inner)) |replacement| {
774 // write previous
775 try w.writeAll(template[i..open_at]);
776 // write template
777 try w.writeAll(replacement);
778 // move one past the ending '}'
779 i = end + 1;
780 } else {
781 // write up to and including the {
782 try w.writeAll(template[i .. open_at + 1]);
783 // move one past it
784 i = open_at + 1;
785 }
786 }
787
788 // write ending
789 try w.writeAll(template[i..]);
790
791 return out.items;
792}
793
794fn writeHtmlEscapedSlice(text: []const u8, writer: anytype, escape_quotes: bool) !void {
795 var i: usize = 0;
796 while (mem.indexOfAnyPos(
797 u8,
798 text,
799 i,
800 if (escape_quotes)
801 &.{ '&', '<', '>', '"' }
802 else
803 &.{ '&', '<', '>' },
804 )) |at| {
805 const replace = switch (text[at]) {
806 '&' => "&amp;",
807 '<' => "&lt;",
808 '>' => "&gt;",
809 '"' => "&quot;",
810 else => unreachable,
811 };
812 try writer.writeAll(text[i..at]);
813 try writer.writeAll(replace);
814 i = at + 1;
815 }
816 try writer.writeAll(text[i..]);
817}
818
819fn fmtHtmlEscape(text: []const u8, escape_quotes: bool) std.fmt.Formatter(fmtHtmlEscapeImpl) {
820 return .{ .data = .{ .text = text, .escape_quotes = escape_quotes } };
821}
822
823fn fmtHtmlEscapeImpl(data: struct { text: []const u8, escape_quotes: bool }, _: []const u8, _: std.fmt.FormatOptions, w: std.io.AnyWriter) !void {
824 try writeHtmlEscapedSlice(data.text, w, data.escape_quotes);
825}
src/manual/template.html+687-132
......@@ -1,134 +1,689 @@
1<!doctype html>
1<!DOCTYPE html>
22<html lang="en">
3 <head>
4 <meta charset="UTF-8" />
5 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6 <title>Language Reference</title>
7 <style>
8 * {
9 margin: 0;
10 padding: 0;
11 box-sizing: border-box;
12 }
13 body {
14 background-color: #fefefe;
15 font-family:
16 system-ui,
17 -apple-system,
18 BlinkMacSystemFont,
19 "Segoe UI",
20 Roboto,
21 Oxygen,
22 Ubuntu,
23 Cantarell,
24 "Open Sans",
25 "Helvetica Neue",
26 sans-serif;
27 max-width: 45rem;
28 padding: 1rem;
29 margin: auto;
30 }
31 h1,
32 h2,
33 h3,
34 h4,
35 h5,
36 h6,
37 p,
38 ul,
39 pre,
40 blockquote,
41 table {
42 margin-bottom: 1rem;
43 }
44 ul {
45 padding-left: 2rem;
46 }
47 li {
48 margin-bottom: 0.25rem;
49 }
50 blockquote {
51 background-color: #ddd;
52 border-left: 4px solid black;
53 padding-left: 16px;
54 padding-top: 8px;
55 padding-bottom: 8px;
56 }
57 pre > code {
58 border: none;
59 padding: 0;
60 background-color: transparent;
61 }
62 code,
63 pre {
64 font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
65 "Liberation Mono", "Courier New", monospace;
66 font-size: 0.9rem;
67 border: 1px solid #9ca0b0;
68 background-color: #eff1f5;
69 border-radius: 4px;
70 padding: 2px 4px;
71 }
72 pre {
73 padding: 0.75rem 0.75rem;
74 }
75 .term {
76 border: 1px solid #11111b;
77 background-color: #24273a;
78 color: #cdd6f4;
79 }
80 table,
81 th,
82 td {
83 border: 1px solid #9ca0b0;
84 border-collapse: collapse;
85 }
86 th,
87 td {
88 padding: 0.5rem;
89 }
90 .ansi-1 {
91 font-weight: bold;
92 }
93 .ansi-31 {
94 color: #ed8796;
95 }
96 .ansi-32 {
97 color: #a6e3a1;
98 }
99 .ansi-33 {
100 color: #eed49f;
101 }
102 .ansi-34 {
103 color: #8aadf4;
104 }
105 .ansi-35 {
106 color: #f5c2e7;
107 }
108 .ansi-36 {
109 color: #94e2d5;
110 }
111 .ansi-2 {
112 color: #8087a2;
113 }
114 .kw {
115 color: #8839ef;
116 font-weight: bold;
117 }
118 .tok-string {
119 color: #40a02b;
120 }
121 .tok-builtin_id {
122 color: #d20f39;
123 font-weight: bold;
124 }
125 .tok-number_int,
126 .tok-number_float {
127 color: #fe640b;
128 }
129 </style>
130 </head>
131 <body>
132 <main>{contents}</main>
133 </body>
3 <head>
4 <meta charset="UTF-8" />
5 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6 <title>Ix Language Manual</title>
7 <style>
8 :root {
9 /* Catppuccin Latte (light) & Mocha (dark) */
10 --mantle: light-dark(#e6e9ef, #181825);
11 --crust: light-dark(#dce0e8, #11111b);
12 --base: light-dark(#eff1f5, #1e1e2e);
13 --surface-0: light-dark(#ccd0da, #313244);
14 --surface-1: light-dark(#bcc0cc, #45475a);
15 --surface-2: light-dark(#acb0be, #585b70);
16 --overlay-0: light-dark(#9ca0b0, #6c7086);
17 --overlay-1: light-dark(#8c8fa1, #7f849c);
18 --overlay-2: light-dark(#7c7f93, #9399b2);
19 --subtext-0: light-dark(#6c6f85, #a6adc8);
20 --subtext-1: light-dark(#5c5f77, #bac2de);
21 --text: light-dark(#4c4f69, #cdd6f4);
22 --accent: light-dark(#179299, #94e2d5);
23 --link: light-dark(#179299, #94e2d5);
24 }
25
26 * {
27 margin: 0;
28 padding: 0;
29 box-sizing: border-box;
30 }
31
32 body {
33 background-color: var(--base);
34 color: var(--text);
35 font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
36 Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue",
37 sans-serif;
38 color-scheme: light dark;
39 display: flex;
40 }
41
42 main {
43 width: 100%;
44 max-width: 80ch;
45 padding: 1rem;
46 padding-top: 2rem;
47 margin: auto;
48 line-height: 1.5;
49 }
50
51 h1,
52 h2,
53 h3,
54 h4,
55 h5,
56 h6,
57 p,
58 ul,
59 pre,
60 blockquote,
61 table {
62 margin-bottom: 1rem;
63 }
64
65 h2,
66 h3,
67 h4,
68 h5,
69 h6 {
70 padding-top: 0.8rem;
71 margin-top: -0.8rem;
72 }
73
74 ul {
75 padding-left: 2rem;
76 }
77
78 li {
79 margin-bottom: 0.25rem;
80 }
81
82 blockquote {
83 background-color: var(--surface-0);
84 border-left: 4px solid var(--overlay-2);
85 padding-left: 16px;
86 padding-top: 8px;
87 padding-bottom: 8px;
88 }
89
90 a {
91 color: inherit;
92 }
93
94 table,
95 th,
96 td {
97 border: 1px solid #9ca0b0;
98 border-collapse: collapse;
99 }
100
101 th,
102 td {
103 padding: 0.5rem;
104 }
105
106 .ansi-1 {
107 font-weight: bold;
108 }
109
110 .ansi-31 {
111 color: light-dark(#d20f39, #f38ba8);
112 }
113
114 .ansi-32 {
115 color: light-dark(#40a02b, #a6e3a1);
116 }
117
118 .ansi-33 {
119 color: light-dark(#de8d1c, #f9e2af);
120 }
121
122 .ansi-34 {
123 color: light-dark(#1e66f5, #89b4fa);
124 }
125
126 .ansi-35 {
127 color: light-dark(#ea76cb, #f5c2e7);
128 }
129
130 .ansi-36 {
131 color: light-dark(#179299, #94e2d5);
132 }
133
134 .ansi-2 {
135 color: light-dark(#bcc0cc, #8087a2);
136 }
137
138 .kw {
139 color: light-dark(#8839ef, #cba6f7);
140 font-weight: bold;
141 }
142
143 .tok-string {
144 color: light-dark(#40a02b, #a6e3a1);
145 }
146
147 .tok-builtin_id {
148 color: light-dark(#d20f39, #f38ba8);
149 font-weight: bold;
150 }
151
152 .tok-number_int,
153 .tok-number_float {
154 color: light-dark(#fe640b, #fab387);
155 }
156
157 .tok-semi,
158 .tok-end_paren,
159 .tok-start_paren,
160 .tok-equal,
161 .tok-colon,
162 .tok-start_brace,
163 .tok-end_brace,
164 .tok-dot {
165 color: light-dark(#7c7f93, #9399b2);
166 }
167
168 aside {
169 position: sticky;
170 top: 0;
171 left: 0;
172 height: 100dvh;
173 height: 100vh;
174 }
175
176 #sidebar-content {
177 display: flex;
178 flex-direction: column;
179 background-color: var(--mantle);
180 height: 100%;
181 padding: 1rem;
182 border-radius: 0 1rem 1rem 0;
183 overflow-y: auto;
184 width: 40ch;
185 }
186
187 #toc,
188 #toc ul,
189 #links {
190 list-style: none;
191 }
192
193 nav header {
194 font-weight: bold;
195 font-size: 1.5rem;
196 }
197
198 #toc {
199 padding: 0;
200 }
201
202 #toc ul {
203 margin: 0rem;
204 padding-left: 1rem;
205 margin-bottom: 0;
206 }
207
208 #toc li {
209 margin: 0;
210 }
211
212 #toc ul li {
213 border-left: 1px solid var(--surface-2);
214 padding-left: 0.5rem;
215 }
216
217 #toc ul li:hover {
218 border-left: 1px solid var(--overlay-2);
219 }
220
221 #toc .number {
222 color: var(--accent);
223 font-weight: bold;
224 }
225
226 aside a {
227 position: relative;
228 /* anchor point for 'aside a::before' */
229 display: block;
230 text-decoration: none;
231 color: var(--text);
232 padding: 0.45rem 0.75rem;
233 border-radius: 0.25rem;
234 }
235
236 /* this invisible hitbox lets you click in the left gutter */
237 aside a::before {
238 content: " ";
239 position: absolute;
240 display: block;
241 right: 100%;
242 top: 0;
243 bottom: 0;
244 width: 100vw;
245 }
246
247 aside a:hover {
248 background-color: var(--surface-0);
249 }
250
251 #links {
252 list-style: none;
253 padding: 0;
254 margin-top: auto;
255 }
256
257 #links li {
258 margin: 0;
259 }
260
261 #hidden-menu-checkbox,
262 [name="tabs"] {
263 display: block;
264 width: 1px;
265 height: 1px;
266 position: fixed;
267 top: -3px;
268 left: -3px;
269 }
270
271 #menu-btn,
272 #clickable-background {
273 display: none;
274 }
275
276 main a {
277 color: var(--link);
278 }
279
280 main a:active {
281 background-color: var(--accent);
282 color: var(--base);
283 border-radius: 0.25rem;
284 text-decoration: none;
285 }
286
287 .external::after {
288 background-color: var(--link);
289 content: "";
290 display: inline-flex;
291 height: 10px;
292 margin-left: 4px;
293 mask-image: url("data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMTYgMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTcuNSAxLjVWMEgxNnY4LjVoLTEuNVYyLjZMNyAxMC4xIDYgOWw3LjUtNy42aC02Wk0xMyAxNmMuNiAwIDEtLjQgMS0xdi01aC0xLjV2NC41aC0xMXYtMTFINlYySDFhMSAxIDAgMCAwLTEgMXYxMmMwIC42LjQgMSAxIDFoMTJaIiBmaWxsPSJyZWQiLz48L3N2Zz4=");
294 mask-size: cover;
295 width: 10px;
296 }
297 .external:active::after {
298 background-color: var(--base);
299 }
300
301 .heading {
302 color: inherit;
303 text-decoration: none;
304 }
305
306 .heading:hover,
307 .heading:focus-visible {
308 text-decoration: underline;
309 position: relative;
310 }
311
312 .heading:hover::before,
313 .heading:focus-visible::before {
314 content: " #";
315 color: var(--subtext-0);
316 position: absolute;
317 right: calc(100% + 0.5rem);
318 top: -0.125em;
319 }
320
321 /* Tablet/small screen layout */
322 @media screen and (max-width: 1000px) {
323 html {
324 scroll-padding-top: 5rem;
325 }
326
327 main {
328 padding-top: 5rem;
329 }
330
331 #hidden-menu-checkbox:focus-visible+aside #menu-btn {
332 outline: 2px solid var(--text);
333 }
334
335 aside {
336 position: fixed;
337 left: -100%;
338 transition: left 500ms, bottom 300ms;
339 z-index: 9;
340 padding-top: 3rem;
341 }
342
343 aside a {
344 padding: 0.5rem 0.75rem;
345 }
346
347 #top-part {
348 position: fixed;
349 inset: 0;
350 height: fit-content;
351 background-color: var(--mantle);
352 padding: 0.5rem;
353 box-shadow: 0 4px 4px rgba(0, 0, 0, 0.3);
354 z-index: 9;
355 }
356
357 #menu-btn {
358 display: grid;
359 padding: 0.5rem;
360 width: fit-content;
361 cursor: pointer;
362 border-radius: 0.25rem;
363 user-select: none;
364 }
365
366
367 #close {
368 display: none;
369 }
370
371 #chevron {
372 display: none;
373 }
374
375 #menu-btn:hover {
376 background-color: var(--surface-0);
377 }
378
379 #hidden-menu-checkbox:checked~aside {
380 left: 0;
381 transition: left 300ms, bottom 300ms;
382 }
383
384 #hidden-menu-checkbox:checked~aside #clickable-background {
385 display: block;
386 position: fixed;
387 width: 100%;
388 height: 100%;
389 inset: 0;
390 background-color: rgba(0, 0, 0, 0.5);
391 z-index: -1;
392 }
393
394 #hidden-menu-checkbox:checked~aside #menu-btn .open {
395 display: none;
396 }
397
398 #hidden-menu-checkbox:checked~aside #menu-btn .close {
399 display: block;
400 }
401 }
402
403 /* Mobile layout */
404 @media screen and (max-width: 600px) {
405 main {
406 padding: 3rem 1rem;
407 }
408
409 aside {
410 position: fixed;
411 top: initial;
412 left: 0;
413 bottom: calc(-100% + 6rem);
414 width: 100%;
415 display: flex;
416 flex-direction: column;
417 }
418
419 aside a {
420 padding: 0.8rem 0.75rem;
421 }
422
423 aside a:hover {
424 background-color: var(--surface-1);
425 }
426
427 #sidebar-content {
428 border-radius: 0px;
429 padding: 1rem;
430 width: 100%;
431 background-color: var(--surface-0);
432 }
433
434 #top-part {
435 position: initial;
436 padding: 0;
437 border-radius: 1.5rem 1.5rem 0 0;
438 box-shadow: 0 -4px 5px rgba(0, 0, 0, 0.15);
439 border-bottom: var(--overlay-2) 1px solid;
440 background-color: var(--surface-0);
441 }
442
443 #open,
444 #close {
445 display: none !important;
446 }
447
448 #chevron {
449 display: block;
450 transition: transform 300ms;
451 }
452
453 #menu-btn {
454 display: flex;
455 justify-content: center;
456 width: 100%;
457 padding: 1.5rem 0;
458 border-radius: 1.5rem 1.5rem 0 0;
459 }
460
461 #hidden-menu-checkbox:checked~aside {
462 bottom: 0;
463 }
464
465 #hidden-menu-checkbox:checked~aside #chevron {
466 transform: rotateX(180deg);
467 }
468
469 #hidden-menu-checkbox:checked~aside #top-part {
470 box-shadow: none;
471 }
472 }
473
474 pre>code {
475 border: none;
476 padding: 0;
477 }
478
479 pre,
480 code {
481 border: 0;
482 background-color: transparent;
483 font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
484 "Liberation Mono", "Courier New", monospace;
485 font-size: 0.9rem;
486 }
487
488 .file {
489 border: 1px solid var(--surface-1);
490 background-color: var(--crust);
491 border-radius: 0.5rem;
492 overflow: hidden;
493 margin-bottom: 0.5rem;
494 }
495
496 .file figcaption {
497 padding: 0.25rem 0.5rem;
498 font-size: 0.75rem;
499 border-bottom: 1px solid var(--surface-1);
500 /* background-color: var(--crust); */
501 background-color: var(--accent);
502 color: var(--base);
503 }
504
505 .file pre {
506 margin: 0;
507 padding: 0.75rem 0.5rem;
508 overflow-y: scroll;
509 color: var(--text);
510 position: relative;
511 }
512
513 .file pre.terminal {
514 background-color: #24273a;
515 color: #cdd6f4;
516 border-radius: 0 0 0.5rem 0.5rem;
517 }
518
519 .file [role="tablist"] {
520 display: flex;
521 border-top: 1px solid var(--surface-1);
522 border-bottom: 1px solid var(--surface-1);
523 background-color: var(--surface-0);
524 }
525
526 .file [role="tab"] {
527 padding: 0.25rem 0.75rem;
528 cursor: pointer;
529 /* flex: 1; */
530 border-top: 2px solid transparent;
531 text-align: center;
532 font-size: 0.9rem;
533 }
534
535 .file .bottom {
536 display: none;
537 }
538
539 [name="tabs"][value="output"]:checked~main .file .tabs .output {
540 display: block;
541 }
542
543 [name="tabs"][value="output"]:checked~main .file .tabs [role="tab"][for="output"] {
544 font-weight: bold;
545 background-color: var(--base);
546 border-top: 2px solid var(--accent);
547 }
548
549 [name="tabs"][value="js"]:checked~main .file .tabs .js {
550 display: block;
551 }
552
553 [name="tabs"][value="js"]:checked~main .file .tabs [role="tab"][for="js"] {
554 font-weight: bold;
555 border-top: 2px solid var(--accent);
556 background-color: var(--base);
557 }
558
559 .file .copy-button {
560 position: absolute;
561 top: 0.25rem;
562 right: 0.25rem;
563 padding: 0.25rem;
564 border: 1px solid var(--surface-1);
565 border-radius: 0.25rem;
566 background-color: var(--base);
567 cursor: pointer;
568 display: grid;
569 }
570
571 .file .copy-button:hover {
572 background-color: var(--crust);
573 }
574
575 .file .copy-button .hidden {
576 display: none;
577 }
578 </style>
579 </head>
580
581 <body>
582 <input type="checkbox" id="hidden-menu-checkbox" />
583 <input name="tabs" type="radio" id="output" value="output" checked="true" />
584 <input name="tabs" type="radio" id="js" value="js" />
585 <aside>
586 <div id="top-part">
587 <label
588 id="menu-btn"
589 for="hidden-menu-checkbox"
590 aria-label="Toggle menu visibility"
591 >
592 <svg
593 xmlns="http://www.w3.org/2000/svg"
594 height="24px"
595 viewBox="0 -960 960 960"
596 width="24px"
597 fill="currentColor"
598 id="open"
599 >
600 <path
601 d="M120-240v-80h720v80H120Zm0-200v-80h720v80H120Zm0-200v-80h720v80H120Z"
602 />
603 </svg>
604 <svg
605 xmlns="http://www.w3.org/2000/svg"
606 height="24px"
607 viewBox="0 -960 960 960"
608 width="24px"
609 fill="currentColor"
610 id="close"
611 >
612 <path
613 d="m256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z"
614 />
615 </svg>
616 <svg
617 id="chevron"
618 width="91"
619 height="8"
620 viewBox="0 0 91 8"
621 fill="none"
622 xmlns="http://www.w3.org/2000/svg"
623 >
624 <path
625 d="M2 6L45.5714 2L89.1429 6"
626 stroke="currentColor"
627 stroke-width="3"
628 stroke-linecap="square"
629 />
630 </svg>
631 </label>
632 </div>
633
634 <div id="sidebar-content">
635 <nav>
636 <header><a href="#top">{nav_h1}</a></header>
637 {navigation}
638 </nav>
639
640 <ul id="links">
641 <li>
642 <a href="{repository}" target="_blank" rel="noopener noreferrer">
643 GitHub
644 </a>
645 </li>
646 </ul>
647 </div>
648
649 <label id="clickable-background" for="hidden-menu-checkbox"></label>
650 </aside>
651 <main id="main">
652 {contents}
653 </main>
654 <script type="text/javascript">
655 const checkbox = document.getElementById("hidden-menu-checkbox");
656 const sidebar = document.getElementById("sidebar-content");
657 const sidebarLinks = sidebar.querySelectorAll("a");
658
659 sidebarLinks.forEach((link) => {
660 link.addEventListener("click", () => {
661 checkbox.checked = false;
662 });
663 });
664
665 addEventListener("keydown", (ev) => {
666 if (ev.key === "Escape" && checkbox.checked) {
667 checkbox.checked = false;
668 }
669 });
670
671 const copyButtons = document.querySelectorAll(".copy-button");
672
673 copyButtons.forEach((button) => {
674 button.addEventListener("click", () => {
675 const code = button.parentElement.querySelector("code");
676 navigator.clipboard.writeText(code.innerText);
677 const copySvg = button.children[0];
678 const checkSvg = button.children[1];
679 copySvg.classList.add('hidden');
680 checkSvg.classList.remove('hidden');
681 setTimeout(() => {
682 copySvg.classList.remove('hidden');
683 checkSvg.classList.add('hidden');
684 }, 1000);
685 });
686 });
687 </script>
688 </body>
134689</html>