syntaxai/tdd.md · main · src / b51_render_sxdoc.test.ts
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("<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(`<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"<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);
});