diff --git a/.claude/tasks/0003-view-skeleton.md b/.claude/tasks/0003-view-skeleton.md new file mode 100644 index 0000000..f2c8a10 --- /dev/null +++ b/.claude/tasks/0003-view-skeleton.md @@ -0,0 +1,71 @@ +--- +spec: cli-view +--- + +## What to build + +The walking skeleton: a runnable `kitchen view ` 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 ` 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 ` 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. diff --git a/bun.lock b/bun.lock index d2af586..2d485a7 100644 --- a/bun.lock +++ b/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=="], } } diff --git a/bun.nix b/bun.nix index 43a395f..d4674ef 100644 --- a/bun.nix +++ b/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=="; + }; } diff --git a/packages/bin/package.json b/packages/bin/package.json index 1887cdb..fc9261c 100644 --- a/packages/bin/package.json +++ b/packages/bin/package.json @@ -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" } } diff --git a/packages/bin/src/index.ts b/packages/bin/src/index.ts index 5ef81e3..62b9343 100644 --- a/packages/bin/src/index.ts +++ b/packages/bin/src/index.ts @@ -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("", "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(); diff --git a/packages/bin/src/index_test.ts b/packages/bin/src/index_test.ts index 8bacff0..489f372 100644 --- a/packages/bin/src/index_test.ts +++ b/packages/bin/src/index_test.ts @@ -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"); + }); }); diff --git a/packages/bin/src/integration_test.ts b/packages/bin/src/integration_test.ts index 160fe43..770565d 100644 --- a/packages/bin/src/integration_test.ts +++ b/packages/bin/src/integration_test.ts @@ -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 }); + } + }); }); diff --git a/packages/bin/src/render.ts b/packages/bin/src/render.ts new file mode 100644 index 0000000..941c7f4 --- /dev/null +++ b/packages/bin/src/render.ts @@ -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(""); +} diff --git a/packages/bin/src/render_test.ts b/packages/bin/src/render_test.ts new file mode 100644 index 0000000..d78a2a0 --- /dev/null +++ b/packages/bin/src/render_test.ts @@ -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; + } + }); +}); diff --git a/packages/bin/src/test-support.ts b/packages/bin/src/test-support.ts new file mode 100644 index 0000000..70dbfbb --- /dev/null +++ b/packages/bin/src/test-support.ts @@ -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 }; +} diff --git a/packages/core/package.json b/packages/core/package.json index 3107281..43c1f38 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -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" } } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index cb0ff5c..3ab6ba4 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1 +1,2 @@ -export {}; +export { parse } from "./parse.ts"; +export type * from "./types.ts"; diff --git a/packages/core/src/index_test.ts b/packages/core/src/index_test.ts index ddd8f6b..8c0ccff 100644 --- a/packages/core/src/index_test.ts +++ b/packages/core/src/index_test.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"); diff --git a/packages/core/src/parse.ts b/packages/core/src/parse.ts new file mode 100644 index 0000000..e5bc52a --- /dev/null +++ b/packages/core/src/parse.ts @@ -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 []; + }); +} diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts new file mode 100644 index 0000000..5232984 --- /dev/null +++ b/packages/core/src/types.ts @@ -0,0 +1,48 @@ +// The public AST node and document types returned by parse. + +export type Frontmatter = Record; + +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[]; +}