import { test, expect } from "bun:test"; import { htmlToSx } from "./a31_sxdoc_parse.ts"; import { SX_DOC_VERSION } from "./a31_sxdoc.ts"; test("returns an empty document for empty input", () => { const doc = htmlToSx(""); expect(doc.v).toBe(SX_DOC_VERSION); expect(doc.blocks).toEqual([]); }); test("parses a simple paragraph", () => { const doc = htmlToSx("

Hello world

"); expect(doc.blocks).toHaveLength(1); expect(doc.blocks[0]).toEqual({ t: "p", c: [{ t: "text", v: "Hello world" }], }); }); test("parses headings with correct level for h1-h6", () => { for (const level of [1, 2, 3, 4, 5, 6] as const) { const doc = htmlToSx(`Title ${level}`); expect(doc.blocks).toHaveLength(1); expect(doc.blocks[0]).toEqual({ t: "h", level, c: [{ t: "text", v: `Title ${level}` }], }); } }); test("parses unordered list with items wrapped as paragraphs", () => { const doc = htmlToSx(""); expect(doc.blocks).toHaveLength(1); expect(doc.blocks[0]).toEqual({ t: "ul", items: [ [{ t: "p", c: [{ t: "text", v: "one" }] }], [{ t: "p", c: [{ t: "text", v: "two" }] }], ], }); }); test("parses ordered list", () => { const doc = htmlToSx("
  1. first
"); const block = doc.blocks[0]; expect(block.t).toBe("ol"); expect((block as { items: unknown }).items).toEqual([ [{ t: "p", c: [{ t: "text", v: "first" }] }], ]); }); test("parses nested lists inside a list item", () => { const doc = htmlToSx(""); const outer = doc.blocks[0] as { t: "ul"; items: unknown[][] }; expect(outer.t).toBe("ul"); expect(outer.items[0]).toHaveLength(2); expect(outer.items[0][0]).toEqual({ t: "p", c: [{ t: "text", v: "outer" }] }); expect(outer.items[0][1]).toEqual({ t: "ul", items: [[{ t: "p", c: [{ t: "text", v: "inner" }] }]], }); }); test("parses blockquote with paragraph inside", () => { const doc = htmlToSx("

quoted

"); expect(doc.blocks).toEqual([{ t: "quote", c: [{ t: "p", c: [{ t: "text", v: "quoted" }] }], }]); }); test("parses blockquote with loose text wraps it in a paragraph", () => { const doc = htmlToSx("
loose
"); expect(doc.blocks[0]).toEqual({ t: "quote", c: [{ t: "p", c: [{ t: "text", v: "loose" }] }], }); }); test("parses pre>code with language hint", () => { const doc = htmlToSx(`
const x = 1;
`); expect(doc.blocks[0]).toEqual({ t: "code", lang: "ts", src: "const x = 1;", }); }); test("parses pre without inner code element", () => { const doc = htmlToSx("
raw text
"); expect(doc.blocks[0]).toEqual({ t: "code", lang: "", src: "raw text", }); }); test("preserves encoded entities in code blocks", () => { const doc = htmlToSx(`
<p>
`); expect(doc.blocks[0]).toEqual({ t: "code", lang: "", src: "

", }); }); test("parses img with src and alt", () => { const doc = htmlToSx(`x icon`); expect(doc.blocks[0]).toEqual({ t: "img", src: "/x.png", alt: "x icon" }); }); test("parses img with width and height attributes", () => { const doc = htmlToSx(``); expect(doc.blocks[0]).toEqual({ t: "img", src: "/a.jpg", w: 200, h: 100 }); }); test("skips img with empty src", () => { const doc = htmlToSx(``); expect(doc.blocks).toEqual([]); }); test("parses figure with figcaption", () => { const doc = htmlToSx(`

nice y
`); expect(doc.blocks[0]).toEqual({ t: "img", src: "/y.png", caption: "nice y", }); }); test("parses hr", () => { const doc = htmlToSx("
"); expect(doc.blocks[0]).toEqual({ t: "hr" }); }); test("parses inline bold and italic marks", () => { const doc = htmlToSx("

bold and ital

"); expect(doc.blocks[0]).toEqual({ t: "p", c: [ { t: "text", v: "bold", m: ["b"] }, { t: "text", v: " and " }, { t: "text", v: "ital", m: ["i"] }, ], }); }); test("composes nested marks into a single mark array", () => { const doc = htmlToSx("

both

"); expect(doc.blocks[0]).toEqual({ t: "p", c: [{ t: "text", v: "both", m: ["b", "i"] }], }); }); test("dedupes repeated marks across nested wrappers", () => { const doc = htmlToSx("

x

"); const para = doc.blocks[0] as { c: Array<{ m?: string[] }> }; expect(para.c[0].m).toEqual(["b"]); }); test("treats
as a newline text run carrying marks", () => { const doc = htmlToSx("

a
b

"); expect(doc.blocks[0]).toEqual({ t: "p", c: [ { t: "text", v: "a" }, { t: "text", v: "\n" }, { t: "text", v: "b" }, ], }); }); test("parses anchor links with href", () => { const doc = htmlToSx(`

click

`); expect(doc.blocks[0]).toEqual({ t: "p", c: [{ t: "a", href: "/x", c: [{ t: "text", v: "click" }] }], }); }); test("strips unknown inline wrappers like span and keeps content", () => { const doc = htmlToSx(`

before middle after

`); expect(doc.blocks[0]).toEqual({ t: "p", c: [ { t: "text", v: "before " }, { t: "text", v: "middle" }, { t: "text", v: " after" }, ], }); }); test("parses a standalone shortcode out of plain text", () => { const doc = htmlToSx("

[[sx:event-count]]

"); expect(doc.blocks).toEqual([ { t: "shortcode", name: "event-count", args: {} }, ]); }); test("parses a shortcode with quoted and bare args", () => { const doc = htmlToSx(`

[[sx:list tag="blog" limit=5]]

`); expect(doc.blocks).toEqual([ { t: "shortcode", name: "list", args: { tag: "blog", limit: "5" } }, ]); }); test("lifts a shortcode out of a mixed paragraph", () => { const doc = htmlToSx("

before [[sx:x]] after

"); expect(doc.blocks).toEqual([ { t: "p", c: [{ t: "text", v: "before " }] }, { t: "shortcode", name: "x", args: {} }, { t: "p", c: [{ t: "text", v: " after" }] }, ]); }); test("recurses into div/section/article containers", () => { const doc = htmlToSx("

one

two

"); expect(doc.blocks).toHaveLength(2); expect(doc.blocks[0]).toEqual({ t: "p", c: [{ t: "text", v: "one" }] }); expect(doc.blocks[1]).toEqual({ t: "p", c: [{ t: "text", v: "two" }] }); }); test("falls back to html escape-hatch for unknown elements", () => { const doc = htmlToSx(`
x
`); expect(doc.blocks).toHaveLength(1); expect(doc.blocks[0].t).toBe("html"); expect((doc.blocks[0] as { src: string }).src).toContain(""); }); test("decodes named entities in inline text", () => { const doc = htmlToSx("

A & B

"); expect(doc.blocks[0]).toEqual({ t: "p", c: [{ t: "text", v: "A & B" }], }); }); test("ignores empty paragraphs", () => { const doc = htmlToSx("

real

"); expect(doc.blocks).toHaveLength(1); expect(doc.blocks[0]).toEqual({ t: "p", c: [{ t: "text", v: "real" }] }); });