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; | |