import fs from "node:fs/promises"; import path from "node:path"; import { FONT_DEBUG_ASSET_PATH } from "./font-debug.js"; export interface PublicPage { route: string; title: string; html: string; } const PUBLIC_PAGE_FILES = new Map([ ["/", "index.md"], ["/docs", "docs/index.md"], ["/docs/architecture", "docs/architecture.md"], ["/docs/security", "docs/security.md"], ]); const MAX_PUBLIC_FILE_BYTES = 64 * 1024; const MAX_PUBLIC_FONT_BYTES = 64 * 1024; const CODE_URL = "https://tangled.org/@cameron.stream/thought-stream"; export async function loadPublicPages(projectRoot: string): Promise> { const publicRoot = path.resolve(projectRoot, "public"); const fontPath = path.resolve(publicRoot, "assets/around-regular.woff2"); if (!fontPath.startsWith(`${publicRoot}${path.sep}`)) throw new Error("Public font path escapes the allowlisted root"); const fontStat = await fs.lstat(fontPath); if (!fontStat.isFile() || fontStat.isSymbolicLink() || fontStat.size > MAX_PUBLIC_FONT_BYTES) { throw new Error("Invalid public Around font"); } const font = await fs.readFile(fontPath); if (font.length < 4 || font.subarray(0, 4).toString("ascii") !== "wOF2") { throw new Error("Invalid public Around font"); } const fontDataUrl = `data:font/woff2;base64,${font.toString("base64")}`; const pages = new Map(); for (const [route, relativePath] of PUBLIC_PAGE_FILES) { const filePath = path.resolve(publicRoot, relativePath); if (filePath !== publicRoot && !filePath.startsWith(`${publicRoot}${path.sep}`)) { throw new Error("Public content path escapes the allowlisted root"); } const stat = await fs.lstat(filePath); if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_PUBLIC_FILE_BYTES) { throw new Error(`Invalid public content file: ${relativePath}`); } const markdown = await fs.readFile(filePath, "utf8"); const title = firstHeading(markdown); pages.set(route, { route, title, html: renderPage(route, title, markdown, fontDataUrl) }); } return pages; } export function publicPageForRoute(pages: Map, pathname: string): PublicPage | undefined { const normalized = pathname.length > 1 && pathname.endsWith("/") ? pathname.slice(0, -1) : pathname; return pages.get(normalized); } function renderPage(route: string, title: string, markdown: string, fontDataUrl: string): string { const body = renderMarkdown(markdown); if (route === "/") return renderLandingPage(title, body, fontDataUrl); return renderDocumentationPage(title, body, fontDataUrl); } function renderLandingPage(title: string, body: string, fontDataUrl: string): string { return ` ${escapeHtml(title)}
${body}
`; } function renderDocumentationPage(title: string, body: string, fontDataUrl: string): string { return ` ${escapeHtml(title)} ยท Stream
Stream
${body}
`; } function renderMarkdown(markdown: string): string { const blocks: string[] = []; const paragraph: string[] = []; const list: string[] = []; const flushParagraph = () => { if (paragraph.length === 0) return; blocks.push(`

${inlineMarkup(paragraph.join(" "))}

`); paragraph.length = 0; }; const flushList = () => { if (list.length === 0) return; blocks.push(`
    ${list.map((item) => `
  • ${inlineMarkup(item)}
  • `).join("")}
`); list.length = 0; }; for (const rawLine of markdown.replaceAll("\r\n", "\n").split("\n")) { const line = rawLine.trim(); const heading = line.match(/^(#{1,2})\s+(.+)$/); const item = line.match(/^[-*]\s+(.+)$/); if (heading) { flushParagraph(); flushList(); const level = heading[1]!.length; blocks.push(`${inlineMarkup(heading[2]!)}`); } else if (item) { flushParagraph(); list.push(item[1]!); } else if (line === "") { flushParagraph(); flushList(); } else { flushList(); paragraph.push(line); } } flushParagraph(); flushList(); return blocks.join("\n"); } function inlineMarkup(value: string): string { return escapeHtml(value).replaceAll(/`([^`]+)`/g, "$1"); } function firstHeading(markdown: string): string { const heading = markdown.match(/^#\s+(.+)$/m)?.[1]?.trim(); if (!heading) throw new Error("Public content requires one level-one heading"); return heading; } function escapeHtml(value: string): string { return value .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); }