/** * This file converts a parse tree into a cooresponding MathML tree. The main * entry point is the `buildMathML` function, which takes a parse tree from the * parser. */ var buildCommon = require("./buildCommon"); var fontMetrics = require("./fontMetrics"); var mathMLTree = require("./mathMLTree"); var ParseError = require("./ParseError"); var symbols = require("./symbols"); var utils = require("./utils"); var makeSpan = buildCommon.makeSpan; var fontMap = buildCommon.fontMap; /** * Takes a symbol and converts it into a MathML text node after performing * optional replacement from symbols.js. */ var makeText = function(text, mode) { if (symbols[mode][text] && symbols[mode][text].replace) { text = symbols[mode][text].replace; } return new mathMLTree.TextNode(text); }; /** * Returns the math variant as a string or null if none is required. */ var getVariant = function(group, options) { var font = options.font; if (!font) { return null; } var mode = group.mode; if (font === "mathit") { return "italic"; } var value = group.value; if (utils.contains(["\\imath", "\\jmath"], value)) { return null; } if (symbols[mode][value] && symbols[mode][value].replace) { value = symbols[mode][value].replace; } var fontName = fontMap[font].fontName; if (fontMetrics.getCharacterMetrics(value, fontName)) { return fontMap[options.font].variant; } return null; }; /** * Functions for handling the different types of groups found in the parse * tree. Each function should take a parse group and return a MathML node. */ var groupTypes = {}; groupTypes.mathord = function(group, options) { var node = new mathMLTree.MathNode( "mi", [makeText(group.value, group.mode)]); var variant = getVariant(group, options); if (variant) { node.setAttribute("mathvariant", variant); } return node; }; groupTypes.textord = function(group, options) { var text = makeText(group.value, group.mode); var variant = getVariant(group, options) || "normal"; var node; if (/[0-9]/.test(group.value)) { // TODO(kevinb) merge adjacent nodes // do it as a post processing step node = new mathMLTree.MathNode("mn", [text]); if (options.font) { node.setAttribute("mathvariant", variant); } } else { node = new mathMLTree.MathNode("mi", [text]); node.setAttribute("mathvariant", variant); } return node; }; groupTypes.bin = function(group) { var node = new mathMLTree.MathNode( "mo", [makeText(group.value, group.mode)]); return node; }; groupTypes.rel = function(group) { var node = new mathMLTree.MathNode( "mo", [makeText(group.value, group.mode)]); return node; }; groupTypes.open = function(group) { var node = new mathMLTree.MathNode( "mo", [makeText(group.value, group.mode)]); return node; }; groupTypes.close = function(group) { var node = new mathMLTree.MathNode( "mo", [makeText(group.value, group.mode)]); return node; }; groupTypes.inner = function(group) { var node = new mathMLTree.MathNode( "mo", [makeText(group.value, group.mode)]); return node; }; groupTypes.punct = function(group) { var node = new mathMLTree.MathNode( "mo", [makeText(group.value, group.mode)]); node.setAttribute("separator", "true"); return node; }; groupTypes.ordgroup = function(group, options) { var inner = buildExpression(group.value, options); var node = new mathMLTree.MathNode("mrow", inner); return node; }; groupTypes.text = function(group, options) { var inner = buildExpression(group.value.body, options); var node = new mathMLTree.MathNode("mtext", inner); return node; }; groupTypes.color = function(group, options) { var inner = buildExpression(group.value.value, options); var node = new mathMLTree.MathNode("mstyle", inner); node.setAttribute("mathcolor", group.value.color); return node; }; groupTypes.supsub = function(group, options) { var children = [buildGroup(group.value.base, options)]; if (group.value.sub) { children.push(buildGroup(group.value.sub, options)); } if (group.value.sup) { children.push(buildGroup(group.value.sup, options)); } var nodeType; if (!group.value.sub) { nodeType = "msup"; } else if (!group.value.sup) { nodeType = "msub"; } else { nodeType = "msubsup"; } var node = new mathMLTree.MathNode(nodeType, children); return node; }; groupTypes.genfrac = function(group, options) { var node = new mathMLTree.MathNode( "mfrac", [buildGroup(group.value.numer, options), buildGroup(group.value.denom, options)]); if (!group.value.hasBarLine) { node.setAttribute("linethickness", "0px"); } if (group.value.leftDelim != null || group.value.rightDelim != null) { var withDelims = []; if (group.value.leftDelim != null) { var leftOp = new mathMLTree.MathNode( "mo", [new mathMLTree.TextNode(group.value.leftDelim)]); leftOp.setAttribute("fence", "true"); withDelims.push(leftOp); } withDelims.push(node); if (group.value.rightDelim != null) { var rightOp = new mathMLTree.MathNode( "mo", [new mathMLTree.TextNode(group.value.rightDelim)]); rightOp.setAttribute("fence", "true"); withDelims.push(rightOp); } var outerNode = new mathMLTree.MathNode("mrow", withDelims); return outerNode; } return node; }; groupTypes.array = function(group, options) { return new mathMLTree.MathNode( "mtable", group.value.body.map(function(row) { return new mathMLTree.MathNode( "mtr", row.map(function(cell) { return new mathMLTree.MathNode( "mtd", [buildGroup(cell, options)]); })); })); }; groupTypes.sqrt = function(group, options) { var node; if (group.value.index) { node = new mathMLTree.MathNode( "mroot", [ buildGroup(group.value.body, options), buildGroup(group.value.index, options), ]); } else { node = new mathMLTree.MathNode( "msqrt", [buildGroup(group.value.body, options)]); } return node; }; groupTypes.leftright = function(group, options) { var inner = buildExpression(group.value.body, options); if (group.value.left !== ".") { var leftNode = new mathMLTree.MathNode( "mo", [makeText(group.value.left, group.mode)]); leftNode.setAttribute("fence", "true"); inner.unshift(leftNode); } if (group.value.right !== ".") { var rightNode = new mathMLTree.MathNode( "mo", [makeText(group.value.right, group.mode)]); rightNode.setAttribute("fence", "true"); inner.push(rightNode); } var outerNode = new mathMLTree.MathNode("mrow", inner); return outerNode; }; groupTypes.accent = function(group, options) { var accentNode = new mathMLTree.MathNode( "mo", [makeText(group.value.accent, group.mode)]); var node = new mathMLTree.MathNode( "mover", [buildGroup(group.value.base, options), accentNode]); node.setAttribute("accent", "true"); return node; }; groupTypes.spacing = function(group) { var node; if (group.value === "\\ " || group.value === "\\space" || group.value === " " || group.value === "~") { node = new mathMLTree.MathNode( "mtext", [new mathMLTree.TextNode("\u00a0")]); } else { node = new mathMLTree.MathNode("mspace"); node.setAttribute( "width", buildCommon.spacingFunctions[group.value].size); } return node; }; groupTypes.op = function(group) { var node; // TODO(emily): handle big operators using the `largeop` attribute if (group.value.symbol) { // This is a symbol. Just add the symbol. node = new mathMLTree.MathNode( "mo", [makeText(group.value.body, group.mode)]); } else { // This is a text operator. Add all of the characters from the // operator's name. // TODO(emily): Add a space in the middle of some of these // operators, like \limsup. node = new mathMLTree.MathNode( "mi", [new mathMLTree.TextNode(group.value.body.slice(1))]); } return node; }; groupTypes.katex = function(group) { var node = new mathMLTree.MathNode( "mtext", [new mathMLTree.TextNode("KaTeX")]); return node; }; groupTypes.font = function(group, options) { var font = group.value.font; return buildGroup(group.value.body, options.withFont(font)); }; groupTypes.delimsizing = function(group) { var children = []; if (group.value.value !== ".") { children.push(makeText(group.value.value, group.mode)); } var node = new mathMLTree.MathNode("mo", children); if (group.value.delimType === "open" || group.value.delimType === "close") { // Only some of the delimsizing functions act as fences, and they // return "open" or "close" delimTypes. node.setAttribute("fence", "true"); } else { // Explicitly disable fencing if it's not a fence, to override the // defaults. node.setAttribute("fence", "false"); } return node; }; groupTypes.styling = function(group, options) { var inner = buildExpression(group.value.value, options); var node = new mathMLTree.MathNode("mstyle", inner); var styleAttributes = { "display": ["0", "true"], "text": ["0", "false"], "script": ["1", "false"], "scriptscript": ["2", "false"], }; var attr = styleAttributes[group.value.style]; node.setAttribute("scriptlevel", attr[0]); node.setAttribute("displaystyle", attr[1]); return node; }; groupTypes.sizing = function(group, options) { var inner = buildExpression(group.value.value, options); var node = new mathMLTree.MathNode("mstyle", inner); // TODO(emily): This doesn't produce the correct size for nested size // changes, because we don't keep state of what style we're currently // in, so we can't reset the size to normal before changing it. Now // that we're passing an options parameter we should be able to fix // this. node.setAttribute( "mathsize", buildCommon.sizingMultiplier[group.value.size] + "em"); return node; }; groupTypes.overline = function(group, options) { var operator = new mathMLTree.MathNode( "mo", [new mathMLTree.TextNode("\u203e")]); operator.setAttribute("stretchy", "true"); var node = new mathMLTree.MathNode( "mover", [buildGroup(group.value.body, options), operator]); node.setAttribute("accent", "true"); return node; }; groupTypes.rule = function(group) { // TODO(emily): Figure out if there's an actual way to draw black boxes // in MathML. var node = new mathMLTree.MathNode("mrow"); return node; }; groupTypes.llap = function(group, options) { var node = new mathMLTree.MathNode( "mpadded", [buildGroup(group.value.body, options)]); node.setAttribute("lspace", "-1width"); node.setAttribute("width", "0px"); return node; }; groupTypes.rlap = function(group, options) { var node = new mathMLTree.MathNode( "mpadded", [buildGroup(group.value.body, options)]); node.setAttribute("width", "0px"); return node; }; groupTypes.phantom = function(group, options, prev) { var inner = buildExpression(group.value.value, options); return new mathMLTree.MathNode("mphantom", inner); }; /** * Takes a list of nodes, builds them, and returns a list of the generated * MathML nodes. A little simpler than the HTML version because we don't do any * previous-node handling. */ var buildExpression = function(expression, options) { var groups = []; for (var i = 0; i < expression.length; i++) { var group = expression[i]; groups.push(buildGroup(group, options)); } return groups; }; /** * Takes a group from the parser and calls the appropriate groupTypes function * on it to produce a MathML node. */ var buildGroup = function(group, options) { if (!group) { return new mathMLTree.MathNode("mrow"); } if (groupTypes[group.type]) { // Call the groupTypes function return groupTypes[group.type](group, options); } 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. */ var buildMathML = function(tree, texExpression, options) { var expression = buildExpression(tree, options); // Wrap up the expression in an mrow so it is presented in the semantics // tag correctly. var wrapper = new mathMLTree.MathNode("mrow", expression); // Build a TeX annotation of the source var annotation = new mathMLTree.MathNode( "annotation", [new mathMLTree.TextNode(texExpression)]); annotation.setAttribute("encoding", "application/x-tex"); var semantics = new mathMLTree.MathNode( "semantics", [wrapper, annotation]); var math = new mathMLTree.MathNode("math", [semantics]); // You can't style nodes, so we wrap the node in a span. return makeSpan(["katex-mathml"], [math]); }; module.exports = buildMathML;