/** * Utility functions for string escaping, math normalization, and particles. */ import { state } from './state.js'; export function esc(s) { return s ? String(s) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'") : ""; } export function escAttr(s) { return String(s || "") .replace(/&/g, "&") .replace(/"/g, """) .replace(//g, ">"); } export function prettyText(s) { return applyGlossaryLinks(formatPrettyMathHtml(normalizePrettyMathSource(s || ""))); } export function normalizePrettyMathSource(text) { let s = String(text || ""); s = s.replace(/\r/g, ""); s = s.replace(/\\left|\\right/g, ""); s = s.replace(/\\,/g, " "); s = s.replace(/\\cdot/g, "·").replace(/\\times/g, "×"); s = s.replace(/\\pi/g, "π").replace(/\\infty/g, "∞"); s = s.replace(/\\to|\\rightarrow|->/g, "→"); s = s.replace(/\\lim/g, "lim"); s = s.replace(/\\int/g, "∫"); s = s.replace(/\\sin/g, "sin").replace(/\\cos/g, "cos").replace(/\\tan/g, "tan"); s = s.replace(/\\ln/g, "ln").replace(/\\log/g, "log").replace(/\\sqrt/g, "sqrt"); s = s.replace(/\\frac\{d(?:\^(\d+)|\{([^{}]+)\})?\}\{d([a-zA-Z])(?:\^(\d+)|\{([^{}]+)\})?\}/g, (_m, p1, p1b, v, p2, p2b) => { const n = p1 || p1b || p2 || p2b || ""; return `d${n ? "^(" + n + ")" : ""}/d${v}${n ? "^(" + n + ")" : ""}`; }); for (let i = 0; i < 6; i++) { const next = s.replace(/\\frac\{([^{}]+)\}\{([^{}]+)\}/g, "($1)/($2)"); if (next === s) break; s = next; } s = s.replace(/[{}]/g, m => m === "{" ? "(" : ")"); s = s.replace(/\\([a-zA-Z]+)/g, "$1"); s = s.replace(/\bsqrt\b/g, "√"); s = s.replace(/[ \t]+/g, " ").replace(/\n{3,}/g, "\n\n").trim(); return s; } export function formatPrettyMathHtml(text) { let t = esc(text || ""); for (let i = 0; i < 6; i++) { const next = t.replace(/\(([^()]+)\)\s*\/\s*\(([^()]+)\)/g, (_m, num, den) => `${num}${den}` ); if (next === t) break; t = next; } t = t.replace(/(^|[\s=+\-*(])([a-zA-Z0-9.]+)\s*\/\s*([a-zA-Z0-9.]+)(?=$|[\s=+\-*)])/g, (_m, pre, num, den) => `${pre}${num}${den}`); t = t.replace(/\^\{([^}]+)\}/g, (_m, exp) => `${exp}`); t = t.replace(/\^\(([^)]+)\)/g, (_m, exp) => `${exp}`); t = t.replace(/\^([a-zA-Z0-9+\-]+)/g, (_m, exp) => `${exp}`); t = t.replace(/_\{([^}]+)\}/g, (_m, sub) => `${sub}`); t = t.replace(/_\(([^)]+)\)/g, (_m, sub) => `${sub}`); t = t.replace(/_([a-zA-Z0-9+\-]+)/g, (_m, sub) => `${sub}`); return t; } export function applyGlossaryLinks(html) { if (!html || !state.glossaryLexicon || !state.glossaryLexicon.length) return html; const root = document.createElement("div"); root.innerHTML = html; const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT); const textNodes = []; let node = walker.nextNode(); while (node) { if (node.nodeValue && node.nodeValue.trim()) textNodes.push(node); node = walker.nextNode(); } textNodes.forEach(textNode => { let text = textNode.nodeValue; let replaced = false; state.glossaryLexicon.slice(0, 30).forEach(entry => { const escaped = entry.token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const re = new RegExp(`\\b${escaped}\\b`, "i"); if (!re.test(text)) return; text = text.replace(re, `$&`); replaced = true; }); if (!replaced) return; const span = document.createElement("span"); span.innerHTML = text; textNode.parentNode.replaceChild(span, textNode); }); return root.innerHTML; } export function normalizeDisplayMath(text) { let s = String(text || ""); s = s.replace(/\r/g, ""); s = s.replace(/\\left|\\right/g, ""); s = s.replace(/\\,/g, " "); s = s.replace(/\\cdot/g, "·").replace(/\\times/g, "×"); s = s.replace(/\\pi/g, "π").replace(/\\infty/g, "∞"); s = s.replace(/\\to|\\rightarrow|->/g, "→"); s = s.replace(/\\lim/g, "lim"); s = s.replace(/\\int/g, "∫"); s = s.replace(/\\sin/g, "sin").replace(/\\cos/g, "cos").replace(/\\tan/g, "tan"); s = s.replace(/\\ln/g, "ln").replace(/\\log/g, "log").replace(/\\sqrt/g, "sqrt"); s = s.replace(/\\frac\{d(?:\^(\d+))?\}\{d([a-zA-Z])(?:\^(\d+))?\}/g, (_, p1, v, p2) => { const n = p1 || p2 || ""; return `d${n ? "^" + n : ""}/d${v}${n ? "^" + n : ""}`; }); s = s.replace(/\\frac\{([^{}]+)\}\{([^{}]+)\}/g, "($1)/($2)"); s = s.replace(/[{}]/g, m => m === "{" ? "(" : ")"); s = s.replace(/\\([a-zA-Z]+)/g, "$1"); s = s.replace(/\bsqrt\b/g, "√"); s = applyPrettyScripts(s); s = s.replace(/[ \t]+/g, " ").replace(/\n{3,}/g, "\n\n").trim(); return s; } export function applyPrettyScripts(s) { s = s.replace(/\^\(([^)]+)\)/g, (_, p1) => toSuperscript("(" + p1 + ")")); s = s.replace(/_\(([^)]+)\)/g, (_, p1) => toSubscript("(" + p1 + ")")); s = s.replace(/\^([+\-]?\d+)/g, (_, p1) => toSuperscript(p1)); s = s.replace(/\^([a-zA-Z]+)/g, (_, p1) => toSuperscript(p1)); s = s.replace(/_([+\-]?\d+)/g, (_, p1) => toSubscript(p1)); s = s.replace(/_([a-zA-Z]+)/g, (_, p1) => toSubscript(p1)); return s; } export function revertPrettyScripts(s) { s = s.replace(/([⁰¹²³⁴⁵⁶⁷⁸⁹⁺⁻⁽⁾⁼ᵃᵇᶜᵈᵉᶠᵍʰⁱʲᵏˡᵐⁿᵒᵖʳˢᵗᵘᵛʷˣʸᶻᴬᴮᴰᴱᴳᴴᴵᴶᴷᴸᴹᴺᴼᴾᴿᵀᵁⱽᵂ]+)/g, (_, p1) => "^" + fromSuperscript(p1)); s = s.replace(/([₀₁₂₃₄₅₆₇₈₉₊₋₍₎₌ₐₑₕᵢⱼₖₗₘₙₒₚᵣₛₜᵤᵥₓ]+)/g, (_, p1) => "_" + fromSubscript(p1)); return s; } export function toSuperscript(raw) { const map = {"0":"⁰","1":"¹","2":"²","3":"³","4":"⁴","5":"⁵","6":"⁶","7":"⁷","8":"⁸","9":"⁹","+":"⁺","-":"⁻","(":"⁽",")":"⁾","=":"⁼","a":"ᵃ","b":"ᵇ","c":"ᶜ","d":"ᵈ","e":"ᵉ","f":"ᶠ","g":"ᵍ","h":"ʰ","i":"ⁱ","j":"ʲ","k":"ᵏ","l":"ˡ","m":"ᵐ","n":"ⁿ","o":"ᵒ","p":"ᵖ","r":"ʳ","s":"ˢ","t":"ᵗ","u":"ᵘ","v":"ᵛ","w":"ʷ","x":"ˣ","y":"ʸ","z":"ᶻ","A":"ᴬ","B":"ᴮ","D":"ᴰ","E":"ᴱ","G":"ᴳ","H":"ᴴ","I":"ᴵ","J":"ᴶ","K":"ᴷ","L":"ᴸ","M":"ᴹ","N":"ᴺ","O":"ᴼ","P":"ᴾ","R":"ᴿ","T":"ᵀ","U":"ᵁ","V":"ⱽ","W":"ᵂ"}; return String(raw).split("").map(ch => map[ch] || ch).join(""); } export function toSubscript(raw) { const map = {"0":"₀","1":"₁","2":"₂","3":"₃","4":"₄","5":"₅","6":"₆","7":"₇","8":"₈","9":"₉","+":"₊","-":"₋","(":"₍",")":"₎","=":"₌","a":"ₐ","e":"ₑ","h":"ₕ","i":"ᵢ","j":"ⱼ","k":"ₖ","l":"ₗ","m":"ₘ","n":"ₙ","o":"ₒ","p":"ₚ","r":"ᵣ","s":"ₛ","t":"ₜ","u":"ᵤ","v":"ᵥ","x":"ₓ"}; return String(raw).split("").map(ch => map[ch] || ch).join(""); } export function fromSuperscript(raw) { const map = {"⁰":"0","¹":"1","²":"2","³":"3","⁴":"4","⁵":"5","⁶":"6","⁷":"7","⁸":"8","⁹":"9","⁺":"+","⁻":"-","⁽":"(","⁾":")","⁼":"=","ᵃ":"a","ᵇ":"b","ᶜ":"c","ᵈ":"d","ᵉ":"e","ᶠ":"f","ᵍ":"g","ʰ":"h","ⁱ":"i","ʲ":"j","ᵏ":"k","ˡ":"l","ᵐ":"m","ⁿ":"n","ᵒ":"o","ᵖ":"p","ʳ":"r","ˢ":"s","ᵗ":"t","ᵘ":"u","ᵛ":"v","ʷ":"w","ˣ":"x","ʸ":"y","ᶻ":"z","ᴬ":"A","ᴮ":"B","ᴰ":"D","ᴱ":"E","ᴳ":"G","ᴴ":"H","ᴵ":"I","ᴶ":"J","ᴷ":"K","ᴸ":"L","ᴹ":"M","ᴺ":"N","ᴼ":"O","ᴾ":"P","ᴿ":"R","ᵀ":"T","ᵁ":"U","ⱽ":"V","ᵂ":"W"}; return String(raw).split("").map(ch => map[ch] || ch).join(""); } export function fromSubscript(raw) { const map = {"₀":"0","₁":"1","₂":"2","₃":"3","₄":"4","₅":"5","₆":"6","₇":"7","₈":"8","₉":"9","₊":"+","₋":"-","₍":"(","₎":")","₌":"=","ₐ":"a","ₑ":"e","ₕ":"h","ᵢ":"i","ⱼ":"j","ₖ":"k","ₗ":"l","ₘ":"m","ₙ":"n","ₒ":"o","ₚ":"p","ᵣ":"r","ₛ":"s","ₜ":"t","ᵤ":"u","ᵥ":"v","ₓ":"x"}; return String(raw).split("").map(ch => map[ch] || ch).join(""); } export function hashString(str) { let h = 2166136261; for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); } return h >>> 0; } export function mulberry32(seed) { let t = seed >>> 0; return function () { t += 0x6D2B79F5; let r = Math.imul(t ^ (t >>> 15), 1 | t); r ^= r + Math.imul(r ^ (r >>> 7), 61 | r); return ((r ^ (r >>> 14)) >>> 0) / 4294967296; }; } export function lerp(a, b, t) { return a + (b - a) * t; } export function copyText(text) { if (!text) return; if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(text).catch(() => fallbackCopy(text)); return; } fallbackCopy(text); } export function fallbackCopy(text) { const ta = document.createElement("textarea"); ta.value = text; ta.style.position = "fixed"; ta.style.left = "-9999px"; document.body.appendChild(ta); ta.select(); try { document.execCommand("copy"); } catch (_) {} ta.remove(); } export function tokenizeGlyphTokens(latex) { const cmdMap = { "\\sin": "sin", "\\cos": "cos", "\\tan": "tan", "\\ln": "ln", "\\log": "log", "\\sqrt": "sqrt", "\\frac": "/", "\\cdot": "·", "\\times": "×", "\\pi": "pi", "\\infty": "∞", }; const raw = String(latex || "").replace(/\\left|\\right|\\,/g, " "); const bits = raw.match(/\\[a-zA-Z]+|[A-Za-z]+(?:\^\d+)?|\d+|[+\-*/=()^]|./g) || []; const out = []; for (const b of bits) { const t = b.trim(); if (!t || t === "{" || t === "}" || t === "_") continue; if (cmdMap[t]) { out.push(cmdMap[t]); continue; } if (t.startsWith("\\")) continue; out.push(t); } const merged = []; for (let i = 0; i < out.length; i++) { const t = out[i]; if (i + 2 < out.length && out[i + 1] === "^" && /^[0-9a-zA-Z]+$/.test(out[i + 2])) { merged.push(`${t}^${out[i + 2]}`); i += 2; continue; } merged.push(t); } const clipped = merged.filter(Boolean).slice(0, 40); return clipped.length ? clipped : ["x"]; }