Implement environments, for arrays and matrices in particular
This commit introduces environments, and implements the parser infrastructure to handle them, even including arguments after the “\begin{name}” construct. It also offers a way to turn array-like data structures, i.e. delimited by “&” and “\\”, into nested arrays of groups. Environments are essentially functions which call back to the parser to parse their body. It is their responsibility to stop at the next “\end”, while the parser takes care of verifing that the names match between “\begin” and “\end”. The environment has to return a ParseResult, to provide the position that goes with the resulting node. One application of this is the “array” environment. So far, it supports column alignment, but no column separators, and no multi-column shorthands using “*{…}”. Building on the same infrastructure, there are “matrix”, “pmatrix”, “bmatrix”, “vmatrix” and “Vmatrix” environments. Internally these are just “\left..\right” wrapped around an array with no margins at its ends. Spacing for arrays and matrices was derived from the LaTeX sources, and comments indicate the appropriate references. Now we have hard-wired breaks in parseExpression, to always break on “}”, “\end”, “\right”, “&”, “\\” and “\cr”. This means that these symbols are never PART of an expression, at least not without some nesting. They may follow AFTER an expression, and the caller of parseExpression should be expecting them. The implicit groups for sizing or styling don't care what ended the expression, which is all right for them. We still have support for breakOnToken, but now it is only used for “]” since that MAY be used to terminate an optional argument, but otherwise it's an ordinary symbol.
This commit is contained in:
parent
5cf5617c09
commit
2f7a54877a
|
@ -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
|
||||
|
|
244
src/Parser.js
244
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;
|
||||
|
|
103
src/buildHTML.js
103
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 === "~") {
|
||||
|
|
|
@ -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) {
|
||||
|
|
132
src/environments.js
Normal file
132
src/environments.js
Normal 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;
|
||||
})();
|
|
@ -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.
|
||||
|
|
|
@ -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
23
src/parseData.js
Normal 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
|
||||
};
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
BIN
test/screenshotter/images/Arrays-firefox.png
Normal file
BIN
test/screenshotter/images/Arrays-firefox.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 39 KiB |
|
@ -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}",
|
||||
|
|
Loading…
Reference in New Issue
Block a user