// Renders a comment's stored plain text to HTML, on the server.//// Comments are stored as PLAIN TEXT, not HTML - a deliberate difference from// wilhelm-scream-db, which stores sanitized rich-text HTML from a contentEditable// editor. On a site about mathematics the thing people need in a comment is// math, not bold and underline, so comments go through the same KaTeX path as// entry statements and support `$inline$` / `$$display$$`.//// The security model follows from that: nothing the user writes is ever treated// as markup. Every non-math segment is HTML-escaped, and KaTeX runs with its// default `trust: false`, which disables \href and \includegraphics. So there is// no sanitizer to get wrong - the text simply never reaches the DOM as HTML. import katex from "katex";import { linkifyEscaped } from "@/lib/linkify";import { TEX_TOKENS, isDisplayMath, isInlineMath, mathBody, unescapeDollars,} from "@/lib/tex-tokens"; function escapeHtml(s: string): string { return s .replace(/&/g, "&") .replace(/</g, "<") .replace(/>/g, ">") .replace(/"/g, """);} function renderMath(tex: string, display: boolean): string { return katex.renderToString(tex, { throwOnError: false, displayMode: display, });} /// Plain text in, safe HTML out. Blank lines separate paragraphs; single/// newlines become line breaks.export function renderCommentHtml(text: string): string { return text .split(/\n{2,}/) .map((paragraph) => { const parts = paragraph.split(TEX_TOKENS); const inner = parts .map((part) => { if (isDisplayMath(part)) return renderMath(mathBody(part), true); if (isInlineMath(part)) return renderMath(mathBody(part), false); const linked = linkifyEscaped(escapeHtml(part)); return unescapeDollars(linked).replace(/\n/g, "<br />"); }) .join(""); return inner ? `<p>${inner}</p>` : ""; }) .join("");} /// Stable, locale-independent date for a comment. Formatted on the server and/// shipped as a string so it cannot cause a hydration mismatch.export function formatCommentDate(d: Date): string { const day = String(d.getUTCDate()).padStart(2, "0"); const months = [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", ]; return `${day} ${months[d.getUTCMonth()]} ${d.getUTCFullYear()}`;} /// The same date with the time on it, for anything that reads as a/// conversation.////// A date alone is enough to place an entry in the record, but it is not/// enough to follow an exchange: several comments a day is normal on an active/// thread, and "04 Aug 2026" three times in a row says nothing about what/// answered what. The admin queues had already worked this out and were each/// hand-assembling this string; they call this now instead.////// UTC, and labelled as such, for the same reason the date is: the value is/// formatted on the server and shipped as a string, so it cannot depend on the/// reader's timezone without risking a hydration mismatch. An explicit "UTC"/// beats a time that is silently somebody else's.export function formatCommentDateTime(d: Date): string { const hh = String(d.getUTCHours()).padStart(2, "0"); const mm = String(d.getUTCMinutes()).padStart(2, "0"); return `${formatCommentDate(d)}, ${hh}:${mm} UTC`;}
// Renders a comment's stored plain text to HTML, on the server.
//
// Comments are stored as PLAIN TEXT, not HTML - a deliberate difference from
// wilhelm-scream-db, which stores sanitized rich-text HTML from a contentEditable
// editor. On a site about mathematics the thing people need in a comment is
// math, not bold and underline, so comments go through the same KaTeX path as
// entry statements and support `$inline$` / `$$display$$`.
//
// The security model follows from that: nothing the user writes is ever treated
// as markup. Every non-math segment is HTML-escaped, and KaTeX runs with its
// default `trust: false`, which disables \href and \includegraphics. So there is
// no sanitizer to get wrong - the text simply never reaches the DOM as HTML.
import katex from "katex";
import { linkifyEscaped } from "@/lib/linkify";
import {
TEX_TOKENS,
isDisplayMath,
isInlineMath,
mathBody,
unescapeDollars,
} from "@/lib/tex-tokens";
function escapeHtml(s: string): string {
return s
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """);
}
function renderMath(tex: string, display: boolean): string {
return katex.renderToString(tex, {
throwOnError: false,
displayMode: display,
});
}
/// Plain text in, safe HTML out. Blank lines separate paragraphs; single
/// newlines become line breaks.
export function renderCommentHtml(text: string): string {
return text
.split(/\n{2,}/)
.map((paragraph) => {
const parts = paragraph.split(TEX_TOKENS);
const inner = parts
.map((part) => {
if (isDisplayMath(part)) return renderMath(mathBody(part), true);
if (isInlineMath(part)) return renderMath(mathBody(part), false);
const linked = linkifyEscaped(escapeHtml(part));
return unescapeDollars(linked).replace(/\n/g, "<br />");
})
.join("");
return inner ? `<p>${inner}</p>` : "";
})
.join("");
}
/// Stable, locale-independent date for a comment. Formatted on the server and
/// shipped as a string so it cannot cause a hydration mismatch.
export function formatCommentDate(d: Date): string {
const day = String(d.getUTCDate()).padStart(2, "0");
const months = [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
];
return `${day} ${months[d.getUTCMonth()]} ${d.getUTCFullYear()}`;
}
/// The same date with the time on it, for anything that reads as a
/// conversation.
///
/// A date alone is enough to place an entry in the record, but it is not
/// enough to follow an exchange: several comments a day is normal on an active
/// thread, and "04 Aug 2026" three times in a row says nothing about what
/// answered what. The admin queues had already worked this out and were each
/// hand-assembling this string; they call this now instead.
///
/// UTC, and labelled as such, for the same reason the date is: the value is
/// formatted on the server and shipped as a string, so it cannot depend on the
/// reader's timezone without risking a hydration mismatch. An explicit "UTC"
/// beats a time that is silently somebody else's.
export function formatCommentDateTime(d: Date): string {
const hh = String(d.getUTCHours()).padStart(2, "0");
const mm = String(d.getUTCMinutes()).padStart(2, "0");
return `${formatCommentDate(d)}, ${hh}:${mm} UTC`;
}