mathlive
Version:
Render and edit beautifully typeset math
1,492 lines (1,313 loc) • 82 kB
JavaScript
/**
* This module parses and outputs an Abstract Syntax Tree representing the
* formula using the {@tutorial MASTON} format.
*
* To use it, use the {@linkcode MathAtom#toAST MathAtom.toAST()} method.
* @module addons/maston
* @private
*/
import Lexer from '../core/lexer.js';
import MathAtom from '../core/mathAtom.js';
import ParserModule from '../core/parser.js';
import Definitions from '../core/definitions.js';
const CANONICAL_NAMES = {
// CONSTANTS
'\\imaginaryI': '\u2148',
'\\imaginaryJ': '\u2149',
'\\pi': 'π',
'\\exponentialE': '\u212f',
// ARITHMETIC
'﹢': '+', // SMALL PLUS SIGN
'+': '+', // FULL WIDTH PLUS SIGN
'−': '-', // MINUS SIGN
'-': '-', // HYPHEN-MINUS
'﹣': '-', // SMALL HYPHEN-MINUS
'-': '-', // FULLWIDTH HYPHEN-MINUS
'\\times': '*',
'\\cdot': '*',
'⨉': '*', // N-ARY TIMES OPERATOR U+
'️✖': '*', // MULTIPLICATION SYMBOL
'️×': '*', // MULTIPLICATION SIGN
'.': '*',
'÷': '/', // DIVISION SIGN
// '/': '/', // SOLIDUS
'⁄': '/', // FRACTION SLASH
'/': '/', // FULLWIDTH SOLIDUS
'!': 'factorial',
'\\mp': 'minusplus', // MINUS-PLUS SIGN
'\\ne': '!=',
'\\coloneq': ':=',
'\\questeq': '?=',
'\\approx': 'approx',
'\\cong': 'congruent',
'\\sim': 'similar',
'\\equiv': 'equiv',
'\\pm': 'plusminus', // PLUS-MINUS SIGN
'\\land': 'and',
'\\wedge': 'and',
'\\lor': 'or',
'\\vee': 'or',
'\\oplus': 'xor',
'\\veebar': 'xor',
'\\lnot': 'not',
'\\neg': 'not',
'\\exists': 'exists',
'\\nexists': '!exists',
'\\forall': 'forAll',
'\\backepsilon': 'suchThat',
'\\therefore': 'therefore',
'\\because': 'because',
'\\nabla': 'nabla',
'\\circ': 'circle',
// '\\oplus': 'oplus',
'\\ominus': 'ominus',
'\\odot': 'odot',
'\\otimes': 'otimes',
'\\zeta': 'Zeta',
'\\Gamma': 'Gamma',
'\\min': 'min',
'\\max': 'max',
'\\mod': 'mod',
'\\lim': 'lim', // BIG OP
'\\sum': 'sum',
'\\prod': 'prod',
'\\int': 'integral',
'\\iint': 'integral2',
'\\iiint': 'integral3',
'\\Re': 'Re',
'\\gothicCapitalR': 'Re',
'\\Im': 'Im',
'\\gothicCapitalI': 'Im',
'\\binom': 'nCr',
'\\partial': 'partial',
'\\differentialD': 'differentialD',
'\\capitalDifferentialD': 'capitalDifferentialD',
'\\Finv': 'Finv',
'\\Game': 'Game',
'\\wp': 'wp',
'\\ast': 'ast',
'\\star': 'star',
'\\asymp': 'asymp',
// Function domain, limits
'\\to': 'to', // Looks like \rightarrow
'\\gets': 'gets', // Looks like \leftarrow
// Logic
'\\rightarrow': 'shortLogicalImplies',
'\\leftarrow': 'shortLogicalImpliedBy',
'\\leftrightarrow': 'shortLogicalEquivalent',
'\\longrightarrow': 'logicalImplies',
'\\longleftarrow': 'logicalImpliedBy',
'\\longleftrightarrow': 'logicalEquivalent',
// Metalogic
'\\Rightarrow': 'shortImplies',
'\\Leftarrow': 'shortImpliedBy',
'\\Leftrightarrow': 'shortEquivalent',
'\\implies': 'implies',
'\\Longrightarrow': 'implies',
'\\impliedby': 'impliedBy',
'\\Longleftarrow': 'impliedBy',
'\\iff': 'equivalent',
'\\Longleftrightarrow': 'equivalent',
}
// The OP_NAME table maps a canonical name to a function name
const OP_NAME = {
'+': 'add',
'*': 'multiply',
'-': 'subtract',
'/': 'divide',
'=': 'equal',
':=': 'assign',
'!=': 'ne',
'?=': 'questeq',
'approx': 'approx',
'congruent': 'congruent',
'similar': 'similar',
'equiv': 'equiv',
'<': 'lt',
'>': 'gt',
'<=': 'le',
'>=': 'ge',
'≤': 'le',
'≥': 'ge',
'>>': 'gg',
'<<': 'll',
'**': 'pow',
'++': 'increment',
'--': 'decrement',
}
// The FUNCTION_TEMPLATE table maps a canonical name to a LaTeX template
const FUNCTION_TEMPLATE = {
'equal': '%0 = %1',
'ne': '%0 \\ne %1',
'questeq': '%0 \\questeq %1',
'approx': '%0 \\approx %1',
'congruent': '%0 \\cong %1',
'similar': '%0 \\sim %1',
'equiv': '%0 \\equiv %1',
'assign': '%0 := %1',
'lt': '%0 < %1',
'gt': '%0 > %1',
'le': '%0 \\le %1',
'ge': '%0 \\ge %1',
// TRIGONOMETRY
'sin': '\\sin%_%^ %0',
'cos': '\\cos%_%^ %0',
'tan': '\\tan%_%^ %0',
'cot': '\\cot%_%^ %0',
'sec': '\\sec%_%^ %0',
'csc': '\\csc%_%^ %0',
'sinh': '\\sinh %0',
'cosh': '\\cosh %0',
'tanh': '\\tanh %0',
'csch': '\\csch %0',
'sech': '\\sech %0',
'coth': '\\coth %0',
'arcsin': '\\arcsin %0',
'arccos': '\\arccos %0',
'arctan': '\\arctan %0',
'arccot': '\\arcctg %0', // Check
'arcsec': '\\arcsec %0',
'arccsc': '\\arccsc %0',
'arsinh': '\\arsinh %0',
'arcosh': '\\arcosh %0',
'artanh': '\\artanh %0',
'arcsch': '\\arcsch %0',
'arsech': '\\arsech %0',
'arcoth': '\\arcoth %0',
// LOGARITHMS
'ln': '\\ln%_%^ %', // Natural logarithm
'log': '\\log%_%^ %', // General logarithm, e.g. log_10
'lg': '\\lg %', // Common, base-10, logarithm
'lb': '\\lb %', // Binary, base-2, logarithm
// Big operator
'sum': '\\sum%_%^ %0',
'prod': '\\prod%_%^ %0',
// OTHER
'Zeta': '\\zeta%_%^ %', // Riemann Zeta function
'Gamma': '\\Gamma %', // Gamma function, such that Gamma(n) = (n - 1)!
'min': '\\min%_%^ %',
'max': '\\max%_%^ %',
'mod': '\\mod%_%^ %',
'lim': '\\lim%_%^ %', // BIG OP
'binom': '\\binom %',
'nabla': '\\nabla %',
'curl': '\\nabla\\times %0',
'div': '\\nabla\\cdot %0',
'floor': '\\lfloor %0 \\rfloor%_%^',
'ceil': '\\lceil %0 \\rceil%_%^',
'abs': '\\left| %0 \\right|%_%^',
'norm': '\\lVert %0 \\rVert%_%^',
'ucorner': '\\ulcorner %0 \\urcorner%_%^',
'lcorner': '\\llcorner %0 \\lrcorner%_%^',
'angle': '\\langle %0 \\rangle%_%^',
'group': '\\lgroup %0 \\rgroup%_%^',
'moustache':'\\lmoustache %0 \\rmoustache%_%^',
'brace': '\\lbrace %0 \\rbrace%_%^',
'sqrt[]': '\\sqrt[%^]{%0}', // Template used when there's an index
'sqrt': '\\sqrt{%0}',
'lcm': '\\operatorname{lcm}%',
'gcd': '\\operatorname{gcd}%',
'erf': '\\operatorname{erf}%',
'erfc': '\\operatorname{erfc}%',
'randomReal': '\\operatorname{randomReal}%',
'randomInteger': '\\operatorname{randomInteger}%',
// Logic operators
'and': '%0 \\land %1',
'or': '%0 \\lor %1',
'xor': '%0 \\oplus %1',
'not': '%0 \\lnot %1',
// Other operators
'circle': '%0 \\circ %1',
'ast': '%0 \\ast %1',
'star': '%0 \\star %1',
'asymp': '%0 \\asymp %1',
'/': '\\frac{%0}{%1}',
'Re': '\\Re{%0}',
'Im': '\\Im{%0}',
'factorial': '%0!',
'factorial2': '%0!!',
}
// From www.w3.org/TR/MathML3/appendixc.html
// The keys of OP_PRECEDENCE are "canonical names"
// (the values of the CANONICAL_NAMES table above, e.g. "?=")
// Those are different from the latex names (e.g. \\questeq)
// and from the function names (e.g. "questeq")
const OP_PRECEDENCE = {
'degree': 880,
'nabla': 740,
'curl': 740, // not in MathML
'partial': 740,
'differentialD': 740, // not in MathML
'capitalDifferentialD': 740, // not in MathML
'**': 720, // not in MathML
'odot': 710,
// Logical not
'not': 680,
// Division
'div': 660, // division sign
'solidus': 660,
'/': 660,
'setminus': 650, // \setminus, \smallsetminus
'%': 640,
'otimes': 410,
// Set operators
'union': 350, // \cup
'intersection': 350, // \cap
// Multiplication, division and modulo
'*': 390,
'ast': 390,
'.': 390,
'oplus': 300, // also logical XOR... @todo
'ominus': 300,
// Addition
'+': 275,
'-': 275,
'+-': 275, // \pm
'-+': 275, // \mp
// Most circled-ops are 265
'circle': 265,
'circledast': 265,
'circledcirc': 265,
'star': 265, // Different from ast
// Range
'..': 263, // Not in MathML
// Unit conversion
'to': 262, // Not in MathLM
'in': 262, // Not in MathML
'|': 261, // Not in MathML (bind is the |_ operator)
// Relational
'congruent': 265,
'equiv': 260, // MathML: "identical to"
'=': 260,
'!=': 255,
'?=': 255,
'similar': 250, // tilde operator in MathML
'approx': 247,
'<': 245,
'>': 243,
'>=': 242,
'≥': 242,
'<=': 241,
// Set operator
'complement': 240,
'subset': 240, // \subset
'superset': 240, // \supset
// @todo and equality and neg operators
'elementof': 240, // \in
'!elementof': 240, // \notin
//
'exists': 230,
'!exists': 230,
'forall': 230,
// Logical operators
'and': 200,
'xor': 195, // MathML had 190
'or': 190,
// Note: 'not' is 680
// center, low, diag, vert ellipsis 150
// Composition/sequence
'suchThat': 110, // \backepsilon
':': 100,
// '..': 100,
// '...': 100,
// Conditional (?:)
// Assignment
'assign': 80,
':=': 80, // MathML had 260 (same with U+2254 COLON EQUALS)
'therefore': 70,
'because': 70,
// Arrows
// Note: MathML had 270 for the arrows, but this
// would not work for (a = b => b = a)
// See also https://en.wikipedia.org/wiki/Logical_connective#Order_of_precedence
// for a suggested precedence (note that in this page lower precedence
// has the opposite meaning as what we use)
'shortLogicalImplies': 52, // ->
'shortImplies': 51, // =>
'logicalImplies': 50, // -->
'implies': 49, // ==>
'shortLogicalImpliedBy': 48,// <-
'shortImpliedBy': 47, // <=
'logicalImpliedBy': 46, // <--
'impliedBy': 45, // <==
'shortLogicalEquivalent':44,// <->
'shortEquivalent': 43, // <=>
'logicalEquivalent':42, // <-->
'equivalent': 41, // <==>
',': 40,
';': 30
}
function getArg(ast, index) {
return Array.isArray(ast.arg) ? ast.arg[index] : undefined;
}
/**
* Given a canonical name, return its precedence
* @param {string} canonicalName, for example "and"
* @return {number}
* @private
*/
function getPrecedence(canonicalName) {
return canonicalName ? (OP_PRECEDENCE[canonicalName] || -1) : -1;
}
function getAssociativity(canonicalName) {
if (/=|=>/.test(canonicalName)) {
return 'right';
}
return 'left';
}
/**
*
* @param {string} name function canonical name
* @return {string}
* @private
*/
function getLatexTemplateForFunction(name) {
let result = FUNCTION_TEMPLATE[name];
if (!result) {
result = name.length > 1 ? '\\operatorname{' + name + '}%^%_ %' : (name + '%^%_ %');
}
return result;
}
/**
*
* @param {string} name symbol name, e.g. "alpha"
* @return {string}
* @private
*/
function getLatexForSymbol(name) {
let result = FUNCTION_TEMPLATE[name];
if (result) {
return result.replace('%1', '').replace('%0', '').replace('%', '');
}
if (name.length > 1) {
const info = Definitions.getInfo('\\' + name, 'math');
if (info &&
(!info.fontFamily || info.fontFamily === 'cmr' || info.fontFamily === 'ams')) {
result = '\\' + name;
}
}
if (!result) {
result = Definitions.unicodeStringToLatex('math', name);
}
return result;
}
function isFunction(canonicalName) {
if (canonicalName === 'f' || canonicalName === 'g') return true;
const t = FUNCTION_TEMPLATE[canonicalName];
if (!t) return false;
// A plain "%" is a placeholder for an argument list, indicating a function
if (/%[^01_^]?/.test(t)) return true;
return false;
}
/**
*
* @param {string} latex, for example '\\times'
* @return {string} the canonical name for the input, for example '*'
* @private
*/
function getCanonicalName(latex) {
latex = (latex || '').trim();
let result = CANONICAL_NAMES[latex];
if (!result) {
if (/^\\[^{}]+$/.test(latex)) {
const info = Definitions.getInfo(latex, 'math', {});
if (info) {
result = info.value || latex.slice(1);
} else {
result = latex.slice(1);
}
} else {
result = latex;
}
}
return result;
}
/**
* Return the operator precedence of the atom
* or -1 if not an operator
* @param {object} atom
* @return {number}
* @private
*/
function opPrec(atom) {
if (!atom) return null;
const name = getCanonicalName(getString(atom));
const result = [getPrecedence(name), getAssociativity(name)];
if (result[0] <= 0) return null
return result;
}
function isOperator(atom) {
return opPrec(atom) !== null;
}
const DELIM_FUNCTION = {
'\\lfloor\\rfloor': 'floor',
'\\lceil\\rceil': 'ceil',
'\\vert\\vert': 'abs',
'\\lvert\\rvert': 'abs',
'||': 'abs',
'\\Vert\\Vert': 'norm',
'\\lVert\\rVert': 'norm',
'\\ulcorner\\urcorner': 'ucorner',
'\\llcorner\\lrcorner': 'lcorner',
'\\langle\\rangle': 'angle',
'\\lgroup\\rgroup': 'group',
'\\lmoustache\\rmoustache': 'moustache',
'\\lbrace\\rbrace': 'brace'
}
const POSTFIX_FUNCTION = {
'!': 'factorial',
'\\dag': 'dagger',
'\\dagger': 'dagger',
'\\ddagger': 'dagger2',
'\\maltese': 'maltese',
'\\backprime': 'backprime',
'\\backdoubleprime': 'backprime2',
'\\prime': 'prime',
'\\doubleprime': 'prime2',
'\\$': '$',
'\\%': '%',
'\\_': '_',
'\\degree': 'degree'
}
const ASSOCIATIVE_FUNCTION = {
'+': 'add',
'-': 'add', // Subtraction is add(), but it's
// handled specifically so that the
// argument is negated
'*': 'multiply',
'=': 'equal',
',': 'list',
';': 'list2',
'and': 'and',
'or': 'or',
'xor': 'xor',
'union': 'union',
// shortLogicalEquivalent and logicalEquivalent map to the same function
// they mean the same thing, but have a difference precedence.
'shortLogicalEquivalent': 'logicalEquivalent', // logical equivalent, iff, biconditional logical connective
'logicalEquivalent': 'logicalEquivalent', // same
// shortEquivalent and equivalent map to the same function
// they mean the same thing, but have a difference precedence.
'shortEquivalent': 'equivalent', // metalogic equivalent
'equivalent': 'equivalent', // same
}
const SUPER_ASSOCIATIVE_FUNCTION = {
',': 'list',
';': 'list2'
}
function getString(atom) {
if (Array.isArray(atom)) {
let result = '';
for (const subAtom of atom) {
result += getString(subAtom);
}
return result;
}
if (atom.latex &&
!/^\\math(op|bin|rel|open|punct|ord|inner)/.test(atom.latex)) {
return atom.latex.trim();
}
if (atom.type === 'leftright') {
return '';
}
if (typeof atom.body === 'string') {
return atom.body;
}
if (Array.isArray(atom.body)) {
let result = '';
for (const subAtom of atom.body) {
result += getString(subAtom);
}
return result;
}
return '';
}
/**
*
* @param {object} expr - Abstract Syntax Tree object
* @return {string} A string, the symbol, or undefined
* @private
*/
function asSymbol(node) {
return typeof node.sym === 'string' ? (getLatexForSymbol(node.sym) || node.sym) : '';
}
/**
*
* @param {object} node - Abstract Syntax Tree node
* @return {number} A JavaScript number, the value of the AST or NaN
* @private
* @private
*/
function asMachineNumber(node) {
return parseFloat(node.num);
}
function isNumber(node) {
return typeof node === 'object' && typeof node.num !== 'undefined';
}
function numberRe(node) {
let result = 0;
if (isNumber(node)) {
if (typeof node.num === 'object') {
result = typeof node.num.re !== 'undefined' ? parseFloatToPrecision(node.num.re) : 0;
} else {
result = parseFloat(node.num);
}
}
return result;
}
function numberIm(node) {
let result = 0;
if (isNumber(node)) {
if (typeof node.num === 'object') {
result = typeof node.num.im !== 'undefined' ? parseFloatToPrecision(node.num.im) : 0;
}
}
return result;
}
function isComplexWithRealAndImaginary(node) {
return numberRe(node) !== 0 && numberIm(node) !== 0;
}
function hasSup(node) {
return node && typeof node.sup !== 'undefined';
}
function hasSub(node) {
return node && typeof node.sub !== 'undefined';
}
/**
* Return true if the current atom is of the specified type and value.
* @param {object} expr
* @param {string} type
* @param {string} value
* @private
*/
function isAtom(expr, type, value) {
let result = false;
const atom = expr.atoms[expr.index];
if (atom && atom.type === type) {
if (value === undefined) {
result = true;
} else {
result = getString(atom) === value;
}
}
return result;
}
/**
*
* @param {string} functionName
* @param {object} params
* @private
*/
function wrapFn(functionName, ...params) {
const result = { fn: functionName };
if (params) {
const args = [];
for (const arg of params) {
if (arg) args.push(arg);
}
if (args.length > 0) result.arg = args;
}
return result;
}
function wrapNum(num) {
if (typeof num === 'number') {
return {num: num.toString() }
} else if (typeof num === 'string') {
return {num: num}
} else if (typeof num === 'object') {
// This is a complex number
console.assert(typeof num.re === 'string' || typeof num.im === 'string');
return {num: num};
}
return undefined;
}
/**
* Return the negative of the expression. Usually { fn:'negate', arg }
* but for numbers, the negated number
* @param {object} node
* @private
*/
function negate(node) {
if (isNumber(node)) {
const re = numberRe(node);
const im = numberIm(node);
if (im !== 0) {
if (re !== 0) {
node.num.re = (-re).toString();
}
node.num.im = (-im).toString();
} else {
node.num = (-re).toString();
}
return node;
}
return wrapFn('negate', node);
}
function nextIsSupsub(expr) {
const atom = expr.atoms[expr.index + 1];
return atom && atom.type === 'msubsup';
}
/**
* Parse for a possible sup/sub attached directly to the current atom
* or to a following 'msubsup' atom.
* After the call, the index points to the next atom to process.
* @param {object} expr
* @private
*/
function parseSupsub(expr, options) {
let atom = expr.atoms[expr.index];
// Is there a supsub directly on this atom?
if (atom && (typeof atom.superscript !== 'undefined' || typeof atom.subscript !== 'undefined')) {
// Move to the following atom
expr.index += 1;
} else {
atom = null;
}
// If this atom didn't have a sup/sub,
// is the following atom a subsup atom?
if (!atom) {
atom = expr.atoms[expr.index + 1];
if (!atom || atom.type !== 'msubsup' || !(atom.superscript || atom.subscript)) {
atom = null;
} else {
// Yes. Skip the current atom and the supsub
expr.index += 2;
}
}
if (atom) {
if (typeof atom.subscript !== 'undefined') {
expr.ast.sub = parse(atom.subscript, options);
}
if (typeof atom.superscript !== 'undefined') {
if (atom.type === 'msubsup') {
if (/['\u2032]|\\prime/.test(getString(atom.superscript))) {
expr.index += 1;
atom = expr.atoms[expr.index + 1];
if (atom && atom.type === 'msubsup' && /['\u2032]|\\prime/.test(getString(atom.superscript))) {
expr.ast.sup = {sym: '\u2033'}; // DOUBLE-PRIME
} else {
expr.ast.sup = {sym: '\u2032'}; // PRIME
expr.index -= 1;
}
} else if (/['\u2033]|\\doubleprime/.test(getString(atom.superscript))) {
expr.ast.sup = {sym: '\u2033'}; // DOUBLE-PRIME
} else if (expr.ast) {
expr.ast.sup = parse(atom.superscript, options);
}
} else {
expr.ast.sup = parse(atom.superscript, options);
}
}
} else {
// Didn't find a supsup either on this atom and there was no 'msubsup'
// Time to move on to the next atom.
expr.index += 1;
}
return expr;
}
/**
* Parse postfix operators, such as "!" (factorial)
* @private
*/
function parsePostfix(expr, options) {
const lhs = expr.ast;
if (nextIsDigraph(expr, '!!')) {
expr.index += 1;
expr.ast = wrapFn('factorial2', lhs);
expr = parseSupsub(expr, options);
expr = parsePostfix(expr, options);
return expr;
}
if (nextIsDigraph(expr, '++')) {
expr.index += 1;
expr.ast = wrapFn('increment', lhs);
expr = parseSupsub(expr, options);
expr = parsePostfix(expr, options);
return expr;
}
if (nextIsDigraph(expr, '--')) {
expr.index += 1;
expr.ast = wrapFn('decrement', lhs);
expr = parseSupsub(expr, options);
expr = parsePostfix(expr, options);
return expr;
}
const atom = expr.atoms[expr.index];
if (atom && atom.latex && POSTFIX_FUNCTION[atom.latex.trim()]) {
expr.ast = wrapFn(POSTFIX_FUNCTION[atom.latex.trim()], lhs);
expr = parseSupsub(expr, options);
expr = parsePostfix(expr, options);
}
return expr;
}
/**
* Delimiters can be expressed:
* - as a matching pair of regular characters: '(a)'
* - a as 'leftright' expression: '\left(a\right)'
* - as a matching pair of 'sizeddelim': '\Bigl(a\Bigr)
*
* Note that the '\delim' command is only used for delimiters in the middle
* of a \left\right pair and not to represent pair-matched delimiters.
*
* This function handles all three cases
*
* @private
*/
function parseDelim(expr, ldelim, rdelim, options) {
expr.index = expr.index || 0;
if (expr.atoms.length === 0 || expr.index >= expr.atoms.length) {
expr.ast = undefined;
return expr;
}
const savedPrec = expr.minPrec;
expr.minPrec = 0;
let atom = expr.atoms[expr.index];
if (!ldelim) {
// If we didn't expect a specific delimiter, parse any delimiter
// and return it as a function application
let pairedDelim = true;
if (atom.type === 'mopen') {
ldelim = atom.latex.trim();
rdelim = Definitions.RIGHT_DELIM[ldelim];
} else if (atom.type === 'sizeddelim') {
ldelim = atom.delim;
rdelim = Definitions.RIGHT_DELIM[ldelim];
} else if (atom.type === 'leftright') {
pairedDelim = false;
ldelim = atom.leftDelim;
rdelim = atom.rightDelim;
// If we have an unclosed smart fence, assume the right delim is
// matching the left delim
if (rdelim === '?') rdelim = Definitions.RIGHT_DELIM[ldelim];
} else if (atom.type === 'textord') {
ldelim = atom.latex.trim();
rdelim = Definitions.RIGHT_DELIM[ldelim];
}
if (ldelim && rdelim) {
if (ldelim === '|' && rdelim === '|') {
// Check if this could be a ||x|| instead of |x|
const atom = expr.atoms[expr.index + 1];
if (atom && atom.type === 'textord' && atom.latex === '|') {
// Yes, it's a ||x||
ldelim = '\\lVert';
rdelim = '\\rVert';
}
}
expr = parseDelim(expr, ldelim, rdelim);
if (expr) {
if (pairedDelim) expr.index += 1;
expr.ast = {
fn: DELIM_FUNCTION[ldelim + rdelim] || (ldelim + rdelim),
arg: [expr.ast]};
expr.minPrec = savedPrec;
return expr;
}
}
return undefined;
}
if (atom.type === 'mopen' && getString(atom) === ldelim) {
expr.index += 1; // Skip the open delim
expr = parseExpression(expr, options);
atom = expr.atoms[expr.index];
if (atom && atom.type === 'mclose' && getString(atom) === rdelim) {
if (nextIsSupsub(expr)) {
// Wrap in a group if we have an upcoming superscript or subscript
expr.ast = {group: expr.ast};
}
expr = parseSupsub(expr, options);
expr = parsePostfix(expr, options);
} // TODO: else, syntax error?
} else if (atom.type === 'textord' && getString(atom) === ldelim) {
expr.index += 1; // Skip the open delim
expr = parseExpression(expr, options);
atom = expr.atoms[expr.index];
if (atom && atom.type === 'textord' && getString(atom) === rdelim) {
expr.index += 1;
expr = parseSupsub(expr, options);
expr = parsePostfix(expr, options);
} // TODO: else, syntax error?
} else if (ldelim === '\\lVert' && atom.type === 'textord' && atom.latex === '|') {
atom = expr.atoms[expr.index + 1];
if (atom && atom.type === 'textord' && atom.latex === '|') {
// This is an opening ||
expr.index += 2; // Skip the open delim
expr = parseExpression(expr, options);
atom = expr.atoms[expr.index];
const atom2 = expr.atoms[expr.index + 1];
if (atom && atom.type === 'textord' && atom.latex === '|' &&
atom2 && atom2.type === 'textord' && atom2.latex === '|') {
// This was a closing ||
expr.index += 2;
expr = parseSupsub(expr, options);
expr = parsePostfix(expr, options);
}
}
} else if (atom.type === 'sizeddelim' && atom.delim === ldelim) {
expr.index += 1; // Skip the open delim
expr = parseExpression(expr, options);
atom = expr.atoms[expr.index];
if (atom && atom.type === 'sizeddelim' && atom.delim === rdelim) {
expr.index += 1;
expr = parseSupsub(expr, options);
expr = parsePostfix(expr, options);
} // TODO: else, syntax error?
} else if (atom.type === 'leftright' &&
atom.leftDelim === ldelim &&
(atom.rightDelim === '?' || atom.rightDelim === rdelim)) {
// This atom type includes the content of the parenthetical expression
// in its body
expr.ast = parse(atom.body, options);
if (nextIsSupsub(expr)) {
// Wrap in a group if we have an upcoming superscript or subscript
expr.ast = {group: expr.ast};
}
expr = parseSupsub(expr, options);
expr = parsePostfix(expr, options);
} else {
return undefined;
}
expr.minPrec = savedPrec;
return expr;
}
function nextIsDigraph(expr, digraph) {
expr.index = expr.index || 0;
if (expr.atoms.length <= 1 || expr.index >= expr.atoms.length - 1) {
return false;
}
return digraph === getString(expr.atoms[expr.index]) +
getString(expr.atoms[expr.index + 1]);
}
/**
* Some symbols are made up of two consecutive characters.
* Handle them here. Return undefined if not a digraph.
* TODO: other digraphs:
* :=
* ++
* **
* =:
* °C U+2103
* °F U+2109
* @private
*
*/
function parseDigraph(expr) {
expr.index = expr.index || 0;
if (expr.atoms.length <= 1 || expr.index >= expr.atoms.length - 1) {
return undefined;
}
if (isAtom(expr, 'textord', '\\nabla')) {
expr.index += 1;
if (isAtom(expr, 'mbin', '\\times')) {
expr.index += 1;
expr.ast = 'curl'; // divergence
return expr;
} else if (isAtom(expr, 'mbin', '\\cdot')) {
expr.index += 1;
expr.ast = 'div';
return expr;
}
expr.index -= 1;
} else {
const digraph = expr.atoms[expr.index].latex +
expr.atoms[expr.index + 1].latex;
const result = /^(>=|<=|>>|<<|:=|!=|\*\*|\+\+|--)$/.test(digraph) ? digraph : '';
if (result) {
expr.index += 1;
}
return result;
}
return undefined;
}
function parsePrimary(expr, options) {
// <primary> := ('-'|'+) <primary> | <number> |
// '(' <expression> ')' | <symbol> | <text> (<expression>)
expr.index = expr.index || 0;
expr.ast = undefined;
if (expr.atoms.length === 0 || expr.index >= expr.atoms.length) {
return expr;
}
let atom = expr.atoms[expr.index];
const val = getCanonicalName(getString(atom));
const digraph = parseDigraph(expr);
if (digraph) {
expr.ast = wrapFn(expr.ast, parsePrimary(expr, options).ast);
} else if (atom.type === 'root') {
expr.index = 0;
expr.atoms = atom.body;
return parsePrimary(expr, options);
} else if (atom.type === 'mbin' && val === '-') {
// Prefix - sign
expr.index += 1; // Skip the '-' symbol
expr = parsePrimary(expr, options);
expr.ast = negate(expr.ast);
} else if (atom.type === 'mbin' && val === '+') {
// Prefix + sign
expr.index += 1; // Skip the '+' symbol
expr = parsePrimary(expr, options);
expr.ast = wrapFn('add', expr.ast);
} else if (atom.type === 'mord' && /^[0-9.]$/.test(atom.latex)) {
// Looks like a number
let num = '';
let done = false;
let pat = /^[0-9.eEdD]$/;
while (expr.index < expr.atoms.length && !done && (isAtom(expr, 'spacing') ||
(
(
isAtom(expr, 'mord') ||
isAtom(expr, 'mpunct', ',') ||
isAtom(expr, 'mbin')
) &&
pat.test(expr.atoms[expr.index].latex)
)
)
) {
if (expr.atoms[expr.index].type === 'spacing') {
expr.index += 1;
} else if (typeof expr.atoms[expr.index].superscript !== 'undefined' ||
typeof expr.atoms[expr.index].subscript !== 'undefined') {
done = true;
} else {
let digit = expr.atoms[expr.index].latex;
if (digit === 'd' || digit === 'D') {
digit = 'e';
pat = /^[0-9+-.]$/;
} else if (digit === 'e' || digit === 'E') {
if (nextIsSupsub(expr)) {
digit = '';
expr.index -= 1;
done = true;
} else {
digit = 'E';
pat = /^[0-9+-.]$/
}
} else if (pat === /^[0-9+-.]$/) {
pat = /^[0-9]$/;
}
num += digit === ',' ? '' : digit;
expr.index += 1;
}
}
expr.ast = num ? wrapNum(num) : undefined;
// This was a number. Is it followed by a fraction, e.g. 2 1/2
atom = expr.atoms[expr.index];
if (atom && atom.type === 'genfrac' && !isNaN(expr.ast.num)) {
// Add an invisible plus, i.e. 2 1/2 = 2 + 1/2
const lhs = expr.ast;
expr = parsePrimary(expr, options);
expr.ast = wrapFn('add', lhs, expr.ast);
}
if (atom && atom.type === 'group' && atom.latex && atom.latex.startsWith('\\nicefrac')) {
// \nicefrac macro, add an invisible plus
const lhs = expr.ast;
expr = parsePrimary(expr, options);
expr.ast = wrapFn('add', lhs, expr.ast);
}
if (atom && atom.type === 'msubsup') {
expr = parseSupsub(expr, options);
}
expr = parsePostfix(expr, options);
} else if (atom.type === 'genfrac' || atom.type === 'surd') {
// A fraction or a square/cube root
expr.ast = atom.toAST(options);
expr = parseSupsub(expr, options);
expr = parsePostfix(expr, options);
} else if (atom.type === 'mord' || atom.type === 'mbin') {
// A 'mord' but not a number: either an identifier ('x') or
// a function ('\\Zeta')
if (isFunction(val) && !isOperator(atom)) {
// A function
expr.ast = { fn: val };
expr = parseSupsub(expr, options);
const fn = expr.ast;
const arg = parsePrimary(expr, options).ast;
if (arg && /^(list0|list|list2)$/.test(arg.fn)) {
fn.arg = fn.arg ? fn.arg.arg : undefined;
} else if (arg) {
fn.arg = [arg]
}
expr.ast = fn;
} else {
// An identifier
expr.ast = atom.toAST(options);
if (expr.ast.sym === 'ⅈ') {
// It's 'i', the imaginary unit
expr.ast = wrapNum({im: "1"});
}
expr = parseSupsub(expr);
}
expr = parsePostfix(expr, options);
} else if (atom.type === 'textord') {
// Note that 'textord' can also be operators, and are handled as such
// in parseExpression()
if (!isOperator(atom)) {
// This doesn't look like a textord operator
if (!Definitions.RIGHT_DELIM[atom.latex ? atom.latex.trim() : atom.body]) {
// Not an operator, not a fence, it's a symbol or a function
if (isFunction(val)) {
// It's a function
expr.ast = { fn: val };
expr = parseSupsub(expr, options);
const fn = expr.ast;
expr.index += 1; // Skip the function name
fn.arg = [parsePrimary(expr, options).ast];
expr.ast = fn;
expr = parsePostfix(expr, options);
} else {
// It was a symbol...
expr.ast = atom.toAST(options);
if (typeof atom.superscript === 'undefined') {
expr.index += 1;
}
expr = parseSupsub(expr, options);
expr = parsePostfix(expr, options);
}
}
}
} else if (atom.type === 'mop') {
// Could be a function or an operator.
if ((/^\\(mathop|operatorname|operatorname\*)/.test(atom.latex) || isFunction(val)) &&
!isOperator(atom)) {
expr.ast = { fn: /^\\(mathop|operatorname|operatorname\*)/.test(atom.latex) ? atom.body : val};
expr = parseSupsub(expr, options);
if (hasSup(expr.ast)) {
// There was an exponent with the function.
// This may be an inverse function
const INVERSE_FUNCTION = {
'sin' : 'arcsin',
'cos': 'arccos',
'tan': 'arctan',
'cot': 'arccot',
'sec': 'arcsec',
'csc': 'arccsc',
'sinh': 'arsinh',
'cosh': 'arcosh',
'tanh': 'artanh',
'csch': 'arcsch',
'sech': 'arsech',
'coth': 'arcoth'
};
if (asMachineNumber(expr.ast.sup) === -1 && INVERSE_FUNCTION[val]) {
expr.ast = wrapFn(INVERSE_FUNCTION[val], parsePrimary(expr, options).ast);
} else {
// Keep the exponent, add the argument
const fn = expr.ast;
fn.arg = [parsePrimary(expr, options).ast];
expr.ast = fn;
}
} else {
const fn = expr.ast;
const arg = parsePrimary(expr, options).ast;
if (arg && /^(list0|list|list2)$/.test(arg.fn)) {
fn.arg = arg.arg;
} else if (arg) {
fn.arg = [arg]
}
expr.ast = fn;
}
}
} else if (atom.type === 'array') {
expr.index += 1;
expr.ast = atom.toAST(options);
} else if (atom.type === 'group') {
expr.index += 1;
expr.ast = atom.toAST(options);
} else if (atom.type === 'mclose') {
return expr;
} else if (atom.type === 'error') {
expr.index += 1;
expr.ast = { error: atom.latex };
return expr;
}
if (expr.ast === undefined) {
// Parse either a group of paren, and return their content as the result
// or a pair of delimiters, and return them as a function applied
// to their content, i.e. "|x|" -> {fn: "||", arg: "x"}
const delim = parseDelim(expr, '(', ')', options) || parseDelim(expr, null, null, options);
if (delim) {
expr = delim;
} else if (!isOperator(atom)) {
// This is not an operator (if it is, it may be an operator
// dealing with an empty lhs. It's possible.
// Couldn't interpret the expression. Output an error.
if (atom.type === 'placeholder') {
// Default value for a placeholder is 0
// (except for the denominator of a 'genfrac')
expr.ast = wrapNum(0);
} else {
expr.ast = {text: '?'};
expr.ast.error = 'Unexpected token ' + "'" + atom.type + "'";
if (atom.latex) {
expr.ast.latex = atom.latex;
} else if (atom.body && atom.toLatex) {
expr.ast.latex = atom.toLatex();
}
}
expr.index += 1; // Skip the unexpected token, and attempt to continue
}
}
atom = expr.atoms[expr.index];
if (atom && (atom.type === 'mord' ||
atom.type === 'surd' ||
atom.type === 'mop' ||
atom.type === 'mopen' ||
atom.type === 'sizeddelim' ||
atom.type === 'leftright')) {
if (atom.type === 'sizeddelim') {
for (const d in Definitions.RIGHT_DELIM) {
if (atom.delim === Definitions.RIGHT_DELIM[d]) {
// This is (most likely) a closing delim, exit.
// There are ambiguous cases, for example |x|y|z|.
expr.index += 1;
return expr;
}
}
}
if ((atom.type === 'mord' || atom.type === 'textord' || atom.type === 'mop') &&
isOperator(atom)) {
// It's actually an operator
return expr;
}
const lhs = expr.ast;
expr.ast = {};
expr = parsePrimary(expr, options);
if (expr && expr.ast && lhs) {
if (isFunction(lhs.fn) &&
typeof lhs.arg === 'undefined' ||
(Array.isArray(lhs.arg) && lhs.arg.length === 0)) {
// A function with no arguments followed by a list ->
// the list becomes the argument to the function
if (expr.ast.fn === 'list2' || expr.ast.fn === 'list') {
expr.ast = wrapFn(lhs.fn, expr.ast.arg);
} else {
// A function "f(x)" or "√x" followed by something else:
// implicit multiply
expr.ast = wrapFn('multiply', lhs, expr.ast);
}
} else {
// Invisible times, e.g. '2x'
if (expr.ast.fn === 'multiply') {
expr.ast.arg.unshift(lhs);
} else if (numberIm(lhs) === 0 && numberRe(lhs) !== 0 &&
numberIm(expr.ast) === 1 && numberRe(expr.ast) === 0) {
// Imaginary number, i.e. "3i"
expr.ast = wrapNum({im: numberRe(lhs).toString()});
} else {
expr.ast = wrapFn('multiply', lhs, expr.ast);
}
}
} else {
expr.ast = lhs;
}
}
return expr;
}
/**
* Given an atom or an array of atoms, return their AST representation as
* an object.
* @param {object} expr An expressions, including expr.atoms, expr.index,
* expr.minPrec the minimum precedence that this parser should parse
* before returning; expr.lhs (optional); expr.ast, the resulting AST.
* @return {object} the expr object, updated
* @private
*/
function parseExpression(expr, options) {
expr.index = expr.index || 0;
expr.ast = undefined;
if (expr.atoms.length === 0 || expr.index >= expr.atoms.length) return expr;
expr.minPrec = expr.minPrec || 0;
let lhs = parsePrimary(expr, options).ast;
let done = false;
const minPrec = expr.minPrec;
while (!done) {
const atom = expr.atoms[expr.index];
const digraph = parseDigraph(expr);
done = !atom || atom.mode === 'text' || (!digraph && !isOperator(atom));
let prec, assoc;
if (!done) {
[prec, assoc] = digraph ?
[getPrecedence(digraph), getAssociativity(digraph)] :
opPrec(atom);
done = prec < minPrec
}
if (!done) {
const opName = digraph || getCanonicalName(getString(atom));
if (assoc === 'left') {
expr.minPrec = prec + 1;
} else {
expr.minPrec = prec;
}
expr.index += 1;
if (opName === '|') {
if (typeof atom.subscript !== 'undefined' ||
(expr.atoms[expr.index] &&
typeof expr.atoms[expr.index].subscript !== 'undefined' &&
expr.atoms[expr.index].type === 'msubsup')
) {
// Bind is a special function. It doesn't have a rhs, and
// its argument is a subscript.
expr.ast = {};
const sub_arg = parseSupsub(expr, options).ast.sub;
lhs = wrapFn('bind', lhs);
if (sub_arg && sub_arg.fn === 'equal' && lhs.arg) {
// This is a subscript of the form "x=..."
lhs.arg.push(getArg(sub_arg, 0));
lhs.arg.push(getArg(sub_arg, 1));
} else if (sub_arg && lhs.arg && (sub_arg.fn === 'list' || sub_arg.fn === 'list2')) {
// Form: "x=0;n=3;z=5"
let currentSym = {sym: "x"};
for (let i = 0; i < sub_arg.arg.length; i++) {
if (sub_arg.arg[i].fn === 'equal') {
currentSym = getArg(sub_arg.arg[i], 0);
lhs.arg.push(currentSym);
lhs.arg.push(getArg(sub_arg.arg[i], 1));
} else {
lhs.arg.push(currentSym);
lhs.arg.push(sub_arg.arg[i]);
}
}
} else if (sub_arg) {
// Default identifier if none provided
lhs.arg.push({sym: "x"});
lhs.arg.push(sub_arg);
}
} else {
// That was a "|", but not with a subscript after, so
// it's the end of the expression, might be a right fence.
done = true;
}
} else {
const rhs = parseExpression(expr, options).ast;
// Some operators (',' and ';' for example) convert into a function
// even if there's only two arguments. They're super associative...
let fn = SUPER_ASSOCIATIVE_FUNCTION[opName];
if (fn && lhs && lhs.fn !== fn) {
// Only promote them if the lhs is not already the same function.
// If it is, we'll combine it below.
lhs = wrapFn(fn, lhs);
}
// Promote subtraction to an addition
if (opName === '-') {
if (lhs && lhs.arg && lhs.fn === 'add') {
// add(x,y) - z -> add(x, y, -z)
if (rhs !== undefined) lhs.arg.push(negate(rhs));
} else if (lhs && lhs.fn === 'subtract') {
// x-y - z -> add(x, -y, -z)
lhs = wrapFn('add', getArg(lhs, 0), negate(getArg(lhs, 1)), negate(rhs));
} else if (isNumber(lhs) && !hasSup(lhs) &&
isNumber(rhs) && !hasSup(rhs) &&
(typeof rhs.num.re === 'undefined' || rhs.num.re === '0') &&
typeof rhs.num.im !== 'undefined') {
lhs = {num: {
re: lhs.num,
im: (-parseFloat(rhs.num.im)).toString()
}};
} else {
lhs = wrapFn('subtract', lhs, rhs);
}
} else {
// Is there a function (e.g. 'add') implementing the
// associative version of this operator (e.g. '+')?
fn = ASSOCIATIVE_FUNCTION[opName];
if (fn === 'add' && lhs && lhs.fn === 'subtract') {
// subtract(x, y) + z -> add(x, -y, z)
lhs = wrapFn('add', getArg(lhs, 0),