// A REPORT'S SLIDES AS ONE HTML FILE — `slides.html`, and printed one 16:9
// page per slide, `slides.pdf` (publish/reportExports.ts). The deck is
// lib/report/slides.ts buildReportSlides of the report's page view — the same
// slides the site's Slides view shows — drawn with the same stylesheet
// (common/styles/slides.css, passed in as `css`, inlined whole).
//
// THE MARKUP MIRRORS common/components/report/slides/Slide.tsx, class for
// class, as a string: this file is built by publish/reportExports.ts, which
// the editor's server bundles, where React's server renderer is not to be
// imported. Change one, change the other (exportSlides.test.ts holds the
// classes the stylesheet styles).
//
// ONE FILE, NO NETWORK: its stylesheet and a few lines of script (fit each
// slide to the window; ← → PgUp PgDn Home End step) inline; every picture
// (a clip's poster, a post's capture, a sentence's still) whatever `image`
// answers — a data: URI, or a relative path in the evidence pack. Colours are
// the light base's, in the site's accent (`accent`, its light value). Each
// citation's number links to the reference list under the deck (on screen;
// the print is the slides alone); "Read this in the article" goes to the
// article on the site, when it has a public URL.
//
// PURE: no I/O, deterministic for its input.
import { citationAnchor } from "../citations/inline";
import { escapeHtml, markdownToHtml, reportExportFooterLine, siteLink, type ReportExportFooter } from "./exportHtml";
import {
buildReportSlides,
slidePointText,
type ClaimSlide,
type EntrySlide,
type FoundSlide,
type SectionSlide,
type SlideView,
type SourcesSlide,
type SummarySlide,
type TitleSlide,
} from "./slides";
import { VERDICT_DEFAULTS, type Verdict, type VerdictStyle } from "./verdicts";
import {
CITATION_KIND_LABELS,
orderedCitations,
reportFullTitle,
reportPagePath,
spanLabel,
type CitationView,
type ReportPageView,
} from "./views";
export type ReportSlidesHtmlOptions = {
// common/styles/slides.css, whole.
css: string;
// The site's accent on the light base (lib/accent.ts resolveAccent().light).
accent: string;
siteUrl?: string;
footer: ReportExportFooter;
// A site-root image path → the , or undefined to leave it out.
image: (sitePath: string) => string | undefined;
};
const style = (vars: Record) => {
const s = Object.entries(vars)
.filter(([, v]) => v !== undefined)
.map(([k, v]) => `${k}:${v}`)
.join(";");
return s ? ` style="${escapeHtml(s)}"` : "";
};
const verdictStyleOf = (view: ReportPageView, v: Verdict): VerdictStyle => view.verdicts?.[v] ?? VERDICT_DEFAULTS[v];
// The site-root paths of the pictures the slides show (each slide's cited
// evidence): with viewImagePaths', what an export must inline or pack.
export function slidesImagePaths(view: ReportPageView): string[] {
const out = new Set();
for (const s of buildReportSlides(view)) {
// A quote shows its words, never its picture.
const id = (s.kind === "claim" || s.kind === "section") && s.layout !== "quote" ? s.cite : undefined;
const c = id ? view.citations[id] : undefined;
const pic = c ? pictureOf(c) : undefined;
if (pic) out.add(pic.src);
}
return [...out].sort();
}
function pictureOf(c: CitationView): { src: string; alt: string } | undefined {
if ((c.kind === "video" || c.kind === "audio") && c.poster) return { src: c.poster, alt: "" };
if (c.kind === "post" && c.shot) return { src: c.shot, alt: `Screenshot of the post${c.author ? ` by ${c.author}` : ""}` };
if (c.kind === "source" && c.image) return { src: c.image, alt: `The sentence as ${c.sourceTitle} shows it: ${c.quote}` };
return undefined;
}
// What CitationCard's heading says: a citation's title and its meta.
function heading(c: CitationView): { title: string; meta: string[] } {
const host = (url: string) => {
try {
return new URL(url).hostname.replace(/^www\./, "");
} catch {
return url;
}
};
switch (c.kind) {
case "video":
case "audio":
return {
title: c.record.title ?? `${c.record.channel}/${c.record.id}`,
meta: [c.record.channelTitle ?? c.record.channel, c.date ?? c.record.date, spanLabel(c.start, c.end)].filter(
(x): x is string => !!x,
),
};
case "post":
return {
title: c.author ?? c.record.channelTitle ?? c.record.channel,
meta: [c.author ? (c.record.channelTitle ?? c.record.channel) : undefined, c.date ?? c.record.date].filter(
(x): x is string => !!x,
),
};
case "source":
return { title: c.sourceTitle, meta: [c.date].filter((x): x is string => !!x) };
case "page":
return { title: c.title ?? host(c.href), meta: [host(c.href), c.date].filter((x): x is string => !!x) };
}
}
// Where a citation is, off this file: its moment page on the site, the
// document, the web page.
function citationUrl(c: CitationView, siteUrl: string | undefined): string | undefined {
if (c.kind === "video" || c.kind === "audio" || c.kind === "post") return siteLink(siteUrl, c.href) ?? c.record.originalUrl;
return c.href;
}
type Ctx = { view: ReportPageView; opts: ReportSlidesHtmlOptions; total: number };
function md(text: string, x: Ctx): string {
const cite = (id: string) => {
const c = x.view.citations[id];
return c ? { number: c.number, anchor: citationAnchor(id) } : undefined;
};
return `