/** 去除 HTML 标签,用于列表摘要与跑马灯纯文本 */ export function stripHtml(html: string): string { if (!html) return ''; if (!/[<>]/.test(html)) return html.trim(); const doc = new DOMParser().parseFromString(html, 'text/html'); return (doc.body.textContent ?? '').replace(/\s+/g, ' ').trim(); } /** 判断富文本是否实质为空 */ export function isHtmlEmpty(html: string): boolean { return !stripHtml(html); } const ALLOWED_TAGS = new Set([ 'p', 'br', 'strong', 'b', 'em', 'i', 'u', 'ul', 'ol', 'li', 'img', 'a', 'h2', 'h3', 'blockquote', 'div', 'span', ]); function sanitizeNode(node: Node): Node | null { if (node.nodeType === Node.TEXT_NODE) { return node.cloneNode(false); } if (node.nodeType !== Node.ELEMENT_NODE) return null; const el = node as HTMLElement; const tag = el.tagName.toLowerCase(); if (!ALLOWED_TAGS.has(tag)) { const frag = document.createDocumentFragment(); for (const child of Array.from(el.childNodes)) { const safe = sanitizeNode(child); if (safe) frag.appendChild(safe); } return frag; } const out = document.createElement(tag); if (tag === 'img') { const src = el.getAttribute('src')?.trim(); if (!src || /^javascript:/i.test(src)) return null; out.setAttribute('src', src); const alt = el.getAttribute('alt'); if (alt) out.setAttribute('alt', alt); return out; } if (tag === 'a') { const href = el.getAttribute('href')?.trim(); if (!href || /^javascript:/i.test(href)) return null; out.setAttribute('href', href); out.setAttribute('target', '_blank'); out.setAttribute('rel', 'noopener noreferrer'); } for (const child of Array.from(el.childNodes)) { const safe = sanitizeNode(child); if (safe) out.appendChild(safe); } return out; } /** 富文本 HTML 白名单净化(公告/站内信预览) */ export function sanitizeAnnouncementHtml(html: string): string { if (!html?.trim()) return ''; if (!/[<>]/.test(html)) return html; const doc = new DOMParser().parseFromString(html, 'text/html'); const container = document.createElement('div'); for (const child of Array.from(doc.body.childNodes)) { const safe = sanitizeNode(child); if (safe) container.appendChild(safe); } return container.innerHTML; }