import { test, expect } from "bun:test"; import { sxToHtml } from "./b51_render_sxdoc.ts"; import { htmlToSx } from "./a31_sxdoc_parse.ts"; import { SX_DOC_VERSION, emptyDocument, type SxDocument } from "./a31_sxdoc.ts"; test("renders the empty document as empty string", () => { expect(sxToHtml(emptyDocument())).toBe(""); }); test("renders a paragraph", () => { const out = sxToHtml({ v: SX_DOC_VERSION, blocks: [{ t: "p", c: [{ t: "text", v: "hello" }] }], }); expect(out).toBe("

hello

"); }); test("renders headings at the correct level", () => { for (const level of [1, 2, 3, 4, 5, 6] as const) { const out = sxToHtml({ v: SX_DOC_VERSION, blocks: [{ t: "h", level, c: [{ t: "text", v: "X" }] }], }); expect(out).toBe(`X`); } }); test("renders ul and ol with li wrappers", () => { const ul = sxToHtml({ v: SX_DOC_VERSION, blocks: [{ t: "ul", items: [ [{ t: "p", c: [{ t: "text", v: "one" }] }], [{ t: "p", c: [{ t: "text", v: "two" }] }], ], }], }); expect(ul).toBe(""); const ol = sxToHtml({ v: SX_DOC_VERSION, blocks: [{ t: "ol", items: [[{ t: "p", c: [{ t: "text", v: "a" }] }]] }], }); expect(ol).toBe("
  1. a

"); }); test("renders blockquote with inner blocks", () => { const out = sxToHtml({ v: SX_DOC_VERSION, blocks: [{ t: "quote", c: [{ t: "p", c: [{ t: "text", v: "quoted" }] }], }], }); expect(out).toBe("

quoted

"); }); test("renders code block with language class", () => { const out = sxToHtml({ v: SX_DOC_VERSION, blocks: [{ t: "code", lang: "ts", src: "const x = 1;" }], }); expect(out).toBe(`
const x = 1;
`); }); test("renders code block without lang as plain pre>code", () => { const out = sxToHtml({ v: SX_DOC_VERSION, blocks: [{ t: "code", src: "raw" }], }); expect(out).toBe(`
raw
`); }); test("escapes html entities inside code source", () => { const out = sxToHtml({ v: SX_DOC_VERSION, blocks: [{ t: "code", src: "

" }], }); expect(out).toContain("<p>"); }); test("renders img with src and alt", () => { const out = sxToHtml({ v: SX_DOC_VERSION, blocks: [{ t: "img", src: "/x.png", alt: "x" }], }); expect(out).toBe(`x`); }); test("wraps captioned img in a figure", () => { const out = sxToHtml({ v: SX_DOC_VERSION, blocks: [{ t: "img", src: "/y.png", caption: "nice" }], }); expect(out).toBe(`

nice
`); }); test("renders hr", () => { const out = sxToHtml({ v: SX_DOC_VERSION, blocks: [{ t: "hr" }], }); expect(out).toBe("
"); }); test("passes html escape-hatch through verbatim", () => { const out = sxToHtml({ v: SX_DOC_VERSION, blocks: [{ t: "html", src: "
x
" }], }); expect(out).toBe("
x
"); }); test("renders shortcodes without args using a compact form", () => { const out = sxToHtml({ v: SX_DOC_VERSION, blocks: [{ t: "shortcode", name: "event-count", args: {} }], }); expect(out).toBe("[[sx:event-count]]"); }); test("renders shortcodes with args quoted", () => { const out = sxToHtml({ v: SX_DOC_VERSION, blocks: [{ t: "shortcode", name: "list", args: { tag: "blog", limit: "5" } }], }); expect(out).toBe(`[[sx:list tag="blog" limit="5"]]`); }); test("renders bold and italic marks deterministically", () => { const out = sxToHtml({ v: SX_DOC_VERSION, blocks: [{ t: "p", c: [{ t: "text", v: "both", m: ["i", "b"] }], }], }); expect(out).toBe("

both

"); }); test("renders anchor links", () => { const out = sxToHtml({ v: SX_DOC_VERSION, blocks: [{ t: "p", c: [{ t: "a", href: "/x", c: [{ t: "text", v: "click" }] }], }], }); expect(out).toBe(`

click

`); }); test("escapes quotes and angle brackets in attributes", () => { const out = sxToHtml({ v: SX_DOC_VERSION, blocks: [{ t: "p", c: [{ t: "a", href: `/a"x

`); }); test("renders inline newline as
", () => { const out = sxToHtml({ v: SX_DOC_VERSION, blocks: [{ t: "p", c: [ { t: "text", v: "a" }, { t: "text", v: "\n" }, { t: "text", v: "b" }, ], }], }); expect(out).toBe("

a
b

"); }); // ─── round-trip property tests ─────────────────────────────────────────── // htmlToSx(sxToHtml(doc)) === doc must hold for representative docs. test("round-trip: simple paragraph", () => { const doc: SxDocument = { v: SX_DOC_VERSION, blocks: [{ t: "p", c: [{ t: "text", v: "hello" }] }], }; expect(htmlToSx(sxToHtml(doc))).toEqual(doc); }); test("round-trip: heading + paragraph + hr", () => { const doc: SxDocument = { v: SX_DOC_VERSION, blocks: [ { t: "h", level: 2, c: [{ t: "text", v: "Title" }] }, { t: "p", c: [{ t: "text", v: "body" }] }, { t: "hr" }, ], }; expect(htmlToSx(sxToHtml(doc))).toEqual(doc); }); test("round-trip: list of paragraphs", () => { const doc: SxDocument = { v: SX_DOC_VERSION, blocks: [{ t: "ul", items: [ [{ t: "p", c: [{ t: "text", v: "one" }] }], [{ t: "p", c: [{ t: "text", v: "two" }] }], ], }], }; expect(htmlToSx(sxToHtml(doc))).toEqual(doc); }); test("round-trip: marks preserved across re-parse", () => { const doc: SxDocument = { v: SX_DOC_VERSION, blocks: [{ t: "p", c: [{ t: "text", v: "x", m: ["b", "i"] }], }], }; expect(htmlToSx(sxToHtml(doc))).toEqual(doc); }); test("round-trip: shortcode survives the trip", () => { const doc: SxDocument = { v: SX_DOC_VERSION, blocks: [{ t: "shortcode", name: "event-count", args: {} }], }; expect(htmlToSx(sxToHtml(doc))).toEqual(doc); }); test("round-trip: code block with language", () => { const doc: SxDocument = { v: SX_DOC_VERSION, blocks: [{ t: "code", lang: "ts", src: "const x = 1;" }], }; expect(htmlToSx(sxToHtml(doc))).toEqual(doc); });