(function(){ "use strict"; /* ===== src/core.js ===== */ /*! * Fibo Math UI Engine v1 - TEAM CORE * Shared math infrastructure for fibotutors.com (owner-authorized). * * Scope: expression contract (safe parser, no eval), normalizer, LaTeX * serializer, FiboMathDisplay (lazy KaTeX with MathJax and minimal fallbacks), * LaTeX sanitizer. Registers under window.__FiboMathNS without overwriting * other teams' methods. * * WordPress-safe source: no bare ampersand character, no logical-and * operator anywhere in this file. De Morgan form is used instead. */ (function () { 'use strict'; var root = typeof window !== 'undefined' ? window : (typeof globalThis !== 'undefined' ? globalThis : this); /* ---------------- tiny helpers ---------------- */ function hasOwn(o, k) { return Object.prototype.hasOwnProperty.call(o, k); } function tokIs(p, t) { return !(!p || p.t !== t); } function mathError(code, message, at) { return { ok: false, error: { code: code, message: message, at: (typeof at === 'number' ? at : -1) } }; } function isErr(x) { return !x || x.ok === false; } /* ---------------- tokenizer ---------------- */ var FUNC_NAMES = { sin: 1, cos: 1, tan: 1, asin: 1, acos: 1, atan: 1, sqrt: 1, log: 1, ln: 1, exp: 1, abs: 1 }; var CONST_NAMES = { pi: 1, e: 1 }; var MAX_INPUT = 500; var MAX_DEPTH = 64; function isDigit(ch) { return !(!(ch >= '0') || !(ch <= '9')); } function isLetter(ch) { return (!(!(ch >= 'a') || !(ch <= 'z'))) || (!(!(ch >= 'A') || !(ch <= 'Z'))); } function tokenize(raw) { var tokens = []; var i = 0; var n = raw.length; while (i < n) { var ch = raw.charAt(i); if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') { i++; continue; } if (isDigit(ch) || ch === '.') { var j = i; var dot = false; var digits = false; while (j < n) { var c = raw.charAt(j); if (isDigit(c)) { digits = true; j++; continue; } if (!(!(c === '.') || !(!dot))) { dot = true; j++; continue; } break; } if (!digits) return mathError('LEX', 'A lone decimal point is not a number.', i); var word = raw.slice(i, j); var val = parseFloat(word); if (!isFinite(val)) return mathError('LEX', 'That number is too large to use.', i); tokens.push({ t: 'num', v: word, num: val, pos: i }); i = j; continue; } if (isLetter(ch) || ch === '_') { var k = i; while (k < n) { var d = raw.charAt(k); if (isLetter(d) || isDigit(d) || d === '_') { k++; continue; } break; } var name = raw.slice(i, k); if (name.indexOf('__') !== -1) { return mathError('LEX', '"' + name + '" is not allowed. Use single letters like x, or the constants pi and e.', i); } if (name.charAt(0) === '_') { return mathError('LEX', 'Names may not start with an underscore.', i); } tokens.push({ t: 'id', v: name, pos: i }); i = k; continue; } if (ch === '+' || ch === '-' || ch === '*' || ch === '/' || ch === '^' || ch === '(' || ch === ')') { tokens.push({ t: ch, pos: i }); i++; continue; } return mathError('LEX', 'Character "' + ch + '" is not math. Use digits, letters, and + - * / ^ ( ).', i); } return { ok: true, tokens: tokens }; } /* ---------------- parser (recursive descent, no eval) ---------------- */ function Parser(tokens) { this.t = tokens; this.i = 0; this.depth = 0; } Parser.prototype.peek = function () { return this.i < this.t.length ? this.t[this.i] : null; }; Parser.prototype.next = function () { var x = this.peek(); if (x) this.i++; return x; }; Parser.prototype.guardDepth = function () { if (this.depth > MAX_DEPTH) return mathError('DEEP', 'That expression is nested too deeply. Try splitting it up.', -1); return null; }; Parser.prototype.run = function () { if (this.t.length === 0) return mathError('EMPTY', 'Type some math first, for example 2x+4.', -1); var e = this.parseExpr(); if (isErr(e)) return e; var extra = this.peek(); if (extra) return mathError('TRAIL', 'I did not understand "' + extra.v + '" here. Check for a missing operator.', extra.pos); return { ok: true, ast: e }; }; Parser.prototype.parseExpr = function () { var g = this.guardDepth(); if (g) return g; this.depth++; var left = this.parseTerm(); if (isErr(left)) { this.depth--; return left; } for (;;) { var p = this.peek(); if (tokIs(p, '+') || tokIs(p, '-')) { this.next(); var r = this.parseTerm(); if (isErr(r)) { this.depth--; return r; } left = { type: 'op', op: p.t, left: left, right: r }; continue; } this.depth--; return left; } }; function startsPrimary(p) { return !(!p || !(p.t === 'num' || p.t === 'id' || p.t === '(')); } Parser.prototype.parseTerm = function () { var g = this.guardDepth(); if (g) return g; this.depth++; var left = this.parseUnary(); if (isErr(left)) { this.depth--; return left; } for (;;) { var p = this.peek(); if (tokIs(p, '*') || tokIs(p, '/')) { this.next(); var r = this.parseUnary(); if (isErr(r)) { this.depth--; return r; } left = { type: 'op', op: p.t, left: left, right: r }; continue; } if (startsPrimary(p)) { var r2 = this.parseFactor(); if (isErr(r2)) { this.depth--; return r2; } left = { type: 'op', op: '*', left: left, right: r2, implicit: true }; continue; } this.depth--; return left; } }; Parser.prototype.parseFactor = function () { var g = this.guardDepth(); if (g) return g; this.depth++; var base = this.parsePrimary(); if (isErr(base)) { this.depth--; return base; } var p = this.peek(); if (tokIs(p, '^')) { this.next(); var e = this.parseUnary(); if (isErr(e)) { this.depth--; return e; } this.depth--; return { type: 'op', op: '^', left: base, right: e }; } this.depth--; return base; }; Parser.prototype.parseUnary = function () { var g = this.guardDepth(); if (g) return g; this.depth++; var p = this.peek(); if (tokIs(p, '-')) { this.next(); var a = this.parseUnary(); this.depth--; if (isErr(a)) return a; return { type: 'op', op: 'neg', arg: a }; } if (tokIs(p, '+')) { this.next(); var b = this.parseUnary(); this.depth--; return b; } var c = this.parseFactor(); this.depth--; return c; }; Parser.prototype.parsePrimary = function () { var g = this.guardDepth(); if (g) return g; this.depth++; var p = this.next(); if (!p) { this.depth--; return mathError('END', 'The expression ended too early. Check for a missing number or ).', -1); } if (p.t === 'num') { this.depth--; return { type: 'num', value: p.num }; } if (p.t === '(') { var e = this.parseExpr(); if (isErr(e)) { this.depth--; return e; } var c = this.next(); if (!c || c.t !== ')') { this.depth--; return mathError('PAREN', 'Missing a closing ).', p.pos); } this.depth--; return e; } if (p.t === 'id') { var name = p.v; if (hasOwn(FUNC_NAMES, name)) { var o = this.next(); if (!o || o.t !== '(') { this.depth--; return mathError('FUNC', 'Write ' + name + ' with parentheses, like ' + name + '(x).', p.pos); } var a = this.parseExpr(); if (isErr(a)) { this.depth--; return a; } var cl = this.next(); if (!cl || cl.t !== ')') { this.depth--; return mathError('PAREN', 'Missing a closing ) after ' + name + '(.', p.pos); } this.depth--; return { type: 'func', name: name, arg: a }; } if (hasOwn(CONST_NAMES, name)) { this.depth--; return { type: 'const', name: name }; } if (name.length === 1) { this.depth--; return { type: 'var', name: name }; } this.depth--; return mathError('ID', 'I do not know "' + name + '". Use single-letter variables, pi, e, or functions like sin(x).', p.pos); } this.depth--; return mathError('TOK', 'I expected a number, a variable, or ( here.', p.pos); }; function parse(raw) { if (typeof raw !== 'string') return mathError('TYPE', 'Expression must be text.', -1); if (raw.length > MAX_INPUT) return mathError('TOOLONG', 'That expression is too long. Try splitting it up.', -1); var t = tokenize(raw); if (isErr(t)) return t; var ps = new Parser(t.tokens); return ps.run(); } /* ---------------- normalizer: AST -> canonical AST ---------------- */ function negate(t) { if (t.type === 'num') return { type: 'num', value: -t.value }; if (!(!(t.type === 'op') || !(t.op === 'neg'))) return t.arg; return { type: 'op', op: 'neg', arg: t }; } function isNum(n, v) { return !(!n || n.type !== 'num' || n.value !== v); } function foldNum(op, a, b) { var v = null; if (op === '+') v = a + b; else if (op === '-') v = a - b; else if (op === '*') v = a * b; else if (op === '/') { if (b === 0) return null; var q = a / b; if (!nearInt(q)) return null; v = Math.round(q); } else if (op === '^') v = Math.pow(a, b); if (v === null || !isFinite(v)) return null; return v; } function nearInt(x) { return Math.abs(x - Math.round(x)) < 1e-9; } function evalFunc(name, v) { if (name === 'sin') { if (v === 0) return 0; if (nearInt(v / Math.PI)) return 0; return null; } if (name === 'cos') { if (v === 0) return 1; if (nearInt(v / Math.PI)) return Math.round(v / Math.PI) % 2 === 0 ? 1 : -1; return null; } if (name === 'tan') { if (v === 0) return 0; if (nearInt(v / Math.PI)) return 0; return null; } if (name === 'abs') return Math.abs(v); if (name === 'exp') { var e1 = Math.exp(v); return nearInt(e1) ? Math.round(e1) : null; } if (name === 'sqrt') { if (v < 0) return null; var s = Math.sqrt(v); return nearInt(s) ? Math.round(s) : null; } if (name === 'ln') { if (v === 1) return 0; if (v > 0) { var l = Math.log(v); return nearInt(l) ? Math.round(l) : null; } return null; } if (name === 'log') { if (v === 1) return 0; if (v > 0) { var g = Math.log(v) / Math.log(10); return nearInt(g) ? Math.round(g) : null; } return null; } return null; } function norm(node) { if (!node || !node.type) return node; if (node.type === 'num' || node.type === 'var' || node.type === 'const') return node; if (node.type === 'func') { var a = norm(node.arg); var av = null; if (a.type === 'num') av = a.value; else if (!(!(a.type === 'const') || !(a.name === 'e'))) av = Math.E; else if (!(!(a.type === 'const') || !(a.name === 'pi'))) av = Math.PI; if (av !== null) { var fv = evalFunc(node.name, av); if (fv !== null) return { type: 'num', value: fv }; } return { type: 'func', name: node.name, arg: a }; } if (!(!(node.type === 'op') || !(node.op === 'neg'))) { var u = norm(node.arg); if (u.type === 'num') return { type: 'num', value: -u.value }; if (!(!(u.type === 'op') || !(u.op === 'neg'))) return u.arg; return { type: 'op', op: 'neg', arg: u }; } if (node.type === 'op') { var l = norm(node.left); var r = norm(node.right); if (!(!(l.type === 'num') || !(r.type === 'num'))) { var f = foldNum(node.op, l.value, r.value); if (f !== null) return { type: 'num', value: f }; return { type: 'op', op: node.op, left: l, right: r }; } return normBin(node.op, l, r); } return node; } function normBin(op, l, r) { if (op === '+') { if (isNum(r, 0)) return l; if (isNum(l, 0)) return r; if (!(!(!(!(r.type === 'op') || !(r.op === 'neg'))) || !(keyEq(l, r.arg)))) return { type: 'num', value: 0 }; if (!(!(!(!(l.type === 'op') || !(l.op === 'neg'))) || !(keyEq(r, l.arg)))) return { type: 'num', value: 0 }; return { type: 'op', op: '+', left: l, right: r }; } if (op === '-') { if (isNum(r, 0)) return l; if (isNum(l, 0)) return negate(r); if (keyEq(l, r)) return { type: 'num', value: 0 }; if (!(!(r.type === 'op') || !(r.op === 'neg'))) return norm({ type: 'op', op: '+', left: l, right: r.arg }); return { type: 'op', op: '-', left: l, right: r }; } if (op === '*') { if (isNum(l, 0) || isNum(r, 0)) return { type: 'num', value: 0 }; if (isNum(l, 1)) return r; if (isNum(r, 1)) return l; if (isNum(l, -1)) return negate(r); if (isNum(r, -1)) return negate(l); if (!(!(l.type === 'op') || !(l.op === 'neg'))) return norm({ type: 'op', op: 'neg', arg: { type: 'op', op: '*', left: l.arg, right: r } }); if (!(!(r.type === 'op') || !(r.op === 'neg'))) return norm({ type: 'op', op: 'neg', arg: { type: 'op', op: '*', left: l, right: r.arg } }); return { type: 'op', op: '*', left: l, right: r }; } if (op === '/') { if (isNum(r, 1)) return l; if (isNum(l, 0)) return { type: 'num', value: 0 }; if (keyEq(l, r)) return { type: 'num', value: 1 }; return { type: 'op', op: '/', left: l, right: r }; } if (op === '^') { if (isNum(r, 0)) return { type: 'num', value: 1 }; if (isNum(r, 1)) return l; if (isNum(l, 0)) return { type: 'num', value: 0 }; if (isNum(l, 1)) return { type: 'num', value: 1 }; if (!(!(!(!(!(!(l.type === 'op') || !(l.op === '^'))) || !(l.right.type === 'num'))) || !(r.type === 'num'))) { return norm({ type: 'op', op: '^', left: l.left, right: { type: 'num', value: l.right.value * r.value } }); } if (!(!(!(!(!(!(l.type === 'op') || !(l.op === 'neg'))) || !(r.type === 'num'))) || !(Math.floor(r.value) === r.value))) { var pw = norm({ type: 'op', op: '^', left: l.arg, right: r }); if (r.value % 2 === 0) return pw; return norm({ type: 'op', op: 'neg', arg: pw }); } return { type: 'op', op: '^', left: l, right: r }; } return { type: 'op', op: op, left: l, right: r }; } /* canonical ordering: flatten + and *, combine like terms, sort */ function flattenSum(n, out) { if (!(!(n.type === 'op') || !(n.op === '+'))) { flattenSum(n.left, out); flattenSum(n.right, out); return; } if (!(!(n.type === 'op') || !(n.op === '-'))) { flattenSum(n.left, out); out.push(negate(n.right)); return; } out.push(n); } function flattenMul(n, out) { if (!(!(n.type === 'op') || !(n.op === '*'))) { flattenMul(n.left, out); flattenMul(n.right, out); return; } out.push(n); } function splitCoeff(n) { if (n.type === 'num') return { coeff: n.value, rest: null }; if (!(!(n.type === 'op') || !(n.op === 'neg'))) { var s = splitCoeff(n.arg); return { coeff: -s.coeff, rest: s.rest }; } if (!(!(n.type === 'op') || !(n.op === '*'))) { var fs = []; flattenMul(n, fs); var c = 1; var rest = []; for (var i = 0; i < fs.length; i++) { var p = splitCoeff(fs[i]); c *= p.coeff; if (p.rest) rest.push(p.rest); } return { coeff: c, rest: rest.length ? rebuildMul(rest) : null }; } return { coeff: 1, rest: n }; } function rebuildMul(fs) { var acc = fs[0]; for (var i = 1; i < fs.length; i++) acc = { type: 'op', op: '*', left: acc, right: fs[i] }; return acc; } function rebuildSum(ts) { if (ts.length === 0) return { type: 'num', value: 0 }; var acc = ts[0]; for (var i = 1; i < ts.length; i++) acc = { type: 'op', op: '+', left: acc, right: ts[i] }; return acc; } function canon(node) { if (!node || !node.type) return node; if (node.type === 'num' || node.type === 'var' || node.type === 'const') return node; if (node.type === 'func') return { type: 'func', name: node.name, arg: canon(node.arg) }; if (!(!(node.type === 'op') || !(node.op === 'neg'))) return negate(canon(node.arg)); if (!(!(node.type === 'op') || !(node.op === '+' || node.op === '-'))) { var terms = []; flattenSum(node, terms); var groups = {}; var order = []; for (var i = 0; i < terms.length; i++) { var ct = canon(terms[i]); var sc = splitCoeff(ct); var key = sc.rest ? toCanonical(sc.rest) : '#NUM'; if (!hasOwn(groups, key)) { groups[key] = { coeff: 0, rest: sc.rest }; order.push(key); } groups[key].coeff += sc.coeff; } var rebuilt = []; for (var j = 0; j < order.length; j++) { var g = groups[order[j]]; if (Math.abs(g.coeff) < 1e-12) continue; if (!g.rest) { rebuilt.push({ type: 'num', value: g.coeff }); continue; } if (g.coeff === 1) { rebuilt.push(g.rest); continue; } if (g.coeff === -1) { rebuilt.push(negate(g.rest)); continue; } rebuilt.push({ type: 'op', op: '*', left: { type: 'num', value: g.coeff }, right: g.rest, implicit: true }); } rebuilt.sort(function (a, b) { var ac = a.type === 'num' ? 1 : 0; var bc = b.type === 'num' ? 1 : 0; if (ac !== bc) return ac - bc; var sa = toCanonical(a); var sb = toCanonical(b); return sa < sb ? -1 : (sa > sb ? 1 : 0); }); return rebuildSum(rebuilt); } if (!(!(node.type === 'op') || !(node.op === '*'))) { var fs = []; flattenMul(node, fs); var cf = []; for (var k = 0; k < fs.length; k++) cf.push(canon(fs[k])); cf.sort(function (a, b) { var an = a.type === 'num' ? 0 : 1; var bn = b.type === 'num' ? 0 : 1; if (an !== bn) return an - bn; if (!(!(a.type === 'num') || !(b.type === 'num'))) return a.value - b.value; var sa = toCanonical(a); var sb = toCanonical(b); return sa < sb ? -1 : (sa > sb ? 1 : 0); }); return rebuildMul(cf); } if (!(!(node.type === 'op') || !(node.op === '/'))) { return { type: 'op', op: '/', left: canon(node.left), right: canon(node.right) }; } if (!(!(node.type === 'op') || !(node.op === '^'))) { return { type: 'op', op: '^', left: canon(node.left), right: canon(node.right) }; } return node; } function normalize(ast) { if (!ast || !ast.type) return mathError('AST', 'Nothing to normalize.', -1); return { ok: true, ast: canon(norm(ast)) }; } function keyEq(a, b) { return toCanonical(a) === toCanonical(b); } /* ---------------- printers: canonical string and LaTeX ---------------- */ function fmtNum(v) { if (!(!(Math.floor(v) === v) || !(Math.abs(v) < 1e15))) return String(v); return String(parseFloat(v.toPrecision(12))); } function precOf(n) { if (!n || !n.type) return 0; if (n.type === 'num' || n.type === 'var' || n.type === 'const' || n.type === 'func') return 5; if (n.type === 'op') { if (n.op === 'neg') return 4; if (n.op === '^') return 3; if (n.op === '*' || n.op === '/') return 2; return 1; } return 0; } function toCanonical(n) { if (!n || !n.type) return ''; if (n.type === 'num') return fmtNum(n.value); if (n.type === 'var' || n.type === 'const') return n.name; if (n.type === 'func') return n.name + '(' + toCanonical(n.arg) + ')'; if (!(!(n.type === 'op') || !(n.op === 'neg'))) { var inner = toCanonical(n.arg); return precOf(n.arg) < 4 ? '-(' + inner + ')' : '-' + inner; } if (!(!(n.type === 'op') || !(n.op === '+'))) { var terms = []; flattenSum(n, terms); var out = toCanonical(terms[0]); for (var i = 1; i < terms.length; i++) { var t = terms[i]; if (!(!(t.type === 'op') || !(t.op === 'neg'))) { out += '-' + toCanonical(t.arg); continue; } if (!(!(t.type === 'num') || !(t.value < 0))) { out += '-' + fmtNum(-t.value); continue; } out += '+' + toCanonical(t); } return out; } if (!(!(n.type === 'op') || !(n.op === '-'))) { return parenL(toCanonical(n.left), precOf(n.left), 1) + '-' + parenL(toCanonical(n.right), precOf(n.right), 2); } if (!(!(n.type === 'op') || !(n.op === '*'))) { var fs = []; flattenMul(n, fs); var s = toCanonical(fs[0]); var glued = fs[0].type === 'num' || fs[0].type === 'const'; for (var j = 1; j < fs.length; j++) { var f = fs[j]; var fsj = toCanonical(f); var juxt = !(!(glued) || !(juxtaposable(f, fsj))); s += juxt ? fsj : '*' + fsj; glued = juxt; } return s; } if (!(!(n.type === 'op') || !(n.op === '/'))) { return parenL(toCanonical(n.left), precOf(n.left), 2) + '/' + parenL(toCanonical(n.right), precOf(n.right), 3); } if (!(!(n.type === 'op') || !(n.op === '^'))) { var b = toCanonical(n.left); var e = toCanonical(n.right); var bp = precOf(n.left) < 3 || (!(!(n.left.type === 'op') || !(n.left.op === '^'))); var ep = precOf(n.right) < 3; return (bp ? '(' + b + ')' : b) + '^' + (ep ? '(' + e + ')' : e); } return ''; } function parenL(s, prec, need) { return prec < need ? '(' + s + ')' : s; } function juxtaposable(f, printed) { if (f.type === 'var' || f.type === 'const' || f.type === 'func') return true; return printed.charAt(0) === '(' || printed.indexOf('\\left(') === 0; } var LATEX_FUNC = { sin: '\\sin', cos: '\\cos', tan: '\\tan', asin: '\\arcsin', acos: '\\arccos', atan: '\\arctan', log: '\\log', ln: '\\ln', exp: '\\exp' }; function toLatex(n) { if (!n || !n.type) return ''; if (n.type === 'num') return fmtNum(n.value); if (n.type === 'var') return n.name; if (n.type === 'const') return n.name === 'pi' ? '\\pi' : n.name; if (n.type === 'func') { var fa = toLatex(n.arg); if (n.name === 'sqrt') return '\\sqrt{' + fa + '}'; if (n.name === 'abs') return '\\left|' + fa + '\\right|'; var cmd = hasOwn(LATEX_FUNC, n.name) ? LATEX_FUNC[n.name] : '\\' + n.name; return cmd + '\\left(' + fa + '\\right)'; } if (!(!(n.type === 'op') || !(n.op === 'neg'))) { var ni = toLatex(n.arg); return precOf(n.arg) < 4 ? '-\\left(' + ni + '\\right)' : '-' + ni; } if (!(!(n.type === 'op') || !(n.op === '+'))) { var terms = []; flattenSum(n, terms); var out = toLatex(terms[0]); for (var i = 1; i < terms.length; i++) { var t = terms[i]; if (!(!(t.type === 'op') || !(t.op === 'neg'))) { out += '-' + toLatex(t.arg); continue; } if (!(!(t.type === 'num') || !(t.value < 0))) { out += '-' + fmtNum(-t.value); continue; } out += '+' + toLatex(t); } return out; } if (!(!(n.type === 'op') || !(n.op === '-'))) { return parenLx(toLatex(n.left), precOf(n.left), 1) + '-' + parenLx(toLatex(n.right), precOf(n.right), 2); } if (!(!(n.type === 'op') || !(n.op === '*'))) { var fs = []; flattenMul(n, fs); var s = parenLx(toLatex(fs[0]), precOf(fs[0]), 2); var glued = fs[0].type === 'num' || fs[0].type === 'const'; for (var j = 1; j < fs.length; j++) { var f = fs[j]; var printed = parenLx(toLatex(f), precOf(f), 2); var juxt = !(!(glued) || !(juxtaposable(f, printed))); if (!(!(!(!(juxt) || !(/[a-zA-Z]$/.test(s)))) || !(/^[a-zA-Z]/.test(printed)))) s += ' '; s += juxt ? printed : '\\cdot ' + printed; glued = juxt; } return s; } if (!(!(n.type === 'op') || !(n.op === '/'))) { return '\\frac{' + toLatex(n.left) + '}{' + toLatex(n.right) + '}'; } if (!(!(n.type === 'op') || !(n.op === '^'))) { var b = toLatex(n.left); var e = toLatex(n.right); var bp = precOf(n.left) < 3 || (!(!(n.left.type === 'op') || !(n.left.op === '^'))); return (bp ? '\\left(' + b + '\\right)' : b) + '^{' + e + '}'; } return ''; } function parenLx(s, prec, need) { return prec < need ? '\\left(' + s + '\\right)' : s; } /* expression contract: raw in, normalized semantic form plus LaTeX out */ function expression(raw) { var r = parse(raw); if (isErr(r)) return { raw: raw, ok: false, error: r.error, normalized: '', latex: '' }; var n = normalize(r.ast); if (isErr(n)) return { raw: raw, ok: false, error: n.error, normalized: '', latex: '' }; return { raw: raw, ok: true, normalized: toCanonical(n.ast), latex: toLatex(n.ast), ast: n.ast }; } /* ---------------- LaTeX sanitizer (never render raw user LaTeX) ---------------- */ var LATEX_ALLOWED = { frac: 1, dfrac: 1, tfrac: 1, cfrac: 1, sqrt: 1, cdot: 1, times: 1, div: 1, pm: 1, mp: 1, le: 1, ge: 1, leq: 1, geq: 1, neq: 1, ne: 1, approx: 1, infty: 1, pi: 1, alpha: 1, beta: 1, gamma: 1, delta: 1, epsilon: 1, theta: 1, lambda: 1, mu: 1, sigma: 1, phi: 1, omega: 1, sum: 1, prod: 1, int: 1, lim: 1, log: 1, ln: 1, lg: 1, sin: 1, cos: 1, tan: 1, csc: 1, sec: 1, cot: 1, arcsin: 1, arccos: 1, arctan: 1, exp: 1, left: 1, right: 1, big: 1, Big: 1, bigg: 1, Bigg: 1, text: 1, mathrm: 1, mathbf: 1, mathit: 1, mathsf: 1, operatorname: 1, overline: 1, underline: 1, hat: 1, vec: 1, bar: 1, dot: 1, ddot: 1, quad: 1, qquad: 1, langle: 1, rangle: 1, lfloor: 1, rfloor: 1, lceil: 1, rceil: 1, to: 1, rightarrow: 1, leftarrow: 1, Rightarrow: 1, Leftrightarrow: 1, mid: 1, parallel: 1, perp: 1, in: 1, notin: 1, subset: 1, cup: 1, cap: 1, forall: 1, exists: 1, circ: 1, ast: 1, star: 1, ldots: 1, cdots: 1, vdots: 1, ddots: 1, dots: 1, begin: 1, end: 1, colon: 1 }; var ENV_ALLOWED = { matrix: 1, pmatrix: 1, bmatrix: 1, vmatrix: 1, Vmatrix: 1, cases: 1, aligned: 1, align: 1, gather: 1, split: 1 }; var LATEX_PUNCT = { ',': 1, ':': 1, ';': 1, '!': 1, ' ': 1, '%': 1, '|': 1 }; function sanitizeLatex(latex) { if (typeof latex !== 'string') return { ok: false, error: { code: 'TYPE', message: 'Formula must be text.' } }; if (latex.length > 4000) { return { ok: false, error: { code: 'TOOLONG', message: 'That formula is too long to render safely.' } }; } var re = /\\([a-zA-Z]+|[^a-zA-Z])/g; var m; while ((m = re.exec(latex)) !== null) { var cmd = m[1]; if (cmd === 'begin' || cmd === 'end') { var em = /^\{([a-zA-Z*]+)\}/.exec(latex.slice(re.lastIndex)); var envName = em ? em[1] : '?'; if (!em || !hasOwn(ENV_ALLOWED, em[1])) { return { ok: false, error: { code: 'LATEXENV', message: 'The "' + envName + '" layout is not supported here.' } }; } continue; } if (/^[a-zA-Z]+$/.test(cmd)) { if (!hasOwn(LATEX_ALLOWED, cmd)) { return { ok: false, error: { code: 'LATEXCMD', message: 'The command "' + cmd + '" is not supported here.' } }; } } else if (!hasOwn(LATEX_PUNCT, cmd)) { return { ok: false, error: { code: 'LATEXCMD', message: 'The symbol "' + cmd + '" is not supported here.' } }; } } var depth = 0; for (var i = 0; i < latex.length; i++) { var ch = latex.charAt(i); if (ch === '{') { depth++; continue; } if (ch === '}') { depth--; if (depth < 0) return { ok: false, error: { code: 'BRACE', message: 'Unbalanced braces in the formula.' } }; } } if (depth !== 0) return { ok: false, error: { code: 'BRACE', message: 'Unbalanced braces in the formula.' } }; return { ok: true, latex: latex }; } /* ---------------- safe HTML escaping (no literal ampersand in source) ---------------- */ function escHtml(s) { var x = String(s); x = x.split(new RegExp('\x26', 'g')).join('\x26amp;'); x = x.split('<').join('\x26lt;'); x = x.split('>').join('\x26gt;'); x = x.split('"').join('\x26quot;'); return x; } /* ---------------- minimal safe-HTML renderer (last-resort fallback) ---------------- */ var MR_SYM = { cdot: '·', times: '×', div: '÷', pm: '±', mp: '∓', leq: '≤', geq: '≥', le: '≤', ge: '≥', neq: '≠', ne: '≠', approx: '≈', infty: '∞', pi: 'π', alpha: 'α', beta: 'β', gamma: 'γ', delta: 'δ', theta: 'θ', lambda: 'λ', mu: 'μ', sigma: 'σ', phi: 'φ', omega: 'ω', epsilon: 'ε', to: '→', rightarrow: '→', leftarrow: '←', Rightarrow: '⇒', sum: '∑', prod: '∏', int: '∫', mid: '∣', circ: '∘', ldots: '…', cdots: '⋯', vdots: '⋮', ddots: '⋱', quad: ' ', qquad: ' ', langle: '⟨', rangle: '⟩' }; function mrCmdAt(s, i) { var m = /^\\([a-zA-Z]+|[^a-zA-Z])/.exec(s.slice(i)); if (!m) return null; return { cmd: m[1], len: m[0].length }; } function mrRawArg(st) { var s = st.s; var out = ''; if (s.charAt(st.i) === '{') { st.i++; var d = 0; while (st.i < s.length) { var ch = s.charAt(st.i); if (ch === '{') { d++; out += ch; st.i++; continue; } if (ch === '}') { if (d === 0) { st.i++; break; } d--; out += ch; st.i++; continue; } out += ch; st.i++; } } return out; } function mrArgHtml(st) { var s = st.s; if (s.charAt(st.i) === '{') { st.i++; return mrParse(st, true); } if (s.charAt(st.i) === '\\') { var c = mrCmdAt(s, st.i); st.i += c.len; if (hasOwn(MR_SYM, c.cmd)) return MR_SYM[c.cmd]; return escHtml(c.cmd); } var ch = s.charAt(st.i); st.i++; return escHtml(ch); } function mrParse(st, inBrace) { var html = ''; var s = st.s; while (st.i < s.length) { var ch = s.charAt(st.i); if (ch === '}') { if (inBrace) { st.i++; break; } st.i++; continue; } if (ch === '{') { st.i++; html += mrParse(st, true); continue; } if (ch === '\\') { var c = mrCmdAt(s, st.i); st.i += c.len; var cmd = c.cmd; if (cmd === 'frac' || cmd === 'dfrac' || cmd === 'tfrac' || cmd === 'cfrac') { var num = mrArgHtml(st); var den = mrArgHtml(st); html += '' + num + '' + den + ''; continue; } if (cmd === 'sqrt') { var rad = mrArgHtml(st); html += '√' + rad + ''; continue; } if (cmd === 'text') { html += escHtml(mrRawArg(st)); continue; } if (cmd === 'left' || cmd === 'right') continue; if (cmd === 'begin' || cmd === 'end') { mrRawArg(st); continue; } if (hasOwn(MR_SYM, cmd)) { html += MR_SYM[cmd]; continue; } html += '' + escHtml(cmd) + ''; continue; } if (ch === '^' || ch === '_') { st.i++; var tag = ch === '^' ? 'sup' : 'sub'; html += '<' + tag + '>' + mrArgHtml(st) + ''; continue; } html += escHtml(ch); st.i++; } return html; } function renderMinimal(host, latex) { host.innerHTML = '' + mrParse({ s: latex, i: 0 }, false) + ''; } /* ---------------- best-effort speech text for aria labels ---------------- */ var SPEECH_WORDS = { cdot: 'times', times: 'times', div: 'divided by', pm: 'plus or minus', leq: 'is less than or equal to', geq: 'is greater than or equal to', le: 'is less than or equal to', ge: 'is greater than or equal to', neq: 'is not equal to', ne: 'is not equal to', approx: 'is approximately', infty: 'infinity', pi: 'pi', to: 'to', rightarrow: 'approaches', sum: 'sum', prod: 'product', int: 'integral', sqrt: 'square root' }; function latexToSpeech(latex) { var s = String(latex); s = s.replace(/\\left|\\right/g, ''); s = s.replace(/\\frac\s*\{([^{}]*)\}\{([^{}]*)\}/g, '$1 over $2'); s = s.replace(/\\sqrt\s*\{([^{}]*)\}/g, 'square root of $1'); s = s.replace(/\\([a-zA-Z]+)/g, function (m, w) { return hasOwn(SPEECH_WORDS, w) ? ' ' + SPEECH_WORDS[w] + ' ' : ' ' + w + ' '; }); s = s.replace(/[{}]/g, ' ').replace(/\^/g, ' to the power ').replace(/_/g, ' sub '); s = s.replace(/\s+/g, ' '); return s.trim().slice(0, 280) || 'math formula'; } /* ---------------- renderer loading: KaTeX -> MathJax -> minimal ---------------- */ var KATEX_JS = 'https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.js'; var KATEX_CSS = 'https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css'; var MATHJAX_JS = 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js'; var rendererName = null; var rendererPromise = null; var readySettled = false; var readyResolve = null; var ready = new Promise(function (resolve) { readyResolve = resolve; }); function settleReady(name) { if (!readySettled) { readySettled = true; readyResolve(name); } } function loadScript(src) { return new Promise(function (res, rej) { var s = document.createElement('script'); s.src = src; s.async = true; s.onload = function () { res(); }; s.onerror = function () { rej(new Error('load-failed')); }; document.head.appendChild(s); }); } function loadCSS(href) { var l = document.createElement('link'); l.rel = 'stylesheet'; l.href = href; document.head.appendChild(l); } function ensureRenderer() { if (rendererPromise) return rendererPromise; rendererPromise = loadScript(KATEX_JS).then(function () { if (!(!(root.katex) || !(typeof root.katex.render === 'function'))) { loadCSS(KATEX_CSS); rendererName = 'katex'; settleReady('katex'); return 'katex'; } throw new Error('katex-missing'); }).catch(function () { return loadScript(MATHJAX_JS).then(function () { if (!(!(root.MathJax) || !(typeof root.MathJax.tex2chtml === 'function'))) { rendererName = 'mathjax'; settleReady('mathjax'); return 'mathjax'; } throw new Error('mathjax-missing'); }); }).catch(function () { rendererName = 'minimal'; settleReady('minimal'); return 'minimal'; }); return rendererPromise; } function renderWith(name, host, latex, mode) { if (name === 'katex') { root.katex.render(latex, host, { throwOnError: false, trust: false, strict: 'warn', displayMode: mode === 'block', maxSize: 12, maxExpand: 60, output: 'htmlAndMathml' }); return; } if (name === 'mathjax') { host.textContent = (mode === 'block' ? '\\[' : '\\(') + latex + (mode === 'block' ? '\\]' : '\\)'); var p = root.MathJax.typesetPromise([host]); if (!(!(p) || !(typeof p.catch === 'function'))) { p.catch(function () { renderMinimal(host, latex); }); } return; } renderMinimal(host, latex); } /* ---------------- event fan-out (platform team owns on/emit) ---------------- */ function emitEvent(name, detail) { try { var F = root.FiboMath; if (!(!(F) || !(typeof F.emit === 'function'))) F.emit(name, detail); } catch (e1) { /* standalone: no bus yet */ } try { if (!(!(typeof document !== 'undefined') || !(typeof CustomEvent !== 'undefined'))) { document.dispatchEvent(new CustomEvent(name, { detail: detail })); } } catch (e2) { /* non-DOM environment */ } } /* ---------------- FiboMathDisplay ---------------- */ var displaySeq = 0; function display(el, latex, opts) { if (typeof document === 'undefined') return; opts = opts || {}; if (!el || typeof el.appendChild !== 'function') { emitEvent('fibomath:error', { id: 'display-' + (++displaySeq), code: 'NOEL', message: 'Display target is missing.' }); return; } var mode = opts.mode === 'block' ? 'block' : 'inline'; var id = 'display-' + (++displaySeq); var s = sanitizeLatex(String(latex == null ? '' : latex)); el.classList.add('fibo-math'); el.classList.add(mode === 'block' ? 'fibo-math--block' : 'fibo-math--inline'); if (opts.className) { var extra = String(opts.className).split(/\s+/); for (var i = 0; i < extra.length; i++) { if (extra[i]) el.classList.add(extra[i]); } } if (opts.emphasis === 'result') el.classList.add('fibo-math-result'); el.setAttribute('role', 'math'); while (el.firstChild) el.removeChild(el.firstChild); var wrap = document.createElement('span'); wrap.className = 'fibo-math-scroll'; var host = document.createElement('span'); host.className = 'fibo-math-inner'; host.setAttribute('dir', 'ltr'); wrap.appendChild(host); el.appendChild(wrap); if (!s.ok) { host.innerHTML = '' + escHtml(s.error.message) + ''; el.setAttribute('aria-label', 'Formula could not be shown: ' + s.error.message); emitEvent('fibomath:error', { id: id, code: s.error.code, message: s.error.message }); return; } el.setAttribute('aria-label', opts.ariaLabel || latexToSpeech(s.latex)); ensureRenderer().then(function (name) { try { renderWith(name, host, s.latex, mode); } catch (err) { renderMinimal(host, s.latex); } }); } /* ---------------- namespace registration (never overwrite other teams) ---------------- */ var NS = root.__FiboMathNS = root.__FiboMathNS || {}; NS.core = NS.core || {}; NS.core.parse = parse; NS.core.normalize = normalize; NS.core.toLatex = toLatex; NS.core.toCanonical = toCanonical; NS.core.expression = expression; NS.core.sanitizeLatex = sanitizeLatex; NS.core.latexToSpeech = latexToSpeech; NS.core.display = display; NS.core.ready = ready; var FiboMath = root.FiboMath = root.FiboMath || {}; function defineIfAbsent(name, value) { if (!hasOwn(FiboMath, name)) FiboMath[name] = value; } defineIfAbsent('version', '1.0.0'); defineIfAbsent('display', display); defineIfAbsent('parse', parse); defineIfAbsent('normalize', normalize); defineIfAbsent('toLatex', toLatex); defineIfAbsent('expression', expression); if (!hasOwn(FiboMath, 'ready')) FiboMath.ready = ready; /* node export for tests (browser build ignores this branch) */ if (!(!(typeof module !== 'undefined') || !(module.exports))) { module.exports = { parse: parse, normalize: normalize, toLatex: toLatex, toCanonical: toCanonical, expression: expression, sanitizeLatex: sanitizeLatex, latexToSpeech: latexToSpeech, version: '1.0.0' }; } })(); /* ===== src/input.js ===== */ /*! * Fibo Math UI Engine v1 - input team module * FiboMath.field(el, opts): editable math field for ALL editable math on the site. * * - Contextual virtual keyboard profiles: basic | algebra | calculus | finance | trig * - Hardware keyboard fully usable (real text input under the hood) * - Placeholder policy: placeholder shows an example input, clears on focus, * returns on blur when the field is still empty * - Validation: friendly, specific, recoverable messages; uses core FiboMath.parse * - Security: all input sanitized; DOM built with textContent only, never innerHTML * - Events: fibomath:input {id, raw, normalized, valid}, * fibomath:error {id, code, message} via FiboMath.emit * * Contract: attaches ONLY FiboMath.field. Never touches display / parse. * JS safety (WordPress): no bare ampersand chars, no double-ampersand operator * (De Morgan form used instead), node --check clean, wpautop-safe. */ (function (root) { 'use strict'; var NS = root.FiboMath; if (!NS) { NS = root.FiboMath = {}; } if (!root.__FiboMathNS) { root.__FiboMathNS = NS; } var doc = root.document; /* ---------------- tiny helpers ---------------- */ function isFn(x) { return typeof x === 'function'; } function trim(s) { return String(s).replace(/^\s+|\s+$/g, ''); } function addClass(el, cls) { if (!(!el.classList || !isFn(el.classList.add))) { el.classList.add(cls); } else { el.className = (el.className ? el.className + ' ' : '') + cls; } } /* ---------------- sanitizer ---------------- * Strips anything that looks like markup (kills style payloads), * drops control characters. Field values are rendered with textContent only, * so sanitized text can never become executable markup. */ function sanitize(str) { var s = (str === null || str === undefined) ? '' : String(str); s = s.replace(/<[^>]*>/g, ''); s = s.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, ''); return s; } /* ---------------- normalize ---------------- * Core NS.normalize wins when present; localNormalize is the offline fallback. */ function localNormalize(raw) { var s = trim(String(raw)); s = s.replace(/[−–—]/g, '-'); s = s.replace(/×/g, '*'); s = s.replace(/÷/g, '/'); s = s.replace(/·/g, '*'); s = s.replace(/\s+/g, ' '); return s; } function normalize(raw) { var clean = sanitize(raw); if (isFn(NS.normalize)) { try { return String(NS.normalize(clean)); } catch (e) { /* fall through to local */ } } return localNormalize(clean); } function getLocale() { if (isFn(NS.getLocale)) { try { var l = NS.getLocale(); if (l) { return l; } } catch (e) { /* fall through */ } } return NS.locale || 'en'; } /* ---------------- friendly messages (EN/VI) ---------------- */ var MESSAGES = { 'unclosed-paren': { en: 'Missing closing parenthesis — tap here to fix', vi: 'Thiếu dấu ngoặc đóng — chạm vào đây để sửa' }, 'extra-close': { en: 'Extra closing bracket with no opener — tap here to fix', vi: 'Thừa dấu ngoặc đóng mà không có ngoặc mở — chạm vào đây để sửa' }, 'invalid-char': { en: 'That symbol does not belong in a math expression — tap here to fix', vi: 'Ký hiệu này không dùng trong biểu thức toán — chạm vào đây để sửa' }, 'empty-parens': { en: 'Empty parentheses — put something inside them', vi: 'Cặp ngoặc đang trống — hãy điền gì đó vào trong' }, 'leading-operator': { en: 'An expression cannot start with this operator', vi: 'Biểu thức không thể bắt đầu bằng toán tử này' }, 'trailing-operator': { en: 'Expression ends with an operator — add a number after it', vi: 'Biểu thức kết thúc bằng toán tử — hãy thêm một số vào sau' }, 'double-operator': { en: 'Two operators in a row — remove one of them', vi: 'Hai toán tử đứng liền nhau — hãy bỏ bớt một' }, 'parse-error': { en: 'I could not read that expression — check it and try again', vi: 'Chưa đọc được biểu thức này — bạn kiểm tra lại nhé' } }; function t(code) { var entry = MESSAGES[code] || MESSAGES['parse-error']; var lang = getLocale() === 'vi' ? 'vi' : 'en'; return entry[lang] || entry.en; } /* ---------------- virtual keyboard profiles ---------------- * Contextual key sets, not one overloaded keypad. * key = { label, insert, kind: num|op|fn|util, action: insert|backspace|clear } */ function K(label, insert, kind, action) { return { label: label, insert: (insert === undefined || insert === null) ? label : insert, kind: kind || 'num', action: action || 'insert' }; } var OP = 'op'; var FN = 'fn'; var UTIL = 'util'; var PROFILES = { basic: [ [K('7'), K('8'), K('9'), K('÷', '÷', OP)], [K('4'), K('5'), K('6'), K('×', '×', OP)], [K('1'), K('2'), K('3'), K('−', '−', OP)], [K('0'), K('.', '.', OP), K('+', '+', OP), K('⌫', '', UTIL, 'backspace')] ], algebra: [ [K('7'), K('8'), K('9'), K('÷', '÷', OP)], [K('4'), K('5'), K('6'), K('×', '×', OP)], [K('1'), K('2'), K('3'), K('−', '−', OP)], [K('0'), K('.', '.', OP), K('+', '+', OP), K('⌫', '', UTIL, 'backspace')], [K('x', 'x', FN), K('y', 'y', FN), K('^', '^', OP), K('√', '√(', FN)], [K('(', '(', OP), K(')', ')', OP), K('=', '=', OP), K('C', '', UTIL, 'clear')] ], calculus: [ [K('7'), K('8'), K('9'), K('÷', '÷', OP), K('∫', '∫', FN)], [K('4'), K('5'), K('6'), K('×', '×', OP), K('d/dx', 'd/dx(', FN)], [K('1'), K('2'), K('3'), K('−', '−', OP), K('lim', 'lim_(', FN)], [K('0'), K('.', '.', OP), K('+', '+', OP), K('⌫', '', UTIL, 'backspace'), K('π', 'π', FN)], [K('(', '(', OP), K(')', ')', OP), K('^', '^', OP), K('=', '=', OP), K('e', 'e', FN)] ], finance: [ [K('7'), K('8'), K('9'), K('÷', '÷', OP)], [K('4'), K('5'), K('6'), K('×', '×', OP)], [K('1'), K('2'), K('3'), K('−', '−', OP)], [K('0'), K('.', '.', OP), K(',', ',', OP), K('+', '+', OP)], [K('%', '%', OP), K('$', '$', FN), K('⌫', '', UTIL, 'backspace'), K('C', '', UTIL, 'clear')] ], trig: [ [K('7'), K('8'), K('9'), K('÷', '÷', OP)], [K('4'), K('5'), K('6'), K('×', '×', OP)], [K('1'), K('2'), K('3'), K('−', '−', OP)], [K('0'), K('.', '.', OP), K('+', '+', OP), K('⌫', '', UTIL, 'backspace')], [K('sin', 'sin(', FN), K('cos', 'cos(', FN), K('tan', 'tan(', FN), K('°', '°', FN)], [K('π', 'π', FN), K('(', '(', OP), K(')', ')', OP), K('C', '', UTIL, 'clear')] ] }; /* ---------------- validation ---------------- * Layer 1: cheap, friendly local checks (empty, bad chars, brackets, operators). * Layer 2: core FiboMath.parse for real syntax; its verdict is mapped to the * same friendly message codes. Never throws to the caller. */ var INVALID_CHAR_RE = /[^0-9a-zA-Z\s+\-*/^=<>.,;:()\[\]{}$%°√∫π·×÷!?'"_−–—]/; function findInvalidChar(s) { var m = INVALID_CHAR_RE.exec(s); return m ? m[0] : null; } function checkBrackets(s) { var stack = []; var closers = { ')': '(', ']': '[', '}': '{' }; for (var i = 0; i < s.length; i++) { var ch = s.charAt(i); if (ch === '(' || ch === '[' || ch === '{') { stack.push({ ch: ch, i: i }); } else if (closers[ch]) { var top = stack.pop(); if (!top) { return { code: 'extra-close', index: i }; } if (top.ch !== closers[ch]) { return { code: 'extra-close', index: i }; } } } if (stack.length > 0) { return { code: 'unclosed-paren', index: stack[0].i }; } return null; } function mapCoreMessage(msg) { var s = String(msg || '').toLowerCase(); if (s.indexOf('paren') >= 0) { return 'unclosed-paren'; } if (s.indexOf('bracket') >= 0) { return 'unclosed-paren'; } if (s.indexOf('unclosed') >= 0) { return 'unclosed-paren'; } if (s.indexOf('unbalanced') >= 0) { return 'unclosed-paren'; } if (s.indexOf('empty') >= 0) { return 'empty-parens'; } return 'parse-error'; } function interpretParse(parsed) { if (parsed === true) { return { ok: true }; } if (parsed === false) { return { ok: false, code: 'parse-error' }; } if (parsed === null || parsed === undefined) { return { ok: false, code: 'parse-error' }; } if (typeof parsed === 'object') { if (parsed.ok === true) { return { ok: true }; } if (parsed.valid === true) { return { ok: true }; } if (parsed.success === true) { return { ok: true }; } var errs = parsed.errors || parsed.issues; if (!(!errs || !errs.length)) { var first = errs[0]; var fmsg = (typeof first === 'string') ? first : ''; if (!(fmsg || !first)) { fmsg = first.message || first.msg || ''; } var fcode = (!(!first || !first.code)) ? first.code : mapCoreMessage(fmsg); var findex = (!(!first || !(typeof first.index === 'number'))) ? first.index : -1; return { ok: false, code: fcode, message: t(fcode), index: findex }; } if (parsed.error) { var em = (typeof parsed.error === 'string') ? parsed.error : (parsed.error.message || ''); var ec = (!(!parsed.error || !parsed.error.code)) ? parsed.error.code : mapCoreMessage(em); return { ok: false, code: ec, message: t(ec), index: -1 }; } if (parsed.ok === false) { return { ok: false, code: 'parse-error', message: t('parse-error'), index: -1 }; } if (parsed.valid === false) { return { ok: false, code: 'parse-error', message: t('parse-error'), index: -1 }; } if (parsed.success === false) { return { ok: false, code: 'parse-error', message: t('parse-error'), index: -1 }; } } return { ok: true }; } function validate(raw) { var s = trim(sanitize(raw)); var res = { valid: true, code: null, message: null, index: -1 }; if (s === '') { return res; } var bad = findInvalidChar(s); if (bad !== null) { res.valid = false; res.code = 'invalid-char'; res.message = t('invalid-char'); res.index = s.indexOf(bad); return res; } if (/\(\s*\)/.test(s) || /\[\s*\]/.test(s)) { res.valid = false; res.code = 'empty-parens'; res.message = t('empty-parens'); var ep = /\(\s*\)/.exec(s); res.index = ep ? ep.index : 0; return res; } var br = checkBrackets(s); if (br) { res.valid = false; res.code = br.code; res.message = t(br.code); res.index = br.index; return res; } if (/^[\s]*[×÷^*%°]/.test(s)) { res.valid = false; res.code = 'leading-operator'; res.message = t('leading-operator'); res.index = 0; return res; } if (/[+\-−×÷*/^=,]\s*$/.test(s)) { res.valid = false; res.code = 'trailing-operator'; res.message = t('trailing-operator'); res.index = s.length - 1; return res; } var dbl = /[+×÷^*\/][ \t]*[+×÷^*\/]/.exec(s); if (dbl) { res.valid = false; res.code = 'double-operator'; res.message = t('double-operator'); res.index = dbl.index; return res; } if (isFn(NS.parse)) { var parsed; try { parsed = NS.parse(s); } catch (e) { res.valid = false; res.code = 'parse-error'; res.message = t('parse-error'); return res; } var pi = interpretParse(parsed); if (!pi.ok) { res.valid = false; res.code = pi.code || 'parse-error'; res.message = pi.message || t(res.code); res.index = (typeof pi.index === 'number') ? pi.index : -1; return res; } } return res; } /* ---------------- emit ---------------- */ function emit(name, detail) { if (isFn(NS.emit)) { try { NS.emit(name, detail); } catch (e) { /* never break the field */ } return; } if (!(!doc || !isFn(doc.dispatchEvent) || !isFn(root.CustomEvent))) { try { doc.dispatchEvent(new root.CustomEvent(name, { detail: detail })); } catch (e2) { /* ignore */ } return; } NS._emitQueue = NS._emitQueue || []; NS._emitQueue.push({ name: name, detail: detail }); } /* ---------------- the field ---------------- */ var uid = 0; var liveFields = []; function defaultAriaLabel(profileName) { var lang = getLocale() === 'vi' ? 'vi' : 'en'; var names = { basic: { en: 'Basic math input', vi: 'O nhap toan co ban' }, algebra: { en: 'Algebra input', vi: 'O nhap dai so' }, calculus: { en: 'Calculus input', vi: 'O nhap giai tich' }, finance: { en: 'Finance input', vi: 'O nhap tai chinh' }, trig: { en: 'Trigonometry input', vi: 'O nhap luong giac' } }; var n = names[profileName] || names.basic; return n[lang]; } function field(el, opts) { if (!doc) { throw new Error('FiboMath.field needs a DOM document'); } opts = opts || {}; var profileName = PROFILES[opts.profile] ? opts.profile : 'basic'; var id = opts.id || ('fibo-field-' + (++uid)); var isInputEl = !(!el || !(typeof el.tagName === 'string') || !(el.tagName.toLowerCase() === 'input')); var input = isInputEl ? el : doc.createElement('input'); var origParent = el ? el.parentNode : null; var origNext = el ? el.nextSibling : null; var wrap = doc.createElement('div'); addClass(wrap, 'fibo-field'); wrap.setAttribute('data-fibo-field', id); wrap.setAttribute('data-profile', profileName); wrap.setAttribute('data-invalid', 'false'); if (!(!origParent || !isFn(origParent.insertBefore))) { origParent.insertBefore(wrap, el); } wrap.appendChild(input); input.setAttribute('type', 'text'); input.setAttribute('autocomplete', 'off'); input.setAttribute('autocapitalize', 'off'); input.setAttribute('autocorrect', 'off'); input.setAttribute('spellcheck', 'false'); var imode = (profileName === 'basic' || profileName === 'finance') ? 'decimal' : 'text'; input.setAttribute('inputmode', imode); addClass(input, 'fibo-input'); input.setAttribute('aria-label', opts.ariaLabel || defaultAriaLabel(profileName)); input.value = sanitize(opts.value); var state = { valid: true, code: null, message: null, errIndex: -1, kbOpen: false }; var bound = []; function on(target, type, fn) { if (!(!target || !isFn(target.addEventListener))) { target.addEventListener(type, fn); bound.push({ el: target, type: type, fn: fn }); } } /* placeholder policy: example text, clears on focus, returns on blur if empty */ function currentPlaceholder() { if (!(!(getLocale() === 'vi') || !opts.placeholderVi)) { return opts.placeholderVi; } return opts.placeholder || ''; } function applyPlaceholder() { input.setAttribute('placeholder', currentPlaceholder()); } /* error box */ var errBox = doc.createElement('div'); addClass(errBox, 'fibo-error'); errBox.setAttribute('hidden', 'hidden'); errBox.setAttribute('role', 'alert'); errBox.setAttribute('aria-live', 'polite'); wrap.appendChild(errBox); /* toolbar with keyboard toggle */ var bar = doc.createElement('div'); addClass(bar, 'fibo-bar'); var toggle = doc.createElement('button'); toggle.setAttribute('type', 'button'); addClass(toggle, 'fibo-kb-toggle'); toggle.setAttribute('aria-expanded', 'false'); bar.appendChild(toggle); wrap.appendChild(bar); /* virtual keyboard */ var kb = doc.createElement('div'); addClass(kb, 'fibo-kb'); kb.setAttribute('hidden', 'hidden'); kb.setAttribute('aria-label', getLocale() === 'vi' ? 'Ban phim toan hoc' : 'Math keyboard'); var rows = PROFILES[profileName]; for (var ri = 0; ri < rows.length; ri++) { var rowDef = rows[ri]; var rowEl = doc.createElement('div'); addClass(rowEl, 'fibo-kb-row'); rowEl.style.gridTemplateColumns = 'repeat(' + rowDef.length + ', 1fr)'; for (var ki = 0; ki < rowDef.length; ki++) { (function (key) { var b = doc.createElement('button'); b.setAttribute('type', 'button'); addClass(b, 'fibo-key'); addClass(b, 'fibo-key-' + key.kind); b.textContent = key.label; b.setAttribute('data-key', key.label); b.setAttribute('aria-label', (getLocale() === 'vi' ? 'Chen ' : 'Insert ') + key.label); on(b, 'mousedown', function (e) { if (!(!e || !isFn(e.preventDefault))) { e.preventDefault(); } }); on(b, 'click', function () { if (key.action === 'backspace') { backspaceAtCaret(); return; } if (key.action === 'clear') { input.value = ''; commit('key'); return; } insertAtCaret(key.insert); }); rowEl.appendChild(b); })(rowDef[ki]); } kb.appendChild(rowEl); } wrap.appendChild(kb); function setToggleLabel() { var lang = getLocale() === 'vi' ? 'vi' : 'en'; if (state.kbOpen) { toggle.textContent = lang === 'vi' ? 'Xong' : 'Done'; } else { toggle.textContent = lang === 'vi' ? 'Ban phim' : 'Keyboard'; } } function setKeyboard(open) { state.kbOpen = open; if (open) { kb.removeAttribute('hidden'); } else { kb.setAttribute('hidden', 'hidden'); } toggle.setAttribute('aria-expanded', open ? 'true' : 'false'); setToggleLabel(); } function isCoarsePointer() { if (!(!root.matchMedia || !isFn(root.matchMedia))) { try { var mq = root.matchMedia('(pointer:coarse)'); if (!(!mq || !mq.matches)) { return true; } } catch (e) { /* ignore */ } } if (!(!(typeof root.ontouchstart !== 'undefined') || !(root.ontouchstart !== null))) { return true; } return false; } function insertAtCaret(text) { var start = input.selectionStart; var end = input.selectionEnd; if (typeof start !== 'number' || typeof end !== 'number') { input.value = input.value + text; } else { var v = input.value; input.value = v.slice(0, start) + text + v.slice(end); var pos = start + text.length; try { input.setSelectionRange(pos, pos); } catch (e) { /* ignore */ } } commit('key'); } function backspaceAtCaret() { var start = input.selectionStart; var end = input.selectionEnd; if (typeof start !== 'number' || typeof end !== 'number') { input.value = input.value.slice(0, -1); } else if (start !== end) { input.value = input.value.slice(0, start) + input.value.slice(end); try { input.setSelectionRange(start, start); } catch (e) { /* ignore */ } } else if (start > 0) { input.value = input.value.slice(0, start - 1) + input.value.slice(end); try { input.setSelectionRange(start - 1, start - 1); } catch (e2) { /* ignore */ } } commit('key'); } function renderValidation(v) { state.valid = v.valid; state.code = v.code; state.message = v.message; state.errIndex = v.index; if (v.valid) { errBox.setAttribute('hidden', 'hidden'); errBox.textContent = ''; wrap.setAttribute('data-invalid', 'false'); input.removeAttribute('aria-invalid'); } else { errBox.textContent = v.message; errBox.removeAttribute('hidden'); wrap.setAttribute('data-invalid', 'true'); input.setAttribute('aria-invalid', 'true'); } } function commit(source) { var raw = sanitize(input.value); if (raw !== input.value) { input.value = raw; } var v = validate(raw); renderValidation(v); var normalized = normalize(raw); var detail = { id: id, raw: raw, normalized: normalized, valid: v.valid }; emit('fibomath:input', detail); if (isFn(opts.onInput)) { try { opts.onInput(detail); } catch (e) { /* never break the field */ } } if (!v.valid) { emit('fibomath:error', { id: id, code: v.code, message: v.message }); } return v; } /* wiring */ on(input, 'input', function () { commit('input'); }); on(input, 'keydown', function (e) { var k = e ? e.key : ''; if (k === 'Enter') { if (!(!e || !isFn(e.preventDefault))) { e.preventDefault(); } var v = commit('enter'); if (isFn(opts.onEnter)) { try { opts.onEnter({ id: id, raw: sanitize(input.value), normalized: normalize(input.value), valid: v.valid }); } catch (err) { /* ignore */ } } setKeyboard(false); } else if (k === 'Escape') { setKeyboard(false); try { input.blur(); } catch (err2) { /* ignore */ } } }); on(input, 'focus', function () { input.setAttribute('data-ph', input.getAttribute('placeholder') || ''); input.setAttribute('placeholder', ''); if (isCoarsePointer()) { setKeyboard(true); } }); on(input, 'blur', function () { if (trim(input.value) === '') { input.setAttribute('placeholder', input.getAttribute('data-ph') || ''); } commit('blur'); }); on(input, 'paste', function (e) { if (!(!e || !isFn(e.preventDefault))) { e.preventDefault(); } var text = ''; if (!(!e || !e.clipboardData || !isFn(e.clipboardData.getData))) { try { text = e.clipboardData.getData('text/plain') || e.clipboardData.getData('text'); } catch (err) { /* ignore */ } } insertAtCaret(sanitize(text)); }); on(toggle, 'click', function () { setKeyboard(!state.kbOpen); }); on(errBox, 'click', function () { try { input.focus(); } catch (e) { /* ignore */ } if (state.errIndex >= 0) { try { input.setSelectionRange(state.errIndex, state.errIndex + 1); } catch (e2) { /* ignore */ } } }); var api = { id: id, el: wrap, input: input, profile: profileName, getValue: function () { return sanitize(input.value); }, getNormalized: function () { return normalize(input.value); }, setValue: function (v) { input.value = sanitize(v); commit('api'); return api; }, validate: function () { return validate(sanitize(input.value)); }, focus: function () { try { input.focus(); } catch (e) { /* ignore */ } return api; }, refreshLocale: function () { applyPlaceholder(); setToggleLabel(); input.setAttribute('aria-label', opts.ariaLabel || defaultAriaLabel(profileName)); kb.setAttribute('aria-label', getLocale() === 'vi' ? 'Ban phim toan hoc' : 'Math keyboard'); renderValidation(validate(input.value)); return api; }, destroy: function () { for (var i = 0; i < bound.length; i++) { var bnd = bound[i]; if (!(!bnd.el || !isFn(bnd.el.removeEventListener))) { try { bnd.el.removeEventListener(bnd.type, bnd.fn); } catch (e) { /* ignore */ } } } var li = liveFields.indexOf(api); if (li >= 0) { liveFields.splice(li, 1); } var wp = wrap.parentNode; if (!(!wp || !isFn(wp.removeChild))) { try { wp.removeChild(wrap); } catch (e2) { /* ignore */ } } if (!(!isInputEl || !origParent || !isFn(origParent.insertBefore))) { try { origParent.insertBefore(input, origNext); } catch (e3) { /* ignore */ } } return api; } }; liveFields.push(api); applyPlaceholder(); setToggleLabel(); renderValidation(validate(input.value)); /* silent initial check, no emit */ return api; } /* public: FiboMath.field (+ introspection helpers for QA) */ function fieldFn(el, opts) { return field(el, opts); } fieldFn.profiles = PROFILES; fieldFn.sanitize = sanitize; fieldFn.normalize = localNormalize; fieldFn.messages = MESSAGES; fieldFn.refreshAll = function () { var copy = liveFields.slice(); for (var i = 0; i < copy.length; i++) { copy[i].refreshLocale(); } }; if (!NS.field) { NS.field = fieldFn; } /* keep placeholders + messages in sync when platform switches locale */ if (!(!doc || !isFn(doc.addEventListener) || !(!NS._inputLocaleHook))) { NS._inputLocaleHook = true; doc.addEventListener('fibomath:locale-change', function () { fieldFn.refreshAll(); }); } })(typeof window !== 'undefined' ? window : (typeof globalThis !== 'undefined' ? globalThis : this)); /* ===== src/patterns.js ===== */ /*! * FiboMath UI Engine v1 - patterns module (team: patterns) * Examples-to-solver rows, structured steps renderer, game overlay API. * * Consumes namespace contracts owned by other teams (never redefined here): * FiboMath.display(el, latex, opts) - core team * FiboMath.emit(name, detail) - platform team * FiboMath.getLocale() / FiboMath.onLocaleChange(cb) - platform team * (falls back to the document event "fibomath:locale-change") * Attaches ONLY exampleRow / steps / overlay. Never touches display, field, parse. * * WordPress-safe: no bare ampersand in source (use \x26 when one is needed), * no double-ampersand operator (nested ifs instead), wpautop-safe plain script. */ (function () { 'use strict'; var win = (typeof window !== 'undefined') ? window : this; var doc = (typeof document !== 'undefined') ? document : null; var nav = win.navigator || ((typeof navigator !== 'undefined') ? navigator : {}); function namespace() { var ns = win.FiboMath || win.__FiboMathNS || {}; win.FiboMath = ns; win.__FiboMathNS = ns; return ns; } var FiboMath = namespace(); /* ---------------- shared helpers ---------------- */ var uidSeq = 0; function uid(prefix) { uidSeq += 1; return String(prefix) + '-' + uidSeq; } function currentLocale() { if (typeof FiboMath.getLocale === 'function') { try { var loc = FiboMath.getLocale(); return loc || 'en'; } catch (e) { return 'en'; } } return 'en'; } function isVi() { return currentLocale() === 'vi'; } function onLocaleChange(cb) { if (typeof FiboMath.onLocaleChange === 'function') { try { FiboMath.onLocaleChange(cb); } catch (e) {} return; } if (doc) { if (typeof doc.addEventListener === 'function') { doc.addEventListener('fibomath:locale-change', function () { cb(currentLocale()); }); } } } function emit(name, detail) { if (typeof FiboMath.emit === 'function') { try { FiboMath.emit(name, detail); } catch (e) {} } } /* All math rendering goes through the core display adapter. */ function renderMath(node, latex, opts) { if (typeof FiboMath.display === 'function') { try { FiboMath.display(node, latex, opts || {}); return; } catch (e) {} } /* Last-resort fallback: readable plain text, never blank. */ node.textContent = latex; } function el(tag, cls, text) { var node = doc.createElement(tag); if (cls) { node.className = cls; } if (typeof text === 'string') { node.textContent = text; } return node; } function clamp01(v, fallback) { if (typeof v !== 'number') { return fallback; } if (isNaN(v)) { return fallback; } if (v < 0) { return 0; } if (v > 1) { return 1; } return v; } /* ---------------- 1. exampleRow : Examples -> Solver ---------------- */ function pickLabel(ex) { if (isVi()) { if (ex.labelVi) { return ex.labelVi; } } if (ex.label) { return ex.label; } return ex.labelVi || ''; } /* Normalize the target into { setValue(value) }. Accepts: * - a field API object with setValue(value) or set(value) (input team contract) * - a CSS selector string resolving to a field element * - a raw DOM element */ function resolveTarget(target) { if (!target) { return null; } if (typeof target === 'string') { if (!doc) { return null; } var node = null; try { node = doc.querySelector(target); } catch (e) { node = null; } if (!node) { return null; } return elementLoader(node); } if (typeof target.setValue === 'function') { return { setValue: function (v) { target.setValue(v); } }; } if (typeof target.set === 'function') { return { setValue: function (v) { target.set(v); } }; } if (target.nodeType === 1) { return elementLoader(target); } return null; } function elementLoader(node) { return { setValue: function (v) { if ('value' in node) { node.value = v; } else { node.textContent = v; } if (typeof win.Event === 'function') { if (typeof node.dispatchEvent === 'function') { try { node.dispatchEvent(new win.Event('input', { bubbles: true })); } catch (e) {} } } if (typeof node.focus === 'function') { try { node.focus(); } catch (e) {} } } }; } function loadExample(rowId, index, ex, data) { var value = ex.value || ''; var loader = resolveTarget(data.target); if (loader) { try { loader.setValue(value); } catch (e) { emit('fibomath:error', { code: 'example-load-failed', id: rowId, index: index, message: 'Could not load example into target' }); } } var detail = { id: rowId, index: index, value: value }; emit('fibomath:example-load', detail); if (typeof data.onLoad === 'function') { try { data.onLoad(detail); } catch (e) {} } } function buildChip(row, ex, index, rowId, data) { ex = ex || {}; var chip = el('button', 'fibo-example-chip'); chip.setAttribute('type', 'button'); chip.id = rowId + '-chip-' + index; var labelEl = el('span', 'fibo-example-chip-label'); var mathEl = el('span', 'fibo-example-chip-math'); chip.appendChild(labelEl); chip.appendChild(mathEl); renderMath(mathEl, ex.value || '', { inline: true }); function paint() { var label = pickLabel(ex); labelEl.textContent = label; chip.setAttribute('aria-label', label); } paint(); chip.addEventListener('click', function () { loadExample(rowId, index, ex, data); }); row.appendChild(chip); return paint; } /* * FiboMath.exampleRow(data) * data = { id, examples: [{label, labelVi, value}], target, onLoad } * Returns the row element (caller appends it where needed). */ function exampleRow(data) { if (!doc) { return null; } data = data || {}; var list = data.examples || []; var rowId = data.id || uid('fibo-examples'); var row = el('div', 'fibo-example-row'); row.id = rowId; row.setAttribute('role', 'group'); var painters = []; for (var i = 0; i < list.length; i++) { painters.push(buildChip(row, list[i], i, rowId, data)); } function refresh() { for (var j = 0; j < painters.length; j++) { painters[j](); } row.setAttribute('aria-label', isVi() ? 'Ví dụ' : 'Examples'); } onLocaleChange(refresh); refresh(); return row; } /* ---------------- 2. steps : structured steps renderer ---------------- */ /* * Split LaTeX on the FIRST top-level "=" (brace-depth aware), so that * "f(x)=x^{2}+1" becomes ["f(x)", "x^{2}+1"] while "=" inside braces is kept. * Returns null when there is no top-level "=". */ function splitEquals(latex) { var depth = 0; for (var i = 0; i < latex.length; i++) { var ch = latex.charAt(i); if (ch === '{') { depth += 1; } else if (ch === '}') { if (depth > 0) { depth -= 1; } } else if (ch === '=') { if (depth === 0) { return [latex.slice(0, i), latex.slice(i + 1)]; } } } return null; } /* * Equation-chain alignment: each step's equation is rendered as a * 3-column grid (right-aligned LHS | "=" | left-aligned RHS). The middle * column holds the same "=" glyph in every row, so all equals signs line * up vertically across steps. Steps without "=" render as a single cell. */ function buildChain(wrap, latex) { var parts = splitEquals(latex || ''); if (!parts) { var single = el('div', 'fibo-eq-single'); renderMath(single, latex || '', { inline: true }); wrap.appendChild(single); return; } var grid = el('div', 'fibo-eq-chain'); grid.setAttribute('role', 'math'); var lhs = el('div', 'fibo-eq-lhs'); var sign = el('div', 'fibo-eq-sign'); var rhs = el('div', 'fibo-eq-rhs'); renderMath(lhs, parts[0], { inline: true }); renderMath(sign, '=', { inline: true }); renderMath(rhs, parts[1], { inline: true }); grid.appendChild(lhs); grid.appendChild(sign); grid.appendChild(rhs); wrap.appendChild(grid); } function copyStep(stepId, index, st, btn) { var payload = st.math || ''; function done(ok) { emit('fibomath:step-copy', { id: stepId, index: index, latex: payload, ok: !!ok }); if (btn) { var label = ok ? (isVi() ? 'Đã chép' : 'Copied') : (isVi() ? 'Sao chép' : 'Copy'); btn.textContent = label; btn.classList.add(ok ? 'is-copied' : 'is-failed'); win.setTimeout(function () { btn.textContent = isVi() ? 'Sao chép' : 'Copy'; btn.classList.remove('is-copied'); btn.classList.remove('is-failed'); }, 1200); } } var handled = false; if (nav.clipboard) { if (typeof nav.clipboard.writeText === 'function') { nav.clipboard.writeText(payload).then( function () { done(true); }, function () { done(false); } ); handled = true; } } if (!handled) { done(legacyCopy(payload)); } } function legacyCopy(text) { if (!doc) { return false; } if (!doc.body) { return false; } var ta = doc.createElement('textarea'); ta.value = text; ta.setAttribute('readonly', ''); ta.style.position = 'fixed'; ta.style.top = '0'; ta.style.left = '0'; ta.style.opacity = '0'; doc.body.appendChild(ta); if (typeof ta.select === 'function') { ta.select(); } var ok = false; if (typeof doc.execCommand === 'function') { try { ok = doc.execCommand('copy'); } catch (e) { ok = false; } } doc.body.removeChild(ta); return !!ok; } function buildStep(list, st, index, stepId) { st = st || {}; var li = el('li', 'fibo-step'); var num = el('span', 'fibo-step-num', String(index + 1)); num.setAttribute('aria-hidden', 'true'); var body = el('div', 'fibo-step-body'); var textEl = el('p', 'fibo-step-text'); var mathWrap = el('div', 'fibo-step-math'); buildChain(mathWrap, st.math || ''); body.appendChild(textEl); body.appendChild(mathWrap); li.appendChild(num); li.appendChild(body); var copyBtn = null; if (st.copyable) { copyBtn = el('button', 'fibo-step-copy'); copyBtn.setAttribute('type', 'button'); copyBtn.addEventListener('click', function () { copyStep(stepId, index, st, copyBtn); }); li.appendChild(copyBtn); } function paint() { var t; if (isVi()) { t = st.textVi || st.text || ''; } else { t = st.text || st.textVi || ''; } textEl.textContent = t; if (copyBtn) { copyBtn.textContent = isVi() ? 'Sao chép' : 'Copy'; } } paint(); list.appendChild(li); return paint; } /* * FiboMath.steps(host, steps) * steps = [{ math (latex), text, textVi, copyable }] * Returns the
    element (also appended to host when given). */ function steps(host, list) { if (!doc) { return null; } list = list || []; var stepId = uid('fibo-steps'); var ol = el('ol', 'fibo-steps'); ol.id = stepId; var painters = []; for (var i = 0; i < list.length; i++) { painters.push(buildStep(ol, list[i], i, stepId)); } function refresh() { for (var j = 0; j < painters.length; j++) { painters[j](); } } onLocaleChange(refresh); refresh(); if (host) { host.appendChild(ol); } return ol; } /* ---------------- 3. overlay : DOM math labels over canvas games ---------------- */ var ANCHORS = [ 'center', 'top', 'bottom', 'left', 'right', 'top-left', 'top-right', 'bottom-left', 'bottom-right' ]; function normAnchor(a) { for (var i = 0; i < ANCHORS.length; i++) { if (ANCHORS[i] === a) { return a; } } return 'center'; } function rectOf(node) { if (typeof node.getBoundingClientRect === 'function') { return node.getBoundingClientRect(); } return { left: 0, top: 0, width: 0, height: 0 }; } /* * FiboMath.overlay(canvas, items) * items = [{ id, latex, x, y (fractions 0..1 of the canvas box), anchor }] * Positions a pointer-transparent DOM layer exactly over the canvas and * places one math label per item (rendered via FiboMath.display, so labels * stay sharp at any DPI - never blurry canvas text). * Returns { el, update(), setItems(list), destroy() }. */ function overlay(canvas, items) { if (!doc) { return null; } items = items || []; var parent = canvas.parentNode; if (!parent) { emit('fibomath:error', { code: 'overlay-no-parent', message: 'FiboMath.overlay: canvas must be attached to the DOM' }); return null; } /* Ensure a positioning context for the absolute layer. */ var pos = ''; if (typeof win.getComputedStyle === 'function') { try { pos = win.getComputedStyle(parent).position || ''; } catch (e) { pos = ''; } } if (!pos) { if (parent.style) { pos = parent.style.position || ''; } } if (pos !== 'relative') { if (pos !== 'absolute') { if (pos !== 'fixed') { if (pos !== 'sticky') { parent.style.position = 'relative'; } } } } var layer = el('div', 'fibo-math-overlay'); layer.setAttribute('aria-hidden', 'true'); parent.appendChild(layer); function syncBox() { var pr = rectOf(parent); var cr = rectOf(canvas); layer.style.position = 'absolute'; layer.style.left = (cr.left - pr.left) + 'px'; layer.style.top = (cr.top - pr.top) + 'px'; layer.style.width = cr.width + 'px'; layer.style.height = cr.height + 'px'; } var itemEls = []; function renderItems(list) { for (var i = 0; i < itemEls.length; i++) { itemEls[i].remove(); } itemEls = []; for (var j = 0; j < list.length; j++) { var it = list[j] || {}; var d = el('div', 'fibo-overlay-item'); d.id = it.id || uid('fibo-overlay'); d.setAttribute('data-anchor', normAnchor(it.anchor)); d.style.left = (clamp01(it.x, 0.5) * 100) + '%'; d.style.top = (clamp01(it.y, 0.5) * 100) + '%'; renderMath(d, it.latex || '', { inline: true }); layer.appendChild(d); itemEls.push(d); } } var current = items.slice(); syncBox(); renderItems(current); function onResize() { syncBox(); } var ro = null; if (typeof win.ResizeObserver === 'function') { try { ro = new win.ResizeObserver(function () { onResize(); }); ro.observe(canvas); } catch (e) { ro = null; } } if (!ro) { if (typeof win.addEventListener === 'function') { win.addEventListener('resize', onResize); } } function destroy() { if (ro) { try { ro.disconnect(); } catch (e) {} ro = null; } else if (typeof win.removeEventListener === 'function') { win.removeEventListener('resize', onResize); } layer.remove(); } return { el: layer, update: function () { syncBox(); }, setItems: function (list) { current = (list || []).slice(); renderItems(current); syncBox(); }, destroy: destroy }; } /* ---------------- attach (never overwrite core contracts) ---------------- */ if (!FiboMath.exampleRow) { FiboMath.exampleRow = exampleRow; } if (!FiboMath.steps) { FiboMath.steps = steps; } if (!FiboMath.overlay) { FiboMath.overlay = overlay; } FiboMath.patternsVersion = '1.0.0'; })(); /* ===== src/platform.js ===== */ /*! * Fibo Math UI Engine v1 - platform module (team: platform) * Shared infrastructure for fibotutors.com (owner-authorized). * * Owns: * - EN/VI locale: FiboMath.setLocale / getLocale / onLocaleChange / t * (two presentations of the SAME math state; switching never resets input) * - Event contract: FiboMath.on / off / emit (+ FiboMath.analytics hook) * - Accessibility helpers: speech text, MathML flag, focus, keyboard, reduced-motion * - Performance: FiboMath.ready / whenReady / _signalReady, onIdle, * deferUntilVisible, lazyRender, shimmer * - DOM id helper: FiboMath.elId (data-fibomath-id or generated) * * Consumes (never redefines): display, field, steps, exampleRow, parse, * normalize, toLatex (core / input / patterns teams). * * WordPress-safe: no bare ampersand in source (use \x26 when one is needed), * no double-ampersand operator (nested ifs instead), wpautop-safe plain script. */ (function () { 'use strict'; var root = (typeof globalThis !== 'undefined') ? globalThis : {}; var win = (typeof root.window !== 'undefined') ? root.window : root; var doc = (typeof root.document !== 'undefined') ? root.document : null; /* Unify the two namespace handles (same convention as the patterns module). */ function namespace() { var ns = win.FiboMath || win.__FiboMathNS || {}; win.FiboMath = ns; win.__FiboMathNS = ns; return ns; } var FiboMath = namespace(); /* Attach without ever overwriting keys owned by other teams. */ function def(name, value) { if (typeof FiboMath[name] === 'undefined') { FiboMath[name] = value; } return FiboMath[name]; } function isFn(x) { return typeof x === 'function'; } /* ---------------- storage (localStorage with memory fallback) ---------------- */ var memStore = {}; function storage() { try { var ls = root.localStorage; if (ls) { if (isFn(ls.getItem)) { return ls; } } } catch (e) {} return { getItem: function (k) { if (Object.prototype.hasOwnProperty.call(memStore, k)) { return memStore[k]; } return null; }, setItem: function (k, v) { memStore[k] = String(v); }, removeItem: function (k) { delete memStore[k]; } }; } /* ================= 1. LOCALE (EN/VI) ================= */ var LOCALE_KEY = 'fibo.site_locale'; var STRINGS = { en: { 'field.placeholder.basic': 'Type a math expression...', 'field.placeholder.algebra': 'Type an equation or expression...', 'field.placeholder.calculus': 'Type a derivative, integral, or limit...', 'field.placeholder.finance': 'Type values, e.g. PV*(1+r)^n...', 'examples.title': 'Examples', 'examples.load': 'Load into solver', 'steps.title': 'Solution steps', 'steps.copy': 'Copy steps', 'steps.copied': 'Steps copied to clipboard.', 'error.parse': 'Could not read that expression. Try simpler input.', 'error.empty': 'Type something first.', 'error.recover': 'Tap the field to edit, then try again.', 'keyboard.open': 'Open math keyboard', 'keyboard.close': 'Close keyboard', 'loading.math': 'Loading math...', 'result.label': 'Result', 'locale.label': 'Language' }, vi: { 'field.placeholder.basic': 'Nhap bieu thuc toan...', 'field.placeholder.algebra': 'Nhap phuong trinh hoac bieu thuc...', 'field.placeholder.calculus': 'Nhap dao ham, tich phan hoac gioi han...', 'field.placeholder.finance': 'Nhap gia tri, vi du PV*(1+r)^n...', 'examples.title': 'Vi du', 'examples.load': 'Nap vao may giai', 'steps.title': 'Cac buoc giai', 'steps.copy': 'Sao chep cac buoc', 'steps.copied': 'Da sao chep cac buoc.', 'error.parse': 'Khong doc duoc bieu thuc. Hay thu nhap don gian hon.', 'error.empty': 'Hay nhap noi dung truoc.', 'error.recover': 'Cham vao o nhap de sua roi thu lai.', 'keyboard.open': 'Mo ban phim toan', 'keyboard.close': 'Dong ban phim', 'loading.math': 'Dang tai cong thuc...', 'result.label': 'Ket qua', 'locale.label': 'Ngon ngu' } }; var currentLocale = 'en'; var localeNodes = []; function t(key) { var pack = STRINGS[currentLocale] || STRINGS.en; var v = pack[key]; if (typeof v === 'undefined') { v = STRINGS.en[key]; } if (typeof v === 'undefined') { return key; } return v; } function renderLocaleNodes() { for (var i = 0; i < localeNodes.length; i++) { var n = localeNodes[i]; try { if (!n) { continue; } if (!n.el) { continue; } if (isFn(n.fn)) { n.fn(n.el, currentLocale); } else if (n.key) { if ('textContent' in n.el) { n.el.textContent = t(n.key); } } if (isFn(n.el.setAttribute)) { n.el.setAttribute('lang', currentLocale); } } catch (e) {} } } function setLocale(code, opts) { if (code !== 'en') { if (code !== 'vi') { emit('fibomath:error', { id: 'site-locale', code: 'bad-locale', message: 'FiboMath.setLocale expects "en" or "vi".' }); return currentLocale; } } var force = false; if (opts) { if (opts.force) { force = true; } } if (code === currentLocale) { if (!force) { return currentLocale; } } currentLocale = code; try { storage().setItem(LOCALE_KEY, code); } catch (e) {} try { if (doc) { if (doc.documentElement) { if (isFn(doc.documentElement.setAttribute)) { doc.documentElement.setAttribute('lang', code); } } } } catch (e2) {} /* Re-render locale-dependent labels in place. Math state (field input, steps, examples) is never touched here. */ renderLocaleNodes(); emit('fibomath:locale-change', { id: 'site-locale', locale: code }); return currentLocale; } function getLocale() { return currentLocale; } function onLocaleChange(cb) { if (!isFn(cb)) { return function () {}; } return on('fibomath:locale-change', function (detail) { var loc = 'en'; if (detail) { if (detail.locale) { loc = detail.locale; } } cb(loc, detail); }); } function registerLocaleText(el, key) { if (!el) { return null; } var node = { el: el, key: key, fn: null }; localeNodes.push(node); try { if ('textContent' in el) { el.textContent = t(key); } if (isFn(el.setAttribute)) { el.setAttribute('lang', currentLocale); } } catch (e) {} return node; } function registerLocaleRender(el, fn) { if (!el) { return null; } if (!isFn(fn)) { return null; } var node = { el: el, key: null, fn: fn }; localeNodes.push(node); try { fn(el, currentLocale); } catch (e) {} return node; } /* * Wire an EN/VI pill: container (or selector) holding buttons with * [data-fibo-locale="en"|"vi"]. Clicking a button calls setLocale; * aria-pressed tracks the active language. */ function bindLocalePill(container) { if (!doc) { return null; } var rootEl = container; if (typeof container === 'string') { try { rootEl = doc.querySelector(container); } catch (e) { rootEl = null; } } if (!rootEl) { return null; } var buttons = []; try { var found = rootEl.querySelectorAll('[data-fibo-locale]'); for (var i = 0; i < found.length; i++) { buttons.push(found[i]); } } catch (e) {} function paint() { for (var j = 0; j < buttons.length; j++) { var b = buttons[j]; var code = null; try { code = b.getAttribute('data-fibo-locale'); } catch (e) {} var active = code === currentLocale; try { b.setAttribute('aria-pressed', active ? 'true' : 'false'); if (b.classList) { if (active) { b.classList.add('is-active'); } else { b.classList.remove('is-active'); } } } catch (e2) {} } } for (var k = 0; k < buttons.length; k++) { (function (btn) { if (isFn(btn.addEventListener)) { btn.addEventListener('click', function () { var code = null; try { code = btn.getAttribute('data-fibo-locale'); } catch (e) {} setLocale(code); }); } })(buttons[k]); } registerLocaleRender(rootEl, paint); paint(); return { el: rootEl, repaint: paint }; } /* Follow the site's own EN/VI pill through the shared storage key. */ function listenForSiteLocale() { try { if (win) { if (isFn(win.addEventListener)) { win.addEventListener('storage', function (e) { if (!e) { return; } if (e.key === LOCALE_KEY) { var v = e.newValue; if (v === 'en') { if (v !== currentLocale) { setLocale(v); } } else if (v === 'vi') { if (v !== currentLocale) { setLocale(v); } } } }); } } } catch (e) {} } (function initLocale() { var v = null; try { v = storage().getItem(LOCALE_KEY); } catch (e) {} if (v === 'en') { currentLocale = 'en'; } else if (v === 'vi') { currentLocale = 'vi'; } })(); listenForSiteLocale(); /* ================= 2. EVENT CONTRACT ================= */ var EVENTS = { EXAMPLE_LOAD: 'fibomath:example-load', STEP_COPY: 'fibomath:step-copy', INPUT: 'fibomath:input', ERROR: 'fibomath:error', LOCALE_CHANGE: 'fibomath:locale-change', READY: 'fibomath:ready' }; var evtListeners = {}; var analyticsSinks = []; var autoIdSeq = 0; var elIdSeq = 0; function on(evt, fn) { if (typeof evt !== 'string') { return function () {}; } if (!isFn(fn)) { return function () {}; } if (!evtListeners[evt]) { evtListeners[evt] = []; } evtListeners[evt].push(fn); return function () { off(evt, fn); }; } function off(evt, fn) { var arr = evtListeners[evt]; if (!arr) { return; } for (var i = arr.length - 1; i >= 0; i--) { if (arr[i] === fn) { arr.splice(i, 1); } } } /* * FiboMath.emit(evt, detail) * detail always carries { id } (element data-fibomath-id or generated), * plus { event, ts }. Also dispatches a bubbling DOM CustomEvent so * non-engine code (e.g. the patterns locale fallback) can listen. * Listener exceptions never break math rendering. */ function emit(evt, detail) { if (typeof evt !== 'string') { return null; } if (!detail) { detail = {}; } else if (typeof detail !== 'object') { detail = { value: detail }; } if (typeof detail.id === 'undefined') { autoIdSeq += 1; detail.id = 'fm-auto-' + autoIdSeq; } else if (detail.id === null) { autoIdSeq += 1; detail.id = 'fm-auto-' + autoIdSeq; } else if (detail.id === '') { autoIdSeq += 1; detail.id = 'fm-auto-' + autoIdSeq; } detail.event = evt; try { detail.ts = Date.now(); } catch (e) {} if (doc) { if (isFn(doc.dispatchEvent)) { try { var CE = root.CustomEvent; if (isFn(CE)) { doc.dispatchEvent(new CE(evt, { detail: detail, bubbles: true })); } } catch (e2) {} } } var arr = evtListeners[evt] || []; for (var i = 0; i < arr.length; i++) { try { arr[i](detail); } catch (e3) {} } for (var j = 0; j < analyticsSinks.length; j++) { try { analyticsSinks[j](evt, detail); } catch (e4) {} } return detail; } /* * FiboMath.analytics(fn) - subscribe an analytics sink. The engine never * beacons on its own; the site wires its own sink here. */ function analytics(fn) { if (!isFn(fn)) { return function () {}; } analyticsSinks.push(fn); return function () { for (var i = analyticsSinks.length - 1; i >= 0; i--) { if (analyticsSinks[i] === fn) { analyticsSinks.splice(i, 1); } } }; } /* * FiboMath.elId(el) - the element's data-fibomath-id, or a generated one * (stored back on the element so it is stable). */ function elId(el) { if (!el) { autoIdSeq += 1; return 'fm-auto-' + autoIdSeq; } if (!isFn(el.getAttribute)) { autoIdSeq += 1; return 'fm-auto-' + autoIdSeq; } var id = null; try { id = el.getAttribute('data-fibomath-id'); } catch (e) {} if (!id) { elIdSeq += 1; id = 'fm-' + elIdSeq; try { if (isFn(el.setAttribute)) { el.setAttribute('data-fibomath-id', id); } } catch (e2) {} } return id; } /* ================= 3. ACCESSIBILITY ================= */ /* * FiboMath.speechText(latex, locale) - rule-based LaTeX to spoken text. * Follows the current locale unless one is passed. Never throws. */ function speechText(latex, locale) { var s = String(latex === null || typeof latex === 'undefined' ? '' : latex); var loc = locale || currentLocale; var vi = loc === 'vi'; function pick(en, v) { return vi ? v : en; } var p, m, a, b; for (p = 0; p < 4; p++) { s = s.replace(/\\d?frac\{([^{}]*)\}\{([^{}]*)\}/g, function (mm, aa, bb) { return ' ' + aa + ' ' + pick('over', 'tren') + ' ' + bb + ' '; }); s = s.replace(/\\sqrt\{([^{}]*)\}/g, function (mm, aa) { return ' ' + pick('square root of', 'can bac hai cua') + ' ' + aa + ' '; }); } var pairs = [ [/\\arcsin/g, 'arcsine', 'acsin'], [/\\arccos/g, 'arccosine', 'accosin'], [/\\arctan/g, 'arctangent', 'actang'], [/\\sin/g, 'sine', 'sin'], [/\\cos/g, 'cosine', 'cosin'], [/\\tan/g, 'tangent', 'tang'], [/\\log/g, 'log', 'logarit'], [/\\ln/g, 'natural log', 'logarit tu nhien'], [/\\lim/g, 'limit', 'gioi han'], [/\\sum/g, 'sum', 'tong'], [/\\prod/g, 'product', 'tich'], [/\\int/g, 'integral', 'tich phan'], [/\\to/g, 'approaches', 'tien toi'], [/\\infty/g, 'infinity', 'vo cuc'], [/\\pi/g, 'pi', 'pi'], [/\\theta/g, 'theta', 'theta'], [/\\alpha/g, 'alpha', 'alpha'], [/\\beta/g, 'beta', 'beta'], [/\\ge/g, 'is greater than or equal to', 'lon hon hoac bang'], [/\\le/g, 'is less than or equal to', 'nho hon hoac bang'], [/\\ne/g, 'is not equal to', 'khac'], [/\\approx/g, 'is approximately', 'xap xi'], [/\\pm/g, 'plus or minus', 'cong hoac tru'], [/\\cdot/g, 'times', 'nhan'], [/\\times/g, 'times', 'nhan'], [/\\div/g, 'divided by', 'chia'], [/\\left/g, ' ', ' '], [/\\right/g, ' ', ' '], [/\\,/g, ' ', ' '], [/\\;/g, ' ', ' '], [/\\!/g, ' ', ' '], [/\\text\{([^{}]*)\}/g, ' $1 ', ' $1 '], [/\\mathrm\{([^{}]*)\}/g, ' $1 ', ' $1 '] ]; for (p = 0; p < pairs.length; p++) { s = s.replace(pairs[p][0], ' ' + pick(pairs[p][1], pairs[p][2]) + ' '); } s = s.replace(/\^/g, ' ' + pick('to the power of', 'mu') + ' '); s = s.replace(/_/g, ' ' + pick('sub', 'chi so') + ' '); s = s.replace(/\(/g, ' ' + pick('open parenthesis', 'mo ngoac') + ' '); s = s.replace(/\)/g, ' ' + pick('close parenthesis', 'dong ngoac') + ' '); s = s.replace(/\[/g, ' ' + pick('open bracket', 'mo ngoac vuong') + ' '); s = s.replace(/\]/g, ' ' + pick('close bracket', 'dong ngoac vuong') + ' '); s = s.replace(/\{/g, ' '); s = s.replace(/\}/g, ' '); s = s.replace(/=/g, ' ' + pick('equals', 'bang') + ' '); s = s.replace(/\+/g, ' ' + pick('plus', 'cong') + ' '); s = s.replace(/-/g, ' ' + pick('minus', 'tru') + ' '); s = s.replace(/\*/g, ' ' + pick('times', 'nhan') + ' '); s = s.replace(/\//g, ' ' + pick('divided by', 'chia') + ' '); s = s.replace(/\|/g, ' '); s = s.replace(/!/g, ' ' + pick('factorial', 'giai thua') + ' '); s = s.replace(/\\[a-zA-Z]+/g, ' '); s = s.replace(/\\/g, ' '); s = s.replace(/\s+/g, ' '); s = s.replace(/^\s+/, ''); s = s.replace(/\s+$/, ''); return s; } /* * FiboMath.applyA11y(el, latex, opts) * Gives a rendered math element role="img" plus an aria-label with speech * text, and stores the latex so FiboMath.respeak can re-render the label * when the locale changes. opts: { speech, locale, mathml }. */ function applyA11y(el, latex, opts) { if (!el) { return ''; } if (!isFn(el.setAttribute)) { return ''; } opts = opts || {}; var speech = opts.speech; if (typeof speech !== 'string') { speech = speechText(latex, opts.locale); } try { el.setAttribute('role', 'img'); el.setAttribute('aria-label', speech); el.setAttribute('data-fibo-latex', String(latex === null || typeof latex === 'undefined' ? '' : latex)); if (opts.mathml) { el.setAttribute('data-fibo-mathml', '1'); } } catch (e) {} return speech; } function respeak(el, locale) { if (!el) { return ''; } var latex = ''; try { if (isFn(el.getAttribute)) { latex = el.getAttribute('data-fibo-latex') || ''; } } catch (e) {} return applyA11y(el, latex, { locale: locale || currentLocale }); } function focusEl(el) { if (!el) { return false; } if (!isFn(el.focus)) { return false; } try { el.focus({ preventScroll: true }); return true; } catch (e) {} try { el.focus(); return true; } catch (e2) {} return false; } /* * FiboMath.bindKeys(el, handlers) - Enter / Escape / Tab handling for * keyboard profiles. Returns an unbind function. */ function bindKeys(el, handlers) { handlers = handlers || {}; if (!el) { return function () {}; } if (!isFn(el.addEventListener)) { return function () {}; } function onKey(e) { if (!e) { return; } var k = e.key; if (k === 'Enter') { if (isFn(handlers.enter)) { handlers.enter(e); } } else if (k === 'Escape') { if (isFn(handlers.escape)) { handlers.escape(e); } } else if (k === 'Esc') { if (isFn(handlers.escape)) { handlers.escape(e); } } else if (k === 'Tab') { if (isFn(handlers.tab)) { handlers.tab(e); } } } el.addEventListener('keydown', onKey); return function () { try { el.removeEventListener('keydown', onKey); } catch (e) {} }; } function reducedMotion() { try { if (win) { if (isFn(win.matchMedia)) { var mq = win.matchMedia('(prefers-reduced-motion: reduce)'); if (mq) { return !!mq.matches; } } } } catch (e) {} return false; } function applyReducedMotionFlag() { if (!doc) { return; } var rootEl = doc.documentElement; if (!rootEl) { return; } if (!isFn(rootEl.setAttribute)) { return; } try { rootEl.setAttribute('data-fibo-reduced-motion', reducedMotion() ? '1' : '0'); } catch (e) {} } (function watchReducedMotion() { applyReducedMotionFlag(); try { if (win) { if (isFn(win.matchMedia)) { var mq = win.matchMedia('(prefers-reduced-motion: reduce)'); if (mq) { if (isFn(mq.addEventListener)) { mq.addEventListener('change', applyReducedMotionFlag); } else if (isFn(mq.addListener)) { mq.addListener(applyReducedMotionFlag); } } } } } catch (e) {} })(); /* ================= 4. PERFORMANCE ================= */ var _readyDone = false; var _readyInfo = null; var _readyCbs = []; var _readyResolve = null; function whenReady(fn) { if (!isFn(fn)) { return; } if (_readyDone) { try { fn(_readyInfo); } catch (e) {} return; } _readyCbs.push(fn); } /* * FiboMath._signalReady(info) - called by the core team once the heavy * renderer (KaTeX/MathJax adapter) has lazy-loaded. Idempotent. */ function signalReady(info) { if (_readyDone) { return _readyInfo; } _readyDone = true; _readyInfo = info || {}; try { _readyInfo.at = Date.now(); } catch (e) {} var cbs = _readyCbs.slice(); _readyCbs = []; for (var i = 0; i < cbs.length; i++) { try { cbs[i](_readyInfo); } catch (e2) {} } if (isFn(_readyResolve)) { try { _readyResolve(_readyInfo); } catch (e3) {} } emit('fibomath:ready', { id: 'engine', info: _readyInfo }); return _readyInfo; } var readyThenable; if (typeof Promise === 'function') { readyThenable = new Promise(function (res) { _readyResolve = res; }); } else { readyThenable = { then: function (fn) { whenReady(fn); return readyThenable; } }; } /* * FiboMath.onIdle(fn, timeout) - requestIdleCallback with setTimeout * fallback, so long pages never render math before the browser is idle. */ function onIdle(fn, timeout) { if (!isFn(fn)) { return 0; } try { if (win) { if (isFn(win.requestIdleCallback)) { return win.requestIdleCallback(fn, { timeout: timeout || 2000 }); } } } catch (e) {} if (typeof setTimeout === 'function') { return setTimeout(fn, 16); } return 0; } /* * FiboMath.deferUntilVisible(el, fn) - wait for the element to approach * the viewport (IntersectionObserver), then run on idle. Returns cancel. */ function deferUntilVisible(el, fn) { if (!isFn(fn)) { return function () {}; } function run() { onIdle(fn); } try { if (el) { if (win) { if (isFn(win.IntersectionObserver)) { var io = new win.IntersectionObserver(function (entries) { for (var i = 0; i < entries.length; i++) { if (entries[i].isIntersecting) { try { io.disconnect(); } catch (e) {} run(); return; } } }, { rootMargin: '200px' }); io.observe(el); return function () { try { io.disconnect(); } catch (e2) {} }; } } } } catch (e3) {} run(); return function () {}; } /* * FiboMath.lazyRender(container, renderFn, opts) * Shows the loading shimmer, defers renderFn until visible and idle, * clears the shimmer afterwards. Render errors become fibomath:error * events (friendly, never raw exceptions to the user). */ function lazyRender(container, renderFn, opts) { if (!container) { return; } if (!isFn(renderFn)) { return; } opts = opts || {}; try { if (container.classList) { container.classList.add('fibo-loading'); } if (isFn(container.setAttribute)) { container.setAttribute('aria-busy', 'true'); } } catch (e) {} deferUntilVisible(container, function () { var ok = true; var errMsg = ''; try { renderFn(); } catch (e2) { ok = false; var em = e2; if (e2) { if (e2.message) { em = e2.message; } } try { errMsg = String(em); } catch (e3) { errMsg = 'render failed'; } } try { if (container.classList) { container.classList.remove('fibo-loading'); } if (isFn(container.setAttribute)) { container.setAttribute('aria-busy', 'false'); } } catch (e4) {} if (!ok) { emit('fibomath:error', { id: elId(container), code: 'render-failed', message: errMsg }); } }); } /* * FiboMath.shimmer(el, rows) - append a skeleton placeholder (used while * the heavy renderer lazy-loads). Styled by platform.css. */ function shimmer(el, rows) { if (!el) { return null; } if (!doc) { return null; } if (!isFn(doc.createElement)) { return null; } var n = typeof rows === 'number' ? rows : 2; if (n < 1) { n = 1; } if (n > 6) { n = 6; } try { var wrap = doc.createElement('div'); wrap.setAttribute('class', 'fibo-shimmer'); wrap.setAttribute('aria-hidden', 'true'); for (var i = 0; i < n; i++) { var bar = doc.createElement('div'); bar.setAttribute('class', 'fibo-shimmer-bar'); wrap.appendChild(bar); } el.appendChild(wrap); return wrap; } catch (e) {} return null; } /* ================= attach ================= */ def('platformVersion', '1.0.0'); def('localeStorageKey', LOCALE_KEY); def('EVENTS', EVENTS); def('setLocale', setLocale); def('getLocale', getLocale); def('onLocaleChange', onLocaleChange); def('t', t); def('registerLocaleText', registerLocaleText); def('registerLocaleRender', registerLocaleRender); def('bindLocalePill', bindLocalePill); def('on', on); def('off', off); def('emit', emit); def('analytics', analytics); def('elId', elId); def('speechText', speechText); def('applyA11y', applyA11y); def('respeak', respeak); def('focusEl', focusEl); def('bindKeys', bindKeys); def('reducedMotion', reducedMotion); def('ready', readyThenable); def('whenReady', whenReady); def('_signalReady', signalReady); def('onIdle', onIdle); def('deferUntilVisible', deferUntilVisible); def('lazyRender', lazyRender); def('shimmer', shimmer); })(); })();