import { describe, expect, test } from "bun:test"; import React from "react"; import { renderToString } from "react-dom/server"; const Markdown = Bun.markdown; /** renderToString the Fragment returned by Markdown.react. * Uses reactVersion: 18 since the project has react-dom@18 installed. */ function reactRender(md: string, components?: any, opts?: any): string { return renderToString(Markdown.react(md, components, { reactVersion: 18, ...opts })); } // ============================================================================ // Bun.markdown.react() — React element AST // ============================================================================ describe("Bun.markdown.react", () => { const REACT_ELEMENT_SYMBOL = Symbol.for("react.element"); const REACT_FRAGMENT_SYMBOL = Symbol.for("react.fragment"); const REACT_TRANSITIONAL_SYMBOL = Symbol.for("react.transitional.element"); /** Helper: get the children array from the Fragment returned by react() */ function children(md: string, components?: any, opts?: any): any[] { return Markdown.react(md, components, opts).props.children; } test("returns a Fragment element", () => { const result = Markdown.react("# Hello\n"); expect(result.$$typeof).toBe(REACT_TRANSITIONAL_SYMBOL); expect(result.type).toBe(REACT_FRAGMENT_SYMBOL); expect(result.key).toBeNull(); expect(result.ref).toBeNull(); expect(result.props.children).toBeArray(); }); test("fragment children are React elements", () => { const els = children("# Hello\n"); expect(els).toHaveLength(1); expect(els[0].$$typeof).toBe(REACT_TRANSITIONAL_SYMBOL); }); test("element has type, key, ref, props", () => { const el = children("# Hello\n")[0]; expect(el.type).toBe("h1"); expect(el.key).toBeNull(); expect(el.ref).toBeNull(); expect(el.props).toEqual({ children: ["Hello"] }); }); test("heading levels 1-6", () => { for (let i = 1; i <= 6; i++) { const md = Buffer.alloc(i, "#").toString() + " Level\n"; const el = children(md)[0]; expect(el.$$typeof).toBe(REACT_TRANSITIONAL_SYMBOL); expect(el.type).toBe(`h${i}`); expect(el.props.children).toEqual(["Level"]); } }); test("text is plain strings in children", () => { expect(children("Hello world\n")[0].props.children).toEqual(["Hello world"]); }); test("nested inline elements are React elements", () => { const p = children("Hello **world**\n")[0]; expect(p.$$typeof).toBe(REACT_TRANSITIONAL_SYMBOL); expect(p.props.children[0]).toBe("Hello "); const strong = p.props.children[1]; expect(strong.$$typeof).toBe(REACT_TRANSITIONAL_SYMBOL); expect(strong.type).toBe("strong"); expect(strong.props.children).toEqual(["world"]); }); test("link has href in props", () => { const link = children("[click](https://example.com)\n")[0].props.children[0]; expect(link.$$typeof).toBe(REACT_TRANSITIONAL_SYMBOL); expect(link.type).toBe("a"); expect(link.props.href).toBe("https://example.com"); expect(link.props.children).toEqual(["click"]); }); test("image has src and alt in props", () => { const img = children("\n")[0].props.children[0]; expect(img.$$typeof).toBe(REACT_TRANSITIONAL_SYMBOL); expect(img.type).toBe("img"); expect(img.props.src).toBe("img.png"); expect(img.props.alt).toBe("alt"); expect(img.props.children).toBeUndefined(); }); test("code block with language", () => { const pre = children("```ts\nconst x = 1;\n```\n")[0]; expect(pre.$$typeof).toBe(REACT_TRANSITIONAL_SYMBOL); expect(pre.type).toBe("pre"); expect(pre.props.language).toBe("ts"); }); test("hr is void element", () => { const hr = children("---\n")[0]; expect(hr.$$typeof).toBe(REACT_TRANSITIONAL_SYMBOL); expect(hr.type).toBe("hr"); expect(hr.key).toBeNull(); expect(hr.ref).toBeNull(); expect(hr.props).toEqual({}); }); test("br produces React element", () => { const pChildren = children("line1 \nline2\n")[0].props.children; const br = pChildren.find((c: any) => typeof c === "object" && c?.type === "br"); expect(br).toBeDefined(); expect(br.$$typeof).toBe(REACT_TRANSITIONAL_SYMBOL); expect(br.props).toEqual({}); }); test("ordered list with start", () => { const ol = children("3. first\n4. second\n")[0]; expect(ol.type).toBe("ol"); expect(ol.props.start).toBe(3); expect(ol.props.children).toHaveLength(2); expect(ol.props.children[0].type).toBe("li"); }); test("table structure", () => { const table = children("| A | B |\n|---|---|\n| 1 | 2 |\n")[0]; expect(table.$$typeof).toBe(REACT_TRANSITIONAL_SYMBOL); expect(table.type).toBe("table"); const thead = table.props.children.find((c: any) => c.type === "thead"); expect(thead).toBeDefined(); expect(thead.$$typeof).toBe(REACT_TRANSITIONAL_SYMBOL); }); test("headingIds adds id to props", () => { const el = children("## Hello World\n", undefined, { headings: { ids: true } })[0]; expect(el.type).toBe("h2"); expect(el.props.id).toBe("hello-world"); expect(el.props.children).toEqual(["Hello World"]); }); test("default $$typeof is react.transitional.element", () => { const result = Markdown.react("# Hi\n"); expect(result.$$typeof).toBe(REACT_TRANSITIONAL_SYMBOL); expect(result.props.children[0].$$typeof).toBe(REACT_TRANSITIONAL_SYMBOL); }); test("reactVersion 18 uses react.element symbol on all elements", () => { const result = Markdown.react("Hello **world**\n", undefined, { reactVersion: 18 }); expect(result.$$typeof).toBe(REACT_ELEMENT_SYMBOL); const p = result.props.children[0]; expect(p.$$typeof).toBe(REACT_ELEMENT_SYMBOL); const strong = p.props.children[1]; expect(strong.$$typeof).toBe(REACT_ELEMENT_SYMBOL); }); test("multiple blocks", () => { const els = children("# Title\n\nParagraph\n"); expect(els).toHaveLength(2); expect(els[0].$$typeof).toBe(REACT_TRANSITIONAL_SYMBOL); expect(els[0].type).toBe("h1"); expect(els[1].$$typeof).toBe(REACT_TRANSITIONAL_SYMBOL); expect(els[1].type).toBe("p"); }); test("complete document", () => { const els = children(`# Hello This is **bold** and *italic*. - item one - item two --- `); expect(els[0].type).toBe("h1"); expect(els[1].type).toBe("p"); expect(els[2].type).toBe("ul"); expect(els[3].type).toBe("hr"); for (const el of els) { expect(el.$$typeof).toBe(REACT_TRANSITIONAL_SYMBOL); } }); test("blockquote contains nested React elements", () => { const bq = children("> quoted text\n")[0]; expect(bq.$$typeof).toBe(REACT_TRANSITIONAL_SYMBOL); expect(bq.type).toBe("blockquote"); const p = bq.props.children[0]; expect(p.$$typeof).toBe(REACT_TRANSITIONAL_SYMBOL); expect(p.type).toBe("p"); expect(p.props.children).toEqual(["quoted text"]); }); test("deeply nested elements are all React elements", () => { const bq = children("> **bold *and italic***\n")[0]; expect(bq.$$typeof).toBe(REACT_TRANSITIONAL_SYMBOL); const p = bq.props.children[0]; expect(p.$$typeof).toBe(REACT_TRANSITIONAL_SYMBOL); const strong = p.props.children[0]; expect(strong.$$typeof).toBe(REACT_TRANSITIONAL_SYMBOL); expect(strong.type).toBe("strong"); const em = strong.props.children[1]; expect(em.$$typeof).toBe(REACT_TRANSITIONAL_SYMBOL); expect(em.type).toBe("em"); expect(em.props.children).toEqual(["and italic"]); }); test("link with title in React element", () => { const link = children('[text](https://example.com "My Title")\n')[0].props.children[0]; expect(link.$$typeof).toBe(REACT_TRANSITIONAL_SYMBOL); expect(link.type).toBe("a"); expect(link.props.href).toBe("https://example.com"); expect(link.props.title).toBe("My Title"); expect(link.props.children).toEqual(["text"]); }); test("image with title in React element", () => { const img = children('\n')[0].props.children[0]; expect(img.$$typeof).toBe(REACT_TRANSITIONAL_SYMBOL); expect(img.type).toBe("img"); expect(img.props.src).toBe("pic.jpg"); expect(img.props.title).toBe("Photo"); expect(img.props.alt).toBe("alt"); expect(img.props.children).toBeUndefined(); }); test("inline code is a React element", () => { const code = children("`code`\n")[0].props.children[0]; expect(code.$$typeof).toBe(REACT_TRANSITIONAL_SYMBOL); expect(code.type).toBe("code"); expect(code.props.children).toEqual(["code"]); }); test("strikethrough is a React element", () => { const del = children("~~deleted~~\n")[0].props.children[0]; expect(del.$$typeof).toBe(REACT_TRANSITIONAL_SYMBOL); expect(del.type).toBe("del"); expect(del.props.children).toEqual(["deleted"]); }); test("unordered list children are React elements", () => { const ul = children("- a\n- b\n")[0]; expect(ul.type).toBe("ul"); for (const li of ul.props.children) { expect(li.$$typeof).toBe(REACT_TRANSITIONAL_SYMBOL); expect(li.type).toBe("li"); } }); test("entities are decoded in React output", () => { const text = children("& < >\n")[0].props.children.join(""); expect(text).toContain("&"); expect(text).toContain("<"); expect(text).toContain(">"); }); test("softbr becomes newline string", () => { const pChildren = children("line1\nline2\n")[0].props.children; expect(pChildren).toContain("\n"); }); }); // ============================================================================ // Bun.markdown.react() + React renderToString integration // ============================================================================ describe("Bun.markdown.react renderToString", () => { test("heading", () => { expect(reactRender("# Hello\n")).toBe("
Hello world
"); }); test("bold text", () => { expect(reactRender("**bold**\n")).toBe("bold
"); }); test("italic text", () => { expect(reactRender("*italic*\n")).toBe("italic
"); }); test("nested bold and italic", () => { expect(reactRender("**bold *and italic***\n")).toBe("bold and italic
"); }); test("strikethrough", () => { expect(reactRender("~~deleted~~\n")).toBe("deleted
code

"); }); test("unordered list", () => { expect(reactRender("- a\n- b\n")).toBe("quoted
| A | "); expect(html).toContain("1 | "); }); test("mixed document", () => { const html = reactRender(`# Title Hello **world**, this is *important*. - item one - item two `); expect(html).toContain("
|---|