// A report's timeline as feeds (./feeds.ts): RSS 2.0 and JSON Feed 1.1 — // newest first, every link absolute, everything escaped. import { test } from "node:test"; import assert from "node:assert/strict"; import { JSON_FEED_VERSION, escapeXml, markdownPlainText, renderReportJsonFeed, renderReportRss, reportFeedUrls, rfc3339Date, rfc822Date, } from "./feeds"; import type { Report } from "./schema"; import { validateReport } from "./validate"; import { buildReportPageView, type ReportPageView } from "./views"; const SITE = "https://site.example"; function report(): Report { return { format: "archilyzer-report", version: 1, id: "living", kind: "sweep", series: "Watch", title: "Bits & ", summary: "It *starts* [here](cite:v1).", published: "2026-10-01", citations: { v1: { kind: "video", channel: "demo-channel", id: "abc123", start: 10, end: 20, quote: "one" }, w1: { kind: "page", url: "https://example.org/p", title: "A page", quote: "two" }, }, entries: [ { id: "older", date: "2026-10-02", title: "First & \"quoted\"", body: "It [began](cite:w1) here." }, { id: "newer", date: "2026-10-05T09:30:00Z", updated: "2026-10-06", title: "Second", body: "See [the section](#s1), [the index](/reports/) and [one](cite:v1).", }, ], sections: [{ id: "s1", title: "Section", body: "A body." }], }; } const view = (r: Report = report()): ReportPageView => { assert.deepEqual(validateReport(r), []); return buildReportPageView(r, { record: (c) => ({ channel: c.channel, id: c.id }), feeds: reportFeedUrls(SITE, r.id), }); }; test("feed URLs beside the report's page", () => { assert.deepEqual(reportFeedUrls(SITE, "living"), { rss: "https://site.example/reports/living/feed.xml", json: "https://site.example/reports/living/feed.json", }); }); test("escaping: the five XML escapes, and the characters XML forbids dropped", () => { assert.equal(escapeXml(`a & b < c > d "e" 'f'`), "a & b < c > d "e" 'f'"); assert.equal(escapeXml("bell\u0007 tab\t ok"), "bell tab\t ok"); assert.equal(markdownPlainText("It *starts* [here](cite:v1).\n\n> quoted `code`"), "It starts here. quoted code"); }); test("dates: a day is midnight UTC, RFC 822 for RSS and RFC 3339 for JSON Feed", () => { assert.equal(rfc822Date("2026-10-02"), "Fri, 02 Oct 2026 00:00:00 GMT"); assert.equal(rfc822Date("2026-10-05T11:30:00+02:00"), "Mon, 05 Oct 2026 09:30:00 GMT"); assert.equal(rfc3339Date("2026-10-02"), "2026-10-02T00:00:00.000Z"); assert.equal(rfc822Date("nope"), undefined); assert.equal(rfc3339Date(undefined), undefined); }); test("RSS 2.0: the channel, then one item per entry newest first, absolute links, escaped", () => { const xml = renderReportRss(view(), { siteUrl: SITE }); assert.match(xml, /^<\?xml version="1\.0" encoding="UTF-8"\?>\n\n\n/); assert.match(xml, /Watch: Bits & <pieces><\/title>/); assert.match(xml, /<link>https:\/\/site\.example\/reports\/living\/<\/link>/); // No subtitle: the summary as plain text. assert.match(xml, /<description>It starts here\.<\/description>/); assert.match(xml, /<atom:link href="https:\/\/site\.example\/reports\/living\/feed\.xml" rel="self" type="application\/rss\+xml"\/>/); // The newest entry's update is the report's. assert.match(xml, /<lastBuildDate>Tue, 06 Oct 2026 00:00:00 GMT<\/lastBuildDate>/); const items = [...xml.matchAll(/<item>([\s\S]*?)<\/item>/g)].map((m) => m[1]); assert.equal(items.length, 2); assert.match(items[0], /<title>Second<\/title>/); assert.match(items[0], /<link>https:\/\/site\.example\/reports\/living\/#newer<\/link>/); assert.match(items[0], /<guid isPermaLink="true">https:\/\/site\.example\/reports\/living\/#newer<\/guid>/); assert.match(items[0], /<pubDate>Mon, 05 Oct 2026 09:30:00 GMT<\/pubDate>/); assert.match(items[1], /<title>First & "quoted"<\/title>/); // The body is HTML, escaped as text: a fragment link to the page, a // site-root link to the site, a citation's marker to its reference on the page. assert.match(items[0], /<a href="https:\/\/site\.example\/reports\/living\/#s1">the section<\/a>/); assert.match(items[0], /<a href="https:\/\/site\.example\/reports\/">the index<\/a>/); assert.match(items[0], /one<sup class="cite"><a href="https:\/\/site\.example\/reports\/living\/#c-v1">\[1\]/); // Raw HTML in an entry is text, escaped twice over in the feed. assert.match(items[1], /&lt;b&gt;here&lt;\/b&gt;/); assert.doesNotMatch(xml, /<b>/); assert.match(xml, /<\/channel>\n<\/rss>\n$/); }); test("JSON Feed 1.1: the same items newest first, ids their permalinks, dates RFC 3339", () => { const feed = renderReportJsonFeed(view(), { siteUrl: SITE }); assert.equal(feed.version, JSON_FEED_VERSION); assert.equal(feed.title, "Watch: Bits & <pieces>"); assert.equal(feed.home_page_url, "https://site.example/reports/living/"); assert.equal(feed.feed_url, "https://site.example/reports/living/feed.json"); assert.equal(feed.description, "It starts here."); assert.deepEqual( feed.items.map((i) => [i.id, i.url, i.title, i.date_published, i.date_modified]), [ ["https://site.example/reports/living/#newer", "https://site.example/reports/living/#newer", "Second", "2026-10-05T09:30:00.000Z", "2026-10-06T00:00:00.000Z"], ["https://site.example/reports/living/#older", "https://site.example/reports/living/#older", "First & \"quoted\"", "2026-10-02T00:00:00.000Z", undefined], ], ); assert.equal( feed.items[1].content_html, '<p>It began<sup class="cite"><a href="https://site.example/reports/living/#c-w1">[2]</a></sup> <b>here</b>.</p>', ); assert.equal("date_modified" in feed.items[1], false); // A round trip through JSON is the same feed. assert.deepEqual(JSON.parse(JSON.stringify(feed)), feed); }); test("the description: the subtitle when there is one, else the title when there is no summary", () => { assert.equal(renderReportJsonFeed(view({ ...report(), subtitle: "A line" }), { siteUrl: SITE }).description, "A line"); assert.equal(renderReportJsonFeed(view({ ...report(), summary: undefined }), { siteUrl: SITE }).description, "Watch: Bits & <pieces>"); });