Fix our parsing strategy so it is the same as (or very similar to) TeX's

Summary:
Rewrote the parser to make this work, and added a bunch of tests to make sure
this does work. In the process, refactored all of our functions into a separate
file, functions.js. Added a bunch of comments to Parser.js. Also, update jasmine
so we can make our tests better, and add a lint_blacklist.txt to not lint bad
jasmine stuff.

Fixes #10
Fixes #12

Test Plan:
- Make sure all of the tests still work, and all of the new ones work also
- Make sure huxley screenshots didn't change

Reviewers: alpert

Reviewed By: alpert

Differential Revision: http://phabricator.khanacademy.org/D12989
This commit is contained in:
Emily Eisenberg 2014-09-09 23:18:37 -07:00
parent 5b4fa72299
commit 0c9e9738c3
15 changed files with 4330 additions and 4026 deletions

View File

@ -132,6 +132,15 @@ Lexer.prototype._innerLexSize = function(pos) {
throw new ParseError("Invalid size", this, pos);
};
Lexer.prototype._innerLexWhitespace = function(pos) {
var input = this._input.slice(pos);
var whitespace = input.match(whitespaceRegex)[0];
pos += whitespace.length;
return new LexResult("whitespace", whitespace, pos);
};
// Lex a single token
Lexer.prototype.lex = function(pos, mode) {
if (mode === "math") {
@ -142,6 +151,8 @@ Lexer.prototype.lex = function(pos, mode) {
return this._innerLexColor(pos);
} else if (mode === "size") {
return this._innerLexSize(pos);
} else if (mode === "whitespace") {
return this._innerLexWhitespace(pos);
}
};

860
Parser.js
View File

@ -1,19 +1,48 @@
var functions = require("./functions");
var Lexer = require("./Lexer");
var utils = require("./utils");
var symbols = require("./symbols");
var utils = require("./utils");
var ParseError = require("./ParseError");
// Main Parser class
function Parser() {
};
// This file contains the parser used to parse out a TeX expression from the
// input. Since TeX isn't context-free, standard parsers don't work particularly
// well.
// Returned by the Parser.parse... functions. Stores the current results and
// the new lexer position.
function ParseResult(result, newPosition) {
this.result = result;
this.position = newPosition;
}
// The strategy of this parser is as such:
//
// The main functions (the `.parse...` ones) take a position in the current
// parse string to parse tokens from. The lexer (found in Lexer.js, stored at
// this.lexer) also supports pulling out tokens at arbitrary places. When
// individual tokens are needed at a position, the lexer is called to pull out a
// token, which is then used.
//
// The main functions also take a mode that the parser is currently in
// (currently "math" or "text"), which denotes whether the current environment
// is a math-y one or a text-y one (e.g. inside \text). Currently, this serves
// to limit the functions which can be used in text mode.
//
// The main functions then return an object which contains the useful data that
// was parsed at its given point, and a new position at the end of the parsed
// data. The main functions can call each other and continue the parsing by
// using the returned position as a new starting point.
//
// There are also extra `.handle...` functions, which pull out some reused
// functionality into self-contained functions.
//
// The earlier functions return `ParseResult`s, which contain a ParseNode and a
// new position.
//
// The later functions (which are called deeper in the parse) sometimes return
// ParseFuncOrArgument, which contain a ParseResult as well as some data about
// 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.
// Main Parser class
function Parser(input) {
// Make a new lexer
this.lexer = new Lexer(input);
};
// The resulting parse tree nodes of the parse tree.
function ParseNode(type, value, mode) {
@ -22,6 +51,26 @@ function ParseNode(type, value, mode) {
this.mode = mode;
}
// A result and final position returned by the `.parse...` functions.
function ParseResult(result, newPosition) {
this.result = result;
this.position = newPosition;
}
// An initial function (without its arguments), or an argument to a function.
// The `result` argument should be a ParseResult.
function ParseFuncOrArgument(result, isFunction, allowedInText, numArgs, argTypes) {
this.result = result;
// Is this a function (i.e. is it something defined in functions.js)?
this.isFunction = isFunction;
// Is it allowed in text mode?
this.allowedInText = allowedInText;
// How many arguments?
this.numArgs = numArgs;
// What types of arguments?
this.argTypes = argTypes;
}
// Checks a result to make sure it has the right type, and throws an
// appropriate error otherwise.
Parser.prototype.expect = function(result, type) {
@ -36,9 +85,6 @@ Parser.prototype.expect = function(result, type) {
// Main parsing function, which parses an entire input. Returns either a list
// of parseNodes or null if the parse fails.
Parser.prototype.parse = function(input) {
// Make a new lexer
this.lexer = new Lexer(input);
// Try to parse the input
var parse = this.parseInput(0, "math");
return parse.result;
@ -54,135 +100,312 @@ Parser.prototype.parseInput = function(pos, mode) {
return expression;
};
// Handles a body of an expression
Parser.prototype.handleExpressionBody = function(pos, mode) {
var body = [];
var atom;
// Keep adding atoms to the body until we can't parse any more atoms (either
// we reached the end, a }, or a \right)
while ((atom = this.parseAtom(pos, mode))) {
body.push(atom.result);
pos = atom.position;
}
return {
body: body,
position: pos
};
};
// Parses an "expression", which is a list of atoms
//
// Returns ParseResult
Parser.prototype.parseExpression = function(pos, mode) {
// Start with a list of nodes
var expression = [];
while (true) {
// Try to parse atoms
var parse = this.parseAtom(pos, mode);
if (parse) {
// Copy them into the list
expression.push(parse.result);
pos = parse.position;
var body = this.handleExpressionBody(pos, mode);
return new ParseResult(body.body, body.position);
};
// The greediness of a superscript or subscript
var SUPSUB_GREEDINESS = 1;
// Handle a subscript or superscript with nice errors
Parser.prototype.handleSupSubscript = function(pos, mode, symbol, name) {
var group = this.parseGroup(pos, mode);
if (!group) {
throw new ParseError(
"Expected group after '" + symbol + "'", this.lexer, pos);
} else if (group.numArgs > 0) {
// ^ and _ have a greediness, so handle interactions with functions'
// greediness
var funcGreediness = functions.getGreediness(group.result.result);
if (funcGreediness > SUPSUB_GREEDINESS) {
return this.parseFunction(pos, mode);
} else {
throw new ParseError(
"Got function '" + group.result.result + "' with no arguments " +
"as " + name,
this.lexer, pos);
}
} else {
return group.result;
}
};
// Parses a group with optional super/subscripts
//
// Returns ParseResult or null
Parser.prototype.parseAtom = function(pos, mode) {
// The body of an atom is an implicit group, so that things like
// \left(x\right)^2 work correctly.
var base = this.parseImplicitGroup(pos, mode);
// In text mode, we don't have superscripts or subscripts
if (mode === "text") {
return base;
}
// Handle an empty base
var currPos;
if (!base) {
currPos = pos;
base = undefined;
} else {
currPos = base.position;
}
var superscript;
var subscript;
while (true) {
// Lex the first token
var lex = this.lexer.lex(currPos, mode);
var group;
if (lex.type === "^") {
// We got a superscript start
if (superscript) {
throw new ParseError(
"Double superscript", this.lexer, currPos);
}
var result = this.handleSupSubscript(
lex.position, mode, lex.type, "superscript");
currPos = result.position;
superscript = result.result;
} else if (lex.type === "_") {
// We got a subscript start
if (subscript) {
throw new ParseError(
"Double subscript", this.lexer, currPos);
}
var result = this.handleSupSubscript(
lex.position, mode, lex.type, "subscript");
currPos = result.position;
subscript = result.result;
} else if (lex.type === "'") {
// We got a prime
var prime = new ParseNode("textord", "\\prime", mode);
// Many primes can be grouped together, so we handle this here
var primes = [prime];
currPos = lex.position;
// Keep lexing tokens until we get something that's not a prime
while ((lex = this.lexer.lex(currPos, mode)).type === "'") {
// For each one, add another prime to the list
primes.push(prime);
currPos = lex.position;
}
// Put them into an ordgroup as the superscript
superscript = new ParseNode("ordgroup", primes, mode);
} else {
// If it wasn't ^, _, or ', stop parsing super/subscripts
break;
}
}
return new ParseResult(expression, pos);
};
// 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", this.lexer, pos);
}
// Try to parse a "^" character
var sup = this.lexer.lex(pos, mode);
if (sup.type === "^") {
// If we got one, parse the corresponding group
var group = this.parseGroup(sup.position, mode);
if (group) {
return group;
} else {
// Throw an error if we didn't find a group
throw new ParseError(
"Couldn't find group after '^'", this.lexer, sup.position);
}
} else if (sup.type === "'") {
var pos = sup.position;
if (superscript || subscript) {
// If we got either a superscript or subscript, create a supsub
return new ParseResult(
new ParseNode("textord", "\\prime", mode), sup.position, mode);
new ParseNode("supsub", {
base: base && base.result,
sup: superscript,
sub: subscript
}, mode),
currPos);
} else {
return null;
// Otherwise return the original body
return base;
}
};
// 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", this.lexer, pos);
// A list of the size-changing functions, for use in parseImplicitGroup
var sizeFuncs = [
"\\tiny", "\\scriptsize", "\\footnotesize", "\\small", "\\normalsize",
"\\large", "\\Large", "\\LARGE", "\\huge", "\\Huge"
];
// Parses an implicit group, which is a group that starts at the end of a
// specified, and ends right before a higher explicit group ends, or at EOL. It
// is used for functions that appear to affect the current style, like \Large or
// \textrm, where instead of keeping a style we just pretend that there is an
// implicit grouping after it until the end of the group. E.g.
// small text {\Large large text} small text again
// It is also used for \left and \right to get the correct grouping.
//
// Returns ParseResult or null
Parser.prototype.parseImplicitGroup = function(pos, mode) {
var start = this.parseSymbol(pos, mode);
if (!start || !start.result) {
// If we didn't get anything we handle, fall back to parseFunction
return this.parseFunction(pos, mode);
}
// Try to parse a "_" character
var sub = this.lexer.lex(pos, mode);
if (sub.type === "_") {
// If we got one, parse the corresponding group
var group = this.parseGroup(sub.position, mode);
if (group) {
return group;
if (start.result.result === "\\left") {
// If we see a left:
// Parse the entire left function (including the delimiter)
var left = this.parseFunction(pos, mode);
// Parse out the implicit body
var body = this.handleExpressionBody(left.position, mode);
// Check the next token
var rightLex = this.parseSymbol(body.position, mode);
if (rightLex && rightLex.result.result === "\\right") {
// If it's a \right, parse the entire right function (including the delimiter)
var right = this.parseFunction(body.position, mode);
return new ParseResult(
new ParseNode("leftright", {
body: body.body,
left: left.result.value.value,
right: right.result.value.value
}, mode),
right.position);
} else {
// Throw an error if we didn't find a group
throw new ParseError(
"Couldn't find group after '_'", this.lexer, sub.position);
throw new ParseError("Missing \\right", this.lexer, body.position);
}
} else if (start.result.result === "\\right") {
// If we see a right, explicitly fail the parsing here so the \left
// handling ends the group
return null;
} else if (utils.contains(sizeFuncs, start.result.result)) {
// If we see a sizing function, parse out the implict body
var body = this.handleExpressionBody(start.result.position, mode);
return new ParseResult(
new ParseNode("sizing", {
// Figure out what size to use based on the list of functions above
size: "size" + (utils.indexOf(sizeFuncs, start.result.result) + 1),
value: body.body
}, mode),
body.position);
} else {
// Defer to parseFunction if it's not a function we handle
return this.parseFunction(pos, mode);
}
};
// Parses an entire function, including its base and all of its arguments
//
// Returns ParseResult or null
Parser.prototype.parseFunction = function(pos, mode) {
var baseGroup = this.parseGroup(pos, mode);
if (baseGroup) {
if (baseGroup.isFunction) {
var func = baseGroup.result.result;
if (mode === "text" && !baseGroup.allowedInText) {
throw new ParseError(
"Can't use function '" + func + "' in text mode",
this.lexer, baseGroup.position);
}
var newPos = baseGroup.result.position;
var result;
if (baseGroup.numArgs > 0) {
var baseGreediness = functions.getGreediness(func);
var args = [func];
var positions = [newPos];
for (var i = 0; i < baseGroup.numArgs; i++) {
var argType = baseGroup.argTypes && baseGroup.argTypes[i];
if (argType) {
var arg = this.parseSpecialGroup(newPos, argType, mode);
} else {
var arg = this.parseGroup(newPos, mode);
}
if (!arg) {
throw new ParseError(
"Expected group after '" + baseGroup.result.result +
"'",
this.lexer, newPos);
}
var argNode;
if (arg.numArgs > 0) {
var argGreediness = functions.getGreediness(arg.result.result);
if (argGreediness > baseGreediness) {
argNode = this.parseFunction(newPos, mode);
} else {
throw new ParseError(
"Got function '" + arg.result.result + "' as " +
"argument to function '" +
baseGroup.result.result + "'",
this.lexer, arg.result.position - 1);
}
} else {
argNode = arg.result;
}
args.push(argNode.result);
positions.push(argNode.position);
newPos = argNode.position;
}
args.push(positions);
result = functions.funcs[func].handler.apply(this, args);
} else {
result = functions.funcs[func].handler.apply(this, [func]);
}
return new ParseResult(
new ParseNode(result.type, result, mode),
newPos);
} else {
return baseGroup.result;
}
} else {
return null;
}
};
// Parses an atom, which consists of a nucleus, and an optional superscript and
// subscript
Parser.prototype.parseAtom = function(pos, mode) {
// Parse the nucleus
var nucleus = this.parseGroup(pos, mode);
var nextPos = pos;
var nucleusNode;
// Text mode doesn't have superscripts or subscripts, so we only parse the
// nucleus in this case
if (mode === "text") {
return nucleus;
}
if (nucleus) {
nextPos = nucleus.position;
nucleusNode = nucleus.result;
}
var sup;
var sub;
// Now, we try to parse a subscript or a superscript (or both!), and
// depending on whether those succeed, we return the correct type.
while (true) {
var node;
if ((node = this.parseSuperscript(nextPos, mode))) {
if (sup) {
throw new ParseError(
"Double superscript", this.lexer, nextPos);
}
nextPos = node.position;
sup = node.result;
continue;
}
if ((node = this.parseSubscript(nextPos, mode))) {
if (sub) {
throw new ParseError(
"Double subscript", this.lexer, nextPos);
}
nextPos = node.position;
sub = node.result;
continue;
}
break;
}
if (sup || sub) {
return new ParseResult(
new ParseNode("supsub", {base: nucleusNode, sup: sup,
sub: sub}, mode),
nextPos);
// Parses a group when the mode is changing. Takes a position, a new mode, and
// an outer mode that is used to parse the outside.
//
// Returns a ParseFuncOrArgument or null
Parser.prototype.parseSpecialGroup = function(pos, mode, outerMode) {
if (mode === "color" || mode === "size") {
// color and size modes are special because they should have braces and
// should only lex a single symbol inside
var openBrace = this.lexer.lex(pos, outerMode);
this.expect(openBrace, "{");
var inner = this.lexer.lex(openBrace.position, mode);
var closeBrace = this.lexer.lex(inner.position, outerMode);
this.expect(closeBrace, "}");
return new ParseFuncOrArgument(
new ParseResult(
new ParseNode("color", inner.text, outerMode),
closeBrace.position),
false);
} else if (mode === "text") {
// text mode is special because it should ignore the whitespace before
// it
var whitespace = this.lexer.lex(pos, "whitespace");
return this.parseGroup(whitespace.position, mode);
} else {
return nucleus;
return this.parseGroup(pos, mode);
}
}
};
// Parses a group, which is either a single nucleus (like "x") or an expression
// in braces (like "{x+y}")
//
// Returns a ParseFuncOrArgument or null
Parser.prototype.parseGroup = function(pos, mode) {
var start = this.lexer.lex(pos, mode);
// Try to parse an open brace
@ -192,395 +415,52 @@ Parser.prototype.parseGroup = function(pos, mode) {
// Make sure we get a close brace
var closeBrace = this.lexer.lex(expression.position, mode);
this.expect(closeBrace, "}");
return new ParseResult(
new ParseNode("ordgroup", expression.result, mode),
closeBrace.position);
return new ParseFuncOrArgument(
new ParseResult(
new ParseNode("ordgroup", expression.result, mode),
closeBrace.position),
false);
} else {
// Otherwise, just return a nucleus
return this.parseNucleus(pos, mode);
return this.parseSymbol(pos, mode);
}
};
// Parses an implicit group, which is a group that starts where you want it, and
// ends right before a higher explicit group ends, or at EOL. It is used for
// functions that appear to affect the current style, like \Large or \textrm,
// where instead of keeping a style we just pretend that there is an implicit
// grouping after it until the end of the group.
Parser.prototype.parseImplicitGroup = function(pos, mode) {
// Since parseExpression already ends where we want it to, we just need to
// call that and it does what we want.
var expression = this.parseExpression(pos, mode);
return new ParseResult(
new ParseNode("ordgroup", expression.result, mode),
expression.position);
};
// Parses a custom color group, which looks like "{#ffffff}"
Parser.prototype.parseColorGroup = function(pos, mode) {
var start = this.lexer.lex(pos, mode);
// Try to parse an open brace
if (start.type === "{") {
// Parse the color
var color = this.lexer.lex(start.position, "color");
// Make sure we get a close brace
var closeBrace = this.lexer.lex(color.position, mode);
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(
"There must be braces around colors",
this.lexer, pos
);
}
};
// Parses a text group, which looks like "{#ffffff}"
Parser.prototype.parseTextGroup = function(pos, mode) {
var start = this.lexer.lex(pos, mode);
// Try to parse an open brace
if (start.type === "{") {
// Parse the text
var text = this.parseExpression(start.position, "text");
// Make sure we get a close brace
var closeBrace = this.lexer.lex(text.position, mode);
this.expect(closeBrace, "}");
return new ParseResult(
new ParseNode("ordgroup", text.result, "text"),
closeBrace.position);
} else {
// It has to have an open brace, so if it doesn't we throw
throw new ParseError(
"There must be braces around text",
this.lexer, pos
);
}
};
Parser.prototype.parseSizeGroup = function(pos, mode) {
var start = this.lexer.lex(pos, mode);
// Try to parse an open brace
if (start.type === "{") {
// Parse the size
var size = this.lexer.lex(start.position, "size");
// Make sure we get a close brace
var closeBrace = this.lexer.lex(size.position, mode);
this.expect(closeBrace, "}");
return new ParseResult(
new ParseNode("size", size.text),
closeBrace.position);
} else {
// It has to have an open brace, so if it doesn't we throw
throw new ParseError(
"There must be braces around sizes",
this.lexer, pos
);
}
};
var delimiters = [
"(", ")", "[", "\\lbrack", "]", "\\rbrack",
"\\{", "\\lbrace", "\\}", "\\rbrace",
"\\lfloor", "\\rfloor", "\\lceil", "\\rceil",
"<", ">", "\\langle", "\\rangle",
"/", "\\backslash",
"|", "\\vert", "\\|", "\\Vert",
"\\uparrow", "\\Uparrow",
"\\downarrow", "\\Downarrow",
"\\updownarrow", "\\Updownarrow",
"."
];
// Parse a single delimiter
Parser.prototype.parseDelimiter = function(pos, mode) {
var delim = this.lexer.lex(pos, mode);
if (utils.contains(delimiters, delim.text)) {
return new ParseResult(
new ParseNode("delimiter", delim.text),
delim.position);
} else {
return null;
}
};
// A list of 1-argument color functions
var colorFuncs = [
"\\blue", "\\orange", "\\pink", "\\red", "\\green", "\\gray", "\\purple"
];
// A list of 1-argument sizing functions
var sizeFuncs = [
"\\tiny", "\\scriptsize", "\\footnotesize", "\\small", "\\normalsize",
"\\large", "\\Large", "\\LARGE", "\\huge", "\\Huge"
];
// A list of math functions replaced by their names
var namedFns = [
"\\arcsin", "\\arccos", "\\arctan", "\\arg", "\\cos", "\\cosh",
"\\cot", "\\coth", "\\csc", "\\deg", "\\dim", "\\exp", "\\hom",
"\\ker", "\\lg", "\\ln", "\\log", "\\sec", "\\sin", "\\sinh",
"\\tan","\\tanh"
];
var delimiterSizes = {
"\\bigl" : {type: "open", size: 1},
"\\Bigl" : {type: "open", size: 2},
"\\biggl": {type: "open", size: 3},
"\\Biggl": {type: "open", size: 4},
"\\bigr" : {type: "close", size: 1},
"\\Bigr" : {type: "close", size: 2},
"\\biggr": {type: "close", size: 3},
"\\Biggr": {type: "close", size: 4},
"\\bigm" : {type: "rel", size: 1},
"\\Bigm" : {type: "rel", size: 2},
"\\biggm": {type: "rel", size: 3},
"\\Biggm": {type: "rel", size: 4},
"\\big" : {type: "textord", size: 1},
"\\Big" : {type: "textord", size: 2},
"\\bigg" : {type: "textord", size: 3},
"\\Bigg" : {type: "textord", size: 4}
};
// Parses a "nucleus", which is either a single token from the tokenizer or a
// function and its arguments
Parser.prototype.parseNucleus = function(pos, mode) {
// Parse a single symbol out of the string. Here, we handle both the functions
// we have defined, as well as the single character symbols
//
// Returns a ParseFuncOrArgument or null
Parser.prototype.parseSymbol = function(pos, mode) {
var nucleus = this.lexer.lex(pos, mode);
if (utils.contains(colorFuncs, nucleus.type)) {
// If this is a color function, parse its argument and return
var group = this.parseGroup(nucleus.position, mode);
if (group) {
var atoms;
if (group.result.type === "ordgroup") {
atoms = group.result.value;
} else {
atoms = [group.result];
}
return new ParseResult(
new ParseNode("color",
{color: "katex-" + nucleus.type.slice(1), value: atoms},
mode),
group.position);
} else {
throw new ParseError(
"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
// custom color and its second argument normally
var color = this.parseColorGroup(nucleus.position, mode);
if (color) {
var inner = this.parseGroup(color.position, mode);
if (inner) {
var atoms;
if (inner.result.type === "ordgroup") {
atoms = inner.result.value;
} else {
atoms = [inner.result];
if (functions.funcs[nucleus.type]) {
// If there is a function with this name, we use its data
var func = functions.funcs[nucleus.type];
// Here, we replace "original" argTypes with the current mode
var argTypes = func.argTypes;
if (argTypes) {
argTypes = argTypes.slice();
for (var i = 0; i < argTypes.length; i++) {
if (argTypes[i] === "original") {
argTypes[i] = mode;
}
return new ParseResult(
new ParseNode("color",
{color: color.result.value, value: atoms},
mode),
inner.position);
} else {
throw new ParseError(
"Expected second group after '" + nucleus.text + "'",
this.lexer, color.position
);
}
} else {
throw new ParseError(
"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
var group = this.parseImplicitGroup(nucleus.position, mode);
return new ParseResult(
new ParseNode("sizing", {
size: "size" + (utils.indexOf(sizeFuncs, nucleus.type) + 1),
value: group.result
}, mode),
group.position);
} else if (mode === "math" && utils.contains(namedFns, nucleus.type)) {
// If this is a named function, just return it plain
return new ParseResult(
new ParseNode("namedfn", nucleus.text, mode),
nucleus.position);
} else if (mode === "math" && delimiterSizes[nucleus.type]) {
// If this is a delimiter size function, we parse a single delimiter
var delim = this.parseDelimiter(nucleus.position, mode);
if (delim) {
var type = delimiterSizes[nucleus.type].type;
return new ParseResult(
new ParseNode("delimsizing", {
size: delimiterSizes[nucleus.type].size,
type: delimiterSizes[nucleus.type].type,
value: delim.result.value
}, mode),
delim.position);
} else {
throw new ParseError(
"Expected delimiter after '" + nucleus.text + "'");
}
} else if (mode === "math" && nucleus.type === "\\left") {
// If we see a \left, first we parse the left delimiter
var leftDelim = this.parseDelimiter(nucleus.position, mode);
if (leftDelim) {
// Then, we parse an inner expression. Due to the handling of \right
// below, this should end just before the \right
var expression = this.parseExpression(leftDelim.position, mode);
// Make sure we see a \right
var right = this.lexer.lex(expression.position, mode);
this.expect(right, "\\right");
// Parse the right delimiter
var rightDelim = this.parseDelimiter(right.position, mode);
if (rightDelim) {
return new ParseResult(
new ParseNode("leftright", {
left: leftDelim.result.value,
right: rightDelim.result.value,
body: expression.result
}, mode),
rightDelim.position);
} else {
throw new ParseError(
"Expected delimiter after '" + right.text + "'");
}
} else {
throw new ParseError(
"Expected delimiter after '" + nucleus.text + "'");
}
} else if (mode === "math" && nucleus.type === "\\right") {
// If we see a right, we explicitly return null to break out of the
// parseExpression loop. The code for \left will handle the delimiter
return null;
} else if (nucleus.type === "\\llap" || nucleus.type === "\\rlap") {
// If this is an llap or rlap, parse its argument and return
var group = this.parseGroup(nucleus.position, mode);
if (group) {
return new ParseResult(
new ParseNode(nucleus.type.slice(1), group.result, mode),
group.position);
} else {
throw new ParseError(
"Expected group after '" + nucleus.text + "'",
this.lexer, nucleus.position
);
}
} else if (mode === "math" && nucleus.type === "\\text") {
var group = this.parseTextGroup(nucleus.position, mode);
if (group) {
return new ParseResult(
new ParseNode(nucleus.type.slice(1), group.result, mode),
group.position);
} else {
throw new ParseError(
"Expected group after '" + nucleus.text + "'",
this.lexer, nucleus.position
);
}
} else if (mode === "math" && (nucleus.type === "\\dfrac" ||
nucleus.type === "\\frac" ||
nucleus.type === "\\tfrac")) {
// If this is a frac, parse its two arguments and return
var numer = this.parseGroup(nucleus.position, mode);
if (numer) {
var denom = this.parseGroup(numer.position, mode);
if (denom) {
return new ParseResult(
new ParseNode("frac", {
numer: numer.result,
denom: denom.result,
size: nucleus.type.slice(1)
}, mode),
denom.position);
} else {
throw new ParseError("Expected denominator after '" +
nucleus.type + "'",
this.lexer, numer.position
);
}
} else {
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
return new ParseResult(
new ParseNode("katex", null, mode),
nucleus.position
);
} else if (mode === "math" && nucleus.type === "\\overline") {
// If this is an overline, parse its argument and return
var group = this.parseGroup(nucleus.position, mode);
if (group) {
return new ParseResult(
new ParseNode("overline", group, mode),
group.position);
} else {
throw new ParseError("Expected group after '" +
nucleus.type + "'",
this.lexer, nucleus.position
);
}
} else if (mode === "math" && nucleus.type === "\\sqrt") {
// If this is a square root, parse its argument and return
var group = this.parseGroup(nucleus.position, mode);
if (group) {
return new ParseResult(
new ParseNode("sqrt", group, mode),
group.position);
} else {
throw new ParseError("Expected group after '" +
nucleus.type + "'",
this.lexer, nucleus.position
);
}
} else if (mode === "math" && nucleus.type === "\\rule") {
// Parse the width of the rule
var widthGroup = this.parseSizeGroup(nucleus.position, mode);
if (widthGroup) {
// Parse the height of the rule
var heightGroup = this.parseSizeGroup(widthGroup.position, mode);
if (heightGroup) {
return new ParseResult(
new ParseNode("rule", {
width: widthGroup.result.value,
height: heightGroup.result.value
}, mode),
heightGroup.position);
} else {
throw new ParseError("Expected second size group after '" +
nucleus.type + "'",
this.lexer, nucleus.position
);
}
} else {
throw new ParseError("Expected size group after '" +
nucleus.type + "'",
this.lexer, nucleus.position
);
}
return new ParseFuncOrArgument(
new ParseResult(nucleus.type, nucleus.position),
true, func.allowedInText, func.numArgs, argTypes);
} else if (symbols[mode][nucleus.text]) {
// Otherwise if this is a no-argument function, find the type it
// corresponds to in the symbols map
return new ParseResult(
new ParseNode(symbols[mode][nucleus.text].group, nucleus.text, mode),
nucleus.position);
return new ParseFuncOrArgument(
new ParseResult(
new ParseNode(symbols[mode][nucleus.text].group,
nucleus.text, mode),
nucleus.position),
false);
} else {
// Otherwise, we couldn't parse it
return null;
}
};

View File

@ -55,7 +55,7 @@ var getTypeOfGroup = function(group) {
} else if (group.type === "sizing") {
return getTypeOfGroup(group.value.value);
} else if (group.type === "delimsizing") {
return groupToType[group.value.type];
return groupToType[group.value.delimType];
} else {
return groupToType[group.type];
}
@ -125,8 +125,7 @@ var groupTypes = {
text: function(group, options, prev) {
return makeSpan(["text mord", options.style.cls()],
[buildGroup(group.value, options.reset())]
);
buildExpression(group.value.body, options.reset()));
},
supsub: function(group, options, prev) {
@ -368,7 +367,7 @@ var groupTypes = {
llap: function(group, options, prev) {
var inner = makeSpan(
["inner"], [buildGroup(group.value, options.reset())]);
["inner"], [buildGroup(group.value.body, options.reset())]);
var fix = makeSpan(["fix"], []);
return makeSpan(
["llap", options.style.cls()], [inner, fix]);
@ -376,7 +375,7 @@ var groupTypes = {
rlap: function(group, options, prev) {
var inner = makeSpan(
["inner"], [buildGroup(group.value, options.reset())]);
["inner"], [buildGroup(group.value.body, options.reset())]);
var fix = makeSpan(["fix"], []);
return makeSpan(
["rlap", options.style.cls()], [inner, fix]);
@ -399,8 +398,8 @@ var groupTypes = {
namedfn: function(group, options, prev) {
var chars = [];
for (var i = 1; i < group.value.length; i++) {
chars.push(buildCommon.mathrm(group.value[i], group.mode));
for (var i = 1; i < group.value.body.length; i++) {
chars.push(buildCommon.mathrm(group.value.body[i], group.mode));
}
return makeSpan(["mop"], chars, options.getColor());
@ -431,7 +430,7 @@ var groupTypes = {
},
sqrt: function(group, options, prev) {
var innerGroup = buildGroup(group.value.result,
var innerGroup = buildGroup(group.value.body,
options.withStyle(options.style.cramp()));
var fontSizer = buildCommon.makeFontSizer(
@ -493,7 +492,7 @@ var groupTypes = {
},
overline: function(group, options, prev) {
var innerGroup = buildGroup(group.value.result,
var innerGroup = buildGroup(group.value.body,
options.withStyle(options.style.cramp()));
var fontSizer = buildCommon.makeFontSizer(options, innerGroup.maxFontSize);
@ -518,12 +517,13 @@ var groupTypes = {
},
sizing: function(group, options, prev) {
var inner = buildGroup(group.value.value,
var inner = buildExpression(group.value.value,
options.withSize(group.value.size), prev);
var span = makeSpan([getTypeOfGroup(group.value.value)],
[makeSpan(["sizing", "reset-" + options.size, group.value.size],
[inner])]);
var span = makeSpan(["mord"],
[makeSpan(["sizing", "reset-" + options.size, group.value.size,
options.style.cls()],
inner)]);
var sizeToFontSize = {
"size1": 0.5,
@ -548,11 +548,13 @@ var groupTypes = {
var delim = group.value.value;
if (delim === ".") {
return buildCommon.makeSpan([groupToType[group.value.type]]);
return makeSpan([groupToType[group.value.delimType]]);
}
return delimiter.sizedDelim(
delim, group.value.size, options, group.mode);
return makeSpan(
[groupToType[group.value.delimType]],
[delimiter.sizedDelim(
delim, group.value.size, options, group.mode)]);
},
leftright: function(group, options, prev) {
@ -594,7 +596,7 @@ var groupTypes = {
rule: function(group, options, prev) {
// Make an empty span for the rule
var rule = makeSpan(["mord", "rule"], []);
var rule = makeSpan(["mord", "rule"], [], options.getColor());
var width = group.value.width.number;
if (group.value.width.unit === "ex") {

349
functions.js Normal file
View File

@ -0,0 +1,349 @@
var utils = require("./utils");
var ParseError = require("./ParseError");
// This file contains a list of functions that we parse. The functions map
// contains the following data:
/*
* Keys are the name of the functions to parse
* The data contains the following keys:
* - numArgs: The number of arguments the function takes.
* - argTypes: (optional) An array corresponding to each argument of the
* function, giving the type of argument that should be parsed.
* Valid types:
* - "size": A size-like thing, such as "1em" or "5ex"
* - "color": An html color, like "#abc" or "blue"
* - "original": The same type as the environment that the
* function being parsed is in (e.g. used for the
* bodies of functions like \color where the first
* argument is special and the second argument is
* parsed normally)
* Other possible types (probably shouldn't be used)
* - "text": Text-like (e.g. \text)
* - "math": Normal math
* If undefined, this will be treated as an appropriate length
* array of "original" strings
* - greediness: (optional) The greediness of the function to use ungrouped
* arguments.
*
* E.g. if you have an expression
* \sqrt \frac 1 2
* since \frac has greediness=2 vs \sqrt's greediness=1, \frac
* will use the two arguments '1' and '2' as its two arguments,
* then that whole function will be used as the argument to
* \sqrt. On the other hand, the expressions
* \frac \frac 1 2 3
* and
* \frac \sqrt 1 2
* will fail because \frac and \frac have equal greediness
* and \sqrt has a lower greediness than \frac respectively. To
* make these parse, we would have to change them to:
* \frac {\frac 1 2} 3
* and
* \frac {\sqrt 1} 2
*
* The default value is `1`
* - allowedInText: (optional) Whether or not the function is allowed inside
* text mode (default false)
* - handler: The function that is called to handle this function and its
* arguments. The arguments are:
* - func: the text of the function
* - [args]: the next arguments are the arguments to the function,
* of which there are numArgs of them
* - positions: the positions in the overall string of the function
* and the arguments. Should only be used to produce
* error messages
* The function should return an object with the following keys:
* - type: The type of element that this is. This is then used in
* buildTree to determine which function should be called
* to build this node into a DOM node
* Any other data can be added to the object, which will be passed
* in to the function in buildTree as `group.value`.
*/
var functions = {
// A normal square root
"\\sqrt": {
numArgs: 1,
handler: function(func, body) {
return {
type: "sqrt",
body: body
};
}
},
// Some non-mathy text
"\\text": {
numArgs: 1,
argTypes: ["text"],
greediness: 2,
handler: function(func, body) {
// Since the corresponding buildTree function expects a list of
// elements, we normalize for different kinds of arguments
// TODO(emily): maybe this should be done somewhere else
var inner;
if (body.type === "ordgroup") {
inner = body.value;
} else {
inner = [body];
}
return {
type: "text",
body: inner
};
}
},
// A two-argument custom color
"\\color": {
numArgs: 2,
allowedInText: true,
argTypes: ["color", "original"],
handler: function(func, color, body) {
// Normalize the different kinds of bodies (see \text above)
var inner;
if (body.type === "ordgroup") {
inner = body.value;
} else {
inner = [body];
}
return {
type: "color",
color: color.value,
value: inner
};
}
},
// An overline
"\\overline": {
numArgs: 1,
handler: function(func, body) {
return {
type: "overline",
body: body
};
}
},
// A box of the width and height
"\\rule": {
numArgs: 2,
argTypes: ["size", "size"],
handler: function(func, width, height) {
return {
type: "rule",
width: width.value,
height: height.value
};
}
},
// A KaTeX logo
"\\KaTeX": {
numArgs: 0,
handler: function(func) {
return {
type: "katex"
};
}
}
};
// Extra data needed for the delimiter handler down below
var delimiterSizes = {
"\\bigl" : {type: "open", size: 1},
"\\Bigl" : {type: "open", size: 2},
"\\biggl": {type: "open", size: 3},
"\\Biggl": {type: "open", size: 4},
"\\bigr" : {type: "close", size: 1},
"\\Bigr" : {type: "close", size: 2},
"\\biggr": {type: "close", size: 3},
"\\Biggr": {type: "close", size: 4},
"\\bigm" : {type: "rel", size: 1},
"\\Bigm" : {type: "rel", size: 2},
"\\biggm": {type: "rel", size: 3},
"\\Biggm": {type: "rel", size: 4},
"\\big" : {type: "textord", size: 1},
"\\Big" : {type: "textord", size: 2},
"\\bigg" : {type: "textord", size: 3},
"\\Bigg" : {type: "textord", size: 4}
};
var delimiters = [
"(", ")", "[", "\\lbrack", "]", "\\rbrack",
"\\{", "\\lbrace", "\\}", "\\rbrace",
"\\lfloor", "\\rfloor", "\\lceil", "\\rceil",
"<", ">", "\\langle", "\\rangle",
"/", "\\backslash",
"|", "\\vert", "\\|", "\\Vert",
"\\uparrow", "\\Uparrow",
"\\downarrow", "\\Downarrow",
"\\updownarrow", "\\Updownarrow",
"."
];
/*
* This is a list of functions which each have the same function but have
* different names so that we don't have to duplicate the data a bunch of times.
* Each element in the list is an object with the following keys:
* - funcs: A list of function names to be associated with the data
* - data: An objecty with the same data as in each value of the `function`
* table above
*/
var duplicatedFunctions = [
// Single-argument color functions
{
funcs: [
"\\blue", "\\orange", "\\pink", "\\red",
"\\green", "\\gray", "\\purple"
],
data: {
numArgs: 1,
allowedInText: true,
handler: function(func, body) {
var atoms;
if (body.type === "ordgroup") {
atoms = body.value;
} else {
atoms = [body];
}
return {
type: "color",
color: "katex-" + func.slice(1),
value: atoms
};
}
}
},
// No-argument mathy operators
{
funcs: [
"\\arcsin", "\\arccos", "\\arctan", "\\arg", "\\cos", "\\cosh",
"\\cot", "\\coth", "\\csc", "\\deg", "\\dim", "\\exp", "\\hom",
"\\ker", "\\lg", "\\ln", "\\log", "\\sec", "\\sin", "\\sinh",
"\\tan","\\tanh"
],
data: {
numArgs: 0,
handler: function(func) {
return {
type: "namedfn",
body: func
};
}
}
},
// Fractions
{
funcs: ["\\dfrac", "\\frac", "\\tfrac"],
data: {
numArgs: 2,
greediness: 2,
handler: function(func, numer, denom) {
return {
type: "frac",
numer: numer,
denom: denom,
size: func.slice(1)
};
}
}
},
// Left and right overlap functions
{
funcs: ["\\llap", "\\rlap"],
data: {
numArgs: 1,
allowedInText: true,
handler: function(func, body) {
return {
type: func.slice(1),
body: body
};
}
}
},
// Delimiter functions
{
funcs: [
"\\bigl", "\\Bigl", "\\biggl", "\\Biggl",
"\\bigr", "\\Bigr", "\\biggr", "\\Biggr",
"\\bigm", "\\Bigm", "\\biggm", "\\Biggm",
"\\big", "\\Big", "\\bigg", "\\Bigg",
"\\left", "\\right"
],
data: {
numArgs: 1,
handler: function(func, delim, positions) {
if (!utils.contains(delimiters, delim.value)) {
throw new ParseError(
"Invalid delimiter: '" + delim.value + "' after '" +
func + "'",
this.lexer, positions[1]);
}
// left and right are caught somewhere in Parser.js, which is
// why this data doesn't match what is in buildTree
if (func === "\\left" || func === "\\right") {
return {
type: "leftright",
value: delim.value
};
} else {
return {
type: "delimsizing",
size: delimiterSizes[func].size,
delimType: delimiterSizes[func].type,
value: delim.value
};
}
}
}
},
// Sizing functions
{
funcs: [
"\\tiny", "\\scriptsize", "\\footnotesize", "\\small",
"\\normalsize", "\\large", "\\Large", "\\LARGE", "\\huge", "\\Huge"
],
data: {
numArgs: 0
}
}
];
var addFuncsWithData = function(funcs, data) {
for (var i = 0; i < funcs.length; i++) {
functions[funcs[i]] = data;
}
};
// Add all of the functions in duplicatedFunctions to the functions map
for (var i = 0; i < duplicatedFunctions.length; i++) {
addFuncsWithData(duplicatedFunctions[i].funcs, duplicatedFunctions[i].data);
}
// Returns the greediness of a given function. Since greediness is optional, we
// use this function to put in the default value if it is undefined.
var getGreediness = function(func) {
if (functions[func].greediness === undefined) {
return 1;
} else {
return functions[func].greediness;
}
};
module.exports = {
funcs: functions,
getGreediness: getGreediness
};

8
lint_blacklist.txt Normal file
View File

@ -0,0 +1,8 @@
.git
# Autogenerated code
build/**
node_modules/**
# Third party code
test/jasmine/**

View File

@ -1,8 +1,10 @@
var Parser = require("./Parser");
var parser = new Parser();
// Parses the expression using the parser
var parseTree = function(toParse) {
return parser.parse(toParse);
var parser = new Parser(toParse);
return parser.parse();
}
module.exports = parseTree;

View File

@ -104,7 +104,7 @@
{
"name": "Overline",
"screenSize": [1024, 768],
"url": "http://localhost:7936/test/huxley/test.html?m=\\overline{x}\\overline{x}\\overline{x^{x^{x^x}}} \\blue\\overline{y}"
"url": "http://localhost:7936/test/huxley/test.html?m=\\overline{x}\\overline{x}\\overline{x^{x^{x^x}}} \\blue{\\overline{y}}"
},
{

120
test/jasmine/boot.js Normal file
View File

@ -0,0 +1,120 @@
/**
Starting with version 2.0, this file "boots" Jasmine, performing all of the necessary initialization before executing the loaded environment and all of a project's specs. This file should be loaded after `jasmine.js`, but before any project source files or spec files are loaded. Thus this file can also be used to customize Jasmine for a project.
If a project is using Jasmine via the standalone distribution, this file can be customized directly. If a project is using Jasmine via the [Ruby gem][jasmine-gem], this file can be copied into the support directory via `jasmine copy_boot_js`. Other environments (e.g., Python) will have different mechanisms.
The location of `boot.js` can be specified and/or overridden in `jasmine.yml`.
[jasmine-gem]: http://github.com/pivotal/jasmine-gem
*/
(function() {
/**
* ## Require &amp; Instantiate
*
* Require Jasmine's core files. Specifically, this requires and attaches all of Jasmine's code to the `jasmine` reference.
*/
window.jasmine = jasmineRequire.core(jasmineRequire);
/**
* Since this is being run in a browser and the results should populate to an HTML page, require the HTML-specific Jasmine code, injecting the same reference.
*/
jasmineRequire.html(jasmine);
/**
* Create the Jasmine environment. This is used to run all specs in a project.
*/
var env = jasmine.getEnv();
/**
* ## The Global Interface
*
* Build up the functions that will be exposed as the Jasmine public interface. A project can customize, rename or alias any of these functions as desired, provided the implementation remains unchanged.
*/
var jasmineInterface = jasmineRequire.interface(jasmine, env);
/**
* Add all of the Jasmine global/public interface to the proper global, so a project can use the public interface directly. For example, calling `describe` in specs instead of `jasmine.getEnv().describe`.
*/
if (typeof window == "undefined" && typeof exports == "object") {
extend(exports, jasmineInterface);
} else {
extend(window, jasmineInterface);
}
/**
* ## Runner Parameters
*
* More browser specific code - wrap the query string in an object and to allow for getting/setting parameters from the runner user interface.
*/
var queryString = new jasmine.QueryString({
getWindowLocation: function() { return window.location; }
});
var catchingExceptions = queryString.getParam("catch");
env.catchExceptions(typeof catchingExceptions === "undefined" ? true : catchingExceptions);
/**
* ## Reporters
* The `HtmlReporter` builds all of the HTML UI for the runner page. This reporter paints the dots, stars, and x's for specs, as well as all spec names and all failures (if any).
*/
var htmlReporter = new jasmine.HtmlReporter({
env: env,
onRaiseExceptionsClick: function() { queryString.setParam("catch", !env.catchingExceptions()); },
getContainer: function() { return document.body; },
createElement: function() { return document.createElement.apply(document, arguments); },
createTextNode: function() { return document.createTextNode.apply(document, arguments); },
timer: new jasmine.Timer()
});
/**
* The `jsApiReporter` also receives spec results, and is used by any environment that needs to extract the results from JavaScript.
*/
env.addReporter(jasmineInterface.jsApiReporter);
env.addReporter(htmlReporter);
/**
* Filter which specs will be run by matching the start of the full name against the `spec` query param.
*/
var specFilter = new jasmine.HtmlSpecFilter({
filterString: function() { return queryString.getParam("spec"); }
});
env.specFilter = function(spec) {
return specFilter.matches(spec.getFullName());
};
/**
* Setting up timing functions to be able to be overridden. Certain browsers (Safari, IE 8, phantomjs) require this hack.
*/
window.setTimeout = window.setTimeout;
window.setInterval = window.setInterval;
window.clearTimeout = window.clearTimeout;
window.clearInterval = window.clearInterval;
/**
* ## Execution
*
* Replace the browser window's `onload`, ensure it's called, and then run all of the loaded specs. This includes initializing the `HtmlReporter` instance and then executing the loaded Jasmine environment. All of this will happen after all of the specs are loaded.
*/
var currentWindowOnload = window.onload;
window.onload = function() {
if (currentWindowOnload) {
currentWindowOnload();
}
htmlReporter.initialize();
env.execute();
};
/**
* Helper function for readability above.
*/
function extend(destination, source) {
for (var property in source) destination[property] = source[property];
return destination;
}
}());

166
test/jasmine/console.js Normal file
View File

@ -0,0 +1,166 @@
/*
Copyright (c) 2008-2014 Pivotal Labs
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
function getJasmineRequireObj() {
if (typeof module !== 'undefined' && module.exports) {
return exports;
} else {
window.jasmineRequire = window.jasmineRequire || {};
return window.jasmineRequire;
}
}
getJasmineRequireObj().console = function(jRequire, j$) {
j$.ConsoleReporter = jRequire.ConsoleReporter();
};
getJasmineRequireObj().ConsoleReporter = function() {
var noopTimer = {
start: function(){},
elapsed: function(){ return 0; }
};
function ConsoleReporter(options) {
var print = options.print,
showColors = options.showColors || false,
onComplete = options.onComplete || function() {},
timer = options.timer || noopTimer,
specCount,
failureCount,
failedSpecs = [],
pendingCount,
ansi = {
green: '\x1B[32m',
red: '\x1B[31m',
yellow: '\x1B[33m',
none: '\x1B[0m'
};
this.jasmineStarted = function() {
specCount = 0;
failureCount = 0;
pendingCount = 0;
print('Started');
printNewline();
timer.start();
};
this.jasmineDone = function() {
printNewline();
for (var i = 0; i < failedSpecs.length; i++) {
specFailureDetails(failedSpecs[i]);
}
if(specCount > 0) {
printNewline();
var specCounts = specCount + ' ' + plural('spec', specCount) + ', ' +
failureCount + ' ' + plural('failure', failureCount);
if (pendingCount) {
specCounts += ', ' + pendingCount + ' pending ' + plural('spec', pendingCount);
}
print(specCounts);
} else {
print('No specs found');
}
printNewline();
var seconds = timer.elapsed() / 1000;
print('Finished in ' + seconds + ' ' + plural('second', seconds));
printNewline();
onComplete(failureCount === 0);
};
this.specDone = function(result) {
specCount++;
if (result.status == 'pending') {
pendingCount++;
print(colored('yellow', '*'));
return;
}
if (result.status == 'passed') {
print(colored('green', '.'));
return;
}
if (result.status == 'failed') {
failureCount++;
failedSpecs.push(result);
print(colored('red', 'F'));
}
};
return this;
function printNewline() {
print('\n');
}
function colored(color, str) {
return showColors ? (ansi[color] + str + ansi.none) : str;
}
function plural(str, count) {
return count == 1 ? str : str + 's';
}
function repeat(thing, times) {
var arr = [];
for (var i = 0; i < times; i++) {
arr.push(thing);
}
return arr;
}
function indent(str, spaces) {
var lines = (str || '').split('\n');
var newArr = [];
for (var i = 0; i < lines.length; i++) {
newArr.push(repeat(' ', spaces).join('') + lines[i]);
}
return newArr.join('\n');
}
function specFailureDetails(result) {
printNewline();
print(result.fullName);
for (var i = 0; i < result.failedExpectations.length; i++) {
var failedExpectation = result.failedExpectations[i];
printNewline();
print(indent(failedExpectation.message, 2));
print(indent(failedExpectation.stack, 2));
}
printNewline();
}
}
return ConsoleReporter;
};

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,11 +1,95 @@
var buildTree = require("../buildTree");
var parseTree = require("../parseTree");
var ParseError = require("../ParseError");
beforeEach(function() {
jasmine.addMatchers({
toParse: function() {
return {
compare: function(actual) {
var result = {
pass: true,
message: "'" + actual + "' succeeded parsing"
};
try {
parseTree(actual);
} catch (e) {
result.pass = false;
if (e instanceof ParseError) {
result.message = "'" + actual + "' failed " +
"parsing with error: " + e.message;
} else {
result.message = "'" + actual + "' failed " +
"parsing with unknown error: " + e.message;
}
}
return result;
}
};
},
toNotParse: function() {
return {
compare: function(actual) {
var result = {
pass: false,
message: "Expected '" + actual + "' to fail " +
"parsing, but it succeeded"
};
try {
parseTree(actual);
} catch (e) {
if (e instanceof ParseError) {
result.pass = true;
result.message = "'" + actual + "' correctly " +
"didn't parse with error: " + e.message;
} else {
result.message = "'" + actual + "' failed " +
"parsing with unknown error: " + e.message;
}
}
return result;
}
};
},
toBuild: function() {
return {
compare: function(actual) {
var result = {
pass: true,
message: "'" + actual + "' succeeded in building"
};
expect(actual).toParse();
try {
buildTree(parseTree(actual));
} catch (e) {
result.pass = false;
if (e instanceof ParseError) {
result.message = "'" + actual + "' failed to " +
"build with error: " + e.message;
} else {
result.message = "'" + actual + "' failed " +
"building with unknown error: " + e.message;
}
}
return result;
}
};
}
});
});
describe("A parser", function() {
it("should not fail on an empty string", function() {
expect(function() {
parseTree("");
}).not.toThrow();
expect("").toParse();
});
it("should ignore whitespace", function() {
@ -19,9 +103,7 @@ describe("An ord parser", function() {
var expression = "1234|/@.\"`abcdefgzABCDEFGZ";
it("should not fail", function() {
expect(function() {
parseTree(expression);
}).not.toThrow();
expect(expression).toParse();
});
it("should build a list of ords", function() {
@ -46,9 +128,7 @@ describe("A bin parser", function() {
var expression = "+-*\\cdot\\pm\\div";
it("should not fail", function() {
expect(function() {
parseTree(expression);
}).not.toThrow();
expect(expression).toParse();
});
it("should build a list of bins", function() {
@ -66,9 +146,7 @@ describe("A rel parser", function() {
var expression = "=<>\\leq\\geq\\neq\\nleq\\ngeq\\cong";
it("should not fail", function() {
expect(function() {
parseTree(expression);
}).not.toThrow();
expect(expression).toParse();
});
it("should build a list of rels", function() {
@ -86,9 +164,7 @@ describe("A punct parser", function() {
var expression = ",;\\colon";
it("should not fail", function() {
expect(function() {
parseTree(expression);
}).not.toThrow();
expect(expression).toParse();
});
it("should build a list of puncts", function() {
@ -106,9 +182,7 @@ describe("An open parser", function() {
var expression = "([";
it("should not fail", function() {
expect(function() {
parseTree(expression);
}).not.toThrow();
expect(expression).toParse();
});
it("should build a list of opens", function() {
@ -126,9 +200,7 @@ describe("A close parser", function() {
var expression = ")]?!";
it("should not fail", function() {
expect(function() {
parseTree(expression);
}).not.toThrow();
expect(expression).toParse();
});
it("should build a list of closes", function() {
@ -142,45 +214,32 @@ describe("A close parser", function() {
});
});
describe("A \\KaTeX parser", function() {
it("should not fail", function() {
expect("\\KaTeX").toParse();
});
});
describe("A subscript and superscript parser", function() {
it("should not fail on superscripts", function() {
expect(function() {
parseTree("x^2");
}).not.toThrow();
expect("x^2").toParse();
});
it("should not fail on subscripts", function() {
expect(function() {
parseTree("x_3");
}).not.toThrow();
expect("x_3").toParse();
});
it("should not fail on both subscripts and superscripts", function() {
expect(function() {
parseTree("x^2_3");
}).not.toThrow();
expect("x^2_3").toParse();
expect(function() {
parseTree("x_2^3");
}).not.toThrow();
expect("x_2^3").toParse();
});
it("should not fail when there is no nucleus", function() {
expect(function() {
parseTree("^3");
}).not.toThrow();
expect(function() {
parseTree("_2");
}).not.toThrow();
expect(function() {
parseTree("^3_2");
}).not.toThrow();
expect(function() {
parseTree("_2^3");
}).not.toThrow();
expect("^3").toParse();
expect("_2").toParse();
expect("^3_2").toParse();
expect("_2^3").toParse();
});
it("should produce supsubs for superscript", function() {
@ -227,91 +286,57 @@ describe("A subscript and superscript parser", function() {
});
it("should not parse double subscripts or superscripts", function() {
expect(function() {
parseTree("x^x^x");
}).toThrow();
expect("x^x^x").toNotParse();
expect(function() {
parseTree("x_x_x");
}).toThrow();
expect("x_x_x").toNotParse();
expect(function() {
parseTree("x_x^x_x");
}).toThrow();
expect("x_x^x_x").toNotParse();
expect(function() {
parseTree("x_x^x^x");
}).toThrow();
expect("x_x^x^x").toNotParse();
expect(function() {
parseTree("x^x_x_x");
}).toThrow();
expect("x^x_x_x").toNotParse();
expect(function() {
parseTree("x^x_x^x");
}).toThrow();
expect("x^x_x^x").toNotParse();
});
it("should work correctly with {}s", function() {
expect(function() {
parseTree("x^{2+3}");
}).not.toThrow();
expect("x^{2+3}").toParse();
expect(function() {
parseTree("x_{3-2}");
}).not.toThrow();
expect("x_{3-2}").toParse();
expect(function() {
parseTree("x^{2+3}_3");
}).not.toThrow();
expect("x^{2+3}_3").toParse();
expect(function() {
parseTree("x^2_{3-2}");
}).not.toThrow();
expect("x^2_{3-2}").toParse();
expect(function() {
parseTree("x^{2+3}_{3-2}");
}).not.toThrow();
expect("x^{2+3}_{3-2}").toParse();
expect(function() {
parseTree("x_{3-2}^{2+3}");
}).not.toThrow();
expect("x_{3-2}^{2+3}").toParse();
expect(function() {
parseTree("x_3^{2+3}");
}).not.toThrow();
expect("x_3^{2+3}").toParse();
expect(function() {
parseTree("x_{3-2}^2");
}).not.toThrow();
expect("x_{3-2}^2").toParse();
});
it("should work with nested super/subscripts", function() {
expect("x^{x^x}").toParse();
expect("x^{x_x}").toParse();
expect("x_{x^x}").toParse();
expect("x_{x_x}").toParse();
});
});
describe("A subscript and superscript tree-builder", function() {
it("should not fail when there is no nucleus", function() {
expect(function() {
buildTree(parseTree("^3"));
}).not.toThrow();
expect(function() {
buildTree(parseTree("_2"));
}).not.toThrow();
expect(function() {
buildTree(parseTree("^3_2"));
}).not.toThrow();
expect(function() {
buildTree(parseTree("_2^3"));
}).not.toThrow();
expect("^3").toBuild();
expect("_2").toBuild();
expect("^3_2").toBuild();
expect("_2^3").toBuild();
});
});
describe("A group parser", function() {
it("should not fail", function() {
expect(function() {
parseTree("{xy}");
}).not.toThrow();
expect("{xy}").toParse();
});
it("should produce a single ord", function() {
@ -328,10 +353,8 @@ describe("A group parser", function() {
describe("An implicit group parser", function() {
it("should not fail", function() {
expect(function() {
parseTree("\\Large x");
parseTree("abc {abc \Large xyz} abc");
}).not.toThrow();
expect("\\Large x").toParse();
expect("abc {abc \\Large xyz} abc").toParse();
});
it("should produce a single object", function() {
@ -353,7 +376,7 @@ describe("An implicit group parser", function() {
var sizing = parse[1];
expect(sizing.type).toMatch("sizing");
expect(sizing.value.value.value.length).toBe(3);
expect(sizing.value.value.length).toBe(3);
});
it("should stop at the ends of groups", function() {
@ -363,61 +386,43 @@ describe("An implicit group parser", function() {
var sizing = group.value[1];
expect(sizing.type).toMatch("sizing");
expect(sizing.value.value.value.length).toBe(1);
expect(sizing.value.value.length).toBe(1);
});
});
describe("A function parser", function() {
it("should parse no argument functions", function() {
expect(function() {
parseTree("\\div");
}).not.toThrow();
expect("\\div").toParse();
});
it("should parse 1 argument functions", function() {
expect(function() {
parseTree("\\blue x");
}).not.toThrow();
expect("\\blue x").toParse();
});
it("should parse 2 argument functions", function() {
expect(function() {
parseTree("\\frac 1 2");
}).not.toThrow();
expect("\\frac 1 2").toParse();
});
it("should not parse 1 argument functions with no arguments", function() {
expect(function() {
parseTree("\\blue");
}).toThrow();
expect("\\blue").toNotParse();
});
it("should not parse 2 argument functions with 0 or 1 arguments", function() {
expect(function() {
parseTree("\\frac");
}).toThrow();
expect("\\frac").toNotParse();
expect(function() {
parseTree("\\frac 1");
}).toThrow();
expect("\\frac 1").toNotParse();
});
it("should not parse a function with text right after it", function() {
expect(function() {
parseTree("\\redx");
}).toThrow();
expect("\\redx").toNotParse();
});
it("should parse a function with a number right after it", function() {
expect(function() {
parseTree("\\frac12");
}).not.toThrow();
expect("\\frac12").toParse();
});
it("should parse some functions with text right after it", function() {
expect(function() {
parseTree("\\;x");
}).not.toThrow();
expect("\\;x").toParse();
});
});
@ -427,9 +432,7 @@ describe("A frac parser", function() {
var tfracExpression = "\\tfrac{x}{y}";
it("should not fail", function() {
expect(function() {
parseTree(expression);
}).not.toThrow();
expect(expression).toParse();
});
it("should produce a frac", function() {
@ -441,13 +444,9 @@ describe("A frac parser", function() {
});
it("should also parse dfrac and tfrac", function() {
expect(function() {
parseTree(dfracExpression);
}).not.toThrow();
expect(dfracExpression).toParse();
expect(function() {
parseTree(tfracExpression);
}).not.toThrow();
expect(tfracExpression).toParse();
});
it("should parse dfrac and tfrac as fracs", function() {
@ -470,9 +469,7 @@ describe("A sizing parser", function() {
var nestedSizeExpression = "\\Huge{\\small{x}}";
it("should not fail", function() {
expect(function() {
parseTree(sizeExpression);
}).not.toThrow();
expect(sizeExpression).toParse();
});
it("should produce a sizing node", function() {
@ -481,26 +478,20 @@ describe("A sizing parser", function() {
expect(parse.type).toMatch("sizing");
expect(parse.value).toBeDefined();
});
it("should not parse a nested size expression", function() {
expect(function() {
parseExpression(nestedSizeExpression);
}).toThrow();
});
});
describe("A text parser", function() {
var textExpression = "\\text{a b}";
var badTextExpression = "\\text{a b%}";
var badTextExpression2 = "\\text x";
var nestedTextExpression = "\\text{a {b} \\blue{c}}";
var noBraceTextExpression = "\\text x";
var nestedTextExpression =
"\\text{a {b} \\blue{c} \\color{#fff}{x} \\llap{x}}";
var spaceTextExpression = "\\text{ a \\ }";
var leadingSpaceTextExpression = "\\text {moo}";
var badTextExpression = "\\text{a b%}";
var badFunctionExpression = "\\text{\\sqrt{x}}";
it("should not fail", function() {
expect(function() {
parseTree(textExpression);
}).not.toThrow();
expect(textExpression).toParse();
});
it("should produce a text", function() {
@ -512,29 +503,30 @@ describe("A text parser", function() {
it("should produce textords instead of mathords", function() {
var parse = parseTree(textExpression)[0];
var group = parse.value.value;
var group = parse.value.body;
expect(group[0].type).toMatch("textord");
});
it("should not parse bad text", function() {
expect(function() {
parseTree(badTextExpression);
}).toThrow();
expect(function() {
parseTree(badTextExpression2);
}).toThrow();
expect(badTextExpression).toNotParse();
});
it("should not parse bad functions inside text", function() {
expect(badFunctionExpression).toNotParse();
});
it("should parse text with no braces around it", function() {
expect(noBraceTextExpression).toParse();
});
it("should parse nested expressions", function() {
expect(function() {
parseTree(nestedTextExpression);
}).not.toThrow();
expect(nestedTextExpression).toParse();
});
it("should contract spaces", function() {
var parse = parseTree(spaceTextExpression)[0];
var group = parse.value.value;
var group = parse.value.body;
expect(group[0].type).toMatch("spacing");
expect(group[1].type).toMatch("textord");
@ -545,9 +537,9 @@ describe("A text parser", function() {
it("should ignore a space before the text group", function() {
var parse = parseTree(leadingSpaceTextExpression)[0];
// [m, o, o]
expect(parse.value.value.length).toBe(3);
expect(parse.value.body.length).toBe(3);
expect(
parse.value.value.map(function(n) { return n.value; }).join("")
parse.value.body.map(function(n) { return n.value; }).join("")
).toBe("moo");
});
});
@ -558,9 +550,7 @@ describe("A color parser", function() {
var badCustomColorExpression = "\\color{bad-color}{x}";
it("should not fail", function() {
expect(function() {
parseTree(colorExpression);
}).not.toThrow();
expect(colorExpression).toParse();
});
it("should build a color node", function() {
@ -572,9 +562,7 @@ describe("A color parser", function() {
});
it("should parse a custom color", function() {
expect(function() {
parseTree(customColorExpression);
}).not.toThrow();
expect(customColorExpression).toParse();
});
it("should correctly extract the custom color", function() {
@ -584,9 +572,7 @@ describe("A color parser", function() {
});
it("should not parse a bad custom color", function() {
expect(function() {
parseTree(badCustomColorExpression);
}).toThrow();
expect(badCustomColorExpression).toNotParse();
});
});
@ -595,15 +581,11 @@ describe("A tie parser", function() {
var textTie = "\\text{a~ b}";
it("should parse ties in math mode", function() {
expect(function() {
parseTree(mathTie);
}).not.toThrow();
expect(mathTie).toParse();
});
it("should parse ties in text mode", function() {
expect(function() {
parseTree(textTie);
}).not.toThrow();
expect(textTie).toParse();
});
it("should produce spacing in math mode", function() {
@ -614,14 +596,14 @@ describe("A tie parser", function() {
it("should produce spacing in text mode", function() {
var text = parseTree(textTie)[0];
var parse = text.value.value;
var parse = text.value.body;
expect(parse[1].type).toMatch("spacing");
});
it("should not contract with spaces in text mode", function() {
var text = parseTree(textTie)[0];
var parse = text.value.value;
var parse = text.value.body;
expect(parse[2].type).toMatch("spacing");
});
@ -633,16 +615,12 @@ describe("A delimiter sizing parser", function() {
var bigDelim = "\\Biggr \\langle";
it("should parse normal delimiters", function() {
expect(function() {
parseTree(normalDelim);
parseTree(bigDelim);
}).not.toThrow();
expect(normalDelim).toParse();
expect(bigDelim).toParse();
});
it("should not parse not-delimiters", function() {
expect(function() {
parseTree(notDelim);
}).toThrow();
expect(notDelim).toNotParse();
});
it("should produce a delimsizing", function() {
@ -655,8 +633,8 @@ describe("A delimiter sizing parser", function() {
var leftParse = parseTree(normalDelim)[0];
var rightParse = parseTree(bigDelim)[0];
expect(leftParse.value.type).toMatch("open");
expect(rightParse.value.type).toMatch("close");
expect(leftParse.value.delimType).toMatch("open");
expect(rightParse.value.delimType).toMatch("close");
});
it("should parse the correct size delimiter", function() {
@ -672,9 +650,7 @@ describe("An overline parser", function() {
var overline = "\\overline{x}";
it("should not fail", function() {
expect(function() {
parseTree(overline);
}).not.toThrow();
expect(overline).toParse();
});
it("should produce an overline", function() {
@ -693,26 +669,18 @@ describe("A rule parser", function() {
var hardNumberRule = "\\rule{ 01.24ex}{2.450 em }";
it("should not fail", function() {
expect(function() {
parseTree(emRule);
parseTree(exRule);
}).not.toThrow();
expect(emRule).toParse();
expect(exRule).toParse();
});
it("should not parse invalid units", function() {
expect(function() {
parseTree(badUnitRule);
}).toThrow();
expect(badUnitRule).toNotParse();
expect(function() {
parseTree(noNumberRule);
}).toThrow();
expect(noNumberRule).toNotParse();
});
it("should not parse incomplete rules", function() {
expect(function() {
parseTree(incompleteRule);
}).toThrow();
expect(incompleteRule).toNotParse();
});
it("should produce a rule", function() {
@ -745,9 +713,7 @@ describe("A left/right parser", function() {
var emptyRight = "\\left( \\dfrac{x}{y} \\right.";
it("should not fail", function() {
expect(function() {
parseTree(normalLeftRight);
}).not.toThrow();
expect(normalLeftRight).toParse();
});
it("should produce a leftright", function() {
@ -762,40 +728,28 @@ describe("A left/right parser", function() {
var unmatchedLeft = "\\left( \\dfrac{x}{y}";
var unmatchedRight = "\\dfrac{x}{y} \\right)";
expect(function() {
parseTree(unmatchedLeft);
}).toThrow();
expect(unmatchedLeft).toNotParse();
expect(function() {
parseTree(unmatchedRight);
}).toThrow();
expect(unmatchedRight).toNotParse();
});
it("should error when braces are mismatched", function() {
var unmatched = "{ \\left( \\dfrac{x}{y} } \\right)";
expect(function() {
parseTree(unmatched);
}).toThrow();
expect(unmatched).toNotParse();
});
it("should error when non-delimiters are provided", function() {
var nonDelimiter = "\\left$ \\dfrac{x}{y} \\right)";
expect(function() {
parseTree(nonDelimiter);
}).toThrow();
expect(nonDelimiter).toNotParse();
});
it("should parse the empty '.' delimiter", function() {
expect(function() {
parseTree(emptyRight);
}).not.toThrow();
expect(emptyRight).toParse();
});
it("should parse the '.' delimiter with normal sizes", function() {
var normalEmpty = "\\Bigl .";
expect(function() {
parseTree(normalEmpty);
}).not.toThrow();
expect(normalEmpty).toParse();
});
});
@ -804,15 +758,11 @@ describe("A sqrt parser", function() {
var missingGroup = "\\sqrt";
it("should parse square roots", function() {
expect(function() {
parseTree(sqrt);
}).not.toThrow();
expect(sqrt).toParse();
});
it("should error when there is no group", function() {
expect(function() {
parseTree(missingGroup);
}).toThrow();
expect(missingGroup).toNotParse();
});
it("should produce sqrts", function() {
@ -821,3 +771,148 @@ describe("A sqrt parser", function() {
expect(parse.type).toMatch("sqrt");
});
});
describe("A TeX-compliant parser", function() {
it("should work", function() {
expect("\\frac 2 3").toParse();
});
it("should fail if there are not enough arguments", function() {
var missingGroups = [
"\\frac{x}",
"\\color{#fff}",
"\\rule{1em}",
"\\llap",
"\\bigl",
"\\text"
];
for (var i = 0; i < missingGroups.length; i++) {
expect(missingGroups[i]).toNotParse();
}
});
it("should fail when there are missing sup/subscripts", function() {
expect("x^").toNotParse();
expect("x_").toNotParse();
});
it("should fail when arguments require arguments", function() {
var badArguments = [
"\\frac \\frac x y z",
"\\frac x \\frac y z",
"\\frac \\sqrt x y",
"\\frac x \\sqrt y",
"\\frac \\llap x y",
"\\frac x \\llap y",
// This actually doesn't work in real TeX, but it is suprisingly
// hard to get this to correctly work. So, we take hit of very small
// amounts of non-compatiblity in order for the rest of the tests to
// work
// "\\llap \\frac x y",
"\\llap \\llap x",
"\\sqrt \\llap x"
];
for (var i = 0; i < badArguments.length; i++) {
expect(badArguments[i]).toNotParse();
}
});
it("should work when the arguments have braces", function() {
var goodArguments = [
"\\frac {\\frac x y} z",
"\\frac x {\\frac y z}",
"\\frac {\\sqrt x} y",
"\\frac x {\\sqrt y}",
"\\frac {\\llap x} y",
"\\frac x {\\llap y}",
"\\llap {\\frac x y}",
"\\llap {\\llap x}",
"\\sqrt {\\llap x}"
];
for (var i = 0; i < goodArguments.length; i++) {
expect(goodArguments[i]).toParse();
}
});
it("should fail when sup/subscripts require arguments", function() {
var badSupSubscripts = [
"x^\\sqrt x",
"x^\\llap x",
"x_\\sqrt x",
"x_\\llap x"
];
for (var i = 0; i < badSupSubscripts.length; i++) {
expect(badSupSubscripts[i]).toNotParse();
}
});
it("should work when sup/subscripts arguments have braces", function() {
var goodSupSubscripts = [
"x^{\\sqrt x}",
"x^{\\llap x}",
"x_{\\sqrt x}",
"x_{\\llap x}"
];
for (var i = 0; i < goodSupSubscripts.length; i++) {
expect(goodSupSubscripts[i]).toParse();
}
});
it("should parse multiple primes correctly", function() {
expect("x''''").toParse();
expect("x_2''").toParse();
expect("x''_2").toParse();
expect("x'_2'").toParse();
});
it("should fail when sup/subscripts are interspersed with arguments", function() {
expect("\\sqrt^23").toNotParse();
expect("\\frac^234").toNotParse();
expect("\\frac2^34").toNotParse();
});
it("should succeed when sup/subscripts come after whole functions", function() {
expect("\\sqrt2^3").toParse();
expect("\\frac23^4").toParse();
});
it("should succeed with a sqrt around a text/frac", function() {
expect("\\sqrt \\frac x y").toParse();
expect("\\sqrt \\text x").toParse();
expect("x^\\frac x y").toParse();
expect("x_\\text x").toParse();
});
it("should fail when arguments are \\left", function() {
var badLeftArguments = [
"\\frac \\left( x \\right) y",
"\\frac x \\left( y \\right)",
"\\llap \\left( x \\right)",
"\\sqrt \\left( x \\right)",
"x^\\left( x \\right)"
];
for (var i = 0; i < badLeftArguments.length; i++) {
expect(badLeftArguments[i]).toNotParse();
}
});
it("should succeed when there are braces around the \\left/\\right", function() {
var goodLeftArguments = [
"\\frac {\\left( x \\right)} y",
"\\frac x {\\left( y \\right)}",
"\\llap {\\left( x \\right)}",
"\\sqrt {\\left( x \\right)}",
"x^{\\left( x \\right)}"
];
for (var i = 0; i < goodLeftArguments.length; i++) {
expect(goodLeftArguments[i]).toParse();
}
});
});

View File

@ -3,19 +3,9 @@
<head>
<script src="jasmine/jasmine.js"></script>
<script src="jasmine/jasmine-html.js"></script>
<script src="jasmine/boot.js"></script>
<link rel="stylesheet" href="jasmine/jasmine.css">
<script src="katex-tests.js"></script>
<script>
var jasmineEnv = jasmine.getEnv();
jasmineEnv.updateInterval = 250;
var htmlReporter = new jasmine.HtmlReporter();
jasmineEnv.addReporter(htmlReporter);
window.onload = function() {
jasmineEnv.execute();
};
</script>
</head>
<body>
</body>