// Renders formatted text from the content module through an allow-list: paragraphs, line breaks, // bold, italic, lists and safe links only. Everything else is dropped, so pasted content can // never inject scripts or styles into the site. const ALLOWED = new Set(['p', 'br', 'strong', 'b', 'em', 'i', 'ul', 'ol', 'li', 'a']) const SAFE_LINK = /^(\/|#|https:\/\/|mailto:|tel:)/i export function sanitize(html: string): string { return (html || '') .replace(/<(script|style|iframe|object|embed|svg|math)[\s\S]*?<\/\1>/gi, '') .replace(//g, '') .replace(/<\s*(\/?)\s*([a-z0-9]+)([^>]*)>/gi, (_m, close: string, tag: string, attrs: string) => { const t = tag.toLowerCase() if (!ALLOWED.has(t)) return '' if (close) return t === 'br' ? '' : `` if (t === 'a') { const href = (attrs.match(/href\s*=\s*"([^"]*)"/i) || attrs.match(/href\s*=\s*'([^']*)'/i) || [])[1] || '' if (!SAFE_LINK.test(href.trim())) return '' const external = /^https:\/\//i.test(href) const safe = href.replace(/"/g, '"') return external ? `` : `` } return `<${t}>` }) } export function RichText({ html, className }: { html: string; className?: string }) { if (!html) return null return
} // Plain text with \n turned into line breaks (for headings that allow two lines). export function Lines({ text }: { text: string }) { const parts = (text || '').split(/\\n|\n/) return ( <> {parts.map((part, i) => ( {part} {i < parts.length - 1 &&
}
))} ) }