// js/chatbot/chatbot-mermaid-renderer.js /** * 本地 HTML 转义函数(用于错误消息防护) * 如果 ChatbotUtils.escapeHtml 不可用时的降级方案 * @param {string} str - 需要转义的字符串 * @returns {string} 转义后的安全字符串 */ function localEscapeHtml(str) { if (typeof str !== 'string') return ''; return str.replace(/[&<>"']/g, function (c) { return {'&':'&','<':'<','>':'>','"':'"','\'':'''}[c]; }); } /** * 安全地转义 HTML(优先使用 ChatbotUtils,降级到本地实现) * @param {string} str - 需要转义的字符串 * @returns {string} 转义后的安全字符串 */ function safeEscapeHtml(str) { if (typeof window.ChatbotUtils !== 'undefined' && typeof window.ChatbotUtils.escapeHtml === 'function') { return window.ChatbotUtils.escapeHtml(str); } return localEscapeHtml(str); } /** * 渲染聊天内容中的所有 Mermaid 代码块。 * * 主要流程: * 1. 查找所有 code.language-mermaid 代码块。 * 2. 对每个代码块: * - 清理和修正 Mermaid 代码(如去除 HTML 标签、等)。 * - 创建 div.mermaid 容器,设置唯一ID。 * - 使用 mermaid.init 渲染 SVG。 * - 渲染成功则显示 SVG,并添加"放大"按钮。 * - 渲染失败则回退显示上一次成功的 SVG,或显示错误信息。 * - 支持多次尝试渲染(异步加载 mermaid.js 时)。 * @param {HTMLElement} chatBodyElement - 聊天消息容器 DOM 元素。 */ async function renderAllMermaidBlocksInternal(chatBodyElement) { if (!window.mermaidLoaded || typeof window.mermaid === 'undefined') return; if (!chatBodyElement) { console.warn('ChatbotMermaidRenderer: chatBodyElement 为空,跳过 Mermaid 渲染。'); return; } // 按钮添加辅助函数(避免代码重复) const addMermaidButtons = (mermaidDiv, currentCodeForError) => { setTimeout(() => { try { // 查看代码按钮 if (!mermaidDiv.querySelector('.mermaid-code-btn')) { const codeBtn = document.createElement('button'); codeBtn.className = 'mermaid-action-btn mermaid-code-btn'; codeBtn.title = '查看/隐藏代码'; codeBtn.innerHTML = ``; codeBtn.style.cssText = ` position:absolute; top:12px; right:48px; background: transparent; color: #64748b; border: none; border-radius: 6px; padding: 4px; cursor: pointer; opacity:0.7; transition: opacity 0.2s, background-color 0.2s; display:flex; align-items:center; justify-content:center; `; codeBtn.onmouseover = function() { this.style.opacity = '1'; this.style.backgroundColor = 'rgba(0,0,0,0.05)'; }; codeBtn.onmouseout = function() { this.style.opacity = '0.7'; this.style.backgroundColor = 'transparent'; }; codeBtn.onclick = function() { const codeContainer = mermaidDiv.querySelector('.mermaid-original-code'); if (codeContainer) { if (codeContainer.style.display === 'none') { codeContainer.style.display = 'block'; } else { codeContainer.style.display = 'none'; } } else { const originalCode = document.createElement('pre'); originalCode.className = 'mermaid-original-code'; originalCode.style.cssText = ` position:relative; background:#f8f9fa; border:1px solid #e9ecef; border-radius:6px; padding:12px; margin-top:12px; font-family:monospace; font-size:13px; white-space:pre-wrap; word-break:break-all; max-height:300px; overflow-y:auto; `; originalCode.textContent = currentCodeForError; const copyBtn = document.createElement('button'); copyBtn.textContent = '复制'; copyBtn.style.cssText = ` position:absolute;top:8px;right:8px; background:#e9ecef;border:none; border-radius:4px;padding:2px 8px; font-size:12px;cursor:pointer; `; copyBtn.onclick = function(e) { e.stopPropagation(); navigator.clipboard.writeText(currentCodeForError) .then(() => { const originalText = copyBtn.textContent; copyBtn.textContent = '已复制!'; setTimeout(() => { copyBtn.textContent = originalText; }, 2000); }) .catch(err => { console.error('复制失败:', err); alert('复制失败: ' + err); }); }; originalCode.appendChild(copyBtn); mermaidDiv.appendChild(originalCode); } }; mermaidDiv.appendChild(codeBtn); } // 放大按钮(完整功能复用) if (!mermaidDiv.querySelector('.mermaid-zoom-btn')) { const zoomBtn = document.createElement('button'); zoomBtn.className = 'mermaid-action-btn mermaid-zoom-btn'; zoomBtn.title = '放大查看'; zoomBtn.innerHTML = ``; zoomBtn.style.cssText = ` position:absolute; top:12px; right:12px; background: transparent; color: #64748b; border: none; border-radius: 6px; padding: 4px; cursor: pointer; opacity:0.7; transition: opacity 0.2s, background-color 0.2s; display:flex; align-items:center; justify-content:center; `; zoomBtn.onmouseover = function() { this.style.opacity = '1'; this.style.backgroundColor = 'rgba(0,0,0,0.05)'; }; zoomBtn.onmouseout = function() { this.style.opacity = '0.7'; this.style.backgroundColor = 'transparent'; }; zoomBtn.onclick = function() { try { // 创建遮罩和弹窗,显示 SVG 大图 const overlay = document.createElement('div'); overlay.style.cssText = 'position:fixed;z-index:999999;left:0;top:0;width:100vw;height:100vh;background:rgba(0,0,0,0.6);display:flex;align-items:center;justify-content:center;padding:20px;box-sizing:border-box;'; const popup = document.createElement('div'); popup.style.cssText = 'background:var(--chatbot-bg, #fff);padding:24px;border-radius:12px;box-shadow:0 10px 30px rgba(0,0,0,0.1), 0 0 0 1px rgba(0,0,0,0.05);width:95vw;max-width:1200px;height:90vh;overflow:auto;position:relative;display:flex;flex-direction:column;align-items:center;'; const title = document.createElement('div'); title.textContent = 'Mermaid 图表预览'; title.style.cssText = 'font-weight:bold;font-size:18px;margin-bottom:18px;'; popup.appendChild(title); const svgInMermaidDiv = mermaidDiv.querySelector('svg'); if (svgInMermaidDiv) { const svgClone = svgInMermaidDiv.cloneNode(true); svgClone.style.width = '100%'; svgClone.style.maxWidth = '100%'; svgClone.style.height = 'auto'; svgClone.style.flexGrow = '1'; svgClone.style.display = 'block'; svgClone.style.margin = '0 auto 16px auto'; popup.appendChild(svgClone); // 弹窗底部操作按钮(导出PNG、SVG、代码、Mermaid.live) const popupActions = document.createElement('div'); popupActions.style.cssText = 'display:flex; flex-wrap:wrap; gap:12px; justify-content:center; padding-top:16px; border-top: 1px solid rgba(0,0,0,0.08); width:100%;'; // Helper function to create icon buttons for popup const createPopupActionButton = (btnTitle, svgIcon, onClickAction) => { const button = document.createElement('button'); button.title = btnTitle; button.innerHTML = svgIcon; button.style.cssText = ` background: rgba(0,0,0,0.05); color: #334155; border: none; border-radius: 8px; padding: 8px 12px; cursor: pointer; transition: background-color 0.2s, color 0.2s; display: flex; align-items: center; gap: 6px; font-size: 13px; `; button.onmouseover = function() { this.style.backgroundColor = 'rgba(0,0,0,0.1)'; }; button.onmouseout = function() { this.style.backgroundColor = 'rgba(0,0,0,0.05)'; }; button.onclick = onClickAction; popupActions.appendChild(button); return button; }; // Icons (Heroicons - Outline) const iconDownload = ''; const iconCode = ''; const iconExternalLink = ''; const iconPhoto = ''; // Export PNG Button (使用 html2canvas) const exportPngBtn = createPopupActionButton('导出PNG', iconPhoto + 'PNG', async function() { try { exportPngBtn.innerHTML = iconPhoto + '导出中...'; exportPngBtn.disabled = true; // 检查 html2canvas 是否可用 if (typeof html2canvas === 'undefined') { throw new Error('html2canvas 库未加载'); } // 创建一个临时容器来包裹 SVG const tempContainer = document.createElement('div'); tempContainer.style.cssText = 'position: absolute; left: -9999px; top: 0; background: white; padding: 20px;'; const svgForExport = svgClone.cloneNode(true); // 获取 SVG 的原始尺寸 const svgRect = svgClone.getBoundingClientRect(); const originalWidth = svgRect.width || 800; const originalHeight = svgRect.height || 600; // 放大 SVG 以提高清晰度 const scaleFactor = 4; // 4倍放大 svgForExport.setAttribute('width', originalWidth * scaleFactor); svgForExport.setAttribute('height', originalHeight * scaleFactor); svgForExport.style.display = 'block'; svgForExport.style.width = (originalWidth * scaleFactor) + 'px'; svgForExport.style.height = (originalHeight * scaleFactor) + 'px'; tempContainer.appendChild(svgForExport); document.body.appendChild(tempContainer); // 使用 html2canvas 截图容器(高分辨率) const canvas = await html2canvas(tempContainer, { backgroundColor: '#ffffff', scale: 1, // SVG 已经放大了,这里用 1 即可 logging: false, useCORS: true, allowTaint: true, width: originalWidth * scaleFactor + 40, // 加上 padding height: originalHeight * scaleFactor + 40 }); // 清理临时容器 document.body.removeChild(tempContainer); // 下载 PNG canvas.toBlob(function(blob) { const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'mermaid-diagram.png'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); exportPngBtn.innerHTML = iconPhoto + '导出PNG'; exportPngBtn.disabled = false; }, 'image/png'); } catch (e) { console.error('PNG 导出失败:', e); alert('PNG 导出失败: ' + (e.message || e) + '\n\n建议:先导出 SVG,然后使用在线工具转换。'); exportPngBtn.innerHTML = iconPhoto + '导出PNG'; exportPngBtn.disabled = false; } }); exportPngBtn.innerHTML = iconPhoto + '导出PNG'; // Export SVG Button const exportSvgBtn = createPopupActionButton('导出SVG', iconDownload + 'SVG', function() { try { // 克隆 SVG 用于导出 const svgCloneForExport = svgClone.cloneNode(true); // 复制所有计算后的样式到内联样式 const copyComputedStyles = (source, target) => { const sourceElements = source.querySelectorAll('*'); const targetElements = target.querySelectorAll('*'); for (let i = 0; i < sourceElements.length && i < targetElements.length; i++) { const computedStyle = window.getComputedStyle(sourceElements[i]); const cssText = computedStyle.cssText; if (cssText) { targetElements[i].setAttribute('style', cssText); } } }; copyComputedStyles(svgClone, svgCloneForExport); const serializer = new XMLSerializer(); let svgString = serializer.serializeToString(svgCloneForExport); // 添加 XML 声明 svgString = '\n' + svgString; // 确保有正确的命名空间 if (!svgString.includes('xmlns="http://www.w3.org/2000/svg"')) { svgString = svgString.replace(/ URL.revokeObjectURL(url), 100); } catch (e) { console.error('SVG 导出失败:', e); alert('SVG 导出失败: ' + (e.message || e)); } }); exportSvgBtn.innerHTML = iconDownload + '导出SVG'; // Export Code Button const exportCodeBtn = createPopupActionButton('导出代码', iconCode + 'MMD', function() { const blob = new Blob([currentCodeForError], {type: 'text/plain'}); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'mermaid-code.mmd'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }); exportCodeBtn.innerHTML = iconCode + '导出MMD'; // Open in Mermaid.live Button const openLiveBtn = createPopupActionButton('在Mermaid.live中打开', iconExternalLink + 'Mermaid.live', function() { const data = { code: currentCodeForError, mermaid: { theme: 'default' } }; const json = JSON.stringify(data); const encoded = btoa(unescape(encodeURIComponent(json))); const liveUrl = `https://mermaid.live/edit#${encoded}`; window.open(liveUrl, '_blank'); }); openLiveBtn.innerHTML = iconExternalLink + 'Mermaid.live'; popup.appendChild(popupActions); } // 关闭按钮 const closeBtn = document.createElement('button'); closeBtn.title = '关闭'; closeBtn.innerHTML = ``; closeBtn.style.cssText = ` position:absolute; top:16px; right:16px; background: transparent; color: #64748b; border: none; border-radius: 50%; padding: 6px; cursor: pointer; opacity:0.7; transition: opacity 0.2s, background-color 0.2s; display:flex; align-items:center; justify-content:center; `; closeBtn.onmouseover = function() { this.style.opacity = '1'; this.style.backgroundColor = 'rgba(0,0,0,0.05)';}; closeBtn.onmouseout = function() { this.style.opacity = '0.7'; this.style.backgroundColor = 'transparent';}; closeBtn.onclick = function() { document.body.removeChild(overlay); }; popup.appendChild(closeBtn); overlay.appendChild(popup); document.body.appendChild(overlay); } catch (e) { alert('放大预览弹窗出错:'+(e.message||e)); } }; mermaidDiv.appendChild(zoomBtn); } } catch (btnError) { console.error('添加按钮时发生错误:', btnError); } }, 100); }; // 查找所有 mermaid 代码块(code.language-mermaid 或 pre code.language-mermaid) const mermaidBlocks = chatBodyElement.querySelectorAll('code.language-mermaid, pre code.language-mermaid'); // 使用 for...of 替代 forEach,确保顺序执行,避免竞态条件 let blockIndex = 0; for (const block of mermaidBlocks) { const idx = blockIndex++; let currentCodeForError = ''; try { // 检查是否已经渲染过(防止重复渲染) if (block.hasAttribute('data-mermaid-rendered')) { console.log(`Mermaid 代码块 ${idx} 已渲染,跳过`); continue; } // 标记为正在渲染 block.setAttribute('data-mermaid-rendered', 'true'); // 增强的代码清理和预处理 let rawCode = block.textContent || ''; if (!rawCode.trim()) { console.warn('ChatbotMermaidRenderer: 空的Mermaid代码块,跳过处理'); return; } currentCodeForError = rawCode; // Assign for potential error reporting // 1. 将 标签替换为换行 rawCode = rawCode.replace(//gi, '\n'); // 2. 移除所有 HTML 标签,但保留内容 rawCode = rawCode.replace(/<[^>]+>/g, ''); // 3. 移除零宽字符和特殊空白字符 rawCode = rawCode.replace(/[\u200B-\u200D\uFEFF]/g, ''); // 4. 统一换行符为 \n rawCode = rawCode.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); // 5. 移除多余的空行(超过2个连续换行) rawCode = rawCode.replace(/\n{3,}/g, '\n\n'); // 6. 移除每行首尾空白,但保持缩进结构 rawCode = rawCode.split('\n').map(line => line.trimEnd()).join('\n'); // 7. **关键修正:处理节点标签内的多行文本** // Mermaid 不支持 [...] 内的换行符,需要将多行标签转为单行 rawCode = rawCode.replace(/([A-Za-z0-9_]+)\[([^\]]+)\]/g, (match, nodeId, labelContent) => { // 将标签内的换行替换为空格 let cleanLabel = labelContent.replace(/\n+/g, ' '); // 压缩多余空格 cleanLabel = cleanLabel.replace(/\s+/g, ' ').trim(); // **替换特殊符号为全角(避免 Mermaid 解析错误)** cleanLabel = cleanLabel .replace(/\[/g, '[') // 方括号左 .replace(/\]/g, ']') // 方括号右 .replace(/\(/g, '(') .replace(/\)/g, ')') .replace(/\|/g, '|'); // 限制标签长度(最多80字符) if (cleanLabel.length > 80) { cleanLabel = cleanLabel.substring(0, 77) + '...'; } return `${nodeId}[${cleanLabel}]`; }); // 8. 修正常见的语法错误 // 8.1 修正箭头语法(-- > 改为 -->) rawCode = rawCode.replace(/--\s+>/g, '-->'); rawCode = rawCode.replace(/<\s+--/g, '<--'); // 8.2 修正节点定义后多余的节点名(A[text]A --> B 改为 A[text] --> B) // 必须在 8.3 之前执行,避免误判 // 匹配:节点ID + [ + 内容(可能包含全角]) + ] + 重复的节点ID rawCode = rawCode.replace(/([A-Za-z0-9_]+)\[([^\]]*(?:][^\]]*)*)\]\1(?=\s|$)/g, '$1[$2]'); // 8.3 修正节点定义后缺少空格的问题(]D --> 改为 ]\nD -->) // 只处理半角右括号后紧跟字母的情况 rawCode = rawCode.replace(/\]([A-Za-z0-9_]+)(\s+-->)/g, ']\n$1$2'); rawCode = rawCode.replace(/\]([A-Za-z0-9_]+)(\s*$)/gm, ']\n$1'); // 8.4 修复缺少箭头的节点连接 // 修复菱形节点缺少结束花括号的情况(如 J{文本 K[...] 改为 J{文本} --> K[...]) rawCode = rawCode.replace(/\{([^}]*?)\s{2,}([A-Z][A-Za-z0-9_]*)\[/g, '{$1} --> $2['); // 修复 } [ 或 }[ 的情况(菱形节点后缺少箭头) rawCode = rawCode.replace(/\}(\s{2,}|\s*)\[/g, '} --> ['); // 修复 ] [ 的情况(方括号节点后缺少箭头,至少2个空格) rawCode = rawCode.replace(/\](\s{2,})\[/g, '] --> ['); // 修复 ) [ 的情况(圆括号节点后缺少箭头) rawCode = rawCode.replace(/\)(\s{2,})\[/g, ') --> ['); // 8.5 移除空的 graph 声明 rawCode = rawCode.replace(/^\s*graph\s*$/gim, ''); // 9. 修正 subgraph 语法 rawCode = rawCode.replace(/^\s*subgraph\s+([^\n]+)$/gm, (match, name) => { // 移除 subgraph 名称中的括号内容 let cleanName = name.replace(/\([^\)]*\)/g, '').trim(); // 移除逗号和句号 cleanName = cleanName.replace(/[.,,。]/g, ''); // 压缩多余空格 cleanName = cleanName.replace(/\s+/g, ' '); return cleanName ? `subgraph ${cleanName}` : 'subgraph'; }); // 10. 修正 class 语句语法(移除多余的 class 关键字) // 例如:class A,B,C class stage1 → class A,B,C stage1 rawCode = rawCode.replace(/^\s*class\s+([A-Za-z0-9_,\s]+)\s+class\s+([A-Za-z0-9_]+)\s*$/gm, 'class $1 $2'); // 11. Trim 最终结果 rawCode = rawCode.trim(); // 选择父节点(兼容 pre > code 或单独 code) let parent; try { parent = block.parentElement.tagName === 'PRE' ? block.parentElement : block; } catch (e) { parent = block; // 兜底 } const code = rawCode; currentCodeForError = code; // 创建 Mermaid 渲染容器 const mermaidDiv = document.createElement('div'); // 生成绝对唯一的 ID(使用时间戳 + 索引 + 随机数 + 性能计时器) const uniqueId = `mermaid-${Date.now()}-${idx}-${Math.floor(Math.random()*100000)}-${Math.floor(performance.now()*1000)}`; mermaidDiv.className = 'mermaid'; mermaidDiv.id = uniqueId; mermaidDiv.setAttribute('data-mermaid-index', idx.toString()); // 卡片样式 mermaidDiv.style.background = 'var(--chatbot-bg, #fff)'; mermaidDiv.style.borderRadius = '12px'; mermaidDiv.style.boxShadow = '0 2px 8px rgba(0,0,0,0.08), 0 0 0 1px rgba(0,0,0,0.03)'; mermaidDiv.style.padding = '20px'; mermaidDiv.style.position = 'relative'; mermaidDiv.style.width = 'fit-content'; mermaidDiv.style.maxWidth = '100%'; mermaidDiv.style.margin = '12px auto'; mermaidDiv.textContent = code; // 检查内容有效性 const codeTrimmed = code.replace(/\s+/g, ''); if (!codeTrimmed || /^graph(TD|LR|RL|BT|TB)?$/i.test(codeTrimmed)) { mermaidDiv.innerHTML = '无有效Mermaid内容'; parent.replaceWith(mermaidDiv); return; } // 记录上一次渲染成功的 SVG(用于回退) let lastSVG = null; if (parent.id && parent.id.startsWith('mermaid-') && parent.querySelector('svg')) { lastSVG = parent.querySelector('svg').cloneNode(true); } else if (parent.firstElementChild && parent.firstElementChild.id && parent.firstElementChild.id.startsWith('mermaid-') && parent.firstElementChild.querySelector('svg')){ lastSVG = parent.firstElementChild.querySelector('svg').cloneNode(true); } // 用新的 mermaidDiv 替换原代码块 parent.replaceWith(mermaidDiv); try { // 使用 mermaid.init 渲染 SVG await window.mermaid.init(undefined, '#' + uniqueId); // 渲染成功,清除错误边框 mermaidDiv.style.border = ''; const existingWarning = mermaidDiv.querySelector('.mermaid-render-warning'); if (existingWarning) existingWarning.remove(); // 添加按钮 addMermaidButtons(mermaidDiv, currentCodeForError); } catch (renderError) { /** * Mermaid 渲染失败处理(增强版): * 1. 自动尝试多级修正(无需 data-mermaid-final 标记) * 2. 若修正成功则渲染,否则继续下一个修正 * 3. 若所有修正都失败,显示详细错误信息 * 4. 若有上一次成功 SVG,可回退显示 */ console.warn('Mermaid 初次渲染失败,尝试自动修正...', renderError); // 增强的修正函数集合(从最保守到最激进) const mermaidFixers = [ // 修正0a: 修复缺少箭头的节点连接(如 J{text} K[text] 改为 J{text} --> K[text]) code => { // 修复菱形节点缺少结束花括号的情况 code = code.replace(/\{([^}]*?)\s{2,}([A-Z][A-Za-z0-9_]*)\[/g, '{$1} --> $2['); // 修复 } [ 或 }[ 的情况(菱形节点后缺少箭头) code = code.replace(/\}(\s{2,}|\s*)\[/g, '} --> ['); // 修复 ] [ 的情况(方括号节点后缺少箭头) code = code.replace(/\](\s{2,})\[/g, '] --> ['); // 修复 ) [ 的情况(圆括号节点后缺少箭头) code = code.replace(/\)(\s{2,})\[/g, ') --> ['); return code; }, // 修正0b: 处理节点标签中的括号、方括号和管道符(最常见问题) code => { return code.replace(/\[([^\]]+)\]/g, (match, labelContent) => { let cleanLabel = labelContent .replace(/\[/g, '[') // 方括号左 .replace(/\]/g, ']') // 方括号右 .replace(/\(/g, '(') .replace(/\)/g, ')') .replace(/\|/g, '|'); return `[${cleanLabel}]`; }); }, // 修正1: 处理多行文本和长度限制 code => { return code.replace(/\[([^\]]+)\]/g, (match, labelContent) => { let cleanLabel = labelContent.replace(/\n+/g, ' ').replace(/\s+/g, ' ').trim(); if (cleanLabel.length > 60) { cleanLabel = cleanLabel.substring(0, 57) + '...'; } return `[${cleanLabel}]`; }); }, // 修正2: 修正常见的引号问题 code => { return code.replace(/["']/g, ''); }, // 修正3: 将节点标签中的斜杠替换为全角 code => { return code.replace(/\[([^\]]*)\]/g, match => match.replace(/\//g, '/')); }, // 修正4: 移除节点标签中的冒号和破折号 code => { return code.replace(/\[([^\]]*)\]/g, match => match.replace(/[:\-]/g, '')); }, // 修正5: 将所有特殊符号替换为全角或空格 code => { return code.replace(/\[([^\]]*)\]/g, match => { return match .replace(/\(/g, '(') .replace(/\)/g, ')') .replace(/\|/g, '|') .replace(//g, '>') .replace(/\{/g, '{') .replace(/\}/g, '}'); }); }, // 修正6: 极端修正 - 只保留节点内的中英文、数字和常见符号 code => { return code.replace(/\[([^\]]*)\]/g, match => { return match.replace(/[^\u4e00-\u9fa5a-zA-Z0-9\[\]\s\.]/g, ''); }); }, // 修正7: 最激进 - 简化所有节点名称为纯文本 code => { return code.replace(/\[([^\]]*)\]/g, match => { const inner = match.slice(1, -1).trim(); // 只保留前20个字符 return '[' + inner.substring(0, 20).replace(/[^\u4e00-\u9fa5a-zA-Z0-9\s]/g, '') + ']'; }); } ]; let fixSuccess = false; let successfulCode = null; for (let i = 0; i < mermaidFixers.length && !fixSuccess; i++) { try { const fixedCode = mermaidFixers[i](code); if (fixedCode === code) continue; // 跳过没有改变的修正 // 重新设置 mermaid div 内容 mermaidDiv.textContent = fixedCode; await window.mermaid.init(undefined, '#' + uniqueId); // 验证 SVG 是否真正生成 const svgElement = mermaidDiv.querySelector('svg'); if (!svgElement) { console.warn(`修正器 ${i + 1} 执行完成但未生成 SVG`); continue; // 未生成 SVG,尝试下一个修正器 } // 修正成功! fixSuccess = true; successfulCode = fixedCode; console.log(`Mermaid 修正成功 (修正器 ${i + 1})`, fixedCode); // 清除错误边框 mermaidDiv.style.border = ''; // 显示警告提示 const warningDiv = document.createElement('div'); warningDiv.className = 'mermaid-auto-fix-warning'; warningDiv.style.cssText = ` color: #d97706; font-size: 12px; background: #fef3c7; border: 1px solid #fcd34d; border-radius: 6px; padding: 8px 12px; margin-top: 12px; text-align: center; `; warningDiv.innerHTML = ` ⚠️ 已自动修正语法 原始代码存在语法问题,已应用修正器 ${i + 1} 进行渲染 `; mermaidDiv.appendChild(warningDiv); // 更新 currentCodeForError 为修正后的代码 currentCodeForError = fixedCode; // 添加按钮 addMermaidButtons(mermaidDiv, currentCodeForError); break; } catch (e) { // 这个修正器失败了,继续尝试下一个 console.warn(`修正器 ${i + 1} 失败:`, e); } } // 如果所有修正都失败 if (!fixSuccess) { // XSS 防护:安全地转义错误消息和代码 const escapedErrorMessage = safeEscapeHtml(renderError.str || renderError.message); const escapedCode = safeEscapeHtml(currentCodeForError); if (lastSVG) { // 回退到上一次成功的 SVG mermaidDiv.innerHTML = ''; mermaidDiv.appendChild(lastSVG); mermaidDiv.style.border = '2px dashed #f59e0b'; let warn = mermaidDiv.querySelector('.mermaid-render-warning'); if (!warn) { warn = document.createElement('div'); warn.className = 'mermaid-render-warning'; warn.style.cssText = 'color:#d97706;font-size:12px;margin-top:4px;text-align:center;'; mermaidDiv.appendChild(warn); } warn.textContent = '⚠️ 当前代码解析失败,显示上一版本。错误: ' + escapedErrorMessage; } else { // 显示详细错误信息 mermaidDiv.innerHTML = ` ❌ Mermaid 渲染失败 错误信息: ${escapedErrorMessage} 查看原始代码 ${escapedCode} 💡 提示: 可尝试在 Mermaid.live 中调试代码 `; mermaidDiv.style.border = '2px solid #e53e3e'; } } } } catch (generalBlockError) { // 兜底:处理 block 解析或 DOM 操作异常 // XSS 防护:安全地转义错误消息 const escapedGeneralErrorMessage = safeEscapeHtml(generalBlockError.message); console.error('处理Mermaid block时发生一般错误:', generalBlockError, block); let errorDisplayDiv = block.parentElement || document.createElement('div'); if (block.parentElement) { let tempDiv = document.createElement('div'); tempDiv.innerHTML = 'Mermaid block处理异常: ' + escapedGeneralErrorMessage + ''; block.replaceWith(tempDiv); } else { block.innerHTML = 'Mermaid block处理异常: ' + escapedGeneralErrorMessage + ''; } } } // 结束 for...of 循环 } // 挂载到全局命名空间 if (typeof window.ChatbotRenderingUtils === 'undefined') { window.ChatbotRenderingUtils = {}; } window.ChatbotRenderingUtils.renderAllMermaidBlocks = renderAllMermaidBlocksInternal;
${escapedCode}