Compare commits
14 Commits
main
...
4ac11865c5
| Author | SHA1 | Date | |
|---|---|---|---|
| 4ac11865c5 | |||
| a425a6b312 | |||
| 02020309eb | |||
| e16f3f957d | |||
| 4aec705f2b | |||
| ae542893da | |||
| 313a5b60b7 | |||
| 11866a0dd5 | |||
| 0b6a52452a | |||
| 3d81d1c76e | |||
| e32b71ebf3 | |||
| 8687b7501a | |||
| 97ecc86d63 | |||
| d1c1f330c5 |
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.
|
||||||
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.
|
||||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -7,6 +7,12 @@ packages/bin/kitchen
|
|||||||
result
|
result
|
||||||
result-*
|
result-*
|
||||||
|
|
||||||
|
# test coverage output
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
# generated test report and its intermediate artifacts
|
||||||
|
reports/
|
||||||
|
|
||||||
# direnv / nix-direnv cache
|
# direnv / nix-direnv cache
|
||||||
.direnv/
|
.direnv/
|
||||||
|
|
||||||
|
|||||||
147
bun.lock
147
bun.lock
@@ -8,6 +8,7 @@
|
|||||||
"@biomejs/biome": "^2.5.3",
|
"@biomejs/biome": "^2.5.3",
|
||||||
"bun-types": "latest",
|
"bun-types": "latest",
|
||||||
"bun2nix": "^2.1.2",
|
"bun2nix": "^2.1.2",
|
||||||
|
"fast-xml-parser": "^5.10.1",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/bin": {
|
"packages/bin": {
|
||||||
@@ -18,11 +19,21 @@
|
|||||||
},
|
},
|
||||||
"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-parse": "^11.0.0",
|
||||||
|
"unified": "^11.0.5",
|
||||||
|
"yaml": "^2.9.0",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages": {
|
"packages": {
|
||||||
@@ -48,16 +59,152 @@
|
|||||||
|
|
||||||
"@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=="],
|
"bun2nix": ["bun2nix@2.1.2", "", { "dependencies": { "sade": "^1.8.1" }, "bin": { "bun2nix": "index.ts" } }, "sha512-0wx6Ar5ccrz4aSD5prbShwymjDEXFh7Bucxs+YrpAMa67TnVB95Hv8FV3oaQEbtOx6QGgIAyOmap6Y3WCRqetg=="],
|
||||||
|
|
||||||
|
"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=="],
|
||||||
|
|
||||||
|
"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-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=="],
|
||||||
|
|
||||||
|
"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-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=="],
|
||||||
|
|
||||||
|
"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-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=="],
|
"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=="],
|
||||||
|
|
||||||
|
"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-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=="],
|
||||||
|
|
||||||
"sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="],
|
"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=="],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
272
bun.nix
272
bun.nix
@@ -51,10 +51,42 @@
|
|||||||
};
|
};
|
||||||
"@kitchen-md/bin" = copyPathToStore ./packages/bin;
|
"@kitchen-md/bin" = copyPathToStore ./packages/bin;
|
||||||
"@kitchen-md/core" = copyPathToStore ./packages/core;
|
"@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 {
|
"@types/node@26.1.1" = fetchurl {
|
||||||
url = "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz";
|
url = "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz";
|
||||||
hash = "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==";
|
hash = "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==";
|
||||||
};
|
};
|
||||||
|
"@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 {
|
"bun-types@1.3.14" = fetchurl {
|
||||||
url = "https://registry.npmjs.org/bun-types/-/bun-types-1.3.14.tgz";
|
url = "https://registry.npmjs.org/bun-types/-/bun-types-1.3.14.tgz";
|
||||||
hash = "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ==";
|
hash = "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ==";
|
||||||
@@ -63,16 +95,256 @@
|
|||||||
url = "https://registry.npmjs.org/bun2nix/-/bun2nix-2.1.2.tgz";
|
url = "https://registry.npmjs.org/bun2nix/-/bun2nix-2.1.2.tgz";
|
||||||
hash = "sha512-0wx6Ar5ccrz4aSD5prbShwymjDEXFh7Bucxs+YrpAMa67TnVB95Hv8FV3oaQEbtOx6QGgIAyOmap6Y3WCRqetg==";
|
hash = "sha512-0wx6Ar5ccrz4aSD5prbShwymjDEXFh7Bucxs+YrpAMa67TnVB95Hv8FV3oaQEbtOx6QGgIAyOmap6Y3WCRqetg==";
|
||||||
};
|
};
|
||||||
|
"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@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==";
|
||||||
|
};
|
||||||
|
"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-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@3.1.0" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz";
|
||||||
|
hash = "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==";
|
||||||
|
};
|
||||||
|
"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-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@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@4.0.0" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz";
|
||||||
|
hash = "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==";
|
||||||
|
};
|
||||||
|
"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-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 {
|
"mri@1.2.0" = fetchurl {
|
||||||
url = "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz";
|
url = "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz";
|
||||||
hash = "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==";
|
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==";
|
||||||
|
};
|
||||||
|
"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-parse@11.0.0" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz";
|
||||||
|
hash = "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==";
|
||||||
|
};
|
||||||
"sade@1.8.1" = fetchurl {
|
"sade@1.8.1" = fetchurl {
|
||||||
url = "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz";
|
url = "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz";
|
||||||
hash = "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==";
|
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 {
|
"undici-types@8.3.0" = fetchurl {
|
||||||
url = "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz";
|
url = "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz";
|
||||||
hash = "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==";
|
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@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.
|
||||||
@@ -93,11 +93,6 @@
|
|||||||
doCheck = true;
|
doCheck = true;
|
||||||
installPhase = "touch $out";
|
installPhase = "touch $out";
|
||||||
};
|
};
|
||||||
|
|
||||||
smoke = pkgs.runCommand "kitchen-smoke" { } ''
|
|
||||||
${kitchen}/bin/kitchen --help
|
|
||||||
touch $out
|
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
],
|
],
|
||||||
"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"
|
"postinstall": "bun2nix -o bun.nix"
|
||||||
@@ -15,6 +17,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^2.5.3",
|
"@biomejs/biome": "^2.5.3",
|
||||||
"bun-types": "latest",
|
"bun-types": "latest",
|
||||||
"bun2nix": "^2.1.2"
|
"bun2nix": "^2.1.2",
|
||||||
|
"fast-xml-parser": "^5.10.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
|
||||||
});
|
|
||||||
46
packages/bin/src/render.ts
Normal file
46
packages/bin/src/render.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import type { Block, DocumentAST, Frontmatter, HeadingBlock, InlineNode } from "@kitchen-md/core";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import { stringify as stringifyYaml } from "yaml";
|
||||||
|
|
||||||
|
const SEPARATOR = "─".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(SEPARATOR)}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderBlock(block: Block): string {
|
||||||
|
if (block.type === "heading") {
|
||||||
|
return `${styleHeading(block.level)(renderInline(block.children))}\n`;
|
||||||
|
}
|
||||||
|
return `${renderInline(block.children)}\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 renderInline(nodes: InlineNode[]): string {
|
||||||
|
return nodes.map((node) => node.value).join("");
|
||||||
|
}
|
||||||
99
packages/bin/src/render_test.ts
Normal file
99
packages/bin/src/render_test.ts
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import type { DocumentAST, HeadingBlock } from "@kitchen-md/core";
|
||||||
|
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`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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,11 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "bun test"
|
"test": "bun test"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"remark-frontmatter": "^5.0.0",
|
||||||
|
"remark-parse": "^11.0.0",
|
||||||
|
"unified": "^11.0.5",
|
||||||
|
"yaml": "^2.9.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
export {};
|
export { parse } from "./parse.ts";
|
||||||
|
export type * from "./types.ts";
|
||||||
|
|||||||
@@ -1,86 +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)");
|
|
||||||
});
|
|
||||||
46
packages/core/src/parse.ts
Normal file
46
packages/core/src/parse.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import type { PhrasingContent, Root, RootContent } from "mdast";
|
||||||
|
import remarkFrontmatter from "remark-frontmatter";
|
||||||
|
import remarkParse from "remark-parse";
|
||||||
|
import { unified } from "unified";
|
||||||
|
import { parse as parseYaml } from "yaml";
|
||||||
|
import type { Block, DocumentAST, Frontmatter, InlineNode } from "./types.ts";
|
||||||
|
|
||||||
|
const processor = unified().use(remarkParse).use(remarkFrontmatter);
|
||||||
|
|
||||||
|
export function parse(input: string): DocumentAST {
|
||||||
|
const tree = processor.parse(input);
|
||||||
|
const frontmatter = extractFrontmatter(tree);
|
||||||
|
const blocks = tree.children.flatMap(translateBlock);
|
||||||
|
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): Block[] {
|
||||||
|
if (node.type === "heading") {
|
||||||
|
return [{ type: "heading", level: node.depth, children: translateInline(node.children) }];
|
||||||
|
}
|
||||||
|
if (node.type === "paragraph") {
|
||||||
|
return [{ type: "paragraph", children: translateInline(node.children) }];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function translateInline(nodes: PhrasingContent[]): InlineNode[] {
|
||||||
|
return nodes.flatMap((node) => {
|
||||||
|
if (node.type === "text") {
|
||||||
|
return [{ type: "text", value: node.value }];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
}
|
||||||
102
packages/core/src/parse_test.ts
Normal file
102
packages/core/src/parse_test.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
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([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
35
packages/core/src/types.ts
Normal file
35
packages/core/src/types.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
// 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 type InlineNode = TextNode;
|
||||||
|
|
||||||
|
export interface HeadingBlock {
|
||||||
|
type: "heading";
|
||||||
|
level: 1 | 2 | 3 | 4 | 5 | 6;
|
||||||
|
children: InlineNode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ParagraphBlock {
|
||||||
|
type: "paragraph";
|
||||||
|
children: InlineNode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Block = HeadingBlock | ParagraphBlock;
|
||||||
|
|
||||||
|
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