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:
parent
5b4fa72299
commit
0c9e9738c3
11
Lexer.js
11
Lexer.js
|
@ -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
860
Parser.js
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
36
buildTree.js
36
buildTree.js
|
@ -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
349
functions.js
Normal 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
8
lint_blacklist.txt
Normal file
|
@ -0,0 +1,8 @@
|
|||
.git
|
||||
|
||||
# Autogenerated code
|
||||
build/**
|
||||
node_modules/**
|
||||
|
||||
# Third party code
|
||||
test/jasmine/**
|
|
@ -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;
|
||||
|
|
|
@ -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
120
test/jasmine/boot.js
Normal 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 & 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
166
test/jasmine/console.js
Normal 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
BIN
test/jasmine/jasmine_favicon.png
Normal file
BIN
test/jasmine/jasmine_favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
|
@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue
Block a user