/** * 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("
")}
${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(`