Math Render

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

2026-03-16 기준 버전입니다. 최신 버전을 확인하세요.

이 스크립트를 설치하려면 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);
            });
        }
    });

})();