// 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, /