mirror of
https://github.com/Smaug123/KaTeX
synced 2025-10-11 05:58:40 +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
@@ -61,16 +61,15 @@ beforeEach(function() {
|
||||
describe("Parser:", function() {
|
||||
|
||||
describe("#handleInfixNodes", function() {
|
||||
// TODO: The position information here is broken, should be fixed.
|
||||
it("rejects repeated infix operators", function() {
|
||||
expect("1\\over 2\\over 3").toFailWithParseError(
|
||||
"only one infix operator per group at position -1: " +
|
||||
"1\\over 2\\over ");
|
||||
"only one infix operator per group at position 9: " +
|
||||
"1\\over 2\\̲o̲v̲e̲r̲ 3");
|
||||
});
|
||||
it("rejects conflicting infix operators", function() {
|
||||
expect("1\\over 2\\choose 3").toFailWithParseError(
|
||||
"only one infix operator per group at position -1: " +
|
||||
"1\\over 2\\choos");
|
||||
"only one infix operator per group at position 9: " +
|
||||
"1\\over 2\\̲c̲h̲o̲o̲s̲e̲ 3");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -91,84 +90,85 @@ describe("Parser:", 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() {
|
||||
expect("\\alpha\\limits\\omega").toFailWithParseError(
|
||||
"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() {
|
||||
expect("\\limits\\omega").toFailWithParseError(
|
||||
"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() {
|
||||
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(
|
||||
"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() {
|
||||
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(
|
||||
"Double subscript at position 9: 1_{2+3}^4̲_5");
|
||||
"Double subscript at position 10: 1_{2+3}^4_̲5");
|
||||
});
|
||||
});
|
||||
|
||||
describe("#parseImplicitGroup", function() {
|
||||
it("reports unknown environments", function() {
|
||||
expect("\\begin{foo}bar\\end{foo}").toFailWithParseError(
|
||||
"No such environment: foo at position 11:" +
|
||||
" \\begin{foo}̲bar\\end{foo}");
|
||||
"No such environment: foo at position 7:" +
|
||||
" \\begin{̲f̲o̲o̲}̲bar\\end{foo}");
|
||||
});
|
||||
it("reports mismatched environments", function() {
|
||||
expect("\\begin{pmatrix}1&2\\\\3&4\\end{bmatrix}+5")
|
||||
.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() {
|
||||
it("rejects math-mode functions in text mode", function() {
|
||||
// TODO: The position info is missing here
|
||||
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() {
|
||||
it("complains about missing argument at end of input", function() {
|
||||
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() {
|
||||
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() {
|
||||
// TODO: The position looks pretty wrong here
|
||||
expect("\\sqrt\\over2").toFailWithParseError(
|
||||
"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() {
|
||||
it("complains about missing argument at end of input", function() {
|
||||
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() {
|
||||
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() {
|
||||
// TODO: The position looks pretty wrong here
|
||||
expect("\\sqrt\\over2").toFailWithParseError(
|
||||
"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() {
|
||||
expect("x\\end{matrix}").toFailWithParseError(
|
||||
"Expected 'EOF', got '\\end' at position 5:" +
|
||||
" x\\end̲{matrix}");
|
||||
"Expected 'EOF', got '\\end' at position 2:" +
|
||||
" x\\̲e̲n̲d̲{matrix}");
|
||||
});
|
||||
it("complains about top-level \\\\", function() {
|
||||
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() {
|
||||
expect("1&2").toFailWithParseError(
|
||||
@@ -199,8 +199,8 @@ describe("Parser.expect calls:", function() {
|
||||
describe("#parseImplicitGroup expecting \\right", function() {
|
||||
it("rejects missing \\right", function() {
|
||||
expect("\\left(1+2)").toFailWithParseError(
|
||||
"Expected '\\right', got 'EOF' at position 10:" +
|
||||
" \\left(1+2)̲");
|
||||
"Expected '\\right', got 'EOF' at end of input:" +
|
||||
" \\left(1+2)");
|
||||
});
|
||||
it("rejects incorrectly scoped \\right", function() {
|
||||
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
|
||||
it("complains about missing } for color", function() {
|
||||
expect("\\color{#ffffff {text}").toFailWithParseError(
|
||||
"Expected '}', got '{' at position 16:" +
|
||||
" color{#ffffff {̲text}");
|
||||
expect("\\color{#ffffff{text}").toFailWithParseError(
|
||||
"Invalid color: '#ffffff{text' at position 8:" +
|
||||
" \\color{#̲f̲f̲f̲f̲f̲f̲{̲t̲e̲x̲t̲}");
|
||||
});
|
||||
it("complains about missing ] for size", function() {
|
||||
expect("\\rule[1em{2em}{3em}").toFailWithParseError(
|
||||
"Expected ']', got '{' at position 10:" +
|
||||
" \\rule[1em{̲2em}{3em}");
|
||||
"Unexpected end of input in size" +
|
||||
" 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() {
|
||||
it("at end of file", function() {
|
||||
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() {
|
||||
it("at end of file", function() {
|
||||
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() {
|
||||
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() {
|
||||
it("rejects missing \\end", function() {
|
||||
expect("\\begin{matrix}1").toFailWithParseError(
|
||||
"Expected & or \\\\ or \\end at position 15:" +
|
||||
" \\begin{matrix}1̲");
|
||||
"Expected & or \\\\ or \\end at end of input:" +
|
||||
" \\begin{matrix}1");
|
||||
});
|
||||
it("rejects incorrectly scoped \\end", function() {
|
||||
expect("{\\begin{matrix}1}\\end{matrix}").toFailWithParseError(
|
||||
"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() {
|
||||
// TODO: The error position here looks strange
|
||||
expect("\\begin{array}{cba}\\end{array}").toFailWithParseError(
|
||||
"Unknown column alignment: b at position 18:" +
|
||||
" gin{array}{cba}̲\\end{array}");
|
||||
"Unknown column alignment: b at position 16:" +
|
||||
" \\begin{array}{cb̲a}\\end{array}");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -298,9 +308,8 @@ describe("functions.js:", function() {
|
||||
|
||||
describe("\\begin and \\end", function() {
|
||||
it("reject invalid environment names", function() {
|
||||
expect("\\begin{foobar}\\end{foobar}").toFailWithParseError(
|
||||
"No such environment: foobar at position 14:" +
|
||||
" \\begin{foobar}̲\\end{foobar}");
|
||||
expect("\\begin x\\end y").toFailWithParseError(
|
||||
"Invalid environment name at position 8: \\begin x̲\\end y");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -311,34 +320,35 @@ describe("Lexer:", function() {
|
||||
describe("#_innerLex", function() {
|
||||
it("rejects lone surrogate char", function() {
|
||||
expect("\udcba").toFailWithParseError(
|
||||
"Unexpected character: '\udcba' at position 0:" +
|
||||
" \u0332\udcba");
|
||||
"Unexpected character: '\udcba' at position 1:" +
|
||||
" \udcba\u0332");
|
||||
});
|
||||
it("rejects lone backslash at end of input", function() {
|
||||
expect("\\").toFailWithParseError(
|
||||
"Unexpected character: '\\' at position 0: ̲\\");
|
||||
"Unexpected character: '\\' at position 1: \\̲");
|
||||
});
|
||||
});
|
||||
|
||||
describe("#_innerLexColor", function() {
|
||||
it("reject hex notation without #", function() {
|
||||
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() {
|
||||
it("reject size without unit", function() {
|
||||
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() {
|
||||
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() {
|
||||
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);
|
||||
};
|
||||
|
||||
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() {
|
||||
jasmine.addMatchers({
|
||||
|
||||
toParse: function() {
|
||||
return {
|
||||
compare: function(actual, settings) {
|
||||
@@ -68,20 +99,7 @@ beforeEach(function() {
|
||||
pass: true,
|
||||
message: "'" + actual + "' succeeded parsing",
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
parseAndSetResult(actual, result, usedSettings);
|
||||
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() {
|
||||
var parseA = getParsed(" x y ");
|
||||
var parseB = getParsed("xy");
|
||||
var parseA = stripPositions(getParsed(" x y "));
|
||||
var parseB = stripPositions(getParsed("xy"));
|
||||
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() {
|
||||
var parseA = getParsed("x^2_3");
|
||||
var parseB = getParsed("x_3^2");
|
||||
var parseA = stripPositions(getParsed("x^2_3"));
|
||||
var parseB = stripPositions(getParsed("x_3^2"));
|
||||
|
||||
expect(parseA).toEqual(parseB);
|
||||
});
|
||||
@@ -623,6 +671,13 @@ describe("An over parser", function() {
|
||||
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() {
|
||||
var nestedOverExpression = "{1 \\over 2} \\over 3";
|
||||
var parse = getParsed(nestedOverExpression)[0];
|
||||
@@ -1523,7 +1578,7 @@ describe("A markup generator", function() {
|
||||
|
||||
describe("A parse tree generator", 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([
|
||||
{
|
||||
"type": "supsub",
|
||||
@@ -1802,3 +1857,24 @@ describe("The symbol table integraty", function() {
|
||||
.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