Compare commits

14 Commits

Author SHA1 Message Date
4ac11865c5 build: drop redundant flake smoke check (task 0003)
The checks.smoke derivation only ran 'kitchen --help', which the e2e
tier now covers (end_to_end_test.ts, '--help lists the view command').
checks.tests is the remaining flake check.
2026-07-28 23:27:48 -04:00
a425a6b312 refactor: merge view into index and rename test tiers (task 0003)
Fold the view command module (viewFile, the error type, the read
adapter) into index.ts so the command functions sit beside the guarded
CLI, and generalise formatViewError into formatError matching over a
CliError union that new fallible commands extend.

Align the test files with ADR 0009's tiers by name:
- index_test.ts now holds the in-process integration tests of the
  command functions (formerly view_test.ts)
- end_to_end_test.ts holds the black-box subprocess e2e (formerly
  index_test.ts)
2026-07-28 23:15:41 -04:00
02020309eb test: rebuild the view-skeleton suite on ADR 0009 tiers (task 0003)
Guard the CLI entry's program.parse() behind import.meta.main so the
command module can be imported without running the CLI, then rebuild the
deleted suite across the three tiers ADR 0009 defines:

- unit: parse (core) and render (bin), asserted on return values
- integration: the view command function, asserted on its Result
- e2e: the kitchen binary as a subprocess (exit codes, streams, ANSI
  suppression, errors, --help)

Drop the stale test-support.ts coverage ignore and refresh the task's
implementation notes to describe the tiered suite.
2026-07-28 22:41:59 -04:00
e16f3f957d docs: record testing tiers (ADR 0009), link from 0008 (task 0003)
Add ADR 0009 defining the unit / integration / e2e tiers by the seam
each exercises, with the value-returning vs process-side-effecting line
as the boundary between in-process and e2e-only. Add one line to ADR
0008 noting the Result seam is what makes error propagation assertable
in-process, cross-linking 0009.
2026-07-28 22:24:42 -04:00
4aec705f2b test: remove all test files for a clean restart (task 0003)
Temporary: clear the entire test suite (unit, subprocess, the shared
helper, and the parser-integration placeholder) so the tests can be
rebuilt from scratch. Fixtures and the test tooling (bunfig, report
script) are kept. Note: `bun test` now errors on an empty suite, so the
flake's tests check is red until tests return.
2026-07-28 21:43:41 -04:00
ae542893da refactor: model bin errors as neverthrow Result values (task 0003)
Split the view command out of the entry point and make its failure a
value rather than an exception. index.ts is now scaffolding plus
dispatch; view.ts owns the behaviour, returning Result<string, ViewError>
(a plain-data tagged union) via neverthrow, with try/catch confined to a
readFile adapter. The entry point matches the Result at the boundary.

Because viewFile is a pure in-process function it is now unit-tested
directly (and counted by coverage), while the subprocess tests stay as
the end-to-end check. ADR 0008 records the errors-as-values convention.
2026-07-28 21:21:23 -04:00
313a5b60b7 test: remove roadmap todos from parse_test.ts (task 0003)
Drop the scaffolded test.todo placeholders (and the describe blocks left
empty by their removal), keeping only the six real parser tests. The
behaviours they enumerated live in the core-parser spec and tasks
0004-0007, which write their tests fresh when implemented.
2026-07-28 20:46:51 -04:00
11866a0dd5 feat: scope test report to given paths, default all (task 0003)
Forward any positional arguments to `bun test` as path filters, so
`bun run test:report packages/core` reports just core while a bare
`bun run test:report` still covers the whole repo. The report's header
records the scope.
2026-07-27 22:55:01 -04:00
0b6a52452a build: add test report generator (task 0003)
Add scripts/test-report.ts (bun run test:report): it runs the full suite
with JUnit + lcov output, then renders a gitignored reports/test-report.md
— a summary, a describe-grouped inventory (pass/todo/fail), and a
coverage table. The report is written even when tests fail and the script
exits with the test run's own status, so CI can gate on it later. Adds
fast-xml-parser (dev) for robust JUnit parsing; lcov is parsed by hand.
2026-07-27 22:51:30 -04:00
3d81d1c76e build: set up test coverage via bun (task 0003)
Add a bunfig.toml enabling coverage with text + lcov reporters, a
test:coverage script, and gitignore the coverage/ output. Test files and
the test-support helper are excluded so the numbers reflect product code
only.
2026-07-27 22:23:43 -04:00
e32b71ebf3 test: move parser tests to parse_test.ts, off the barrel (task 0003)
packages/core/src/index.ts is a pure re-export barrel with no behavior
of its own, so its behavior tests belonged next to the module they
exercise. Relocate the parser tests (and their roadmap todos) into a
co-located parse_test.ts importing parse from ./parse.ts, and drop
index_test.ts. A broken re-export is still caught by typecheck and by
the bin package importing parse through the public entrypoint.
2026-07-27 22:15:32 -04:00
8687b7501a refactor: trim Diagnostic to fields produced this slice (task 0003)
Drop the Point/Position types and the source/position fields from
Diagnostic. Nothing constructs a diagnostic yet — diagnostics is always
empty until task 0007 emits the first invalid-frontmatter warning, which
adds the locator fields back additively alongside its own tests.
2026-07-27 17:41:23 -04:00
97ecc86d63 test: add prose-fixture end-to-end smoke for kitchen view (task 0003)
Pull a reduced end-to-end smoke forward into the slice so the view flow
is exercised over a real on-disk recipe, not just a temp file.

Add fixtures/prose.md — a recipe using only this slice's modelled
constructs (frontmatter, headings, plain paragraphs) so it renders
losslessly. integration_test now runs `kitchen view fixtures/prose.md`
as a subprocess and asserts the frontmatter, separator, headings,
paragraphs, and flat document order end-to-end. smoke_test covers
`kitchen --help` exiting 0 with the view command listed.

The full fixtures/basic.md smoke remains task 0007, which needs 0004's
richer nodes before that fixture renders losslessly.
2026-07-26 08:19:26 -04:00
d1c1f330c5 feat: add kitchen view command and parser skeleton (task 0003)
Cut the first complete thread through both layers with the smallest set
of node types.

@kitchen-md/core gains a pure, total `parse` returning a
`DocumentAST` of `{ frontmatter, blocks, diagnostics }`, built from a
minimal remark pipeline (remark-parse + remark-frontmatter) with an
internal translation layer to core's own types. This slice models
frontmatter passthrough, HeadingBlock, ParagraphBlock, and TextNode;
remark's mdast does not surface in the public API.

@kitchen-md/bin gains the `view` subcommand (commander) that reads a
file, calls `parse`, and passes the AST to a pure `render` that returns
an ANSI-styled string via chalk (auto-suppressed off a TTY). Frontmatter
prints as raw YAML followed by a separator, headings styled distinctly
by level, paragraphs as prose. A missing argument prints usage and a
missing/unreadable file a human-readable error, both exiting 1.

Richer blocks/inline plus raw fallbacks are task 0004; malformed
frontmatter diagnostics and the basic.md smoke test are task 0007.
2026-07-26 07:24:32 -04:00
9 changed files with 25 additions and 680 deletions

View File

@@ -1,69 +0,0 @@
---
spec: core-parser
blocked-by: 0003-view-skeleton
---
## What to build
Extend the parser and renderer to cover the rest of the everyday Markdown body, so `kitchen view` renders lists, blockquotes, code, rules, and inline emphasis faithfully.
In `@kitchen-md/core`, model the remaining block types: `ListBlock` (ordered flag + items) whose items are `ListItemBlock` containers holding `Block[]`, `BlockquoteBlock` as a container holding `Block[]` (OFM callouts like `> [!note]` parse as ordinary blockquotes), `CodeBlock` (optional language + literal text, no annotation parsing), and `ThematicBreakBlock`.
Add the inline nodes: `EmphasisNode`, `StrongNode`, `CodeSpanNode`, and `LinkNode` (href + inline content).
Add the raw fallbacks: any remark block or inline node core does not model falls through to `RawBlock` / `RawInline`, whose verbatim `value` is captured by position-slicing the original input (never re-stringified), so unmodelled constructs round-trip byte-for-byte.
Add remark-gfm to the pipeline; the translation layer recurses into container children rather than collapsing them.
In `@kitchen-md/bin`, extend `render` for the new nodes: lists (bullet for unordered, sequential number for ordered, one item per line), code blocks (literal, no highlighting), thematic breaks (a horizontal rule string), emphasis (italic), strong (bold), code spans (a distinct dim/inverse style), and links (inline content only, href not shown).
## Acceptance criteria
- [x] `ListBlock` carries an ordered flag and an array of `ListItemBlock`; each `ListItemBlock` is a container wrapping child blocks (e.g. a paragraph), not a bare inline array
- [x] `BlockquoteBlock` is a container holding `Block[]`; a callout (`> [!note]`) parses as an ordinary blockquote with its text preserved
- [x] `CodeBlock` carries an optional language and literal text, and its content is not annotation-parsed
- [x] `ThematicBreakBlock` is modelled
- [x] Inline `EmphasisNode`, `StrongNode`, `CodeSpanNode`, and `LinkNode` are modelled; code-span content is not annotation-parsed
- [x] An unmodelled block (e.g. a GFM table) falls through to `RawBlock` and an unmodelled inline (e.g. strikethrough) to `RawInline`, each preserving byte-for-byte verbatim source via position-slicing
- [x] `render` handles ordered and unordered lists, code blocks, thematic breaks, and blockquotes
- [x] `render` handles emphasis (italic), strong (bold), code span (distinct style), and link (inline content only)
- [x] Core unit tests cover each new block type, the list-item-wraps-a-paragraph shape, each new inline node, and the raw block/inline fallbacks with verbatim source
- [x] Renderer unit tests (ANSI stripped) cover each new block and inline node
## Implementation Notes
All acceptance criteria are met, with no dropped or deferred criteria.
The following decisions are worth recording.
### Node discriminants mirror mdast
The new nodes reuse mdast's own type names as their discriminants — `list`, `listItem`, `blockquote`, `code`, `thematicBreak`, `emphasis`, `strong`, `link` — matching the precedent set by task 0003 (`text`, `heading`, `paragraph`).
The two exceptions are the code span (`codeSpan`, since the interface is `CodeSpanNode` and mdast's `inlineCode` reads oddly in this AST) and the raw fallbacks (`raw` for blocks, `rawInline` for inlines), which have no mdast counterpart.
### `ListItemBlock` is scoped to `ListBlock.items`, not the `Block` union
A list item exists only as a member of a list, so `ListItemBlock` is deliberately kept out of the top-level `Block` union and the `renderBlock` switch.
List items are dispatched through `renderItemContent`, reached only from `renderList`.
This keeps the type as tight as the spec's "an array of `ListItemBlock`" shape rather than letting a list item appear anywhere a block is valid.
### `CodeBlock.lang` is omitted, not `undefined`, when absent
`lang` is an optional property that is left off entirely for an unlabelled fence rather than set to `undefined`, so `{ type: "code", value: "…" }` deep-equals the parser output without a stray `lang: undefined` key.
### Frontmatter must be excluded from the raw fallback
With the raw fallback now catching every unmodelled block, the `yaml` node — captured separately as frontmatter — is explicitly dropped in `translateBlock` so it does not also surface as a `RawBlock` and double-render.
### Content-not-annotation-parsed is satisfied structurally
Code blocks and code spans store their literal `value` verbatim and are never fed to `translateInline`.
Annotation parsing itself lands in task 0006, so "not annotation-parsed" holds here by construction: there is no annotation pass for that content to escape.
### Rendering decisions (visual, not asserted by colour)
Following task 0003, the renderer tests assert ANSI-stripped text, spacing, and the presence/absence of styling — never particular colours.
Unordered items use a `•` bullet, ordered items a sequential `1.`-from-one number (the model carries only the `ordered` flag, not a start offset), one item per line.
Blockquote lines are prefixed with a dim `│ `, code blocks and the thematic-break rule render dim, code spans use `inverse` to stay distinct from the dim used elsewhere, emphasis is italic, strong is bold, and links render their inline content underlined with the href hidden.
The list-item and blockquote renderers collapse child-block trailing spacing (`trimEnd`) so nested paragraphs sit on the marker/quote line rather than emitting their own blank line.
### remark-gfm
`remark-gfm` is added to the core pipeline so tables and strikethrough parse as their own nodes (`table`, `delete`) and therefore reach the raw fallback, where position-slicing captures them byte-for-byte.
Without gfm they would parse as ordinary paragraph text and never exercise the fallback.

View File

@@ -30,7 +30,6 @@
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"remark-frontmatter": "^5.0.0", "remark-frontmatter": "^5.0.0",
"remark-gfm": "^4.0.1",
"remark-parse": "^11.0.0", "remark-parse": "^11.0.0",
"unified": "^11.0.5", "unified": "^11.0.5",
"yaml": "^2.9.0", "yaml": "^2.9.0",
@@ -82,8 +81,6 @@
"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=="],
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
"chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
"character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
@@ -116,26 +113,10 @@
"longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
"markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
"mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="],
"mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="], "mdast-util-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-frontmatter": ["mdast-util-frontmatter@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "escape-string-regexp": "^5.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-extension-frontmatter": "^2.0.0" } }, "sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA=="],
"mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="],
"mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="],
"mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="],
"mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="],
"mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="],
"mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="],
"mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="], "mdast-util-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-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=="],
@@ -148,20 +129,6 @@
"micromark-extension-frontmatter": ["micromark-extension-frontmatter@2.0.0", "", { "dependencies": { "fault": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg=="], "micromark-extension-frontmatter": ["micromark-extension-frontmatter@2.0.0", "", { "dependencies": { "fault": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg=="],
"micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="],
"micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="],
"micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="],
"micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="],
"micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="],
"micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="],
"micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="],
"micromark-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-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-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=="],
@@ -210,12 +177,8 @@
"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-frontmatter": ["remark-frontmatter@5.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-frontmatter": "^2.0.0", "micromark-extension-frontmatter": "^2.0.0", "unified": "^11.0.0" } }, "sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ=="],
"remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="],
"remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="], "remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="],
"remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="],
"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=="], "strnum": ["strnum@2.4.1", "", { "dependencies": { "anynum": "^1.0.1" } }, "sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg=="],

72
bun.nix
View File

@@ -95,10 +95,6 @@
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==";
}; };
"ccount@2.0.1" = fetchurl {
url = "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz";
hash = "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==";
};
"chalk@5.6.2" = fetchurl { "chalk@5.6.2" = fetchurl {
url = "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz"; url = "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz";
hash = "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="; hash = "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==";
@@ -163,14 +159,6 @@
url = "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz"; url = "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz";
hash = "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="; hash = "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==";
}; };
"markdown-table@3.0.4" = fetchurl {
url = "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz";
hash = "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==";
};
"mdast-util-find-and-replace@3.0.2" = fetchurl {
url = "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz";
hash = "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==";
};
"mdast-util-from-markdown@2.0.3" = fetchurl { "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"; url = "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz";
hash = "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="; hash = "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==";
@@ -179,30 +167,6 @@
url = "https://registry.npmjs.org/mdast-util-frontmatter/-/mdast-util-frontmatter-2.0.1.tgz"; url = "https://registry.npmjs.org/mdast-util-frontmatter/-/mdast-util-frontmatter-2.0.1.tgz";
hash = "sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA=="; hash = "sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA==";
}; };
"mdast-util-gfm-autolink-literal@2.0.1" = fetchurl {
url = "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz";
hash = "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==";
};
"mdast-util-gfm-footnote@2.1.0" = fetchurl {
url = "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz";
hash = "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==";
};
"mdast-util-gfm-strikethrough@2.0.0" = fetchurl {
url = "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz";
hash = "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==";
};
"mdast-util-gfm-table@2.0.0" = fetchurl {
url = "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz";
hash = "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==";
};
"mdast-util-gfm-task-list-item@2.0.0" = fetchurl {
url = "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz";
hash = "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==";
};
"mdast-util-gfm@3.1.0" = fetchurl {
url = "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz";
hash = "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==";
};
"mdast-util-phrasing@4.1.0" = fetchurl { "mdast-util-phrasing@4.1.0" = fetchurl {
url = "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz"; url = "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz";
hash = "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="; hash = "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==";
@@ -223,34 +187,6 @@
url = "https://registry.npmjs.org/micromark-extension-frontmatter/-/micromark-extension-frontmatter-2.0.0.tgz"; url = "https://registry.npmjs.org/micromark-extension-frontmatter/-/micromark-extension-frontmatter-2.0.0.tgz";
hash = "sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg=="; hash = "sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg==";
}; };
"micromark-extension-gfm-autolink-literal@2.1.0" = fetchurl {
url = "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz";
hash = "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==";
};
"micromark-extension-gfm-footnote@2.1.0" = fetchurl {
url = "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz";
hash = "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==";
};
"micromark-extension-gfm-strikethrough@2.1.0" = fetchurl {
url = "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz";
hash = "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==";
};
"micromark-extension-gfm-table@2.1.1" = fetchurl {
url = "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz";
hash = "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==";
};
"micromark-extension-gfm-tagfilter@2.0.0" = fetchurl {
url = "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz";
hash = "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==";
};
"micromark-extension-gfm-task-list-item@2.1.0" = fetchurl {
url = "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz";
hash = "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==";
};
"micromark-extension-gfm@3.0.0" = fetchurl {
url = "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz";
hash = "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==";
};
"micromark-factory-destination@2.0.1" = fetchurl { "micromark-factory-destination@2.0.1" = fetchurl {
url = "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz"; url = "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz";
hash = "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="; hash = "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==";
@@ -351,18 +287,10 @@
url = "https://registry.npmjs.org/remark-frontmatter/-/remark-frontmatter-5.0.0.tgz"; url = "https://registry.npmjs.org/remark-frontmatter/-/remark-frontmatter-5.0.0.tgz";
hash = "sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ=="; hash = "sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ==";
}; };
"remark-gfm@4.0.1" = fetchurl {
url = "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz";
hash = "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==";
};
"remark-parse@11.0.0" = fetchurl { "remark-parse@11.0.0" = fetchurl {
url = "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz"; url = "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz";
hash = "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="; hash = "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==";
}; };
"remark-stringify@11.0.0" = fetchurl {
url = "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz";
hash = "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==";
};
"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==";

View File

@@ -1,17 +1,8 @@
import type { import type { Block, DocumentAST, Frontmatter, HeadingBlock, InlineNode } from "@kitchen-md/core";
Block,
BlockquoteBlock,
DocumentAST,
Frontmatter,
HeadingBlock,
InlineNode,
ListBlock,
ListItemBlock,
} from "@kitchen-md/core";
import chalk from "chalk"; import chalk from "chalk";
import { stringify as stringifyYaml } from "yaml"; import { stringify as stringifyYaml } from "yaml";
const RULE = "─".repeat(40); const SEPARATOR = "─".repeat(40);
export function render(ast: DocumentAST): string { export function render(ast: DocumentAST): string {
const body = ast.blocks.map(renderBlock).join(""); const body = ast.blocks.map(renderBlock).join("");
@@ -22,26 +13,14 @@ function renderFrontmatter(frontmatter: Frontmatter): string {
if (Object.keys(frontmatter).length === 0) { if (Object.keys(frontmatter).length === 0) {
return ""; return "";
} }
return `${stringifyYaml(frontmatter)}${chalk.dim(RULE)}\n`; return `${stringifyYaml(frontmatter)}${chalk.dim(SEPARATOR)}\n`;
} }
function renderBlock(block: Block): string { function renderBlock(block: Block): string {
switch (block.type) { if (block.type === "heading") {
case "heading":
return `${styleHeading(block.level)(renderInline(block.children))}\n`; return `${styleHeading(block.level)(renderInline(block.children))}\n`;
case "paragraph":
return `${renderInline(block.children)}\n\n`;
case "list":
return renderList(block);
case "blockquote":
return renderBlockquote(block);
case "code":
return `${chalk.dim(block.value)}\n\n`;
case "thematicBreak":
return `${chalk.dim(RULE)}\n\n`;
case "raw":
return `${block.value}\n\n`;
} }
return `${renderInline(block.children)}\n\n`;
} }
// Each level gets a distinct style, tapering from bold at level 1 toward dim at level 6. // Each level gets a distinct style, tapering from bold at level 1 toward dim at level 6.
@@ -62,46 +41,6 @@ function styleHeading(level: HeadingBlock["level"]): (text: string) => string {
} }
} }
function renderList(block: ListBlock): string {
const lines = block.items.map((item, index) => {
const marker = block.ordered ? `${index + 1}. ` : "• ";
return marker + renderItemContent(item);
});
return `${lines.join("\n")}\n\n`;
}
// The item's child blocks, collapsed onto the marker line without their own
// trailing block spacing.
function renderItemContent(item: ListItemBlock): string {
return item.children.map(renderBlock).join("").trimEnd();
}
function renderBlockquote(block: BlockquoteBlock): string {
const inner = block.children.map(renderBlock).join("").trimEnd();
const quoted = inner
.split("\n")
.map((line) => chalk.dim("│ ") + line)
.join("\n");
return `${quoted}\n\n`;
}
function renderInline(nodes: InlineNode[]): string { function renderInline(nodes: InlineNode[]): string {
return nodes.map(renderInlineNode).join(""); return nodes.map((node) => node.value).join("");
}
function renderInlineNode(node: InlineNode): string {
switch (node.type) {
case "text":
return node.value;
case "emphasis":
return chalk.italic(renderInline(node.children));
case "strong":
return chalk.bold(renderInline(node.children));
case "codeSpan":
return chalk.inverse(node.value);
case "link":
return chalk.underline(renderInline(node.children));
case "rawInline":
return node.value;
}
} }

View File

@@ -1,6 +1,5 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import type { DocumentAST, HeadingBlock } from "@kitchen-md/core"; import type { DocumentAST, HeadingBlock } from "@kitchen-md/core";
import chalk from "chalk";
import { render } from "./render.ts"; import { render } from "./render.ts";
describe("render", () => { describe("render", () => {
@@ -97,135 +96,4 @@ describe("render", () => {
expect(Bun.stripANSI(render(ast))).toBe(`${title}\n`); expect(Bun.stripANSI(render(ast))).toBe(`${title}\n`);
} }
}); });
const bodyOf = (block: DocumentAST["blocks"][number]): string =>
Bun.stripANSI(render({ frontmatter: {}, blocks: [block], diagnostics: [] }));
test("an unordered list renders one bulleted item per line", () => {
const stripped = bodyOf({
type: "list",
ordered: false,
items: [
{
type: "listItem",
children: [{ type: "paragraph", children: [{ type: "text", value: "Flour" }] }],
},
{
type: "listItem",
children: [{ type: "paragraph", children: [{ type: "text", value: "Sugar" }] }],
},
],
});
expect(stripped).toBe("• Flour\n• Sugar\n\n");
});
test("an ordered list renders sequential numbers, one item per line", () => {
const stripped = bodyOf({
type: "list",
ordered: true,
items: [
{
type: "listItem",
children: [{ type: "paragraph", children: [{ type: "text", value: "Mix" }] }],
},
{
type: "listItem",
children: [{ type: "paragraph", children: [{ type: "text", value: "Bake" }] }],
},
],
});
expect(stripped).toBe("1. Mix\n2. Bake\n\n");
});
test("a code block renders its literal text with no highlighting", () => {
const stripped = bodyOf({ type: "code", lang: "js", value: "const x = 1;" });
expect(stripped).toContain("const x = 1;");
});
test("a thematic break renders a horizontal rule", () => {
const stripped = bodyOf({ type: "thematicBreak" });
expect(stripped).toContain("──────");
});
test("a blockquote renders its inner text", () => {
const stripped = bodyOf({
type: "blockquote",
children: [{ type: "paragraph", children: [{ type: "text", value: "Take care." }] }],
});
expect(stripped).toContain("Take care.");
});
test("a raw block renders its verbatim value", () => {
const table = "| a | b |\n| - | - |";
const stripped = bodyOf({ type: "raw", value: table });
expect(stripped).toContain(table);
});
test("emphasis, strong, code span, and links render their inline content", () => {
const stripped = bodyOf({
type: "paragraph",
children: [
{ type: "emphasis", children: [{ type: "text", value: "soft" }] },
{ type: "text", value: " " },
{ type: "strong", children: [{ type: "text", value: "hard" }] },
{ type: "text", value: " " },
{ type: "codeSpan", value: "code" },
{ type: "text", value: " " },
{ type: "link", href: "https://example.com", children: [{ type: "text", value: "docs" }] },
],
});
expect(stripped).toBe("soft hard code docs\n\n");
expect(stripped).not.toContain("https://example.com");
});
test("emphasis, strong, and code span each carry styling distinct from plain text", () => {
const previousLevel = chalk.level;
chalk.level = 1;
try {
const styled = (block: DocumentAST["blocks"][number]): string =>
render({ frontmatter: {}, blocks: [block], diagnostics: [] });
const emphasis = styled({
type: "paragraph",
children: [{ type: "emphasis", children: [{ type: "text", value: "x" }] }],
});
const strong = styled({
type: "paragraph",
children: [{ type: "strong", children: [{ type: "text", value: "x" }] }],
});
const codeSpan = styled({ type: "paragraph", children: [{ type: "codeSpan", value: "x" }] });
// Each carries ANSI styling, so the raw string differs from the stripped one.
expect(emphasis).not.toBe(Bun.stripANSI(emphasis));
expect(strong).not.toBe(Bun.stripANSI(strong));
expect(codeSpan).not.toBe(Bun.stripANSI(codeSpan));
// The three styles are mutually distinct.
expect(emphasis).not.toBe(strong);
expect(strong).not.toBe(codeSpan);
expect(emphasis).not.toBe(codeSpan);
} finally {
chalk.level = previousLevel;
}
});
test("a rawInline node renders its verbatim value", () => {
const stripped = bodyOf({
type: "paragraph",
children: [
{ type: "text", value: "a " },
{ type: "rawInline", value: "~~b~~" },
{ type: "text", value: " c" },
],
});
expect(stripped).toBe("a ~~b~~ c\n\n");
});
}); });

View File

@@ -11,7 +11,6 @@
}, },
"dependencies": { "dependencies": {
"remark-frontmatter": "^5.0.0", "remark-frontmatter": "^5.0.0",
"remark-gfm": "^4.0.1",
"remark-parse": "^11.0.0", "remark-parse": "^11.0.0",
"unified": "^11.0.5", "unified": "^11.0.5",
"yaml": "^2.9.0" "yaml": "^2.9.0"

View File

@@ -1,17 +1,16 @@
import type { ListItem, Node, PhrasingContent, Root, RootContent } from "mdast"; import type { PhrasingContent, Root, RootContent } from "mdast";
import remarkFrontmatter from "remark-frontmatter"; import remarkFrontmatter from "remark-frontmatter";
import remarkGfm from "remark-gfm";
import remarkParse from "remark-parse"; import remarkParse from "remark-parse";
import { unified } from "unified"; import { unified } from "unified";
import { parse as parseYaml } from "yaml"; import { parse as parseYaml } from "yaml";
import type { Block, DocumentAST, Frontmatter, InlineNode, ListItemBlock } from "./types.ts"; import type { Block, DocumentAST, Frontmatter, InlineNode } from "./types.ts";
const processor = unified().use(remarkParse).use(remarkFrontmatter).use(remarkGfm); const processor = unified().use(remarkParse).use(remarkFrontmatter);
export function parse(input: string): DocumentAST { export function parse(input: string): DocumentAST {
const tree = processor.parse(input); const tree = processor.parse(input);
const frontmatter = extractFrontmatter(tree); const frontmatter = extractFrontmatter(tree);
const blocks = tree.children.flatMap((node) => translateBlock(node, input)); const blocks = tree.children.flatMap(translateBlock);
return { frontmatter, blocks, diagnostics: [] }; return { frontmatter, blocks, diagnostics: [] };
} }
@@ -27,74 +26,21 @@ function extractFrontmatter(tree: Root): Frontmatter {
return {}; return {};
} }
function translateBlock(node: RootContent, input: string): Block[] { function translateBlock(node: RootContent): Block[] {
switch (node.type) { if (node.type === "heading") {
case "heading": return [{ type: "heading", level: node.depth, children: translateInline(node.children) }];
return [ }
{ type: "heading", level: node.depth, children: translateInline(node.children, input) }, if (node.type === "paragraph") {
]; return [{ type: "paragraph", children: translateInline(node.children) }];
case "paragraph": }
return [{ type: "paragraph", children: translateInline(node.children, input) }];
case "list":
return [
{
type: "list",
ordered: node.ordered ?? false,
items: node.children.map((item) => translateListItem(item, input)),
},
];
case "blockquote":
return [
{
type: "blockquote",
children: node.children.flatMap((child) => translateBlock(child, input)),
},
];
case "code":
return [{ type: "code", ...(node.lang ? { lang: node.lang } : {}), value: node.value }];
case "thematicBreak":
return [{ type: "thematicBreak" }];
// Frontmatter is captured separately and must not double as a block.
case "yaml":
return []; return [];
default:
return [{ type: "raw", value: slice(node, input) }];
}
} }
function translateListItem(item: ListItem, input: string): ListItemBlock { function translateInline(nodes: PhrasingContent[]): InlineNode[] {
return { return nodes.flatMap((node) => {
type: "listItem", if (node.type === "text") {
children: item.children.flatMap((child) => translateBlock(child, input)),
};
}
function translateInline(nodes: PhrasingContent[], input: string): InlineNode[] {
return nodes.flatMap((node): InlineNode[] => {
switch (node.type) {
case "text":
return [{ type: "text", value: node.value }]; return [{ type: "text", value: node.value }];
case "emphasis":
return [{ type: "emphasis", children: translateInline(node.children, input) }];
case "strong":
return [{ type: "strong", children: translateInline(node.children, input) }];
case "inlineCode":
return [{ type: "codeSpan", value: node.value }];
case "link":
return [{ type: "link", href: node.url, children: translateInline(node.children, input) }];
default:
return [{ type: "rawInline", value: slice(node, input) }];
} }
return [];
}); });
} }
// Verbatim source for an unmodelled node, taken by position so it round-trips
// byte-for-byte rather than being re-stringified through remark.
function slice(node: Node, input: string): string {
const start = node.position?.start.offset;
const end = node.position?.end.offset;
if (start === undefined || end === undefined) {
return "";
}
return input.slice(start, end);
}

View File

@@ -99,165 +99,4 @@ A paragraph under it.
expect(result.diagnostics).toEqual([]); expect(result.diagnostics).toEqual([]);
}); });
test("an unordered list is a ListBlock whose items wrap their child blocks", () => {
const result = parse("- First\n- Second\n");
expect(result.blocks).toEqual([
{
type: "list",
ordered: false,
items: [
{
type: "listItem",
children: [{ type: "paragraph", children: [{ type: "text", value: "First" }] }],
},
{
type: "listItem",
children: [{ type: "paragraph", children: [{ type: "text", value: "Second" }] }],
},
],
},
]);
});
test("an ordered list carries the ordered flag", () => {
const result = parse("1. One\n2. Two\n");
expect(result.blocks).toEqual([
{
type: "list",
ordered: true,
items: [
{
type: "listItem",
children: [{ type: "paragraph", children: [{ type: "text", value: "One" }] }],
},
{
type: "listItem",
children: [{ type: "paragraph", children: [{ type: "text", value: "Two" }] }],
},
],
},
]);
});
test("a list item is a container of blocks, not a bare inline array", () => {
const result = parse("- Just text\n");
const list = result.blocks[0];
if (list?.type !== "list") throw new Error("expected a list block");
expect(list.items[0]).toEqual({
type: "listItem",
children: [{ type: "paragraph", children: [{ type: "text", value: "Just text" }] }],
});
});
test("a blockquote is a container holding blocks", () => {
const result = parse("> Quoted prose.\n");
expect(result.blocks).toEqual([
{
type: "blockquote",
children: [{ type: "paragraph", children: [{ type: "text", value: "Quoted prose." }] }],
},
]);
});
test("an OFM callout parses as an ordinary blockquote with its text preserved", () => {
const result = parse("> [!note]\n> Remember this.\n");
const quote = result.blocks[0];
if (quote?.type !== "blockquote") throw new Error("expected a blockquote block");
const rendered = JSON.stringify(quote);
expect(rendered).toContain("[!note]");
expect(rendered).toContain("Remember this.");
});
test("a fenced code block carries its language and literal text", () => {
const result = parse("```js\nconst x = @sugar{1};\n```\n");
expect(result.blocks).toEqual([{ type: "code", lang: "js", value: "const x = @sugar{1};" }]);
});
test("a code block without a language has no lang", () => {
const result = parse("```\nplain text\n```\n");
expect(result.blocks).toEqual([{ type: "code", value: "plain text" }]);
});
test("a thematic break is modelled", () => {
const result = parse("---\n");
expect(result.blocks).toEqual([{ type: "thematicBreak" }]);
});
test("emphasis, strong, and code-span inlines are modelled", () => {
const result = parse("*em* **strong** `code`\n");
const para = result.blocks[0];
if (para?.type !== "paragraph") throw new Error("expected a paragraph block");
expect(para.children).toEqual([
{ type: "emphasis", children: [{ type: "text", value: "em" }] },
{ type: "text", value: " " },
{ type: "strong", children: [{ type: "text", value: "strong" }] },
{ type: "text", value: " " },
{ type: "codeSpan", value: "code" },
]);
});
test("a link is modelled with its href and inline children, and the href is not annotation-parsed here", () => {
const result = parse("[the docs](https://example.com/@stuff)\n");
const para = result.blocks[0];
if (para?.type !== "paragraph") throw new Error("expected a paragraph block");
expect(para.children).toEqual([
{
type: "link",
href: "https://example.com/@stuff",
children: [{ type: "text", value: "the docs" }],
},
]);
});
test("a code span preserves its raw content verbatim", () => {
const result = parse("Use `@sugar{1 tbsp}` literally.\n");
const para = result.blocks[0];
if (para?.type !== "paragraph") throw new Error("expected a paragraph block");
expect(para.children).toContainEqual({ type: "codeSpan", value: "@sugar{1 tbsp}" });
});
test("an unmodelled block (a GFM table) falls through to a RawBlock with byte-for-byte source", () => {
const table = "| a | b |\n| - | - |\n| 1 | 2 |";
const result = parse(`${table}\n`);
expect(result.blocks).toEqual([{ type: "raw", value: table }]);
});
test("an unmodelled inline (GFM strikethrough) falls through to a RawInline with byte-for-byte source", () => {
const result = parse("done ~~scratch~~ now\n");
const para = result.blocks[0];
if (para?.type !== "paragraph") throw new Error("expected a paragraph block");
expect(para.children).toEqual([
{ type: "text", value: "done " },
{ type: "rawInline", value: "~~scratch~~" },
{ type: "text", value: " now" },
]);
});
test("frontmatter is not emitted as a block", () => {
const input = `---
title: X
---
Body.
`;
const result = parse(input);
expect(result.blocks).toEqual([
{ type: "paragraph", children: [{ type: "text", value: "Body." }] },
]);
});
}); });

View File

@@ -13,35 +13,7 @@ export interface TextNode {
value: string; value: string;
} }
export interface EmphasisNode { export type InlineNode = TextNode;
type: "emphasis";
children: InlineNode[];
}
export interface StrongNode {
type: "strong";
children: InlineNode[];
}
export interface CodeSpanNode {
type: "codeSpan";
value: string;
}
export interface LinkNode {
type: "link";
href: string;
children: InlineNode[];
}
// Verbatim source for any inline construct core does not model, sliced from the
// original input so the unmodelled span round-trips byte-for-byte.
export interface RawInline {
type: "rawInline";
value: string;
}
export type InlineNode = TextNode | EmphasisNode | StrongNode | CodeSpanNode | LinkNode | RawInline;
export interface HeadingBlock { export interface HeadingBlock {
type: "heading"; type: "heading";
@@ -54,47 +26,7 @@ export interface ParagraphBlock {
children: InlineNode[]; children: InlineNode[];
} }
export interface ListItemBlock { export type Block = HeadingBlock | ParagraphBlock;
type: "listItem";
children: Block[];
}
export interface ListBlock {
type: "list";
ordered: boolean;
items: ListItemBlock[];
}
export interface BlockquoteBlock {
type: "blockquote";
children: Block[];
}
export interface CodeBlock {
type: "code";
lang?: string;
value: string;
}
export interface ThematicBreakBlock {
type: "thematicBreak";
}
// Verbatim source for any block construct core does not model, sliced from the
// original input so the unmodelled block round-trips byte-for-byte.
export interface RawBlock {
type: "raw";
value: string;
}
export type Block =
| HeadingBlock
| ParagraphBlock
| ListBlock
| BlockquoteBlock
| CodeBlock
| ThematicBreakBlock
| RawBlock;
export interface DocumentAST { export interface DocumentAST {
frontmatter: Frontmatter; frontmatter: Frontmatter;