const ALLOWED_TAGS = new Set([ "a", "b", "blockquote", "br", "code", "em", "h1", "h2", "h3", "h4", "h5", "h6", "i", "li", "ol", "p", "pre", "s", "strong", "strike", "u", "ul", ]); const BLOCKED_TAGS = /<(script|style|iframe|object|embed|svg|math|template)[\s\S]*?<\/\1>/gi; const TAG_RE = /<\/?([a-zA-Z][a-zA-Z0-9-]*)([^>]*)>/g; const ATTR_RE = /([a-zA-Z_:][a-zA-Z0-9_:.-]*)\s*=\s*("[^"]*"|'[^']*'|[^\s"'=<>`]+)/g; export function sanitizeUrl(url: string) { const trimmed = url.trim(); if (!trimmed) return ""; try { const parsed = new URL(trimmed, "https://erika.sudloh.com"); if (parsed.protocol === "http:" || parsed.protocol === "https:" || parsed.protocol === "mailto:") { return trimmed; } } catch { return ""; } return ""; } function escapeAttribute(value: string) { return value .replace(/&/g, "&") .replace(/"/g, """) .replace(//g, ">"); } export function sanitizeHtml(html: string) { return html.replace(BLOCKED_TAGS, "").replace(TAG_RE, (tag, rawName: string, rawAttrs: string) => { const name = rawName.toLowerCase(); if (!ALLOWED_TAGS.has(name)) return ""; if (tag.startsWith("`; } if (name !== "a") { return `<${name}>`; } let href = ""; for (const match of rawAttrs.matchAll(ATTR_RE)) { if (match[1].toLowerCase() === "href") { href = sanitizeUrl(match[2].replace(/^["']|["']$/g, "")); break; } } return href ? `` : ""; }); }