Compare commits
5 Commits
135ec392ff
...
task-0005-
| Author | SHA1 | Date | |
|---|---|---|---|
| b0fdc22165 | |||
| 22befaaa71 | |||
| ab309d3226 | |||
| fdede89e0c | |||
| 8fd2e53c53 |
31
.claude/adr/0008-errors-as-values-at-cli-boundary.md
Normal file
31
.claude/adr/0008-errors-as-values-at-cli-boundary.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# ADR 0008 — Errors as Values at the CLI Boundary
|
||||||
|
|
||||||
|
**Status**: Accepted
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
`@kitchen-md/bin`'s `view` command performs a fallible operation: reading a file from disk.
|
||||||
|
Node's `readFileSync` signals failure by throwing.
|
||||||
|
Two idioms are available: let exceptions propagate and catch them at the entry point, or represent failure as a value the type system tracks.
|
||||||
|
`@kitchen-md/core`'s parser already takes the second path — it is a total function that never throws and reports problems through the Document AST's diagnostics (see ADR 0004).
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Model fallible operations in `@kitchen-md/bin` as a neverthrow `Result<T, E>`, with error types expressed as plain-data tagged unions (e.g. `ViewError = { tag: "read-failed"; … }`).
|
||||||
|
A throwing API is wrapped in a small adapter that converts the exception into an `err`, so nothing above the adapter leaks exceptions.
|
||||||
|
The CLI entry point matches the `Result` at the boundary: stdout on success, stderr plus a non-zero exit on failure.
|
||||||
|
|
||||||
|
## Rationale
|
||||||
|
|
||||||
|
It keeps the bin layer consistent with core's total-function stance, so the whole codebase treats recoverable failure as data rather than control flow.
|
||||||
|
The possibility of failure becomes visible in a function's signature instead of hidden behind a `throw`.
|
||||||
|
The success value cannot be read without first handling the error case, which removes a class of mistakes at compile time.
|
||||||
|
A tagged-union error type gives exhaustive handling: a new failure mode is a new tag that every match must account for.
|
||||||
|
Because failure is a returned value rather than a side effect, error propagation can be asserted in-process by the integration tier, without spawning the binary (see ADR 0009).
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
`@kitchen-md/bin` takes a dependency on neverthrow.
|
||||||
|
`try`/`catch` is confined to the thin adapters that wrap throwing APIs, and the rest of the layer is exception-free.
|
||||||
|
Must-use enforcement — that a `Result` is never silently dropped — is a convention (always terminate a `Result` at a `.match` at the boundary), not a lint rule, because the project uses Biome alone and does not add ESLint's `eslint-plugin-neverthrow` for now.
|
||||||
|
New failure modes stay additive: a new tag on the error union, handled at the boundary.
|
||||||
44
.claude/adr/0009-testing-tiers-and-boundaries.md
Normal file
44
.claude/adr/0009-testing-tiers-and-boundaries.md
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# ADR 0009 — Testing Tiers and Boundaries
|
||||||
|
|
||||||
|
**Status**: Accepted
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The test suite accumulated overlapping files — unit, integration, smoke, and subprocess — with no crisp definition of what each was responsible for.
|
||||||
|
The result was duplication and confusion: the same behaviour asserted in more than one tier, and no rule for where a given test belonged.
|
||||||
|
Both specs already referred to unit, integration, and smoke tests, but none of them pinned the boundaries.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Recognise three test tiers, each defined by the seam it exercises.
|
||||||
|
|
||||||
|
**Unit** — one component in isolation, a pure function, asserted by its return value.
|
||||||
|
`parse` in core and `render` in bin are the unit seams.
|
||||||
|
|
||||||
|
**Integration** — several components composed in-process, asserted by the value the composed function returns.
|
||||||
|
The `view` command function — file read, then `parse`, then `render`, returning a `Result` — is the integration seam.
|
||||||
|
|
||||||
|
**E2E** — the binary as a black box.
|
||||||
|
It is spawned as a subprocess and asserted on its exit code and its stdout and stderr, with no knowledge of the internal structure.
|
||||||
|
|
||||||
|
The line between what is unit- or integration-testable and what is e2e-only is whether a function returns a value or performs a process-level side effect.
|
||||||
|
A function that returns a value can be asserted in-process.
|
||||||
|
A function that calls `process.exit` or `process.stdout.write` can only be observed by spawning the binary.
|
||||||
|
So all logic is pushed into value-returning functions, and the entry shell is kept as thin as possible, because it is the one part reachable only through a subprocess.
|
||||||
|
|
||||||
|
Core exposes a single public seam, `parse`, so it has unit tests only.
|
||||||
|
A whole-fixture parse test is still a unit test on that same seam with a broad input — a corpus test — not a separate tier.
|
||||||
|
|
||||||
|
## Rationale
|
||||||
|
|
||||||
|
Precise, non-overlapping definitions prevent the duplication that a vague unit-integration-smoke split produced.
|
||||||
|
Classifying by seam matches what is actually cheap or expensive to test.
|
||||||
|
Value-returning code runs fast and is visible to coverage in-process, while process-side-effecting code needs a subprocess and is invisible to coverage.
|
||||||
|
Concentrating the process-boundary surface in one thin shell keeps the amount of e2e-only code to a minimum.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
Each behaviour is tested at exactly one tier: logic at unit or integration in-process, the process boundary at e2e.
|
||||||
|
The e2e tier is deliberately minimal — it verifies wiring such as exit codes and stream routing, not content already proven in-process.
|
||||||
|
Tests are co-located as `{module}_test.ts`, so the command-function tests live beside the entry module and are integration tests despite the file name.
|
||||||
|
When a command function shares a file with the top-level `program.parse()`, that call is guarded with `import.meta.main`, so importing the module for a test does not run the CLI.
|
||||||
53
.claude/tasks/0001-flake-build.md
Normal file
53
.claude/tasks/0001-flake-build.md
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
---
|
||||||
|
spec: nix-flake-packaging
|
||||||
|
---
|
||||||
|
|
||||||
|
## What to build
|
||||||
|
|
||||||
|
The tracer bullet for packaging the repository as a Nix flake: a flake-parts flake whose default package Bun-compiles the `kitchen` CLI into a self-contained native binary, built reproducibly in the sandbox with vendored dependencies.
|
||||||
|
|
||||||
|
This is the whole risk seam — a Nix build is sandboxed with no network, while `bun install` fetches from npm, so the dependency closure must be vendored before anything compiles.
|
||||||
|
Bun dependencies are vendored via bun2nix (a flake input) reading the workspace lockfile into a checked-in generated expression, kept in sync with the lockfile by a postinstall hook so a dependency change needs no separate manual regeneration.
|
||||||
|
A single pinned Bun version is shared by the build derivation (and later the dev shell), avoiding point releases known to produce empty binaries under sandboxed native compilation.
|
||||||
|
|
||||||
|
Once this compiles, the remaining outputs (`nix run`, `nix develop`, checks) hang off the working flake in the next slice.
|
||||||
|
|
||||||
|
## Acceptance criteria
|
||||||
|
|
||||||
|
- [x] The flake is structured with flake-parts, with `x86_64-linux` in the systems list and nixpkgs tracking `nixpkgs-unstable`.
|
||||||
|
- [x] bun2nix is a flake input, and its generated vendoring expression is checked in and regenerated by a postinstall hook on lockfile changes.
|
||||||
|
- [x] A single Bun version is pinned in one place and consumed by the build derivation.
|
||||||
|
- [x] `nix build` produces a runnable `kitchen` binary reproducibly with no network access during the build.
|
||||||
|
- [x] The compiled binary embeds its dependencies and runs on a machine with no Node.js or Bun runtime present.
|
||||||
|
- [x] The default package is consumable as a flake input from another configuration.
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
The tracer bullet builds and all six criteria were verified end-to-end against a real `nix build`, not just by inspection.
|
||||||
|
|
||||||
|
### The Bun pin
|
||||||
|
|
||||||
|
bun2nix compiles the binary with the `bun` from *its own* nixpkgs, baked into its setup hook at bun2nix build time.
|
||||||
|
So pinning Bun means controlling the nixpkgs that bun2nix follows, not overlaying `bun` in this flake's package set.
|
||||||
|
The pin is a dedicated `nixpkgs-bun` input fixed to one revision (Bun 1.3.13), with `bun2nix.inputs.nixpkgs.follows = "nixpkgs-bun"`.
|
||||||
|
That revision is the one bun2nix 2.1.2 itself locks, so the compile toolchain matches what bun2nix was tested against.
|
||||||
|
`nixpkgs` still tracks `nixpkgs-unstable` for everything else, and updating it cannot move Bun.
|
||||||
|
The dev shell in task 0002 consumes the same `nixpkgs-bun`, keeping "develop with the version that compiles" true from one place.
|
||||||
|
|
||||||
|
### Vendoring and the postinstall hook
|
||||||
|
|
||||||
|
bun2nix is both a flake input (native builder) and an npm devDependency, so the `postinstall: bun2nix -o bun.nix` hook regenerates the checked-in `bun.nix` on any `bun install` even outside the Nix dev shell.
|
||||||
|
The build passes `dontRunLifecycleScripts = true` so that same postinstall does not fire redundantly inside the sandbox.
|
||||||
|
|
||||||
|
### Verification performed
|
||||||
|
|
||||||
|
`nix build` produced a 101 MB native ELF that runs to exit 0 under `env -i` (empty environment, no Node or Bun, `ldd` shows only glibc), confirming the embedded-runtime and no-network claims — the compile derivation is a normal sandboxed derivation with networking disabled.
|
||||||
|
A throwaway consumer flake built the package through `inputs.kitchen.packages.x86_64-linux.default`, confirming criterion 6.
|
||||||
|
|
||||||
|
### Deviations from the plan
|
||||||
|
|
||||||
|
- The nix-community cachix `nixConfig` block that the bun2nix templates ship was dropped, because the spec lists "any binary cache or substituter setup" as out of scope.
|
||||||
|
Consequence: a first build with a cold store compiles bun2nix from source.
|
||||||
|
A developer who wants the prebuilt bun2nix can add the substituter to their own Nix configuration.
|
||||||
|
- `packages/bin/src/index.ts` is still the placeholder entry point.
|
||||||
|
The CLI's behaviour belongs to the core-parser and cli-view specs, so the compiled binary is a no-op that exits 0 — enough to prove the packaging seam.
|
||||||
52
.claude/tasks/0002-flake-outputs.md
Normal file
52
.claude/tasks/0002-flake-outputs.md
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
---
|
||||||
|
spec: nix-flake-packaging
|
||||||
|
blocked-by: 0001-flake-build
|
||||||
|
---
|
||||||
|
|
||||||
|
## What to build
|
||||||
|
|
||||||
|
The remaining flake outputs layered onto the working build: a runnable app, a development shell, direnv auto-loading, and a checks set that gates the project reproducibly.
|
||||||
|
|
||||||
|
The default app points at the compiled binary so the CLI runs without a prior install.
|
||||||
|
The default dev shell exposes the same pinned Bun as the build plus biome, and a `use flake` direnv configuration loads it automatically on entering the directory.
|
||||||
|
The checks set aggregates the Bun test tiers, a build-seam smoke check that invokes the built binary (for example `--help`) and asserts a zero exit, and biome lint run through the treefmt-nix flake-parts module sharing the existing biome configuration.
|
||||||
|
|
||||||
|
## Acceptance criteria
|
||||||
|
|
||||||
|
- [x] `nix run` executes the CLI without a prior install.
|
||||||
|
- [x] `nix develop` drops into a shell exposing the pinned Bun and biome at their expected versions.
|
||||||
|
- [x] The dev shell's Bun version is the same pin the build derivation uses.
|
||||||
|
- [x] A `use flake` `.envrc` auto-loads the dev shell for direnv users.
|
||||||
|
- [x] `nix flake check` runs the Bun test tiers, the build-seam smoke check against the Nix-built binary, and biome lint via treefmt-nix.
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
All five criteria were verified end-to-end: `nix run` exits 0, `nix develop` exposes Bun 1.3.13, biome 2.5.4, and bun2nix, and `nix flake check` runs all three checks green.
|
||||||
|
|
||||||
|
### Outputs
|
||||||
|
|
||||||
|
The default app points at the compiled binary via its store path.
|
||||||
|
The dev shell carries the pinned Bun, biome, and bun2nix — bun2nix so the repo's `postinstall` regeneration of `bun.nix` works inside the shell.
|
||||||
|
`.envrc` is a one-line `use flake`, and `.direnv/` is gitignored.
|
||||||
|
|
||||||
|
### Bun pin sameness
|
||||||
|
|
||||||
|
The dev shell takes Bun from `inputs.nixpkgs-bun` directly, while the build takes it through bun2nix, whose nixpkgs `follows` the same `nixpkgs-bun`.
|
||||||
|
Both therefore resolve to the one pinned input, which is the single source of the Bun version.
|
||||||
|
The value is not threaded through a shared binding because bun2nix bakes its own `pkgs.bun` into its build hook, so the compile-side Bun cannot be passed in — the shared `nixpkgs-bun` input is the seam instead.
|
||||||
|
|
||||||
|
### Checks
|
||||||
|
|
||||||
|
`tests` runs `bun test` across the workspace through bun2nix's check phase.
|
||||||
|
`smoke` runs the Nix-built binary with `--help` and asserts a zero exit; while the CLI is still a placeholder this passes for any argument, and it will exercise real `--help` once the CLI lands.
|
||||||
|
Biome runs through the treefmt-nix flake-parts module, which auto-adds its own check.
|
||||||
|
|
||||||
|
### Biome via treefmt-nix
|
||||||
|
|
||||||
|
The treefmt biome program reads the repo's `biome.json` (via `importJSON`) so there is one configuration, but with `vcs.enabled` overridden off: the reproducible check has no git tree, and biome's `useIgnoreFile` lookup errors without one, while treefmt already selects the files.
|
||||||
|
Schema validation is skipped because nixpkgs' biome is newer than the schema treefmt-nix would validate against, and the config is the one biome itself runs with.
|
||||||
|
|
||||||
|
### Deviation
|
||||||
|
|
||||||
|
`packages/core/src/index_test.ts` was reformatted by biome (two `test.todo` lines exceeded the repo's `lineWidth`).
|
||||||
|
This is a pre-existing file, formatted only to make the new lint gate green — a necessary consequence of adding the gate, not a change of its behaviour.
|
||||||
77
.claude/tasks/0003-view-skeleton.md
Normal file
77
.claude/tasks/0003-view-skeleton.md
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
---
|
||||||
|
spec: cli-view
|
||||||
|
---
|
||||||
|
|
||||||
|
## What to build
|
||||||
|
|
||||||
|
The walking skeleton: a runnable `kitchen view <file>` that reads a Recipe File, parses it, and prints it styled in the terminal.
|
||||||
|
This slice cuts the first complete thread through both layers with the smallest set of node types.
|
||||||
|
|
||||||
|
In `@kitchen-md/core`, stand up the `parse` function and the types module, modelling only what this slice renders: frontmatter passthrough, `HeadingBlock`, `ParagraphBlock`, and `TextNode`.
|
||||||
|
Set up the minimal internal remark pipeline needed for these (parse + frontmatter), with the translation layer from remark's output to core's own types.
|
||||||
|
`parse` is a pure, total function returning a `DocumentAST` of shape `{ frontmatter, blocks, diagnostics }`.
|
||||||
|
|
||||||
|
In `@kitchen-md/bin`, implement the `view` subcommand with commander taking one required file-path argument.
|
||||||
|
The entry point owns file I/O: it reads the file, calls `parse`, and passes the `DocumentAST` to a pure `render(ast)` function.
|
||||||
|
`render` walks the AST and returns an ANSI-styled string using chalk, which auto-suppresses colour when stdout is not a TTY.
|
||||||
|
Frontmatter prints as raw YAML followed by a visual separator, then the body: headings styled by level, paragraphs as prose with blank-line spacing.
|
||||||
|
|
||||||
|
The demoable outcome: `kitchen view <recipe.md>` shows metadata, headings, and prose; a missing file prints a human-readable error to stderr and exits non-zero.
|
||||||
|
|
||||||
|
## Acceptance criteria
|
||||||
|
|
||||||
|
- [x] `parse` returns a `DocumentAST` `{ frontmatter, blocks, diagnostics }`; frontmatter is a plain object (empty when absent), diagnostics empty in the normal case
|
||||||
|
- [x] Core types live in a dedicated types module and are re-exported from the package entry point alongside `parse`
|
||||||
|
- [x] Headings (levels 1–6) and paragraphs are modelled as `HeadingBlock` and `ParagraphBlock`, with paragraph content as an inline array of `TextNode`
|
||||||
|
- [x] Blocks are flat and in document order (a heading is a sibling of the following paragraph, not its parent)
|
||||||
|
- [x] remark types do not appear in core's public API
|
||||||
|
- [x] `kitchen view <file>` reads the file, calls `parse`, and prints the rendered output
|
||||||
|
- [x] `render(ast)` is pure (no I/O, no side effects) and returns an ANSI-styled string
|
||||||
|
- [x] Frontmatter renders as raw YAML before the body, followed by a visual separator
|
||||||
|
- [x] Headings render bold and distinct by level; paragraphs render prose followed by a blank line
|
||||||
|
- [x] chalk styling is suppressed automatically when stdout is not a TTY
|
||||||
|
- [x] A missing file path prints commander usage to stderr and exits 1; an unreadable/nonexistent file prints a human-readable error to stderr and exits 1
|
||||||
|
- [x] Renderer unit tests (ANSI stripped) cover frontmatter, the separator, headings, and paragraphs
|
||||||
|
- [x] Core unit tests cover frontmatter passthrough (arbitrary fields, empty, absent), headings at every level, and paragraphs with `TextNode` content
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
All acceptance criteria are met. The following decisions and scope boundaries are worth recording.
|
||||||
|
|
||||||
|
### Scope boundaries carried by the slice
|
||||||
|
|
||||||
|
Only `HeadingBlock`, `ParagraphBlock`, and `TextNode` are modelled, as the slice specifies.
|
||||||
|
The remark→core translation therefore skips any block that is not a heading or paragraph (lists, blockquotes, code, thematic breaks), and `translateInline` keeps only text nodes, dropping every other inline node type.
|
||||||
|
The drop is by whole node, so emphasised or linked text is currently lost, not merely unstyled.
|
||||||
|
This is within task 0003's stated scope; the lossless `RawInline`/`RawBlock` fallback and the typed `EmphasisNode`/`StrongNode`/`LinkNode` land in task 0004, which also makes the translation recurse into container children.
|
||||||
|
|
||||||
|
Malformed-frontmatter handling is out of scope here and owned by task 0007.
|
||||||
|
This slice parses well-formed frontmatter and returns `{}` for the empty and absent cases; a genuinely malformed YAML block would currently throw from the YAML parser.
|
||||||
|
The total-function guarantee for that case (returning `{}` plus an `invalid-frontmatter` diagnostic) arrives with 0007.
|
||||||
|
|
||||||
|
### Types defined ahead of full use
|
||||||
|
|
||||||
|
`Diagnostic`, `Point`, and `Position` are defined in the types module because `DocumentAST.diagnostics` is typed `Diagnostic[]`, even though only the empty case (`diagnostics: []`) is produced in this slice.
|
||||||
|
This keeps the public shape stable; 0007 populates the channel.
|
||||||
|
|
||||||
|
### Rendering decisions
|
||||||
|
|
||||||
|
Headings render distinct-by-level via chalk, tapering from bold at level 1 toward dim at level 6; a heading is followed by a single newline and a paragraph by a blank line, which is what visually separates them once ANSI is stripped.
|
||||||
|
Per the cli-view spec, the specific colour and weight choices are visual decisions verified by inspection, not asserted in tests — the renderer tests assert ANSI-stripped text, spacing, and the presence/absence of styling, not particular colours.
|
||||||
|
The frontmatter separator is a dimmed 40-character box-drawing rule.
|
||||||
|
|
||||||
|
### Test suite structured on ADR 0009's tiers
|
||||||
|
|
||||||
|
The suite is organised by the three tiers ADR 0009 defines, each seam tested at exactly one tier.
|
||||||
|
Because task 0003's code was already built, these are characterization tests — green on arrival — asserting observable behaviour against independent literals rather than restating the implementation.
|
||||||
|
|
||||||
|
Unit tests cover the two pure seams.
|
||||||
|
`packages/core/src/parse_test.ts` asserts `parse` through the package barrel: frontmatter passthrough for arbitrary, empty, and absent blocks, headings at every level 1–6, paragraphs with `TextNode` content, flat document order, and empty diagnostics.
|
||||||
|
`packages/bin/src/render_test.ts` asserts `render` on ANSI-stripped output: headings at every level, paragraph blank-line spacing, the frontmatter YAML with its separator, and their absence when frontmatter is empty.
|
||||||
|
|
||||||
|
The `view` command function and the CLI share `packages/bin/src/index.ts`: `viewFile`, `formatError`, and the `CliError` union are value-returning and tested in-process, while the thin `program.parse()` dispatch is guarded behind `import.meta.main` so importing the module for a test never runs the CLI.
|
||||||
|
Integration tests (`packages/bin/src/index_test.ts`) assert that command function's returned `Result` in-process: `ok` with rendered output for a readable file, frontmatter passthrough, a `read-failed` error for a missing path, and `formatError`'s message.
|
||||||
|
|
||||||
|
End-to-end tests (`packages/bin/src/end_to_end_test.ts`) drive the `kitchen` binary as a subprocess, asserting exit codes, stream routing, document order, ANSI suppression on a pipe, the missing-file and missing-argument errors, and `--help`.
|
||||||
|
`fixtures/prose.md` is a committed recipe using only the constructs this slice models — frontmatter, headings at levels 1–3, and plain-text paragraphs — so it renders losslessly today.
|
||||||
|
The full `fixtures/basic.md` end-to-end remains task 0007's, once 0004's richer nodes make that fixture render losslessly.
|
||||||
69
.claude/tasks/0004-richer-blocks-and-inline.md
Normal file
69
.claude/tasks/0004-richer-blocks-and-inline.md
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
---
|
||||||
|
spec: core-parser
|
||||||
|
blocked-by: 0003-view-skeleton
|
||||||
|
---
|
||||||
|
|
||||||
|
## What to build
|
||||||
|
|
||||||
|
Extend the parser and renderer to cover the rest of the everyday Markdown body, so `kitchen view` renders lists, blockquotes, code, rules, and inline emphasis faithfully.
|
||||||
|
|
||||||
|
In `@kitchen-md/core`, model the remaining block types: `ListBlock` (ordered flag + items) whose items are `ListItemBlock` containers holding `Block[]`, `BlockquoteBlock` as a container holding `Block[]` (OFM callouts like `> [!note]` parse as ordinary blockquotes), `CodeBlock` (optional language + literal text, no annotation parsing), and `ThematicBreakBlock`.
|
||||||
|
Add the inline nodes: `EmphasisNode`, `StrongNode`, `CodeSpanNode`, and `LinkNode` (href + inline content).
|
||||||
|
Add the raw fallbacks: any remark block or inline node core does not model falls through to `RawBlock` / `RawInline`, whose verbatim `value` is captured by position-slicing the original input (never re-stringified), so unmodelled constructs round-trip byte-for-byte.
|
||||||
|
Add remark-gfm to the pipeline; the translation layer recurses into container children rather than collapsing them.
|
||||||
|
|
||||||
|
In `@kitchen-md/bin`, extend `render` for the new nodes: lists (bullet for unordered, sequential number for ordered, one item per line), code blocks (literal, no highlighting), thematic breaks (a horizontal rule string), emphasis (italic), strong (bold), code spans (a distinct dim/inverse style), and links (inline content only, href not shown).
|
||||||
|
|
||||||
|
## Acceptance criteria
|
||||||
|
|
||||||
|
- [x] `ListBlock` carries an ordered flag and an array of `ListItemBlock`; each `ListItemBlock` is a container wrapping child blocks (e.g. a paragraph), not a bare inline array
|
||||||
|
- [x] `BlockquoteBlock` is a container holding `Block[]`; a callout (`> [!note]`) parses as an ordinary blockquote with its text preserved
|
||||||
|
- [x] `CodeBlock` carries an optional language and literal text, and its content is not annotation-parsed
|
||||||
|
- [x] `ThematicBreakBlock` is modelled
|
||||||
|
- [x] Inline `EmphasisNode`, `StrongNode`, `CodeSpanNode`, and `LinkNode` are modelled; code-span content is not annotation-parsed
|
||||||
|
- [x] An unmodelled block (e.g. a GFM table) falls through to `RawBlock` and an unmodelled inline (e.g. strikethrough) to `RawInline`, each preserving byte-for-byte verbatim source via position-slicing
|
||||||
|
- [x] `render` handles ordered and unordered lists, code blocks, thematic breaks, and blockquotes
|
||||||
|
- [x] `render` handles emphasis (italic), strong (bold), code span (distinct style), and link (inline content only)
|
||||||
|
- [x] Core unit tests cover each new block type, the list-item-wraps-a-paragraph shape, each new inline node, and the raw block/inline fallbacks with verbatim source
|
||||||
|
- [x] Renderer unit tests (ANSI stripped) cover each new block and inline node
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
All acceptance criteria are met, with no dropped or deferred criteria.
|
||||||
|
The following decisions are worth recording.
|
||||||
|
|
||||||
|
### Node discriminants mirror mdast
|
||||||
|
|
||||||
|
The new nodes reuse mdast's own type names as their discriminants — `list`, `listItem`, `blockquote`, `code`, `thematicBreak`, `emphasis`, `strong`, `link` — matching the precedent set by task 0003 (`text`, `heading`, `paragraph`).
|
||||||
|
The two exceptions are the code span (`codeSpan`, since the interface is `CodeSpanNode` and mdast's `inlineCode` reads oddly in this AST) and the raw fallbacks (`raw` for blocks, `rawInline` for inlines), which have no mdast counterpart.
|
||||||
|
|
||||||
|
### `ListItemBlock` is scoped to `ListBlock.items`, not the `Block` union
|
||||||
|
|
||||||
|
A list item exists only as a member of a list, so `ListItemBlock` is deliberately kept out of the top-level `Block` union and the `renderBlock` switch.
|
||||||
|
List items are dispatched through `renderItemContent`, reached only from `renderList`.
|
||||||
|
This keeps the type as tight as the spec's "an array of `ListItemBlock`" shape rather than letting a list item appear anywhere a block is valid.
|
||||||
|
|
||||||
|
### `CodeBlock.lang` is omitted, not `undefined`, when absent
|
||||||
|
|
||||||
|
`lang` is an optional property that is left off entirely for an unlabelled fence rather than set to `undefined`, so `{ type: "code", value: "…" }` deep-equals the parser output without a stray `lang: undefined` key.
|
||||||
|
|
||||||
|
### Frontmatter must be excluded from the raw fallback
|
||||||
|
|
||||||
|
With the raw fallback now catching every unmodelled block, the `yaml` node — captured separately as frontmatter — is explicitly dropped in `translateBlock` so it does not also surface as a `RawBlock` and double-render.
|
||||||
|
|
||||||
|
### Content-not-annotation-parsed is satisfied structurally
|
||||||
|
|
||||||
|
Code blocks and code spans store their literal `value` verbatim and are never fed to `translateInline`.
|
||||||
|
Annotation parsing itself lands in task 0006, so "not annotation-parsed" holds here by construction: there is no annotation pass for that content to escape.
|
||||||
|
|
||||||
|
### Rendering decisions (visual, not asserted by colour)
|
||||||
|
|
||||||
|
Following task 0003, the renderer tests assert ANSI-stripped text, spacing, and the presence/absence of styling — never particular colours.
|
||||||
|
Unordered items use a `•` bullet, ordered items a sequential `1.`-from-one number (the model carries only the `ordered` flag, not a start offset), one item per line.
|
||||||
|
Blockquote lines are prefixed with a dim `│ `, code blocks and the thematic-break rule render dim, code spans use `inverse` to stay distinct from the dim used elsewhere, emphasis is italic, strong is bold, and links render their inline content underlined with the href hidden.
|
||||||
|
The list-item and blockquote renderers collapse child-block trailing spacing (`trimEnd`) so nested paragraphs sit on the marker/quote line rather than emitting their own blank line.
|
||||||
|
|
||||||
|
### remark-gfm
|
||||||
|
|
||||||
|
`remark-gfm` is added to the core pipeline so tables and strikethrough parse as their own nodes (`table`, `delete`) and therefore reach the raw fallback, where position-slicing captures them byte-for-byte.
|
||||||
|
Without gfm they would parse as ordinary paragraph text and never exercise the fallback.
|
||||||
63
.claude/tasks/0005-cross-references.md
Normal file
63
.claude/tasks/0005-cross-references.md
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
---
|
||||||
|
spec: core-parser
|
||||||
|
blocked-by: 0004-richer-blocks-and-inline
|
||||||
|
---
|
||||||
|
|
||||||
|
## What to build
|
||||||
|
|
||||||
|
Add Obsidian cross-references so `[[links]]` and `![[embeds]]` — including KitchenMD Step References — are typed nodes the parser surfaces and the renderer styles.
|
||||||
|
|
||||||
|
In `@kitchen-md/core`, model `WikilinkNode` and `TransclusionNode`, both `{ target, anchor?, display? }`: `target` is the filename without extension, `anchor` is the part after `#` passed through verbatim, `display` is the alias after `|`.
|
||||||
|
The node type — not a boolean flag — discriminates a reference (`[[…]]`) from an embed (`![[…]]`).
|
||||||
|
`TransclusionNode` covers KitchenMD Step References (`![[file#section:N]]`, `![[file#N]]`), whose anchor is passed through as-is; Step Reference resolution is out of scope.
|
||||||
|
Add remark-wiki-link to the pipeline.
|
||||||
|
|
||||||
|
In `@kitchen-md/bin`, render wikilinks distinctly from surrounding prose (underline or distinct colour), using display text or the target when there is no display text.
|
||||||
|
Render transclusions as their raw source text (e.g. `![[file#section:1]]`).
|
||||||
|
|
||||||
|
## Acceptance criteria
|
||||||
|
|
||||||
|
- [x] `WikilinkNode` and `TransclusionNode` share the `{ target, anchor?, display? }` shape and are distinct node types
|
||||||
|
- [x] Wikilinks parse with a bare target, with an anchor, and with a display alias; the anchor is passed through verbatim
|
||||||
|
- [x] Transclusions parse with a display alias and with a Step Reference anchor (`#section:N` and `#N`) passed through as-is
|
||||||
|
- [x] `render` shows wikilinks distinctly (display text, or target when absent)
|
||||||
|
- [x] `render` shows transclusions as their raw source text
|
||||||
|
- [x] Core unit tests cover wikilink (bare, anchor, display) and transclusion (bare, display, Step Reference anchor)
|
||||||
|
- [x] Renderer unit tests (ANSI stripped) cover wikilink and transclusion rendering
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
All acceptance criteria are met, with no dropped or deferred criteria.
|
||||||
|
The following decisions are worth recording.
|
||||||
|
|
||||||
|
### remark-wiki-link handles only `[[…]]`, so transclusions are recovered separately
|
||||||
|
|
||||||
|
The installed `remark-wiki-link@2.0.1` (landakram) parses `[[…]]` into `wikiLink` mdast nodes but does not recognise `![[…]]` embeds.
|
||||||
|
The leading `!` makes remark attempt an image, which fails and leaves the whole span as literal text.
|
||||||
|
Transclusions are therefore recovered in the translation layer by scanning each text run for `!\[\[…]]` and splitting it into text and `TransclusionNode` parts.
|
||||||
|
This keeps the plugin in the pipeline as the spec asks while still surfacing transclusions as typed nodes.
|
||||||
|
|
||||||
|
### Anchors are split in the translation layer, not by the plugin
|
||||||
|
|
||||||
|
remark-wiki-link leaves the `#anchor` attached to the node's `value` and does not separate it.
|
||||||
|
A shared `splitAnchor` helper splits `target#anchor` at the first `#` for both node types, so the anchor is passed through verbatim and the key is omitted entirely when absent.
|
||||||
|
|
||||||
|
### The alias divider is set to `|`
|
||||||
|
|
||||||
|
The plugin defaults its alias divider to `:`, which would consume a Step Reference anchor such as `#rolling:2`.
|
||||||
|
It is configured with `aliasDivider: "|"` so that only a real Obsidian alias is split off, and a display alias is recorded only when it differs from the node's value.
|
||||||
|
|
||||||
|
### Discriminants mirror the interface names
|
||||||
|
|
||||||
|
Following task 0004's precedent for `codeSpan`, the discriminants are `"wikilink"` and `"transclusion"` — the interface names lowercased — since neither construct has an mdast counterpart to borrow a type name from.
|
||||||
|
|
||||||
|
### Optional keys are omitted when absent
|
||||||
|
|
||||||
|
Consistent with task 0004, `anchor` and `display` are spread in only when present rather than set to `undefined`, so parser output deep-equals the expected node shape without stray keys.
|
||||||
|
|
||||||
|
### Rendering follows task 0003/0004 conventions
|
||||||
|
|
||||||
|
Wikilinks render underlined, showing the display text or the target when there is no display, matching how links render (both are references).
|
||||||
|
When a wikilink has an anchor but no display, only the target text shows — this follows the spec's wording ("display text, or the target when there is no display text") exactly.
|
||||||
|
Transclusions render as their reconstructed raw source (`![[target#anchor|display]]`), unstyled, since resolving the embed is out of scope.
|
||||||
|
The renderer tests assert ANSI-stripped text plus the presence of styling, never particular colours.
|
||||||
13
.gitignore
vendored
13
.gitignore
vendored
@@ -3,6 +3,19 @@ node_modules/
|
|||||||
# compiled CLI binary
|
# compiled CLI binary
|
||||||
packages/bin/kitchen
|
packages/bin/kitchen
|
||||||
|
|
||||||
|
# nix build result symlinks
|
||||||
|
result
|
||||||
|
result-*
|
||||||
|
|
||||||
|
# test coverage output
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
# generated test report and its intermediate artifacts
|
||||||
|
reports/
|
||||||
|
|
||||||
|
# direnv / nix-direnv cache
|
||||||
|
.direnv/
|
||||||
|
|
||||||
# local environment overrides
|
# local environment overrides
|
||||||
.env
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
|
|||||||
228
bun.lock
228
bun.lock
@@ -7,6 +7,8 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^2.5.3",
|
"@biomejs/biome": "^2.5.3",
|
||||||
"bun-types": "latest",
|
"bun-types": "latest",
|
||||||
|
"bun2nix": "^2.1.2",
|
||||||
|
"fast-xml-parser": "^5.10.1",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/bin": {
|
"packages/bin": {
|
||||||
@@ -17,14 +19,28 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kitchen-md/core": "workspace:*",
|
"@kitchen-md/core": "workspace:*",
|
||||||
|
"chalk": "^5.6.2",
|
||||||
|
"commander": "^15.0.0",
|
||||||
|
"neverthrow": "^8.2.0",
|
||||||
|
"yaml": "^2.9.0",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/core": {
|
"packages/core": {
|
||||||
"name": "@kitchen-md/core",
|
"name": "@kitchen-md/core",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"remark-frontmatter": "^5.0.0",
|
||||||
|
"remark-gfm": "^4.0.1",
|
||||||
|
"remark-parse": "^11.0.0",
|
||||||
|
"remark-wiki-link": "^2.0.1",
|
||||||
|
"unified": "^11.0.5",
|
||||||
|
"yaml": "^2.9.0",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages": {
|
"packages": {
|
||||||
|
"@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="],
|
||||||
|
|
||||||
"@biomejs/biome": ["@biomejs/biome@2.5.3", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.5.3", "@biomejs/cli-darwin-x64": "2.5.3", "@biomejs/cli-linux-arm64": "2.5.3", "@biomejs/cli-linux-arm64-musl": "2.5.3", "@biomejs/cli-linux-x64": "2.5.3", "@biomejs/cli-linux-x64-musl": "2.5.3", "@biomejs/cli-win32-arm64": "2.5.3", "@biomejs/cli-win32-x64": "2.5.3" }, "bin": { "biome": "bin/biome" } }, "sha512-MrJswFdei9EfDwwUy2tQrPDpK0AO+RmMFvBoaaJ6ayBc3sUbHdCE+XG5N8vp+5So41ZupZJQm0roHFFhMGVD7A=="],
|
"@biomejs/biome": ["@biomejs/biome@2.5.3", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.5.3", "@biomejs/cli-darwin-x64": "2.5.3", "@biomejs/cli-linux-arm64": "2.5.3", "@biomejs/cli-linux-arm64-musl": "2.5.3", "@biomejs/cli-linux-x64": "2.5.3", "@biomejs/cli-linux-x64-musl": "2.5.3", "@biomejs/cli-win32-arm64": "2.5.3", "@biomejs/cli-win32-x64": "2.5.3" }, "bin": { "biome": "bin/biome" } }, "sha512-MrJswFdei9EfDwwUy2tQrPDpK0AO+RmMFvBoaaJ6ayBc3sUbHdCE+XG5N8vp+5So41ZupZJQm0roHFFhMGVD7A=="],
|
||||||
|
|
||||||
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.5.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QhYP9muVQ0nUO5zztFuPbEwi4+94sJWVjaZds9aMi1l/KNZBiUjdiSUrGHsTaMGDXrYl+r4AS2sUKfgH3w+V3g=="],
|
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.5.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QhYP9muVQ0nUO5zztFuPbEwi4+94sJWVjaZds9aMi1l/KNZBiUjdiSUrGHsTaMGDXrYl+r4AS2sUKfgH3w+V3g=="],
|
||||||
@@ -47,10 +63,222 @@
|
|||||||
|
|
||||||
"@kitchen-md/core": ["@kitchen-md/core@workspace:packages/core"],
|
"@kitchen-md/core": ["@kitchen-md/core@workspace:packages/core"],
|
||||||
|
|
||||||
|
"@nodable/entities": ["@nodable/entities@3.0.0", "", {}, "sha512-8L9xFeTYKhm49xfIypoe2W5wV1m/3Z58kT+7kR9A8OyFxcPduI4VmxaUMQyKYrRjUoLLSXv6EKKID5Tvj9cUVw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.62.3", "", { "os": "linux", "cpu": "x64" }, "sha512-V4KtWtQfAFMU7+9/A/VDps/VI8CHd3cYz0L8sgJzz8qK7eY7wI4ruFD82UYIYvW9Z4DtlTfhQcsl4XyPHW5uSg=="],
|
||||||
|
|
||||||
|
"@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="],
|
||||||
|
|
||||||
|
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
|
||||||
|
|
||||||
|
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
|
||||||
|
|
||||||
"@types/node": ["@types/node@26.1.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw=="],
|
"@types/node": ["@types/node@26.1.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw=="],
|
||||||
|
|
||||||
|
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
|
||||||
|
|
||||||
|
"anynum": ["anynum@1.0.1", "", {}, "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A=="],
|
||||||
|
|
||||||
|
"bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
|
||||||
|
|
||||||
"bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
|
"bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
|
||||||
|
|
||||||
|
"bun2nix": ["bun2nix@2.1.2", "", { "dependencies": { "sade": "^1.8.1" }, "bin": { "bun2nix": "index.ts" } }, "sha512-0wx6Ar5ccrz4aSD5prbShwymjDEXFh7Bucxs+YrpAMa67TnVB95Hv8FV3oaQEbtOx6QGgIAyOmap6Y3WCRqetg=="],
|
||||||
|
|
||||||
|
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
|
||||||
|
|
||||||
|
"chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
|
||||||
|
|
||||||
|
"character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
|
||||||
|
|
||||||
|
"character-entities-legacy": ["character-entities-legacy@1.1.4", "", {}, "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA=="],
|
||||||
|
|
||||||
|
"character-reference-invalid": ["character-reference-invalid@1.1.4", "", {}, "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg=="],
|
||||||
|
|
||||||
|
"commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="],
|
||||||
|
|
||||||
|
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||||
|
|
||||||
|
"decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="],
|
||||||
|
|
||||||
|
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
|
||||||
|
|
||||||
|
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
|
||||||
|
|
||||||
|
"escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
|
||||||
|
|
||||||
|
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
|
||||||
|
|
||||||
|
"fast-xml-builder": ["fast-xml-builder@1.3.0", "", { "dependencies": { "path-expression-matcher": "^1.6.2", "xml-naming": "^0.3.0" } }, "sha512-F74cZEdCvuw9P41GAC3rod4X04jjWGM1JPEv/GWSqFTWLsdyMSBMBMlm9Hk3GLBgLBbdBNY8yee0pQh2RBVESQ=="],
|
||||||
|
|
||||||
|
"fast-xml-parser": ["fast-xml-parser@5.10.1", "", { "dependencies": { "@nodable/entities": "^3.0.0", "fast-xml-builder": "^1.2.0", "is-unsafe": "^2.0.0", "path-expression-matcher": "^1.6.2", "strnum": "^2.4.1", "xml-naming": "^0.3.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-IEMIf7298kXuZSRFoGfMYrl7is8LpavODgbNz1cwIudv7KwVFnuU+UsMporfq6PD6aXSlawZlARiA3UywCTfMw=="],
|
||||||
|
|
||||||
|
"fault": ["fault@2.0.1", "", { "dependencies": { "format": "^0.2.0" } }, "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ=="],
|
||||||
|
|
||||||
|
"format": ["format@0.2.2", "", {}, "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww=="],
|
||||||
|
|
||||||
|
"is-alphabetical": ["is-alphabetical@1.0.4", "", {}, "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg=="],
|
||||||
|
|
||||||
|
"is-alphanumerical": ["is-alphanumerical@1.0.4", "", { "dependencies": { "is-alphabetical": "^1.0.0", "is-decimal": "^1.0.0" } }, "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A=="],
|
||||||
|
|
||||||
|
"is-decimal": ["is-decimal@1.0.4", "", {}, "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw=="],
|
||||||
|
|
||||||
|
"is-hexadecimal": ["is-hexadecimal@1.0.4", "", {}, "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw=="],
|
||||||
|
|
||||||
|
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
|
||||||
|
|
||||||
|
"is-unsafe": ["is-unsafe@2.0.0", "", {}, "sha512-2LdV822R+wmI86unXA93WCFpL6g+av8ynWk0nrHyJqGop5VoocYsSLFgN8jrfalT6iGeLNM4KXuVSsULP53kEA=="],
|
||||||
|
|
||||||
|
"longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
|
||||||
|
|
||||||
|
"markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
|
||||||
|
|
||||||
|
"mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="],
|
||||||
|
|
||||||
|
"mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="],
|
||||||
|
|
||||||
|
"mdast-util-frontmatter": ["mdast-util-frontmatter@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "escape-string-regexp": "^5.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-extension-frontmatter": "^2.0.0" } }, "sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA=="],
|
||||||
|
|
||||||
|
"mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="],
|
||||||
|
|
||||||
|
"mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="],
|
||||||
|
|
||||||
|
"mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="],
|
||||||
|
|
||||||
|
"mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="],
|
||||||
|
|
||||||
|
"mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="],
|
||||||
|
|
||||||
|
"mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="],
|
||||||
|
|
||||||
|
"mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="],
|
||||||
|
|
||||||
|
"mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="],
|
||||||
|
|
||||||
|
"mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="],
|
||||||
|
|
||||||
|
"mdast-util-wiki-link": ["mdast-util-wiki-link@0.1.2", "", { "dependencies": { "@babel/runtime": "^7.12.1", "mdast-util-to-markdown": "^0.6.5" } }, "sha512-DTcDyOxKDo3pB3fc0zQlD8myfQjYkW4hazUKI9PUyhtoj9JBeHC2eIdlVXmaT22bZkFAVU2d47B6y2jVKGoUQg=="],
|
||||||
|
|
||||||
|
"micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="],
|
||||||
|
|
||||||
|
"micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="],
|
||||||
|
|
||||||
|
"micromark-extension-frontmatter": ["micromark-extension-frontmatter@2.0.0", "", { "dependencies": { "fault": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg=="],
|
||||||
|
|
||||||
|
"micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="],
|
||||||
|
|
||||||
|
"micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="],
|
||||||
|
|
||||||
|
"micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="],
|
||||||
|
|
||||||
|
"micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="],
|
||||||
|
|
||||||
|
"micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="],
|
||||||
|
|
||||||
|
"micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="],
|
||||||
|
|
||||||
|
"micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="],
|
||||||
|
|
||||||
|
"micromark-extension-wiki-link": ["micromark-extension-wiki-link@0.0.4", "", { "dependencies": { "@babel/runtime": "^7.12.1" } }, "sha512-dJc8AfnoU8BHkN+7fWZvIS20SMsMS1ZlxQUn6We67MqeKbOiEDZV5eEvCpwqGBijbJbxX3Kxz879L4K9HIiOvw=="],
|
||||||
|
|
||||||
|
"micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="],
|
||||||
|
|
||||||
|
"micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="],
|
||||||
|
|
||||||
|
"micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="],
|
||||||
|
|
||||||
|
"micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="],
|
||||||
|
|
||||||
|
"micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="],
|
||||||
|
|
||||||
|
"micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="],
|
||||||
|
|
||||||
|
"micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="],
|
||||||
|
|
||||||
|
"micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="],
|
||||||
|
|
||||||
|
"micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="],
|
||||||
|
|
||||||
|
"micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="],
|
||||||
|
|
||||||
|
"micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="],
|
||||||
|
|
||||||
|
"micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="],
|
||||||
|
|
||||||
|
"micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="],
|
||||||
|
|
||||||
|
"micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="],
|
||||||
|
|
||||||
|
"micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="],
|
||||||
|
|
||||||
|
"micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="],
|
||||||
|
|
||||||
|
"micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="],
|
||||||
|
|
||||||
|
"micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="],
|
||||||
|
|
||||||
|
"micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
|
||||||
|
|
||||||
|
"mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="],
|
||||||
|
|
||||||
|
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
|
"neverthrow": ["neverthrow@8.2.0", "", { "optionalDependencies": { "@rollup/rollup-linux-x64-gnu": "^4.24.0" } }, "sha512-kOCT/1MCPAxY5iUV3wytNFUMUolzuwd/VF/1KCx7kf6CutrOsTie+84zTGTpgQycjvfLdBBdvBvFLqFD2c0wkQ=="],
|
||||||
|
|
||||||
|
"parse-entities": ["parse-entities@2.0.0", "", { "dependencies": { "character-entities": "^1.0.0", "character-entities-legacy": "^1.0.0", "character-reference-invalid": "^1.0.0", "is-alphanumerical": "^1.0.0", "is-decimal": "^1.0.0", "is-hexadecimal": "^1.0.0" } }, "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ=="],
|
||||||
|
|
||||||
|
"path-expression-matcher": ["path-expression-matcher@1.6.2", "", {}, "sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ=="],
|
||||||
|
|
||||||
|
"remark-frontmatter": ["remark-frontmatter@5.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-frontmatter": "^2.0.0", "micromark-extension-frontmatter": "^2.0.0", "unified": "^11.0.0" } }, "sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ=="],
|
||||||
|
|
||||||
|
"remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="],
|
||||||
|
|
||||||
|
"remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="],
|
||||||
|
|
||||||
|
"remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="],
|
||||||
|
|
||||||
|
"remark-wiki-link": ["remark-wiki-link@2.0.1", "", { "dependencies": { "@babel/runtime": "^7.4.4", "mdast-util-wiki-link": "^0.1.2", "micromark-extension-wiki-link": "^0.0.4" } }, "sha512-F8Eut1E7GWfFm4ZDTI6/4ejeZEHZgnVk6E933Yqd/ssYsc4AyI32aGakxwsGcEzbbE7dkWi1EfLlGAdGgOZOsA=="],
|
||||||
|
|
||||||
|
"repeat-string": ["repeat-string@1.6.1", "", {}, "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w=="],
|
||||||
|
|
||||||
|
"sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="],
|
||||||
|
|
||||||
|
"strnum": ["strnum@2.4.1", "", { "dependencies": { "anynum": "^1.0.1" } }, "sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg=="],
|
||||||
|
|
||||||
|
"trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
|
||||||
|
|
||||||
"undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
|
"undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
|
||||||
|
|
||||||
|
"unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],
|
||||||
|
|
||||||
|
"unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="],
|
||||||
|
|
||||||
|
"unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
|
||||||
|
|
||||||
|
"unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="],
|
||||||
|
|
||||||
|
"unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="],
|
||||||
|
|
||||||
|
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
|
||||||
|
|
||||||
|
"vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
|
||||||
|
|
||||||
|
"xml-naming": ["xml-naming@0.3.0", "", {}, "sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ=="],
|
||||||
|
|
||||||
|
"yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
|
||||||
|
|
||||||
|
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
||||||
|
|
||||||
|
"mdast-util-wiki-link/mdast-util-to-markdown": ["mdast-util-to-markdown@0.6.5", "", { "dependencies": { "@types/unist": "^2.0.0", "longest-streak": "^2.0.0", "mdast-util-to-string": "^2.0.0", "parse-entities": "^2.0.0", "repeat-string": "^1.0.0", "zwitch": "^1.0.0" } }, "sha512-XeV9sDE7ZlOQvs45C9UKMtfTcctcaj/pGwH8YLbMHoMOXNNCn2LsqVQOqrF1+/NU8lKDAqozme9SCXWyo9oAcQ=="],
|
||||||
|
|
||||||
|
"parse-entities/character-entities": ["character-entities@1.2.4", "", {}, "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw=="],
|
||||||
|
|
||||||
|
"mdast-util-wiki-link/mdast-util-to-markdown/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
||||||
|
|
||||||
|
"mdast-util-wiki-link/mdast-util-to-markdown/longest-streak": ["longest-streak@2.0.4", "", {}, "sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg=="],
|
||||||
|
|
||||||
|
"mdast-util-wiki-link/mdast-util-to-markdown/mdast-util-to-string": ["mdast-util-to-string@2.0.0", "", {}, "sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w=="],
|
||||||
|
|
||||||
|
"mdast-util-wiki-link/mdast-util-to-markdown/zwitch": ["zwitch@1.0.5", "", {}, "sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw=="],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
494
bun.nix
Normal file
494
bun.nix
Normal file
@@ -0,0 +1,494 @@
|
|||||||
|
# Autogenerated by `bun2nix`, editing manually is not recommended
|
||||||
|
#
|
||||||
|
# Set of Bun packages to install
|
||||||
|
#
|
||||||
|
# Consume this with `fetchBunDeps` (recommended)
|
||||||
|
# or `pkgs.callPackage` if you wish to handle
|
||||||
|
# it manually.
|
||||||
|
{
|
||||||
|
copyPathToStore,
|
||||||
|
fetchFromGitHub,
|
||||||
|
fetchgit,
|
||||||
|
fetchurl,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
{
|
||||||
|
"@babel/runtime@7.29.7" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz";
|
||||||
|
hash = "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==";
|
||||||
|
};
|
||||||
|
"@biomejs/biome@2.5.3" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/@biomejs/biome/-/biome-2.5.3.tgz";
|
||||||
|
hash = "sha512-MrJswFdei9EfDwwUy2tQrPDpK0AO+RmMFvBoaaJ6ayBc3sUbHdCE+XG5N8vp+5So41ZupZJQm0roHFFhMGVD7A==";
|
||||||
|
};
|
||||||
|
"@biomejs/cli-darwin-arm64@2.5.3" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.5.3.tgz";
|
||||||
|
hash = "sha512-QhYP9muVQ0nUO5zztFuPbEwi4+94sJWVjaZds9aMi1l/KNZBiUjdiSUrGHsTaMGDXrYl+r4AS2sUKfgH3w+V3g==";
|
||||||
|
};
|
||||||
|
"@biomejs/cli-darwin-x64@2.5.3" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.5.3.tgz";
|
||||||
|
hash = "sha512-NC1Ss13UaW7QZX+y8j44bF7AP0jSJdBl6iRhe0MAkvaSqZy+mWg3GaXsrb+eSoHoGDBtaXWEbMVV0iVN2cZ7cQ==";
|
||||||
|
};
|
||||||
|
"@biomejs/cli-linux-arm64-musl@2.5.3" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.5.3.tgz";
|
||||||
|
hash = "sha512-fccix0w6xp6csCXgxeC0dU/3ecgRQal0y+cv2SP9ajNlhe7Yrk2Ug7UDe2j9AT9ZDYitkXpvUKgZjjuoYeP4Vg==";
|
||||||
|
};
|
||||||
|
"@biomejs/cli-linux-arm64@2.5.3" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.5.3.tgz";
|
||||||
|
hash = "sha512-ksx1KWeyYW18ILL04msF/J4ZBtBDN33znYK8Z/aNv/vlBVxL9/g3mGP+omgHJKy4+KWbK87vcmmpmurfNjSgiA==";
|
||||||
|
};
|
||||||
|
"@biomejs/cli-linux-x64-musl@2.5.3" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.5.3.tgz";
|
||||||
|
hash = "sha512-O/yU9YKRUiHhmcjF2f38PSjseVk3G4VLWYc0G2HWpzdBVREV6G8IGWIVEFf7MFPfWIzNUIvPsEjeAZQIOgnLcQ==";
|
||||||
|
};
|
||||||
|
"@biomejs/cli-linux-x64@2.5.3" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.5.3.tgz";
|
||||||
|
hash = "sha512-yMkJtilsgvILDcVkh187aVLTb64xYsrxYajx5kym+r1ULkO5HUOfu9AYKLGQbOVLwJtT2utNw7hhFNg+17mUYA==";
|
||||||
|
};
|
||||||
|
"@biomejs/cli-win32-arm64@2.5.3" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.5.3.tgz";
|
||||||
|
hash = "sha512-cX5z+GYwRcqEok0AH3KSfQGgqYd0Nomfp6Fbe1uiTtELE38hdH2k842wQ9wLNaF/JJ7r4rjJQ4VR+ce+fRmQbw==";
|
||||||
|
};
|
||||||
|
"@biomejs/cli-win32-x64@2.5.3" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.5.3.tgz";
|
||||||
|
hash = "sha512-ExSaJWi4/u6+GXCszlSKpWSjKNbDseAYqqkCznsCsZ/4uidZ/BEqsCc5/3ctlq6dfIubdIIRSVLC/PG9xPl70Q==";
|
||||||
|
};
|
||||||
|
"@kitchen-md/bin" = copyPathToStore ./packages/bin;
|
||||||
|
"@kitchen-md/core" = copyPathToStore ./packages/core;
|
||||||
|
"@nodable/entities@3.0.0" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/@nodable/entities/-/entities-3.0.0.tgz";
|
||||||
|
hash = "sha512-8L9xFeTYKhm49xfIypoe2W5wV1m/3Z58kT+7kR9A8OyFxcPduI4VmxaUMQyKYrRjUoLLSXv6EKKID5Tvj9cUVw==";
|
||||||
|
};
|
||||||
|
"@rollup/rollup-linux-x64-gnu@4.62.3" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.3.tgz";
|
||||||
|
hash = "sha512-V4KtWtQfAFMU7+9/A/VDps/VI8CHd3cYz0L8sgJzz8qK7eY7wI4ruFD82UYIYvW9Z4DtlTfhQcsl4XyPHW5uSg==";
|
||||||
|
};
|
||||||
|
"@types/debug@4.1.13" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz";
|
||||||
|
hash = "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==";
|
||||||
|
};
|
||||||
|
"@types/mdast@4.0.4" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz";
|
||||||
|
hash = "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==";
|
||||||
|
};
|
||||||
|
"@types/ms@2.1.0" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz";
|
||||||
|
hash = "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==";
|
||||||
|
};
|
||||||
|
"@types/node@26.1.1" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz";
|
||||||
|
hash = "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==";
|
||||||
|
};
|
||||||
|
"@types/unist@2.0.11" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz";
|
||||||
|
hash = "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==";
|
||||||
|
};
|
||||||
|
"@types/unist@3.0.3" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz";
|
||||||
|
hash = "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==";
|
||||||
|
};
|
||||||
|
"anynum@1.0.1" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/anynum/-/anynum-1.0.1.tgz";
|
||||||
|
hash = "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==";
|
||||||
|
};
|
||||||
|
"bail@2.0.2" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz";
|
||||||
|
hash = "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==";
|
||||||
|
};
|
||||||
|
"bun-types@1.3.14" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/bun-types/-/bun-types-1.3.14.tgz";
|
||||||
|
hash = "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ==";
|
||||||
|
};
|
||||||
|
"bun2nix@2.1.2" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/bun2nix/-/bun2nix-2.1.2.tgz";
|
||||||
|
hash = "sha512-0wx6Ar5ccrz4aSD5prbShwymjDEXFh7Bucxs+YrpAMa67TnVB95Hv8FV3oaQEbtOx6QGgIAyOmap6Y3WCRqetg==";
|
||||||
|
};
|
||||||
|
"ccount@2.0.1" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz";
|
||||||
|
hash = "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==";
|
||||||
|
};
|
||||||
|
"chalk@5.6.2" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz";
|
||||||
|
hash = "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==";
|
||||||
|
};
|
||||||
|
"character-entities-legacy@1.1.4" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz";
|
||||||
|
hash = "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==";
|
||||||
|
};
|
||||||
|
"character-entities@1.2.4" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz";
|
||||||
|
hash = "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==";
|
||||||
|
};
|
||||||
|
"character-entities@2.0.2" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz";
|
||||||
|
hash = "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==";
|
||||||
|
};
|
||||||
|
"character-reference-invalid@1.1.4" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz";
|
||||||
|
hash = "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==";
|
||||||
|
};
|
||||||
|
"commander@15.0.0" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz";
|
||||||
|
hash = "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==";
|
||||||
|
};
|
||||||
|
"debug@4.4.3" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz";
|
||||||
|
hash = "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==";
|
||||||
|
};
|
||||||
|
"decode-named-character-reference@1.3.0" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz";
|
||||||
|
hash = "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==";
|
||||||
|
};
|
||||||
|
"dequal@2.0.3" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz";
|
||||||
|
hash = "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==";
|
||||||
|
};
|
||||||
|
"devlop@1.1.0" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz";
|
||||||
|
hash = "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==";
|
||||||
|
};
|
||||||
|
"escape-string-regexp@5.0.0" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz";
|
||||||
|
hash = "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==";
|
||||||
|
};
|
||||||
|
"extend@3.0.2" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz";
|
||||||
|
hash = "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==";
|
||||||
|
};
|
||||||
|
"fast-xml-builder@1.3.0" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.3.0.tgz";
|
||||||
|
hash = "sha512-F74cZEdCvuw9P41GAC3rod4X04jjWGM1JPEv/GWSqFTWLsdyMSBMBMlm9Hk3GLBgLBbdBNY8yee0pQh2RBVESQ==";
|
||||||
|
};
|
||||||
|
"fast-xml-parser@5.10.1" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.10.1.tgz";
|
||||||
|
hash = "sha512-IEMIf7298kXuZSRFoGfMYrl7is8LpavODgbNz1cwIudv7KwVFnuU+UsMporfq6PD6aXSlawZlARiA3UywCTfMw==";
|
||||||
|
};
|
||||||
|
"fault@2.0.1" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/fault/-/fault-2.0.1.tgz";
|
||||||
|
hash = "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==";
|
||||||
|
};
|
||||||
|
"format@0.2.2" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/format/-/format-0.2.2.tgz";
|
||||||
|
hash = "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==";
|
||||||
|
};
|
||||||
|
"is-alphabetical@1.0.4" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz";
|
||||||
|
hash = "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==";
|
||||||
|
};
|
||||||
|
"is-alphanumerical@1.0.4" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz";
|
||||||
|
hash = "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==";
|
||||||
|
};
|
||||||
|
"is-decimal@1.0.4" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz";
|
||||||
|
hash = "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==";
|
||||||
|
};
|
||||||
|
"is-hexadecimal@1.0.4" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz";
|
||||||
|
hash = "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==";
|
||||||
|
};
|
||||||
|
"is-plain-obj@4.1.0" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz";
|
||||||
|
hash = "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==";
|
||||||
|
};
|
||||||
|
"is-unsafe@2.0.0" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/is-unsafe/-/is-unsafe-2.0.0.tgz";
|
||||||
|
hash = "sha512-2LdV822R+wmI86unXA93WCFpL6g+av8ynWk0nrHyJqGop5VoocYsSLFgN8jrfalT6iGeLNM4KXuVSsULP53kEA==";
|
||||||
|
};
|
||||||
|
"longest-streak@2.0.4" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/longest-streak/-/longest-streak-2.0.4.tgz";
|
||||||
|
hash = "sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg==";
|
||||||
|
};
|
||||||
|
"longest-streak@3.1.0" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz";
|
||||||
|
hash = "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==";
|
||||||
|
};
|
||||||
|
"markdown-table@3.0.4" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz";
|
||||||
|
hash = "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==";
|
||||||
|
};
|
||||||
|
"mdast-util-find-and-replace@3.0.2" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz";
|
||||||
|
hash = "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==";
|
||||||
|
};
|
||||||
|
"mdast-util-from-markdown@2.0.3" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz";
|
||||||
|
hash = "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==";
|
||||||
|
};
|
||||||
|
"mdast-util-frontmatter@2.0.1" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/mdast-util-frontmatter/-/mdast-util-frontmatter-2.0.1.tgz";
|
||||||
|
hash = "sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA==";
|
||||||
|
};
|
||||||
|
"mdast-util-gfm-autolink-literal@2.0.1" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz";
|
||||||
|
hash = "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==";
|
||||||
|
};
|
||||||
|
"mdast-util-gfm-footnote@2.1.0" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz";
|
||||||
|
hash = "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==";
|
||||||
|
};
|
||||||
|
"mdast-util-gfm-strikethrough@2.0.0" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz";
|
||||||
|
hash = "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==";
|
||||||
|
};
|
||||||
|
"mdast-util-gfm-table@2.0.0" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz";
|
||||||
|
hash = "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==";
|
||||||
|
};
|
||||||
|
"mdast-util-gfm-task-list-item@2.0.0" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz";
|
||||||
|
hash = "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==";
|
||||||
|
};
|
||||||
|
"mdast-util-gfm@3.1.0" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz";
|
||||||
|
hash = "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==";
|
||||||
|
};
|
||||||
|
"mdast-util-phrasing@4.1.0" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz";
|
||||||
|
hash = "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==";
|
||||||
|
};
|
||||||
|
"mdast-util-to-markdown@0.6.5" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-0.6.5.tgz";
|
||||||
|
hash = "sha512-XeV9sDE7ZlOQvs45C9UKMtfTcctcaj/pGwH8YLbMHoMOXNNCn2LsqVQOqrF1+/NU8lKDAqozme9SCXWyo9oAcQ==";
|
||||||
|
};
|
||||||
|
"mdast-util-to-markdown@2.1.2" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz";
|
||||||
|
hash = "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==";
|
||||||
|
};
|
||||||
|
"mdast-util-to-string@2.0.0" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-2.0.0.tgz";
|
||||||
|
hash = "sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w==";
|
||||||
|
};
|
||||||
|
"mdast-util-to-string@4.0.0" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz";
|
||||||
|
hash = "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==";
|
||||||
|
};
|
||||||
|
"mdast-util-wiki-link@0.1.2" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/mdast-util-wiki-link/-/mdast-util-wiki-link-0.1.2.tgz";
|
||||||
|
hash = "sha512-DTcDyOxKDo3pB3fc0zQlD8myfQjYkW4hazUKI9PUyhtoj9JBeHC2eIdlVXmaT22bZkFAVU2d47B6y2jVKGoUQg==";
|
||||||
|
};
|
||||||
|
"micromark-core-commonmark@2.0.3" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz";
|
||||||
|
hash = "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==";
|
||||||
|
};
|
||||||
|
"micromark-extension-frontmatter@2.0.0" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/micromark-extension-frontmatter/-/micromark-extension-frontmatter-2.0.0.tgz";
|
||||||
|
hash = "sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg==";
|
||||||
|
};
|
||||||
|
"micromark-extension-gfm-autolink-literal@2.1.0" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz";
|
||||||
|
hash = "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==";
|
||||||
|
};
|
||||||
|
"micromark-extension-gfm-footnote@2.1.0" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz";
|
||||||
|
hash = "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==";
|
||||||
|
};
|
||||||
|
"micromark-extension-gfm-strikethrough@2.1.0" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz";
|
||||||
|
hash = "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==";
|
||||||
|
};
|
||||||
|
"micromark-extension-gfm-table@2.1.1" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz";
|
||||||
|
hash = "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==";
|
||||||
|
};
|
||||||
|
"micromark-extension-gfm-tagfilter@2.0.0" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz";
|
||||||
|
hash = "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==";
|
||||||
|
};
|
||||||
|
"micromark-extension-gfm-task-list-item@2.1.0" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz";
|
||||||
|
hash = "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==";
|
||||||
|
};
|
||||||
|
"micromark-extension-gfm@3.0.0" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz";
|
||||||
|
hash = "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==";
|
||||||
|
};
|
||||||
|
"micromark-extension-wiki-link@0.0.4" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/micromark-extension-wiki-link/-/micromark-extension-wiki-link-0.0.4.tgz";
|
||||||
|
hash = "sha512-dJc8AfnoU8BHkN+7fWZvIS20SMsMS1ZlxQUn6We67MqeKbOiEDZV5eEvCpwqGBijbJbxX3Kxz879L4K9HIiOvw==";
|
||||||
|
};
|
||||||
|
"micromark-factory-destination@2.0.1" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz";
|
||||||
|
hash = "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==";
|
||||||
|
};
|
||||||
|
"micromark-factory-label@2.0.1" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz";
|
||||||
|
hash = "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==";
|
||||||
|
};
|
||||||
|
"micromark-factory-space@2.0.1" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz";
|
||||||
|
hash = "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==";
|
||||||
|
};
|
||||||
|
"micromark-factory-title@2.0.1" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz";
|
||||||
|
hash = "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==";
|
||||||
|
};
|
||||||
|
"micromark-factory-whitespace@2.0.1" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz";
|
||||||
|
hash = "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==";
|
||||||
|
};
|
||||||
|
"micromark-util-character@2.1.1" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz";
|
||||||
|
hash = "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==";
|
||||||
|
};
|
||||||
|
"micromark-util-chunked@2.0.1" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz";
|
||||||
|
hash = "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==";
|
||||||
|
};
|
||||||
|
"micromark-util-classify-character@2.0.1" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz";
|
||||||
|
hash = "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==";
|
||||||
|
};
|
||||||
|
"micromark-util-combine-extensions@2.0.1" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz";
|
||||||
|
hash = "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==";
|
||||||
|
};
|
||||||
|
"micromark-util-decode-numeric-character-reference@2.0.2" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz";
|
||||||
|
hash = "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==";
|
||||||
|
};
|
||||||
|
"micromark-util-decode-string@2.0.1" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz";
|
||||||
|
hash = "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==";
|
||||||
|
};
|
||||||
|
"micromark-util-encode@2.0.1" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz";
|
||||||
|
hash = "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==";
|
||||||
|
};
|
||||||
|
"micromark-util-html-tag-name@2.0.1" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz";
|
||||||
|
hash = "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==";
|
||||||
|
};
|
||||||
|
"micromark-util-normalize-identifier@2.0.1" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz";
|
||||||
|
hash = "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==";
|
||||||
|
};
|
||||||
|
"micromark-util-resolve-all@2.0.1" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz";
|
||||||
|
hash = "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==";
|
||||||
|
};
|
||||||
|
"micromark-util-sanitize-uri@2.0.1" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz";
|
||||||
|
hash = "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==";
|
||||||
|
};
|
||||||
|
"micromark-util-subtokenize@2.1.0" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz";
|
||||||
|
hash = "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==";
|
||||||
|
};
|
||||||
|
"micromark-util-symbol@2.0.1" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz";
|
||||||
|
hash = "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==";
|
||||||
|
};
|
||||||
|
"micromark-util-types@2.0.2" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz";
|
||||||
|
hash = "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==";
|
||||||
|
};
|
||||||
|
"micromark@4.0.2" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz";
|
||||||
|
hash = "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==";
|
||||||
|
};
|
||||||
|
"mri@1.2.0" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz";
|
||||||
|
hash = "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==";
|
||||||
|
};
|
||||||
|
"ms@2.1.3" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz";
|
||||||
|
hash = "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==";
|
||||||
|
};
|
||||||
|
"neverthrow@8.2.0" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/neverthrow/-/neverthrow-8.2.0.tgz";
|
||||||
|
hash = "sha512-kOCT/1MCPAxY5iUV3wytNFUMUolzuwd/VF/1KCx7kf6CutrOsTie+84zTGTpgQycjvfLdBBdvBvFLqFD2c0wkQ==";
|
||||||
|
};
|
||||||
|
"parse-entities@2.0.0" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz";
|
||||||
|
hash = "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==";
|
||||||
|
};
|
||||||
|
"path-expression-matcher@1.6.2" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.6.2.tgz";
|
||||||
|
hash = "sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==";
|
||||||
|
};
|
||||||
|
"remark-frontmatter@5.0.0" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/remark-frontmatter/-/remark-frontmatter-5.0.0.tgz";
|
||||||
|
hash = "sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ==";
|
||||||
|
};
|
||||||
|
"remark-gfm@4.0.1" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz";
|
||||||
|
hash = "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==";
|
||||||
|
};
|
||||||
|
"remark-parse@11.0.0" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz";
|
||||||
|
hash = "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==";
|
||||||
|
};
|
||||||
|
"remark-stringify@11.0.0" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz";
|
||||||
|
hash = "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==";
|
||||||
|
};
|
||||||
|
"remark-wiki-link@2.0.1" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/remark-wiki-link/-/remark-wiki-link-2.0.1.tgz";
|
||||||
|
hash = "sha512-F8Eut1E7GWfFm4ZDTI6/4ejeZEHZgnVk6E933Yqd/ssYsc4AyI32aGakxwsGcEzbbE7dkWi1EfLlGAdGgOZOsA==";
|
||||||
|
};
|
||||||
|
"repeat-string@1.6.1" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz";
|
||||||
|
hash = "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==";
|
||||||
|
};
|
||||||
|
"sade@1.8.1" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz";
|
||||||
|
hash = "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==";
|
||||||
|
};
|
||||||
|
"strnum@2.4.1" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/strnum/-/strnum-2.4.1.tgz";
|
||||||
|
hash = "sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg==";
|
||||||
|
};
|
||||||
|
"trough@2.2.0" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz";
|
||||||
|
hash = "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==";
|
||||||
|
};
|
||||||
|
"undici-types@8.3.0" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz";
|
||||||
|
hash = "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==";
|
||||||
|
};
|
||||||
|
"unified@11.0.5" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz";
|
||||||
|
hash = "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==";
|
||||||
|
};
|
||||||
|
"unist-util-is@6.0.1" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz";
|
||||||
|
hash = "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==";
|
||||||
|
};
|
||||||
|
"unist-util-stringify-position@4.0.0" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz";
|
||||||
|
hash = "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==";
|
||||||
|
};
|
||||||
|
"unist-util-visit-parents@6.0.2" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz";
|
||||||
|
hash = "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==";
|
||||||
|
};
|
||||||
|
"unist-util-visit@5.1.0" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz";
|
||||||
|
hash = "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==";
|
||||||
|
};
|
||||||
|
"vfile-message@4.0.3" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz";
|
||||||
|
hash = "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==";
|
||||||
|
};
|
||||||
|
"vfile@6.0.3" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz";
|
||||||
|
hash = "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==";
|
||||||
|
};
|
||||||
|
"xml-naming@0.3.0" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/xml-naming/-/xml-naming-0.3.0.tgz";
|
||||||
|
hash = "sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ==";
|
||||||
|
};
|
||||||
|
"yaml@2.9.0" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz";
|
||||||
|
hash = "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==";
|
||||||
|
};
|
||||||
|
"zwitch@1.0.5" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/zwitch/-/zwitch-1.0.5.tgz";
|
||||||
|
hash = "sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==";
|
||||||
|
};
|
||||||
|
"zwitch@2.0.4" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz";
|
||||||
|
hash = "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==";
|
||||||
|
};
|
||||||
|
}
|
||||||
6
bunfig.toml
Normal file
6
bunfig.toml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
[test]
|
||||||
|
# Coverage is opt-in via `bun test --coverage` (see the test:coverage script).
|
||||||
|
# These settings only take effect when that flag is passed.
|
||||||
|
coverageReporter = ["text", "lcov"]
|
||||||
|
coverageDir = "coverage"
|
||||||
|
coverageSkipTestFiles = true
|
||||||
22
fixtures/prose.md
Normal file
22
fixtures/prose.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
title: Buttered Toast
|
||||||
|
servings: 2
|
||||||
|
tags: [breakfast, simple]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Buttered Toast
|
||||||
|
|
||||||
|
The simplest breakfast, and a good way to smoke-test the renderer.
|
||||||
|
Everything here is plain prose — no lists, code, or annotations yet.
|
||||||
|
|
||||||
|
## Method
|
||||||
|
|
||||||
|
Toast the bread until golden on both sides.
|
||||||
|
|
||||||
|
### Timing
|
||||||
|
|
||||||
|
Spread the butter while the toast is still warm so it soaks in.
|
||||||
|
|
||||||
|
## Serving
|
||||||
|
|
||||||
|
Cut into triangles and serve at once.
|
||||||
184
flake.lock
generated
Normal file
184
flake.lock
generated
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"bun2nix": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-parts": "flake-parts",
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs-bun"
|
||||||
|
],
|
||||||
|
"systems": [
|
||||||
|
"systems"
|
||||||
|
],
|
||||||
|
"treefmt-nix": "treefmt-nix"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1784665499,
|
||||||
|
"narHash": "sha256-9BMxlTxCCDAeoNLtb1a/st7udtTIJep+wpUzquA29VU=",
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "bun2nix",
|
||||||
|
"rev": "0f2a1f0b6f42cebe3b149bf62d38754c5e0e9729",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-community",
|
||||||
|
"ref": "2.1.2",
|
||||||
|
"repo": "bun2nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-parts": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs-lib": [
|
||||||
|
"bun2nix",
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1782949081,
|
||||||
|
"narHash": "sha256-vp6Y/Grm98ESt6ceOkWiHWyZRDV3J1RID4w+6NWK9yA=",
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "flake-parts",
|
||||||
|
"rev": "17c9d6cdfc60c64f4ee8d306f9bc0b4ccb51481e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "flake-parts",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-parts_2": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs-lib": "nixpkgs-lib"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1782949081,
|
||||||
|
"narHash": "sha256-vp6Y/Grm98ESt6ceOkWiHWyZRDV3J1RID4w+6NWK9yA=",
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "flake-parts",
|
||||||
|
"rev": "17c9d6cdfc60c64f4ee8d306f9bc0b4ccb51481e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "flake-parts",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1784872115,
|
||||||
|
"narHash": "sha256-THPEF2po0fsoH8gNtp+Ae0XFDJH3N/ol7xO3v6VMTJU=",
|
||||||
|
"owner": "nixos",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "335f0738cb2fa9708f3f428e39d2eae975d1338d",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nixos",
|
||||||
|
"ref": "nixpkgs-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs-bun": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1784497964,
|
||||||
|
"narHash": "sha256-vlHUuqAcbcH2RKmHbPiuQzbv1pnzzavXnI62RD0bqCU=",
|
||||||
|
"owner": "nixos",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "241313f4e8e508cb9b13278c2b0fa25b9ca27163",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nixos",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "241313f4e8e508cb9b13278c2b0fa25b9ca27163",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs-lib": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1782614948,
|
||||||
|
"narHash": "sha256-ePjCwr1sNm9NYUqywL7QfK3JnlS015msC+eBu2zKlp8=",
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "nixpkgs.lib",
|
||||||
|
"rev": "db3f255737b94216eb71cce308e2912cf6bc2d7c",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "nixpkgs.lib",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"bun2nix": "bun2nix",
|
||||||
|
"flake-parts": "flake-parts_2",
|
||||||
|
"nixpkgs": "nixpkgs",
|
||||||
|
"nixpkgs-bun": "nixpkgs-bun",
|
||||||
|
"systems": "systems",
|
||||||
|
"treefmt-nix": "treefmt-nix_2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1680978846,
|
||||||
|
"narHash": "sha256-Gtqg8b/v49BFDpDetjclCYXm8mAnTrUzR0JnE2nv5aw=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "x86_64-linux",
|
||||||
|
"rev": "2ecfcac5e15790ba6ce360ceccddb15ad16d08a8",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "x86_64-linux",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"treefmt-nix": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"bun2nix",
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1784369104,
|
||||||
|
"narHash": "sha256-47cxbcZODibHv3rELFQ9vZly0vUNkND/atn/U7HLeb0=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "treefmt-nix",
|
||||||
|
"rev": "df3c0640565d04a0261253cdd89fce78ec50168a",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "treefmt-nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"treefmt-nix_2": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1784369104,
|
||||||
|
"narHash": "sha256-47cxbcZODibHv3rELFQ9vZly0vUNkND/atn/U7HLeb0=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "treefmt-nix",
|
||||||
|
"rev": "df3c0640565d04a0261253cdd89fce78ec50168a",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "treefmt-nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
99
flake.nix
Normal file
99
flake.nix
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
{
|
||||||
|
description = "kitchen — the KitchenMD CLI, packaged as a Nix flake";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
|
||||||
|
|
||||||
|
# The single pinned Bun, used to compile the binary and to develop against.
|
||||||
|
# It is held apart from `nixpkgs` so a `nixpkgs` update cannot pull in a Bun
|
||||||
|
# release that emits empty binaries under sandboxed native compilation.
|
||||||
|
nixpkgs-bun.url = "github:nixos/nixpkgs/241313f4e8e508cb9b13278c2b0fa25b9ca27163";
|
||||||
|
|
||||||
|
flake-parts.url = "github:hercules-ci/flake-parts";
|
||||||
|
|
||||||
|
systems.url = "github:nix-systems/x86_64-linux";
|
||||||
|
|
||||||
|
bun2nix = {
|
||||||
|
url = "github:nix-community/bun2nix?ref=2.1.2";
|
||||||
|
# Compile against the pinned Bun rather than `nixpkgs`'s.
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs-bun";
|
||||||
|
inputs.systems.follows = "systems";
|
||||||
|
};
|
||||||
|
|
||||||
|
treefmt-nix = {
|
||||||
|
url = "github:numtide/treefmt-nix";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs =
|
||||||
|
inputs:
|
||||||
|
inputs.flake-parts.lib.mkFlake { inherit inputs; } {
|
||||||
|
systems = import inputs.systems;
|
||||||
|
|
||||||
|
imports = [ inputs.treefmt-nix.flakeModule ];
|
||||||
|
|
||||||
|
perSystem =
|
||||||
|
{ system, ... }:
|
||||||
|
let
|
||||||
|
pkgs = import inputs.nixpkgs {
|
||||||
|
inherit system;
|
||||||
|
overlays = [ inputs.bun2nix.overlays.default ];
|
||||||
|
};
|
||||||
|
|
||||||
|
# The Bun from the same pinned `nixpkgs-bun` the build compiles with.
|
||||||
|
bun = inputs.nixpkgs-bun.legacyPackages.${system}.bun;
|
||||||
|
|
||||||
|
kitchen = pkgs.callPackage ./package.nix { };
|
||||||
|
|
||||||
|
bunDeps = pkgs.bun2nix.fetchBunDeps { bunNix = ./bun.nix; };
|
||||||
|
in
|
||||||
|
{
|
||||||
|
# Share the overlaid package set with the imported modules.
|
||||||
|
_module.args.pkgs = pkgs;
|
||||||
|
|
||||||
|
packages.default = kitchen;
|
||||||
|
|
||||||
|
apps.default = {
|
||||||
|
type = "app";
|
||||||
|
program = "${kitchen}/bin/kitchen";
|
||||||
|
};
|
||||||
|
|
||||||
|
devShells.default = pkgs.mkShell {
|
||||||
|
packages = [
|
||||||
|
bun
|
||||||
|
pkgs.biome
|
||||||
|
pkgs.bun2nix
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
treefmt = {
|
||||||
|
projectRootFile = "flake.nix";
|
||||||
|
programs.biome = {
|
||||||
|
enable = true;
|
||||||
|
# Reuse the repo's biome configuration as the single source.
|
||||||
|
# The sandboxed check has no git tree, so biome's VCS-ignore
|
||||||
|
# lookup must be off.
|
||||||
|
settings = pkgs.lib.recursiveUpdate (pkgs.lib.importJSON ./biome.json) {
|
||||||
|
vcs.enabled = false;
|
||||||
|
};
|
||||||
|
# Skip re-validating the config; biome consumes it directly.
|
||||||
|
validate.enable = false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
checks = {
|
||||||
|
tests = pkgs.bun2nix.mkDerivation {
|
||||||
|
pname = "kitchen-tests";
|
||||||
|
inherit (kitchen) version;
|
||||||
|
src = ./.;
|
||||||
|
inherit bunDeps;
|
||||||
|
dontRunLifecycleScripts = true;
|
||||||
|
dontUseBunBuild = true;
|
||||||
|
doCheck = true;
|
||||||
|
installPhase = "touch $out";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -8,11 +8,16 @@
|
|||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "bun test",
|
"test": "bun test",
|
||||||
|
"test:coverage": "bun test --coverage",
|
||||||
|
"test:report": "bun run scripts/test-report.ts",
|
||||||
"lint": "biome check .",
|
"lint": "biome check .",
|
||||||
"format": "biome check --write ."
|
"format": "biome check --write .",
|
||||||
|
"postinstall": "bun2nix -o bun.nix"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^2.5.3",
|
"@biomejs/biome": "^2.5.3",
|
||||||
"bun-types": "latest"
|
"bun-types": "latest",
|
||||||
|
"bun2nix": "^2.1.2",
|
||||||
|
"fast-xml-parser": "^5.10.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
19
package.nix
Normal file
19
package.nix
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# The `kitchen` CLI compiled to a self-contained native binary.
|
||||||
|
{ lib, bun2nix }:
|
||||||
|
bun2nix.mkDerivation {
|
||||||
|
pname = "kitchen";
|
||||||
|
version = (lib.importJSON ./packages/bin/package.json).version;
|
||||||
|
|
||||||
|
src = ./.;
|
||||||
|
|
||||||
|
module = "packages/bin/src/index.ts";
|
||||||
|
|
||||||
|
bunDeps = bun2nix.fetchBunDeps {
|
||||||
|
bunNix = ./bun.nix;
|
||||||
|
};
|
||||||
|
|
||||||
|
# No vendored dependency needs install-time lifecycle scripts, and running
|
||||||
|
# them would fire this repo's own `postinstall` (bun2nix) inside the sandbox,
|
||||||
|
# where it is redundant.
|
||||||
|
dontRunLifecycleScripts = true;
|
||||||
|
}
|
||||||
@@ -11,6 +11,10 @@
|
|||||||
"build": "bun build --compile ./src/index.ts --outfile kitchen"
|
"build": "bun build --compile ./src/index.ts --outfile kitchen"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kitchen-md/core": "workspace:*"
|
"@kitchen-md/core": "workspace:*",
|
||||||
|
"chalk": "^5.6.2",
|
||||||
|
"commander": "^15.0.0",
|
||||||
|
"neverthrow": "^8.2.0",
|
||||||
|
"yaml": "^2.9.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
65
packages/bin/src/end_to_end_test.ts
Normal file
65
packages/bin/src/end_to_end_test.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import { join } from "node:path";
|
||||||
|
|
||||||
|
const repoRoot = join(import.meta.dir, "..", "..", "..");
|
||||||
|
const entry = join(repoRoot, "packages", "bin", "src", "index.ts");
|
||||||
|
|
||||||
|
async function runKitchen(args: string[]) {
|
||||||
|
const proc = Bun.spawn(["bun", entry, ...args], {
|
||||||
|
cwd: repoRoot,
|
||||||
|
stdout: "pipe",
|
||||||
|
stderr: "pipe",
|
||||||
|
});
|
||||||
|
const [stdout, stderr] = await Promise.all([
|
||||||
|
new Response(proc.stdout).text(),
|
||||||
|
new Response(proc.stderr).text(),
|
||||||
|
]);
|
||||||
|
const exitCode = await proc.exited;
|
||||||
|
return { exitCode, stdout, stderr };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("kitchen CLI (e2e)", () => {
|
||||||
|
test("view renders a recipe file to stdout and exits 0", async () => {
|
||||||
|
const { exitCode, stdout, stderr } = await runKitchen(["view", "fixtures/prose.md"]);
|
||||||
|
const out = Bun.stripANSI(stdout);
|
||||||
|
expect(exitCode).toBe(0);
|
||||||
|
expect(stderr).toBe("");
|
||||||
|
expect(out).toContain("Buttered Toast");
|
||||||
|
expect(out).toContain("Method");
|
||||||
|
expect(out).toContain("Serving");
|
||||||
|
expect(out).toContain("Cut into triangles and serve at once.");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("view preserves document order in the output", async () => {
|
||||||
|
const { stdout } = await runKitchen(["view", "fixtures/prose.md"]);
|
||||||
|
const out = Bun.stripANSI(stdout);
|
||||||
|
expect(out.indexOf("Method")).toBeLessThan(out.indexOf("Serving"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("ANSI styling is suppressed when stdout is not a TTY (piped)", async () => {
|
||||||
|
const { stdout } = await runKitchen(["view", "fixtures/prose.md"]);
|
||||||
|
expect(stdout).not.toContain("\x1b");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a nonexistent file prints a human-readable error to stderr and exits 1", async () => {
|
||||||
|
const { exitCode, stdout, stderr } = await runKitchen(["view", "does/not/exist.md"]);
|
||||||
|
expect(exitCode).toBe(1);
|
||||||
|
expect(stdout).toBe("");
|
||||||
|
expect(stderr).toContain("cannot read");
|
||||||
|
expect(stderr).toContain("does/not/exist.md");
|
||||||
|
expect(stderr.startsWith("kitchen:")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a missing file argument prints usage to stderr and exits 1", async () => {
|
||||||
|
const { exitCode, stderr } = await runKitchen(["view"]);
|
||||||
|
expect(exitCode).toBe(1);
|
||||||
|
expect(stderr).not.toBe("");
|
||||||
|
expect(stderr.toLowerCase()).toContain("missing required argument");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("--help lists the view command and exits 0", async () => {
|
||||||
|
const { exitCode, stdout } = await runKitchen(["--help"]);
|
||||||
|
expect(exitCode).toBe(0);
|
||||||
|
expect(stdout).toContain("view");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1 +1,54 @@
|
|||||||
// CLI entry point
|
#!/usr/bin/env bun
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { parse } from "@kitchen-md/core";
|
||||||
|
import { Command } from "commander";
|
||||||
|
import { err, ok, type Result } from "neverthrow";
|
||||||
|
import { render } from "./render.ts";
|
||||||
|
|
||||||
|
export type ViewError = { tag: "read-failed"; path: string; cause: string };
|
||||||
|
|
||||||
|
// The union of every error a command can surface to the boundary; new fallible commands add their variants here.
|
||||||
|
export type CliError = ViewError;
|
||||||
|
|
||||||
|
export function viewFile(path: string): Result<string, ViewError> {
|
||||||
|
return readFile(path).map((content) => render(parse(content)));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatError(error: CliError): string {
|
||||||
|
switch (error.tag) {
|
||||||
|
case "read-failed":
|
||||||
|
return `cannot read '${error.path}': ${error.cause}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The one place a throwing API is turned into a Result; nothing above this leaks exceptions.
|
||||||
|
function readFile(path: string): Result<string, ViewError> {
|
||||||
|
try {
|
||||||
|
return ok(readFileSync(path, "utf8"));
|
||||||
|
} catch (error) {
|
||||||
|
const cause = error instanceof Error ? error.message : String(error);
|
||||||
|
return err({ tag: "read-failed", path, cause });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const program = new Command();
|
||||||
|
|
||||||
|
program.name("kitchen").description("Read and view KitchenMD Recipe Files");
|
||||||
|
|
||||||
|
program
|
||||||
|
.command("view")
|
||||||
|
.description("Render a Recipe File to the terminal")
|
||||||
|
.argument("<file>", "path to a Recipe File")
|
||||||
|
.action((file: string) => {
|
||||||
|
viewFile(file).match(
|
||||||
|
(output) => process.stdout.write(output),
|
||||||
|
(error) => {
|
||||||
|
process.stderr.write(`kitchen: ${formatError(error)}\n`);
|
||||||
|
process.exit(1);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (import.meta.main) {
|
||||||
|
program.parse();
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,86 @@
|
|||||||
import { describe, test } from "bun:test";
|
import { afterEach, describe, expect, test } from "bun:test";
|
||||||
|
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { formatError, type ViewError, viewFile } from "./index.ts";
|
||||||
|
|
||||||
describe("cli", () => {
|
describe("viewFile", () => {
|
||||||
test.todo("exits with non-zero code when no file argument is given");
|
let dir: string | undefined;
|
||||||
test.todo("exits with non-zero code when file does not exist");
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (dir) {
|
||||||
|
rmSync(dir, { recursive: true, force: true });
|
||||||
|
dir = undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const writeRecipe = (name: string, content: string): string => {
|
||||||
|
dir = mkdtempSync(join(tmpdir(), "kitchen-view-"));
|
||||||
|
const path = join(dir, name);
|
||||||
|
writeFileSync(path, content);
|
||||||
|
return path;
|
||||||
|
};
|
||||||
|
|
||||||
|
test("viewFile returns ok with the rendered document for a readable file", () => {
|
||||||
|
const path = writeRecipe(
|
||||||
|
"toast.md",
|
||||||
|
`# Buttered Toast
|
||||||
|
|
||||||
|
Toast the bread until golden.
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = viewFile(path);
|
||||||
|
|
||||||
|
expect(result.isOk()).toBe(true);
|
||||||
|
const rendered = Bun.stripANSI(result._unsafeUnwrap());
|
||||||
|
expect(rendered).toContain("Buttered Toast");
|
||||||
|
expect(rendered).toContain("Toast the bread until golden.");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("viewFile returns ok and passes frontmatter through to the rendered output", () => {
|
||||||
|
const path = writeRecipe(
|
||||||
|
"toast.md",
|
||||||
|
`---
|
||||||
|
title: Buttered Toast
|
||||||
|
servings: 2
|
||||||
|
---
|
||||||
|
|
||||||
|
# Buttered Toast
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = viewFile(path);
|
||||||
|
|
||||||
|
expect(result.isOk()).toBe(true);
|
||||||
|
const rendered = Bun.stripANSI(result._unsafeUnwrap());
|
||||||
|
expect(rendered).toContain("title: Buttered Toast");
|
||||||
|
expect(rendered).toContain("servings: 2");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("viewFile returns a read-failed error for a nonexistent path", () => {
|
||||||
|
dir = mkdtempSync(join(tmpdir(), "kitchen-view-"));
|
||||||
|
const missing = join(dir, "does-not-exist.md");
|
||||||
|
|
||||||
|
const result = viewFile(missing);
|
||||||
|
|
||||||
|
expect(result.isErr()).toBe(true);
|
||||||
|
const e = result._unsafeUnwrapErr();
|
||||||
|
expect(e.tag).toBe("read-failed");
|
||||||
|
expect(e.path).toBe(missing);
|
||||||
|
expect(typeof e.cause).toBe("string");
|
||||||
|
expect(e.cause.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("formatError", () => {
|
||||||
|
test("formatError renders a read-failed error as a human-readable line", () => {
|
||||||
|
const error: ViewError = {
|
||||||
|
tag: "read-failed",
|
||||||
|
path: "/nope/recipe.md",
|
||||||
|
cause: "no such file",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(formatError(error)).toBe("cannot read '/nope/recipe.md': no such file");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
import { describe, test } from "bun:test";
|
|
||||||
|
|
||||||
describe("cli — integration", () => {
|
|
||||||
test.todo("invokes core parser and produces output for a real fixture file");
|
|
||||||
});
|
|
||||||
120
packages/bin/src/render.ts
Normal file
120
packages/bin/src/render.ts
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import type {
|
||||||
|
Block,
|
||||||
|
BlockquoteBlock,
|
||||||
|
DocumentAST,
|
||||||
|
Frontmatter,
|
||||||
|
HeadingBlock,
|
||||||
|
InlineNode,
|
||||||
|
ListBlock,
|
||||||
|
ListItemBlock,
|
||||||
|
TransclusionNode,
|
||||||
|
} from "@kitchen-md/core";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import { stringify as stringifyYaml } from "yaml";
|
||||||
|
|
||||||
|
const RULE = "─".repeat(40);
|
||||||
|
|
||||||
|
export function render(ast: DocumentAST): string {
|
||||||
|
const body = ast.blocks.map(renderBlock).join("");
|
||||||
|
return renderFrontmatter(ast.frontmatter) + body;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderFrontmatter(frontmatter: Frontmatter): string {
|
||||||
|
if (Object.keys(frontmatter).length === 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return `${stringifyYaml(frontmatter)}${chalk.dim(RULE)}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderBlock(block: Block): string {
|
||||||
|
switch (block.type) {
|
||||||
|
case "heading":
|
||||||
|
return `${styleHeading(block.level)(renderInline(block.children))}\n`;
|
||||||
|
case "paragraph":
|
||||||
|
return `${renderInline(block.children)}\n\n`;
|
||||||
|
case "list":
|
||||||
|
return renderList(block);
|
||||||
|
case "blockquote":
|
||||||
|
return renderBlockquote(block);
|
||||||
|
case "code":
|
||||||
|
return `${chalk.dim(block.value)}\n\n`;
|
||||||
|
case "thematicBreak":
|
||||||
|
return `${chalk.dim(RULE)}\n\n`;
|
||||||
|
case "raw":
|
||||||
|
return `${block.value}\n\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Each level gets a distinct style, tapering from bold at level 1 toward dim at level 6.
|
||||||
|
function styleHeading(level: HeadingBlock["level"]): (text: string) => string {
|
||||||
|
switch (level) {
|
||||||
|
case 1:
|
||||||
|
return chalk.bold.underline;
|
||||||
|
case 2:
|
||||||
|
return chalk.bold;
|
||||||
|
case 3:
|
||||||
|
return chalk.bold.dim;
|
||||||
|
case 4:
|
||||||
|
return chalk.dim.underline;
|
||||||
|
case 5:
|
||||||
|
return chalk.dim;
|
||||||
|
case 6:
|
||||||
|
return chalk.dim.italic;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderList(block: ListBlock): string {
|
||||||
|
const lines = block.items.map((item, index) => {
|
||||||
|
const marker = block.ordered ? `${index + 1}. ` : "• ";
|
||||||
|
return marker + renderItemContent(item);
|
||||||
|
});
|
||||||
|
return `${lines.join("\n")}\n\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The item's child blocks, collapsed onto the marker line without their own
|
||||||
|
// trailing block spacing.
|
||||||
|
function renderItemContent(item: ListItemBlock): string {
|
||||||
|
return item.children.map(renderBlock).join("").trimEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderBlockquote(block: BlockquoteBlock): string {
|
||||||
|
const inner = block.children.map(renderBlock).join("").trimEnd();
|
||||||
|
const quoted = inner
|
||||||
|
.split("\n")
|
||||||
|
.map((line) => chalk.dim("│ ") + line)
|
||||||
|
.join("\n");
|
||||||
|
return `${quoted}\n\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderInline(nodes: InlineNode[]): string {
|
||||||
|
return nodes.map(renderInlineNode).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderInlineNode(node: InlineNode): string {
|
||||||
|
switch (node.type) {
|
||||||
|
case "text":
|
||||||
|
return node.value;
|
||||||
|
case "emphasis":
|
||||||
|
return chalk.italic(renderInline(node.children));
|
||||||
|
case "strong":
|
||||||
|
return chalk.bold(renderInline(node.children));
|
||||||
|
case "codeSpan":
|
||||||
|
return chalk.inverse(node.value);
|
||||||
|
case "link":
|
||||||
|
return chalk.underline(renderInline(node.children));
|
||||||
|
case "wikilink":
|
||||||
|
return chalk.underline(node.display ?? node.target);
|
||||||
|
case "transclusion":
|
||||||
|
return renderTransclusion(node);
|
||||||
|
case "rawInline":
|
||||||
|
return node.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A transclusion shows as its raw source text.
|
||||||
|
// Resolving the embed is out of scope.
|
||||||
|
function renderTransclusion(node: TransclusionNode): string {
|
||||||
|
const anchor = node.anchor !== undefined ? `#${node.anchor}` : "";
|
||||||
|
const display = node.display !== undefined ? `|${node.display}` : "";
|
||||||
|
return `![[${node.target}${anchor}${display}]]`;
|
||||||
|
}
|
||||||
284
packages/bin/src/render_test.ts
Normal file
284
packages/bin/src/render_test.ts
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import type { DocumentAST, HeadingBlock } from "@kitchen-md/core";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import { render } from "./render.ts";
|
||||||
|
|
||||||
|
describe("render", () => {
|
||||||
|
test("a heading renders its text followed by a single newline", () => {
|
||||||
|
const ast: DocumentAST = {
|
||||||
|
frontmatter: {},
|
||||||
|
blocks: [{ type: "heading", level: 2, children: [{ type: "text", value: "Method" }] }],
|
||||||
|
diagnostics: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(Bun.stripANSI(render(ast))).toBe("Method\n");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a paragraph renders its text followed by a blank line", () => {
|
||||||
|
const ast: DocumentAST = {
|
||||||
|
frontmatter: {},
|
||||||
|
blocks: [{ type: "paragraph", children: [{ type: "text", value: "Toast the bread." }] }],
|
||||||
|
diagnostics: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(Bun.stripANSI(render(ast))).toBe("Toast the bread.\n\n");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("frontmatter renders as YAML before the body, followed by a separator rule", () => {
|
||||||
|
const ast: DocumentAST = {
|
||||||
|
frontmatter: { title: "Buttered Toast", servings: 2 },
|
||||||
|
blocks: [
|
||||||
|
{ type: "heading", level: 1, children: [{ type: "text", value: "Buttered Toast" }] },
|
||||||
|
],
|
||||||
|
diagnostics: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const stripped = Bun.stripANSI(render(ast));
|
||||||
|
|
||||||
|
expect(stripped).toContain("title: Buttered Toast");
|
||||||
|
expect(stripped).toContain("servings: 2");
|
||||||
|
expect(stripped).toContain("──────");
|
||||||
|
|
||||||
|
const titleIndex = stripped.indexOf("title: Buttered Toast");
|
||||||
|
const separatorIndex = stripped.indexOf("──────");
|
||||||
|
const bodyIndex = stripped.lastIndexOf("Buttered Toast");
|
||||||
|
|
||||||
|
expect(titleIndex).toBeLessThan(separatorIndex);
|
||||||
|
expect(separatorIndex).toBeLessThan(bodyIndex);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("no frontmatter emits neither YAML nor a separator", () => {
|
||||||
|
const ast: DocumentAST = {
|
||||||
|
frontmatter: {},
|
||||||
|
blocks: [{ type: "paragraph", children: [{ type: "text", value: "Body only." }] }],
|
||||||
|
diagnostics: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const stripped = Bun.stripANSI(render(ast));
|
||||||
|
|
||||||
|
expect(stripped).toBe("Body only.\n\n");
|
||||||
|
expect(stripped).not.toContain("─");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("blocks render in document order", () => {
|
||||||
|
const ast: DocumentAST = {
|
||||||
|
frontmatter: {},
|
||||||
|
blocks: [
|
||||||
|
{ type: "heading", level: 1, children: [{ type: "text", value: "Title" }] },
|
||||||
|
{ type: "paragraph", children: [{ type: "text", value: "First para." }] },
|
||||||
|
{ type: "heading", level: 2, children: [{ type: "text", value: "Next" }] },
|
||||||
|
],
|
||||||
|
diagnostics: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const stripped = Bun.stripANSI(render(ast));
|
||||||
|
|
||||||
|
expect(stripped.indexOf("Title")).toBeLessThan(stripped.indexOf("First para."));
|
||||||
|
expect(stripped.indexOf("First para.")).toBeLessThan(stripped.indexOf("Next"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a heading renders its text at every level 1–6", () => {
|
||||||
|
const cases: [HeadingBlock["level"], string][] = [
|
||||||
|
[1, "Level One"],
|
||||||
|
[2, "Level Two"],
|
||||||
|
[3, "Level Three"],
|
||||||
|
[4, "Level Four"],
|
||||||
|
[5, "Level Five"],
|
||||||
|
[6, "Level Six"],
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [level, title] of cases) {
|
||||||
|
const ast: DocumentAST = {
|
||||||
|
frontmatter: {},
|
||||||
|
blocks: [{ type: "heading", level, children: [{ type: "text", value: title }] }],
|
||||||
|
diagnostics: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(Bun.stripANSI(render(ast))).toBe(`${title}\n`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const bodyOf = (block: DocumentAST["blocks"][number]): string =>
|
||||||
|
Bun.stripANSI(render({ frontmatter: {}, blocks: [block], diagnostics: [] }));
|
||||||
|
|
||||||
|
test("an unordered list renders one bulleted item per line", () => {
|
||||||
|
const stripped = bodyOf({
|
||||||
|
type: "list",
|
||||||
|
ordered: false,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
type: "listItem",
|
||||||
|
children: [{ type: "paragraph", children: [{ type: "text", value: "Flour" }] }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "listItem",
|
||||||
|
children: [{ type: "paragraph", children: [{ type: "text", value: "Sugar" }] }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(stripped).toBe("• Flour\n• Sugar\n\n");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("an ordered list renders sequential numbers, one item per line", () => {
|
||||||
|
const stripped = bodyOf({
|
||||||
|
type: "list",
|
||||||
|
ordered: true,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
type: "listItem",
|
||||||
|
children: [{ type: "paragraph", children: [{ type: "text", value: "Mix" }] }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "listItem",
|
||||||
|
children: [{ type: "paragraph", children: [{ type: "text", value: "Bake" }] }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(stripped).toBe("1. Mix\n2. Bake\n\n");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a code block renders its literal text with no highlighting", () => {
|
||||||
|
const stripped = bodyOf({ type: "code", lang: "js", value: "const x = 1;" });
|
||||||
|
|
||||||
|
expect(stripped).toContain("const x = 1;");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a thematic break renders a horizontal rule", () => {
|
||||||
|
const stripped = bodyOf({ type: "thematicBreak" });
|
||||||
|
|
||||||
|
expect(stripped).toContain("──────");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a blockquote renders its inner text", () => {
|
||||||
|
const stripped = bodyOf({
|
||||||
|
type: "blockquote",
|
||||||
|
children: [{ type: "paragraph", children: [{ type: "text", value: "Take care." }] }],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(stripped).toContain("Take care.");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a raw block renders its verbatim value", () => {
|
||||||
|
const table = "| a | b |\n| - | - |";
|
||||||
|
const stripped = bodyOf({ type: "raw", value: table });
|
||||||
|
|
||||||
|
expect(stripped).toContain(table);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("emphasis, strong, code span, and links render their inline content", () => {
|
||||||
|
const stripped = bodyOf({
|
||||||
|
type: "paragraph",
|
||||||
|
children: [
|
||||||
|
{ type: "emphasis", children: [{ type: "text", value: "soft" }] },
|
||||||
|
{ type: "text", value: " " },
|
||||||
|
{ type: "strong", children: [{ type: "text", value: "hard" }] },
|
||||||
|
{ type: "text", value: " " },
|
||||||
|
{ type: "codeSpan", value: "code" },
|
||||||
|
{ type: "text", value: " " },
|
||||||
|
{ type: "link", href: "https://example.com", children: [{ type: "text", value: "docs" }] },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(stripped).toBe("soft hard code docs\n\n");
|
||||||
|
expect(stripped).not.toContain("https://example.com");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("emphasis, strong, and code span each carry styling distinct from plain text", () => {
|
||||||
|
const previousLevel = chalk.level;
|
||||||
|
chalk.level = 1;
|
||||||
|
try {
|
||||||
|
const styled = (block: DocumentAST["blocks"][number]): string =>
|
||||||
|
render({ frontmatter: {}, blocks: [block], diagnostics: [] });
|
||||||
|
|
||||||
|
const emphasis = styled({
|
||||||
|
type: "paragraph",
|
||||||
|
children: [{ type: "emphasis", children: [{ type: "text", value: "x" }] }],
|
||||||
|
});
|
||||||
|
const strong = styled({
|
||||||
|
type: "paragraph",
|
||||||
|
children: [{ type: "strong", children: [{ type: "text", value: "x" }] }],
|
||||||
|
});
|
||||||
|
const codeSpan = styled({ type: "paragraph", children: [{ type: "codeSpan", value: "x" }] });
|
||||||
|
|
||||||
|
// Each carries ANSI styling, so the raw string differs from the stripped one.
|
||||||
|
expect(emphasis).not.toBe(Bun.stripANSI(emphasis));
|
||||||
|
expect(strong).not.toBe(Bun.stripANSI(strong));
|
||||||
|
expect(codeSpan).not.toBe(Bun.stripANSI(codeSpan));
|
||||||
|
|
||||||
|
// The three styles are mutually distinct.
|
||||||
|
expect(emphasis).not.toBe(strong);
|
||||||
|
expect(strong).not.toBe(codeSpan);
|
||||||
|
expect(emphasis).not.toBe(codeSpan);
|
||||||
|
} finally {
|
||||||
|
chalk.level = previousLevel;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a wikilink renders its display text, or the target when there is none", () => {
|
||||||
|
const withDisplay = bodyOf({
|
||||||
|
type: "paragraph",
|
||||||
|
children: [{ type: "wikilink", target: "basic-brine", anchor: "step", display: "the brine" }],
|
||||||
|
});
|
||||||
|
const bare = bodyOf({
|
||||||
|
type: "paragraph",
|
||||||
|
children: [{ type: "wikilink", target: "Maple Syrup" }],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(withDisplay).toBe("the brine\n\n");
|
||||||
|
expect(bare).toBe("Maple Syrup\n\n");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a wikilink carries styling distinct from plain text", () => {
|
||||||
|
const previousLevel = chalk.level;
|
||||||
|
chalk.level = 1;
|
||||||
|
try {
|
||||||
|
const styled = render({
|
||||||
|
frontmatter: {},
|
||||||
|
blocks: [{ type: "paragraph", children: [{ type: "wikilink", target: "x" }] }],
|
||||||
|
diagnostics: [],
|
||||||
|
});
|
||||||
|
expect(styled).not.toBe(Bun.stripANSI(styled));
|
||||||
|
} finally {
|
||||||
|
chalk.level = previousLevel;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a transclusion renders as its raw source text", () => {
|
||||||
|
const sectioned = bodyOf({
|
||||||
|
type: "paragraph",
|
||||||
|
children: [{ type: "transclusion", target: "italian meatballs", anchor: "rolling:2" }],
|
||||||
|
});
|
||||||
|
const headingless = bodyOf({
|
||||||
|
type: "paragraph",
|
||||||
|
children: [{ type: "transclusion", target: "basic brine", anchor: "3" }],
|
||||||
|
});
|
||||||
|
const aliased = bodyOf({
|
||||||
|
type: "paragraph",
|
||||||
|
children: [{ type: "transclusion", target: "recipe", display: "As shown" }],
|
||||||
|
});
|
||||||
|
const bare = bodyOf({
|
||||||
|
type: "paragraph",
|
||||||
|
children: [{ type: "transclusion", target: "maple syrup" }],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(sectioned).toBe("![[italian meatballs#rolling:2]]\n\n");
|
||||||
|
expect(headingless).toBe("![[basic brine#3]]\n\n");
|
||||||
|
expect(aliased).toBe("![[recipe|As shown]]\n\n");
|
||||||
|
expect(bare).toBe("![[maple syrup]]\n\n");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a rawInline node renders its verbatim value", () => {
|
||||||
|
const stripped = bodyOf({
|
||||||
|
type: "paragraph",
|
||||||
|
children: [
|
||||||
|
{ type: "text", value: "a " },
|
||||||
|
{ type: "rawInline", value: "~~b~~" },
|
||||||
|
{ type: "text", value: " c" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(stripped).toBe("a ~~b~~ c\n\n");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { describe, test } from "bun:test";
|
|
||||||
|
|
||||||
describe("smoke", () => {
|
|
||||||
test.todo("kitchen --help exits with code 0");
|
|
||||||
test.todo("kitchen parse <fixture> outputs structured JSON covering all annotation types");
|
|
||||||
});
|
|
||||||
@@ -8,5 +8,13 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "bun test"
|
"test": "bun test"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"remark-frontmatter": "^5.0.0",
|
||||||
|
"remark-gfm": "^4.0.1",
|
||||||
|
"remark-parse": "^11.0.0",
|
||||||
|
"remark-wiki-link": "^2.0.1",
|
||||||
|
"unified": "^11.0.5",
|
||||||
|
"yaml": "^2.9.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
export {};
|
export { parse } from "./parse.ts";
|
||||||
|
export type * from "./types.ts";
|
||||||
|
|||||||
@@ -1,82 +0,0 @@
|
|||||||
import { describe, test } from "bun:test";
|
|
||||||
|
|
||||||
describe("parser", () => {
|
|
||||||
describe("frontmatter", () => {
|
|
||||||
test.todo("parses frontmatter fields as-is");
|
|
||||||
test.todo("returns an empty object for empty frontmatter");
|
|
||||||
test.todo("returns an empty object when there is no frontmatter");
|
|
||||||
test.todo(
|
|
||||||
"does not throw on malformed frontmatter: frontmatter is {}, body still parses, and an invalid-frontmatter diagnostic preserves the raw YAML",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("blocks", () => {
|
|
||||||
test.todo("parses headings at every level (1-6)");
|
|
||||||
test.todo("parses paragraphs with typed inline nodes");
|
|
||||||
test.todo("parses an ordered list");
|
|
||||||
test.todo("parses an unordered list");
|
|
||||||
test.todo("models a list item as a container wrapping a paragraph, not a bare inline array");
|
|
||||||
test.todo("parses a code block and does not annotate its content");
|
|
||||||
test.todo("parses a thematic break");
|
|
||||||
test.todo("parses a blockquote as a container block");
|
|
||||||
test.todo("parses a callout (> [!note]) as an ordinary blockquote");
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("inline nodes", () => {
|
|
||||||
test.todo("parses plain text");
|
|
||||||
test.todo("parses emphasis");
|
|
||||||
test.todo("parses strong");
|
|
||||||
test.todo("parses a code span and does not annotate its content");
|
|
||||||
test.todo("parses a link with href and inline content");
|
|
||||||
test.todo("parses a wikilink");
|
|
||||||
test.todo("parses a wikilink with an anchor");
|
|
||||||
test.todo("parses a wikilink with a display alias");
|
|
||||||
test.todo("parses a transclusion");
|
|
||||||
test.todo("parses a transclusion with a display alias");
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("raw fallbacks", () => {
|
|
||||||
test.todo("preserves an unmodelled block (e.g. a GFM table) as a RawBlock with verbatim source");
|
|
||||||
test.todo("preserves an unmodelled inline (e.g. strikethrough) as a RawInline with verbatim source");
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("ingredient annotations", () => {
|
|
||||||
test.todo("extracts ingredient name, quantity, and unit");
|
|
||||||
test.todo("extracts multi-word ingredient name");
|
|
||||||
test.todo("extracts ingredient with a unit-less quantity");
|
|
||||||
test.todo("extracts ingredient with no quantity");
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("cookware annotations", () => {
|
|
||||||
test.todo("extracts cookware with quantity and unit");
|
|
||||||
test.todo("extracts cookware with no quantity");
|
|
||||||
test.todo("extracts multi-word cookware name");
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("timer annotations", () => {
|
|
||||||
test.todo("extracts timer as a single value");
|
|
||||||
test.todo("extracts timer as a range");
|
|
||||||
test.todo("normalises timer unit aliases to canonical form");
|
|
||||||
test.todo("matches timer units case-insensitively (~5 Mins -> min)");
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("unit normalisation", () => {
|
|
||||||
test.todo("normalises a known alias to canonical (grams -> g)");
|
|
||||||
test.todo("matches unit aliases case-insensitively (Tbsp -> tbsp)");
|
|
||||||
test.todo("normalises a multi-word alias (fluid ounces -> fl oz)");
|
|
||||||
test.todo("passes an unknown unit through verbatim");
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("annotation scope", () => {
|
|
||||||
test.todo("captures annotations embedded mid-sentence");
|
|
||||||
test.todo("captures annotations inside a container (blockquote or list item)");
|
|
||||||
test.todo("does not extract annotations inside a code span");
|
|
||||||
test.todo("does not extract annotations inside a code block");
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("transclusion", () => {
|
|
||||||
test.todo("passes a Step Reference anchor through as-is");
|
|
||||||
});
|
|
||||||
|
|
||||||
test.todo("standard Markdown elements pass through without interference");
|
|
||||||
});
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import { describe, test } from "bun:test";
|
|
||||||
|
|
||||||
describe("parser — integration", () => {
|
|
||||||
test.todo("parses the primary fixture into the complete Document AST");
|
|
||||||
test.todo("extracts every annotation type from the primary fixture");
|
|
||||||
test.todo("extracts an annotation from inside the fixture's blockquote");
|
|
||||||
test.todo("normalises the fixture's non-canonical unit (tablespoons -> tbsp)");
|
|
||||||
});
|
|
||||||
183
packages/core/src/parse.ts
Normal file
183
packages/core/src/parse.ts
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
import type { ListItem, Node, PhrasingContent, Root, RootContent } from "mdast";
|
||||||
|
import remarkFrontmatter from "remark-frontmatter";
|
||||||
|
import remarkGfm from "remark-gfm";
|
||||||
|
import remarkParse from "remark-parse";
|
||||||
|
import remarkWikiLink from "remark-wiki-link";
|
||||||
|
import { unified } from "unified";
|
||||||
|
import { parse as parseYaml } from "yaml";
|
||||||
|
import type {
|
||||||
|
Block,
|
||||||
|
DocumentAST,
|
||||||
|
Frontmatter,
|
||||||
|
InlineNode,
|
||||||
|
ListItemBlock,
|
||||||
|
TransclusionNode,
|
||||||
|
WikilinkNode,
|
||||||
|
} from "./types.ts";
|
||||||
|
|
||||||
|
// `|` is the Obsidian alias divider.
|
||||||
|
// The plugin defaults to `:`, which would swallow anchors like `#rolling:2`.
|
||||||
|
const processor = unified()
|
||||||
|
.use(remarkParse)
|
||||||
|
.use(remarkFrontmatter)
|
||||||
|
.use(remarkGfm)
|
||||||
|
.use(remarkWikiLink, { aliasDivider: "|" });
|
||||||
|
|
||||||
|
// The mdast node remark-wiki-link injects for `[[…]]`.
|
||||||
|
// Its `value` is the target with any `#anchor` still attached.
|
||||||
|
// `data.alias` is the display text, and equals `value` when no alias was written.
|
||||||
|
interface WikiLinkMdast {
|
||||||
|
type: "wikiLink";
|
||||||
|
value: string;
|
||||||
|
data?: { alias?: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
type InlineMdast = PhrasingContent | WikiLinkMdast;
|
||||||
|
|
||||||
|
// Transclusions (`![[…]]`) are not matched by remark-wiki-link — the leading `!`
|
||||||
|
// makes remark treat the brackets as a failed image, leaving the whole span as
|
||||||
|
// literal text — so they are recovered by scanning text with this pattern.
|
||||||
|
const TRANSCLUSION = /!\[\[([^[\]]+)]]/g;
|
||||||
|
|
||||||
|
export function parse(input: string): DocumentAST {
|
||||||
|
const tree = processor.parse(input);
|
||||||
|
const frontmatter = extractFrontmatter(tree);
|
||||||
|
const blocks = tree.children.flatMap((node) => translateBlock(node, input));
|
||||||
|
return { frontmatter, blocks, diagnostics: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractFrontmatter(tree: Root): Frontmatter {
|
||||||
|
const yamlNode = tree.children.find((node) => node.type === "yaml");
|
||||||
|
if (!yamlNode) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const data = parseYaml(yamlNode.value);
|
||||||
|
if (data !== null && typeof data === "object" && !Array.isArray(data)) {
|
||||||
|
return data as Frontmatter;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function translateBlock(node: RootContent, input: string): Block[] {
|
||||||
|
switch (node.type) {
|
||||||
|
case "heading":
|
||||||
|
return [
|
||||||
|
{ type: "heading", level: node.depth, children: translateInline(node.children, input) },
|
||||||
|
];
|
||||||
|
case "paragraph":
|
||||||
|
return [{ type: "paragraph", children: translateInline(node.children, input) }];
|
||||||
|
case "list":
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
type: "list",
|
||||||
|
ordered: node.ordered ?? false,
|
||||||
|
items: node.children.map((item) => translateListItem(item, input)),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
case "blockquote":
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
type: "blockquote",
|
||||||
|
children: node.children.flatMap((child) => translateBlock(child, input)),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
case "code":
|
||||||
|
return [{ type: "code", ...(node.lang ? { lang: node.lang } : {}), value: node.value }];
|
||||||
|
case "thematicBreak":
|
||||||
|
return [{ type: "thematicBreak" }];
|
||||||
|
// Frontmatter is captured separately and must not double as a block.
|
||||||
|
case "yaml":
|
||||||
|
return [];
|
||||||
|
default:
|
||||||
|
return [{ type: "raw", value: slice(node, input) }];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function translateListItem(item: ListItem, input: string): ListItemBlock {
|
||||||
|
return {
|
||||||
|
type: "listItem",
|
||||||
|
children: item.children.flatMap((child) => translateBlock(child, input)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function translateInline(nodes: InlineMdast[], input: string): InlineNode[] {
|
||||||
|
return nodes.flatMap((node): InlineNode[] => {
|
||||||
|
switch (node.type) {
|
||||||
|
case "text":
|
||||||
|
return splitTransclusions(node.value);
|
||||||
|
case "emphasis":
|
||||||
|
return [{ type: "emphasis", children: translateInline(node.children, input) }];
|
||||||
|
case "strong":
|
||||||
|
return [{ type: "strong", children: translateInline(node.children, input) }];
|
||||||
|
case "inlineCode":
|
||||||
|
return [{ type: "codeSpan", value: node.value }];
|
||||||
|
case "link":
|
||||||
|
return [{ type: "link", href: node.url, children: translateInline(node.children, input) }];
|
||||||
|
case "wikiLink":
|
||||||
|
return [translateWikilink(node)];
|
||||||
|
default:
|
||||||
|
return [{ type: "rawInline", value: slice(node, input) }];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function translateWikilink(node: WikiLinkMdast): WikilinkNode {
|
||||||
|
const display =
|
||||||
|
node.data?.alias !== undefined && node.data.alias !== node.value ? node.data.alias : undefined;
|
||||||
|
return {
|
||||||
|
type: "wikilink",
|
||||||
|
...splitAnchor(node.value),
|
||||||
|
...(display !== undefined ? { display } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split a text run into plain text and the transclusions embedded in it, preserving order.
|
||||||
|
// A run with no transclusion yields a single text node.
|
||||||
|
function splitTransclusions(value: string): InlineNode[] {
|
||||||
|
const out: InlineNode[] = [];
|
||||||
|
let cursor = 0;
|
||||||
|
for (const match of value.matchAll(TRANSCLUSION)) {
|
||||||
|
const at = match.index;
|
||||||
|
if (at > cursor) {
|
||||||
|
out.push({ type: "text", value: value.slice(cursor, at) });
|
||||||
|
}
|
||||||
|
out.push(buildTransclusion(match[1]));
|
||||||
|
cursor = at + match[0].length;
|
||||||
|
}
|
||||||
|
if (out.length === 0 || cursor < value.length) {
|
||||||
|
out.push({ type: "text", value: value.slice(cursor) });
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTransclusion(inner: string): TransclusionNode {
|
||||||
|
const pipe = inner.indexOf("|");
|
||||||
|
const display = pipe === -1 ? undefined : inner.slice(pipe + 1);
|
||||||
|
const targetPart = pipe === -1 ? inner : inner.slice(0, pipe);
|
||||||
|
return {
|
||||||
|
type: "transclusion",
|
||||||
|
...splitAnchor(targetPart),
|
||||||
|
...(display !== undefined ? { display } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split `target#anchor` at the first `#`.
|
||||||
|
// The anchor is passed through verbatim, and is omitted entirely when absent.
|
||||||
|
function splitAnchor(value: string): { target: string; anchor?: string } {
|
||||||
|
const hash = value.indexOf("#");
|
||||||
|
if (hash === -1) {
|
||||||
|
return { target: value };
|
||||||
|
}
|
||||||
|
return { target: value.slice(0, hash), anchor: value.slice(hash + 1) };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verbatim source for an unmodelled node, taken by position so it round-trips
|
||||||
|
// byte-for-byte rather than being re-stringified through remark.
|
||||||
|
function slice(node: Node, input: string): string {
|
||||||
|
const start = node.position?.start.offset;
|
||||||
|
const end = node.position?.end.offset;
|
||||||
|
if (start === undefined || end === undefined) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return input.slice(start, end);
|
||||||
|
}
|
||||||
349
packages/core/src/parse_test.ts
Normal file
349
packages/core/src/parse_test.ts
Normal file
@@ -0,0 +1,349 @@
|
|||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import { parse } from "@kitchen-md/core";
|
||||||
|
|
||||||
|
describe("parse", () => {
|
||||||
|
test("frontmatter passthrough — arbitrary fields become a plain object", () => {
|
||||||
|
const input = `---
|
||||||
|
title: Buttered Toast
|
||||||
|
servings: 2
|
||||||
|
tags: [breakfast, simple]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Buttered Toast
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = parse(input);
|
||||||
|
|
||||||
|
expect(result.frontmatter).toEqual({
|
||||||
|
title: "Buttered Toast",
|
||||||
|
servings: 2,
|
||||||
|
tags: ["breakfast", "simple"],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("frontmatter is an empty object when absent", () => {
|
||||||
|
const result = parse("# Just a heading");
|
||||||
|
|
||||||
|
expect(result.frontmatter).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("frontmatter is an empty object when the block is empty", () => {
|
||||||
|
const input = `---
|
||||||
|
---
|
||||||
|
|
||||||
|
# Heading
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = parse(input);
|
||||||
|
|
||||||
|
expect(result.frontmatter).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("headings are modelled at every level 1–6 with TextNode children", () => {
|
||||||
|
const cases: [string, 1 | 2 | 3 | 4 | 5 | 6, string][] = [
|
||||||
|
["# Level One", 1, "Level One"],
|
||||||
|
["## Level Two", 2, "Level Two"],
|
||||||
|
["### Level Three", 3, "Level Three"],
|
||||||
|
["#### Level Four", 4, "Level Four"],
|
||||||
|
["##### Level Five", 5, "Level Five"],
|
||||||
|
["###### Level Six", 6, "Level Six"],
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [markdown, level, text] of cases) {
|
||||||
|
const result = parse(markdown);
|
||||||
|
|
||||||
|
expect(result.blocks).toEqual([
|
||||||
|
{
|
||||||
|
type: "heading",
|
||||||
|
level,
|
||||||
|
children: [{ type: "text", value: text }],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a paragraph is a ParagraphBlock with TextNode content", () => {
|
||||||
|
const result = parse("Just some plain prose.");
|
||||||
|
|
||||||
|
expect(result.blocks).toEqual([
|
||||||
|
{
|
||||||
|
type: "paragraph",
|
||||||
|
children: [{ type: "text", value: "Just some plain prose." }],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("blocks are flat and in document order — a heading is a sibling of the following paragraph", () => {
|
||||||
|
const input = `# Title
|
||||||
|
|
||||||
|
A paragraph under it.
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = parse(input);
|
||||||
|
|
||||||
|
expect(result.blocks).toEqual([
|
||||||
|
{
|
||||||
|
type: "heading",
|
||||||
|
level: 1,
|
||||||
|
children: [{ type: "text", value: "Title" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "paragraph",
|
||||||
|
children: [{ type: "text", value: "A paragraph under it." }],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("diagnostics are empty in the normal case", () => {
|
||||||
|
const result = parse("# Ok");
|
||||||
|
|
||||||
|
expect(result.diagnostics).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("an unordered list is a ListBlock whose items wrap their child blocks", () => {
|
||||||
|
const result = parse("- First\n- Second\n");
|
||||||
|
|
||||||
|
expect(result.blocks).toEqual([
|
||||||
|
{
|
||||||
|
type: "list",
|
||||||
|
ordered: false,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
type: "listItem",
|
||||||
|
children: [{ type: "paragraph", children: [{ type: "text", value: "First" }] }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "listItem",
|
||||||
|
children: [{ type: "paragraph", children: [{ type: "text", value: "Second" }] }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("an ordered list carries the ordered flag", () => {
|
||||||
|
const result = parse("1. One\n2. Two\n");
|
||||||
|
|
||||||
|
expect(result.blocks).toEqual([
|
||||||
|
{
|
||||||
|
type: "list",
|
||||||
|
ordered: true,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
type: "listItem",
|
||||||
|
children: [{ type: "paragraph", children: [{ type: "text", value: "One" }] }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "listItem",
|
||||||
|
children: [{ type: "paragraph", children: [{ type: "text", value: "Two" }] }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a list item is a container of blocks, not a bare inline array", () => {
|
||||||
|
const result = parse("- Just text\n");
|
||||||
|
const list = result.blocks[0];
|
||||||
|
|
||||||
|
if (list?.type !== "list") throw new Error("expected a list block");
|
||||||
|
expect(list.items[0]).toEqual({
|
||||||
|
type: "listItem",
|
||||||
|
children: [{ type: "paragraph", children: [{ type: "text", value: "Just text" }] }],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a blockquote is a container holding blocks", () => {
|
||||||
|
const result = parse("> Quoted prose.\n");
|
||||||
|
|
||||||
|
expect(result.blocks).toEqual([
|
||||||
|
{
|
||||||
|
type: "blockquote",
|
||||||
|
children: [{ type: "paragraph", children: [{ type: "text", value: "Quoted prose." }] }],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("an OFM callout parses as an ordinary blockquote with its text preserved", () => {
|
||||||
|
const result = parse("> [!note]\n> Remember this.\n");
|
||||||
|
const quote = result.blocks[0];
|
||||||
|
|
||||||
|
if (quote?.type !== "blockquote") throw new Error("expected a blockquote block");
|
||||||
|
const rendered = JSON.stringify(quote);
|
||||||
|
expect(rendered).toContain("[!note]");
|
||||||
|
expect(rendered).toContain("Remember this.");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a fenced code block carries its language and literal text", () => {
|
||||||
|
const result = parse("```js\nconst x = @sugar{1};\n```\n");
|
||||||
|
|
||||||
|
expect(result.blocks).toEqual([{ type: "code", lang: "js", value: "const x = @sugar{1};" }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a code block without a language has no lang", () => {
|
||||||
|
const result = parse("```\nplain text\n```\n");
|
||||||
|
|
||||||
|
expect(result.blocks).toEqual([{ type: "code", value: "plain text" }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a thematic break is modelled", () => {
|
||||||
|
const result = parse("---\n");
|
||||||
|
|
||||||
|
expect(result.blocks).toEqual([{ type: "thematicBreak" }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("emphasis, strong, and code-span inlines are modelled", () => {
|
||||||
|
const result = parse("*em* **strong** `code`\n");
|
||||||
|
const para = result.blocks[0];
|
||||||
|
|
||||||
|
if (para?.type !== "paragraph") throw new Error("expected a paragraph block");
|
||||||
|
expect(para.children).toEqual([
|
||||||
|
{ type: "emphasis", children: [{ type: "text", value: "em" }] },
|
||||||
|
{ type: "text", value: " " },
|
||||||
|
{ type: "strong", children: [{ type: "text", value: "strong" }] },
|
||||||
|
{ type: "text", value: " " },
|
||||||
|
{ type: "codeSpan", value: "code" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a link is modelled with its href and inline children, and the href is not annotation-parsed here", () => {
|
||||||
|
const result = parse("[the docs](https://example.com/@stuff)\n");
|
||||||
|
const para = result.blocks[0];
|
||||||
|
|
||||||
|
if (para?.type !== "paragraph") throw new Error("expected a paragraph block");
|
||||||
|
expect(para.children).toEqual([
|
||||||
|
{
|
||||||
|
type: "link",
|
||||||
|
href: "https://example.com/@stuff",
|
||||||
|
children: [{ type: "text", value: "the docs" }],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a code span preserves its raw content verbatim", () => {
|
||||||
|
const result = parse("Use `@sugar{1 tbsp}` literally.\n");
|
||||||
|
const para = result.blocks[0];
|
||||||
|
|
||||||
|
if (para?.type !== "paragraph") throw new Error("expected a paragraph block");
|
||||||
|
expect(para.children).toContainEqual({ type: "codeSpan", value: "@sugar{1 tbsp}" });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("an unmodelled block (a GFM table) falls through to a RawBlock with byte-for-byte source", () => {
|
||||||
|
const table = "| a | b |\n| - | - |\n| 1 | 2 |";
|
||||||
|
const result = parse(`${table}\n`);
|
||||||
|
|
||||||
|
expect(result.blocks).toEqual([{ type: "raw", value: table }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("an unmodelled inline (GFM strikethrough) falls through to a RawInline with byte-for-byte source", () => {
|
||||||
|
const result = parse("done ~~scratch~~ now\n");
|
||||||
|
const para = result.blocks[0];
|
||||||
|
|
||||||
|
if (para?.type !== "paragraph") throw new Error("expected a paragraph block");
|
||||||
|
expect(para.children).toEqual([
|
||||||
|
{ type: "text", value: "done " },
|
||||||
|
{ type: "rawInline", value: "~~scratch~~" },
|
||||||
|
{ type: "text", value: " now" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a bare wikilink carries only its target", () => {
|
||||||
|
const result = parse("See [[Basic Brine]] first.\n");
|
||||||
|
const para = result.blocks[0];
|
||||||
|
|
||||||
|
if (para?.type !== "paragraph") throw new Error("expected a paragraph block");
|
||||||
|
expect(para.children).toContainEqual({ type: "wikilink", target: "Basic Brine" });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a wikilink anchor is passed through verbatim", () => {
|
||||||
|
const result = parse("[[recipe#the section]]\n");
|
||||||
|
const para = result.blocks[0];
|
||||||
|
|
||||||
|
if (para?.type !== "paragraph") throw new Error("expected a paragraph block");
|
||||||
|
expect(para.children).toEqual([{ type: "wikilink", target: "recipe", anchor: "the section" }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a wikilink display alias is captured after the pipe", () => {
|
||||||
|
const result = parse("[[recipe#anchor|Read this]]\n");
|
||||||
|
const para = result.blocks[0];
|
||||||
|
|
||||||
|
if (para?.type !== "paragraph") throw new Error("expected a paragraph block");
|
||||||
|
expect(para.children).toEqual([
|
||||||
|
{ type: "wikilink", target: "recipe", anchor: "anchor", display: "Read this" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a wikilink and a transclusion are distinct node types sharing one shape", () => {
|
||||||
|
const result = parse("[[recipe]] versus ![[recipe]]\n");
|
||||||
|
const para = result.blocks[0];
|
||||||
|
|
||||||
|
if (para?.type !== "paragraph") throw new Error("expected a paragraph block");
|
||||||
|
expect(para.children).toEqual([
|
||||||
|
{ type: "wikilink", target: "recipe" },
|
||||||
|
{ type: "text", value: " versus " },
|
||||||
|
{ type: "transclusion", target: "recipe" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a transclusion display alias is captured after the pipe", () => {
|
||||||
|
const result = parse("![[recipe|As shown]]\n");
|
||||||
|
const para = result.blocks[0];
|
||||||
|
|
||||||
|
if (para?.type !== "paragraph") throw new Error("expected a paragraph block");
|
||||||
|
expect(para.children).toContainEqual({
|
||||||
|
type: "transclusion",
|
||||||
|
target: "recipe",
|
||||||
|
display: "As shown",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a Step Reference transclusion passes its section anchor through as-is", () => {
|
||||||
|
const result = parse("![[italian meatballs#rolling:2]]\n");
|
||||||
|
const para = result.blocks[0];
|
||||||
|
|
||||||
|
if (para?.type !== "paragraph") throw new Error("expected a paragraph block");
|
||||||
|
expect(para.children).toContainEqual({
|
||||||
|
type: "transclusion",
|
||||||
|
target: "italian meatballs",
|
||||||
|
anchor: "rolling:2",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a headingless Step Reference transclusion passes its bare step anchor through as-is", () => {
|
||||||
|
const result = parse("![[basic brine#3]]\n");
|
||||||
|
const para = result.blocks[0];
|
||||||
|
|
||||||
|
if (para?.type !== "paragraph") throw new Error("expected a paragraph block");
|
||||||
|
expect(para.children).toContainEqual({
|
||||||
|
type: "transclusion",
|
||||||
|
target: "basic brine",
|
||||||
|
anchor: "3",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a transclusion is extracted from surrounding prose", () => {
|
||||||
|
const result = parse("Finish with ![[maple syrup#2]] on top.\n");
|
||||||
|
const para = result.blocks[0];
|
||||||
|
|
||||||
|
if (para?.type !== "paragraph") throw new Error("expected a paragraph block");
|
||||||
|
expect(para.children).toEqual([
|
||||||
|
{ type: "text", value: "Finish with " },
|
||||||
|
{ type: "transclusion", target: "maple syrup", anchor: "2" },
|
||||||
|
{ type: "text", value: " on top." },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("frontmatter is not emitted as a block", () => {
|
||||||
|
const input = `---
|
||||||
|
title: X
|
||||||
|
---
|
||||||
|
|
||||||
|
Body.
|
||||||
|
`;
|
||||||
|
const result = parse(input);
|
||||||
|
|
||||||
|
expect(result.blocks).toEqual([
|
||||||
|
{ type: "paragraph", children: [{ type: "text", value: "Body." }] },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
131
packages/core/src/types.ts
Normal file
131
packages/core/src/types.ts
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
// The public AST node and document types returned by parse.
|
||||||
|
|
||||||
|
export type Frontmatter = Record<string, unknown>;
|
||||||
|
|
||||||
|
export interface Diagnostic {
|
||||||
|
severity: "warning";
|
||||||
|
code: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TextNode {
|
||||||
|
type: "text";
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmphasisNode {
|
||||||
|
type: "emphasis";
|
||||||
|
children: InlineNode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StrongNode {
|
||||||
|
type: "strong";
|
||||||
|
children: InlineNode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CodeSpanNode {
|
||||||
|
type: "codeSpan";
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LinkNode {
|
||||||
|
type: "link";
|
||||||
|
href: string;
|
||||||
|
children: InlineNode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// An Obsidian reference `[[target#anchor|display]]`: target is the filename
|
||||||
|
// without extension, anchor is the part after `#` verbatim, display the alias after `|`.
|
||||||
|
// The node type — not a flag — distinguishes it from a transclusion.
|
||||||
|
export interface WikilinkNode {
|
||||||
|
type: "wikilink";
|
||||||
|
target: string;
|
||||||
|
anchor?: string;
|
||||||
|
display?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// An Obsidian embed `![[target#anchor|display]]`, covering step references like
|
||||||
|
// `![[file#section:N]]` and `![[file#N]]` whose anchor is passed through as-is.
|
||||||
|
// It shares WikilinkNode's shape, so the type is what tells the two apart.
|
||||||
|
export interface TransclusionNode {
|
||||||
|
type: "transclusion";
|
||||||
|
target: string;
|
||||||
|
anchor?: string;
|
||||||
|
display?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verbatim source for any inline construct core does not model, sliced from the
|
||||||
|
// original input so the unmodelled span round-trips byte-for-byte.
|
||||||
|
export interface RawInline {
|
||||||
|
type: "rawInline";
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type InlineNode =
|
||||||
|
| TextNode
|
||||||
|
| EmphasisNode
|
||||||
|
| StrongNode
|
||||||
|
| CodeSpanNode
|
||||||
|
| LinkNode
|
||||||
|
| WikilinkNode
|
||||||
|
| TransclusionNode
|
||||||
|
| RawInline;
|
||||||
|
|
||||||
|
export interface HeadingBlock {
|
||||||
|
type: "heading";
|
||||||
|
level: 1 | 2 | 3 | 4 | 5 | 6;
|
||||||
|
children: InlineNode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ParagraphBlock {
|
||||||
|
type: "paragraph";
|
||||||
|
children: InlineNode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ListItemBlock {
|
||||||
|
type: "listItem";
|
||||||
|
children: Block[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ListBlock {
|
||||||
|
type: "list";
|
||||||
|
ordered: boolean;
|
||||||
|
items: ListItemBlock[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BlockquoteBlock {
|
||||||
|
type: "blockquote";
|
||||||
|
children: Block[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CodeBlock {
|
||||||
|
type: "code";
|
||||||
|
lang?: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ThematicBreakBlock {
|
||||||
|
type: "thematicBreak";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verbatim source for any block construct core does not model, sliced from the
|
||||||
|
// original input so the unmodelled block round-trips byte-for-byte.
|
||||||
|
export interface RawBlock {
|
||||||
|
type: "raw";
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Block =
|
||||||
|
| HeadingBlock
|
||||||
|
| ParagraphBlock
|
||||||
|
| ListBlock
|
||||||
|
| BlockquoteBlock
|
||||||
|
| CodeBlock
|
||||||
|
| ThematicBreakBlock
|
||||||
|
| RawBlock;
|
||||||
|
|
||||||
|
export interface DocumentAST {
|
||||||
|
frontmatter: Frontmatter;
|
||||||
|
blocks: Block[];
|
||||||
|
diagnostics: Diagnostic[];
|
||||||
|
}
|
||||||
278
scripts/test-report.ts
Normal file
278
scripts/test-report.ts
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
// Runs the test suite and renders reports/test-report.md: a summary, a
|
||||||
|
// describe-grouped inventory, and a coverage table.
|
||||||
|
// Any positional arguments are forwarded to `bun test` as path filters; with
|
||||||
|
// none, the whole repo runs.
|
||||||
|
// The report is written even when tests fail, and the process exits with the
|
||||||
|
// test run's own status so CI can gate on it.
|
||||||
|
// Split into run -> parse -> render so a future CI job can reuse parse+render
|
||||||
|
// on artifacts it already produced.
|
||||||
|
|
||||||
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||||
|
import { XMLParser } from "fast-xml-parser";
|
||||||
|
|
||||||
|
const REPORTS_DIR = "reports";
|
||||||
|
const JUNIT_PATH = `${REPORTS_DIR}/junit.xml`;
|
||||||
|
const LCOV_PATH = "coverage/lcov.info";
|
||||||
|
const REPORT_PATH = `${REPORTS_DIR}/test-report.md`;
|
||||||
|
|
||||||
|
type TestStatus = "passed" | "failed" | "todo";
|
||||||
|
|
||||||
|
interface TestCase {
|
||||||
|
name: string;
|
||||||
|
status: TestStatus;
|
||||||
|
time: number;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Suite {
|
||||||
|
name: string;
|
||||||
|
suites: Suite[];
|
||||||
|
tests: TestCase[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Junit {
|
||||||
|
totals: { tests: number; failures: number; skipped: number; time: number };
|
||||||
|
files: Suite[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FileCoverage {
|
||||||
|
file: string;
|
||||||
|
linesFound: number;
|
||||||
|
linesHit: number;
|
||||||
|
funcsFound: number;
|
||||||
|
funcsHit: number;
|
||||||
|
uncovered: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- run ---
|
||||||
|
|
||||||
|
function runTests(paths: string[]): number {
|
||||||
|
mkdirSync(REPORTS_DIR, { recursive: true });
|
||||||
|
const proc = Bun.spawnSync(
|
||||||
|
[
|
||||||
|
"bun",
|
||||||
|
"test",
|
||||||
|
...paths,
|
||||||
|
"--coverage",
|
||||||
|
"--coverage-reporter=lcov",
|
||||||
|
"--reporter=junit",
|
||||||
|
`--reporter-outfile=${JUNIT_PATH}`,
|
||||||
|
],
|
||||||
|
{ stdout: "inherit", stderr: "inherit" },
|
||||||
|
);
|
||||||
|
return proc.exitCode ?? 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- parse ---
|
||||||
|
|
||||||
|
const xml = new XMLParser({
|
||||||
|
ignoreAttributes: false,
|
||||||
|
attributeNamePrefix: "@_",
|
||||||
|
isArray: (name) => name === "testsuite" || name === "testcase",
|
||||||
|
});
|
||||||
|
|
||||||
|
function parseJunit(path: string): Junit {
|
||||||
|
const root = xml.parse(readFileSync(path, "utf8")).testsuites;
|
||||||
|
return {
|
||||||
|
totals: {
|
||||||
|
tests: Number(root["@_tests"] ?? 0),
|
||||||
|
failures: Number(root["@_failures"] ?? 0),
|
||||||
|
skipped: Number(root["@_skipped"] ?? 0),
|
||||||
|
time: Number(root["@_time"] ?? 0),
|
||||||
|
},
|
||||||
|
files: (root.testsuite ?? []).map(parseSuite),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: raw fast-xml-parser nodes are untyped
|
||||||
|
function parseSuite(node: any): Suite {
|
||||||
|
return {
|
||||||
|
name: node["@_name"] ?? "(unnamed)",
|
||||||
|
suites: (node.testsuite ?? []).map(parseSuite),
|
||||||
|
tests: (node.testcase ?? []).map(parseCase),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: raw fast-xml-parser nodes are untyped
|
||||||
|
function parseCase(node: any): TestCase {
|
||||||
|
const name = node["@_name"] ?? "(unnamed)";
|
||||||
|
const time = Number(node["@_time"] ?? 0);
|
||||||
|
if (node.failure !== undefined) {
|
||||||
|
const message = node.failure["@_message"] ?? node.failure["#text"] ?? "";
|
||||||
|
return { name, status: "failed", time, message: String(message) };
|
||||||
|
}
|
||||||
|
if (node.skipped !== undefined) {
|
||||||
|
return { name, status: "todo", time };
|
||||||
|
}
|
||||||
|
return { name, status: "passed", time };
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseLcov(path: string): FileCoverage[] {
|
||||||
|
if (!existsSync(path)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const records: FileCoverage[] = [];
|
||||||
|
let current: FileCoverage | null = null;
|
||||||
|
for (const line of readFileSync(path, "utf8").split("\n")) {
|
||||||
|
if (line.startsWith("SF:")) {
|
||||||
|
current = {
|
||||||
|
file: line.slice(3).trim(),
|
||||||
|
linesFound: 0,
|
||||||
|
linesHit: 0,
|
||||||
|
funcsFound: 0,
|
||||||
|
funcsHit: 0,
|
||||||
|
uncovered: [],
|
||||||
|
};
|
||||||
|
} else if (!current) {
|
||||||
|
// Skip anything before the first source record.
|
||||||
|
} else if (line.startsWith("FNF:")) {
|
||||||
|
current.funcsFound = Number(line.slice(4));
|
||||||
|
} else if (line.startsWith("FNH:")) {
|
||||||
|
current.funcsHit = Number(line.slice(4));
|
||||||
|
} else if (line.startsWith("DA:")) {
|
||||||
|
const [lineNo, hits] = line.slice(3).split(",");
|
||||||
|
current.linesFound++;
|
||||||
|
if (Number(hits) > 0) {
|
||||||
|
current.linesHit++;
|
||||||
|
} else {
|
||||||
|
current.uncovered.push(Number(lineNo));
|
||||||
|
}
|
||||||
|
} else if (line.startsWith("end_of_record")) {
|
||||||
|
records.push(current);
|
||||||
|
current = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return records;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- render ---
|
||||||
|
|
||||||
|
const ICON: Record<TestStatus, string> = {
|
||||||
|
passed: "✅",
|
||||||
|
failed: "❌",
|
||||||
|
todo: "⏭️",
|
||||||
|
};
|
||||||
|
|
||||||
|
function pct(hit: number, found: number): string {
|
||||||
|
if (found === 0) {
|
||||||
|
return "—";
|
||||||
|
}
|
||||||
|
return `${((hit / found) * 100).toFixed(1)}% (${hit}/${found})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collapse consecutive line numbers into ranges, e.g. 33,34,...,40 -> "33-40".
|
||||||
|
function ranges(lines: number[]): string {
|
||||||
|
if (lines.length === 0) {
|
||||||
|
return "—";
|
||||||
|
}
|
||||||
|
const sorted = [...lines].sort((a, b) => a - b);
|
||||||
|
const out: string[] = [];
|
||||||
|
let start = sorted[0];
|
||||||
|
let prev = sorted[0];
|
||||||
|
for (const n of sorted.slice(1)) {
|
||||||
|
if (n === prev + 1) {
|
||||||
|
prev = n;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
out.push(start === prev ? `${start}` : `${start}-${prev}`);
|
||||||
|
start = n;
|
||||||
|
prev = n;
|
||||||
|
}
|
||||||
|
out.push(start === prev ? `${start}` : `${start}-${prev}`);
|
||||||
|
return out.join(", ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSuite(suite: Suite, depth: number, lines: string[]): void {
|
||||||
|
const indent = " ".repeat(depth);
|
||||||
|
for (const child of suite.suites) {
|
||||||
|
lines.push(`${indent}- **${child.name}**`);
|
||||||
|
renderSuite(child, depth + 1, lines);
|
||||||
|
}
|
||||||
|
for (const test of suite.tests) {
|
||||||
|
const timing =
|
||||||
|
test.status === "passed" && test.time > 0 ? ` _(${(test.time * 1000).toFixed(0)}ms)_` : "";
|
||||||
|
lines.push(`${indent}- ${ICON[test.status]} ${test.name}${timing}`);
|
||||||
|
if (test.status === "failed" && test.message) {
|
||||||
|
lines.push(`${indent} - \`${test.message.replace(/`/g, "'").replace(/\n/g, " ")}\``);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderReport(junit: Junit, coverage: FileCoverage[], meta: string): string {
|
||||||
|
const { tests, failures, skipped, time } = junit.totals;
|
||||||
|
const passed = tests - failures - skipped;
|
||||||
|
const result = failures === 0 ? "✅ All passing" : `❌ ${failures} failing`;
|
||||||
|
|
||||||
|
const lines: string[] = [];
|
||||||
|
lines.push("# Test Report", "", `_${meta}_`, "");
|
||||||
|
|
||||||
|
lines.push("## Summary", "");
|
||||||
|
lines.push("| Metric | Value |", "| --- | --- |");
|
||||||
|
lines.push(`| Result | ${result} |`);
|
||||||
|
lines.push(`| Passed | ${passed} |`);
|
||||||
|
lines.push(`| Failed | ${failures} |`);
|
||||||
|
lines.push(`| Todo | ${skipped} |`);
|
||||||
|
lines.push(`| Total | ${tests} |`);
|
||||||
|
lines.push(`| Duration | ${time.toFixed(2)}s |`, "");
|
||||||
|
|
||||||
|
lines.push("## Test inventory", "");
|
||||||
|
for (const file of junit.files) {
|
||||||
|
lines.push(`### \`${file.name}\``, "");
|
||||||
|
renderSuite(file, 0, lines);
|
||||||
|
lines.push("");
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push("## Coverage", "");
|
||||||
|
if (coverage.length === 0) {
|
||||||
|
lines.push("_No coverage data found._", "");
|
||||||
|
} else {
|
||||||
|
lines.push("| File | Lines | Functions | Uncovered |", "| --- | --- | --- | --- |");
|
||||||
|
const total = { linesFound: 0, linesHit: 0, funcsFound: 0, funcsHit: 0 };
|
||||||
|
for (const file of coverage) {
|
||||||
|
total.linesFound += file.linesFound;
|
||||||
|
total.linesHit += file.linesHit;
|
||||||
|
total.funcsFound += file.funcsFound;
|
||||||
|
total.funcsHit += file.funcsHit;
|
||||||
|
lines.push(
|
||||||
|
`| \`${file.file}\` | ${pct(file.linesHit, file.linesFound)} | ${pct(file.funcsHit, file.funcsFound)} | ${ranges(file.uncovered)} |`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
lines.push(
|
||||||
|
`| **Total** | **${pct(total.linesHit, total.linesFound)}** | **${pct(total.funcsHit, total.funcsFound)}** | |`,
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
lines.push(
|
||||||
|
"> Coverage instruments in-process code only; code exercised solely through a subprocess (such as the CLI entry point) is not counted.",
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- main ---
|
||||||
|
|
||||||
|
function gitSha(): string {
|
||||||
|
const proc = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"]);
|
||||||
|
return proc.exitCode === 0 ? proc.stdout.toString().trim() : "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
const paths = process.argv.slice(2);
|
||||||
|
const exitCode = runTests(paths);
|
||||||
|
mkdirSync(REPORTS_DIR, { recursive: true });
|
||||||
|
|
||||||
|
const scope = paths.length > 0 ? paths.join(", ") : "whole repo";
|
||||||
|
const meta = `Generated ${new Date().toISOString()} · commit ${gitSha()} · ${scope}`;
|
||||||
|
|
||||||
|
if (!existsSync(JUNIT_PATH)) {
|
||||||
|
writeFileSync(
|
||||||
|
REPORT_PATH,
|
||||||
|
`# Test Report\n\n_${meta}_\n\nThe test run produced no JUnit output; it likely failed to start.\n`,
|
||||||
|
);
|
||||||
|
console.error(`Test run produced no JUnit output. Wrote a stub to ${REPORT_PATH}.`);
|
||||||
|
process.exit(exitCode || 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFileSync(REPORT_PATH, renderReport(parseJunit(JUNIT_PATH), parseLcov(LCOV_PATH), meta));
|
||||||
|
console.log(`Wrote ${REPORT_PATH}`);
|
||||||
|
process.exit(exitCode);
|
||||||
Reference in New Issue
Block a user