V10终极版升级:引入DOM标记隔离机制,还原时只针对脚本渲染的部分,绝不破坏网页原生公式。
От
// ==UserScript== // @name Math Render // @namespace http://tampermonkey.net/ // @version 10.3 // @description V10终极版升级:引入DOM标记隔离机制,还原时只针对脚本渲染的部分,绝不破坏网页原生公式。 // @author Monica // @license MIT // @match *://monica.im/* // @grant GM_addStyle // @grant GM_getResourceText // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.js // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/contrib/auto-render.min.js // ==/UserScript== (function() { 'use strict'; // 强制注入 KaTeX CSS function loadKatexCSS() { if (document.querySelector('link[href*="katex.min.css"]')) return; const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = 'https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.css'; link.crossOrigin = 'anonymous'; document.head.appendChild(link); } loadKatexCSS(); // 注入浮动 UI 样式 GM_addStyle(` #katex-ui-container { position: fixed; bottom: 20px; right: 0; display: flex; align-items: center; z-index: 2147483647; transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); padding-right: 20px; } #katex-ui-container.is-hidden { transform: translateX(calc(100% - 24px)); } #katex-toggle-btn { width: 24px; height: 48px; background: #fff; color: #666; border: 1px solid #ddd; border-right: none; border-radius: 8px 0 0 8px; display: flex; align-items: center; justify-content: center; cursor: pointer; box-shadow: -2px 2px 6px rgba(0,0,0,0.1); font-size: 12px; margin-right: 10px; user-select: none; transition: background 0.2s; } #katex-toggle-btn:hover { background: #f0f0f0; color: #333; } #katex-trigger-btn { width: 50px; height: 50px; background: #fbc02d; color: #333; border-radius: 50%; text-align: center; line-height: 50px; font-size: 14px; font-weight: bold; cursor: pointer; box-shadow: 0 4px 12px rgba(0,0,0,0.3); user-select: none; transition: transform 0.2s, background 0.2s; font-family: sans-serif; } #katex-trigger-btn:hover { transform: scale(1.1); } `); // 创建浮动 UI const container = document.createElement('div'); container.id = 'katex-ui-container'; const toggleBtn = document.createElement('div'); toggleBtn.id = 'katex-toggle-btn'; toggleBtn.innerHTML = '▶'; toggleBtn.title = "隐藏/显示面板"; const mainBtn = document.createElement('div'); mainBtn.id = 'katex-trigger-btn'; mainBtn.textContent = 'Math Render'; mainBtn.title = "点击渲染公式"; container.appendChild(toggleBtn); container.appendChild(mainBtn); document.body.appendChild(container); let isRendered = false; let isHidden = false; // 智能清洗与结构修复 function cleanAndFixLatex(latex) { latex = latex.replace(/&/g, '&').replace(/amp;/g, '&'); latex = latex.replace(/</g, '<').replace(/>/g, '>'); latex = latex.replace(/ /g, ' '); if (latex.trim().startsWith('{') && latex.trim().endsWith('}') && !latex.includes('begin{cases}')) { if (latex.includes('\\\\') || latex.includes('=')) { let inner = latex.trim().slice(1, -1); latex = "\\begin{cases}" + inner + "\\end{cases}"; } } latex = latex.replace(/(\s)\\\s+(?=[a-zA-Z\\])/g, '$1\\\\ '); latex = latex.replace(/(\})\s*\\\s+(u)/g, '$1\\\\ $2'); latex = latex.replace(/(\})\s*\\\s+(\\frac)/g, '$1\\\\ $2'); latex = latex.replace(/(\})\s*\\\s+(\\partial)/g, '$1\\\\ $2'); return latex; } // 主处理流程 (渲染) function processPage(root) { loadKatexCSS(); // --- 保护原生公式 --- // 在我们动手之前,先把页面上已有的 KaTeX 元素打上保护标记 const existingMath = root.querySelectorAll('.katex, .katex-display'); existingMath.forEach(el => { if (!el.hasAttribute('data-tm-math')) { el.setAttribute('data-native-math', 'true'); } }); // 修复 [ ... ] 块级公式 const elements = root.querySelectorAll('div, p, li, dd, td, span, blockquote'); elements.forEach(el => { if (el.classList.contains('katex') || el.closest('.katex')) return; if (el.tagName === 'SCRIPT' || el.tagName === 'STYLE' || el.tagName === 'TEXTAREA') return; if (el.querySelector('div, p, li, blockquote')) return; let html = el.innerHTML; let originalHtml = html; const regex = /\\?\[\s*([^<>]*?)\s*\\?\](?!\()/g; html = html.replace(regex, (match, content) => { if (/^[\d,\s-]*$/.test(content)) return match; if (match.includes('](')) return match; try { let fixedLatex = cleanAndFixLatex(content); return katex.renderToString(fixedLatex, { displayMode: true, throwOnError: false, errorColor: "#cc0000" }); } catch (e) { return match; } }); if (html !== originalHtml) { el.innerHTML = html; } }); // 修复行内 $...$ 公式 renderMathInElement(root, { delimiters: [ {left: "$$", right: "$$", display: true}, {left: "$", right: "$", display: false}, {left: "\\(", right: "\\)", display: false}, {left: "\\[", right: "\\]", display: true} ], ignoredTags: ["script", "noscript", "style", "textarea", "pre", "code"], ignoredClasses: ["katex", "katex-display"], // 核心隔离机制 B:防止引擎二次渲染原生公式 throwOnError: false }); // --- 标记我们渲染的公式 --- // 渲染结束后,找到所有没有保护标记的公式,打上我们的专属标记 const allMath = root.querySelectorAll('.katex, .katex-display'); allMath.forEach(el => { if (!el.hasAttribute('data-native-math') && !el.hasAttribute('data-tm-math')) { el.setAttribute('data-tm-math', 'true'); } }); } // 还原处理流程 (精准无损提取) function restorePage(root) { // 查找带有我们专属标记的元素,不碰原生公式 const elements = root.querySelectorAll('.katex-display[data-tm-math="true"], .katex[data-tm-math="true"]'); const processed = new Set(); elements.forEach(el => { if (processed.has(el)) return; let isDisplay = el.classList.contains('katex-display'); let katexSpan = isDisplay ? el.querySelector('.katex') : el; if (!katexSpan) return; const annotation = katexSpan.querySelector('annotation[encoding="application/x-tex"]'); if (annotation) { let tex = annotation.textContent; let restoredText = isDisplay ? `[ ${tex} ]` : `$ ${tex} $`; const textNode = document.createTextNode(restoredText); if (el.parentNode) { el.parentNode.replaceChild(textNode, el); } if (isDisplay) { const innerKatex = el.querySelectorAll('.katex'); innerKatex.forEach(inner => processed.add(inner)); } } }); } // 交互事件绑定 toggleBtn.addEventListener('click', () => { isHidden = !isHidden; if (isHidden) { container.classList.add('is-hidden'); toggleBtn.innerHTML = '◀'; } else { container.classList.remove('is-hidden'); toggleBtn.innerHTML = '▶'; } }); mainBtn.addEventListener('click', () => { if (isRendered) { mainBtn.textContent = '...'; requestAnimationFrame(() => { setTimeout(() => { try { restorePage(document.body); isRendered = false; mainBtn.textContent = 'Math Render'; mainBtn.style.backgroundColor = '#fbc02d'; mainBtn.title = "点击渲染公式"; } catch (e) { console.error("Restore Error:", e); mainBtn.textContent = 'Error'; mainBtn.style.backgroundColor = '#f44336'; } }, 50); }); } else { mainBtn.textContent = '...'; requestAnimationFrame(() => { setTimeout(() => { try { processPage(document.body); isRendered = true; mainBtn.textContent = 'recover'; mainBtn.style.backgroundColor = '#4caf50'; mainBtn.title = "点击还原为未渲染状态"; } catch (e) { console.error("Script Error:", e); mainBtn.textContent = 'Error'; mainBtn.style.backgroundColor = '#f44336'; setTimeout(() => { mainBtn.textContent = 'Math Render'; mainBtn.style.backgroundColor = '#fbc02d'; }, 2000); } }, 50); }); } }); })();