Merge pull request #246 from gagern/matrices

Matrices, arrays, environments
This commit is contained in:
Kevin Barabash 2015-06-18 14:35:48 -06:00
commit 32e8ffef4f
13 changed files with 537 additions and 153 deletions

View File

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

View File

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

View File

@ -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 === "~") {

View File

@ -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) {

View File

@ -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));
}
}

132
src/environments.js Normal file
View File

@ -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;
})();

View File

@ -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.

View File

@ -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]
};
}
}
}
];

23
src/parseData.js Normal file
View File

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

View File

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

View File

@ -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();

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View File

@ -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}",