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