mirror of
https://github.com/Smaug123/KaTeX
synced 2025-10-11 22:18:41 +00:00
Merge pull request #386 from gagern/nextToken
Avoid re-lexing, move position to internal state
This commit is contained in:
373
src/Parser.js
373
src/Parser.js
@@ -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;
|
||||||
|
@@ -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);
|
||||||
|
@@ -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
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user