RL-Explorer / web /js /math.js
AdithyaSK's picture
AdithyaSK HF Staff
Deploy HF RL Explorer
da5cba1 verified
Raw History Blame Contribute Delete
3.49 kB
// Math in prompts, titles and task lists ($$…$$, \[…\], \(…\), $…$), typeset with KaTeX, which loads on the first page
// that has some. A "$…$" pair is math only when what's between looks like TeX (a backslash, ^, _ or braces, or a lone
// variable), so "spent $300 and set aside $200" stays prose. Code, preformatted text and anything marked .no-math are
// left alone; KaTeX runs untrusted (no \href, no \html*), and a formula it can't read stays as its source.
const KATEX = "https://cdn.jsdelivr.net/npm/katex@0.19.0/dist/";
const SRI = {
js: "sha384-QFFtAGzvvj+bfgCGxXJlNZZR1nXEZgvG8tDLCCY1F19xl20WlfTYgguB4VcNdxYk",
css: "sha384-3rdsX6e5mueWyoweR9NIVmtEsUkokpBT/0ALqKKIBMr9j4qhHkaIkAcGgsE6uVlp",
};
const RX = /\$\$([\s\S]{1,4000}?)\$\$|\\\[([\s\S]{1,4000}?)\\\]|\\\(([\s\S]{1,1000}?)\\\)|\$([^$\n]{1,400}?)\$/g;
const texy = (s) => /[\\^_{}]/.test(s) || /^\s*[A-Za-z]\s*$/.test(s);
const SKIP = "code, pre, textarea, script, style, .katex, .no-math, .fv, .runbox";
let loading = null;
function load() {
if (window.katex) return Promise.resolve();
if (loading) return loading;
const css = Object.assign(document.createElement("link"), { rel: "stylesheet", href: KATEX + "katex.min.css", integrity: SRI.css, crossOrigin: "anonymous" });
document.head.append(css);
loading = new Promise((resolve, reject) => {
const s = Object.assign(document.createElement("script"), { src: KATEX + "katex.min.js", integrity: SRI.js, crossOrigin: "anonymous", async: true });
s.onload = () => resolve();
s.onerror = () => { loading = null; reject(new Error("KaTeX didn't load")); };
document.head.append(s);
});
return loading;
}
function* formulas(text) {
RX.lastIndex = 0;
for (const m of text.matchAll(RX)) {
const [whole, dd, br, pa, d] = m;
if (d != null && !texy(d)) continue;
yield { at: m.index, whole, tex: dd ?? br ?? pa ?? d, display: dd != null || br != null };
}
}
export function hasMath(text) {
return !formulas(String(text || "")).next().done;
}
/** Typeset every formula under `root` (or each of several roots); a no-op, and no download, when there's none. */
export async function typeset(...roots) {
roots = roots.flat().filter(Boolean);
if (!roots.some((r) => hasMath(r.textContent))) return;
try { await load(); } catch { return; } // offline, or blocked: the formulas stay as their source
for (const root of roots) {
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
acceptNode: (n) => (n.parentElement?.closest(SKIP) ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT),
});
const nodes = [];
while (walker.nextNode()) nodes.push(walker.currentNode);
for (const n of nodes) {
const t = n.nodeValue;
if (!t.includes("$") && !t.includes("\\")) continue;
let last = 0, frag = null;
for (const f of formulas(t)) {
frag ??= document.createDocumentFragment();
frag.append(t.slice(last, f.at));
const span = document.createElement("span");
span.className = f.display ? "math display" : "math";
try {
span.innerHTML = window.katex.renderToString(f.tex, { displayMode: f.display, throwOnError: false, trust: false, maxSize: 20, maxExpand: 300, output: "html" });
} catch {
span.textContent = f.whole;
}
frag.append(span);
last = f.at + f.whole.length;
}
if (frag) { frag.append(t.slice(last)); n.replaceWith(frag); }
}
}
}