export function esc(s: string): string {
return s
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """);
}
/** cgit-style relative age with its color class, e.g. "3 days" / age-days. */
export function age(unixSecs: number): string {
if (!unixSecs) return "";
const secs = Math.max(0, Math.floor(Date.now() / 1000) - unixSecs);
const fmt = (n: number, unit: string, cls: string) =>
`${n} ${unit}${n === 1 ? "" : unit.endsWith(".") ? "" : "s"}`;
if (secs < 3600) return fmt(Math.max(1, Math.floor(secs / 60)), "min.", "mins");
if (secs < 24 * 3600) return fmt(Math.floor(secs / 3600), "hour", "hours");
if (secs < 7 * 24 * 3600) return fmt(Math.floor(secs / (24 * 3600)), "day", "days");
if (secs < 31 * 24 * 3600) return fmt(Math.floor(secs / (7 * 24 * 3600)), "week", "weeks");
if (secs < 365 * 24 * 3600) return fmt(Math.floor(secs / (30 * 24 * 3600)), "month", "months");
return fmt(Math.floor(secs / (365 * 24 * 3600)), "year", "years");
}
export function fmtDate(unixSecs: number, tz: string): string {
const d = new Date(unixSecs * 1000);
const pad = (n: number) => String(n).padStart(2, "0");
return (
`${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())} ` +
`${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())} ${tz}`
);
}
const DAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
/** RFC 2822 date in the person's own timezone (for patch/atom output). */
export function fmtDate2822(unixSecs: number, tz: string): string {
const offMin = (parseInt(tz.slice(0, 3), 10) || 0) * 60 + (parseInt(tz.slice(0, 1) + tz.slice(3), 10) || 0);
const d = new Date((unixSecs + offMin * 60) * 1000);
const pad = (n: number) => String(n).padStart(2, "0");
return (
`${DAYS[d.getUTCDay()]}, ${d.getUTCDate()} ${MONTHS[d.getUTCMonth()]} ${d.getUTCFullYear()} ` +
`${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())} ${tz}`
);
}
export interface LayoutOpts {
site: string;
siteDesc: string;
title: string;
/** repo name when rendering a repo page */
repo?: string;
/** description line under the title */
sub: string;
/** active tab id */
tab?: string;
/** current ref for tab links */
ref?: string;
/** breadcrumb path html (tree pages) */
pathBar?: string;
/** show the about tab (repo has a README) */
hasAbout?: boolean;
/** branch names for the switcher dropdown */
branches?: string[];
/** form action for the branch switcher (current page path) */
formAction?: string;
body: string;
}
export function layout(o: LayoutOpts): string {
const r = o.repo ? `/${encodeURIComponent(o.repo)}` : "";
const q = o.ref ? `?h=${encodeURIComponent(o.ref)}` : "";
const tabs: [string, string][] = o.repo
? [
...(o.hasAbout ? [["about", `${r}/about/${q}`] as [string, string]] : []),
["summary", `${r}/${q}`],
["refs", `${r}/refs/${q}`],
["log", `${r}/log/${q}`],
["tree", `${r}/tree/${q}`],
["commit", `${r}/commit/${q}`],
["diff", `${r}/diff/${q}`],
["stats", `${r}/stats/${q}`],
]
: [["index", "/"]];
const tabHtml = tabs
.map(([id, href]) => `${id}`)
.join("");
const crumb = o.repo ? ` : ${esc(o.repo)}` : "";
const switcher =
o.repo && o.branches && o.branches.length
? `
| `
: "";
const atom = o.repo
? ``
: "";
return `
${esc(o.title)}
${atom}
${o.pathBar ? `
${o.pathBar}
` : ""}
${o.body}
`;
}
export function htmlResponse(html: string, status = 200): Response {
return new Response(html, {
status,
headers: { "content-type": "text/html; charset=utf-8" },
});
}
export function errorPage(o: Omit, msg: string, status = 404): Response {
return htmlResponse(layout({ ...o, body: `${esc(msg)}
` }), status);
}