commit 32e8ffef4fabd74e8419972367baa38e3d7edc00
parent 5cf5617c0948db18716ed963cc8112aeb0cc8266
Author: Kevin Barabash <kevinb7@gmail.com>
Date: Thu, 18 Jun 2015 14:35:48 -0600
Merge pull request #246 from gagern/matrices
Matrices, arrays, environments
Diffstat:
13 files changed, 537 insertions(+), 153 deletions(-)
diff --git a/src/Lexer.js b/src/Lexer.js
@@ -37,7 +37,9 @@ var mathNormals = [
/['\^_{}]/, // misc
/[(\[]/, // opens
/[)\]?!]/, // closes
- /~/ // spacing
+ /~/, // spacing
+ /&/, // horizontal alignment
+ /\\\\/ // line break
];
// These are "normal" tokens like above, but should instead be parsed in text
@@ -45,7 +47,9 @@ var mathNormals = [
var textNormals = [
/[a-zA-Z0-9`!@*()-=+\[\]'";:?\/.,]/, // ords
/[{}]/, // grouping
- /~/ // spacing
+ /~/, // spacing
+ /&/, // horizontal alignment
+ /\\\\/ // line break
];
// Regexes for matching whitespace
diff --git a/src/Parser.js b/src/Parser.js
@@ -1,8 +1,10 @@
var functions = require("./functions");
+var environments = require("./environments");
var Lexer = require("./Lexer");
var symbols = require("./symbols");
var utils = require("./utils");
+var parseData = require("./parseData");
var ParseError = require("./ParseError");
/**
@@ -50,22 +52,8 @@ function Parser(input, settings) {
this.settings = settings;
}
-/**
- * The resulting parse tree nodes of the parse tree.
- */
-function ParseNode(type, value, mode) {
- this.type = type;
- this.value = value;
- this.mode = mode;
-}
-
-/**
- * A result and final position returned by the `.parse...` functions.
- */
-function ParseResult(result, newPosition) {
- this.result = result;
- this.position = newPosition;
-}
+var ParseNode = parseData.ParseNode;
+var ParseResult = parseData.ParseResult;
/**
* An initial function (without its arguments), or an argument to a function.
@@ -106,13 +94,14 @@ Parser.prototype.parse = function(input) {
*/
Parser.prototype.parseInput = function(pos, mode) {
// Parse an expression
- var expression = this.parseExpression(pos, mode, false, null);
+ var expression = this.parseExpression(pos, mode, false);
// If we succeeded, make sure there's an EOF at the end
- var EOF = this.lexer.lex(expression.position, mode);
- this.expect(EOF, "EOF");
+ this.expect(expression.peek, "EOF");
return expression;
};
+var endOfExpression = ["}", "\\end", "\\right", "&", "\\\\", "\\cr"];
+
/**
* Parses an "expression", which is a list of atoms.
*
@@ -127,11 +116,15 @@ Parser.prototype.parseInput = function(pos, mode) {
*/
Parser.prototype.parseExpression = function(pos, mode, breakOnInfix, breakOnToken) {
var body = [];
+ var lex = null;
// Keep adding atoms to the body until we can't parse any more atoms (either
// we reached the end, a }, or a \right)
while (true) {
- var lex = this.lexer.lex(pos, mode);
- if (breakOnToken != null && lex.text === breakOnToken) {
+ lex = this.lexer.lex(pos, mode);
+ if (endOfExpression.indexOf(lex.text) !== -1) {
+ break;
+ }
+ if (breakOnToken && lex.text === breakOnToken) {
break;
}
var atom = this.parseAtom(pos, mode);
@@ -144,7 +137,9 @@ Parser.prototype.parseExpression = function(pos, mode, breakOnInfix, breakOnToke
body.push(atom.result);
pos = atom.position;
}
- return new ParseResult(this.handleInfixNodes(body, mode), pos);
+ var res = new ParseResult(this.handleInfixNodes(body, mode), pos);
+ res.peek = lex;
+ return res;
};
/**
@@ -353,31 +348,48 @@ Parser.prototype.parseImplicitGroup = function(pos, mode) {
// Parse the entire left function (including the delimiter)
var left = this.parseFunction(pos, mode);
// Parse out the implicit body
- body = this.parseExpression(left.position, mode, false, "}");
+ body = this.parseExpression(left.position, mode, false);
// Check the next token
- var rightLex = this.parseSymbol(body.position, mode);
-
- if (rightLex && rightLex.result.result === "\\right") {
- // If it's a \right, parse the entire right function (including the delimiter)
- var right = this.parseFunction(body.position, mode);
-
- return new ParseResult(
- new ParseNode("leftright", {
- body: body.result,
- left: left.result.value.value,
- right: right.result.value.value
- }, mode),
- right.position);
- } else {
- throw new ParseError("Missing \\right", this.lexer, body.position);
+ this.expect(body.peek, "\\right");
+ var right = this.parseFunction(body.position, mode);
+ return new ParseResult(
+ new ParseNode("leftright", {
+ body: body.result,
+ left: left.result.value.value,
+ right: right.result.value.value
+ }, mode),
+ right.position);
+ } else if (func === "\\begin") {
+ // begin...end is similar to left...right
+ var begin = this.parseFunction(pos, mode);
+ var envName = begin.result.value.name;
+ if (!environments.hasOwnProperty(envName)) {
+ throw new ParseError(
+ "No such environment: " + envName,
+ this.lexer, begin.result.value.namepos);
}
- } else if (func === "\\right") {
- // If we see a right, explicitly fail the parsing here so the \left
- // handling ends the group
- return null;
+ // Build the environment object. Arguments and other information will
+ // be made available to the begin and end methods using properties.
+ var env = environments[envName];
+ var args = [null, mode, envName];
+ var newPos = this.parseArguments(
+ begin.position, mode, "\\begin{" + envName + "}", env, args);
+ args[0] = newPos;
+ var result = env.handler.apply(this, args);
+ var endLex = this.lexer.lex(result.position, mode);
+ this.expect(endLex, "\\end");
+ var end = this.parseFunction(result.position, mode);
+ if (end.result.value.name !== envName) {
+ throw new ParseError(
+ "Mismatch: \\begin{" + envName + "} matched " +
+ "by \\end{" + end.result.value.name + "}",
+ this.lexer, end.namepos);
+ }
+ result.position = end.position;
+ return result;
} else if (utils.contains(sizeFuncs, func)) {
// If we see a sizing function, parse out the implict body
- body = this.parseExpression(start.result.position, mode, false, "}");
+ body = this.parseExpression(start.result.position, mode, false);
return new ParseResult(
new ParseNode("sizing", {
// Figure out what size to use based on the list of functions above
@@ -387,7 +399,7 @@ Parser.prototype.parseImplicitGroup = function(pos, mode) {
body.position);
} else if (utils.contains(styleFuncs, func)) {
// If we see a styling function, parse out the implict body
- body = this.parseExpression(start.result.position, mode, true, "}");
+ body = this.parseExpression(start.result.position, mode, true);
return new ParseResult(
new ParseNode("styling", {
// Figure out what style to use by pulling out the style from
@@ -420,71 +432,10 @@ Parser.prototype.parseFunction = function(pos, mode) {
this.lexer, baseGroup.position);
}
- var newPos = baseGroup.result.position;
- var result;
-
- var totalArgs = funcData.numArgs + funcData.numOptionalArgs;
-
- if (totalArgs > 0) {
- var baseGreediness = funcData.greediness;
- var args = [func];
- var positions = [newPos];
-
- for (var i = 0; i < totalArgs; i++) {
- var argType = funcData.argTypes && funcData.argTypes[i];
- var arg;
- if (i < funcData.numOptionalArgs) {
- if (argType) {
- arg = this.parseSpecialGroup(newPos, argType, mode, true);
- } else {
- arg = this.parseOptionalGroup(newPos, mode);
- }
- if (!arg) {
- args.push(null);
- positions.push(newPos);
- continue;
- }
- } else {
- if (argType) {
- arg = this.parseSpecialGroup(newPos, argType, mode);
- } else {
- arg = this.parseGroup(newPos, mode);
- }
- if (!arg) {
- throw new ParseError(
- "Expected group after '" + baseGroup.result.result +
- "'",
- this.lexer, newPos);
- }
- }
- var argNode;
- if (arg.isFunction) {
- var argGreediness =
- functions.funcs[arg.result.result].greediness;
- if (argGreediness > baseGreediness) {
- argNode = this.parseFunction(newPos, mode);
- } else {
- throw new ParseError(
- "Got function '" + arg.result.result + "' as " +
- "argument to function '" +
- baseGroup.result.result + "'",
- this.lexer, arg.result.position - 1);
- }
- } else {
- argNode = arg.result;
- }
- args.push(argNode.result);
- positions.push(argNode.position);
- newPos = argNode.position;
- }
-
- args.push(positions);
-
- result = functions.funcs[func].handler.apply(this, args);
- } else {
- result = functions.funcs[func].handler.apply(this, [func]);
- }
-
+ var args = [func];
+ var newPos = this.parseArguments(
+ baseGroup.result.position, mode, func, funcData, args);
+ var result = functions.funcs[func].handler.apply(this, args);
return new ParseResult(
new ParseNode(result.type, result, mode),
newPos);
@@ -496,6 +447,77 @@ Parser.prototype.parseFunction = function(pos, mode) {
}
};
+
+/**
+ * Parses the arguments of a function or environment
+ *
+ * @param {string} func "\name" or "\begin{name}"
+ * @param {{numArgs:number,numOptionalArgs:number|undefined}} funcData
+ * @param {Array} args list of arguments to which new ones will be pushed
+ * @return the position after all arguments have been parsed
+ */
+Parser.prototype.parseArguments = function(pos, mode, func, funcData, args) {
+ var totalArgs = funcData.numArgs + funcData.numOptionalArgs;
+ if (totalArgs === 0) {
+ return pos;
+ }
+
+ var newPos = pos;
+ var baseGreediness = funcData.greediness;
+ var positions = [newPos];
+
+ for (var i = 0; i < totalArgs; i++) {
+ var argType = funcData.argTypes && funcData.argTypes[i];
+ var arg;
+ if (i < funcData.numOptionalArgs) {
+ if (argType) {
+ arg = this.parseSpecialGroup(newPos, argType, mode, true);
+ } else {
+ arg = this.parseOptionalGroup(newPos, mode);
+ }
+ if (!arg) {
+ args.push(null);
+ positions.push(newPos);
+ continue;
+ }
+ } else {
+ if (argType) {
+ arg = this.parseSpecialGroup(newPos, argType, mode);
+ } else {
+ arg = this.parseGroup(newPos, mode);
+ }
+ if (!arg) {
+ throw new ParseError(
+ "Expected group after '" + func + "'",
+ this.lexer, newPos);
+ }
+ }
+ var argNode;
+ if (arg.isFunction) {
+ var argGreediness =
+ functions.funcs[arg.result.result].greediness;
+ if (argGreediness > baseGreediness) {
+ argNode = this.parseFunction(newPos, mode);
+ } else {
+ throw new ParseError(
+ "Got function '" + arg.result.result + "' as " +
+ "argument to '" + func + "'",
+ this.lexer, arg.result.position - 1);
+ }
+ } else {
+ argNode = arg.result;
+ }
+ args.push(argNode.result);
+ positions.push(argNode.position);
+ newPos = argNode.position;
+ }
+
+ args.push(positions);
+
+ return newPos;
+};
+
+
/**
* Parses a group when the mode is changing. Takes a position, a new mode, and
* an outer mode that is used to parse the outside.
@@ -556,7 +578,7 @@ Parser.prototype.parseGroup = function(pos, mode) {
// Try to parse an open brace
if (start.text === "{") {
// If we get a brace, parse an expression
- var expression = this.parseExpression(start.position, mode, false, "}");
+ var expression = this.parseExpression(start.position, mode, false);
// Make sure we get a close brace
var closeBrace = this.lexer.lex(expression.position, mode);
this.expect(closeBrace, "}");
@@ -625,4 +647,6 @@ Parser.prototype.parseSymbol = function(pos, mode) {
}
};
+Parser.prototype.ParseNode = ParseNode;
+
module.exports = Parser;
diff --git a/src/buildHTML.js b/src/buildHTML.js
@@ -43,6 +43,7 @@ var groupToType = {
close: "mclose",
inner: "minner",
genfrac: "minner",
+ array: "minner",
spacing: "mord",
punct: "mpunct",
ordgroup: "mord",
@@ -498,6 +499,108 @@ var groupTypes = {
options.getColor());
},
+ array: function(group, options, prev) {
+ var r, c;
+ var nr = group.value.body.length;
+ var nc = 0;
+ var body = new Array(nr);
+
+ // Horizontal spacing
+ var pt = 1 / fontMetrics.metrics.ptPerEm;
+ var arraycolsep = 5 * pt; // \arraycolsep in article.cls
+
+ // Vertical spacing
+ var baselineskip = 12 * pt; // see size10.clo
+ var arraystretch = 1; // factor, see lttab.dtx
+ var arrayskip = arraystretch * baselineskip;
+ var arstrutHeight = 0.7 * arrayskip; // \strutbox in ltfsstrc.dtx and
+ var arstrutDepth = 0.3 * arrayskip; // \@arstrutbox in lttab.dtx
+
+ var totalHeight = 0;
+ for (r = 0; r < group.value.body.length; ++r) {
+ var inrow = group.value.body[r];
+ var height = arstrutHeight; // \@array adds an \@arstrut
+ var depth = arstrutDepth; // to each tow (via the template)
+ if (nc < inrow.length) {
+ nc = inrow.length;
+ }
+ var outrow = new Array(inrow.length);
+ for (c = 0; c < inrow.length; ++c) {
+ var elt = buildGroup(inrow[c], options);
+ if (depth < elt.depth) {
+ depth = elt.depth;
+ }
+ if (height < elt.height) {
+ height = elt.height;
+ }
+ outrow[c] = elt;
+ }
+ var gap = 0;
+ if (group.value.rowGaps[r]) {
+ gap = group.value.rowGaps[r].value;
+ switch (gap.unit) {
+ case "em":
+ gap = gap.number;
+ break;
+ case "ex":
+ gap = gap.number * fontMetrics.metrics.emPerEx;
+ break;
+ default:
+ console.error("Can't handle unit " + gap.unit);
+ gap = 0;
+ }
+ if (gap > 0) { // \@argarraycr
+ gap += arstrutDepth;
+ if (depth < gap) {
+ depth = gap; // \@xargarraycr
+ }
+ gap = 0;
+ }
+ }
+ outrow.height = height;
+ outrow.depth = depth;
+ totalHeight += height;
+ outrow.pos = totalHeight;
+ totalHeight += depth + gap; // \@yargarraycr
+ body[r] = outrow;
+ }
+ var offset = totalHeight / 2 + fontMetrics.metrics.axisHeight;
+ var colalign = group.value.colalign || [];
+ var cols = [];
+ var colsep;
+ for (c = 0; c < nc; ++c) {
+ if (c > 0 || group.value.hskipBeforeAndAfter) {
+ colsep = makeSpan(["arraycolsep"], []);
+ colsep.style.width = arraycolsep + "em";
+ cols.push(colsep);
+ }
+ var col = [];
+ for (r = 0; r < nr; ++r) {
+ var row = body[r];
+ var elem = row[c];
+ if (!elem) {
+ continue;
+ }
+ var shift = row.pos - offset;
+ elem.depth = row.depth;
+ elem.height = row.height;
+ col.push({type: "elem", elem: elem, shift: shift});
+ }
+ col = buildCommon.makeVList(col, "individualShift", null, options);
+ col = makeSpan(
+ ["col-align-" + (colalign[c] || "c")],
+ [col]);
+ cols.push(col);
+ if (c < nc - 1 || group.value.hskipBeforeAndAfter) {
+ colsep = makeSpan(["arraycolsep"], []);
+ colsep.style.width = arraycolsep + "em";
+ cols.push(colsep);
+ }
+ }
+ body = makeSpan(["mtable"], cols);
+ return makeSpan(["minner"], [body], options.getColor());
+ },
+
spacing: function(group, options, prev) {
if (group.value === "\\ " || group.value === "\\space" ||
group.value === " " || group.value === "~") {
diff --git a/src/buildMathML.js b/src/buildMathML.js
@@ -186,6 +186,17 @@ var groupTypes = {
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) {
diff --git a/src/delimiter.js b/src/delimiter.js
@@ -231,28 +231,25 @@ var makeStackedDelim = function(delim, heightTotal, center, options, mode) {
var repeatHeightTotal = repeatMetrics.height + repeatMetrics.depth;
var bottomMetrics = getMetrics(bottom, font);
var bottomHeightTotal = bottomMetrics.height + bottomMetrics.depth;
- var middleMetrics, middleHeightTotal;
+ var middleHeightTotal = 0;
+ var middleFactor = 1;
if (middle !== null) {
- middleMetrics = getMetrics(middle, font);
+ var middleMetrics = getMetrics(middle, font);
middleHeightTotal = middleMetrics.height + middleMetrics.depth;
+ middleFactor = 2; // repeat symmetrically above and below middle
}
- // Calcuate the real height that the delimiter will have. It is at least the
- // size of the top, bottom, and optional middle combined.
- var realHeightTotal = topHeightTotal + bottomHeightTotal;
- if (middle !== null) {
- realHeightTotal += middleHeightTotal;
- }
+ // Calcuate the minimal height that the delimiter can have.
+ // It is at least the size of the top, bottom, and optional middle combined.
+ var minHeight = topHeightTotal + bottomHeightTotal + middleHeightTotal;
- // Then add repeated pieces until we reach the specified height.
- while (realHeightTotal < heightTotal) {
- realHeightTotal += repeatHeightTotal;
- if (middle !== null) {
- // If there is a middle section, we need an equal number of pieces
- // on the top and bottom.
- realHeightTotal += repeatHeightTotal;
- }
- }
+ // Compute the number of copies of the repeat symbol we will need
+ var repeatCount = Math.ceil(
+ (heightTotal - minHeight) / (middleFactor * repeatHeightTotal));
+
+ // Compute the total height of the delimiter including all the symbols
+ var realHeightTotal =
+ minHeight + repeatCount * middleFactor * repeatHeightTotal;
// The center of the delimiter is placed at the center of the axis. Note
// that in this context, "center" means that the delimiter should be
@@ -275,39 +272,18 @@ var makeStackedDelim = function(delim, heightTotal, center, options, mode) {
var i;
if (middle === null) {
- // Calculate the number of repeated symbols we need
- var repeatHeight = realHeightTotal - topHeightTotal - bottomHeightTotal;
- var symbolCount = Math.ceil(repeatHeight / repeatHeightTotal);
-
// Add that many symbols
- for (i = 0; i < symbolCount; i++) {
+ for (i = 0; i < repeatCount; i++) {
inners.push(makeInner(repeat, font, mode));
}
} else {
// When there is a middle bit, we need the middle part and two repeated
// sections
-
- // Calculate the number of symbols needed for the top and bottom
- // repeated parts
- var topRepeatHeight =
- realHeightTotal / 2 - topHeightTotal - middleHeightTotal / 2;
- var topSymbolCount = Math.ceil(topRepeatHeight / repeatHeightTotal);
-
- var bottomRepeatHeight =
- realHeightTotal / 2 - topHeightTotal - middleHeightTotal / 2;
- var bottomSymbolCount =
- Math.ceil(bottomRepeatHeight / repeatHeightTotal);
-
- // Add the top repeated part
- for (i = 0; i < topSymbolCount; i++) {
+ for (i = 0; i < repeatCount; i++) {
inners.push(makeInner(repeat, font, mode));
}
-
- // Add the middle piece
inners.push(makeInner(middle, font, mode));
-
- // Add the bottom repeated part
- for (i = 0; i < bottomSymbolCount; i++) {
+ for (i = 0; i < repeatCount; i++) {
inners.push(makeInner(repeat, font, mode));
}
}
diff --git a/src/environments.js b/src/environments.js
@@ -0,0 +1,132 @@
+var parseData = require("./parseData");
+var ParseError = require("./ParseError");
+
+var ParseNode = parseData.ParseNode;
+var ParseResult = parseData.ParseResult;
+
+/**
+ * Parse the body of the environment, with rows delimited by \\ and
+ * columns delimited by &, and create a nested list in row-major order
+ * with one group per cell.
+ */
+function parseArray(parser, pos, mode, result) {
+ var row = [], body = [row], rowGaps = [];
+ while (true) {
+ var cell = parser.parseExpression(pos, mode, false, null);
+ row.push(new ParseNode("ordgroup", cell.result, mode));
+ pos = cell.position;
+ var next = cell.peek.text;
+ if (next === "&") {
+ pos = cell.peek.position;
+ } else if (next === "\\end") {
+ break;
+ } else if (next === "\\\\" || next === "\\cr") {
+ var cr = parser.parseFunction(pos, mode);
+ rowGaps.push(cr.result.value.size);
+ pos = cr.position;
+ row = [];
+ body.push(row);
+ } else {
+ throw new ParseError("Expected & or \\\\ or \\end",
+ parser.lexer, cell.peek.position);
+ }
+ }
+ result.body = body;
+ result.rowGaps = rowGaps;
+ return new ParseResult(new ParseNode(result.type, result, mode), pos);
+}
+
+/*
+ * An environment definition is very similar to a function definition.
+ * Each element of the following array may contain
+ * - names: The names associated with a function. This can be used to
+ * share one implementation between several similar environments.
+ * - numArgs: The number of arguments after the \begin{name} function.
+ * - argTypes: (optional) Just like for a function
+ * - allowedInText: (optional) Whether or not the environment is allowed inside
+ * text mode (default false) (not enforced yet)
+ * - numOptionalArgs: (optional) Just like for a function
+ * - handler: The function that is called to handle this environment.
+ * It will receive the following arguments:
+ * - pos: the current position of the parser.
+ * - mode: the current parsing mode.
+ * - envName: the name of the environment, one of the listed names.
+ * - [args]: the arguments passed to \begin.
+ * - positions: the positions associated with these arguments.
+ */
+
+var environmentDefinitions = [
+
+ // Arrays are part of LaTeX, defined in lttab.dtx so its documentation
+ // is part of the source2e.pdf file of LaTeX2e source documentation.
+ {
+ names: ["array"],
+ numArgs: 1,
+ handler: function(pos, mode, envName, colalign, positions) {
+ var parser = this;
+ // Currently only supports alignment, no separators like | yet.
+ colalign = colalign.value.map ? colalign.value : [colalign];
+ colalign = colalign.map(function(node) {
+ var ca = node.value;
+ if ("lcr".indexOf(ca) !== -1) {
+ return ca;
+ }
+ throw new ParseError(
+ "Unknown column alignment: " + node.value,
+ parser.lexer, positions[1]);
+ });
+ var res = {
+ type: "array",
+ colalign: colalign,
+ hskipBeforeAndAfter: true // \@preamble in lttab.dtx
+ };
+ res = parseArray(parser, pos, mode, res);
+ return res;
+ }
+ },
+
+ // The matrix environments of amsmath builds on the array environment
+ // of LaTeX, which is discussed above.
+ {
+ names: ["matrix", "pmatrix", "bmatrix", "vmatrix", "Vmatrix"],
+ handler: function(pos, mode, envName) {
+ var delimiters = {
+ "matrix": null,
+ "pmatrix": ["(", ")"],
+ "bmatrix": ["[", "]"],
+ "vmatrix": ["|", "|"],
+ "Vmatrix": ["\\Vert", "\\Vert"]
+ }[envName];
+ var res = {
+ type: "array",
+ hskipBeforeAndAfter: false // \hskip -\arraycolsep in amsmath
+ };
+ res = parseArray(this, pos, mode, res);
+ if (delimiters) {
+ res.result = new ParseNode("leftright", {
+ body: [res.result],
+ left: delimiters[0],
+ right: delimiters[1]
+ }, mode);
+ }
+ return res;
+ }
+ }
+
+];
+
+module.exports = (function() {
+ // nested function so we don't leak i and j into the module scope
+ var exports = {};
+ for (var i = 0; i < environmentDefinitions.length; ++i) {
+ var def = environmentDefinitions[i];
+ def.greediness = 1;
+ def.allowedInText = !!def.allowedInText;
+ def.numArgs = def.numArgs || 0;
+ def.numOptionalArgs = def.numOptionalArgs || 0;
+ for (var j = 0; j < def.names.length; ++j) {
+ exports[def.names[j]] = def;
+ }
+ }
+ return exports;
+})();
diff --git a/src/fontMetrics.js b/src/fontMetrics.js
@@ -93,6 +93,7 @@ var metrics = {
bigOpSpacing4: xi12,
bigOpSpacing5: xi13,
ptPerEm: ptPerEm,
+ emPerEx: sigma5 / sigma6,
// TODO(alpert): Missing parallel structure here. We should probably add
// style-specific metrics for all of these.
diff --git a/src/functions.js b/src/functions.js
@@ -517,6 +517,47 @@ var duplicatedFunctions = [
};
}
}
+ },
+
+ // Row breaks for aligned data
+ {
+ funcs: ["\\\\", "\\cr"],
+ data: {
+ numArgs: 0,
+ numOptionalArgs: 1,
+ argTypes: ["size"],
+ handler: function(func, size) {
+ return {
+ type: "cr",
+ size: size
+ };
+ }
+ }
+ },
+
+ // Environment delimiters
+ {
+ funcs: ["\\begin", "\\end"],
+ data: {
+ numArgs: 1,
+ argTypes: ["text"],
+ handler: function(func, nameGroup, positions) {
+ if (nameGroup.type !== "ordgroup") {
+ throw new ParseError(
+ "Invalid environment name",
+ this.lexer, positions[1]);
+ }
+ var name = "";
+ for (var i = 0; i < nameGroup.value.length; ++i) {
+ name += nameGroup.value[i].value;
+ }
+ return {
+ type: "environment",
+ name: name,
+ namepos: positions[1]
+ };
+ }
+ }
}
];
diff --git a/src/parseData.js b/src/parseData.js
@@ -0,0 +1,23 @@
+/**
+ * The resulting parse tree nodes of the parse tree.
+ */
+function ParseNode(type, value, mode) {
+ this.type = type;
+ this.value = value;
+ this.mode = mode;
+}
+
+/**
+ * A result and final position returned by the `.parse...` functions.
+ *
+ */
+function ParseResult(result, newPosition, peek) {
+ this.result = result;
+ this.position = newPosition;
+}
+
+module.exports = {
+ ParseNode: ParseNode,
+ ParseResult: ParseResult
+};
+
diff --git a/static/katex.less b/static/katex.less
@@ -462,4 +462,21 @@
left: 0.326em;
}
}
+
+ .arraycolsep {
+ display: inline-block;
+ }
+
+ .col-align-c > .vlist {
+ text-align: center;
+ }
+
+ .col-align-l > .vlist {
+ text-align: left;
+ }
+
+ .col-align-r > .vlist {
+ text-align: right;
+ }
+
}
diff --git a/test/katex-spec.js b/test/katex-spec.js
@@ -880,6 +880,47 @@ describe("A left/right parser", function() {
});
});
+describe("A begin/end parser", function() {
+
+ it("should parse a simple environment", function() {
+ expect("\\begin{matrix}a&b\\\\c&d\\end{matrix}").toParse();
+ });
+
+ it("should parse an environment with argument", function() {
+ expect("\\begin{array}{cc}a&b\\\\c&d\\end{array}").toParse();
+ });
+
+ it("should error when name is mismatched", function() {
+ expect("\\begin{matrix}a&b\\\\c&d\\end{pmatrix}").toNotParse();
+ });
+
+ it("should error when commands are mismatched", function() {
+ expect("\\begin{matrix}a&b\\\\c&d\\right{pmatrix}").toNotParse();
+ });
+
+ it("should error when end is missing", function() {
+ expect("\\begin{matrix}a&b\\\\c&d").toNotParse();
+ });
+
+ it("should error when braces are mismatched", function() {
+ expect("{\\begin{matrix}a&b\\\\c&d}\\end{matrix}").toNotParse();
+ });
+
+ it("should cooperate with infix notation", function() {
+ expect("\\begin{matrix}0&1\\over2&3\\\\4&5&6\\end{matrix}").toParse();
+ });
+
+ it("should nest", function() {
+ var m1 = "\\begin{pmatrix}1&2\\\\3&4\\end{pmatrix}";
+ var m2 = "\\begin{array}{rl}" + m1 + "&0\\\\0&" + m1 + "\\end{array}";
+ expect(m2).toParse();
+ });
+
+ it("should allow \\cr as a line terminator", function() {
+ expect("\\begin{matrix}a&b\\cr c&d\\end{matrix}").toParse();
+ });
+});
+
describe("A sqrt parser", function() {
var sqrt = "\\sqrt{x}";
var missingGroup = "\\sqrt";
@@ -1264,6 +1305,16 @@ describe("An optional argument parser", function() {
});
});
+describe("An array environment", function() {
+
+ it("should accept a single alignment character", function() {
+ var parse = getParsed("\\begin{array}r1\\\\20\\end{array}");
+ expect(parse[0].type).toBe("array");
+ expect(parse[0].value.colalign).toEqual(["r"]);
+ });
+
+});
+
var getMathML = function(expr) {
expect(expr).toParse();
diff --git a/test/screenshotter/images/Arrays-firefox.png b/test/screenshotter/images/Arrays-firefox.png
Binary files differ.
diff --git a/test/screenshotter/ss_data.json b/test/screenshotter/ss_data.json
@@ -1,5 +1,6 @@
{
"Accents": "http://localhost:7936/test/screenshotter/test.html?m=\\vec{A}\\vec{x}\\vec x^2\\vec{x}_2^2\\vec{A}^2\\vec{xA}^2",
+ "Arrays": "http://localhost:7936/test/screenshotter/test.html?m=\\left(\\begin{array}{rlc}1%262%263\\\\1+1%262+1%263+1\\cr1\\over2%26\\scriptstyle 1/2%26\\frac12\\\\[1ex]\\begin{pmatrix}x\\\\y\\end{pmatrix}%260%26\\begin{vmatrix}a%26b\\\\c%26d\\end{vmatrix}\\end{array}\\right]",
"Baseline": "http://localhost:7936/test/screenshotter/test.html?m=a+b-c\\cdot d/e",
"BasicTest": "http://localhost:7936/test/screenshotter/test.html?m=a",
"BinomTest": "http://localhost:7936/test/screenshotter/test.html?m=\\dbinom{a}{b}\\tbinom{a}{b}^{\\binom{a}{b}+17}",