Make errors more informative

Make error messages from the lexing and parsing stages be a bit more helpful. If
provided with the input and a position, the error will display the error
position, and the nearby input with the error position underlined (yay combining
marks). Also, standardize the errors a bit (remove doubled "Error:" strings)

Test plan:
 - Make sure the errors look totally sweet (before: {F15602}, after: {F15603})
 - Trigger every error (that can be triggered) in Parser, Lexer, and buildTree
   using the inputs:
  `a^`
  `a_`
  `a^x^x`
  `a_x_x`
  `\color f`
  `\blue `
  `\Huge`
  `\llap`
  `\text`
  `\dfrac`
  `\dfrac{x}`
  `\d`
  `\blue{`
  `\color{#f`
  `{\Huge{x}}`
 - See that the tests still work

Auditors: alpert
This commit is contained in:
Emily Eisenberg 2014-03-29 23:30:25 -04:00
parent c22d8644cc
commit e68cc472c6
4 changed files with 72 additions and 27 deletions

View File

@ -80,7 +80,7 @@ Lexer.prototype._innerLex = function(pos, normals, ignoreWhitespace) {
// We didn't match any of the tokens, so throw an error.
throw new ParseError("Unexpected character: '" + input[0] +
"' at position " + pos);
"'", this, pos);
}
// A regex to match a CSS color (like #ffffff or BlueViolet)
@ -101,7 +101,7 @@ Lexer.prototype._innerLexColor = function(pos) {
}
// We didn't match a color, so throw an error.
throw new ParseError("Invalid color at position " + pos);
throw new ParseError("Invalid color", this, pos);
};
// Lex a single token

View File

@ -1,5 +1,24 @@
function ParseError(message) {
var self = new Error("TeX parse error: " + message);
function ParseError(message, lexer, position) {
var error = "KaTeX parse error: " + message;
if (lexer !== undefined && position !== undefined) {
// If we have the input and a position, make the error a bit fancier
// Prepend some information
error += " at position " + position + ": ";
// Get the input
var input = lexer._input;
// Insert a combining underscore at the correct position
input = input.slice(0, position) + "\u0332" +
input.slice(position);
// Extract some context from the input and add it to the error
var begin = Math.max(0, position - 15);
var end = position + 15;
error += input.slice(begin, end);
}
var self = new Error(error);
self.name = "ParseError";
self.__proto__ = ParseError.prototype;
return self;

View File

@ -24,10 +24,12 @@ function ParseNode(type, value, mode) {
// Checks a result to make sure it has the right type, and throws an
// appropriate error otherwise.
var expect = function(result, type) {
Parser.prototype.expect = function(result, type) {
if (result.type !== type) {
throw new ParseError(
"Expected '" + type + "', got '" + result.type + "'");
"Expected '" + type + "', got '" + result.type + "'",
this.lexer, result.position
);
}
};
@ -48,7 +50,7 @@ Parser.prototype.parseInput = function(pos, mode) {
var expression = this.parseExpression(pos, mode);
// If we succeeded, make sure there's an EOF at the end
var EOF = this.lexer.lex(expression.position, mode);
expect(EOF, "EOF");
this.expect(EOF, "EOF");
return expression;
};
@ -73,7 +75,8 @@ Parser.prototype.parseExpression = function(pos, mode) {
// Parses a superscript expression, like "^3"
Parser.prototype.parseSuperscript = function(pos, mode) {
if (mode !== "math") {
throw new ParseError("Trying to parse superscript in non-math mode");
throw new ParseError(
"Trying to parse superscript in non-math mode", this.lexer, pos);
}
// Try to parse a "^" character
@ -85,7 +88,8 @@ Parser.prototype.parseSuperscript = function(pos, mode) {
return group;
} else {
// Throw an error if we didn't find a group
throw new ParseError("Couldn't find group after '^'");
throw new ParseError(
"Couldn't find group after '^'", this.lexer, sup.position);
}
} else if (sup.type === "'") {
var pos = sup.position;
@ -99,7 +103,8 @@ Parser.prototype.parseSuperscript = function(pos, mode) {
// Parses a subscript expression, like "_3"
Parser.prototype.parseSubscript = function(pos, mode) {
if (mode !== "math") {
throw new ParseError("Trying to parse subscript in non-math mode");
throw new ParseError(
"Trying to parse subscript in non-math mode", this.lexer, pos);
}
// Try to parse a "_" character
@ -111,7 +116,8 @@ Parser.prototype.parseSubscript = function(pos, mode) {
return group;
} else {
// Throw an error if we didn't find a group
throw new ParseError("Couldn't find group after '_'");
throw new ParseError(
"Couldn't find group after '_'", this.lexer, sub.position);
}
} else {
return null;
@ -146,7 +152,8 @@ Parser.prototype.parseAtom = function(pos, mode) {
var node;
if ((node = this.parseSuperscript(nextPos, mode))) {
if (sup) {
throw new ParseError("Parse error: Double superscript");
throw new ParseError(
"Double superscript", this.lexer, nextPos);
}
nextPos = node.position;
sup = node.result;
@ -154,7 +161,8 @@ Parser.prototype.parseAtom = function(pos, mode) {
}
if ((node = this.parseSubscript(nextPos, mode))) {
if (sub) {
throw new ParseError("Parse error: Double subscript");
throw new ParseError(
"Double subscript", this.lexer, nextPos);
}
nextPos = node.position;
sub = node.result;
@ -183,7 +191,7 @@ Parser.prototype.parseGroup = function(pos, mode) {
var expression = this.parseExpression(start.position, mode);
// Make sure we get a close brace
var closeBrace = this.lexer.lex(expression.position, mode);
expect(closeBrace, "}");
this.expect(closeBrace, "}");
return new ParseResult(
new ParseNode("ordgroup", expression.result, mode),
closeBrace.position);
@ -202,14 +210,16 @@ Parser.prototype.parseColorGroup = function(pos, mode) {
var color = this.lexer.lex(start.position, "color");
// Make sure we get a close brace
var closeBrace = this.lexer.lex(color.position, mode);
expect(closeBrace, "}");
this.expect(closeBrace, "}");
return new ParseResult(
new ParseNode("color", color.text),
closeBrace.position);
} else {
// It has to have an open brace, so if it doesn't we throw
throw new ParseError(
"Parse error: There must be braces around colors");
"There must be braces around colors",
this.lexer, pos
);
}
};
@ -254,7 +264,9 @@ Parser.prototype.parseNucleus = function(pos, mode) {
group.position);
} else {
throw new ParseError(
"Expected group after '" + nucleus.text + "'");
"Expected group after '" + nucleus.text + "'",
this.lexer, nucleus.position
);
}
} else if (nucleus.type === "\\color") {
// If this is a custom color function, parse its first argument as a
@ -276,11 +288,15 @@ Parser.prototype.parseNucleus = function(pos, mode) {
inner.position);
} else {
throw new ParseError(
"Expected second group after '" + nucleus.text + "'");
"Expected second group after '" + nucleus.text + "'",
this.lexer, color.position
);
}
} else {
throw new ParseError(
"Expected color after '" + nucleus.text + "'");
"Expected color after '" + nucleus.text + "'",
this.lexer, nucleus.position
);
}
} else if (mode === "math" && utils.contains(sizeFuncs, nucleus.type)) {
// If this is a size function, parse its argument and return
@ -294,7 +310,9 @@ Parser.prototype.parseNucleus = function(pos, mode) {
group.position);
} else {
throw new ParseError(
"Expected group after '" + nucleus.text + "'");
"Expected group after '" + nucleus.text + "'",
this.lexer, nucleus.position
);
}
} else if (mode === "math" && utils.contains(namedFns, nucleus.type)) {
// If this is a named function, just return it plain
@ -310,7 +328,9 @@ Parser.prototype.parseNucleus = function(pos, mode) {
group.position);
} else {
throw new ParseError(
"Expected group after '" + nucleus.text + "'");
"Expected group after '" + nucleus.text + "'",
this.lexer, nucleus.position
);
}
} else if (mode === "math" && nucleus.type === "\\text") {
var group = this.parseGroup(nucleus.position, "text");
@ -320,7 +340,9 @@ Parser.prototype.parseNucleus = function(pos, mode) {
group.position);
} else {
throw new ParseError(
"Expected group after '" + nucleus.text + "'");
"Expected group after '" + nucleus.text + "'",
this.lexer, nucleus.position
);
}
} else if (mode === "math" && (nucleus.type === "\\dfrac" ||
nucleus.type === "\\frac" ||
@ -339,11 +361,15 @@ Parser.prototype.parseNucleus = function(pos, mode) {
denom.position);
} else {
throw new ParseError("Expected denominator after '" +
nucleus.type + "'");
nucleus.type + "'",
this.lexer, numer.position
);
}
} else {
throw new ParseError("Parse error: Expected numerator after '" +
nucleus.type + "'");
throw new ParseError("Expected numerator after '" +
nucleus.type + "'",
this.lexer, nucleus.position
);
}
} else if (mode === "math" && nucleus.type === "\\KaTeX") {
// If this is a KaTeX node, return the special katex result

View File

@ -440,7 +440,7 @@ var buildGroup = function(group, options, prev) {
if (options.depth > 1) {
throw new ParseError(
"Error: Can't use sizing outside of the root node");
"Can't use sizing outside of the root node");
}
groupNode.height *= multiplier;
@ -450,7 +450,7 @@ var buildGroup = function(group, options, prev) {
return groupNode;
} else {
throw new ParseError(
"Lex error: Got group of unknown type: '" + group.type + "'");
"Got group of unknown type: '" + group.type + "'");
}
};