// js/processing/markdown_text_fix.js
// 专门修复数学文本显示问题的补丁
(function MarkdownTextFix(global) {
/**
* 修复长段落中的数学公式显示问题
* @param {string} markdown - 原始 markdown 文本
* @returns {string} 修复后的 markdown
*/
function fixMathTextDisplay(markdown) {
if (!markdown || typeof markdown !== 'string') {
return markdown;
}
// 1. 保护已有的数学公式,避免被段落分割
let processed = markdown;
// 2. 修复段落内的公式换行问题
// 将段落内不必要的换行转换为空格,但保持公式完整
processed = processed.replace(/([^.\n])\n(?![#\-*+\d\s])/g, '$1 ');
// 3. 确保数学公式前后有适当的空格
processed = processed.replace(/([^\s])\$\$/g, '$1 $$');
processed = processed.replace(/\$\$([^\s])/g, '$$ $1');
processed = processed.replace(/([^\s])\$([^$])/g, '$1 $$$2');
processed = processed.replace(/([^$])\$([^\s])/g, '$1$ $2');
// 4. 修复中文和公式之间的空格问题
processed = processed.replace(/([\u4e00-\u9fff])\$\$/g, '$1 $$');
processed = processed.replace(/\$\$([\u4e00-\u9fff])/g, '$$ $1');
processed = processed.replace(/([\u4e00-\u9fff])\$([^$])/g, '$1 $$$2');
processed = processed.replace(/([^$])\$([\u4e00-\u9fff])/g, '$1$ $2');
return processed;
}
/**
* 增强的 KaTeX 渲染选项
*/
const enhancedKatexOptions = {
displayMode: false,
throwOnError: false,
errorColor: '#cc0000',
strict: 'ignore',
output: 'html',
trust: false,
fleqn: false,
leqno: false,
minRuleThickness: 0.04,
colorIsTextColor: false,
maxSize: Infinity,
maxExpand: 1000,
globalGroup: false,
macros: {
"\\RR": "\\mathbb{R}",
"\\NN": "\\mathbb{N}",
"\\ZZ": "\\mathbb{Z}",
"\\QQ": "\\mathbb{Q}",
"\\CC": "\\mathbb{C}",
"\\sum": "\\sum",
"\\times": "\\times",
"\\frac": "\\frac",
"\\div": "\\div"
}
};
/**
* 改进的数学公式渲染函数
* @param {string} content - 公式内容
* @param {boolean} displayMode - 是否为显示模式
* @returns {string} 渲染后的 HTML
*/
function renderMathImproved(content, displayMode = false) {
if (!content || typeof content !== 'string') {
return '';
}
const options = {
...enhancedKatexOptions,
displayMode: displayMode
};
try {
// 预处理公式内容
let processedContent = content.trim();
// 清理零宽字符/组合下划线/误入的中文标点等边缘字符
processedContent = processedContent
.replace(/[\u200B-\u200D\uFEFF]/g, '')
.replace(/^[\u0300-\u036F]+|[\u0300-\u036F]+$/g, '')
.replace(/^[\s\u3000。,、;::“”\((\))\[\]【】《》‘’'"–—-]+/, '')
.replace(/[\s\u3000。,、;::“”\((\))\[\]【】《》‘’'"–—-]+$/, '')
.replace(/\s{2,}/g, ' ');
// 修复常见的公式问题
if (/\\right\s*$/.test(processedContent)) {
let close = ')';
try {
const re = /\\left\s*([\(\[\{])/g;
let m;
while ((m = re.exec(processedContent)) !== null) {
const ch = m[1];
close = ch === '(' ? ')' : ch === '[' ? ']' : '}';
}
} catch(_) { /* ignore */ }
processedContent = processedContent.replace(/\\right\s*$/, `\\right${close}`);
}
// Degree unit normalization: \mathrm{ ^\circ C } or \mathrm{ \;^\circ C }
processedContent = processedContent.replace(/\\mathrm\{\s*(?:\\;|\s)*\^\s*\{?\s*\\?circ\s*\}?\s*([A-Za-z])\s*\}/g, '^{\\circ}\\mathrm{$1}');
// Unicode triangles
processedContent = processedContent.replace(/▲/g, '\\blacktriangle').replace(/△/g, '\\triangle');
processedContent = processedContent.replace(/\\times/g, ' \\times ');
processedContent = processedContent.replace(/([a-zA-Z])([0-9])/g, '$1_{$2}');
const rendered = katex.renderToString(processedContent, options);
const containerClass = displayMode ? 'katex-display-fixed' : 'katex-inline-fixed';
const originalAttr = ` data-original-text="${escapeHtml(processedContent)}"`;
return displayMode
? `
${rendered}
`
: `${rendered}`;
} catch (error) {
console.warn('[MathFix] KaTeX rendering failed:', error.message);
const escapedContent = escapeHtml(content);
const errorTitle = `数学公式渲染失败: ${error.message}`;
const ariaLabel = escapeHtml(errorTitle);
const containerTag = displayMode ? 'div' : 'span';
const innerTag = displayMode ? 'pre' : 'span';
const containerClass = displayMode ? 'katex-fallback katex-block math-error-block' : 'katex-fallback katex-inline math-error-inline';
const dataAttr = ` data-katex-error="${escapeHtml(error.message)}" title="${ariaLabel}"`;
return displayMode
? `
<${containerTag} class="${containerClass}"${dataAttr}><${innerTag} class="katex-fallback-source">${escapedContent}${innerTag}>${containerTag}>
`
: `<${containerTag} class="${containerClass}"${dataAttr}><${innerTag} class="katex-fallback-source">${escapedContent}${innerTag}>${containerTag}>`;
}
}
/**
* 改进的 markdown 渲染,专门处理数学文本
* @param {string} markdown - 输入的 markdown
* @param {Array} images - 图片数组
* @returns {string} 渲染后的 HTML
*/
function renderMathMarkdown(markdown, images = []) {
if (!markdown) return '';
// 1. 预处理文本
let processed = fixMathTextDisplay(markdown);
// 2. 处理图片
if (Array.isArray(images) && images.length > 0) {
const imgMap = new Map();
images.forEach((img, idx) => {
if (img && img.data) {
const keys = [
img.name, img.id,
`img-${idx}.jpeg.png`,
`img-${idx + 1}.jpeg.png`
].filter(Boolean);
keys.forEach(k => {
imgMap.set(k, img.data.startsWith('data:') ? img.data : `data:image/png;base64,${img.data}`);
imgMap.set(`images/${k}`, imgMap.get(k));
});
}
});
processed = processed.replace(/!\[([^\]]*)\]\((?:images\/)?(img-\d+\.jpeg\.png)\)/gi, (match, alt, fname) => {
return imgMap.has(fname)
? `})`
: `[图片: ${alt || fname}]`;
});
}
// 3. 处理数学公式
// 先处理块级公式
processed = processed.replace(/\$\$([\s\S]*?)\$\$/g, (match, content) => {
return renderMathImproved(content, true);
});
// 再处理行内公式
processed = processed.replace(/\$([^$\n]+?)\$/g, (match, content) => {
return renderMathImproved(content, false);
});
// 4. 使用 marked 处理其余 markdown
try {
return marked.parse(processed, {
breaks: false, // 重要:不要将换行转换为
gfm: true,
sanitize: false
});
} catch (error) {
console.error('[MathFix] Marked parsing failed:', error);
return `Markdown 解析失败: ${escapeHtml(error.message)}
`;
}
}
/**
* HTML 转义函数
*/
function escapeHtml(text) {
if (typeof text !== 'string') return '';
const map = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
};
return text.replace(/[&<>"']/g, m => map[m]);
}
/**
* 修复已渲染的 DOM 中的数学显示问题
* @param {Element} container - 容器元素
*/
function fixRenderedMath(container) {
if (!container) return;
// 查找所有可能的数学公式元素
const mathElements = container.querySelectorAll('.katex, .katex-display, .katex-inline, [class*="katex"]');
mathElements.forEach(el => {
// 确保数学公式有适当的间距
if (el.classList.contains('katex-display') || el.classList.contains('katex-display-fixed')) {
el.style.margin = '16px 0';
el.style.textAlign = 'center';
el.style.display = 'block';
} else if (el.classList.contains('katex-inline') || el.classList.contains('katex-inline-fixed')) {
el.style.margin = '0 2px';
el.style.display = 'inline';
}
});
// 修复段落中的数学公式换行问题
const paragraphs = container.querySelectorAll('p');
paragraphs.forEach(p => {
// 移除数学公式前后不必要的换行
const html = p.innerHTML;
const fixed = html
.replace(/\s*(<(?:span|div)[^>]*katex[^>]*>.*?<\/(?:span|div)>)\s*/g, ' $1 ')
.replace(/\s{2,}/g, ' ')
.trim();
if (fixed !== html) {
p.innerHTML = fixed;
}
});
}
// 暴露公共接口
global.MarkdownTextFix = {
fixMathTextDisplay: fixMathTextDisplay,
renderMathImproved: renderMathImproved,
renderMathMarkdown: renderMathMarkdown,
fixRenderedMath: fixRenderedMath,
enhancedKatexOptions: enhancedKatexOptions
};
// 如果 MarkdownProcessor 存在,则扩展它
if (global.MarkdownProcessor) {
global.MarkdownProcessor.fixMathTextDisplay = fixMathTextDisplay;
global.MarkdownProcessor.renderMathMarkdown = renderMathMarkdown;
global.MarkdownProcessor.fixRenderedMath = fixRenderedMath;
}
// 如果 MarkdownProcessorEnhanced 存在,则扩展它
if (global.MarkdownProcessorEnhanced) {
global.MarkdownProcessorEnhanced.fixMathTextDisplay = fixMathTextDisplay;
global.MarkdownProcessorEnhanced.renderMathMarkdown = renderMathMarkdown;
global.MarkdownProcessorEnhanced.fixRenderedMath = fixRenderedMath;
}
})(window);