// @flow /** * This file converts a parse tree into a corresponding MathML tree. The main * entry point is the `buildMathML` function, which takes a parse tree from the * parser. */ import buildCommon from "./buildCommon"; import {getCharacterMetrics} from "./fontMetrics"; import mathMLTree from "./mathMLTree"; import ParseError from "./ParseError"; import symbols, {ligatures} from "./symbols"; import utils from "./utils"; import {_mathmlGroupBuilders as groupBuilders} from "./defineFunction"; import {MathNode, TextNode} from "./mathMLTree"; import type Options from "./Options"; import type {AnyParseNode, SymbolParseNode} from "./parseNode"; import type {DomSpan} from "./domTree"; import type {MathDomNode} from "./mathMLTree"; import type {FontVariant, Mode} from "./types"; /** * Takes a symbol and converts it into a MathML text node after performing * optional replacement from symbols.js. */ export const makeText = function( text: string, mode: Mode, options?: Options, ): TextNode { if (symbols[mode][text] && symbols[mode][text].replace && text.charCodeAt(0) !== 0xD835 && !(ligatures.hasOwnProperty(text) && options && ((options.fontFamily && options.fontFamily.slice(4, 6) === "tt") || (options.font && options.font.slice(4, 6) === "tt")))) { text = symbols[mode][text].replace; } return new mathMLTree.TextNode(text); }; /** * Wrap the given array of nodes in an node if needed, i.e., * unless the array has length 1. Always returns a single node. */ export const makeRow = function(body: $ReadOnlyArray): MathDomNode { if (body.length === 1) { return body[0]; } else { return new mathMLTree.MathNode("mrow", body); } }; /** * Returns the math variant as a string or null if none is required. */ export const getVariant = function( group: SymbolParseNode, options: Options, ): ?FontVariant { // Handle \text... font specifiers as best we can. // MathML has a limited list of allowable mathvariant specifiers; see // https://www.w3.org/TR/MathML3/chapter3.html#presm.commatt if (options.fontFamily === "texttt") { return "monospace"; } else if (options.fontFamily === "textsf") { if (options.fontShape === "textit" && options.fontWeight === "textbf") { return "sans-serif-bold-italic"; } else if (options.fontShape === "textit") { return "sans-serif-italic"; } else if (options.fontWeight === "textbf") { return "bold-sans-serif"; } else { return "sans-serif"; } } else if (options.fontShape === "textit" && options.fontWeight === "textbf") { return "bold-italic"; } else if (options.fontShape === "textit") { return "italic"; } else if (options.fontWeight === "textbf") { return "bold"; } const font = options.font; if (!font || font === "mathnormal") { return null; } const mode = group.mode; if (font === "mathit") { return "italic"; } else if (font === "boldsymbol") { return group.type === "textord" ? "bold" : "bold-italic"; } else if (font === "mathbf") { return "bold"; } else if (font === "mathbb") { return "double-struck"; } else if (font === "mathsfit") { return "sans-serif-italic"; } else if (font === "mathfrak") { return "fraktur"; } else if (font === "mathscr" || font === "mathcal") { // MathML makes no distinction between script and calligraphic return "script"; } else if (font === "mathsf") { return "sans-serif"; } else if (font === "mathtt") { return "monospace"; } let text = group.text; if (utils.contains(["\\imath", "\\jmath"], text)) { return null; } if (symbols[mode][text] && symbols[mode][text].replace) { text = symbols[mode][text].replace; } const fontName = buildCommon.fontMap[font].fontName; if (getCharacterMetrics(text, fontName, mode)) { return buildCommon.fontMap[font].variant; } return null; }; /** * Check for . which is how a dot renders in MathML, * or , * which is how a braced comma {,} renders in MathML */ function isNumberPunctuation(group: ?MathNode): boolean { if (!group) { return false; } if (group.type === 'mi' && group.children.length === 1) { const child = group.children[0]; return child instanceof TextNode && child.text === '.'; } else if (group.type === 'mo' && group.children.length === 1 && group.getAttribute('separator') === 'true' && group.getAttribute('lspace') === '0em' && group.getAttribute('rspace') === '0em' ) { const child = group.children[0]; return child instanceof TextNode && child.text === ','; } else { return false; } } /** * Takes a list of nodes, builds them, and returns a list of the generated * MathML nodes. Also combine consecutive outputs into a single * tag. */ export const buildExpression = function( expression: AnyParseNode[], options: Options, isOrdgroup?: boolean, ): MathNode[] { if (expression.length === 1) { const group = buildGroup(expression[0], options); if (isOrdgroup && group instanceof MathNode && group.type === "mo") { // When TeX writers want to suppress spacing on an operator, // they often put the operator by itself inside braces. group.setAttribute("lspace", "0em"); group.setAttribute("rspace", "0em"); } return [group]; } const groups = []; let lastGroup; for (let i = 0; i < expression.length; i++) { const group = buildGroup(expression[i], options); if (group instanceof MathNode && lastGroup instanceof MathNode) { // Concatenate adjacent s if (group.type === 'mtext' && lastGroup.type === 'mtext' && group.getAttribute('mathvariant') === lastGroup.getAttribute('mathvariant')) { lastGroup.children.push(...group.children); continue; // Concatenate adjacent s } else if (group.type === 'mn' && lastGroup.type === 'mn') { lastGroup.children.push(...group.children); continue; // Concatenate ... followed by . } else if (isNumberPunctuation(group) && lastGroup.type === 'mn') { lastGroup.children.push(...group.children); continue; // Concatenate . followed by ... } else if (group.type === 'mn' && isNumberPunctuation(lastGroup)) { group.children = [...lastGroup.children, ...group.children]; groups.pop(); // Put preceding ... or . inside base of // ...base......exponent... (or ) } else if ((group.type === 'msup' || group.type === 'msub') && group.children.length >= 1 && (lastGroup.type === 'mn' || isNumberPunctuation(lastGroup)) ) { const base = group.children[0]; if (base instanceof MathNode && base.type === 'mn') { base.children = [...lastGroup.children, ...base.children]; groups.pop(); } // \not } else if (lastGroup.type === 'mi' && lastGroup.children.length === 1) { const lastChild = lastGroup.children[0]; if (lastChild instanceof TextNode && lastChild.text === '\u0338' && (group.type === 'mo' || group.type === 'mi' || group.type === 'mn')) { const child = group.children[0]; if (child instanceof TextNode && child.text.length > 0) { // Overlay with combining character long solidus child.text = child.text.slice(0, 1) + "\u0338" + child.text.slice(1); groups.pop(); } } } } groups.push(group); lastGroup = group; } return groups; }; /** * Equivalent to buildExpression, but wraps the elements in an * if there's more than one. Returns a single node instead of an array. */ export const buildExpressionRow = function( expression: AnyParseNode[], options: Options, isOrdgroup?: boolean, ): MathDomNode { return makeRow(buildExpression(expression, options, isOrdgroup)); }; /** * Takes a group from the parser and calls the appropriate groupBuilders function * on it to produce a MathML node. */ export const buildGroup = function( group: ?AnyParseNode, options: Options, ): MathNode { if (!group) { return new mathMLTree.MathNode("mrow"); } if (groupBuilders[group.type]) { // Call the groupBuilders function // $FlowFixMe const result: MathDomNode = groupBuilders[group.type](group, options); // $FlowFixMe return result; } else { throw new ParseError( "Got group of unknown type: '" + group.type + "'"); } }; /** * Takes a full parse tree and settings and builds a MathML representation of * it. In particular, we put the elements from building the parse tree into a * tag so we can also include that TeX source as an annotation. * * Note that we actually return a domTree element with a `` inside it so * we can do appropriate styling. */ export default function buildMathML( tree: AnyParseNode[], texExpression: string, options: Options, isDisplayMode: boolean, forMathmlOnly: boolean, ): DomSpan { const expression = buildExpression(tree, options); // TODO: Make a pass thru the MathML similar to buildHTML.traverseNonSpaceNodes // and add spacing nodes. This is necessary only adjacent to math operators // like \sin or \lim or to subsup elements that contain math operators. // MathML takes care of the other spacing issues. // Wrap up the expression in an mrow so it is presented in the semantics // tag correctly, unless it's a single or . let wrapper; if (expression.length === 1 && expression[0] instanceof MathNode && utils.contains(["mrow", "mtable"], expression[0].type)) { wrapper = expression[0]; } else { wrapper = new mathMLTree.MathNode("mrow", expression); } // Build a TeX annotation of the source const annotation = new mathMLTree.MathNode( "annotation", [new mathMLTree.TextNode(texExpression)]); annotation.setAttribute("encoding", "application/x-tex"); const semantics = new mathMLTree.MathNode( "semantics", [wrapper, annotation]); const math = new mathMLTree.MathNode("math", [semantics]); math.setAttribute("xmlns", "http://www.w3.org/1998/Math/MathML"); if (isDisplayMode) { math.setAttribute("display", "block"); } // You can't style nodes, so we wrap the node in a span. // NOTE: The span class is not typed to have nodes as children, and // we don't want to make the children type more generic since the children // of span are expected to have more fields in `buildHtml` contexts. const wrapperClass = forMathmlOnly ? "katex" : "katex-mathml"; // $FlowFixMe return buildCommon.makeSpan([wrapperClass], [math]); }