Merge pull request #386 from gagern/nextToken

Avoid re-lexing, move position to internal state
This commit is contained in:
Kevin Barabash
2015-11-23 08:55:11 -08:00
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.result value: body
}, 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.result value: body
}, 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
}; };