Merge pull request #246 from gagern/matrices
Matrices, arrays, environments
This commit is contained in:
commit
32e8ffef4f
|
@ -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) {
|
||||
|
|
|
@ -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
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