import { readFileSync } from "node:fs" import path from "node:path" import { createHash } from "node:crypto" import { loadBrand, type Brand } from "./config.ts" import { parseSlots, type SlotSpec } from "./slot-parser.ts" import { resolveSlotContent, renderSlotSvg, renderSlotBackground } from "./resolve.ts" export type RenderArgs = { template: string title?: string subtitle?: string slots?: Record out?: string } export type RenderResult = { path: string hash: string status: "rendered" | "skipped" } const SLOT_COMMENT_RE = /\n?/g export function buildSvg( templateSvg: string, brand: Brand, args: RenderArgs, drawImageDir: string, ): string { const slots = parseSlots(templateSvg) let svg = templateSvg const slotSvgs: string[] = [] for (const slot of slots) { const value = args.slots?.[slot.name] if (value) { const bg = renderSlotBackground(slot, brand) if (bg) slotSvgs.push(bg) const resolved = resolveSlotContent(slot, brand, drawImageDir, value) if (resolved) slotSvgs.push(renderSlotSvg(slot, resolved)) } } svg = svg.replace(SLOT_COMMENT_RE, "") svg = svg.replace(/\{\{base\}\}/g, brand.base) svg = svg.replace(/\{\{surface\}\}/g, brand.surface) svg = svg.replace(/\{\{fg\}\}/g, brand.fg) svg = svg.replace(/\{\{muted\}\}/g, brand.muted) svg = svg.replace(/\{\{accent\}\}/g, brand.accent) svg = svg.replace(/\{\{line\}\}/g, brand.line) svg = svg.replace(/\{\{title\}\}/g, escapeXml(args.title ?? "")) if (args.subtitle) { svg = svg.replace(/\{\{subtitle\}\}/g, escapeXml(args.subtitle)) } else { svg = svg.replace(/[^\n]*\{\{subtitle\}\}[^\n]*\n?/g, "") } const insertPoint = svg.indexOf("") if (insertPoint === -1) return svg return svg.slice(0, insertPoint) + slotSvgs.join("\n") + "\n" + svg.slice(insertPoint) } export function computeHash(brand: Brand, templateSvg: string, args: RenderArgs): string { const data = JSON.stringify({ brand, template: templateSvg, template_name: args.template, title: args.title ?? "", subtitle: args.subtitle ?? "", slots: args.slots ?? {}, out: args.out ?? "", }) return createHash("sha256").update(data).digest("hex") } export function loadTemplate(drawImageDir: string, name: string): string { const templatePath = path.join(drawImageDir, "templates", `${name}.svg`) return readFileSync(templatePath, "utf-8") } function escapeXml(s: string): string { return s .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'") }