Math Render

V10终极版升级:引入DOM标记隔离机制,还原时只针对脚本渲染的部分,绝不破坏网页原生公式。

От 16.03.2026. Виж последната версия.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да инсталирате разширение, като например Tampermonkey .

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

// ==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(/&lt;/g, '<').replace(/&gt;/g, '>');
        latex = latex.replace(/&nbsp;/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);
            });
        }
    });

})();