/** * Renderer unit tests — pure-function assertions for render.ts, smartypants.ts, * and print-css.ts. No Playwright, no PDF generation. */ import { describe, expect, test } from "bun:test"; import { render, sanitizeUntrustedHtml } from "../src/render"; import { smartypants } from "../src/smartypants"; import { printCss } from "../src/print-css"; // ─── smartypants ────────────────────────────────────────────── describe("smartypants", () => { test("converts straight double quotes to curly", () => { const out = smartypants(`

She said "hello" to him.

`); expect(out).toContain("\u201chello\u201d"); }); test("converts em dash (--)", () => { const out = smartypants(`

This is it -- the answer.

`); expect(out).toContain("\u2014"); }); test("converts ellipsis (...)", () => { const out = smartypants(`

Wait...

`); expect(out).toContain("\u2026"); }); test("converts apostrophes in contractions", () => { const out = smartypants(`

don't you know?

`); expect(out).toContain("don\u2019t"); }); test("does NOT touch content inside blocks", () => { const input = `
const x = "hello"; // it's fine
`; const out = smartypants(input); expect(out).toBe(input); // unchanged }); test("does NOT touch content inside
 blocks", () => {
    const input = `
"quoted" -- don't
`; const out = smartypants(input); expect(out).toBe(input); }); test("does NOT touch inline code", () => { const out = smartypants(`

Use it's like this: "hello".

`); expect(out).toContain("it's"); expect(out).toContain("\u201chello\u201d"); }); test("does NOT touch URLs", () => { const out = smartypants(`

Visit https://example.com/it's-page for "details".

`); expect(out).toContain("https://example.com/it's-page"); expect(out).toContain("\u201cdetails\u201d"); }); test("does NOT touch HTML attribute values", () => { const out = smartypants(`link`); expect(out).toContain(`href="it's-a-test.html"`); }); test("does NOT convert -- in CLI flags", () => { // Prose like "try --verbose mode" should not turn -- into em dash const out = smartypants(`

Try --verbose mode.

`); // Since "--" is followed by a word char but not preceded by word/space, // it should remain intact. We're lenient here — acceptable either way. expect(out).toMatch(/--verbose|—verbose/); }); }); // ─── sanitizer ────────────────────────────────────────────── describe("sanitizeUntrustedHtml", () => { test("strips

world

`; const out = sanitizeUntrustedHtml(input); expect(out).not.toContain("hello

"); expect(out).toContain("

world

"); }); test("strips `; expect(sanitizeUntrustedHtml(input)).not.toContain(" { const input = `click`; const out = sanitizeUntrustedHtml(input); expect(out).not.toContain("onclick"); expect(out).toContain("href=\"#\""); }); test("strips event handlers with mixed case (onClick, ONCLICK)", () => { const input1 = `a`; const input2 = `b`; expect(sanitizeUntrustedHtml(input1)).not.toContain("onClick"); expect(sanitizeUntrustedHtml(input2)).not.toContain("ONCLICK"); }); test("rewrites javascript: URLs in href to #", () => { const input = `bad`; const out = sanitizeUntrustedHtml(input); expect(out).not.toContain("javascript:"); expect(out).toContain('href="#"'); }); test("strips inline SVG `; const out = sanitizeUntrustedHtml(input); expect(out).not.toContain(", , , , ,
", () => { const input = `
`; const out = sanitizeUntrustedHtml(input); expect(out).not.toContain(" { const input = `
hi
`; expect(sanitizeUntrustedHtml(input)).not.toContain("srcdoc"); }); }); // ─── end-to-end render ────────────────────────────────────────────── describe("render (end-to-end)", () => { test("produces a full HTML document with title, body, and CSS", () => { const result = render({ markdown: `# Hello\n\nA paragraph with "quotes" and -- dashes.\n`, }); expect(result.html).toContain(""); expect(result.html).toContain("Hello"); expect(result.html).toContain("... expect(result.html).toMatch(/