mirror of
https://github.com/Smaug123/KaTeX
synced 2025-10-05 11:18:39 +00:00
Allow macro definitions in settings (#493)
* Introduce MacroExpander The job of the MacroExpander is turning a stream of possibly expandable tokens, as obtained from the Lexer, into a stream of non-expandable tokens (in KaTeX, even though they may well be expandable in TeX) which can be processed by the Parser. The challenge here is that we don't have mode-specific lexer implementations any more, so we need to do everything on the token level, including reassembly of sizes and colors. * Make macros available in development server Now one can specify macro definitions like \foo=bar as part of the query string and use these macros in the formula being typeset. * Add tests for macro expansions * Handle end of input in special groups This avoids an infinite loop if input ends prematurely. * Simplify parseSpecialGroup The parseSpecialGroup methos now returns a single token spanning the whole special group, and leaves matching that string against a suitable regular expression to whoever is calling the method. Suggested by @cbreeden. * Incorporate review suggestions Add improvements suggested by Kevin Barabash during review. * Input range sanity checks Ensure that both tokens of a token range come from the same lexer, and that the range has a non-negative length. * Improved wording of two comments
This commit is contained in:
committed by
Kevin Barabash
parent
b49eee4de7
commit
8c55aed39a
151
src/Lexer.js
151
src/Lexer.js
@@ -17,16 +17,50 @@ var ParseError = require("./ParseError");
|
|||||||
|
|
||||||
// The main lexer class
|
// The main lexer class
|
||||||
function Lexer(input) {
|
function Lexer(input) {
|
||||||
this._input = input;
|
this.input = input;
|
||||||
|
this.pos = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The resulting token returned from `lex`.
|
/**
|
||||||
function Token(text, data, position) {
|
* The resulting token returned from `lex`.
|
||||||
|
*
|
||||||
|
* It consists of the token text plus some position information.
|
||||||
|
* The position information is essentially a range in an input string,
|
||||||
|
* but instead of referencing the bare input string, we refer to the lexer.
|
||||||
|
* That way it is possible to attach extra metadata to the input string,
|
||||||
|
* like for example a file name or similar.
|
||||||
|
*
|
||||||
|
* The position information (all three parameters) is optional,
|
||||||
|
* so it is OK to construct synthetic tokens if appropriate.
|
||||||
|
* Not providing available position information may lead to
|
||||||
|
* degraded error reporting, though.
|
||||||
|
*
|
||||||
|
* @param {string} text the text of this token
|
||||||
|
* @param {number=} start the start offset, zero-based inclusive
|
||||||
|
* @param {number=} end the end offset, zero-based exclusive
|
||||||
|
* @param {Lexer=} lexer the lexer which in turn holds the input string
|
||||||
|
*/
|
||||||
|
function Token(text, start, end, lexer) {
|
||||||
this.text = text;
|
this.text = text;
|
||||||
this.data = data;
|
this.start = start;
|
||||||
this.position = position;
|
this.end = end;
|
||||||
|
this.lexer = lexer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a pair of tokens (this and endToken), compute a “Token” encompassing
|
||||||
|
* the whole input range enclosed by these two.
|
||||||
|
*
|
||||||
|
* @param {Token} endToken last token of the range, inclusive
|
||||||
|
* @param {string} text the text of the newly constructed token
|
||||||
|
*/
|
||||||
|
Token.prototype.range = function(endToken, text) {
|
||||||
|
if (endToken.lexer !== this.lexer) {
|
||||||
|
return new Token(text); // sorry, no position information available
|
||||||
|
}
|
||||||
|
return new Token(text, this.start, endToken.end, this.lexer);
|
||||||
|
};
|
||||||
|
|
||||||
/* The following tokenRegex
|
/* The following tokenRegex
|
||||||
* - matches typical whitespace (but not NBSP etc.) using its first group
|
* - matches typical whitespace (but not NBSP etc.) using its first group
|
||||||
* - matches symbol combinations which result in a single output character
|
* - matches symbol combinations which result in a single output character
|
||||||
@@ -52,111 +86,26 @@ var tokenRegex = new RegExp(
|
|||||||
")"
|
")"
|
||||||
);
|
);
|
||||||
|
|
||||||
var whitespaceRegex = /\s*/;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function lexes a single normal token. It takes a position and
|
* This function lexes a single token.
|
||||||
* whether it should completely ignore whitespace or not.
|
|
||||||
*/
|
*/
|
||||||
Lexer.prototype._innerLex = function(pos, ignoreWhitespace) {
|
Lexer.prototype.lex = function() {
|
||||||
var input = this._input;
|
var input = this.input;
|
||||||
|
var pos = this.pos;
|
||||||
if (pos === input.length) {
|
if (pos === input.length) {
|
||||||
return new Token("EOF", null, pos);
|
return new Token("EOF", pos, pos, this);
|
||||||
}
|
}
|
||||||
var match = matchAt(tokenRegex, input, pos);
|
var match = matchAt(tokenRegex, input, pos);
|
||||||
if (match === null) {
|
if (match === null) {
|
||||||
throw new ParseError(
|
throw new ParseError(
|
||||||
"Unexpected character: '" + input[pos] + "'",
|
"Unexpected character: '" + input[pos] + "'",
|
||||||
this, pos);
|
new Token(input[pos], pos, pos + 1, this));
|
||||||
} else if (match[2]) { // matched non-whitespace
|
|
||||||
return new Token(match[2], null, pos + match[2].length);
|
|
||||||
} else if (ignoreWhitespace) {
|
|
||||||
return this._innerLex(pos + match[1].length, true);
|
|
||||||
} else { // concatenate whitespace to a single space
|
|
||||||
return new Token(" ", null, pos + match[1].length);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// A regex to match a CSS color (like #ffffff or BlueViolet)
|
|
||||||
var cssColor = /#[a-z0-9]+|[a-z]+/i;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This function lexes a CSS color.
|
|
||||||
*/
|
|
||||||
Lexer.prototype._innerLexColor = function(pos) {
|
|
||||||
var input = this._input;
|
|
||||||
|
|
||||||
// Ignore whitespace
|
|
||||||
var whitespace = matchAt(whitespaceRegex, input, pos)[0];
|
|
||||||
pos += whitespace.length;
|
|
||||||
|
|
||||||
var match;
|
|
||||||
if ((match = matchAt(cssColor, input, pos))) {
|
|
||||||
// If we look like a color, return a color
|
|
||||||
return new Token(match[0], null, pos + match[0].length);
|
|
||||||
} else {
|
|
||||||
throw new ParseError("Invalid color", this, pos);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// A regex to match a dimension. Dimensions look like
|
|
||||||
// "1.2em" or ".4pt" or "1 ex"
|
|
||||||
var sizeRegex = /(-?)\s*(\d+(?:\.\d*)?|\.\d+)\s*([a-z]{2})/;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This function lexes a dimension.
|
|
||||||
*/
|
|
||||||
Lexer.prototype._innerLexSize = function(pos) {
|
|
||||||
var input = this._input;
|
|
||||||
|
|
||||||
// Ignore whitespace
|
|
||||||
var whitespace = matchAt(whitespaceRegex, input, pos)[0];
|
|
||||||
pos += whitespace.length;
|
|
||||||
|
|
||||||
var match;
|
|
||||||
if ((match = matchAt(sizeRegex, input, pos))) {
|
|
||||||
var unit = match[3];
|
|
||||||
// We only currently handle "em" and "ex" units
|
|
||||||
if (unit !== "em" && unit !== "ex") {
|
|
||||||
throw new ParseError("Invalid unit: '" + unit + "'", this, pos);
|
|
||||||
}
|
|
||||||
return new Token(match[0], {
|
|
||||||
number: +(match[1] + match[2]),
|
|
||||||
unit: unit,
|
|
||||||
}, pos + match[0].length);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new ParseError("Invalid size", this, pos);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This function lexes a string of whitespace.
|
|
||||||
*/
|
|
||||||
Lexer.prototype._innerLexWhitespace = function(pos) {
|
|
||||||
var input = this._input;
|
|
||||||
|
|
||||||
var whitespace = matchAt(whitespaceRegex, input, pos)[0];
|
|
||||||
pos += whitespace.length;
|
|
||||||
|
|
||||||
return new Token(whitespace[0], null, pos);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This function lexes a single token starting at `pos` and of the given mode.
|
|
||||||
* Based on the mode, we defer to one of the `_innerLex` functions.
|
|
||||||
*/
|
|
||||||
Lexer.prototype.lex = function(pos, mode) {
|
|
||||||
if (mode === "math") {
|
|
||||||
return this._innerLex(pos, true);
|
|
||||||
} else if (mode === "text") {
|
|
||||||
return this._innerLex(pos, false);
|
|
||||||
} else if (mode === "color") {
|
|
||||||
return this._innerLexColor(pos);
|
|
||||||
} else if (mode === "size") {
|
|
||||||
return this._innerLexSize(pos);
|
|
||||||
} else if (mode === "whitespace") {
|
|
||||||
return this._innerLexWhitespace(pos);
|
|
||||||
}
|
}
|
||||||
|
var text = match[2] || " ";
|
||||||
|
var start = this.pos;
|
||||||
|
this.pos += match[0].length;
|
||||||
|
var end = this.pos;
|
||||||
|
return new Token(text, start, end, this);
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = Lexer;
|
module.exports = Lexer;
|
||||||
|
70
src/MacroExpander.js
Normal file
70
src/MacroExpander.js
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
/**
|
||||||
|
* This file contains the “gullet” where macros are expanded
|
||||||
|
* until only non-macro tokens remain.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var Lexer = require("./Lexer");
|
||||||
|
|
||||||
|
function MacroExpander(input, macros) {
|
||||||
|
this.lexer = new Lexer(input);
|
||||||
|
this.macros = macros;
|
||||||
|
this.stack = []; // contains tokens in REVERSE order
|
||||||
|
this.discardedWhiteSpace = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively expand first token, then return first non-expandable token.
|
||||||
|
*/
|
||||||
|
MacroExpander.prototype.nextToken = function() {
|
||||||
|
for (;;) {
|
||||||
|
if (this.stack.length === 0) {
|
||||||
|
this.stack.push(this.lexer.lex());
|
||||||
|
}
|
||||||
|
var topToken = this.stack.pop();
|
||||||
|
var name = topToken.text;
|
||||||
|
if (!(name.charAt(0) === "\\" && this.macros.hasOwnProperty(name))) {
|
||||||
|
return topToken;
|
||||||
|
}
|
||||||
|
var expansion = this.macros[name];
|
||||||
|
if (typeof expansion === "string") {
|
||||||
|
var bodyLexer = new Lexer(expansion);
|
||||||
|
expansion = [];
|
||||||
|
var tok = bodyLexer.lex();
|
||||||
|
while (tok.text !== "EOF") {
|
||||||
|
expansion.push(tok);
|
||||||
|
tok = bodyLexer.lex();
|
||||||
|
}
|
||||||
|
expansion.reverse(); // to fit in with stack using push and pop
|
||||||
|
this.macros[name] = expansion;
|
||||||
|
}
|
||||||
|
this.stack = this.stack.concat(expansion);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
MacroExpander.prototype.get = function(ignoreSpace) {
|
||||||
|
this.discardedWhiteSpace = [];
|
||||||
|
var token = this.nextToken();
|
||||||
|
if (ignoreSpace) {
|
||||||
|
while (token.text === " ") {
|
||||||
|
this.discardedWhiteSpace.push(token);
|
||||||
|
token = this.nextToken();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Undo the effect of the preceding call to the get method.
|
||||||
|
* A call to this method MUST be immediately preceded and immediately followed
|
||||||
|
* by a call to get. Only used during mode switching, i.e. after one token
|
||||||
|
* was got in the old mode but should get got again in a new mode
|
||||||
|
* with possibly different whitespace handling.
|
||||||
|
*/
|
||||||
|
MacroExpander.prototype.unget = function(token) {
|
||||||
|
this.stack.push(token);
|
||||||
|
while (this.discardedWhiteSpace.length !== 0) {
|
||||||
|
this.stack.push(this.discardedWhiteSpace.pop());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = MacroExpander;
|
@@ -2,26 +2,50 @@
|
|||||||
* This is the ParseError class, which is the main error thrown by KaTeX
|
* This is the ParseError class, which is the main error thrown by KaTeX
|
||||||
* functions when something has gone wrong. This is used to distinguish internal
|
* functions when something has gone wrong. This is used to distinguish internal
|
||||||
* errors from errors in the expression that the user provided.
|
* errors from errors in the expression that the user provided.
|
||||||
|
*
|
||||||
|
* If possible, a caller should provide a Token or ParseNode with information
|
||||||
|
* about where in the source string the problem occurred.
|
||||||
|
*
|
||||||
|
* @param {string} message The error message
|
||||||
|
* @param {(Token|ParseNode)=} token An object providing position information
|
||||||
*/
|
*/
|
||||||
function ParseError(message, lexer, position) {
|
function ParseError(message, token) {
|
||||||
var error = "KaTeX parse error: " + message;
|
var error = "KaTeX parse error: " + message;
|
||||||
|
var start;
|
||||||
|
var end;
|
||||||
|
|
||||||
if (lexer !== undefined && position !== undefined) {
|
if (token && token.lexer && token.start <= token.end) {
|
||||||
// If we have the input and a position, make the error a bit fancier
|
// If we have the input and a position, make the error a bit fancier
|
||||||
|
|
||||||
// Prepend some information
|
|
||||||
error += " at position " + position + ": ";
|
|
||||||
|
|
||||||
// Get the input
|
// Get the input
|
||||||
var input = lexer._input;
|
var input = token.lexer.input;
|
||||||
// Insert a combining underscore at the correct position
|
|
||||||
input = input.slice(0, position) + "\u0332" +
|
// Prepend some information
|
||||||
input.slice(position);
|
start = token.start;
|
||||||
|
end = token.end;
|
||||||
|
if (start === input.length) {
|
||||||
|
error += " at end of input: ";
|
||||||
|
} else {
|
||||||
|
error += " at position " + (start + 1) + ": ";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Underline token in question using combining underscores
|
||||||
|
var underlined = input.slice(start, end).replace(/[^]/g, "$&\u0332");
|
||||||
|
|
||||||
// Extract some context from the input and add it to the error
|
// Extract some context from the input and add it to the error
|
||||||
var begin = Math.max(0, position - 15);
|
var left;
|
||||||
var end = position + 15;
|
if (start > 15) {
|
||||||
error += input.slice(begin, end);
|
left = "…" + input.slice(start - 15, start);
|
||||||
|
} else {
|
||||||
|
left = input.slice(0, start);
|
||||||
|
}
|
||||||
|
var right;
|
||||||
|
if (end + 15 < input.length) {
|
||||||
|
right = input.slice(end, end + 15) + "…";
|
||||||
|
} else {
|
||||||
|
right = input.slice(end);
|
||||||
|
}
|
||||||
|
error += left + underlined + right;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Some hackery to make ParseError a prototype of Error
|
// Some hackery to make ParseError a prototype of Error
|
||||||
@@ -30,7 +54,7 @@ function ParseError(message, lexer, position) {
|
|||||||
self.name = "ParseError";
|
self.name = "ParseError";
|
||||||
self.__proto__ = ParseError.prototype;
|
self.__proto__ = ParseError.prototype;
|
||||||
|
|
||||||
self.position = position;
|
self.position = start;
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
234
src/Parser.js
234
src/Parser.js
@@ -1,7 +1,7 @@
|
|||||||
/* eslint no-constant-condition:0 */
|
/* eslint no-constant-condition:0 */
|
||||||
var functions = require("./functions");
|
var functions = require("./functions");
|
||||||
var environments = require("./environments");
|
var environments = require("./environments");
|
||||||
var Lexer = require("./Lexer");
|
var MacroExpander = require("./MacroExpander");
|
||||||
var symbols = require("./symbols");
|
var symbols = require("./symbols");
|
||||||
var utils = require("./utils");
|
var utils = require("./utils");
|
||||||
|
|
||||||
@@ -46,8 +46,9 @@ var ParseError = require("./ParseError");
|
|||||||
* Main Parser class
|
* Main Parser class
|
||||||
*/
|
*/
|
||||||
function Parser(input, settings) {
|
function Parser(input, settings) {
|
||||||
// Make a new lexer
|
// Create a new macro expander (gullet) and (indirectly via that) also a
|
||||||
this.lexer = new Lexer(input);
|
// new lexer (mouth) for this parser (stomach, in the language of TeX)
|
||||||
|
this.gullet = new MacroExpander(input, settings.macros);
|
||||||
// Store the settings for use in parsing
|
// Store the settings for use in parsing
|
||||||
this.settings = settings;
|
this.settings = settings;
|
||||||
}
|
}
|
||||||
@@ -58,10 +59,11 @@ var ParseNode = parseData.ParseNode;
|
|||||||
* An initial function (without its arguments), or an argument to a function.
|
* An initial function (without its arguments), or an argument to a function.
|
||||||
* The `result` argument should be a ParseNode.
|
* The `result` argument should be a ParseNode.
|
||||||
*/
|
*/
|
||||||
function ParseFuncOrArgument(result, isFunction) {
|
function ParseFuncOrArgument(result, isFunction, token) {
|
||||||
this.result = result;
|
this.result = result;
|
||||||
// Is this a function (i.e. is it something defined in functions.js)?
|
// Is this a function (i.e. is it something defined in functions.js)?
|
||||||
this.isFunction = isFunction;
|
this.isFunction = isFunction;
|
||||||
|
this.token = token;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -75,7 +77,7 @@ Parser.prototype.expect = function(text, consume) {
|
|||||||
if (this.nextToken.text !== text) {
|
if (this.nextToken.text !== text) {
|
||||||
throw new ParseError(
|
throw new ParseError(
|
||||||
"Expected '" + text + "', got '" + this.nextToken.text + "'",
|
"Expected '" + text + "', got '" + this.nextToken.text + "'",
|
||||||
this.lexer, this.nextToken.position
|
this.nextToken
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (consume !== false) {
|
if (consume !== false) {
|
||||||
@@ -88,8 +90,13 @@ Parser.prototype.expect = function(text, consume) {
|
|||||||
* and fetches the one after that as the new look ahead.
|
* and fetches the one after that as the new look ahead.
|
||||||
*/
|
*/
|
||||||
Parser.prototype.consume = function() {
|
Parser.prototype.consume = function() {
|
||||||
this.pos = this.nextToken.position;
|
this.nextToken = this.gullet.get(this.mode === "math");
|
||||||
this.nextToken = this.lexer.lex(this.pos, this.mode);
|
};
|
||||||
|
|
||||||
|
Parser.prototype.switchMode = function(newMode) {
|
||||||
|
this.gullet.unget(this.nextToken);
|
||||||
|
this.mode = newMode;
|
||||||
|
this.consume();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -100,8 +107,7 @@ Parser.prototype.consume = function() {
|
|||||||
Parser.prototype.parse = function() {
|
Parser.prototype.parse = function() {
|
||||||
// Try to parse the input
|
// Try to parse the input
|
||||||
this.mode = "math";
|
this.mode = "math";
|
||||||
this.pos = 0;
|
this.consume();
|
||||||
this.nextToken = this.lexer.lex(this.pos, this.mode);
|
|
||||||
var parse = this.parseInput();
|
var parse = this.parseInput();
|
||||||
return parse;
|
return parse;
|
||||||
};
|
};
|
||||||
@@ -126,22 +132,25 @@ var endOfExpression = ["}", "\\end", "\\right", "&", "\\\\", "\\cr"];
|
|||||||
* nodes? This happens when functions have higher precendence
|
* nodes? This happens when functions have higher precendence
|
||||||
* than infix nodes in implicit parses.
|
* than infix nodes in implicit parses.
|
||||||
*
|
*
|
||||||
* @param {?string} breakOnToken The token that the expression should end with,
|
* @param {?string} breakOnTokenText The text of the token that the expression
|
||||||
* or `null` if something else should end the expression.
|
* should end with, or `null` if something else should end the
|
||||||
|
* expression.
|
||||||
*
|
*
|
||||||
* @return {ParseNode}
|
* @return {ParseNode}
|
||||||
*/
|
*/
|
||||||
Parser.prototype.parseExpression = function(breakOnInfix, breakOnToken) {
|
Parser.prototype.parseExpression = function(breakOnInfix, breakOnTokenText) {
|
||||||
var body = [];
|
var body = [];
|
||||||
// Keep adding atoms to the body until we can't parse any more atoms (either
|
// Keep adding atoms to the body until we can't parse any more atoms (either
|
||||||
// we reached the end, a }, or a \right)
|
// we reached the end, a }, or a \right)
|
||||||
while (true) {
|
while (true) {
|
||||||
var lex = this.nextToken;
|
var lex = this.nextToken;
|
||||||
var pos = this.pos;
|
|
||||||
if (endOfExpression.indexOf(lex.text) !== -1) {
|
if (endOfExpression.indexOf(lex.text) !== -1) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (breakOnToken && lex.text === breakOnToken) {
|
if (breakOnTokenText && lex.text === breakOnTokenText) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (breakOnInfix && functions[lex.text] && functions[lex.text].infix) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
var atom = this.parseAtom();
|
var atom = this.parseAtom();
|
||||||
@@ -149,19 +158,11 @@ Parser.prototype.parseExpression = function(breakOnInfix, breakOnToken) {
|
|||||||
if (!this.settings.throwOnError && lex.text[0] === "\\") {
|
if (!this.settings.throwOnError && lex.text[0] === "\\") {
|
||||||
var errorNode = this.handleUnsupportedCmd();
|
var errorNode = this.handleUnsupportedCmd();
|
||||||
body.push(errorNode);
|
body.push(errorNode);
|
||||||
|
|
||||||
pos = lex.position;
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (breakOnInfix && atom.type === "infix") {
|
|
||||||
// rewind so we can parse the infix atom again
|
|
||||||
this.pos = pos;
|
|
||||||
this.nextToken = lex;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
body.push(atom);
|
body.push(atom);
|
||||||
}
|
}
|
||||||
return this.handleInfixNodes(body);
|
return this.handleInfixNodes(body);
|
||||||
@@ -184,8 +185,9 @@ Parser.prototype.handleInfixNodes = function(body) {
|
|||||||
var node = body[i];
|
var node = body[i];
|
||||||
if (node.type === "infix") {
|
if (node.type === "infix") {
|
||||||
if (overIndex !== -1) {
|
if (overIndex !== -1) {
|
||||||
throw new ParseError("only one infix operator per group",
|
throw new ParseError(
|
||||||
this.lexer, -1);
|
"only one infix operator per group",
|
||||||
|
node.value.token);
|
||||||
}
|
}
|
||||||
overIndex = i;
|
overIndex = i;
|
||||||
funcName = node.value.replaceWith;
|
funcName = node.value.replaceWith;
|
||||||
@@ -226,8 +228,8 @@ var SUPSUB_GREEDINESS = 1;
|
|||||||
* Handle a subscript or superscript with nice errors.
|
* Handle a subscript or superscript with nice errors.
|
||||||
*/
|
*/
|
||||||
Parser.prototype.handleSupSubscript = function(name) {
|
Parser.prototype.handleSupSubscript = function(name) {
|
||||||
var symbol = this.nextToken.text;
|
var symbolToken = this.nextToken;
|
||||||
var symPos = this.pos;
|
var symbol = symbolToken.text;
|
||||||
this.consume();
|
this.consume();
|
||||||
var group = this.parseGroup();
|
var group = this.parseGroup();
|
||||||
|
|
||||||
@@ -237,8 +239,7 @@ Parser.prototype.handleSupSubscript = function(name) {
|
|||||||
} else {
|
} else {
|
||||||
throw new ParseError(
|
throw new ParseError(
|
||||||
"Expected group after '" + symbol + "'",
|
"Expected group after '" + symbol + "'",
|
||||||
this.lexer,
|
symbolToken
|
||||||
symPos + 1
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (group.isFunction) {
|
} else if (group.isFunction) {
|
||||||
@@ -250,8 +251,7 @@ Parser.prototype.handleSupSubscript = function(name) {
|
|||||||
} else {
|
} else {
|
||||||
throw new ParseError(
|
throw new ParseError(
|
||||||
"Got function '" + group.result + "' with no arguments " +
|
"Got function '" + group.result + "' with no arguments " +
|
||||||
"as " + name,
|
"as " + name, symbolToken);
|
||||||
this.lexer, symPos + 1);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return group.result;
|
return group.result;
|
||||||
@@ -319,7 +319,7 @@ Parser.prototype.parseAtom = function() {
|
|||||||
if (!base || base.type !== "op") {
|
if (!base || base.type !== "op") {
|
||||||
throw new ParseError(
|
throw new ParseError(
|
||||||
"Limit controls must follow a math operator",
|
"Limit controls must follow a math operator",
|
||||||
this.lexer, this.pos);
|
lex);
|
||||||
} else {
|
} else {
|
||||||
var limits = lex.text === "\\limits";
|
var limits = lex.text === "\\limits";
|
||||||
base.value.limits = limits;
|
base.value.limits = limits;
|
||||||
@@ -329,15 +329,13 @@ Parser.prototype.parseAtom = function() {
|
|||||||
} else if (lex.text === "^") {
|
} else if (lex.text === "^") {
|
||||||
// We got a superscript start
|
// We got a superscript start
|
||||||
if (superscript) {
|
if (superscript) {
|
||||||
throw new ParseError(
|
throw new ParseError("Double superscript", lex);
|
||||||
"Double superscript", this.lexer, this.pos);
|
|
||||||
}
|
}
|
||||||
superscript = this.handleSupSubscript("superscript");
|
superscript = this.handleSupSubscript("superscript");
|
||||||
} else if (lex.text === "_") {
|
} else if (lex.text === "_") {
|
||||||
// We got a subscript start
|
// We got a subscript start
|
||||||
if (subscript) {
|
if (subscript) {
|
||||||
throw new ParseError(
|
throw new ParseError("Double subscript", lex);
|
||||||
"Double subscript", this.lexer, this.pos);
|
|
||||||
}
|
}
|
||||||
subscript = this.handleSupSubscript("subscript");
|
subscript = this.handleSupSubscript("subscript");
|
||||||
} else if (lex.text === "'") {
|
} else if (lex.text === "'") {
|
||||||
@@ -427,8 +425,7 @@ Parser.prototype.parseImplicitGroup = function() {
|
|||||||
var envName = begin.value.name;
|
var envName = begin.value.name;
|
||||||
if (!environments.hasOwnProperty(envName)) {
|
if (!environments.hasOwnProperty(envName)) {
|
||||||
throw new ParseError(
|
throw new ParseError(
|
||||||
"No such environment: " + envName,
|
"No such environment: " + envName, begin.value.nameGroup);
|
||||||
this.lexer, begin.value.namepos);
|
|
||||||
}
|
}
|
||||||
// Build the environment object. Arguments and other information will
|
// Build the environment object. Arguments and other information will
|
||||||
// be made available to the begin and end methods using properties.
|
// be made available to the begin and end methods using properties.
|
||||||
@@ -438,19 +435,17 @@ Parser.prototype.parseImplicitGroup = function() {
|
|||||||
mode: this.mode,
|
mode: this.mode,
|
||||||
envName: envName,
|
envName: envName,
|
||||||
parser: this,
|
parser: this,
|
||||||
lexer: this.lexer,
|
|
||||||
positions: args.pop(),
|
positions: args.pop(),
|
||||||
};
|
};
|
||||||
var result = env.handler(context, args);
|
var result = env.handler(context, args);
|
||||||
this.expect("\\end", false);
|
this.expect("\\end", false);
|
||||||
|
var endNameToken = this.nextToken;
|
||||||
var end = this.parseFunction();
|
var end = this.parseFunction();
|
||||||
if (end.value.name !== envName) {
|
if (end.value.name !== envName) {
|
||||||
throw new ParseError(
|
throw new ParseError(
|
||||||
"Mismatch: \\begin{" + envName + "} matched " +
|
"Mismatch: \\begin{" + envName + "} matched " +
|
||||||
"by \\end{" + end.value.name + "}",
|
"by \\end{" + end.value.name + "}",
|
||||||
this.lexer /* , end.value.namepos */);
|
endNameToken);
|
||||||
// TODO: Add position to the above line and adjust test case,
|
|
||||||
// requires #385 to get merged first
|
|
||||||
}
|
}
|
||||||
result.position = end.position;
|
result.position = end.position;
|
||||||
return result;
|
return result;
|
||||||
@@ -497,11 +492,12 @@ Parser.prototype.parseFunction = function(baseGroup) {
|
|||||||
if (this.mode === "text" && !funcData.allowedInText) {
|
if (this.mode === "text" && !funcData.allowedInText) {
|
||||||
throw new ParseError(
|
throw new ParseError(
|
||||||
"Can't use function '" + func + "' in text mode",
|
"Can't use function '" + func + "' in text mode",
|
||||||
this.lexer, baseGroup.position);
|
baseGroup.token);
|
||||||
}
|
}
|
||||||
|
|
||||||
var args = this.parseArguments(func, funcData);
|
var args = this.parseArguments(func, funcData);
|
||||||
var result = this.callFunction(func, args, args.pop());
|
var token = baseGroup.token;
|
||||||
|
var result = this.callFunction(func, args, args.pop(), token);
|
||||||
return new ParseNode(result.type, result, this.mode);
|
return new ParseNode(result.type, result, this.mode);
|
||||||
} else {
|
} else {
|
||||||
return baseGroup.result;
|
return baseGroup.result;
|
||||||
@@ -514,12 +510,12 @@ Parser.prototype.parseFunction = function(baseGroup) {
|
|||||||
/**
|
/**
|
||||||
* Call a function handler with a suitable context and arguments.
|
* Call a function handler with a suitable context and arguments.
|
||||||
*/
|
*/
|
||||||
Parser.prototype.callFunction = function(name, args, positions) {
|
Parser.prototype.callFunction = function(name, args, positions, token) {
|
||||||
var context = {
|
var context = {
|
||||||
funcName: name,
|
funcName: name,
|
||||||
parser: this,
|
parser: this,
|
||||||
lexer: this.lexer,
|
|
||||||
positions: positions,
|
positions: positions,
|
||||||
|
token: token,
|
||||||
};
|
};
|
||||||
return functions[name].handler(context, args);
|
return functions[name].handler(context, args);
|
||||||
};
|
};
|
||||||
@@ -542,11 +538,12 @@ Parser.prototype.parseArguments = function(func, funcData) {
|
|||||||
var args = [];
|
var args = [];
|
||||||
|
|
||||||
for (var i = 0; i < totalArgs; i++) {
|
for (var i = 0; i < totalArgs; i++) {
|
||||||
|
var nextToken = this.nextToken;
|
||||||
var argType = funcData.argTypes && funcData.argTypes[i];
|
var argType = funcData.argTypes && funcData.argTypes[i];
|
||||||
var arg;
|
var arg;
|
||||||
if (i < funcData.numOptionalArgs) {
|
if (i < funcData.numOptionalArgs) {
|
||||||
if (argType) {
|
if (argType) {
|
||||||
arg = this.parseSpecialGroup(argType, true);
|
arg = this.parseGroupOfType(argType, true);
|
||||||
} else {
|
} else {
|
||||||
arg = this.parseOptionalGroup();
|
arg = this.parseOptionalGroup();
|
||||||
}
|
}
|
||||||
@@ -557,7 +554,7 @@ Parser.prototype.parseArguments = function(func, funcData) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (argType) {
|
if (argType) {
|
||||||
arg = this.parseSpecialGroup(argType);
|
arg = this.parseGroupOfType(argType);
|
||||||
} else {
|
} else {
|
||||||
arg = this.parseGroup();
|
arg = this.parseGroup();
|
||||||
}
|
}
|
||||||
@@ -569,8 +566,7 @@ Parser.prototype.parseArguments = function(func, funcData) {
|
|||||||
false);
|
false);
|
||||||
} else {
|
} else {
|
||||||
throw new ParseError(
|
throw new ParseError(
|
||||||
"Expected group after '" + func + "'",
|
"Expected group after '" + func + "'", nextToken);
|
||||||
this.lexer, this.pos);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -583,8 +579,7 @@ Parser.prototype.parseArguments = function(func, funcData) {
|
|||||||
} else {
|
} else {
|
||||||
throw new ParseError(
|
throw new ParseError(
|
||||||
"Got function '" + arg.result + "' as " +
|
"Got function '" + arg.result + "' as " +
|
||||||
"argument to '" + func + "'",
|
"argument to '" + func + "'", nextToken);
|
||||||
this.lexer, this.pos - 1);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
argNode = arg.result;
|
argNode = arg.result;
|
||||||
@@ -600,64 +595,117 @@ Parser.prototype.parseArguments = function(func, funcData) {
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses a group when the mode is changing. Takes a position, a new mode, and
|
* Parses a group when the mode is changing.
|
||||||
* an outer mode that is used to parse the outside.
|
|
||||||
*
|
*
|
||||||
* @return {?ParseFuncOrArgument}
|
* @return {?ParseFuncOrArgument}
|
||||||
*/
|
*/
|
||||||
Parser.prototype.parseSpecialGroup = function(innerMode, optional) {
|
Parser.prototype.parseGroupOfType = function(innerMode, optional) {
|
||||||
var outerMode = this.mode;
|
var outerMode = this.mode;
|
||||||
// Handle `original` argTypes
|
// Handle `original` argTypes
|
||||||
if (innerMode === "original") {
|
if (innerMode === "original") {
|
||||||
innerMode = outerMode;
|
innerMode = outerMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (innerMode === "color" || innerMode === "size") {
|
|
||||||
// color and size modes are special because they should have braces and
|
|
||||||
// should only lex a single symbol inside
|
|
||||||
var openBrace = this.nextToken;
|
|
||||||
if (optional && openBrace.text !== "[") {
|
|
||||||
// optional arguments should return null if they don't exist
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
// The call to expect will lex the token after the '{' in inner mode
|
|
||||||
this.mode = innerMode;
|
|
||||||
this.expect(optional ? "[" : "{");
|
|
||||||
var inner = this.nextToken;
|
|
||||||
this.mode = outerMode;
|
|
||||||
var data;
|
|
||||||
if (innerMode === "color") {
|
if (innerMode === "color") {
|
||||||
data = inner.text;
|
return this.parseColorGroup(optional);
|
||||||
} else {
|
|
||||||
data = inner.data;
|
|
||||||
}
|
}
|
||||||
this.consume(); // consume the token stored in inner
|
if (innerMode === "size") {
|
||||||
this.expect(optional ? "]" : "}");
|
return this.parseSizeGroup(optional);
|
||||||
return new ParseFuncOrArgument(
|
|
||||||
new ParseNode(innerMode, data, outerMode),
|
|
||||||
false);
|
|
||||||
} else if (innerMode === "text") {
|
|
||||||
// text mode is special because it should ignore the whitespace before
|
|
||||||
// it
|
|
||||||
var whitespace = this.lexer.lex(this.pos, "whitespace");
|
|
||||||
this.pos = whitespace.position;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.switchMode(innerMode);
|
||||||
|
if (innerMode === "text") {
|
||||||
|
// text mode is special because it should ignore the whitespace before
|
||||||
|
// it
|
||||||
|
while (this.nextToken.text === " ") {
|
||||||
|
this.consume();
|
||||||
|
}
|
||||||
|
}
|
||||||
// By the time we get here, innerMode is one of "text" or "math".
|
// By the time we get here, innerMode is one of "text" or "math".
|
||||||
// We switch the mode of the parser, recurse, then restore the old mode.
|
// We switch the mode of the parser, recurse, then restore the old mode.
|
||||||
this.mode = innerMode;
|
|
||||||
this.nextToken = this.lexer.lex(this.pos, innerMode);
|
|
||||||
var res;
|
var res;
|
||||||
if (optional) {
|
if (optional) {
|
||||||
res = this.parseOptionalGroup();
|
res = this.parseOptionalGroup();
|
||||||
} else {
|
} else {
|
||||||
res = this.parseGroup();
|
res = this.parseGroup();
|
||||||
}
|
}
|
||||||
this.mode = outerMode;
|
this.switchMode(outerMode);
|
||||||
this.nextToken = this.lexer.lex(this.pos, outerMode);
|
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a group, essentially returning the string formed by the
|
||||||
|
* brace-enclosed tokens plus some position information.
|
||||||
|
*
|
||||||
|
* @param {string} modeName Used to describe the mode in error messages
|
||||||
|
* @param {boolean} optional Whether the group is optional or required
|
||||||
|
*/
|
||||||
|
Parser.prototype.parseStringGroup = function(modeName, optional) {
|
||||||
|
if (optional && this.nextToken.text !== "[") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var outerMode = this.mode;
|
||||||
|
this.mode = "text";
|
||||||
|
this.expect(optional ? "[" : "{");
|
||||||
|
var str = "";
|
||||||
|
var firstToken = this.nextToken;
|
||||||
|
var lastToken = firstToken;
|
||||||
|
while (this.nextToken.text !== (optional ? "]" : "}")) {
|
||||||
|
if (this.nextToken.text === "EOF") {
|
||||||
|
throw new ParseError(
|
||||||
|
"Unexpected end of input in " + modeName,
|
||||||
|
firstToken.range(this.nextToken, str));
|
||||||
|
}
|
||||||
|
lastToken = this.nextToken;
|
||||||
|
str += lastToken.text;
|
||||||
|
this.consume();
|
||||||
|
}
|
||||||
|
this.mode = outerMode;
|
||||||
|
this.expect(optional ? "]" : "}");
|
||||||
|
return firstToken.range(lastToken, str);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a color description.
|
||||||
|
*/
|
||||||
|
Parser.prototype.parseColorGroup = function(optional) {
|
||||||
|
var res = this.parseStringGroup("color", optional);
|
||||||
|
if (!res) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var match = (/^(#[a-z0-9]+|[a-z]+)$/i).exec(res.text);
|
||||||
|
if (!match) {
|
||||||
|
throw new ParseError("Invalid color: '" + res.text + "'", res);
|
||||||
|
}
|
||||||
|
return new ParseFuncOrArgument(
|
||||||
|
new ParseNode("color", match[0], this.mode),
|
||||||
|
false);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a size specification, consisting of magnitude and unit.
|
||||||
|
*/
|
||||||
|
Parser.prototype.parseSizeGroup = function(optional) {
|
||||||
|
var res = this.parseStringGroup("size", optional);
|
||||||
|
if (!res) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var match = (/(-?) *(\d+(?:\.\d*)?|\.\d+) *([a-z]{2})/).exec(res.text);
|
||||||
|
if (!match) {
|
||||||
|
throw new ParseError("Invalid size: '" + res.text + "'", res);
|
||||||
|
}
|
||||||
|
var data = {
|
||||||
|
number: +(match[1] + match[2]), // sign + magnitude, cast to number
|
||||||
|
unit: match[3],
|
||||||
|
};
|
||||||
|
if (data.unit !== "em" && data.unit !== "ex") {
|
||||||
|
throw new ParseError("Invalid unit: '" + data.unit + "'", res);
|
||||||
|
}
|
||||||
|
return new ParseFuncOrArgument(
|
||||||
|
new ParseNode("color", data, this.mode),
|
||||||
|
false);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses a group, which is either a single nucleus (like "x") or an expression
|
* Parses a group, which is either a single nucleus (like "x") or an expression
|
||||||
* in braces (like "{x+y}")
|
* in braces (like "{x+y}")
|
||||||
@@ -665,15 +713,18 @@ Parser.prototype.parseSpecialGroup = function(innerMode, optional) {
|
|||||||
* @return {?ParseFuncOrArgument}
|
* @return {?ParseFuncOrArgument}
|
||||||
*/
|
*/
|
||||||
Parser.prototype.parseGroup = function() {
|
Parser.prototype.parseGroup = function() {
|
||||||
|
var firstToken = this.nextToken;
|
||||||
// Try to parse an open brace
|
// Try to parse an open brace
|
||||||
if (this.nextToken.text === "{") {
|
if (this.nextToken.text === "{") {
|
||||||
// If we get a brace, parse an expression
|
// If we get a brace, parse an expression
|
||||||
this.consume();
|
this.consume();
|
||||||
var expression = this.parseExpression(false);
|
var expression = this.parseExpression(false);
|
||||||
|
var lastToken = this.nextToken;
|
||||||
// Make sure we get a close brace
|
// Make sure we get a close brace
|
||||||
this.expect("}");
|
this.expect("}");
|
||||||
return new ParseFuncOrArgument(
|
return new ParseFuncOrArgument(
|
||||||
new ParseNode("ordgroup", expression, this.mode),
|
new ParseNode("ordgroup", expression, this.mode,
|
||||||
|
firstToken, lastToken),
|
||||||
false);
|
false);
|
||||||
} else {
|
} else {
|
||||||
// Otherwise, just return a nucleus
|
// Otherwise, just return a nucleus
|
||||||
@@ -687,15 +738,18 @@ Parser.prototype.parseGroup = function() {
|
|||||||
* @return {?ParseFuncOrArgument}
|
* @return {?ParseFuncOrArgument}
|
||||||
*/
|
*/
|
||||||
Parser.prototype.parseOptionalGroup = function() {
|
Parser.prototype.parseOptionalGroup = function() {
|
||||||
|
var firstToken = this.nextToken;
|
||||||
// Try to parse an open bracket
|
// Try to parse an open bracket
|
||||||
if (this.nextToken.text === "[") {
|
if (this.nextToken.text === "[") {
|
||||||
// If we get a brace, parse an expression
|
// If we get a brace, parse an expression
|
||||||
this.consume();
|
this.consume();
|
||||||
var expression = this.parseExpression(false, "]");
|
var expression = this.parseExpression(false, "]");
|
||||||
|
var lastToken = this.nextToken;
|
||||||
// Make sure we get a close bracket
|
// Make sure we get a close bracket
|
||||||
this.expect("]");
|
this.expect("]");
|
||||||
return new ParseFuncOrArgument(
|
return new ParseFuncOrArgument(
|
||||||
new ParseNode("ordgroup", expression, this.mode),
|
new ParseNode("ordgroup", expression, this.mode,
|
||||||
|
firstToken, lastToken),
|
||||||
false);
|
false);
|
||||||
} else {
|
} else {
|
||||||
// Otherwise, return null,
|
// Otherwise, return null,
|
||||||
@@ -718,15 +772,15 @@ Parser.prototype.parseSymbol = function() {
|
|||||||
// say that it is a function.
|
// say that it is a function.
|
||||||
return new ParseFuncOrArgument(
|
return new ParseFuncOrArgument(
|
||||||
nucleus.text,
|
nucleus.text,
|
||||||
true);
|
true, nucleus);
|
||||||
} else if (symbols[this.mode][nucleus.text]) {
|
} else if (symbols[this.mode][nucleus.text]) {
|
||||||
this.consume();
|
this.consume();
|
||||||
// Otherwise if this is a no-argument function, find the type it
|
// Otherwise if this is a no-argument function, find the type it
|
||||||
// corresponds to in the symbols map
|
// corresponds to in the symbols map
|
||||||
return new ParseFuncOrArgument(
|
return new ParseFuncOrArgument(
|
||||||
new ParseNode(symbols[this.mode][nucleus.text].group,
|
new ParseNode(symbols[this.mode][nucleus.text].group,
|
||||||
nucleus.text, this.mode),
|
nucleus.text, this.mode, nucleus),
|
||||||
false);
|
false, nucleus);
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@@ -23,6 +23,7 @@ function Settings(options) {
|
|||||||
this.displayMode = get(options.displayMode, false);
|
this.displayMode = get(options.displayMode, false);
|
||||||
this.throwOnError = get(options.throwOnError, true);
|
this.throwOnError = get(options.throwOnError, true);
|
||||||
this.errorColor = get(options.errorColor, "#cc0000");
|
this.errorColor = get(options.errorColor, "#cc0000");
|
||||||
|
this.macros = options.macros || {};
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Settings;
|
module.exports = Settings;
|
||||||
|
@@ -28,10 +28,8 @@ function parseArray(parser, result) {
|
|||||||
row = [];
|
row = [];
|
||||||
body.push(row);
|
body.push(row);
|
||||||
} else {
|
} else {
|
||||||
// TODO: Clean up the following hack once #385 got merged
|
|
||||||
var pos = Math.min(parser.pos + 1, parser.lexer._input.length);
|
|
||||||
throw new ParseError("Expected & or \\\\ or \\end",
|
throw new ParseError("Expected & or \\\\ or \\end",
|
||||||
parser.lexer, pos);
|
parser.nextToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result.body = body;
|
result.body = body;
|
||||||
@@ -106,7 +104,7 @@ defineEnvironment("array", {
|
|||||||
}
|
}
|
||||||
throw new ParseError(
|
throw new ParseError(
|
||||||
"Unknown column alignment: " + node.value,
|
"Unknown column alignment: " + node.value,
|
||||||
context.lexer, context.positions[1]);
|
node);
|
||||||
});
|
});
|
||||||
var res = {
|
var res = {
|
||||||
type: "array",
|
type: "array",
|
||||||
|
@@ -55,6 +55,7 @@ var ParseError = require("./ParseError");
|
|||||||
* should parse. If the optional arguments aren't found,
|
* should parse. If the optional arguments aren't found,
|
||||||
* `null` will be passed to the handler in their place.
|
* `null` will be passed to the handler in their place.
|
||||||
* (default 0)
|
* (default 0)
|
||||||
|
* - infix: (optional) Must be true if the function is an infix operator.
|
||||||
*
|
*
|
||||||
* The last argument is that implementation, the handler for the function(s).
|
* The last argument is that implementation, the handler for the function(s).
|
||||||
* It is called to handle these functions and their arguments.
|
* It is called to handle these functions and their arguments.
|
||||||
@@ -91,6 +92,7 @@ function defineFunction(names, props, handler) {
|
|||||||
greediness: (props.greediness === undefined) ? 1 : props.greediness,
|
greediness: (props.greediness === undefined) ? 1 : props.greediness,
|
||||||
allowedInText: !!props.allowedInText,
|
allowedInText: !!props.allowedInText,
|
||||||
numOptionalArgs: props.numOptionalArgs || 0,
|
numOptionalArgs: props.numOptionalArgs || 0,
|
||||||
|
infix: !!props.infix,
|
||||||
handler: handler,
|
handler: handler,
|
||||||
};
|
};
|
||||||
for (var i = 0; i < names.length; ++i) {
|
for (var i = 0; i < names.length; ++i) {
|
||||||
@@ -456,8 +458,7 @@ defineFunction([
|
|||||||
if (!utils.contains(delimiters, delim.value)) {
|
if (!utils.contains(delimiters, delim.value)) {
|
||||||
throw new ParseError(
|
throw new ParseError(
|
||||||
"Invalid delimiter: '" + delim.value + "' after '" +
|
"Invalid delimiter: '" + delim.value + "' after '" +
|
||||||
context.funcName + "'",
|
context.funcName + "'", delim);
|
||||||
context.lexer, context.positions[1]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// \left and \right are caught somewhere in Parser.js, which is
|
// \left and \right are caught somewhere in Parser.js, which is
|
||||||
@@ -536,6 +537,7 @@ defineFunction([
|
|||||||
// Infix generalized fractions
|
// Infix generalized fractions
|
||||||
defineFunction(["\\over", "\\choose"], {
|
defineFunction(["\\over", "\\choose"], {
|
||||||
numArgs: 0,
|
numArgs: 0,
|
||||||
|
infix: true,
|
||||||
}, function(context) {
|
}, function(context) {
|
||||||
var replaceWith;
|
var replaceWith;
|
||||||
switch (context.funcName) {
|
switch (context.funcName) {
|
||||||
@@ -551,6 +553,7 @@ defineFunction(["\\over", "\\choose"], {
|
|||||||
return {
|
return {
|
||||||
type: "infix",
|
type: "infix",
|
||||||
replaceWith: replaceWith,
|
replaceWith: replaceWith,
|
||||||
|
token: context.token,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -574,9 +577,7 @@ defineFunction(["\\begin", "\\end"], {
|
|||||||
}, function(context, args) {
|
}, function(context, args) {
|
||||||
var nameGroup = args[0];
|
var nameGroup = args[0];
|
||||||
if (nameGroup.type !== "ordgroup") {
|
if (nameGroup.type !== "ordgroup") {
|
||||||
throw new ParseError(
|
throw new ParseError("Invalid environment name", nameGroup);
|
||||||
"Invalid environment name",
|
|
||||||
context.lexer, context.positions[1]);
|
|
||||||
}
|
}
|
||||||
var name = "";
|
var name = "";
|
||||||
for (var i = 0; i < nameGroup.value.length; ++i) {
|
for (var i = 0; i < nameGroup.value.length; ++i) {
|
||||||
@@ -585,6 +586,6 @@ defineFunction(["\\begin", "\\end"], {
|
|||||||
return {
|
return {
|
||||||
type: "environment",
|
type: "environment",
|
||||||
name: name,
|
name: name,
|
||||||
namepos: context.positions[1],
|
nameGroup: nameGroup,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@@ -1,10 +1,29 @@
|
|||||||
/**
|
/**
|
||||||
* The resulting parse tree nodes of the parse tree.
|
* The resulting parse tree nodes of the parse tree.
|
||||||
|
*
|
||||||
|
* It is possible to provide position information, so that a ParseNode can
|
||||||
|
* fulfil a role similar to a Token in error reporting.
|
||||||
|
* For details on the corresponding properties see Token constructor.
|
||||||
|
* Providing such information can lead to better error reporting.
|
||||||
|
*
|
||||||
|
* @param {string} type type of node, like e.g. "ordgroup"
|
||||||
|
* @param {?object} value type-specific representation of the node
|
||||||
|
* @param {string} mode parse mode in action for this node,
|
||||||
|
* "math" or "text"
|
||||||
|
* @param {Token=} firstToken first token of the input for this node,
|
||||||
|
* will omit position information if unset
|
||||||
|
* @param {Token=} lastToken last token of the input for this node,
|
||||||
|
* will default to firstToken if unset
|
||||||
*/
|
*/
|
||||||
function ParseNode(type, value, mode) {
|
function ParseNode(type, value, mode, firstToken, lastToken) {
|
||||||
this.type = type;
|
this.type = type;
|
||||||
this.value = value;
|
this.value = value;
|
||||||
this.mode = mode;
|
this.mode = mode;
|
||||||
|
if (firstToken && (!lastToken || lastToken.lexer === firstToken.lexer)) {
|
||||||
|
this.lexer = firstToken.lexer;
|
||||||
|
this.start = firstToken.start;
|
||||||
|
this.end = (lastToken || firstToken).end;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
@@ -10,13 +10,9 @@ function init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ("addEventListener" in permalink) {
|
if ("addEventListener" in permalink) {
|
||||||
permalink.addEventListener("click", function() {
|
permalink.addEventListener("click", setSearch);
|
||||||
window.location.search = "?text=" + encodeURIComponent(input.value);
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
permalink.attachEvent("click", function() {
|
permalink.attachEvent("click", setSearch);
|
||||||
window.location.search = "?text=" + encodeURIComponent(input.value);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var match = (/(?:^\?|&)text=([^&]*)/).exec(window.location.search);
|
var match = (/(?:^\?|&)text=([^&]*)/).exec(window.location.search);
|
||||||
@@ -24,11 +20,26 @@ function init() {
|
|||||||
input.value = decodeURIComponent(match[1]);
|
input.value = decodeURIComponent(match[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var macros = {};
|
||||||
|
var options = {};
|
||||||
|
var macroRegex = /(?:^\?|&)(?:\\|%5[Cc])([A-Za-z]+)=([^&]*)/g;
|
||||||
|
var macroString = "";
|
||||||
|
while ((match = macroRegex.exec(window.location.search)) !== null) {
|
||||||
|
options.macros = macros;
|
||||||
|
macros["\\" + match[1]] = decodeURIComponent(match[2]);
|
||||||
|
macroString += "&" + match[0].substr(1);
|
||||||
|
}
|
||||||
|
|
||||||
reprocess();
|
reprocess();
|
||||||
|
|
||||||
|
function setSearch() {
|
||||||
|
window.location.search =
|
||||||
|
"?text=" + encodeURIComponent(input.value) + macroString;
|
||||||
|
}
|
||||||
|
|
||||||
function reprocess() {
|
function reprocess() {
|
||||||
try {
|
try {
|
||||||
katex.render(input.value, math);
|
katex.render(input.value, math, options);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.__proto__ == katex.ParseError.prototype) {
|
if (e.__proto__ == katex.ParseError.prototype) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
@@ -61,16 +61,15 @@ beforeEach(function() {
|
|||||||
describe("Parser:", function() {
|
describe("Parser:", function() {
|
||||||
|
|
||||||
describe("#handleInfixNodes", function() {
|
describe("#handleInfixNodes", function() {
|
||||||
// TODO: The position information here is broken, should be fixed.
|
|
||||||
it("rejects repeated infix operators", function() {
|
it("rejects repeated infix operators", function() {
|
||||||
expect("1\\over 2\\over 3").toFailWithParseError(
|
expect("1\\over 2\\over 3").toFailWithParseError(
|
||||||
"only one infix operator per group at position -1: " +
|
"only one infix operator per group at position 9: " +
|
||||||
"1\\over 2\\over ");
|
"1\\over 2\\̲o̲v̲e̲r̲ 3");
|
||||||
});
|
});
|
||||||
it("rejects conflicting infix operators", function() {
|
it("rejects conflicting infix operators", function() {
|
||||||
expect("1\\over 2\\choose 3").toFailWithParseError(
|
expect("1\\over 2\\choose 3").toFailWithParseError(
|
||||||
"only one infix operator per group at position -1: " +
|
"only one infix operator per group at position 9: " +
|
||||||
"1\\over 2\\choos");
|
"1\\over 2\\̲c̲h̲o̲o̲s̲e̲ 3");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -91,84 +90,85 @@ describe("Parser:", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("#parseAtom", function() {
|
describe("#parseAtom", function() {
|
||||||
// TODO: The positions in the following error messages appear to be
|
|
||||||
// off by one, i.e. they should be one character later.
|
|
||||||
it("rejects \\limits without operator", function() {
|
it("rejects \\limits without operator", function() {
|
||||||
expect("\\alpha\\limits\\omega").toFailWithParseError(
|
expect("\\alpha\\limits\\omega").toFailWithParseError(
|
||||||
"Limit controls must follow a math operator" +
|
"Limit controls must follow a math operator" +
|
||||||
" at position 6: \\alpha̲\\limits\\omega");
|
" at position 7: \\alpha\\̲l̲i̲m̲i̲t̲s̲\\omega");
|
||||||
});
|
});
|
||||||
it("rejects \\limits at the beginning of the input", function() {
|
it("rejects \\limits at the beginning of the input", function() {
|
||||||
expect("\\limits\\omega").toFailWithParseError(
|
expect("\\limits\\omega").toFailWithParseError(
|
||||||
"Limit controls must follow a math operator" +
|
"Limit controls must follow a math operator" +
|
||||||
" at position 0: ̲\\limits\\omega");
|
" at position 1: \\̲l̲i̲m̲i̲t̲s̲\\omega");
|
||||||
});
|
});
|
||||||
it("rejects double superscripts", function() {
|
it("rejects double superscripts", function() {
|
||||||
expect("1^2^3").toFailWithParseError(
|
expect("1^2^3").toFailWithParseError(
|
||||||
"Double superscript at position 3: 1^2̲^3");
|
"Double superscript at position 4: 1^2^̲3");
|
||||||
expect("1^{2+3}_4^5").toFailWithParseError(
|
expect("1^{2+3}_4^5").toFailWithParseError(
|
||||||
"Double superscript at position 9: 1^{2+3}_4̲^5");
|
"Double superscript at position 10: 1^{2+3}_4^̲5");
|
||||||
});
|
});
|
||||||
it("rejects double subscripts", function() {
|
it("rejects double subscripts", function() {
|
||||||
expect("1_2_3").toFailWithParseError(
|
expect("1_2_3").toFailWithParseError(
|
||||||
"Double subscript at position 3: 1_2̲_3");
|
"Double subscript at position 4: 1_2_̲3");
|
||||||
expect("1_{2+3}^4_5").toFailWithParseError(
|
expect("1_{2+3}^4_5").toFailWithParseError(
|
||||||
"Double subscript at position 9: 1_{2+3}^4̲_5");
|
"Double subscript at position 10: 1_{2+3}^4_̲5");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("#parseImplicitGroup", function() {
|
describe("#parseImplicitGroup", function() {
|
||||||
it("reports unknown environments", function() {
|
it("reports unknown environments", function() {
|
||||||
expect("\\begin{foo}bar\\end{foo}").toFailWithParseError(
|
expect("\\begin{foo}bar\\end{foo}").toFailWithParseError(
|
||||||
"No such environment: foo at position 11:" +
|
"No such environment: foo at position 7:" +
|
||||||
" \\begin{foo}̲bar\\end{foo}");
|
" \\begin{̲f̲o̲o̲}̲bar\\end{foo}");
|
||||||
});
|
});
|
||||||
it("reports mismatched environments", function() {
|
it("reports mismatched environments", function() {
|
||||||
expect("\\begin{pmatrix}1&2\\\\3&4\\end{bmatrix}+5")
|
expect("\\begin{pmatrix}1&2\\\\3&4\\end{bmatrix}+5")
|
||||||
.toFailWithParseError(
|
.toFailWithParseError(
|
||||||
"Mismatch: \\begin{pmatrix} matched by \\end{bmatrix}");
|
"Mismatch: \\begin{pmatrix} matched by \\end{bmatrix}" +
|
||||||
|
" at position 24: …matrix}1&2\\\\3&4\\̲e̲n̲d̲{bmatrix}+5");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("#parseFunction", function() {
|
describe("#parseFunction", function() {
|
||||||
it("rejects math-mode functions in text mode", function() {
|
it("rejects math-mode functions in text mode", function() {
|
||||||
// TODO: The position info is missing here
|
|
||||||
expect("\\text{\\sqrt2 is irrational}").toFailWithParseError(
|
expect("\\text{\\sqrt2 is irrational}").toFailWithParseError(
|
||||||
"Can't use function '\\sqrt' in text mode");
|
"Can't use function '\\sqrt' in text mode" +
|
||||||
|
" at position 7: \\text{\\̲s̲q̲r̲t̲2 is irrational…");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("#parseArguments", function() {
|
describe("#parseArguments", function() {
|
||||||
it("complains about missing argument at end of input", function() {
|
it("complains about missing argument at end of input", function() {
|
||||||
expect("2\\sqrt").toFailWithParseError(
|
expect("2\\sqrt").toFailWithParseError(
|
||||||
"Expected group after '\\sqrt' at position 6: 2\\sqrt̲");
|
"Expected group after '\\sqrt' at end of input: 2\\sqrt");
|
||||||
});
|
});
|
||||||
it("complains about missing argument at end of group", function() {
|
it("complains about missing argument at end of group", function() {
|
||||||
expect("1^{2\\sqrt}").toFailWithParseError(
|
expect("1^{2\\sqrt}").toFailWithParseError(
|
||||||
"Expected group after '\\sqrt' at position 9: 1^{2\\sqrt̲}");
|
"Expected group after '\\sqrt'" +
|
||||||
|
" at position 10: 1^{2\\sqrt}̲");
|
||||||
});
|
});
|
||||||
it("complains about functions as arguments to others", function() {
|
it("complains about functions as arguments to others", function() {
|
||||||
// TODO: The position looks pretty wrong here
|
// TODO: The position looks pretty wrong here
|
||||||
expect("\\sqrt\\over2").toFailWithParseError(
|
expect("\\sqrt\\over2").toFailWithParseError(
|
||||||
"Got function '\\over' as argument to '\\sqrt'" +
|
"Got function '\\over' as argument to '\\sqrt'" +
|
||||||
" at position 9: \\sqrt\\ove̲r2");
|
" at position 6: \\sqrt\\̲o̲v̲e̲r̲2");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("#parseArguments", function() {
|
describe("#parseArguments", function() {
|
||||||
it("complains about missing argument at end of input", function() {
|
it("complains about missing argument at end of input", function() {
|
||||||
expect("2\\sqrt").toFailWithParseError(
|
expect("2\\sqrt").toFailWithParseError(
|
||||||
"Expected group after '\\sqrt' at position 6: 2\\sqrt̲");
|
"Expected group after '\\sqrt' at end of input: 2\\sqrt");
|
||||||
});
|
});
|
||||||
it("complains about missing argument at end of group", function() {
|
it("complains about missing argument at end of group", function() {
|
||||||
expect("1^{2\\sqrt}").toFailWithParseError(
|
expect("1^{2\\sqrt}").toFailWithParseError(
|
||||||
"Expected group after '\\sqrt' at position 9: 1^{2\\sqrt̲}");
|
"Expected group after '\\sqrt'" +
|
||||||
|
" at position 10: 1^{2\\sqrt}̲");
|
||||||
});
|
});
|
||||||
it("complains about functions as arguments to others", function() {
|
it("complains about functions as arguments to others", function() {
|
||||||
// TODO: The position looks pretty wrong here
|
// TODO: The position looks pretty wrong here
|
||||||
expect("\\sqrt\\over2").toFailWithParseError(
|
expect("\\sqrt\\over2").toFailWithParseError(
|
||||||
"Got function '\\over' as argument to '\\sqrt'" +
|
"Got function '\\over' as argument to '\\sqrt'" +
|
||||||
" at position 9: \\sqrt\\ove̲r2");
|
" at position 6: \\sqrt\\̲o̲v̲e̲r̲2");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -183,12 +183,12 @@ describe("Parser.expect calls:", function() {
|
|||||||
});
|
});
|
||||||
it("complains about extra \\end", function() {
|
it("complains about extra \\end", function() {
|
||||||
expect("x\\end{matrix}").toFailWithParseError(
|
expect("x\\end{matrix}").toFailWithParseError(
|
||||||
"Expected 'EOF', got '\\end' at position 5:" +
|
"Expected 'EOF', got '\\end' at position 2:" +
|
||||||
" x\\end̲{matrix}");
|
" x\\̲e̲n̲d̲{matrix}");
|
||||||
});
|
});
|
||||||
it("complains about top-level \\\\", function() {
|
it("complains about top-level \\\\", function() {
|
||||||
expect("1\\\\2").toFailWithParseError(
|
expect("1\\\\2").toFailWithParseError(
|
||||||
"Expected 'EOF', got '\\\\' at position 3: 1\\\\̲2");
|
"Expected 'EOF', got '\\\\' at position 2: 1\\̲\\̲2");
|
||||||
});
|
});
|
||||||
it("complains about top-level &", function() {
|
it("complains about top-level &", function() {
|
||||||
expect("1&2").toFailWithParseError(
|
expect("1&2").toFailWithParseError(
|
||||||
@@ -199,8 +199,8 @@ describe("Parser.expect calls:", function() {
|
|||||||
describe("#parseImplicitGroup expecting \\right", function() {
|
describe("#parseImplicitGroup expecting \\right", function() {
|
||||||
it("rejects missing \\right", function() {
|
it("rejects missing \\right", function() {
|
||||||
expect("\\left(1+2)").toFailWithParseError(
|
expect("\\left(1+2)").toFailWithParseError(
|
||||||
"Expected '\\right', got 'EOF' at position 10:" +
|
"Expected '\\right', got 'EOF' at end of input:" +
|
||||||
" \\left(1+2)̲");
|
" \\left(1+2)");
|
||||||
});
|
});
|
||||||
it("rejects incorrectly scoped \\right", function() {
|
it("rejects incorrectly scoped \\right", function() {
|
||||||
expect("{\\left(1+2}\\right)").toFailWithParseError(
|
expect("{\\left(1+2}\\right)").toFailWithParseError(
|
||||||
@@ -224,32 +224,42 @@ describe("Parser.expect calls:", function() {
|
|||||||
});
|
});
|
||||||
// Can't test for the [ of an optional group since it's optional
|
// Can't test for the [ of an optional group since it's optional
|
||||||
it("complains about missing } for color", function() {
|
it("complains about missing } for color", function() {
|
||||||
expect("\\color{#ffffff {text}").toFailWithParseError(
|
expect("\\color{#ffffff{text}").toFailWithParseError(
|
||||||
"Expected '}', got '{' at position 16:" +
|
"Invalid color: '#ffffff{text' at position 8:" +
|
||||||
" color{#ffffff {̲text}");
|
" \\color{#̲f̲f̲f̲f̲f̲f̲{̲t̲e̲x̲t̲}");
|
||||||
});
|
});
|
||||||
it("complains about missing ] for size", function() {
|
it("complains about missing ] for size", function() {
|
||||||
expect("\\rule[1em{2em}{3em}").toFailWithParseError(
|
expect("\\rule[1em{2em}{3em}").toFailWithParseError(
|
||||||
"Expected ']', got '{' at position 10:" +
|
"Unexpected end of input in size" +
|
||||||
" \\rule[1em{̲2em}{3em}");
|
" at position 7: \\rule[1̲e̲m̲{̲2̲e̲m̲}̲{̲3̲e̲m̲}̲");
|
||||||
|
});
|
||||||
|
it("complains about missing ] for size at end of input", function() {
|
||||||
|
expect("\\rule[1em").toFailWithParseError(
|
||||||
|
"Unexpected end of input in size" +
|
||||||
|
" at position 7: \\rule[1̲e̲m̲");
|
||||||
|
});
|
||||||
|
it("complains about missing } for color at end of input", function() {
|
||||||
|
expect("\\color{#123456").toFailWithParseError(
|
||||||
|
"Unexpected end of input in color" +
|
||||||
|
" at position 8: \\color{#̲1̲2̲3̲4̲5̲6̲");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("#parseGroup expecting }", function() {
|
describe("#parseGroup expecting }", function() {
|
||||||
it("at end of file", function() {
|
it("at end of file", function() {
|
||||||
expect("\\sqrt{2").toFailWithParseError(
|
expect("\\sqrt{2").toFailWithParseError(
|
||||||
"Expected '}', got 'EOF' at position 7: \\sqrt{2̲");
|
"Expected '}', got 'EOF' at end of input: \\sqrt{2");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("#parseOptionalGroup expecting ]", function() {
|
describe("#parseOptionalGroup expecting ]", function() {
|
||||||
it("at end of file", function() {
|
it("at end of file", function() {
|
||||||
expect("\\sqrt[3").toFailWithParseError(
|
expect("\\sqrt[3").toFailWithParseError(
|
||||||
"Expected ']', got 'EOF' at position 7: \\sqrt[3̲");
|
"Expected ']', got 'EOF' at end of input: \\sqrt[3");
|
||||||
});
|
});
|
||||||
it("before group", function() {
|
it("before group", function() {
|
||||||
expect("\\sqrt[3{2}").toFailWithParseError(
|
expect("\\sqrt[3{2}").toFailWithParseError(
|
||||||
"Expected ']', got 'EOF' at position 10: \\sqrt[3{2}̲");
|
"Expected ']', got 'EOF' at end of input: \\sqrt[3{2}");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -260,13 +270,13 @@ describe("environments.js:", function() {
|
|||||||
describe("parseArray", function() {
|
describe("parseArray", function() {
|
||||||
it("rejects missing \\end", function() {
|
it("rejects missing \\end", function() {
|
||||||
expect("\\begin{matrix}1").toFailWithParseError(
|
expect("\\begin{matrix}1").toFailWithParseError(
|
||||||
"Expected & or \\\\ or \\end at position 15:" +
|
"Expected & or \\\\ or \\end at end of input:" +
|
||||||
" \\begin{matrix}1̲");
|
" \\begin{matrix}1");
|
||||||
});
|
});
|
||||||
it("rejects incorrectly scoped \\end", function() {
|
it("rejects incorrectly scoped \\end", function() {
|
||||||
expect("{\\begin{matrix}1}\\end{matrix}").toFailWithParseError(
|
expect("{\\begin{matrix}1}\\end{matrix}").toFailWithParseError(
|
||||||
"Expected & or \\\\\ or \\end at position 17:" +
|
"Expected & or \\\\\ or \\end at position 17:" +
|
||||||
" begin{matrix}1}̲\\end{matrix}");
|
" …\\begin{matrix}1}̲\\end{matrix}");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -274,8 +284,8 @@ describe("environments.js:", function() {
|
|||||||
it("rejects unknown column types", function() {
|
it("rejects unknown column types", function() {
|
||||||
// TODO: The error position here looks strange
|
// TODO: The error position here looks strange
|
||||||
expect("\\begin{array}{cba}\\end{array}").toFailWithParseError(
|
expect("\\begin{array}{cba}\\end{array}").toFailWithParseError(
|
||||||
"Unknown column alignment: b at position 18:" +
|
"Unknown column alignment: b at position 16:" +
|
||||||
" gin{array}{cba}̲\\end{array}");
|
" \\begin{array}{cb̲a}\\end{array}");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -298,9 +308,8 @@ describe("functions.js:", function() {
|
|||||||
|
|
||||||
describe("\\begin and \\end", function() {
|
describe("\\begin and \\end", function() {
|
||||||
it("reject invalid environment names", function() {
|
it("reject invalid environment names", function() {
|
||||||
expect("\\begin{foobar}\\end{foobar}").toFailWithParseError(
|
expect("\\begin x\\end y").toFailWithParseError(
|
||||||
"No such environment: foobar at position 14:" +
|
"Invalid environment name at position 8: \\begin x̲\\end y");
|
||||||
" \\begin{foobar}̲\\end{foobar}");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -311,34 +320,35 @@ describe("Lexer:", function() {
|
|||||||
describe("#_innerLex", function() {
|
describe("#_innerLex", function() {
|
||||||
it("rejects lone surrogate char", function() {
|
it("rejects lone surrogate char", function() {
|
||||||
expect("\udcba").toFailWithParseError(
|
expect("\udcba").toFailWithParseError(
|
||||||
"Unexpected character: '\udcba' at position 0:" +
|
"Unexpected character: '\udcba' at position 1:" +
|
||||||
" \u0332\udcba");
|
" \udcba\u0332");
|
||||||
});
|
});
|
||||||
it("rejects lone backslash at end of input", function() {
|
it("rejects lone backslash at end of input", function() {
|
||||||
expect("\\").toFailWithParseError(
|
expect("\\").toFailWithParseError(
|
||||||
"Unexpected character: '\\' at position 0: ̲\\");
|
"Unexpected character: '\\' at position 1: \\̲");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("#_innerLexColor", function() {
|
describe("#_innerLexColor", function() {
|
||||||
it("reject hex notation without #", function() {
|
it("reject hex notation without #", function() {
|
||||||
expect("\\color{1a2b3c}{foo}").toFailWithParseError(
|
expect("\\color{1a2b3c}{foo}").toFailWithParseError(
|
||||||
"Invalid color at position 7: \\color{̲1a2b3c}{foo}");
|
"Invalid color: '1a2b3c'" +
|
||||||
|
" at position 8: \\color{1̲a̲2̲b̲3̲c̲}{foo}");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("#_innerLexSize", function() {
|
describe("#_innerLexSize", function() {
|
||||||
it("reject size without unit", function() {
|
it("reject size without unit", function() {
|
||||||
expect("\\rule{0}{2em}").toFailWithParseError(
|
expect("\\rule{0}{2em}").toFailWithParseError(
|
||||||
"Invalid size at position 6: \\rule{̲0}{2em}");
|
"Invalid size: '0' at position 7: \\rule{0̲}{2em}");
|
||||||
});
|
});
|
||||||
it("reject size with bogus unit", function() {
|
it("reject size with bogus unit", function() {
|
||||||
expect("\\rule{1au}{2em}").toFailWithParseError(
|
expect("\\rule{1au}{2em}").toFailWithParseError(
|
||||||
"Invalid unit: 'au' at position 6: \\rule{̲1au}{2em}");
|
"Invalid unit: 'au' at position 7: \\rule{1̲a̲u̲}{2em}");
|
||||||
});
|
});
|
||||||
it("reject size without number", function() {
|
it("reject size without number", function() {
|
||||||
expect("\\rule{em}{2em}").toFailWithParseError(
|
expect("\\rule{em}{2em}").toFailWithParseError(
|
||||||
"Invalid size at position 6: \\rule{̲em}{2em}");
|
"Invalid size: 'em' at position 7: \\rule{e̲m̲}{2em}");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -57,8 +57,39 @@ var getParsed = function(expr, settings) {
|
|||||||
return parseTree(expr, usedSettings);
|
return parseTree(expr, usedSettings);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var stripPositions = function(expr) {
|
||||||
|
if (typeof expr !== "object" || expr === null) {
|
||||||
|
return expr;
|
||||||
|
}
|
||||||
|
if (expr.lexer && typeof expr.start === "number") {
|
||||||
|
delete expr.lexer;
|
||||||
|
delete expr.start;
|
||||||
|
delete expr.end;
|
||||||
|
}
|
||||||
|
Object.keys(expr).forEach(function(key) {
|
||||||
|
stripPositions(expr[key]);
|
||||||
|
});
|
||||||
|
return expr;
|
||||||
|
};
|
||||||
|
|
||||||
|
var parseAndSetResult = function(expr, result, settings) {
|
||||||
|
try {
|
||||||
|
return parseTree(expr, settings || defaultSettings);
|
||||||
|
} catch (e) {
|
||||||
|
result.pass = false;
|
||||||
|
if (e instanceof ParseError) {
|
||||||
|
result.message = "'" + expr + "' failed " +
|
||||||
|
"parsing with error: " + e.message;
|
||||||
|
} else {
|
||||||
|
result.message = "'" + expr + "' failed " +
|
||||||
|
"parsing with unknown error: " + e.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
jasmine.addMatchers({
|
jasmine.addMatchers({
|
||||||
|
|
||||||
toParse: function() {
|
toParse: function() {
|
||||||
return {
|
return {
|
||||||
compare: function(actual, settings) {
|
compare: function(actual, settings) {
|
||||||
@@ -68,20 +99,7 @@ beforeEach(function() {
|
|||||||
pass: true,
|
pass: true,
|
||||||
message: "'" + actual + "' succeeded parsing",
|
message: "'" + actual + "' succeeded parsing",
|
||||||
};
|
};
|
||||||
|
parseAndSetResult(actual, result, usedSettings);
|
||||||
try {
|
|
||||||
parseTree(actual, usedSettings);
|
|
||||||
} 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;
|
return result;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -145,6 +163,36 @@ beforeEach(function() {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
toParseLike: function(util, baton) {
|
||||||
|
return {
|
||||||
|
compare: function(actual, expected) {
|
||||||
|
var result = {
|
||||||
|
pass: true,
|
||||||
|
message: "Parse trees of '" + actual +
|
||||||
|
"' and '" + expected + "' are equivalent",
|
||||||
|
};
|
||||||
|
|
||||||
|
var actualTree = parseAndSetResult(actual, result);
|
||||||
|
if (!actualTree) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
var expectedTree = parseAndSetResult(expected, result);
|
||||||
|
if (!expectedTree) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
stripPositions(actualTree);
|
||||||
|
stripPositions(expectedTree);
|
||||||
|
if (!util.equals(actualTree, expectedTree, baton)) {
|
||||||
|
result.pass = false;
|
||||||
|
result.message = "Parse trees of '" + actual +
|
||||||
|
"' and '" + expected + "' are not equivalent";
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -154,8 +202,8 @@ describe("A parser", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should ignore whitespace", function() {
|
it("should ignore whitespace", function() {
|
||||||
var parseA = getParsed(" x y ");
|
var parseA = stripPositions(getParsed(" x y "));
|
||||||
var parseB = getParsed("xy");
|
var parseB = stripPositions(getParsed("xy"));
|
||||||
expect(parseA).toEqual(parseB);
|
expect(parseA).toEqual(parseB);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -340,8 +388,8 @@ describe("A subscript and superscript parser", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should produce the same thing regardless of order", function() {
|
it("should produce the same thing regardless of order", function() {
|
||||||
var parseA = getParsed("x^2_3");
|
var parseA = stripPositions(getParsed("x^2_3"));
|
||||||
var parseB = getParsed("x_3^2");
|
var parseB = stripPositions(getParsed("x_3^2"));
|
||||||
|
|
||||||
expect(parseA).toEqual(parseB);
|
expect(parseA).toEqual(parseB);
|
||||||
});
|
});
|
||||||
@@ -623,6 +671,13 @@ describe("An over parser", function() {
|
|||||||
expect(parse.value.denom).toBeDefined();
|
expect(parse.value.denom).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should handle \\textstyle correctly", function() {
|
||||||
|
expect("\\textstyle 1 \\over 2")
|
||||||
|
.toParseLike("\\frac{\\textstyle 1}{2}");
|
||||||
|
expect("{\\textstyle 1} \\over 2")
|
||||||
|
.toParseLike("\\frac{\\textstyle 1}{2}");
|
||||||
|
});
|
||||||
|
|
||||||
it("should handle nested factions", function() {
|
it("should handle nested factions", function() {
|
||||||
var nestedOverExpression = "{1 \\over 2} \\over 3";
|
var nestedOverExpression = "{1 \\over 2} \\over 3";
|
||||||
var parse = getParsed(nestedOverExpression)[0];
|
var parse = getParsed(nestedOverExpression)[0];
|
||||||
@@ -1523,7 +1578,7 @@ describe("A markup generator", function() {
|
|||||||
|
|
||||||
describe("A parse tree generator", function() {
|
describe("A parse tree generator", function() {
|
||||||
it("generates a tree", function() {
|
it("generates a tree", function() {
|
||||||
var tree = katex.__parse("\\sigma^2");
|
var tree = stripPositions(katex.__parse("\\sigma^2"));
|
||||||
expect(JSON.stringify(tree)).toEqual(JSON.stringify([
|
expect(JSON.stringify(tree)).toEqual(JSON.stringify([
|
||||||
{
|
{
|
||||||
"type": "supsub",
|
"type": "supsub",
|
||||||
@@ -1802,3 +1857,24 @@ describe("The symbol table integraty", function() {
|
|||||||
.toEqual(getBuilt("\\left\\lt\\frac{1}{x}\\right\\gt"));
|
.toEqual(getBuilt("\\left\\lt\\frac{1}{x}\\right\\gt"));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("A macro expander", function() {
|
||||||
|
|
||||||
|
var compareParseTree = function(actual, expected, macros) {
|
||||||
|
var settings = new Settings({macros: macros});
|
||||||
|
actual = stripPositions(parseTree(actual, settings));
|
||||||
|
expected = stripPositions(parseTree(expected, defaultSettings));
|
||||||
|
expect(actual).toEqual(expected);
|
||||||
|
};
|
||||||
|
|
||||||
|
it("should produce individual tokens", function() {
|
||||||
|
compareParseTree("e^\\foo", "e^1 23", {"\\foo": "123"});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should allow for multiple expansion", function() {
|
||||||
|
compareParseTree("1\\foo2", "1aa2", {
|
||||||
|
"\\foo": "\\bar\\bar",
|
||||||
|
"\\bar": "a",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
Reference in New Issue
Block a user