(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) + '' + tag + '>';
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