
This commit introduces environments, and implements the parser infrastructure to handle them, even including arguments after the “\begin{name}” construct. It also offers a way to turn array-like data structures, i.e. delimited by “&” and “\\”, into nested arrays of groups. Environments are essentially functions which call back to the parser to parse their body. It is their responsibility to stop at the next “\end”, while the parser takes care of verifing that the names match between “\begin” and “\end”. The environment has to return a ParseResult, to provide the position that goes with the resulting node. One application of this is the “array” environment. So far, it supports column alignment, but no column separators, and no multi-column shorthands using “*{…}”. Building on the same infrastructure, there are “matrix”, “pmatrix”, “bmatrix”, “vmatrix” and “Vmatrix” environments. Internally these are just “\left..\right” wrapped around an array with no margins at its ends. Spacing for arrays and matrices was derived from the LaTeX sources, and comments indicate the appropriate references. Now we have hard-wired breaks in parseExpression, to always break on “}”, “\end”, “\right”, “&”, “\\” and “\cr”. This means that these symbols are never PART of an expression, at least not without some nesting. They may follow AFTER an expression, and the caller of parseExpression should be expecting them. The implicit groups for sizing or styling don't care what ended the expression, which is all right for them. We still have support for breakOnToken, but now it is only used for “]” since that MAY be used to terminate an optional argument, but otherwise it's an ordinary symbol.
1359 lines
40 KiB
JavaScript
1359 lines
40 KiB
JavaScript
/* global beforeEach: false */
|
|
/* global jasmine: false */
|
|
/* global expect: false */
|
|
/* global it: false */
|
|
/* global describe: false */
|
|
|
|
var buildHTML = require("../src/buildHTML");
|
|
var buildMathML = require("../src/buildMathML");
|
|
var katex = require("../katex");
|
|
var ParseError = require("../src/ParseError");
|
|
var parseTree = require("../src/parseTree");
|
|
var Settings = require("../src/Settings");
|
|
|
|
var defaultSettings = new Settings({});
|
|
|
|
var getBuilt = function(expr) {
|
|
expect(expr).toBuild();
|
|
|
|
var built = buildHTML(parseTree(expr), defaultSettings);
|
|
|
|
// Remove the outer .katex and .katex-inner layers
|
|
return built.children[2].children;
|
|
};
|
|
|
|
var getParsed = function(expr) {
|
|
expect(expr).toParse();
|
|
|
|
return parseTree(expr, defaultSettings);
|
|
};
|
|
|
|
beforeEach(function() {
|
|
jasmine.addMatchers({
|
|
toParse: function() {
|
|
return {
|
|
compare: function(actual) {
|
|
var result = {
|
|
pass: true,
|
|
message: "'" + actual + "' succeeded parsing"
|
|
};
|
|
|
|
try {
|
|
parseTree(actual, defaultSettings);
|
|
} catch (e) {
|
|
result.pass = false;
|
|
if (e instanceof ParseError) {
|
|
result.message = "'" + actual + "' failed " +
|
|
"parsing with error: " + e.message;
|
|
} else {
|
|
result.message = "'" + actual + "' failed " +
|
|
"parsing with unknown error: " + e.message;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
};
|
|
},
|
|
|
|
toNotParse: function() {
|
|
return {
|
|
compare: function(actual) {
|
|
var result = {
|
|
pass: false,
|
|
message: "Expected '" + actual + "' to fail " +
|
|
"parsing, but it succeeded"
|
|
};
|
|
|
|
try {
|
|
parseTree(actual, defaultSettings);
|
|
} catch (e) {
|
|
if (e instanceof ParseError) {
|
|
result.pass = true;
|
|
result.message = "'" + actual + "' correctly " +
|
|
"didn't parse with error: " + e.message;
|
|
} else {
|
|
result.message = "'" + actual + "' failed " +
|
|
"parsing with unknown error: " + e.message;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
};
|
|
},
|
|
|
|
toBuild: function() {
|
|
return {
|
|
compare: function(actual) {
|
|
var result = {
|
|
pass: true,
|
|
message: "'" + actual + "' succeeded in building"
|
|
};
|
|
|
|
expect(actual).toParse();
|
|
|
|
try {
|
|
buildHTML(parseTree(actual), defaultSettings);
|
|
} catch (e) {
|
|
result.pass = false;
|
|
if (e instanceof ParseError) {
|
|
result.message = "'" + actual + "' failed to " +
|
|
"build with error: " + e.message;
|
|
} else {
|
|
result.message = "'" + actual + "' failed " +
|
|
"building with unknown error: " + e.message;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
};
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("A parser", function() {
|
|
it("should not fail on an empty string", function() {
|
|
expect("").toParse();
|
|
});
|
|
|
|
it("should ignore whitespace", function() {
|
|
var parseA = getParsed(" x y ");
|
|
var parseB = getParsed("xy");
|
|
expect(parseA).toEqual(parseB);
|
|
});
|
|
});
|
|
|
|
describe("An ord parser", function() {
|
|
var expression = "1234|/@.\"`abcdefgzABCDEFGZ";
|
|
|
|
it("should not fail", function() {
|
|
expect(expression).toParse();
|
|
});
|
|
|
|
it("should build a list of ords", function() {
|
|
var parse = getParsed(expression);
|
|
|
|
expect(parse).toBeTruthy();
|
|
|
|
for (var i = 0; i < parse.length; i++) {
|
|
var group = parse[i];
|
|
expect(group.type).toMatch("ord");
|
|
}
|
|
});
|
|
|
|
it("should parse the right number of ords", function() {
|
|
var parse = getParsed(expression);
|
|
|
|
expect(parse.length).toBe(expression.length);
|
|
});
|
|
});
|
|
|
|
describe("A bin parser", function() {
|
|
var expression = "+-*\\cdot\\pm\\div";
|
|
|
|
it("should not fail", function() {
|
|
expect(expression).toParse();
|
|
});
|
|
|
|
it("should build a list of bins", function() {
|
|
var parse = getParsed(expression);
|
|
expect(parse).toBeTruthy();
|
|
|
|
for (var i = 0; i < parse.length; i++) {
|
|
var group = parse[i];
|
|
expect(group.type).toMatch("bin");
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("A rel parser", function() {
|
|
var expression = "=<>\\leq\\geq\\neq\\nleq\\ngeq\\cong";
|
|
|
|
it("should not fail", function() {
|
|
expect(expression).toParse();
|
|
});
|
|
|
|
it("should build a list of rels", function() {
|
|
var parse = getParsed(expression);
|
|
expect(parse).toBeTruthy();
|
|
|
|
for (var i = 0; i < parse.length; i++) {
|
|
var group = parse[i];
|
|
expect(group.type).toMatch("rel");
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("A punct parser", function() {
|
|
var expression = ",;\\colon";
|
|
|
|
it("should not fail", function() {
|
|
expect(expression).toParse();
|
|
});
|
|
|
|
it("should build a list of puncts", function() {
|
|
var parse = getParsed(expression);
|
|
expect(parse).toBeTruthy();
|
|
|
|
for (var i = 0; i < parse.length; i++) {
|
|
var group = parse[i];
|
|
expect(group.type).toMatch("punct");
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("An open parser", function() {
|
|
var expression = "([";
|
|
|
|
it("should not fail", function() {
|
|
expect(expression).toParse();
|
|
});
|
|
|
|
it("should build a list of opens", function() {
|
|
var parse = getParsed(expression);
|
|
expect(parse).toBeTruthy();
|
|
|
|
for (var i = 0; i < parse.length; i++) {
|
|
var group = parse[i];
|
|
expect(group.type).toMatch("open");
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("A close parser", function() {
|
|
var expression = ")]?!";
|
|
|
|
it("should not fail", function() {
|
|
expect(expression).toParse();
|
|
});
|
|
|
|
it("should build a list of closes", function() {
|
|
var parse = getParsed(expression);
|
|
expect(parse).toBeTruthy();
|
|
|
|
for (var i = 0; i < parse.length; i++) {
|
|
var group = parse[i];
|
|
expect(group.type).toMatch("close");
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("A \\KaTeX parser", function() {
|
|
it("should not fail", function() {
|
|
expect("\\KaTeX").toParse();
|
|
});
|
|
});
|
|
|
|
describe("A subscript and superscript parser", function() {
|
|
it("should not fail on superscripts", function() {
|
|
expect("x^2").toParse();
|
|
});
|
|
|
|
it("should not fail on subscripts", function() {
|
|
expect("x_3").toParse();
|
|
});
|
|
|
|
it("should not fail on both subscripts and superscripts", function() {
|
|
expect("x^2_3").toParse();
|
|
|
|
expect("x_2^3").toParse();
|
|
});
|
|
|
|
it("should not fail when there is no nucleus", function() {
|
|
expect("^3").toParse();
|
|
expect("_2").toParse();
|
|
expect("^3_2").toParse();
|
|
expect("_2^3").toParse();
|
|
});
|
|
|
|
it("should produce supsubs for superscript", function() {
|
|
var parse = getParsed("x^2")[0];
|
|
|
|
expect(parse.type).toBe("supsub");
|
|
expect(parse.value.base).toBeDefined();
|
|
expect(parse.value.sup).toBeDefined();
|
|
expect(parse.value.sub).toBeUndefined();
|
|
});
|
|
|
|
it("should produce supsubs for subscript", function() {
|
|
var parse = getParsed("x_3")[0];
|
|
|
|
expect(parse.type).toBe("supsub");
|
|
expect(parse.value.base).toBeDefined();
|
|
expect(parse.value.sub).toBeDefined();
|
|
expect(parse.value.sup).toBeUndefined();
|
|
});
|
|
|
|
it("should produce supsubs for ^_", function() {
|
|
var parse = getParsed("x^2_3")[0];
|
|
|
|
expect(parse.type).toBe("supsub");
|
|
expect(parse.value.base).toBeDefined();
|
|
expect(parse.value.sup).toBeDefined();
|
|
expect(parse.value.sub).toBeDefined();
|
|
});
|
|
|
|
it("should produce supsubs for _^", function() {
|
|
var parse = getParsed("x_3^2")[0];
|
|
|
|
expect(parse.type).toBe("supsub");
|
|
expect(parse.value.base).toBeDefined();
|
|
expect(parse.value.sup).toBeDefined();
|
|
expect(parse.value.sub).toBeDefined();
|
|
});
|
|
|
|
it("should produce the same thing regardless of order", function() {
|
|
var parseA = getParsed("x^2_3");
|
|
var parseB = getParsed("x_3^2");
|
|
|
|
expect(parseA).toEqual(parseB);
|
|
});
|
|
|
|
it("should not parse double subscripts or superscripts", function() {
|
|
expect("x^x^x").toNotParse();
|
|
|
|
expect("x_x_x").toNotParse();
|
|
|
|
expect("x_x^x_x").toNotParse();
|
|
|
|
expect("x_x^x^x").toNotParse();
|
|
|
|
expect("x^x_x_x").toNotParse();
|
|
|
|
expect("x^x_x^x").toNotParse();
|
|
});
|
|
|
|
it("should work correctly with {}s", function() {
|
|
expect("x^{2+3}").toParse();
|
|
|
|
expect("x_{3-2}").toParse();
|
|
|
|
expect("x^{2+3}_3").toParse();
|
|
|
|
expect("x^2_{3-2}").toParse();
|
|
|
|
expect("x^{2+3}_{3-2}").toParse();
|
|
|
|
expect("x_{3-2}^{2+3}").toParse();
|
|
|
|
expect("x_3^{2+3}").toParse();
|
|
|
|
expect("x_{3-2}^2").toParse();
|
|
});
|
|
|
|
it("should work with nested super/subscripts", function() {
|
|
expect("x^{x^x}").toParse();
|
|
expect("x^{x_x}").toParse();
|
|
expect("x_{x^x}").toParse();
|
|
expect("x_{x_x}").toParse();
|
|
});
|
|
});
|
|
|
|
describe("A subscript and superscript tree-builder", function() {
|
|
it("should not fail when there is no nucleus", function() {
|
|
expect("^3").toBuild();
|
|
expect("_2").toBuild();
|
|
expect("^3_2").toBuild();
|
|
expect("_2^3").toBuild();
|
|
});
|
|
});
|
|
|
|
describe("A group parser", function() {
|
|
it("should not fail", function() {
|
|
expect("{xy}").toParse();
|
|
});
|
|
|
|
it("should produce a single ord", function() {
|
|
var parse = getParsed("{xy}");
|
|
|
|
expect(parse.length).toBe(1);
|
|
|
|
var ord = parse[0];
|
|
|
|
expect(ord.type).toMatch("ord");
|
|
expect(ord.value).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
describe("An implicit group parser", function() {
|
|
it("should not fail", function() {
|
|
expect("\\Large x").toParse();
|
|
expect("abc {abc \\Large xyz} abc").toParse();
|
|
});
|
|
|
|
it("should produce a single object", function() {
|
|
var parse = getParsed("\\Large abc");
|
|
|
|
expect(parse.length).toBe(1);
|
|
|
|
var sizing = parse[0];
|
|
|
|
expect(sizing.type).toMatch("sizing");
|
|
expect(sizing.value).toBeTruthy();
|
|
});
|
|
|
|
it("should apply only after the function", function() {
|
|
var parse = getParsed("a \\Large abc");
|
|
|
|
expect(parse.length).toBe(2);
|
|
|
|
var sizing = parse[1];
|
|
|
|
expect(sizing.type).toMatch("sizing");
|
|
expect(sizing.value.value.length).toBe(3);
|
|
});
|
|
|
|
it("should stop at the ends of groups", function() {
|
|
var parse = getParsed("a { b \\Large c } d");
|
|
|
|
var group = parse[1];
|
|
var sizing = group.value[1];
|
|
|
|
expect(sizing.type).toMatch("sizing");
|
|
expect(sizing.value.value.length).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe("A function parser", function() {
|
|
it("should parse no argument functions", function() {
|
|
expect("\\div").toParse();
|
|
});
|
|
|
|
it("should parse 1 argument functions", function() {
|
|
expect("\\blue x").toParse();
|
|
});
|
|
|
|
it("should parse 2 argument functions", function() {
|
|
expect("\\frac 1 2").toParse();
|
|
});
|
|
|
|
it("should not parse 1 argument functions with no arguments", function() {
|
|
expect("\\blue").toNotParse();
|
|
});
|
|
|
|
it("should not parse 2 argument functions with 0 or 1 arguments", function() {
|
|
expect("\\frac").toNotParse();
|
|
|
|
expect("\\frac 1").toNotParse();
|
|
});
|
|
|
|
it("should not parse a function with text right after it", function() {
|
|
expect("\\redx").toNotParse();
|
|
});
|
|
|
|
it("should parse a function with a number right after it", function() {
|
|
expect("\\frac12").toParse();
|
|
});
|
|
|
|
it("should parse some functions with text right after it", function() {
|
|
expect("\\;x").toParse();
|
|
});
|
|
});
|
|
|
|
describe("A frac parser", function() {
|
|
var expression = "\\frac{x}{y}";
|
|
var dfracExpression = "\\dfrac{x}{y}";
|
|
var tfracExpression = "\\tfrac{x}{y}";
|
|
|
|
it("should not fail", function() {
|
|
expect(expression).toParse();
|
|
});
|
|
|
|
it("should produce a frac", function() {
|
|
var parse = getParsed(expression)[0];
|
|
|
|
expect(parse.type).toMatch("frac");
|
|
expect(parse.value.numer).toBeDefined();
|
|
expect(parse.value.denom).toBeDefined();
|
|
});
|
|
|
|
it("should also parse dfrac and tfrac", function() {
|
|
expect(dfracExpression).toParse();
|
|
|
|
expect(tfracExpression).toParse();
|
|
});
|
|
|
|
it("should parse dfrac and tfrac as fracs", function() {
|
|
var dfracParse = getParsed(dfracExpression)[0];
|
|
|
|
expect(dfracParse.type).toMatch("frac");
|
|
expect(dfracParse.value.numer).toBeDefined();
|
|
expect(dfracParse.value.denom).toBeDefined();
|
|
|
|
var tfracParse = getParsed(tfracExpression)[0];
|
|
|
|
expect(tfracParse.type).toMatch("frac");
|
|
expect(tfracParse.value.numer).toBeDefined();
|
|
expect(tfracParse.value.denom).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe("An over parser", function() {
|
|
var simpleOver = "1 \\over x";
|
|
var complexOver = "1+2i \\over 3+4i";
|
|
|
|
it("should not fail", function () {
|
|
expect(simpleOver).toParse();
|
|
expect(complexOver).toParse();
|
|
});
|
|
|
|
it("should produce a frac", function() {
|
|
var parse;
|
|
|
|
parse = getParsed(simpleOver)[0];
|
|
|
|
expect(parse.type).toMatch("frac");
|
|
expect(parse.value.numer).toBeDefined();
|
|
expect(parse.value.denom).toBeDefined();
|
|
|
|
parse = getParsed(complexOver)[0];
|
|
|
|
expect(parse.type).toMatch("frac");
|
|
expect(parse.value.numer).toBeDefined();
|
|
expect(parse.value.denom).toBeDefined();
|
|
});
|
|
|
|
it("should create a numerator from the atoms before \\over", function () {
|
|
var parse = getParsed(complexOver)[0];
|
|
|
|
var numer = parse.value.numer;
|
|
expect(numer.value.length).toEqual(4);
|
|
});
|
|
|
|
it("should create a demonimator from the atoms after \\over", function () {
|
|
var parse = getParsed(complexOver)[0];
|
|
|
|
var denom = parse.value.numer;
|
|
expect(denom.value.length).toEqual(4);
|
|
});
|
|
|
|
it("should handle empty numerators", function () {
|
|
var emptyNumerator = "\\over x";
|
|
var parse = getParsed(emptyNumerator)[0];
|
|
expect(parse.type).toMatch("frac");
|
|
expect(parse.value.numer).toBeDefined();
|
|
expect(parse.value.denom).toBeDefined();
|
|
});
|
|
|
|
it("should handle empty denominators", function () {
|
|
var emptyDenominator = "1 \\over";
|
|
var parse = getParsed(emptyDenominator)[0];
|
|
expect(parse.type).toMatch("frac");
|
|
expect(parse.value.numer).toBeDefined();
|
|
expect(parse.value.denom).toBeDefined();
|
|
});
|
|
|
|
it("should handle \\displaystyle correctly", function () {
|
|
var displaystyleExpression = "\\displaystyle 1 \\over 2";
|
|
var parse = getParsed(displaystyleExpression)[0];
|
|
expect(parse.type).toMatch("frac");
|
|
expect(parse.value.numer.value[0].type).toMatch("styling");
|
|
expect(parse.value.denom).toBeDefined();
|
|
});
|
|
|
|
it("should handle nested factions", function () {
|
|
var nestedOverExpression = "{1 \\over 2} \\over 3";
|
|
var parse = getParsed(nestedOverExpression)[0];
|
|
expect(parse.type).toMatch("frac");
|
|
expect(parse.value.numer.value[0].type).toMatch("frac");
|
|
expect(parse.value.numer.value[0].value.numer.value[0].value).toMatch(1);
|
|
expect(parse.value.numer.value[0].value.denom.value[0].value).toMatch(2);
|
|
expect(parse.value.denom).toBeDefined();
|
|
expect(parse.value.denom.value[0].value).toMatch(3);
|
|
});
|
|
|
|
it("should fail with multiple overs in the same group", function () {
|
|
var badMultipleOvers = "1 \\over 2 + 3 \\over 4";
|
|
expect(badMultipleOvers).toNotParse();
|
|
|
|
var badOverChoose = "1 \\over 2 \\choose 3";
|
|
expect(badOverChoose).toNotParse();
|
|
});
|
|
});
|
|
|
|
describe("A sizing parser", function() {
|
|
var sizeExpression = "\\Huge{x}\\small{x}";
|
|
|
|
it("should not fail", function() {
|
|
expect(sizeExpression).toParse();
|
|
});
|
|
|
|
it("should produce a sizing node", function() {
|
|
var parse = getParsed(sizeExpression)[0];
|
|
|
|
expect(parse.type).toMatch("sizing");
|
|
expect(parse.value).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe("A text parser", function() {
|
|
var textExpression = "\\text{a b}";
|
|
var noBraceTextExpression = "\\text x";
|
|
var nestedTextExpression =
|
|
"\\text{a {b} \\blue{c} \\color{#fff}{x} \\llap{x}}";
|
|
var spaceTextExpression = "\\text{ a \\ }";
|
|
var leadingSpaceTextExpression = "\\text {moo}";
|
|
var badTextExpression = "\\text{a b%}";
|
|
var badFunctionExpression = "\\text{\\sqrt{x}}";
|
|
|
|
it("should not fail", function() {
|
|
expect(textExpression).toParse();
|
|
});
|
|
|
|
it("should produce a text", function() {
|
|
var parse = getParsed(textExpression)[0];
|
|
|
|
expect(parse.type).toMatch("text");
|
|
expect(parse.value).toBeDefined();
|
|
});
|
|
|
|
it("should produce textords instead of mathords", function() {
|
|
var parse = getParsed(textExpression)[0];
|
|
var group = parse.value.body;
|
|
|
|
expect(group[0].type).toMatch("textord");
|
|
});
|
|
|
|
it("should not parse bad text", function() {
|
|
expect(badTextExpression).toNotParse();
|
|
});
|
|
|
|
it("should not parse bad functions inside text", function() {
|
|
expect(badFunctionExpression).toNotParse();
|
|
});
|
|
|
|
it("should parse text with no braces around it", function() {
|
|
expect(noBraceTextExpression).toParse();
|
|
});
|
|
|
|
it("should parse nested expressions", function() {
|
|
expect(nestedTextExpression).toParse();
|
|
});
|
|
|
|
it("should contract spaces", function() {
|
|
var parse = getParsed(spaceTextExpression)[0];
|
|
var group = parse.value.body;
|
|
|
|
expect(group[0].type).toMatch("spacing");
|
|
expect(group[1].type).toMatch("textord");
|
|
expect(group[2].type).toMatch("spacing");
|
|
expect(group[3].type).toMatch("spacing");
|
|
});
|
|
|
|
it("should ignore a space before the text group", function() {
|
|
var parse = getParsed(leadingSpaceTextExpression)[0];
|
|
// [m, o, o]
|
|
expect(parse.value.body.length).toBe(3);
|
|
expect(
|
|
parse.value.body.map(function(n) { return n.value; }).join("")
|
|
).toBe("moo");
|
|
});
|
|
});
|
|
|
|
describe("A color parser", function() {
|
|
var colorExpression = "\\blue{x}";
|
|
var newColorExpression = "\\redA{x}";
|
|
var customColorExpression = "\\color{#fA6}{x}";
|
|
var badCustomColorExpression = "\\color{bad-color}{x}";
|
|
|
|
it("should not fail", function() {
|
|
expect(colorExpression).toParse();
|
|
});
|
|
|
|
it("should build a color node", function() {
|
|
var parse = getParsed(colorExpression)[0];
|
|
|
|
expect(parse.type).toMatch("color");
|
|
expect(parse.value.color).toBeDefined();
|
|
expect(parse.value.value).toBeDefined();
|
|
});
|
|
|
|
it("should parse a custom color", function() {
|
|
expect(customColorExpression).toParse();
|
|
});
|
|
|
|
it("should correctly extract the custom color", function() {
|
|
var parse = getParsed(customColorExpression)[0];
|
|
|
|
expect(parse.value.color).toMatch("#fA6");
|
|
});
|
|
|
|
it("should not parse a bad custom color", function() {
|
|
expect(badCustomColorExpression).toNotParse();
|
|
});
|
|
|
|
it("should parse new colors from the branding guide", function(){
|
|
expect(newColorExpression).toParse();
|
|
});
|
|
|
|
it("should have correct greediness", function() {
|
|
expect("\\color{red}a").toParse();
|
|
expect("\\color{red}{\\text{a}}").toParse();
|
|
expect("\\color{red}\\text{a}").toNotParse();
|
|
expect("\\color{red}\\frac12").toNotParse();
|
|
});
|
|
});
|
|
|
|
describe("A tie parser", function() {
|
|
var mathTie = "a~b";
|
|
var textTie = "\\text{a~ b}";
|
|
|
|
it("should parse ties in math mode", function() {
|
|
expect(mathTie).toParse();
|
|
});
|
|
|
|
it("should parse ties in text mode", function() {
|
|
expect(textTie).toParse();
|
|
});
|
|
|
|
it("should produce spacing in math mode", function() {
|
|
var parse = getParsed(mathTie);
|
|
|
|
expect(parse[1].type).toMatch("spacing");
|
|
});
|
|
|
|
it("should produce spacing in text mode", function() {
|
|
var text = getParsed(textTie)[0];
|
|
var parse = text.value.body;
|
|
|
|
expect(parse[1].type).toMatch("spacing");
|
|
});
|
|
|
|
it("should not contract with spaces in text mode", function() {
|
|
var text = getParsed(textTie)[0];
|
|
var parse = text.value.body;
|
|
|
|
expect(parse[2].type).toMatch("spacing");
|
|
});
|
|
});
|
|
|
|
describe("A delimiter sizing parser", function() {
|
|
var normalDelim = "\\bigl |";
|
|
var notDelim = "\\bigl x";
|
|
var bigDelim = "\\Biggr \\langle";
|
|
|
|
it("should parse normal delimiters", function() {
|
|
expect(normalDelim).toParse();
|
|
expect(bigDelim).toParse();
|
|
});
|
|
|
|
it("should not parse not-delimiters", function() {
|
|
expect(notDelim).toNotParse();
|
|
});
|
|
|
|
it("should produce a delimsizing", function() {
|
|
var parse = getParsed(normalDelim)[0];
|
|
|
|
expect(parse.type).toMatch("delimsizing");
|
|
});
|
|
|
|
it("should produce the correct direction delimiter", function() {
|
|
var leftParse = getParsed(normalDelim)[0];
|
|
var rightParse = getParsed(bigDelim)[0];
|
|
|
|
expect(leftParse.value.delimType).toMatch("open");
|
|
expect(rightParse.value.delimType).toMatch("close");
|
|
});
|
|
|
|
it("should parse the correct size delimiter", function() {
|
|
var smallParse = getParsed(normalDelim)[0];
|
|
var bigParse = getParsed(bigDelim)[0];
|
|
|
|
expect(smallParse.value.size).toEqual(1);
|
|
expect(bigParse.value.size).toEqual(4);
|
|
});
|
|
});
|
|
|
|
describe("An overline parser", function() {
|
|
var overline = "\\overline{x}";
|
|
|
|
it("should not fail", function() {
|
|
expect(overline).toParse();
|
|
});
|
|
|
|
it("should produce an overline", function() {
|
|
var parse = getParsed(overline)[0];
|
|
|
|
expect(parse.type).toMatch("overline");
|
|
});
|
|
});
|
|
|
|
describe("A rule parser", function() {
|
|
var emRule = "\\rule{1em}{2em}";
|
|
var exRule = "\\rule{1ex}{2em}";
|
|
var badUnitRule = "\\rule{1px}{2em}";
|
|
var noNumberRule = "\\rule{1em}{em}";
|
|
var incompleteRule = "\\rule{1em}";
|
|
var hardNumberRule = "\\rule{ 01.24ex}{2.450 em }";
|
|
|
|
it("should not fail", function() {
|
|
expect(emRule).toParse();
|
|
expect(exRule).toParse();
|
|
});
|
|
|
|
it("should not parse invalid units", function() {
|
|
expect(badUnitRule).toNotParse();
|
|
|
|
expect(noNumberRule).toNotParse();
|
|
});
|
|
|
|
it("should not parse incomplete rules", function() {
|
|
expect(incompleteRule).toNotParse();
|
|
});
|
|
|
|
it("should produce a rule", function() {
|
|
var parse = getParsed(emRule)[0];
|
|
|
|
expect(parse.type).toMatch("rule");
|
|
});
|
|
|
|
it("should list the correct units", function() {
|
|
var emParse = getParsed(emRule)[0];
|
|
var exParse = getParsed(exRule)[0];
|
|
|
|
expect(emParse.value.width.unit).toMatch("em");
|
|
expect(emParse.value.height.unit).toMatch("em");
|
|
|
|
expect(exParse.value.width.unit).toMatch("ex");
|
|
expect(exParse.value.height.unit).toMatch("em");
|
|
});
|
|
|
|
it("should parse the number correctly", function() {
|
|
var hardNumberParse = getParsed(hardNumberRule)[0];
|
|
|
|
expect(hardNumberParse.value.width.number).toBeCloseTo(1.24);
|
|
expect(hardNumberParse.value.height.number).toBeCloseTo(2.45);
|
|
});
|
|
|
|
it("should parse negative sizes", function() {
|
|
var parse = getParsed("\\rule{-1em}{- 0.2em}")[0];
|
|
|
|
expect(parse.value.width.number).toBeCloseTo(-1);
|
|
expect(parse.value.height.number).toBeCloseTo(-0.2);
|
|
});
|
|
});
|
|
|
|
describe("A left/right parser", function() {
|
|
var normalLeftRight = "\\left( \\dfrac{x}{y} \\right)";
|
|
var emptyRight = "\\left( \\dfrac{x}{y} \\right.";
|
|
|
|
it("should not fail", function() {
|
|
expect(normalLeftRight).toParse();
|
|
});
|
|
|
|
it("should produce a leftright", function() {
|
|
var parse = getParsed(normalLeftRight)[0];
|
|
|
|
expect(parse.type).toMatch("leftright");
|
|
expect(parse.value.left).toMatch("\\(");
|
|
expect(parse.value.right).toMatch("\\)");
|
|
});
|
|
|
|
it("should error when it is mismatched", function() {
|
|
var unmatchedLeft = "\\left( \\dfrac{x}{y}";
|
|
var unmatchedRight = "\\dfrac{x}{y} \\right)";
|
|
|
|
expect(unmatchedLeft).toNotParse();
|
|
|
|
expect(unmatchedRight).toNotParse();
|
|
});
|
|
|
|
it("should error when braces are mismatched", function() {
|
|
var unmatched = "{ \\left( \\dfrac{x}{y} } \\right)";
|
|
expect(unmatched).toNotParse();
|
|
});
|
|
|
|
it("should error when non-delimiters are provided", function() {
|
|
var nonDelimiter = "\\left$ \\dfrac{x}{y} \\right)";
|
|
expect(nonDelimiter).toNotParse();
|
|
});
|
|
|
|
it("should parse the empty '.' delimiter", function() {
|
|
expect(emptyRight).toParse();
|
|
});
|
|
|
|
it("should parse the '.' delimiter with normal sizes", function() {
|
|
var normalEmpty = "\\Bigl .";
|
|
expect(normalEmpty).toParse();
|
|
});
|
|
});
|
|
|
|
describe("A begin/end parser", function() {
|
|
|
|
it("should parse a simple environment", function() {
|
|
expect("\\begin{matrix}a&b\\\\c&d\\end{matrix}").toParse();
|
|
});
|
|
|
|
it("should parse an environment with argument", function() {
|
|
expect("\\begin{array}{cc}a&b\\\\c&d\\end{array}").toParse();
|
|
});
|
|
|
|
it("should error when name is mismatched", function() {
|
|
expect("\\begin{matrix}a&b\\\\c&d\\end{pmatrix}").toNotParse();
|
|
});
|
|
|
|
it("should error when commands are mismatched", function() {
|
|
expect("\\begin{matrix}a&b\\\\c&d\\right{pmatrix}").toNotParse();
|
|
});
|
|
|
|
it("should error when end is missing", function() {
|
|
expect("\\begin{matrix}a&b\\\\c&d").toNotParse();
|
|
});
|
|
|
|
it("should error when braces are mismatched", function() {
|
|
expect("{\\begin{matrix}a&b\\\\c&d}\\end{matrix}").toNotParse();
|
|
});
|
|
|
|
it("should cooperate with infix notation", function() {
|
|
expect("\\begin{matrix}0&1\\over2&3\\\\4&5&6\\end{matrix}").toParse();
|
|
});
|
|
|
|
it("should nest", function() {
|
|
var m1 = "\\begin{pmatrix}1&2\\\\3&4\\end{pmatrix}";
|
|
var m2 = "\\begin{array}{rl}" + m1 + "&0\\\\0&" + m1 + "\\end{array}";
|
|
expect(m2).toParse();
|
|
});
|
|
|
|
it("should allow \\cr as a line terminator", function() {
|
|
expect("\\begin{matrix}a&b\\cr c&d\\end{matrix}").toParse();
|
|
});
|
|
});
|
|
|
|
describe("A sqrt parser", function() {
|
|
var sqrt = "\\sqrt{x}";
|
|
var missingGroup = "\\sqrt";
|
|
|
|
it("should parse square roots", function() {
|
|
expect(sqrt).toParse();
|
|
});
|
|
|
|
it("should error when there is no group", function() {
|
|
expect(missingGroup).toNotParse();
|
|
});
|
|
|
|
it("should produce sqrts", function() {
|
|
var parse = getParsed(sqrt)[0];
|
|
|
|
expect(parse.type).toMatch("sqrt");
|
|
});
|
|
});
|
|
|
|
describe("A TeX-compliant parser", function() {
|
|
it("should work", function() {
|
|
expect("\\frac 2 3").toParse();
|
|
});
|
|
|
|
it("should fail if there are not enough arguments", function() {
|
|
var missingGroups = [
|
|
"\\frac{x}",
|
|
"\\color{#fff}",
|
|
"\\rule{1em}",
|
|
"\\llap",
|
|
"\\bigl",
|
|
"\\text"
|
|
];
|
|
|
|
for (var i = 0; i < missingGroups.length; i++) {
|
|
expect(missingGroups[i]).toNotParse();
|
|
}
|
|
});
|
|
|
|
it("should fail when there are missing sup/subscripts", function() {
|
|
expect("x^").toNotParse();
|
|
expect("x_").toNotParse();
|
|
});
|
|
|
|
it("should fail when arguments require arguments", function() {
|
|
var badArguments = [
|
|
"\\frac \\frac x y z",
|
|
"\\frac x \\frac y z",
|
|
"\\frac \\sqrt x y",
|
|
"\\frac x \\sqrt y",
|
|
"\\frac \\llap x y",
|
|
"\\frac x \\llap y",
|
|
// This actually doesn't work in real TeX, but it is suprisingly
|
|
// hard to get this to correctly work. So, we take hit of very small
|
|
// amounts of non-compatiblity in order for the rest of the tests to
|
|
// work
|
|
// "\\llap \\frac x y",
|
|
"\\llap \\llap x",
|
|
"\\sqrt \\llap x"
|
|
];
|
|
|
|
for (var i = 0; i < badArguments.length; i++) {
|
|
expect(badArguments[i]).toNotParse();
|
|
}
|
|
});
|
|
|
|
it("should work when the arguments have braces", function() {
|
|
var goodArguments = [
|
|
"\\frac {\\frac x y} z",
|
|
"\\frac x {\\frac y z}",
|
|
"\\frac {\\sqrt x} y",
|
|
"\\frac x {\\sqrt y}",
|
|
"\\frac {\\llap x} y",
|
|
"\\frac x {\\llap y}",
|
|
"\\llap {\\frac x y}",
|
|
"\\llap {\\llap x}",
|
|
"\\sqrt {\\llap x}"
|
|
];
|
|
|
|
for (var i = 0; i < goodArguments.length; i++) {
|
|
expect(goodArguments[i]).toParse();
|
|
}
|
|
});
|
|
|
|
it("should fail when sup/subscripts require arguments", function() {
|
|
var badSupSubscripts = [
|
|
"x^\\sqrt x",
|
|
"x^\\llap x",
|
|
"x_\\sqrt x",
|
|
"x_\\llap x"
|
|
];
|
|
|
|
for (var i = 0; i < badSupSubscripts.length; i++) {
|
|
expect(badSupSubscripts[i]).toNotParse();
|
|
}
|
|
});
|
|
|
|
it("should work when sup/subscripts arguments have braces", function() {
|
|
var goodSupSubscripts = [
|
|
"x^{\\sqrt x}",
|
|
"x^{\\llap x}",
|
|
"x_{\\sqrt x}",
|
|
"x_{\\llap x}"
|
|
];
|
|
|
|
for (var i = 0; i < goodSupSubscripts.length; i++) {
|
|
expect(goodSupSubscripts[i]).toParse();
|
|
}
|
|
});
|
|
|
|
it("should parse multiple primes correctly", function() {
|
|
expect("x''''").toParse();
|
|
expect("x_2''").toParse();
|
|
expect("x''_2").toParse();
|
|
expect("x'_2'").toParse();
|
|
});
|
|
|
|
it("should fail when sup/subscripts are interspersed with arguments", function() {
|
|
expect("\\sqrt^23").toNotParse();
|
|
expect("\\frac^234").toNotParse();
|
|
expect("\\frac2^34").toNotParse();
|
|
});
|
|
|
|
it("should succeed when sup/subscripts come after whole functions", function() {
|
|
expect("\\sqrt2^3").toParse();
|
|
expect("\\frac23^4").toParse();
|
|
});
|
|
|
|
it("should succeed with a sqrt around a text/frac", function() {
|
|
expect("\\sqrt \\frac x y").toParse();
|
|
expect("\\sqrt \\text x").toParse();
|
|
expect("x^\\frac x y").toParse();
|
|
expect("x_\\text x").toParse();
|
|
});
|
|
|
|
it("should fail when arguments are \\left", function() {
|
|
var badLeftArguments = [
|
|
"\\frac \\left( x \\right) y",
|
|
"\\frac x \\left( y \\right)",
|
|
"\\llap \\left( x \\right)",
|
|
"\\sqrt \\left( x \\right)",
|
|
"x^\\left( x \\right)"
|
|
];
|
|
|
|
for (var i = 0; i < badLeftArguments.length; i++) {
|
|
expect(badLeftArguments[i]).toNotParse();
|
|
}
|
|
});
|
|
|
|
it("should succeed when there are braces around the \\left/\\right", function() {
|
|
var goodLeftArguments = [
|
|
"\\frac {\\left( x \\right)} y",
|
|
"\\frac x {\\left( y \\right)}",
|
|
"\\llap {\\left( x \\right)}",
|
|
"\\sqrt {\\left( x \\right)}",
|
|
"x^{\\left( x \\right)}"
|
|
];
|
|
|
|
for (var i = 0; i < goodLeftArguments.length; i++) {
|
|
expect(goodLeftArguments[i]).toParse();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("A style change parser", function() {
|
|
it("should not fail", function() {
|
|
expect("\\displaystyle x").toParse();
|
|
expect("\\textstyle x").toParse();
|
|
expect("\\scriptstyle x").toParse();
|
|
expect("\\scriptscriptstyle x").toParse();
|
|
});
|
|
|
|
it("should produce the correct style", function() {
|
|
var displayParse = getParsed("\\displaystyle x")[0];
|
|
expect(displayParse.value.style).toMatch("display");
|
|
|
|
var scriptscriptParse = getParsed("\\scriptscriptstyle x")[0];
|
|
expect(scriptscriptParse.value.style).toMatch("scriptscript");
|
|
});
|
|
|
|
it("should only change the style within its group", function() {
|
|
var text = "a b { c d \\displaystyle e f } g h";
|
|
var parse = getParsed(text);
|
|
|
|
var displayNode = parse[2].value[2];
|
|
|
|
expect(displayNode.type).toMatch("styling");
|
|
|
|
var displayBody = displayNode.value.value;
|
|
|
|
expect(displayBody.length).toEqual(2);
|
|
expect(displayBody[0].value).toMatch("e");
|
|
});
|
|
});
|
|
|
|
describe("A bin builder", function() {
|
|
it("should create mbins normally", function() {
|
|
var built = getBuilt("x + y");
|
|
|
|
expect(built[1].classes).toContain("mbin");
|
|
});
|
|
|
|
it("should create ords when at the beginning of lists", function() {
|
|
var built = getBuilt("+ x");
|
|
|
|
expect(built[0].classes).toContain("mord");
|
|
expect(built[0].classes).not.toContain("mbin");
|
|
});
|
|
|
|
it("should create ords after some other objects", function() {
|
|
expect(getBuilt("x + + 2")[2].classes).toContain("mord");
|
|
expect(getBuilt("( + 2")[1].classes).toContain("mord");
|
|
expect(getBuilt("= + 2")[1].classes).toContain("mord");
|
|
expect(getBuilt("\\sin + 2")[1].classes).toContain("mord");
|
|
expect(getBuilt(", + 2")[1].classes).toContain("mord");
|
|
});
|
|
|
|
it("should correctly interact with color objects", function() {
|
|
expect(getBuilt("\\blue{x}+y")[1].classes).toContain("mbin");
|
|
expect(getBuilt("\\blue{x+}+y")[1].classes).toContain("mord");
|
|
});
|
|
});
|
|
|
|
describe("A markup generator", function() {
|
|
it("marks trees up", function() {
|
|
// Just a few quick sanity checks here...
|
|
var markup = katex.renderToString("\\sigma^2");
|
|
expect(markup.indexOf("<span")).toBe(0);
|
|
expect(markup).toContain("\u03c3"); // sigma
|
|
expect(markup).toContain("margin-right");
|
|
expect(markup).not.toContain("marginRight");
|
|
});
|
|
|
|
it("generates both MathML and HTML", function() {
|
|
var markup = katex.renderToString("a");
|
|
|
|
expect(markup).toContain("<span");
|
|
expect(markup).toContain("<math");
|
|
});
|
|
});
|
|
|
|
describe("A parse tree generator", function() {
|
|
it("generates a tree", function() {
|
|
var tree = katex.__parse("\\sigma^2");
|
|
expect(JSON.stringify(tree)).toEqual(JSON.stringify([
|
|
{
|
|
"type": "supsub",
|
|
"value": {
|
|
"base": {
|
|
"type": "mathord",
|
|
"value": "\\sigma",
|
|
"mode": "math"
|
|
},
|
|
"sup": {
|
|
"type": "textord",
|
|
"value": "2",
|
|
"mode": "math"
|
|
},
|
|
"sub": undefined
|
|
},
|
|
"mode": "math"
|
|
}
|
|
]));
|
|
});
|
|
});
|
|
|
|
describe("An accent parser", function() {
|
|
it("should not fail", function() {
|
|
expect("\\vec{x}").toParse();
|
|
expect("\\vec{x^2}").toParse();
|
|
expect("\\vec{x}^2").toParse();
|
|
expect("\\vec x").toParse();
|
|
});
|
|
|
|
it("should produce accents", function() {
|
|
var parse = getParsed("\\vec x")[0];
|
|
|
|
expect(parse.type).toMatch("accent");
|
|
});
|
|
|
|
it("should be grouped more tightly than supsubs", function() {
|
|
var parse = getParsed("\\vec x^2")[0];
|
|
|
|
expect(parse.type).toMatch("supsub");
|
|
});
|
|
|
|
it("should not parse expanding accents", function() {
|
|
expect("\\widehat{x}").toNotParse();
|
|
});
|
|
});
|
|
|
|
describe("An accent builder", function() {
|
|
it("should not fail", function() {
|
|
expect("\\vec{x}").toBuild();
|
|
expect("\\vec{x}^2").toBuild();
|
|
expect("\\vec{x}_2").toBuild();
|
|
expect("\\vec{x}_2^2").toBuild();
|
|
});
|
|
|
|
it("should produce mords", function() {
|
|
expect(getBuilt("\\vec x")[0].classes).toContain("mord");
|
|
expect(getBuilt("\\vec +")[0].classes).toContain("mord");
|
|
expect(getBuilt("\\vec +")[0].classes).not.toContain("mbin");
|
|
expect(getBuilt("\\vec )^2")[0].classes).toContain("mord");
|
|
expect(getBuilt("\\vec )^2")[0].classes).not.toContain("mclose");
|
|
});
|
|
});
|
|
|
|
describe("A phantom parser", function() {
|
|
it("should not fail", function() {
|
|
expect("\\phantom{x}").toParse();
|
|
expect("\\phantom{x^2}").toParse();
|
|
expect("\\phantom{x}^2").toParse();
|
|
expect("\\phantom x").toParse();
|
|
});
|
|
|
|
it("should build a phantom node", function() {
|
|
var parse = getParsed("\\phantom{x}")[0];
|
|
|
|
expect(parse.type).toMatch("phantom");
|
|
expect(parse.value.value).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe("A phantom builder", function() {
|
|
it("should not fail", function() {
|
|
expect("\\phantom{x}").toBuild();
|
|
expect("\\phantom{x^2}").toBuild();
|
|
expect("\\phantom{x}^2").toBuild();
|
|
expect("\\phantom x").toBuild();
|
|
});
|
|
|
|
it("should make the children transparent", function() {
|
|
var children = getBuilt("\\phantom{x+1}")[0].children;
|
|
expect(children[0].style.color).toBe("transparent");
|
|
expect(children[1].style.color).toBe("transparent");
|
|
expect(children[2].style.color).toBe("transparent");
|
|
});
|
|
|
|
it("should make all descendants transparent", function() {
|
|
var children = getBuilt("\\phantom{x+\\blue{1}}")[0].children;
|
|
expect(children[0].style.color).toBe("transparent");
|
|
expect(children[1].style.color).toBe("transparent");
|
|
expect(children[2].children[0].style.color).toBe("transparent");
|
|
});
|
|
});
|
|
|
|
describe("A parser error", function () {
|
|
it("should report the position of an error", function () {
|
|
try {
|
|
parseTree("\\sqrt}", defaultSettings);
|
|
} catch (e) {
|
|
expect(e.position).toEqual(5);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("An optional argument parser", function() {
|
|
it("should not fail", function() {
|
|
// Note this doesn't actually make an optional argument, but still
|
|
// should work
|
|
expect("\\frac[1]{2}{3}").toParse();
|
|
|
|
expect("\\rule[0.2em]{1em}{1em}").toParse();
|
|
});
|
|
|
|
it("should work with sqrts with optional arguments", function() {
|
|
expect("\\sqrt[3]{2}").toParse();
|
|
});
|
|
|
|
it("should work when the optional argument is missing", function() {
|
|
expect("\\sqrt{2}").toParse();
|
|
expect("\\rule{1em}{2em}").toParse();
|
|
});
|
|
|
|
it("should fail when the optional argument is malformed", function() {
|
|
expect("\\rule[1]{2em}{3em}").toNotParse();
|
|
});
|
|
|
|
it("should not work if the optional argument isn't closed", function() {
|
|
expect("\\sqrt[").toNotParse();
|
|
});
|
|
});
|
|
|
|
describe("An array environment", function() {
|
|
|
|
it("should accept a single alignment character", function() {
|
|
var parse = getParsed("\\begin{array}r1\\\\20\\end{array}");
|
|
expect(parse[0].type).toBe("array");
|
|
expect(parse[0].value.colalign).toEqual(["r"]);
|
|
});
|
|
|
|
});
|
|
|
|
var getMathML = function(expr) {
|
|
expect(expr).toParse();
|
|
|
|
var built = buildMathML(parseTree(expr));
|
|
|
|
// Strip off the surrounding <span>
|
|
return built.children[0];
|
|
};
|
|
|
|
describe("A MathML builder", function() {
|
|
it("should generate math nodes", function() {
|
|
var node = getMathML("x^2");
|
|
|
|
expect(node.type).toEqual("math");
|
|
});
|
|
|
|
it("should generate appropriate MathML types", function() {
|
|
var identifier = getMathML("x").children[0].children[0];
|
|
expect(identifier.children[0].type).toEqual("mi");
|
|
|
|
var number = getMathML("1").children[0].children[0];
|
|
expect(number.children[0].type).toEqual("mn");
|
|
|
|
var operator = getMathML("+").children[0].children[0];
|
|
expect(operator.children[0].type).toEqual("mo");
|
|
|
|
var space = getMathML("\\;").children[0].children[0];
|
|
expect(space.children[0].type).toEqual("mspace");
|
|
|
|
var text = getMathML("\\text{a}").children[0].children[0];
|
|
expect(text.children[0].type).toEqual("mtext");
|
|
|
|
var textop = getMathML("\\sin").children[0].children[0];
|
|
expect(textop.children[0].type).toEqual("mi");
|
|
});
|
|
|
|
it("should generate a <mphantom> node for \\phantom", function() {
|
|
var phantom = getMathML("\\phantom{x}").children[0].children[0];
|
|
expect(phantom.children[0].type).toEqual("mphantom");
|
|
});
|
|
});
|