import { expect, test } from "vitest"; import { parse } from "./parse_tree.ts"; import { extendedMarkdownLanguage } from "./parser.ts"; import type { ParseTree } from "../../plug-api/lib/tree.ts"; function parseHtml(md: string): ParseTree { return parse(extendedMarkdownLanguage, md); } /** Recursively collect all node types in tree order. */ function collectTypes(tree: ParseTree): (string | undefined)[] { const result: (string | undefined)[] = []; if (tree.type) result.push(tree.type); if (tree.children) { for (const child of tree.children) { result.push(...collectTypes(child)); } } return result; } /** Get the first child of the Document node. */ function topNode(tree: ParseTree): ParseTree { return tree.children![0]; } /** Get the text content of a node (from its text children). */ function nodeText(tree: ParseTree): string { if (tree.text !== undefined) return tree.text; return tree.children?.map((c) => nodeText(c)).join("") ?? ""; } // ── Basic structure ────────────────────────────────────────────────── test("simple table produces HTMLBlock with structured children", () => { const tree = parseHtml("
hello
"); const block = topNode(tree); expect(block.type).toBe("HTMLBlock"); const childTypes = block.children! .filter((c) => c.type) .map((c) => c.type); expect(childTypes).toEqual([ "HTMLOpenTag", "HTMLOpenTag", "HTMLOpenTag", "HTMLCloseTag", "HTMLCloseTag", "HTMLCloseTag", ]); }); test("tag text is preserved in children", () => { const tree = parseHtml("
content
"); const block = topNode(tree); const openTag = block.children!.find((c) => c.type === "HTMLOpenTag")!; expect(nodeText(openTag)).toBe("
"); const closeTag = block.children!.find((c) => c.type === "HTMLCloseTag")!; expect(nodeText(closeTag)).toBe("
"); // Text between tags const textNodes = block.children!.filter((c) => !c.type); expect(textNodes.some((t) => t.text === "content")).toBe(true); }); // ── Attributes on tags ────────────────────────────────────────────── test("attributes are preserved in HTMLOpenTag text", () => { const tree = parseHtml( '42', ); const block = topNode(tree); const openTag = block.children!.find((c) => c.type === "HTMLOpenTag")!; expect(nodeText(openTag)).toBe(''); }); // ── Self-closing tags ─────────────────────────────────────────────── test("self-closing tag produces HTMLSelfClosingTag", () => { const tree = parseHtml("


"); const block = topNode(tree); const selfClosing = block.children!.filter( (c) => c.type === "HTMLSelfClosingTag", ); expect(selfClosing).toHaveLength(2); expect(nodeText(selfClosing[0])).toBe("
"); expect(nodeText(selfClosing[1])).toBe("
"); }); // ── Markdown inside HTML ──────────────────────────────────────────── test("bold markdown inside td is parsed", () => { const tree = parseHtml("hello **world**"); const block = topNode(tree); const types = collectTypes(block); expect(types).toContain("StrongEmphasis"); }); test("wiki link inside td is parsed", () => { const tree = parseHtml("see [[MyPage]]"); const block = topNode(tree); const types = collectTypes(block); expect(types).toContain("WikiLink"); expect(types).toContain("WikiLinkPage"); }); test("hashtag inside td is parsed", () => { const tree = parseHtml("#sometag"); const block = topNode(tree); const types = collectTypes(block); expect(types).toContain("Hashtag"); }); test("italic and bold in list item", () => { const tree = parseHtml(""); const block = topNode(tree); const types = collectTypes(block); expect(types).toContain("Emphasis"); expect(types).toContain("StrongEmphasis"); }); // ── Multi-line HTML blocks ────────────────────────────────────────── test("multi-line table is parsed correctly", () => { const md = `
row1
row2
`; const tree = parseHtml(md); const block = topNode(tree); expect(block.type).toBe("HTMLBlock"); const openTags = block.children!.filter((c) => c.type === "HTMLOpenTag"); // table + 2x tr + 2x td = 5 expect(openTags).toHaveLength(5); }); // ── Nested same-name tags ─────────────────────────────────────────── test("nested same-name tags are handled", () => { const tree = parseHtml("
inner
"); const block = topNode(tree); const opens = block.children!.filter((c) => c.type === "HTMLOpenTag"); const closes = block.children!.filter((c) => c.type === "HTMLCloseTag"); expect(opens).toHaveLength(2); expect(closes).toHaveLength(2); }); // ── Complex table with data attributes and markdown ───────────────── test("table with data attributes and markdown in cells", () => { const md = `
nameage
[[Alice]]30
`; const tree = parseHtml(md); const block = topNode(tree); expect(block.type).toBe("HTMLBlock"); // Check wiki link is parsed const types = collectTypes(block); expect(types).toContain("WikiLink"); // Check data attributes are in tag text const openTags = block.children!.filter((c) => c.type === "HTMLOpenTag"); const tdWithAttr = openTags.find((t) => nodeText(t).includes("data-table-cell-type") ); expect(tdWithAttr).toBeDefined(); expect(nodeText(tdWithAttr!)).toContain('data-table-cell-type="string"'); }); // ── Comments, scripts, processing instructions ────────────────────── test("HTML comments produce CommentBlock", () => { const tree = parseHtml(""); expect(topNode(tree).type).toBe("CommentBlock"); }); test("multi-line comment produces CommentBlock", () => { const md = ``; const tree = parseHtml(md); expect(topNode(tree).type).toBe("CommentBlock"); }); test("script block produces HTMLBlock (not structured)", () => { const tree = parseHtml(""); const block = topNode(tree); expect(block.type).toBe("HTMLBlock"); // Should be a flat text blob, not structured const hasOpenTag = block.children?.some((c) => c.type === "HTMLOpenTag"); expect(hasOpenTag).toBeFalsy(); }); test("pre block produces HTMLBlock (not structured)", () => { const tree = parseHtml("
code here
"); const block = topNode(tree); expect(block.type).toBe("HTMLBlock"); const hasOpenTag = block.children?.some((c) => c.type === "HTMLOpenTag"); expect(hasOpenTag).toBeFalsy(); }); test("processing instruction produces ProcessingInstructionBlock", () => { const tree = parseHtml(""); expect(topNode(tree).type).toBe("ProcessingInstructionBlock"); }); // ── Non-HTML content is unaffected ────────────────────────────────── test("regular paragraph is not affected", () => { const tree = parseHtml("Just some text"); expect(topNode(tree).type).toBe("Paragraph"); }); test("inline HTML in paragraph still works", () => { const tree = parseHtml("Hello bold world"); const para = topNode(tree); expect(para.type).toBe("Paragraph"); const types = collectTypes(para); expect(types).toContain("HTMLTag"); }); test("markdown table is not affected", () => { const md = `| a | b | | -- | -- | | 1 | 2 |`; const tree = parseHtml(md); expect(topNode(tree).type).toBe("Table"); }); // ── Empty and edge cases ──────────────────────────────────────────── test("empty div", () => { const tree = parseHtml("
"); const block = topNode(tree); expect(block.type).toBe("HTMLBlock"); const opens = block.children!.filter((c) => c.type === "HTMLOpenTag"); const closes = block.children!.filter((c) => c.type === "HTMLCloseTag"); expect(opens).toHaveLength(1); expect(closes).toHaveLength(1); }); test("table with empty cells", () => { const tree = parseHtml("
"); const block = topNode(tree); expect(block.type).toBe("HTMLBlock"); const opens = block.children!.filter((c) => c.type === "HTMLOpenTag"); // table + tr + 2x td = 4 expect(opens).toHaveLength(4); }); test("data-table-empty attribute on table", () => { const tree = parseHtml('
'); const block = topNode(tree); expect(block.type).toBe("HTMLBlock"); const openTag = block.children!.find((c) => c.type === "HTMLOpenTag")!; expect(nodeText(openTag)).toContain("data-table-empty"); });