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