// 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' ? '' : `${t}>`
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 &&
}
))}
>
)
}