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 1–6 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." }] }, ]); }); });