Math Render

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

Verze ze dne 16. 03. 2026. Zobrazit nejnovější verzi.

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

You will need to install an extension such as Tampermonkey to install this script.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

You will need to install an extension such as Tampermonkey to install this script.

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

// ==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);
            });
        }
    });

})();