/** * 剥离 HTML 标签,提取纯文本 * - 自动将 // 替换为 [图片]/[视频]/[音频] 占位 * - 自动解码 & 等 HTML entity * - 自动合并多余空白 * - 可按需截断 */ export function stripHtml(html: string | null | undefined, maxLen?: number): string { if (!html) return ''; let text: string = html; // 1. 媒体标签替换为占位符(保留语义提示) text = text .replace(/]*>/gi, '[图片]') .replace(/]*>.*?<\/video>/gi, '[视频]') .replace(/]*>.*?<\/audio>/gi, '[音频]') .replace(/]*>.*?<\/iframe>/gi, '[嵌入内容]'); // 2. 剥离所有剩余 HTML 标签( ...) text = text.replace(/<[^>]+>/g, ''); // 3. HTML entity 解码 text = decodeHtmlEntity(text); // 4. 合并多余空白(多个空格/换行 → 单个空格) text = text.replace(/\s+/g, ' ').trim(); // 5. 截断 if (maxLen && text.length > maxLen) { text = text.slice(0, maxLen) + '...'; } return text; } /** * 解码 HTML 实体 * 浏览器环境用 DOMParser,非浏览器环境用正则 fallback */ function decodeHtmlEntity(str: string): string { if (typeof DOMParser === 'undefined') { const entityMap: Record = { ' ': ' ', '&': '&', '<': '<', '>': '>', '"': '"', ''': "'", ''': "'", '/': '/', '©': '©', '®': '®' }; return str.replace(/&[a-z]+;|\d+;|[0-9a-f]+;/gi, (match) => { if (entityMap[match]) return entityMap[match]; const num = match.match(/(\d+);/)?.[1]; if (num) return String.fromCharCode(Number(num)); const hex = match.match(/([0-9a-f]+);/i)?.[1]; if (hex) return String.fromCharCode(parseInt(hex, 16)); return match; }); } const doc = new DOMParser().parseFromString(str, 'text/html'); return doc.documentElement.textContent ?? ''; }