Fork me on GitHub

addons/maston.js

            
            define([
    'mathlive/core/lexer', 
    'mathlive/core/mathAtom', 
    'mathlive/core/parser', 
    'mathlive/core/definitions',
],
    function(Lexer, MathAtom, ParserModule, Definitions) {

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':          '*',
    '⨉':                '*',        // N-ARY TIMES OPERATOR U+
    '️✖':                '*',        // MULTIPLICATION SYMBOL
    '️×':                '*',        // MULTIPLICATION SIGN
    '.':                '*',
    '÷':                '/',        // DIVISION SIGN
    // '/':             '/',        // SOLIDUS
    '⁄':                 '/',        // FRACTION SLASH
    '/':                '/',        // FULLWIDTH SOLIDUS
    '!':                'factorial',
    '️\\pm':             'plusminus', // PLUS-MINUS SIGN
    '\\mp':             'minusplus', // MINUS-PLUS 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',

}


const FUNCTION_TEMPLATE = {
    // TRIGONOMETRY
    'sin':      '\\sin%_%^ %',
    'cos':      '\\cos%_%^ %',
    'tan':      '\\tan%_%^ %',
    'cot':      '\\cot%_%^ %',
    'sec':      '\\sec%_%^ %',
    'csc':      '\\csc%_%^ %',

    'sinh':     '\\sinh %',
    'cosh':     '\\cosh %',
    'tanh':     '\\tanh %',
    'csch':     '\\csch %',
    'sech':     '\\sech %',
    'coth':     '\\coth %',

    'arcsin':      '\\arcsin %',
    'arccos':      '\\arccos %',
    'arctan':      '\\arctan %',
    'arccot':      '\\arcctg %',        // Check
    'arcsec':      '\\arcsec %',
    'arccsc':      '\\arccsc %',

    'arsinh':     '\\arsinh %',
    'arcosh':     '\\arcosh %',
    'artanh':     '\\artanh %',
    'arcsch':     '\\arcsch %',
    'arsech':     '\\arsech %',
    'arcoth':     '\\arcoth %',

    // 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%_%^ %',

    // 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 %',
    'div':      '\\nabla\\cdot %',
    'floor':    '\\lfloor % \\rfloor%_%^',
    'ceil':     '\\lceil % \\rceil%_%^',
    'abs':      '\\vert % \\vert%_%^',
    'norm':     '\\lVert % \\rVert%_%^',
    'ucorner':  '\\ulcorner % \\urcorner%_%^',
    'lcorner':  '\\llcorner % \\lrcorner%_%^',
    'angle':    '\\langle % \\rangle%_%^',
    'group':    '\\lgroup % \\rgroup%_%^',
    'moustache':'\\lmoustache % \\rmoustache%_%^',
    'brace':    '\\lbrace % \\rbrace%_%^',
    'sqrt':     '\\sqrt[%^]{%}',
    'lcm':      '\\mathop{lcm}%',
    'gcd':      '\\mathop{gcd}%',
    'erf':      '\\mathop{erf}%',
    'erfc':     '\\mathop{erfc}%',
    'randomReal': '\\mathop{randomReal}%',
    'randomInteger': '\\mathop{randomInteger}%',
    

    // Arithmetic operators
    '*':        '%0 \\times %1',

    // 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{%}',
    'Im':       '\\Im{%}',
    'factorial': '%!',
    'factorial2': '%!!',
}





// From www.w3.org/TR/MathML3/appendixc.html

const OP_PRECEDENCE = {
    'degree':               880,
    'nabla':                740,
    'curl':                 740,    // not in MathML
    'partial':              740,
    'differentialD':        740,    // not in MathML
    'capitalDifferentialD': 740,    // 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


    // Relational
    '=':                    260,
    '!=':                   255,

    'approx':               247,
    '<':                    245,
    '>':                    243,
    '≥':                    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 (?:)
    

    // Assignement    
    ':=':                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
}

/**
 * Given a canonical name, return its precedence
 * @param {string} canonicalName, for example "and"
 * @return {number}
 */
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}
 */
function getLatexTemplateForFunction(name) {
    let result = FUNCTION_TEMPLATE[name];
    if (!result) {
        result = name.length > 1 ? '\\mathop{' + name + '} %' : (name + ' %');
    }

    return result;
}

/**
 * 
 * @param {string} name symbol name
 * @return {string}
 */
function getLatexForSymbol(name) {
    let result = FUNCTION_TEMPLATE[name];
    if (result) {
        return result.replace('%1', '').replace('%0', '').replace('%', '');
    }
    const info = Definitions.getInfo('\\' + name, 'math');
    if (info && info.type !== 'error' && 
        (!info.fontFamily || info.fontFamily === 'main' || info.fontFamily === 'ams')) {
        result = '\\' + name;
    }

    return result;
}

/**
 * 
 * @param {string} name function or operator canonical name
 * @return {string}
 */
function getLatexTemplateForOperator(name) {
    let result = FUNCTION_TEMPLATE[name];
    if (!result) {
        result = '%0 \\mathbin{' + name + '} %1';
    }

    return result;
}

function isFunction(canonicalName) {
    if (canonicalName === 'f' || canonicalName === 'g') return true;
    let t = FUNCTION_TEMPLATE[canonicalName];
    if (!t) return false;
    // %0 and %1 are the lhs and rhs arguments. Remove those from the template
    t = t.replace('%0', '').replace('%1', '');
    // If we're left with a plain %, it's the argument list, and therefore
    // this is a function.
    if (/%/.test(t)) return true;
    return false;
}


/**
 * 
 * @param {string} latex, for example '\\times'
 * @return {string} the canonical name for the input, for example '*'
 */
function getCanonicalName(latex) {
    latex = (latex || '').trim();
    let result = CANONICAL_NAMES[latex];
    if (!result) {
        if (latex.charAt(0) === '\\') {
            const info = Definitions.getInfo(latex, 'math', {});
            if (info && info.type !== 'error') {
                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 {*} atom 
 * @return {number}
 */
function opPrec(atom) {
    if (!atom) return null;
    const name = getCanonicalName(getString(atom));
    const result = {
        prec: getPrecedence(name), 
        assoc: getAssociativity(name)
    }
    if (result.prec <= 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',
    '\\ddager':             'dagger2',
    '\\maltese':            'maltese',
    '\\backprime':          'backprime',
    '\\backdoubleprime':    'backprime2',
    '\\prime':              'prime',
    '\\doubleprime':        'prime2',
    '\\$':                  '$',
    '\\%':                  '%',
    '\\_':                  '_',
    '\\degree':             'degree'
}

const ASSOCIATIVE_FUNCTION = {
    '+':                    '+',
    '-':                    '+',      // Substraction is add(), but it's
                                        // handled specifically so that the 
                                        // argument is negated
    '*':                    '*',

    ',':                    '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 (atom.latex && atom.latex !== '\\mathop ' && atom.latex !== '\\mathbin ' &&
        atom.latex !== '\\mathrel ' && atom.latex !== '\\mathopen ' && 
        atom.latex !== '\\mathpunct ' && atom.latex !== '\\mathord ' && 
        atom.latex !== '\\mathinner ' && atom.type !== 'font') {
        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
 */
function asSymbol(expr) {
    let result = expr;
    if (typeof result !== 'string') {
        result = expr !== undefined ? expr.sym : undefined;
    }
    if (result) {
        const latex = getLatexForSymbol(result);
        result = latex || result;
    }
    return result;
}



/**
 * 
 * @param {Object} num -- Abstract Syntax Tree object
 * @return {number} -- A Javascript number, the value of the AST or NaN
 */
function asMachineNumber(num) {
    let result = undefined;
    if (num !== undefined) {
        if (num.num !== undefined) {
            if (num.num.toString().match(/^[+-]?[0-9]*[.]?[0-9]*[eE]?[+-]?[0-9]?$/)) {
                result = parseFloat(num.num);
            }
        } else if (typeof num === 'number') {
            result = parseFloat(num);
        }
    }
    return result;
}

function isNumber(expr) {
    return typeof expr === 'number' || 
        (expr !== undefined && expr.num !== undefined);
}

/**
 * Return true if the current atom is of the specified type and value.
 * @param {Object} expr 
 * @param {string} type 
 * @param {string} value 
 */
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;
}


/**
 * Return the negative of the expression. Usually {op:'-', lhs:expr}
 * but for numbers, the negated number
 * @param {*} expr 
 */
function negate(expr) {
    if (typeof expr === 'number') {
        return -expr;
    } else if (expr && typeof expr.num === 'number') {
        expr.num = -expr.num;
        return expr;
    }
    return {op:'-', lhs:expr};
}


/**
 * Parse for a possible sup/sub at the current token location.
 * Handles both sup/sub attached directly to the current atom 
 * as well as "empty" atoms with a sup/sub following the current
 * atom.
 * @param {Object} expr 
 */
function parseSupsub(expr, options) {
    let atom = expr.atoms[expr.index - 1];

    // Is there a supsub directly on this atom?
    if (!atom || !(atom.superscript || atom.subscript)) {
        atom = null;
    }

    // Is the following atom a subsup atom?
    if (!atom) {
        atom = expr.atoms[expr.index];
        if (isAtom(expr, 'msubsup') && (atom.superscript || atom.subscript)) {
            expr.index += 1;
        } else {
            atom = null;
        }
    }

    if (atom) {
        if (typeof expr.ast === 'string') {
            expr.ast = {sym: expr.ast};            
        } else if (typeof expr.ast === 'number') {
            expr.ast = {num: expr.ast};            
        } else if (!expr.ast.group && !expr.ast.fn && !expr.ast.sym) {
            expr.ast = {group: expr.ast};
        }
        if (atom.subscript) expr.ast.sub = parse(atom.subscript, options);
        if (atom.superscript) expr.ast.sup = parse(atom.superscript, options);
    }

    return expr;
}


/**
 * Parse postfix operators, such as "!" (factorial)
 */
function parsePostfix(expr, options) {
    const atom = expr.atoms[expr.index];
    const lhs = expr.ast;
    const digraph = parseDigraph(expr);
    if (digraph) {
        expr.ast = {op: digraph.ast, lhs: lhs};
        expr = parseSupsub(expr, options);
        expr = parsePostfix(expr, options);
    } else if (atom && atom.latex && atom.latex.match(/\^{.*}/)) {
        expr.index += 1;
        // It's a superscript Unicode char (e.g. ⁰¹²³⁴⁵⁶⁷⁸⁹ⁱ⁺⁻⁼...)
        if (typeof expr.ast === 'string') {
            expr.ast = {sym: expr.ast};            
        } else if (typeof expr.ast === 'number') {
            expr.ast = {num: expr.ast};            
        } else if (!expr.ast.group && !expr.ast.fn && !expr.ast.sym) {
            expr.ast = {group: expr.ast};
        }
        const sup = atom.latex.match(/\^{(.*)}/)[1];
        const n = parseInt(sup);
        if (!isNaN(n)) {
            expr.ast.sup = n;
        } else {
            expr.ast.sup = sup;
        }

    } else if (atom && atom.type === 'textord' && POSTFIX_FUNCTION[atom.latex.trim()]) {
        expr.index += 1;
        expr.ast = {fn: POSTFIX_FUNCTION[atom.latex.trim()], arg: 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
 * 
 */ 
 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;
        } else if (atom.type === 'textord') {
            ldelim = atom.latex.trim();
            rdelim = Definitions.RIGHT_DELIM[ldelim];
        }
        if (ldelim && rdelim) {
            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) {
            expr.index += 1;
            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 (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);
        expr.index += 1;
        expr = parseSupsub(expr, options);
        expr = parsePostfix(expr, options);

    } else {
        return undefined;
    }
    
    expr.minPrec = savedPrec;
    return expr;
}


/** 
 * 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
 * 
*/
function parseDigraph(expr) {
    expr.index = expr.index || 0;

    if (expr.atoms.length === 0 || expr.index >= expr.atoms.length) {
        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 if (isAtom(expr, 'textord', '!')) {
        expr.index += 1;
        if (isAtom(expr, 'textord', '!')) {
            expr.index += 1;
            expr.ast = 'factorial2';
            return expr;
        }
        expr.index -= 1;
    }

    return undefined;
}


function parsePrimary(expr, options) {

    // <primary> := ('-'|'+) <primary> | <number> | '(' <expression> ')' | <symbol> 

     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 = parseSupsub(expr, options);
        const fn = {op: expr.ast};
        fn.lhs = parsePrimary(expr, options).ast;
        expr.ast = fn;

    } else if (atom.type === 'mbin' && (val === '-' || val === '+')) {
        // Prefix + or - sign
        expr.index += 1;  // Skip the '+' or '-' symbol
        atom = expr.atoms[expr.index];
        expr = parsePrimary(expr, options);
        if (atom && '0123456789.'.indexOf(atom.latex) >= 0) {
            if (expr.ast.num && typeof expr.ast.num === 'number') {
                expr.ast.num = val === '-' ? -expr.ast.num : expr.ast.num;
            } else if (typeof expr.ast === 'number') {
                expr.ast = val === '-' ? -expr.ast : expr.ast;
            } else {
                expr.ast = {op: val, rhs: expr.ast};
            }
        } else {
            expr.ast = {op: val, rhs: expr.ast};
        }

    } else if ((atom.type === 'mord' && '0123456789.'.indexOf(atom.latex) >= 0)
         || isAtom(expr, 'mpunct', ',')) {
        // Looks like a number
        let num = '';
        let done = false;
        let pat = '0123456789.eEdD';
        while (expr.index < expr.atoms.length && !done && (isAtom(expr, 'spacing') ||
                (
                    (
                        isAtom(expr, 'mord') || 
                        isAtom(expr, 'mpunct', ',') || 
                        isAtom(expr, 'mbin')
                    ) &&
                        pat.indexOf(expr.atoms[expr.index].latex) >= 0
                    )
                )
            ) {
            if (expr.atoms[expr.index].type === 'spacing') {
                expr.index += 1;
            } else {
                let digit = expr.atoms[expr.index].latex;
                if (digit === 'd' || digit === 'D') {
                    digit = 'e';
                    pat = '0123456789.+-'
                } else if (digit === 'e' || digit === 'E') {
                    pat = '0123456789.+-'
                } else if (pat === '0123456789.+-') {
                    pat = '0123456789';
                }
                num += digit === ',' ? '' : digit;
                if (atom.superscript !== undefined || atom.underscript !== undefined) {
                    done = true; 
                } else {
                    expr.index += 1;
                }                
            }
        }
        expr.ast = parseFloat(num);

        // 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' && 
            (expr.ast.num !== undefined || !isNaN(expr.ast))) {
            // Add an invisible plus, i.e. 2 1/2 = 2 + 1/2
            const lhs = expr.ast;
            expr = parsePrimary(expr, options);
            expr.ast = {lhs: lhs, op:'+', rhs: 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 = {lhs: lhs, op:'+', rhs: expr.ast};            
        }
        expr = parseSupsub(expr, options);
        expr = parsePostfix(expr, options);
        
    } else if (atom.type === 'genfrac' || atom.type === 'surd') {
        expr.index += 1;
        expr.ast = atom.toAST(options);
        expr = parseSupsub(expr, options);
        expr = parsePostfix(expr, options);

    } else if (atom.type === 'font') {
        expr.ast = atom.toAST(options);
        if (expr.ast.sym && expr.ast.variant === 'normal' && 
            isFunction(expr.ast.sym)) {
            // This is a function (for example used with \\mathrm{foo}
            expr.ast = {fn: expr.ast.sym};
            expr = parseSupsub(expr, options);

            const fn = expr.ast;
            expr.index += 1;  // Skip the function name
            fn.arg = parsePrimary(expr, options).ast;
            expr.ast = fn;
            
        } else {
            // It's an identifier of some kind...
            if (atom.superscript === undefined) {
                expr.index += 1;
            }
            expr = parseSupsub(expr, options);
        }
        expr = parsePostfix(expr, options);

    } else if (atom.type === 'mord') {
        // A 'mord' but not a number, either an identifier ('x') or a function
        // ('\\Zeta')
        const name = getCanonicalName(getString(atom));
        if (isFunction(name) && !isOperator(atom)) {
            // A function
            expr.ast = {fn: name};
            expr = parseSupsub(expr, options);

            const fn = expr.ast;
            expr.index += 1;  // Skip the function name
            fn.arg = parsePrimary(expr, options).ast;
            if (fn.arg && (fn.arg.fn === 'list2' || fn.arg.fn === 'list')) {
                fn.arg = fn.arg.arg;
            }
            expr.ast = fn;
        } else {
            // An identifier
            expr.ast = atom.toAST(options);
            if (atom.superscript === undefined) {
                expr.index += 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.trim()]) {
                // Not an operator, not a fence, it's a symbol or a function
                const name = getCanonicalName(getString(atom));
                if (isFunction(name)) {
                    // It's a function
                    expr.ast = {fn: name};
                    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 (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.
        const name = getCanonicalName(getString(atom));
        if (isFunction(name) && !isOperator(atom)) {
            expr.index += 1;
            expr.ast = {fn: name};
            expr = parseSupsub(expr, options);

            if (expr.ast.sup) {
                // There was an exponent with the function. 
                if (expr.ast.sup === -1 || (expr.ast.sup.op === '-' && expr.ast.sup.rhs === 1)) {
                    // This is the 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 (INVERSE_FUNCTION[expr.ast.fn]) {
                        const fn = {fn: INVERSE_FUNCTION[expr.ast.fn]};
                        fn.arg = parsePrimary(expr, options).ast;
                        expr.ast = fn;
                    } else {
                        const fn = expr.ast;
                        fn.arg = parsePrimary(expr, options).ast;
                        expr.ast = fn;
                    }
                } 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;
                fn.arg = parsePrimary(expr, options).ast;
                expr.ast = fn;
            }
        }
    } else if (atom.type === 'array') {
       expr.index += 1;
       expr.ast = atom.toAST(options); 
    } else if (atom.type === 'sizing') {
       expr.index += 1;
       return parsePrimary(expr, options); 
    } else if (atom.type === 'group') {
        expr.index += 1;
        expr.ast = atom.toAST(options);
    }


    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.
                expr.ast = {text: '?'};
                expr.ast.error = 'Unexpected token ' + 
                    "'" + atom.type + "' = " + atom.body + ' = ' + atom.latex;
                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 = parsePrimary(expr, options);
       if (expr && expr.ast) {
            if (isFunction(lhs)) {
                // A function followed by a list -> the list becomes the 
                // argument to the function
                if (expr.ast.fn === 'list2' || expr.ast.fn === 'list') {
                    expr.ast = {fn: lhs, arg: expr.ast.arg};
                } else {
                    expr.ast = {fn: lhs, arg: expr.ast};
                }
            } else {
                // Invisible times, e.g. '2x'
                if (expr.ast.fn === '*') {
                    expr.ast.arg.unshift(lhs);
                } else if (expr.ast.op === '*') {
                    expr.ast = {fn:'*', arg:[lhs, expr.ast.lhs, expr.ast.rhs]};
                } else {
                    expr.ast = {lhs: lhs, op:'*', rhs: 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];
        done = !atom || !isOperator(atom) || opPrec(atom).prec < minPrec;
        if (!done) {
            const opName = getCanonicalName(getString(atom));
            const {prec, assoc} = opPrec(atom);
            if (assoc === 'left') {
                expr.minPrec = prec + 1;
            } else {
                expr.minPrec = prec;
            }
            expr.index += 1;
            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.
                const arg = [];
                if (lhs !== undefined) arg.push(lhs);
                lhs = {fn: fn, arg:arg};
            }

            // Promote substraction to an addition
            if (opName === '-') {
                if (lhs && lhs.op === '+') {
                    // x+y - z      -> add(x, y, -z)
                    const arg = [];
                    if (lhs.lhs !== undefined) arg.push(lhs.lhs);
                    if (lhs.rhs !== undefined) arg.push(lhs.rhs);
                    if (rhs !== undefined) arg.push(negate(rhs));
                    lhs = {fn:'+', arg:arg}
                } else if (lhs && lhs.op === '-') {
                    // x-y - z      -> add(x, -y, -z)
                    const arg = [];
                    if (lhs.lhs !== undefined) arg.push(lhs.lhs);
                    if (lhs.rhs !== undefined) arg.push(negate(lhs.rhs));
                    if (rhs !== undefined) arg.push(negate(rhs));
                    lhs = {fn:'+', arg:arg}
                } else if (lhs && lhs.fn === '+') {
                    // add(x,y) - z -> add(x, y, -z)
                    if (rhs !== undefined) lhs.arg.push(negate(rhs));
                } else {
                    lhs = {lhs: lhs, op: opName, rhs: rhs};
                }
            } else {
                // Is there a function (e.g. '+') implementing the 
                // associative version of this operator (e.g. '+')?
                fn = ASSOCIATIVE_FUNCTION[opName];
                if (fn === '+' && lhs && lhs.op === '-') {
                    const arg = [];
                    if (lhs.lhs !== undefined) arg.push(lhs.lhs);
                    if (lhs.rhs !== undefined) arg.push(negate(lhs.rhs));
                    if (rhs !== undefined) arg.push(rhs);
                    lhs = {fn: fn, arg:arg};
                } else if (fn && lhs && lhs.op === opName) {
                    // x+y + z -> add(x, y, z)
                    const arg = [];
                    if (lhs.lhs !== undefined) arg.push(lhs.lhs);
                    if (lhs.rhs !== undefined) arg.push(lhs.rhs);
                    if (rhs !== undefined) arg.push(rhs);
                    lhs = {fn: fn, arg:arg};
                } else if (fn && lhs && lhs.fn === fn) {
                    // add(x,y) + z -> add(x, y, z)
                    if (rhs !== undefined) lhs.arg.push(rhs);
                } else {
                    lhs = {lhs: lhs, op: opName, rhs: rhs};
                }
            }
        }
    }

    expr.ast = lhs;
    return expr;
}



function toString(atoms) {
    let result = '';
    for (const atom of atoms) {
        if (atom.type === 'textord' || atom.type === 'mord') {
            result += atom.body;
        }
    }
    return escapeText(result);
}


/**
 * Return a string escaped as necessary to comply with the JSON format
 * @param {string} s 
 * @return {string}
 */
function escapeText(s) {
    return s
    .replace(/[\\]/g, '\\\\')
    .replace(/["]/g, '\\"')
    .replace(/[\b]/g, "\\b")
    .replace(/[\f]/g, "\\f")
    .replace(/[\n]/g, "\\n")
    .replace(/[\r]/g, "\\r")
    .replace(/[\t]/g, "\\t");
}

/**
 * Return an AST representation of a single atom
 * 
 * @return {Object}
 */
MathAtom.MathAtom.prototype.toAST = function(options) {
    const MATH_VARIANTS = {
        'mathrm':   'normal',
        'mathbb':   'double-struck',
        'mathbf':   'bold',
        'mathcal':  'script',
        'mathfrak': 'fraktur',
        'mathscr':  'script',
        'mathsf':   'sans-serif',
        'mathtt':   'monospace'
    };
    // TODO: See https://www.w3.org/TR/MathML2/chapter6.html#chars.letter-like-tables

    let result = {};
    let sym = '';
    let m;
    let lhs, rhs;
    const variant = MATH_VARIANTS[this.fontFamily || this.font];

    const command = this.latex ? this.latex.trim() : null;
    switch(this.type) {
        case 'root':
        case 'group':
            // Macros appear as group as well. Handle some of them.
            if (this.latex && this.latex.startsWith('\\nicefrac')) {
                m = this.latex.slice(9).match(/({.*}|[^}])({.*}|[^}])/);
                if (m) {
                    if (m[1].length === 1) {
                        lhs = m[1];
                    } else {
                        lhs = m[1].substr(1, m[1].length - 2);
                    }
                    lhs = ParserModule.parseTokens(Lexer.tokenize(lhs),
                        'math', null, options.macros);
                    result.lhs = parse(lhs, options);

                    if (m[2].length === 1) {
                        rhs = m[2];
                    } else {
                        rhs = m[2].substr(1, m[2].length - 2);
                    }
                    rhs = ParserModule.parseTokens(Lexer.tokenize(rhs),
                        'math', null, options.macros);

                    result.op = '/';
                    result.rhs = parse(rhs, options);                    
                } else {
                    result.op = '/';
                }
            } else {
                result.group = parse(this.body, options);
            }
            break;

        case 'genfrac':
            lhs = parse(this.numer, options);
            rhs = parse(this.denom, options);
            result.lhs = lhs;
            result.op = '/';
            result.rhs = rhs;
            break;

        case 'surd':
            if (this.index) {
                result.fn = 'pow';
                result.arg = [parse(this.body, options)];
                result.arg[1] = {lhs: 1, op: '/', rhs: parse(this.index, options)};
            } else {
                result.fn = 'sqrt';
                result.arg = parse(this.body, options);
            }
            break;

        case 'rule':
            break;

        case 'font':
            if (this.latex === '\\text ') {
                result.text = toString(this.body);
                if (this.toLatex) {
                    result.latex = this.toLatex();
                }
            } else {
                result.sym = toString(this.body);
            }
            break;

        case 'line':
        case 'overlap':
        case 'accent':
            break;

        case 'overunder':
            break;

        case 'mord':
        case 'textord':
            // Check to see if it's a \char command
            m = !command ? undefined : command.match(/[{]?\\char"([0-9abcdefABCDEF]*)[}]?/);
            if (m) {
                sym = String.fromCodePoint(parseInt(m[1], 16));
            } else {
                sym = getCanonicalName(getString(this));
                if (sym.length > 0 && sym.charAt(0) === '\\') {
                    // This is an identifier with no special handling. 
                    // Use the Unicode value if outside ASCII range
                    if (typeof this.body === 'string') {
                        // TODO: consider making this an option?
                        // if (this.body.charCodeAt(0) > 255) {
                        //     sym = '&#x' + ('000000' + 
                        //         this.body.charCodeAt(0).toString(16)).substr(-4) + ';';
                        // } else {
                            sym = this.body;
                        // }
                    }
                }
            }
            result = escapeText(Definitions.mathVariantToUnicode(
                    sym, this.fontFamily || this.font));
            // if (variant) {
            //     result.sym = escapeText(sym);
            // } else {
            //     result = escapeText(sym);       // Shortcut: symbol as string
            // }
            break;

        // case 'mbin':
        // break;

        // case 'mpunct':
        //     result = '<mo separator="true">' + command + '</mo>';
        //     break;

        case 'minner':
            break;

        case 'mop':
            break;

        case 'color':
        case 'box':
            result = parse(this.body, options);
            break;

        case 'enclose':
            // result = '<menclose notation="';
            // for (const notation in this.notation) {
            //     if (this.notation.hasOwnProperty(notation) && 
            //         this.notation[notation]) {
            //         result += sep + notation;
            //         sep = ' ';
            //     }
            // }
            // result += '">' + toAST(this.body).mathML + '</menclose>';
            break;

        case 'array': 
            if (this.env.name === 'cardinality') {
                result = {fn:'card', arg:[parse(this.array, options)]};
            }
            break;

        case 'spacing':
        case 'space':
        case 'sizing':
        case 'mathstyle':
            break;
        default: 
            result = undefined;
            console.log('Unhandled atom ' + this.type + ' - ' + this.body);            
    }

    if (variant && result) {
        result.variant = variant;
    }

    return result;
}


function filterPresentationAtoms(atoms) {
    if (!atoms) return [];
    let result;
    if (Array.isArray(atoms)) {
        result = [];
        for (const atom of atoms) {
            const filter = filterPresentationAtoms(atom);
            result = result.concat(filter);
        }
    } else {
        if (atoms.type === 'spacing') {
            return [];
        } else if (atoms.type === 'first' || atoms.type === 'color' || atoms.type === 'box' || 
                atoms.type === 'sizing') {
            result = filterPresentationAtoms(atoms.body);
        } else {
            if (atoms.body && Array.isArray(atoms.body)) {
                atoms.body = filterPresentationAtoms(atoms.body);
            }
            if (atoms.superscript && Array.isArray(atoms.superscript)) {
                atoms.superscript = filterPresentationAtoms(atoms.superscript);
            }
            if (atoms.subscript && Array.isArray(atoms.subscript)) {
                atoms.subscript = filterPresentationAtoms(atoms.subscript);
            }
            if (atoms.index && Array.isArray(atoms.index)) {
                atoms.index = filterPresentationAtoms(atoms.index);
            }
            if (atoms.denom && Array.isArray(atoms.denom)) {
                atoms.denom = filterPresentationAtoms(atoms.denom);
            }
            if (atoms.numer && Array.isArray(atoms.numer)) {
                atoms.numer = filterPresentationAtoms(atoms.numer);
            }
            if (atoms.array && Array.isArray(atoms.array)) {
                atoms.array = filterPresentationAtoms(atoms.array);
            }
            result = [atoms];
        }
    }
    return result;
}

/**
 * 
 * @param {*} atoms an array of atoms
 * @return  {string}
 */
function parse(atoms, options) {

    return parseExpression({atoms: filterPresentationAtoms(atoms)}, options).ast;
}

function normalize(ast) {
    if (!ast) return ast;
    if (typeof ast === 'string') return ast;
    if (typeof ast === 'number') return ast;
    if (typeof ast === 'object') {
        if (ast.sup) {
            ast.sup = normalize(ast.sup);
        }
        if (ast.op) {
            if (ast.lhs && ast.rhs) {
                // if (ast.op === '+') ast.op = 'add';
                // if (ast.op === '*') ast.op = 'multiply';
                // if (ast.op === '-') ast.op = 'substract';
                // if (ast.op === '/') ast.op = 'divide';
                return {fn:ast.op, arg:[normalize(ast.lhs), normalize(ast.rhs)]};
            }
            return {fn:ast.op, arg:[normalize(ast.rhs)]};
        }
        if (ast.fn && Array.isArray(ast.arg)) {
            return {fn:ast.fn, arg:ast.arg.map(x => normalize(x))};
        }
        if (ast.fn) {
            return {fn:ast.fn, arg:normalize(ast.arg)};
        }
        if (ast.group) {
            return {group: normalize(ast.group)};
        }    
    }


    return ast;
}

MathAtom.toAST = function(atoms, options) {
    return normalize(parse(atoms, options));
}

/**
 * 
 * @param {string} fence -- The fence to validate
 * @param {string} -- Default values, in case no fence is provided
 * @return {string} -- A valid fence
 */
function validateFence(fence, defaultFence) {
    let result = fence || defaultFence;

    // Make sure there are some default values, even if no default fence was 
    // provided.
    // A fence can be up to three characters:
    // - open fence
    // - close fence
    // - middle fence
    // '.' indicate and empty, invisible fence.

    result += '...';

    return result;
}


/**
 * Return a formatted mantissa:
 * 1234567 -> 123 456 7...
 * 1233333 -> 12(3)
 * @param {*} m 
 * @param {*} config 
 */
function formatMantissa(m, config) {
    const originalLength = m.length;
    // The last digit may have been rounded, if it exceeds the precison, 
    // which could throw off the 
    // repeating pattern detection. Ignore it.
    m = m.substr(0, config.precision - 2);

    for (let i = 0; i < m.length - 16; i++) {
        // Offset is the part of the mantissa that is not repeating
        const offset = m.substr(0, i);
        // Try to find a repeating pattern of length j
        for (let j = 0; j < 17; j++) {
            const cycle = m.substr(i, j + 1);
            const times = Math.floor((m.length - offset.length) / cycle.length);
            if (times > 1) {
                if ((offset + cycle.repeat(times + 1)).startsWith(m)) {
                    // We've found a repeating pattern!
                    if (cycle === '0') {
                        return offset.replace(/(\d{3})/g, '$1' + config.groupSeparator);
                    }
                    return offset.replace(/(\d{3})/g, '$1' + config.groupSeparator) +
                        config.beginRepeatingDigits + 
                        cycle.replace(/(\d{3})/g, '$1' + config.groupSeparator) + 
                        config.endRepeatingDigits;
                }
            }
        }
    }
    if (originalLength !== m.length) {
        m += '\\ldots';
    }
    return  m.replace(/(\d{3})/g, '$1' + config.groupSeparator);
}

 /**
 * 
 * @param {Object|number} num -- A number element, or a number or a bignumber or a fraction
 * @return {string} -- A LaTeX representation of the AST
 */
function numberAsLatex(num, config) {
    let result = '';

    if (typeof config.precision === 'number') {
        if (typeof num === 'number') {
            num = parseFloat(num.toFixed(Math.min(20, config.precision)));
        } else if (typeof num === 'string' && num.indexOf('/') >= 0) {
            // It's a fraction. We can ignore the precision
        } else {
            let sign = '';
            if (num[0] === '-') {
                sign = '-';
                num = num.substr(1);
            } else if (num[0] === '+') {
                num = num.substr(1);
            }
            if (num.indexOf('.') >= 0) {
                // if (num.length - 1 < config.precision) {
                //     // 
                //     return sign + formatMantissa(num, config);
                // }
                const m = num.match(/(\d*).(\d*)/);
                const base = m[1];
                const mantissa = m[2];
                
                if (base === '0') {
                    let p = 2;  // Index of the first non-zero digit after the decimal
                    while (num[p] === '0' && p < num.length - 1) {
                        p += 1;
                    }
                    let r = '';
                    if (p <= 6) {
                        r = '0' + config.decimalMarker;
                        r += num.substr(2, p - 2);
                        r += formatMantissa(num.substr(r.length), config);
                    } else {
                        r = num[p];
                        const f = formatMantissa(num.substr(p + 1), config);
                        if (f) {
                            r += config.decimalMarker + f;
                        }
                    }
                    if (num.length - 1 > config.precision && !r.endsWith('}') && !r.endsWith('\\ldots')) {
                        r += '\\ldots';
                    }
                    if (p > 6) {
                        r += config.exponentProduct;
                        if (config.exponentMarker) {
                            r += config.exponentMarker + (1 - p).toString();
                        } else {
                            r += '10^{' + (1 - p).toString() + '}';
                        }
                    }
                    num = r;
                } else {
                    num = base.replace(/\B(?=(\d{3})+(?!\d))/g, config.groupSeparator);
                    const f = formatMantissa(mantissa, config);
                    if (f) {
                        num += config.decimalMarker + f;
                        if (num.length - 1 > config.precision && !num.endsWith('}') && !num.endsWith('\\ldots')) {
                            num += '\\ldots';
                        }
                    }
                }
            } else if (num.length > config.precision) {
                const len = num.length;
                let r = num[0];
                const f = formatMantissa(num.substr(2), config);
                if (f) {
                    r += config.decimalMarker + f;
                    if (r[r.length - 1] !== '}') {
                        r += '\\ldots';
                    }
                }
                if (r !== '1') {
                    r += config.exponentProduct;
                } else {
                    r = '';
                }
                if (config.exponentMarker) {
                    r += config.exponentMarker + (len - 2).toString();
                } else {
                    r += '10^{' + (len - 2).toString() + '}';
                }
                num = r;
            } else {
                num = num.replace(/\B(?=(\d{3})+(?!\d))/g, config.groupSeparator);
            }
            return sign + num;
        }
    }
    if (config.scientificNotation === 'engineering') {
        // Ensure the exponent is a multiple of 3
        if (num === 0) {
            result = '0';
        } else {
            const y = Math.abs(num);
            let exponent = Math.round(Math.log10(y));
            exponent = exponent - exponent % 3;
            if (y < 1000) exponent = 0;
            let mantissa = y / Math.pow(10, exponent);
            const m = mantissa.toString().match(/^(.*)\.(.*)$/);
            if (m && m[1] && m[2]) {
                mantissa = m[1] + config.decimalMarker + m[2];
            }
            if (config.groupSeparator) {
                mantissa = formatMantissa(mantissa.toExponential(), config);
            }
            if (exponent === 0) {
                exponent = '';
            } else if (config.exponentMarker) {
                exponent = config.exponentMarker + exponent;
            } else {
                exponent = config.exponentProduct + ' 10^{' + exponent + '}';
            }
            result = (num < 0 ? '-' : '') + mantissa + exponent;
        }
    } else {
        const valString = typeof num === 'string' ? num : num.toString();
        let m = valString.match(/^(.*)[e|E]([-+]?[0-9]*)$/i);
        let base, exponent, mantissa;
        base = valString;
        mantissa = '';
        if (m && m[1] && m[2]) {
            // There is an exponent...
            base = m[1];
            if (config.exponentMarker) {
                exponent = config.exponentMarker + m[2];
            } else {
                exponent = config.exponentProduct + ' 10^{' + m[2] + '}';
            }
        }
        m = base.match(/^(.*)\.(.*)$/);
        if (m && m[1] && m[2]) {
            base = m[1];
            mantissa = m[2];
        }
        if (config.groupSeparator) {
            base = base.replace(/\B(?=(\d{3})+(?!\d))/g, config.groupSeparator);
            mantissa = formatMantissa(mantissa, config);
        }
        if (mantissa) mantissa = config.decimalMarker + mantissa;
        result = base + mantissa + (exponent || '');
    }
    return result;
}



 /**
 * 
 * @param {Object} ast -- Abstract Syntax Tree object
 * @return {string} -- A LaTeX representation of the AST
 */
function asLatex(ast, options) {
    const config = Object.assign({
        precision:              14,
        decimalMarker:          '.',
        groupSeparator:         '\\, ',
        product:                '\\cdot ',   // \\times, \\,
        exponentProduct:        '\\cdot ',
        exponentMarker:         '',
        arcSeparator:           '\\,',
        scientificNotation:     'auto', // 'engineering', 'auto', 'on'
        beginRepeatingDigits:   '\\overline{',
        endRepeatingDigits:     '}',
    }, options);

    let result = '';

    if (ast === undefined) return '';

    if (ast.latex) {
        // If ast.latex key is present, use it to render the element
        result = ast.latex;

    } else if (ast.num !== undefined && ast.num.match(/([+-]?[0-9]+)\/([0-9]+)/)) {
        result = numberAsLatex(ast.num, config);
        if (ast.sup) result += '^{' + asLatex(ast.sup, config) + '}';
        if (ast.sub) result += '_{' + asLatex(ast.sub, config) + '}';

    } else if (isNumber(ast)) {
        const val = typeof ast === 'number' ? ast : ast.num;
        if (isNaN(val)) {
            result = '\\text{NaN}';
        } else if (val === -Infinity) {
            result = '-\\infty ';
        } else if (val === Infinity) {
            result = '\\infty ';
        } else {
            result = numberAsLatex(ast.num || ast, config);
        }
        if (ast.sup) result += '^{' + asLatex(ast.sup, config) + '}';
        if (ast.sub) result += '_{' + asLatex(ast.sub, config) + '}';

    } else if (ast.re !== undefined || ast.im !== undefined ) {
        let wrap = false;
        if (Math.abs(ast.im) <= 1e-14 && Math.abs(ast.re) <= 1e-14) {
            result = '0';
        } else {
            if (ast.re && Math.abs(ast.re) > 1e-14) {
                result = numberAsLatex(ast.re, config);
            }
            if (Math.abs(ast.im) > 1e-14) {
                const im = asMachineNumber(ast.im); 
                if (Math.abs(ast.re) > 1e-14) {
                    result += im > 0 ? '+' : '';
                    wrap = true;
                }
                result += (Math.abs(im) !== 1 ? 
                    numberAsLatex(ast.im, config) : '') + '\\imaginaryI ';
            }
            if (wrap) {
                const fence = validateFence(ast.fence, '(),');
                result = fence[0] + result + fence[1];
            }
        }
        if (ast.sup) result += '^{' + asLatex(ast.sup, config) + '}';
        if (ast.sub) result += '_{' + asLatex(ast.sub, config) + '}';

    } else if (ast.group) {
        result = asLatex(ast.group);
        if (!isNumber(ast.group) && !asSymbol(ast.group)) {
            const fence = validateFence(ast.fence, '(),');
            result = fence[0] + result + fence[1];
        }
        if (ast.sup) result += '^{' + asLatex(ast.sup, config) + '}';
        if (ast.sub) result += '_{' + asLatex(ast.sub, config) + '}';

    } else if (ast.fn) {
        if (ast.fn === 'pow' && Array.isArray(ast.arg) && ast.arg.length >= 2) {
            result = asLatex(ast.arg[0], config);
            if (!isNumber(ast.arg[0]) && !asSymbol(ast.arg[0])) {
                const fence = validateFence(ast.fence, '(),');
                result = fence[0] + result + fence[1];
            }
    
            result += '^{' + asLatex(ast.arg[1], config) + '}';
        } else {
            const fn = getLatexTemplateForFunction(ast.fn);
            let argstring = '';
            const optionalParen = ast.fn.match(/^(factorial(2)?|(((ar|arc)?(sin|cos|tan|cot|sec|csc)h?)|ln|log|lb))$/);
            if (Array.isArray(ast.arg) || !optionalParen) {
                let sep = '';
                const fence = validateFence(ast.fence, '(),');
                if (fence[0] !== '.') argstring += fence[0];
                if (Array.isArray(ast.arg)) {
                    for (const arg of ast.arg) {
                        argstring += sep + asLatex(arg, config);
                        sep = ', ';
                    }
                } else if (ast.arg) {
                    argstring += asLatex(ast.arg, config);
                }
                if (fence[1] !== '.') argstring += fence[1];
            } else if (ast.arg !== undefined) {
                // The parenthesis may be option...
                if (typeof ast.arg === 'number' || 
                    typeof ast.arg === 'string' ||
                    ast.arg.num !== undefined ||
                    ast.arg.sym !== undefined ||
                    ast.arg.op === '/' ||
                    ast.arg.fn === 'sqrt') {
                    // A simple argument, no need for parentheses
                    argstring = asLatex(ast.arg, config);

                } else {
                    // A complex expression, use parentheses if the arguments 
                    // are the last element of the function template.
                    // For example, for abs()... |%|, parenthesis are not necessary.
                    if (fn[fn.length - 1] === '%') {
                        const fence = validateFence(ast.fence, '(),');
                        argstring += fence[0];
                        argstring += asLatex(ast.arg, config);
                        argstring += fence[1];
                    } else {
                        argstring = asLatex(ast.arg, config);
                    }
                }
            }

            result = fn;
            if (ast.over || ast.sup) {
                result = result.replace('%^','^{' + asLatex(ast.over || ast.sup, config) + '}');
            } else {
                result = result.replace('%^','');
            }
            if (ast.under || ast.sub) {
                result = result.replace('%_','_{' + asLatex(ast.under || ast.sub, config) + '}');
            } else {
                result = result.replace('%_','');
            }

            // Insert the arguments in the function template
            result = result.replace('%', argstring);
        }

    } else if (ast.sym !== undefined || typeof ast === 'string') {
        result = asSymbol(ast);
        // Is it a Unicode value?
        let m = result.match(/^&#x([0-9a-f]+);$/i);
        if (m && m[1]) {
            result = String.fromCodePoint(parseInt(m[1], 16));
        } else {
            m = result.match(/^&#([0-9]+);$/i);
            if (m && m[1]) {
                result = String.fromCodePoint(parseInt(m[1]));
            }
        }

        // Is there a variant info attached to it?
        if (ast.variant) {
            const MATH_VARIANTS = {
                'normal':   'mathrm',
                'double-struck': 'mathbb',
                'bold': 'mathbf',
                // 'script': 'mathcal',
                'fraktur': 'mathfrak',
                'script': 'mathscr',
                'sans-serif': 'mathsf',
                'monospace': 'mathtt'
            };
            result = '\\' + MATH_VARIANTS[ast.variant] + 
                '{' + result + '}';
        }
        if (ast.sup) result += '^{' + asLatex(ast.sup, config) + '}';
        if (ast.sub) result += '_{' + asLatex(ast.sub, config) + '}';

    } else if (ast.op) {
        if (ast.op === '/') {
            result = '\\frac{' + asLatex(ast.lhs, config) + '}{' + asLatex(ast.rhs, config) + '}';
            if (ast.sup || ast.sub) {
                result = '(' + result + ')';
                if (ast.sup) result += '^{' + asLatex(ast.sup, config) + '}';
                if (ast.sub) result += '_{' + asLatex(ast.sub, config) + '}';
            }
        } else {
            let lhs, rhs;
            lhs = asLatex(ast.lhs, config);
            if (ast.lhs && ast.lhs.op && getPrecedence(ast.lhs.op) < getPrecedence(ast.op)) {
                lhs = '(' + lhs + ')';
            }
            
            rhs = asLatex(ast.rhs, config);
            if (ast.rhs && ast.rhs.op && getPrecedence(ast.rhs.op) < getPrecedence(ast.op)) {
                rhs = '(' + rhs + ')';
            }

            if (ast.op === '*') {
                result = '%0 ' + config.product + ' %1';
            } else {
                result = getLatexTemplateForOperator(ast.op);
            }
            result = result.replace('%^', ast.sup ? '^{' + asLatex(ast.sup, config) + '}' : '');
            result = result.replace('%_', ast.sub ? '_{' + asLatex(ast.sub, config) + '}' : '');
            result = result.replace('%0', lhs).replace('%1', rhs).replace('%', lhs);
        }

    } else if (ast.text) {
        result = '\\text{' + ast.text + '}';

    } else if (ast.array) {
        // TODO
    }

    // If there was an error attached to this node, 
    // display it on a red background
    if (ast.error) {
        result = '\\bbox[#F56165]{' + result + '}';
    }

    return result;
}


// Export the public interface for this module
return { 
    asLatex,
    asMachineNumber,
    isNumber,
    asSymbol,
}


})