paper-burner/js/chatbot/utils/markdown-katex-render-cache...

388 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Phase 4.2+: KaTeX 缓存版本的 Markdown 渲染
*
* 优化:将所有 katex.renderToString() 替换为 renderKatexCached()
* 解决:打开充满公式的 chatbot 时 4.6s 主线程阻塞问题
*
* 预期收益:
* - 重复公式渲染时间减少 99%
* - 打开时间从 4.6s 降至 0.5s 以下
*/
// Phase 3 优化: Marked.js 轻量化配置
if (typeof marked !== 'undefined' && typeof marked.setOptions === 'function') {
marked.setOptions({
gfm: true, // 启用 GitHub Flavored Markdown
breaks: true, // 支持换行符
pedantic: false,
sanitize: false,
smartLists: false, // 禁用智能列表(性能优化)
smartypants: false, // 禁用智能标点(性能优化)
mangle: false, // 禁用邮箱混淆(性能优化)
headerIds: false // 禁用标题ID生成性能优化
});
}
/**
* 使用缓存渲染 KaTeX 公式
* 自动降级到 katex.renderToString 如果缓存不可用
*/
function renderKatexWithCache(tex, options) {
if (typeof window.renderKatexCached === 'function') {
return window.renderKatexCached(tex, options);
}
// 降级到直接渲染
return katex.renderToString(tex, options);
}
window.renderWithKatexStreaming = function(md) {
const codeBlocks = [];
let codeBlockCounter = 0;
const FORMULA_BLOCK_HINTS = [
/\r|\n/,
/\\\\/,
/\\tag\b/,
/\\label\b/,
/\\eqref\b/,
/\\display(?:style|limits)\b/,
/\\begin\{(?:align\*?|aligned|flalign\*?|gather\*?|multline\*?|split|cases|array|pmatrix|bmatrix|vmatrix|Vmatrix|matrix|smallmatrix)\}/,
/\\end\{(?:align\*?|aligned|flalign\*?|gather\*?|multline\*?|split|cases|array|pmatrix|bmatrix|vmatrix|Vmatrix|matrix|smallmatrix)\}/
];
function escapeHtml(text) {
if (typeof text !== 'string') {
return '';
}
const map = {
'&': '&',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
};
return text.replace(/[&<>"']/g, ch => map[ch]);
}
function analyzeFormula(tex, displayHint) {
const normalized = typeof tex === 'string' ? tex.trim() : '';
if (!normalized) {
return { text: '', displayMode: !!displayHint };
}
let displayMode = !!displayHint;
if (!displayMode) {
displayMode = FORMULA_BLOCK_HINTS.some(pattern => pattern.test(normalized));
}
return { text: normalized, displayMode };
}
function buildFallback(tex, displayMode, error) {
const sanitized = escapeHtml(tex || '');
const message = error && error.message ? error.message : (typeof error === 'string' ? error : '');
const dataAttr = message ? ` data-katex-error="${escapeHtml(message)}" title="Formula rendering failed: ${escapeHtml(message)}"` : '';
if (displayMode) {
return `
<div class="katex-fallback katex-block"${dataAttr}><pre class="katex-fallback-source">${sanitized}</pre></div>
`;
}
return `<span class="katex-fallback katex-inline"${dataAttr}><span class="katex-fallback-source">${sanitized}</span></span>`;
}
// 保护代码块
md = md.replace(/```([\s\S]+?)```/g, function(match) {
const placeholder = `__CODE_BLOCK_${codeBlockCounter}__`;
codeBlocks[codeBlockCounter] = match;
codeBlockCounter++;
return placeholder;
});
md = md.replace(/`([^`]+?)`/g, function(match) {
const placeholder = `__CODE_BLOCK_${codeBlockCounter}__`;
codeBlocks[codeBlockCounter] = match;
codeBlockCounter++;
return placeholder;
});
// $$ ... $$ 块级公式(带缓存)
md = md.replace(/\$\$([\s\S]+?)\$\$/g, function(_, tex) {
const analysis = analyzeFormula(tex, true);
try {
const html = renderKatexWithCache(analysis.text, {
displayMode: true,
output: 'html',
strict: 'ignore',
throwOnError: false,
trust: false,
macros: {},
maxSize: 50,
maxExpand: 100
});
return `
<div class="katex-block" data-formula-display="block" data-original-text="${escapeHtml(analysis.text)}">${html}</div>
`;
} catch (e) {
return buildFallback(analysis.text, true, e);
}
});
// \[ ... \] 块级公式(带缓存)
md = md.replace(/\\\[([\s\S]+?)\\\]/g, function(_, tex) {
const analysis = analyzeFormula(tex, true);
try {
const html = renderKatexWithCache(analysis.text, {
displayMode: true,
output: 'html',
strict: 'ignore',
throwOnError: false,
trust: false,
macros: {},
maxSize: 50,
maxExpand: 100
});
return `
<div class="katex-block" data-formula-display="block" data-original-text="${escapeHtml(analysis.text)}">${html}</div>
`;
} catch (e) {
return buildFallback(analysis.text, true, e);
}
});
// $ ... $ 行内或块级公式(带缓存)
md = md.replace(/\$([^\$]+?)\$/g, function(_, tex) {
const analysis = analyzeFormula(tex, false);
try {
if (analysis.displayMode) {
const html = renderKatexWithCache(analysis.text, {
displayMode: true,
output: 'html',
strict: 'ignore',
throwOnError: false,
trust: false,
macros: {},
maxSize: 50,
maxExpand: 100
});
return `
<div class="katex-block" data-formula-display="block" data-original-text="${escapeHtml(analysis.text)}">${html}</div>
`;
}
const html = renderKatexWithCache(analysis.text, {
displayMode: false,
output: 'html',
strict: 'ignore',
throwOnError: false,
trust: false,
macros: {},
maxSize: 50,
maxExpand: 100
});
return `<span class="katex-inline" data-formula-display="inline" data-original-text="${escapeHtml(analysis.text)}">${html}</span>`;
} catch (e) {
return buildFallback(analysis.text, analysis.displayMode, e);
}
});
// \( ... \) 行内或块级公式(带缓存)
md = md.replace(/\\\(([^)]+?)\\\)/g, function(_, tex) {
const analysis = analyzeFormula(tex, false);
try {
if (analysis.displayMode) {
const html = renderKatexWithCache(analysis.text, {
displayMode: true,
output: 'html',
strict: 'ignore',
throwOnError: false,
trust: false,
macros: {},
maxSize: 50,
maxExpand: 100
});
return `
<div class="katex-block" data-formula-display="block" data-original-text="${escapeHtml(analysis.text)}">${html}</div>
`;
}
const html = renderKatexWithCache(analysis.text, {
displayMode: false,
output: 'html',
strict: 'ignore',
throwOnError: false,
trust: false,
macros: {},
maxSize: 50,
maxExpand: 100
});
return `<span class="katex-inline" data-formula-display="inline" data-original-text="${escapeHtml(analysis.text)}">${html}</span>`;
} catch (e) {
return buildFallback(analysis.text, analysis.displayMode, e);
}
});
// 还原代码块
for (let i = 0; i < codeBlockCounter; i++) {
md = md.replace(`__CODE_BLOCK_${i}__`, codeBlocks[i]);
}
// XSS 防<><E998B2><EFBFBD>使用 safeRenderMarkdown 替代直接 marked.parse()
if (typeof window.safeRenderMarkdown === 'function') {
return window.safeRenderMarkdown(md);
}
// 降级方案:如果 safeRenderMarkdown 不可用,仍使用 marked.parse
console.warn('[Security] safeRenderMarkdown not available, using unsafe marked.parse()');
return marked.parse(md);
};
/**
* Phase 4.2 - 长公式增量渲染原型(带缓存支持)
*/
window.ChatbotMathStreaming = (function() {
function escapeHtml(text) {
if (typeof text !== 'string') {
return '';
}
const map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
};
return text.replace(/[&<>"']/g, function(ch) {
return map[ch];
});
}
function renderPlainMarkdown(text) {
if (!text) return '';
if (typeof window.safeRenderMarkdown === 'function') {
return window.safeRenderMarkdown(text);
}
if (typeof marked !== 'undefined' && typeof marked.parse === 'function') {
return marked.parse(text);
}
return escapeHtml(text).replace(/\n/g, '<br>');
}
function renderBlockFormula(tex) {
const normalized = typeof tex === 'string' ? tex.trim() : '';
if (!normalized) return '';
if (typeof katex === 'undefined' || typeof katex.renderToString !== 'function') {
return escapeHtml(normalized);
}
try {
// 使用缓存渲染
const html = renderKatexWithCache(normalized, {
displayMode: true,
output: 'html',
strict: 'ignore',
throwOnError: false,
trust: false,
macros: {},
maxSize: 50,
maxExpand: 100
});
return `
<div class="katex-block" data-formula-display="block" data-original-text="${escapeHtml(normalized)}">${html}</div>
`;
} catch (e) {
const message = e && e.message ? e.message : '';
const dataAttr = message
? ` data-katex-error="${escapeHtml(message)}" title="Formula rendering failed: ${escapeHtml(message)}"`
: '';
return `
<div class="katex-fallback katex-block"${dataAttr}><pre class="katex-fallback-source">${escapeHtml(normalized)}</pre></div>
`;
}
}
function renderIncremental(prevState, appendedText) {
if (!appendedText) {
return { html: '', state: prevState || null };
}
const state = prevState && typeof prevState === 'object'
? {
pendingFormula: prevState.pendingFormula || null
}
: {
pendingFormula: null
};
const text = String(appendedText);
const len = text.length;
let i = 0;
let htmlParts = [];
let plainBuffer = '';
function flushPlain() {
if (!plainBuffer) return;
htmlParts.push(renderPlainMarkdown(plainBuffer));
plainBuffer = '';
}
while (i < len) {
if (!state.pendingFormula) {
if (text.startsWith('$$', i)) {
flushPlain();
state.pendingFormula = {
delimiter: '$$',
text: ''
};
i += 2;
continue;
}
if (text.startsWith('\\[', i)) {
flushPlain();
state.pendingFormula = {
delimiter: '\\[',
text: ''
};
i += 2;
continue;
}
plainBuffer += text[i];
i += 1;
continue;
}
const delimiter = state.pendingFormula.delimiter;
if (delimiter === '$$' && text.startsWith('$$', i)) {
const formulaText = state.pendingFormula.text;
htmlParts.push(renderBlockFormula(formulaText));
state.pendingFormula = null;
i += 2;
continue;
}
if (delimiter === '\\[' && text.startsWith('\\]', i)) {
const formulaText = state.pendingFormula.text;
htmlParts.push(renderBlockFormula(formulaText));
state.pendingFormula = null;
i += 2;
continue;
}
state.pendingFormula.text += text[i];
i += 1;
}
flushPlain();
return {
html: htmlParts.join(''),
state: state
};
}
return {
renderIncremental: renderIncremental
};
})();
console.log('[Phase 4.2+] KaTeX cached rendering enabled');