| 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 | 1 | ## 0.1.7 |
| 2 | 2 | |
| 3 | - chore: rename the language to "ix" | |
| 3 | 4 | - feat: allow any expression as a function body |
| 4 | 5 | - fix: coerce `break` operands and implicit breaks to result type |
| 5 | 6 | - fix: coerce results when `if` expression branches merge |
README.md+21-8| ... | ... | @@ -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, | |
| 4 | Zig, and JavaScript's great points. | |
| 3 | Ix (**I**nferable E**x**pression) is a high-level, general purpose programming | |
| 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 | |
| 7 | in the binary distribution. (TODO: host a rendered version) | |
| 6 | For a guide on how to use Ix, see the manual included in the binary | |
| 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 | 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 | 13 | available, the `./build` and `.\build.cmd` scripts can substitute for running |
| 13 | 14 | `zig build` on POSIX and Windows systems, respectively. With a Zig compiler, a |
| 14 | 15 | release build can be produced in a single command: |
| 15 | 16 | |
| 16 | 17 | zig build --release |
| 17 | 18 | |
| 18 | To run the standard packaging procedure, which builds the SDK for all officially | |
| 19 | supported targets: | |
| 19 | To run the standard packaging procedure, which builds the compiler for all | |
| 20 | officially supported targets: | |
| 20 | 21 | |
| 21 | 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 | 18 | config: BuildConfig, |
| 19 | 19 | }; |
| 20 | 20 | |
| 21 | /// `createArtifactGroup` multiple times in order to have different | |
| 22 | /// targets and configurations of the SDK at the same time. | |
| 21 | /// `createArtifactGroup` is called multiple times in order to have different | |
| 22 | /// targets and configurations of Ix at the same time. | |
| 23 | 23 | const ArtifactGroup = struct { |
| 24 | 24 | build: *Build, |
| 25 | 25 | opts: Options, |
| ... | ... | @@ -446,10 +446,10 @@ pub fn build(b: *Build) !void { |
| 446 | 446 | test_all.dependOn(run_step); |
| 447 | 447 | } |
| 448 | 448 | |
| 449 | // Language Reference Generator | |
| 449 | // Language Manual Generator | |
| 450 | 450 | const manual_generator = b.addExecutable(.{ |
| 451 | .name = "generate-man", | |
| 452 | .root_source_file = b.path("src/manual/generate-man.zig"), | |
| 451 | .name = "generate_manual", | |
| 452 | .root_source_file = b.path("src/manual/generate_manual.zig"), | |
| 453 | 453 | .target = b.graph.host, |
| 454 | 454 | .optimize = .Debug, |
| 455 | 455 | .link_libc = true, |
| ... | ... | @@ -533,30 +533,21 @@ pub fn build(b: *Build) !void { |
| 533 | 533 | .version = version, |
| 534 | 534 | .have_embedded_quickjs = have_embedded_quickjs, |
| 535 | 535 | }, |
| 536 | .optimize = optimize, | |
| 536 | .optimize = .ReleaseSafe, | |
| 537 | 537 | .target = cross_target, |
| 538 | 538 | .no_llvm = no_llvm, |
| 539 | 539 | }); |
| 540 | 540 | |
| 541 | inline for (.{ | |
| 542 | .{ artifacts.cli, "compiler" }, | |
| 543 | }) |entry| { | |
| 544 | const artifact, const bin_name = entry; | |
| 545 | package.dependOn(&b.addInstallFile( | |
| 546 | artifact.getEmittedBin(), | |
| 547 | b.pathJoin(&.{ | |
| 548 | "package", | |
| 549 | name, | |
| 550 | b.fmt("{s}{s}", .{ | |
| 551 | bin_name, | |
| 552 | cross_target.result.exeFileExt(), | |
| 553 | }), | |
| 554 | }), | |
| 555 | ).step); | |
| 556 | } | |
| 557 | ||
| 541 | package.dependOn(&b.addInstallFile( | |
| 542 | artifacts.cli.getEmittedBin(), | |
| 543 | b.pathJoin(&.{ | |
| 544 | "package", | |
| 545 | name, | |
| 546 | b.fmt("ix{s}", .{cross_target.result.exeFileExt()}), | |
| 547 | }), | |
| 548 | ).step); | |
| 558 | 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 | 11 | ./build --version |
| 12 | 12 | typos |
| 13 | 13 | dprint check |
| 14 | ./build all --summary new | |
| 15 | 14 | |
| 16 | 15 | if [ -n "$BUILDKITE" ]; then |
| 16 | # perform the entire build | |
| 17 | ./build all --summary all | |
| 18 | ||
| 17 | 19 | pushd zig-out/package |
| 18 | 20 | zip -r compiler-$commit.zip * |
| 19 | 21 | buildkite-agent artifact upload compiler-$commit.zip |
| ... | ... | @@ -21,12 +23,15 @@ if [ -n "$BUILDKITE" ]; then |
| 21 | 23 | |
| 22 | 24 | rm -fr zig-out/site |
| 23 | 25 | mkdir zig-out/site |
| 24 | cp zig-out/package/README.html zig-out/site/index.html | |
| 25 | branch=$BUILDKITE_BRANCH | |
| 26 | cp zig-out/package/ix.html zig-out/site/index.html | |
| 27 | branch="$BUILDKITE_BRANCH" | |
| 26 | 28 | if [ "$branch" = "dev" ]; then |
| 27 | 29 | branch=main |
| 28 | 30 | fi |
| 29 | CLOUDFLARE_API_TOKEN=$(buildkite-agent secret get CLOUDFLARE_API_TOKEN) \ | |
| 30 | CLOUDFLARE_ACCOUNT_ID=$(buildkite-agent secret get CLOUDFLARE_ACCOUNT_ID) \ | |
| 31 | CLOUDFLARE_API_TOKEN="$(buildkite-agent secret get CLOUDFLARE_API_TOKEN)" \ | |
| 32 | CLOUDFLARE_ACCOUNT_ID="$(buildkite-agent secret get CLOUDFLARE_ACCOUNT_ID)" \ | |
| 31 | 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 | 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 | # 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 | 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 | write. | |
| 9 | - **Fast Iteration**: The compiler is designed for quick incremental compilation | |
| 10 | and hot module reloading. (note: incremental is not yet available) | |
| 11 | - TODO: some more | |
| 8 | - **Ease of Use**: Use of type inference and simple syntax makes code easy to | |
| 9 | read and write. | |
| 10 | - **Fast Iteration**: Incremental compilation keeps development productivity | |
| 11 | high. | |
| 12 | - **Safety**: Compiler errors catch most mistakes at compile-time, and | |
| 13 | additional tooling to catch mistakes at runtime. | |
| 14 | - **Universal**: One codebase can seamlessly target different web browsers and | |
| 15 | native targets. | |
| 12 | 16 | |
| 13 | 17 | It primarily focuses on merging some great points of Rust, Zig, and JavaScript's |
| 14 | 18 | language designs: |
| 15 | 19 | |
| 16 | - **Rust** - Expressive control flow that is easy to write. | |
| 17 | - **Zig** - Execute any code at compile time; everything is an expression. | |
| 18 | - **JavaScript** - Garbage collected values with a versatile type system. | |
| 20 | - [**Rust**](https://www.rust-lang.org) - Expressive and concise control flow | |
| 21 | that is easy to write. | |
| 22 | - [**Zig**](https://ziglang.org) - Execute any code at compile time; everything | |
| 23 | is an expression. | |
| 24 | - [**JavaScript**](https://developer.mozilla.org/en-US/docs/Web/JavaScript) - | |
| 25 | Garbage collected values with a versatile type system. | |
| 19 | 26 | |
| 20 | ## Standard Library | |
| 21 | ||
| 22 | Currently, there is no standard library. One will be introduced later. | |
| 27 | This manual is a standalone HTML document. Code samples are compiled and tested | |
| 28 | as a part of the release cycle. | |
| 23 | 29 | |
| 24 | 30 | ## Hello World |
| 25 | 31 | |
| 26 | 32 | ```lang:run |
| 27 | 33 | let print = @extern("console").log; |
| 28 | ||
| 29 | 34 | fn main { |
| 30 | 35 | print("Hello, world!"); |
| 31 | 36 | } |
| 32 | 37 | ``` |
| 33 | 38 | |
| 34 | 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 | 46 | ## Primitive Data Types |
| 38 | 47 | |
| 39 | | name | ts equivalent | description | | |
| 40 | | -------- | --------------------- | ---------------------------------------------- | | |
| 41 | | `int` | `number` | a number in signed 32-bit range | | |
| 42 | | `float` | `number` | a 64-bit floating point number | | |
| 43 | | `bool` | `boolean` | either `true` or `false` | | |
| 44 | | `string` | `string` | a sequence of text characters | | |
| 45 | | `void` | `undefined` or `null` | detectable lack of a value | | |
| 46 | | `opaque` | `unknown` or `any` | could be any value | | |
| 47 | | `never` | `never` | control flow prevents this value from existing | | |
| 48 | | name | ts equivalent | description | | |
| 49 | | ---------- | ----------------------------- | ---------------------------------------------- | | |
| 50 | | `bool` | `boolean` | either `true` or `false` | | |
| 51 | | `float` | `number` | a 64-bit floating point number | | |
| 52 | | `int` | `number` | an integer number in signed 32-bit range | | |
| 53 | | `bigint` | `bigint` | an integer number with no range limit | | |
| 54 | | `string` | `string` | a sequence of text characters | | |
| 55 | | `void` | `undefined` / `null` / `void` | detectable lack of a value | | |
| 56 | | `opaque` | `unknown` / `any` | could be any value | | |
| 57 | | `noreturn` | `never` | control flow prevents this value from existing | | |
| 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 | 64 | In addition to the above types being built-in constants, the following are |
| 54 | 65 | available. |
| ... | ... | @@ -62,9 +73,48 @@ available. |
| 62 | 73 | These constants are reserved names, and it is an error to re-declare them. |
| 63 | 74 | |
| 64 | 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 | 118 | ## Variables |
| 69 | 119 | |
| 70 | 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 | 124 | where it was used, aside from side effect duplication. |
| 75 | 125 | |
| 76 | 126 | ```lang:run |
| 77 | let hello: int = 1; | |
| 78 | let world = hello + 2; | |
| 79 | let hello = hello * world; | |
| 127 | let print = @extern("console").log; | |
| 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 | 138 | `var` bindings create mutable memory locations at runtime. This means they can |
| 85 | 139 | be re-assigned to: |
| 86 | 140 | |
| 87 | 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 { | |
| 91 | message = "second message"; | |
| 92 | } | |
| 145 | if @extern("Math").random() > 0.5 { | |
| 146 | message = "second message"; | |
| 147 | } | |
| 93 | 148 | |
| 94 | @extern("console").log(message); | |
| 149 | @extern("console").log(message); | |
| 150 | } | |
| 95 | 151 | ``` |
| 96 | 152 | |
| 97 | 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 | 155 | as: |
| 100 | 156 | |
| 101 | 157 | ```lang:run |
| 102 | let message = if @extern("Math").random() > 0.5 { | |
| 103 | "second message" | |
| 104 | } else { | |
| 105 | "first message" | |
| 106 | }; | |
| 158 | fn main { | |
| 159 | let message = if @extern("Math").random() > 0.5 { | |
| 160 | "second message" | |
| 161 | } else { | |
| 162 | "first message" | |
| 163 | }; | |
| 107 | 164 | |
| 108 | @extern("console").log(message); | |
| 165 | @extern("console").log(message); | |
| 166 | } | |
| 109 | 167 | ``` |
| 110 | 168 | |
| 111 | 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 | 172 | float, but a compiler warning will be emitted. |
| 115 | 173 | |
| 116 | 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 | 176 | var number: int = 0; // `number` is `int` |
| 119 | 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 | 183 | The function body is a block, which may use implicit return to return. |
| 126 | 184 | |
| 127 | 185 | ```lang:run |
| 186 | let print = @extern("console").log; | |
| 187 | ||
| 128 | 188 | fn add(a: int, b: int) -> int { |
| 129 | 189 | a + b |
| 130 | 190 | } |
| 131 | 191 | |
| 132 | let print = @extern("console").log; | |
| 133 | print("Sum of 2 and 7:", add(2, 7)); | |
| 192 | fn main { | |
| 193 | print("Sum of 2 and 7:", add(2, 7)); | |
| 194 | } | |
| 134 | 195 | ``` |
| 135 | 196 | |
| 136 | 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 | 201 | The `return` expression can be used within functions to return early. |
| 141 | 202 | |
| 142 | 203 | ```lang:run |
| 204 | let print = @extern("console").log; | |
| 205 | ||
| 143 | 206 | fn something(a: int) -> bool { |
| 144 | 207 | if a == 0 { |
| 145 | 208 | return false; |
| ... | ... | @@ -148,8 +211,9 @@ fn something(a: int) -> bool { |
| 148 | 211 | a > 100 |
| 149 | 212 | } |
| 150 | 213 | |
| 151 | let print = @extern("console").log; | |
| 152 | print(something(0xFF), something(0)); | |
| 214 | fn main { | |
| 215 | print(something(0xFF), something(0)); | |
| 216 | } | |
| 153 | 217 | ``` |
| 154 | 218 | |
| 155 | 219 | ## Number Literals |
| ... | ... | @@ -178,7 +242,9 @@ fn currentYear() -> int { |
| 178 | 242 | @jsNew(@extern("Date")).getFullYear() |
| 179 | 243 | } |
| 180 | 244 | |
| 181 | print(currentYear()); | |
| 245 | fn main { | |
| 246 | print(currentYear()); | |
| 247 | } | |
| 182 | 248 | ``` |
| 183 | 249 | |
| 184 | 250 | A field access on an external handle is allowed to be performed at compile time, |
| ... | ... | @@ -197,19 +263,21 @@ struct Position { |
| 197 | 263 | y: int, |
| 198 | 264 | } |
| 199 | 265 | |
| 200 | let center = Position{ x: 0, y: -2 }; | |
| 201 | print(center.x, center.y); | |
| 266 | fn main { | |
| 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 | 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( | |
| 209 | .{ x: 1, y: -4 }, | |
| 210 | .{ x: 5, y: 8 }, | |
| 211 | ); | |
| 212 | print("Add result:", added.x, added.y); | |
| 278 | fn addPositions(a: Position, b: Position) -> Position { | |
| 279 | .{ x: a.x + b.x, y: a.y + b.y } | |
| 280 | } | |
| 213 | 281 | ``` |
| 214 | 282 | |
| 215 | 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 | 285 | function is not allowed. |
| 218 | 286 | |
| 219 | 287 | ```lang:build |
| 288 | let print = @extern("console").log; | |
| 289 | ||
| 220 | 290 | struct Person { |
| 221 | 291 | name: string, |
| 222 | 292 | age: int, |
| 223 | 293 | } |
| 224 | 294 | |
| 225 | let print = @extern("console").log; | |
| 226 | print(Person{ name: "hello", age: 42 }); | |
| 295 | fn main { | |
| 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 | 2 | "plugins": [ |
| 3 | 3 | // sort-lines: start |
| 4 | 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 | 6 | "https://plugins.dprint.dev/json-0.19.3.wasm", |
| 6 | 7 | "https://plugins.dprint.dev/markdown-0.17.2.wasm", |
| 7 | 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 | 666 | |
| 667 | 667 | const Options = struct { |
| 668 | 668 | dump_ir: bool = false, |
| 669 | verbose_sema: bool = true, | |
| 669 | verbose_sema: bool = false, | |
| 670 | 670 | }; |
| 671 | 671 | |
| 672 | 672 | /// The compiler backend is stored separately from the compilation, and |
src/core/InternPool.zig+1-1| ... | ... | @@ -2,7 +2,7 @@ |
| 2 | 2 | //! https://github.com/ziglang/zig/blob/0.12.0/src/InternPool.zig |
| 3 | 3 | //! |
| 4 | 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 | 6 | //! This data structure is intended to be serializable for incremental |
| 7 | 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 | 693 | const lhs_type = s.typeOfRef(lhs); |
| 694 | 694 | |
| 695 | 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 | 717 | try s.ensureAtRuntime(s.lazyNode(.lhs)); |
| 697 | 718 | return s.addRef(.field_opaque, .{ |
| 698 | 719 | .lhs = lhs, |
| ... | ... | @@ -704,25 +725,6 @@ fn stepInner(s: *Sema, comptime tag: Teg.Tag) Error!InstructionKind.fromTag(tag) |
| 704 | 725 | return try s.fieldAccessStruct(lhs, lhs_type, struct_type, field.rhs); |
| 705 | 726 | } else if (lhs_type.toTupleType(s.ip)) |tuple_type| { |
| 706 | 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 | 730 | return s.fail(.field_invalid_type, .{ |
src/core/Teg.zig+1-1| ... | ... | @@ -1,4 +1,4 @@ |
| 1 | //! TODO: rename this to something else. Teg is not a graph | |
| 1 | //! TODO: rename this to EIR (expression intermediate representation) | |
| 2 | 2 | |
| 3 | 3 | insts: Inst.List.Slice, |
| 4 | 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 | 76 | } |
| 77 | 77 | |
| 78 | 78 | try writer.writeAll(std.fmt.comptimePrint( |
| 79 | "// Language Compiler v{}. Alpha quality software.\n", | |
| 79 | "// ix compiler v{}. alpha quality software.\n", | |
| 80 | 80 | .{@import("config").version}, |
| 81 | 81 | )); |
| 82 | 82 | |
| ... | ... | @@ -514,16 +514,18 @@ fn printExprLevel( |
| 514 | 514 | |
| 515 | 515 | try writer.writeAll(")"); |
| 516 | 516 | }, |
| 517 | ||
| 517 | 518 | .js_new => { |
| 518 | 519 | const call: Tree.Call = tree.nodeData(.js_new, node); |
| 519 | 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 | 524 | try writer.writeAll("("); |
| 523 | 525 | try writer.writeAll("new "); |
| 524 | 526 | try link.printExpr(tree, call.base, writer); |
| 525 | 527 | |
| 526 | if (args.len > 0) { | |
| 528 | if (args.len > 0 or level == .member) { | |
| 527 | 529 | try writer.writeAll("("); |
| 528 | 530 | |
| 529 | 531 | for (args.slice(tree), 0..) |arg, i| { |
| ... | ... | @@ -533,14 +535,14 @@ fn printExprLevel( |
| 533 | 535 | |
| 534 | 536 | try writer.writeAll(")"); |
| 535 | 537 | } |
| 536 | if (Level.new.needsParens(level)) | |
| 538 | if (wrap) | |
| 537 | 539 | try writer.writeAll(")"); |
| 538 | 540 | }, |
| 539 | 541 | |
| 540 | 542 | .field => { |
| 541 | 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 | 546 | try writer.writeAll("."); |
| 545 | 547 | try writer.writeAll(field.field.toSlice(&link.comp.intern_pool)); |
| 546 | 548 | }, |
src/core/mod.zig+1-1| ... | ... | @@ -1,4 +1,4 @@ |
| 1 | //! Core language compiler module. | |
| 1 | //! Core module for the Ix compiler | |
| 2 | 2 | |
| 3 | 3 | // sort-lines: start |
| 4 | 4 |
src/main.zig+1-1| ... | ... | @@ -1,7 +1,7 @@ |
| 1 | 1 | //! This is the user-facing command line, combining all features into |
| 2 | 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 | 6 | const Command = struct { |
| 7 | 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 | <!doctype html> | |
| 1 | <!DOCTYPE html> | |
| 2 | 2 | <html lang="en"> |
| 3 | <head> | |
| 4 | <meta charset="UTF-8" /> | |
| 5 | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| 6 | <title>Language Reference</title> | |
| 7 | <style> | |
| 8 | * { | |
| 9 | margin: 0; | |
| 10 | padding: 0; | |
| 11 | box-sizing: border-box; | |
| 12 | } | |
| 13 | body { | |
| 14 | background-color: #fefefe; | |
| 15 | font-family: | |
| 16 | system-ui, | |
| 17 | -apple-system, | |
| 18 | BlinkMacSystemFont, | |
| 19 | "Segoe UI", | |
| 20 | Roboto, | |
| 21 | Oxygen, | |
| 22 | Ubuntu, | |
| 23 | Cantarell, | |
| 24 | "Open Sans", | |
| 25 | "Helvetica Neue", | |
| 26 | sans-serif; | |
| 27 | max-width: 45rem; | |
| 28 | padding: 1rem; | |
| 29 | margin: auto; | |
| 30 | } | |
| 31 | h1, | |
| 32 | h2, | |
| 33 | h3, | |
| 34 | h4, | |
| 35 | h5, | |
| 36 | h6, | |
| 37 | p, | |
| 38 | ul, | |
| 39 | pre, | |
| 40 | blockquote, | |
| 41 | table { | |
| 42 | margin-bottom: 1rem; | |
| 43 | } | |
| 44 | ul { | |
| 45 | padding-left: 2rem; | |
| 46 | } | |
| 47 | li { | |
| 48 | margin-bottom: 0.25rem; | |
| 49 | } | |
| 50 | blockquote { | |
| 51 | background-color: #ddd; | |
| 52 | border-left: 4px solid black; | |
| 53 | padding-left: 16px; | |
| 54 | padding-top: 8px; | |
| 55 | padding-bottom: 8px; | |
| 56 | } | |
| 57 | pre > code { | |
| 58 | border: none; | |
| 59 | padding: 0; | |
| 60 | background-color: transparent; | |
| 61 | } | |
| 62 | code, | |
| 63 | pre { | |
| 64 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, | |
| 65 | "Liberation Mono", "Courier New", monospace; | |
| 66 | font-size: 0.9rem; | |
| 67 | border: 1px solid #9ca0b0; | |
| 68 | background-color: #eff1f5; | |
| 69 | border-radius: 4px; | |
| 70 | padding: 2px 4px; | |
| 71 | } | |
| 72 | pre { | |
| 73 | padding: 0.75rem 0.75rem; | |
| 74 | } | |
| 75 | .term { | |
| 76 | border: 1px solid #11111b; | |
| 77 | background-color: #24273a; | |
| 78 | color: #cdd6f4; | |
| 79 | } | |
| 80 | table, | |
| 81 | th, | |
| 82 | td { | |
| 83 | border: 1px solid #9ca0b0; | |
| 84 | border-collapse: collapse; | |
| 85 | } | |
| 86 | th, | |
| 87 | td { | |
| 88 | padding: 0.5rem; | |
| 89 | } | |
| 90 | .ansi-1 { | |
| 91 | font-weight: bold; | |
| 92 | } | |
| 93 | .ansi-31 { | |
| 94 | color: #ed8796; | |
| 95 | } | |
| 96 | .ansi-32 { | |
| 97 | color: #a6e3a1; | |
| 98 | } | |
| 99 | .ansi-33 { | |
| 100 | color: #eed49f; | |
| 101 | } | |
| 102 | .ansi-34 { | |
| 103 | color: #8aadf4; | |
| 104 | } | |
| 105 | .ansi-35 { | |
| 106 | color: #f5c2e7; | |
| 107 | } | |
| 108 | .ansi-36 { | |
| 109 | color: #94e2d5; | |
| 110 | } | |
| 111 | .ansi-2 { | |
| 112 | color: #8087a2; | |
| 113 | } | |
| 114 | .kw { | |
| 115 | color: #8839ef; | |
| 116 | font-weight: bold; | |
| 117 | } | |
| 118 | .tok-string { | |
| 119 | color: #40a02b; | |
| 120 | } | |
| 121 | .tok-builtin_id { | |
| 122 | color: #d20f39; | |
| 123 | font-weight: bold; | |
| 124 | } | |
| 125 | .tok-number_int, | |
| 126 | .tok-number_float { | |
| 127 | color: #fe640b; | |
| 128 | } | |
| 129 | </style> | |
| 130 | </head> | |
| 131 | <body> | |
| 132 | <main>{contents}</main> | |
| 133 | </body> | |
| 3 | <head> | |
| 4 | <meta charset="UTF-8" /> | |
| 5 | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| 6 | <title>Ix Language Manual</title> | |
| 7 | <style> | |
| 8 | :root { | |
| 9 | /* Catppuccin Latte (light) & Mocha (dark) */ | |
| 10 | --mantle: light-dark(#e6e9ef, #181825); | |
| 11 | --crust: light-dark(#dce0e8, #11111b); | |
| 12 | --base: light-dark(#eff1f5, #1e1e2e); | |
| 13 | --surface-0: light-dark(#ccd0da, #313244); | |
| 14 | --surface-1: light-dark(#bcc0cc, #45475a); | |
| 15 | --surface-2: light-dark(#acb0be, #585b70); | |
| 16 | --overlay-0: light-dark(#9ca0b0, #6c7086); | |
| 17 | --overlay-1: light-dark(#8c8fa1, #7f849c); | |
| 18 | --overlay-2: light-dark(#7c7f93, #9399b2); | |
| 19 | --subtext-0: light-dark(#6c6f85, #a6adc8); | |
| 20 | --subtext-1: light-dark(#5c5f77, #bac2de); | |
| 21 | --text: light-dark(#4c4f69, #cdd6f4); | |
| 22 | --accent: light-dark(#179299, #94e2d5); | |
| 23 | --link: light-dark(#179299, #94e2d5); | |
| 24 | } | |
| 25 | ||
| 26 | * { | |
| 27 | margin: 0; | |
| 28 | padding: 0; | |
| 29 | box-sizing: border-box; | |
| 30 | } | |
| 31 | ||
| 32 | body { | |
| 33 | background-color: var(--base); | |
| 34 | color: var(--text); | |
| 35 | font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", | |
| 36 | Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", | |
| 37 | sans-serif; | |
| 38 | color-scheme: light dark; | |
| 39 | display: flex; | |
| 40 | } | |
| 41 | ||
| 42 | main { | |
| 43 | width: 100%; | |
| 44 | max-width: 80ch; | |
| 45 | padding: 1rem; | |
| 46 | padding-top: 2rem; | |
| 47 | margin: auto; | |
| 48 | line-height: 1.5; | |
| 49 | } | |
| 50 | ||
| 51 | h1, | |
| 52 | h2, | |
| 53 | h3, | |
| 54 | h4, | |
| 55 | h5, | |
| 56 | h6, | |
| 57 | p, | |
| 58 | ul, | |
| 59 | pre, | |
| 60 | blockquote, | |
| 61 | table { | |
| 62 | margin-bottom: 1rem; | |
| 63 | } | |
| 64 | ||
| 65 | h2, | |
| 66 | h3, | |
| 67 | h4, | |
| 68 | h5, | |
| 69 | h6 { | |
| 70 | padding-top: 0.8rem; | |
| 71 | margin-top: -0.8rem; | |
| 72 | } | |
| 73 | ||
| 74 | ul { | |
| 75 | padding-left: 2rem; | |
| 76 | } | |
| 77 | ||
| 78 | li { | |
| 79 | margin-bottom: 0.25rem; | |
| 80 | } | |
| 81 | ||
| 82 | blockquote { | |
| 83 | background-color: var(--surface-0); | |
| 84 | border-left: 4px solid var(--overlay-2); | |
| 85 | padding-left: 16px; | |
| 86 | padding-top: 8px; | |
| 87 | padding-bottom: 8px; | |
| 88 | } | |
| 89 | ||
| 90 | a { | |
| 91 | color: inherit; | |
| 92 | } | |
| 93 | ||
| 94 | table, | |
| 95 | th, | |
| 96 | td { | |
| 97 | border: 1px solid #9ca0b0; | |
| 98 | border-collapse: collapse; | |
| 99 | } | |
| 100 | ||
| 101 | th, | |
| 102 | td { | |
| 103 | padding: 0.5rem; | |
| 104 | } | |
| 105 | ||
| 106 | .ansi-1 { | |
| 107 | font-weight: bold; | |
| 108 | } | |
| 109 | ||
| 110 | .ansi-31 { | |
| 111 | color: light-dark(#d20f39, #f38ba8); | |
| 112 | } | |
| 113 | ||
| 114 | .ansi-32 { | |
| 115 | color: light-dark(#40a02b, #a6e3a1); | |
| 116 | } | |
| 117 | ||
| 118 | .ansi-33 { | |
| 119 | color: light-dark(#de8d1c, #f9e2af); | |
| 120 | } | |
| 121 | ||
| 122 | .ansi-34 { | |
| 123 | color: light-dark(#1e66f5, #89b4fa); | |
| 124 | } | |
| 125 | ||
| 126 | .ansi-35 { | |
| 127 | color: light-dark(#ea76cb, #f5c2e7); | |
| 128 | } | |
| 129 | ||
| 130 | .ansi-36 { | |
| 131 | color: light-dark(#179299, #94e2d5); | |
| 132 | } | |
| 133 | ||
| 134 | .ansi-2 { | |
| 135 | color: light-dark(#bcc0cc, #8087a2); | |
| 136 | } | |
| 137 | ||
| 138 | .kw { | |
| 139 | color: light-dark(#8839ef, #cba6f7); | |
| 140 | font-weight: bold; | |
| 141 | } | |
| 142 | ||
| 143 | .tok-string { | |
| 144 | color: light-dark(#40a02b, #a6e3a1); | |
| 145 | } | |
| 146 | ||
| 147 | .tok-builtin_id { | |
| 148 | color: light-dark(#d20f39, #f38ba8); | |
| 149 | font-weight: bold; | |
| 150 | } | |
| 151 | ||
| 152 | .tok-number_int, | |
| 153 | .tok-number_float { | |
| 154 | color: light-dark(#fe640b, #fab387); | |
| 155 | } | |
| 156 | ||
| 157 | .tok-semi, | |
| 158 | .tok-end_paren, | |
| 159 | .tok-start_paren, | |
| 160 | .tok-equal, | |
| 161 | .tok-colon, | |
| 162 | .tok-start_brace, | |
| 163 | .tok-end_brace, | |
| 164 | .tok-dot { | |
| 165 | color: light-dark(#7c7f93, #9399b2); | |
| 166 | } | |
| 167 | ||
| 168 | aside { | |
| 169 | position: sticky; | |
| 170 | top: 0; | |
| 171 | left: 0; | |
| 172 | height: 100dvh; | |
| 173 | height: 100vh; | |
| 174 | } | |
| 175 | ||
| 176 | #sidebar-content { | |
| 177 | display: flex; | |
| 178 | flex-direction: column; | |
| 179 | background-color: var(--mantle); | |
| 180 | height: 100%; | |
| 181 | padding: 1rem; | |
| 182 | border-radius: 0 1rem 1rem 0; | |
| 183 | overflow-y: auto; | |
| 184 | width: 40ch; | |
| 185 | } | |
| 186 | ||
| 187 | #toc, | |
| 188 | #toc ul, | |
| 189 | #links { | |
| 190 | list-style: none; | |
| 191 | } | |
| 192 | ||
| 193 | nav header { | |
| 194 | font-weight: bold; | |
| 195 | font-size: 1.5rem; | |
| 196 | } | |
| 197 | ||
| 198 | #toc { | |
| 199 | padding: 0; | |
| 200 | } | |
| 201 | ||
| 202 | #toc ul { | |
| 203 | margin: 0rem; | |
| 204 | padding-left: 1rem; | |
| 205 | margin-bottom: 0; | |
| 206 | } | |
| 207 | ||
| 208 | #toc li { | |
| 209 | margin: 0; | |
| 210 | } | |
| 211 | ||
| 212 | #toc ul li { | |
| 213 | border-left: 1px solid var(--surface-2); | |
| 214 | padding-left: 0.5rem; | |
| 215 | } | |
| 216 | ||
| 217 | #toc ul li:hover { | |
| 218 | border-left: 1px solid var(--overlay-2); | |
| 219 | } | |
| 220 | ||
| 221 | #toc .number { | |
| 222 | color: var(--accent); | |
| 223 | font-weight: bold; | |
| 224 | } | |
| 225 | ||
| 226 | aside a { | |
| 227 | position: relative; | |
| 228 | /* anchor point for 'aside a::before' */ | |
| 229 | display: block; | |
| 230 | text-decoration: none; | |
| 231 | color: var(--text); | |
| 232 | padding: 0.45rem 0.75rem; | |
| 233 | border-radius: 0.25rem; | |
| 234 | } | |
| 235 | ||
| 236 | /* this invisible hitbox lets you click in the left gutter */ | |
| 237 | aside a::before { | |
| 238 | content: " "; | |
| 239 | position: absolute; | |
| 240 | display: block; | |
| 241 | right: 100%; | |
| 242 | top: 0; | |
| 243 | bottom: 0; | |
| 244 | width: 100vw; | |
| 245 | } | |
| 246 | ||
| 247 | aside a:hover { | |
| 248 | background-color: var(--surface-0); | |
| 249 | } | |
| 250 | ||
| 251 | #links { | |
| 252 | list-style: none; | |
| 253 | padding: 0; | |
| 254 | margin-top: auto; | |
| 255 | } | |
| 256 | ||
| 257 | #links li { | |
| 258 | margin: 0; | |
| 259 | } | |
| 260 | ||
| 261 | #hidden-menu-checkbox, | |
| 262 | [name="tabs"] { | |
| 263 | display: block; | |
| 264 | width: 1px; | |
| 265 | height: 1px; | |
| 266 | position: fixed; | |
| 267 | top: -3px; | |
| 268 | left: -3px; | |
| 269 | } | |
| 270 | ||
| 271 | #menu-btn, | |
| 272 | #clickable-background { | |
| 273 | display: none; | |
| 274 | } | |
| 275 | ||
| 276 | main a { | |
| 277 | color: var(--link); | |
| 278 | } | |
| 279 | ||
| 280 | main a:active { | |
| 281 | background-color: var(--accent); | |
| 282 | color: var(--base); | |
| 283 | border-radius: 0.25rem; | |
| 284 | text-decoration: none; | |
| 285 | } | |
| 286 | ||
| 287 | .external::after { | |
| 288 | background-color: var(--link); | |
| 289 | content: ""; | |
| 290 | display: inline-flex; | |
| 291 | height: 10px; | |
| 292 | margin-left: 4px; | |
| 293 | mask-image: url("data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMTYgMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTcuNSAxLjVWMEgxNnY4LjVoLTEuNVYyLjZMNyAxMC4xIDYgOWw3LjUtNy42aC02Wk0xMyAxNmMuNiAwIDEtLjQgMS0xdi01aC0xLjV2NC41aC0xMXYtMTFINlYySDFhMSAxIDAgMCAwLTEgMXYxMmMwIC42LjQgMSAxIDFoMTJaIiBmaWxsPSJyZWQiLz48L3N2Zz4="); | |
| 294 | mask-size: cover; | |
| 295 | width: 10px; | |
| 296 | } | |
| 297 | .external:active::after { | |
| 298 | background-color: var(--base); | |
| 299 | } | |
| 300 | ||
| 301 | .heading { | |
| 302 | color: inherit; | |
| 303 | text-decoration: none; | |
| 304 | } | |
| 305 | ||
| 306 | .heading:hover, | |
| 307 | .heading:focus-visible { | |
| 308 | text-decoration: underline; | |
| 309 | position: relative; | |
| 310 | } | |
| 311 | ||
| 312 | .heading:hover::before, | |
| 313 | .heading:focus-visible::before { | |
| 314 | content: " #"; | |
| 315 | color: var(--subtext-0); | |
| 316 | position: absolute; | |
| 317 | right: calc(100% + 0.5rem); | |
| 318 | top: -0.125em; | |
| 319 | } | |
| 320 | ||
| 321 | /* Tablet/small screen layout */ | |
| 322 | @media screen and (max-width: 1000px) { | |
| 323 | html { | |
| 324 | scroll-padding-top: 5rem; | |
| 325 | } | |
| 326 | ||
| 327 | main { | |
| 328 | padding-top: 5rem; | |
| 329 | } | |
| 330 | ||
| 331 | #hidden-menu-checkbox:focus-visible+aside #menu-btn { | |
| 332 | outline: 2px solid var(--text); | |
| 333 | } | |
| 334 | ||
| 335 | aside { | |
| 336 | position: fixed; | |
| 337 | left: -100%; | |
| 338 | transition: left 500ms, bottom 300ms; | |
| 339 | z-index: 9; | |
| 340 | padding-top: 3rem; | |
| 341 | } | |
| 342 | ||
| 343 | aside a { | |
| 344 | padding: 0.5rem 0.75rem; | |
| 345 | } | |
| 346 | ||
| 347 | #top-part { | |
| 348 | position: fixed; | |
| 349 | inset: 0; | |
| 350 | height: fit-content; | |
| 351 | background-color: var(--mantle); | |
| 352 | padding: 0.5rem; | |
| 353 | box-shadow: 0 4px 4px rgba(0, 0, 0, 0.3); | |
| 354 | z-index: 9; | |
| 355 | } | |
| 356 | ||
| 357 | #menu-btn { | |
| 358 | display: grid; | |
| 359 | padding: 0.5rem; | |
| 360 | width: fit-content; | |
| 361 | cursor: pointer; | |
| 362 | border-radius: 0.25rem; | |
| 363 | user-select: none; | |
| 364 | } | |
| 365 | ||
| 366 | ||
| 367 | #close { | |
| 368 | display: none; | |
| 369 | } | |
| 370 | ||
| 371 | #chevron { | |
| 372 | display: none; | |
| 373 | } | |
| 374 | ||
| 375 | #menu-btn:hover { | |
| 376 | background-color: var(--surface-0); | |
| 377 | } | |
| 378 | ||
| 379 | #hidden-menu-checkbox:checked~aside { | |
| 380 | left: 0; | |
| 381 | transition: left 300ms, bottom 300ms; | |
| 382 | } | |
| 383 | ||
| 384 | #hidden-menu-checkbox:checked~aside #clickable-background { | |
| 385 | display: block; | |
| 386 | position: fixed; | |
| 387 | width: 100%; | |
| 388 | height: 100%; | |
| 389 | inset: 0; | |
| 390 | background-color: rgba(0, 0, 0, 0.5); | |
| 391 | z-index: -1; | |
| 392 | } | |
| 393 | ||
| 394 | #hidden-menu-checkbox:checked~aside #menu-btn .open { | |
| 395 | display: none; | |
| 396 | } | |
| 397 | ||
| 398 | #hidden-menu-checkbox:checked~aside #menu-btn .close { | |
| 399 | display: block; | |
| 400 | } | |
| 401 | } | |
| 402 | ||
| 403 | /* Mobile layout */ | |
| 404 | @media screen and (max-width: 600px) { | |
| 405 | main { | |
| 406 | padding: 3rem 1rem; | |
| 407 | } | |
| 408 | ||
| 409 | aside { | |
| 410 | position: fixed; | |
| 411 | top: initial; | |
| 412 | left: 0; | |
| 413 | bottom: calc(-100% + 6rem); | |
| 414 | width: 100%; | |
| 415 | display: flex; | |
| 416 | flex-direction: column; | |
| 417 | } | |
| 418 | ||
| 419 | aside a { | |
| 420 | padding: 0.8rem 0.75rem; | |
| 421 | } | |
| 422 | ||
| 423 | aside a:hover { | |
| 424 | background-color: var(--surface-1); | |
| 425 | } | |
| 426 | ||
| 427 | #sidebar-content { | |
| 428 | border-radius: 0px; | |
| 429 | padding: 1rem; | |
| 430 | width: 100%; | |
| 431 | background-color: var(--surface-0); | |
| 432 | } | |
| 433 | ||
| 434 | #top-part { | |
| 435 | position: initial; | |
| 436 | padding: 0; | |
| 437 | border-radius: 1.5rem 1.5rem 0 0; | |
| 438 | box-shadow: 0 -4px 5px rgba(0, 0, 0, 0.15); | |
| 439 | border-bottom: var(--overlay-2) 1px solid; | |
| 440 | background-color: var(--surface-0); | |
| 441 | } | |
| 442 | ||
| 443 | #open, | |
| 444 | #close { | |
| 445 | display: none !important; | |
| 446 | } | |
| 447 | ||
| 448 | #chevron { | |
| 449 | display: block; | |
| 450 | transition: transform 300ms; | |
| 451 | } | |
| 452 | ||
| 453 | #menu-btn { | |
| 454 | display: flex; | |
| 455 | justify-content: center; | |
| 456 | width: 100%; | |
| 457 | padding: 1.5rem 0; | |
| 458 | border-radius: 1.5rem 1.5rem 0 0; | |
| 459 | } | |
| 460 | ||
| 461 | #hidden-menu-checkbox:checked~aside { | |
| 462 | bottom: 0; | |
| 463 | } | |
| 464 | ||
| 465 | #hidden-menu-checkbox:checked~aside #chevron { | |
| 466 | transform: rotateX(180deg); | |
| 467 | } | |
| 468 | ||
| 469 | #hidden-menu-checkbox:checked~aside #top-part { | |
| 470 | box-shadow: none; | |
| 471 | } | |
| 472 | } | |
| 473 | ||
| 474 | pre>code { | |
| 475 | border: none; | |
| 476 | padding: 0; | |
| 477 | } | |
| 478 | ||
| 479 | pre, | |
| 480 | code { | |
| 481 | border: 0; | |
| 482 | background-color: transparent; | |
| 483 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, | |
| 484 | "Liberation Mono", "Courier New", monospace; | |
| 485 | font-size: 0.9rem; | |
| 486 | } | |
| 487 | ||
| 488 | .file { | |
| 489 | border: 1px solid var(--surface-1); | |
| 490 | background-color: var(--crust); | |
| 491 | border-radius: 0.5rem; | |
| 492 | overflow: hidden; | |
| 493 | margin-bottom: 0.5rem; | |
| 494 | } | |
| 495 | ||
| 496 | .file figcaption { | |
| 497 | padding: 0.25rem 0.5rem; | |
| 498 | font-size: 0.75rem; | |
| 499 | border-bottom: 1px solid var(--surface-1); | |
| 500 | /* background-color: var(--crust); */ | |
| 501 | background-color: var(--accent); | |
| 502 | color: var(--base); | |
| 503 | } | |
| 504 | ||
| 505 | .file pre { | |
| 506 | margin: 0; | |
| 507 | padding: 0.75rem 0.5rem; | |
| 508 | overflow-y: scroll; | |
| 509 | color: var(--text); | |
| 510 | position: relative; | |
| 511 | } | |
| 512 | ||
| 513 | .file pre.terminal { | |
| 514 | background-color: #24273a; | |
| 515 | color: #cdd6f4; | |
| 516 | border-radius: 0 0 0.5rem 0.5rem; | |
| 517 | } | |
| 518 | ||
| 519 | .file [role="tablist"] { | |
| 520 | display: flex; | |
| 521 | border-top: 1px solid var(--surface-1); | |
| 522 | border-bottom: 1px solid var(--surface-1); | |
| 523 | background-color: var(--surface-0); | |
| 524 | } | |
| 525 | ||
| 526 | .file [role="tab"] { | |
| 527 | padding: 0.25rem 0.75rem; | |
| 528 | cursor: pointer; | |
| 529 | /* flex: 1; */ | |
| 530 | border-top: 2px solid transparent; | |
| 531 | text-align: center; | |
| 532 | font-size: 0.9rem; | |
| 533 | } | |
| 534 | ||
| 535 | .file .bottom { | |
| 536 | display: none; | |
| 537 | } | |
| 538 | ||
| 539 | [name="tabs"][value="output"]:checked~main .file .tabs .output { | |
| 540 | display: block; | |
| 541 | } | |
| 542 | ||
| 543 | [name="tabs"][value="output"]:checked~main .file .tabs [role="tab"][for="output"] { | |
| 544 | font-weight: bold; | |
| 545 | background-color: var(--base); | |
| 546 | border-top: 2px solid var(--accent); | |
| 547 | } | |
| 548 | ||
| 549 | [name="tabs"][value="js"]:checked~main .file .tabs .js { | |
| 550 | display: block; | |
| 551 | } | |
| 552 | ||
| 553 | [name="tabs"][value="js"]:checked~main .file .tabs [role="tab"][for="js"] { | |
| 554 | font-weight: bold; | |
| 555 | border-top: 2px solid var(--accent); | |
| 556 | background-color: var(--base); | |
| 557 | } | |
| 558 | ||
| 559 | .file .copy-button { | |
| 560 | position: absolute; | |
| 561 | top: 0.25rem; | |
| 562 | right: 0.25rem; | |
| 563 | padding: 0.25rem; | |
| 564 | border: 1px solid var(--surface-1); | |
| 565 | border-radius: 0.25rem; | |
| 566 | background-color: var(--base); | |
| 567 | cursor: pointer; | |
| 568 | display: grid; | |
| 569 | } | |
| 570 | ||
| 571 | .file .copy-button:hover { | |
| 572 | background-color: var(--crust); | |
| 573 | } | |
| 574 | ||
| 575 | .file .copy-button .hidden { | |
| 576 | display: none; | |
| 577 | } | |
| 578 | </style> | |
| 579 | </head> | |
| 580 | ||
| 581 | <body> | |
| 582 | <input type="checkbox" id="hidden-menu-checkbox" /> | |
| 583 | <input name="tabs" type="radio" id="output" value="output" checked="true" /> | |
| 584 | <input name="tabs" type="radio" id="js" value="js" /> | |
| 585 | <aside> | |
| 586 | <div id="top-part"> | |
| 587 | <label | |
| 588 | id="menu-btn" | |
| 589 | for="hidden-menu-checkbox" | |
| 590 | aria-label="Toggle menu visibility" | |
| 591 | > | |
| 592 | <svg | |
| 593 | xmlns="http://www.w3.org/2000/svg" | |
| 594 | height="24px" | |
| 595 | viewBox="0 -960 960 960" | |
| 596 | width="24px" | |
| 597 | fill="currentColor" | |
| 598 | id="open" | |
| 599 | > | |
| 600 | <path | |
| 601 | d="M120-240v-80h720v80H120Zm0-200v-80h720v80H120Zm0-200v-80h720v80H120Z" | |
| 602 | /> | |
| 603 | </svg> | |
| 604 | <svg | |
| 605 | xmlns="http://www.w3.org/2000/svg" | |
| 606 | height="24px" | |
| 607 | viewBox="0 -960 960 960" | |
| 608 | width="24px" | |
| 609 | fill="currentColor" | |
| 610 | id="close" | |
| 611 | > | |
| 612 | <path | |
| 613 | d="m256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z" | |
| 614 | /> | |
| 615 | </svg> | |
| 616 | <svg | |
| 617 | id="chevron" | |
| 618 | width="91" | |
| 619 | height="8" | |
| 620 | viewBox="0 0 91 8" | |
| 621 | fill="none" | |
| 622 | xmlns="http://www.w3.org/2000/svg" | |
| 623 | > | |
| 624 | <path | |
| 625 | d="M2 6L45.5714 2L89.1429 6" | |
| 626 | stroke="currentColor" | |
| 627 | stroke-width="3" | |
| 628 | stroke-linecap="square" | |
| 629 | /> | |
| 630 | </svg> | |
| 631 | </label> | |
| 632 | </div> | |
| 633 | ||
| 634 | <div id="sidebar-content"> | |
| 635 | <nav> | |
| 636 | <header><a href="#top">{nav_h1}</a></header> | |
| 637 | {navigation} | |
| 638 | </nav> | |
| 639 | ||
| 640 | <ul id="links"> | |
| 641 | <li> | |
| 642 | <a href="{repository}" target="_blank" rel="noopener noreferrer"> | |
| 643 | GitHub | |
| 644 | </a> | |
| 645 | </li> | |
| 646 | </ul> | |
| 647 | </div> | |
| 648 | ||
| 649 | <label id="clickable-background" for="hidden-menu-checkbox"></label> | |
| 650 | </aside> | |
| 651 | <main id="main"> | |
| 652 | {contents} | |
| 653 | </main> | |
| 654 | <script type="text/javascript"> | |
| 655 | const checkbox = document.getElementById("hidden-menu-checkbox"); | |
| 656 | const sidebar = document.getElementById("sidebar-content"); | |
| 657 | const sidebarLinks = sidebar.querySelectorAll("a"); | |
| 658 | ||
| 659 | sidebarLinks.forEach((link) => { | |
| 660 | link.addEventListener("click", () => { | |
| 661 | checkbox.checked = false; | |
| 662 | }); | |
| 663 | }); | |
| 664 | ||
| 665 | addEventListener("keydown", (ev) => { | |
| 666 | if (ev.key === "Escape" && checkbox.checked) { | |
| 667 | checkbox.checked = false; | |
| 668 | } | |
| 669 | }); | |
| 670 | ||
| 671 | const copyButtons = document.querySelectorAll(".copy-button"); | |
| 672 | ||
| 673 | copyButtons.forEach((button) => { | |
| 674 | button.addEventListener("click", () => { | |
| 675 | const code = button.parentElement.querySelector("code"); | |
| 676 | navigator.clipboard.writeText(code.innerText); | |
| 677 | const copySvg = button.children[0]; | |
| 678 | const checkSvg = button.children[1]; | |
| 679 | copySvg.classList.add('hidden'); | |
| 680 | checkSvg.classList.remove('hidden'); | |
| 681 | setTimeout(() => { | |
| 682 | copySvg.classList.remove('hidden'); | |
| 683 | checkSvg.classList.add('hidden'); | |
| 684 | }, 1000); | |
| 685 | }); | |
| 686 | }); | |
| 687 | </script> | |
| 688 | </body> | |
| 134 | 689 | </html> |