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:
71
.claude/tasks/0003-view-skeleton.md
Normal file
71
.claude/tasks/0003-view-skeleton.md
Normal 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 1–6) and paragraphs are modelled as `HeadingBlock` and `ParagraphBlock`, with paragraph content as an inline array of `TextNode`
|
||||
- [x] Blocks are flat and in document order (a heading is a sibling of the following paragraph, not its parent)
|
||||
- [x] remark types do not appear in core's public API
|
||||
- [x] `kitchen view <file>` reads the file, calls `parse`, and prints the rendered output
|
||||
- [x] `render(ast)` is pure (no I/O, no side effects) and returns an ANSI-styled string
|
||||
- [x] Frontmatter renders as raw YAML before the body, followed by a visual separator
|
||||
- [x] Headings render bold and distinct by level; paragraphs render prose followed by a blank line
|
||||
- [x] chalk styling is suppressed automatically when stdout is not a TTY
|
||||
- [x] A missing file path prints commander usage to stderr and exits 1; an unreadable/nonexistent file prints a human-readable error to stderr and exits 1
|
||||
- [x] Renderer unit tests (ANSI stripped) cover frontmatter, the separator, headings, and paragraphs
|
||||
- [x] Core unit tests cover frontmatter passthrough (arbitrary fields, empty, absent), headings at every level, and paragraphs with `TextNode` content
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
All acceptance criteria are met. The following decisions and scope boundaries are worth recording.
|
||||
|
||||
### Scope boundaries carried by the slice
|
||||
|
||||
Only `HeadingBlock`, `ParagraphBlock`, and `TextNode` are modelled, as the slice specifies.
|
||||
The remark→core translation therefore skips any block that is not a heading or paragraph (lists, blockquotes, code, thematic breaks), and `translateInline` keeps only text nodes, dropping every other inline node type.
|
||||
The drop is by whole node, so emphasised or linked text is currently lost, not merely unstyled.
|
||||
This is within task 0003's stated scope; the lossless `RawInline`/`RawBlock` fallback and the typed `EmphasisNode`/`StrongNode`/`LinkNode` land in task 0004, which also makes the translation recurse into container children.
|
||||
|
||||
Malformed-frontmatter handling is out of scope here and owned by task 0007.
|
||||
This slice parses well-formed frontmatter and returns `{}` for the empty and absent cases; a genuinely malformed YAML block would currently throw from the YAML parser.
|
||||
The total-function guarantee for that case (returning `{}` plus an `invalid-frontmatter` diagnostic) arrives with 0007.
|
||||
|
||||
### Types defined ahead of full use
|
||||
|
||||
`Diagnostic`, `Point`, and `Position` are defined in the types module because `DocumentAST.diagnostics` is typed `Diagnostic[]`, even though only the empty case (`diagnostics: []`) is produced in this slice.
|
||||
This keeps the public shape stable; 0007 populates the channel.
|
||||
|
||||
### Rendering decisions
|
||||
|
||||
Headings render distinct-by-level via chalk, tapering from bold at level 1 toward dim at level 6; a heading is followed by a single newline and a paragraph by a blank line, which is what visually separates them once ANSI is stripped.
|
||||
Per the cli-view spec, the specific colour and weight choices are visual decisions verified by inspection, not asserted in tests — the renderer tests assert ANSI-stripped text, spacing, and the presence/absence of styling, not particular colours.
|
||||
The frontmatter separator is a dimmed 40-character box-drawing rule.
|
||||
|
||||
### 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
125
bun.lock
@@ -18,11 +18,20 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@kitchen-md/core": "workspace:*",
|
||||
"chalk": "^5.6.2",
|
||||
"commander": "^15.0.0",
|
||||
"yaml": "^2.9.0",
|
||||
},
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@kitchen-md/core",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"remark-frontmatter": "^5.0.0",
|
||||
"remark-parse": "^11.0.0",
|
||||
"unified": "^11.0.5",
|
||||
"yaml": "^2.9.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
@@ -48,16 +57,132 @@
|
||||
|
||||
"@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/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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
|
||||
|
||||
"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
232
bun.nix
@@ -51,10 +51,30 @@
|
||||
};
|
||||
"@kitchen-md/bin" = copyPathToStore ./packages/bin;
|
||||
"@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 {
|
||||
url = "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz";
|
||||
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 {
|
||||
url = "https://registry.npmjs.org/bun-types/-/bun-types-1.3.14.tgz";
|
||||
hash = "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ==";
|
||||
@@ -63,16 +83,228 @@
|
||||
url = "https://registry.npmjs.org/bun2nix/-/bun2nix-2.1.2.tgz";
|
||||
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 {
|
||||
url = "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz";
|
||||
hash = "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==";
|
||||
};
|
||||
"ms@2.1.3" = fetchurl {
|
||||
url = "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz";
|
||||
hash = "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==";
|
||||
};
|
||||
"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 {
|
||||
url = "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz";
|
||||
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 {
|
||||
url = "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz";
|
||||
hash = "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==";
|
||||
};
|
||||
"unified@11.0.5" = fetchurl {
|
||||
url = "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz";
|
||||
hash = "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==";
|
||||
};
|
||||
"unist-util-is@6.0.1" = fetchurl {
|
||||
url = "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz";
|
||||
hash = "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==";
|
||||
};
|
||||
"unist-util-stringify-position@4.0.0" = fetchurl {
|
||||
url = "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz";
|
||||
hash = "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==";
|
||||
};
|
||||
"unist-util-visit-parents@6.0.2" = fetchurl {
|
||||
url = "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz";
|
||||
hash = "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==";
|
||||
};
|
||||
"unist-util-visit@5.1.0" = fetchurl {
|
||||
url = "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz";
|
||||
hash = "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==";
|
||||
};
|
||||
"vfile-message@4.0.3" = fetchurl {
|
||||
url = "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz";
|
||||
hash = "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==";
|
||||
};
|
||||
"vfile@6.0.3" = fetchurl {
|
||||
url = "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz";
|
||||
hash = "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==";
|
||||
};
|
||||
"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==";
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
"build": "bun build --compile ./src/index.ts --outfile kitchen"
|
||||
},
|
||||
"dependencies": {
|
||||
"@kitchen-md/core": "workspace:*"
|
||||
"@kitchen-md/core": "workspace:*",
|
||||
"chalk": "^5.6.2",
|
||||
"commander": "^15.0.0",
|
||||
"yaml": "^2.9.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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", () => {
|
||||
test.todo("exits with non-zero code when no file argument is given");
|
||||
test.todo("exits with non-zero code when file does not exist");
|
||||
test("exits with non-zero code when no file argument is given", async () => {
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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", () => {
|
||||
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 });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
46
packages/bin/src/render.ts
Normal file
46
packages/bin/src/render.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { Block, DocumentAST, Frontmatter, HeadingBlock, InlineNode } from "@kitchen-md/core";
|
||||
import chalk from "chalk";
|
||||
import { stringify as stringifyYaml } from "yaml";
|
||||
|
||||
const SEPARATOR = "─".repeat(40);
|
||||
|
||||
export function render(ast: DocumentAST): string {
|
||||
const body = ast.blocks.map(renderBlock).join("");
|
||||
return renderFrontmatter(ast.frontmatter) + body;
|
||||
}
|
||||
|
||||
function renderFrontmatter(frontmatter: Frontmatter): string {
|
||||
if (Object.keys(frontmatter).length === 0) {
|
||||
return "";
|
||||
}
|
||||
return `${stringifyYaml(frontmatter)}${chalk.dim(SEPARATOR)}\n`;
|
||||
}
|
||||
|
||||
function renderBlock(block: Block): string {
|
||||
if (block.type === "heading") {
|
||||
return `${styleHeading(block.level)(renderInline(block.children))}\n`;
|
||||
}
|
||||
return `${renderInline(block.children)}\n\n`;
|
||||
}
|
||||
|
||||
// Each level gets a distinct style, tapering from bold at level 1 toward dim at level 6.
|
||||
function styleHeading(level: HeadingBlock["level"]): (text: string) => string {
|
||||
switch (level) {
|
||||
case 1:
|
||||
return chalk.bold.underline;
|
||||
case 2:
|
||||
return chalk.bold;
|
||||
case 3:
|
||||
return chalk.bold.dim;
|
||||
case 4:
|
||||
return chalk.dim.underline;
|
||||
case 5:
|
||||
return chalk.dim;
|
||||
case 6:
|
||||
return chalk.dim.italic;
|
||||
}
|
||||
}
|
||||
|
||||
function renderInline(nodes: InlineNode[]): string {
|
||||
return nodes.map((node) => node.value).join("");
|
||||
}
|
||||
87
packages/bin/src/render_test.ts
Normal file
87
packages/bin/src/render_test.ts
Normal 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;
|
||||
}
|
||||
});
|
||||
});
|
||||
9
packages/bin/src/test-support.ts
Normal file
9
packages/bin/src/test-support.ts
Normal 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 };
|
||||
}
|
||||
@@ -8,5 +8,11 @@
|
||||
},
|
||||
"scripts": {
|
||||
"test": "bun test"
|
||||
},
|
||||
"dependencies": {
|
||||
"remark-frontmatter": "^5.0.0",
|
||||
"remark-parse": "^11.0.0",
|
||||
"unified": "^11.0.5",
|
||||
"yaml": "^2.9.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export {};
|
||||
export { parse } from "./parse.ts";
|
||||
export type * from "./types.ts";
|
||||
|
||||
@@ -1,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("frontmatter", () => {
|
||||
test.todo("parses frontmatter fields as-is");
|
||||
test.todo("returns an empty object for empty frontmatter");
|
||||
test.todo("returns an empty object when there is no frontmatter");
|
||||
test("parses frontmatter fields as-is", () => {
|
||||
const source =
|
||||
"---\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(
|
||||
"does not throw on malformed frontmatter: frontmatter is {}, body still parses, and an invalid-frontmatter diagnostic preserves the raw YAML",
|
||||
);
|
||||
});
|
||||
|
||||
describe("blocks", () => {
|
||||
test.todo("parses headings at every level (1-6)");
|
||||
test.todo("parses paragraphs with typed inline nodes");
|
||||
test("parses headings at every level (1-6)", () => {
|
||||
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 unordered list");
|
||||
test.todo("models a list item as a container wrapping a paragraph, not a bare inline array");
|
||||
|
||||
46
packages/core/src/parse.ts
Normal file
46
packages/core/src/parse.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { PhrasingContent, Root, RootContent } from "mdast";
|
||||
import remarkFrontmatter from "remark-frontmatter";
|
||||
import remarkParse from "remark-parse";
|
||||
import { unified } from "unified";
|
||||
import { parse as parseYaml } from "yaml";
|
||||
import type { Block, DocumentAST, Frontmatter, InlineNode } from "./types.ts";
|
||||
|
||||
const processor = unified().use(remarkParse).use(remarkFrontmatter);
|
||||
|
||||
export function parse(input: string): DocumentAST {
|
||||
const tree = processor.parse(input);
|
||||
const frontmatter = extractFrontmatter(tree);
|
||||
const blocks = tree.children.flatMap(translateBlock);
|
||||
return { frontmatter, blocks, diagnostics: [] };
|
||||
}
|
||||
|
||||
function extractFrontmatter(tree: Root): Frontmatter {
|
||||
const yamlNode = tree.children.find((node) => node.type === "yaml");
|
||||
if (!yamlNode) {
|
||||
return {};
|
||||
}
|
||||
const data = parseYaml(yamlNode.value);
|
||||
if (data !== null && typeof data === "object" && !Array.isArray(data)) {
|
||||
return data as Frontmatter;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
function translateBlock(node: RootContent): Block[] {
|
||||
if (node.type === "heading") {
|
||||
return [{ type: "heading", level: node.depth, children: translateInline(node.children) }];
|
||||
}
|
||||
if (node.type === "paragraph") {
|
||||
return [{ type: "paragraph", children: translateInline(node.children) }];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function translateInline(nodes: PhrasingContent[]): InlineNode[] {
|
||||
return nodes.flatMap((node) => {
|
||||
if (node.type === "text") {
|
||||
return [{ type: "text", value: node.value }];
|
||||
}
|
||||
return [];
|
||||
});
|
||||
}
|
||||
48
packages/core/src/types.ts
Normal file
48
packages/core/src/types.ts
Normal 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[];
|
||||
}
|
||||
Reference in New Issue
Block a user