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 `
${LOCK_ICON_SVG}
登录后可见(${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;
}