Universal Inline & Display LaTeX Renderer (KaTeX)

Render inline and display LaTeX math on any website using KaTeX. Careful with input fields! Be sure to have rendering OFF when entering an input field, otherwise you can mess up your delimiters. I have made a fix button for this, but it might not be exactly correct.

目前為 2025-07-13 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Universal Inline & Display LaTeX Renderer (KaTeX)
// @namespace    http://tampermonkey.net/
// @version      2025-07-13.3
// @description  Render inline and display LaTeX math on any website using KaTeX. Careful with input fields! Be sure to have rendering OFF when entering an input field, otherwise you can mess up your delimiters. I have made a fix button for this, but it might not be exactly correct.
// @match        *://*/*
// @author       ParaMigi and ChatGPT
// @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
// @icon         https://images-wixmp-ed30a86b8c4ca887773594c2.wixmp.com/f/81c7f261-f956-486d-b688-8737c82fe364/d89cugg-d51ff456-9ced-4b87-97ab-f6ff06bb9cf2.png?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1cm46YXBwOjdlMGQxODg5ODIyNjQzNzNhNWYwZDQxNWVhMGQyNmUwIiwiaXNzIjoidXJuOmFwcDo3ZTBkMTg4OTgyMjY0MzczYTVmMGQ0MTVlYTBkMjZlMCIsIm9iaiI6W1t7InBhdGgiOiJcL2ZcLzgxYzdmMjYxLWY5NTYtNDg2ZC1iNjg4LTg3MzdjODJmZTM2NFwvZDg5Y3VnZy1kNTFmZjQ1Ni05Y2VkLTRiODctOTdhYi1mNmZmMDZiYjljZjIucG5nIn1dXSwiYXVkIjpbInVybjpzZXJ2aWNlOmZpbGUuZG93bmxvYWQiXX0.gdj9FL-s9pYJa6xIhrkmsn5E4vpH2-VeEZPDcqBbHSo
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    // User defined constants

    // LaTeX delimiters you want to support
    const delimiters = [
        { left: '$$', right: '$$', display: true },
        { left: '\\[', right: '\\]', display: true },
        { left: '\\(', right: '\\)', display: false },
        { left: '$', right: '$', display: false },
        { left: '[;', right: ';]', display: false },
        // same but with backtick
        { left: '`$$', right: '$$`', display: true },
        { left: '`\\[', right: '\\]`', display: true },
        { left: '`\\(', right: '\\)`', display: false },
        { left: '`$', right: '$`', display: false },
        { left: '`[;', right: ';]`', display: false }
    ];

    // Color of the rendered LaTeX
    const renderedLatexTextColor = 'red'; // set to null or false if you want to keep the original color
    const renderedLatexBackgroundColor = '#ffeeee'; // set to null or false if you don't want to have a background color
    const renderedLatexBorderColor = 'red'; // set to null or false if you don't want to have a border

    // How the buttons look
    const buttonTransparentOpacity = '0.5'; // 0 is fully transparent, 1 is fully solid.

    const toggleButtonTransparentText = '✨∫ π';
    const toggleButtonActiveText = '✨∫ π✨ LaTeX rendering is currently ON';
    const toggleButtonInactiveText = '✨∫ π✨ LaTeX rendering is currently OFF';

    const fixButtonTransparentText = '🛠️';
    const fixButtonText = '🛠️ Fix Input Field';




    // Inject KaTeX CSS
    const katexCSS = document.createElement('link');
    katexCSS.rel = 'stylesheet';
    katexCSS.href = 'https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.css';
    document.head.appendChild(katexCSS);

    // Create toggle button (initially hidden)
    const toggleButton = document.createElement('button');
    toggleButton.textContent = toggleButtonTransparentText;
    toggleButton.style.position = 'fixed';
    toggleButton.style.bottom = '15px';
    toggleButton.style.right = '50px';
    toggleButton.style.zIndex = 9999;
    toggleButton.style.padding = '3px 10px 6px 10px';
    toggleButton.style.background = '#333';
    toggleButton.style.color = 'white';
    toggleButton.style.border = '1px solid #999';
    toggleButton.style.borderRadius = '15px';
    toggleButton.style.cursor = 'pointer';
    toggleButton.style.fontSize = '14px';
    toggleButton.style.fontFamily = 'sans-serif';
    toggleButton.style.opacity = buttonTransparentOpacity; // Semi-transparent
    toggleButton.style.display = 'none'; // Hidden by default
    document.body.appendChild(toggleButton);

    let renderingEnabled = false;

    // Helper: strip delimiters from LaTeX string, e.g. "$...$" -> "..."
    function stripDelimiters(latex) {
        for (const d of delimiters) {
            if (latex.startsWith(d.left) && latex.endsWith(d.right)) {
                return latex.slice(d.left.length, latex.length - d.right.length);
            }
        }
        return latex;
    }

    // Render LaTeX math in the page by replacing text nodes with KaTeX-rendered spans
    function renderLatex() {
        if (!renderingEnabled) return;

        const latexPattern = new RegExp(
            delimiters
            .map(d => `(${d.left.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')}[^]*?${d.right.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')})`)
            .join('|'),
            'g'
        );

        const forbiddenTags = ['SCRIPT', 'STYLE', 'TEXTAREA', 'INPUT', 'BUTTON', 'SELECT'];

        const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, {
            acceptNode: function (node) {
                const el = node.parentElement;
                if (!el) return NodeFilter.FILTER_REJECT;

                if (forbiddenTags.includes(el.tagName)) return NodeFilter.FILTER_REJECT;
                if (el.closest('.katex-rendered')) return NodeFilter.FILTER_REJECT;
                if (!latexPattern.test(node.nodeValue)) return NodeFilter.FILTER_REJECT;

                return NodeFilter.FILTER_ACCEPT;
            }
        });


        const nodesToReplace = [];
        let node;
        while ((node = walker.nextNode())) {
            nodesToReplace.push(node);
        }

        for (const textNode of nodesToReplace) {
            const original = textNode.nodeValue;
            const parts = original.split(latexPattern).filter(p => p != null && p !== '');

            const fragment = document.createDocumentFragment();

            for (let part of parts) {
                const matched = delimiters.find(d => part.startsWith(d.left) && part.endsWith(d.right));
                if (matched) {
                    const latex = part.slice(matched.left.length, part.length - matched.right.length).replace(/\\mbox\b/g, '\\textnormal');
                    try {
                        const span = document.createElement('span');
                        const wrapper = document.createElement('div');
                        if (renderedLatexTextColor) {span.style.color = renderedLatexTextColor;};

                        wrapper.innerHTML = katex.renderToString(latex, {
                            throwOnError: false,
                            displayMode: matched.display
                        });

                        // Get the inner .katex element
                        const visualKatexElement = wrapper.querySelector('.katex-mathml') || wrapper.firstChild;
                        if (visualKatexElement) {
                            if (renderedLatexTextColor) {visualKatexElement.style.color = renderedLatexTextColor;};
                            if (renderedLatexBackgroundColor) {visualKatexElement.style.backgroundColor = renderedLatexBackgroundColor;};
                            if (renderedLatexBorderColor) {visualKatexElement.style.border = '1px solid '+renderedLatexBorderColor;};
                            if (renderedLatexBackgroundColor || renderedLatexBorderColor) {
                                visualKatexElement.style.padding = '4px';
                                visualKatexElement.style.borderRadius = '6px';
                                visualKatexElement.style.display = 'inline-block'; // ensure it wraps around the content properly
                            }
                        }

                        span.appendChild(visualKatexElement.cloneNode(true));

                        span.classList.add('katex-rendered');
                        span.dataset.latexSrc = part;
                        fragment.appendChild(span);
                    } catch (e) {
                        fragment.appendChild(document.createTextNode(part));
                    }
                } else {
                    fragment.appendChild(document.createTextNode(part));
                }
            }

            textNode.parentElement.replaceChild(fragment, textNode);
        }
    }


    // Revert rendered math back to original LaTeX source text
    function revertLatex() {
        document.querySelectorAll('.katex-rendered').forEach(el => {
            const originalLatex = el.dataset.latexSrc || el.textContent;
            const textNode = document.createTextNode(originalLatex);
            el.parentElement.replaceChild(textNode, el);
        });
    }


    // Check if page has any LaTeX delimiters to decide if toggle button should be shown
    function pageHasLatex() {
        const bodyText = document.body.innerText;
        return delimiters.some(d => {
            const pattern = new RegExp(d.left.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'));
            return pattern.test(bodyText);
        });
    }

    // Show the toggle button if LaTeX is detected in the page
    function updateButtonVisibility() {
        if (pageHasLatex()) {
            toggleButton.style.display = 'block';
        }
    }

    // Toggle button click handler
    toggleButton.onclick = () => {
        renderingEnabled = !renderingEnabled;
        toggleButton.textContent = renderingEnabled ? toggleButtonActiveText : toggleButtonInactiveText;
        if (renderingEnabled) {
            renderLatex();
        } else {
            revertLatex();
        }
    };

    // Button opacity hover effect and CTRL key hiding logic
    let ctrlHeld = false;

    toggleButton.addEventListener('mouseover', () => {
        toggleButton.style.opacity = '1';
        toggleButton.textContent = renderingEnabled ? toggleButtonActiveText : toggleButtonInactiveText;
    });

    toggleButton.addEventListener('mouseout', () => {
        if (!ctrlHeld) toggleButton.style.opacity = buttonTransparentOpacity;
        toggleButton.textContent = toggleButtonTransparentText;
    });

    document.addEventListener('keydown', (e) => {
        if (e.ctrlKey) {
            ctrlHeld = true;
            toggleButton.style.opacity = '0';
            toggleButton.style.pointerEvents = 'none';
            toggleButton.style.zIndex = 1;
        }
    });

    document.addEventListener('keyup', (e) => {
        if (!e.ctrlKey) {
            ctrlHeld = false;
            toggleButton.style.opacity = buttonTransparentOpacity;
            toggleButton.style.pointerEvents = 'auto';
            toggleButton.style.zIndex = 9999;
        }
    });

    // Create the fix button (only shown when rendering is off but still detected)
    const fixButton = document.createElement('button');
    fixButton.textContent = fixButtonTransparentText;
    fixButton.style.position = 'fixed';
    fixButton.style.bottom = '15px';
    fixButton.style.right = '200px'; // Left of the toggle-button
    fixButton.style.zIndex = 9999;
    fixButton.style.padding = '3px 10px 6px 10px';
    fixButton.style.background = '#444';
    fixButton.style.color = 'white';
    fixButton.style.border = '1px solid #999';
    fixButton.style.borderRadius = '15px';
    fixButton.style.cursor = 'pointer';
    fixButton.style.fontSize = '14px';
    fixButton.style.fontFamily = 'sans-serif';
    fixButton.style.opacity = buttonTransparentOpacity;
    fixButton.style.display = 'none'; // hidden initially
    document.body.appendChild(fixButton);


    // Function to update the visibility of the fix button
    let fixButtonVisible = false;

    function updateFixButtonVisibility() {
        if (renderingEnabled) return;
        const hasKatexRendered = document.querySelector('.katex-rendered') !== null;
        fixButton.style.display = hasKatexRendered ? 'block' : 'none';
        fixButtonVisible = hasKatexRendered;
    }

    fixButton.addEventListener('mouseover', () => {
        fixButton.style.opacity = '1';
        fixButton.textContent = fixButtonText;
    });
    fixButton.addEventListener('mouseout', () => {
        fixButton.style.opacity = buttonTransparentOpacity;
        fixButton.textContent = fixButtonTransparentText;
    });


    // Fix button click handler
    fixButton.onclick = () => {
        const renderedSpans = Array.from(document.querySelectorAll('.katex-rendered'));
        for (const span of renderedSpans) {
            const mathml = span.querySelector('.katex-mathml');
            if (!mathml) continue;

            const text = mathml.textContent.trim();

            // Remove first "word" before first space
            const firstSpaceIndex = text.indexOf(' ');
            const latexContent = firstSpaceIndex === -1
            ? text
            : text.slice(firstSpaceIndex + 1).trim();

            // Check if the span is the only content inside a paragraph
            const parent = span.parentElement;
            let finalLatex;
            if (parent && parent.tagName === 'P') {
                // Check if the paragraph only has this span and/or whitespace text nodes
                const onlyKatex = Array.from(parent.childNodes).every(node => {
                    return node === span ||
                        (node.nodeType === Node.TEXT_NODE && node.textContent.trim() === '');
                });

                if (onlyKatex) {
                    finalLatex = '\\[' + latexContent + '\\]';
                } else {
                    finalLatex = '[; ' + latexContent + ' ;]';
                }
            } else {
                finalLatex = '[; ' + latexContent + ' ;]';
            }

            const newNode = document.createTextNode(finalLatex);
            span.parentElement.replaceChild(newNode, span);
        }
        console.log('Fix Input Field replacement done');
    };


    // On start, check if page has LaTeX and show button if so
    setTimeout(() => {
        updateButtonVisibility();
        updateFixButtonVisibility();
        if (renderingEnabled) renderLatex();
    }, 500);

    setInterval(() => {
        updateButtonVisibility();
        updateFixButtonVisibility();
    }, 1000);


})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址