/** * Ensures a description renders nicely as HTML in the storefront. * - If it already contains HTML tags, it is returned as-is (sanitized). * - Plain text (e.g. older AI-generated descriptions) is converted into * paragraphs and lists so it doesn't render as a wall of text. */ export function formatRichText(raw: string | null | undefined): string { const text = (raw ?? '').trim(); if (!text) return ''; if (/<\s*(p|ul|ol|li|h[1-6]|div|br|strong|em|b|i|a|table|blockquote)\b/i.test(text)) { return text .replace(/<\s*(script|style|iframe|object|embed|form)\b[\s\S]*?<\s*\/\s*\1\s*>/gi, '') .replace(/<\s*(script|style|iframe|object|embed|form|link|meta)\b[^>]*\/?>/gi, '') .replace(/\son\w+\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi, '') .replace(/javascript\s*:/gi, ''); } const escape = (s: string) => s.replace(/&/g, '&').replace(//g, '>'); const blocks: string[] = []; let listItems: string[] = []; const flushList = () => { if (listItems.length) { blocks.push(``); listItems = []; } }; for (const paragraph of text.split(/\n{2,}/)) { const lines = paragraph.split('\n').map((l) => l.trim()).filter(Boolean); if (!lines.length) continue; if (lines.every((l) => /^[-*•·]\s+/.test(l))) { for (const line of lines) listItems.push(escape(line.replace(/^[-*•·]\s+/, ''))); continue; } flushList(); const inline = lines.map((line) => escape(line.replace(/^[-*•·]\s+/, ''))).join('
'); blocks.push(`

${inline.replace(/\*\*([^*]+)\*\*/g, '$1')}

`); } flushList(); return blocks.join(''); }