import TextCache from './textcache';
import MathML from './mathml';
import ServiceProvider from './serviceprovider';
import Constants from './constants';
import Util from './util';
/**
* @classdesc
* This class represents a LaTeX parser. Manages the services which allows to convert
* LaTeX into MathML and MathML into LaTeX.
*/
export default class Latex {
/**
* Static property.
* Return latex cache.
* @private
* @type {Cache}
*/
static get cache() {
return Latex._cache;
}
/**
* Static property setter.
* Set latex cache.
* @param {Cache} value - The property value.
* @ignore
*/
static set cache(value) {
Latex._cache = value;
}
/**
* Converts MathML to LaTeX by calling mathml2latex service. For text services
* we call a text service with the param mathml2latex.
* @param {String} mathml - MathML String.
* @return {String} LaTeX string generated by the MathML argument.
*/
static getLatexFromMathML(mathml) {
const mathmlWithoutSemantics = MathML.removeSemantics(mathml);
/**
* @type {TextCache}
*/
const { cache } = Latex;
const data = {
service: 'mathml2latex',
mml: mathmlWithoutSemantics,
};
const jsonResponse = JSON.parse(ServiceProvider.getService('service', data));
// TODO: Error handling.
let latex = '';
if (jsonResponse.status === 'ok') {
latex = jsonResponse.result.text;
const latexHtmlEntitiesEncoded = Util.htmlEntities(latex);
// Inserting LaTeX semantics.
const mathmlWithSemantics = MathML.addAnnotation(mathml, latexHtmlEntitiesEncoded, 'LaTeX');
cache.populate(latex, mathmlWithSemantics);
}
return latex;
}
/**
* Converts LaTeX to MathML by calling latex2mathml service. For text services
* we call a text service with the param latex2mathml.
* @param {String} latex - String containing a LaTeX formula.
* @param {Boolean} includeLatexOnSemantics
* - If true LaTeX would me included into MathML semantics.
* @return {String} MathML string generated by the LaTeX argument.
*/
static getMathMLFromLatex(latex, includeLatexOnSemantics) {
/**
* @type {TextCache}
*/
const latexCache = Latex.cache;
if (Latex.cache.get(latex)) {
return Latex.cache.get(latex);
}
const data = {
service: 'latex2mathml',
latex,
};
if (includeLatexOnSemantics) {
data.saveLatex = '';
}
const jsonResponse = JSON.parse(ServiceProvider.getService('service', data));
let output;
if (jsonResponse.status === 'ok') {
let mathml = jsonResponse.result.text;
mathml = mathml.split('\r').join('').split('\n').join(' ');
// Populate LatexCache.
if (mathml.indexOf('semantics') === -1 && mathml.indexOf('annotation') === -1) {
mathml = MathML.addAnnotation(mathml, latex, 'LaTeX');
output = mathml;
} else {
output = mathml;
}
if (!latexCache.get(latex)) {
latexCache.populate(latex, mathml);
}
} else {
output = `$$${latex}$$`;
}
return output;
}
/**
* Converts all occurrences of MathML code to LaTeX.
* The MathML code should containing <annotation encoding="LaTeX"/> to be converted.
* @param {String} content - A string containing MathML valid code.
* @param {Object} characters - An object containing special characters.
* @return {String} A string containing all MathML annotated occurrences
* replaced by the corresponding LaTeX code.
*/
static parseMathmlToLatex(content, characters) {
let output = '';
const mathTagBegin = `${characters.tagOpener}math`;
const mathTagEnd = `${characters.tagOpener}/math${characters.tagCloser}`;
const openTarget = `${characters.tagOpener}annotation encoding=${characters.doubleQuote}LaTeX${characters.doubleQuote}${characters.tagCloser}`;
const closeTarget = `${characters.tagOpener}/annotation${characters.tagCloser}`;
let start = content.indexOf(mathTagBegin);
let end = 0;
let mathml;
let startAnnotation;
let closeAnnotation;
while (start !== -1) {
output += content.substring(end, start);
end = content.indexOf(mathTagEnd, start);
if (end === -1) {
end = content.length - 1;
} else {
end += mathTagEnd.length;
}
mathml = content.substring(start, end);
startAnnotation = mathml.indexOf(openTarget);
if (startAnnotation !== -1) {
startAnnotation += openTarget.length;
closeAnnotation = mathml.indexOf(closeTarget);
let latex = mathml.substring(startAnnotation, closeAnnotation);
if (characters === Constants.safeXmlCharacters) {
latex = MathML.safeXmlDecode(latex);
}
output += `$$${latex}$$`;
// Populate latex into cache.
Latex.cache.populate(latex, mathml);
} else {
output += mathml;
}
start = content.indexOf(mathTagBegin, end);
}
output += content.substring(end, content.length);
return output;
}
/**
* Extracts the latex of a determined position in a text.
* @param {Node} textNode - textNode to extract the LaTeX
* @param {Number} caretPosition - Starting position to find LaTeX.
* @param {Object} latexTags - Optional parameter representing tags between latex is inserted.
* It has the 'open' attribute for the open tag and the 'close' attribute for the close tag.
* "$$" by default.
* @return {Object} An object with 3 keys: 'latex', 'start' and 'end'. Null if latex is not found.
* @static
*/
static getLatexFromTextNode(textNode, caretPosition, latexTags) {
// TODO: Set LaTeX Tags as Core variable. Fix the call to this function (third argument).
// Tags used for LaTeX formulas.
const defaultLatexTags = {
open: '$$',
close: '$$',
};
// latexTags is an optional parameter. If is not set, use default latexTags.
if (typeof latexTags === 'undefined' || latexTags == null) {
latexTags = defaultLatexTags;
}
// Looking for the first textNode.
let startNode = textNode;
while (startNode.previousSibling && startNode.previousSibling.nodeType === 3) { // TEXT_NODE.
startNode = startNode.previousSibling;
}
/**
* Returns the next latex position and node from a specific node and position.
* @param {Node} currentNode - Node where searching latex.
* @param {Number} currentPosition - Current position inside the currentNode.
* @param {Object} latexTagsToUse - Tags used at latex beginning and latex final.
* "$$" by default.
* @param {Boolean} tag - Tag containing the current search.
* @returns {Object} Object containing the current node and the position.
*/
function getNextLatexPosition(currentNode, currentPosition, tag) {
let position = currentNode.nodeValue.indexOf(tag, currentPosition);
while (position === -1) {
currentNode = currentNode.nextSibling;
if (!currentNode) { // TEXT_NODE.
return null; // Not found.
}
position = currentNode.nodeValue ? currentNode.nodeValue.indexOf(latexTags.close) : -1;
}
return {
node: currentNode,
position,
};
}
/**
* Determines if a node is previous, or not, to a second one.
* @param {Node} node - Start node.
* @param {Number} position - Start node position.
* @param {Node} endNode - End node.
* @param {Number} endPosition - End node position.
* @returns {Boolean} True if the starting node is previous thant the en node. false otherwise.
*/
function isPrevious(node, position, endNode, endPosition) {
if (node === endNode) {
return (position <= endPosition);
}
while (node && node !== endNode) {
node = node.nextSibling;
}
return (node === endNode);
}
let start;
let end = {
node: startNode,
position: 0,
};
// Is supposed that open and close tags has the same length.
const tagLength = latexTags.open.length;
do {
start = getNextLatexPosition(end.node, end.position, latexTags.open);
if (start == null || isPrevious(textNode, caretPosition, start.node, start.position)) {
return null;
}
end = getNextLatexPosition(start.node, start.position + tagLength, latexTags.close);
if (end == null) {
return null;
}
end.position += tagLength;
} while (isPrevious(end.node, end.position, textNode, caretPosition));
// Isolating latex.
let latex;
if (start.node === end.node) {
latex = start.node.nodeValue.substring(start.position + tagLength, end.position - tagLength);
} else {
const index = start.position + tagLength;
latex = start.node.nodeValue.substring(index, start.node.nodeValue.length);
let currentNode = start.node;
do {
currentNode = currentNode.nextSibling;
if (currentNode === end.node) {
latex += end.node.nodeValue.substring(0, end.position - tagLength);
} else {
latex += currentNode.nodeValue ? currentNode.nodeValue : '';
}
} while (currentNode !== end.node);
}
return {
latex,
startNode: start.node,
startPosition: start.position,
endNode: end.node,
endPosition: end.position,
};
}
}
/**
* Text cache. Stores all processed LaTeX strings and it's correspondent MathML string.
* @type {Cache}
* @static
*/
Latex._cache = new TextCache();