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. // We didn't match any of the tokens, so throw an error.
throw new ParseError("Unexpected character: '" + input[0] + throw new ParseError("Unexpected character: '" + input[0] +
"' at position " + pos); "'", this, pos);
} }
// A regex to match a CSS color (like #ffffff or BlueViolet) // 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. // 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 // Lex a single token

View File

@ -1,5 +1,24 @@
function ParseError(message) { function ParseError(message, lexer, position) {
var self = new Error("TeX parse error: " + message); 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.name = "ParseError";
self.__proto__ = ParseError.prototype; self.__proto__ = ParseError.prototype;
return self; 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 // Checks a result to make sure it has the right type, and throws an
// appropriate error otherwise. // appropriate error otherwise.
var expect = function(result, type) { Parser.prototype.expect = function(result, type) {
if (result.type !== type) { if (result.type !== type) {
throw new ParseError( 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); var expression = this.parseExpression(pos, mode);
// If we succeeded, make sure there's an EOF at the end // If we succeeded, make sure there's an EOF at the end
var EOF = this.lexer.lex(expression.position, mode); var EOF = this.lexer.lex(expression.position, mode);
expect(EOF, "EOF"); this.expect(EOF, "EOF");
return expression; return expression;
}; };
@ -73,7 +75,8 @@ Parser.prototype.parseExpression = function(pos, mode) {
// Parses a superscript expression, like "^3" // Parses a superscript expression, like "^3"
Parser.prototype.parseSuperscript = function(pos, mode) { Parser.prototype.parseSuperscript = function(pos, mode) {
if (mode !== "math") { 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 // Try to parse a "^" character
@ -85,7 +88,8 @@ Parser.prototype.parseSuperscript = function(pos, mode) {
return group; return group;
} else { } else {
// Throw an error if we didn't find a group // 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 === "'") { } else if (sup.type === "'") {
var pos = sup.position; var pos = sup.position;
@ -99,7 +103,8 @@ Parser.prototype.parseSuperscript = function(pos, mode) {
// Parses a subscript expression, like "_3" // Parses a subscript expression, like "_3"
Parser.prototype.parseSubscript = function(pos, mode) { Parser.prototype.parseSubscript = function(pos, mode) {
if (mode !== "math") { 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 // Try to parse a "_" character
@ -111,7 +116,8 @@ Parser.prototype.parseSubscript = function(pos, mode) {
return group; return group;
} else { } else {
// Throw an error if we didn't find a group // 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 { } else {
return null; return null;
@ -146,7 +152,8 @@ Parser.prototype.parseAtom = function(pos, mode) {
var node; var node;
if ((node = this.parseSuperscript(nextPos, mode))) { if ((node = this.parseSuperscript(nextPos, mode))) {
if (sup) { if (sup) {
throw new ParseError("Parse error: Double superscript"); throw new ParseError(
"Double superscript", this.lexer, nextPos);
} }
nextPos = node.position; nextPos = node.position;
sup = node.result; sup = node.result;
@ -154,7 +161,8 @@ Parser.prototype.parseAtom = function(pos, mode) {
} }
if ((node = this.parseSubscript(nextPos, mode))) { if ((node = this.parseSubscript(nextPos, mode))) {
if (sub) { if (sub) {
throw new ParseError("Parse error: Double subscript"); throw new ParseError(
"Double subscript", this.lexer, nextPos);
} }
nextPos = node.position; nextPos = node.position;
sub = node.result; sub = node.result;
@ -183,7 +191,7 @@ Parser.prototype.parseGroup = function(pos, mode) {
var expression = this.parseExpression(start.position, mode); var expression = this.parseExpression(start.position, mode);
// Make sure we get a close brace // Make sure we get a close brace
var closeBrace = this.lexer.lex(expression.position, mode); var closeBrace = this.lexer.lex(expression.position, mode);
expect(closeBrace, "}"); this.expect(closeBrace, "}");
return new ParseResult( return new ParseResult(
new ParseNode("ordgroup", expression.result, mode), new ParseNode("ordgroup", expression.result, mode),
closeBrace.position); closeBrace.position);
@ -202,14 +210,16 @@ Parser.prototype.parseColorGroup = function(pos, mode) {
var color = this.lexer.lex(start.position, "color"); var color = this.lexer.lex(start.position, "color");
// Make sure we get a close brace // Make sure we get a close brace
var closeBrace = this.lexer.lex(color.position, mode); var closeBrace = this.lexer.lex(color.position, mode);
expect(closeBrace, "}"); this.expect(closeBrace, "}");
return new ParseResult( return new ParseResult(
new ParseNode("color", color.text), new ParseNode("color", color.text),
closeBrace.position); closeBrace.position);
} else { } else {
// It has to have an open brace, so if it doesn't we throw // It has to have an open brace, so if it doesn't we throw
throw new ParseError( 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); group.position);
} else { } else {
throw new ParseError( throw new ParseError(
"Expected group after '" + nucleus.text + "'"); "Expected group after '" + nucleus.text + "'",
this.lexer, nucleus.position
);
} }
} else if (nucleus.type === "\\color") { } else if (nucleus.type === "\\color") {
// If this is a custom color function, parse its first argument as a // 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); inner.position);
} else { } else {
throw new ParseError( throw new ParseError(
"Expected second group after '" + nucleus.text + "'"); "Expected second group after '" + nucleus.text + "'",
this.lexer, color.position
);
} }
} else { } else {
throw new ParseError( 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)) { } else if (mode === "math" && utils.contains(sizeFuncs, nucleus.type)) {
// If this is a size function, parse its argument and return // If this is a size function, parse its argument and return
@ -294,7 +310,9 @@ Parser.prototype.parseNucleus = function(pos, mode) {
group.position); group.position);
} else { } else {
throw new ParseError( 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)) { } else if (mode === "math" && utils.contains(namedFns, nucleus.type)) {
// If this is a named function, just return it plain // If this is a named function, just return it plain
@ -310,7 +328,9 @@ Parser.prototype.parseNucleus = function(pos, mode) {
group.position); group.position);
} else { } else {
throw new ParseError( throw new ParseError(
"Expected group after '" + nucleus.text + "'"); "Expected group after '" + nucleus.text + "'",
this.lexer, nucleus.position
);
} }
} else if (mode === "math" && nucleus.type === "\\text") { } else if (mode === "math" && nucleus.type === "\\text") {
var group = this.parseGroup(nucleus.position, "text"); var group = this.parseGroup(nucleus.position, "text");
@ -320,7 +340,9 @@ Parser.prototype.parseNucleus = function(pos, mode) {
group.position); group.position);
} else { } else {
throw new ParseError( throw new ParseError(
"Expected group after '" + nucleus.text + "'"); "Expected group after '" + nucleus.text + "'",
this.lexer, nucleus.position
);
} }
} else if (mode === "math" && (nucleus.type === "\\dfrac" || } else if (mode === "math" && (nucleus.type === "\\dfrac" ||
nucleus.type === "\\frac" || nucleus.type === "\\frac" ||
@ -339,11 +361,15 @@ Parser.prototype.parseNucleus = function(pos, mode) {
denom.position); denom.position);
} else { } else {
throw new ParseError("Expected denominator after '" + throw new ParseError("Expected denominator after '" +
nucleus.type + "'"); nucleus.type + "'",
this.lexer, numer.position
);
} }
} else { } else {
throw new ParseError("Parse error: Expected numerator after '" + throw new ParseError("Expected numerator after '" +
nucleus.type + "'"); nucleus.type + "'",
this.lexer, nucleus.position
);
} }
} else if (mode === "math" && nucleus.type === "\\KaTeX") { } else if (mode === "math" && nucleus.type === "\\KaTeX") {
// If this is a KaTeX node, return the special katex result // 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) { if (options.depth > 1) {
throw new ParseError( 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; groupNode.height *= multiplier;
@ -450,7 +450,7 @@ var buildGroup = function(group, options, prev) {
return groupNode; return groupNode;
} else { } else {
throw new ParseError( throw new ParseError(
"Lex error: Got group of unknown type: '" + group.type + "'"); "Got group of unknown type: '" + group.type + "'");
} }
}; };