define(['mathlive/core/mathAtom', 'mathlive/core/color'],
function(MathAtom, Color) {
const SPECIAL_OPERATORS = {
'\\pm': '±',
'\\times': '×',
'\\colon': ':',
'\\vert': '|',
'\\Vert': '\u2225',
'\\mid': '\u2223',
'\\lbrace': '{',
'\\rbrace': '}',
'\\langle': '\u27e8',
'\\rangle': '\u27e9',
'\\lfloor': '\u230a',
'\\rfloor': '\u230b',
'\\lceil': '\u2308',
'\\rceil': '\u2309',
'\\vec': '⃗',
'\\acute': '´',
'\\grave': '`',
'\\dot': '˙',
'\\ddot': '¨',
'\\tilde': '~',
'\\bar': '¯',
'\\breve': '˘',
'\\check': 'ˇ',
'\\hat': '^'
};
function xmlEscape(str) {
return str
// .replace(/&/g, '&')
.replace(/"/g, '"')
.replace(/'/g, ''')
.replace(/</g, '<')
.replace(/>/g, '>');
}
function makeID(id, options) {
if (!id || !options.generateID) return '';
// Note: the 'extid' attribute is recognized by SRE as an attribute
// to be passed to SSML as a <mark> tag.
return ' extid="' + id + '"';
}
function scanIdentifier(stream, final, options) {
let result = false;
final = final || stream.atoms.length;
let mathML = '';
let body = '';
let superscript = -1;
let subscript = -1;
const atom = stream.atoms[stream.index];
if (stream.index < final &&
(atom.type === 'mord' || atom.type === 'textord') &&
'0123456789,.'.indexOf(atom.latex) < 0) {
body = atom.toMathML(options);
if (atom.superscript) {
superscript = stream.index;
}
if (atom.subscript) {
subscript = stream.index;
}
stream.index += 1;
}
if (body.length > 0) {
result = true;
// If there are separate atoms for sub/sup, record them
if (isSuperscriptAtom(stream)) {
superscript = stream.index;
stream.index += 1;
}
if (isSubscriptAtom(stream)) {
subscript = stream.index;
stream.index += 1;
}
if (superscript >= 0 && subscript >= 0) {
mathML = '<msubsup>' + body;
mathML += toMathML(stream.atoms[subscript].subscript, 0, 0, options).mathML;
mathML += toMathML(stream.atoms[superscript].superscript, 0, 0, options).mathML;
mathML += '</msubsup>';
} else if (superscript >= 0) {
mathML = '<msup>' + body;
mathML += toMathML(stream.atoms[superscript].superscript, 0, 0, options).mathML;
mathML += '</msup>';
} else if (subscript >= 0) {
mathML = '<msub>' + body;
mathML += toMathML(stream.atoms[subscript].subscript, 0, 0, options).mathML;
mathML += '</msub>';
} else {
mathML = body;
}
if ((stream.lastType === 'mi' ||
stream.lastType === 'mn' ||
stream.lastType === 'fence') &&
!/^<mo>(.*)<\/mo>$/.test(mathML)) {
mathML = '<mo>⁢</mo>' + mathML;
}
if (body.endsWith('>f</mi>') || body.endsWith('>g</mi>')) {
mathML += '<mo> ⁡ </mo>';
stream.lastType = 'applyfunction';
} else {
stream.lastType = /^<mo>(.*)<\/mo>$/.test(mathML) ? 'mo' : 'mi';
}
stream.mathML += mathML;
}
return result;
}
/**
* Return true if the current atom is a standalone superscript atom
* i.e. an atom with no content, except of a superscript.
* Superscripts can be encoded either as an attribute on the last atom
* or as a standalone, empty, atom following the one to which it applies.
* @param {*} stream
*/
function isSuperscriptAtom(stream) {
return stream.index < stream.atoms.length &&
stream.atoms[stream.index].superscript &&
stream.atoms[stream.index].type === 'msubsup'
}
function isSubscriptAtom(stream) {
return stream.index < stream.atoms.length &&
stream.atoms[stream.index].subscript &&
stream.atoms[stream.index].type === 'msubsup'
}
function indexOfSuperscriptInNumber(stream) {
let result = -1;
let i = stream.index;
let done = false;
let found = false;
while (i < stream.atoms.length && !done && !found) {
done = stream.atoms[i].type !== 'mord' ||
'0123456789,.'.indexOf(stream.atoms[i].latex) < 0;
found = !done && stream.atoms[i].superscript;
i++
}
if (found) {
result = i;
}
return result;
}
function parseSubsup(base, stream, options) {
let result = false;
let mathML = '';
let atom = stream.atoms[stream.index - 1];
if (!atom) return false;
if (!atom.superscript && !atom.subscript) {
if (isSuperscriptAtom(stream) || isSubscriptAtom(stream)) {
atom = stream.atoms[stream.index];
stream.index += 1;
}
}
if (!atom) return false;
if (atom.superscript && atom.subscript) {
mathML = '<msubsup>' + base;
mathML += toMathML(atom.subscript, 0, 0, options).mathML;
mathML += toMathML(atom.superscript, 0, 0, options).mathML;
mathML += '</msubsup>';
} else if (atom.superscript) {
mathML = '<msup>' + base;
mathML += toMathML(atom.superscript, 0, 0, options).mathML;
mathML += '</msup>';
} else if (atom.subscript) {
mathML = '<msub>' + base;
mathML += toMathML(atom.subscript, 0, 0, options).mathML;
mathML += '</msub>';
}
if (mathML.length > 0) {
result = true;
stream.mathML += mathML;
stream.lastType = '';
}
return result;
}
function scanNumber(stream, final, options) {
let result = false;
final = final || stream.atoms.length;
const initial = stream.index;
let mathML = '';
let superscript = indexOfSuperscriptInNumber(stream);
if (superscript >= 0 && superscript < final) {
final = superscript;
}
while (stream.index < final &&
stream.atoms[stream.index].type === 'mord' &&
'0123456789,.'.indexOf(stream.atoms[stream.index].latex) >= 0
) {
mathML += stream.atoms[stream.index].latex;
stream.index += 1;
}
if (mathML.length > 0) {
result = true;
mathML = '<mn' + makeID(stream.atoms[initial].id, options) + '>' + mathML + '</mn>';
if (superscript < 0 && isSuperscriptAtom(stream)) {
superscript = stream.index;
stream.index += 1;
}
if (superscript >= 0) {
mathML = '<msup>' + mathML;
mathML += toMathML(stream.atoms[superscript].superscript, 0, 0, options).mathML;
mathML += '</msup>';
}
stream.mathML += mathML;
stream.lastType = 'mn';
}
return result;
}
function scanFence(stream, final, options) {
let result = false;
final = final || stream.atoms.length;
let mathML = '';
let lastType = '';
if (stream.index < final &&
stream.atoms[stream.index].type === 'mopen') {
let found = false;
let depth = 0;
const openIndex = stream.index;
let closeIndex = -1;
let index = openIndex + 1;
while (index < final && !found) {
if (stream.atoms[index].type === 'mopen') {
depth += 1;
} else if (stream.atoms[index].type === 'mclose') {
depth -= 1;
}
if (depth === -1) {
found = true;
closeIndex = index;
}
index += 1;
}
if (found) {
// TODO: could add attribute indicating it's a fence (fence=true)
mathML = '<mrow>';
mathML += toMo(stream.atoms[openIndex], options);
mathML += toMathML(stream.atoms, openIndex + 1, closeIndex, options).mathML;
// TODO: could add attribute indicating it's a fence (fence=true)
mathML += toMo(stream.atoms[closeIndex], options);
mathML += '</mrow>';
if (stream.lastType === 'mi' ||
stream.lastType === 'mn' ||
stream.lastType === 'mfrac' ||
stream.lastType === 'fence') {
mathML = '<mo>⁢</mo>' + mathML;
}
stream.index = closeIndex + 1;
if (parseSubsup(mathML, stream, options)) {
result = true;
stream.lastType = '';
mathML = '';
}
lastType = 'fence';
}
}
if (mathML.length > 0) {
result = true;
stream.mathML += mathML;
stream.lastType = lastType;
}
return result;
}
function scanOperator(stream, final, options) {
let result = false;
final = final || stream.atoms.length;
let mathML = '';
let lastType = '';
const atom = stream.atoms[stream.index];
if (stream.index < final && (
atom.type === 'mbin' || atom.type === 'mrel')) {
mathML += stream.atoms[stream.index].toMathML(options);
stream.index += 1;
lastType = 'mo';
} else if (stream.index < final && atom.type === 'mop') {
// mathML += '<mrow>';
if (atom.limits && (atom.superscript || atom.subscript)) {
// Operator with limits, e.g. \sum
const op = toMo(atom, options);
if (atom.superscript && atom.subscript) {
// Both superscript and subscript
mathML += (atom.limits !== 'nolimits' ? '<munderover>' : '<msubsup>') + op;
mathML += toMathML(atom.subscript, 0, 0, options).mathML;
mathML += toMathML(atom.superscript, 0, 0, options).mathML;
mathML += (atom.limits !== 'nolimits' ? '</munderover>' : '</msubsup>');
} else if (atom.superscript) {
// Superscript only
mathML += (atom.limits !== 'nolimits' ? '<mover>' : '<msup>') + op;
mathML += toMathML(atom.superscript, 0, 0, options).mathML;
mathML += (atom.limits !== 'nolimits' ? '</mover>' : '</msup>');
} else {
// Subscript only
mathML += (atom.limits !== 'nolimits' ? '<munder>' : '<msub>') + op;
mathML += toMathML(atom.subscript, 0, 0, options).mathML;
mathML += (atom.limits !== 'nolimits' ? '</munder>' : '</msub>');
}
lastType = 'mo';
} else {
const op = toMo(stream.atoms[stream.index], options);
mathML += op;
if (!/^<mo>(.*)<\/mo>$/.test(op)) {
mathML += '<mo> ⁡ </mo>';
// mathML += scanArgument(stream);
lastType = 'applyfunction';
} else {
lastType = 'mo';
}
}
// mathML += '</mrow>';
if ((stream.lastType === 'mi' || stream.lastType === 'mn') &&
!/^<mo>(.*)<\/mo>$/.test(mathML)) {
mathML = '<mo>⁢</mo>' + mathML;
}
stream.index += 1;
}
if (mathML.length > 0) {
result = true;
stream.mathML += mathML;
stream.lastType = lastType;
}
return result;
}
/**
* Given an atom or an array of atoms, return their MathML representation as
* a string.
* @return {string}
* @param {string|MathAtom|MathAtom[]} input
* @param {number} initial index of the input to start conversion from
* @param {number} final last index of the input to stop conversion to
* @private
*/
function toMathML(input, initial, final, options) {
const result = {
atoms: input,
index: initial || 0,
mathML: '',
lastType: ''
};
final = final || (input ? input.length : 0);
if (typeof input === 'number' || typeof input === 'boolean') {
result.mathML = input.toString();
} else if (typeof input === 'string') {
result.mathML = input;
} else if (input && typeof input.toMathML === 'function') {
result.mathML = input.toMathML(options);
} else if (Array.isArray(input)) {
let count = 0;
while (result.index < final) {
if (scanNumber(result, final, options) ||
scanIdentifier(result, final, options) ||
scanOperator(result, final, options) ||
scanFence(result, final, options)) {
count += 1;
} else if (result.index < final) {
let mathML = result.atoms[result.index].toMathML(options);
if (result.lastType === 'mn' && mathML.length > 0 &&
result.atoms[result.index].type === 'genfrac') {
// If this is a fraction preceded by a number (e.g. 2 1/2),
// add an "invisible plus" (U+0264) character in front of it
mathML = '<mo>⁤</mo>' + mathML;
}
if (result.atoms[result.index].type === 'genfrac') {
result.lastType = 'mfrac';
} else {
result.lastType = '';
}
if (mathML.length > 0) {
result.mathML += mathML;
count += 1;
}
result.index += 1;
}
}
// If there are more than a single element, wrap them in a mrow tag.
if (count > 1) {
result.mathML = '<mrow>' + result.mathML + '</mrow>';
}
}
return result;
}
function toMo(atom, options) {
let result = '';
if (atom) {
const body = toString(atom.body);
if (body) {
result = '<mo' + makeID(atom.id, options) + '>' + body + '</mo>';
}
}
return result;
}
function toString(atoms) {
if (!atoms) return '';
if (typeof atoms === 'string') return xmlEscape(atoms);
if (!Array.isArray(atoms) && typeof atoms.body === 'string') {
return xmlEscape(atoms.body);
}
let result = '';
for (const atom of atoms) {
if (typeof atom.body === 'string') {
result += atom.body;
}
}
return xmlEscape(result);
}
/**
* Return a MathML fragment representation of a single atom
*
* @return {string}
*/
MathAtom.MathAtom.prototype.toMathML = function(options) {
const SPECIAL_IDENTIFIERS = {
'\\exponentialE': 'ⅇ',
'\\imaginaryI': 'ⅈ',
'\\differentialD': 'ⅆ',
'\\capitalDifferentialD': 'ⅅ',
'\\alpha': 'α',
'\\pi': 'π',
'\\infty' : '∞',
'\\forall' : '∀',
'\\nexists': '∄',
'\\exists': '∃',
'\\hbar': '\u210f',
'\\cdotp': '\u22c5',
'\\ldots': '\u2026',
'\\cdots': '\u22ef',
'\\ddots': '\u22f1',
'\\vdots': '\u22ee',
'\\ldotp': '\u002e',
// TODO: include all the 'textord' that are identifiers, not operators.
};
const MATH_VARIANTS = {
'mathbb': 'double-struck',
'mathbf': 'bold',
'mathcal': 'script',
'mathfrak': 'fraktur',
'mathscr': 'script',
'mathsf': 'sans-serif',
'mathtt': 'monospace'
};
const SPACING = {
'\\!': -3 / 18,
'\\ ': 6 / 18,
'\\,': 3 / 18,
'\\:': 4 / 18,
'\\;': 5 / 18,
'\\enspace': .5,
'\\quad': 1,
'\\qquad': 2,
'\\enskip': .5,
};
let result = '';
let sep = '';
let col, row, i;
let underscript, overscript, body;
let variant = MATH_VARIANTS[this.fontFamily || this.font] || '';
if (variant) {
variant = ' mathvariant="' + variant + '"';
}
const command = this.latex ? this.latex.trim() : null;
let m;
switch(this.type) {
case 'group':
case 'root':
result = toMathML(this.body, 0, 0, options).mathML;
break;
case 'array':
if ((this.lFence && this.lFence !== '.') ||
(this.rFence && this.rFence !== '.')) {
result += '<mrow>';
if ((this.lFence && this.lFence !== '.')) {
result += '<mo>' + (SPECIAL_OPERATORS[this.lFence] || this.lFence) + '</mo>';
}
}
result += '<mtable';
if (this.colFormat) {
result += ' columnalign="';
for (i = 0; i < this.colFormat.length; i++) {
if (this.colFormat[i].align) {
result += {l:'left', c:'center', r:'right'}[this.colFormat[i].align] + ' ';
}
}
result += '"';
}
result += '>';
for (row = 0; row < this.array.length; row++) {
result += '<mtr>';
for (col = 0; col < this.array[row].length; col++) {
result += '<mtd>' + toMathML(this.array[row][col], 0, 0, options).mathML + '</mtd>';
}
result += '</mtr>';
}
result += '</mtable>';
if ((this.lFence && this.lFence !== '.') ||
(this.rFence && this.rFence !== '.')) {
if ((this.rFence && this.rFence !== '.')) {
result += '<mo>' + (SPECIAL_OPERATORS[this.lFence] || this.rFence) + '</mo>';
}
result += '</mrow>';
}
break;
case 'genfrac':
if (this.leftDelim || this.rightDelim) {
result += '<mrow>';
}
if (this.leftDelim && this.leftDelim !== '.') {
result += '<mo' + makeID(this.id, options) + '>' + (SPECIAL_OPERATORS[this.leftDelim] || this.leftDelim) + '</mo>';
}
if (this.hasBarLine) {
result += '<mfrac>';
result += toMathML(this.numer, 0, 0, options).mathML || '<mi> </mi>';
result += toMathML(this.denom, 0, 0, options).mathML || '<mi> </mi>';
result += '</mfrac>';
} else {
// No bar line, i.e. \choose, etc...
result += '<mtable' + makeID(this.id, options) + '>';
result += '<mtr>' + toMathML(this.numer, 0, 0, options).mathML + '</mtr>';
result += '<mtr>' + toMathML(this.denom, 0, 0, options).mathML + '</mtr>';
result += '</mtable>';
}
if (this.rightDelim && this.rightDelim !== '.') {
result += '<mo' + makeID(this.id, options) + '>' + (SPECIAL_OPERATORS[this.rightDelim] || this.rightDelim) + '</mo>';
}
if (this.leftDelim || this.rightDelim) {
result += '</mrow>';
}
break;
case 'surd':
if (this.index) {
result += '<mroot' + makeID(this.id, options) + '>';
result += toMathML(this.body, 0, 0, options).mathML;
result += toMathML(this.index, 0, 0, options).mathML;
result += '</mroot>';
} else {
result += '<msqrt' + makeID(this.id, options) + '>';
result += toMathML(this.body, 0, 0, options).mathML;
result += '</msqrt>';
}
break;
case 'leftright':
// TODO: could add fence=true attribute
result = '<mrow>';
if (this.leftDelim && this.leftDelim !== '.') {
result += '<mo' + makeID(this.id, options) + '>' + (SPECIAL_OPERATORS[this.leftDelim] || this.leftDelim) + '</mo>';
}
if (this.body) result += toMathML(this.body, 0, 0, options).mathML;
if (this.rightDelim && this.rightDelim !== '.') {
result += '<mo' + makeID(this.id, options) + '>' + (SPECIAL_OPERATORS[this.rightDelim] || this.rightDelim) + '</mo>';
}
result += '</mrow>';
break;
case 'sizeddelim':
case 'delim':
result += '<mo separator="true"' + makeID(this.id, options) + '>' + (SPECIAL_OPERATORS[this.delim] || this.delim) + '</mo>';
break;
case 'font':
if (command === '\\text' || command === '\\textrm' ||
command === '\\textsf' || command === '\\texttt' ||
command === '\\textnormal' || command === '\\textbf' ||
command === '\\textit') {
result += '<mtext' + variant + makeID(this.id, options) + '>';
// Replace first and last space in text with a to ensure they
// are actually displayed (content surrounded by a tag gets trimmed)
// TODO: alternative: use <mspace>
result += toString(this.body).
replace(/^\s/, ' ').
replace(/\s$/, ' ');
result += '</mtext>';
} else {
result += '<mi' + variant + '>' + toString(this.body) + '</mi>';
}
break;
case 'accent':
result += '<mover accent="true"' + makeID(this.id, options) + '>';
result += toMathML(this.body, 0, 0, options).mathML;
result += '<mo>' + (SPECIAL_OPERATORS[command] || this.accent) + '</mo>';
result += '</mover>'
break;
case 'line':
case 'overlap':
break;
case 'overunder':
overscript = this.overscript;
underscript = this.underscript;
if (overscript && underscript) {
body = this.body;
} else if (overscript) {
body = this.body;
if (this.body[0] && this.body[0].underscript) {
underscript = this.body[0].underscript;
body = this.body[0].body;
} else if (this.body[0] && this.body[0].type === 'first' && this.body[1] && this.body[1].underscript) {
underscript = this.body[1].underscript;
body = this.body[1].body;
}
} else if (underscript) {
body = this.body;
if (this.body[0] && this.body[0].overscript) {
overscript = this.body[0].overscript;
body = this.body[0].body;
} else if (this.body[0] && this.body[0].type === 'first' && this.body[1] && this.body[1].overscript) {
overscript = this.body[1].overscript;
body = this.body[1].body;
}
}
if (overscript && underscript) {
result += '<munderover' + variant + makeID(this.id, options) + '>' + toMathML(body, 0, 0, options).mathML;
result += toMathML(underscript, 0, 0, options).mathML;
result += toMathML(overscript, 0, 0, options).mathML;
result += '</munderover>';
} else if (overscript) {
result += '<mover' + variant + makeID(this.id, options) + '>' + toMathML(body, options).mathML;
result += toMathML(overscript, 0, 0, options).mathML;
result += '</mover>';
} else if (underscript) {
result += '<munder' + variant + makeID(this.id, options) + '>' + toMathML(body, options).mathML;
result += toMathML(underscript, 0, 0, options).mathML;
result += '</munder>';
}
break;
case 'mord':
result = SPECIAL_IDENTIFIERS[command] || command || (typeof this.body === 'string' ? this.body : '');
m = command ? command.match(/[{]?\\char"([0-9abcdefABCDEF]*)[}]?/) : null;
if (m) {
// It's a \char command
result = '&#x' + m[1] + ';'
} else if (result.length > 0 && result.charAt(0) === '\\') {
// This is an identifier with no special handling. Use the
// Unicode value
if (typeof this.body === 'string' && this.body.charCodeAt(0) > 255) {
result = '&#x' + ('000000' +
this.body.charCodeAt(0).toString(16)).substr(-4) + ';';
} else if (typeof this.body === 'string') {
result = this.body.charAt(0);
} else {
result = this.latex;
}
}
result = '<mi' + variant + makeID(this.id, options) + '>' + xmlEscape(result) + '</mi>';
break;
case 'mbin':
case 'mrel':
case 'textord':
case 'minner':
if (command && SPECIAL_IDENTIFIERS[command]) {
// Some 'textord' are actually identifiers. Check them here.
result = '<mi' + makeID(this.id, options) + '>' + SPECIAL_IDENTIFIERS[command] + '</mi>';
} else if (command && SPECIAL_OPERATORS[command]) {
result = '<mo' + makeID(this.id, options) + '>' + SPECIAL_OPERATORS[command] + '</mo>';
} else {
result = toMo(this, options);
}
break;
case 'mpunct':
result = '<mo separator="true"' + makeID(this.id, options) + '>' + (SPECIAL_OPERATORS[command] || command) + '</mo>';
break;
case 'mop':
if (this.body !== '\u200b') {
// Not ZERO-WIDTH
result = '<mo' + makeID(this.id, options) + '>';
if (command === '\\operatorname') {
result += this.body;
} else {
result += command || this.body;
}
result += '</mo>';
}
break;
case 'color':
if (this.textcolor) {
result += '<mstyle color="' + Color.stringToColor(this.textcolor) + '"';
result += makeID(this.id, options) + '>';
result += toMathML(this.body, 0, 0, options).mathML;
result += '</mstyle>';
}
break;
case 'mathstyle':
// TODO: mathstyle is a switch. Need to figure out its scope to properly wrap it around a <mstyle> tag
// if (this.mathstyle === 'displaystyle') {
// result += '<mstyle displaystyle="true">';
// result += '</mstyle>';
// } else {
// result += '<mstyle displaystyle="false">';
// result += '</mstyle>';
// };
break;
case 'box':
result = '<menclose notation="box"';
if (this.backgroundcolor) {
result += ' mathbackground="' + Color.stringToColor(this.backgroundcolor) + '"';
}
result += makeID(this.id, options) + '>' + toMathML(this.body, 0, 0, options).mathML + '</menclose>';
break;
case 'spacing':
result += '<mspace width="' + (SPACING[command] || 0) + 'em"/>';
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 += makeID(this.id, options) + '">' + toMathML(this.body, 0, 0, options).mathML + '</menclose>';
break;
case 'sizing':
break;
case 'space':
result += ' '
break;
}
return result;
}
MathAtom.toMathML = function(atoms, options) {
return toMathML(atoms, 0, 0, options).mathML;
}
// Export the public interface for this module
return {
}
})