diff --git a/.github/workflows/extension-ci.yml b/.github/workflows/extension-ci.yml new file mode 100644 index 0000000..a4b123c --- /dev/null +++ b/.github/workflows/extension-ci.yml @@ -0,0 +1,75 @@ +name: Extension CI + +on: + pull_request: + branches: [master, hdoc-2.0] + push: + branches: [master, hdoc-2.0] + +jobs: + build-and-package: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Zig + uses: mlugg/setup-zig@v2 + with: + version: 0.15.2 + + - name: Build Zig artifacts + run: zig build install + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install extension dependencies + run: npm ci + working-directory: vscode-ext + + - name: Copy wasm artifacts into extension bundle + run: | + mkdir -p vscode-ext/wasm + cp zig-out/www/hyperdoc_wasm.wasm vscode-ext/wasm/ || true + cp zig-out/www/hyperdoc_wasm_lsp.wasm vscode-ext/wasm/ + + - name: Build extension + run: npm run compile + working-directory: vscode-ext + + - name: Test extension + run: npm test + working-directory: vscode-ext + + - name: Package extension + run: | + npm run package + mv *.vsix hyperdoc-vscode.vsix + working-directory: vscode-ext + + - name: Upload packaged extension + uses: actions/upload-artifact@v4 + with: + name: hyperdoc-vscode.vsix + path: vscode-ext/hyperdoc-vscode.vsix + + publish: + needs: build-and-package + if: github.event_name == 'push' && github.ref == 'refs/heads/hdoc-2.0' + runs-on: ubuntu-latest + environment: + name: vscode-marketplace + steps: + - name: Download packaged extension + uses: actions/download-artifact@v4 + with: + name: hyperdoc-vscode.vsix + path: artifacts + + - name: Publish to VS Code Marketplace + run: npx --yes vsce publish --packagePath artifacts/hyperdoc-vscode.vsix -p "$VSCE_PAT" + env: + VSCE_PAT: ${{ secrets.VSCE_PAT }} diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..7704611 --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,42 @@ +name: Pages + +on: + push: + branches: [hdoc-2.0] + +jobs: + build: + runs-on: ubuntu-latest + # Grant GITHUB_TOKEN the permissions required to make a Pages deployment + permissions: + pages: write # to deploy to Pages + id-token: write # to verify the deployment originates from an appropriate source + environment: + name: github-pages + url: ${{steps.deployment.outputs.page_url}} + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup Zig + uses: mlugg/setup-zig@v2 + with: + version: 0.15.2 + + - name: Build + run: | + zig build install + + - name: Test + run: | + zig build test + + - name: Upload static files as artifact + id: pages-upload + uses: actions/upload-pages-artifact@v3 + with: + path: zig-out/www + + - name: Deploy artifact + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index f8d28d6..9f2c982 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -2,9 +2,9 @@ name: Build on: pull_request: - branches: [master] + branches: [master, hdoc-2.0] push: - branches: [master] + branches: [master, hdoc-2.0] jobs: build: @@ -16,8 +16,12 @@ jobs: - name: Setup Zig uses: mlugg/setup-zig@v2 with: - version: 0.15.1 + version: 0.15.2 - name: Build run: | - zig build + zig build install + + - name: Test + run: | + zig build test diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..f10cdca --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,30 @@ +# AGENTS + +## General guidelines + +- Keep changes focused and incremental; prefer small, reviewable commits. +- Follow existing code style and formatting conventions. +- Use `zig fmt` on Zig source files after edits. +- Ensure new tests are added or updated when behavior changes. +- Run relevant tests (`zig build test`) when making code changes. +- Run `zig build` to validate the main application still compiles +- Test `./zig-out/bin/hyperdoc` with the `.hdoc` files in `examples/` and `test/`. +- Avoid editing documentation unless the request explicitly asks for it. +- `src/hyperdoc.zig` must not contain locale- or rendering-specific parts. +- Treat `docs/specification.md` as the authoritative source of behavior; examples may be outdated or incorrect. +- If the spec is unclear or conflicts with code/tests, ask before changing behavior. +- Do not implement "just make it work" fallbacks that alter semantics to satisfy examples. +- Diagnostics must not store dynamic strings (e.g., slices to parsed source). Keep diagnostic payloads POD/small and avoid holding arena-backed text. +- Do not hide crashes by removing safety checks or switching off DebugAllocator; fix the root cause instead. A signal 6 from DebugAllocator indicates memory corruption or a similar misuse. + +## Zig Programming Style + +- Do not use "inline functions" like `const func = struct { fn func(…) {} }.func;` +- Zig has no methods. Functions used by "method like" functions can still be placed next to them, no need to put them into global scope nor into local scope. + +## Snapshot Files + +- If you add a `hdoc` file to `test/snapshot`, also: + - Generate the corresponding html and yaml file + - Add the file inside build.zig to the snapshot_files global +- If you change behaviour, the snapshot tests will fail. Validate the failure against your expectations and see if you broke something unexpected. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..637c23a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 HyperDoc Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 1755e29..08847f7 100644 --- a/README.md +++ b/README.md @@ -1,62 +1,33 @@ # Ashet HyperDocument Format -This format is used for both the _Hyper Wiki_ as well as the _Gateway_ application to store and display -hyperlinked documents. - -The format is a rich-text format that can encode/store/display the following document blocks: - -- paragraphs (consisting of a sequence of spans) - - regular text - - links - - bold/emphasised text - - monospaced text - - line break -- 3 levels of headings -- ordered and unordered lists - - each list item is a paragraph or another list -- quotes (paragraph with special styling) -- preformatted text (code blocks, also uses the paragraph formatting) -- images - -Regular text is assumed to use a proportional font, while preformatted text is required to be rendered as monospace. - -## Storage - -HyperDocument is stored as a trivial-to-parse plain text format, not necessarily meant to be edited by humans, -but still human readable. - -**Example:** - -```lua -hdoc "1.0" -p { - span "Hello, World!\n" - link "http://google.com" "Visit Google!" - span "\n" - emph "This is fat!" - span "\n" - mono "int main()" - span "\n" -} -enumerate { - item { p { span "first" } } - item { p { span "second" } } - item { p { span "third" } } -} -itemize { - item { p { span "first" } } - item { p { span "second" } } - item { p { span "third" } } -} -quote { - span "Life is what happens when you're busy making other plans.\n - John Lennon" -} -pre { - span "const std = @import(\"std\");\n" - span "\n" - span "pub fn main() !void {\n" - span " std.debug.print(\"Hello, World!\\n\", .{});\n" - span "}\n" -} -image "dog.png" +## Motivation + +> TODO: Write motivation + +## Specification + +[Read the specification](docs/specification.md). + +## Building + +Requires [Zig 0.15.2](https://ziglang.org/) installed. + +### Build debug application + +```sh-session +[user@host] hyperdoc$ zig build ``` + +### Build release application + +```sh-session +[user@host] hyperdoc$ zig build -Drelease +``` + +### Run test suite + +```sh-session +[user@host] hyperdoc$ zig build test +``` + +> Optional: installing Node.js enables the WASM integration tests that exercise the compiled `hyperdoc_wasm.wasm` via `node test/wasm/validate.js`. diff --git a/SPEC_TODO.md b/SPEC_TODO.md new file mode 100644 index 0000000..1e879a9 --- /dev/null +++ b/SPEC_TODO.md @@ -0,0 +1,6 @@ +# Spec compliance TODOs + +- Title/header interplay lacks the required comparison. + - Expect: When both `hdoc(title=...)` and `title { ... }` are present, their plaintext forms are compared and a redundancy hint is emitted if they match (§8.1). + - Actual: The block title is used and the header title is ignored without any comparison or diagnostics. + - Proposed: Compare the plaintext values, warn when redundant, and keep emitting hints when neither title form is present. diff --git a/build.zig b/build.zig index a6f8daa..3c33a0f 100644 --- a/build.zig +++ b/build.zig @@ -1,28 +1,54 @@ const std = @import("std"); +const snapshot_files: []const []const u8 = &.{ + "test/snapshot/admonition_blocks.hdoc", + "test/snapshot/document_header.hdoc", + "test/snapshot/media_and_toc.hdoc", + "test/snapshot/nesting_and_inlines.hdoc", + "test/snapshot/paragraph_styles.hdoc", + "test/snapshot/tables.hdoc", + "test/snapshot/footnotes.hdoc", +}; + +const conformance_accept_files: []const []const u8 = &.{ + "test/conformance/accept/header_and_title_order.hdoc", + "test/conformance/accept/image_with_required_path.hdoc", + "test/conformance/accept/inline_escape.hdoc", + "test/conformance/accept/no_title_document.hdoc", + "test/conformance/accept/title_header_redundant.hdoc", +}; + +const conformance_reject_files: []const []const u8 = &.{ + "test/conformance/reject/container_children.hdoc", + "test/conformance/reject/duplicate_header.hdoc", + "test/conformance/reject/hdoc_body_non_empty.hdoc", + "test/conformance/reject/heading_sequence.hdoc", + "test/conformance/reject/image_missing_path.hdoc", + "test/conformance/reject/inline_identifier_dash.hdoc", + "test/conformance/reject/missing_header.hdoc", + "test/conformance/reject/nested_top_level.hdoc", + "test/conformance/reject/time_relative_fmt.hdoc", + "test/conformance/reject/ref_in_heading.hdoc", + "test/conformance/reject/string_cr_escape.hdoc", + "test/conformance/reject/title_after_content.hdoc", +}; + +const www_dir: std.Build.InstallDir = .{ .custom = "www" }; + pub fn build(b: *std.Build) void { // Options: - const target = b.standardTargetOptions(.{}); - const optimize = b.standardOptimizeOption(.{}); + const optimize = b.standardOptimizeOption(.{ .preferred_optimize_mode = .ReleaseSafe }); // Targets: - const run_step = b.step("run", "Run the app"); const test_step = b.step("test", "Run unit tests"); + const wasm_target = b.resolveTargetQuery(.{ .cpu_arch = .wasm32, .os_tag = .freestanding }); // Build: - - const pt_dep = b.dependency("parser_toolkit", .{}); - const args = b.dependency("args", .{}); - - const hyperdoc = b.addModule( - "hyperdoc", - .{ - .root_source_file = b.path("src/hyperdoc.zig"), - }, - ); - hyperdoc.addImport("parser-toolkit", pt_dep.module("parser-toolkit")); + const hyperdoc = b.addModule("hyperdoc", .{ + .root_source_file = b.path("src/hyperdoc.zig"), + }); const exe = b.addExecutable(.{ .name = "hyperdoc", @@ -30,14 +56,62 @@ pub fn build(b: *std.Build) void { .root_source_file = b.path("src/main.zig"), .target = target, .optimize = optimize, + .imports = &.{ + .{ .name = "hyperdoc", .module = hyperdoc }, + }, + }), + }); + b.installArtifact(exe); + + const wasm_exe = b.addExecutable(.{ + .name = "hyperdoc_wasm", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/wasm.zig"), + .target = wasm_target, + .optimize = optimize, + .single_threaded = true, + .imports = &.{ + .{ .name = "hyperdoc", .module = hyperdoc }, + }, }), - .use_llvm = true, }); + wasm_exe.root_module.export_symbol_names = comptime &.{ + "hdoc_set_document_len", + "hdoc_document_ptr", + "hdoc_process", + "hdoc_html_ptr", + "hdoc_html_len", + "hdoc_diagnostic_count", + "hdoc_diagnostic_line", + "hdoc_diagnostic_column", + "hdoc_diagnostic_fatal", + "hdoc_diagnostic_message_ptr", + "hdoc_diagnostic_message_len", + }; + const install_wasm = b.addInstallArtifact(wasm_exe, .{ + .dest_dir = .{ .override = www_dir }, + }); + b.getInstallStep().dependOn(&install_wasm.step); - exe.root_module.addImport("hyperdoc", hyperdoc); - exe.root_module.addImport("args", args.module("args")); + const wasm_lsp_exe = b.addExecutable(.{ + .name = "hyperdoc_wasm_lsp", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/wasm-lsp.zig"), + .target = wasm_target, + .optimize = optimize, + .single_threaded = true, + .imports = &.{ + .{ .name = "hyperdoc", .module = hyperdoc }, + }, + }), + }); + const install_wasm_lsp = b.addInstallArtifact(wasm_lsp_exe, .{ + .dest_dir = .{ .override = www_dir }, + }); + b.getInstallStep().dependOn(&install_wasm_lsp.step); - b.installArtifact(exe); + const install_web = b.addInstallFileWithDir(b.path("src/playground.html"), www_dir, "index.html"); + b.getInstallStep().dependOn(&install_web.step); const run_cmd = b.addRunArtifact(exe); run_cmd.step.dependOn(b.getInstallStep()); @@ -47,16 +121,116 @@ pub fn build(b: *std.Build) void { run_step.dependOn(&run_cmd.step); + const snapshot_diff = b.addExecutable(.{ + .name = "diff", + .root_module = b.createModule(.{ + .root_source_file = b.path("test/compare.zig"), + .target = b.graph.host, + .optimize = .Debug, + }), + }); + + // Snapshot tests: + for (snapshot_files) |path| { + std.debug.assert(std.mem.endsWith(u8, path, ".hdoc")); + const html_file = b.fmt("{s}.html", .{path[0 .. path.len - 5]}); + const yaml_file = b.fmt("{s}.yaml", .{path[0 .. path.len - 5]}); + + for (&[2][]const u8{ html_file, yaml_file }) |snapshot_file| { + const test_run = b.addRunArtifact(exe); + test_run.addArgs(&.{ "--format", snapshot_file[snapshot_file.len - 4 ..] }); + test_run.addFileArg(b.path(path)); + const generated_file = test_run.captureStdOut(); + + const compare_run = b.addRunArtifact(snapshot_diff); + compare_run.addFileArg(b.path(snapshot_file)); + compare_run.addFileArg(generated_file); + + test_step.dependOn(&compare_run.step); + } + } + + // Conformance snapshots: accept cases (YAML only): + for (conformance_accept_files) |path| { + std.debug.assert(std.mem.endsWith(u8, path, ".hdoc")); + const yaml_file = b.fmt("{s}.yaml", .{path[0 .. path.len - 5]}); + + const test_run = b.addRunArtifact(exe); + test_run.addArgs(&.{ "--format", "yaml" }); + test_run.addFileArg(b.path(path)); + const generated_file = test_run.captureStdOut(); + + const compare_run = b.addRunArtifact(snapshot_diff); + compare_run.addFileArg(b.path(yaml_file)); + compare_run.addFileArg(generated_file); + + test_step.dependOn(&compare_run.step); + } + + // Conformance snapshots: reject cases (diagnostics on stderr, expect exit code 1): + for (conformance_reject_files) |path| { + std.debug.assert(std.mem.endsWith(u8, path, ".hdoc")); + const diag_file = b.fmt("{s}.diag", .{path[0 .. path.len - 5]}); + + const test_run = b.addRunArtifact(exe); + test_run.addArgs(&.{"--json-diagnostics"}); + test_run.addFileArg(b.path(path)); + test_run.expectExitCode(1); + const generated_diag = test_run.captureStdErr(); + + const compare_run = b.addRunArtifact(snapshot_diff); + compare_run.addFileArg(b.path(diag_file)); + compare_run.addFileArg(generated_diag); + + test_step.dependOn(&compare_run.step); + } + + // Unit tests: const exe_tests = b.addTest(.{ .root_module = b.createModule(.{ .root_source_file = b.path("src/testsuite.zig"), .target = target, .optimize = optimize, + .imports = &.{ + rawFileMod(b, "examples/tables.hdoc"), + rawFileMod(b, "examples/featurematrix.hdoc"), + rawFileMod(b, "examples/demo.hdoc"), + rawFileMod(b, "examples/guide.hdoc"), + rawFileMod(b, "test/accept/stress.hdoc"), + }, }), .use_llvm = true, }); + test_step.dependOn(&b.addRunArtifact(exe_tests).step); - exe_tests.root_module.addImport("hyperdoc", hyperdoc); + const main_tests = b.addTest(.{ + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + .imports = &.{ + .{ .name = "hyperdoc", .module = hyperdoc }, + }, + }), + .use_llvm = true, + }); + test_step.dependOn(&b.addRunArtifact(main_tests).step); - test_step.dependOn(&b.addRunArtifact(exe_tests).step); + const node_path = b.findProgram(&.{"node"}, &.{}) catch null; + if (node_path) |node| { + const wasm_validate = b.addSystemCommand(&.{ node, "test/wasm/validate.js" }); + wasm_validate.step.dependOn(&install_wasm.step); + test_step.dependOn(&wasm_validate.step); + } else { + std.debug.print("node not found; skipping WASM integration tests\n", .{}); + } +} + +fn rawFileMod(b: *std.Build, path: []const u8) std.Build.Module.Import { + return .{ + .name = path, + .module = b.createModule(.{ + .root_source_file = b.path(path), + }), + }; } diff --git a/build.zig.zon b/build.zig.zon index 9b78c87..e098508 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -2,16 +2,17 @@ .name = .hyperdoc, .version = "0.1.0", .fingerprint = 0xfd1a4802abc4739e, + .minimum_zig_version = "0.15.0", .dependencies = .{ - .parser_toolkit = .{ - .url = "git+https://github.com/ikskuh/parser-toolkit.git#62e0a3dca3632bb361df59407b2d7805280ab1b9", - .hash = "parser_toolkit-0.1.0-baYGPUVCEwBaVmu09ORh0lLlVjRaJ489TdSIdTa_8VWg", - }, - .args = .{ - .url = "git+https://github.com/ikskuh/zig-args.git#8ae26b44a884ff20dca98ee84c098e8f8e94902f", - .hash = "args-0.0.0-CiLiqojRAACGzDRO7A9dw7kWSchNk29caJZkXuMCb0Cn", - }, + // .parser_toolkit = .{ + // .url = "git+https://github.com/ikskuh/parser-toolkit.git#62e0a3dca3632bb361df59407b2d7805280ab1b9", + // .hash = "parser_toolkit-0.1.0-baYGPUVCEwBaVmu09ORh0lLlVjRaJ489TdSIdTa_8VWg", + // }, + // .args = .{ + // .url = "git+https://github.com/ikskuh/zig-args.git#8ae26b44a884ff20dca98ee84c098e8f8e94902f", + // .hash = "args-0.0.0-CiLiqojRAACGzDRO7A9dw7kWSchNk29caJZkXuMCb0Cn", + // }, }, .paths = .{""}, diff --git a/docs/AGENTS.md b/docs/AGENTS.md new file mode 100644 index 0000000..43026ff --- /dev/null +++ b/docs/AGENTS.md @@ -0,0 +1,16 @@ +# Specification Editing + +## General + +- `specification.md` is the current "status quo" specifiction. Do not edit unless explicitly asked. + - This file contains a chapter `0. Chapter Status`. This chapter marks each other chapter of the file as FROZEN, DONE, DRAFT or MISSING + - If a chapter is marked FROZEN, you are not permitted to change anything in it. + - If a chapter is marked DONE, you are only permitted to perform language changes, but not semantic changes. + - If a chapter is marked DRAFT, you are permitted to change it's semantic meaning. + - If a chapter is marked MISSING, the chapter does not yet exist and shall be added eventually. You are permitted to do so. + - A block quote starting with `> TODO:` notes some tasks that shall be done. These lines can be removed if, and only if the task was fully completed. + +## Formatting + +- Do not use any dashes except for `-`. Do NOT use En-Dashes (`–`) or Em-Dashes (`—`). +- Stick to ASCII text as good as possible. If you require symbols from the unicode plane, use them, but inform the user about it. diff --git a/docs/TODO.md b/docs/TODO.md new file mode 100644 index 0000000..d005d62 --- /dev/null +++ b/docs/TODO.md @@ -0,0 +1,103 @@ +# Specification TODOs + +## Tasks + +- Assign semantics to node types, paragraph kinds, ... +- Add links to RFCs where possible +- Span attribute semantics are referenced but not fully defined. §8.2 introduces spans with an “attribute set (e.g. emphasis/monospace/link…)” but the spec never fully defines the canonical attribute keys, nesting behavior (e.g., \em inside \mono), or how lang overrides interact at span level. That’s a major interoperability risk because renderers may differ even if parsers agree. +- Refine that `hdoc(title)` is metadata while `title{}` is rendered rich text +- Ambiguity of Inline Unicode: + - Finding: String literals ("...") support \u{...} escapes (§7.2.1). Inline text streams (bodies of p, h1) do not (§6.1 only lists \\, \{, \}). + - Issue: Authors cannot enter invisible characters (like Non-Breaking Space U+00A0 or Zero Width Space U+200B) into a paragraph without pasting the raw invisible character, which is brittle and invisible in editors. +- Issue: "Lexical" implies only regex-level matching. It does not strictly forbid 2023-02-31. For a strict format, "Semantic" validity (Gregorian correctness) should be enforced to prevent invalid metadata. + +## Potential Future Features + +### `hr;` or `break;` + +Purpose: Explicit scene/topic breaks within prose (equivalent to HTML
"); - try renderSpans(writer, content.contents); - try writer.writeAll("
\n"); - }, - - .ordered_list => |content| { - try writer.writeAll(""); - try renderSpans(writer, content.contents); - try writer.writeAll("\n"); - }, - - .preformatted => |content| { - if (!std.mem.eql(u8, content.language, "")) { - try writer.print("
", .{content.language});
- } else {
- try writer.writeAll("");
- }
- try renderSpans(writer, content.contents);
- try writer.writeAll("\n");
- },
- .image => |content| {
- try writer.print("
\n", .{content.path});
- },
- .heading => |content| {
- try writer.writeAll(switch (content.level) {
- .document => " " " 0) {
- try writer.print(" id=\"{s}\"", .{content.anchor});
- }
- try writer.writeAll(">");
-
- try writer.print("{f}", .{escapeHtml(content.title)});
-
- try writer.writeAll(switch (content.level) {
- .document => "
\n",
- .chapter => "\n",
- .section => "\n",
- });
- },
- .table_of_contents => |content| {
- // TODO: Render TOC
- _ = content;
- },
- }
-}
-
-fn renderSpans(
- writer: *std.Io.Writer,
- spans: []const hdoc.Span,
-) WriteError!void {
- for (spans) |span| {
- try renderSpan(writer, span);
- }
-}
-
-fn renderSpan(writer: *std.Io.Writer, span: hdoc.Span) WriteError!void {
- switch (span) {
- .text => |val| {
- try writer.print("{f}", .{escapeHtml(val)});
- },
- .emphasis => |val| {
- try writer.writeAll("");
- try writer.print("{f}", .{escapeHtml(val)});
- try writer.writeAll("");
- },
- .monospace => |val| {
- try writer.writeAll("");
- try writer.print("{f}", .{escapeHtml(val)});
- try writer.writeAll("");
- },
- .link => |val| {
- try writer.print("{f}", .{
- val.href,
- escapeHtml(val.text),
- });
- },
- }
-}
-
-fn escapeHtml(string: []const u8) HtmlEscaper {
- return .{ .string = string };
-}
-
-const HtmlEscaper = struct {
- string: []const u8,
-
- pub fn format(html: HtmlEscaper, writer: *std.Io.Writer) !void {
- for (html.string) |char| {
- switch (char) {
- '&' => try writer.writeAll("&"),
- '<' => try writer.writeAll("<"),
- '>' => try writer.writeAll(">"),
- '\"' => try writer.writeAll("""),
- '\'' => try writer.writeAll("'"),
- '\n' => try writer.writeAll("
"),
- else => try writer.writeByte(char),
- }
- }
- }
-};
diff --git a/src/renderer/HyperDoc.zig b/src/renderer/HyperDoc.zig
deleted file mode 100644
index 5aa508f..0000000
--- a/src/renderer/HyperDoc.zig
+++ /dev/null
@@ -1,158 +0,0 @@
-const std = @import("std");
-const hdoc = @import("hyperdoc");
-
-pub const WriteError = std.Io.Writer.Error;
-
-pub fn render(writer: *std.Io.Writer, document: hdoc.Document) WriteError!void {
- try writer.writeAll("hdoc \"1.0\"\n");
- try renderBlocks(writer, document, document.contents, 0);
-}
-
-fn renderBlocks(
- writer: *std.Io.Writer,
- document: hdoc.Document,
- blocks: []const hdoc.Block,
- indent: usize,
-) WriteError!void {
- for (blocks) |block| {
- try renderBlock(writer, document, block, indent);
- }
-}
-
-fn renderBlock(
- writer: *std.Io.Writer,
- document: hdoc.Document,
- block: hdoc.Block,
- indent: usize,
-) WriteError!void {
- try writer.splatByteAll(' ', 2 * indent);
- switch (block) {
- .paragraph => |content| {
- try writer.writeAll("p {\n");
- try renderSpans(writer, content.contents, indent + 1);
- try writer.splatByteAll(' ', 2 * indent);
- try writer.writeAll("}\n");
- },
-
- .ordered_list => |content| {
- try writer.writeAll("enumerate {\n");
- for (content) |item| {
- try writer.splatByteAll(' ', 2 * indent + 2);
- try writer.writeAll("item {\n");
-
- try renderBlocks(writer, document, item.contents, indent + 2);
-
- try writer.splatByteAll(' ', 2 * indent + 2);
- try writer.writeAll("}\n");
- }
- try writer.splatByteAll(' ', 2 * indent);
- try writer.writeAll("}\n");
- },
-
- .unordered_list => |content| {
- try writer.writeAll("itemize {\n");
- for (content) |item| {
- try writer.splatByteAll(' ', 2 * indent + 2);
- try writer.writeAll("item {\n");
-
- try renderBlocks(writer, document, item.contents, indent + 2);
-
- try writer.splatByteAll(' ', 2 * indent + 2);
- try writer.writeAll("}\n");
- }
- try writer.splatByteAll(' ', 2 * indent);
- try writer.writeAll("}\n");
- },
-
- .quote => |content| {
- try writer.writeAll("quote {\n");
- try renderSpans(writer, content.contents, indent + 1);
- try writer.splatByteAll(' ', 2 * indent);
- try writer.writeAll("}\n");
- },
-
- .preformatted => |content| {
- try writer.print("pre \"{f}\" {{\n", .{
- escape(content.language),
- });
- try renderSpans(writer, content.contents, indent + 1);
- try writer.splatByteAll(' ', 2 * indent);
- try writer.writeAll("}\n");
- },
- .image => |content| {
- try writer.print("image \"{f}\"\n", .{
- escape(content.path),
- });
- },
- .heading => |content| {
- try writer.writeAll(switch (content.level) {
- .document => "h1",
- .chapter => "h2",
- .section => "h3",
- });
- try writer.print(" \"{f}\" \"{f}\"\n", .{
- escape(content.anchor),
- escape(content.title),
- });
- },
- .table_of_contents => {
- try writer.writeAll("toc {}\n");
- },
- }
-}
-
-fn renderSpans(
- writer: *std.Io.Writer,
- spans: []const hdoc.Span,
- indent: usize,
-) WriteError!void {
- for (spans) |span| {
- try renderSpan(writer, span, indent);
- }
-}
-
-fn renderSpan(
- writer: *std.Io.Writer,
- span: hdoc.Span,
- indent: usize,
-) WriteError!void {
- try writer.splatByteAll(' ', 2 * indent);
- switch (span) {
- .text => |val| {
- try writer.print("span \"{f}\"\n", .{escape(val)});
- },
- .emphasis => |val| {
- try writer.print("emph \"{f}\"\n", .{escape(val)});
- },
- .monospace => |val| {
- try writer.print("mono \"{f}\"\n", .{escape(val)});
- },
- .link => |val| {
- try writer.print("link \"{f}\" \"{f}\"\n", .{
- escape(val.href),
- escape(val.text),
- });
- },
- }
-}
-
-fn escape(string: []const u8) HDocEscaper {
- return .{ .string = string };
-}
-
-const HDocEscaper = struct {
- string: []const u8,
-
- pub fn format(html: HDocEscaper, writer: *std.Io.Writer) !void {
- for (html.string) |char| {
- switch (char) {
- '\n' => try writer.writeAll("\\n"),
- '\r' => try writer.writeAll("\\r"),
- '\x1B' => try writer.writeAll("\\e"),
- '\'' => try writer.writeAll("\\\'"),
- '\"' => try writer.writeAll("\\\""),
- else => try writer.writeByte(char),
- }
- }
- }
-};
diff --git a/src/renderer/Markdown.zig b/src/renderer/Markdown.zig
deleted file mode 100644
index e8ba9ab..0000000
--- a/src/renderer/Markdown.zig
+++ /dev/null
@@ -1,131 +0,0 @@
-const std = @import("std");
-const hdoc = @import("hyperdoc");
-
-const WriteError = std.Io.Writer.Error;
-
-pub fn render(writer: *std.Io.Writer, document: hdoc.Document) WriteError!void {
- try renderBlocks(writer, document, document.contents);
-}
-
-fn renderBlocks(
- writer: *std.Io.Writer,
- document: hdoc.Document,
- blocks: []const hdoc.Block,
-) WriteError!void {
- for (blocks) |block| {
- try renderBlock(writer, document, block);
- }
-}
-
-fn renderBlock(
- writer: *std.Io.Writer,
- document: hdoc.Document,
- block: hdoc.Block,
-) WriteError!void {
- switch (block) {
- .paragraph => |content| {
- try renderSpans(writer, content.contents);
- try writer.writeAll("\n\n");
- },
-
- .ordered_list => |content| {
- for (content) |item| {
- try writer.writeAll("- ");
- try renderBlocks(writer, document, item.contents);
- }
- },
-
- .unordered_list => |content| {
- for (content, 1..) |item, index| {
- try writer.print("{}. ", .{index});
- try renderBlocks(writer, document, item.contents);
- }
- },
-
- .quote => |content| {
- try writer.writeAll("> ");
- try renderSpans(writer, content.contents);
- try writer.writeAll("\n\n");
- },
-
- .preformatted => |content| {
- try writer.print("```{s}\n", .{content.language});
- try renderSpans(writer, content.contents);
- try writer.writeAll("```\n\n");
- },
- .image => |content| {
- try writer.print("\n\n", .{content.path});
- },
- .heading => |content| {
- try writer.writeAll(switch (content.level) {
- .document => "# ",
- .chapter => "## ",
- .section => "### ",
- });
- if (content.anchor.len > 0) {
- std.log.warn("anchor not supported in markdown!", .{});
- }
-
- try writer.print("{f}\n\n", .{escapeMd(content.title)});
- },
- .table_of_contents => |content| {
- // TODO: Render TOC
- _ = content;
- },
- }
-}
-
-fn renderSpans(
- writer: *std.Io.Writer,
- spans: []const hdoc.Span,
-) WriteError!void {
- for (spans) |span| {
- try renderSpan(writer, span);
- }
-}
-
-fn renderSpan(writer: *std.Io.Writer, span: hdoc.Span) WriteError!void {
- switch (span) {
- .text => |val| {
- try writer.print("{f}", .{escapeMd(val)});
- },
- .emphasis => |val| {
- try writer.writeAll("**");
- try writer.print("{f}", .{escapeMd(val)});
- try writer.writeAll("**");
- },
- .monospace => |val| {
- try writer.writeAll("`");
- try writer.print("{f}", .{escapeMd(val)});
- try writer.writeAll("`");
- },
- .link => |val| {
- try writer.print("[{f}]({s})", .{
- escapeMd(val.text),
- val.href,
- });
- },
- }
-}
-
-fn escapeMd(string: []const u8) MarkdownEscaper {
- return .{ .string = string };
-}
-
-const MarkdownEscaper = struct {
- string: []const u8,
-
- pub fn format(html: MarkdownEscaper, writer: *std.Io.Writer) !void {
- for (html.string) |char| {
- switch (char) {
- '&' => try writer.writeAll("&"),
- '<' => try writer.writeAll("<"),
- '>' => try writer.writeAll(">"),
- '\"' => try writer.writeAll("""),
- '\'' => try writer.writeAll("'"),
- '\n' => try writer.writeAll(" \n"),
- else => try writer.writeByte(char),
- }
- }
- }
-};
diff --git a/src/testsuite.zig b/src/testsuite.zig
index 4c0d4ac..7d98959 100644
--- a/src/testsuite.zig
+++ b/src/testsuite.zig
@@ -1,246 +1,1251 @@
const std = @import("std");
-const hdoc = @import("hyperdoc");
+const hdoc = @import("./hyperdoc.zig");
-fn testAcceptDocument(document: []const u8) !void {
- var doc = try hdoc.parse(std.testing.allocator, document, null);
- defer doc.deinit();
+// TODO: Write unit test for trailing comma in attribute lists
+// TODO: Write unit test for invalid escape sequence detection when more than 6 (hex) chars are used
+// TODO: Write unit test for invalid version detection (must be 2.0)
+// TODO: Write unit test for duplicate header recognition
+// TODO: Write unit test for clean_utf8_input() passthrough
+// TODO: Write unit test for clean_utf8_input() BOM detection
+// TODO: Write unit test for clean_utf8_input() invalid UTF-8 detection
+// TODO: Write unit test for clean_utf8_input() illegal codepoint detection (bare CR -> error)
+// TODO: Write unit test for clean_utf8_input() illegal codepoint detection (TAB -> warning)
+// TODO: Write unit test for clean_utf8_input() illegal codepoint detection (any other control character -> error)
+
+test "validate examples directory" {
+ try parseDirectoryTree("examples");
}
-test "empty document" {
- try testAcceptDocument(
- \\hdoc "1.0"
- );
+test "validate tests directory" {
+ try parseDirectoryTree("test/accept");
}
-test "invalid document" {
- try std.testing.expectError(error.InvalidFormat, testAcceptDocument(
- \\
- ));
- try std.testing.expectError(error.InvalidFormat, testAcceptDocument(
- \\hdoc
- ));
- try std.testing.expectError(error.InvalidFormat, testAcceptDocument(
- \\hdoc {
- ));
- try std.testing.expectError(error.InvalidFormat, testAcceptDocument(
- \\span
- ));
- try std.testing.expectError(error.InvalidFormat, testAcceptDocument(
- \\blob
- ));
-}
-
-test "invalid version" {
- try std.testing.expectError(error.InvalidFormat, testAcceptDocument(
- \\hdoc 1.0
- ));
- try std.testing.expectError(error.InvalidVersion, testAcceptDocument(
- \\hdoc ""
- ));
- try std.testing.expectError(error.InvalidVersion, testAcceptDocument(
- \\hdoc "1.2"
- ));
-}
-
-test "accept toc" {
- try testAcceptDocument(
- \\hdoc "1.0"
- \\toc {}
- );
+fn parseDirectoryTree(path: []const u8) !void {
+ var dir = try std.fs.cwd().openDir(path, .{ .iterate = true });
+ defer dir.close();
+
+ var walker = try dir.walk(std.testing.allocator);
+ defer walker.deinit();
+
+ var path_buffer: std.array_list.Managed(u8) = .init(std.testing.allocator);
+ defer path_buffer.deinit();
+
+ while (try walker.next()) |entry| {
+ if (entry.kind != .file)
+ continue;
+ if (!std.mem.endsWith(u8, entry.path, ".hdoc"))
+ continue;
+
+ errdefer std.log.err("failed to process \"{f}/{f}\"", .{ std.zig.fmtString(path), std.zig.fmtString(entry.path) });
+
+ const source = try entry.dir.readFileAlloc(std.testing.allocator, entry.basename, 10 * 1024 * 1024);
+ defer std.testing.allocator.free(source);
+
+ path_buffer.clearRetainingCapacity();
+ try path_buffer.appendSlice(path);
+ try path_buffer.append('/');
+ try path_buffer.appendSlice(entry.path);
+
+ try expectParseOk(.{ .file_path = path_buffer.items }, source);
+ }
}
-test "accept multiple blocks" {
- try testAcceptDocument(
- \\hdoc "1.0"
- \\toc {}
- \\toc {}
- \\toc {}
- \\toc {}
- );
+test "parser accept identifier and word tokens" {
+ var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
+ defer arena.deinit();
+
+ var parser: hdoc.Parser = .{
+ .code = "h1 word\\em{test}",
+ .arena = arena.allocator(),
+ .diagnostics = null,
+ };
+
+ const ident = try parser.accept_identifier(.node);
+ try std.testing.expectEqualStrings("h1", ident.text);
+ try std.testing.expectEqual(@as(usize, 0), ident.location.offset);
+ try std.testing.expectEqual(@as(usize, 2), ident.location.length);
+
+ const word = try parser.accept_word();
+ try std.testing.expectEqualStrings("word", word.text);
+ try std.testing.expectEqual(@as(usize, 3), word.location.offset);
+ try std.testing.expectEqual(@as(usize, 4), word.location.length);
+ try std.testing.expectEqual(@as(usize, 7), parser.offset);
}
-test "accept image" {
- try testAcceptDocument(
- \\hdoc "1.0"
- \\image "dog.png"
- );
+test "parser rejects identifiers with invalid start characters" {
+ var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
+ defer arena.deinit();
+
+ var parser: hdoc.Parser = .{
+ .code = "*abc",
+ .arena = arena.allocator(),
+ .diagnostics = null,
+ };
+
+ try std.testing.expectError(error.InvalidCharacter, parser.accept_identifier(.node));
}
-test "accept headers" {
- try testAcceptDocument(
- \\hdoc "1.0"
- \\h1 "" "Empty anchor"
- \\h2 "chapter" "Chapter anchor"
- \\h3 "section" "Section anchor"
- );
+test "parser accept string literals and unescape" {
+ var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
+ defer arena.deinit();
+
+ var parser: hdoc.Parser = .{
+ .code = "\"hello\\\\n\"",
+ .arena = arena.allocator(),
+ .diagnostics = null,
+ };
+
+ const token = try parser.accept_string();
+ try std.testing.expectEqualStrings("\"hello\\\\n\"", token.text);
}
-test "invalid top level items" {
- try std.testing.expectError(error.InvalidTopLevelItem, testAcceptDocument(
- \\hdoc "1.0"
- \\span
- ));
- try std.testing.expectError(error.InvalidTopLevelItem, testAcceptDocument(
- \\hdoc "1.0"
- \\link
- ));
- try std.testing.expectError(error.InvalidTopLevelItem, testAcceptDocument(
- \\hdoc "1.0"
- \\emph
- ));
- try std.testing.expectError(error.InvalidTopLevelItem, testAcceptDocument(
- \\hdoc "1.0"
- \\mono
- ));
-}
-
-test "empty ordered lists" {
- try testAcceptDocument(
- \\hdoc "1.0"
- \\enumerate {}
- );
+test "semantic analyzer unescapes string literals" {
+ var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
+ defer arena.deinit();
+
+ const source = "\"line\\\\break\\nquote \\\" unicode \\u{1F600}\"";
+
+ var diagnostics: hdoc.Diagnostics = .init(std.testing.allocator);
+ defer diagnostics.deinit();
+
+ var sema: hdoc.SemanticAnalyzer = .{
+ .arena = arena.allocator(),
+ .diagnostics = &diagnostics,
+ .code = source,
+ };
+
+ const token: hdoc.Parser.Token = .{ .text = source, .location = .{ .offset = 0, .length = source.len } };
+
+ const text = try sema.unescape_string(token);
+ try std.testing.expectEqualStrings("line\\break\nquote \" unicode 😀", text);
+ try std.testing.expect(!diagnostics.has_error());
}
-test "ordered lists" {
- try testAcceptDocument(
- \\hdoc "1.0"
- \\enumerate {
- \\ item { toc {} }
- \\ item { toc {} }
- \\ item { toc {} }
- \\}
- );
+test "semantic analyzer reports invalid string escapes" {
+ var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
+ defer arena.deinit();
+
+ const source = "\"oops\\q\"";
+
+ var diagnostics: hdoc.Diagnostics = .init(std.testing.allocator);
+ defer diagnostics.deinit();
+
+ var sema: hdoc.SemanticAnalyzer = .{
+ .arena = arena.allocator(),
+ .diagnostics = &diagnostics,
+ .code = source,
+ };
+
+ const token: hdoc.Parser.Token = .{ .text = source, .location = .{ .offset = 0, .length = source.len } };
+
+ const text = try sema.unescape_string(token);
+ try std.testing.expectEqualStrings("oops\\q", text);
+ try std.testing.expectEqual(@as(usize, 1), diagnostics.items.items.len);
+ try std.testing.expect(diagnosticCodesEqual(diagnostics.items.items[0].code, .{ .invalid_string_escape = .{ .codepoint = 'q' } }));
}
-test "unordered lists" {
- try testAcceptDocument(
- \\hdoc "1.0"
- \\itemize {
- \\ item { toc {} }
- \\ item { toc {} }
- \\ item { toc {} }
- \\}
- );
+test "semantic analyzer flags forbidden control characters" {
+ var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
+ defer arena.deinit();
+
+ const source = "\"tab\\u{9}\"";
+
+ var diagnostics: hdoc.Diagnostics = .init(std.testing.allocator);
+ defer diagnostics.deinit();
+
+ var sema: hdoc.SemanticAnalyzer = .{
+ .arena = arena.allocator(),
+ .diagnostics = &diagnostics,
+ .code = source,
+ };
+
+ const token: hdoc.Parser.Token = .{ .text = source, .location = .{ .offset = 0, .length = source.len } };
+
+ const text = try sema.unescape_string(token);
+ try std.testing.expectEqualStrings("tab\t", text);
+ try std.testing.expectEqual(@as(usize, 1), diagnostics.items.items.len);
+ try std.testing.expect(diagnosticCodesEqual(diagnostics.items.items[0].code, .{ .illegal_character = .{ .codepoint = 0x9 } }));
+}
+
+test "semantic analyzer forbids raw control characters" {
+ var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
+ defer arena.deinit();
+
+ const source = "\"bad\tvalue\"";
+
+ var diagnostics: hdoc.Diagnostics = .init(std.testing.allocator);
+ defer diagnostics.deinit();
+
+ var sema: hdoc.SemanticAnalyzer = .{
+ .arena = arena.allocator(),
+ .diagnostics = &diagnostics,
+ .code = source,
+ };
+
+ const token: hdoc.Parser.Token = .{ .text = source, .location = .{ .offset = 0, .length = source.len } };
+ _ = try sema.unescape_string(token);
+
+ try std.testing.expectEqual(@as(usize, 1), diagnostics.items.items.len);
+ try std.testing.expect(diagnosticCodesEqual(diagnostics.items.items[0].code, .{ .illegal_character = .{ .codepoint = 0x9 } }));
+}
+
+test "span merger preserves whitespace after inline mono" {
+ var diagnostics: hdoc.Diagnostics = .init(std.testing.allocator);
+ defer diagnostics.deinit();
+
+ const source =
+ \\hdoc(version="2.0",lang="en");
+ \\p{ \mono{monospace} text. }
+ ;
+
+ var doc = try hdoc.parse(std.testing.allocator, source, &diagnostics);
+ defer doc.deinit();
+
+ try std.testing.expect(!diagnostics.has_error());
+ try std.testing.expectEqual(@as(usize, 1), doc.contents.len);
+
+ switch (doc.contents[0]) {
+ .paragraph => |para| {
+ try std.testing.expectEqual(@as(usize, 2), para.content.len);
+ try std.testing.expect(para.content[0].attribs.mono);
+ try std.testing.expect(!para.content[1].attribs.mono);
+
+ switch (para.content[0].content) {
+ .text => |text| try std.testing.expectEqualStrings("monospace", text),
+ else => return error.TestExpectedEqual,
+ }
+
+ switch (para.content[1].content) {
+ .text => |text| try std.testing.expectEqualStrings(" text.", text),
+ else => return error.TestExpectedEqual,
+ }
+ },
+ else => return error.TestExpectedEqual,
+ }
}
-test "nested lists" {
- try testAcceptDocument(
- \\hdoc "1.0"
- \\enumerate {
- \\ item { itemize { } }
- \\ item { enumerate { } }
- \\ item { toc { } }
- \\ item { itemize { item { toc { } } } }
- \\ item { enumerate { item { toc { } } } }
+test "admonition supports block-list bodies" {
+ var diagnostics: hdoc.Diagnostics = .init(std.testing.allocator);
+ defer diagnostics.deinit();
+
+ const source =
+ \\hdoc(version="2.0",lang="en");
+ \\note{
+ \\ p "Outer block text."
+ \\ ul{li "Nested item"}
\\}
- );
+ ;
+
+ var doc = try hdoc.parse(std.testing.allocator, source, &diagnostics);
+ defer doc.deinit();
+
+ try std.testing.expect(!diagnostics.has_error());
+ try std.testing.expectEqual(@as(usize, 1), doc.contents.len);
+
+ const admonition = doc.contents[0].admonition;
+ try std.testing.expectEqual(hdoc.Block.AdmonitionKind.note, admonition.kind);
+ try std.testing.expectEqual(@as(usize, 2), admonition.content.len);
+
+ switch (admonition.content[0]) {
+ .paragraph => |para| {
+ try std.testing.expectEqual(@as(usize, 1), para.content.len);
+ try std.testing.expectEqualStrings("Outer block text.", para.content[0].content.text);
+ },
+ else => return error.TestExpectedEqual,
+ }
+
+ switch (admonition.content[1]) {
+ .list => |list| {
+ try std.testing.expectEqual(@as(usize, 1), list.items.len);
+ try std.testing.expectEqual(@as(?u32, null), list.first);
+ try std.testing.expectEqual(@as(usize, 1), list.items[0].content.len);
+ },
+ else => return error.TestExpectedEqual,
+ }
}
-test "empty paragraph" {
- try testAcceptDocument(
- \\hdoc "1.0"
- \\p{}
- \\p{}
- \\p{}
- );
+test "admonition shorthand promotes inline bodies to paragraphs" {
+ var diagnostics: hdoc.Diagnostics = .init(std.testing.allocator);
+ defer diagnostics.deinit();
+
+ const source =
+ "hdoc(version=\"2.0\",lang=\"en\");\n" ++
+ "warning \"Be careful.\" \n" ++
+ "tip:\n" ++
+ "| first line\n" ++
+ "| second line\n";
+
+ var doc = try hdoc.parse(std.testing.allocator, source, &diagnostics);
+ defer doc.deinit();
+
+ try std.testing.expect(!diagnostics.has_error());
+ try std.testing.expectEqual(@as(usize, 2), doc.contents.len);
+
+ const warning_block = doc.contents[0].admonition;
+ try std.testing.expectEqual(hdoc.Block.AdmonitionKind.warning, warning_block.kind);
+ try std.testing.expectEqual(@as(usize, 1), warning_block.content.len);
+ switch (warning_block.content[0]) {
+ .paragraph => |para| {
+ try std.testing.expectEqualStrings("Be careful.", para.content[0].content.text);
+ },
+ else => return error.TestExpectedEqual,
+ }
+
+ const tip_block = doc.contents[1].admonition;
+ try std.testing.expectEqual(hdoc.Block.AdmonitionKind.tip, tip_block.kind);
+ try std.testing.expectEqual(@as(usize, 1), tip_block.content.len);
+ switch (tip_block.content[0]) {
+ .paragraph => |para| {
+ try std.testing.expectEqualStrings("first line\nsecond line", para.content[0].content.text);
+ },
+ else => return error.TestExpectedEqual,
+ }
}
-test "empty quote" {
- try testAcceptDocument(
- \\hdoc "1.0"
- \\quote{}
- \\quote{}
- \\quote{}
- );
+test "pre verbatim preserves trailing whitespace" {
+ var diagnostics: hdoc.Diagnostics = .init(std.testing.allocator);
+ defer diagnostics.deinit();
+
+ const source =
+ "hdoc(version=\"2.0\",lang=\"en\");\n" ++ "pre:\n" ++ "| line with trailing spaces \n" ++ "| indented line \n";
+
+ var doc = try hdoc.parse(std.testing.allocator, source, &diagnostics);
+ defer doc.deinit();
+
+ try std.testing.expect(!diagnostics.has_error());
+ try std.testing.expectEqual(@as(usize, 1), doc.contents.len);
+
+ const preformatted = doc.contents[0].preformatted;
+ try std.testing.expectEqual(@as(usize, 1), preformatted.content.len);
+
+ const expected = "line with trailing spaces \n indented line ";
+ switch (preformatted.content[0].content) {
+ .text => |text| try std.testing.expectEqualStrings(expected, text),
+ else => return error.TestExpectedEqual,
+ }
}
-test "spans" {
- try testAcceptDocument(
- \\hdoc "1.0"
- \\p{ span "hello" }
- \\p{ span "\n" }
- \\p{ span "" }
- );
+test "parser reports unterminated string literals" {
+ var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
+ defer arena.deinit();
+
+ var parser: hdoc.Parser = .{
+ .code = "\"unterminated\n",
+ .arena = arena.allocator(),
+ .diagnostics = null,
+ };
+
+ try std.testing.expectError(error.UnterminatedStringLiteral, parser.accept_string());
}
-test "mono" {
- try testAcceptDocument(
- \\hdoc "1.0"
- \\p{ mono "hello" }
- \\p{ mono "\n" }
- \\p{ mono "" }
- );
+test "parser handles attributes and empty bodies" {
+ var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
+ defer arena.deinit();
+
+ var parser: hdoc.Parser = .{
+ .code = "h1(title=\"Hello\",author=\"World\");",
+ .arena = arena.allocator(),
+ .diagnostics = null,
+ };
+
+ const node = try parser.accept_node(.top_level);
+ try std.testing.expectEqual(hdoc.Parser.NodeType.h1, node.type);
+ try std.testing.expectEqual(@as(usize, 2), node.attributes.items.len);
+
+ const attribs = node.attributes.items;
+
+ const title = attribs[0];
+ try std.testing.expectEqualStrings("title", title.name.text);
+ try std.testing.expectEqualStrings("\"Hello\"", title.value.text);
+
+ const author = attribs[1];
+ try std.testing.expectEqualStrings("author", author.name.text);
+ try std.testing.expectEqualStrings("\"World\"", author.value.text);
+
+ try std.testing.expect(node.body == .empty);
}
-test "emph" {
- try testAcceptDocument(
- \\hdoc "1.0"
- \\p{ emph "hello" }
- \\p{ emph "\n" }
- \\p{ emph "" }
- );
+test "parser handles string bodies" {
+ var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
+ defer arena.deinit();
+
+ var parser: hdoc.Parser = .{
+ .code = "p \"Hello world\"",
+ .arena = arena.allocator(),
+ .diagnostics = null,
+ };
+
+ const node = try parser.accept_node(.top_level);
+ try std.testing.expectEqual(hdoc.Parser.NodeType.p, node.type);
+ switch (node.body) {
+ .string => |token| try std.testing.expectEqualStrings("\"Hello world\"", token.text),
+ else => return error.TestExpectedEqual,
+ }
}
-test "links" {
- try testAcceptDocument(
- \\hdoc "1.0"
- \\p{ link "" "hello" }
- \\p{ link "" "\n" }
- \\p{ link "" "" }
- \\p{ link "https://www.example.com/deep/path.txt" "hello" }
- \\p{ link "https://www.example.com/deep/path.txt" "\n" }
- \\p{ link "https://www.example.com/deep/path.txt" "" }
- \\p{ link "#anchor" "hello" }
- \\p{ link "#anchor" "\n" }
- \\p{ link "#anchor" "" }
- );
+test "parser handles verbatim blocks" {
+ var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
+ defer arena.deinit();
+
+ var parser: hdoc.Parser = .{
+ .code = "pre:\n|line one\n|line two\n",
+ .arena = arena.allocator(),
+ .diagnostics = null,
+ };
+
+ const node = try parser.accept_node(.top_level);
+ try std.testing.expectEqual(hdoc.Parser.NodeType.pre, node.type);
+ switch (node.body) {
+ .verbatim => |lines| {
+ try std.testing.expectEqual(@as(usize, 2), lines.len);
+ try std.testing.expectEqualStrings("|line one", lines[0].text);
+ try std.testing.expectEqualStrings("|line two", lines[1].text);
+ },
+ else => return error.TestExpectedEqual,
+ }
+}
+
+test "parser handles block node lists" {
+ var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
+ defer arena.deinit();
+
+ var parser: hdoc.Parser = .{
+ .code = "hdoc{h1 \"Title\" p \"Body\"}",
+ .arena = arena.allocator(),
+ .diagnostics = null,
+ };
+
+ const node = try parser.accept_node(.top_level);
+ try std.testing.expectEqual(hdoc.Parser.NodeType.hdoc, node.type);
+ switch (node.body) {
+ .list => |children| {
+ try std.testing.expectEqual(@as(usize, 2), children.len);
+ try std.testing.expectEqual(hdoc.Parser.NodeType.h1, children[0].type);
+ try std.testing.expectEqual(hdoc.Parser.NodeType.p, children[1].type);
+ },
+ else => return error.TestExpectedEqual,
+ }
+}
+
+test "parser handles inline node lists" {
+ var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
+ defer arena.deinit();
+
+ var parser: hdoc.Parser = .{
+ .code = "p { Hello \\em{world} }",
+ .arena = arena.allocator(),
+ .diagnostics = null,
+ };
+
+ const node = try parser.accept_node(.top_level);
+ try std.testing.expectEqual(hdoc.Parser.NodeType.p, node.type);
+ switch (node.body) {
+ .list => |children| {
+ try std.testing.expectEqual(@as(usize, 5), children.len);
+
+ try std.testing.expectEqual(.text, children[0].type);
+ try std.testing.expectEqual(.text, children[1].type);
+ try std.testing.expectEqual(.text, children[2].type);
+ try std.testing.expectEqual(.@"\\em", children[3].type);
+ try std.testing.expectEqual(.text, children[4].type);
+
+ try std.testing.expectEqual(" ".len, children[0].location.length);
+ try std.testing.expectEqual("Hello".len, children[1].location.length);
+ try std.testing.expectEqual(" ".len, children[2].location.length);
+ try std.testing.expectEqual("\\em{world}".len, children[3].location.length);
+ try std.testing.expectEqual(" ".len, children[4].location.length);
+
+ switch (children[3].body) {
+ .list => |inline_children| {
+ try std.testing.expectEqual(1, inline_children.len);
+ try std.testing.expectEqual(.text, inline_children[0].type);
+ try std.testing.expectEqual("world".len, inline_children[0].location.length);
+ },
+ else => return error.TestExpectedEqual,
+ }
+ },
+ else => return error.TestExpectedEqual,
+ }
+}
+
+test "parser handles unknown node types" {
+ var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
+ defer arena.deinit();
+
+ var parser: hdoc.Parser = .{
+ .code = "\\madeup{} mystery{}",
+ .arena = arena.allocator(),
+ .diagnostics = null,
+ };
+
+ const inline_node = try parser.accept_node(.top_level);
+ try std.testing.expectEqual(hdoc.Parser.NodeType.unknown_inline, inline_node.type);
+ switch (inline_node.body) {
+ .list => |children| try std.testing.expectEqual(@as(usize, 0), children.len),
+ else => return error.TestExpectedEqual,
+ }
+
+ const block_node = try parser.accept_node(.top_level);
+ try std.testing.expectEqual(hdoc.Parser.NodeType.unknown_block, block_node.type);
+ switch (block_node.body) {
+ .list => |children| try std.testing.expectEqual(@as(usize, 0), children.len),
+ else => return error.TestExpectedEqual,
+ }
+}
+
+test "\\ref synthesizes heading text for empty bodies" {
+ var diagnostics: hdoc.Diagnostics = .init(std.testing.allocator);
+ defer diagnostics.deinit();
+
+ const source =
+ \\hdoc(version="2.0",lang="en");
+ \\h1(id="intro"){Introduction}
+ \\p{See \ref(ref="intro"); and \ref(ref="intro",fmt="name"); and \ref(ref="intro",fmt="index");}
+ ;
+
+ var doc = try hdoc.parse(std.testing.allocator, source, &diagnostics);
+ defer doc.deinit();
+
+ try std.testing.expect(!diagnostics.has_error());
+ try std.testing.expectEqual(@as(usize, 2), doc.contents.len);
+
+ const paragraph = doc.contents[1].paragraph;
+ const expected_formats = [_]hdoc.Span.ReferenceFormat{ .full, .name, .index };
+
+ var seen: usize = 0;
+ for (paragraph.content) |span| {
+ if (span.content != .reference) continue;
+
+ const reference = span.content.reference;
+ try std.testing.expect(seen < expected_formats.len);
+ try std.testing.expectEqual(expected_formats[seen], reference.fmt);
+ try std.testing.expectEqual(@as(?usize, 0), reference.target_block);
+
+ switch (span.attribs.link) {
+ .ref => |link| try std.testing.expectEqual(@as(?usize, 0), link.block_index),
+ else => return error.TestExpectedEqual,
+ }
+
+ seen += 1;
+ }
+
+ try std.testing.expectEqual(expected_formats.len, seen);
+}
+
+test "\\ref empty body rejects non-heading targets" {
+ var diagnostics: hdoc.Diagnostics = .init(std.testing.allocator);
+ defer diagnostics.deinit();
+
+ const source =
+ \\hdoc(version="2.0",lang="en");
+ \\p(id="p1"){Body}
+ \\p{\ref(ref="p1");}
+ ;
+
+ var doc = try hdoc.parse(std.testing.allocator, source, &diagnostics);
+ defer doc.deinit();
+
+ try std.testing.expect(diagnostics.has_error());
+ try std.testing.expect(diagnosticCodesEqual(diagnostics.items.items[0].code, .empty_ref_body_target));
+}
+
+test "table of contents inserts automatic headings when skipping levels" {
+ const source =
+ \\hdoc(version="2.0");
+ \\h3{Third}
+ \\h2{Second}
+ \\h1{First}
+ ;
+
+ var diagnostics: hdoc.Diagnostics = .init(std.testing.allocator);
+ defer diagnostics.deinit();
+
+ var doc = try hdoc.parse(std.testing.allocator, source, &diagnostics);
+ defer doc.deinit();
+
+ try std.testing.expectEqual(@as(usize, 5), diagnostics.items.items.len);
+ try std.testing.expect(diagnosticCodesEqual(diagnostics.items.items[0].code, .missing_document_language));
+ try std.testing.expect(diagnosticCodesEqual(diagnostics.items.items[1].code, .{
+ .invalid_heading_sequence = .{ .level = .h3, .missing = .h2 },
+ }));
+ try std.testing.expect(diagnosticCodesEqual(diagnostics.items.items[2].code, .{
+ .invalid_heading_sequence = .{ .level = .h2, .missing = .h1 },
+ }));
+ try std.testing.expect(diagnosticCodesEqual(diagnostics.items.items[3].code, .{ .automatic_heading_insertion = .{ .level = .h1 } }));
+ try std.testing.expect(diagnosticCodesEqual(diagnostics.items.items[4].code, .{ .automatic_heading_insertion = .{ .level = .h2 } }));
+
+ const toc = doc.toc;
+ try std.testing.expectEqual(.h1, toc.level);
+ try std.testing.expectEqualSlices(usize, &.{ 0, 2 }, toc.headings);
+ try std.testing.expectEqual(@as(usize, 2), toc.children.len);
+
+ const auto_h1 = toc.children[0];
+ try std.testing.expectEqual(.h2, auto_h1.level);
+ try std.testing.expectEqualSlices(usize, &.{ 0, 1 }, auto_h1.headings);
+ try std.testing.expectEqual(@as(usize, 2), auto_h1.children.len);
+
+ const auto_h2 = auto_h1.children[0];
+ try std.testing.expectEqual(.h3, auto_h2.level);
+ try std.testing.expectEqualSlices(usize, &.{0}, auto_h2.headings);
+
+ const h2_child = auto_h1.children[1];
+ try std.testing.expectEqual(.h3, h2_child.level);
+ try std.testing.expectEqual(@as(usize, 0), h2_child.headings.len);
+ try std.testing.expectEqual(@as(usize, 0), h2_child.children.len);
+
+ const trailing_h1_child = toc.children[1];
+ try std.testing.expectEqual(.h2, trailing_h1_child.level);
+ try std.testing.expectEqual(@as(usize, 0), trailing_h1_child.headings.len);
+ try std.testing.expectEqual(@as(usize, 0), trailing_h1_child.children.len);
+}
+
+test "footnotes collect entries per dump" {
+ const source =
+ \\hdoc(version="2.0",lang="en");
+ \\p{Intro \footnote{first} \footnote(kind="citation",key="cite1"){c1}}
+ \\footnotes;
+ \\p{Again \footnote(ref="cite1"); \footnote{second}}
+ \\footnotes(kind="citation");
+ \\footnotes;
+ ;
+
+ var diagnostics: hdoc.Diagnostics = .init(std.testing.allocator);
+ defer diagnostics.deinit();
+
+ var doc = try hdoc.parse(std.testing.allocator, source, &diagnostics);
+ defer doc.deinit();
+
+ try std.testing.expect(!diagnostics.has_error());
+ try std.testing.expectEqual(@as(usize, 5), doc.contents.len);
+
+ const first_dump = switch (doc.contents[1]) {
+ .footnotes => |value| value,
+ else => return error.TestExpectedEqual,
+ };
+ try std.testing.expectEqual(@as(usize, 2), first_dump.entries.len);
+ try std.testing.expectEqual(hdoc.FootnoteKind.footnote, first_dump.entries[0].kind);
+ try std.testing.expectEqual(@as(usize, 1), first_dump.entries[0].index);
+ try std.testing.expectEqual(hdoc.FootnoteKind.citation, first_dump.entries[1].kind);
+ try std.testing.expectEqual(@as(usize, 1), first_dump.entries[1].index);
+
+ const second_dump = switch (doc.contents[3]) {
+ .footnotes => |value| value,
+ else => return error.TestExpectedEqual,
+ };
+ try std.testing.expectEqual(@as(usize, 1), second_dump.entries.len);
+ try std.testing.expectEqual(hdoc.FootnoteKind.citation, second_dump.entries[0].kind);
+ try std.testing.expectEqual(@as(usize, 1), second_dump.entries[0].index);
+
+ const final_dump = switch (doc.contents[4]) {
+ .footnotes => |value| value,
+ else => return error.TestExpectedEqual,
+ };
+ try std.testing.expectEqual(@as(usize, 1), final_dump.entries.len);
+ try std.testing.expectEqual(hdoc.FootnoteKind.footnote, final_dump.entries[0].kind);
+ try std.testing.expectEqual(@as(usize, 2), final_dump.entries[0].index);
+}
+
+test "warn when footnotes are missing dumps" {
+ const source =
+ \\hdoc(version="2.0",lang="en");
+ \\p{Body \footnote{content}}
+ ;
+
+ var diagnostics: hdoc.Diagnostics = .init(std.testing.allocator);
+ defer diagnostics.deinit();
+
+ var doc = try hdoc.parse(std.testing.allocator, source, &diagnostics);
+ defer doc.deinit();
+
+ var saw_warning = false;
+ for (diagnostics.items.items) |item| {
+ if (diagnosticCodesEqual(item.code, .footnote_missing_dump)) {
+ saw_warning = true;
+ break;
+ }
+ }
+ try std.testing.expect(saw_warning);
+}
+
+test "warn when footnotes remain after intermediate dump" {
+ const source =
+ \\hdoc(version="2.0",lang="en");
+ \\p{First \footnote{one}}
+ \\footnotes{}
+ \\p{Second \footnote{two}}
+ ;
+
+ var diagnostics: hdoc.Diagnostics = .init(std.testing.allocator);
+ defer diagnostics.deinit();
+
+ var doc = try hdoc.parse(std.testing.allocator, source, &diagnostics);
+ defer doc.deinit();
+
+ var saw_warning = false;
+ for (diagnostics.items.items) |item| {
+ if (diagnosticCodesEqual(item.code, .footnote_missing_dump)) {
+ saw_warning = true;
+ break;
+ }
+ }
+ try std.testing.expect(saw_warning);
+}
+
+test "footnote missing dump warning points to earliest remaining kind" {
+ const source =
+ \\hdoc(version="2.0",lang="en");
+ \\p{First \footnote{one}}
+ \\p{C \footnote(kind="citation",key="cite"){two}}
+ \\footnotes(kind="footnote");
+ ;
+
+ var diagnostics: hdoc.Diagnostics = .init(std.testing.allocator);
+ defer diagnostics.deinit();
+
+ var doc = try hdoc.parse(std.testing.allocator, source, &diagnostics);
+ defer doc.deinit();
+
+ var warning: ?hdoc.Diagnostic = null;
+ for (diagnostics.items.items) |item| {
+ if (diagnosticCodesEqual(item.code, .footnote_missing_dump)) {
+ warning = item;
+ break;
+ }
+ }
+
+ try std.testing.expect(warning != null);
+ try std.testing.expectEqual(@as(u32, 3), warning.?.location.line);
+ try std.testing.expectEqual(@as(u32, 5), warning.?.location.column);
+}
+
+test "no warning when footnotes are drained after later dump" {
+ const source =
+ \\hdoc(version="2.0",lang="en");
+ \\p{First \footnote{one}}
+ \\footnotes{}
+ \\p{Second \footnote{two}}
+ \\footnotes{}
+ ;
+
+ var diagnostics: hdoc.Diagnostics = .init(std.testing.allocator);
+ defer diagnostics.deinit();
+
+ var doc = try hdoc.parse(std.testing.allocator, source, &diagnostics);
+ defer doc.deinit();
+
+ for (diagnostics.items.items) |item| {
+ if (diagnosticCodesEqual(item.code, .footnote_missing_dump)) {
+ return error.TestExpectedEqual;
+ }
+ }
}
-test "code block" {
- try testAcceptDocument(
- \\hdoc "1.0"
- \\pre "" { }
- \\pre "c++" { }
- \\pre "zig" { }
- \\pre "c++" { span "#include " }
- \\pre "zig" { span "const std = @import(\"std\");" }
+fn diagnosticCodesEqual(lhs: hdoc.Diagnostic.Code, rhs: hdoc.Diagnostic.Code) bool {
+ if (std.meta.activeTag(lhs) != std.meta.activeTag(rhs))
+ return false;
+
+ switch (lhs) {
+ inline else => |_, tag_value| {
+ const tag = @tagName(tag_value);
+ const a_struct = @field(lhs, tag);
+ const b_struct = @field(rhs, tag);
+
+ const TagField = @FieldType(hdoc.Diagnostic.Code, tag);
+ const info = @typeInfo(TagField);
+
+ switch (info) {
+ .void => return true,
+
+ .@"struct" => |struct_info| {
+ inline for (struct_info.fields) |fld| {
+ const a = @field(a_struct, fld.name);
+ const b = @field(b_struct, fld.name);
+ const eql = switch (fld.type) {
+ []const u8 => std.mem.eql(u8, a, b),
+ else => (a == b),
+ };
+ if (!eql)
+ return false;
+ }
+ return true;
+ },
+
+ else => @compileError("Unsupported type: " ++ @typeName(TagField)),
+ }
+ },
+ }
+}
+
+const LogDiagOptions = struct {
+ file_path: []const u8 = "",
+};
+
+fn logDiagnostics(diag: *const hdoc.Diagnostics, opts: LogDiagOptions) void {
+ for (diag.items.items) |item| {
+ var buf: [256]u8 = undefined;
+ var stream = std.io.fixedBufferStream(&buf);
+ item.code.format(stream.writer()) catch {};
+ std.log.err("Diagnostic {s}:{d}:{d}: {s}", .{ opts.file_path, item.location.line, item.location.column, stream.getWritten() });
+ }
+}
+
+fn validateDiagnostics(opts: LogDiagOptions, code: []const u8, expected: []const hdoc.Diagnostic.Code) !void {
+ try std.testing.expect(expected.len > 0);
+
+ var diagnostics: hdoc.Diagnostics = .init(std.testing.allocator);
+ defer diagnostics.deinit();
+
+ const maybe_doc = hdoc.parse(std.testing.allocator, code, &diagnostics) catch |err| switch (err) {
+ error.OutOfMemory => return err,
+ else => null,
+ };
+ if (maybe_doc) |doc| {
+ var owned = doc;
+ defer owned.deinit();
+ }
+
+ if (diagnostics.items.items.len != expected.len) {
+ logDiagnostics(&diagnostics, opts);
+ }
+ try std.testing.expectEqual(expected.len, diagnostics.items.items.len);
+ for (expected, 0..) |exp, idx| {
+ const actual = diagnostics.items.items[idx].code;
+ if (!diagnosticCodesEqual(actual, exp)) {
+ logDiagnostics(&diagnostics, opts);
+ return error.MissingDiagnosticCode;
+ }
+ }
+}
+
+fn expectParseOk(opts: LogDiagOptions, code: []const u8) !void {
+ var diagnostics: hdoc.Diagnostics = .init(std.testing.allocator);
+ defer diagnostics.deinit();
+
+ var doc = try hdoc.parse(std.testing.allocator, code, &diagnostics);
+ defer doc.deinit();
+
+ if (diagnostics.has_error()) {
+ logDiagnostics(&diagnostics, opts);
+ return error.TestExpectedNoDiagnostics;
+ }
+
+ for (diagnostics.items.items) |item| {
+ if (item.code.severity() != .warning)
+ continue;
+ switch (item.code) {
+ .missing_document_language => {},
+ else => {
+ logDiagnostics(&diagnostics, opts);
+ return error.TestExpectedNoDiagnostics;
+ },
+ }
+ }
+}
+
+fn expectParseNoFail(opts: LogDiagOptions, code: []const u8) !void {
+ var diagnostics: hdoc.Diagnostics = .init(std.testing.allocator);
+ defer diagnostics.deinit();
+
+ var doc = hdoc.parse(std.testing.allocator, code, &diagnostics) catch |err| switch (err) {
+ error.OutOfMemory => return err,
+ else => {
+ logDiagnostics(&diagnostics);
+ return error.TestExpectedEqual;
+ },
+ };
+ defer doc.deinit();
+
+ if (diagnostics.has_error()) {
+ logDiagnostics(&diagnostics, opts);
+ return error.TestExpectedNoErrors;
+ }
+}
+
+test "parsing valid document yields empty diagnostics" {
+ try expectParseOk(.{}, "hdoc(version=\"2.0\",lang=\"en\");");
+}
+
+test "diagnostic codes are emitted for expected samples" {
+ try validateDiagnostics(.{}, "hdoc(version=\"2.0\",lang=\"en\"); h1(", &.{.{ .unexpected_eof = .{ .context = "identifier", .expected_char = null } }});
+ try validateDiagnostics(.{}, "hdoc(version=\"2.0\",lang=\"en\"); h1 123", &.{.{ .unexpected_character = .{ .expected = '{', .found = '1' } }});
+ try validateDiagnostics(.{}, "hdoc(version=\"2.0\",lang=\"en\"); h1 \"unterminated", &.{.unterminated_string});
+ try validateDiagnostics(.{}, "hdoc(version=\"2.0\",lang=\"en\"); *abc", &.{.{ .invalid_identifier_start = .{ .char = '*' } }});
+ try validateDiagnostics(.{}, "hdoc{h1 \"x\"", &.{.unterminated_block_list});
+ try validateDiagnostics(.{}, "hdoc(version=\"2.0\",lang=\"en\"); p {hello", &.{.unterminated_inline_list});
+ try validateDiagnostics(
+ .{},
+ "hdoc(version=\"2.0\",lang=\"en\"); h1(lang=\"a\",lang=\"b\");",
+ &.{ .{ .duplicate_attribute = .{ .name = "lang" } }, .empty_inline_body },
);
+ try validateDiagnostics(.{}, "hdoc(version=\"2.0\",lang=\"en\"); pre:\n", &.{.empty_verbatim_block});
+ try validateDiagnostics(.{}, "hdoc(version=\"2.0\",lang=\"en\"); pre:\n| line", &.{.verbatim_missing_trailing_newline});
+ try validateDiagnostics(.{}, "hdoc(version=\"2.0\",lang=\"en\"); pre:\n|nospace\n", &.{.verbatim_missing_space});
+ try validateDiagnostics(.{}, "hdoc(version=\"2.0\",lang=\"en\"); pre:\n| trailing \n", &.{.trailing_whitespace});
+ try validateDiagnostics(.{}, "h1 \"Title\"", &.{.missing_hdoc_header});
+ try validateDiagnostics(.{}, "hdoc(version=\"2.0\",lang=\"en\"); hdoc(version=\"2.0\",lang=\"en\");", &.{ .misplaced_hdoc_header, .duplicate_hdoc_header });
+ try validateDiagnostics(.{}, "hdoc(version=\"2.0\",lang=\"en\"); h1 \"bad\\q\"", &.{.{ .invalid_string_escape = .{ .codepoint = 'q' } }});
+ try validateDiagnostics(.{}, "hdoc(version=\"2.0\",lang=\"en\"); h1 \"bad\\u{9}\"", &.{.{ .illegal_character = .{ .codepoint = 0x9 } }});
+ try validateDiagnostics(.{}, "hdoc(version=\"2.0\",lang=\"en\"); ul{ li{ toc; } }", &.{ .illegal_child_item, .list_body_required });
}
-test "example document" {
- try testAcceptDocument(
- \\hdoc "1.0"
- \\h1 "intro" "Introduction"
- \\toc { }
- \\p {
- \\ span "Hello, World!\n"
- \\ link "http://google.com" "Visit Google!"
- \\ span "\n"
- \\ emph "This is fat!"
- \\ span "\n"
- \\ mono "int main()"
- \\ span "\n"
- \\}
- \\enumerate {
- \\ item { p { span "first" } }
- \\ item { p { span "second" } }
- \\ item { p { span "third" } }
+test "table derives column count from first data row" {
+ const code =
+ \\hdoc(version="2.0",lang="en");
+ \\table {
+ \\ row(title="headered") {
+ \\ td { p "A" }
+ \\ td(colspan="2") { p "B" }
+ \\ }
\\}
- \\itemize {
- \\ item { p { span "first" } }
- \\ item { p { span "second" } }
- \\ item { p { span "third" } }
+ ;
+
+ var diagnostics: hdoc.Diagnostics = .init(std.testing.allocator);
+ defer diagnostics.deinit();
+
+ var doc = try hdoc.parse(std.testing.allocator, code, &diagnostics);
+ defer doc.deinit();
+
+ try std.testing.expect(!diagnostics.has_error());
+ try std.testing.expectEqual(@as(usize, 1), doc.contents.len);
+
+ switch (doc.contents[0]) {
+ .table => |table| {
+ try std.testing.expectEqual(@as(usize, 3), table.column_count);
+ try std.testing.expect(table.has_row_titles);
+ },
+ else => return error.TestExpectedEqual,
+ }
+}
+
+test "table without header or data rows is rejected" {
+ try validateDiagnostics(.{}, "hdoc(version=\"2.0\",lang=\"en\"); table { group \"Topic\" }", &.{.missing_table_column_count});
+}
+
+test "columns row must come first" {
+ const code =
+ \\hdoc(version="2.0",lang="en");
+ \\table {
+ \\ row { td "A" }
+ \\ columns { td "B" }
\\}
- \\quote {
- \\ span "Life is what happens when you're busy making other plans.\n - John Lennon"
+ ;
+
+ try validateDiagnostics(.{}, code, &.{.misplaced_columns_row});
+}
+
+test "table allows only one columns row" {
+ const code =
+ \\hdoc(version="2.0",lang="en");
+ \\table {
+ \\ columns { td "A" }
+ \\ columns { td "B" }
\\}
- \\pre "zig" {
- \\ span "const std = @import(\"std\");\n"
- \\ span "\n"
- \\ span "pub fn main() !void {\n"
- \\ span " std.debug.print(\"Hello, World!\\n\", .{});\n"
- \\ span "}\n"
+ ;
+
+ try validateDiagnostics(.{}, code, &.{.duplicate_columns_row});
+}
+
+test "table tracks presence of row titles" {
+ const code =
+ \\hdoc(version="2.0",lang="en");
+ \\table {
+ \\ row { td "A" }
+ \\ group { "Topic" }
\\}
- \\image "dog.png"
- );
+ ;
+
+ var diagnostics: hdoc.Diagnostics = .init(std.testing.allocator);
+ defer diagnostics.deinit();
+
+ var doc = try hdoc.parse(std.testing.allocator, code, &diagnostics);
+ defer doc.deinit();
+
+ try std.testing.expect(!diagnostics.has_error());
+ try std.testing.expectEqual(@as(usize, 1), doc.contents.len);
+
+ switch (doc.contents[0]) {
+ .table => |table| {
+ try std.testing.expect(!table.has_row_titles);
+ },
+ else => return error.TestExpectedEqual,
+ }
+}
+
+test "title block populates metadata and warns on inline date" {
+ const code = "hdoc(version=\"2.0\",lang=\"en\");\ntitle { Hello \\date{2020-01-02} }\nh1 \"Body\"";
+
+ var diagnostics: hdoc.Diagnostics = .init(std.testing.allocator);
+ defer diagnostics.deinit();
+
+ var doc = try hdoc.parse(std.testing.allocator, code, &diagnostics);
+ defer doc.deinit();
+
+ try std.testing.expect(!diagnostics.has_error());
+ try std.testing.expectEqual(@as(usize, 1), diagnostics.items.items.len);
+ try std.testing.expect(diagnostics.items.items[0].code == .title_inline_date_time_without_header);
+
+ const title = doc.title orelse return error.TestExpectedEqual;
+ const full = title.full;
+ try std.testing.expectEqualStrings("Hello 2020-01-02", title.simple);
+ try std.testing.expectEqual(@as(usize, 3), full.content.len);
+}
+
+test "header title synthesizes full title representation" {
+ const code = "hdoc(version=\"2.0\",title=\"Metadata\",lang=\"en\");\nh1 \"Body\"";
+
+ var diagnostics: hdoc.Diagnostics = .init(std.testing.allocator);
+ defer diagnostics.deinit();
+
+ var doc = try hdoc.parse(std.testing.allocator, code, &diagnostics);
+ defer doc.deinit();
+
+ try std.testing.expect(!diagnostics.has_error());
+ try std.testing.expectEqual(@as(usize, 0), diagnostics.items.items.len);
+
+ const title = doc.title orelse return error.TestExpectedEqual;
+ try std.testing.expectEqualStrings("Metadata", title.simple);
+
+ const full = title.full;
+ try std.testing.expectEqual(@as(usize, 1), full.content.len);
+ switch (full.content[0].content) {
+ .text => |text| try std.testing.expectEqualStrings("Metadata", text),
+ else => return error.TestExpectedEqual,
+ }
+}
+
+test "parser reports unterminated inline lists" {
+ var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
+ defer arena.deinit();
+
+ var parser: hdoc.Parser = .{
+ .code = "p { word",
+ .arena = arena.allocator(),
+ .diagnostics = null,
+ };
+
+ try std.testing.expectError(error.UnterminatedList, parser.accept_node(.top_level));
+}
+
+test "parser maps diagnostic locations" {
+ var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
+ defer arena.deinit();
+
+ var parser: hdoc.Parser = .{
+ .code = "a\nb\nc",
+ .arena = arena.allocator(),
+ .diagnostics = null,
+ };
+
+ const loc = parser.make_diagnostic_location(4);
+ try std.testing.expectEqual(@as(u32, 3), loc.line);
+ try std.testing.expectEqual(@as(u32, 1), loc.column);
+}
+
+test "Version.parse accepts dotted versions" {
+ const version = try hdoc.Version.parse("2.0");
+ try std.testing.expectEqual(@as(u16, 2), version.major);
+ try std.testing.expectEqual(@as(u16, 0), version.minor);
+
+ try std.testing.expectError(error.InvalidValue, hdoc.Version.parse("2"));
+ try std.testing.expectError(error.InvalidValue, hdoc.Version.parse("2."));
+ try std.testing.expectError(error.InvalidValue, hdoc.Version.parse("2.0.1"));
+ try std.testing.expectError(error.InvalidValue, hdoc.Version.parse(".1"));
+ try std.testing.expectError(error.InvalidValue, hdoc.Version.parse("2.a"));
+}
+
+test "Date.parse accepts ISO dates" {
+ const date = try hdoc.Date.parse("2025-12-25");
+ try std.testing.expectEqual(@as(i32, 2025), date.year);
+ try std.testing.expectEqual(@as(u4, 12), date.month);
+ try std.testing.expectEqual(@as(u5, 25), date.day);
+
+ try std.testing.expectError(error.InvalidValue, hdoc.Date.parse("2025-1-01"));
+ try std.testing.expectError(error.InvalidValue, hdoc.Date.parse("2025-13-01"));
+ try std.testing.expectError(error.InvalidValue, hdoc.Date.parse("2025-12-32"));
+ try std.testing.expectError(error.InvalidValue, hdoc.Date.parse("1-01-01"));
+}
+
+test "Time.parse accepts ISO times with zones" {
+ const utc = try hdoc.Time.parse("22:30:46Z", null);
+ try std.testing.expectEqual(@as(u5, 22), utc.hour);
+ try std.testing.expectEqual(@as(u6, 30), utc.minute);
+ try std.testing.expectEqual(@as(u6, 46), utc.second);
+ try std.testing.expectEqual(@as(u20, 0), utc.microsecond);
+ try std.testing.expectEqual(.utc, utc.timezone);
+
+ const utc_hint = try hdoc.Time.parse("22:30:46", .utc);
+ try std.testing.expectEqual(@as(u5, 22), utc_hint.hour);
+ try std.testing.expectEqual(@as(u6, 30), utc_hint.minute);
+ try std.testing.expectEqual(@as(u6, 46), utc_hint.second);
+ try std.testing.expectEqual(@as(u20, 0), utc_hint.microsecond);
+ try std.testing.expectEqual(.utc, utc_hint.timezone);
+
+ const fractional = try hdoc.Time.parse("22:30:46.136-01:00", null);
+ try std.testing.expectEqual(@as(u20, 136_000), fractional.microsecond);
+ try std.testing.expectEqual(try hdoc.TimeZoneOffset.from_hhmm(-1, 0), fractional.timezone);
+
+ const fractional_hint = try hdoc.Time.parse("22:30:46.136", try .parse("+01:30"));
+ try std.testing.expectEqual(@as(u20, 136_000), fractional_hint.microsecond);
+ try std.testing.expectEqual(@as(hdoc.TimeZoneOffset, @enumFromInt(90)), fractional_hint.timezone);
+
+ const nanos = try hdoc.Time.parse("21:30:46.136797358-05:30", null);
+ try std.testing.expectEqual(@as(u20, 136_797), nanos.microsecond);
+ try std.testing.expectEqual(@as(hdoc.TimeZoneOffset, @enumFromInt(-330)), nanos.timezone);
+
+ try std.testing.expectError(error.InvalidValue, hdoc.Time.parse("21:30:46,1Z", null));
+ try std.testing.expectError(error.MissingTimezone, hdoc.Time.parse("22:30:46", null));
+ try std.testing.expectError(error.InvalidValue, hdoc.Time.parse("24:00:00Z", null));
+ try std.testing.expectError(error.InvalidValue, hdoc.Time.parse("23:60:00Z", null));
+ try std.testing.expectError(error.InvalidValue, hdoc.Time.parse("23:59:60Z", null));
+ try std.testing.expectError(error.InvalidValue, hdoc.Time.parse("23:59:59.1234Z", null));
+}
+
+test "DateTime.parse accepts ISO date-time" {
+ const datetime = try hdoc.DateTime.parse("2025-12-25T22:31:50.13+01:00", null);
+ try std.testing.expectEqual(@as(i32, 2025), datetime.date.year);
+ try std.testing.expectEqual(@as(u4, 12), datetime.date.month);
+ try std.testing.expectEqual(@as(u5, 25), datetime.date.day);
+ try std.testing.expectEqual(@as(u5, 22), datetime.time.hour);
+ try std.testing.expectEqual(@as(u6, 31), datetime.time.minute);
+ try std.testing.expectEqual(@as(u6, 50), datetime.time.second);
+ try std.testing.expectEqual(@as(u20, 130_000), datetime.time.microsecond);
+ try std.testing.expectEqual(@as(hdoc.TimeZoneOffset, @enumFromInt(60)), datetime.time.timezone);
+
+ try std.testing.expectError(error.InvalidValue, hdoc.DateTime.parse("2025-12-25 22:31:50Z", null));
+}
+
+test "diagnostics for missing language and empty image attributes" {
+ var diagnostics: hdoc.Diagnostics = .init(std.testing.allocator);
+ defer diagnostics.deinit();
+
+ const source =
+ \\hdoc(version="2.0");
+ \\img(path="", alt="");
+ ;
+
+ var doc = try hdoc.parse(std.testing.allocator, source, &diagnostics);
+ defer doc.deinit();
+
+ var saw_missing_lang = false;
+ var saw_empty_path = false;
+ var saw_empty_alt = false;
+
+ for (diagnostics.items.items) |item| {
+ switch (item.code) {
+ .missing_document_language => saw_missing_lang = true,
+ .empty_attribute => |ctx| {
+ if (ctx.type == .img and std.mem.eql(u8, ctx.name, "path")) {
+ saw_empty_path = true;
+ }
+ if (ctx.type == .img and std.mem.eql(u8, ctx.name, "alt")) {
+ saw_empty_alt = true;
+ }
+ },
+ else => {},
+ }
+ }
+
+ try std.testing.expect(saw_missing_lang);
+ try std.testing.expect(saw_empty_path);
+ try std.testing.expect(saw_empty_alt);
+}
+
+test "diagnostics for missing timezone and unknown id" {
+ var diagnostics: hdoc.Diagnostics = .init(std.testing.allocator);
+ defer diagnostics.deinit();
+
+ const source =
+ \\hdoc(version="2.0");
+ \\p{ \time"12:00:00" \ref(ref="missing"){missing} }
+ ;
+
+ var doc = try hdoc.parse(std.testing.allocator, source, &diagnostics);
+ defer doc.deinit();
+
+ var saw_missing_timezone = false;
+ var saw_unknown_id = false;
+
+ for (diagnostics.items.items) |item| {
+ switch (item.code) {
+ .missing_timezone => saw_missing_timezone = true,
+ .unknown_id => |ctx| {
+ if (std.mem.eql(u8, ctx.ref, "missing")) {
+ saw_unknown_id = true;
+ }
+ },
+ else => {},
+ }
+ }
+
+ try std.testing.expect(saw_missing_timezone);
+ try std.testing.expect(saw_unknown_id);
+}
+
+test "diagnostics for tab characters" {
+ var diagnostics: hdoc.Diagnostics = .init(std.testing.allocator);
+ defer diagnostics.deinit();
+
+ const source = "hdoc(version=\"2.0\");\n\tp{ ok }";
+
+ var doc = try hdoc.parse(std.testing.allocator, source, &diagnostics);
+ defer doc.deinit();
+
+ var saw_tab = false;
+
+ for (diagnostics.items.items) |item| {
+ switch (item.code) {
+ .tab_character => saw_tab = true,
+ else => {},
+ }
+ }
+
+ try std.testing.expect(saw_tab);
+}
+
+test "diagnostics for bare carriage return" {
+ var diagnostics: hdoc.Diagnostics = .init(std.testing.allocator);
+ defer diagnostics.deinit();
+
+ const source = "hdoc(version=\"2.0\");\r";
+
+ try std.testing.expectError(error.InvalidUtf8, hdoc.parse(std.testing.allocator, source, &diagnostics));
+
+ var saw_bare_cr = false;
+ for (diagnostics.items.items) |item| {
+ switch (item.code) {
+ .bare_carriage_return => saw_bare_cr = true,
+ else => {},
+ }
+ }
+
+ try std.testing.expect(saw_bare_cr);
+}
+
+test "hdoc header date uses timezone hint for missing zone" {
+ var diagnostics: hdoc.Diagnostics = .init(std.testing.allocator);
+ defer diagnostics.deinit();
+
+ const source = "hdoc(version=\"2.0\",lang=\"en\",tz=\"-01:30\",date=\"2026-01-01T12:00:00\");";
+ var doc = try hdoc.parse(std.testing.allocator, source, &diagnostics);
+ defer doc.deinit();
+
+ try std.testing.expect(!diagnostics.has_error());
+ const parsed = doc.date orelse return error.TestExpectedEqual;
+ try std.testing.expectEqual(@as(i32, 2026), parsed.date.year);
+ try std.testing.expectEqual(@as(u4, 1), parsed.date.month);
+ try std.testing.expectEqual(@as(u5, 1), parsed.date.day);
+ try std.testing.expectEqual(@as(u5, 12), parsed.time.hour);
+ try std.testing.expectEqual(@as(u6, 0), parsed.time.minute);
+ try std.testing.expectEqual(@as(u6, 0), parsed.time.second);
+ try std.testing.expectEqual(@as(u20, 0), parsed.time.microsecond);
+ try std.testing.expectEqual(try hdoc.TimeZoneOffset.parse("-01:30"), parsed.time.timezone);
+}
+
+test "\\date rejects bad body" {
+ try validateDiagnostics(.{}, "hdoc(version=\"2.0\",lang=\"en\"); p { \\date; }", &.{
+ .invalid_date_time_body,
+ });
+ try validateDiagnostics(.{}, "hdoc(version=\"2.0\",lang=\"en\"); p { \\date{start \\em{inner}} }", &.{
+ .invalid_date_time_body,
+ });
}
diff --git a/src/wasm-lsp.zig b/src/wasm-lsp.zig
new file mode 100644
index 0000000..0160f40
--- /dev/null
+++ b/src/wasm-lsp.zig
@@ -0,0 +1,9 @@
+const std = @import("std");
+
+pub export fn _start() void {}
+
+pub export fn hyperdoc_lsp_ping() void {
+ // Placeholder entrypoint for a wasm-based language server.
+ // Real initialization will be wired once the wasm server is implemented.
+ std.mem.doNotOptimizeAway(@as(u32, 0));
+}
diff --git a/src/wasm.zig b/src/wasm.zig
new file mode 100644
index 0000000..fb6001c
--- /dev/null
+++ b/src/wasm.zig
@@ -0,0 +1,238 @@
+const std = @import("std");
+const hyperdoc = @import("hyperdoc");
+
+const LogLevel = enum(u8) { err, warn, info, debug };
+
+extern fn reset_log() void;
+extern fn append_log(ptr: [*]const u8, len: usize) void;
+extern fn flush_log(level: LogLevel) void;
+
+const LogWriter = struct {
+ fn appendWrite(self: LogWriter, chunk: []const u8) error{OutOfMemory}!usize {
+ _ = self;
+ append_log(chunk.ptr, chunk.len);
+ return chunk.len;
+ }
+
+ fn writer(self: LogWriter) std.io.GenericWriter(LogWriter, error{OutOfMemory}, appendWrite) {
+ return .{ .context = self };
+ }
+};
+
+fn log_to_host(
+ comptime level: std.log.Level,
+ comptime _scope: @TypeOf(.enum_literal),
+ comptime format: []const u8,
+ args: anytype,
+) void {
+ _ = _scope;
+
+ reset_log();
+
+ const log_writer = LogWriter{};
+ const writer = log_writer.writer();
+ _ = std.fmt.format(writer, format, args) catch {};
+
+ const mapped: LogLevel = switch (level) {
+ .err => .err,
+ .warn => .warn,
+ .info => .info,
+ .debug => .debug,
+ };
+
+ flush_log(mapped);
+}
+
+fn fixedPageSize() usize {
+ return 4096;
+}
+
+fn zeroRandom(buffer: []u8) void {
+ @memset(buffer, 0);
+}
+
+pub const std_options: std.Options = .{
+ .enable_segfault_handler = false,
+ .logFn = log_to_host,
+ .queryPageSize = fixedPageSize,
+ .cryptoRandomSeed = zeroRandom,
+};
+
+const allocator = std.heap.wasm_allocator;
+
+pub fn panic(message: []const u8, stack_trace: ?*std.builtin.StackTrace, ret_addr: ?usize) noreturn {
+ _ = message;
+ _ = stack_trace;
+ _ = ret_addr;
+ @breakpoint();
+ unreachable;
+}
+
+pub fn main() !void {}
+
+const DiagnosticView = struct {
+ line: u32,
+ column: u32,
+ message: []u8,
+ is_fatal: bool,
+};
+
+var document_buffer: std.array_list.Managed(u8) = std.array_list.Managed(u8).init(allocator);
+var html_buffer: std.array_list.Managed(u8) = std.array_list.Managed(u8).init(allocator);
+var diagnostic_views: std.array_list.Managed(DiagnosticView) = std.array_list.Managed(DiagnosticView).init(allocator);
+var diagnostic_text: std.array_list.Managed(u8) = std.array_list.Managed(u8).init(allocator);
+
+const CountingWriter = struct {
+ count: usize = 0,
+
+ fn write(self: *CountingWriter, bytes: []const u8) error{}!usize {
+ self.count += bytes.len;
+ return bytes.len;
+ }
+
+ fn generic(self: *CountingWriter) std.Io.GenericWriter(*CountingWriter, error{}, write) {
+ return .{ .context = self };
+ }
+};
+
+fn capture_diagnostics(source: *hyperdoc.Diagnostics) !void {
+ diagnostic_views.clearRetainingCapacity();
+ diagnostic_text.clearRetainingCapacity();
+
+ if (source.items.items.len == 0) return;
+
+ var total: usize = 0;
+ for (source.items.items) |diag| {
+ var cw: CountingWriter = .{};
+ _ = diag.code.format(cw.generic()) catch {};
+ total += cw.count;
+ }
+
+ diagnostic_text.ensureTotalCapacityPrecise(total) catch return;
+
+ var diag_writer = diagnostic_text.writer();
+ var adapter_buffer: [256]u8 = undefined;
+ var adapter = diag_writer.any().adaptToNewApi(&adapter_buffer);
+
+ for (source.items.items) |diag| {
+ const start = diagnostic_text.items.len;
+ diag.code.format(&adapter.new_interface) catch {
+ adapter.err = error.WriteFailed;
+ };
+ adapter.new_interface.flush() catch {
+ adapter.err = error.WriteFailed;
+ };
+ if (adapter.err) |_| return;
+
+ const rendered = diagnostic_text.items[start..];
+ try diagnostic_views.append(.{
+ .line = diag.location.line,
+ .column = diag.location.column,
+ .message = rendered,
+ .is_fatal = switch (diag.code.severity()) {
+ .warning => false,
+ .@"error" => true,
+ },
+ });
+ }
+}
+
+export fn hdoc_set_document_len(len: usize) bool {
+ document_buffer.clearRetainingCapacity();
+ document_buffer.items.len = 0;
+
+ if (len == 0) return true;
+
+ document_buffer.ensureTotalCapacityPrecise(len) catch return false;
+ document_buffer.items.len = len;
+ return true;
+}
+
+export fn hdoc_document_ptr() [*]u8 {
+ return document_buffer.items.ptr;
+}
+
+export fn hdoc_process() bool {
+ html_buffer.clearRetainingCapacity();
+ diagnostic_views.clearRetainingCapacity();
+ diagnostic_text.clearRetainingCapacity();
+
+ const source: []const u8 = document_buffer.items;
+
+ var diagnostics = hyperdoc.Diagnostics.init(allocator);
+ defer diagnostics.deinit();
+
+ var parsed = hyperdoc.parse(allocator, source, &diagnostics) catch {
+ capture_diagnostics(&diagnostics) catch {};
+ return false;
+ };
+ defer parsed.deinit();
+
+ if (diagnostics.has_error()) {
+ capture_diagnostics(&diagnostics) catch {};
+ return false;
+ }
+
+ var html_writer = html_buffer.writer();
+ var html_adapter_buffer: [256]u8 = undefined;
+ var html_adapter = html_writer.any().adaptToNewApi(&html_adapter_buffer);
+
+ hyperdoc.render.html5(parsed, &html_adapter.new_interface) catch {
+ html_adapter.err = error.WriteFailed;
+ };
+ html_adapter.new_interface.flush() catch {
+ html_adapter.err = error.WriteFailed;
+ };
+ if (html_adapter.err) |_| {
+ capture_diagnostics(&diagnostics) catch {};
+ return false;
+ }
+
+ capture_diagnostics(&diagnostics) catch {};
+ return true;
+}
+
+export fn hdoc_html_ptr() ?[*]const u8 {
+ if (html_buffer.items.len == 0) return null;
+ return html_buffer.items.ptr;
+}
+
+export fn hdoc_html_len() usize {
+ return html_buffer.items.len;
+}
+
+export fn hdoc_diagnostic_count() usize {
+ return diagnostic_views.items.len;
+}
+
+export fn hdoc_diagnostic_line(index: usize) u32 {
+ if (index >= diagnostic_views.items.len) return 0;
+
+ return diagnostic_views.items[index].line;
+}
+
+export fn hdoc_diagnostic_column(index: usize) u32 {
+ if (index >= diagnostic_views.items.len) return 0;
+
+ return diagnostic_views.items[index].column;
+}
+
+export fn hdoc_diagnostic_fatal(index: usize) bool {
+ if (index >= diagnostic_views.items.len) return false;
+
+ return diagnostic_views.items[index].is_fatal;
+}
+
+export fn hdoc_diagnostic_message_ptr(index: usize) ?[*]const u8 {
+ if (index >= diagnostic_views.items.len) return null;
+
+ if (diagnostic_views.items[index].message.len == 0) return null;
+
+ return diagnostic_views.items[index].message.ptr;
+}
+
+export fn hdoc_diagnostic_message_len(index: usize) usize {
+ if (index >= diagnostic_views.items.len) return 0;
+
+ return diagnostic_views.items[index].message.len;
+}
diff --git a/test/accept/stress.hdoc b/test/accept/stress.hdoc
new file mode 100644
index 0000000..bec3b0f
--- /dev/null
+++ b/test/accept/stress.hdoc
@@ -0,0 +1,74 @@
+hdoc(version="2.0");
+
+p {
+ On Monday at 09:07, the support desk logged a ticket titled "Login loop (again)". The user wrote, "I click
+ 'Sign in', the page flashes, and I'm back where I started." Someone replied, "That sounds like a cookie
+ issue—clear your cache," but the customer insisted they’d already tried: "Chrome, Safari, even a private window."
+ The message ended with an oddly specific note: "It only happens when the Wi-Fi name is 'Office-Guest'." Nobody
+ knew whether to laugh or worry.
+}
+
+p {
+ The product manager’s summary was short but loaded: "We shipped the hotfix; we didn't fix the root
+ cause." In the same breath, she added, "Don't roll back unless you absolutely have to—it's worse." Later,
+ in a longer thread, she used quotes inside quotes: "When QA says 'it’s fine', what they mean is 'it hasn’t
+ exploded yet'." The tone wasn’t cruel, just tired, and the timestamps (11:58, 12:01, 12:03) made it feel like
+ a miniature drama.
+}
+
+p {
+ In the meeting notes (version 3.2.1), someone wrote: The "simple" migration is no longer simple. They listed
+ steps like: export → transform → validate → import, then added a parenthetical aside (which itself contained
+ parentheses): "Use the staging key (not the production key (seriously))". A different person pasted a pseudo-path,
+ /var/tmp/builds/2025-12-23/, and then warned, "If you see `NULL` in the output, don't 'fix' it by replacing it with
+ '0'—that's how we broke reporting last time."
+}
+
+p {
+ When the vendor called, they insisted everything was "within spec"; our engineer disagreed. "Within spec" can mean
+ two opposite things, she said: either the spec is strict, or the spec is meaningless. She pulled up a screenshot and
+ quoted the line: "Error: expected ']' but found '\}'". Then she joked, "At least it’s honest," and forwarded the log
+ snippet with a subject line that read, "Re: Re: RE: Please confirm ASAP!!!" (three exclamation marks included, unfortunately).
+}
+
+p {
+ The draft contract read like a puzzle: "Client shall provide 'reasonable access' to systems," while another clause said,
+ "Provider may request access at any time." Someone circled the phrase "reasonable access" and wrote, "Reasonable for whom?"
+ A lawyer suggested adding: "…as mutually agreed in writing," but the team worried that "in writing" would exclude Slack, email,
+ and tickets—so they proposed: "…in writing (including electronic messages)". Even that sparked debate: does "electronic messages"
+ include chat reactions, like 👍 or ✅?
+}
+
+p {
+ A teammate tried to reproduce the bug and wrote a narrative that sounded like a short story: "I opened the dashboard, clicked
+ 'Reports', then typed 'Q4' into the search field." The UI responded with "No results found" even though the sidebar clearly showed "Q4
+ Forecast". The odd part: if you type "Q4 " (with a trailing space), results appear. He ended the note with, "Yes, I know that sounds
+ fake," and added, "But watch: 'Q4' ≠ 'Q4 '." It’s the sort of thing parsers and humans both hate.
+}
+
+p {
+ The incident timeline included times in different formats—09:15 CET, 08:15 UTC, and "around 8-ish"—which made the postmortem messy.
+ One line said, "Database CPU hit 92% (again)," another said, "CPU was fine; it was I/O." Someone pasted a link:
+ \link(uri="https://example.com/status?from=2025-12-23T08:00:00Z&to=2025-12-23T10:00:00Z"){https://example.com/status?from=2025-12-23T08:00:00Z&to=2025-12-23T10:00:00Z} and then asked, "Why does
+ the graph show a dip at 08:37?" The answer was "maintenance," but the maintenance note was filed under "misc"—not "maintenance."
+}
+
+p {
+ In the customer’s feedback form, the message was polite but pointed: "Your app is great—until it isn't." They described a checkout flow where the
+ total briefly showed €19.99, then flipped to €1,999.00, then back again. "I know it's probably formatting," they wrote, "but seeing that number
+ made me think I'd been scammed." Another sentence used both quote styles: "The label says 'Total', but the tooltip says "Estimated total"." The
+ difference matters when people are anxious.
+}
+
+p {
+ A developer left a comment in the code review: "This is readable, but it's not maintainable." When asked what that meant, he replied, "Readable
+ means I can understand it today; maintainable means I can change it tomorrow without breaking it." He pointed at a line that looked harmless — \mono(syntax="c"){if (a < b && c > d) return;} — and said, "It encodes policy with no explanation." Then, in the same comment, he used
+ markdown-like fragments that shouldn’t be parsed as such: *not emphasis*, *not italics*, and [not a link](just text).
+}
+
+p {
+ Before the release, the checklist included items that were half instruction, half superstition: "Update changelog; tag release; don't forget the
+ 'README' typo." Someone wrote "DONE" in all caps, then later edited it to "done" because the automation treats "DONE" as a keyword. Another item
+ read: "Confirm the 'rollback plan' exists (even if we never use it)." The final note—"If anything feels off, stop"—was simple, but it carried the
+ weight of every prior incident, every "it’s fine," every quiet "…maybe not."
+}
diff --git a/test/accept/workset.hdoc b/test/accept/workset.hdoc
new file mode 100644
index 0000000..72d4f44
--- /dev/null
+++ b/test/accept/workset.hdoc
@@ -0,0 +1,22 @@
+hdoc(version="2.0", lang="en");
+
+h1 "First"
+
+h2 "First.1"
+h2 "First.2"
+h2 "First.3"
+
+p { We can mix \em{emphasis}, \strike{strike}, \mono{monospace} text. Superscript x\sup{2} and subscript x\sub{2} also appear. }
+
+h1 "Second"
+h2 "Second.2"
+h3 "Second.2.first"
+h3 "Second.2.second"
+h3 "Second.2.third"
+h2 "Second.3"
+
+h1 "Third"
+h2 "Third.1"
+h3 "Third.1.first"
+h3 "Third.1.second"
+h3 "Third.1.third"
diff --git a/test/compare.zig b/test/compare.zig
new file mode 100644
index 0000000..69c9b82
--- /dev/null
+++ b/test/compare.zig
@@ -0,0 +1,67 @@
+//!
+//! compare
+//!
+const std = @import("std");
+
+var arena: std.heap.ArenaAllocator = .init(std.heap.page_allocator);
+
+const allocator = arena.allocator();
+
+pub fn main() !u8 {
+ defer arena.deinit();
+
+ const argv = try std.process.argsAlloc(allocator);
+ defer std.process.argsFree(allocator, argv);
+
+ if (argv.len != 3) {
+ std.debug.print("usage: {s} \n", .{argv[0]});
+ return 2;
+ }
+
+ const ground_truth_path = argv[1];
+ const new_input_path = argv[2];
+
+ var files_ok = true;
+ const ground_truth = readFileAlloc(allocator, ground_truth_path, 10 * 1024 * 1024) catch |err| switch (err) {
+ error.FileNotFound => blk: {
+ files_ok = false;
+ break :blk "";
+ },
+ else => |e| return e,
+ };
+ defer allocator.free(ground_truth);
+
+ const new_input = readFileAlloc(allocator, new_input_path, 10 * 1024 * 1024) catch |err| switch (err) {
+ error.FileNotFound => blk: {
+ files_ok = false;
+ break :blk "";
+ },
+ else => |e| return e,
+ };
+ defer allocator.free(new_input);
+
+ // Compare full file contents for now. This keeps the snapshot tests simple and
+ // uses std.testing's string mismatch reporting.
+ std.testing.expectEqualStrings(ground_truth, new_input) catch |err| switch (err) {
+ error.TestExpectedEqual => return 1,
+ else => return err,
+ };
+
+ if (!files_ok)
+ return 1;
+
+ return 0;
+}
+
+fn readFileAlloc(alloc: std.mem.Allocator, path: []const u8, max_bytes: usize) ![]u8 {
+ const file = try openFile(path);
+ defer file.close();
+ return file.readToEndAlloc(alloc, max_bytes);
+}
+
+fn openFile(path: []const u8) !std.fs.File {
+ if (std.fs.path.isAbsolute(path)) {
+ return std.fs.openFileAbsolute(path, .{});
+ }
+ return std.fs.cwd().openFile(path, .{});
+}
diff --git a/test/conformance/accept/header_and_title_order.hdoc b/test/conformance/accept/header_and_title_order.hdoc
new file mode 100644
index 0000000..3357233
--- /dev/null
+++ b/test/conformance/accept/header_and_title_order.hdoc
@@ -0,0 +1,5 @@
+hdoc(version="2.0", lang="en");
+
+title { Proper Order }
+
+p "Body content"
diff --git a/test/conformance/accept/header_and_title_order.yaml b/test/conformance/accept/header_and_title_order.yaml
new file mode 100644
index 0000000..604bdd5
--- /dev/null
+++ b/test/conformance/accept/header_and_title_order.yaml
@@ -0,0 +1,24 @@
+document:
+ version:
+ major: 2
+ minor: 0
+ lang: "en"
+ title:
+ simple: "Proper Order"
+ full:
+ lang: ""
+ content:
+ - [] "Proper Order"
+ author: null
+ date: null
+ toc:
+ level: h1
+ headings: []
+ children: []
+ contents:
+ - paragraph:
+ lang: ""
+ content:
+ - [] "Body content"
+ ids:
+ - null
diff --git a/test/conformance/accept/image_with_required_path.hdoc b/test/conformance/accept/image_with_required_path.hdoc
new file mode 100644
index 0000000..5152870
--- /dev/null
+++ b/test/conformance/accept/image_with_required_path.hdoc
@@ -0,0 +1,3 @@
+hdoc(version="2.0", lang="en");
+
+img(path="media/picture.png", alt="Example figure") { Figure caption }
diff --git a/test/conformance/accept/image_with_required_path.yaml b/test/conformance/accept/image_with_required_path.yaml
new file mode 100644
index 0000000..9376937
--- /dev/null
+++ b/test/conformance/accept/image_with_required_path.yaml
@@ -0,0 +1,21 @@
+document:
+ version:
+ major: 2
+ minor: 0
+ lang: "en"
+ title: null
+ author: null
+ date: null
+ toc:
+ level: h1
+ headings: []
+ children: []
+ contents:
+ - image:
+ lang: ""
+ alt: "Example figure"
+ path: "media/picture.png"
+ content:
+ - [] "Figure caption"
+ ids:
+ - null
diff --git a/test/conformance/accept/inline_escape.hdoc b/test/conformance/accept/inline_escape.hdoc
new file mode 100644
index 0000000..5988ae8
--- /dev/null
+++ b/test/conformance/accept/inline_escape.hdoc
@@ -0,0 +1,3 @@
+hdoc(version="2.0", lang="en");
+
+p { backslash \\ brace-open \{ brace-close \} }
diff --git a/test/conformance/accept/inline_escape.yaml b/test/conformance/accept/inline_escape.yaml
new file mode 100644
index 0000000..4f58ab7
--- /dev/null
+++ b/test/conformance/accept/inline_escape.yaml
@@ -0,0 +1,19 @@
+document:
+ version:
+ major: 2
+ minor: 0
+ lang: "en"
+ title: null
+ author: null
+ date: null
+ toc:
+ level: h1
+ headings: []
+ children: []
+ contents:
+ - paragraph:
+ lang: ""
+ content:
+ - [] "backslash \\ brace-open { brace-close }"
+ ids:
+ - null
diff --git a/test/conformance/accept/no_title_document.hdoc b/test/conformance/accept/no_title_document.hdoc
new file mode 100644
index 0000000..1c046ef
--- /dev/null
+++ b/test/conformance/accept/no_title_document.hdoc
@@ -0,0 +1,3 @@
+hdoc(version="2.0", lang="en");
+
+p "Untitled body"
diff --git a/test/conformance/accept/no_title_document.yaml b/test/conformance/accept/no_title_document.yaml
new file mode 100644
index 0000000..4be7da4
--- /dev/null
+++ b/test/conformance/accept/no_title_document.yaml
@@ -0,0 +1,19 @@
+document:
+ version:
+ major: 2
+ minor: 0
+ lang: "en"
+ title: null
+ author: null
+ date: null
+ toc:
+ level: h1
+ headings: []
+ children: []
+ contents:
+ - paragraph:
+ lang: ""
+ content:
+ - [] "Untitled body"
+ ids:
+ - null
diff --git a/test/conformance/accept/title_header_redundant.hdoc b/test/conformance/accept/title_header_redundant.hdoc
new file mode 100644
index 0000000..acd0c0a
--- /dev/null
+++ b/test/conformance/accept/title_header_redundant.hdoc
@@ -0,0 +1,5 @@
+hdoc(version="2.0", lang="en", title="Header Title");
+
+title { Header Title }
+
+p "body"
diff --git a/test/conformance/accept/title_header_redundant.yaml b/test/conformance/accept/title_header_redundant.yaml
new file mode 100644
index 0000000..5e82b26
--- /dev/null
+++ b/test/conformance/accept/title_header_redundant.yaml
@@ -0,0 +1,24 @@
+document:
+ version:
+ major: 2
+ minor: 0
+ lang: "en"
+ title:
+ simple: "Header Title"
+ full:
+ lang: ""
+ content:
+ - [] "Header Title"
+ author: null
+ date: null
+ toc:
+ level: h1
+ headings: []
+ children: []
+ contents:
+ - paragraph:
+ lang: ""
+ content:
+ - [] "body"
+ ids:
+ - null
diff --git a/test/conformance/reject/container_children.diag b/test/conformance/reject/container_children.diag
new file mode 100644
index 0000000..9d4bba4
--- /dev/null
+++ b/test/conformance/reject/container_children.diag
@@ -0,0 +1,29 @@
+[
+ {
+ "code": {
+ "illegal_child_item": {}
+ },
+ "location": {
+ "line": 5,
+ "column": 5
+ }
+ },
+ {
+ "code": {
+ "list_body_required": {}
+ },
+ "location": {
+ "line": 4,
+ "column": 3
+ }
+ },
+ {
+ "code": {
+ "illegal_child_item": {}
+ },
+ "location": {
+ "line": 10,
+ "column": 3
+ }
+ }
+]
diff --git a/test/conformance/reject/container_children.hdoc b/test/conformance/reject/container_children.hdoc
new file mode 100644
index 0000000..71ce4ad
--- /dev/null
+++ b/test/conformance/reject/container_children.hdoc
@@ -0,0 +1,11 @@
+hdoc(version="2.0", lang="en");
+
+ul {
+ li {
+ h1 "Heading child"
+ }
+}
+
+note {
+ h1 "Inside note"
+}
diff --git a/test/conformance/reject/duplicate_header.diag b/test/conformance/reject/duplicate_header.diag
new file mode 100644
index 0000000..79d5d3b
--- /dev/null
+++ b/test/conformance/reject/duplicate_header.diag
@@ -0,0 +1,20 @@
+[
+ {
+ "code": {
+ "misplaced_hdoc_header": {}
+ },
+ "location": {
+ "line": 3,
+ "column": 1
+ }
+ },
+ {
+ "code": {
+ "duplicate_hdoc_header": {}
+ },
+ "location": {
+ "line": 3,
+ "column": 1
+ }
+ }
+]
diff --git a/test/conformance/reject/duplicate_header.hdoc b/test/conformance/reject/duplicate_header.hdoc
new file mode 100644
index 0000000..faeb809
--- /dev/null
+++ b/test/conformance/reject/duplicate_header.hdoc
@@ -0,0 +1,5 @@
+hdoc(version="2.0", lang="en");
+
+hdoc(version="2.0", lang="en");
+
+p "Duplicate headers"
diff --git a/test/conformance/reject/hdoc_body_non_empty.diag b/test/conformance/reject/hdoc_body_non_empty.diag
new file mode 100644
index 0000000..1b0ff8b
--- /dev/null
+++ b/test/conformance/reject/hdoc_body_non_empty.diag
@@ -0,0 +1,11 @@
+[
+ {
+ "code": {
+ "non_empty_hdoc_body": {}
+ },
+ "location": {
+ "line": 1,
+ "column": 1
+ }
+ }
+]
diff --git a/test/conformance/reject/hdoc_body_non_empty.hdoc b/test/conformance/reject/hdoc_body_non_empty.hdoc
new file mode 100644
index 0000000..cf1aa2a
--- /dev/null
+++ b/test/conformance/reject/hdoc_body_non_empty.hdoc
@@ -0,0 +1 @@
+hdoc(version="2.0", lang="en") "not empty"
diff --git a/test/conformance/reject/heading_sequence.diag b/test/conformance/reject/heading_sequence.diag
new file mode 100644
index 0000000..02e90f2
--- /dev/null
+++ b/test/conformance/reject/heading_sequence.diag
@@ -0,0 +1,59 @@
+[
+ {
+ "code": {
+ "invalid_heading_sequence": {
+ "level": "h3",
+ "missing": "h2"
+ }
+ },
+ "location": {
+ "line": 3,
+ "column": 1
+ }
+ },
+ {
+ "code": {
+ "invalid_heading_sequence": {
+ "level": "h3",
+ "missing": "h2"
+ }
+ },
+ "location": {
+ "line": 5,
+ "column": 1
+ }
+ },
+ {
+ "code": {
+ "automatic_heading_insertion": {
+ "level": "h1"
+ }
+ },
+ "location": {
+ "line": 3,
+ "column": 1
+ }
+ },
+ {
+ "code": {
+ "automatic_heading_insertion": {
+ "level": "h2"
+ }
+ },
+ "location": {
+ "line": 3,
+ "column": 1
+ }
+ },
+ {
+ "code": {
+ "automatic_heading_insertion": {
+ "level": "h2"
+ }
+ },
+ "location": {
+ "line": 5,
+ "column": 1
+ }
+ }
+]
diff --git a/test/conformance/reject/heading_sequence.hdoc b/test/conformance/reject/heading_sequence.hdoc
new file mode 100644
index 0000000..c8c9b43
--- /dev/null
+++ b/test/conformance/reject/heading_sequence.hdoc
@@ -0,0 +1,5 @@
+hdoc(version="2.0", lang="en");
+
+h3 "Third level first"
+h1 "Top"
+h3 "Third without second"
diff --git a/test/conformance/reject/image_missing_path.diag b/test/conformance/reject/image_missing_path.diag
new file mode 100644
index 0000000..9cc8cbe
--- /dev/null
+++ b/test/conformance/reject/image_missing_path.diag
@@ -0,0 +1,14 @@
+[
+ {
+ "code": {
+ "missing_attribute": {
+ "type": "img",
+ "name": "path"
+ }
+ },
+ "location": {
+ "line": 3,
+ "column": 1
+ }
+ }
+]
diff --git a/test/conformance/reject/image_missing_path.hdoc b/test/conformance/reject/image_missing_path.hdoc
new file mode 100644
index 0000000..3051dc6
--- /dev/null
+++ b/test/conformance/reject/image_missing_path.hdoc
@@ -0,0 +1,3 @@
+hdoc(version="2.0", lang="en");
+
+img { Figure caption }
diff --git a/test/conformance/reject/inline_identifier_dash.diag b/test/conformance/reject/inline_identifier_dash.diag
new file mode 100644
index 0000000..3c8dfb8
--- /dev/null
+++ b/test/conformance/reject/inline_identifier_dash.diag
@@ -0,0 +1,13 @@
+[
+ {
+ "code": {
+ "invalid_identifier_character": {
+ "char": 45
+ }
+ },
+ "location": {
+ "line": 3,
+ "column": 9
+ }
+ }
+]
diff --git a/test/conformance/reject/inline_identifier_dash.hdoc b/test/conformance/reject/inline_identifier_dash.hdoc
new file mode 100644
index 0000000..1948b61
--- /dev/null
+++ b/test/conformance/reject/inline_identifier_dash.hdoc
@@ -0,0 +1,3 @@
+hdoc(version="2.0", lang="en");
+
+p { \bad-name "ok" }
diff --git a/test/conformance/reject/missing_header.diag b/test/conformance/reject/missing_header.diag
new file mode 100644
index 0000000..901fdd5
--- /dev/null
+++ b/test/conformance/reject/missing_header.diag
@@ -0,0 +1,11 @@
+[
+ {
+ "code": {
+ "missing_hdoc_header": {}
+ },
+ "location": {
+ "line": 1,
+ "column": 1
+ }
+ }
+]
diff --git a/test/conformance/reject/missing_header.hdoc b/test/conformance/reject/missing_header.hdoc
new file mode 100644
index 0000000..f942349
--- /dev/null
+++ b/test/conformance/reject/missing_header.hdoc
@@ -0,0 +1 @@
+p "No header present"
diff --git a/test/conformance/reject/nested_top_level.diag b/test/conformance/reject/nested_top_level.diag
new file mode 100644
index 0000000..76ea6e6
--- /dev/null
+++ b/test/conformance/reject/nested_top_level.diag
@@ -0,0 +1,11 @@
+[
+ {
+ "code": {
+ "illegal_child_item": {}
+ },
+ "location": {
+ "line": 4,
+ "column": 3
+ }
+ }
+]
diff --git a/test/conformance/reject/nested_top_level.hdoc b/test/conformance/reject/nested_top_level.hdoc
new file mode 100644
index 0000000..b418705
--- /dev/null
+++ b/test/conformance/reject/nested_top_level.hdoc
@@ -0,0 +1,5 @@
+hdoc(version="2.0", lang="en");
+
+note {
+ h1 "Nested heading"
+}
diff --git a/test/conformance/reject/ref_in_heading.diag b/test/conformance/reject/ref_in_heading.diag
new file mode 100644
index 0000000..68602ea
--- /dev/null
+++ b/test/conformance/reject/ref_in_heading.diag
@@ -0,0 +1,13 @@
+[
+ {
+ "code": {
+ "inline_not_allowed": {
+ "node_type": "\\ref"
+ }
+ },
+ "location": {
+ "line": 5,
+ "column": 14
+ }
+ }
+]
diff --git a/test/conformance/reject/ref_in_heading.hdoc b/test/conformance/reject/ref_in_heading.hdoc
new file mode 100644
index 0000000..fcd2ace
--- /dev/null
+++ b/test/conformance/reject/ref_in_heading.hdoc
@@ -0,0 +1,5 @@
+hdoc(version="2.0", lang="en");
+
+p(id="target") "Target"
+
+h1 { Heading \ref(ref="target") "see"; }
diff --git a/test/conformance/reject/string_cr_escape.diag b/test/conformance/reject/string_cr_escape.diag
new file mode 100644
index 0000000..ac57a8b
--- /dev/null
+++ b/test/conformance/reject/string_cr_escape.diag
@@ -0,0 +1,13 @@
+[
+ {
+ "code": {
+ "illegal_character": {
+ "codepoint": 13
+ }
+ },
+ "location": {
+ "line": 3,
+ "column": 8
+ }
+ }
+]
diff --git a/test/conformance/reject/string_cr_escape.hdoc b/test/conformance/reject/string_cr_escape.hdoc
new file mode 100644
index 0000000..204b3de
--- /dev/null
+++ b/test/conformance/reject/string_cr_escape.hdoc
@@ -0,0 +1,3 @@
+hdoc(version="2.0", lang="en");
+
+p "line\rline"
diff --git a/test/conformance/reject/time_relative_fmt.diag b/test/conformance/reject/time_relative_fmt.diag
new file mode 100644
index 0000000..decc5a5
--- /dev/null
+++ b/test/conformance/reject/time_relative_fmt.diag
@@ -0,0 +1,11 @@
+[
+ {
+ "code": {
+ "invalid_date_time_fmt": {}
+ },
+ "location": {
+ "line": 3,
+ "column": 15
+ }
+ }
+]
diff --git a/test/conformance/reject/time_relative_fmt.hdoc b/test/conformance/reject/time_relative_fmt.hdoc
new file mode 100644
index 0000000..767ed26
--- /dev/null
+++ b/test/conformance/reject/time_relative_fmt.hdoc
@@ -0,0 +1,3 @@
+hdoc(version="2.0", lang="en", tz="+00:00");
+
+p { \time(fmt="relative") "12:00:00Z" }
diff --git a/test/conformance/reject/title_after_content.diag b/test/conformance/reject/title_after_content.diag
new file mode 100644
index 0000000..8d0abcb
--- /dev/null
+++ b/test/conformance/reject/title_after_content.diag
@@ -0,0 +1,11 @@
+[
+ {
+ "code": {
+ "misplaced_title_block": {}
+ },
+ "location": {
+ "line": 5,
+ "column": 1
+ }
+ }
+]
diff --git a/test/conformance/reject/title_after_content.hdoc b/test/conformance/reject/title_after_content.hdoc
new file mode 100644
index 0000000..8aa7651
--- /dev/null
+++ b/test/conformance/reject/title_after_content.hdoc
@@ -0,0 +1,5 @@
+hdoc(version="2.0", lang="en");
+
+p "First content"
+
+title { Late Title }
diff --git a/test/snapshot/AGENTS.md b/test/snapshot/AGENTS.md
new file mode 100644
index 0000000..b79d9ed
--- /dev/null
+++ b/test/snapshot/AGENTS.md
@@ -0,0 +1,7 @@
+# AGENTS
+
+These files are HTML5 renderer golden tests.
+
+- Each `.hdoc` example here is paired with a `.html` file rendered by `./zig-out/bin/hyperdoc`.
+- When changing the HTML5 renderer, update the corresponding `.html` outputs to match the new behavior.
+- Keep scenarios focused: each example should target specific constructs (paragraph styles, nesting, tables, media/toc, etc.).
diff --git a/test/snapshot/admonition_blocks.hdoc b/test/snapshot/admonition_blocks.hdoc
new file mode 100644
index 0000000..3636623
--- /dev/null
+++ b/test/snapshot/admonition_blocks.hdoc
@@ -0,0 +1,17 @@
+hdoc(version="2.0", title="Admonition Blocks", lang="en");
+
+h1 "Admonitions as Containers"
+
+note{
+ p "A note can span multiple blocks."
+ ul{
+ li "Lists are allowed."
+ li "They render inside the note container."
+ }
+}
+
+danger "String bodies become paragraphs inside the container."
+
+spoiler:
+| Hidden detail
+| spans multiple lines.
diff --git a/test/snapshot/admonition_blocks.html b/test/snapshot/admonition_blocks.html
new file mode 100644
index 0000000..a298e1e
--- /dev/null
+++ b/test/snapshot/admonition_blocks.html
@@ -0,0 +1,22 @@
+
+ Admonition Blocks
+
+§1 Admonitions as Containers
+
+ A note can span multiple blocks.
+
+ -
+
Lists are allowed.
+
+ -
+
They render inside the note container.
+
+
+
+
+ String bodies become paragraphs inside the container.
+
+
+ Hidden detail
+spans multiple lines.
+
diff --git a/test/snapshot/admonition_blocks.yaml b/test/snapshot/admonition_blocks.yaml
new file mode 100644
index 0000000..d40b0fc
--- /dev/null
+++ b/test/snapshot/admonition_blocks.yaml
@@ -0,0 +1,73 @@
+document:
+ version:
+ major: 2
+ minor: 0
+ lang: "en"
+ title:
+ simple: "Admonition Blocks"
+ full:
+ lang: "en"
+ content:
+ - [] "Admonition Blocks"
+ author: null
+ date: null
+ toc:
+ level: h1
+ headings:
+ - 0
+ children:
+ -
+ level: h2
+ headings: []
+ children: []
+ contents:
+ - heading:
+ level: h1
+ lang: ""
+ content:
+ - [] "Admonitions as Containers"
+ - admonition:
+ kind: note
+ lang: ""
+ content:
+ - paragraph:
+ lang: ""
+ content:
+ - [] "A note can span multiple blocks."
+ - list:
+ lang: ""
+ first: null
+ items:
+ - lang: ""
+ content:
+ - paragraph:
+ lang: ""
+ content:
+ - [] "Lists are allowed."
+ - lang: ""
+ content:
+ - paragraph:
+ lang: ""
+ content:
+ - [] "They render inside the note container."
+ - admonition:
+ kind: danger
+ lang: ""
+ content:
+ - paragraph:
+ lang: ""
+ content:
+ - [] "String bodies become paragraphs inside the container."
+ - admonition:
+ kind: spoiler
+ lang: ""
+ content:
+ - paragraph:
+ lang: ""
+ content:
+ - [] "Hidden detail\nspans multiple lines."
+ ids:
+ - null
+ - null
+ - null
+ - null
diff --git a/test/snapshot/document_header.hdoc b/test/snapshot/document_header.hdoc
new file mode 100644
index 0000000..3366121
--- /dev/null
+++ b/test/snapshot/document_header.hdoc
@@ -0,0 +1,5 @@
+hdoc(version="2.0", title="Metadata Title", date="2024-08-16T09:30:00", lang="en", tz="+02:00");
+
+title { Metadata Title From Block }
+
+p { This document uses header metadata and a title block without any explicit headings. }
diff --git a/test/snapshot/document_header.html b/test/snapshot/document_header.html
new file mode 100644
index 0000000..f5fdb71
--- /dev/null
+++ b/test/snapshot/document_header.html
@@ -0,0 +1,5 @@
+
+ Metadata Title From Block
+
+
+This document uses header metadata and a title block without any explicit headings.
diff --git a/test/snapshot/document_header.yaml b/test/snapshot/document_header.yaml
new file mode 100644
index 0000000..eae9439
--- /dev/null
+++ b/test/snapshot/document_header.yaml
@@ -0,0 +1,33 @@
+document:
+ version:
+ major: 2
+ minor: 0
+ lang: "en"
+ title:
+ simple: "Metadata Title From Block"
+ full:
+ lang: ""
+ content:
+ - [] "Metadata Title From Block"
+ author: null
+ date:
+ date:
+ year: 2024
+ month: 8
+ day: 16
+ time:
+ hour: 9
+ minute: 30
+ second: 0
+ microsecond: 0
+ toc:
+ level: h1
+ headings: []
+ children: []
+ contents:
+ - paragraph:
+ lang: ""
+ content:
+ - [] "This document uses header metadata and a title block without any explicit headings."
+ ids:
+ - null
diff --git a/test/snapshot/footnotes.hdoc b/test/snapshot/footnotes.hdoc
new file mode 100644
index 0000000..ae66d1b
--- /dev/null
+++ b/test/snapshot/footnotes.hdoc
@@ -0,0 +1,7 @@
+hdoc(version="2.0",lang="en");
+title{Footnotes Demo}
+p{Intro with footnote\footnote{Footnote text} and citation\footnote(kind="citation",key="ref1"){Citation text}.}
+footnotes;
+p{Next section references \footnote(ref="ref1"); and adds another\footnote{Second footnote}.}
+footnotes(kind="citation");
+footnotes;
diff --git a/test/snapshot/footnotes.html b/test/snapshot/footnotes.html
new file mode 100644
index 0000000..8ecbc1d
--- /dev/null
+++ b/test/snapshot/footnotes.html
@@ -0,0 +1,31 @@
+
+ Footnotes Demo
+
+Intro with footnote1 and citation1.
+
+
+ -
+
Footnote text
+
+
+
+ -
+
Citation text
+
+
+
+Next section references 1 and adds another2.
+
+
+ -
+
Citation text
+
+
+
+
+
+ -
+
Second footnote
+
+
+
diff --git a/test/snapshot/footnotes.yaml b/test/snapshot/footnotes.yaml
new file mode 100644
index 0000000..5340dde
--- /dev/null
+++ b/test/snapshot/footnotes.yaml
@@ -0,0 +1,69 @@
+document:
+ version:
+ major: 2
+ minor: 0
+ lang: "en"
+ title:
+ simple: "Footnotes Demo"
+ full:
+ lang: ""
+ content:
+ - [] "Footnotes Demo"
+ author: null
+ date: null
+ toc:
+ level: h1
+ headings: []
+ children: []
+ contents:
+ - paragraph:
+ lang: ""
+ content:
+ - [] "Intro with footnote"
+ - [] "footnote:footnote:1"
+ - [] " and citation"
+ - [] "footnote:citation:1"
+ - [] "."
+ - footnotes:
+ lang: ""
+ entries:
+ - index: 1
+ kind: footnote
+ lang: ""
+ content:
+ - [] "Footnote text"
+ - index: 1
+ kind: citation
+ lang: ""
+ content:
+ - [] "Citation text"
+ - paragraph:
+ lang: ""
+ content:
+ - [] "Next section references "
+ - [] "footnote:citation:1"
+ - [] " and adds another"
+ - [] "footnote:footnote:2"
+ - [] "."
+ - footnotes:
+ lang: ""
+ entries:
+ - index: 1
+ kind: citation
+ lang: ""
+ content:
+ - [] "Citation text"
+ - footnotes:
+ lang: ""
+ entries:
+ - index: 2
+ kind: footnote
+ lang: ""
+ content:
+ - [] "Second footnote"
+ ids:
+ - null
+ - null
+ - null
+ - null
+ - null
diff --git a/test/snapshot/media_and_toc.hdoc b/test/snapshot/media_and_toc.hdoc
new file mode 100644
index 0000000..a4f0cf4
--- /dev/null
+++ b/test/snapshot/media_and_toc.hdoc
@@ -0,0 +1,21 @@
+hdoc(version="2.0", title="Media and TOC", lang="en", tz="+00:00");
+
+h1(id="intro") "Media and TOC"
+
+toc(depth="3");
+
+h2(id="code") "Preformatted"
+
+pre(syntax="python") { print("hello world") }
+
+h2(id="figure") "Figure"
+
+img(id="fig-code",path="./example.png",alt="Example figure") { Figure caption text. }
+
+h2(id="dates") "Dates and Times"
+
+p { Today is \date(fmt="iso"){2024-03-01}. }
+
+p { The meeting is at \time(fmt="long"){14:30:45+00:00}. }
+
+p { Release happens on \datetime(fmt="short"){2024-04-15T08:00:00+00:00}. }
diff --git a/test/snapshot/media_and_toc.html b/test/snapshot/media_and_toc.html
new file mode 100644
index 0000000..5cf5739
--- /dev/null
+++ b/test/snapshot/media_and_toc.html
@@ -0,0 +1,26 @@
+
+ Media and TOC
+
+§1 Media and TOC
+
+§1.1 Preformatted
+ print("hello world")
+§1.2 Figure
+
+
+ Figure caption text.
+
+§1.3 Dates and Times
+Today is .
+The meeting is at .
+Release happens on .
diff --git a/test/snapshot/media_and_toc.yaml b/test/snapshot/media_and_toc.yaml
new file mode 100644
index 0000000..cd0bd24
--- /dev/null
+++ b/test/snapshot/media_and_toc.yaml
@@ -0,0 +1,101 @@
+document:
+ version:
+ major: 2
+ minor: 0
+ lang: "en"
+ title:
+ simple: "Media and TOC"
+ full:
+ lang: "en"
+ content:
+ - [] "Media and TOC"
+ author: null
+ date: null
+ toc:
+ level: h1
+ headings:
+ - 0
+ children:
+ -
+ level: h2
+ headings:
+ - 2
+ - 4
+ - 6
+ children:
+ -
+ level: h3
+ headings: []
+ children: []
+ -
+ level: h3
+ headings: []
+ children: []
+ -
+ level: h3
+ headings: []
+ children: []
+ contents:
+ - heading:
+ level: h1
+ lang: ""
+ content:
+ - [] "Media and TOC"
+ - toc:
+ lang: ""
+ depth: 3
+ - heading:
+ level: h2
+ lang: ""
+ content:
+ - [] "Preformatted"
+ - preformatted:
+ lang: ""
+ syntax: "python"
+ content:
+ - [] " print(\"hello world\") "
+ - heading:
+ level: h2
+ lang: ""
+ content:
+ - [] "Figure"
+ - image:
+ lang: ""
+ alt: "Example figure"
+ path: "./example.png"
+ content:
+ - [] "Figure caption text."
+ - heading:
+ level: h2
+ lang: ""
+ content:
+ - [] "Dates and Times"
+ - paragraph:
+ lang: ""
+ content:
+ - [] "Today is "
+ - [] "date:+2024-03-01@iso"
+ - [] "."
+ - paragraph:
+ lang: ""
+ content:
+ - [] "The meeting is at "
+ - [] "time:14:30:45@long"
+ - [] "."
+ - paragraph:
+ lang: ""
+ content:
+ - [] "Release happens on "
+ - [] "datetime:+2024-04-15T08:00:00"
+ - [] "."
+ ids:
+ - "intro"
+ - null
+ - "code"
+ - null
+ - "figure"
+ - "fig-code"
+ - "dates"
+ - null
+ - null
+ - null
diff --git a/test/snapshot/nesting_and_inlines.hdoc b/test/snapshot/nesting_and_inlines.hdoc
new file mode 100644
index 0000000..fcfd8fc
--- /dev/null
+++ b/test/snapshot/nesting_and_inlines.hdoc
@@ -0,0 +1,25 @@
+hdoc(version="2.0", title="Nesting and Inlines", lang="en");
+
+h1(id="top") "Nesting and Inline Styling"
+
+p "This document exercises inline formatting and nested lists."
+
+p { We can mix \em{emphasis}, \strike{strike}, \mono{monospace} text. Superscript x\sup{2} and subscript x\sub{2} also appear. }
+
+p { Links point to \ref(ref="top"){local anchors} or \link(uri="https://example.com"){external sites}. }
+
+h2(id="formatted") {Formatted \em{Heading}}
+
+p { Empty-body references become \ref(ref="formatted",fmt="full"); \ref(ref="formatted",fmt="name"); and \ref(ref="formatted",fmt="index"); }
+
+ul {
+ li { p "Top-level item one" }
+ li {
+ p "Top-level item two with nested list"
+ ol(first="1") {
+ li "Nested ordered item A"
+ li "Nested ordered item B"
+ }
+ }
+ li { p "Top-level item three" }
+}
diff --git a/test/snapshot/nesting_and_inlines.html b/test/snapshot/nesting_and_inlines.html
new file mode 100644
index 0000000..1ee2e94
--- /dev/null
+++ b/test/snapshot/nesting_and_inlines.html
@@ -0,0 +1,28 @@
+
+ Nesting and Inlines
+
+§1 Nesting and Inline Styling
+This document exercises inline formatting and nested lists.
+We can mix emphasis, strike, monospace text. Superscript x2 and subscript x2 also appear.
+Links point to local anchors or external sites.
+§1.1 Formatted Heading
+Empty-body references become 1.1. Formatted Heading Formatted Heading and 1.1.
+
+ -
+
Top-level item one
+
+ -
+
Top-level item two with nested list
+
+ -
+
Nested ordered item A
+
+ -
+
Nested ordered item B
+
+
+
+ -
+
Top-level item three
+
+
diff --git a/test/snapshot/nesting_and_inlines.yaml b/test/snapshot/nesting_and_inlines.yaml
new file mode 100644
index 0000000..498844e
--- /dev/null
+++ b/test/snapshot/nesting_and_inlines.yaml
@@ -0,0 +1,121 @@
+document:
+ version:
+ major: 2
+ minor: 0
+ lang: "en"
+ title:
+ simple: "Nesting and Inlines"
+ full:
+ lang: "en"
+ content:
+ - [] "Nesting and Inlines"
+ author: null
+ date: null
+ toc:
+ level: h1
+ headings:
+ - 0
+ children:
+ -
+ level: h2
+ headings:
+ - 4
+ children:
+ -
+ level: h3
+ headings: []
+ children: []
+ contents:
+ - heading:
+ level: h1
+ lang: ""
+ content:
+ - [] "Nesting and Inline Styling"
+ - paragraph:
+ lang: ""
+ content:
+ - [] "This document exercises inline formatting and nested lists."
+ - paragraph:
+ lang: ""
+ content:
+ - [] "We can mix "
+ - [em] "emphasis"
+ - [] ", "
+ - [strike] "strike"
+ - [] ", "
+ - [mono] "monospace"
+ - [] " text. Superscript x"
+ - [position="superscript"] "2"
+ - [] " and subscript x"
+ - [position="subscript"] "2"
+ - [] " also appear."
+ - paragraph:
+ lang: ""
+ content:
+ - [] "Links point to "
+ - [link="ref:top#0"] "local anchors"
+ - [] " or "
+ - [link="uri:https://example.com"] "external sites"
+ - [] "."
+ - heading:
+ level: h2
+ lang: ""
+ content:
+ - [] "Formatted "
+ - [em] "Heading"
+ - paragraph:
+ lang: ""
+ content:
+ - [] "Empty-body references become "
+ - [link="ref:formatted#4"] "ref:formatted@full#4"
+ - [] " "
+ - [link="ref:formatted#4"] "ref:formatted@name#4"
+ - [] " and "
+ - [link="ref:formatted#4"] "ref:formatted@index#4"
+ - [] ""
+ - list:
+ lang: ""
+ first: null
+ items:
+ - lang: ""
+ content:
+ - paragraph:
+ lang: ""
+ content:
+ - [] "Top-level item one"
+ - lang: ""
+ content:
+ - paragraph:
+ lang: ""
+ content:
+ - [] "Top-level item two with nested list"
+ - list:
+ lang: ""
+ first: 1
+ items:
+ - lang: ""
+ content:
+ - paragraph:
+ lang: ""
+ content:
+ - [] "Nested ordered item A"
+ - lang: ""
+ content:
+ - paragraph:
+ lang: ""
+ content:
+ - [] "Nested ordered item B"
+ - lang: ""
+ content:
+ - paragraph:
+ lang: ""
+ content:
+ - [] "Top-level item three"
+ ids:
+ - "top"
+ - null
+ - null
+ - null
+ - "formatted"
+ - null
+ - null
diff --git a/test/snapshot/paragraph_styles.hdoc b/test/snapshot/paragraph_styles.hdoc
new file mode 100644
index 0000000..f5b3a96
--- /dev/null
+++ b/test/snapshot/paragraph_styles.hdoc
@@ -0,0 +1,17 @@
+hdoc(version="2.0", title="Paragraph Styles", lang="en");
+
+h1 "Paragraph Styles"
+
+p "A standard paragraph introducing the styles below."
+
+note "Notes provide informational context without urgency."
+
+warning "Warnings highlight potential issues to watch for."
+
+danger "Danger blocks signal critical problems."
+
+tip "Tips offer helpful hints for readers."
+
+quote "Quoted material sits in its own paragraph style."
+
+spoiler "This is a spoiler; renderers may hide or blur this content."
diff --git a/test/snapshot/paragraph_styles.html b/test/snapshot/paragraph_styles.html
new file mode 100644
index 0000000..1bdd369
--- /dev/null
+++ b/test/snapshot/paragraph_styles.html
@@ -0,0 +1,23 @@
+
+ Paragraph Styles
+
+§1 Paragraph Styles
+A standard paragraph introducing the styles below.
+
+ Notes provide informational context without urgency.
+
+
+ Warnings highlight potential issues to watch for.
+
+
+ Danger blocks signal critical problems.
+
+
+ Tips offer helpful hints for readers.
+
+
+ Quoted material sits in its own paragraph style.
+
+
+ This is a spoiler; renderers may hide or blur this content.
+
diff --git a/test/snapshot/paragraph_styles.yaml b/test/snapshot/paragraph_styles.yaml
new file mode 100644
index 0000000..e8d82e9
--- /dev/null
+++ b/test/snapshot/paragraph_styles.yaml
@@ -0,0 +1,89 @@
+document:
+ version:
+ major: 2
+ minor: 0
+ lang: "en"
+ title:
+ simple: "Paragraph Styles"
+ full:
+ lang: "en"
+ content:
+ - [] "Paragraph Styles"
+ author: null
+ date: null
+ toc:
+ level: h1
+ headings:
+ - 0
+ children:
+ -
+ level: h2
+ headings: []
+ children: []
+ contents:
+ - heading:
+ level: h1
+ lang: ""
+ content:
+ - [] "Paragraph Styles"
+ - paragraph:
+ lang: ""
+ content:
+ - [] "A standard paragraph introducing the styles below."
+ - admonition:
+ kind: note
+ lang: ""
+ content:
+ - paragraph:
+ lang: ""
+ content:
+ - [] "Notes provide informational context without urgency."
+ - admonition:
+ kind: warning
+ lang: ""
+ content:
+ - paragraph:
+ lang: ""
+ content:
+ - [] "Warnings highlight potential issues to watch for."
+ - admonition:
+ kind: danger
+ lang: ""
+ content:
+ - paragraph:
+ lang: ""
+ content:
+ - [] "Danger blocks signal critical problems."
+ - admonition:
+ kind: tip
+ lang: ""
+ content:
+ - paragraph:
+ lang: ""
+ content:
+ - [] "Tips offer helpful hints for readers."
+ - admonition:
+ kind: quote
+ lang: ""
+ content:
+ - paragraph:
+ lang: ""
+ content:
+ - [] "Quoted material sits in its own paragraph style."
+ - admonition:
+ kind: spoiler
+ lang: ""
+ content:
+ - paragraph:
+ lang: ""
+ content:
+ - [] "This is a spoiler; renderers may hide or blur this content."
+ ids:
+ - null
+ - null
+ - null
+ - null
+ - null
+ - null
+ - null
+ - null
diff --git a/test/snapshot/tables.hdoc b/test/snapshot/tables.hdoc
new file mode 100644
index 0000000..22e728f
--- /dev/null
+++ b/test/snapshot/tables.hdoc
@@ -0,0 +1,28 @@
+hdoc(version="2.0", title="Tables", lang="en");
+
+h1 "Table Coverage"
+
+p "This file covers header rows, data rows with titles, groups, and colspans."
+
+table {
+ columns {
+ td { p "Column A" }
+ td { p "Column B" }
+ td { p "Column C" }
+ }
+ group { "Section One" }
+ row(title="Row 1") {
+ td { p "A1" }
+ td(colspan="2") { p "B1-C1" }
+ }
+ row(title="Row 2") {
+ td(colspan="2") { p "A2-B2" }
+ td { p "C2" }
+ }
+ group { "Section Two" }
+ row(title="Row 3") {
+ td { p "A3" }
+ td { p "B3" }
+ td { p "C3" }
+ }
+}
diff --git a/test/snapshot/tables.html b/test/snapshot/tables.html
new file mode 100644
index 0000000..9ffe48c
--- /dev/null
+++ b/test/snapshot/tables.html
@@ -0,0 +1,61 @@
+
+ Tables
+
+§1 Table Coverage
+This file covers header rows, data rows with titles, groups, and colspans.
+
+
+
+
+
+ Column A
+
+
+ Column B
+
+
+ Column C
+
+
+
+
+
+
+ "Section One"
+
+
+ Row 1
+
+ A1
+
+
+ B1-C1
+
+
+
+ Row 2
+
+ A2-B2
+
+
+ C2
+
+
+
+
+ "Section Two"
+
+
+ Row 3
+
+ A3
+
+
+ B3
+
+
+ C3
+
+
+
+
diff --git a/test/snapshot/tables.yaml b/test/snapshot/tables.yaml
new file mode 100644
index 0000000..a3e7b4f
--- /dev/null
+++ b/test/snapshot/tables.yaml
@@ -0,0 +1,134 @@
+document:
+ version:
+ major: 2
+ minor: 0
+ lang: "en"
+ title:
+ simple: "Tables"
+ full:
+ lang: "en"
+ content:
+ - [] "Tables"
+ author: null
+ date: null
+ toc:
+ level: h1
+ headings:
+ - 0
+ children:
+ -
+ level: h2
+ headings: []
+ children: []
+ contents:
+ - heading:
+ level: h1
+ lang: ""
+ content:
+ - [] "Table Coverage"
+ - paragraph:
+ lang: ""
+ content:
+ - [] "This file covers header rows, data rows with titles, groups, and colspans."
+ - table:
+ lang: ""
+ column_count: 3
+ has_row_titles: true
+ rows:
+ - columns:
+ lang: ""
+ cells:
+ - lang: ""
+ colspan: 1
+ content:
+ - paragraph:
+ lang: ""
+ content:
+ - [] "Column A"
+ - lang: ""
+ colspan: 1
+ content:
+ - paragraph:
+ lang: ""
+ content:
+ - [] "Column B"
+ - lang: ""
+ colspan: 1
+ content:
+ - paragraph:
+ lang: ""
+ content:
+ - [] "Column C"
+ - group:
+ lang: ""
+ content:
+ - [] "\"Section One\""
+ - row:
+ lang: ""
+ title: "Row 1"
+ cells:
+ - lang: ""
+ colspan: 1
+ content:
+ - paragraph:
+ lang: ""
+ content:
+ - [] "A1"
+ - lang: ""
+ colspan: 2
+ content:
+ - paragraph:
+ lang: ""
+ content:
+ - [] "B1-C1"
+ - row:
+ lang: ""
+ title: "Row 2"
+ cells:
+ - lang: ""
+ colspan: 2
+ content:
+ - paragraph:
+ lang: ""
+ content:
+ - [] "A2-B2"
+ - lang: ""
+ colspan: 1
+ content:
+ - paragraph:
+ lang: ""
+ content:
+ - [] "C2"
+ - group:
+ lang: ""
+ content:
+ - [] "\"Section Two\""
+ - row:
+ lang: ""
+ title: "Row 3"
+ cells:
+ - lang: ""
+ colspan: 1
+ content:
+ - paragraph:
+ lang: ""
+ content:
+ - [] "A3"
+ - lang: ""
+ colspan: 1
+ content:
+ - paragraph:
+ lang: ""
+ content:
+ - [] "B3"
+ - lang: ""
+ colspan: 1
+ content:
+ - paragraph:
+ lang: ""
+ content:
+ - [] "C3"
+ ids:
+ - null
+ - null
+ - null
diff --git a/test/wasm/diagnostic_accepted.hdoc b/test/wasm/diagnostic_accepted.hdoc
new file mode 100644
index 0000000..fcd9f85
--- /dev/null
+++ b/test/wasm/diagnostic_accepted.hdoc
@@ -0,0 +1,3 @@
+hdoc(version="2.0");
+title "WASM Warning Coverage"
+p { The header intentionally omits a lang attribute. }
diff --git a/test/wasm/diagnostic_rejected.hdoc b/test/wasm/diagnostic_rejected.hdoc
new file mode 100644
index 0000000..a43140b
--- /dev/null
+++ b/test/wasm/diagnostic_rejected.hdoc
@@ -0,0 +1,2 @@
+h1 "Missing header"
+p { This file lacks the required hdoc header. }
diff --git a/test/wasm/diagnostics_expected.json b/test/wasm/diagnostics_expected.json
new file mode 100644
index 0000000..703225c
--- /dev/null
+++ b/test/wasm/diagnostics_expected.json
@@ -0,0 +1,16 @@
+{
+ "accepted": [
+ {
+ "line": 1,
+ "column": 1,
+ "message": "Document language is missing; set lang on the hdoc header."
+ }
+ ],
+ "rejected": [
+ {
+ "line": 1,
+ "column": 1,
+ "message": "Document must start with an 'hdoc' header."
+ }
+ ]
+}
diff --git a/test/wasm/validate.js b/test/wasm/validate.js
new file mode 100644
index 0000000..5e4318f
--- /dev/null
+++ b/test/wasm/validate.js
@@ -0,0 +1,177 @@
+#!/usr/bin/env node
+'use strict';
+
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const path = require('node:path');
+
+const textEncoder = new TextEncoder();
+const textDecoder = new TextDecoder();
+
+const repoRoot = path.join(__dirname, '..', '..');
+const wasmPath = path.join(repoRoot, 'zig-out', 'www', 'hyperdoc_wasm.wasm');
+
+const htmlSnapshotTests = [
+ {
+ name: 'document_header',
+ source: path.join(repoRoot, 'test', 'snapshot', 'document_header.hdoc'),
+ expected: path.join(repoRoot, 'test', 'snapshot', 'document_header.html'),
+ },
+ {
+ name: 'paragraph_styles',
+ source: path.join(repoRoot, 'test', 'snapshot', 'paragraph_styles.hdoc'),
+ expected: path.join(repoRoot, 'test', 'snapshot', 'paragraph_styles.html'),
+ },
+ {
+ name: 'tables',
+ source: path.join(repoRoot, 'test', 'snapshot', 'tables.hdoc'),
+ expected: path.join(repoRoot, 'test', 'snapshot', 'tables.html'),
+ },
+];
+
+const diagnosticsInput = {
+ accepted: path.join(__dirname, 'diagnostic_accepted.hdoc'),
+ rejected: path.join(__dirname, 'diagnostic_rejected.hdoc'),
+ expected: path.join(__dirname, 'diagnostics_expected.json'),
+};
+
+function assertFileExists(filePath) {
+ if (!fs.existsSync(filePath)) {
+ throw new Error(`Missing required file: ${filePath}`);
+ }
+}
+
+function readUtf8(filePath) {
+ return fs.readFileSync(filePath, 'utf8');
+}
+
+function createLogImports(memoryRef) {
+ const state = { buffer: '' };
+ return {
+ reset_log() {
+ state.buffer = '';
+ },
+ append_log(ptr, len) {
+ if (len === 0 || ptr === 0) return;
+ const memory = memoryRef.current;
+ if (!memory) return;
+ const view = new Uint8Array(memory.buffer, ptr, len);
+ state.buffer += textDecoder.decode(view);
+ },
+ flush_log(level) {
+ if (state.buffer.length === 0) return;
+ const method = ['error', 'warn', 'info', 'debug'][level] || 'log';
+ console[method](`[wasm ${method}] ${state.buffer}`);
+ state.buffer = '';
+ },
+ };
+}
+
+function getMemory(wasm, memoryRef) {
+ const memory = wasm.memory || memoryRef.current;
+ memoryRef.current = memory;
+ if (!memory) {
+ throw new Error('WASM memory is unavailable');
+ }
+ return memory;
+}
+
+async function instantiateWasm() {
+ assertFileExists(wasmPath);
+ const bytes = await fs.promises.readFile(wasmPath);
+ const memoryRef = { current: null };
+ const env = createLogImports(memoryRef);
+ const { instance } = await WebAssembly.instantiate(bytes, { env });
+ memoryRef.current = instance.exports.memory;
+ return { wasm: instance.exports, memoryRef };
+}
+
+function readString(memory, ptr, len) {
+ if (!ptr || len === 0) return '';
+ const view = new Uint8Array(memory.buffer, ptr, len);
+ return textDecoder.decode(view);
+}
+
+function processDocument(ctx, sourceText) {
+ const { wasm, memoryRef } = ctx;
+ const bytes = textEncoder.encode(sourceText);
+
+ if (!wasm.hdoc_set_document_len(bytes.length)) {
+ throw new Error('Failed to allocate WASM document buffer');
+ }
+
+ const memoryForInput = getMemory(wasm, memoryRef);
+ const docPtr = wasm.hdoc_document_ptr();
+ if (bytes.length > 0) {
+ new Uint8Array(memoryForInput.buffer, docPtr, bytes.length).set(bytes);
+ }
+
+ const ok = wasm.hdoc_process() !== 0;
+ const memory = getMemory(wasm, memoryRef);
+
+ const htmlPtr = wasm.hdoc_html_ptr();
+ const htmlLen = wasm.hdoc_html_len();
+ const html = readString(memory, htmlPtr ?? 0, htmlLen);
+
+ const diagnostics = [];
+ const diagCount = wasm.hdoc_diagnostic_count();
+ for (let i = 0; i < diagCount; i += 1) {
+ const msgPtr = wasm.hdoc_diagnostic_message_ptr(i) ?? 0;
+ const msgLen = wasm.hdoc_diagnostic_message_len(i);
+ diagnostics.push({
+ line: wasm.hdoc_diagnostic_line(i),
+ column: wasm.hdoc_diagnostic_column(i),
+ message: readString(memory, msgPtr, msgLen),
+ });
+ }
+
+ return { ok, html, diagnostics };
+}
+
+function compareDiagnostics(actual, expected, label) {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ `${label} diagnostics differ.\nExpected: ${JSON.stringify(expected, null, 2)}\nActual: ${JSON.stringify(actual, null, 2)}`,
+ );
+}
+
+async function runHtmlTests(ctx) {
+ for (const test of htmlSnapshotTests) {
+ assertFileExists(test.source);
+ assertFileExists(test.expected);
+ const { ok, html, diagnostics } = processDocument(ctx, readUtf8(test.source));
+ assert.equal(ok, true, `WASM processing failed for ${test.name}`);
+ assert.deepStrictEqual(diagnostics, [], `Expected no diagnostics for ${test.name}`);
+ const expectedHtml = readUtf8(test.expected);
+ assert.equal(html, expectedHtml, `Rendered HTML mismatch for ${test.name}`);
+ }
+}
+
+async function runDiagnosticTests(ctx) {
+ assertFileExists(diagnosticsInput.accepted);
+ assertFileExists(diagnosticsInput.rejected);
+ assertFileExists(diagnosticsInput.expected);
+
+ const expectations = JSON.parse(readUtf8(diagnosticsInput.expected));
+
+ const acceptedResult = processDocument(ctx, readUtf8(diagnosticsInput.accepted));
+ assert.equal(acceptedResult.ok, true, 'Accepted diagnostic test should render successfully');
+ compareDiagnostics(acceptedResult.diagnostics, expectations.accepted, 'Accepted');
+
+ const rejectedResult = processDocument(ctx, readUtf8(diagnosticsInput.rejected));
+ assert.equal(rejectedResult.ok, false, 'Rejected diagnostic test should fail');
+ compareDiagnostics(rejectedResult.diagnostics, expectations.rejected, 'Rejected');
+}
+
+async function main() {
+ const ctx = await instantiateWasm();
+ await runHtmlTests(ctx);
+ await runDiagnosticTests(ctx);
+ console.log('WASM integration tests passed.');
+}
+
+main().catch((error) => {
+ console.error(error);
+ process.exitCode = 1;
+});
diff --git a/vscode-ext/.gitignore b/vscode-ext/.gitignore
new file mode 100644
index 0000000..b5d8d59
--- /dev/null
+++ b/vscode-ext/.gitignore
@@ -0,0 +1,6 @@
+node_modules
+out
+*.vsix
+.vscode-test
+.DS_Store
+wasm/
diff --git a/vscode-ext/justfile b/vscode-ext/justfile
new file mode 100644
index 0000000..6cdaa8a
--- /dev/null
+++ b/vscode-ext/justfile
@@ -0,0 +1,13 @@
+default: setup build test
+
+setup:
+ npm install
+
+build:
+ npm run compile
+
+test:
+ npm test
+
+package:
+ npm run package
diff --git a/vscode-ext/language-configuration.json b/vscode-ext/language-configuration.json
new file mode 100644
index 0000000..de94959
--- /dev/null
+++ b/vscode-ext/language-configuration.json
@@ -0,0 +1,44 @@
+{
+ "wordPattern": "(-?\\d*\\.\\d\\w*)|([^\\`\\~\\!\\@\\#\\%\\^\\&\\*\\(\\)\\=\\+\\[\\{\\]\\}\\\\\\|\\;\\:\\'\\\"\\,\\.\\<\\>\\/?\\s]+)",
+ "brackets": [
+ [
+ "{",
+ "}"
+ ],
+ [
+ "(",
+ ")"
+ ]
+ ],
+ "autoClosingPairs": [
+ {
+ "open": "{",
+ "close": "}"
+ },
+ {
+ "open": "(",
+ "close": ")"
+ },
+ {
+ "open": "\"",
+ "close": "\"",
+ "notIn": [
+ "string"
+ ]
+ }
+ ],
+ "surroundingPairs": [
+ [
+ "{",
+ "}"
+ ],
+ [
+ "(",
+ ")"
+ ],
+ [
+ "\"",
+ "\""
+ ]
+ ]
+}
diff --git a/vscode-ext/package-lock.json b/vscode-ext/package-lock.json
new file mode 100644
index 0000000..4bfd06c
--- /dev/null
+++ b/vscode-ext/package-lock.json
@@ -0,0 +1,3140 @@
+{
+ "name": "hyperdoc-vscode",
+ "version": "0.0.1",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "hyperdoc-vscode",
+ "version": "0.0.1",
+ "license": "MIT",
+ "devDependencies": {
+ "@types/mocha": "^10.0.6",
+ "@types/node": "^18.19.0",
+ "@types/vscode": "1.85.0",
+ "mocha": "^10.4.0",
+ "ts-node": "^10.9.2",
+ "typescript": "^5.4.0",
+ "vsce": "^2.15.0",
+ "vscode": "^1.1.37",
+ "vscode-languageclient": "^9.0.1"
+ },
+ "engines": {
+ "vscode": "^1.85.0"
+ }
+ },
+ "node_modules/@cspotcode/source-map-support": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
+ "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "0.3.9"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.9",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
+ "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.0.3",
+ "@jridgewell/sourcemap-codec": "^1.4.10"
+ }
+ },
+ "node_modules/@tootallnate/once": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz",
+ "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/@tsconfig/node10": {
+ "version": "1.0.12",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz",
+ "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tsconfig/node12": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
+ "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tsconfig/node14": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
+ "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tsconfig/node16": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
+ "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/mocha": {
+ "version": "10.0.10",
+ "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz",
+ "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "18.19.130",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz",
+ "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~5.26.4"
+ }
+ },
+ "node_modules/@types/vscode": {
+ "version": "1.85.0",
+ "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.85.0.tgz",
+ "integrity": "sha512-CF/RBon/GXwdfmnjZj0WTUMZN5H6YITOfBCP4iEZlOtVQXuzw6t7Le7+cR+7JzdMrnlm7Mfp49Oj2TuSXIWo3g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/acorn": {
+ "version": "8.15.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
+ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-walk": {
+ "version": "8.3.4",
+ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
+ "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "acorn": "^8.11.0"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+ "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/ansi-colors": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
+ "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/arg": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
+ "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/azure-devops-node-api": {
+ "version": "11.2.0",
+ "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-11.2.0.tgz",
+ "integrity": "sha512-XdiGPhrpaT5J8wdERRKs5g8E0Zy1pvOYTli7z9E8nmOn3YGp4FhtjhrOyFmX/8veWCwdI69mCHKJw6l+4J/bHA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tunnel": "0.0.6",
+ "typed-rest-client": "^1.8.4"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/bl": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
+ "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "buffer": "^5.5.0",
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.4.0"
+ }
+ },
+ "node_modules/boolbase": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
+ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browser-stdout": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
+ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/buffer": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+ "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.1.13"
+ }
+ },
+ "node_modules/buffer-crc32": {
+ "version": "0.2.13",
+ "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
+ "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/buffer-from": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/camelcase": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
+ "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/cheerio": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.1.2.tgz",
+ "integrity": "sha512-IkxPpb5rS/d1IiLbHMgfPuS0FgiWTtFIm/Nj+2woXDLTZ7fOT2eqzgYbdMlLweqlHbsZjxEChoVK+7iph7jyQg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cheerio-select": "^2.1.0",
+ "dom-serializer": "^2.0.0",
+ "domhandler": "^5.0.3",
+ "domutils": "^3.2.2",
+ "encoding-sniffer": "^0.2.1",
+ "htmlparser2": "^10.0.0",
+ "parse5": "^7.3.0",
+ "parse5-htmlparser2-tree-adapter": "^7.1.0",
+ "parse5-parser-stream": "^7.1.2",
+ "undici": "^7.12.0",
+ "whatwg-mimetype": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=20.18.1"
+ },
+ "funding": {
+ "url": "https://github.com/cheeriojs/cheerio?sponsor=1"
+ }
+ },
+ "node_modules/cheerio-select": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz",
+ "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "boolbase": "^1.0.0",
+ "css-select": "^5.1.0",
+ "css-what": "^6.1.0",
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3",
+ "domutils": "^3.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/chownr": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
+ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/cliui": {
+ "version": "7.0.4",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
+ "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.0",
+ "wrap-ansi": "^7.0.0"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "1.1.3"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/commander": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz",
+ "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/create-require": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
+ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/css-select": {
+ "version": "5.2.2",
+ "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
+ "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "boolbase": "^1.0.0",
+ "css-what": "^6.1.0",
+ "domhandler": "^5.0.2",
+ "domutils": "^3.0.1",
+ "nth-check": "^2.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
+ "node_modules/css-what": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
+ "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">= 6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decamelize": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz",
+ "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/decompress-response": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
+ "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mimic-response": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/deep-extend": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
+ "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/diff": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz",
+ "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
+ "node_modules/dom-serializer": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
+ "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.2",
+ "entities": "^4.2.0"
+ },
+ "funding": {
+ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
+ }
+ },
+ "node_modules/domelementtype": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
+ "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/domhandler": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
+ "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "domelementtype": "^2.3.0"
+ },
+ "engines": {
+ "node": ">= 4"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domhandler?sponsor=1"
+ }
+ },
+ "node_modules/domutils": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
+ "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "dom-serializer": "^2.0.0",
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domutils?sponsor=1"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/encoding-sniffer": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz",
+ "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "iconv-lite": "^0.6.3",
+ "whatwg-encoding": "^3.1.1"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/encoding-sniffer?sponsor=1"
+ }
+ },
+ "node_modules/end-of-stream": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
+ "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "once": "^1.4.0"
+ }
+ },
+ "node_modules/entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es6-promise": {
+ "version": "4.2.8",
+ "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
+ "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/es6-promisify": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
+ "integrity": "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es6-promise": "^4.0.3"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/expand-template": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
+ "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
+ "dev": true,
+ "license": "(MIT OR WTFPL)",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/fd-slicer": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
+ "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pend": "~1.2.0"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
+ "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "bin": {
+ "flat": "cli.js"
+ }
+ },
+ "node_modules/fs-constants": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
+ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/github-from-package": {
+ "version": "0.0.0",
+ "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
+ "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "deprecated": "Glob versions prior to v9 are no longer supported",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/growl": {
+ "version": "1.10.5",
+ "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz",
+ "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.x"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/he": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "he": "bin/he"
+ }
+ },
+ "node_modules/hosted-git-info": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz",
+ "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/htmlparser2": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz",
+ "integrity": "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==",
+ "dev": true,
+ "funding": [
+ "https://github.com/fb55/htmlparser2?sponsor=1",
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3",
+ "domutils": "^3.2.1",
+ "entities": "^6.0.0"
+ }
+ },
+ "node_modules/htmlparser2/node_modules/entities": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
+ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/http-proxy-agent": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz",
+ "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@tootallnate/once": "1",
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+ "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/ini": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
+ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-plain-obj": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
+ "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-unicode-supported": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
+ "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/keytar": {
+ "version": "7.9.0",
+ "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz",
+ "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "node-addon-api": "^4.3.0",
+ "prebuild-install": "^7.0.1"
+ }
+ },
+ "node_modules/leven": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
+ "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/linkify-it": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz",
+ "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "uc.micro": "^1.0.1"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/log-symbols": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
+ "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^4.1.0",
+ "is-unicode-supported": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/log-symbols/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/log-symbols/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/log-symbols/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/log-symbols/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/log-symbols/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/log-symbols/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/make-error": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
+ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/markdown-it": {
+ "version": "12.3.2",
+ "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz",
+ "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1",
+ "entities": "~2.1.0",
+ "linkify-it": "^3.0.1",
+ "mdurl": "^1.0.1",
+ "uc.micro": "^1.0.5"
+ },
+ "bin": {
+ "markdown-it": "bin/markdown-it.js"
+ }
+ },
+ "node_modules/markdown-it/node_modules/entities": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz",
+ "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/mdurl": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
+ "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mimic-response": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
+ "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/mkdirp": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+ "integrity": "sha512-SknJC52obPfGQPnjIkXbmA6+5H15E+fR+E4iR2oQ3zzCLbd7/ONua69R/Gw7AgkTLsRG+r5fzksYwWe1AgTyWA==",
+ "deprecated": "Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "minimist": "0.0.8"
+ },
+ "bin": {
+ "mkdirp": "bin/cmd.js"
+ }
+ },
+ "node_modules/mkdirp-classic": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
+ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/mkdirp/node_modules/minimist": {
+ "version": "0.0.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
+ "integrity": "sha512-miQKw5Hv4NS1Psg2517mV4e4dYNaO3++hjAvLOAzKqZ61rH8NS1SK+vbfBWZ5PY/Me/bEWhUwqMghEW5Fb9T7Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/mocha": {
+ "version": "10.8.2",
+ "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz",
+ "integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-colors": "^4.1.3",
+ "browser-stdout": "^1.3.1",
+ "chokidar": "^3.5.3",
+ "debug": "^4.3.5",
+ "diff": "^5.2.0",
+ "escape-string-regexp": "^4.0.0",
+ "find-up": "^5.0.0",
+ "glob": "^8.1.0",
+ "he": "^1.2.0",
+ "js-yaml": "^4.1.0",
+ "log-symbols": "^4.1.0",
+ "minimatch": "^5.1.6",
+ "ms": "^2.1.3",
+ "serialize-javascript": "^6.0.2",
+ "strip-json-comments": "^3.1.1",
+ "supports-color": "^8.1.1",
+ "workerpool": "^6.5.1",
+ "yargs": "^16.2.0",
+ "yargs-parser": "^20.2.9",
+ "yargs-unparser": "^2.0.0"
+ },
+ "bin": {
+ "_mocha": "bin/_mocha",
+ "mocha": "bin/mocha.js"
+ },
+ "engines": {
+ "node": ">= 14.0.0"
+ }
+ },
+ "node_modules/mocha/node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/mocha/node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/mocha/node_modules/glob": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz",
+ "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==",
+ "deprecated": "Glob versions prior to v9 are no longer supported",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^5.0.1",
+ "once": "^1.3.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/mocha/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/mocha/node_modules/minimatch": {
+ "version": "5.1.6",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
+ "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/mocha/node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/mocha/node_modules/supports-color": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/mute-stream": {
+ "version": "0.0.8",
+ "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
+ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/napi-build-utils": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
+ "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/node-abi": {
+ "version": "3.85.0",
+ "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.85.0.tgz",
+ "integrity": "sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/node-abi/node_modules/semver": {
+ "version": "7.7.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/node-addon-api": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz",
+ "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/nth-check": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
+ "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "boolbase": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/nth-check?sponsor=1"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parse-semver": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/parse-semver/-/parse-semver-1.1.1.tgz",
+ "integrity": "sha512-Eg1OuNntBMH0ojvEKSrvDSnwLmvVuUOSdylH/pSCPNMIspLlweJyIWXCE+k/5hm3cj/EBUYwmWkjhBALNP4LXQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^5.1.0"
+ }
+ },
+ "node_modules/parse5": {
+ "version": "7.3.0",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
+ "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^6.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/parse5-htmlparser2-tree-adapter": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz",
+ "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "domhandler": "^5.0.3",
+ "parse5": "^7.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/parse5-parser-stream": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz",
+ "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parse5": "^7.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/parse5/node_modules/entities": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
+ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/pend": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
+ "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/prebuild-install": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
+ "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "detect-libc": "^2.0.0",
+ "expand-template": "^2.0.3",
+ "github-from-package": "0.0.0",
+ "minimist": "^1.2.3",
+ "mkdirp-classic": "^0.5.3",
+ "napi-build-utils": "^2.0.0",
+ "node-abi": "^3.3.0",
+ "pump": "^3.0.0",
+ "rc": "^1.2.7",
+ "simple-get": "^4.0.0",
+ "tar-fs": "^2.0.0",
+ "tunnel-agent": "^0.6.0"
+ },
+ "bin": {
+ "prebuild-install": "bin.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/pump": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
+ "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.14.1",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
+ "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/randombytes": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
+ "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "^5.1.0"
+ }
+ },
+ "node_modules/rc": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
+ "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
+ "dev": true,
+ "license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
+ "dependencies": {
+ "deep-extend": "^0.6.0",
+ "ini": "~1.3.0",
+ "minimist": "^1.2.0",
+ "strip-json-comments": "~2.0.1"
+ },
+ "bin": {
+ "rc": "cli.js"
+ }
+ },
+ "node_modules/read": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz",
+ "integrity": "sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "mute-stream": "~0.0.4"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/sax": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.3.tgz",
+ "integrity": "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==",
+ "dev": true,
+ "license": "BlueOak-1.0.0"
+ },
+ "node_modules/semver": {
+ "version": "5.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
+ "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
+ "node_modules/serialize-javascript": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
+ "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "randombytes": "^2.1.0"
+ }
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/simple-concat": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
+ "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/simple-get": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
+ "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "decompress-response": "^6.0.0",
+ "once": "^1.3.1",
+ "simple-concat": "^1.0.0"
+ }
+ },
+ "node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-support": {
+ "version": "0.5.21",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
+ "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ },
+ "node_modules/string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+ "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/tar-fs": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
+ "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chownr": "^1.1.1",
+ "mkdirp-classic": "^0.5.2",
+ "pump": "^3.0.0",
+ "tar-stream": "^2.1.4"
+ }
+ },
+ "node_modules/tar-stream": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
+ "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bl": "^4.0.3",
+ "end-of-stream": "^1.4.1",
+ "fs-constants": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/tmp": {
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
+ "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.14"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/ts-node": {
+ "version": "10.9.2",
+ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
+ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@cspotcode/source-map-support": "^0.8.0",
+ "@tsconfig/node10": "^1.0.7",
+ "@tsconfig/node12": "^1.0.7",
+ "@tsconfig/node14": "^1.0.0",
+ "@tsconfig/node16": "^1.0.2",
+ "acorn": "^8.4.1",
+ "acorn-walk": "^8.1.1",
+ "arg": "^4.1.0",
+ "create-require": "^1.1.0",
+ "diff": "^4.0.1",
+ "make-error": "^1.1.1",
+ "v8-compile-cache-lib": "^3.0.1",
+ "yn": "3.1.1"
+ },
+ "bin": {
+ "ts-node": "dist/bin.js",
+ "ts-node-cwd": "dist/bin-cwd.js",
+ "ts-node-esm": "dist/bin-esm.js",
+ "ts-node-script": "dist/bin-script.js",
+ "ts-node-transpile-only": "dist/bin-transpile.js",
+ "ts-script": "dist/bin-script-deprecated.js"
+ },
+ "peerDependencies": {
+ "@swc/core": ">=1.2.50",
+ "@swc/wasm": ">=1.2.50",
+ "@types/node": "*",
+ "typescript": ">=2.7"
+ },
+ "peerDependenciesMeta": {
+ "@swc/core": {
+ "optional": true
+ },
+ "@swc/wasm": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/ts-node/node_modules/diff": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
+ "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
+ "node_modules/tunnel": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
+ "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6.11 <=0.7.0 || >=0.7.3"
+ }
+ },
+ "node_modules/tunnel-agent": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
+ "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/typed-rest-client": {
+ "version": "1.8.11",
+ "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.11.tgz",
+ "integrity": "sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "qs": "^6.9.1",
+ "tunnel": "0.0.6",
+ "underscore": "^1.12.1"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/uc.micro": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
+ "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/underscore": {
+ "version": "1.13.7",
+ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz",
+ "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/undici": {
+ "version": "7.16.0",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz",
+ "integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.18.1"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "5.26.5",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
+ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/url-join": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz",
+ "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/v8-compile-cache-lib": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
+ "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/vsce": {
+ "version": "2.15.0",
+ "resolved": "https://registry.npmjs.org/vsce/-/vsce-2.15.0.tgz",
+ "integrity": "sha512-P8E9LAZvBCQnoGoizw65JfGvyMqNGlHdlUXD1VAuxtvYAaHBKLBdKPnpy60XKVDAkQCfmMu53g+gq9FM+ydepw==",
+ "deprecated": "vsce has been renamed to @vscode/vsce. Install using @vscode/vsce instead.",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "azure-devops-node-api": "^11.0.1",
+ "chalk": "^2.4.2",
+ "cheerio": "^1.0.0-rc.9",
+ "commander": "^6.1.0",
+ "glob": "^7.0.6",
+ "hosted-git-info": "^4.0.2",
+ "keytar": "^7.7.0",
+ "leven": "^3.1.0",
+ "markdown-it": "^12.3.2",
+ "mime": "^1.3.4",
+ "minimatch": "^3.0.3",
+ "parse-semver": "^1.1.1",
+ "read": "^1.0.7",
+ "semver": "^5.1.0",
+ "tmp": "^0.2.1",
+ "typed-rest-client": "^1.8.4",
+ "url-join": "^4.0.1",
+ "xml2js": "^0.4.23",
+ "yauzl": "^2.3.1",
+ "yazl": "^2.2.2"
+ },
+ "bin": {
+ "vsce": "vsce"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/vscode": {
+ "version": "1.1.37",
+ "resolved": "https://registry.npmjs.org/vscode/-/vscode-1.1.37.tgz",
+ "integrity": "sha512-vJNj6IlN7IJPdMavlQa1KoFB3Ihn06q1AiN3ZFI/HfzPNzbKZWPPuiU+XkpNOfGU5k15m4r80nxNPlM7wcc0wg==",
+ "deprecated": "This package is deprecated in favor of @types/vscode and vscode-test. For more information please read: https://code.visualstudio.com/updates/v1_36#_splitting-vscode-package-into-typesvscode-and-vscodetest",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "glob": "^7.1.2",
+ "http-proxy-agent": "^4.0.1",
+ "https-proxy-agent": "^5.0.0",
+ "mocha": "^5.2.0",
+ "semver": "^5.4.1",
+ "source-map-support": "^0.5.0",
+ "vscode-test": "^0.4.1"
+ },
+ "bin": {
+ "vscode-install": "bin/install"
+ },
+ "engines": {
+ "node": ">=8.9.3"
+ }
+ },
+ "node_modules/vscode-jsonrpc": {
+ "version": "8.2.0",
+ "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz",
+ "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/vscode-languageclient": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-9.0.1.tgz",
+ "integrity": "sha512-JZiimVdvimEuHh5olxhxkht09m3JzUGwggb5eRUkzzJhZ2KjCN0nh55VfiED9oez9DyF8/fz1g1iBV3h+0Z2EA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "minimatch": "^5.1.0",
+ "semver": "^7.3.7",
+ "vscode-languageserver-protocol": "3.17.5"
+ },
+ "engines": {
+ "vscode": "^1.82.0"
+ }
+ },
+ "node_modules/vscode-languageclient/node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/vscode-languageclient/node_modules/minimatch": {
+ "version": "5.1.6",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
+ "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/vscode-languageclient/node_modules/semver": {
+ "version": "7.7.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/vscode-languageserver-protocol": {
+ "version": "3.17.5",
+ "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz",
+ "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "vscode-jsonrpc": "8.2.0",
+ "vscode-languageserver-types": "3.17.5"
+ }
+ },
+ "node_modules/vscode-languageserver-types": {
+ "version": "3.17.5",
+ "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz",
+ "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/vscode-test": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/vscode-test/-/vscode-test-0.4.3.tgz",
+ "integrity": "sha512-EkMGqBSefZH2MgW65nY05rdRSko15uvzq4VAPM5jVmwYuFQKE7eikKXNJDRxL+OITXHB6pI+a3XqqD32Y3KC5w==",
+ "deprecated": "This package has been renamed to @vscode/test-electron, please update to the new name",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "http-proxy-agent": "^2.1.0",
+ "https-proxy-agent": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.9.3"
+ }
+ },
+ "node_modules/vscode-test/node_modules/agent-base": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz",
+ "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es6-promisify": "^5.0.0"
+ },
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ },
+ "node_modules/vscode-test/node_modules/debug": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+ "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/vscode-test/node_modules/http-proxy-agent": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz",
+ "integrity": "sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "4",
+ "debug": "3.1.0"
+ },
+ "engines": {
+ "node": ">= 4.5.0"
+ }
+ },
+ "node_modules/vscode-test/node_modules/https-proxy-agent": {
+ "version": "2.2.4",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz",
+ "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^4.3.0",
+ "debug": "^3.1.0"
+ },
+ "engines": {
+ "node": ">= 4.5.0"
+ }
+ },
+ "node_modules/vscode-test/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/vscode/node_modules/commander": {
+ "version": "2.15.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz",
+ "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/vscode/node_modules/debug": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+ "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/vscode/node_modules/diff": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
+ "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
+ "node_modules/vscode/node_modules/glob": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
+ "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
+ "deprecated": "Glob versions prior to v9 are no longer supported",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/vscode/node_modules/he": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz",
+ "integrity": "sha512-z/GDPjlRMNOa2XJiB4em8wJpuuBfrFOlYKTZxtpkdr1uPdibHI8rYA3MY0KDObpVyaes0e/aunid/t88ZI2EKA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "he": "bin/he"
+ }
+ },
+ "node_modules/vscode/node_modules/minimatch": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+ "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/vscode/node_modules/mocha": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz",
+ "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "browser-stdout": "1.3.1",
+ "commander": "2.15.1",
+ "debug": "3.1.0",
+ "diff": "3.5.0",
+ "escape-string-regexp": "1.0.5",
+ "glob": "7.1.2",
+ "growl": "1.10.5",
+ "he": "1.1.1",
+ "minimatch": "3.0.4",
+ "mkdirp": "0.5.1",
+ "supports-color": "5.4.0"
+ },
+ "bin": {
+ "_mocha": "bin/_mocha",
+ "mocha": "bin/mocha"
+ },
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ },
+ "node_modules/vscode/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/vscode/node_modules/supports-color": {
+ "version": "5.4.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz",
+ "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/whatwg-encoding": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
+ "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
+ "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "iconv-lite": "0.6.3"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/whatwg-mimetype": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
+ "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/workerpool": {
+ "version": "6.5.1",
+ "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz",
+ "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/xml2js": {
+ "version": "0.4.23",
+ "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
+ "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "sax": ">=0.6.0",
+ "xmlbuilder": "~11.0.0"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/xmlbuilder": {
+ "version": "11.0.1",
+ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
+ "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/y18n": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/yargs": {
+ "version": "16.2.0",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
+ "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^7.0.2",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.0",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^20.2.2"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "20.2.9",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
+ "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yargs-unparser": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz",
+ "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "camelcase": "^6.0.0",
+ "decamelize": "^4.0.0",
+ "flat": "^5.0.2",
+ "is-plain-obj": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yauzl": {
+ "version": "2.10.0",
+ "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
+ "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "buffer-crc32": "~0.2.3",
+ "fd-slicer": "~1.1.0"
+ }
+ },
+ "node_modules/yazl": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz",
+ "integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "buffer-crc32": "~0.2.3"
+ }
+ },
+ "node_modules/yn": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
+ "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ }
+ }
+}
diff --git a/vscode-ext/package.json b/vscode-ext/package.json
new file mode 100644
index 0000000..a72ce19
--- /dev/null
+++ b/vscode-ext/package.json
@@ -0,0 +1,80 @@
+{
+ "name": "hyperdoc-vscode",
+ "displayName": "HyperDoc",
+ "description": "HyperDoc language basics, highlighting, and completion.",
+ "version": "0.0.1",
+ "publisher": "hyperdoc",
+ "license": "MIT",
+ "engines": {
+ "vscode": "^1.85.0"
+ },
+ "categories": [
+ "Programming Languages"
+ ],
+ "activationEvents": [
+ "onLanguage:hyperdoc",
+ "onCommand:hyperdoc.startWasmLanguageServer"
+ ],
+ "main": "./out/src/extension.js",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/Ashet-Technologies/hyperdoc.git"
+ },
+ "contributes": {
+ "languages": [
+ {
+ "id": "hyperdoc",
+ "aliases": [
+ "HyperDoc",
+ "hyperdoc"
+ ],
+ "extensions": [
+ ".hdoc"
+ ],
+ "configuration": "./language-configuration.json"
+ }
+ ],
+ "grammars": [
+ {
+ "language": "hyperdoc",
+ "scopeName": "source.hyperdoc",
+ "path": "./syntaxes/hyperdoc.tmLanguage.json"
+ }
+ ],
+ "commands": [
+ {
+ "command": "hyperdoc.startWasmLanguageServer",
+ "title": "HyperDoc: Start Wasm Language Server"
+ }
+ ],
+ "configuration": {
+ "title": "HyperDoc",
+ "properties": {
+ "hyperdoc.languageServer.wasmPath": {
+ "type": "string",
+ "default": "",
+ "description": "Path to a HyperDoc language server WebAssembly binary. Leave empty to disable the wasm language server stub."
+ }
+ }
+ }
+ },
+ "scripts": {
+ "vscode:prepublish": "npm run compile",
+ "compile": "tsc -p ./",
+ "watch": "tsc -watch -p ./",
+ "lint": "echo \"No lint configured\"",
+ "test": "mocha -r ts-node/register test/**/*.test.ts",
+ "package": "vsce package"
+ },
+ "devDependencies": {
+ "@types/node": "^18.19.0",
+ "@types/vscode": "1.85.0",
+ "@types/mocha": "^10.0.6",
+ "mocha": "^10.4.0",
+ "ts-node": "^10.9.2",
+ "typescript": "^5.4.0",
+ "vsce": "^2.15.0",
+ "vscode": "^1.1.37",
+ "vscode-languageclient": "^9.0.1"
+ }
+}
diff --git a/vscode-ext/src/extension.ts b/vscode-ext/src/extension.ts
new file mode 100644
index 0000000..59da2fc
--- /dev/null
+++ b/vscode-ext/src/extension.ts
@@ -0,0 +1,141 @@
+import * as fs from "fs/promises";
+import * as vscode from "vscode";
+import {
+ ATTRIBUTE_SUGGESTIONS,
+ ELEMENT_SUGGESTIONS,
+ Suggestion,
+ computeIsInAttributeList,
+ mapSuggestionKind,
+ resolveWasmPath
+} from "./utils";
+
+class HyperdocCompletionProvider implements vscode.CompletionItemProvider {
+ provideCompletionItems(
+ document: vscode.TextDocument,
+ position: vscode.Position
+ ): vscode.ProviderResult {
+ const inAttributeList = isInAttributeList(document, position);
+ const pool = inAttributeList ? ATTRIBUTE_SUGGESTIONS : ELEMENT_SUGGESTIONS;
+
+ return pool.map((item) => createCompletionItem(item));
+ }
+}
+
+function createCompletionItem(item: Suggestion): vscode.CompletionItem {
+ const completion = new vscode.CompletionItem(
+ item.label,
+ mapSuggestionKind(item.kind)
+ );
+ completion.detail = item.detail;
+ return completion;
+}
+
+export function isInAttributeList(
+ document: vscode.TextDocument,
+ position: vscode.Position
+): boolean {
+ const text = document.getText(
+ new vscode.Range(new vscode.Position(0, 0), position)
+ );
+ return computeIsInAttributeList(text);
+}
+
+class WasmLanguageServerController {
+ private wasmModule: WebAssembly.Module | undefined;
+ private readonly output: vscode.OutputChannel;
+
+ constructor(private readonly context: vscode.ExtensionContext) {
+ this.output = vscode.window.createOutputChannel("HyperDoc");
+ }
+
+ async prepareFromConfiguration(): Promise {
+ const configuredPath = vscode.workspace
+ .getConfiguration("hyperdoc")
+ .get("languageServer.wasmPath")
+ ?.trim();
+
+ if (!configuredPath) {
+ this.wasmModule = undefined;
+ this.output.appendLine(
+ "HyperDoc wasm language server is disabled (no path configured)."
+ );
+ return;
+ }
+
+ await this.loadWasmModule(configuredPath);
+ }
+
+ dispose(): void {
+ this.wasmModule = undefined;
+ this.output.dispose();
+ }
+
+ private async loadWasmModule(rawPath: string): Promise {
+ const resolvedPath = resolveWasmPath(rawPath, {
+ extensionPath: this.context.extensionPath,
+ workspaceFolders: vscode.workspace.workspaceFolders?.map(
+ (folder) => folder.uri.fsPath
+ )
+ });
+ this.output.appendLine(
+ `Preparing HyperDoc wasm language server stub from: ${resolvedPath}`
+ );
+
+ try {
+ const bytes = await fs.readFile(resolvedPath);
+ const wasmBytes = Uint8Array.from(bytes);
+ this.wasmModule = await WebAssembly.compile(wasmBytes);
+ this.output.appendLine(
+ "Wasm module compiled. Language client wiring is intentionally disabled until the server shim is available."
+ );
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ vscode.window.showWarningMessage(
+ `HyperDoc: failed to load wasm language server (${message}).`
+ );
+ }
+ }
+}
+
+export async function activate(context: vscode.ExtensionContext): Promise {
+ const completionProvider = vscode.languages.registerCompletionItemProvider(
+ { language: "hyperdoc" },
+ new HyperdocCompletionProvider(),
+ "\\",
+ "{",
+ "("
+ );
+
+ const wasmController = new WasmLanguageServerController(context);
+
+ const startWasmCommand = vscode.commands.registerCommand(
+ "hyperdoc.startWasmLanguageServer",
+ async () => {
+ await wasmController.prepareFromConfiguration();
+ vscode.window.showInformationMessage(
+ "HyperDoc wasm language server stub prepared (when configured)."
+ );
+ }
+ );
+
+ const configChangeListener = vscode.workspace.onDidChangeConfiguration(
+ async (event) => {
+ if (event.affectsConfiguration("hyperdoc.languageServer.wasmPath")) {
+ await wasmController.prepareFromConfiguration();
+ }
+ }
+ );
+
+ context.subscriptions.push(
+ completionProvider,
+ wasmController,
+ startWasmCommand,
+ configChangeListener
+ );
+
+ await wasmController.prepareFromConfiguration();
+}
+
+export function deactivate(): void {
+ // No-op
+}
diff --git a/vscode-ext/src/utils.ts b/vscode-ext/src/utils.ts
new file mode 100644
index 0000000..9d896be
--- /dev/null
+++ b/vscode-ext/src/utils.ts
@@ -0,0 +1,98 @@
+import * as path from "path";
+
+export type Suggestion = {
+ label: string;
+ detail: string;
+ kind: "class" | "function" | "property";
+};
+
+export const ELEMENT_SUGGESTIONS: Suggestion[] = [
+ { label: "hdoc", detail: "Document header", kind: "class" },
+ { label: "title", detail: "Document title", kind: "class" },
+ { label: "h1", detail: "Heading level 1", kind: "class" },
+ { label: "h2", detail: "Heading level 2", kind: "class" },
+ { label: "h3", detail: "Heading level 3", kind: "class" },
+ { label: "toc", detail: "Table of contents", kind: "class" },
+ { label: "footnotes", detail: "Footnote dump", kind: "class" },
+ { label: "p", detail: "Paragraph", kind: "class" },
+ { label: "note", detail: "Admonition block: note", kind: "class" },
+ { label: "warning", detail: "Admonition block: warning", kind: "class" },
+ { label: "danger", detail: "Admonition block: danger", kind: "class" },
+ { label: "tip", detail: "Admonition block: tip", kind: "class" },
+ { label: "quote", detail: "Admonition block: quote", kind: "class" },
+ { label: "spoiler", detail: "Admonition block: spoiler", kind: "class" },
+ { label: "ul", detail: "Unordered list", kind: "class" },
+ { label: "ol", detail: "Ordered list", kind: "class" },
+ { label: "li", detail: "List item", kind: "class" },
+ { label: "img", detail: "Figure/image", kind: "class" },
+ { label: "pre", detail: "Preformatted block", kind: "class" },
+ { label: "table", detail: "Table", kind: "class" },
+ { label: "columns", detail: "Table columns header", kind: "class" },
+ { label: "row", detail: "Table row", kind: "class" },
+ { label: "group", detail: "Table row group", kind: "class" },
+ { label: "td", detail: "Table cell", kind: "class" },
+ { label: "\\em", detail: "Inline emphasis", kind: "function" },
+ { label: "\\mono", detail: "Inline monospace", kind: "function" },
+ { label: "\\strike", detail: "Inline strikethrough", kind: "function" },
+ { label: "\\sub", detail: "Inline subscript", kind: "function" },
+ { label: "\\sup", detail: "Inline superscript", kind: "function" },
+ { label: "\\link", detail: "Inline link", kind: "function" },
+ { label: "\\date", detail: "Inline date", kind: "function" },
+ { label: "\\time", detail: "Inline time", kind: "function" },
+ { label: "\\datetime", detail: "Inline datetime", kind: "function" },
+ { label: "\\ref", detail: "Inline reference", kind: "function" },
+ { label: "\\footnote", detail: "Inline footnote", kind: "function" }
+];
+
+export const ATTRIBUTE_SUGGESTIONS: Suggestion[] = [
+ { label: "id", detail: "Block identifier", kind: "property" },
+ { label: "title", detail: "Title attribute", kind: "property" },
+ { label: "lang", detail: "Language override", kind: "property" },
+ { label: "fmt", detail: "Format selection", kind: "property" },
+ { label: "ref", detail: "Reference target", kind: "property" },
+ { label: "key", detail: "Footnote key", kind: "property" }
+];
+
+export function computeIsInAttributeList(text: string): boolean {
+ const lastOpen = text.lastIndexOf("(");
+ if (lastOpen === -1) {
+ return false;
+ }
+
+ const lastClose = text.lastIndexOf(")");
+ if (lastClose > lastOpen) {
+ return false;
+ }
+
+ const afterOpen = text.slice(lastOpen + 1);
+ return !afterOpen.includes("{") && !afterOpen.includes("}");
+}
+
+export function mapSuggestionKind(kind: Suggestion["kind"]): number {
+ switch (kind) {
+ case "class":
+ return 6;
+ case "function":
+ return 3;
+ case "property":
+ return 10;
+ default:
+ return 9;
+ }
+}
+
+export function resolveWasmPath(
+ rawPath: string,
+ context: { extensionPath: string; workspaceFolders?: string[] }
+): string {
+ if (path.isAbsolute(rawPath)) {
+ return rawPath;
+ }
+
+ const workspaceFolder = context.workspaceFolders?.[0];
+ if (workspaceFolder) {
+ return path.join(workspaceFolder, rawPath);
+ }
+
+ return path.join(context.extensionPath, rawPath);
+}
diff --git a/vscode-ext/syntaxes/hyperdoc.tmLanguage.json b/vscode-ext/syntaxes/hyperdoc.tmLanguage.json
new file mode 100644
index 0000000..0fa088e
--- /dev/null
+++ b/vscode-ext/syntaxes/hyperdoc.tmLanguage.json
@@ -0,0 +1,74 @@
+{
+ "name": "HyperDoc",
+ "scopeName": "source.hyperdoc",
+ "patterns": [
+ {
+ "name": "entity.name.type.hyperdoc",
+ "match": "^(\\s*)(\\\\?[A-Za-z0-9_]+)",
+ "captures": {
+ "1": {
+ "name": "punctuation.whitespace.leading.hyperdoc"
+ },
+ "2": {
+ "name": "entity.name.tag.hyperdoc"
+ }
+ }
+ },
+ {
+ "name": "support.function.inline.hyperdoc",
+ "match": "\\\\[A-Za-z0-9_]+"
+ },
+ {
+ "name": "variable.parameter.attribute.hyperdoc",
+ "match": "([A-Za-z0-9_]+(?:-[A-Za-z0-9_]+)?)(\\s*)(=)",
+ "captures": {
+ "1": {
+ "name": "variable.parameter.attribute.hyperdoc"
+ },
+ "2": {
+ "name": "punctuation.separator.attribute.hyperdoc"
+ },
+ "3": {
+ "name": "keyword.operator.assignment.hyperdoc"
+ }
+ }
+ },
+ {
+ "name": "string.quoted.double.hyperdoc",
+ "match": "\"(?:\\\\.|[^\"\\\\])*\""
+ },
+ {
+ "name": "punctuation.section.braces.hyperdoc",
+ "match": "[{}]"
+ },
+ {
+ "name": "punctuation.section.parens.hyperdoc",
+ "match": "[()]"
+ },
+ {
+ "name": "punctuation.terminator.empty-body.hyperdoc",
+ "match": ";"
+ },
+ {
+ "name": "punctuation.definition.verbatim.hyperdoc",
+ "match": "(:)(?=\\s*(?:$|\\n|\\|))",
+ "captures": {
+ "1": {
+ "name": "punctuation.definition.verbatim.start.hyperdoc"
+ }
+ }
+ },
+ {
+ "name": "meta.verbatim.line.hyperdoc",
+ "match": "^\\s*(\\|)(.*)$",
+ "captures": {
+ "1": {
+ "name": "punctuation.definition.verbatim.bar.hyperdoc"
+ },
+ "2": {
+ "name": "string.unquoted.verbatim.hyperdoc"
+ }
+ }
+ }
+ ]
+}
diff --git a/vscode-ext/test/utils.test.ts b/vscode-ext/test/utils.test.ts
new file mode 100644
index 0000000..e201bd8
--- /dev/null
+++ b/vscode-ext/test/utils.test.ts
@@ -0,0 +1,79 @@
+import { strict as assert } from "node:assert";
+import path from "path";
+import {
+ ATTRIBUTE_SUGGESTIONS,
+ ELEMENT_SUGGESTIONS,
+ computeIsInAttributeList,
+ mapSuggestionKind,
+ resolveWasmPath
+} from "../src/utils";
+
+describe("computeIsInAttributeList", () => {
+ it("returns false when no opening paren is present", () => {
+ assert.equal(computeIsInAttributeList("hdoc "), false);
+ });
+
+ it("returns true between parentheses before closing", () => {
+ const text = 'node(attr="1"';
+ assert.equal(computeIsInAttributeList(text), true);
+ });
+
+ it("returns false after the closing parenthesis", () => {
+ const text = 'node(attr="1") ';
+ assert.equal(computeIsInAttributeList(text), false);
+ });
+
+ it("returns false if a block brace appears after the last open paren", () => {
+ const text = 'node(attr="1"{';
+ assert.equal(computeIsInAttributeList(text), false);
+ });
+});
+
+describe("completion suggestions", () => {
+ it("exposes element suggestions with both block and inline names", () => {
+ const labels = ELEMENT_SUGGESTIONS.map((s) => s.label);
+ assert(labels.includes("hdoc"));
+ assert(labels.includes("\\em"));
+ });
+
+ it("exposes attribute suggestions", () => {
+ const labels = ATTRIBUTE_SUGGESTIONS.map((s) => s.label);
+ assert(labels.includes("id"));
+ assert(labels.includes("fmt"));
+ });
+});
+
+describe("mapSuggestionKind", () => {
+ it("maps to completion item kinds", () => {
+ assert.equal(mapSuggestionKind("class"), 6);
+ assert.equal(mapSuggestionKind("function"), 3);
+ assert.equal(mapSuggestionKind("property"), 10);
+ });
+});
+
+describe("resolveWasmPath", () => {
+ const extPath = "/extension";
+
+ it("returns absolute paths unchanged", () => {
+ const input = "/tmp/server.wasm";
+ assert.equal(
+ resolveWasmPath(input, { extensionPath: extPath }),
+ input
+ );
+ });
+
+ it("uses workspace folder when available", () => {
+ const output = resolveWasmPath("server.wasm", {
+ extensionPath: extPath,
+ workspaceFolders: ["/workspace/project"]
+ });
+ assert.equal(output, path.join("/workspace/project", "server.wasm"));
+ });
+
+ it("falls back to the extension path", () => {
+ const output = resolveWasmPath("server.wasm", {
+ extensionPath: extPath
+ });
+ assert.equal(output, path.join(extPath, "server.wasm"));
+ });
+});
diff --git a/vscode-ext/tsconfig.json b/vscode-ext/tsconfig.json
new file mode 100644
index 0000000..c24d51b
--- /dev/null
+++ b/vscode-ext/tsconfig.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "module": "commonjs",
+ "target": "ES2020",
+ "outDir": "out",
+ "lib": [
+ "ES2020",
+ "DOM"
+ ],
+ "sourceMap": true,
+ "rootDir": ".",
+ "strict": true,
+ "moduleResolution": "node",
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "types": [
+ "node",
+ "vscode",
+ "mocha"
+ ]
+ },
+ "include": [
+ "src",
+ "test"
+ ]
+}