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 @@...@@ -1,5 +1,6 @@
1## 0.1.71## 0.1.7
22
3- chore: rename the language to "ix"
3- feat: allow any expression as a function body4- feat: allow any expression as a function body
4- fix: coerce `break` operands and implicit breaks to result type5- fix: coerce `break` operands and implicit breaks to result type
5- fix: coerce results when `if` expression branches merge6- fix: coerce results when `if` expression branches merge
README.md+21-8
...@@ -1,21 +1,34 @@...@@ -1,21 +1,34 @@
1# New Language Project1# Ix
22
3This is a high-level, general purpose programming language. It is a mix of Rust,3Ix (**I**nferable E**x**pression) is a high-level, general purpose programming
4Zig, and JavaScript's great points.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 included6For a guide on how to use Ix, see the manual included in the binary
7in the binary distribution. (TODO: host a rendered version)7distribution. Additionally, the latest commit of the manual is hosted online at
8[language-manual.pages.dev](https://language-manual.pages.dev).
89
9## Building10## Building
1011
11The primary dependency of the language SDK is a Zig compiler. If one is not12The primary dependency of the Ix compiler is a Zig compiler. If one is not
12available, the `./build` and `.\build.cmd` scripts can substitute for running13available, the `./build` and `.\build.cmd` scripts can substitute for running
13`zig build` on POSIX and Windows systems, respectively. With a Zig compiler, a14`zig build` on POSIX and Windows systems, respectively. With a Zig compiler, a
14release build can be produced in a single command:15release build can be produced in a single command:
1516
16 zig build --release17 zig build --release
1718
18To run the standard packaging procedure, which builds the SDK for all officially19To run the standard packaging procedure, which builds the compiler for all
19supported targets:20officially supported targets:
2021
21 zig build package22 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 {...@@ -18,8 +18,8 @@ pub const Options = struct {
18 config: BuildConfig,18 config: BuildConfig,
19};19};
2020
21/// `createArtifactGroup` multiple times in order to have different21/// `createArtifactGroup` is called multiple times in order to have different
22/// targets and configurations of the SDK at the same time.22/// targets and configurations of Ix at the same time.
23const ArtifactGroup = struct {23const ArtifactGroup = struct {
24 build: *Build,24 build: *Build,
25 opts: Options,25 opts: Options,
...@@ -446,10 +446,10 @@ pub fn build(b: *Build) !void {...@@ -446,10 +446,10 @@ pub fn build(b: *Build) !void {
446 test_all.dependOn(run_step);446 test_all.dependOn(run_step);
447 }447 }
448448
449 // Language Reference Generator449 // Language Manual Generator
450 const manual_generator = b.addExecutable(.{450 const manual_generator = b.addExecutable(.{
451 .name = "generate-man",451 .name = "generate_manual",
452 .root_source_file = b.path("src/manual/generate-man.zig"),452 .root_source_file = b.path("src/manual/generate_manual.zig"),
453 .target = b.graph.host,453 .target = b.graph.host,
454 .optimize = .Debug,454 .optimize = .Debug,
455 .link_libc = true,455 .link_libc = true,
...@@ -533,30 +533,21 @@ pub fn build(b: *Build) !void {...@@ -533,30 +533,21 @@ pub fn build(b: *Build) !void {
533 .version = version,533 .version = version,
534 .have_embedded_quickjs = have_embedded_quickjs,534 .have_embedded_quickjs = have_embedded_quickjs,
535 },535 },
536 .optimize = optimize,536 .optimize = .ReleaseSafe,
537 .target = cross_target,537 .target = cross_target,
538 .no_llvm = no_llvm,538 .no_llvm = no_llvm,
539 });539 });
540540
541 inline for (.{541 package.dependOn(&b.addInstallFile(
542 .{ artifacts.cli, "compiler" },542 artifacts.cli.getEmittedBin(),
543 }) |entry| {543 b.pathJoin(&.{
544 const artifact, const bin_name = entry;544 "package",
545 package.dependOn(&b.addInstallFile(545 name,
546 artifact.getEmittedBin(),546 b.fmt("ix{s}", .{cross_target.result.exeFileExt()}),
547 b.pathJoin(&.{547 }),
548 "package",548 ).step);
549 name,
550 b.fmt("{s}{s}", .{
551 bin_name,
552 cross_target.result.exeFileExt(),
553 }),
554 }),
555 ).step);
556 }
557
558 package.dependOn(&b.addInstallFile(b.path("CHANGELOG.md"), "package/CHANGELOG.md").step);549 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);
560 }551 }
561552
562 {553 {
ci+10-5
...@@ -11,9 +11,11 @@ commit_short=$(echo "$commit" | cut -c -9)...@@ -11,9 +11,11 @@ commit_short=$(echo "$commit" | cut -c -9)
11./build --version11./build --version
12typos12typos
13dprint check13dprint check
14./build all --summary new
1514
16if [ -n "$BUILDKITE" ]; then15if [ -n "$BUILDKITE" ]; then
16 # perform the entire build
17 ./build all --summary all
18
17 pushd zig-out/package19 pushd zig-out/package
18 zip -r compiler-$commit.zip *20 zip -r compiler-$commit.zip *
19 buildkite-agent artifact upload compiler-$commit.zip21 buildkite-agent artifact upload compiler-$commit.zip
...@@ -21,12 +23,15 @@ if [ -n "$BUILDKITE" ]; then...@@ -21,12 +23,15 @@ if [ -n "$BUILDKITE" ]; then
2123
22 rm -fr zig-out/site24 rm -fr zig-out/site
23 mkdir zig-out/site25 mkdir zig-out/site
24 cp zig-out/package/README.html zig-out/site/index.html26 cp zig-out/package/ix.html zig-out/site/index.html
25 branch=$BUILDKITE_BRANCH27 branch="$BUILDKITE_BRANCH"
26 if [ "$branch" = "dev" ]; then28 if [ "$branch" = "dev" ]; then
27 branch=main29 branch=main
28 fi30 fi
29 CLOUDFLARE_API_TOKEN=$(buildkite-agent secret get CLOUDFLARE_API_TOKEN) \31 CLOUDFLARE_API_TOKEN="$(buildkite-agent secret get CLOUDFLARE_API_TOKEN)" \
30 CLOUDFLARE_ACCOUNT_ID=$(buildkite-agent secret get CLOUDFLARE_ACCOUNT_ID) \32 CLOUDFLARE_ACCOUNT_ID="$(buildkite-agent secret get CLOUDFLARE_ACCOUNT_ID)" \
31 wrangler pages deploy zig-out/site --branch "$branch" --commit-hash "$commit" --no-bundle --project-name language-manual33 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
32fi37fi
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,54 +1,65 @@
1# New Language Project1# 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
5Key goals of this project are:6Key goals of this project are:
67
7- **Ease of Use**: Use of type inference keeps code concise and easy to read and8- **Ease of Use**: Use of type inference and simple syntax makes code easy to
8 write.9 read and write.
9- **Fast Iteration**: The compiler is designed for quick incremental compilation10- **Fast Iteration**: Incremental compilation keeps development productivity
10 and hot module reloading. (note: incremental is not yet available)11 high.
11- TODO: some more12- **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
13It primarily focuses on merging some great points of Rust, Zig, and JavaScript's17It primarily focuses on merging some great points of Rust, Zig, and JavaScript's
14language designs:18language designs:
1519
16- **Rust** - Expressive control flow that is easy to write.20- [**Rust**](https://www.rust-lang.org) - Expressive and concise control flow
17- **Zig** - Execute any code at compile time; everything is an expression.21 that is easy to write.
18- **JavaScript** - Garbage collected values with a versatile type system.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 Library27This manual is a standalone HTML document. Code samples are compiled and tested
2128as a part of the release cycle.
22Currently, there is no standard library. One will be introduced later.
2329
24## Hello World30## Hello World
2531
26```lang:run32```lang:run
27let print = @extern("console").log;33let print = @extern("console").log;
28
29fn main {34fn main {
30 print("Hello, world!");35 print("Hello, world!");
31}36}
32```37```
3338
34The `print` function is implemented by getting an external reference to the39The `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
37## Primitive Data Types46## Primitive Data Types
3847
39| name | ts equivalent | description |48| name | ts equivalent | description |
40| -------- | --------------------- | ---------------------------------------------- |49| ---------- | ----------------------------- | ---------------------------------------------- |
41| `int` | `number` | a number in signed 32-bit range |50| `bool` | `boolean` | either `true` or `false` |
42| `float` | `number` | a 64-bit floating point number |51| `float` | `number` | a 64-bit floating point number |
43| `bool` | `boolean` | either `true` or `false` |52| `int` | `number` | an integer number in signed 32-bit range |
44| `string` | `string` | a sequence of text characters |53| `bigint` | `bigint` | an integer number with no range limit |
45| `void` | `undefined` or `null` | detectable lack of a value |54| `string` | `string` | a sequence of text characters |
46| `opaque` | `unknown` or `any` | could be any value |55| `void` | `undefined` / `null` / `void` | detectable lack of a value |
47| `never` | `never` | control flow prevents this value from existing |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 added59> **Note**: Unlike JavaScript, the values `undefined` and `null` are merged into
60> one concept: [void](#void)
5061
51## Built-in Constants62### Built-in Constants
5263
53In addition to the above types being built-in constants, the following are64In addition to the above types being built-in constants, the following are
54available.65available.
...@@ -62,9 +73,48 @@ available....@@ -62,9 +73,48 @@ available.
62These constants are reserved names, and it is an error to re-declare them.73These constants are reserved names, and it is an error to re-declare them.
6374
64```lang:build75```lang:build
65let true = 1 == 0;76fn main {
77 let true = 1 == 0;
78}
66```79```
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
68## Variables118## Variables
69119
70There are two types of variables, `let` and `var`.120There are two types of variables, `let` and `var`.
...@@ -74,24 +124,30 @@ expression within. Referring to these identifiers is equivalent to inlining it...@@ -74,24 +124,30 @@ expression within. Referring to these identifiers is equivalent to inlining it
74where it was used, aside from side effect duplication.124where it was used, aside from side effect duplication.
75125
76```lang:run126```lang:run
77let hello: int = 1;127let print = @extern("console").log;
78let world = hello + 2;
79let hello = hello * world;
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}
82```136```
83137
84`var` bindings create mutable memory locations at runtime. This means they can138`var` bindings create mutable memory locations at runtime. This means they can
85be re-assigned to:139be re-assigned to:
86140
87```lang:run141```lang:run
88var message = "first message";142fn main {
143 var message = "first message";
89144
90if @extern("Math").random() > 0.5 {145 if @extern("Math").random() > 0.5 {
91 message = "second message";146 message = "second message";
92}147 }
93148
94@extern("console").log(message);149 @extern("console").log(message);
150}
95```151```
96152
97Using `let` is preferred whenever possible, as it will lead to more readable153Using `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...@@ -99,13 +155,15 @@ code, and the compiler is able to optimize it better. The above can be rewritten
99as:155as:
100156
101```lang:run157```lang:run
102let message = if @extern("Math").random() > 0.5 {158fn main {
103 "second message"159 let message = if @extern("Math").random() > 0.5 {
104} else {160 "second message"
105 "first message"161 } else {
106};162 "first message"
163 };
107164
108@extern("console").log(message);165 @extern("console").log(message);
166}
109```167```
110168
111It should be noted that numeric literals are a comptime-only data type, meaning169It 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...@@ -114,7 +172,7 @@ it cannot be assigned to a `var` without an explicit type annotation. Since
114float, but a compiler warning will be emitted.172float, but a compiler warning will be emitted.
115173
116```lang174```lang
117var number = 0; // `number` is a `float`, but not explicit175var number = 0; // `number` is a `float`, but not explicit
118var number: int = 0; // `number` is `int`176var number: int = 0; // `number` is `int`
119var number: float = 0; // `number` is `float`177var number: float = 0; // `number` is `float`
120```178```
...@@ -125,12 +183,15 @@ Functions are declared with the `fn` keyword. Parameter types must be annotated....@@ -125,12 +183,15 @@ Functions are declared with the `fn` keyword. Parameter types must be annotated.
125The function body is a block, which may use implicit return to return.183The function body is a block, which may use implicit return to return.
126184
127```lang:run185```lang:run
186let print = @extern("console").log;
187
128fn add(a: int, b: int) -> int {188fn add(a: int, b: int) -> int {
129 a + b189 a + b
130}190}
131191
132let print = @extern("console").log;192fn main {
133print("Sum of 2 and 7:", add(2, 7));193 print("Sum of 2 and 7:", add(2, 7));
194}
134```195```
135196
136> **Bug**: Return types are currently required. Omitting the `-> type` syntax197> **Bug**: Return types are currently required. Omitting the `-> type` syntax
...@@ -140,6 +201,8 @@ print("Sum of 2 and 7:", add(2, 7));...@@ -140,6 +201,8 @@ print("Sum of 2 and 7:", add(2, 7));
140The `return` expression can be used within functions to return early.201The `return` expression can be used within functions to return early.
141202
142```lang:run203```lang:run
204let print = @extern("console").log;
205
143fn something(a: int) -> bool {206fn something(a: int) -> bool {
144 if a == 0 {207 if a == 0 {
145 return false;208 return false;
...@@ -148,8 +211,9 @@ fn something(a: int) -> bool {...@@ -148,8 +211,9 @@ fn something(a: int) -> bool {
148 a > 100211 a > 100
149}212}
150213
151let print = @extern("console").log;214fn main {
152print(something(0xFF), something(0));215 print(something(0xFF), something(0));
216}
153```217```
154218
155## Number Literals219## Number Literals
...@@ -178,7 +242,9 @@ fn currentYear() -> int {...@@ -178,7 +242,9 @@ fn currentYear() -> int {
178 @jsNew(@extern("Date")).getFullYear()242 @jsNew(@extern("Date")).getFullYear()
179}243}
180244
181print(currentYear());245fn main {
246 print(currentYear());
247}
182```248```
183249
184A field access on an external handle is allowed to be performed at compile time,250A field access on an external handle is allowed to be performed at compile time,
...@@ -197,19 +263,21 @@ struct Position {...@@ -197,19 +263,21 @@ struct Position {
197 y: int,263 y: int,
198}264}
199265
200let center = Position{ x: 0, y: -2 };266fn main {
201print(center.x, center.y);267 let center = Position{ x: 0, y: -2 };
268 print(center.x, center.y);
202269
203fn addPositions(a: Position, b: Position) -> Position {
204 // To allow less verbose code, the struct type can be inferred with `.`270 // 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);
206}276}
207277
208let added = addPositions(278fn addPositions(a: Position, b: Position) -> Position {
209 .{ x: 1, y: -4 },279 .{ x: a.x + b.x, y: a.y + b.y }
210 .{ x: 5, y: 8 },280}
211);
212print("Add result:", added.x, added.y);
213```281```
214282
215To allow optimizations, `struct`s by default do not have a defined283To 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...@@ -217,11 +285,80 @@ representation in JavaScript. This means passing the struct to an opaque
217function is not allowed.285function is not allowed.
218286
219```lang:build287```lang:build
288let print = @extern("console").log;
289
220struct Person {290struct Person {
221 name: string,291 name: string,
222 age: int,292 age: int,
223}293}
224294
225let print = @extern("console").log;295fn main {
226print(Person{ name: "hello", age: 42 });296 print(Person{ name: "hello", age: 42 });
297}
227```298```
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 @@...@@ -2,6 +2,7 @@
2 "plugins": [2 "plugins": [
3 // sort-lines: start3 // sort-lines: start
4 "https://plugins.dprint.dev/exec-0.5.0.json@8d9972eee71fa1590e04873540421f3eda7674d0f1aae3d7c788615e7b7413d0",4 "https://plugins.dprint.dev/exec-0.5.0.json@8d9972eee71fa1590e04873540421f3eda7674d0f1aae3d7c788615e7b7413d0",
5 "https://plugins.dprint.dev/g-plane/markup_fmt-v0.12.0.wasm",
5 "https://plugins.dprint.dev/json-0.19.3.wasm",6 "https://plugins.dprint.dev/json-0.19.3.wasm",
6 "https://plugins.dprint.dev/markdown-0.17.2.wasm",7 "https://plugins.dprint.dev/markdown-0.17.2.wasm",
7 // sort-lines: end8 // 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...@@ -666,7 +666,7 @@ pub fn indexTegModule(comp: *Comp, file: *File, module_index: Teg.Inst.Index) !N
666666
667const Options = struct {667const Options = struct {
668 dump_ir: bool = false,668 dump_ir: bool = false,
669 verbose_sema: bool = true,669 verbose_sema: bool = false,
670};670};
671671
672/// The compiler backend is stored separately from the compilation, and672/// The compiler backend is stored separately from the compilation, and
src/core/InternPool.zig+1-1
...@@ -2,7 +2,7 @@...@@ -2,7 +2,7 @@
2//! https://github.com/ziglang/zig/blob/0.12.0/src/InternPool.zig2//! https://github.com/ziglang/zig/blob/0.12.0/src/InternPool.zig
3//!3//!
4//! All interned objects have both a Value and a Type. Type is a subset of4//! 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.
6//! This data structure is intended to be serializable for incremental6//! This data structure is intended to be serializable for incremental
7//! compilation efforts.7//! compilation efforts.
8//!8//!
src/core/Sema.zig+21-19
...@@ -693,6 +693,27 @@ fn stepInner(s: *Sema, comptime tag: Teg.Tag) Error!InstructionKind.fromTag(tag)...@@ -693,6 +693,27 @@ fn stepInner(s: *Sema, comptime tag: Teg.Tag) Error!InstructionKind.fromTag(tag)
693 const lhs_type = s.typeOfRef(lhs);693 const lhs_type = s.typeOfRef(lhs);
694694
695 if (lhs_type == .@"opaque") {695 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
696 try s.ensureAtRuntime(s.lazyNode(.lhs));717 try s.ensureAtRuntime(s.lazyNode(.lhs));
697 return s.addRef(.field_opaque, .{718 return s.addRef(.field_opaque, .{
698 .lhs = lhs,719 .lhs = lhs,
...@@ -704,25 +725,6 @@ fn stepInner(s: *Sema, comptime tag: Teg.Tag) Error!InstructionKind.fromTag(tag)...@@ -704,25 +725,6 @@ fn stepInner(s: *Sema, comptime tag: Teg.Tag) Error!InstructionKind.fromTag(tag)
704 return try s.fieldAccessStruct(lhs, lhs_type, struct_type, field.rhs);725 return try s.fieldAccessStruct(lhs, lhs_type, struct_type, field.rhs);
705 } else if (lhs_type.toTupleType(s.ip)) |tuple_type| {726 } else if (lhs_type.toTupleType(s.ip)) |tuple_type| {
706 return try s.fieldAccessTuple(lhs, lhs_type, tuple_type, field.rhs);727 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 }
726 }728 }
727729
728 return s.fail(.field_invalid_type, .{730 return s.fail(.field_invalid_type, .{
src/core/Teg.zig+1-1
...@@ -1,4 +1,4 @@...@@ -1,4 +1,4 @@
1//! TODO: rename this to something else. Teg is not a graph1//! TODO: rename this to EIR (expression intermediate representation)
22
3insts: Inst.List.Slice,3insts: Inst.List.Slice,
4string_bytes: []const u8,4string_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 {...@@ -76,7 +76,7 @@ pub fn linkJsCode(link: *Link, entry_decl: Decl.Index, writer: Writer) !void {
76 }76 }
7777
78 try writer.writeAll(std.fmt.comptimePrint(78 try writer.writeAll(std.fmt.comptimePrint(
79 "// Language Compiler v{}. Alpha quality software.\n",79 "// ix compiler v{}. alpha quality software.\n",
80 .{@import("config").version},80 .{@import("config").version},
81 ));81 ));
8282
...@@ -514,16 +514,18 @@ fn printExprLevel(...@@ -514,16 +514,18 @@ fn printExprLevel(
514514
515 try writer.writeAll(")");515 try writer.writeAll(")");
516 },516 },
517
517 .js_new => {518 .js_new => {
518 const call: Tree.Call = tree.nodeData(.js_new, node);519 const call: Tree.Call = tree.nodeData(.js_new, node);
519 const args = call.args.get(tree);520 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)
522 try writer.writeAll("(");524 try writer.writeAll("(");
523 try writer.writeAll("new ");525 try writer.writeAll("new ");
524 try link.printExpr(tree, call.base, writer);526 try link.printExpr(tree, call.base, writer);
525527
526 if (args.len > 0) {528 if (args.len > 0 or level == .member) {
527 try writer.writeAll("(");529 try writer.writeAll("(");
528530
529 for (args.slice(tree), 0..) |arg, i| {531 for (args.slice(tree), 0..) |arg, i| {
...@@ -533,14 +535,14 @@ fn printExprLevel(...@@ -533,14 +535,14 @@ fn printExprLevel(
533535
534 try writer.writeAll(")");536 try writer.writeAll(")");
535 }537 }
536 if (Level.new.needsParens(level))538 if (wrap)
537 try writer.writeAll(")");539 try writer.writeAll(")");
538 },540 },
539541
540 .field => {542 .field => {
541 const field: Tree.Field = tree.nodeData(.field, node);543 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);
544 try writer.writeAll(".");546 try writer.writeAll(".");
545 try writer.writeAll(field.field.toSlice(&link.comp.intern_pool));547 try writer.writeAll(field.field.toSlice(&link.comp.intern_pool));
546 },548 },
src/core/mod.zig+1-1
...@@ -1,4 +1,4 @@...@@ -1,4 +1,4 @@
1//! Core language compiler module.1//! Core module for the Ix compiler
22
3// sort-lines: start3// sort-lines: start
44
src/main.zig+1-1
...@@ -1,7 +1,7 @@...@@ -1,7 +1,7 @@
1//! This is the user-facing command line, combining all features into1//! This is the user-facing command line, combining all features into
2//! different subcommands, each with their own option set.2//! different subcommands, each with their own option set.
33
4const default_program_name = "wlang"; // fallback when argv[0] is missing4const default_program_name = "ix"; // fallback when argv[0] is missing
55
6const Command = struct {6const Command = struct {
7 name: [:0]const u8,7 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,134 +1,689 @@
1<!doctype html>1<!DOCTYPE html>
2<html lang="en">2<html lang="en">
3 <head>3 <head>
4 <meta charset="UTF-8" />4 <meta charset="UTF-8" />
5 <meta name="viewport" content="width=device-width, initial-scale=1.0" />5 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6 <title>Language Reference</title>6 <title>Ix Language Manual</title>
7 <style>7 <style>
8 * {8 :root {
9 margin: 0;9 /* Catppuccin Latte (light) & Mocha (dark) */
10 padding: 0;10 --mantle: light-dark(#e6e9ef, #181825);
11 box-sizing: border-box;11 --crust: light-dark(#dce0e8, #11111b);
12 }12 --base: light-dark(#eff1f5, #1e1e2e);
13 body {13 --surface-0: light-dark(#ccd0da, #313244);
14 background-color: #fefefe;14 --surface-1: light-dark(#bcc0cc, #45475a);
15 font-family:15 --surface-2: light-dark(#acb0be, #585b70);
16 system-ui,16 --overlay-0: light-dark(#9ca0b0, #6c7086);
17 -apple-system,17 --overlay-1: light-dark(#8c8fa1, #7f849c);
18 BlinkMacSystemFont,18 --overlay-2: light-dark(#7c7f93, #9399b2);
19 "Segoe UI",19 --subtext-0: light-dark(#6c6f85, #a6adc8);
20 Roboto,20 --subtext-1: light-dark(#5c5f77, #bac2de);
21 Oxygen,21 --text: light-dark(#4c4f69, #cdd6f4);
22 Ubuntu,22 --accent: light-dark(#179299, #94e2d5);
23 Cantarell,23 --link: light-dark(#179299, #94e2d5);
24 "Open Sans",24 }
25 "Helvetica Neue",25
26 sans-serif;26 * {
27 max-width: 45rem;27 margin: 0;
28 padding: 1rem;28 padding: 0;
29 margin: auto;29 box-sizing: border-box;
30 }30 }
31 h1,31
32 h2,32 body {
33 h3,33 background-color: var(--base);
34 h4,34 color: var(--text);
35 h5,35 font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
36 h6,36 Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue",
37 p,37 sans-serif;
38 ul,38 color-scheme: light dark;
39 pre,39 display: flex;
40 blockquote,40 }
41 table {41
42 margin-bottom: 1rem;42 main {
43 }43 width: 100%;
44 ul {44 max-width: 80ch;
45 padding-left: 2rem;45 padding: 1rem;
46 }46 padding-top: 2rem;
47 li {47 margin: auto;
48 margin-bottom: 0.25rem;48 line-height: 1.5;
49 }49 }
50 blockquote {50
51 background-color: #ddd;51 h1,
52 border-left: 4px solid black;52 h2,
53 padding-left: 16px;53 h3,
54 padding-top: 8px;54 h4,
55 padding-bottom: 8px;55 h5,
56 }56 h6,
57 pre > code {57 p,
58 border: none;58 ul,
59 padding: 0;59 pre,
60 background-color: transparent;60 blockquote,
61 }61 table {
62 code,62 margin-bottom: 1rem;
63 pre {63 }
64 font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,64
65 "Liberation Mono", "Courier New", monospace;65 h2,
66 font-size: 0.9rem;66 h3,
67 border: 1px solid #9ca0b0;67 h4,
68 background-color: #eff1f5;68 h5,
69 border-radius: 4px;69 h6 {
70 padding: 2px 4px;70 padding-top: 0.8rem;
71 }71 margin-top: -0.8rem;
72 pre {72 }
73 padding: 0.75rem 0.75rem;73
74 }74 ul {
75 .term {75 padding-left: 2rem;
76 border: 1px solid #11111b;76 }
77 background-color: #24273a;77
78 color: #cdd6f4;78 li {
79 }79 margin-bottom: 0.25rem;
80 table,80 }
81 th,81
82 td {82 blockquote {
83 border: 1px solid #9ca0b0;83 background-color: var(--surface-0);
84 border-collapse: collapse;84 border-left: 4px solid var(--overlay-2);
85 }85 padding-left: 16px;
86 th,86 padding-top: 8px;
87 td {87 padding-bottom: 8px;
88 padding: 0.5rem;88 }
89 }89
90 .ansi-1 {90 a {
91 font-weight: bold;91 color: inherit;
92 }92 }
93 .ansi-31 {93
94 color: #ed8796;94 table,
95 }95 th,
96 .ansi-32 {96 td {
97 color: #a6e3a1;97 border: 1px solid #9ca0b0;
98 }98 border-collapse: collapse;
99 .ansi-33 {99 }
100 color: #eed49f;100
101 }101 th,
102 .ansi-34 {102 td {
103 color: #8aadf4;103 padding: 0.5rem;
104 }104 }
105 .ansi-35 {105
106 color: #f5c2e7;106 .ansi-1 {
107 }107 font-weight: bold;
108 .ansi-36 {108 }
109 color: #94e2d5;109
110 }110 .ansi-31 {
111 .ansi-2 {111 color: light-dark(#d20f39, #f38ba8);
112 color: #8087a2;112 }
113 }113
114 .kw {114 .ansi-32 {
115 color: #8839ef;115 color: light-dark(#40a02b, #a6e3a1);
116 font-weight: bold;116 }
117 }117
118 .tok-string {118 .ansi-33 {
119 color: #40a02b;119 color: light-dark(#de8d1c, #f9e2af);
120 }120 }
121 .tok-builtin_id {121
122 color: #d20f39;122 .ansi-34 {
123 font-weight: bold;123 color: light-dark(#1e66f5, #89b4fa);
124 }124 }
125 .tok-number_int,125
126 .tok-number_float {126 .ansi-35 {
127 color: #fe640b;127 color: light-dark(#ea76cb, #f5c2e7);
128 }128 }
129 </style>129
130 </head>130 .ansi-36 {
131 <body>131 color: light-dark(#179299, #94e2d5);
132 <main>{contents}</main>132 }
133 </body>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>
134</html>689</html>