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.
This commit is contained in:
2026-07-26 07:24:32 -04:00
parent fdede89e0c
commit d1c1f330c5
15 changed files with 836 additions and 14 deletions

View File

@@ -0,0 +1,71 @@
---
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 16) 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.
### Tests that are green on arrival
A few required-coverage tests document behaviour that the minimal implementation already satisfies and so pass without a preceding red (frontmatter empty/absent, flat document order, and the level-distinctness/suppression renderer test).
They assert real observable behaviour against independent literals rather than restating the implementation.
### Review follow-up applied
The `runCli` and `stripAnsi` test helpers were extracted into `packages/bin/src/test-support.ts` to remove duplication the review flagged across the bin test files.
The scaffold's remaining `test.todo` placeholders (future block/inline types, annotations, the smoke test) are left intact for their owning tasks.

125
bun.lock
View File

@@ -18,11 +18,20 @@
}, },
"dependencies": { "dependencies": {
"@kitchen-md/core": "workspace:*", "@kitchen-md/core": "workspace:*",
"chalk": "^5.6.2",
"commander": "^15.0.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 +57,132 @@
"@kitchen-md/core": ["@kitchen-md/core@workspace:packages/core"], "@kitchen-md/core": ["@kitchen-md/core@workspace:packages/core"],
"@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=="],
"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=="],
"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=="],
"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=="],
"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=="],
"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=="],
"yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
} }
} }

232
bun.nix
View File

@@ -51,10 +51,30 @@
}; };
"@kitchen-md/bin" = copyPathToStore ./packages/bin; "@kitchen-md/bin" = copyPathToStore ./packages/bin;
"@kitchen-md/core" = copyPathToStore ./packages/core; "@kitchen-md/core" = copyPathToStore ./packages/core;
"@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==";
};
"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 +83,228 @@
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==";
};
"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==";
};
"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==";
};
"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==";
}; };
"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==";
};
"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==";
};
} }

View File

@@ -11,6 +11,9 @@
"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",
"yaml": "^2.9.0"
} }
} }

View File

@@ -1 +1,27 @@
// CLI entry point #!/usr/bin/env bun
import { readFileSync } from "node:fs";
import { parse } from "@kitchen-md/core";
import { Command } from "commander";
import { render } from "./render.ts";
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) => {
let content: string;
try {
content = readFileSync(file, "utf8");
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
process.stderr.write(`kitchen: cannot read '${file}': ${reason}\n`);
process.exit(1);
}
process.stdout.write(render(parse(content)));
});
program.parse();

View File

@@ -1,6 +1,25 @@
import { describe, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { runCli } from "./test-support.ts";
const CLI = `${import.meta.dir}/index.ts`;
describe("cli", () => { describe("cli", () => {
test.todo("exits with non-zero code when no file argument is given"); test("exits with non-zero code when no file argument is given", async () => {
test.todo("exits with non-zero code when file does not exist"); const { exitCode, stderr } = await runCli(["view"], CLI);
expect(exitCode).not.toBe(0);
expect(stderr).toMatch(/missing required argument|usage/i);
});
test("exits with non-zero code when file does not exist", async () => {
const { exitCode, stderr } = await runCli(
["view", "/no/such/kitchen-recipe-does-not-exist.md"],
CLI,
);
expect(exitCode).toBe(1);
expect(stderr.trim().length).toBeGreaterThan(0);
expect(stderr).toMatch(/cannot read|no such file|ENOENT/i);
expect(stderr).toContain("/no/such/kitchen-recipe-does-not-exist.md");
});
}); });

View File

@@ -1,5 +1,33 @@
import { describe, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { runCli, stripAnsi } from "./test-support.ts";
const CLI = `${import.meta.dir}/index.ts`;
describe("cli — integration", () => { describe("cli — integration", () => {
test.todo("invokes core parser and produces output for a real fixture file"); test("invokes core parser and produces output for a real fixture file", async () => {
const dir = mkdtempSync(join(tmpdir(), "kitchen-md-"));
const file = join(dir, "recipe.md");
writeFileSync(
file,
"---\ntitle: Test Recipe\nservings: 2\n---\n\n# Heading One\n\nA plain paragraph of prose.\n",
);
try {
const { exitCode, stdout } = await runCli(["view", file], CLI);
const output = stripAnsi(stdout);
expect(exitCode).toBe(0);
expect(output).toContain("title: Test Recipe");
expect(output).toContain("servings: 2");
expect(output).toMatch(/─+/);
expect(output).toContain("Heading One");
expect(output).toContain("A plain paragraph of prose.");
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
}); });

View 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("");
}

View File

@@ -0,0 +1,87 @@
import { describe, expect, test } from "bun:test";
import type { DocumentAST } from "@kitchen-md/core";
import chalk from "chalk";
import { render } from "./render.ts";
import { stripAnsi } from "./test-support.ts";
describe("render", () => {
test("renders a paragraph as prose followed by a blank line", () => {
const ast: DocumentAST = {
frontmatter: {},
blocks: [{ type: "paragraph", children: [{ type: "text", value: "Hello world" }] }],
diagnostics: [],
};
expect(stripAnsi(render(ast))).toBe("Hello world\n\n");
});
test("renders a heading followed by a single newline (not a blank line)", () => {
const ast: DocumentAST = {
frontmatter: {},
blocks: [{ type: "heading", level: 2, children: [{ type: "text", value: "Batter" }] }],
diagnostics: [],
};
expect(stripAnsi(render(ast))).toBe("Batter\n");
});
test("renders frontmatter as raw YAML before the body", () => {
const ast: DocumentAST = {
frontmatter: { title: "Classic Pancakes", servings: 4 },
blocks: [{ type: "paragraph", children: [{ type: "text", value: "A simple breakfast." }] }],
diagnostics: [],
};
const output = stripAnsi(render(ast));
expect(output.startsWith("title: Classic Pancakes\nservings: 4\n")).toBe(true);
expect(output).toContain("A simple breakfast.");
expect(output.indexOf("title: Classic Pancakes")).toBeLessThan(
output.indexOf("A simple breakfast."),
);
});
test("renders a visual separator between the frontmatter and the body", () => {
const ast: DocumentAST = {
frontmatter: { title: "Classic Pancakes", servings: 4 },
blocks: [{ type: "paragraph", children: [{ type: "text", value: "A simple breakfast." }] }],
diagnostics: [],
};
const lines = stripAnsi(render(ast)).split("\n");
const separatorIndex = lines.findIndex((line) => /^─+$/.test(line));
const frontmatterIndex = lines.findIndex((line) => line.includes("servings: 4"));
const bodyIndex = lines.findIndex((line) => line.includes("A simple breakfast."));
expect(separatorIndex).toBeGreaterThan(-1);
expect(separatorIndex).toBeGreaterThan(frontmatterIndex);
expect(separatorIndex).toBeLessThan(bodyIndex);
});
test("styles headings distinctly by level and suppresses ANSI when colour is disabled", () => {
const h1: DocumentAST = {
frontmatter: {},
blocks: [{ type: "heading", level: 1, children: [{ type: "text", value: "Title" }] }],
diagnostics: [],
};
const h2: DocumentAST = {
frontmatter: {},
blocks: [{ type: "heading", level: 2, children: [{ type: "text", value: "Title" }] }],
diagnostics: [],
};
const original = chalk.level;
try {
chalk.level = 1;
expect(render(h1)).toContain("\x1b[");
expect(render(h1)).not.toBe(render(h2));
chalk.level = 0;
expect(render(h1)).not.toContain("\x1b[");
} finally {
chalk.level = original;
}
});
});

View File

@@ -0,0 +1,9 @@
export const stripAnsi = (s: string): string => s.replace(/\[[0-9;]*m/g, "");
export async function runCli(args: string[], cli: string) {
const proc = Bun.spawn(["bun", cli, ...args], { stdout: "pipe", stderr: "pipe" });
const exitCode = await proc.exited;
const stdout = await new Response(proc.stdout).text();
const stderr = await new Response(proc.stderr).text();
return { exitCode, stdout, stderr };
}

View File

@@ -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"
} }
} }

View File

@@ -1 +1,2 @@
export {}; export { parse } from "./parse.ts";
export type * from "./types.ts";

View File

@@ -1,18 +1,93 @@
import { describe, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { parse } from "@kitchen-md/core";
describe("parser", () => { describe("parser", () => {
describe("frontmatter", () => { describe("frontmatter", () => {
test.todo("parses frontmatter fields as-is"); test("parses frontmatter fields as-is", () => {
test.todo("returns an empty object for empty frontmatter"); const source =
test.todo("returns an empty object when there is no frontmatter"); "---\ntitle: Classic Pancakes\nservings: 4\ntags: [breakfast, quick]\n---\n\n# Classic Pancakes";
const result = parse(source);
expect(result.frontmatter).toEqual({
title: "Classic Pancakes",
servings: 4,
tags: ["breakfast", "quick"],
});
});
test("returns an empty object for empty frontmatter", () => {
const source = "---\n---\n\n# Title";
const result = parse(source);
expect(result.frontmatter).toEqual({});
expect(result.blocks).toContainEqual({
type: "heading",
level: 1,
children: [{ type: "text", value: "Title" }],
});
expect(result.diagnostics).toEqual([]);
});
test("returns an empty object when there is no frontmatter", () => {
const result = parse("# Title");
expect(result.frontmatter).toEqual({});
expect(result.diagnostics).toEqual([]);
});
test.todo( test.todo(
"does not throw on malformed frontmatter: frontmatter is {}, body still parses, and an invalid-frontmatter diagnostic preserves the raw YAML", "does not throw on malformed frontmatter: frontmatter is {}, body still parses, and an invalid-frontmatter diagnostic preserves the raw YAML",
); );
}); });
describe("blocks", () => { describe("blocks", () => {
test.todo("parses headings at every level (1-6)"); test("parses headings at every level (1-6)", () => {
test.todo("parses paragraphs with typed inline nodes"); const source = "# H1\n\n## H2\n\n### H3\n\n#### H4\n\n##### H5\n\n###### H6";
const result = parse(source);
expect(result).toEqual({
frontmatter: {},
blocks: [
{ type: "heading", level: 1, children: [{ type: "text", value: "H1" }] },
{ type: "heading", level: 2, children: [{ type: "text", value: "H2" }] },
{ type: "heading", level: 3, children: [{ type: "text", value: "H3" }] },
{ type: "heading", level: 4, children: [{ type: "text", value: "H4" }] },
{ type: "heading", level: 5, children: [{ type: "text", value: "H5" }] },
{ type: "heading", level: 6, children: [{ type: "text", value: "H6" }] },
],
diagnostics: [],
});
});
test("keeps blocks flat and in document order (a heading is a sibling of the following paragraph)", () => {
const result = parse("# Batter\n\nSift the flour into a bowl.");
expect(result).toEqual({
frontmatter: {},
blocks: [
{ type: "heading", level: 1, children: [{ type: "text", value: "Batter" }] },
{
type: "paragraph",
children: [{ type: "text", value: "Sift the flour into a bowl." }],
},
],
diagnostics: [],
});
});
test("parses paragraphs with typed inline nodes", () => {
const result = parse("Hello world");
expect(result).toEqual({
frontmatter: {},
blocks: [
{
type: "paragraph",
children: [{ type: "text", value: "Hello world" }],
},
],
diagnostics: [],
});
});
test.todo("parses an ordered list"); test.todo("parses an ordered list");
test.todo("parses an unordered 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("models a list item as a container wrapping a paragraph, not a bare inline array");

View 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 [];
});
}

View File

@@ -0,0 +1,48 @@
// The public AST node and document types returned by parse.
export type Frontmatter = Record<string, unknown>;
export interface Point {
line: number;
column: number;
offset?: number;
}
export interface Position {
start: Point;
end: Point;
}
export interface Diagnostic {
severity: "warning";
code: string;
message: string;
source?: string;
position?: Position;
}
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[];
}