Spaces:
Running
on
T4
Running
on
T4
| /** | |
| * 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 <mn> 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.underline = function(group, options) { | |
| var operator = new mathMLTree.MathNode( | |
| "mo", [new mathMLTree.TextNode("\u203e")]); | |
| operator.setAttribute("stretchy", "true"); | |
| var node = new mathMLTree.MathNode( | |
| "munder", | |
| [buildGroup(group.value.body, options), | |
| operator]); | |
| node.setAttribute("accentunder", "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 | |
| * <semantics> tag so we can also include that TeX source as an annotation. | |
| * | |
| * Note that we actually return a domTree element with a `<math>` 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 <math> nodes, so we wrap the node in a span. | |
| return makeSpan(["katex-mathml"], [math]); | |
| }; | |
| module.exports = buildMathML; | |