syntaxai/tdd.md · main · src / b51_render_sxdoc.test.ts

b51_render_sxdoc.test.ts 241 lines · 6416 bytes raw
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("<p>hello</p>");
});

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(`<h${level}>X</h${level}>`);
  }
});

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("<ul><li><p>one</p></li><li><p>two</p></li></ul>");
  const ol = sxToHtml({
    v: SX_DOC_VERSION,
    blocks: [{ t: "ol", items: [[{ t: "p", c: [{ t: "text", v: "a" }] }]] }],
  });
  expect(ol).toBe("<ol><li><p>a</p></li></ol>");
});

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("<blockquote><p>quoted</p></blockquote>");
});

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(`<pre><code class="language-ts">const x = 1;</code></pre>`);
});

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(`<pre><code>raw</code></pre>`);
});

test("escapes html entities inside code source", () => {
  const out = sxToHtml({
    v: SX_DOC_VERSION,
    blocks: [{ t: "code", src: "<p>" }],
  });
  expect(out).toContain("&lt;p&gt;");
});

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(`<img src="/x.png" alt="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(`<figure><img src="/y.png"><figcaption>nice</figcaption></figure>`);
});

test("renders hr", () => {
  const out = sxToHtml({
    v: SX_DOC_VERSION,
    blocks: [{ t: "hr" }],
  });
  expect(out).toBe("<hr>");
});

test("passes html escape-hatch through verbatim", () => {
  const out = sxToHtml({
    v: SX_DOC_VERSION,
    blocks: [{ t: "html", src: "<table><tr><td>x</td></tr></table>" }],
  });
  expect(out).toBe("<table><tr><td>x</td></tr></table>");
});

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("<p><strong><em>both</em></strong></p>");
});

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(`<p><a href="/x">click</a></p>`);
});

test("escapes quotes and angle brackets in attributes", () => {
  const out = sxToHtml({
    v: SX_DOC_VERSION,
    blocks: [{
      t: "p",
      c: [{ t: "a", href: `/a"<b`, c: [{ t: "text", v: "x" }] }],
    }],
  });
  expect(out).toBe(`<p><a href="/a&quot;&lt;b">x</a></p>`);
});

test("renders inline newline as <br>", () => {
  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("<p>a<br>b</p>");
});

// ─── 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);
});