From 2f7a54877a412058fe2a134d973aa6fb0d448f50 Mon Sep 17 00:00:00 2001 From: Martin von Gagern Date: Fri, 12 Jun 2015 18:58:16 +0200 Subject: [PATCH] Implement environments, for arrays and matrices in particular MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/Lexer.js | 8 +- src/Parser.js | 244 ++++++++++--------- src/buildHTML.js | 103 ++++++++ src/buildMathML.js | 11 + src/environments.js | 132 ++++++++++ src/fontMetrics.js | 1 + src/functions.js | 41 ++++ src/parseData.js | 23 ++ static/katex.less | 17 ++ test/katex-spec.js | 51 ++++ test/screenshotter/images/Arrays-firefox.png | Bin 0 -> 40207 bytes test/screenshotter/ss_data.json | 1 + 12 files changed, 520 insertions(+), 112 deletions(-) create mode 100644 src/environments.js create mode 100644 src/parseData.js create mode 100644 test/screenshotter/images/Arrays-firefox.png diff --git a/src/Lexer.js b/src/Lexer.js index 8e1ca0085..1d7a0b412 100644 --- 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 index 5a1cbb5ea..9de048759 100644 --- 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 index c3cc4e970..d34ac6a39 100644 --- 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 index a681decaa..736a61aec 100644 --- 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/environments.js b/src/environments.js new file mode 100644 index 000000000..9da6a22e1 --- /dev/null +++ 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 index ee618f435..5af457af8 100644 --- 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 index 06f5668f8..9b1d2a4ef 100644 --- 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 new file mode 100644 index 000000000..abaad818d --- /dev/null +++ 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 index 44bc34536..87ff6e54d 100644 --- 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 index 6be14f86b..abfc2a633 100644 --- 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 new file mode 100644 index 0000000000000000000000000000000000000000..bdec8ec520df1584712d34100a2ac82b1aa70e7b GIT binary patch literal 40207 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