/** * Developer-authored template HTML handling. * * Pure module — safe to import client-side. HTML templates are restricted to * the developer role at the field level; this module sanitizes them (defense * in depth) and splits prepared markdown around sentinel markers emitted by * template expansion so raw HTML can be rendered without rehype-raw. */ const HTML_SENTINEL_RE = /PTF-TEMPLATE-HTML:(.+?):END/g; const HTML_DANGEROUS_TAGS_RE = /<\s*\/?\s*(script|iframe|object|embed|link|meta|base)\b[\s\S]*?(?:\/>|<\/\s*\1\s*>|$)/gi; const HTML_EVENT_ATTR_RE = /\son[a-z]+\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi; const HTML_JS_URL_RE = /\s(?:href|src|action|formaction)\s*=\s*("\s*javascript:[^"]*"|'\s*javascript:[^']*'|javascript:[^\s>]*)/gi; export function sanitizeTemplateHtml(html: string): string { return html .replace(HTML_DANGEROUS_TAGS_RE, "") .replace(HTML_EVENT_ATTR_RE, "") .replace(HTML_JS_URL_RE, ""); } export interface PreparedSegment { readonly type: "markdown" | "html"; readonly value: string; readonly name?: string; } export function splitPreparedHtml( prepared: string, htmlTemplates: Record, ): PreparedSegment[] { if ( Object.keys(htmlTemplates).length === 0 || !prepared.includes("PTF-TEMPLATE-HTML:") ) { return [{ type: "markdown", value: prepared }]; } const segments: PreparedSegment[] = []; let last = 0; for (const match of prepared.matchAll(HTML_SENTINEL_RE)) { const name = match[1].trim(); const html = htmlTemplates[name]; if (html === undefined) continue; if (match.index > last) { segments.push({ type: "markdown", value: prepared.slice(last, match.index) }); } segments.push({ type: "html", value: sanitizeTemplateHtml(html), name }); last = match.index + match[0].length; } if (last < prepared.length) { segments.push({ type: "markdown", value: prepared.slice(last) }); } return segments; }