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 000000000..bdec8ec52 Binary files /dev/null and b/test/screenshotter/images/Arrays-firefox.png differ diff --git a/test/screenshotter/ss_data.json b/test/screenshotter/ss_data.json index 5f0416f48..9cac1ba89 100644 --- 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}",