/** * 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 mathMLTree = require("./mathMLTree"); var ParseError = require("./ParseError"); var symbols = require("./symbols"); var makeSpan = buildCommon.makeSpan; /** * 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); }; /** * 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 = { mathord: function(group) { var node = new mathMLTree.MathNode( "mi", [makeText(group.value, group.mode)]); return node; }, textord: function(group) { var text = makeText(group.value, group.mode); var node; if (/[0-9]/.test(group.value)) { node = new mathMLTree.MathNode("mn", [text]); } else { node = new mathMLTree.MathNode("mi", [text]); node.setAttribute("mathvariant", "normal"); } return node; }, bin: function(group) { var node = new mathMLTree.MathNode( "mo", [makeText(group.value, group.mode)]); return node; }, rel: function(group) { var node = new mathMLTree.MathNode( "mo", [makeText(group.value, group.mode)]); return node; }, open: function(group) { var node = new mathMLTree.MathNode( "mo", [makeText(group.value, group.mode)]); return node; }, close: function(group) { var node = new mathMLTree.MathNode( "mo", [makeText(group.value, group.mode)]); return node; }, inner: function(group) { var node = new mathMLTree.MathNode( "mo", [makeText(group.value, group.mode)]); return node; }, punct: function(group) { var node = new mathMLTree.MathNode( "mo", [makeText(group.value, group.mode)]); node.setAttribute("separator", "true"); return node; }, ordgroup: function(group) { var inner = buildExpression(group.value); var node = new mathMLTree.MathNode("mrow", inner); return node; }, text: function(group) { var inner = buildExpression(group.value.body); var node = new mathMLTree.MathNode("mtext", inner); return node; }, color: function(group) { var inner = buildExpression(group.value.value); var node = new mathMLTree.MathNode("mstyle", inner); node.setAttribute("mathcolor", group.value.color); return node; }, supsub: function(group) { var children = [buildGroup(group.value.base)]; if (group.value.sub) { children.push(buildGroup(group.value.sub)); } if (group.value.sup) { children.push(buildGroup(group.value.sup)); } 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; }, genfrac: function(group) { var node = new mathMLTree.MathNode( "mfrac", [buildGroup(group.value.numer), buildGroup(group.value.denom)]); 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; }, array: function(group) { 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)]); })); })); }, sqrt: function(group) { var node; if (group.value.index) { node = new mathMLTree.MathNode( "mroot", [ buildGroup(group.value.body), buildGroup(group.value.index) ]); } else { node = new mathMLTree.MathNode( "msqrt", [buildGroup(group.value.body)]); } return node; }, leftright: function(group) { var inner = buildExpression(group.value.body); 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; }, accent: function(group) { var accentNode = new mathMLTree.MathNode( "mo", [makeText(group.value.accent, group.mode)]); var node = new mathMLTree.MathNode( "mover", [buildGroup(group.value.base), accentNode]); node.setAttribute("accent", "true"); return node; }, 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; }, 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; }, katex: function(group) { var node = new mathMLTree.MathNode( "mtext", [new mathMLTree.TextNode("KaTeX")]); return node; }, 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; }, styling: function(group) { var inner = buildExpression(group.value.value, inner); 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; }, sizing: function(group) { var inner = buildExpression(group.value.value); 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. node.setAttribute( "mathsize", buildCommon.sizingMultiplier[group.value.size] + "em"); return node; }, overline: function(group) { var operator = new mathMLTree.MathNode( "mo", [new mathMLTree.TextNode("\u203e")]); operator.setAttribute("stretchy", "true"); var node = new mathMLTree.MathNode( "mover", [buildGroup(group.value.body), operator]); node.setAttribute("accent", "true"); return node; }, 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; }, llap: function(group) { var node = new mathMLTree.MathNode( "mpadded", [buildGroup(group.value.body)]); node.setAttribute("lspace", "-1width"); node.setAttribute("width", "0px"); return node; }, rlap: function(group) { var node = new mathMLTree.MathNode( "mpadded", [buildGroup(group.value.body)]); node.setAttribute("width", "0px"); return node; }, phantom: function(group, options, prev) { var inner = buildExpression(group.value.value); 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) { var groups = []; for (var i = 0; i < expression.length; i++) { var group = expression[i]; groups.push(buildGroup(group)); } 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) { if (!group) { return new mathMLTree.MathNode("mrow"); } if (groupTypes[group.type]) { // Call the groupTypes function return groupTypes[group.type](group); } 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, settings) { var expression = buildExpression(tree); // 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;