This repository has been archived on 2026-07-29. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
kitchen-md/packages/core/src/parse_test.ts
alexion 22befaaa71 feat: render richer blocks and inline nodes (task 0004)
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.
2026-07-29 07:34:56 -04:00

264 lines
7.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, test } from "bun:test";
import { parse } from "@kitchen-md/core";
describe("parse", () => {
test("frontmatter passthrough — arbitrary fields become a plain object", () => {
const input = `---
title: Buttered Toast
servings: 2
tags: [breakfast, simple]
---
# Buttered Toast
`;
const result = parse(input);
expect(result.frontmatter).toEqual({
title: "Buttered Toast",
servings: 2,
tags: ["breakfast", "simple"],
});
});
test("frontmatter is an empty object when absent", () => {
const result = parse("# Just a heading");
expect(result.frontmatter).toEqual({});
});
test("frontmatter is an empty object when the block is empty", () => {
const input = `---
---
# Heading
`;
const result = parse(input);
expect(result.frontmatter).toEqual({});
});
test("headings are modelled at every level 16 with TextNode children", () => {
const cases: [string, 1 | 2 | 3 | 4 | 5 | 6, string][] = [
["# Level One", 1, "Level One"],
["## Level Two", 2, "Level Two"],
["### Level Three", 3, "Level Three"],
["#### Level Four", 4, "Level Four"],
["##### Level Five", 5, "Level Five"],
["###### Level Six", 6, "Level Six"],
];
for (const [markdown, level, text] of cases) {
const result = parse(markdown);
expect(result.blocks).toEqual([
{
type: "heading",
level,
children: [{ type: "text", value: text }],
},
]);
}
});
test("a paragraph is a ParagraphBlock with TextNode content", () => {
const result = parse("Just some plain prose.");
expect(result.blocks).toEqual([
{
type: "paragraph",
children: [{ type: "text", value: "Just some plain prose." }],
},
]);
});
test("blocks are flat and in document order — a heading is a sibling of the following paragraph", () => {
const input = `# Title
A paragraph under it.
`;
const result = parse(input);
expect(result.blocks).toEqual([
{
type: "heading",
level: 1,
children: [{ type: "text", value: "Title" }],
},
{
type: "paragraph",
children: [{ type: "text", value: "A paragraph under it." }],
},
]);
});
test("diagnostics are empty in the normal case", () => {
const result = parse("# Ok");
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("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." }] },
]);
});
});