Renderiza el formato KaTeX durante los chats de Janitor.
// ==UserScript== // @name JaniTeX // @name:es JaniTeX // @namespace https://janitorai.com/ // @version 1.8.1 // @description Render KaTeX formatting during Janitor chats. // @description:es Renderiza el formato KaTeX durante los chats de Janitor. // @match https://janitorai.com/chats/* // @grant none // @license MIT // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.js // @run-at document-end // ==/UserScript== (function () { 'use strict'; // ========================= // Load KaTeX CSS // ========================= const css = document.createElement('link'); css.rel = 'stylesheet'; css.href = 'https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.css'; document.head.appendChild(css); // ========================= // Hide KaTeX HTML layer // ========================= const fixCss = document.createElement('style'); fixCss.textContent = ` .katex .katex-html { display: none !important; } .janitor-katex { max-width: 100%; } .janitor-katex.janitor-katex-wrapped { display: block; max-width: 100%; overflow-x: hidden; text-align: left; } .janitor-katex.janitor-katex-wrapped .katex-display { margin: 0; text-align: left; } .janitor-katex.janitor-katex-wrapped .katex { max-width: 100%; } `; document.head.appendChild(fixCss); // ========================= // Supported delimiters // ========================= const mathRegex = /\[\$([\s\S]*?)\$\]|\$\$([\s\S]*?)\$\$|\\\(([\s\S]*?)\\\)|\\\[([\s\S]*?)\\\]/; // ========================= // Get text nodes // ========================= function getTextNodes(message) { const walker = document.createTreeWalker( message, NodeFilter.SHOW_TEXT ); const nodes = []; let node; while ((node = walker.nextNode())) { if (node.parentElement?.closest('.janitor-katex')) { continue; } if (node.parentElement?.closest('.katex')) { continue; } nodes.push(node); } return nodes; } // ========================= // Find math across nodes // ========================= function findMath(message) { const nodes = getTextNodes(message); if (!nodes.length) { return null; } let combined = ''; const positions = []; for (const node of nodes) { const start = combined.length; combined += node.nodeValue; const end = combined.length; positions.push({ node, start, end }); } const match = mathRegex.exec(combined); if (!match) { return null; } const mathSource = match[1] ?? match[2] ?? match[3] ?? match[4]; const displayMode = match[2] !== undefined || match[4] !== undefined; const start = match.index; const end = match.index + match[0].length; let startInfo = null; let endInfo = null; for (const info of positions) { if ( startInfo === null && start >= info.start && start <= info.end ) { startInfo = info; } if ( end >= info.start && end <= info.end ) { endInfo = info; break; } } if (!startInfo || !endInfo) { return null; } return { startNode: startInfo.node, startOffset: start - startInfo.start, endNode: endInfo.node, endOffset: end - endInfo.start, mathSource, displayMode }; } // ========================= // Find a top-level \text{...} // ========================= function findTextBlock(source) { const start = source.indexOf('\\text{'); if (start === -1) { return null; } let depth = 0; let escaped = false; for (let i = start + 5; i < source.length; i++) { const char = source[i]; if (escaped) { escaped = false; continue; } if (char === '\\') { escaped = true; continue; } if (char === '{') { depth++; continue; } if (char === '}') { depth--; if (depth === 0) { return { start, end: i + 1, text: source.slice(start + 6, i) }; } } } return null; } // ========================= // Split long \text{...} // at spaces // ========================= function splitTextMath(source, availableWidth) { const block = findTextBlock(source); if (!block) { return null; } const words = block.text.trim().split(/\s+/); if (words.length < 2) { return null; } const lines = []; let current = ''; const tester = document.createElement('span'); tester.style.position = 'absolute'; tester.style.visibility = 'hidden'; tester.style.pointerEvents = 'none'; tester.style.whiteSpace = 'nowrap'; document.body.appendChild(tester); for (const word of words) { const candidate = current ? `${current} ${word}` : word; tester.innerHTML = ''; window.katex.render( `\\text{${candidate}}`, tester, { displayMode: false, throwOnError: false, strict: (errorCode) => { if (errorCode === 'newLineInDisplayMode') { return 'ignore'; } return 'warn'; } } ); const width = tester.getBoundingClientRect().width; if ( current && width > availableWidth ) { lines.push(current); current = word; } else { current = candidate; } } if (current) { lines.push(current); } tester.remove(); if (lines.length < 2) { return null; } const wrappedText = lines .map(line => `\\text{${line}}`) .join('\\\\'); return ( source.slice(0, block.start) + '\\begin{array}{l}' + wrappedText + '\\end{array}' + source.slice(block.end) ); } // ========================= // Split normal math // at top-level operators // ========================= function splitOperatorMath(source) { const candidates = []; let parenDepth = 0; let braceDepth = 0; let escaped = false; for (let i = 0; i < source.length; i++) { const char = source[i]; if (escaped) { escaped = false; continue; } if (char === '\\') { escaped = true; continue; } if (char === '{') { braceDepth++; continue; } if (char === '}') { braceDepth = Math.max(0, braceDepth - 1); continue; } if (char === '(' || char === '[') { parenDepth++; continue; } if (char === ')' || char === ']') { parenDepth = Math.max(0, parenDepth - 1); continue; } if (braceDepth !== 0 || parenDepth !== 0) { continue; } if ( char === '+' || char === '-' || char === '=' ) { candidates.push(i); } if (source.startsWith('\\cdot', i)) { candidates.push(i); i += 4; } if (source.startsWith('\\times', i)) { candidates.push(i); i += 5; } if (source.startsWith('\\div', i)) { candidates.push(i); i += 3; } } if (!candidates.length) { return null; } const lines = []; let start = 0; for (const position of candidates) { const piece = source.slice(start, position).trim(); if (piece.length >= 15) { lines.push(piece); start = position; } } lines.push( source.slice(start).trim() ); if (lines.length < 2) { return null; } return ( '\\begin{aligned}' + lines .map((line, index) => { if (index === 0) return line; return '&' + line; }) .join('\\\\') + '\\end{aligned}' ); } // ========================= // Render KaTeX // ========================= function renderKatex( source, displayMode, container ) { container.innerHTML = ''; window.katex.render( source, container, { displayMode, throwOnError: false, strict: (errorCode) => { if ( errorCode === 'newLineInDisplayMode' ) { return 'ignore'; } return 'warn'; } } ); } // ========================= // Get available width // ========================= function getAvailableWidth(container) { const message = container.parentElement?.closest('p[node]'); if (!message) { return 0; } const containerRect = container.getBoundingClientRect(); const messageRect = message.getBoundingClientRect(); return Math.max( 0, messageRect.right - containerRect.left ); } // ========================= // Automatically wrap // ========================= function autoWrap( container, originalSource, availableWidth ) { requestAnimationFrame(() => { if (!availableWidth) { return; } let wrappedSource = splitTextMath( originalSource, availableWidth ); if (!wrappedSource) { wrappedSource = splitOperatorMath( originalSource ); } if (!wrappedSource) { return; } container.classList.add( 'janitor-katex-wrapped' ); container.dataset.wrapped = 'true'; renderKatex( wrappedSource, true, container ); }); } // ========================= // Replace one expression // ========================= function replaceMath(info) { const range = document.createRange(); range.setStart( info.startNode, info.startOffset ); range.setEnd( info.endNode, info.endOffset ); const lineStartRange = document.createRange(); lineStartRange.setStart( info.startNode, info.startOffset ); lineStartRange.collapse(true); const caretRect = lineStartRange.getBoundingClientRect(); const message = info.startNode.parentElement?.closest('p[node]'); let availableWidth = 0; if (message) { const messageRect = message.getBoundingClientRect(); availableWidth = messageRect.right - caretRect.left; } lineStartRange.detach(); const container = document.createElement('span'); container.className = 'janitor-katex'; container.dataset.originalMath = info.mathSource; renderKatex( info.mathSource, info.displayMode, container ); range.deleteContents(); range.insertNode(container); autoWrap( container, info.mathSource, availableWidth ); } // ========================= // Render one message // ========================= function renderMessage(message) { if (typeof window.katex === 'undefined') { return; } let safety = 0; while (safety++ < 100) { const math = findMath(message); if (!math) { break; } replaceMath(math); } } // ========================= // Re-check existing equations // ========================= function recheckWrapping() { document .querySelectorAll('.janitor-katex') .forEach(container => { const original = container.dataset.originalMath; if (!original) { return; } container.classList.remove( 'janitor-katex-wrapped' ); delete container.dataset.wrapped; renderKatex( original, false, container ); const availableWidth = getAvailableWidth(container); autoWrap( container, original, availableWidth ); }); } // ========================= // Render everything // ========================= function renderMath() { document .querySelectorAll('p[node]') .forEach(renderMessage); } // ========================= // Initial run // ========================= function run() { renderMath(); setTimeout(renderMath, 50); setTimeout(renderMath, 150); setTimeout(renderMath, 300); setTimeout(renderMath, 600); setTimeout(renderMath, 1000); } console.log( '[JaniTeX] v1.8.1 loaded!' ); run(); // ========================= // Watch Janitor // ========================= let timeout = null; const observer = new MutationObserver(() => { clearTimeout(timeout); timeout = setTimeout(() => { renderMath(); }, 100); }); observer.observe(document.body, { childList: true, subtree: true }); // ========================= // Handle window resizing // ========================= let resizeTimeout = null; window.addEventListener( 'resize', () => { clearTimeout(resizeTimeout); resizeTimeout = setTimeout(() => { recheckWrapping(); }, 150); } ); })();