// The report exports' renderers (exportHtml.ts, exportMarkdown.ts), from a // fixture view: deterministic, self-contained, numbered. // // Run with: node_modules/.bin/tsx --test lib/report/export.test.ts import { test } from "node:test"; import assert from "node:assert/strict"; import type { Report } from "./schema"; import { buildReportPageView, type RecordView } from "./views"; import { markdownToHtml, reportExportFooterLine, reportExportHtml, type ReportExportHtmlOptions } from "./exportHtml"; import { citedMarkdownToPlain, mdText, reportExportMarkdown } from "./exportMarkdown"; const report: Report = { format: "archilyzer-report", version: 1, id: "demo", kind: "factcheck", series: "Demo Checks", title: "A demo fact-check", subtitle: "Two claims, tested", summary: "It starts [here](cite:v1). and [a link](javascript:alert(1)).", method: "Each quote was checked against the recording.", published: "2026-10-01", updated: "2026-10-04T10:00:00Z", subject: { source: "s0" }, verdicts: { PARTLY: { label: "Half" } }, sources: { s0: { kind: "article", title: "An article", url: "https://example.org/a", author: "A. Writer", publisher: "Example Gazette", accent: "#aa3300", archives: [{ label: "archive", url: "https://archive.example.org/a", context: "the first edition" }], saved: "sources/s0/page.html", }, }, citations: { v1: { kind: "video", channel: "demo-channel", id: "abc123", start: 3126, end: 3151, quote: "one * two", origin: "added", verification: { quoteScore: 0.97, quoteCheckedAt: "2026-10-04T12:00:00Z" } }, a1: { kind: "audio", channel: "demo-podcast", id: "ep-1", start: 10.5, end: 20, quote: "two", speaker: "Guest", origin: "subject" }, p1: { kind: "post", channel: "demo-social", id: "123", quote: "three" }, s1: { kind: "source", source: "s0", quote: "four", image: "stills/s1.png" }, w1: { kind: "page", url: "https://example.org/p", title: "A page", quote: "five", archiveUrl: "https://archive.example.org/p" }, }, sections: [ { id: "one", title: "One", claims: [ { id: "c1", title: "One", text: "Claim one.", verdict: "CONTRADICTED", gist: "The recording says otherwise.", flag: "No source given", sourceQuote: { citation: "s1" }, findings: "See [this](cite:w1), **bold**, and `[code](cite:never)`.\n\n- a list item\n- another", citations: ["v1", "a1"], }, ], }, { id: "two", title: "Two", body: "A body citing [a post](cite:p1).", claims: [{ id: "c2", text: "Claim two.", verdict: "PARTLY", citations: ["a1"] }], }, ], }; const RECORDS: Record = { "demo-channel/abc123": { channel: "demo-channel", channelTitle: "Demo Channel", id: "abc123", title: "Demo stream", date: "2026-01-10", originalUrl: "https://media.example.org/abc123?t=3126" }, "demo-podcast/ep-1": { channel: "demo-podcast", channelTitle: "Demo Podcast", id: "ep-1", title: "Episode 1", date: "2026-02-02", originalUrl: "https://media.example.org/ep-1" }, "demo-social/123": { channel: "demo-social", channelTitle: "Demo Social", id: "123", date: "2025-03-14", originalUrl: "https://social.example.org/123" }, }; const view = buildReportPageView(report, { record: (c) => RECORDS[`${c.channel}/${c.id}`], post: () => ({ author: "@demo", text: "three", shot: "/media/posts/demo-social/123/shot.png" }), }); const PNG = "data:image/png;base64,iVBORw0KGgo="; const opts: ReportExportHtmlOptions = { siteUrl: "https://reports.example.org/", footer: { date: "2026-10-04", reportSha256: "0123456789abcdef".repeat(4) }, image: () => PNG, clip: (c) => ({ href: `https://reports.example.org/media/clips/${c.moment}.mp4`, kind: c.kind }), }; test("the HTML export is deterministic, holds no script, and every image is a data: URI", () => { const html = reportExportHtml(view, opts); assert.equal(reportExportHtml(view, opts), html); assert.doesNotMatch(html, /