您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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.
// ==UserScript== // @name Universal Inline & Display LaTeX Renderer (KaTeX) // @namespace http://tampermonkey.net/ // @version 2025-07-13.5.2 // @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'; // Render automatically on loading the page const autoRender = false; // 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 = autoRender ? true : 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) // The following lines are hacks to fix some formatting issues on some websites, or commands that KaTeX does not recognize .replace(/\\mbox\b/g, '\\textnormal') // hack for mbox not being recognized by KaTeX .replace(/\\left\{/g, '\\left\\{') // hack for when \left\{ is already being formatted into \left{ (like on reddit) .replace(/\\right\}/g, '\\right\\}') // same for right .replace(/\\begin\{(array|tabular|matrix)[^}]*\}([\s\S]*?)\\end\{\1\}/g, (match, env, content) => { const fixedContent = content.replace(/(^|[^\\])\\\s/g, '$1\\\\ '); return `\\begin{${env}}${fixedContent}\\end{${env}}`; }); // hack for when \\ in an array, table, or matrix environment is being formatted to \ (like on reddit). 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 (but only the rendered part, not the fallback plaintext) const visualKatexElement = wrapper.querySelector('.katex-mathml') || wrapper.firstChild; // Set the styling as defined at the start 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(); revertLatex(); 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(); revertLatex(); renderLatex(); } }, 500); setInterval(() => { updateButtonVisibility(); updateFixButtonVisibility(); }, 1000); })();