| author | |
| committer | |
| log | 5f480e3bc33c572ac26aefc905d9426cad259954 |
| tree | d2a2feee03a169ca6c485879f9974e90a511a0b7 |
| parent | 5556f630ad43a64e22a760df66534eb135dd9671 |
| signature | Signed by PGP key B5690EEEBB952194 |
18 files changed, 2034 insertions(+), 962 deletions(-)
CHANGELOG.md+1| ... | @@ -1,5 +1,6 @@ | ... | @@ -1,5 +1,6 @@ |
| 1 | ## 0.1.7 | 1 | ## 0.1.7 |
| 2 | 2 | ||
| 3 | - chore: rename the language to "ix" | ||
| 3 | - feat: allow any expression as a function body | 4 | - feat: allow any expression as a function body |
| 4 | - fix: coerce `break` operands and implicit breaks to result type | 5 | - fix: coerce `break` operands and implicit breaks to result type |
| 5 | - fix: coerce results when `if` expression branches merge | 6 | - fix: coerce results when `if` expression branches merge |
README.md+21-8| ... | @@ -1,21 +1,34 @@ | ... | @@ -1,21 +1,34 @@ |
| 1 | # New Language Project | 1 | # Ix |
| 2 | 2 | ||
| 3 | This is a high-level, general purpose programming language. It is a mix of Rust, | 3 | Ix (**I**nferable E**x**pression) is a high-level, general purpose programming |
| 4 | Zig, and JavaScript's great points. | 4 | language. It features a mix of Rust, Zig, and JavaScript's great points. |
| 5 | 5 | ||
| 6 | For a guide on how to use the language, please see the Language Manual included | 6 | For a guide on how to use Ix, see the manual included in the binary |
| 7 | in the binary distribution. (TODO: host a rendered version) | 7 | distribution. Additionally, the latest commit of the manual is hosted online at |
| 8 | [language-manual.pages.dev](https://language-manual.pages.dev). | ||
| 8 | 9 | ||
| 9 | ## Building | 10 | ## Building |
| 10 | 11 | ||
| 11 | The primary dependency of the language SDK is a Zig compiler. If one is not | 12 | The primary dependency of the Ix compiler is a Zig compiler. If one is not |
| 12 | available, the `./build` and `.\build.cmd` scripts can substitute for running | 13 | available, the `./build` and `.\build.cmd` scripts can substitute for running |
| 13 | `zig build` on POSIX and Windows systems, respectively. With a Zig compiler, a | 14 | `zig build` on POSIX and Windows systems, respectively. With a Zig compiler, a |
| 14 | release build can be produced in a single command: | 15 | release build can be produced in a single command: |
| 15 | 16 | ||
| 16 | zig build --release | 17 | zig build --release |
| 17 | 18 | ||
| 18 | To run the standard packaging procedure, which builds the SDK for all officially | 19 | To run the standard packaging procedure, which builds the compiler for all |
| 19 | supported targets: | 20 | officially supported targets: |
| 20 | 21 | ||
| 21 | zig build package | 22 | zig build package |
| 23 | |||
| 24 | ## Development | ||
| 25 | |||
| 26 | Continuous integration is provided via a `./ci` (script for POSIX), which is run | ||
| 27 | by [self hosted Buildkite runners][ci]. This script uses the [`typos`][typos] | ||
| 28 | CLI to check spelling mistakes and [`dprint`][dprint] for code formatting. It is | ||
| 29 | recommended to setup editor integration with these two tools to prevent pushing | ||
| 30 | broken 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 | }; |
| 20 | 20 | ||
| 21 | /// `createArtifactGroup` multiple times in order to have different | 21 | /// `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. |
| 23 | const ArtifactGroup = struct { | 23 | const 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 | } |
| 448 | 448 | ||
| 449 | // Language Reference Generator | 449 | // 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 | }); |
| 540 | 540 | ||
| 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 | } |
| 561 | 552 | ||
| 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 --version | 11 | ./build --version |
| 12 | typos | 12 | typos |
| 13 | dprint check | 13 | dprint check |
| 14 | ./build all --summary new | ||
| 15 | 14 | ||
| 16 | if [ -n "$BUILDKITE" ]; then | 15 | if [ -n "$BUILDKITE" ]; then |
| 16 | # perform the entire build | ||
| 17 | ./build all --summary all | ||
| 18 | |||
| 17 | pushd zig-out/package | 19 | pushd zig-out/package |
| 18 | zip -r compiler-$commit.zip * | 20 | zip -r compiler-$commit.zip * |
| 19 | buildkite-agent artifact upload compiler-$commit.zip | 21 | buildkite-agent artifact upload compiler-$commit.zip |
| ... | @@ -21,12 +23,15 @@ if [ -n "$BUILDKITE" ]; then | ... | @@ -21,12 +23,15 @@ if [ -n "$BUILDKITE" ]; then |
| 21 | 23 | ||
| 22 | rm -fr zig-out/site | 24 | rm -fr zig-out/site |
| 23 | mkdir zig-out/site | 25 | mkdir zig-out/site |
| 24 | cp zig-out/package/README.html zig-out/site/index.html | 26 | cp zig-out/package/ix.html zig-out/site/index.html |
| 25 | branch=$BUILDKITE_BRANCH | 27 | branch="$BUILDKITE_BRANCH" |
| 26 | if [ "$branch" = "dev" ]; then | 28 | if [ "$branch" = "dev" ]; then |
| 27 | branch=main | 29 | branch=main |
| 28 | fi | 30 | 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-manual | 33 | wrangler pages deploy zig-out/site --branch "$branch" --commit-hash "$commit" --no-bundle --project-name language-manual |
| 34 | else | ||
| 35 | # perform only the tests, with a shorter summary | ||
| 36 | ./build test --summary new | ||
| 32 | fi | 37 | fi |
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 | |||
| 7 | typos | ||
| 8 | if %errorlevel% neq 0 exit /b %errorlevel% | ||
| 9 | |||
| 10 | dprint check | ||
| 11 | if %errorlevel% neq 0 exit /b %errorlevel% | ||
| 12 | |||
| 13 | build.cmd test | ||
| 14 | if %errorlevel% neq 0 exit /b %errorlevel% | ||
doc/language-manual.md+196-59| ... | @@ -1,54 +1,65 @@ | ... | @@ -1,54 +1,65 @@ |
| 1 | # New Language Project | 1 | # Ix Programming Language |
| 2 | 2 | ||
| 3 | This is a high-level, general purpose programming language. | 3 | Ix (**I**nferable E**x**pression) is a high-level programming language for |
| 4 | developing web applications and games. | ||
| 4 | 5 | ||
| 5 | Key goals of this project are: | 6 | Key goals of this project are: |
| 6 | 7 | ||
| 7 | - **Ease of Use**: Use of type inference keeps code concise and easy to read and | 8 | - **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 compilation | 10 | - **Fast Iteration**: Incremental compilation keeps development productivity |
| 10 | and hot module reloading. (note: incremental is not yet available) | 11 | high. |
| 11 | - TODO: some more | 12 | - **Safety**: Compiler errors catch most mistakes at compile-time, and |
| 13 | additional tooling to catch mistakes at runtime. | ||
| 14 | - **Universal**: One codebase can seamlessly target different web browsers and | ||
| 15 | native targets. | ||
| 12 | 16 | ||
| 13 | It primarily focuses on merging some great points of Rust, Zig, and JavaScript's | 17 | It primarily focuses on merging some great points of Rust, Zig, and JavaScript's |
| 14 | language designs: | 18 | language designs: |
| 15 | 19 | ||
| 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. | ||
| 19 | 26 | ||
| 20 | ## Standard Library | 27 | This manual is a standalone HTML document. Code samples are compiled and tested |
| 21 | 28 | as a part of the release cycle. | |
| 22 | Currently, there is no standard library. One will be introduced later. | ||
| 23 | 29 | ||
| 24 | ## Hello World | 30 | ## Hello World |
| 25 | 31 | ||
| 26 | ```lang:run | 32 | ```lang:run |
| 27 | let print = @extern("console").log; | 33 | let print = @extern("console").log; |
| 28 | |||
| 29 | fn main { | 34 | fn main { |
| 30 | print("Hello, world!"); | 35 | print("Hello, world!"); |
| 31 | } | 36 | } |
| 32 | ``` | 37 | ``` |
| 33 | 38 | ||
| 34 | The `print` function is implemented by getting an external reference to the | 39 | The `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 |
| 41 | calling 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. | ||
| 36 | 45 | ||
| 37 | ## Primitive Data Types | 46 | ## Primitive Data Types |
| 38 | 47 | ||
| 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 | | ||
| 48 | 58 | ||
| 49 | > **note**: `bigint` is planned but not added | 59 | > **Note**: Unlike JavaScript, the values `undefined` and `null` are merged into |
| 60 | > one concept: [void](#void) | ||
| 50 | 61 | ||
| 51 | ## Built-in Constants | 62 | ### Built-in Constants |
| 52 | 63 | ||
| 53 | In addition to the above types being built-in constants, the following are | 64 | In addition to the above types being built-in constants, the following are |
| 54 | available. | 65 | available. |
| ... | @@ -62,9 +73,48 @@ available. | ... | @@ -62,9 +73,48 @@ available. |
| 62 | These constants are reserved names, and it is an error to re-declare them. | 73 | These constants are reserved names, and it is an error to re-declare them. |
| 63 | 74 | ||
| 64 | ```lang:build | 75 | ```lang:build |
| 65 | let true = 1 == 0; | 76 | fn main { |
| 77 | let true = 1 == 0; | ||
| 78 | } | ||
| 66 | ``` | 79 | ``` |
| 67 | 80 | ||
| 81 | ### Numbers | ||
| 82 | |||
| 83 | Numbers 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 | |||
| 93 | Numbers | ||
| 94 | |||
| 95 | ### Multi-line string literals | ||
| 96 | |||
| 97 | ```lang | ||
| 98 | let bwaa = | ||
| 99 | \\hello | ||
| 100 | \\ world | ||
| 101 | \\end | ||
| 102 | ; | ||
| 103 | ``` | ||
| 104 | |||
| 105 | ### Void | ||
| 106 | |||
| 107 | Void is the lack a value. For example, a function that does nothing returns | ||
| 108 | `void`. | ||
| 109 | |||
| 110 | ```lang | ||
| 111 | pub fn hello() { | ||
| 112 | // do nothing | ||
| 113 | } | ||
| 114 | ``` | ||
| 115 | |||
| 116 | <!-- TODO: change this section to "Optionals" --> | ||
| 117 | |||
| 68 | ## Variables | 118 | ## Variables |
| 69 | 119 | ||
| 70 | There are two types of variables, `let` and `var`. | 120 | There 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 |
| 74 | where it was used, aside from side effect duplication. | 124 | where it was used, aside from side effect duplication. |
| 75 | 125 | ||
| 76 | ```lang:run | 126 | ```lang:run |
| 77 | let hello: int = 1; | 127 | let print = @extern("console").log; |
| 78 | let world = hello + 2; | ||
| 79 | let hello = hello * world; | ||
| 80 | 128 | ||
| 81 | @extern("console").log(hello); | 129 | fn main { |
| 130 | let hello: int = 1; | ||
| 131 | let world = hello + 2; | ||
| 132 | let hello = hello * world; | ||
| 133 | |||
| 134 | print(hello); | ||
| 135 | } | ||
| 82 | ``` | 136 | ``` |
| 83 | 137 | ||
| 84 | `var` bindings create mutable memory locations at runtime. This means they can | 138 | `var` bindings create mutable memory locations at runtime. This means they can |
| 85 | be re-assigned to: | 139 | be re-assigned to: |
| 86 | 140 | ||
| 87 | ```lang:run | 141 | ```lang:run |
| 88 | var message = "first message"; | 142 | fn main { |
| 143 | var message = "first message"; | ||
| 89 | 144 | ||
| 90 | if @extern("Math").random() > 0.5 { | 145 | if @extern("Math").random() > 0.5 { |
| 91 | message = "second message"; | 146 | message = "second message"; |
| 92 | } | 147 | } |
| 93 | 148 | ||
| 94 | @extern("console").log(message); | 149 | @extern("console").log(message); |
| 150 | } | ||
| 95 | ``` | 151 | ``` |
| 96 | 152 | ||
| 97 | Using `let` is preferred whenever possible, as it will lead to more readable | 153 | Using `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 |
| 99 | as: | 155 | as: |
| 100 | 156 | ||
| 101 | ```lang:run | 157 | ```lang:run |
| 102 | let message = if @extern("Math").random() > 0.5 { | 158 | fn 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 | }; | ||
| 107 | 164 | ||
| 108 | @extern("console").log(message); | 165 | @extern("console").log(message); |
| 166 | } | ||
| 109 | ``` | 167 | ``` |
| 110 | 168 | ||
| 111 | It should be noted that numeric literals are a comptime-only data type, meaning | 169 | It 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 |
| 114 | float, but a compiler warning will be emitted. | 172 | float, but a compiler warning will be emitted. |
| 115 | 173 | ||
| 116 | ```lang | 174 | ```lang |
| 117 | var number = 0; // `number` is a `float`, but not explicit | 175 | var number = 0; // `number` is a `float`, but not explicit |
| 118 | var number: int = 0; // `number` is `int` | 176 | var number: int = 0; // `number` is `int` |
| 119 | var number: float = 0; // `number` is `float` | 177 | var 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. |
| 125 | The function body is a block, which may use implicit return to return. | 183 | The function body is a block, which may use implicit return to return. |
| 126 | 184 | ||
| 127 | ```lang:run | 185 | ```lang:run |
| 186 | let print = @extern("console").log; | ||
| 187 | |||
| 128 | fn add(a: int, b: int) -> int { | 188 | fn add(a: int, b: int) -> int { |
| 129 | a + b | 189 | a + b |
| 130 | } | 190 | } |
| 131 | 191 | ||
| 132 | let print = @extern("console").log; | 192 | fn main { |
| 133 | print("Sum of 2 and 7:", add(2, 7)); | 193 | print("Sum of 2 and 7:", add(2, 7)); |
| 194 | } | ||
| 134 | ``` | 195 | ``` |
| 135 | 196 | ||
| 136 | > **Bug**: Return types are currently required. Omitting the `-> type` syntax | 197 | > **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)); |
| 140 | The `return` expression can be used within functions to return early. | 201 | The `return` expression can be used within functions to return early. |
| 141 | 202 | ||
| 142 | ```lang:run | 203 | ```lang:run |
| 204 | let print = @extern("console").log; | ||
| 205 | |||
| 143 | fn something(a: int) -> bool { | 206 | fn 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 > 100 | 211 | a > 100 |
| 149 | } | 212 | } |
| 150 | 213 | ||
| 151 | let print = @extern("console").log; | 214 | fn main { |
| 152 | print(something(0xFF), something(0)); | 215 | print(something(0xFF), something(0)); |
| 216 | } | ||
| 153 | ``` | 217 | ``` |
| 154 | 218 | ||
| 155 | ## Number Literals | 219 | ## 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 | } |
| 180 | 244 | ||
| 181 | print(currentYear()); | 245 | fn main { |
| 246 | print(currentYear()); | ||
| 247 | } | ||
| 182 | ``` | 248 | ``` |
| 183 | 249 | ||
| 184 | A field access on an external handle is allowed to be performed at compile time, | 250 | A 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 | } |
| 199 | 265 | ||
| 200 | let center = Position{ x: 0, y: -2 }; | 266 | fn main { |
| 201 | print(center.x, center.y); | 267 | let center = Position{ x: 0, y: -2 }; |
| 268 | print(center.x, center.y); | ||
| 202 | 269 | ||
| 203 | fn 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 | } |
| 207 | 277 | ||
| 208 | let added = addPositions( | 278 | fn 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 | ); | ||
| 212 | print("Add result:", added.x, added.y); | ||
| 213 | ``` | 281 | ``` |
| 214 | 282 | ||
| 215 | To allow optimizations, `struct`s by default do not have a defined | 283 | To 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 |
| 217 | function is not allowed. | 285 | function is not allowed. |
| 218 | 286 | ||
| 219 | ```lang:build | 287 | ```lang:build |
| 288 | let print = @extern("console").log; | ||
| 289 | |||
| 220 | struct Person { | 290 | struct Person { |
| 221 | name: string, | 291 | name: string, |
| 222 | age: int, | 292 | age: int, |
| 223 | } | 293 | } |
| 224 | 294 | ||
| 225 | let print = @extern("console").log; | 295 | fn main { |
| 226 | print(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 | |||
| 312 | TODO | ||
| 313 | |||
| 314 | #### String Literals 2 | ||
| 315 | |||
| 316 | TODO | ||
| 317 | |||
| 318 | #### String WAdsafdsa | ||
| 319 | |||
| 320 | TODO | ||
| 321 | |||
| 322 | ### hewwo | ||
| 323 | |||
| 324 | cat | ||
| 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: start | 3 | // 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: end | 8 | // 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 |
| 666 | 666 | ||
| 667 | const Options = struct { | 667 | const Options = struct { |
| 668 | dump_ir: bool = false, | 668 | dump_ir: bool = false, |
| 669 | verbose_sema: bool = true, | 669 | verbose_sema: bool = false, |
| 670 | }; | 670 | }; |
| 671 | 671 | ||
| 672 | /// The compiler backend is stored separately from the compilation, and | 672 | /// 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.zig | 2 | //! 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 of | 4 | //! 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 incremental | 6 | //! 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); |
| 694 | 694 | ||
| 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 | } |
| 727 | 729 | ||
| 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 graph | 1 | //! TODO: rename this to EIR (expression intermediate representation) |
| 2 | 2 | ||
| 3 | insts: Inst.List.Slice, | 3 | insts: Inst.List.Slice, |
| 4 | string_bytes: []const u8, | 4 | string_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 | } |
| 77 | 77 | ||
| 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 | )); |
| 82 | 82 | ||
| ... | @@ -514,16 +514,18 @@ fn printExprLevel( | ... | @@ -514,16 +514,18 @@ fn printExprLevel( |
| 514 | 514 | ||
| 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); |
| 520 | 521 | ||
| 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); |
| 525 | 527 | ||
| 526 | if (args.len > 0) { | 528 | if (args.len > 0 or level == .member) { |
| 527 | try writer.writeAll("("); | 529 | try writer.writeAll("("); |
| 528 | 530 | ||
| 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( |
| 533 | 535 | ||
| 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 | }, |
| 539 | 541 | ||
| 540 | .field => { | 542 | .field => { |
| 541 | const field: Tree.Field = tree.nodeData(.field, node); | 543 | const field: Tree.Field = tree.nodeData(.field, node); |
| 542 | 544 | ||
| 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 |
| 2 | 2 | ||
| 3 | // sort-lines: start | 3 | // sort-lines: start |
| 4 | 4 |
src/main.zig+1-1| ... | @@ -1,7 +1,7 @@ | ... | @@ -1,7 +1,7 @@ |
| 1 | //! This is the user-facing command line, combining all features into | 1 | //! 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. |
| 3 | 3 | ||
| 4 | const default_program_name = "wlang"; // fallback when argv[0] is missing | 4 | const default_program_name = "ix"; // fallback when argv[0] is missing |
| 5 | 5 | ||
| 6 | const Command = struct { | 6 | const 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. | ||
| 4 | input: []const u8, | ||
| 5 | output: std.ArrayList(u8), | ||
| 6 | |||
| 7 | const ws = " \t\r\n"; | ||
| 8 | |||
| 9 | pub 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 | |||
| 24 | pub 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 | |||
| 35 | fn 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 | |||
| 44 | fn 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 | |||
| 126 | fn 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 | |||
| 143 | fn 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 | |||
| 197 | fn 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 | |||
| 201 | fn 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 | |||
| 208 | fn 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 | |||
| 215 | fn 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 | |||
| 228 | const NaiveHtmlMinifier = @This(); | ||
| 229 | const std = @import("std"); | ||
| 230 | const mem = std.mem; | ||
| 231 | const 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 | ||
| 5 | pub const std = @import("std"); | ||
| 6 | pub const core = @import("core"); | ||
| 7 | |||
| 8 | const mem = std.mem; | ||
| 9 | const Allocator = mem.Allocator; | ||
| 10 | const ArrayList = std.ArrayList; | ||
| 11 | const ArrayListUnmanaged = std.ArrayListUnmanaged; | ||
| 12 | const assert = std.debug.assert; | ||
| 13 | |||
| 14 | comptime { | ||
| 15 | assert(core.runtime_safety); | ||
| 16 | } | ||
| 17 | |||
| 18 | pub 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 | |||
| 73 | const 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 | |||
| 90 | const InlineType = enum { | ||
| 91 | bold, | ||
| 92 | code, | ||
| 93 | link, | ||
| 94 | }; | ||
| 95 | |||
| 96 | const 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 | |||
| 167 | const 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 | |||
| 205 | fn 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 | |||
| 497 | pub 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 | |||
| 514 | fn 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. | ||
| 568 | fn 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 | |||
| 590 | fn 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 | |||
| 622 | fn 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 | |||
| 644 | fn 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 | |||
| 682 | fn 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 | ||
| 5 | pub const std = @import("std"); | ||
| 6 | pub const core = @import("core"); | ||
| 7 | const NaiveHtmlMinifier = @import("NaiveHtmlMinifier.zig"); | ||
| 8 | |||
| 9 | const mem = std.mem; | ||
| 10 | const Allocator = mem.Allocator; | ||
| 11 | const ArrayList = std.ArrayList; | ||
| 12 | const ArrayListUnmanaged = std.ArrayListUnmanaged; | ||
| 13 | const assert = std.debug.assert; | ||
| 14 | |||
| 15 | comptime { | ||
| 16 | assert(@import("builtin").mode == .Debug); | ||
| 17 | } | ||
| 18 | |||
| 19 | const cli_name = "wlang"; | ||
| 20 | const build_cli = cli_name ++ " build"; | ||
| 21 | const test_cli = cli_name ++ " test"; | ||
| 22 | const run_cli = cli_name ++ " run"; | ||
| 23 | |||
| 24 | pub 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 | |||
| 85 | const 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 | |||
| 102 | const InlineType = enum { | ||
| 103 | bold, | ||
| 104 | code, | ||
| 105 | link, | ||
| 106 | }; | ||
| 107 | |||
| 108 | const 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 | |||
| 179 | const 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 | |||
| 235 | const 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 | |||
| 312 | fn 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 | |||
| 609 | pub 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 | |||
| 626 | fn 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. | ||
| 680 | fn 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 | |||
| 702 | fn 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 | |||
| 734 | fn 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 | |||
| 756 | fn 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 | |||
| 794 | fn 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 | |||
| 819 | fn fmtHtmlEscape(text: []const u8, escape_quotes: bool) std.fmt.Formatter(fmtHtmlEscapeImpl) { | ||
| 820 | return .{ .data = .{ .text = text, .escape_quotes = escape_quotes } }; | ||
| 821 | } | ||
| 822 | |||
| 823 | fn 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> |