Compare commits
3 Commits
4ac11865c5
...
task-0005-
| Author | SHA1 | Date | |
|---|---|---|---|
| b0fdc22165 | |||
| 22befaaa71 | |||
| ab309d3226 |
69
.claude/tasks/0004-richer-blocks-and-inline.md
Normal file
69
.claude/tasks/0004-richer-blocks-and-inline.md
Normal file
@@ -0,0 +1,69 @@
|
||||
---
|
||||
spec: core-parser
|
||||
blocked-by: 0003-view-skeleton
|
||||
---
|
||||
|
||||
## What to build
|
||||
|
||||
Extend the parser and renderer to cover the rest of the everyday Markdown body, so `kitchen view` renders lists, blockquotes, code, rules, and inline emphasis faithfully.
|
||||
|
||||
In `@kitchen-md/core`, model the remaining block types: `ListBlock` (ordered flag + items) whose items are `ListItemBlock` containers holding `Block[]`, `BlockquoteBlock` as a container holding `Block[]` (OFM callouts like `> [!note]` parse as ordinary blockquotes), `CodeBlock` (optional language + literal text, no annotation parsing), and `ThematicBreakBlock`.
|
||||
Add the inline nodes: `EmphasisNode`, `StrongNode`, `CodeSpanNode`, and `LinkNode` (href + inline content).
|
||||
Add the raw fallbacks: any remark block or inline node core does not model falls through to `RawBlock` / `RawInline`, whose verbatim `value` is captured by position-slicing the original input (never re-stringified), so unmodelled constructs round-trip byte-for-byte.
|
||||
Add remark-gfm to the pipeline; the translation layer recurses into container children rather than collapsing them.
|
||||
|
||||
In `@kitchen-md/bin`, extend `render` for the new nodes: lists (bullet for unordered, sequential number for ordered, one item per line), code blocks (literal, no highlighting), thematic breaks (a horizontal rule string), emphasis (italic), strong (bold), code spans (a distinct dim/inverse style), and links (inline content only, href not shown).
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [x] `ListBlock` carries an ordered flag and an array of `ListItemBlock`; each `ListItemBlock` is a container wrapping child blocks (e.g. a paragraph), not a bare inline array
|
||||
- [x] `BlockquoteBlock` is a container holding `Block[]`; a callout (`> [!note]`) parses as an ordinary blockquote with its text preserved
|
||||
- [x] `CodeBlock` carries an optional language and literal text, and its content is not annotation-parsed
|
||||
- [x] `ThematicBreakBlock` is modelled
|
||||
- [x] Inline `EmphasisNode`, `StrongNode`, `CodeSpanNode`, and `LinkNode` are modelled; code-span content is not annotation-parsed
|
||||
- [x] An unmodelled block (e.g. a GFM table) falls through to `RawBlock` and an unmodelled inline (e.g. strikethrough) to `RawInline`, each preserving byte-for-byte verbatim source via position-slicing
|
||||
- [x] `render` handles ordered and unordered lists, code blocks, thematic breaks, and blockquotes
|
||||
- [x] `render` handles emphasis (italic), strong (bold), code span (distinct style), and link (inline content only)
|
||||
- [x] Core unit tests cover each new block type, the list-item-wraps-a-paragraph shape, each new inline node, and the raw block/inline fallbacks with verbatim source
|
||||
- [x] Renderer unit tests (ANSI stripped) cover each new block and inline node
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
All acceptance criteria are met, with no dropped or deferred criteria.
|
||||
The following decisions are worth recording.
|
||||
|
||||
### Node discriminants mirror mdast
|
||||
|
||||
The new nodes reuse mdast's own type names as their discriminants — `list`, `listItem`, `blockquote`, `code`, `thematicBreak`, `emphasis`, `strong`, `link` — matching the precedent set by task 0003 (`text`, `heading`, `paragraph`).
|
||||
The two exceptions are the code span (`codeSpan`, since the interface is `CodeSpanNode` and mdast's `inlineCode` reads oddly in this AST) and the raw fallbacks (`raw` for blocks, `rawInline` for inlines), which have no mdast counterpart.
|
||||
|
||||
### `ListItemBlock` is scoped to `ListBlock.items`, not the `Block` union
|
||||
|
||||
A list item exists only as a member of a list, so `ListItemBlock` is deliberately kept out of the top-level `Block` union and the `renderBlock` switch.
|
||||
List items are dispatched through `renderItemContent`, reached only from `renderList`.
|
||||
This keeps the type as tight as the spec's "an array of `ListItemBlock`" shape rather than letting a list item appear anywhere a block is valid.
|
||||
|
||||
### `CodeBlock.lang` is omitted, not `undefined`, when absent
|
||||
|
||||
`lang` is an optional property that is left off entirely for an unlabelled fence rather than set to `undefined`, so `{ type: "code", value: "…" }` deep-equals the parser output without a stray `lang: undefined` key.
|
||||
|
||||
### Frontmatter must be excluded from the raw fallback
|
||||
|
||||
With the raw fallback now catching every unmodelled block, the `yaml` node — captured separately as frontmatter — is explicitly dropped in `translateBlock` so it does not also surface as a `RawBlock` and double-render.
|
||||
|
||||
### Content-not-annotation-parsed is satisfied structurally
|
||||
|
||||
Code blocks and code spans store their literal `value` verbatim and are never fed to `translateInline`.
|
||||
Annotation parsing itself lands in task 0006, so "not annotation-parsed" holds here by construction: there is no annotation pass for that content to escape.
|
||||
|
||||
### Rendering decisions (visual, not asserted by colour)
|
||||
|
||||
Following task 0003, the renderer tests assert ANSI-stripped text, spacing, and the presence/absence of styling — never particular colours.
|
||||
Unordered items use a `•` bullet, ordered items a sequential `1.`-from-one number (the model carries only the `ordered` flag, not a start offset), one item per line.
|
||||
Blockquote lines are prefixed with a dim `│ `, code blocks and the thematic-break rule render dim, code spans use `inverse` to stay distinct from the dim used elsewhere, emphasis is italic, strong is bold, and links render their inline content underlined with the href hidden.
|
||||
The list-item and blockquote renderers collapse child-block trailing spacing (`trimEnd`) so nested paragraphs sit on the marker/quote line rather than emitting their own blank line.
|
||||
|
||||
### remark-gfm
|
||||
|
||||
`remark-gfm` is added to the core pipeline so tables and strikethrough parse as their own nodes (`table`, `delete`) and therefore reach the raw fallback, where position-slicing captures them byte-for-byte.
|
||||
Without gfm they would parse as ordinary paragraph text and never exercise the fallback.
|
||||
63
.claude/tasks/0005-cross-references.md
Normal file
63
.claude/tasks/0005-cross-references.md
Normal file
@@ -0,0 +1,63 @@
|
||||
---
|
||||
spec: core-parser
|
||||
blocked-by: 0004-richer-blocks-and-inline
|
||||
---
|
||||
|
||||
## What to build
|
||||
|
||||
Add Obsidian cross-references so `[[links]]` and `![[embeds]]` — including KitchenMD Step References — are typed nodes the parser surfaces and the renderer styles.
|
||||
|
||||
In `@kitchen-md/core`, model `WikilinkNode` and `TransclusionNode`, both `{ target, anchor?, display? }`: `target` is the filename without extension, `anchor` is the part after `#` passed through verbatim, `display` is the alias after `|`.
|
||||
The node type — not a boolean flag — discriminates a reference (`[[…]]`) from an embed (`![[…]]`).
|
||||
`TransclusionNode` covers KitchenMD Step References (`![[file#section:N]]`, `![[file#N]]`), whose anchor is passed through as-is; Step Reference resolution is out of scope.
|
||||
Add remark-wiki-link to the pipeline.
|
||||
|
||||
In `@kitchen-md/bin`, render wikilinks distinctly from surrounding prose (underline or distinct colour), using display text or the target when there is no display text.
|
||||
Render transclusions as their raw source text (e.g. `![[file#section:1]]`).
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [x] `WikilinkNode` and `TransclusionNode` share the `{ target, anchor?, display? }` shape and are distinct node types
|
||||
- [x] Wikilinks parse with a bare target, with an anchor, and with a display alias; the anchor is passed through verbatim
|
||||
- [x] Transclusions parse with a display alias and with a Step Reference anchor (`#section:N` and `#N`) passed through as-is
|
||||
- [x] `render` shows wikilinks distinctly (display text, or target when absent)
|
||||
- [x] `render` shows transclusions as their raw source text
|
||||
- [x] Core unit tests cover wikilink (bare, anchor, display) and transclusion (bare, display, Step Reference anchor)
|
||||
- [x] Renderer unit tests (ANSI stripped) cover wikilink and transclusion rendering
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
All acceptance criteria are met, with no dropped or deferred criteria.
|
||||
The following decisions are worth recording.
|
||||
|
||||
### remark-wiki-link handles only `[[…]]`, so transclusions are recovered separately
|
||||
|
||||
The installed `remark-wiki-link@2.0.1` (landakram) parses `[[…]]` into `wikiLink` mdast nodes but does not recognise `![[…]]` embeds.
|
||||
The leading `!` makes remark attempt an image, which fails and leaves the whole span as literal text.
|
||||
Transclusions are therefore recovered in the translation layer by scanning each text run for `!\[\[…]]` and splitting it into text and `TransclusionNode` parts.
|
||||
This keeps the plugin in the pipeline as the spec asks while still surfacing transclusions as typed nodes.
|
||||
|
||||
### Anchors are split in the translation layer, not by the plugin
|
||||
|
||||
remark-wiki-link leaves the `#anchor` attached to the node's `value` and does not separate it.
|
||||
A shared `splitAnchor` helper splits `target#anchor` at the first `#` for both node types, so the anchor is passed through verbatim and the key is omitted entirely when absent.
|
||||
|
||||
### The alias divider is set to `|`
|
||||
|
||||
The plugin defaults its alias divider to `:`, which would consume a Step Reference anchor such as `#rolling:2`.
|
||||
It is configured with `aliasDivider: "|"` so that only a real Obsidian alias is split off, and a display alias is recorded only when it differs from the node's value.
|
||||
|
||||
### Discriminants mirror the interface names
|
||||
|
||||
Following task 0004's precedent for `codeSpan`, the discriminants are `"wikilink"` and `"transclusion"` — the interface names lowercased — since neither construct has an mdast counterpart to borrow a type name from.
|
||||
|
||||
### Optional keys are omitted when absent
|
||||
|
||||
Consistent with task 0004, `anchor` and `display` are spread in only when present rather than set to `undefined`, so parser output deep-equals the expected node shape without stray keys.
|
||||
|
||||
### Rendering follows task 0003/0004 conventions
|
||||
|
||||
Wikilinks render underlined, showing the display text or the target when there is no display, matching how links render (both are references).
|
||||
When a wikilink has an anchor but no display, only the target text shows — this follows the spec's wording ("display text, or the target when there is no display text") exactly.
|
||||
Transclusions render as their reconstructed raw source (`![[target#anchor|display]]`), unstyled, since resolving the embed is out of scope.
|
||||
The renderer tests assert ANSI-stripped text plus the presence of styling, never particular colours.
|
||||
74
bun.lock
74
bun.lock
@@ -30,13 +30,17 @@
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"remark-frontmatter": "^5.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"remark-parse": "^11.0.0",
|
||||
"remark-wiki-link": "^2.0.1",
|
||||
"unified": "^11.0.5",
|
||||
"yaml": "^2.9.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="],
|
||||
|
||||
"@biomejs/biome": ["@biomejs/biome@2.5.3", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.5.3", "@biomejs/cli-darwin-x64": "2.5.3", "@biomejs/cli-linux-arm64": "2.5.3", "@biomejs/cli-linux-arm64-musl": "2.5.3", "@biomejs/cli-linux-x64": "2.5.3", "@biomejs/cli-linux-x64-musl": "2.5.3", "@biomejs/cli-win32-arm64": "2.5.3", "@biomejs/cli-win32-x64": "2.5.3" }, "bin": { "biome": "bin/biome" } }, "sha512-MrJswFdei9EfDwwUy2tQrPDpK0AO+RmMFvBoaaJ6ayBc3sUbHdCE+XG5N8vp+5So41ZupZJQm0roHFFhMGVD7A=="],
|
||||
|
||||
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.5.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QhYP9muVQ0nUO5zztFuPbEwi4+94sJWVjaZds9aMi1l/KNZBiUjdiSUrGHsTaMGDXrYl+r4AS2sUKfgH3w+V3g=="],
|
||||
@@ -81,10 +85,16 @@
|
||||
|
||||
"bun2nix": ["bun2nix@2.1.2", "", { "dependencies": { "sade": "^1.8.1" }, "bin": { "bun2nix": "index.ts" } }, "sha512-0wx6Ar5ccrz4aSD5prbShwymjDEXFh7Bucxs+YrpAMa67TnVB95Hv8FV3oaQEbtOx6QGgIAyOmap6Y3WCRqetg=="],
|
||||
|
||||
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
|
||||
|
||||
"chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
|
||||
|
||||
"character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
|
||||
|
||||
"character-entities-legacy": ["character-entities-legacy@1.1.4", "", {}, "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA=="],
|
||||
|
||||
"character-reference-invalid": ["character-reference-invalid@1.1.4", "", {}, "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg=="],
|
||||
|
||||
"commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
@@ -107,28 +117,70 @@
|
||||
|
||||
"format": ["format@0.2.2", "", {}, "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww=="],
|
||||
|
||||
"is-alphabetical": ["is-alphabetical@1.0.4", "", {}, "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg=="],
|
||||
|
||||
"is-alphanumerical": ["is-alphanumerical@1.0.4", "", { "dependencies": { "is-alphabetical": "^1.0.0", "is-decimal": "^1.0.0" } }, "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A=="],
|
||||
|
||||
"is-decimal": ["is-decimal@1.0.4", "", {}, "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw=="],
|
||||
|
||||
"is-hexadecimal": ["is-hexadecimal@1.0.4", "", {}, "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw=="],
|
||||
|
||||
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
|
||||
|
||||
"is-unsafe": ["is-unsafe@2.0.0", "", {}, "sha512-2LdV822R+wmI86unXA93WCFpL6g+av8ynWk0nrHyJqGop5VoocYsSLFgN8jrfalT6iGeLNM4KXuVSsULP53kEA=="],
|
||||
|
||||
"longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
|
||||
|
||||
"markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
|
||||
|
||||
"mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="],
|
||||
|
||||
"mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="],
|
||||
|
||||
"mdast-util-frontmatter": ["mdast-util-frontmatter@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "escape-string-regexp": "^5.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-extension-frontmatter": "^2.0.0" } }, "sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA=="],
|
||||
|
||||
"mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="],
|
||||
|
||||
"mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="],
|
||||
|
||||
"mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="],
|
||||
|
||||
"mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="],
|
||||
|
||||
"mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="],
|
||||
|
||||
"mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="],
|
||||
|
||||
"mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="],
|
||||
|
||||
"mdast-util-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=="],
|
||||
|
||||
"mdast-util-wiki-link": ["mdast-util-wiki-link@0.1.2", "", { "dependencies": { "@babel/runtime": "^7.12.1", "mdast-util-to-markdown": "^0.6.5" } }, "sha512-DTcDyOxKDo3pB3fc0zQlD8myfQjYkW4hazUKI9PUyhtoj9JBeHC2eIdlVXmaT22bZkFAVU2d47B6y2jVKGoUQg=="],
|
||||
|
||||
"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-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="],
|
||||
|
||||
"micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="],
|
||||
|
||||
"micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="],
|
||||
|
||||
"micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="],
|
||||
|
||||
"micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="],
|
||||
|
||||
"micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="],
|
||||
|
||||
"micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="],
|
||||
|
||||
"micromark-extension-wiki-link": ["micromark-extension-wiki-link@0.0.4", "", { "dependencies": { "@babel/runtime": "^7.12.1" } }, "sha512-dJc8AfnoU8BHkN+7fWZvIS20SMsMS1ZlxQUn6We67MqeKbOiEDZV5eEvCpwqGBijbJbxX3Kxz879L4K9HIiOvw=="],
|
||||
|
||||
"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=="],
|
||||
@@ -173,12 +225,22 @@
|
||||
|
||||
"neverthrow": ["neverthrow@8.2.0", "", { "optionalDependencies": { "@rollup/rollup-linux-x64-gnu": "^4.24.0" } }, "sha512-kOCT/1MCPAxY5iUV3wytNFUMUolzuwd/VF/1KCx7kf6CutrOsTie+84zTGTpgQycjvfLdBBdvBvFLqFD2c0wkQ=="],
|
||||
|
||||
"parse-entities": ["parse-entities@2.0.0", "", { "dependencies": { "character-entities": "^1.0.0", "character-entities-legacy": "^1.0.0", "character-reference-invalid": "^1.0.0", "is-alphanumerical": "^1.0.0", "is-decimal": "^1.0.0", "is-hexadecimal": "^1.0.0" } }, "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ=="],
|
||||
|
||||
"path-expression-matcher": ["path-expression-matcher@1.6.2", "", {}, "sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ=="],
|
||||
|
||||
"remark-frontmatter": ["remark-frontmatter@5.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-frontmatter": "^2.0.0", "micromark-extension-frontmatter": "^2.0.0", "unified": "^11.0.0" } }, "sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ=="],
|
||||
|
||||
"remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="],
|
||||
|
||||
"remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="],
|
||||
|
||||
"remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="],
|
||||
|
||||
"remark-wiki-link": ["remark-wiki-link@2.0.1", "", { "dependencies": { "@babel/runtime": "^7.4.4", "mdast-util-wiki-link": "^0.1.2", "micromark-extension-wiki-link": "^0.0.4" } }, "sha512-F8Eut1E7GWfFm4ZDTI6/4ejeZEHZgnVk6E933Yqd/ssYsc4AyI32aGakxwsGcEzbbE7dkWi1EfLlGAdGgOZOsA=="],
|
||||
|
||||
"repeat-string": ["repeat-string@1.6.1", "", {}, "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w=="],
|
||||
|
||||
"sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="],
|
||||
|
||||
"strnum": ["strnum@2.4.1", "", { "dependencies": { "anynum": "^1.0.1" } }, "sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg=="],
|
||||
@@ -206,5 +268,17 @@
|
||||
"yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
|
||||
|
||||
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
||||
|
||||
"mdast-util-wiki-link/mdast-util-to-markdown": ["mdast-util-to-markdown@0.6.5", "", { "dependencies": { "@types/unist": "^2.0.0", "longest-streak": "^2.0.0", "mdast-util-to-string": "^2.0.0", "parse-entities": "^2.0.0", "repeat-string": "^1.0.0", "zwitch": "^1.0.0" } }, "sha512-XeV9sDE7ZlOQvs45C9UKMtfTcctcaj/pGwH8YLbMHoMOXNNCn2LsqVQOqrF1+/NU8lKDAqozme9SCXWyo9oAcQ=="],
|
||||
|
||||
"parse-entities/character-entities": ["character-entities@1.2.4", "", {}, "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw=="],
|
||||
|
||||
"mdast-util-wiki-link/mdast-util-to-markdown/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
||||
|
||||
"mdast-util-wiki-link/mdast-util-to-markdown/longest-streak": ["longest-streak@2.0.4", "", {}, "sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg=="],
|
||||
|
||||
"mdast-util-wiki-link/mdast-util-to-markdown/mdast-util-to-string": ["mdast-util-to-string@2.0.0", "", {}, "sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w=="],
|
||||
|
||||
"mdast-util-wiki-link/mdast-util-to-markdown/zwitch": ["zwitch@1.0.5", "", {}, "sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw=="],
|
||||
}
|
||||
}
|
||||
|
||||
144
bun.nix
144
bun.nix
@@ -13,6 +13,10 @@
|
||||
...
|
||||
}:
|
||||
{
|
||||
"@babel/runtime@7.29.7" = fetchurl {
|
||||
url = "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz";
|
||||
hash = "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==";
|
||||
};
|
||||
"@biomejs/biome@2.5.3" = fetchurl {
|
||||
url = "https://registry.npmjs.org/@biomejs/biome/-/biome-2.5.3.tgz";
|
||||
hash = "sha512-MrJswFdei9EfDwwUy2tQrPDpK0AO+RmMFvBoaaJ6ayBc3sUbHdCE+XG5N8vp+5So41ZupZJQm0roHFFhMGVD7A==";
|
||||
@@ -75,6 +79,10 @@
|
||||
url = "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz";
|
||||
hash = "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==";
|
||||
};
|
||||
"@types/unist@2.0.11" = fetchurl {
|
||||
url = "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz";
|
||||
hash = "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==";
|
||||
};
|
||||
"@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==";
|
||||
@@ -95,14 +103,30 @@
|
||||
url = "https://registry.npmjs.org/bun2nix/-/bun2nix-2.1.2.tgz";
|
||||
hash = "sha512-0wx6Ar5ccrz4aSD5prbShwymjDEXFh7Bucxs+YrpAMa67TnVB95Hv8FV3oaQEbtOx6QGgIAyOmap6Y3WCRqetg==";
|
||||
};
|
||||
"ccount@2.0.1" = fetchurl {
|
||||
url = "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz";
|
||||
hash = "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==";
|
||||
};
|
||||
"chalk@5.6.2" = fetchurl {
|
||||
url = "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz";
|
||||
hash = "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==";
|
||||
};
|
||||
"character-entities-legacy@1.1.4" = fetchurl {
|
||||
url = "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz";
|
||||
hash = "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==";
|
||||
};
|
||||
"character-entities@1.2.4" = fetchurl {
|
||||
url = "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz";
|
||||
hash = "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==";
|
||||
};
|
||||
"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==";
|
||||
};
|
||||
"character-reference-invalid@1.1.4" = fetchurl {
|
||||
url = "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz";
|
||||
hash = "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==";
|
||||
};
|
||||
"commander@15.0.0" = fetchurl {
|
||||
url = "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz";
|
||||
hash = "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==";
|
||||
@@ -147,6 +171,22 @@
|
||||
url = "https://registry.npmjs.org/format/-/format-0.2.2.tgz";
|
||||
hash = "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==";
|
||||
};
|
||||
"is-alphabetical@1.0.4" = fetchurl {
|
||||
url = "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz";
|
||||
hash = "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==";
|
||||
};
|
||||
"is-alphanumerical@1.0.4" = fetchurl {
|
||||
url = "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz";
|
||||
hash = "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==";
|
||||
};
|
||||
"is-decimal@1.0.4" = fetchurl {
|
||||
url = "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz";
|
||||
hash = "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==";
|
||||
};
|
||||
"is-hexadecimal@1.0.4" = fetchurl {
|
||||
url = "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz";
|
||||
hash = "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==";
|
||||
};
|
||||
"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==";
|
||||
@@ -155,10 +195,22 @@
|
||||
url = "https://registry.npmjs.org/is-unsafe/-/is-unsafe-2.0.0.tgz";
|
||||
hash = "sha512-2LdV822R+wmI86unXA93WCFpL6g+av8ynWk0nrHyJqGop5VoocYsSLFgN8jrfalT6iGeLNM4KXuVSsULP53kEA==";
|
||||
};
|
||||
"longest-streak@2.0.4" = fetchurl {
|
||||
url = "https://registry.npmjs.org/longest-streak/-/longest-streak-2.0.4.tgz";
|
||||
hash = "sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg==";
|
||||
};
|
||||
"longest-streak@3.1.0" = fetchurl {
|
||||
url = "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz";
|
||||
hash = "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==";
|
||||
};
|
||||
"markdown-table@3.0.4" = fetchurl {
|
||||
url = "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz";
|
||||
hash = "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==";
|
||||
};
|
||||
"mdast-util-find-and-replace@3.0.2" = fetchurl {
|
||||
url = "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz";
|
||||
hash = "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==";
|
||||
};
|
||||
"mdast-util-from-markdown@2.0.3" = fetchurl {
|
||||
url = "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz";
|
||||
hash = "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==";
|
||||
@@ -167,18 +219,54 @@
|
||||
url = "https://registry.npmjs.org/mdast-util-frontmatter/-/mdast-util-frontmatter-2.0.1.tgz";
|
||||
hash = "sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA==";
|
||||
};
|
||||
"mdast-util-gfm-autolink-literal@2.0.1" = fetchurl {
|
||||
url = "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz";
|
||||
hash = "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==";
|
||||
};
|
||||
"mdast-util-gfm-footnote@2.1.0" = fetchurl {
|
||||
url = "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz";
|
||||
hash = "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==";
|
||||
};
|
||||
"mdast-util-gfm-strikethrough@2.0.0" = fetchurl {
|
||||
url = "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz";
|
||||
hash = "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==";
|
||||
};
|
||||
"mdast-util-gfm-table@2.0.0" = fetchurl {
|
||||
url = "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz";
|
||||
hash = "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==";
|
||||
};
|
||||
"mdast-util-gfm-task-list-item@2.0.0" = fetchurl {
|
||||
url = "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz";
|
||||
hash = "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==";
|
||||
};
|
||||
"mdast-util-gfm@3.1.0" = fetchurl {
|
||||
url = "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz";
|
||||
hash = "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==";
|
||||
};
|
||||
"mdast-util-phrasing@4.1.0" = fetchurl {
|
||||
url = "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz";
|
||||
hash = "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==";
|
||||
};
|
||||
"mdast-util-to-markdown@0.6.5" = fetchurl {
|
||||
url = "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-0.6.5.tgz";
|
||||
hash = "sha512-XeV9sDE7ZlOQvs45C9UKMtfTcctcaj/pGwH8YLbMHoMOXNNCn2LsqVQOqrF1+/NU8lKDAqozme9SCXWyo9oAcQ==";
|
||||
};
|
||||
"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@2.0.0" = fetchurl {
|
||||
url = "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-2.0.0.tgz";
|
||||
hash = "sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w==";
|
||||
};
|
||||
"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==";
|
||||
};
|
||||
"mdast-util-wiki-link@0.1.2" = fetchurl {
|
||||
url = "https://registry.npmjs.org/mdast-util-wiki-link/-/mdast-util-wiki-link-0.1.2.tgz";
|
||||
hash = "sha512-DTcDyOxKDo3pB3fc0zQlD8myfQjYkW4hazUKI9PUyhtoj9JBeHC2eIdlVXmaT22bZkFAVU2d47B6y2jVKGoUQg==";
|
||||
};
|
||||
"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==";
|
||||
@@ -187,6 +275,38 @@
|
||||
url = "https://registry.npmjs.org/micromark-extension-frontmatter/-/micromark-extension-frontmatter-2.0.0.tgz";
|
||||
hash = "sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg==";
|
||||
};
|
||||
"micromark-extension-gfm-autolink-literal@2.1.0" = fetchurl {
|
||||
url = "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz";
|
||||
hash = "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==";
|
||||
};
|
||||
"micromark-extension-gfm-footnote@2.1.0" = fetchurl {
|
||||
url = "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz";
|
||||
hash = "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==";
|
||||
};
|
||||
"micromark-extension-gfm-strikethrough@2.1.0" = fetchurl {
|
||||
url = "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz";
|
||||
hash = "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==";
|
||||
};
|
||||
"micromark-extension-gfm-table@2.1.1" = fetchurl {
|
||||
url = "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz";
|
||||
hash = "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==";
|
||||
};
|
||||
"micromark-extension-gfm-tagfilter@2.0.0" = fetchurl {
|
||||
url = "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz";
|
||||
hash = "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==";
|
||||
};
|
||||
"micromark-extension-gfm-task-list-item@2.1.0" = fetchurl {
|
||||
url = "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz";
|
||||
hash = "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==";
|
||||
};
|
||||
"micromark-extension-gfm@3.0.0" = fetchurl {
|
||||
url = "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz";
|
||||
hash = "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==";
|
||||
};
|
||||
"micromark-extension-wiki-link@0.0.4" = fetchurl {
|
||||
url = "https://registry.npmjs.org/micromark-extension-wiki-link/-/micromark-extension-wiki-link-0.0.4.tgz";
|
||||
hash = "sha512-dJc8AfnoU8BHkN+7fWZvIS20SMsMS1ZlxQUn6We67MqeKbOiEDZV5eEvCpwqGBijbJbxX3Kxz879L4K9HIiOvw==";
|
||||
};
|
||||
"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==";
|
||||
@@ -279,6 +399,10 @@
|
||||
url = "https://registry.npmjs.org/neverthrow/-/neverthrow-8.2.0.tgz";
|
||||
hash = "sha512-kOCT/1MCPAxY5iUV3wytNFUMUolzuwd/VF/1KCx7kf6CutrOsTie+84zTGTpgQycjvfLdBBdvBvFLqFD2c0wkQ==";
|
||||
};
|
||||
"parse-entities@2.0.0" = fetchurl {
|
||||
url = "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz";
|
||||
hash = "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==";
|
||||
};
|
||||
"path-expression-matcher@1.6.2" = fetchurl {
|
||||
url = "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.6.2.tgz";
|
||||
hash = "sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==";
|
||||
@@ -287,10 +411,26 @@
|
||||
url = "https://registry.npmjs.org/remark-frontmatter/-/remark-frontmatter-5.0.0.tgz";
|
||||
hash = "sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ==";
|
||||
};
|
||||
"remark-gfm@4.0.1" = fetchurl {
|
||||
url = "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz";
|
||||
hash = "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==";
|
||||
};
|
||||
"remark-parse@11.0.0" = fetchurl {
|
||||
url = "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz";
|
||||
hash = "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==";
|
||||
};
|
||||
"remark-stringify@11.0.0" = fetchurl {
|
||||
url = "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz";
|
||||
hash = "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==";
|
||||
};
|
||||
"remark-wiki-link@2.0.1" = fetchurl {
|
||||
url = "https://registry.npmjs.org/remark-wiki-link/-/remark-wiki-link-2.0.1.tgz";
|
||||
hash = "sha512-F8Eut1E7GWfFm4ZDTI6/4ejeZEHZgnVk6E933Yqd/ssYsc4AyI32aGakxwsGcEzbbE7dkWi1EfLlGAdGgOZOsA==";
|
||||
};
|
||||
"repeat-string@1.6.1" = fetchurl {
|
||||
url = "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz";
|
||||
hash = "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==";
|
||||
};
|
||||
"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==";
|
||||
@@ -343,6 +483,10 @@
|
||||
url = "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz";
|
||||
hash = "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==";
|
||||
};
|
||||
"zwitch@1.0.5" = fetchurl {
|
||||
url = "https://registry.npmjs.org/zwitch/-/zwitch-1.0.5.tgz";
|
||||
hash = "sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==";
|
||||
};
|
||||
"zwitch@2.0.4" = fetchurl {
|
||||
url = "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz";
|
||||
hash = "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==";
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
import type { Block, DocumentAST, Frontmatter, HeadingBlock, InlineNode } from "@kitchen-md/core";
|
||||
import type {
|
||||
Block,
|
||||
BlockquoteBlock,
|
||||
DocumentAST,
|
||||
Frontmatter,
|
||||
HeadingBlock,
|
||||
InlineNode,
|
||||
ListBlock,
|
||||
ListItemBlock,
|
||||
TransclusionNode,
|
||||
} from "@kitchen-md/core";
|
||||
import chalk from "chalk";
|
||||
import { stringify as stringifyYaml } from "yaml";
|
||||
|
||||
const SEPARATOR = "─".repeat(40);
|
||||
const RULE = "─".repeat(40);
|
||||
|
||||
export function render(ast: DocumentAST): string {
|
||||
const body = ast.blocks.map(renderBlock).join("");
|
||||
@@ -13,14 +23,26 @@ function renderFrontmatter(frontmatter: Frontmatter): string {
|
||||
if (Object.keys(frontmatter).length === 0) {
|
||||
return "";
|
||||
}
|
||||
return `${stringifyYaml(frontmatter)}${chalk.dim(SEPARATOR)}\n`;
|
||||
return `${stringifyYaml(frontmatter)}${chalk.dim(RULE)}\n`;
|
||||
}
|
||||
|
||||
function renderBlock(block: Block): string {
|
||||
if (block.type === "heading") {
|
||||
return `${styleHeading(block.level)(renderInline(block.children))}\n`;
|
||||
switch (block.type) {
|
||||
case "heading":
|
||||
return `${styleHeading(block.level)(renderInline(block.children))}\n`;
|
||||
case "paragraph":
|
||||
return `${renderInline(block.children)}\n\n`;
|
||||
case "list":
|
||||
return renderList(block);
|
||||
case "blockquote":
|
||||
return renderBlockquote(block);
|
||||
case "code":
|
||||
return `${chalk.dim(block.value)}\n\n`;
|
||||
case "thematicBreak":
|
||||
return `${chalk.dim(RULE)}\n\n`;
|
||||
case "raw":
|
||||
return `${block.value}\n\n`;
|
||||
}
|
||||
return `${renderInline(block.children)}\n\n`;
|
||||
}
|
||||
|
||||
// Each level gets a distinct style, tapering from bold at level 1 toward dim at level 6.
|
||||
@@ -41,6 +63,58 @@ function styleHeading(level: HeadingBlock["level"]): (text: string) => string {
|
||||
}
|
||||
}
|
||||
|
||||
function renderInline(nodes: InlineNode[]): string {
|
||||
return nodes.map((node) => node.value).join("");
|
||||
function renderList(block: ListBlock): string {
|
||||
const lines = block.items.map((item, index) => {
|
||||
const marker = block.ordered ? `${index + 1}. ` : "• ";
|
||||
return marker + renderItemContent(item);
|
||||
});
|
||||
return `${lines.join("\n")}\n\n`;
|
||||
}
|
||||
|
||||
// The item's child blocks, collapsed onto the marker line without their own
|
||||
// trailing block spacing.
|
||||
function renderItemContent(item: ListItemBlock): string {
|
||||
return item.children.map(renderBlock).join("").trimEnd();
|
||||
}
|
||||
|
||||
function renderBlockquote(block: BlockquoteBlock): string {
|
||||
const inner = block.children.map(renderBlock).join("").trimEnd();
|
||||
const quoted = inner
|
||||
.split("\n")
|
||||
.map((line) => chalk.dim("│ ") + line)
|
||||
.join("\n");
|
||||
return `${quoted}\n\n`;
|
||||
}
|
||||
|
||||
function renderInline(nodes: InlineNode[]): string {
|
||||
return nodes.map(renderInlineNode).join("");
|
||||
}
|
||||
|
||||
function renderInlineNode(node: InlineNode): string {
|
||||
switch (node.type) {
|
||||
case "text":
|
||||
return node.value;
|
||||
case "emphasis":
|
||||
return chalk.italic(renderInline(node.children));
|
||||
case "strong":
|
||||
return chalk.bold(renderInline(node.children));
|
||||
case "codeSpan":
|
||||
return chalk.inverse(node.value);
|
||||
case "link":
|
||||
return chalk.underline(renderInline(node.children));
|
||||
case "wikilink":
|
||||
return chalk.underline(node.display ?? node.target);
|
||||
case "transclusion":
|
||||
return renderTransclusion(node);
|
||||
case "rawInline":
|
||||
return node.value;
|
||||
}
|
||||
}
|
||||
|
||||
// A transclusion shows as its raw source text.
|
||||
// Resolving the embed is out of scope.
|
||||
function renderTransclusion(node: TransclusionNode): string {
|
||||
const anchor = node.anchor !== undefined ? `#${node.anchor}` : "";
|
||||
const display = node.display !== undefined ? `|${node.display}` : "";
|
||||
return `![[${node.target}${anchor}${display}]]`;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import type { DocumentAST, HeadingBlock } from "@kitchen-md/core";
|
||||
import chalk from "chalk";
|
||||
import { render } from "./render.ts";
|
||||
|
||||
describe("render", () => {
|
||||
@@ -96,4 +97,188 @@ describe("render", () => {
|
||||
expect(Bun.stripANSI(render(ast))).toBe(`${title}\n`);
|
||||
}
|
||||
});
|
||||
|
||||
const bodyOf = (block: DocumentAST["blocks"][number]): string =>
|
||||
Bun.stripANSI(render({ frontmatter: {}, blocks: [block], diagnostics: [] }));
|
||||
|
||||
test("an unordered list renders one bulleted item per line", () => {
|
||||
const stripped = bodyOf({
|
||||
type: "list",
|
||||
ordered: false,
|
||||
items: [
|
||||
{
|
||||
type: "listItem",
|
||||
children: [{ type: "paragraph", children: [{ type: "text", value: "Flour" }] }],
|
||||
},
|
||||
{
|
||||
type: "listItem",
|
||||
children: [{ type: "paragraph", children: [{ type: "text", value: "Sugar" }] }],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(stripped).toBe("• Flour\n• Sugar\n\n");
|
||||
});
|
||||
|
||||
test("an ordered list renders sequential numbers, one item per line", () => {
|
||||
const stripped = bodyOf({
|
||||
type: "list",
|
||||
ordered: true,
|
||||
items: [
|
||||
{
|
||||
type: "listItem",
|
||||
children: [{ type: "paragraph", children: [{ type: "text", value: "Mix" }] }],
|
||||
},
|
||||
{
|
||||
type: "listItem",
|
||||
children: [{ type: "paragraph", children: [{ type: "text", value: "Bake" }] }],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(stripped).toBe("1. Mix\n2. Bake\n\n");
|
||||
});
|
||||
|
||||
test("a code block renders its literal text with no highlighting", () => {
|
||||
const stripped = bodyOf({ type: "code", lang: "js", value: "const x = 1;" });
|
||||
|
||||
expect(stripped).toContain("const x = 1;");
|
||||
});
|
||||
|
||||
test("a thematic break renders a horizontal rule", () => {
|
||||
const stripped = bodyOf({ type: "thematicBreak" });
|
||||
|
||||
expect(stripped).toContain("──────");
|
||||
});
|
||||
|
||||
test("a blockquote renders its inner text", () => {
|
||||
const stripped = bodyOf({
|
||||
type: "blockquote",
|
||||
children: [{ type: "paragraph", children: [{ type: "text", value: "Take care." }] }],
|
||||
});
|
||||
|
||||
expect(stripped).toContain("Take care.");
|
||||
});
|
||||
|
||||
test("a raw block renders its verbatim value", () => {
|
||||
const table = "| a | b |\n| - | - |";
|
||||
const stripped = bodyOf({ type: "raw", value: table });
|
||||
|
||||
expect(stripped).toContain(table);
|
||||
});
|
||||
|
||||
test("emphasis, strong, code span, and links render their inline content", () => {
|
||||
const stripped = bodyOf({
|
||||
type: "paragraph",
|
||||
children: [
|
||||
{ type: "emphasis", children: [{ type: "text", value: "soft" }] },
|
||||
{ type: "text", value: " " },
|
||||
{ type: "strong", children: [{ type: "text", value: "hard" }] },
|
||||
{ type: "text", value: " " },
|
||||
{ type: "codeSpan", value: "code" },
|
||||
{ type: "text", value: " " },
|
||||
{ type: "link", href: "https://example.com", children: [{ type: "text", value: "docs" }] },
|
||||
],
|
||||
});
|
||||
|
||||
expect(stripped).toBe("soft hard code docs\n\n");
|
||||
expect(stripped).not.toContain("https://example.com");
|
||||
});
|
||||
|
||||
test("emphasis, strong, and code span each carry styling distinct from plain text", () => {
|
||||
const previousLevel = chalk.level;
|
||||
chalk.level = 1;
|
||||
try {
|
||||
const styled = (block: DocumentAST["blocks"][number]): string =>
|
||||
render({ frontmatter: {}, blocks: [block], diagnostics: [] });
|
||||
|
||||
const emphasis = styled({
|
||||
type: "paragraph",
|
||||
children: [{ type: "emphasis", children: [{ type: "text", value: "x" }] }],
|
||||
});
|
||||
const strong = styled({
|
||||
type: "paragraph",
|
||||
children: [{ type: "strong", children: [{ type: "text", value: "x" }] }],
|
||||
});
|
||||
const codeSpan = styled({ type: "paragraph", children: [{ type: "codeSpan", value: "x" }] });
|
||||
|
||||
// Each carries ANSI styling, so the raw string differs from the stripped one.
|
||||
expect(emphasis).not.toBe(Bun.stripANSI(emphasis));
|
||||
expect(strong).not.toBe(Bun.stripANSI(strong));
|
||||
expect(codeSpan).not.toBe(Bun.stripANSI(codeSpan));
|
||||
|
||||
// The three styles are mutually distinct.
|
||||
expect(emphasis).not.toBe(strong);
|
||||
expect(strong).not.toBe(codeSpan);
|
||||
expect(emphasis).not.toBe(codeSpan);
|
||||
} finally {
|
||||
chalk.level = previousLevel;
|
||||
}
|
||||
});
|
||||
|
||||
test("a wikilink renders its display text, or the target when there is none", () => {
|
||||
const withDisplay = bodyOf({
|
||||
type: "paragraph",
|
||||
children: [{ type: "wikilink", target: "basic-brine", anchor: "step", display: "the brine" }],
|
||||
});
|
||||
const bare = bodyOf({
|
||||
type: "paragraph",
|
||||
children: [{ type: "wikilink", target: "Maple Syrup" }],
|
||||
});
|
||||
|
||||
expect(withDisplay).toBe("the brine\n\n");
|
||||
expect(bare).toBe("Maple Syrup\n\n");
|
||||
});
|
||||
|
||||
test("a wikilink carries styling distinct from plain text", () => {
|
||||
const previousLevel = chalk.level;
|
||||
chalk.level = 1;
|
||||
try {
|
||||
const styled = render({
|
||||
frontmatter: {},
|
||||
blocks: [{ type: "paragraph", children: [{ type: "wikilink", target: "x" }] }],
|
||||
diagnostics: [],
|
||||
});
|
||||
expect(styled).not.toBe(Bun.stripANSI(styled));
|
||||
} finally {
|
||||
chalk.level = previousLevel;
|
||||
}
|
||||
});
|
||||
|
||||
test("a transclusion renders as its raw source text", () => {
|
||||
const sectioned = bodyOf({
|
||||
type: "paragraph",
|
||||
children: [{ type: "transclusion", target: "italian meatballs", anchor: "rolling:2" }],
|
||||
});
|
||||
const headingless = bodyOf({
|
||||
type: "paragraph",
|
||||
children: [{ type: "transclusion", target: "basic brine", anchor: "3" }],
|
||||
});
|
||||
const aliased = bodyOf({
|
||||
type: "paragraph",
|
||||
children: [{ type: "transclusion", target: "recipe", display: "As shown" }],
|
||||
});
|
||||
const bare = bodyOf({
|
||||
type: "paragraph",
|
||||
children: [{ type: "transclusion", target: "maple syrup" }],
|
||||
});
|
||||
|
||||
expect(sectioned).toBe("![[italian meatballs#rolling:2]]\n\n");
|
||||
expect(headingless).toBe("![[basic brine#3]]\n\n");
|
||||
expect(aliased).toBe("![[recipe|As shown]]\n\n");
|
||||
expect(bare).toBe("![[maple syrup]]\n\n");
|
||||
});
|
||||
|
||||
test("a rawInline node renders its verbatim value", () => {
|
||||
const stripped = bodyOf({
|
||||
type: "paragraph",
|
||||
children: [
|
||||
{ type: "text", value: "a " },
|
||||
{ type: "rawInline", value: "~~b~~" },
|
||||
{ type: "text", value: " c" },
|
||||
],
|
||||
});
|
||||
|
||||
expect(stripped).toBe("a ~~b~~ c\n\n");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,7 +11,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"remark-frontmatter": "^5.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"remark-parse": "^11.0.0",
|
||||
"remark-wiki-link": "^2.0.1",
|
||||
"unified": "^11.0.5",
|
||||
"yaml": "^2.9.0"
|
||||
}
|
||||
|
||||
@@ -1,16 +1,48 @@
|
||||
import type { PhrasingContent, Root, RootContent } from "mdast";
|
||||
import type { ListItem, Node, PhrasingContent, Root, RootContent } from "mdast";
|
||||
import remarkFrontmatter from "remark-frontmatter";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import remarkParse from "remark-parse";
|
||||
import remarkWikiLink from "remark-wiki-link";
|
||||
import { unified } from "unified";
|
||||
import { parse as parseYaml } from "yaml";
|
||||
import type { Block, DocumentAST, Frontmatter, InlineNode } from "./types.ts";
|
||||
import type {
|
||||
Block,
|
||||
DocumentAST,
|
||||
Frontmatter,
|
||||
InlineNode,
|
||||
ListItemBlock,
|
||||
TransclusionNode,
|
||||
WikilinkNode,
|
||||
} from "./types.ts";
|
||||
|
||||
const processor = unified().use(remarkParse).use(remarkFrontmatter);
|
||||
// `|` is the Obsidian alias divider.
|
||||
// The plugin defaults to `:`, which would swallow anchors like `#rolling:2`.
|
||||
const processor = unified()
|
||||
.use(remarkParse)
|
||||
.use(remarkFrontmatter)
|
||||
.use(remarkGfm)
|
||||
.use(remarkWikiLink, { aliasDivider: "|" });
|
||||
|
||||
// The mdast node remark-wiki-link injects for `[[…]]`.
|
||||
// Its `value` is the target with any `#anchor` still attached.
|
||||
// `data.alias` is the display text, and equals `value` when no alias was written.
|
||||
interface WikiLinkMdast {
|
||||
type: "wikiLink";
|
||||
value: string;
|
||||
data?: { alias?: string };
|
||||
}
|
||||
|
||||
type InlineMdast = PhrasingContent | WikiLinkMdast;
|
||||
|
||||
// Transclusions (`![[…]]`) are not matched by remark-wiki-link — the leading `!`
|
||||
// makes remark treat the brackets as a failed image, leaving the whole span as
|
||||
// literal text — so they are recovered by scanning text with this pattern.
|
||||
const TRANSCLUSION = /!\[\[([^[\]]+)]]/g;
|
||||
|
||||
export function parse(input: string): DocumentAST {
|
||||
const tree = processor.parse(input);
|
||||
const frontmatter = extractFrontmatter(tree);
|
||||
const blocks = tree.children.flatMap(translateBlock);
|
||||
const blocks = tree.children.flatMap((node) => translateBlock(node, input));
|
||||
return { frontmatter, blocks, diagnostics: [] };
|
||||
}
|
||||
|
||||
@@ -26,21 +58,126 @@ function extractFrontmatter(tree: Root): Frontmatter {
|
||||
return {};
|
||||
}
|
||||
|
||||
function translateBlock(node: RootContent): Block[] {
|
||||
if (node.type === "heading") {
|
||||
return [{ type: "heading", level: node.depth, children: translateInline(node.children) }];
|
||||
function translateBlock(node: RootContent, input: string): Block[] {
|
||||
switch (node.type) {
|
||||
case "heading":
|
||||
return [
|
||||
{ type: "heading", level: node.depth, children: translateInline(node.children, input) },
|
||||
];
|
||||
case "paragraph":
|
||||
return [{ type: "paragraph", children: translateInline(node.children, input) }];
|
||||
case "list":
|
||||
return [
|
||||
{
|
||||
type: "list",
|
||||
ordered: node.ordered ?? false,
|
||||
items: node.children.map((item) => translateListItem(item, input)),
|
||||
},
|
||||
];
|
||||
case "blockquote":
|
||||
return [
|
||||
{
|
||||
type: "blockquote",
|
||||
children: node.children.flatMap((child) => translateBlock(child, input)),
|
||||
},
|
||||
];
|
||||
case "code":
|
||||
return [{ type: "code", ...(node.lang ? { lang: node.lang } : {}), value: node.value }];
|
||||
case "thematicBreak":
|
||||
return [{ type: "thematicBreak" }];
|
||||
// Frontmatter is captured separately and must not double as a block.
|
||||
case "yaml":
|
||||
return [];
|
||||
default:
|
||||
return [{ type: "raw", value: slice(node, input) }];
|
||||
}
|
||||
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 }];
|
||||
function translateListItem(item: ListItem, input: string): ListItemBlock {
|
||||
return {
|
||||
type: "listItem",
|
||||
children: item.children.flatMap((child) => translateBlock(child, input)),
|
||||
};
|
||||
}
|
||||
|
||||
function translateInline(nodes: InlineMdast[], input: string): InlineNode[] {
|
||||
return nodes.flatMap((node): InlineNode[] => {
|
||||
switch (node.type) {
|
||||
case "text":
|
||||
return splitTransclusions(node.value);
|
||||
case "emphasis":
|
||||
return [{ type: "emphasis", children: translateInline(node.children, input) }];
|
||||
case "strong":
|
||||
return [{ type: "strong", children: translateInline(node.children, input) }];
|
||||
case "inlineCode":
|
||||
return [{ type: "codeSpan", value: node.value }];
|
||||
case "link":
|
||||
return [{ type: "link", href: node.url, children: translateInline(node.children, input) }];
|
||||
case "wikiLink":
|
||||
return [translateWikilink(node)];
|
||||
default:
|
||||
return [{ type: "rawInline", value: slice(node, input) }];
|
||||
}
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
function translateWikilink(node: WikiLinkMdast): WikilinkNode {
|
||||
const display =
|
||||
node.data?.alias !== undefined && node.data.alias !== node.value ? node.data.alias : undefined;
|
||||
return {
|
||||
type: "wikilink",
|
||||
...splitAnchor(node.value),
|
||||
...(display !== undefined ? { display } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
// Split a text run into plain text and the transclusions embedded in it, preserving order.
|
||||
// A run with no transclusion yields a single text node.
|
||||
function splitTransclusions(value: string): InlineNode[] {
|
||||
const out: InlineNode[] = [];
|
||||
let cursor = 0;
|
||||
for (const match of value.matchAll(TRANSCLUSION)) {
|
||||
const at = match.index;
|
||||
if (at > cursor) {
|
||||
out.push({ type: "text", value: value.slice(cursor, at) });
|
||||
}
|
||||
out.push(buildTransclusion(match[1]));
|
||||
cursor = at + match[0].length;
|
||||
}
|
||||
if (out.length === 0 || cursor < value.length) {
|
||||
out.push({ type: "text", value: value.slice(cursor) });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function buildTransclusion(inner: string): TransclusionNode {
|
||||
const pipe = inner.indexOf("|");
|
||||
const display = pipe === -1 ? undefined : inner.slice(pipe + 1);
|
||||
const targetPart = pipe === -1 ? inner : inner.slice(0, pipe);
|
||||
return {
|
||||
type: "transclusion",
|
||||
...splitAnchor(targetPart),
|
||||
...(display !== undefined ? { display } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
// Split `target#anchor` at the first `#`.
|
||||
// The anchor is passed through verbatim, and is omitted entirely when absent.
|
||||
function splitAnchor(value: string): { target: string; anchor?: string } {
|
||||
const hash = value.indexOf("#");
|
||||
if (hash === -1) {
|
||||
return { target: value };
|
||||
}
|
||||
return { target: value.slice(0, hash), anchor: value.slice(hash + 1) };
|
||||
}
|
||||
|
||||
// Verbatim source for an unmodelled node, taken by position so it round-trips
|
||||
// byte-for-byte rather than being re-stringified through remark.
|
||||
function slice(node: Node, input: string): string {
|
||||
const start = node.position?.start.offset;
|
||||
const end = node.position?.end.offset;
|
||||
if (start === undefined || end === undefined) {
|
||||
return "";
|
||||
}
|
||||
return input.slice(start, end);
|
||||
}
|
||||
|
||||
@@ -99,4 +99,251 @@ A paragraph under it.
|
||||
|
||||
expect(result.diagnostics).toEqual([]);
|
||||
});
|
||||
|
||||
test("an unordered list is a ListBlock whose items wrap their child blocks", () => {
|
||||
const result = parse("- First\n- Second\n");
|
||||
|
||||
expect(result.blocks).toEqual([
|
||||
{
|
||||
type: "list",
|
||||
ordered: false,
|
||||
items: [
|
||||
{
|
||||
type: "listItem",
|
||||
children: [{ type: "paragraph", children: [{ type: "text", value: "First" }] }],
|
||||
},
|
||||
{
|
||||
type: "listItem",
|
||||
children: [{ type: "paragraph", children: [{ type: "text", value: "Second" }] }],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("an ordered list carries the ordered flag", () => {
|
||||
const result = parse("1. One\n2. Two\n");
|
||||
|
||||
expect(result.blocks).toEqual([
|
||||
{
|
||||
type: "list",
|
||||
ordered: true,
|
||||
items: [
|
||||
{
|
||||
type: "listItem",
|
||||
children: [{ type: "paragraph", children: [{ type: "text", value: "One" }] }],
|
||||
},
|
||||
{
|
||||
type: "listItem",
|
||||
children: [{ type: "paragraph", children: [{ type: "text", value: "Two" }] }],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("a list item is a container of blocks, not a bare inline array", () => {
|
||||
const result = parse("- Just text\n");
|
||||
const list = result.blocks[0];
|
||||
|
||||
if (list?.type !== "list") throw new Error("expected a list block");
|
||||
expect(list.items[0]).toEqual({
|
||||
type: "listItem",
|
||||
children: [{ type: "paragraph", children: [{ type: "text", value: "Just text" }] }],
|
||||
});
|
||||
});
|
||||
|
||||
test("a blockquote is a container holding blocks", () => {
|
||||
const result = parse("> Quoted prose.\n");
|
||||
|
||||
expect(result.blocks).toEqual([
|
||||
{
|
||||
type: "blockquote",
|
||||
children: [{ type: "paragraph", children: [{ type: "text", value: "Quoted prose." }] }],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("an OFM callout parses as an ordinary blockquote with its text preserved", () => {
|
||||
const result = parse("> [!note]\n> Remember this.\n");
|
||||
const quote = result.blocks[0];
|
||||
|
||||
if (quote?.type !== "blockquote") throw new Error("expected a blockquote block");
|
||||
const rendered = JSON.stringify(quote);
|
||||
expect(rendered).toContain("[!note]");
|
||||
expect(rendered).toContain("Remember this.");
|
||||
});
|
||||
|
||||
test("a fenced code block carries its language and literal text", () => {
|
||||
const result = parse("```js\nconst x = @sugar{1};\n```\n");
|
||||
|
||||
expect(result.blocks).toEqual([{ type: "code", lang: "js", value: "const x = @sugar{1};" }]);
|
||||
});
|
||||
|
||||
test("a code block without a language has no lang", () => {
|
||||
const result = parse("```\nplain text\n```\n");
|
||||
|
||||
expect(result.blocks).toEqual([{ type: "code", value: "plain text" }]);
|
||||
});
|
||||
|
||||
test("a thematic break is modelled", () => {
|
||||
const result = parse("---\n");
|
||||
|
||||
expect(result.blocks).toEqual([{ type: "thematicBreak" }]);
|
||||
});
|
||||
|
||||
test("emphasis, strong, and code-span inlines are modelled", () => {
|
||||
const result = parse("*em* **strong** `code`\n");
|
||||
const para = result.blocks[0];
|
||||
|
||||
if (para?.type !== "paragraph") throw new Error("expected a paragraph block");
|
||||
expect(para.children).toEqual([
|
||||
{ type: "emphasis", children: [{ type: "text", value: "em" }] },
|
||||
{ type: "text", value: " " },
|
||||
{ type: "strong", children: [{ type: "text", value: "strong" }] },
|
||||
{ type: "text", value: " " },
|
||||
{ type: "codeSpan", value: "code" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("a link is modelled with its href and inline children, and the href is not annotation-parsed here", () => {
|
||||
const result = parse("[the docs](https://example.com/@stuff)\n");
|
||||
const para = result.blocks[0];
|
||||
|
||||
if (para?.type !== "paragraph") throw new Error("expected a paragraph block");
|
||||
expect(para.children).toEqual([
|
||||
{
|
||||
type: "link",
|
||||
href: "https://example.com/@stuff",
|
||||
children: [{ type: "text", value: "the docs" }],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("a code span preserves its raw content verbatim", () => {
|
||||
const result = parse("Use `@sugar{1 tbsp}` literally.\n");
|
||||
const para = result.blocks[0];
|
||||
|
||||
if (para?.type !== "paragraph") throw new Error("expected a paragraph block");
|
||||
expect(para.children).toContainEqual({ type: "codeSpan", value: "@sugar{1 tbsp}" });
|
||||
});
|
||||
|
||||
test("an unmodelled block (a GFM table) falls through to a RawBlock with byte-for-byte source", () => {
|
||||
const table = "| a | b |\n| - | - |\n| 1 | 2 |";
|
||||
const result = parse(`${table}\n`);
|
||||
|
||||
expect(result.blocks).toEqual([{ type: "raw", value: table }]);
|
||||
});
|
||||
|
||||
test("an unmodelled inline (GFM strikethrough) falls through to a RawInline with byte-for-byte source", () => {
|
||||
const result = parse("done ~~scratch~~ now\n");
|
||||
const para = result.blocks[0];
|
||||
|
||||
if (para?.type !== "paragraph") throw new Error("expected a paragraph block");
|
||||
expect(para.children).toEqual([
|
||||
{ type: "text", value: "done " },
|
||||
{ type: "rawInline", value: "~~scratch~~" },
|
||||
{ type: "text", value: " now" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("a bare wikilink carries only its target", () => {
|
||||
const result = parse("See [[Basic Brine]] first.\n");
|
||||
const para = result.blocks[0];
|
||||
|
||||
if (para?.type !== "paragraph") throw new Error("expected a paragraph block");
|
||||
expect(para.children).toContainEqual({ type: "wikilink", target: "Basic Brine" });
|
||||
});
|
||||
|
||||
test("a wikilink anchor is passed through verbatim", () => {
|
||||
const result = parse("[[recipe#the section]]\n");
|
||||
const para = result.blocks[0];
|
||||
|
||||
if (para?.type !== "paragraph") throw new Error("expected a paragraph block");
|
||||
expect(para.children).toEqual([{ type: "wikilink", target: "recipe", anchor: "the section" }]);
|
||||
});
|
||||
|
||||
test("a wikilink display alias is captured after the pipe", () => {
|
||||
const result = parse("[[recipe#anchor|Read this]]\n");
|
||||
const para = result.blocks[0];
|
||||
|
||||
if (para?.type !== "paragraph") throw new Error("expected a paragraph block");
|
||||
expect(para.children).toEqual([
|
||||
{ type: "wikilink", target: "recipe", anchor: "anchor", display: "Read this" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("a wikilink and a transclusion are distinct node types sharing one shape", () => {
|
||||
const result = parse("[[recipe]] versus ![[recipe]]\n");
|
||||
const para = result.blocks[0];
|
||||
|
||||
if (para?.type !== "paragraph") throw new Error("expected a paragraph block");
|
||||
expect(para.children).toEqual([
|
||||
{ type: "wikilink", target: "recipe" },
|
||||
{ type: "text", value: " versus " },
|
||||
{ type: "transclusion", target: "recipe" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("a transclusion display alias is captured after the pipe", () => {
|
||||
const result = parse("![[recipe|As shown]]\n");
|
||||
const para = result.blocks[0];
|
||||
|
||||
if (para?.type !== "paragraph") throw new Error("expected a paragraph block");
|
||||
expect(para.children).toContainEqual({
|
||||
type: "transclusion",
|
||||
target: "recipe",
|
||||
display: "As shown",
|
||||
});
|
||||
});
|
||||
|
||||
test("a Step Reference transclusion passes its section anchor through as-is", () => {
|
||||
const result = parse("![[italian meatballs#rolling:2]]\n");
|
||||
const para = result.blocks[0];
|
||||
|
||||
if (para?.type !== "paragraph") throw new Error("expected a paragraph block");
|
||||
expect(para.children).toContainEqual({
|
||||
type: "transclusion",
|
||||
target: "italian meatballs",
|
||||
anchor: "rolling:2",
|
||||
});
|
||||
});
|
||||
|
||||
test("a headingless Step Reference transclusion passes its bare step anchor through as-is", () => {
|
||||
const result = parse("![[basic brine#3]]\n");
|
||||
const para = result.blocks[0];
|
||||
|
||||
if (para?.type !== "paragraph") throw new Error("expected a paragraph block");
|
||||
expect(para.children).toContainEqual({
|
||||
type: "transclusion",
|
||||
target: "basic brine",
|
||||
anchor: "3",
|
||||
});
|
||||
});
|
||||
|
||||
test("a transclusion is extracted from surrounding prose", () => {
|
||||
const result = parse("Finish with ![[maple syrup#2]] on top.\n");
|
||||
const para = result.blocks[0];
|
||||
|
||||
if (para?.type !== "paragraph") throw new Error("expected a paragraph block");
|
||||
expect(para.children).toEqual([
|
||||
{ type: "text", value: "Finish with " },
|
||||
{ type: "transclusion", target: "maple syrup", anchor: "2" },
|
||||
{ type: "text", value: " on top." },
|
||||
]);
|
||||
});
|
||||
|
||||
test("frontmatter is not emitted as a block", () => {
|
||||
const input = `---
|
||||
title: X
|
||||
---
|
||||
|
||||
Body.
|
||||
`;
|
||||
const result = parse(input);
|
||||
|
||||
expect(result.blocks).toEqual([
|
||||
{ type: "paragraph", children: [{ type: "text", value: "Body." }] },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,7 +13,63 @@ export interface TextNode {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export type InlineNode = TextNode;
|
||||
export interface EmphasisNode {
|
||||
type: "emphasis";
|
||||
children: InlineNode[];
|
||||
}
|
||||
|
||||
export interface StrongNode {
|
||||
type: "strong";
|
||||
children: InlineNode[];
|
||||
}
|
||||
|
||||
export interface CodeSpanNode {
|
||||
type: "codeSpan";
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface LinkNode {
|
||||
type: "link";
|
||||
href: string;
|
||||
children: InlineNode[];
|
||||
}
|
||||
|
||||
// An Obsidian reference `[[target#anchor|display]]`: target is the filename
|
||||
// without extension, anchor is the part after `#` verbatim, display the alias after `|`.
|
||||
// The node type — not a flag — distinguishes it from a transclusion.
|
||||
export interface WikilinkNode {
|
||||
type: "wikilink";
|
||||
target: string;
|
||||
anchor?: string;
|
||||
display?: string;
|
||||
}
|
||||
|
||||
// An Obsidian embed `![[target#anchor|display]]`, covering step references like
|
||||
// `![[file#section:N]]` and `![[file#N]]` whose anchor is passed through as-is.
|
||||
// It shares WikilinkNode's shape, so the type is what tells the two apart.
|
||||
export interface TransclusionNode {
|
||||
type: "transclusion";
|
||||
target: string;
|
||||
anchor?: string;
|
||||
display?: string;
|
||||
}
|
||||
|
||||
// Verbatim source for any inline construct core does not model, sliced from the
|
||||
// original input so the unmodelled span round-trips byte-for-byte.
|
||||
export interface RawInline {
|
||||
type: "rawInline";
|
||||
value: string;
|
||||
}
|
||||
|
||||
export type InlineNode =
|
||||
| TextNode
|
||||
| EmphasisNode
|
||||
| StrongNode
|
||||
| CodeSpanNode
|
||||
| LinkNode
|
||||
| WikilinkNode
|
||||
| TransclusionNode
|
||||
| RawInline;
|
||||
|
||||
export interface HeadingBlock {
|
||||
type: "heading";
|
||||
@@ -26,7 +82,47 @@ export interface ParagraphBlock {
|
||||
children: InlineNode[];
|
||||
}
|
||||
|
||||
export type Block = HeadingBlock | ParagraphBlock;
|
||||
export interface ListItemBlock {
|
||||
type: "listItem";
|
||||
children: Block[];
|
||||
}
|
||||
|
||||
export interface ListBlock {
|
||||
type: "list";
|
||||
ordered: boolean;
|
||||
items: ListItemBlock[];
|
||||
}
|
||||
|
||||
export interface BlockquoteBlock {
|
||||
type: "blockquote";
|
||||
children: Block[];
|
||||
}
|
||||
|
||||
export interface CodeBlock {
|
||||
type: "code";
|
||||
lang?: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface ThematicBreakBlock {
|
||||
type: "thematicBreak";
|
||||
}
|
||||
|
||||
// Verbatim source for any block construct core does not model, sliced from the
|
||||
// original input so the unmodelled block round-trips byte-for-byte.
|
||||
export interface RawBlock {
|
||||
type: "raw";
|
||||
value: string;
|
||||
}
|
||||
|
||||
export type Block =
|
||||
| HeadingBlock
|
||||
| ParagraphBlock
|
||||
| ListBlock
|
||||
| BlockquoteBlock
|
||||
| CodeBlock
|
||||
| ThematicBreakBlock
|
||||
| RawBlock;
|
||||
|
||||
export interface DocumentAST {
|
||||
frontmatter: Frontmatter;
|
||||
|
||||
Reference in New Issue
Block a user