/** * packages/shared โ€” minimal, XSS-safe markdown โ†’ HTML renderer. * * Used by the SEND_EMAIL automation action (TASK-0112 ยง7) to render the * (interpolated) markdown body to HTML. Deliberately small + safe: ALL input * is HTML-escaped FIRST, then a fixed set of inline/block transforms is * applied on the escaped text, so no raw HTML from the body can survive. * * Supported: #/##/### headings, **bold**, *italic*, `code`, [text](url), * unordered lists (- / *), ordered lists (1.), paragraphs + line breaks. * Everything else passes through as plain text (escaped). */ function esc(s: string): string { return s .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } /** Only allow http(s) link targets (defang javascript: etc.). */ function safeHref(href: string): string { const h = href.trim(); return /^https?:\/\//i.test(h) ? h : "#"; } /** * Render markdown to HTML. Pure + total โ€” any string in, a safe string out. */ export function renderMarkdown(md: string): string { const src = md.replace(/\r\n/g, "\n"); const lines = src.split("\n"); const out: string[] = []; let inUl = false; let inOl = false; let para: string[] = []; const closeLists = () => { if (inUl) out.push(""); if (inOl) out.push(""); inUl = inOl = false; }; const flushPara = () => { if (para.length) { out.push(`

${para.map(inline).join("
")}

`); para = []; } }; const inline = (s: string): string => { let t = esc(s); // inline code (protect from further transforms) t = t.replace(/`([^`]+)`/g, (_m, c) => `${c}`); // bold then italic t = t.replace(/\*\*([^*]+)\*\*/g, "$1"); t = t.replace(/(^|[^*])\*([^*\n]+)\*/g, "$1$2"); // links t = t.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_m, text, href) => `${text}`); return t; }; for (const raw of lines) { const line = raw; const h = /^(#{1,3})\s+(.*)$/.exec(line); if (h) { flushPara(); closeLists(); const level = h[1]!.length; out.push(`${inline(h[2]!)}`); continue; } const ul = /^\s*[-*]\s+(.*)$/.exec(line); if (ul) { flushPara(); if (inOl) closeLists(); if (!inUl) { out.push("