import DOMPurify from 'dompurify'; import type { Config } from 'dompurify'; import { enhanceCodeBlocks } from './enhanceCodeBlocks'; import { enhanceHeadingAnchors } from './postHeadings'; /** DOMPurify 配置:允许会员专属自定义标签与链接 target */ export const POST_CONTENT_PURIFY_CONFIG: Config = { ADD_TAGS: ['members-only'], ADD_ATTR: ['data-locked', 'data-length', 'target', 'rel', 'data-code-copy', 'data-lang'], }; const LOCK_ICON_SVG = ``; /** 游客看到的锁定区块:流内嵌条 + 登录引导(精简高度) */ function buildLockedGateHtml(charLength: number): string { const lengthHint = charLength > 0 ? `约 ${charLength} 字` : '专属内容'; return `

登录后可见(${lengthHint})

作者将此段设为仅登录用户可读

`; } /** 判断 HTML 正文是否为空(忽略空段落等) */ export function isHtmlEmpty(html: string): boolean { if (!html.trim()) return true; const doc = new DOMParser().parseFromString( DOMPurify.sanitize(html, POST_CONTENT_PURIFY_CONFIG) as string, 'text/html', ); return (doc.body.textContent ?? '').trim().length === 0; } /** 根据登录状态渲染帖子正文 HTML */ export function renderPostContentHtml( html: string, isLoggedIn: boolean, opts?: { openLinksInNewTab?: boolean }, ): string { if (!html.trim()) return ''; const doc = new DOMParser().parseFromString( DOMPurify.sanitize(html, POST_CONTENT_PURIFY_CONFIG) as string, 'text/html', ); doc.querySelectorAll('members-only').forEach(el => { const locked = el.getAttribute('data-locked') === 'true' || !isLoggedIn; if (locked) { const charLength = parseInt(el.getAttribute('data-length') || '0', 10) || 0; el.setAttribute('data-locked', 'true'); el.className = 'post-members-only post-members-only--locked'; el.innerHTML = buildLockedGateHtml(charLength); return; } const innerHtml = el.querySelector('.post-members-only__body')?.innerHTML ?? Array.from(el.childNodes) .filter(n => !(n instanceof Element && n.classList.contains('post-members-only__badge'))) .map(n => (n instanceof Element ? n.outerHTML : n.textContent ?? '')) .join(''); // 已登录:降噪,不展示醒目 badge,仅保留结构容器 el.className = 'post-members-only post-members-only--visible'; el.innerHTML = `
${innerHtml}
`; }); doc.querySelectorAll('img').forEach(img => { if (!img.getAttribute('loading')) img.setAttribute('loading', 'lazy'); if (!img.getAttribute('decoding')) img.setAttribute('decoding', 'async'); }); if (opts?.openLinksInNewTab) { doc.querySelectorAll('a[href]').forEach(a => { const href = a.getAttribute('href') || ''; if (!href || href.startsWith('#') || href.startsWith('javascript:')) return; a.setAttribute('target', '_blank'); const rel = new Set((a.getAttribute('rel') || '').split(/\s+/).filter(Boolean)); rel.add('noopener'); rel.add('noreferrer'); a.setAttribute('rel', Array.from(rel).join(' ')); }); } enhanceHeadingAnchors(doc.body); enhanceCodeBlocks(doc.body); return doc.body.innerHTML; }