Avoid re-lexing, move position to internal state

Instead of passing around the current position as an argument, we now have a
parser property called pos to keep track of that.  Instead of repeatedly
re-lexing at the current position we now have a property called nextToken
which contains the token beginning at the current position.  We may need to
re-lex if we switch mode.  Since the position is kept in the parser state,
we don't need to return it from parsing methods, which obsoletes the
ParseResult class.
This commit is contained in:
Martin von Gagern 2015-06-29 11:45:40 +02:00
parent 5f275aa9c1
commit 4debcb34af
3 changed files with 209 additions and 239 deletions

View File

@ -34,11 +34,9 @@ var ParseError = require("./ParseError");
* There are also extra `.handle...` functions, which pull out some reused * There are also extra `.handle...` functions, which pull out some reused
* functionality into self-contained functions. * functionality into self-contained functions.
* *
* The earlier functions return `ParseResult`s, which contain a ParseNode and a * The earlier functions return ParseNodes.
* new position.
*
* The later functions (which are called deeper in the parse) sometimes return * The later functions (which are called deeper in the parse) sometimes return
* ParseFuncOrArgument, which contain a ParseResult as well as some data about * ParseFuncOrArgument, which contain a ParseNode as well as some data about
* whether the parsed object is a function which is missing some arguments, or a * whether the parsed object is a function which is missing some arguments, or a
* standalone object which can be used as an argument to another function. * standalone object which can be used as an argument to another function.
*/ */
@ -54,11 +52,10 @@ function Parser(input, settings) {
} }
var ParseNode = parseData.ParseNode; var ParseNode = parseData.ParseNode;
var ParseResult = parseData.ParseResult;
/** /**
* An initial function (without its arguments), or an argument to a function. * An initial function (without its arguments), or an argument to a function.
* The `result` argument should be a ParseResult. * The `result` argument should be a ParseNode.
*/ */
function ParseFuncOrArgument(result, isFunction) { function ParseFuncOrArgument(result, isFunction) {
this.result = result; this.result = result;
@ -69,14 +66,29 @@ function ParseFuncOrArgument(result, isFunction) {
/** /**
* Checks a result to make sure it has the right type, and throws an * Checks a result to make sure it has the right type, and throws an
* appropriate error otherwise. * appropriate error otherwise.
*
* @param {boolean=} consume whether to consume the expected token,
* defaults to true
*/ */
Parser.prototype.expect = function(result, text) { Parser.prototype.expect = function(text, consume) {
if (result.text !== text) { if (this.nextToken.text !== text) {
throw new ParseError( throw new ParseError(
"Expected '" + text + "', got '" + result.text + "'", "Expected '" + text + "', got '" + this.nextToken.text + "'",
this.lexer, result.position this.lexer, this.nextToken.position
); );
} }
if (consume !== false) {
this.consume();
}
};
/**
* Considers the current look ahead token as consumed,
* and fetches the one after that as the new look ahead.
*/
Parser.prototype.consume = function() {
this.pos = this.nextToken.position;
this.nextToken = this.lexer.lex(this.pos, this.mode);
}; };
/** /**
@ -84,21 +96,23 @@ Parser.prototype.expect = function(result, text) {
* *
* @return {?Array.<ParseNode>} * @return {?Array.<ParseNode>}
*/ */
Parser.prototype.parse = function(input) { Parser.prototype.parse = function() {
// Try to parse the input // Try to parse the input
this.mode = "math"; this.mode = "math";
var parse = this.parseInput(0); this.pos = 0;
return parse.result; this.nextToken = this.lexer.lex(this.pos, this.mode);
var parse = this.parseInput();
return parse;
}; };
/** /**
* Parses an entire input tree. * Parses an entire input tree.
*/ */
Parser.prototype.parseInput = function(pos) { Parser.prototype.parseInput = function() {
// Parse an expression // Parse an expression
var expression = this.parseExpression(pos, false); var expression = this.parseExpression(false);
// If we succeeded, make sure there's an EOF at the end // If we succeeded, make sure there's an EOF at the end
this.expect(expression.peek, "EOF"); this.expect("EOF", false);
return expression; return expression;
}; };
@ -114,25 +128,25 @@ var endOfExpression = ["}", "\\end", "\\right", "&", "\\\\", "\\cr"];
* @param {?string} breakOnToken The token that the expression should end with, * @param {?string} breakOnToken The token that the expression should end with,
* or `null` if something else should end the expression. * or `null` if something else should end the expression.
* *
* @return {ParseResult} * @return {ParseNode}
*/ */
Parser.prototype.parseExpression = function(pos, breakOnInfix, breakOnToken) { Parser.prototype.parseExpression = function(breakOnInfix, breakOnToken) {
var body = []; var body = [];
var lex = null;
// Keep adding atoms to the body until we can't parse any more atoms (either // Keep adding atoms to the body until we can't parse any more atoms (either
// we reached the end, a }, or a \right) // we reached the end, a }, or a \right)
while (true) { while (true) {
lex = this.lexer.lex(pos, this.mode); var lex = this.nextToken;
var pos = this.pos;
if (endOfExpression.indexOf(lex.text) !== -1) { if (endOfExpression.indexOf(lex.text) !== -1) {
break; break;
} }
if (breakOnToken && lex.text === breakOnToken) { if (breakOnToken && lex.text === breakOnToken) {
break; break;
} }
var atom = this.parseAtom(pos); var atom = this.parseAtom();
if (!atom) { if (!atom) {
if (!this.settings.throwOnError && lex.text[0] === "\\") { if (!this.settings.throwOnError && lex.text[0] === "\\") {
var errorNode = this.handleUnsupportedCmd(lex.text); var errorNode = this.handleUnsupportedCmd();
body.push(errorNode); body.push(errorNode);
pos = lex.position; pos = lex.position;
@ -141,15 +155,15 @@ Parser.prototype.parseExpression = function(pos, breakOnInfix, breakOnToken) {
break; break;
} }
if (breakOnInfix && atom.result.type === "infix") { if (breakOnInfix && atom.type === "infix") {
// rewind so we can parse the infix atom again
this.pos = pos;
this.nextToken = lex;
break; break;
} }
body.push(atom.result); body.push(atom);
pos = atom.position;
} }
var res = new ParseResult(this.handleInfixNodes(body), pos); return this.handleInfixNodes(body);
res.peek = lex;
return res;
}; };
/** /**
@ -211,31 +225,30 @@ var SUPSUB_GREEDINESS = 1;
/** /**
* Handle a subscript or superscript with nice errors. * Handle a subscript or superscript with nice errors.
*/ */
Parser.prototype.handleSupSubscript = function(pos, symbol, name) { Parser.prototype.handleSupSubscript = function(name) {
var group = this.parseGroup(pos); var symbol = this.nextToken.text;
var symPos = this.pos;
this.consume();
var group = this.parseGroup();
if (!group) { if (!group) {
var lex = this.lexer.lex(pos, this.mode); if (!this.settings.throwOnError && this.nextToken.text[0] === "\\") {
return this.handleUnsupportedCmd();
if (!this.settings.throwOnError && lex.text[0] === "\\") {
return new ParseResult(
this.handleUnsupportedCmd(lex.text),
lex.position);
} else { } else {
throw new ParseError( throw new ParseError(
"Expected group after '" + symbol + "'", this.lexer, pos); "Expected group after '" + symbol + "'", this.lexer, symPos + 1);
} }
} else if (group.isFunction) { } else if (group.isFunction) {
// ^ and _ have a greediness, so handle interactions with functions' // ^ and _ have a greediness, so handle interactions with functions'
// greediness // greediness
var funcGreediness = functions[group.result.result].greediness; var funcGreediness = functions[group.result].greediness;
if (funcGreediness > SUPSUB_GREEDINESS) { if (funcGreediness > SUPSUB_GREEDINESS) {
return this.parseFunction(pos); return this.parseFunction(group);
} else { } else {
throw new ParseError( throw new ParseError(
"Got function '" + group.result.result + "' with no arguments " + "Got function '" + group.result + "' with no arguments " +
"as " + name, "as " + name,
this.lexer, pos); this.lexer, symPos + 1);
} }
} else { } else {
return group.result; return group.result;
@ -246,7 +259,8 @@ Parser.prototype.handleSupSubscript = function(pos, symbol, name) {
* Converts the textual input of an unsupported command into a text node * Converts the textual input of an unsupported command into a text node
* contained within a color node whose color is determined by errorColor * contained within a color node whose color is determined by errorColor
*/ */
Parser.prototype.handleUnsupportedCmd = function(text) { Parser.prototype.handleUnsupportedCmd = function() {
var text = this.nextToken.text;
var textordArray = []; var textordArray = [];
for (var i = 0; i < text.length; i++) { for (var i = 0; i < text.length; i++) {
@ -270,84 +284,71 @@ Parser.prototype.handleSupSubscript = function(pos, symbol, name) {
}, },
this.mode); this.mode);
this.consume();
return colorNode; return colorNode;
}; };
/** /**
* Parses a group with optional super/subscripts. * Parses a group with optional super/subscripts.
* *
* @return {?ParseResult} * @return {?ParseNode}
*/ */
Parser.prototype.parseAtom = function(pos) { Parser.prototype.parseAtom = function() {
// The body of an atom is an implicit group, so that things like // The body of an atom is an implicit group, so that things like
// \left(x\right)^2 work correctly. // \left(x\right)^2 work correctly.
var base = this.parseImplicitGroup(pos); var base = this.parseImplicitGroup();
// In text mode, we don't have superscripts or subscripts // In text mode, we don't have superscripts or subscripts
if (this.mode === "text") { if (this.mode === "text") {
return base; return base;
} }
// Handle an empty base // Note that base may be empty (i.e. null) at this point.
var currPos;
if (!base) {
currPos = pos;
base = undefined;
} else {
currPos = base.position;
}
var superscript; var superscript;
var subscript; var subscript;
var result;
while (true) { while (true) {
// Lex the first token // Lex the first token
var lex = this.lexer.lex(currPos, this.mode); var lex = this.nextToken;
if (lex.text === "\\limits" || lex.text === "\\nolimits") { if (lex.text === "\\limits" || lex.text === "\\nolimits") {
// We got a limit control // We got a limit control
if (!base || base.result.type !== "op") { if (!base || base.type !== "op") {
throw new ParseError("Limit controls must follow a math operator", throw new ParseError("Limit controls must follow a math operator",
this.lexer, currPos); this.lexer, this.pos);
} }
else { else {
var limits = lex.text === "\\limits"; var limits = lex.text === "\\limits";
base.result.value.limits = limits; base.value.limits = limits;
base.result.value.alwaysHandleSupSub = true; base.value.alwaysHandleSupSub = true;
currPos = lex.position;
} }
this.consume();
} else if (lex.text === "^") { } else if (lex.text === "^") {
// We got a superscript start // We got a superscript start
if (superscript) { if (superscript) {
throw new ParseError( throw new ParseError(
"Double superscript", this.lexer, currPos); "Double superscript", this.lexer, this.pos);
} }
result = this.handleSupSubscript( superscript = this.handleSupSubscript("superscript");
lex.position, lex.text, "superscript");
currPos = result.position;
superscript = result.result;
} else if (lex.text === "_") { } else if (lex.text === "_") {
// We got a subscript start // We got a subscript start
if (subscript) { if (subscript) {
throw new ParseError( throw new ParseError(
"Double subscript", this.lexer, currPos); "Double subscript", this.lexer, this.pos);
} }
result = this.handleSupSubscript( subscript = this.handleSupSubscript("subscript");
lex.position, lex.text, "subscript");
currPos = result.position;
subscript = result.result;
} else if (lex.text === "'") { } else if (lex.text === "'") {
// We got a prime // We got a prime
var prime = new ParseNode("textord", "\\prime", this.mode); var prime = new ParseNode("textord", "\\prime", this.mode);
// Many primes can be grouped together, so we handle this here // Many primes can be grouped together, so we handle this here
var primes = [prime]; var primes = [prime];
currPos = lex.position; this.consume();
// Keep lexing tokens until we get something that's not a prime // Keep lexing tokens until we get something that's not a prime
while ((lex = this.lexer.lex(currPos, this.mode)).text === "'") { while (this.nextToken.text === "'") {
// For each one, add another prime to the list // For each one, add another prime to the list
primes.push(prime); primes.push(prime);
currPos = lex.position; this.consume();
} }
// Put them into an ordgroup as the superscript // Put them into an ordgroup as the superscript
superscript = new ParseNode("ordgroup", primes, this.mode); superscript = new ParseNode("ordgroup", primes, this.mode);
@ -359,13 +360,11 @@ Parser.prototype.parseAtom = function(pos) {
if (superscript || subscript) { if (superscript || subscript) {
// If we got either a superscript or subscript, create a supsub // If we got either a superscript or subscript, create a supsub
return new ParseResult( return new ParseNode("supsub", {
new ParseNode("supsub", { base: base,
base: base && base.result, sup: superscript,
sup: superscript, sub: subscript
sub: subscript }, this.mode);
}, this.mode),
currPos);
} else { } else {
// Otherwise return the original body // Otherwise return the original body
return base; return base;
@ -392,52 +391,47 @@ var styleFuncs = [
* small text {\Large large text} small text again * small text {\Large large text} small text again
* It is also used for \left and \right to get the correct grouping. * It is also used for \left and \right to get the correct grouping.
* *
* @return {?ParseResult} * @return {?ParseNode}
*/ */
Parser.prototype.parseImplicitGroup = function(pos) { Parser.prototype.parseImplicitGroup = function() {
var start = this.parseSymbol(pos); var start = this.parseSymbol();
if (!start || !start.result) { if (start == null) {
// If we didn't get anything we handle, fall back to parseFunction // If we didn't get anything we handle, fall back to parseFunction
return this.parseFunction(pos); return this.parseFunction();
} }
var func = start.result.result; var func = start.result;
var body; var body;
if (func === "\\left") { if (func === "\\left") {
// If we see a left: // If we see a left:
// Parse the entire left function (including the delimiter) // Parse the entire left function (including the delimiter)
var left = this.parseFunction(pos); var left = this.parseFunction(start);
// Parse out the implicit body // Parse out the implicit body
body = this.parseExpression(left.position, false); body = this.parseExpression(false);
// Check the next token // Check the next token
this.expect(body.peek, "\\right"); this.expect("\\right", false);
var right = this.parseFunction(body.position); var right = this.parseFunction();
return new ParseResult( return new ParseNode("leftright", {
new ParseNode("leftright", { body: body,
body: body.result, left: left.value.value,
left: left.result.value.value, right: right.value.value
right: right.result.value.value }, this.mode);
}, this.mode),
right.position);
} else if (func === "\\begin") { } else if (func === "\\begin") {
// begin...end is similar to left...right // begin...end is similar to left...right
var begin = this.parseFunction(pos); var begin = this.parseFunction(start);
var envName = begin.result.value.name; var envName = begin.value.name;
if (!environments.hasOwnProperty(envName)) { if (!environments.hasOwnProperty(envName)) {
throw new ParseError( throw new ParseError(
"No such environment: " + envName, "No such environment: " + envName,
this.lexer, begin.result.value.namepos); this.lexer, begin.value.namepos);
} }
// Build the environment object. Arguments and other information will // Build the environment object. Arguments and other information will
// be made available to the begin and end methods using properties. // be made available to the begin and end methods using properties.
var env = environments[envName]; var env = environments[envName];
var args = []; var args = this.parseArguments("\\begin{" + envName + "}", env);
var newPos = this.parseArguments(
begin.position, "\\begin{" + envName + "}", env, args);
var context = { var context = {
pos: newPos,
mode: this.mode, mode: this.mode,
envName: envName, envName: envName,
parser: this, parser: this,
@ -445,55 +439,57 @@ Parser.prototype.parseImplicitGroup = function(pos) {
positions: args.pop() positions: args.pop()
}; };
var result = env.handler(context, args); var result = env.handler(context, args);
var endLex = this.lexer.lex(result.position, this.mode); this.expect("\\end", false);
this.expect(endLex, "\\end"); var end = this.parseFunction();
var end = this.parseFunction(result.position); if (end.value.name !== envName) {
if (end.result.value.name !== envName) {
throw new ParseError( throw new ParseError(
"Mismatch: \\begin{" + envName + "} matched " + "Mismatch: \\begin{" + envName + "} matched " +
"by \\end{" + end.result.value.name + "}", "by \\end{" + end.value.name + "}",
this.lexer, end.namepos); this.lexer /* , end.value.namepos */);
// TODO: Add position to the above line and adjust test case,
// requires #385 to get merged first
} }
result.position = end.position; result.position = end.position;
return result; return result;
} else if (utils.contains(sizeFuncs, func)) { } else if (utils.contains(sizeFuncs, func)) {
// If we see a sizing function, parse out the implict body // If we see a sizing function, parse out the implict body
body = this.parseExpression(start.result.position, false); body = this.parseExpression(false);
return new ParseResult( return new ParseNode("sizing", {
new ParseNode("sizing", { // Figure out what size to use based on the list of functions above
// Figure out what size to use based on the list of functions above size: "size" + (utils.indexOf(sizeFuncs, func) + 1),
size: "size" + (utils.indexOf(sizeFuncs, func) + 1), value: body
value: body.result }, this.mode);
}, this.mode),
body.position);
} else if (utils.contains(styleFuncs, func)) { } else if (utils.contains(styleFuncs, func)) {
// If we see a styling function, parse out the implict body // If we see a styling function, parse out the implict body
body = this.parseExpression(start.result.position, true); body = this.parseExpression(true);
return new ParseResult( return new ParseNode("styling", {
new ParseNode("styling", { // Figure out what style to use by pulling out the style from
// Figure out what style to use by pulling out the style from // the function name
// the function name style: func.slice(1, func.length - 5),
style: func.slice(1, func.length - 5), value: body
value: body.result }, this.mode);
}, this.mode),
body.position);
} else { } else {
// Defer to parseFunction if it's not a function we handle // Defer to parseFunction if it's not a function we handle
return this.parseFunction(pos); return this.parseFunction(start);
} }
}; };
/** /**
* Parses an entire function, including its base and all of its arguments * Parses an entire function, including its base and all of its arguments.
* The base might either have been parsed already, in which case
* it is provided as an argument, or it's the next group in the input.
* *
* @return {?ParseResult} * @param {ParseFuncOrArgument=} baseGroup optional as described above
* @return {?ParseNode}
*/ */
Parser.prototype.parseFunction = function(pos) { Parser.prototype.parseFunction = function(baseGroup) {
var baseGroup = this.parseGroup(pos); if (!baseGroup) {
baseGroup = this.parseGroup();
}
if (baseGroup) { if (baseGroup) {
if (baseGroup.isFunction) { if (baseGroup.isFunction) {
var func = baseGroup.result.result; var func = baseGroup.result;
var funcData = functions[func]; var funcData = functions[func];
if (this.mode === "text" && !funcData.allowedInText) { if (this.mode === "text" && !funcData.allowedInText) {
throw new ParseError( throw new ParseError(
@ -501,13 +497,9 @@ Parser.prototype.parseFunction = function(pos) {
this.lexer, baseGroup.position); this.lexer, baseGroup.position);
} }
var args = []; var args = this.parseArguments(func, funcData);
var newPos = this.parseArguments(
baseGroup.result.position, func, funcData, args);
var result = this.callFunction(func, args, args.pop()); var result = this.callFunction(func, args, args.pop());
return new ParseResult( return new ParseNode(result.type, result, this.mode);
new ParseNode(result.type, result, this.mode),
newPos);
} else { } else {
return baseGroup.result; return baseGroup.result;
} }
@ -534,77 +526,73 @@ Parser.prototype.callFunction = function(name, args, positions) {
* *
* @param {string} func "\name" or "\begin{name}" * @param {string} func "\name" or "\begin{name}"
* @param {{numArgs:number,numOptionalArgs:number|undefined}} funcData * @param {{numArgs:number,numOptionalArgs:number|undefined}} funcData
* @param {Array} args list of arguments to which new ones will be pushed * @return the array of arguments, with the list of positions as last element
* @return the position after all arguments have been parsed
*/ */
Parser.prototype.parseArguments = function(pos, func, funcData, args) { Parser.prototype.parseArguments = function(func, funcData) {
var totalArgs = funcData.numArgs + funcData.numOptionalArgs; var totalArgs = funcData.numArgs + funcData.numOptionalArgs;
if (totalArgs === 0) { if (totalArgs === 0) {
return pos; return [[this.pos]];
} }
var newPos = pos;
var baseGreediness = funcData.greediness; var baseGreediness = funcData.greediness;
var positions = [newPos]; var positions = [this.pos];
var args = [];
for (var i = 0; i < totalArgs; i++) { for (var i = 0; i < totalArgs; i++) {
var argType = funcData.argTypes && funcData.argTypes[i]; var argType = funcData.argTypes && funcData.argTypes[i];
var arg; var arg;
if (i < funcData.numOptionalArgs) { if (i < funcData.numOptionalArgs) {
if (argType) { if (argType) {
arg = this.parseSpecialGroup(newPos, argType, true); arg = this.parseSpecialGroup(argType, true);
} else { } else {
arg = this.parseOptionalGroup(newPos); arg = this.parseOptionalGroup();
} }
if (!arg) { if (!arg) {
args.push(null); args.push(null);
positions.push(newPos); positions.push(this.pos);
continue; continue;
} }
} else { } else {
if (argType) { if (argType) {
arg = this.parseSpecialGroup(newPos, argType); arg = this.parseSpecialGroup(argType);
} else { } else {
arg = this.parseGroup(newPos); arg = this.parseGroup();
} }
if (!arg) { if (!arg) {
var lex = this.lexer.lex(newPos, this.mode); if (!this.settings.throwOnError &&
this.nextToken.text[0] === "\\") {
if (!this.settings.throwOnError && lex.text[0] === "\\") {
arg = new ParseFuncOrArgument( arg = new ParseFuncOrArgument(
new ParseResult( this.handleUnsupportedCmd(this.nextToken.text),
this.handleUnsupportedCmd(lex.text),
lex.position),
false); false);
} else { } else {
throw new ParseError( throw new ParseError(
"Expected group after '" + func + "'", this.lexer, pos); "Expected group after '" + func + "'",
this.lexer, this.pos);
} }
} }
} }
var argNode; var argNode;
if (arg.isFunction) { if (arg.isFunction) {
var argGreediness = var argGreediness =
functions[arg.result.result].greediness; functions[arg.result].greediness;
if (argGreediness > baseGreediness) { if (argGreediness > baseGreediness) {
argNode = this.parseFunction(newPos); argNode = this.parseFunction(arg);
} else { } else {
throw new ParseError( throw new ParseError(
"Got function '" + arg.result.result + "' as " + "Got function '" + arg.result + "' as " +
"argument to '" + func + "'", "argument to '" + func + "'",
this.lexer, arg.result.position - 1); this.lexer, this.pos - 1);
} }
} else { } else {
argNode = arg.result; argNode = arg.result;
} }
args.push(argNode.result); args.push(argNode);
positions.push(argNode.position); positions.push(this.pos);
newPos = argNode.position;
} }
args.push(positions); args.push(positions);
return newPos; return args;
}; };
@ -614,7 +602,7 @@ Parser.prototype.parseArguments = function(pos, func, funcData, args) {
* *
* @return {?ParseFuncOrArgument} * @return {?ParseFuncOrArgument}
*/ */
Parser.prototype.parseSpecialGroup = function(pos, innerMode, optional) { Parser.prototype.parseSpecialGroup = function(innerMode, optional) {
var outerMode = this.mode; var outerMode = this.mode;
// Handle `original` argTypes // Handle `original` argTypes
if (innerMode === "original") { if (innerMode === "original") {
@ -624,43 +612,46 @@ Parser.prototype.parseSpecialGroup = function(pos, innerMode, optional) {
if (innerMode === "color" || innerMode === "size") { if (innerMode === "color" || innerMode === "size") {
// color and size modes are special because they should have braces and // color and size modes are special because they should have braces and
// should only lex a single symbol inside // should only lex a single symbol inside
var openBrace = this.lexer.lex(pos, outerMode); var openBrace = this.nextToken;
if (optional && openBrace.text !== "[") { if (optional && openBrace.text !== "[") {
// optional arguments should return null if they don't exist // optional arguments should return null if they don't exist
return null; return null;
} }
this.expect(openBrace, optional ? "[" : "{"); // The call to expect will lex the token after the '{' in inner mode
var inner = this.lexer.lex(openBrace.position, innerMode); this.mode = innerMode;
this.expect(optional ? "[" : "{");
var inner = this.nextToken;
this.mode = outerMode;
var data; var data;
if (innerMode === "color") { if (innerMode === "color") {
data = inner.text; data = inner.text;
} else { } else {
data = inner.data; data = inner.data;
} }
var closeBrace = this.lexer.lex(inner.position, outerMode); this.consume(); // consume the token stored in inner
this.expect(closeBrace, optional ? "]" : "}"); this.expect(optional ? "]" : "}");
return new ParseFuncOrArgument( return new ParseFuncOrArgument(
new ParseResult( new ParseNode(innerMode, data, outerMode),
new ParseNode(innerMode, data, outerMode),
closeBrace.position),
false); false);
} else if (innerMode === "text") { } else if (innerMode === "text") {
// text mode is special because it should ignore the whitespace before // text mode is special because it should ignore the whitespace before
// it // it
var whitespace = this.lexer.lex(pos, "whitespace"); var whitespace = this.lexer.lex(this.pos, "whitespace");
pos = whitespace.position; this.pos = whitespace.position;
} }
// By the time we get here, innerMode is one of "text" or "math". // By the time we get here, innerMode is one of "text" or "math".
// We switch the mode of the parser, recurse, then restore the old mode. // We switch the mode of the parser, recurse, then restore the old mode.
this.mode = innerMode; this.mode = innerMode;
this.nextToken = this.lexer.lex(this.pos, innerMode);
var res; var res;
if (optional) { if (optional) {
res = this.parseOptionalGroup(pos); res = this.parseOptionalGroup();
} else { } else {
res = this.parseGroup(pos); res = this.parseGroup();
} }
this.mode = outerMode; this.mode = outerMode;
this.nextToken = this.lexer.lex(this.pos, outerMode);
return res; return res;
}; };
@ -670,23 +661,20 @@ Parser.prototype.parseSpecialGroup = function(pos, innerMode, optional) {
* *
* @return {?ParseFuncOrArgument} * @return {?ParseFuncOrArgument}
*/ */
Parser.prototype.parseGroup = function(pos) { Parser.prototype.parseGroup = function() {
var start = this.lexer.lex(pos, this.mode);
// Try to parse an open brace // Try to parse an open brace
if (start.text === "{") { if (this.nextToken.text === "{") {
// If we get a brace, parse an expression // If we get a brace, parse an expression
var expression = this.parseExpression(start.position, false); this.consume();
var expression = this.parseExpression(false);
// Make sure we get a close brace // Make sure we get a close brace
var closeBrace = this.lexer.lex(expression.position, this.mode); this.expect("}");
this.expect(closeBrace, "}");
return new ParseFuncOrArgument( return new ParseFuncOrArgument(
new ParseResult( new ParseNode("ordgroup", expression, this.mode),
new ParseNode("ordgroup", expression.result, this.mode),
closeBrace.position),
false); false);
} else { } else {
// Otherwise, just return a nucleus // Otherwise, just return a nucleus
return this.parseSymbol(pos); return this.parseSymbol();
} }
}; };
@ -695,19 +683,16 @@ Parser.prototype.parseGroup = function(pos) {
* *
* @return {?ParseFuncOrArgument} * @return {?ParseFuncOrArgument}
*/ */
Parser.prototype.parseOptionalGroup = function(pos) { Parser.prototype.parseOptionalGroup = function() {
var start = this.lexer.lex(pos, this.mode);
// Try to parse an open bracket // Try to parse an open bracket
if (start.text === "[") { if (this.nextToken.text === "[") {
// If we get a brace, parse an expression // If we get a brace, parse an expression
var expression = this.parseExpression(start.position, false, "]"); this.consume();
var expression = this.parseExpression(false, "]");
// Make sure we get a close bracket // Make sure we get a close bracket
var closeBracket = this.lexer.lex(expression.position, this.mode); this.expect("]");
this.expect(closeBracket, "]");
return new ParseFuncOrArgument( return new ParseFuncOrArgument(
new ParseResult( new ParseNode("ordgroup", expression, this.mode),
new ParseNode("ordgroup", expression.result, this.mode),
closeBracket.position),
false); false);
} else { } else {
// Otherwise, return null, // Otherwise, return null,
@ -721,23 +706,23 @@ Parser.prototype.parseOptionalGroup = function(pos) {
* *
* @return {?ParseFuncOrArgument} * @return {?ParseFuncOrArgument}
*/ */
Parser.prototype.parseSymbol = function(pos) { Parser.prototype.parseSymbol = function() {
var nucleus = this.lexer.lex(pos, this.mode); var nucleus = this.nextToken;
if (functions[nucleus.text]) { if (functions[nucleus.text]) {
this.consume();
// If there exists a function with this name, we return the function and // If there exists a function with this name, we return the function and
// say that it is a function. // say that it is a function.
return new ParseFuncOrArgument( return new ParseFuncOrArgument(
new ParseResult(nucleus.text, nucleus.position), nucleus.text,
true); true);
} else if (symbols[this.mode][nucleus.text]) { } else if (symbols[this.mode][nucleus.text]) {
this.consume();
// Otherwise if this is a no-argument function, find the type it // Otherwise if this is a no-argument function, find the type it
// corresponds to in the symbols map // corresponds to in the symbols map
return new ParseFuncOrArgument( return new ParseFuncOrArgument(
new ParseResult( new ParseNode(symbols[this.mode][nucleus.text].group,
new ParseNode(symbols[this.mode][nucleus.text].group, nucleus.text, this.mode),
nucleus.text, this.mode),
nucleus.position),
false); false);
} else { } else {
return null; return null;

View File

@ -3,39 +3,37 @@ var parseData = require("./parseData");
var ParseError = require("./ParseError"); var ParseError = require("./ParseError");
var ParseNode = parseData.ParseNode; var ParseNode = parseData.ParseNode;
var ParseResult = parseData.ParseResult;
/** /**
* Parse the body of the environment, with rows delimited by \\ and * Parse the body of the environment, with rows delimited by \\ and
* columns delimited by &, and create a nested list in row-major order * columns delimited by &, and create a nested list in row-major order
* with one group per cell. * with one group per cell.
*/ */
function parseArray(parser, pos, result) { function parseArray(parser, result) {
var row = [], body = [row], rowGaps = []; var row = [], body = [row], rowGaps = [];
while (true) { while (true) {
var cell = parser.parseExpression(pos, false, null); var cell = parser.parseExpression(false, null);
row.push(new ParseNode("ordgroup", cell.result, parser.mode)); row.push(new ParseNode("ordgroup", cell, parser.mode));
pos = cell.position; var next = parser.nextToken.text;
var next = cell.peek.text;
if (next === "&") { if (next === "&") {
pos = cell.peek.position; parser.consume();
} else if (next === "\\end") { } else if (next === "\\end") {
break; break;
} else if (next === "\\\\" || next === "\\cr") { } else if (next === "\\\\" || next === "\\cr") {
var cr = parser.parseFunction(pos); var cr = parser.parseFunction();
rowGaps.push(cr.result.value.size); rowGaps.push(cr.value.size);
pos = cr.position;
row = []; row = [];
body.push(row); body.push(row);
} else { } else {
// TODO: Clean up the following hack once #385 got merged
var pos = Math.min(parser.pos + 1, parser.lexer._input.length);
throw new ParseError("Expected & or \\\\ or \\end", throw new ParseError("Expected & or \\\\ or \\end",
parser.lexer, cell.peek.position); parser.lexer, pos);
} }
} }
result.body = body; result.body = body;
result.rowGaps = rowGaps; result.rowGaps = rowGaps;
return new ParseResult( return new ParseNode(result.type, result, parser.mode);
new ParseNode(result.type, result, parser.mode), pos);
} }
/* /*
@ -55,7 +53,6 @@ function parseArray(parser, pos, result) {
* - context: information and references provided by the parser * - context: information and references provided by the parser
* - args: an array of arguments passed to \begin{name} * - args: an array of arguments passed to \begin{name}
* The context contains the following properties: * The context contains the following properties:
* - pos: the current position of the parser.
* - envName: the name of the environment, one of the listed names. * - envName: the name of the environment, one of the listed names.
* - parser: the parser object * - parser: the parser object
* - lexer: the lexer object * - lexer: the lexer object
@ -90,8 +87,6 @@ defineEnvironment("array", {
numArgs: 1 numArgs: 1
}, function(context, args) { }, function(context, args) {
var colalign = args[0]; var colalign = args[0];
var lexer = context.lexer;
var positions = context.positions;
colalign = colalign.value.map ? colalign.value : [colalign]; colalign = colalign.value.map ? colalign.value : [colalign];
var cols = colalign.map(function(node) { var cols = colalign.map(function(node) {
var ca = node.value; var ca = node.value;
@ -108,14 +103,14 @@ defineEnvironment("array", {
} }
throw new ParseError( throw new ParseError(
"Unknown column alignment: " + node.value, "Unknown column alignment: " + node.value,
lexer, positions[1]); context.lexer, context.positions[1]);
}); });
var res = { var res = {
type: "array", type: "array",
cols: cols, cols: cols,
hskipBeforeAndAfter: true // \@preamble in lttab.dtx hskipBeforeAndAfter: true // \@preamble in lttab.dtx
}; };
res = parseArray(context.parser, context.pos, res); res = parseArray(context.parser, res);
return res; return res;
}); });
@ -142,10 +137,10 @@ defineEnvironment([
type: "array", type: "array",
hskipBeforeAndAfter: false // \hskip -\arraycolsep in amsmath hskipBeforeAndAfter: false // \hskip -\arraycolsep in amsmath
}; };
res = parseArray(context.parser, context.pos, res); res = parseArray(context.parser, res);
if (delimiters) { if (delimiters) {
res.result = new ParseNode("leftright", { res = new ParseNode("leftright", {
body: [res.result], body: [res],
left: delimiters[0], left: delimiters[0],
right: delimiters[1] right: delimiters[1]
}, context.mode); }, context.mode);
@ -173,9 +168,9 @@ defineEnvironment("cases", {
postgap: 0 postgap: 0
}] }]
}; };
res = parseArray(context.parser, context.pos, res); res = parseArray(context.parser, res);
res.result = new ParseNode("leftright", { res = new ParseNode("leftright", {
body: [res.result], body: [res],
left: "\\{", left: "\\{",
right: "." right: "."
}, context.mode); }, context.mode);

View File

@ -7,17 +7,7 @@ function ParseNode(type, value, mode) {
this.mode = mode; 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 = { module.exports = {
ParseNode: ParseNode, ParseNode: ParseNode
ParseResult: ParseResult
}; };