/** * RichEditor - 共享富文本工具栏组件 * 用于聊天和论坛的输入区域 */ // ========== 常量 ========== const RE_FONTS = [ { label: '默认', value: '' }, { label: '宋体', value: '宋体,SimSun' }, { label: '黑体', value: '黑体,SimHei' }, { label: '楷体', value: '楷体,KaiTi' }, { label: '仿宋', value: '仿宋,FangSong' }, { label: '微软雅黑', value: '微软雅黑,Microsoft YaHei' }, { label: '华文行楷', value: '华文行楷,STXingkai' }, { label: '华文新魏', value: '华文新魏,STXinwei' }, { label: '隶书', value: '隶书,LiSu' }, { label: '幼圆', value: '幼圆,YouYuan' }, { label: 'Consolas', value: 'Consolas,monospace' }, { label: 'Georgia', value: 'Georgia,serif' }, { label: 'Comic Sans', value: 'Comic Sans MS,cursive' }, { label: 'Times New Roman', value: 'Times New Roman,serif' }, ]; const RE_EMOJIS = { '表情': ['😀','😁','😂','🤣','😊','😍','🥰','😘','😜','🤪'], '手势': ['👍','👎','👏','🙌','🤝','✌️','🤞','👌','🫶','🙏'], '心形': ['❤️','🧡','💛','💚','💙','💜','🖤','🤍','💖','💝'], '动物': ['🐱','🐶','🐼','🦊','🐰','🐸','🐵','🦁','🐯','🐮'], '物品': ['🎉','🎊','🔥','⭐','💡','📚','✏️','🏆','🎯','💻'], '符号': ['✅','❌','⚠️','💯','🔴','🟢','🔵','⬆️','⬇️','➡️'], }; const RE_COLORS = [ '#ef4444','#f97316','#eab308','#22c55e','#06b6d4', '#3b82f6','#8b5cf6','#ec4899','#6b7280','#000000', '#dc2626','#ea580c','#ca8a04','#16a34a','#0891b2', '#2563eb','#7c3aed','#db2777','#9ca3af','#ffffff', ]; const RE_MATH_SYMBOLS = { '基础运算': [ { label: '+', tex: '+' }, { label: '−', tex: '-' }, { label: '±', tex: '\\pm' }, { label: '∓', tex: '\\mp' }, { label: '×', tex: '\\times' }, { label: '÷', tex: '\\div' }, { label: '·', tex: '\\cdot' }, { label: '=', tex: '=' }, { label: '≠', tex: '\\neq' }, { label: '≈', tex: '\\approx' }, { label: '≡', tex: '\\equiv' }, { label: '<', tex: '<' }, { label: '>', tex: '>' }, { label: '≤', tex: '\\leq' }, { label: '≥', tex: '\\geq' }, { label: '≪', tex: '\\ll' }, { label: '≫', tex: '\\gg' }, { label: '∝', tex: '\\propto' }, ], '上下标': [ { label: 'x²', tex: '^{2}' }, { label: 'x³', tex: '^{3}' }, { label: 'xⁿ', tex: '^{}', cursor: -1 }, { label: 'x₁', tex: '_{}', cursor: -1 }, { label: 'xₙ', tex: '_{n}' }, { label: 'x₁²', tex: '_{}^{}', cursor: -4 }, ], '分数根号': [ { label: '分数', tex: '\\frac{}{}', cursor: -3 }, { label: '√', tex: '\\sqrt{}', cursor: -1 }, { label: 'ⁿ√', tex: '\\sqrt[n]{}', cursor: -1 }, { label: 'ᵃ⁄ᵦ', tex: '\\dfrac{}{}', cursor: -3 }, ], '希腊字母': [ { label: 'α', tex: '\\alpha' }, { label: 'β', tex: '\\beta' }, { label: 'γ', tex: '\\gamma' }, { label: 'δ', tex: '\\delta' }, { label: 'ε', tex: '\\epsilon' }, { label: 'ζ', tex: '\\zeta' }, { label: 'η', tex: '\\eta' }, { label: 'θ', tex: '\\theta' }, { label: 'λ', tex: '\\lambda' }, { label: 'μ', tex: '\\mu' }, { label: 'ξ', tex: '\\xi' }, { label: 'π', tex: '\\pi' }, { label: 'ρ', tex: '\\rho' }, { label: 'σ', tex: '\\sigma' }, { label: 'τ', tex: '\\tau' }, { label: 'φ', tex: '\\varphi' }, { label: 'ω', tex: '\\omega' }, { label: 'Δ', tex: '\\Delta' }, { label: 'Σ', tex: '\\Sigma' }, { label: 'Ω', tex: '\\Omega' }, { label: 'Φ', tex: '\\Phi' }, { label: 'Π', tex: '\\Pi' }, { label: 'Λ', tex: '\\Lambda' }, { label: 'Γ', tex: '\\Gamma' }, ], '高等数学': [ { label: '∑', tex: '\\sum_{i=1}^{n}', cursor: 0 }, { label: '∏', tex: '\\prod_{i=1}^{n}', cursor: 0 }, { label: '∫', tex: '\\int_{a}^{b}', cursor: 0 }, { label: '∬', tex: '\\iint' }, { label: '∮', tex: '\\oint' }, { label: 'lim', tex: '\\lim_{x \\to }', cursor: -1 }, { label: '∞', tex: '\\infty' }, { label: 'log', tex: '\\log_{}', cursor: -1 }, { label: 'ln', tex: '\\ln' }, { label: 'sin', tex: '\\sin' }, { label: 'cos', tex: '\\cos' }, { label: 'tan', tex: '\\tan' }, { label: 'arcsin', tex: '\\arcsin' }, { label: 'arccos', tex: '\\arccos' }, { label: 'arctan', tex: '\\arctan' }, { label: '∂', tex: '\\partial' }, { label: '∇', tex: '\\nabla' }, { label: 'dx', tex: '\\mathrm{d}x' }, ], '集合逻辑': [ { label: '∈', tex: '\\in' }, { label: '∉', tex: '\\notin' }, { label: '⊂', tex: '\\subset' }, { label: '⊃', tex: '\\supset' }, { label: '⊆', tex: '\\subseteq' }, { label: '⊇', tex: '\\supseteq' }, { label: '∪', tex: '\\cup' }, { label: '∩', tex: '\\cap' }, { label: '∅', tex: '\\emptyset' }, { label: '∀', tex: '\\forall' }, { label: '∃', tex: '\\exists' }, { label: '¬', tex: '\\neg' }, { label: '∧', tex: '\\land' }, { label: '∨', tex: '\\lor' }, { label: '⇒', tex: '\\Rightarrow' }, { label: '⇔', tex: '\\Leftrightarrow' }, { label: '→', tex: '\\to' }, { label: '↦', tex: '\\mapsto' }, ], '括号矩阵': [ { label: '()', tex: '\\left( \\right)', cursor: -8 }, { label: '[]', tex: '\\left[ \\right]', cursor: -8 }, { label: '{}', tex: '\\left\\{ \\right\\}', cursor: -9 }, { label: '||', tex: '\\left| \\right|', cursor: -8 }, { label: '⌈⌉', tex: '\\lceil \\rceil', cursor: -7 }, { label: '⌊⌋', tex: '\\lfloor \\rfloor', cursor: -8 }, { label: '矩阵', tex: '\\begin{pmatrix} \\\\ \\end{pmatrix}', cursor: -18 }, { label: '行列式', tex: '\\begin{vmatrix} \\\\ \\end{vmatrix}', cursor: -18 }, { label: '方程组', tex: '\\begin{cases} \\\\ \\end{cases}', cursor: -15 }, ], '其他符号': [ { label: '…', tex: '\\cdots' }, { label: '⋮', tex: '\\vdots' }, { label: '⋱', tex: '\\ddots' }, { label: '°', tex: '^{\\circ}' }, { label: '‰', tex: '\\permil' }, { label: '→', tex: '\\vec{}', cursor: -1 }, { label: 'â', tex: '\\hat{}', cursor: -1 }, { label: 'ā', tex: '\\bar{}', cursor: -1 }, { label: 'ã', tex: '\\tilde{}', cursor: -1 }, { label: '⊥', tex: '\\perp' }, { label: '∥', tex: '\\parallel' }, { label: '∠', tex: '\\angle' }, { label: '△', tex: '\\triangle' }, { label: '□', tex: '\\square' }, { label: '⊕', tex: '\\oplus' }, { label: '⊗', tex: '\\otimes' }, { label: '★', tex: '\\star' }, { label: '♠', tex: '\\spadesuit' }, ], }; const RE_FORMULA_TEMPLATES = [ { label: '二次公式', tex: 'x = \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a}' }, { label: '勾股定理', tex: 'a^2 + b^2 = c^2' }, { label: '求和公式', tex: '\\sum_{i=1}^{n} i = \\frac{n(n+1)}{2}' }, { label: '二项式定理', tex: '(a+b)^n = \\sum_{k=0}^{n} \\binom{n}{k} a^{n-k} b^k' }, { label: '欧拉公式', tex: 'e^{i\\pi} + 1 = 0' }, { label: '三角恒等式', tex: '\\sin^2\\theta + \\cos^2\\theta = 1' }, { label: '对数换底', tex: '\\log_a b = \\frac{\\ln b}{\\ln a}' }, { label: '导数定义', tex: "f'(x) = \\lim_{h \\to 0} \\frac{f(x+h) - f(x)}{h}" }, { label: '定积分', tex: '\\int_a^b f(x)\\,dx = F(b) - F(a)' }, { label: '矩阵乘法', tex: '\\begin{pmatrix} a & b \\\\ c & d \\end{pmatrix} \\begin{pmatrix} e \\\\ f \\end{pmatrix} = \\begin{pmatrix} ae+bf \\\\ ce+df \\end{pmatrix}' }, { label: '方程组', tex: '\\begin{cases} x + y = 5 \\\\ 2x - y = 1 \\end{cases}' }, { label: '极限', tex: '\\lim_{x \\to \\infty} \\left(1 + \\frac{1}{x}\\right)^x = e' }, ]; // ========== RichEditor 类 ========== class RichEditor { constructor(textareaId, options = {}) { this.ta = document.getElementById(textareaId); if (!this.ta) return; this.compact = options.compact || false; // 紧凑模式(聊天用) this.onEnter = options.onEnter || null; this._buildToolbar(); this._buildPreview(); this._bindKeys(); this._bindPreviewUpdate(); } _buildToolbar() { const bar = document.createElement('div'); bar.className = 're-toolbar' + (this.compact ? ' re-compact' : ''); // 粗体 bar.appendChild(this._btn('B', '粗体', 're-bold', () => this._wrap('**', '**'))); // 斜体 bar.appendChild(this._btn('I', '斜体', 're-italic', () => this._wrap('*', '*'))); // 代码 bar.appendChild(this._btn('<>', '代码', 're-code', () => this._wrap('`', '`'))); bar.appendChild(this._sep()); // 颜色 const colorWrap = document.createElement('div'); colorWrap.className = 're-dropdown-wrap'; const colorBtn = this._btn('A', '文字颜色', 're-color-btn', () => this._togglePanel(colorWrap)); colorBtn.innerHTML = 'A'; colorWrap.appendChild(colorBtn); colorWrap.appendChild(this._buildColorPanel(colorBtn)); bar.appendChild(colorWrap); // 字体 const fontWrap = document.createElement('div'); fontWrap.className = 're-dropdown-wrap'; fontWrap.appendChild(this._btn('字', '字体', 're-font-btn', () => this._togglePanel(fontWrap))); fontWrap.appendChild(this._buildFontPanel()); bar.appendChild(fontWrap); bar.appendChild(this._sep()); // Emoji const emojiWrap = document.createElement('div'); emojiWrap.className = 're-dropdown-wrap'; emojiWrap.appendChild(this._btn('😊', 'Emoji', 're-emoji-btn', () => this._togglePanel(emojiWrap))); emojiWrap.appendChild(this._buildEmojiPanel()); bar.appendChild(emojiWrap); // 公式编辑器按钮 bar.appendChild(this._sep()); const self = this; bar.appendChild(this._btn('fx', '公式编辑器', 're-fx-btn', () => openMathEditor(self.ta))); this.ta.parentNode.insertBefore(bar, this.ta); this.toolbar = bar; // 点击外部关闭面板 document.addEventListener('click', (e) => { if (!bar.contains(e.target)) { bar.querySelectorAll('.re-panel').forEach(p => p.classList.add('hidden')); } }); } _btn(html, title, cls, onclick) { const b = document.createElement('button'); b.type = 'button'; b.className = 're-btn ' + (cls || ''); b.title = title; b.innerHTML = html; b.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); onclick(); }); return b; } _sep() { const s = document.createElement('span'); s.className = 're-sep'; return s; } _togglePanel(wrap) { const panel = wrap.querySelector('.re-panel'); const wasHidden = panel.classList.contains('hidden'); this.toolbar.querySelectorAll('.re-panel').forEach(p => p.classList.add('hidden')); if (wasHidden) panel.classList.remove('hidden'); } _wrap(pre, suf) { const s = this.ta.selectionStart, e = this.ta.selectionEnd; const txt = this.ta.value; const sel = txt.substring(s, e) || '文本'; this.ta.value = txt.substring(0, s) + pre + sel + suf + txt.substring(e); this.ta.focus(); this.ta.selectionStart = s + pre.length; this.ta.selectionEnd = s + pre.length + sel.length; this._refreshPreview(); } _insertAt(text, cursorOffset) { const s = this.ta.selectionStart; const v = this.ta.value; this.ta.value = v.substring(0, s) + text + v.substring(s); this.ta.focus(); this.ta.selectionStart = this.ta.selectionEnd = s + text.length + (cursorOffset || 0); this._refreshPreview(); } _bindKeys() { if (!this.onEnter) return; this.ta.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); this.onEnter(); } }); } _buildPreview() { // 实时预览面板,插在 textarea 后面 const pv = document.createElement('div'); pv.className = 're-live-preview'; pv.innerHTML = '
预览
'; // 插到 textarea 的父容器末尾(border 容器内) this.ta.parentNode.appendChild(pv); this.previewEl = pv.querySelector('.re-pv-body'); } _bindPreviewUpdate() { let timer = null; const update = () => { if (!this.previewEl) return; const val = this.ta.value.trim(); if (!val) { this.previewEl.innerHTML = '输入内容后这里会显示实时预览...'; return; } this.previewEl.innerHTML = renderRichContent(val); }; this.ta.addEventListener('input', () => { clearTimeout(timer); timer = setTimeout(update, 150); }); update(); } _refreshPreview() { if (!this.previewEl) return; const val = this.ta.value.trim(); if (!val) { this.previewEl.innerHTML = '输入内容后这里会显示实时预览...'; return; } this.previewEl.innerHTML = renderRichContent(val); } // ===== 颜色面板 ===== _buildColorPanel(colorBtn) { const panel = document.createElement('div'); panel.className = 're-panel re-color-panel hidden'; RE_COLORS.forEach(c => { const swatch = document.createElement('button'); swatch.type = 'button'; swatch.className = 're-swatch'; swatch.style.background = c; if (c === '#ffffff') swatch.style.border = '1px solid #e2e8f0'; swatch.addEventListener('click', (e) => { e.stopPropagation(); this._wrap(`[color:${c}]`, '[/color]'); colorBtn.querySelector('span').style.borderBottomColor = c; panel.classList.add('hidden'); }); panel.appendChild(swatch); }); return panel; } // ===== 字体面板 ===== _buildFontPanel() { const panel = document.createElement('div'); panel.className = 're-panel re-font-panel hidden'; RE_FONTS.forEach(f => { const item = document.createElement('button'); item.type = 'button'; item.className = 're-font-item'; item.textContent = f.label; if (f.value) item.style.fontFamily = f.value; item.addEventListener('click', (e) => { e.stopPropagation(); if (f.value) { this._wrap(`[font:${f.label}]`, '[/font]'); } panel.classList.add('hidden'); }); panel.appendChild(item); }); return panel; } // ===== Emoji 面板 ===== _buildEmojiPanel() { const panel = document.createElement('div'); panel.className = 're-panel re-emoji-panel hidden'; const tabs = document.createElement('div'); tabs.className = 're-emoji-tabs'; const body = document.createElement('div'); body.className = 're-emoji-body'; const categories = Object.keys(RE_EMOJIS); categories.forEach((cat, i) => { const tab = document.createElement('button'); tab.type = 'button'; tab.className = 're-emoji-tab' + (i === 0 ? ' active' : ''); tab.textContent = cat; tab.addEventListener('click', (e) => { e.stopPropagation(); tabs.querySelectorAll('.re-emoji-tab').forEach(t => t.classList.remove('active')); tab.classList.add('active'); this._showEmojiCat(body, cat); }); tabs.appendChild(tab); }); panel.appendChild(tabs); panel.appendChild(body); this._showEmojiCat(body, categories[0]); return panel; } _showEmojiCat(body, cat) { body.innerHTML = ''; RE_EMOJIS[cat].forEach(em => { const btn = document.createElement('button'); btn.type = 'button'; btn.className = 're-emoji-item'; btn.textContent = em; btn.addEventListener('click', (e) => { e.stopPropagation(); this._insertAt(em); }); body.appendChild(btn); }); } } // ========== MathFormulaEditor 单例类 ========== class MathFormulaEditor { constructor() { if (MathFormulaEditor._instance) return MathFormulaEditor._instance; MathFormulaEditor._instance = this; this._built = false; this._target = null; this._onInsert = null; this._debounceTimer = null; } open(targetTextarea, onInsert) { this._target = targetTextarea; this._onInsert = onInsert || null; if (!this._built) this._buildDOM(); this._input.value = ''; this._previewEl.innerHTML = '在上方输入 LaTeX 公式,这里实时预览'; this._overlay.classList.remove('hidden'); document.body.style.overflow = 'hidden'; // 激活第一个符号分类 tab const firstTab = this._symbolTabs.querySelector('.re-emoji-tab'); if (firstTab) firstTab.click(); setTimeout(() => this._input.focus(), 50); } close() { this._overlay.classList.add('hidden'); document.body.style.overflow = ''; } _buildDOM() { this._built = true; const overlay = document.createElement('div'); overlay.className = 're-math-editor-overlay hidden'; overlay.addEventListener('click', (e) => { if (e.target === overlay) this.close(); }); const modal = document.createElement('div'); modal.className = 're-math-editor-modal'; // Header const header = document.createElement('div'); header.className = 're-math-editor-header'; header.innerHTML = '数学公式编辑器'; const closeBtn = document.createElement('button'); closeBtn.type = 'button'; closeBtn.className = 're-math-editor-close'; closeBtn.innerHTML = '×'; closeBtn.addEventListener('click', () => this.close()); header.appendChild(closeBtn); modal.appendChild(header); // Body const body = document.createElement('div'); body.className = 're-math-editor-body'; // LaTeX input const inputLabel = document.createElement('div'); inputLabel.style.cssText = 'font-size:12px;color:#64748b;margin-bottom:4px;font-weight:500'; inputLabel.textContent = 'LaTeX 公式'; body.appendChild(inputLabel); const input = document.createElement('textarea'); input.className = 're-math-editor-input'; input.rows = 3; input.placeholder = '输入 LaTeX 公式,如 \\frac{a}{b}、x^2 + y^2 = r^2'; input.addEventListener('input', () => this._updatePreview()); this._input = input; body.appendChild(input); // Preview const pvLabel = document.createElement('div'); pvLabel.style.cssText = 'font-size:12px;color:#64748b;margin:12px 0 4px;font-weight:500'; pvLabel.textContent = '实时预览'; body.appendChild(pvLabel); const preview = document.createElement('div'); preview.className = 're-math-editor-preview'; this._previewEl = preview; body.appendChild(preview); // Symbol panel const symLabel = document.createElement('div'); symLabel.style.cssText = 'font-size:12px;color:#64748b;margin:12px 0 4px;font-weight:500'; symLabel.textContent = '符号面板'; body.appendChild(symLabel); // PLACEHOLDER_SYMBOL_PANEL const symTabs = document.createElement('div'); symTabs.className = 're-emoji-tabs'; symTabs.style.cssText = 'border:1px solid #e2e8f0;border-radius:8px 8px 0 0'; this._symbolTabs = symTabs; const symGrid = document.createElement('div'); symGrid.className = 're-math-grid'; symGrid.style.cssText = 'border:1px solid #e2e8f0;border-top:none;border-radius:0 0 8px 8px;margin-bottom:0'; const categories = Object.keys(RE_MATH_SYMBOLS); categories.forEach((cat, i) => { const tab = document.createElement('button'); tab.type = 'button'; tab.className = 're-emoji-tab' + (i === 0 ? ' active' : ''); tab.textContent = cat; tab.addEventListener('click', (e) => { e.stopPropagation(); symTabs.querySelectorAll('.re-emoji-tab').forEach(t => t.classList.remove('active')); tab.classList.add('active'); this._showSymCat(symGrid, cat); }); symTabs.appendChild(tab); }); body.appendChild(symTabs); body.appendChild(symGrid); this._showSymCat(symGrid, categories[0]); // Template section const tplLabel = document.createElement('div'); tplLabel.style.cssText = 'font-size:12px;color:#64748b;margin:14px 0 6px;font-weight:500'; tplLabel.textContent = '常用公式模板'; body.appendChild(tplLabel); const tplGrid = document.createElement('div'); tplGrid.className = 're-math-editor-templates'; RE_FORMULA_TEMPLATES.forEach(tpl => { const card = document.createElement('button'); card.type = 'button'; card.className = 're-math-editor-tpl-card'; const texDiv = document.createElement('div'); texDiv.className = 're-math-editor-tpl-tex'; if (window.katex) { try { texDiv.innerHTML = katex.renderToString(tpl.tex, { throwOnError: false, displayMode: false }); } catch(e) { texDiv.textContent = tpl.tex; } } else { texDiv.textContent = tpl.tex; } const labelDiv = document.createElement('div'); labelDiv.className = 're-math-editor-tpl-label'; labelDiv.textContent = tpl.label; card.appendChild(texDiv); card.appendChild(labelDiv); card.addEventListener('click', () => this._applyTemplate(tpl.tex)); tplGrid.appendChild(card); }); body.appendChild(tplGrid); modal.appendChild(body); // Footer const footer = document.createElement('div'); footer.className = 're-math-editor-footer'; const cancelBtn = document.createElement('button'); cancelBtn.type = 'button'; cancelBtn.className = 're-math-editor-btn-cancel'; cancelBtn.textContent = '取消'; cancelBtn.addEventListener('click', () => this.close()); const confirmBtn = document.createElement('button'); confirmBtn.type = 'button'; confirmBtn.className = 're-math-editor-btn-confirm'; confirmBtn.textContent = '插入公式'; confirmBtn.addEventListener('click', () => this._confirm()); footer.appendChild(cancelBtn); footer.appendChild(confirmBtn); modal.appendChild(footer); overlay.appendChild(modal); document.body.appendChild(overlay); this._overlay = overlay; // ESC to close document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && !this._overlay.classList.contains('hidden')) this.close(); }); } _updatePreview() { clearTimeout(this._debounceTimer); this._debounceTimer = setTimeout(() => { const tex = this._input.value.trim(); if (!tex) { this._previewEl.innerHTML = '在上方输入 LaTeX 公式,这里实时预览'; return; } if (window.katex) { try { this._previewEl.innerHTML = katex.renderToString(tex, { throwOnError: true, displayMode: true }); } catch(e) { this._previewEl.innerHTML = '语法错误: ' + e.message.replace(/'; } } else { this._previewEl.textContent = tex; } }, 150); } _insertSymbol(tex, cursor) { const start = this._input.selectionStart; const val = this._input.value; this._input.value = val.substring(0, start) + tex + val.substring(start); this._input.focus(); this._input.selectionStart = this._input.selectionEnd = start + tex.length + (cursor || 0); this._updatePreview(); } _applyTemplate(tex) { this._input.value = tex; this._input.focus(); this._input.selectionStart = this._input.selectionEnd = tex.length; this._updatePreview(); } _confirm() { const tex = this._input.value.trim(); if (!tex) { this.close(); return; } const formula = '$' + tex + '$'; if (this._target) { const start = this._target.selectionStart || this._target.value.length; const val = this._target.value; this._target.value = val.substring(0, start) + formula + val.substring(start); this._target.selectionStart = this._target.selectionEnd = start + formula.length; this._target.dispatchEvent(new Event('input', { bubbles: true })); } if (typeof this._onInsert === 'function') this._onInsert(formula); this.close(); } _showSymCat(grid, cat) { grid.innerHTML = ''; RE_MATH_SYMBOLS[cat].forEach(sym => { const btn = document.createElement('button'); btn.type = 'button'; btn.className = 're-math-item'; btn.textContent = sym.label; btn.title = sym.tex; btn.addEventListener('click', (e) => { e.stopPropagation(); this._insertSymbol(sym.tex, sym.cursor || 0); }); grid.appendChild(btn); }); } } function openMathEditor(textarea, callback) { new MathFormulaEditor().open(textarea, callback); } // ========== 全局渲染函数 ========== const RE_FONT_MAP = {}; RE_FONTS.forEach(f => { if (f.value) RE_FONT_MAP[f.label] = f.value; }); function _reEsc(text) { const d = document.createElement('div'); d.textContent = text; return d.innerHTML; } function renderRichContent(text) { if (!text) return ''; // 1. 先提取公式,避免 HTML 转义破坏 LaTeX const formulas = []; let raw = text.replace(/\$([^$\n]+?)\$/g, function(m, tex) { const idx = formulas.length; formulas.push(tex); return '\x00MATH' + idx + '\x00'; }); // 2. HTML 转义 let h = _reEsc(raw); // 3. 图片标签 [img:url] h = h.replace(/\[img:(\/static\/uploads\/[^\]]+)\]/g, ''); // 4. 还原公式并用 KaTeX 渲染 h = h.replace(/\x00MATH(\d+)\x00/g, function(m, idx) { const tex = formulas[parseInt(idx)]; if (window.katex) { try { return katex.renderToString(tex, { throwOnError: false }); } catch(e) { return '$' + _reEsc(tex) + '$'; } } // KaTeX 未加载时显示原始公式并标记 class 供后续渲染 return '$' + _reEsc(tex) + '$'; }); // 5. 粗体 **...** h = h.replace(/\*\*(.+?)\*\*/g, '$1'); // 6. 斜体 *...* h = h.replace(/(?$1'); // 7. 行内代码 `...` h = h.replace(/`([^`]+?)`/g, '$1'); // 8. 颜色 [color:#hex]...[/color] h = h.replace(/\[color:(#[0-9a-fA-F]{3,8})\]([\s\S]*?)\[\/color\]/g, '$2'); // 9. 字体 [font:name]...[/font] h = h.replace(/\[font:([^\]]+)\]([\s\S]*?)\[\/font\]/g, function(m, name, content) { const family = RE_FONT_MAP[name]; if (family) return '' + content + ''; return content; }); return h; } // KaTeX 延迟加载后,重新渲染页面上未渲染的公式 function _reRenderPendingMath() { if (!window.katex) return; document.querySelectorAll('.re-pending-math').forEach(el => { try { el.innerHTML = katex.renderToString(el.dataset.tex, { throwOnError: false }); el.classList.remove('re-pending-math'); } catch(e) {} }); } // 定期检查 KaTeX 是否加载完成 (function _waitKatex() { if (window.katex) { _reRenderPendingMath(); return; } setTimeout(_waitKatex, 500); })();