diff --git a/CHANGELOG.md b/CHANGELOG.md index dd71745e131b8617bc823abc843657391a016c7a..0e55238195a81a5e3ec6237f0e259933850c70fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## 0.1.7 +- chore: rename the language to "ix" - feat: allow any expression as a function body - fix: coerce `break` operands and implicit breaks to result type - fix: coerce results when `if` expression branches merge diff --git a/README.md b/README.md index 34342911d673b11ae23120ee79629e5ff1a9001b..c36e87250efe7a2648ecb5858ea9c7cdd919f603 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,34 @@ -# New Language Project +# Ix -This is a high-level, general purpose programming language. It is a mix of Rust, -Zig, and JavaScript's great points. +Ix (**I**nferable E**x**pression) is a high-level, general purpose programming +language. It features a mix of Rust, Zig, and JavaScript's great points. -For a guide on how to use the language, please see the Language Manual included -in the binary distribution. (TODO: host a rendered version) +For a guide on how to use Ix, see the manual included in the binary +distribution. Additionally, the latest commit of the manual is hosted online at +[language-manual.pages.dev](https://language-manual.pages.dev). ## Building -The primary dependency of the language SDK is a Zig compiler. If one is not +The primary dependency of the Ix compiler is a Zig compiler. If one is not available, the `./build` and `.\build.cmd` scripts can substitute for running `zig build` on POSIX and Windows systems, respectively. With a Zig compiler, a release build can be produced in a single command: zig build --release -To run the standard packaging procedure, which builds the SDK for all officially -supported targets: +To run the standard packaging procedure, which builds the compiler for all +officially supported targets: zig build package + +## Development + +Continuous integration is provided via a `./ci` (script for POSIX), which is run +by [self hosted Buildkite runners][ci]. This script uses the [`typos`][typos] +CLI to check spelling mistakes and [`dprint`][dprint] for code formatting. It is +recommended to setup editor integration with these two tools to prevent pushing +broken commits. + +[ci]: https://buildkite.com/paperdave/compiler +[typos]: https://github.com/crate-ci/typos +[dprint]: https://dprint.dev diff --git a/build.zig b/build.zig index 18e0ca831f79de13881cd284060a88d42a0c1385..58f4952d58492c27162cbf6fdaa99c4d90a2c0c2 100644 --- a/build.zig +++ b/build.zig @@ -18,8 +18,8 @@ pub const Options = struct { config: BuildConfig, }; -/// `createArtifactGroup` multiple times in order to have different -/// targets and configurations of the SDK at the same time. +/// `createArtifactGroup` is called multiple times in order to have different +/// targets and configurations of Ix at the same time. const ArtifactGroup = struct { build: *Build, opts: Options, @@ -446,10 +446,10 @@ pub fn build(b: *Build) !void { test_all.dependOn(run_step); } - // Language Reference Generator + // Language Manual Generator const manual_generator = b.addExecutable(.{ - .name = "generate-man", - .root_source_file = b.path("src/manual/generate-man.zig"), + .name = "generate_manual", + .root_source_file = b.path("src/manual/generate_manual.zig"), .target = b.graph.host, .optimize = .Debug, .link_libc = true, @@ -533,30 +533,21 @@ pub fn build(b: *Build) !void { .version = version, .have_embedded_quickjs = have_embedded_quickjs, }, - .optimize = optimize, + .optimize = .ReleaseSafe, .target = cross_target, .no_llvm = no_llvm, }); - inline for (.{ - .{ artifacts.cli, "compiler" }, - }) |entry| { - const artifact, const bin_name = entry; - package.dependOn(&b.addInstallFile( - artifact.getEmittedBin(), - b.pathJoin(&.{ - "package", - name, - b.fmt("{s}{s}", .{ - bin_name, - cross_target.result.exeFileExt(), - }), - }), - ).step); - } - + package.dependOn(&b.addInstallFile( + artifacts.cli.getEmittedBin(), + b.pathJoin(&.{ + "package", + name, + b.fmt("ix{s}", .{cross_target.result.exeFileExt()}), + }), + ).step); package.dependOn(&b.addInstallFile(b.path("CHANGELOG.md"), "package/CHANGELOG.md").step); - package.dependOn(&b.addInstallFile(manual_out, "package/README.html").step); + package.dependOn(&b.addInstallFile(manual_out, "package/ix.html").step); } { diff --git a/ci b/ci index 1b32e72911810d8cc7b55936344c096ac9a578a0..556ae5ae133efd4692e72c47cf8a677c64034e93 100755 --- a/ci +++ b/ci @@ -11,9 +11,11 @@ commit_short=$(echo "$commit" | cut -c -9) ./build --version typos dprint check -./build all --summary new if [ -n "$BUILDKITE" ]; then + # perform the entire build + ./build all --summary all + pushd zig-out/package zip -r compiler-$commit.zip * buildkite-agent artifact upload compiler-$commit.zip @@ -21,12 +23,15 @@ if [ -n "$BUILDKITE" ]; then rm -fr zig-out/site mkdir zig-out/site - cp zig-out/package/README.html zig-out/site/index.html - branch=$BUILDKITE_BRANCH + cp zig-out/package/ix.html zig-out/site/index.html + branch="$BUILDKITE_BRANCH" if [ "$branch" = "dev" ]; then branch=main fi - CLOUDFLARE_API_TOKEN=$(buildkite-agent secret get CLOUDFLARE_API_TOKEN) \ - CLOUDFLARE_ACCOUNT_ID=$(buildkite-agent secret get CLOUDFLARE_ACCOUNT_ID) \ + CLOUDFLARE_API_TOKEN="$(buildkite-agent secret get CLOUDFLARE_API_TOKEN)" \ + CLOUDFLARE_ACCOUNT_ID="$(buildkite-agent secret get CLOUDFLARE_ACCOUNT_ID)" \ wrangler pages deploy zig-out/site --branch "$branch" --commit-hash "$commit" --no-bundle --project-name language-manual +else + # perform only the tests, with a shorter summary + ./build test --summary new fi diff --git a/ci.cmd b/ci.cmd new file mode 100644 index 0000000000000000000000000000000000000000..4a48b012cc5bd1ffc68650d2225ed3badad64ceb --- /dev/null +++ b/ci.cmd @@ -0,0 +1,14 @@ +@ECHO OFF +@REM Continuous Integration Script +@REM This is a local-only version of ./ci as none of the +@REM self-hosted CI runners use Windows. This is just +@REM for local verification of builds. + +typos +if %errorlevel% neq 0 exit /b %errorlevel% + +dprint check +if %errorlevel% neq 0 exit /b %errorlevel% + +build.cmd test +if %errorlevel% neq 0 exit /b %errorlevel% diff --git a/doc/language-manual.md b/doc/language-manual.md index c26d08865ca2495a2e4e3e0178a65436151f2382..ffca26d424414617bbe800242efbf1d5dc49b9b3 100644 --- a/doc/language-manual.md +++ b/doc/language-manual.md @@ -1,54 +1,65 @@ -# New Language Project +# Ix Programming Language -This is a high-level, general purpose programming language. +Ix (**I**nferable E**x**pression) is a high-level programming language for +developing web applications and games. Key goals of this project are: -- **Ease of Use**: Use of type inference keeps code concise and easy to read and - write. -- **Fast Iteration**: The compiler is designed for quick incremental compilation - and hot module reloading. (note: incremental is not yet available) -- TODO: some more +- **Ease of Use**: Use of type inference and simple syntax makes code easy to + read and write. +- **Fast Iteration**: Incremental compilation keeps development productivity + high. +- **Safety**: Compiler errors catch most mistakes at compile-time, and + additional tooling to catch mistakes at runtime. +- **Universal**: One codebase can seamlessly target different web browsers and + native targets. It primarily focuses on merging some great points of Rust, Zig, and JavaScript's language designs: -- **Rust** - Expressive control flow that is easy to write. -- **Zig** - Execute any code at compile time; everything is an expression. -- **JavaScript** - Garbage collected values with a versatile type system. +- [**Rust**](https://www.rust-lang.org) - Expressive and concise control flow + that is easy to write. +- [**Zig**](https://ziglang.org) - Execute any code at compile time; everything + is an expression. +- [**JavaScript**](https://developer.mozilla.org/en-US/docs/Web/JavaScript) - + Garbage collected values with a versatile type system. -## Standard Library - -Currently, there is no standard library. One will be introduced later. +This manual is a standalone HTML document. Code samples are compiled and tested +as a part of the release cycle. ## Hello World ```lang:run let print = @extern("console").log; - fn main { print("Hello, world!"); } ``` The `print` function is implemented by getting an external reference to the -`console` JavaScript global and then retrieving the `log` property from it. +`console` JavaScript global, retrieving the `log` property from it, and then +calling it when the program starts. + +> In the future, common APIs will be available in the standard library, such as +> `std.log.info()`. For now, no such library exists. ## Primitive Data Types -| name | ts equivalent | description | -| -------- | --------------------- | ---------------------------------------------- | -| `int` | `number` | a number in signed 32-bit range | -| `float` | `number` | a 64-bit floating point number | -| `bool` | `boolean` | either `true` or `false` | -| `string` | `string` | a sequence of text characters | -| `void` | `undefined` or `null` | detectable lack of a value | -| `opaque` | `unknown` or `any` | could be any value | -| `never` | `never` | control flow prevents this value from existing | +| name | ts equivalent | description | +| ---------- | ----------------------------- | ---------------------------------------------- | +| `bool` | `boolean` | either `true` or `false` | +| `float` | `number` | a 64-bit floating point number | +| `int` | `number` | an integer number in signed 32-bit range | +| `bigint` | `bigint` | an integer number with no range limit | +| `string` | `string` | a sequence of text characters | +| `void` | `undefined` / `null` / `void` | detectable lack of a value | +| `opaque` | `unknown` / `any` | could be any value | +| `noreturn` | `never` | control flow prevents this value from existing | -> **note**: `bigint` is planned but not added +> **Note**: Unlike JavaScript, the values `undefined` and `null` are merged into +> one concept: [void](#void) -## Built-in Constants +### Built-in Constants In addition to the above types being built-in constants, the following are available. @@ -62,9 +73,48 @@ available. These constants are reserved names, and it is an error to re-declare them. ```lang:build -let true = 1 == 0; +fn main { + let true = 1 == 0; +} ``` +### Numbers + +Numbers have the following rules: + +- May start with a `.`, but cannot end with `.` +- May contain leading zeros, but transform pass will warn. +- May contain a `_` separator, cannot have two in a row. +- May contain `e` or `E` for exponents. +- May use 0x, 0b, 0o prefixes for hex, binary, octal numbers. + +### Strings + +Numbers + +### Multi-line string literals + +```lang +let bwaa = + \\hello + \\ world + \\end +; +``` + +### Void + +Void is the lack a value. For example, a function that does nothing returns +`void`. + +```lang +pub fn hello() { + // do nothing +} +``` + + + ## Variables There are two types of variables, `let` and `var`. @@ -74,24 +124,30 @@ expression within. Referring to these identifiers is equivalent to inlining it where it was used, aside from side effect duplication. ```lang:run -let hello: int = 1; -let world = hello + 2; -let hello = hello * world; +let print = @extern("console").log; -@extern("console").log(hello); +fn main { + let hello: int = 1; + let world = hello + 2; + let hello = hello * world; + + print(hello); +} ``` `var` bindings create mutable memory locations at runtime. This means they can be re-assigned to: ```lang:run -var message = "first message"; +fn main { + var message = "first message"; -if @extern("Math").random() > 0.5 { - message = "second message"; + if @extern("Math").random() > 0.5 { + message = "second message"; + } + + @extern("console").log(message); } - -@extern("console").log(message); ``` 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 as: ```lang:run -let message = if @extern("Math").random() > 0.5 { - "second message" -} else { - "first message" -}; +fn main { + let message = if @extern("Math").random() > 0.5 { + "second message" + } else { + "first message" + }; -@extern("console").log(message); + @extern("console").log(message); +} ``` 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 float, but a compiler warning will be emitted. ```lang -var number = 0; // `number` is a `float`, but not explicit +var number = 0; // `number` is a `float`, but not explicit var number: int = 0; // `number` is `int` var number: float = 0; // `number` is `float` ``` @@ -125,12 +183,15 @@ Functions are declared with the `fn` keyword. Parameter types must be annotated. The function body is a block, which may use implicit return to return. ```lang:run +let print = @extern("console").log; + fn add(a: int, b: int) -> int { a + b } -let print = @extern("console").log; -print("Sum of 2 and 7:", add(2, 7)); +fn main { + print("Sum of 2 and 7:", add(2, 7)); +} ``` > **Bug**: Return types are currently required. Omitting the `-> type` syntax @@ -140,6 +201,8 @@ print("Sum of 2 and 7:", add(2, 7)); The `return` expression can be used within functions to return early. ```lang:run +let print = @extern("console").log; + fn something(a: int) -> bool { if a == 0 { return false; @@ -148,8 +211,9 @@ fn something(a: int) -> bool { a > 100 } -let print = @extern("console").log; -print(something(0xFF), something(0)); +fn main { + print(something(0xFF), something(0)); +} ``` ## Number Literals @@ -178,7 +242,9 @@ fn currentYear() -> int { @jsNew(@extern("Date")).getFullYear() } -print(currentYear()); +fn main { + print(currentYear()); +} ``` A field access on an external handle is allowed to be performed at compile time, @@ -197,19 +263,21 @@ struct Position { y: int, } -let center = Position{ x: 0, y: -2 }; -print(center.x, center.y); +fn main { + let center = Position{ x: 0, y: -2 }; + print(center.x, center.y); -fn addPositions(a: Position, b: Position) -> Position { // To allow less verbose code, the struct type can be inferred with `.` + let added = addPositions( + .{ x: 1, y: -4 }, + .{ x: 5, y: 8 }, + ); + print("Add result:", added.x, added.y); +} + +fn addPositions(a: Position, b: Position) -> Position { .{ x: a.x + b.x, y: a.y + b.y } } - -let added = addPositions( - .{ x: 1, y: -4 }, - .{ x: 5, y: 8 }, -); -print("Add result:", added.x, added.y); ``` 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 function is not allowed. ```lang:build +let print = @extern("console").log; + struct Person { name: string, age: int, } -let print = @extern("console").log; -print(Person{ name: "hello", age: 42 }); +fn main { + print(Person{ name: "hello", age: 42 }); +} ``` + +## A + +### B + +## C + +### a + +### somewhat long header name but still is within dave's reasonable-ish constraints + +#### Number Literals 2 + +TODO + +#### String Literals 2 + +TODO + +#### String WAdsafdsa + +TODO + +### hewwo + +cat + +### cat + +#### yeah + +## what + +## omg + +## Lorem ipsum dolor sit + +## amet, consectetur adipiscing + +## elit. Proin vulputate ligula + +## ac risus blandit, eu accumsan + +## rna feugiat. Phasellus quis + +## metus felis. Semper mattis + +## augue eu elementum. Sed nec odio + +## cursus neque fringilla rutrum. + +## Cras vestibulum bibendum hendrerit. + +## Duis porta, leo vel + +## sollicitudin rhoncus, arcu nulla + +## lobortis est, vitae interdum + +## quam ante eu ante. Integer luctus + +## justo a odio cursus auctor. Phasellus + +## blandit urna vitae quam dapibus + +## efficitur. Proin sagittis urna non diff --git a/dprint.jsonc b/dprint.jsonc index d5ecbae66371478101102d84ea719c1cf0d23f0e..a4f51e8c2bcde9c62c1998c87bbb5388fd97c48c 100644 --- a/dprint.jsonc +++ b/dprint.jsonc @@ -2,6 +2,7 @@ "plugins": [ // sort-lines: start "https://plugins.dprint.dev/exec-0.5.0.json@8d9972eee71fa1590e04873540421f3eda7674d0f1aae3d7c788615e7b7413d0", + "https://plugins.dprint.dev/g-plane/markup_fmt-v0.12.0.wasm", "https://plugins.dprint.dev/json-0.19.3.wasm", "https://plugins.dprint.dev/markdown-0.17.2.wasm", // sort-lines: end diff --git a/src/core/Comp.zig b/src/core/Comp.zig index bf511722ceea1e75aee9457c1f78a7f90a7d8e0c..118b19df45534443da4ddd7fd4b786b25d8d1e41 100644 --- a/src/core/Comp.zig +++ b/src/core/Comp.zig @@ -666,7 +666,7 @@ pub fn indexTegModule(comp: *Comp, file: *File, module_index: Teg.Inst.Index) !N const Options = struct { dump_ir: bool = false, - verbose_sema: bool = true, + verbose_sema: bool = false, }; /// The compiler backend is stored separately from the compilation, and diff --git a/src/core/InternPool.zig b/src/core/InternPool.zig index 9001b9a78d2fc4b917124b600606a8e32e1c72c8..01aea56e759f7bded40c5df6bb801c5c634ec0ca 100644 --- a/src/core/InternPool.zig +++ b/src/core/InternPool.zig @@ -2,7 +2,7 @@ //! https://github.com/ziglang/zig/blob/0.12.0/src/InternPool.zig //! //! All interned objects have both a Value and a Type. Type is a subset of -//! Value, since the language allows operating on types the same as values. +//! Value, since Ix allows operating on types the same as values. //! This data structure is intended to be serializable for incremental //! compilation efforts. //! diff --git a/src/core/Sema.zig b/src/core/Sema.zig index 0a8846387ce3325a24496a4ce1921abbf6894c12..1366aed00beae7d9b392bff8adddcefe7a381346 100644 --- a/src/core/Sema.zig +++ b/src/core/Sema.zig @@ -693,6 +693,27 @@ fn stepInner(s: *Sema, comptime tag: Teg.Tag) Error!InstructionKind.fromTag(tag) const lhs_type = s.typeOfRef(lhs); if (lhs_type == .@"opaque") { + if (lhs.value()) |value| { + if (value.valueTag(s.ip) == .external_value) { + const rhs = try s.getCoercedValue(s.lazyNode(.rhs), field.rhs, .string); + const field_key = rhs.asString(s.ip).tryToNullTerminatedString(s.ip, s.gpa) catch { + @panic("TODO: support null in these strings"); + }; + + const previous_key = value.toKey(s.ip).external_value.key; + + const new_key = try std.mem.concat(s.arena(), NullTerminatedString, &.{ + previous_key, + &.{field_key}, + }); + defer s.arena().free(new_key); + + return s.makeValueAir(.{ .external_value = .{ + .key = new_key, + } }); + } + } + try s.ensureAtRuntime(s.lazyNode(.lhs)); return s.addRef(.field_opaque, .{ .lhs = lhs, @@ -704,25 +725,6 @@ fn stepInner(s: *Sema, comptime tag: Teg.Tag) Error!InstructionKind.fromTag(tag) return try s.fieldAccessStruct(lhs, lhs_type, struct_type, field.rhs); } else if (lhs_type.toTupleType(s.ip)) |tuple_type| { return try s.fieldAccessTuple(lhs, lhs_type, tuple_type, field.rhs); - } else if (lhs.value()) |value| { - if (value.valueTag(s.ip) == .external_value) { - const rhs = try s.getCoercedValue(s.lazyNode(.rhs), field.rhs, .string); - const field_key = rhs.asString(s.ip).tryToNullTerminatedString(s.ip, s.gpa) catch { - @panic("TODO: support null in these strings"); - }; - - const previous_key = value.toKey(s.ip).external_value.key; - - const new_key = try std.mem.concat(s.arena(), NullTerminatedString, &.{ - previous_key, - &.{field_key}, - }); - defer s.arena().free(new_key); - - return s.makeValueAir(.{ .external_value = .{ - .key = new_key, - } }); - } } return s.fail(.field_invalid_type, .{ diff --git a/src/core/Teg.zig b/src/core/Teg.zig index d207cf72561b3ab027aa7e5859f565dc9b76dd4f..11b844392eb6864598b6b79f087ba3af50f1b51e 100644 --- a/src/core/Teg.zig +++ b/src/core/Teg.zig @@ -1,4 +1,4 @@ -//! TODO: rename this to something else. Teg is not a graph +//! TODO: rename this to EIR (expression intermediate representation) insts: Inst.List.Slice, string_bytes: []const u8, diff --git a/src/core/js/Link.zig b/src/core/js/Link.zig index a60404f119611e2fd5e5823044f36caebf92c403..fb436e07f7413b812bdb416542ebfb4944faa6b9 100644 --- a/src/core/js/Link.zig +++ b/src/core/js/Link.zig @@ -76,7 +76,7 @@ pub fn linkJsCode(link: *Link, entry_decl: Decl.Index, writer: Writer) !void { } try writer.writeAll(std.fmt.comptimePrint( - "// Language Compiler v{}. Alpha quality software.\n", + "// ix compiler v{}. alpha quality software.\n", .{@import("config").version}, )); @@ -514,16 +514,18 @@ fn printExprLevel( try writer.writeAll(")"); }, + .js_new => { const call: Tree.Call = tree.nodeData(.js_new, node); const args = call.args.get(tree); - if (Level.new.needsParens(level)) + const wrap = Level.new.needsParens(level) and level != .member; + if (wrap) try writer.writeAll("("); try writer.writeAll("new "); try link.printExpr(tree, call.base, writer); - if (args.len > 0) { + if (args.len > 0 or level == .member) { try writer.writeAll("("); for (args.slice(tree), 0..) |arg, i| { @@ -533,14 +535,14 @@ fn printExprLevel( try writer.writeAll(")"); } - if (Level.new.needsParens(level)) + if (wrap) try writer.writeAll(")"); }, .field => { const field: Tree.Field = tree.nodeData(.field, node); - try link.printExpr(tree, field.base, writer); + try link.printExprLevel(tree, field.base, .member, writer); try writer.writeAll("."); try writer.writeAll(field.field.toSlice(&link.comp.intern_pool)); }, diff --git a/src/core/mod.zig b/src/core/mod.zig index 23014e0f5a75b2f3310af971a8324b8790dc9afe..5329216e139b9bcb06515327656c5a6832e3401e 100644 --- a/src/core/mod.zig +++ b/src/core/mod.zig @@ -1,4 +1,4 @@ -//! Core language compiler module. +//! Core module for the Ix compiler // sort-lines: start diff --git a/src/main.zig b/src/main.zig index 2a2c61c5049c039497e9d8693d30c3871f2970cb..531b4dc0d1a694c3b30fd0b916c6a2ccd97b3144 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,7 +1,7 @@ //! This is the user-facing command line, combining all features into //! different subcommands, each with their own option set. -const default_program_name = "wlang"; // fallback when argv[0] is missing +const default_program_name = "ix"; // fallback when argv[0] is missing const Command = struct { name: [:0]const u8, diff --git a/src/manual/NaiveHtmlMinifier.zig b/src/manual/NaiveHtmlMinifier.zig new file mode 100644 index 0000000000000000000000000000000000000000..61ed6588c769079ffee39acdbab95927534dbda3 --- /dev/null +++ b/src/manual/NaiveHtmlMinifier.zig @@ -0,0 +1,231 @@ +//! Removes whitespace between HTML/CSS markup. +//! This function is exclusively intended for `template.html`, +//! and doesn't handle many edge cases. +input: []const u8, +output: std.ArrayList(u8), + +const ws = " \t\r\n"; + +pub fn minify(input: []const u8, allocator: Allocator) ![]u8 { + var m: NaiveHtmlMinifier = .{ + .input = input, + .output = std.ArrayList(u8).init(allocator), + }; + errdefer m.output.deinit(); + + while (m.input.len > 0) { + m.skipWhitespace(); + try m.processHtml(); + } + + return try m.output.toOwnedSlice(); +} + +pub inline fn minifyComptime(input: []const u8) []const u8 { + comptime { + @setEvalBranchQuota(99999999); + var buf: [input.len * 2]u8 = undefined; + var fba = std.heap.FixedBufferAllocator.init(&buf); + const result_slice = minify(input, fba.allocator()) catch unreachable; // should not increase size + const result = result_slice[0..].*; + return &result; + } +} + +fn skipWhitespace(m: *NaiveHtmlMinifier) void { + var i: usize = 0; + while (m.input.len > i) switch (m.input[i]) { + ' ', '\t', '\n', '\r' => i += 1, + else => break, + }; + m.input = m.input[i..]; +} + +fn processHtml(m: *NaiveHtmlMinifier) !void { + switch (m.input[0]) { + ' ', '\t', '\n', '\r' => unreachable, // did not skip whitespace + + '<' => switch (m.input[1]) { + '!' => try m.moveTokenWith(">"), + '/' => try m.moveTokenWith(">"), + else => { + const first_tok = getToken(m, ws); + try m.moveToken(ws); + if (first_tok[first_tok.len - 1] == '>') { + try m.maybeProcessBody(first_tok[1 .. first_tok.len - 1]); + return; + } + const tag_name = first_tok[1..]; + var need_space_before_attr = true; + while (true) switch (m.input[0]) { + ' ', '\t', '\n', '\r' => unreachable, // did not skip whitespace + '>' => { + m.input = m.input[1..]; + try m.output.appendSlice(">"); + try m.maybeProcessBody(tag_name); + return; + }, + '/' => { + const tok = m.getToken(">"); + m.input = m.input[tok.len + 1 ..]; + try m.output.appendSlice("/>"); + try m.maybeProcessBody(tag_name); + return; + }, + else => { + if (need_space_before_attr) + try m.output.appendSlice(" "); + + try m.moveToken("=>" ++ ws); + if (m.input[0] == '=') { + m.input = m.input[1..]; + m.skipWhitespace(); + try m.output.appendSlice("="); + + // move attr value + const end = switch (m.input[0]) { + '"' => brk: { + need_space_before_attr = false; + try m.output.append('"'); + m.input = m.input[1..]; + break :brk "\""; + }, + '\'' => brk: { + need_space_before_attr = false; + try m.output.append('\''); + m.input = m.input[1..]; + break :brk "'"; + }, + else => brk: { + need_space_before_attr = true; + break :brk ws ++ ">"; + }, + }; + try m.moveTokenWith(end); + + m.skipWhitespace(); + } + }, + }; + }, + }, + + else => { + const text_block = m.getToken("<"); + m.input = m.input[text_block.len..]; + var it = mem.tokenizeAny(u8, text_block, ws); + try m.output.appendSlice(it.next().?); + while (it.next()) |token| { + try m.output.append(' '); + try m.output.appendSlice(token); + } + }, + } +} + +fn maybeProcessBody(m: *NaiveHtmlMinifier, tag_name: []const u8) !void { + inline for (.{ .style, .script, .pre }) |tag| { + if (mem.eql(u8, tag_name, @tagName(tag))) { + const end = mem.indexOf(u8, m.input, "") orelse @panic("no end"); + const tok = m.input[0 .. end + @tagName(tag).len + 3]; + + if (tag == .style) { + try m.processCss(m.input[0..end]); + } else { + try m.output.appendSlice(tok); + } + + m.input = m.input[end..]; + } + } +} + +fn processCss(m: *NaiveHtmlMinifier, input_css: []const u8) !void { + var without_comments = try std.ArrayList(u8).initCapacity(m.output.allocator, input_css.len); + defer without_comments.deinit(); + + { + var i: usize = 0; + while (mem.indexOfScalarPos(u8, input_css, i, '/')) |j| { + if (j + 1 < input_css.len and input_css[j + 1] == '*') { + try without_comments.appendSlice(input_css[i..j]); + i = mem.indexOfPos(u8, input_css, j + 2, "*/") orelse + std.debug.panic("no end comment in {s}", .{input_css[j..]}); + } else { + i = j + 1; + } + } + try without_comments.appendSlice(input_css[i..]); + } + + var cm: NaiveHtmlMinifier = .{ .input = without_comments.items, .output = undefined }; + cm.skipWhitespace(); + + while (cm.input.len > 0) { + const part = cm.getToken("{};"); + const last = cm.input[part.len]; + cm.input = cm.input[part.len + 1 ..]; + + switch (last) { + '{' => { + try appendWithoutCssComment(part, &m.output); + }, + ';', '}' => rule: { + if (part.len == 0) { + if (m.output.items[m.output.items.len - 1] == ';') + m.output.items.len -= 1; + break :rule; + } + + const i = mem.indexOfScalar(u8, part, ':') orelse + std.debug.panic("missing colon in {s}", .{part}); + const key = part[0..i]; + const value = part[i + 1 ..]; + + try appendWithoutCssComment(key, &m.output); + try m.output.append(':'); + try appendWithoutCssComment(value, &m.output); + }, + else => @panic("no end"), + } + + try m.output.append(last); + cm.skipWhitespace(); + } +} + +fn getToken(m: *const NaiveHtmlMinifier, ends: []const u8) []const u8 { + return m.input[0 .. mem.indexOfAny(u8, m.input, ends) orelse return m.input]; +} + +fn moveToken(m: *NaiveHtmlMinifier, ends: []const u8) !void { + const tok = m.getToken(ends); + m.input = m.input[tok.len..]; + m.skipWhitespace(); + try m.output.appendSlice(tok); +} + +fn moveTokenWith(m: *NaiveHtmlMinifier, ends: []const u8) !void { + const tok = m.getToken(ends); + try m.output.appendSlice(m.input[0 .. tok.len + 1]); + m.input = m.input[tok.len + 1 ..]; + m.skipWhitespace(); +} + +fn appendWithoutCssComment(text: []const u8, output: *std.ArrayList(u8)) !void { + var it = mem.tokenizeAny(u8, text, ws); + var needs_space = false; + while (it.next()) |tok| { + if (needs_space) try output.append(' '); + try output.appendSlice(tok); + needs_space = switch (tok[tok.len - 1]) { + ',' => false, + else => true, + }; + } +} + +const NaiveHtmlMinifier = @This(); +const std = @import("std"); +const mem = std.mem; +const Allocator = mem.Allocator; diff --git a/src/manual/generate-man.zig b/src/manual/generate-man.zig deleted file mode 100644 index 81ae60d2318b5dcebdb1b7ed708b8f71d28b1e0e..0000000000000000000000000000000000000000 --- a/src/manual/generate-man.zig +++ /dev/null @@ -1,705 +0,0 @@ -//! Language reference compiler. Converts markdown into HTML, using -//! the Tokenizer to highlight code, and the compiler and node.js to -//! show code results. Only ever built in Debug mode. We are not -//! smart about memory. Initially written at 3am on a Thursday. Enjoy -pub const std = @import("std"); -pub const core = @import("core"); - -const mem = std.mem; -const Allocator = mem.Allocator; -const ArrayList = std.ArrayList; -const ArrayListUnmanaged = std.ArrayListUnmanaged; -const assert = std.debug.assert; - -comptime { - assert(core.runtime_safety); -} - -pub fn main() !void { - comptime std.debug.assert(@import("builtin").mode == .Debug); - var gpa_state: std.heap.GeneralPurposeAllocator(.{}) = .{}; - const gpa = gpa_state.allocator(); - - const progress = std.Progress.start(.{ - .root_name = "Build manual", - .estimated_total_items = 0, - }); - - var args = try std.process.argsWithAllocator(gpa); - _ = args.next().?; - - const compiler_path = args.next().?; - const runtime_path = args.next().?; - const markdown_src = try std.fs.cwd().readFileAlloc(gpa, args.next().?, std.math.maxInt(usize)); - const template_src = try std.fs.cwd().readFileAlloc(gpa, args.next().?, std.math.maxInt(usize)); - const out_path = args.next().?; - - assert(args.next() == null); - - var contents = ArrayList(u8).init(gpa); - var header: HeaderWriter = .{}; - - try processMarkdown( - gpa, - progress, - markdown_src, - compiler_path, - runtime_path, - &header, - contents.writer(), - ); - - try header.end(gpa); - - const applied_template = try replaceInTemplate(gpa, template_src, .{ - .contents = contents.items, - .navigation = header.data.items, - .nav_h1 = header.h1.?, - .repository = "https://github.com/paperdave/wl", - }); - - const out_file = try std.fs.createFileAbsolute(out_path, .{}); - defer out_file.close(); - try out_file.writeAll(applied_template); - - progress.end(); - - std.debug.lockStdErr(); - std.debug.unlockStdErr(); - - std.process.exit(0); -} - -const BlockType = enum { - para, - h1, - h2, - h3, - h4, - h5, - h6, - code_run, - code_build, - code, - code_generic, - ul, - table, - blockquote, -}; - -const InlineType = enum { - bold, - code, - link, -}; - -const MarkdownTokenizer = struct { - /// Remaining text - str: []const u8, - - fn skipWhitespace(t: *MarkdownTokenizer) void { - t.str = mem.trimLeft(u8, t.str, " \r\n"); - } - - fn advance(t: *MarkdownTokenizer, n: usize) []const u8 { - defer { - t.str = t.str[n..]; - } - return t.str[0..n]; - } - - fn advanceLine(t: *MarkdownTokenizer) []const u8 { - return mem.trim(u8, t.advanceLineRaw(), " \r\n"); - } - - fn advanceLineRaw(t: *MarkdownTokenizer) []const u8 { - const n = mem.indexOfScalar(u8, t.str, '\n'); - const line = t.advance(if (n) |x| x + 1 else t.str.len); - return line; - } - - fn blockType(t: *MarkdownTokenizer) BlockType { - switch (t.str[0]) { - '#' => { - defer { - while (t.str[0] == '#') - _ = t.advance(1); - } - - if (t.str[1] != '#') return .h1; - if (t.str[2] != '#') return .h2; - if (t.str[3] != '#') return .h3; - if (t.str[4] != '#') return .h4; - if (t.str[5] != '#') return .h5; - if (t.str[6] != '#') return .h6; - @panic("headers nested too deep"); - }, - '>' => { - _ = t.advance(1); - return .blockquote; - }, - '|' => { - return .table; - }, - '`' => { - if (mem.startsWith(u8, t.str, "```")) { - defer _ = t.advanceLine(); - if (mem.startsWith(u8, t.str, "```lang:run")) - return .code_run; - if (mem.startsWith(u8, t.str, "```lang:build")) - return .code_build; - if (mem.startsWith(u8, t.str, "```lang")) - return .code; - return .code_generic; - } - return .para; - }, - '-' => { - return .ul; - }, - else => { - return .para; - }, - } - } -}; - -const HeaderWriter = struct { - data: std.ArrayListUnmanaged(u8) = .{}, - level: u8 = 0, - h1: ?[]const u8 = null, - - pub fn add(header: *HeaderWriter, alloc: Allocator, name: []const u8, id: []const u8, level: u8) !void { - const w = header.data.writer(alloc); - - while (header.level > level) { - header.level -= 1; - try w.writeAll(""); - } - - while (header.level < level) { - if (header.level == 0) { - try w.writeAll(""); - } - } -}; - -fn processMarkdown( - gpa: Allocator, - progress: std.Progress.Node, - src: []const u8, - compiler_path: []const u8, - runtime_path: []const u8, - header: *HeaderWriter, - writer: anytype, -) !void { - var t = MarkdownTokenizer{ .str = src }; - - t.skipWhitespace(); - while (t.str.len > 0) : (t.skipWhitespace()) { - switch (t.blockType()) { - .para, - => { - try writer.writeAll("

\n"); - - var para = ArrayList(u8).init(gpa); - try para.appendSlice(t.advanceLine()); - - var next_line = t.advanceLine(); - while (t.str.len > 0 and next_line.len > 0) { - try para.append(' '); - try para.appendSlice(next_line); - next_line = t.advanceLine(); - } - - try processInlineMarkdown(gpa, para.items, writer); - - try writer.writeAll("\n

\n"); - }, - .h1, - .h2, - .h3, - .h4, - .h5, - .h6, - => |tag| { - const html_tag = @tagName(tag); - const name = t.advanceLine(); - const id = try nameToHeaderId(gpa, name); - - if (tag != .h1) { - try writer.print("<{0s} id=\"{2s}\">{1s}\n", .{ - html_tag, - name, - id, - }); - - if (tag != .h1) - try header.add(gpa, name, id, switch (tag) { - .h2 => 1, - .h3 => 2, - .h4 => 3, - .h5 => 4, - .h6 => 5, - else => unreachable, - }); - } else { - assert(header.h1 == null); - - header.h1 = try std.fmt.allocPrint( - gpa, - "{s}", - .{name}, - ); - - try writer.print("<{0s}>{1s}\n", .{ - html_tag, - name, - }); - } - }, - .code_run, - .code_build, - .code, - .code_generic, - => |tag| { - var code = ArrayList(u8).init(gpa); - - var next_line = t.advanceLineRaw(); - while (t.str.len > 0 and !mem.startsWith(u8, next_line, "```")) { - try code.appendSlice(next_line); - next_line = t.advanceLineRaw(); - } - - if (tag == .code_run or tag == .code_build or tag == .code) { - try writer.writeAll("
hello.wl
"); - } else { - try writer.writeAll("
"); - } - - try writer.writeAll("
");
-                if (tag == .code_generic) {
-                    try writeHtmlEscapedSlice(code.items, writer, false);
-                } else {
-                    try writeTokenizedCode(gpa, code.items, writer);
-                }
-                try writer.writeAll("
"); - - try code.append('\n'); - - if (tag == .code_run or tag == .code_build) { - var prog = progress.start("Code Snippet", if (tag == .code_run) 2 else 1); - defer prog.end(); - - var prog_step = prog.start("Compile", 1); - var build = std.process.Child.init(&.{ compiler_path, "build", "/dev/stdin" }, gpa); - build.stdin_behavior = .Pipe; - build.stdout_behavior = .Pipe; - build.stderr_behavior = .Pipe; - var env_map = try std.process.getEnvMap(gpa); - defer env_map.deinit(); - try env_map.put("YES_COLOR", "1"); - build.env_map = &env_map; - try build.spawn(); - try build.stdin.?.writeAll(code.items); - var out = ArrayList(u8).init(gpa); - var err = ArrayList(u8).init(gpa); - try build.collectOutput(&out, &err, std.math.maxInt(usize)); - const result = try build.wait(); - if (result != .Exited or result.Exited != 0) { - try err.writer().print("[Process {s} with {d}]", .{ - switch (result) { - .Exited => "exited", - .Stopped => "stopped", - .Signal => "signaled", - .Unknown => "hit unknown state", - }, - switch (result) { - inline else => |d| @as(u32, d), - }, - }); - } - if (mem.trim(u8, err.items, " \n\r").len > 0) { - try writer.writeAll("
");
-                        try writeAnsi(gpa, err.items, writer);
-                        try writer.writeAll("
"); - } - const trimmed_out = mem.trim(u8, out.items, " \n\r"); - if (trimmed_out.len > 0) { - try writer.writeAll("
");
-                        try writer.writeAll("// Compiled JavaScript Code");
-                        try writer.writeAll(trimmed_out[mem.indexOfScalar(u8, trimmed_out, '\n').?..]);
-                        try writer.writeAll("
"); - } - - prog.completeOne(); - prog_step.end(); - - if (tag == .code_run and runtime_path.len > 0) { - const run_step = prog.start("Execute", 0); - defer run_step.end(); - - var temp_dir = std.testing.tmpDir(.{}); - defer temp_dir.cleanup(); - - const input_sub_path = "result.js"; - try temp_dir.dir.writeFile(.{ - .sub_path = input_sub_path, - .data = trimmed_out, - }); - - var env_map_run = try std.process.getEnvMap(gpa); - defer env_map_run.deinit(); - try env_map_run.put("NO_COLOR", "1"); - try env_map_run.put("TERM", "dumb"); - - var run_out = ArrayList(u8).init(gpa); - var run_err = ArrayList(u8).init(gpa); - - // Different JavaScript runtimes have different quirks with - // regards to CLI parsing and standard input streams. - const run_sub_command = std.mem.indexOf(u8, runtime_path, "deno") != null or - std.mem.indexOf(u8, runtime_path, "porf") != null; - - const cmd: []const []const u8 = if (run_sub_command) - &.{ runtime_path, "run", input_sub_path } - else - &.{ runtime_path, input_sub_path }; - - var run = std.process.Child.init(cmd, gpa); - - run.cwd_dir = temp_dir.dir; - var path_buf: [std.fs.max_path_bytes]u8 = undefined; - if (@import("builtin").os.tag == .windows) - run.cwd = try std.os.getFdPath(temp_dir.dir.fd, &path_buf); - - run.stdin_behavior = .Ignore; - run.stdout_behavior = .Pipe; - run.stderr_behavior = .Pipe; - - run.env_map = &env_map_run; - try run.spawn(); - - try run.collectOutput(&run_out, &run_err, std.math.maxInt(usize)); - const run_result = run.wait() catch |e| { - if (e == error.FileNotFound) { - std.debug.panic( - \\Missing JavaScript evaluator: {s} - \\Specify another one using -Djs_runtime=... or set to "" to skip evaluation. - \\ - , .{runtime_path}); - } - return e; - }; - - if (run_result != .Exited or run_result.Exited != 0) { - try run_err.writer().print("[Process {s} with {d}]", .{ - switch (run_result) { - .Exited => "exited", - .Stopped => "stopped", - .Signal => "signaled", - .Unknown => "hit unknown state", - }, - switch (run_result) { - inline else => |d| @as(u32, d), - }, - }); - } - - try writer.writeAll("
");
-                        try writer.writeAll(run_out.items);
-                        try writer.writeAll(run_err.items);
-                        try writer.writeAll("
"); - } - } - - try writer.writeAll("
"); - }, - .ul, - => { - try writer.writeAll(""); - }, - .table, - => { - try writer.writeAll(""); - { - try writer.writeAll(""); - var it = mem.tokenizeScalar(u8, t.advanceLine()[1..], '|'); - while (it.next()) |tok| { - try writer.writeAll(""); - } - try writer.writeAll(""); - } - _ = t.advanceLine(); - t.skipWhitespace(); - while (t.str.len > 0 and t.str[0] == '|') { - try writer.writeAll(""); - var it = mem.tokenizeScalar(u8, t.advanceLine()[1..], '|'); - while (it.next()) |tok| { - try writer.writeAll(""); - } - try writer.writeAll(""); - } - try writer.writeAll("
"); - try processInlineMarkdown(gpa, tok, writer); - try writer.writeAll("
"); - try processInlineMarkdown(gpa, tok, writer); - try writer.writeAll("
"); - }, - .blockquote, - => { - try writer.writeAll("
\n"); - - var para = ArrayList(u8).init(gpa); - try para.appendSlice(t.advanceLine()); - - var next_line = t.advanceLine(); - while (t.str.len > 0 and next_line.len > 0) { - try para.append(' '); - try para.appendSlice(next_line); - next_line = t.advanceLine(); - } - - try processInlineMarkdown(gpa, para.items, writer); - - try writer.writeAll("\n
\n"); - }, - } - } -} - -pub fn nextInlineToken(src: []const u8) ?struct { InlineType, usize } { - var pos: usize = 0; - while (true) : (pos += 1) { - pos = mem.indexOfAnyPos(u8, src, pos, "*`[") orelse return null; - - switch (src[pos]) { - '*' => switch (src[pos + 1]) { - '*' => return .{ .bold, pos }, - else => continue, - }, - '`' => return .{ .code, pos }, - '[' => return .{ .link, pos }, - else => unreachable, - } - } -} - -fn processInlineMarkdown(gpa: Allocator, src: []const u8, writer: anytype) !void { - var bold = false; - var remain = src; - - while (nextInlineToken(remain)) |tok| { - try writer.writeAll(remain[0..tok[1]]); - remain = remain[tok[1]..]; - switch (tok[0]) { - .bold => { - bold = !bold; - try writer.writeAll(switch (bold) { - true => "", - false => "", - }); - remain = remain[2..]; - }, - .code => { - const end = mem.indexOfScalarPos(u8, remain, 1, '`') orelse @panic("invalid code block"); - const code = remain[1..end]; - try writer.writeAll(""); - try writeTokenizedCode(gpa, code, writer); - try writer.writeAll(""); - remain = remain[end + 1 ..]; - }, - .link => { - const label_end = mem.indexOfScalarPos(u8, remain, 1, ']') orelse @panic("invalid link block"); - assert(remain[label_end + 1] == '('); // invalid link. '[' syntax is not implemented - - const href_end = mem.indexOfScalarPos(u8, remain, label_end + 2, ')') orelse @panic("invalid link block"); - - const label = remain[1..label_end]; - const href = remain[label_end + 2 .. href_end]; - - try writer.writeAll(""); - try processInlineMarkdown(gpa, label, writer); - try writer.writeAll(""); - remain = remain[href_end + 1 ..]; - }, - } - } - try writer.writeAll(remain); - - if (bold) { - std.debug.panic("Unterminated Bold in '{s}'", .{src}); - } -} - -// TODO: Change this to use Tree-sitter, which would also make it very simple -// to highlight JavaScript code in addition to the language. -fn writeTokenizedCode(gpa: Allocator, code: []const u8, writer: anytype) !void { - const code_z = try gpa.dupeZ(u8, code); - defer gpa.free(code_z); - - var t = core.Tokenizer{ .code = code_z }; - - t.skipWhitespace(); - var last: usize = 0; - while (true) { - t.skipWhitespace(); - const tok = t.next() orelse break; - try writeHtmlEscapedSlice(t.code[last..tok.offset], writer, false); - try writer.print("{s}", .{ - @tagName(tok.tag), - if (tok.tag.isKeyword()) " kw" else "", - t.code[tok.offset..t.pos], - }); - last = t.pos; - } - try writeHtmlEscapedSlice(t.code[last..], writer, false); -} - -fn writeAnsi(gpa: Allocator, code: []const u8, writer: anytype) !void { - var remain = code; - var count: usize = 0; - while (mem.indexOfScalar(u8, remain, '\x1b')) |esc_i| { - try writeHtmlEscapedSlice(remain[0..esc_i], writer, false); - switch (remain[esc_i + 1]) { - '[' => switch (remain[esc_i + 2]) { - '0'...'9' => { - const end = mem.indexOfScalarPos(u8, remain, esc_i + 2, 'm').?; - const int = std.fmt.parseInt(u8, remain[esc_i + 2 .. end], 10) catch unreachable; - switch (int) { - else => { - try writer.print("", .{int}); - count += 1; - }, - 0 => { - for (0..count) |_| { - try writer.writeAll(""); - } - count = 0; - }, - } - remain = remain[end + 1 ..]; - }, - else => |x| std.debug.panic("Unknown esc + 2: {d}", .{x}), - }, - else => |x| std.debug.panic("Unknown esc + 1: {d}", .{x}), - } - } - _ = gpa; -} - -fn nameToHeaderId(alloc: Allocator, input: []const u8) ![]const u8 { - var out = try ArrayList(u8).initCapacity(alloc, input.len); - - var is_split = false; - for (input) |char| { - switch (char) { - 'a'...'z', 'A'...'Z', '0'...'9' => { - if (is_split) { - out.appendAssumeCapacity('-'); - is_split = false; - } - out.appendAssumeCapacity(std.ascii.toLower(char)); - }, - else => { - is_split = true; - }, - } - } - - return try out.toOwnedSlice(); -} - -fn replaceInTemplate(alloc: Allocator, template: []const u8, replacements: anytype) ![]const u8 { - const Replacements = @TypeOf(replacements); - const Map = std.StringArrayHashMapUnmanaged([]const u8); - - var map: Map = .{}; - inline for (@typeInfo(Replacements).@"struct".fields) |field| { - try map.put(alloc, field.name, @field(replacements, field.name)); - } - - var out: ArrayListUnmanaged(u8) = .{}; - const w = out.writer(alloc); - - var i: usize = 0; - while (mem.indexOfScalarPos(u8, template, i, '{')) |open_at| { - const end = mem.indexOfScalarPos(u8, template, open_at + 1, '}') orelse break; - const inner = template[open_at + 1 .. end]; - - if (map.get(inner)) |replacement| { - // write previous - try w.writeAll(template[i..open_at]); - // write template - try w.writeAll(replacement); - // move one past the ending '}' - i = end + 1; - } else { - // write up to and including the { - try w.writeAll(template[i .. open_at + 1]); - // move one past it - i = open_at + 1; - } - } - - // write ending - try w.writeAll(template[i..]); - - return out.items; -} - -fn writeHtmlEscapedSlice(text: []const u8, writer: anytype, escape_quotes: bool) !void { - var i: usize = 0; - while (mem.indexOfAnyPos( - u8, - text, - i, - if (escape_quotes) - &.{ '&', '<', '>', '"' } - else - &.{ '&', '<', '>' }, - )) |at| { - const replace = switch (text[at]) { - '&' => "&", - '<' => "<", - '>' => ">", - '"' => """, - else => unreachable, - }; - try writer.writeAll(text[i..at]); - try writer.writeAll(replace); - i = at + 1; - } - try writer.writeAll(text[i..]); -} diff --git a/src/manual/generate_manual.zig b/src/manual/generate_manual.zig new file mode 100644 index 0000000000000000000000000000000000000000..6f4ff77e5d66bbcf9dc87cc29a5bc1b2992f51c3 --- /dev/null +++ b/src/manual/generate_manual.zig @@ -0,0 +1,825 @@ +//! Language reference compiler. Converts markdown into HTML, using +//! the Tokenizer to highlight code, and the compiler and QJS to +//! show code results. Only ever built in Debug mode. We are not +//! smart about memory. Initially written at 3am on a Thursday. Enjoy +pub const std = @import("std"); +pub const core = @import("core"); +const NaiveHtmlMinifier = @import("NaiveHtmlMinifier.zig"); + +const mem = std.mem; +const Allocator = mem.Allocator; +const ArrayList = std.ArrayList; +const ArrayListUnmanaged = std.ArrayListUnmanaged; +const assert = std.debug.assert; + +comptime { + assert(@import("builtin").mode == .Debug); +} + +const cli_name = "wlang"; +const build_cli = cli_name ++ " build"; +const test_cli = cli_name ++ " test"; +const run_cli = cli_name ++ " run"; + +pub fn main() !void { + comptime std.debug.assert(@import("builtin").mode == .Debug); + const gpa = std.heap.c_allocator; + + const progress = std.Progress.start(.{ + .root_name = "Build manual", + .estimated_total_items = 0, + }); + + var args = try std.process.argsWithAllocator(gpa); + _ = args.next().?; + + const compiler_path = args.next().?; + const runtime_path = args.next().?; + const markdown_src = try std.fs.cwd().readFileAlloc(gpa, args.next().?, std.math.maxInt(usize)); + const template_src = try std.fs.cwd().readFileAlloc(gpa, args.next().?, std.math.maxInt(usize)); + const out_path = args.next().?; + + assert(args.next() == null); + + var contents = ArrayList(u8).init(gpa); + var header: HeaderWriter = .{}; + + const minify = false; + + try processMarkdown( + gpa, + progress, + markdown_src, + compiler_path, + runtime_path, + &header, + contents.writer(), + ); + + try header.end(gpa); + + const minified_template = if (minify) + try NaiveHtmlMinifier.minify(template_src, gpa) + else + template_src; + + const applied_template = try replaceInTemplate(gpa, minified_template, .{ + .contents = contents.items, + .navigation = header.data.items, + .nav_h1 = "Ix Language", + .repository = "https://github.com/paperdave/wl", + }); + + const out_file = try std.fs.createFileAbsolute(out_path, .{}); + defer out_file.close(); + try out_file.writeAll(applied_template); + + progress.end(); + + std.debug.lockStdErr(); + std.debug.unlockStdErr(); + + std.process.exit(0); +} + +const BlockType = enum { + para, + h1, + h2, + h3, + h4, + h5, + h6, + code_run, + code_build, + code, + code_generic, + ul, + table, + blockquote, +}; + +const InlineType = enum { + bold, + code, + link, +}; + +const MarkdownTokenizer = struct { + /// Remaining text + str: []const u8, + + fn skipWhitespace(t: *MarkdownTokenizer) void { + t.str = mem.trimLeft(u8, t.str, " \r\n"); + } + + fn advance(t: *MarkdownTokenizer, n: usize) []const u8 { + defer { + t.str = t.str[n..]; + } + return t.str[0..n]; + } + + fn advanceLine(t: *MarkdownTokenizer) []const u8 { + return mem.trim(u8, t.advanceLineRaw(), " \r\n"); + } + + fn advanceLineRaw(t: *MarkdownTokenizer) []const u8 { + const n = mem.indexOfScalar(u8, t.str, '\n'); + const line = t.advance(if (n) |x| x + 1 else t.str.len); + return line; + } + + fn blockType(t: *MarkdownTokenizer) BlockType { + switch (t.str[0]) { + '#' => { + defer { + while (t.str[0] == '#') + _ = t.advance(1); + } + + if (t.str[1] != '#') return .h1; + if (t.str[2] != '#') return .h2; + if (t.str[3] != '#') return .h3; + if (t.str[4] != '#') return .h4; + if (t.str[5] != '#') return .h5; + if (t.str[6] != '#') return .h6; + @panic("headers nested too deep"); + }, + '>' => { + _ = t.advance(1); + return .blockquote; + }, + '|' => { + return .table; + }, + '`' => { + if (mem.startsWith(u8, t.str, "```")) { + defer _ = t.advanceLine(); + if (mem.startsWith(u8, t.str, "```lang:run")) + return .code_run; + if (mem.startsWith(u8, t.str, "```lang:build")) + return .code_build; + if (mem.startsWith(u8, t.str, "```lang")) + return .code; + return .code_generic; + } + return .para; + }, + '-' => { + return .ul; + }, + else => { + return .para; + }, + } + } +}; + +const HeaderWriter = struct { + data: std.ArrayListUnmanaged(u8) = .{}, + level: u8 = 0, + h1: ?[]const u8 = null, + + numbers: [5]u32 = .{ 0, 0, 0, 0, 0 }, + + has_closed_li: bool = true, + + fn add(header: *HeaderWriter, alloc: Allocator, name: []const u8, id: []const u8, level: u8) !void { + const w = header.data.writer(alloc); + + if (header.level == level) { + try w.writeAll(""); + } + + while (header.level > level) { + header.level -= 1; + header.numbers[header.level] = 0; + try w.writeAll(""); + } + + while (header.level < level) { + if (header.level == 0) { + try w.writeAll(""); + } + } +}; + +const CodeExample = struct { + filename: []const u8, + source: union(enum) { tokenized: []const u8, raw: []const u8 }, + terminal: ?[]const u8 = null, + javascript: ?[]const u8 = null, + + const copy_button = NaiveHtmlMinifier.minifyComptime( + \\ + ); + + pub fn write(code: CodeExample, w: std.io.AnyWriter) !void { + try w.writeAll("
"); + try w.print("
{}
", .{fmtHtmlEscape(code.filename, false)}); + try w.writeAll("
");
+        try w.writeAll(copy_button);
+        try w.writeAll("");
+        switch (code.source) {
+            .tokenized => |str| try w.writeAll(str),
+            .raw => |str| try writeHtmlEscapedSlice(str, w, false),
+        }
+        try w.writeAll("
"); + + if (code.terminal) |terminal| { + try w.writeAll("
"); + if (code.javascript != null) { + try w.writeAll("
"); + try w.writeAll(""); + try w.writeAll(""); + try w.writeAll("
"); + } else { + // ??? + } + + try w.writeAll("
"); + try w.writeAll("
");
+            try writeAnsi(terminal, w);
+            try w.writeAll("
"); + try w.writeAll("
"); + + if (code.javascript) |js| { + try w.writeAll("
"); + try w.print("
{}
", .{fmtHtmlEscape(js, false)}); + try w.writeAll("
"); + } + + try w.writeAll("
"); + } else { + assert(code.javascript == null); + } + try w.writeAll("
"); + } +}; + +fn processMarkdown( + gpa: Allocator, + progress: std.Progress.Node, + src: []const u8, + compiler_path: []const u8, + runtime_path: []const u8, + header: *HeaderWriter, + writer: anytype, +) !void { + var t = MarkdownTokenizer{ .str = src }; + + t.skipWhitespace(); + while (t.str.len > 0) : (t.skipWhitespace()) { + switch (t.blockType()) { + .para => { + try writer.writeAll("

"); + + var para = ArrayList(u8).init(gpa); + try para.appendSlice(t.advanceLine()); + + var next_line = t.advanceLine(); + while (t.str.len > 0 and next_line.len > 0) { + try para.append(' '); + try para.appendSlice(next_line); + next_line = t.advanceLine(); + } + + try processInlineMarkdown(gpa, para.items, writer); + + try writer.writeAll("

"); + }, + .h1, + .h2, + .h3, + .h4, + .h5, + .h6, + => |tag| { + const html_tag = @tagName(tag); + const name = t.advanceLine(); + const id = try nameToHeaderId(gpa, name); + + if (tag != .h1) { + try writer.print("<{0s} id=\"{2s}\">{1s}", .{ + html_tag, + name, + id, + }); + + if (tag != .h1) + try header.add(gpa, name, id, switch (tag) { + .h2 => 1, + .h3 => 2, + .h4 => 3, + .h5 => 4, + .h6 => 5, + else => unreachable, + }); + } else { + assert(header.h1 == null); + + header.h1 = try std.fmt.allocPrint( + gpa, + "{s}", + .{name}, + ); + + try writer.print("<{0s}>{1s}", .{ + html_tag, + name, + }); + } + }, + .code_run, + .code_build, + .code, + .code_generic, + => |tag| { + var code = ArrayList(u8).init(gpa); + + var next_line = t.advanceLineRaw(); + while (t.str.len > 0 and !mem.startsWith(u8, next_line, "```")) { + try code.appendSlice(next_line); + next_line = t.advanceLineRaw(); + } + + var example: CodeExample = .{ + .filename = "example.wl", + .source = .{ .raw = code.items }, + }; + + if (tag != .code_generic) { + var tmp = ArrayList(u8).init(gpa); + try writeTokenizedCode(gpa, code.items, tmp.writer()); + example.source = .{ .tokenized = tmp.items }; + } + + try code.append('\n'); + + if (tag == .code_run or tag == .code_build) { + var prog = progress.start("Code Snippet", if (tag == .code_run) 2 else 1); + defer prog.end(); + + var prog_step = prog.start("Compile", 1); + var build = std.process.Child.init(&.{ compiler_path, "build", "/dev/stdin" }, gpa); + build.stdin_behavior = .Pipe; + build.stdout_behavior = .Pipe; + build.stderr_behavior = .Pipe; + var env_map = try std.process.getEnvMap(gpa); + defer env_map.deinit(); + try env_map.put("YES_COLOR", "1"); + build.env_map = &env_map; + try build.spawn(); + try build.stdin.?.writeAll(code.items); + var out = ArrayList(u8).init(gpa); + var err = ArrayList(u8).init(gpa); + + try build.collectOutput(&out, &err, std.math.maxInt(usize)); + const result = try build.wait(); + if (result != .Exited or result.Exited != 0) { + try err.writer().print("[Process {s} with {d}]", .{ + switch (result) { + .Exited => "exited", + .Stopped => "stopped", + .Signal => "signaled", + .Unknown => "hit unknown state", + }, + switch (result) { + inline else => |d| @as(u32, d), + }, + }); + } + example.terminal = try mem.concat(gpa, u8, &.{ + "$ ", + switch (tag) { + .code_run => run_cli, + .code_build => build_cli, + else => unreachable, + }, + " ", + example.filename, + "\n", + mem.trim(u8, err.items, " \n\r"), + }); + const trimmed_out = mem.trim(u8, out.items, " \n\r"); + if (trimmed_out.len > 0) { + example.javascript = try std.mem.concat(gpa, u8, &.{ + "// Compiled JavaScript Code", + trimmed_out[mem.indexOfScalar(u8, trimmed_out, '\n').?..], + }); + } + + prog.completeOne(); + prog_step.end(); + + if (tag == .code_run and runtime_path.len > 0 and trimmed_out.len > 0) { + const run_step = prog.start("Execute", 0); + defer run_step.end(); + + var temp_dir = std.testing.tmpDir(.{}); + defer temp_dir.cleanup(); + + const input_sub_path = "result.js"; + try temp_dir.dir.writeFile(.{ + .sub_path = input_sub_path, + .data = example.javascript.?, + }); + + var env_map_run = try std.process.getEnvMap(gpa); + defer env_map_run.deinit(); + try env_map_run.put("NO_COLOR", "1"); + try env_map_run.put("TERM", "dumb"); + + var run_out = ArrayList(u8).init(gpa); + var run_err = ArrayList(u8).init(gpa); + + const run_sub_command = std.mem.indexOf(u8, runtime_path, "deno") != null or + std.mem.indexOf(u8, runtime_path, "porf") != null; + + const cmd: []const []const u8 = if (run_sub_command) + &.{ runtime_path, "run", input_sub_path } + else + &.{ runtime_path, input_sub_path }; + + var run = std.process.Child.init(cmd, gpa); + + run.cwd_dir = temp_dir.dir; + var path_buf: [std.fs.max_path_bytes]u8 = undefined; + if (@import("builtin").os.tag == .windows) + run.cwd = try std.os.getFdPath(temp_dir.dir.fd, &path_buf); + + run.stdin_behavior = .Ignore; + run.stdout_behavior = .Pipe; + run.stderr_behavior = .Pipe; + + run.env_map = &env_map_run; + try run.spawn(); + + try run.collectOutput(&run_out, &run_err, std.math.maxInt(usize)); + const run_result = run.wait() catch |e| { + if (e == error.FileNotFound) { + std.debug.panic( + \\Missing JavaScript evaluator: {s} + \\Specify another one using -Djs_runtime=... or set to "" to skip evaluation. + \\ + , .{runtime_path}); + } + return e; + }; + + if (run_result != .Exited or run_result.Exited != 0) { + try run_err.writer().print("[Process {s} with {d}]", .{ + switch (run_result) { + .Exited => "exited", + .Stopped => "stopped", + .Signal => "signaled", + .Unknown => "hit unknown state", + }, + switch (run_result) { + inline else => |d| @as(u32, d), + }, + }); + } + + example.terminal = try std.mem.concat(gpa, u8, &.{ + example.terminal.?, + run_out.items, + run_err.items, + }); + } + } + + try example.write(writer.any()); + }, + .ul => { + try writer.writeAll(""); + }, + .table => { + try writer.writeAll(""); + { + try writer.writeAll(""); + var it = mem.tokenizeScalar(u8, t.advanceLine()[1..], '|'); + while (it.next()) |tok| { + try writer.writeAll(""); + } + try writer.writeAll(""); + } + _ = t.advanceLine(); + t.skipWhitespace(); + while (t.str.len > 0 and t.str[0] == '|') { + try writer.writeAll(""); + var it = mem.tokenizeScalar(u8, t.advanceLine()[1..], '|'); + while (it.next()) |tok| { + try writer.writeAll(""); + } + try writer.writeAll(""); + } + try writer.writeAll("
"); + try processInlineMarkdown(gpa, tok, writer); + try writer.writeAll("
"); + try processInlineMarkdown(gpa, tok, writer); + try writer.writeAll("
"); + }, + .blockquote => { + try writer.writeAll("
"); + + var para = ArrayList(u8).init(gpa); + try para.appendSlice(t.advanceLine()); + + var next_line = t.advanceLine(); + while (t.str.len > 0 and next_line.len > 0) { + try para.append(' '); + try para.appendSlice(next_line); + next_line = t.advanceLine(); + } + + try processInlineMarkdown(gpa, para.items, writer); + + try writer.writeAll("
"); + }, + } + } +} + +pub fn nextInlineToken(src: []const u8) ?struct { InlineType, usize } { + var pos: usize = 0; + while (true) : (pos += 1) { + pos = mem.indexOfAnyPos(u8, src, pos, "*`[") orelse return null; + + switch (src[pos]) { + '*' => switch (src[pos + 1]) { + '*' => return .{ .bold, pos }, + else => continue, + }, + '`' => return .{ .code, pos }, + '[' => return .{ .link, pos }, + else => unreachable, + } + } +} + +fn processInlineMarkdown(gpa: Allocator, src: []const u8, writer: anytype) !void { + var bold = false; + var remain = src; + + while (nextInlineToken(remain)) |tok| { + try writer.writeAll(remain[0..tok[1]]); + remain = remain[tok[1]..]; + switch (tok[0]) { + .bold => { + bold = !bold; + try writer.writeAll(switch (bold) { + true => "", + false => "", + }); + remain = remain[2..]; + }, + .code => { + const end = mem.indexOfScalarPos(u8, remain, 1, '`') orelse @panic("invalid code block"); + const code = remain[1..end]; + try writer.writeAll(""); + try writeTokenizedCode(gpa, code, writer); + try writer.writeAll(""); + remain = remain[end + 1 ..]; + }, + .link => { + const label_end = mem.indexOfScalarPos(u8, remain, 1, ']') orelse @panic("invalid link block"); + assert(remain[label_end + 1] == '('); // invalid link. '[' syntax is not implemented + + const href_end = mem.indexOfScalarPos(u8, remain, label_end + 2, ')') orelse @panic("invalid link block"); + + const label = remain[1..label_end]; + const href = remain[label_end + 2 .. href_end]; + + try writer.writeAll(""); + try processInlineMarkdown(gpa, label, writer); + try writer.writeAll(""); + remain = remain[href_end + 1 ..]; + }, + } + } + try writer.writeAll(remain); + + if (bold) { + std.debug.panic("Unterminated Bold in '{s}'", .{src}); + } +} + +// TODO: Change this to use Tree-sitter, which would also make it very simple +// to highlight JavaScript code in addition to the language. +fn writeTokenizedCode(gpa: Allocator, code: []const u8, writer: anytype) !void { + const code_z = try gpa.dupeZ(u8, code); + defer gpa.free(code_z); + + var t = core.Tokenizer{ .code = code_z }; + + t.skipWhitespace(); + var last: usize = 0; + while (true) { + t.skipWhitespace(); + const tok = t.next() orelse break; + try writeHtmlEscapedSlice(t.code[last..tok.offset], writer, false); + try writer.print("{s}", .{ + @tagName(tok.tag), + if (tok.tag.isKeyword()) " kw" else "", + t.code[tok.offset..t.pos], + }); + last = t.pos; + } + try writeHtmlEscapedSlice(t.code[last..], writer, false); +} + +fn writeAnsi(code: []const u8, writer: anytype) !void { + var remain = code; + var count: usize = 0; + while (mem.indexOfScalar(u8, remain, '\x1b')) |esc_i| { + try writeHtmlEscapedSlice(remain[0..esc_i], writer, false); + switch (remain[esc_i + 1]) { + '[' => switch (remain[esc_i + 2]) { + '0'...'9' => { + const end = mem.indexOfScalarPos(u8, remain, esc_i + 2, 'm').?; + const int = std.fmt.parseInt(u8, remain[esc_i + 2 .. end], 10) catch unreachable; + switch (int) { + else => { + try writer.print("", .{int}); + count += 1; + }, + 0 => { + for (0..count) |_| { + try writer.writeAll(""); + } + count = 0; + }, + } + remain = remain[end + 1 ..]; + }, + else => |x| std.debug.panic("Unknown esc + 2: {d}", .{x}), + }, + else => |x| std.debug.panic("Unknown esc + 1: {d}", .{x}), + } + } + try writeHtmlEscapedSlice(remain, writer, false); +} + +fn nameToHeaderId(alloc: Allocator, input: []const u8) ![]const u8 { + var out = try ArrayList(u8).initCapacity(alloc, input.len); + + var is_split = false; + for (input) |char| { + switch (char) { + 'a'...'z', 'A'...'Z', '0'...'9' => { + if (is_split) { + out.appendAssumeCapacity('-'); + is_split = false; + } + out.appendAssumeCapacity(std.ascii.toLower(char)); + }, + else => { + is_split = true; + }, + } + } + + return try out.toOwnedSlice(); +} + +fn replaceInTemplate(alloc: Allocator, template: []const u8, replacements: anytype) ![]const u8 { + const Replacements = @TypeOf(replacements); + const Map = std.StringArrayHashMapUnmanaged([]const u8); + + var map: Map = .{}; + inline for (@typeInfo(Replacements).@"struct".fields) |field| { + try map.put(alloc, field.name, @field(replacements, field.name)); + } + + var out: ArrayListUnmanaged(u8) = .{}; + const w = out.writer(alloc); + + var i: usize = 0; + while (mem.indexOfScalarPos(u8, template, i, '{')) |open_at| { + const end = mem.indexOfScalarPos(u8, template, open_at + 1, '}') orelse break; + const inner = template[open_at + 1 .. end]; + + if (map.get(inner)) |replacement| { + // write previous + try w.writeAll(template[i..open_at]); + // write template + try w.writeAll(replacement); + // move one past the ending '}' + i = end + 1; + } else { + // write up to and including the { + try w.writeAll(template[i .. open_at + 1]); + // move one past it + i = open_at + 1; + } + } + + // write ending + try w.writeAll(template[i..]); + + return out.items; +} + +fn writeHtmlEscapedSlice(text: []const u8, writer: anytype, escape_quotes: bool) !void { + var i: usize = 0; + while (mem.indexOfAnyPos( + u8, + text, + i, + if (escape_quotes) + &.{ '&', '<', '>', '"' } + else + &.{ '&', '<', '>' }, + )) |at| { + const replace = switch (text[at]) { + '&' => "&", + '<' => "<", + '>' => ">", + '"' => """, + else => unreachable, + }; + try writer.writeAll(text[i..at]); + try writer.writeAll(replace); + i = at + 1; + } + try writer.writeAll(text[i..]); +} + +fn fmtHtmlEscape(text: []const u8, escape_quotes: bool) std.fmt.Formatter(fmtHtmlEscapeImpl) { + return .{ .data = .{ .text = text, .escape_quotes = escape_quotes } }; +} + +fn fmtHtmlEscapeImpl(data: struct { text: []const u8, escape_quotes: bool }, _: []const u8, _: std.fmt.FormatOptions, w: std.io.AnyWriter) !void { + try writeHtmlEscapedSlice(data.text, w, data.escape_quotes); +} diff --git a/src/manual/template.html b/src/manual/template.html index a50003ed0cb3b3bd4c3aad9dbf685e7f22ddf201..39cc4cd5d1a8825a55c0808cc9d02b4c5334c972 100644 --- a/src/manual/template.html +++ b/src/manual/template.html @@ -1,134 +1,689 @@ - + - - - - Language Reference - - - -
{contents}
- + + + + Ix Language Manual + + + + + + + + +
+ {contents} +
+ +