scribble-math/src/buildMathML.js
Martin von Gagern 2f7a54877a Implement environments, for arrays and matrices in particular
This commit introduces environments, and implements the parser
infrastructure to handle them, even including arguments after the
“\begin{name}” construct.  It also offers a way to turn array-like data
structures, i.e. delimited by “&” and “\\”, into nested arrays of groups.
Environments are essentially functions which call back to the parser to
parse their body.  It is their responsibility to stop at the next “\end”,
while the parser takes care of verifing that the names match between
“\begin” and “\end”.  The environment has to return a ParseResult, to
provide the position that goes with the resulting node.

One application of this is the “array” environment.  So far, it supports
column alignment, but no column separators, and no multi-column shorthands
using “*{…}”.  Building on the same infrastructure, there are “matrix”,
“pmatrix”, “bmatrix”, “vmatrix” and “Vmatrix” environments.  Internally
these are just “\left..\right” wrapped around an array with no margins at
its ends.  Spacing for arrays and matrices was derived from the LaTeX
sources, and comments indicate the appropriate references.

Now we have hard-wired breaks in parseExpression, to always break on “}”,
“\end”, “\right”, “&”, “\\” and “\cr”.  This means that these symbols are
never PART of an expression, at least not without some nesting.  They may
follow AFTER an expression, and the caller of parseExpression should be
expecting them.  The implicit groups for sizing or styling don't care what
ended the expression, which is all right for them.  We still have support
for breakOnToken, but now it is only used for “]” since that MAY be used to
terminate an optional argument, but otherwise it's an ordinary symbol.
2015-06-18 22:24:40 +02:00

468 lines
13 KiB
JavaScript

/**
* 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
* <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, 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 <math> nodes, so we wrap the node in a span.
return makeSpan(["katex-mathml"], [math]);
};
module.exports = buildMathML;