45 lines
1.7 KiB
TypeScript
45 lines
1.7 KiB
TypeScript
/**
|
|
* 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, '<').replace(/>/g, '>');
|
|
|
|
const blocks: string[] = [];
|
|
let listItems: string[] = [];
|
|
const flushList = () => {
|
|
if (listItems.length) {
|
|
blocks.push(`<ul>${listItems.map((li) => `<li>${li}</li>`).join('')}</ul>`);
|
|
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('<br/>');
|
|
blocks.push(`<p>${inline.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')}</p>`);
|
|
}
|
|
flushList();
|
|
return blocks.join('');
|
|
}
|