/**
* 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;
}