// ========================================== // KaTeX Math Renderer — Browser Extension // ========================================== // Renders ```latex / ```math code blocks and inline $...$ / $$...$$ syntax. // Loads KaTeX dynamically on first use. // // Registers with sw.renderers via the sw:ready event. // ========================================== (function () { 'use strict'; let _katexReady = false; let _katexLoading = null; function _escapeHtml(str) { const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } // ── KaTeX Library Loading ────────────── function _loadKaTeX() { if (_katexReady) return Promise.resolve(); if (_katexLoading) return _katexLoading; _katexLoading = new Promise((resolve, reject) => { if (typeof katex !== 'undefined') { _katexReady = true; resolve(); return; } const base = (window.__BASE__ || ''); const localCSS = `${base}/vendor/katex/katex.min.css`; const localJS = `${base}/vendor/katex/katex.min.js`; const cdnCSS = 'https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css'; const cdnJS = 'https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.js'; const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = localCSS; link.onerror = () => { link.href = cdnCSS; }; document.head.appendChild(link); const script = document.createElement('script'); script.src = localJS; script.onload = () => { _katexReady = true; resolve(); }; script.onerror = () => { console.warn('[KaTeX] Local vendor not found, trying CDN'); const cdn = document.createElement('script'); cdn.src = cdnJS; cdn.onload = () => { _katexReady = true; resolve(); }; cdn.onerror = () => { console.error('[KaTeX] Failed to load from both local and CDN'); reject(new Error('Failed to load KaTeX')); }; document.head.appendChild(cdn); }; document.head.appendChild(script); }); return _katexLoading; } // ── Block Rendering ────────────────────── async function _renderBlock(el) { const code = decodeURIComponent(el.dataset.katexSrc); const displayMode = el.dataset.katexDisplay === 'true'; try { await _loadKaTeX(); el.innerHTML = katex.renderToString(code, { displayMode, throwOnError: false, trust: false, strict: false, }); el.dataset.rendered = 'true'; } catch (e) { el.innerHTML = `
Math error: ${_escapeHtml(e.message || String(e))}
`; el.dataset.rendered = 'error'; } } // ── Inline Math Processing ─────────────── async function _processInlineMath(container) { if (!container.textContent || !container.textContent.includes('$')) return; try { await _loadKaTeX(); } catch { return; } const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, { acceptNode(node) { const parent = node.parentElement; if (!parent) return NodeFilter.FILTER_REJECT; const tag = parent.tagName; if (tag === 'CODE' || tag === 'PRE' || tag === 'SCRIPT' || tag === 'TEXTAREA' || tag === 'STYLE') return NodeFilter.FILTER_REJECT; if (parent.closest('.katex, .katex-block, .katex-display-container, code, pre')) return NodeFilter.FILTER_REJECT; if (!node.textContent.includes('$')) return NodeFilter.FILTER_REJECT; return NodeFilter.FILTER_ACCEPT; } }); const textNodes = []; let node; while (node = walker.nextNode()) textNodes.push(node); for (const textNode of textNodes) _replaceInlineMath(textNode); } function _replaceInlineMath(textNode) { const text = textNode.textContent; const pattern = /\$\$([^$]+?)\$\$|\$([^$\n]+?)\$/g; let match; const parts = []; let lastIndex = 0; while ((match = pattern.exec(text)) !== null) { if (match.index > lastIndex) { parts.push({ type: 'text', value: text.slice(lastIndex, match.index) }); } const displayMode = !!match[1]; const expr = match[1] || match[2]; try { const html = katex.renderToString(expr.trim(), { displayMode, throwOnError: false, trust: false, strict: false, }); parts.push({ type: 'katex', value: html, displayMode }); } catch { parts.push({ type: 'text', value: match[0] }); } lastIndex = match.index + match[0].length; } if (parts.length === 0) return; if (lastIndex < text.length) parts.push({ type: 'text', value: text.slice(lastIndex) }); const span = document.createElement('span'); span.className = 'katex-inline-container'; for (const part of parts) { if (part.type === 'text') { span.appendChild(document.createTextNode(part.value)); } else { const wrapper = document.createElement('span'); wrapper.className = part.displayMode ? 'katex-display' : 'katex-inline'; wrapper.innerHTML = part.value; span.appendChild(wrapper); } } textNode.parentNode.replaceChild(span, textNode); } // ── Styles ────────────────────────────── function _injectStyles() { if (document.getElementById('ext-style-katex-renderer')) return; const style = document.createElement('style'); style.id = 'ext-style-katex-renderer'; style.textContent = ` .katex-block { background: var(--bg-2); border: 1px solid var(--border); border-radius: 8px; overflow: hidden; margin: 12px 0; } .katex-display-container { padding: 16px; text-align: center; min-height: 40px; } .katex-loading { color: var(--text-3); font-size: 13px; padding: 20px; display: flex; align-items: center; justify-content: center; gap: 8px; } .katex-spinner { display: inline-block; width: 14px; height: 14px; border: 2px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: katex-spin 0.8s linear infinite; } @keyframes katex-spin { to { transform: rotate(360deg); } } .katex-error { color: var(--error, #e74c3c); background: var(--bg-3, rgba(231,76,60,0.1)); padding: 12px 16px; border-radius: 4px; font-size: 13px; font-family: var(--mono); white-space: pre-wrap; } .katex-source { border-top: 1px solid var(--border); font-size: 12px; } .katex-source summary { padding: 6px 12px; cursor: pointer; color: var(--text-3); user-select: none; } .katex-source summary:hover { color: var(--text-2); } .katex-source pre { margin: 0; border-radius: 0; border: none; max-height: 200px; overflow: auto; } .katex-inline-container { display: inline; } .katex-inline .katex { font-size: 1.05em; } .katex-display { display: block; text-align: center; margin: 8px 0; }`; document.head.appendChild(style); } // ── Registration ──────────────────────── function register() { if (!window.sw?.renderers) return; _injectStyles(); // Block renderer: match ```latex, ```math, ```tex, ```katex sw.renderers.register('katex-block', { type: 'block', priority: 10, match(lang) { const l = (lang || '').toLowerCase(); return l === 'latex' || l === 'math' || l === 'tex' || l === 'katex'; }, render(lang, code, container) { const id = 'katex-' + Math.random().toString(36).slice(2, 9); container.innerHTML = `
Rendering math\u2026
\ud835\udc53(\ud835\udc65) View source
${_escapeHtml(code.trim())}
`; } }); // Post renderer: render KaTeX blocks + find inline math sw.renderers.register('katex-post', { type: 'post', priority: 10, render(container) { const blocks = container.querySelectorAll('.katex-display-container[data-katex-src]'); if (blocks.length > 0) { blocks.forEach(el => { if (el.dataset.rendered) return; el.dataset.rendered = 'pending'; _renderBlock(el); }); } _processInlineMath(container); } }); _loadKaTeX(); } if (window.sw?._sdk) { register(); } else { document.addEventListener('sw:ready', register, { once: true }); } })();