/**
* 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 = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
};
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 `
`;
}
return `${sanitized}`;
}
// 保护代码块
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 `
${html}
`;
} 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 `
${html}
`;
} 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 `
${html}
`;
}
const html = renderKatexWithCache(analysis.text, {
displayMode: false,
output: 'html',
strict: 'ignore',
throwOnError: false,
trust: false,
macros: {},
maxSize: 50,
maxExpand: 100
});
return `${html}`;
} 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 `
${html}
`;
}
const html = renderKatexWithCache(analysis.text, {
displayMode: false,
output: 'html',
strict: 'ignore',
throwOnError: false,
trust: false,
macros: {},
maxSize: 50,
maxExpand: 100
});
return `${html}`;
} 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 防���:使用 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 = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
};
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, '
');
}
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 `
${html}
`;
} catch (e) {
const message = e && e.message ? e.message : '';
const dataAttr = message
? ` data-katex-error="${escapeHtml(message)}" title="Formula rendering failed: ${escapeHtml(message)}"`
: '';
return `
${escapeHtml(normalized)}
`;
}
}
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');