Extend the core parser and the CLI renderer to cover the everyday Markdown body beyond headings and paragraphs: lists, blockquotes, fenced code, thematic breaks, and inline emphasis, strong, code spans, and links. Add remark-gfm to the pipeline so tables and strikethrough parse as their own nodes. Any block or inline node core does not model falls through to RawBlock / RawInline, whose verbatim value is position-sliced from the original input so unmodelled constructs round-trip byte-for-byte.
5.7 KiB
spec, blocked-by
| spec | blocked-by |
|---|---|
| core-parser | 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
ListBlockcarries an ordered flag and an array ofListItemBlock; eachListItemBlockis a container wrapping child blocks (e.g. a paragraph), not a bare inline arrayBlockquoteBlockis a container holdingBlock[]; a callout (> [!note]) parses as an ordinary blockquote with its text preservedCodeBlockcarries an optional language and literal text, and its content is not annotation-parsedThematicBreakBlockis modelled- Inline
EmphasisNode,StrongNode,CodeSpanNode, andLinkNodeare modelled; code-span content is not annotation-parsed - An unmodelled block (e.g. a GFM table) falls through to
RawBlockand an unmodelled inline (e.g. strikethrough) toRawInline, each preserving byte-for-byte verbatim source via position-slicing renderhandles ordered and unordered lists, code blocks, thematic breaks, and blockquotesrenderhandles emphasis (italic), strong (bold), code span (distinct style), and link (inline content only)- 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
- 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.