Files
KaTeX/test/katex-spec.js
ylemkimon e5333ad04d Add HTML extension (#2082)
* Add html extension

* Fix flow error

* Update documentation

* Add tests

* Call buildA11yStrings for "html" node

* Throw ParseError when parsing \htmlData fails

* Improve documentation

* Add a screenshotter test

* Add dummy screenshot

* Update screenshots
2019-12-01 17:49:28 -05:00

3735 lines
134 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* eslint max-len:0 */
/* global expect: false */
/* global it: false */
/* global describe: false */
/* global beforeAll: false */
import buildMathML from "../src/buildMathML";
import buildTree from "../src/buildTree";
import katex from "../katex";
import parseTree from "../src/parseTree";
import Options from "../src/Options";
import Settings from "../src/Settings";
import Style from "../src/Style";
import {
strictSettings, nonstrictSettings, trustSettings, r,
getBuilt, getParsed, stripPositions,
} from "./helpers";
const defaultOptions = new Options({
style: Style.TEXT,
size: 5,
maxSize: Infinity,
});
describe("A parser", function() {
it("should not fail on an empty string", function() {
expect``.toParse(strictSettings);
});
it("should ignore whitespace", function() {
expect` x y `.toParseLike("xy", strictSettings);
});
it("should ignore whitespace in atom", function() {
expect` x ^ y `.toParseLike("x^y", strictSettings);
});
});
describe("An ord parser", function() {
const expression = "1234|/@.\"`abcdefgzABCDEFGZ";
it("should not fail", function() {
expect(expression).toParse();
});
it("should build a list of ords", function() {
const parse = getParsed(expression);
for (let i = 0; i < parse.length; i++) {
const group = parse[i];
expect(group.type).toMatch("ord");
}
});
it("should parse the right number of ords", function() {
const parse = getParsed(expression);
expect(parse).toHaveLength(expression.length);
});
});
describe("A bin parser", function() {
const expression = r`+-*\cdot\pm\div`;
it("should not fail", function() {
expect(expression).toParse();
});
it("should build a list of bins", function() {
const parse = getParsed(expression);
for (let i = 0; i < parse.length; i++) {
const group = parse[i];
expect(group.type).toEqual("atom");
expect(group.family).toEqual("bin");
}
});
});
describe("A rel parser", function() {
const expression = r`=<>\leq\geq\neq\nleq\ngeq\cong`;
const notExpression = r`\not=\not<\not>\not\leq\not\geq\not\in`;
it("should not fail", function() {
expect(expression).toParse();
expect(notExpression).toParse();
});
it("should build a list of rels", function() {
const parse = getParsed(expression);
for (let i = 0; i < parse.length; i++) {
let group = parse[i];
if (group.type === "htmlmathml") {
expect(group.html).toHaveLength(1);
group = group.html[0];
}
if (group.type === "mclass") {
expect(group.mclass).toEqual("mrel");
} else {
expect(group.type).toEqual("atom");
expect(group.family).toEqual("rel");
}
}
});
});
describe("A punct parser", function() {
const expression = ",;";
it("should not fail", function() {
expect(expression).toParse(strictSettings);
});
it("should build a list of puncts", function() {
const parse = getParsed(expression);
for (let i = 0; i < parse.length; i++) {
const group = parse[i];
expect(group.type).toEqual("atom");
expect(group.family).toEqual("punct");
}
});
});
describe("An open parser", function() {
const expression = "([";
it("should not fail", function() {
expect(expression).toParse();
});
it("should build a list of opens", function() {
const parse = getParsed(expression);
for (let i = 0; i < parse.length; i++) {
const group = parse[i];
expect(group.type).toEqual("atom");
expect(group.family).toEqual("open");
}
});
});
describe("A close parser", function() {
const expression = ")]?!";
it("should not fail", function() {
expect(expression).toParse();
});
it("should build a list of closes", function() {
const parse = getParsed(expression);
for (let i = 0; i < parse.length; i++) {
const group = parse[i];
expect(group.type).toEqual("atom");
expect(group.family).toEqual("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`^3+`.toParse();
expect`_2`.toParse();
expect`^3_2`.toParse();
expect`_2^3`.toParse();
});
it("should produce supsubs for superscript", function() {
const parse = getParsed`x^2`[0];
expect(parse.type).toBe("supsub");
expect(parse.base).toBeDefined();
expect(parse.sup).toBeDefined();
expect(parse.sub).toBeUndefined();
});
it("should produce supsubs for subscript", function() {
const parse = getParsed`x_3`[0];
expect(parse.type).toBe("supsub");
expect(parse.base).toBeDefined();
expect(parse.sub).toBeDefined();
expect(parse.sup).toBeUndefined();
});
it("should produce supsubs for ^_", function() {
const parse = getParsed`x^2_3`[0];
expect(parse.type).toBe("supsub");
expect(parse.base).toBeDefined();
expect(parse.sup).toBeDefined();
expect(parse.sub).toBeDefined();
});
it("should produce supsubs for _^", function() {
const parse = getParsed`x_3^2`[0];
expect(parse.type).toBe("supsub");
expect(parse.base).toBeDefined();
expect(parse.sup).toBeDefined();
expect(parse.sub).toBeDefined();
});
it("should produce the same thing regardless of order", function() {
expect`x^2_3`.toParseLike`x_3^2`;
});
it("should not parse double subscripts or superscripts", function() {
expect`x^x^x`.not.toParse();
expect`x_x_x`.not.toParse();
expect`x_x^x_x`.not.toParse();
expect`x_x^x^x`.not.toParse();
expect`x^x_x_x`.not.toParse();
expect`x^x_x^x`.not.toParse();
});
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 parser with limit controls", function() {
it("should fail when the limit control is not preceded by an op node", function() {
expect`3\nolimits_2^2`.not.toParse();
expect`\sqrt\limits_2^2`.not.toParse();
expect`45 +\nolimits 45`.not.toParse();
});
it("should parse when the limit control directly follows an op node", function() {
expect`\int\limits_2^2 3`.toParse();
expect`\sum\nolimits_3^4 4`.toParse();
});
it("should parse when the limit control is in the sup/sub area of an op node", function() {
expect`\int_2^2\limits`.toParse();
expect`\int^2\nolimits_2`.toParse();
expect`\int_2\limits^2`.toParse();
});
it("should allow multiple limit controls in the sup/sub area of an op node", function() {
expect`\int_2\nolimits^2\limits 3`.toParse();
expect`\int\nolimits\limits_2^2`.toParse();
expect`\int\limits\limits\limits_2^2`.toParse();
});
it("should have the rightmost limit control determine the limits property " +
"of the preceding op node", function() {
let parsedInput = getParsed`\int\nolimits\limits_2^2`;
expect(parsedInput[0].base.limits).toBe(true);
parsedInput = getParsed`\int\limits_2\nolimits^2`;
expect(parsedInput[0].base.limits).toBe(false);
});
});
describe("A group parser", function() {
it("should not fail", function() {
expect`{xy}`.toParse();
});
it("should produce a single ord", function() {
const parse = getParsed`{xy}`;
expect(parse).toHaveLength(1);
const ord = parse[0];
expect(ord.type).toMatch("ord");
expect(ord.body).toBeTruthy();
});
});
describe("A \\begingroup...\\endgroup parser", function() {
it("should not fail", function() {
expect`\begingroup xy \endgroup`.toParse();
});
it("should fail when it is mismatched", function() {
expect`\begingroup xy`.not.toParse();
expect`\begingroup xy }`.not.toParse();
});
it("should produce a semi-simple group", function() {
const parse = getParsed`\begingroup xy \endgroup`;
expect(parse).toHaveLength(1);
const ord = parse[0];
expect(ord.type).toMatch("ord");
expect(ord.body).toBeTruthy();
expect(ord.semisimple).toBeTruthy();
});
it("should not affect spacing in math mode", function() {
expect`\begingroup x+ \endgroup y`.toBuildLike`x+y`;
});
});
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() {
const parse = getParsed`\Large abc`;
expect(parse).toHaveLength(1);
const sizing = parse[0];
expect(sizing.type).toEqual("sizing");
expect(sizing.body).toBeTruthy();
expect(sizing.size).toBeDefined();
});
it("should apply only after the function", function() {
const parse = getParsed`a \Large abc`;
expect(parse).toHaveLength(2);
const sizing = parse[1];
expect(sizing.type).toEqual("sizing");
expect(sizing.body).toHaveLength(3);
});
it("should stop at the ends of groups", function() {
const parse = getParsed`a { b \Large c } d`;
const group = parse[1];
const sizing = group.body[1];
expect(sizing.type).toEqual("sizing");
expect(sizing.body).toHaveLength(1);
});
describe("within optional groups", () => {
it("should work with sizing commands: \\sqrt[\\small 3]{x}", () => {
const tree = stripPositions(getParsed`\sqrt[\small 3]{x}`);
expect(tree).toMatchSnapshot();
});
it("should work with \\color: \\sqrt[\\color{red} 3]{x}", () => {
const tree = stripPositions(getParsed`\sqrt[\color{red} 3]{x}`);
expect(tree).toMatchSnapshot();
});
it("should work style commands \\sqrt[\\textstyle 3]{x}", () => {
const tree = stripPositions(getParsed`\sqrt[\textstyle 3]{x}`);
expect(tree).toMatchSnapshot();
});
it("should work with old font functions: \\sqrt[\\tt 3]{x}", () => {
const tree = stripPositions(getParsed`\sqrt[\tt 3]{x}`);
expect(tree).toMatchSnapshot();
});
});
});
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`.not.toParse();
});
it("should not parse 2 argument functions with 0 or 1 arguments", function() {
expect`\frac`.not.toParse();
expect`\frac 1`.not.toParse();
});
it("should not parse a function with text right after it", function() {
expect`\redx`.not.toParse();
});
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() {
const expression = r`\frac{x}{y}`;
const dfracExpression = r`\dfrac{x}{y}`;
const tfracExpression = r`\tfrac{x}{y}`;
const cfracExpression = r`\cfrac{x}{y}`;
const genfrac1 = r`\genfrac ( ] {0.06em}{0}{a}{b+c}`;
const genfrac2 = r`\genfrac ( ] {0.8pt}{}{a}{b+c}`;
it("should not fail", function() {
expect(expression).toParse();
});
it("should produce a frac", function() {
const parse = getParsed(expression)[0];
expect(parse.type).toEqual("genfrac");
expect(parse.numer).toBeDefined();
expect(parse.denom).toBeDefined();
});
it("should also parse cfrac, dfrac, tfrac, and genfrac", function() {
expect(cfracExpression).toParse();
expect(dfracExpression).toParse();
expect(tfracExpression).toParse();
expect(genfrac1).toParse();
expect(genfrac2).toParse();
});
it("should parse cfrac, dfrac, tfrac, and genfrac as fracs", function() {
const dfracParse = getParsed(dfracExpression)[0];
expect(dfracParse.type).toEqual("genfrac");
expect(dfracParse.numer).toBeDefined();
expect(dfracParse.denom).toBeDefined();
const tfracParse = getParsed(tfracExpression)[0];
expect(tfracParse.type).toEqual("genfrac");
expect(tfracParse.numer).toBeDefined();
expect(tfracParse.denom).toBeDefined();
const cfracParse = getParsed(cfracExpression)[0];
expect(cfracParse.type).toEqual("genfrac");
expect(cfracParse.numer).toBeDefined();
expect(cfracParse.denom).toBeDefined();
const genfracParse = getParsed(genfrac1)[0];
expect(genfracParse.type).toEqual("genfrac");
expect(genfracParse.numer).toBeDefined();
expect(genfracParse.denom).toBeDefined();
expect(genfracParse.leftDelim).toBeDefined();
expect(genfracParse.rightDelim).toBeDefined();
});
it("should fail, given math as a line thickness to genfrac", function() {
const badGenFrac = "\\genfrac ( ] {b+c}{0}{a}{b+c}";
expect(badGenFrac).not.toParse();
});
it("should fail if genfrac is given less than 6 arguments", function() {
const badGenFrac = "\\genfrac ( ] {0.06em}{0}{a}";
expect(badGenFrac).not.toParse();
});
it("should parse atop", function() {
const parse = getParsed`x \atop y`[0];
expect(parse.type).toEqual("genfrac");
expect(parse.numer).toBeDefined();
expect(parse.denom).toBeDefined();
expect(parse.hasBarLine).toEqual(false);
});
});
describe("An over/brace/brack parser", function() {
const simpleOver = r`1 \over x`;
const complexOver = r`1+2i \over 3+4i`;
const braceFrac = r`a+b \brace c+d`;
const brackFrac = r`a+b \brack c+d`;
it("should not fail", function() {
expect(simpleOver).toParse();
expect(complexOver).toParse();
expect(braceFrac).toParse();
expect(brackFrac).toParse();
});
it("should produce a frac", function() {
let parse;
parse = getParsed(simpleOver)[0];
expect(parse.type).toEqual("genfrac");
expect(parse.numer).toBeDefined();
expect(parse.denom).toBeDefined();
parse = getParsed(complexOver)[0];
expect(parse.type).toEqual("genfrac");
expect(parse.numer).toBeDefined();
expect(parse.denom).toBeDefined();
const parseBraceFrac = getParsed(braceFrac)[0];
expect(parseBraceFrac.type).toEqual("genfrac");
expect(parseBraceFrac.numer).toBeDefined();
expect(parseBraceFrac.denom).toBeDefined();
expect(parseBraceFrac.leftDelim).toBeDefined();
expect(parseBraceFrac.rightDelim).toBeDefined();
const parseBrackFrac = getParsed(brackFrac)[0];
expect(parseBrackFrac.type).toEqual("genfrac");
expect(parseBrackFrac.numer).toBeDefined();
expect(parseBrackFrac.denom).toBeDefined();
expect(parseBrackFrac.leftDelim).toBeDefined();
expect(parseBrackFrac.rightDelim).toBeDefined();
});
it("should create a numerator from the atoms before \\over", function() {
const parse = getParsed(complexOver)[0];
const numer = parse.numer;
expect(numer.body).toHaveLength(4);
});
it("should create a denominator from the atoms after \\over", function() {
const parse = getParsed(complexOver)[0];
const denom = parse.denom;
expect(denom.body).toHaveLength(4);
});
it("should handle empty numerators", function() {
const emptyNumerator = r`\over x`;
const parse = getParsed(emptyNumerator)[0];
expect(parse.type).toEqual("genfrac");
expect(parse.numer).toBeDefined();
expect(parse.denom).toBeDefined();
});
it("should handle empty denominators", function() {
const emptyDenominator = r`1 \over`;
const parse = getParsed(emptyDenominator)[0];
expect(parse.type).toEqual("genfrac");
expect(parse.numer).toBeDefined();
expect(parse.denom).toBeDefined();
});
it("should handle \\displaystyle correctly", function() {
const displaystyleExpression = r`\displaystyle 1 \over 2`;
const parse = getParsed(displaystyleExpression)[0];
expect(parse.type).toEqual("genfrac");
expect(parse.numer.body[0].type).toEqual("styling");
expect(parse.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() {
const nestedOverExpression = r`{1 \over 2} \over 3`;
const parse = getParsed(nestedOverExpression)[0];
expect(parse.type).toEqual("genfrac");
expect(parse.numer.body[0].type).toEqual("genfrac");
expect(parse.numer.body[0].numer.body[0].text).toEqual("1");
expect(parse.numer.body[0].denom.body[0].text).toEqual("2");
expect(parse.denom).toBeDefined();
expect(parse.denom.body[0].text).toEqual("3");
});
it("should fail with multiple overs in the same group", function() {
const badMultipleOvers = r`1 \over 2 + 3 \over 4`;
expect(badMultipleOvers).not.toParse();
const badOverChoose = r`1 \over 2 \choose 3`;
expect(badOverChoose).not.toParse();
});
});
describe("A genfrac builder", function() {
it("should not fail", function() {
expect("\\frac{x}{y}").toBuild();
expect("\\dfrac{x}{y}").toBuild();
expect("\\tfrac{x}{y}").toBuild();
expect("\\cfrac{x}{y}").toBuild();
expect("\\genfrac ( ] {0.06em}{0}{a}{b+c}").toBuild();
expect("\\genfrac ( ] {0.8pt}{}{a}{b+c}").toBuild();
expect("\\genfrac {} {} {0.8pt}{}{a}{b+c}").toBuild();
expect("\\genfrac [ {} {0.8pt}{}{a}{b+c}").toBuild();
});
});
describe("A infix builder", function() {
it("should not fail", function() {
expect("a \\over b").toBuild();
expect("a \\atop b").toBuild();
expect("a \\choose b").toBuild();
expect("a \\brace b").toBuild();
expect("a \\brack b").toBuild();
});
});
describe("A sizing parser", function() {
const sizeExpression = r`\Huge{x}\small{x}`;
it("should not fail", function() {
expect(sizeExpression).toParse();
});
it("should produce a sizing node", function() {
const parse = getParsed(sizeExpression)[0];
expect(parse.type).toEqual("sizing");
expect(parse.size).toBeDefined();
expect(parse.body).toBeDefined();
});
});
describe("A text parser", function() {
const textExpression = r`\text{a b}`;
const noBraceTextExpression = r`\text x`;
const nestedTextExpression =
r`\text{a {b} \blue{c} \textcolor{#fff}{x} \llap{x}}`;
const spaceTextExpression = r`\text{ a \ }`;
const leadingSpaceTextExpression = r`\text {moo}`;
const badTextExpression = r`\text{a b%}`;
const badFunctionExpression = r`\text{\sqrt{x}}`;
const mathTokenAfterText = r`\text{sin}^2`;
it("should not fail", function() {
expect(textExpression).toParse();
});
it("should produce a text", function() {
const parse = getParsed(textExpression)[0];
expect(parse.type).toEqual("text");
expect(parse.body).toBeDefined();
});
it("should produce textords instead of mathords", function() {
const parse = getParsed(textExpression)[0];
const group = parse.body;
expect(group[0].type).toEqual("textord");
});
it("should not parse bad text", function() {
expect(badTextExpression).not.toParse();
});
it("should not parse bad functions inside text", function() {
expect(badFunctionExpression).not.toParse();
});
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() {
const parse = getParsed(spaceTextExpression)[0];
const group = parse.body;
expect(group[0].type).toEqual("spacing");
expect(group[1].type).toEqual("textord");
expect(group[2].type).toEqual("spacing");
expect(group[3].type).toEqual("spacing");
});
it("should accept math mode tokens after its argument", function() {
expect(mathTokenAfterText).toParse();
});
it("should ignore a space before the text group", function() {
const parse = getParsed(leadingSpaceTextExpression)[0];
// [m, o, o]
expect(parse.body).toHaveLength(3);
expect(parse.body.map(n => n.text).join("")).toBe("moo");
});
it("should parse math within text group", function() {
expect`\text{graph: $y = mx + b$}`.toParse(strictSettings);
expect`\text{graph: \(y = mx + b\)}`.toParse(strictSettings);
});
it("should parse math within text within math within text", function() {
expect`\text{hello $x + \text{world $y$} + z$}`.toParse(strictSettings);
expect`\text{hello \(x + \text{world $y$} + z\)}`.toParse(strictSettings);
expect`\text{hello $x + \text{world \(y\)} + z$}`.toParse(strictSettings);
expect`\text{hello \(x + \text{world \(y\)} + z\)}`.toParse(strictSettings);
});
it("should forbid \\( within math mode", function() {
expect`\(`.not.toParse();
expect`\text{$\(x\)$}`.not.toParse();
});
it("should forbid $ within math mode", function() {
expect`$x$`.not.toParse();
expect`\text{\($x$\)}`.not.toParse();
});
it("should detect unbalanced \\)", function() {
expect`\)`.not.toParse();
expect`\text{\)}`.not.toParse();
});
it("should detect unbalanced $", function() {
expect`$`.not.toParse();
expect`\text{$}`.not.toParse();
});
it("should not mix $ and \\(..\\)", function() {
expect`\text{$x\)}`.not.toParse();
expect`\text{\(x$}`.not.toParse();
});
it("should parse spacing functions", function() {
expect`a b\, \; \! \: \> ~ \thinspace \medspace \quad \ `.toBuild();
expect`\enspace \thickspace \qquad \space \nobreakspace`.toBuild();
});
it("should omit spaces after commands", function() {
expect`\text{\textellipsis !}`.toParseLike`\text{\textellipsis!}`;
});
});
describe("A texvc builder", function() {
it("should not fail", function() {
expect("\\lang\\N\\darr\\R\\dArr\\Z\\Darr\\alef\\rang").toBuild();
expect("\\alefsym\\uarr\\Alpha\\uArr\\Beta\\Uarr\\Chi").toBuild();
expect("\\clubs\\diamonds\\hearts\\spades\\cnums\\Complex").toBuild();
expect("\\Dagger\\empty\\harr\\Epsilon\\hArr\\Eta\\Harr\\exist").toBuild();
expect("\\image\\larr\\infin\\lArr\\Iota\\Larr\\isin\\Kappa").toBuild();
expect("\\Mu\\lrarr\\natnums\\lrArr\\Nu\\Lrarr\\Omicron").toBuild();
expect("\\real\\rarr\\plusmn\\rArr\\reals\\Rarr\\Reals\\Rho").toBuild();
expect("\\text{\\sect}\\sdot\\sub\\sube\\supe").toBuild();
expect("\\Tau\\thetasym\\weierp\\Zeta").toBuild();
});
});
describe("A color parser", function() {
const colorExpression = r`\blue{x}`;
const newColorExpression = r`\redA{x}`;
const customColorExpression1 = r`\textcolor{#fA6}{x}`;
const customColorExpression2 = r`\textcolor{#fA6fA6}{x}`;
const customColorExpression3 = r`\textcolor{fA6fA6}{x}`;
const badCustomColorExpression1 = r`\textcolor{bad-color}{x}`;
const badCustomColorExpression2 = r`\textcolor{#fA6f}{x}`;
const badCustomColorExpression3 = r`\textcolor{#gA6}{x}`;
const oldColorExpression = r`\color{#fA6}xy`;
it("should not fail", function() {
expect(colorExpression).toParse();
});
it("should build a color node", function() {
const parse = getParsed(colorExpression)[0];
expect(parse.type).toEqual("color");
expect(parse.color).toBeDefined();
expect(parse.body).toBeDefined();
});
it("should parse a custom color", function() {
expect(customColorExpression1).toParse();
expect(customColorExpression2).toParse();
expect(customColorExpression3).toParse();
});
it("should correctly extract the custom color", function() {
const parse1 = getParsed(customColorExpression1)[0];
const parse2 = getParsed(customColorExpression2)[0];
const parse3 = getParsed(customColorExpression3)[0];
expect(parse1.color).toEqual("#fA6");
expect(parse2.color).toEqual("#fA6fA6");
expect(parse3.color).toEqual("#fA6fA6");
});
it("should not parse a bad custom color", function() {
expect(badCustomColorExpression1).not.toParse();
expect(badCustomColorExpression2).not.toParse();
expect(badCustomColorExpression3).not.toParse();
});
it("should parse new colors from the branding guide", function() {
expect(newColorExpression).toParse();
});
it("should have correct greediness", function() {
expect`\textcolor{red}a`.toParse();
expect`\textcolor{red}{\text{a}}`.toParse();
expect`\textcolor{red}\text{a}`.not.toParse();
expect`\textcolor{red}\frac12`.not.toParse();
});
it("should use one-argument \\color by default", function() {
expect(oldColorExpression).toParseLike`\textcolor{#fA6}{xy}`;
});
it("should use one-argument \\color if requested", function() {
expect(oldColorExpression).toParseLike(r`\textcolor{#fA6}{xy}`, {
colorIsTextColor: false,
});
});
it("should use two-argument \\color if requested", function() {
expect(oldColorExpression).toParseLike(r`\textcolor{#fA6}{x}y`, {
colorIsTextColor: true,
});
});
it("should not define \\color in global context", function() {
const macros = {};
expect(oldColorExpression).toParseLike(r`\textcolor{#fA6}{x}y`, {
colorIsTextColor: true,
macros: macros,
});
expect(macros).toEqual({});
});
});
describe("A tie parser", function() {
const mathTie = "a~b";
const textTie = r`\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() {
const parse = getParsed(mathTie);
expect(parse[1].type).toEqual("spacing");
});
it("should produce spacing in text mode", function() {
const text = getParsed(textTie)[0];
const parse = text.body;
expect(parse[1].type).toEqual("spacing");
});
it("should not contract with spaces in text mode", function() {
const text = getParsed(textTie)[0];
const parse = text.body;
expect(parse[2].type).toEqual("spacing");
});
});
describe("A delimiter sizing parser", function() {
const normalDelim = r`\bigl |`;
const notDelim = r`\bigl x`;
const bigDelim = r`\Biggr \langle`;
it("should parse normal delimiters", function() {
expect(normalDelim).toParse();
expect(bigDelim).toParse();
});
it("should not parse not-delimiters", function() {
expect(notDelim).not.toParse();
});
it("should produce a delimsizing", function() {
const parse = getParsed(normalDelim)[0];
expect(parse.type).toEqual("delimsizing");
});
it("should produce the correct direction delimiter", function() {
const leftParse = getParsed(normalDelim)[0];
const rightParse = getParsed(bigDelim)[0];
expect(leftParse.mclass).toEqual("mopen");
expect(rightParse.mclass).toEqual("mclose");
});
it("should parse the correct size delimiter", function() {
const smallParse = getParsed(normalDelim)[0];
const bigParse = getParsed(bigDelim)[0];
expect(smallParse.size).toEqual(1);
expect(bigParse.size).toEqual(4);
});
});
describe("An overline parser", function() {
const overline = r`\overline{x}`;
it("should not fail", function() {
expect(overline).toParse();
});
it("should produce an overline", function() {
const parse = getParsed(overline)[0];
expect(parse.type).toEqual("overline");
});
});
describe("An lap parser", function() {
it("should not fail on a text argument", function() {
expect`\rlap{\,/}{=}`.toParse();
expect`\mathrlap{\,/}{=}`.toParse();
expect`{=}\llap{/\,}`.toParse();
expect`{=}\mathllap{/\,}`.toParse();
expect`\sum_{\clap{ABCDEFG}}`.toParse();
expect`\sum_{\mathclap{ABCDEFG}}`.toParse();
});
it("should not fail if math version is used", function() {
expect`\mathrlap{\frac{a}{b}}{=}`.toParse();
expect`{=}\mathllap{\frac{a}{b}}`.toParse();
expect`\sum_{\mathclap{\frac{a}{b}}}`.toParse();
});
it("should fail on math if AMS version is used", function() {
expect`\rlap{\frac{a}{b}}{=}`.not.toParse();
expect`{=}\llap{\frac{a}{b}}`.not.toParse();
expect`\sum_{\clap{\frac{a}{b}}}`.not.toParse();
});
it("should produce a lap", function() {
const parse = getParsed`\mathrlap{\,/}`[0];
expect(parse.type).toEqual("lap");
});
});
describe("A rule parser", function() {
const emRule = r`\rule{1em}{2em}`;
const exRule = r`\rule{1ex}{2em}`;
const badUnitRule = r`\rule{1au}{2em}`;
const noNumberRule = r`\rule{1em}{em}`;
const incompleteRule = r`\rule{1em}`;
const hardNumberRule = r`\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).not.toParse();
expect(noNumberRule).not.toParse();
});
it("should not parse incomplete rules", function() {
expect(incompleteRule).not.toParse();
});
it("should produce a rule", function() {
const parse = getParsed(emRule)[0];
expect(parse.type).toEqual("rule");
});
it("should list the correct units", function() {
const emParse = getParsed(emRule)[0];
const exParse = getParsed(exRule)[0];
expect(emParse.width.unit).toEqual("em");
expect(emParse.height.unit).toEqual("em");
expect(exParse.width.unit).toEqual("ex");
expect(exParse.height.unit).toEqual("em");
});
it("should parse the number correctly", function() {
const hardNumberParse = getParsed(hardNumberRule)[0];
expect(hardNumberParse.width.number).toBeCloseTo(1.24);
expect(hardNumberParse.height.number).toBeCloseTo(2.45);
});
it("should parse negative sizes", function() {
const parse = getParsed`\rule{-1em}{- 0.2em}`[0];
expect(parse.width.number).toBeCloseTo(-1);
expect(parse.height.number).toBeCloseTo(-0.2);
});
});
describe("A kern parser", function() {
const emKern = r`\kern{1em}`;
const exKern = r`\kern{1ex}`;
const muKern = r`\mkern{1mu}`;
const abKern = r`a\kern{1em}b`;
const badUnitRule = r`\kern{1au}`;
const noNumberRule = r`\kern{em}`;
it("should list the correct units", function() {
const emParse = getParsed(emKern)[0];
const exParse = getParsed(exKern)[0];
const muParse = getParsed(muKern)[0];
const abParse = getParsed(abKern)[1];
expect(emParse.dimension.unit).toEqual("em");
expect(exParse.dimension.unit).toEqual("ex");
expect(muParse.dimension.unit).toEqual("mu");
expect(abParse.dimension.unit).toEqual("em");
});
it("should not parse invalid units", function() {
expect(badUnitRule).not.toParse();
expect(noNumberRule).not.toParse();
});
it("should parse negative sizes", function() {
const parse = getParsed`\kern{-1em}`[0];
expect(parse.dimension.number).toBeCloseTo(-1);
});
it("should parse positive sizes", function() {
const parse = getParsed`\kern{+1em}`[0];
expect(parse.dimension.number).toBeCloseTo(1);
});
});
describe("A non-braced kern parser", function() {
const emKern = r`\kern1em`;
const exKern = r`\kern 1 ex`;
const muKern = r`\mkern 1mu`;
const abKern1 = r`a\mkern1mub`;
const abKern2 = r`a\mkern-1mub`;
const abKern3 = r`a\mkern-1mu b`;
const badUnitRule = r`\kern1au`;
const noNumberRule = r`\kern em`;
it("should list the correct units", function() {
const emParse = getParsed(emKern)[0];
const exParse = getParsed(exKern)[0];
const muParse = getParsed(muKern)[0];
const abParse1 = getParsed(abKern1)[1];
const abParse2 = getParsed(abKern2)[1];
const abParse3 = getParsed(abKern3)[1];
expect(emParse.dimension.unit).toEqual("em");
expect(exParse.dimension.unit).toEqual("ex");
expect(muParse.dimension.unit).toEqual("mu");
expect(abParse1.dimension.unit).toEqual("mu");
expect(abParse2.dimension.unit).toEqual("mu");
expect(abParse3.dimension.unit).toEqual("mu");
});
it("should parse elements on either side of a kern", function() {
const abParse1 = getParsed(abKern1);
const abParse2 = getParsed(abKern2);
const abParse3 = getParsed(abKern3);
expect(abParse1).toHaveLength(3);
expect(abParse1[0].text).toEqual("a");
expect(abParse1[2].text).toEqual("b");
expect(abParse2).toHaveLength(3);
expect(abParse2[0].text).toEqual("a");
expect(abParse2[2].text).toEqual("b");
expect(abParse3).toHaveLength(3);
expect(abParse3[0].text).toEqual("a");
expect(abParse3[2].text).toEqual("b");
});
it("should not parse invalid units", function() {
expect(badUnitRule).not.toParse();
expect(noNumberRule).not.toParse();
});
it("should parse negative sizes", function() {
const parse = getParsed`\kern-1em`[0];
expect(parse.dimension.number).toBeCloseTo(-1);
});
it("should parse positive sizes", function() {
const parse = getParsed`\kern+1em`[0];
expect(parse.dimension.number).toBeCloseTo(1);
});
it("should handle whitespace", function() {
const abKern = "a\\mkern\t-\r1 \n mu\nb";
const abParse = getParsed(abKern);
expect(abParse).toHaveLength(3);
expect(abParse[0].text).toEqual("a");
expect(abParse[1].dimension.unit).toEqual("mu");
expect(abParse[2].text).toEqual("b");
});
});
describe("A left/right parser", function() {
const normalLeftRight = r`\left( \dfrac{x}{y} \right)`;
const emptyRight = r`\left( \dfrac{x}{y} \right.`;
it("should not fail", function() {
expect(normalLeftRight).toParse();
});
it("should produce a leftright", function() {
const parse = getParsed(normalLeftRight)[0];
expect(parse.type).toEqual("leftright");
expect(parse.left).toEqual("(");
expect(parse.right).toEqual(")");
});
it("should error when it is mismatched", function() {
const unmatchedLeft = r`\left( \dfrac{x}{y}`;
const unmatchedRight = r`\dfrac{x}{y} \right)`;
expect(unmatchedLeft).not.toParse();
expect(unmatchedRight).not.toParse();
});
it("should error when braces are mismatched", function() {
const unmatched = r`{ \left( \dfrac{x}{y} } \right)`;
expect(unmatched).not.toParse();
});
it("should error when non-delimiters are provided", function() {
const nonDelimiter = r`\left$ \dfrac{x}{y} \right)`;
expect(nonDelimiter).not.toParse();
});
it("should parse the empty '.' delimiter", function() {
expect(emptyRight).toParse();
});
it("should parse the '.' delimiter with normal sizes", function() {
const normalEmpty = r`\Bigl .`;
expect(normalEmpty).toParse();
});
it("should handle \\middle", function() {
const normalMiddle = r`\left( \dfrac{x}{y} \middle| \dfrac{y}{z} \right)`;
expect(normalMiddle).toParse();
});
it("should handle multiple \\middles", function() {
const multiMiddle = r`\left( \dfrac{x}{y} \middle| \dfrac{y}{z} \middle/ \dfrac{z}{q} \right)`;
expect(multiMiddle).toParse();
});
it("should handle nested \\middles", function() {
const nestedMiddle = r`\left( a^2 \middle| \left( b \middle/ c \right) \right)`;
expect(nestedMiddle).toParse();
});
it("should error when \\middle is not in \\left...\\right", function() {
const unmatchedMiddle = r`(\middle|\dfrac{x}{y})`;
expect(unmatchedMiddle).not.toParse();
});
});
describe("left/right builder", () => {
const cases = [
[r`\left\langle \right\rangle`, r`\left< \right>`],
[r`\left\langle \right\rangle`, '\\left\u27e8 \\right\u27e9'],
[r`\left\lparen \right\rparen`, r`\left( \right)`],
];
for (const [actual, expected] of cases) {
it(`should build "${actual}" like "${expected}"`, () => {
expect(actual).toBuildLike(expected);
});
}
});
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 parse an environment with hlines", function() {
expect`\begin{matrix}\hline a&b\\ \hline c&d\end{matrix}`.toParse();
expect`\begin{matrix}\hdashline a&b\\ \hdashline c&d\end{matrix}`.toParse();
});
it("should forbid hlines outside array environment", () => {
expect`\hline`.not.toParse();
});
it("should error when name is mismatched", function() {
expect`\begin{matrix}a&b\\c&d\end{pmatrix}`.not.toParse();
});
it("should error when commands are mismatched", function() {
expect`\begin{matrix}a&b\\c&d\right{pmatrix}`.not.toParse();
});
it("should error when end is missing", function() {
expect`\begin{matrix}a&b\\c&d`.not.toParse();
});
it("should error when braces are mismatched", function() {
expect`{\begin{matrix}a&b\\c&d}\end{matrix}`.not.toParse();
});
it("should cooperate with infix notation", function() {
expect`\begin{matrix}0&1\over2&3\\4&5&6\end{matrix}`.toParse();
});
it("should nest", function() {
const m1 = r`\begin{pmatrix}1&2\\3&4\end{pmatrix}`;
const 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();
});
it("should eat a final newline", function() {
const m3 = getParsed`\begin{matrix}a&b\\ c&d \\ \end{matrix}`[0];
expect(m3.body).toHaveLength(2);
});
it("should grab \\arraystretch", function() {
const parse = getParsed`\def\arraystretch{1.5}\begin{matrix}a&b\\c&d\end{matrix}`;
expect(parse).toMatchSnapshot();
});
});
describe("A sqrt parser", function() {
const sqrt = r`\sqrt{x}`;
const missingGroup = r`\sqrt`;
it("should parse square roots", function() {
expect(sqrt).toParse();
});
it("should error when there is no group", function() {
expect(missingGroup).not.toParse();
});
it("should produce sqrts", function() {
const parse = getParsed(sqrt)[0];
expect(parse.type).toEqual("sqrt");
});
it("should build sized square roots", function() {
expect("\\Large\\sqrt[3]{x}").toBuild();
});
});
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() {
const missingGroups = [
r`\frac{x}`,
r`\textcolor{#fff}`,
r`\rule{1em}`,
r`\llap`,
r`\bigl`,
r`\text`,
];
for (let i = 0; i < missingGroups.length; i++) {
expect(missingGroups[i]).not.toParse();
}
});
it("should fail when there are missing sup/subscripts", function() {
expect`x^`.not.toParse();
expect`x_`.not.toParse();
});
it("should fail when arguments require arguments", function() {
const badArguments = [
r`\frac \frac x y z`,
r`\frac x \frac y z`,
r`\frac \sqrt x y`,
r`\frac x \sqrt y`,
r`\frac \mathllap x y`,
r`\frac x \mathllap 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
// r`\llap \frac x y`,
r`\mathllap \mathllap x`,
r`\sqrt \mathllap x`,
];
for (let i = 0; i < badArguments.length; i++) {
expect(badArguments[i]).not.toParse();
}
});
it("should work when the arguments have braces", function() {
const goodArguments = [
r`\frac {\frac x y} z`,
r`\frac x {\frac y z}`,
r`\frac {\sqrt x} y`,
r`\frac x {\sqrt y}`,
r`\frac {\mathllap x} y`,
r`\frac x {\mathllap y}`,
r`\mathllap {\frac x y}`,
r`\mathllap {\mathllap x}`,
r`\sqrt {\mathllap x}`,
];
for (let i = 0; i < goodArguments.length; i++) {
expect(goodArguments[i]).toParse();
}
});
it("should fail when sup/subscripts require arguments", function() {
const badSupSubscripts = [
r`x^\sqrt x`,
r`x^\mathllap x`,
r`x_\sqrt x`,
r`x_\mathllap x`,
];
for (let i = 0; i < badSupSubscripts.length; i++) {
expect(badSupSubscripts[i]).not.toParse();
}
});
it("should work when sup/subscripts arguments have braces", function() {
const goodSupSubscripts = [
r`x^{\sqrt x}`,
r`x^{\mathllap x}`,
r`x_{\sqrt x}`,
r`x_{\mathllap x}`,
];
for (let 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();
});
it("should fail when sup/subscripts are interspersed with arguments", function() {
expect`\sqrt^23`.not.toParse();
expect`\frac^234`.not.toParse();
expect`\frac2^34`.not.toParse();
});
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() {
const badLeftArguments = [
r`\frac \left( x \right) y`,
r`\frac x \left( y \right)`,
r`\mathllap \left( x \right)`,
r`\sqrt \left( x \right)`,
r`x^\left( x \right)`,
];
for (let i = 0; i < badLeftArguments.length; i++) {
expect(badLeftArguments[i]).not.toParse();
}
});
it("should succeed when there are braces around the \\left/\\right", function() {
const goodLeftArguments = [
r`\frac {\left( x \right)} y`,
r`\frac x {\left( y \right)}`,
r`\mathllap {\left( x \right)}`,
r`\sqrt {\left( x \right)}`,
r`x^{\left( x \right)}`,
];
for (let i = 0; i < goodLeftArguments.length; i++) {
expect(goodLeftArguments[i]).toParse();
}
});
});
describe("An op symbol builder", function() {
it("should not fail", function() {
expect("\\int_i^n").toBuild();
expect("\\iint_i^n").toBuild();
expect("\\iiint_i^n").toBuild();
expect("\\int\nolimits_i^n").toBuild();
expect("\\iint\nolimits_i^n").toBuild();
expect("\\iiint\nolimits_i^n").toBuild();
expect("\\oint_i^n").toBuild();
expect("\\oiint_i^n").toBuild();
expect("\\oiiint_i^n").toBuild();
expect("\\oint\nolimits_i^n").toBuild();
expect("\\oiint\nolimits_i^n").toBuild();
expect("\\oiiint\nolimits_i^n").toBuild();
});
});
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() {
const displayParse = getParsed`\displaystyle x`[0];
expect(displayParse.style).toEqual("display");
const scriptscriptParse = getParsed`\scriptscriptstyle x`[0];
expect(scriptscriptParse.style).toEqual("scriptscript");
});
it("should only change the style within its group", function() {
const text = r`a b { c d \displaystyle e f } g h`;
const parse = getParsed(text);
const displayNode = parse[2].body[2];
expect(displayNode.type).toEqual("styling");
const displayBody = displayNode.body;
expect(displayBody).toHaveLength(2);
expect(displayBody[0].text).toEqual("e");
});
});
describe("A font parser", function() {
it("should parse \\mathrm, \\mathbb, \\mathit, and \\mathnormal", function() {
expect`\mathrm x`.toParse();
expect`\mathbb x`.toParse();
expect`\mathit x`.toParse();
expect`\mathnormal x`.toParse();
expect`\mathrm {x + 1}`.toParse();
expect`\mathbb {x + 1}`.toParse();
expect`\mathit {x + 1}`.toParse();
expect`\mathnormal {x + 1}`.toParse();
});
it("should parse \\mathcal and \\mathfrak", function() {
expect`\mathcal{ABC123}`.toParse();
expect`\mathfrak{abcABC123}`.toParse();
});
it("should produce the correct fonts", function() {
const mathbbParse = getParsed`\mathbb x`[0];
expect(mathbbParse.font).toEqual("mathbb");
expect(mathbbParse.type).toEqual("font");
const mathrmParse = getParsed`\mathrm x`[0];
expect(mathrmParse.font).toEqual("mathrm");
expect(mathrmParse.type).toEqual("font");
const mathitParse = getParsed`\mathit x`[0];
expect(mathitParse.font).toEqual("mathit");
expect(mathitParse.type).toEqual("font");
const mathnormalParse = getParsed`\mathnormal x`[0];
expect(mathnormalParse.font).toEqual("mathnormal");
expect(mathnormalParse.type).toEqual("font");
const mathcalParse = getParsed`\mathcal C`[0];
expect(mathcalParse.font).toEqual("mathcal");
expect(mathcalParse.type).toEqual("font");
const mathfrakParse = getParsed`\mathfrak C`[0];
expect(mathfrakParse.font).toEqual("mathfrak");
expect(mathfrakParse.type).toEqual("font");
});
it("should parse nested font commands", function() {
const nestedParse = getParsed`\mathbb{R \neq \mathrm{R}}`[0];
expect(nestedParse.font).toEqual("mathbb");
expect(nestedParse.type).toEqual("font");
const bbBody = nestedParse.body.body;
expect(bbBody).toHaveLength(3);
expect(bbBody[0].type).toEqual("mathord");
expect(bbBody[2].type).toEqual("font");
expect(bbBody[2].font).toEqual("mathrm");
expect(bbBody[2].type).toEqual("font");
});
it("should work with \\textcolor", function() {
const colorMathbbParse = getParsed`\textcolor{blue}{\mathbb R}`[0];
expect(colorMathbbParse.type).toEqual("color");
expect(colorMathbbParse.color).toEqual("blue");
const body = colorMathbbParse.body;
expect(body).toHaveLength(1);
expect(body[0].type).toEqual("font");
expect(body[0].font).toEqual("mathbb");
});
it("should not parse a series of font commands", function() {
expect`\mathbb \mathrm R`.not.toParse();
});
it("should nest fonts correctly", function() {
const bf = getParsed`\mathbf{a\mathrm{b}c}`[0];
expect(bf.type).toEqual("font");
expect(bf.font).toEqual("mathbf");
expect(bf.body.body).toHaveLength(3);
expect(bf.body.body[0].text).toEqual("a");
expect(bf.body.body[1].type).toEqual("font");
expect(bf.body.body[1].font).toEqual("mathrm");
expect(bf.body.body[2].text).toEqual("c");
});
it("should have the correct greediness", function() {
expect`e^\mathbf{x}`.toParse();
});
it("\\boldsymbol should inherit mbin/mrel from argument", () => {
const built = getBuilt`a\boldsymbol{}b\boldsymbol{=}c\boldsymbol{+}d\boldsymbol{++}e\boldsymbol{xyz}f`;
expect(built).toMatchSnapshot();
});
it("old-style fonts work like new-style fonts", () => {
expect`\rm xyz`.toParseLike`\mathrm{xyz}`;
expect`\sf xyz`.toParseLike`\mathsf{xyz}`;
expect`\tt xyz`.toParseLike`\mathtt{xyz}`;
expect`\bf xyz`.toParseLike`\mathbf{xyz}`;
expect`\it xyz`.toParseLike`\mathit{xyz}`;
expect`\cal xyz`.toParseLike`\mathcal{xyz}`;
});
});
describe("A \\pmb builder", function() {
it("should not fail", function() {
expect("\\pmb{\\mu}").toBuild();
expect("\\pmb{=}").toBuild();
expect("\\pmb{+}").toBuild();
expect("\\pmb{\\frac{x^2}{x_1}}").toBuild();
expect("\\pmb{}").toBuild();
expect("\\def\\x{1}\\pmb{\\x\\def\\x{2}}").toParseLike("\\pmb{1}");
});
});
describe("A comment parser", function() {
it("should parse comments at the end of a line", () => {
expect("a^2 + b^2 = c^2 % Pythagoras' Theorem\n").toParse();
});
it("should parse comments at the start of a line", () => {
expect("% comment\n").toParse();
});
it("should parse multiple lines of comments in a row", () => {
expect("% comment 1\n% comment 2\n").toParse();
});
it("should parse comments between subscript and superscript", () => {
expect("x_3 %comment\n^2").toParseLike`x_3^2`;
expect("x^ %comment\n{2}").toParseLike`x^{2}`;
expect("x^ %comment\n\\frac{1}{2}").toParseLike`x^\frac{1}{2}`;
});
it("should parse comments in size and color groups", () => {
expect("\\kern{1 %kern\nem}").toParse();
expect("\\kern1 %kern\nem").toParse();
expect("\\color{#f00%red\n}").toParse();
});
it("should parse comments before an expression", () => {
expect("%comment\n{2}").toParseLike`{2}`;
});
it("should parse comments before and between \\hline", () => {
expect("\\begin{matrix}a&b\\\\ %hline\n" +
"\\hline %hline\n" +
"\\hline c&d\\end{matrix}").toParse();
});
it("should parse comments in the macro definition", () => {
expect("\\def\\foo{1 %}\n2}\n\\foo").toParseLike`12`;
});
it("should not expand nor ignore spaces after a command sequence in a comment", () => {
expect("\\def\\foo{1\n2}\nx %\\foo\n").toParseLike`x`;
});
it("should not parse a comment without newline in strict mode", () => {
expect`x%y`.not.toParse(strictSettings);
expect`x%y`.toParse(nonstrictSettings);
});
it("should not produce or consume space", () => {
expect("\\text{hello% comment 1\nworld}").toParseLike`\text{helloworld}`;
expect("\\text{hello% comment\n\nworld}").toParseLike`\text{hello world}`;
});
it("should not include comments in the output", () => {
expect("5 % comment\n").toParseLike`5`;
});
});
describe("An HTML font tree-builder", function() {
it("should render \\mathbb{R} with the correct font", function() {
const markup = katex.renderToString(r`\mathbb{R}`);
expect(markup).toContain("<span class=\"mord mathbb\">R</span>");
});
it("should render \\mathrm{R} with the correct font", function() {
const markup = katex.renderToString(r`\mathrm{R}`);
expect(markup).toContain("<span class=\"mord mathrm\">R</span>");
});
it("should render \\mathcal{R} with the correct font", function() {
const markup = katex.renderToString(r`\mathcal{R}`);
expect(markup).toContain("<span class=\"mord mathcal\">R</span>");
});
it("should render \\mathfrak{R} with the correct font", function() {
const markup = katex.renderToString(r`\mathfrak{R}`);
expect(markup).toContain("<span class=\"mord mathfrak\">R</span>");
});
it("should render \\text{R} with the correct font", function() {
const markup = katex.renderToString(r`\text{R}`);
expect(markup).toContain("<span class=\"mord\">R</span>");
});
it("should render \\textit{R} with the correct font", function() {
const markup = katex.renderToString(r`\textit{R}`);
expect(markup).toContain("<span class=\"mord textit\">R</span>");
});
it("should render \\text{\\textit{R}} with the correct font", function() {
const markup = katex.renderToString(r`\text{\textit{R}}`);
expect(markup).toContain("<span class=\"mord textit\">R</span>");
});
it("should render \\textup{R} with the correct font", function() {
const markup1 = katex.renderToString(r`\textup{R}`);
expect(markup1).toContain("<span class=\"mord textup\">R</span>");
const markup2 = katex.renderToString(r`\textit{\textup{R}}`);
expect(markup2).toContain("<span class=\"mord textup\">R</span>");
const markup3 = katex.renderToString(r`\textup{\textit{R}}`);
expect(markup3).toContain("<span class=\"mord textit\">R</span>");
});
it("should render \\text{R\\textit{S}T} with the correct fonts", function() {
const markup = katex.renderToString(r`\text{R\textit{S}T}`);
expect(markup).toContain("<span class=\"mord\">R</span>");
expect(markup).toContain("<span class=\"mord textit\">S</span>");
expect(markup).toContain("<span class=\"mord\">T</span>");
});
it("should render \\textbf{R} with the correct font", function() {
const markup = katex.renderToString(r`\textbf{R}`);
expect(markup).toContain("<span class=\"mord textbf\">R</span>");
});
it("should render \\textmd{R} with the correct font", function() {
const markup1 = katex.renderToString(r`\textmd{R}`);
expect(markup1).toContain("<span class=\"mord textmd\">R</span>");
const markup2 = katex.renderToString(r`\textbf{\textmd{R}}`);
expect(markup2).toContain("<span class=\"mord textmd\">R</span>");
const markup3 = katex.renderToString(r`\textmd{\textbf{R}}`);
expect(markup3).toContain("<span class=\"mord textbf\">R</span>");
});
it("should render \\textsf{R} with the correct font", function() {
const markup = katex.renderToString(r`\textsf{R}`);
expect(markup).toContain("<span class=\"mord textsf\">R</span>");
});
it("should render \\textsf{\\textit{R}G\\textbf{B}} with the correct font", function() {
const markup = katex.renderToString(r`\textsf{\textit{R}G\textbf{B}}`);
expect(markup).toContain("<span class=\"mord textsf textit\">R</span>");
expect(markup).toContain("<span class=\"mord textsf\">G</span>");
expect(markup).toContain("<span class=\"mord textsf textbf\">B</span>");
});
it("should render \\textsf{\\textbf{$\\mathrm{A}$}} with the correct font", function() {
const markup = katex.renderToString(r`\textsf{\textbf{$\mathrm{A}$}}`);
expect(markup).toContain("<span class=\"mord mathrm\">A</span>");
});
it("should render \\textsf{\\textbf{$\\mathrm{\\textsf{A}}$}} with the correct font", function() {
const markup = katex.renderToString(r`\textsf{\textbf{$\mathrm{\textsf{A}}$}}`);
expect(markup).toContain("<span class=\"mord textsf textbf\">A</span>");
});
it("should render \\texttt{R} with the correct font", function() {
const markup = katex.renderToString(r`\texttt{R}`);
expect(markup).toContain("<span class=\"mord texttt\">R</span>");
});
it("should render a combination of font and color changes", function() {
let markup = katex.renderToString(r`\textcolor{blue}{\mathbb R}`);
let span = "<span class=\"mord mathbb\" style=\"color:blue;\">R</span>";
expect(markup).toContain(span);
markup = katex.renderToString(r`\mathbb{\textcolor{blue}{R}}`);
span = "<span class=\"mord mathbb\" style=\"color:blue;\">R</span>";
expect(markup).toContain(span);
});
it("should render wide characters with mord and with the correct font", function() {
const markup = katex.renderToString(String.fromCharCode(0xD835, 0xDC00));
expect(markup).toContain("<span class=\"mord mathbf\">A</span>");
expect(String.fromCharCode(0xD835, 0xDC00) +
" = " + String.fromCharCode(0xD835, 0xDC1A))
.toBuildLike`\mathbf A = \mathbf a`;
});
it("should throw TypeError when the expression is of the wrong type", function() {
expect(function() {
katex.renderToString({badInputType: "yes"});
}).toThrowError(TypeError);
expect(function() {
katex.renderToString([1, 2]);
}).toThrowError(TypeError);
expect(function() {
katex.renderToString(undefined);
}).toThrowError(TypeError);
expect(function() {
katex.renderToString(null);
}).toThrowError(TypeError);
expect(function() {
katex.renderToString(1.234);
}).toThrowError(TypeError);
});
it("should not throw TypeError when the expression is a supported type", function() {
expect(function() {
katex.renderToString(r`\sqrt{123}`);
}).not.toThrowError(TypeError);
expect(function() {
katex.renderToString(new String(r`\sqrt{123}`));
}).not.toThrowError(TypeError);
});
});
describe("A MathML font tree-builder", function() {
const contents = r`Ax2k\omega\Omega\imath+`;
it("should render " + contents + " with the correct mathvariants", function() {
const tree = getParsed(contents);
const markup = buildMathML(tree, contents, defaultOptions).toMarkup();
expect(markup).toContain("<mi>A</mi>");
expect(markup).toContain("<mi>x</mi>");
expect(markup).toContain("<mn>2</mn>");
expect(markup).toContain("<mi>\u03c9</mi>"); // \omega
expect(markup).toContain("<mi mathvariant=\"normal\">\u03A9</mi>"); // \Omega
expect(markup).toContain("<mi>\u0131</mi>"); // \imath
expect(markup).toContain("<mo>+</mo>");
});
it("should render \\mathbb{" + contents + "} with the correct mathvariants", function() {
const tex = `\\mathbb{${contents}}`;
const tree = getParsed(tex);
const markup = buildMathML(tree, tex, defaultOptions).toMarkup();
expect(markup).toContain("<mi mathvariant=\"double-struck\">A</mi>");
expect(markup).toContain("<mi mathvariant=\"double-struck\">x</mi>");
expect(markup).toContain("<mn mathvariant=\"double-struck\">2</mn>");
expect(markup).toContain("<mi mathvariant=\"double-struck\">\u03c9</mi>"); // \omega
expect(markup).toContain("<mi mathvariant=\"double-struck\">\u03A9</mi>"); // \Omega
expect(markup).toContain("<mi mathvariant=\"double-struck\">\u0131</mi>"); // \imath
expect(markup).toContain("<mo>+</mo>");
});
it("should render \\mathrm{" + contents + "} with the correct mathvariants", function() {
const tex = `\\mathrm{${contents}}`;
const tree = getParsed(tex);
const markup = buildMathML(tree, tex, defaultOptions).toMarkup();
expect(markup).toContain("<mi mathvariant=\"normal\">A</mi>");
expect(markup).toContain("<mi mathvariant=\"normal\">x</mi>");
expect(markup).toContain("<mn>2</mn>");
expect(markup).toContain("<mi>\u03c9</mi>"); // \omega
expect(markup).toContain("<mi mathvariant=\"normal\">\u03A9</mi>"); // \Omega
expect(markup).toContain("<mi>\u0131</mi>"); // \imath
expect(markup).toContain("<mo>+</mo>");
});
it("should render \\mathit{" + contents + "} with the correct mathvariants", function() {
const tex = `\\mathit{${contents}}`;
const tree = getParsed(tex);
const markup = buildMathML(tree, tex, defaultOptions).toMarkup();
expect(markup).toContain("<mi>A</mi>");
expect(markup).toContain("<mi>x</mi>");
expect(markup).toContain("<mn mathvariant=\"italic\">2</mn>");
expect(markup).toContain("<mi>\u03c9</mi>"); // \omega
expect(markup).toContain("<mi>\u03A9</mi>"); // \Omega
expect(markup).toContain("<mi>\u0131</mi>"); // \imath
expect(markup).toContain("<mo>+</mo>");
});
it("should render \\mathnormal{" + contents + "} with the correct mathvariants", function() {
const tex = `\\mathnormal{${contents}}`;
const tree = getParsed(tex);
const markup = buildMathML(tree, tex, defaultOptions).toMarkup();
expect(markup).toContain("<mi>A</mi>");
expect(markup).toContain("<mi>x</mi>");
expect(markup).toContain("<mn>2</mn>");
expect(markup).toContain("<mi>\u03c9</mi>"); // \omega
expect(markup).toContain("<mi mathvariant=\"normal\">\u03A9</mi>"); // \Omega
expect(markup).toContain("<mi>\u0131</mi>"); // \imath
expect(markup).toContain("<mo>+</mo>");
});
it("should render \\mathbf{" + contents + "} with the correct mathvariants", function() {
const tex = `\\mathbf{${contents}}`;
const tree = getParsed(tex);
const markup = buildMathML(tree, tex, defaultOptions).toMarkup();
expect(markup).toContain("<mi mathvariant=\"bold\">A</mi>");
expect(markup).toContain("<mi mathvariant=\"bold\">x</mi>");
expect(markup).toContain("<mn mathvariant=\"bold\">2</mn>");
expect(markup).toContain("<mi mathvariant=\"bold\">\u03c9</mi>"); // \omega
expect(markup).toContain("<mi mathvariant=\"bold\">\u03A9</mi>"); // \Omega
expect(markup).toContain("<mi mathvariant=\"bold\">\u0131</mi>"); // \imath
expect(markup).toContain("<mo>+</mo>");
});
it("should render \\mathcal{" + contents + "} with the correct mathvariants", function() {
const tex = `\\mathcal{${contents}}`;
const tree = getParsed(tex);
const markup = buildMathML(tree, tex, defaultOptions).toMarkup();
expect(markup).toContain("<mi mathvariant=\"script\">A</mi>");
expect(markup).toContain("<mi mathvariant=\"script\">x</mi>");
expect(markup).toContain("<mn mathvariant=\"script\">2</mn>");
expect(markup).toContain("<mi mathvariant=\"script\">\u03c9</mi>"); // \omega
expect(markup).toContain("<mi mathvariant=\"script\">\u03A9</mi>"); // \Omega
expect(markup).toContain("<mi mathvariant=\"script\">\u0131</mi>"); // \imath
expect(markup).toContain("<mo>+</mo>");
});
it("should render \\mathfrak{" + contents + "} with the correct mathvariants", function() {
const tex = `\\mathfrak{${contents}}`;
const tree = getParsed(tex);
const markup = buildMathML(tree, tex, defaultOptions).toMarkup();
expect(markup).toContain("<mi mathvariant=\"fraktur\">A</mi>");
expect(markup).toContain("<mi mathvariant=\"fraktur\">x</mi>");
expect(markup).toContain("<mn mathvariant=\"fraktur\">2</mn>");
expect(markup).toContain("<mi mathvariant=\"fraktur\">\u03c9</mi>"); // \omega
expect(markup).toContain("<mi mathvariant=\"fraktur\">\u03A9</mi>"); // \Omega
expect(markup).toContain("<mi mathvariant=\"fraktur\">\u0131</mi>"); // \imath
expect(markup).toContain("<mo>+</mo>");
});
it("should render \\mathscr{" + contents + "} with the correct mathvariants", function() {
const tex = `\\mathscr{${contents}}`;
const tree = getParsed(tex);
const markup = buildMathML(tree, tex, defaultOptions).toMarkup();
expect(markup).toContain("<mi mathvariant=\"script\">A</mi>");
expect(markup).toContain("<mi mathvariant=\"script\">x</mi>");
expect(markup).toContain("<mn mathvariant=\"script\">2</mn>");
expect(markup).toContain("<mi mathvariant=\"script\">\u03c9</mi>"); // \omega
expect(markup).toContain("<mi mathvariant=\"script\">\u03A9</mi>"); // \Omega
expect(markup).toContain("<mi mathvariant=\"script\">\u0131</mi>"); // \imath
expect(markup).toContain("<mo>+</mo>");
});
it("should render \\mathsf{" + contents + "} with the correct mathvariants", function() {
const tex = `\\mathsf{${contents}}`;
const tree = getParsed(tex);
const markup = buildMathML(tree, tex, defaultOptions).toMarkup();
expect(markup).toContain("<mi mathvariant=\"sans-serif\">A</mi>");
expect(markup).toContain("<mi mathvariant=\"sans-serif\">x</mi>");
expect(markup).toContain("<mn mathvariant=\"sans-serif\">2</mn>");
expect(markup).toContain("<mi mathvariant=\"sans-serif\">\u03c9</mi>"); // \omega
expect(markup).toContain("<mi mathvariant=\"sans-serif\">\u03A9</mi>"); // \Omega
expect(markup).toContain("<mi mathvariant=\"sans-serif\">\u0131</mi>"); // \imath
expect(markup).toContain("<mo>+</mo>");
});
it("should render a combination of font and color changes", function() {
let tex = r`\textcolor{blue}{\mathbb R}`;
let tree = getParsed(tex);
let markup = buildMathML(tree, tex, defaultOptions).toMarkup();
let node = "<mstyle mathcolor=\"blue\">" +
"<mi mathvariant=\"double-struck\">R</mi>" +
"</mstyle>";
expect(markup).toContain(node);
// reverse the order of the commands
tex = r`\mathbb{\textcolor{blue}{R}}`;
tree = getParsed(tex);
markup = buildMathML(tree, tex, defaultOptions).toMarkup();
node = "<mstyle mathcolor=\"blue\">" +
"<mi mathvariant=\"double-struck\">R</mi>" +
"</mstyle>";
expect(markup).toContain(node);
});
it("should render text as <mtext>", function() {
const tex = r`\text{for }`;
const tree = getParsed(tex);
const markup = buildMathML(tree, tex, defaultOptions).toMarkup();
expect(markup).toContain("<mtext>for\u00a0</mtext>");
});
it("should render math within text as side-by-side children", function() {
const tex = r`\text{graph: $y = mx + b$}`;
const tree = getParsed(tex);
const markup = buildMathML(tree, tex, defaultOptions).toMarkup();
expect(markup).toContain("<mrow><mtext>graph:\u00a0</mtext>");
expect(markup).toContain(
"<mi>y</mi><mo>=</mo><mi>m</mi><mi>x</mi><mo>+</mo><mi>b</mi>");
});
});
describe("An includegraphics builder", function() {
const img = "\\includegraphics[height=0.9em, totalheight=0.9em, width=0.9em, alt=KA logo]{https://cdn.kastatic.org/images/apple-touch-icon-57x57-precomposed.new.png}";
it("should not fail", function() {
expect(img).toBuild(trustSettings);
});
it("should produce mords", function() {
expect(getBuilt(img, trustSettings)[0].classes).toContain("mord");
});
it("should not render without trust setting", function() {
const built = getBuilt(img);
expect(built).toMatchSnapshot();
});
it("should render with trust setting", function() {
const built = getBuilt(img, trustSettings);
expect(built).toMatchSnapshot();
});
});
describe("An HTML extension builder", function() {
const html =
"\\htmlId{bar}{x}\\htmlClass{foo}{x}\\htmlStyle{color: red;}{x}\\htmlData{foo=a, bar=b}{x}";
const trustNonStrictSettings = new Settings({trust: true, strict: false});
it("should not fail", function() {
expect(html).toBuild(trustNonStrictSettings);
});
it("should set HTML attributes", function() {
const built = getBuilt(html, trustNonStrictSettings);
expect(built[0].attributes.id).toMatch("bar");
expect(built[1].classes).toContain("foo");
expect(built[2].attributes.style).toMatch("color: red");
expect(built[3].attributes).toEqual({
"data-bar": "b",
"data-foo": "a",
});
});
it("should not affect spacing", function() {
const built = getBuilt("\\htmlId{a}{x+}y", trustNonStrictSettings);
expect(built).toMatchSnapshot();
});
it("should render with trust and strict setting", function() {
const built = getBuilt(html, trustNonStrictSettings);
expect(built).toMatchSnapshot();
});
});
describe("A bin builder", function() {
it("should create mbins normally", function() {
const built = getBuilt`x + y`;
// we add glue elements around the '+'
expect(built[2].classes).toContain("mbin");
});
it("should create ords when at the beginning of lists", function() {
const 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`[4].classes).toContain("mord");
expect(getBuilt`( + 2`[2].classes).toContain("mord");
expect(getBuilt`= + 2`[2].classes).toContain("mord");
expect(getBuilt`\sin + 2`[2].classes).toContain("mord");
expect(getBuilt`, + 2`[2].classes).toContain("mord");
});
it("should correctly interact with color objects", function() {
expect(getBuilt`\blue{x}+y`[2].classes).toContain("mbin");
expect(getBuilt`\blue{x+}+y`[2].classes).toContain("mbin");
expect(getBuilt`\blue{x+}+y`[4].classes).toContain("mord");
});
});
describe("A \\phantom builder and \\smash builder", function() {
it("should both build a mord", function() {
expect(getBuilt`\hphantom{a}`[0].classes).toContain("mord");
expect(getBuilt`a\hphantom{=}b`[2].classes).toContain("mord");
expect(getBuilt`a\hphantom{+}b`[2].classes).toContain("mord");
expect(getBuilt`\smash{a}`[0].classes).toContain("mord");
expect(getBuilt`\smash{=}`[0].classes).toContain("mord");
expect(getBuilt`a\smash{+}b`[2].classes).toContain("mord");
});
});
describe("A markup generator", function() {
it("marks trees up", function() {
// Just a few quick sanity checks here...
const markup = katex.renderToString(r`\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() {
const markup = katex.renderToString("a");
expect(markup).toContain("<span");
expect(markup).toContain("<math");
});
});
describe("A parse tree generator", function() {
it("generates a tree", function() {
const tree = stripPositions(getParsed`\sigma^2`);
expect(tree).toMatchSnapshot();
});
});
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() {
const parse = getParsed`\vec x`[0];
expect(parse.type).toEqual("accent");
});
it("should be grouped more tightly than supsubs", function() {
const parse = getParsed`\vec x^2`[0];
expect(parse.type).toEqual("supsub");
});
it("should parse stretchy, shifty accents", function() {
expect`\widehat{x}`.toParse();
expect`\widecheck{x}`.toParse();
});
it("should parse stretchy, non-shifty accents", function() {
expect`\overrightarrow{x}`.toParse();
});
});
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 stretchy and shifty accent builder", function() {
it("should not fail", function() {
expect`\widehat{AB}`.toBuild();
expect`\widecheck{AB}`.toBuild();
expect`\widehat{AB}^2`.toBuild();
expect`\widehat{AB}_2`.toBuild();
expect`\widehat{AB}_2^2`.toBuild();
});
it("should produce mords", function() {
expect(getBuilt`\widehat{AB}`[0].classes).toContain("mord");
expect(getBuilt`\widehat +`[0].classes).toContain("mord");
expect(getBuilt`\widehat +`[0].classes).not.toContain("mbin");
expect(getBuilt`\widehat )^2`[0].classes).toContain("mord");
expect(getBuilt`\widehat )^2`[0].classes).not.toContain("mclose");
});
});
describe("A stretchy and non-shifty accent builder", function() {
it("should not fail", function() {
expect`\overrightarrow{AB}`.toBuild();
expect`\overrightarrow{AB}^2`.toBuild();
expect`\overrightarrow{AB}_2`.toBuild();
expect`\overrightarrow{AB}_2^2`.toBuild();
});
it("should produce mords", function() {
expect(getBuilt`\overrightarrow{AB}`[0].classes).toContain("mord");
expect(getBuilt`\overrightarrow +`[0].classes).toContain("mord");
expect(getBuilt`\overrightarrow +`[0].classes).not.toContain("mbin");
expect(getBuilt`\overrightarrow )^2`[0].classes).toContain("mord");
expect(getBuilt`\overrightarrow )^2`[0].classes).not.toContain("mclose");
});
});
describe("An under-accent parser", function() {
it("should not fail", function() {
expect("\\underrightarrow{x}").toParse();
expect("\\underrightarrow{x^2}").toParse();
expect("\\underrightarrow{x}^2").toParse();
expect("\\underrightarrow x").toParse();
});
it("should produce accentUnder", function() {
const parse = getParsed("\\underrightarrow x")[0];
expect(parse.type).toEqual("accentUnder");
});
it("should be grouped more tightly than supsubs", function() {
const parse = getParsed("\\underrightarrow x^2")[0];
expect(parse.type).toEqual("supsub");
});
});
describe("An under-accent builder", function() {
it("should not fail", function() {
expect("\\underrightarrow{x}").toBuild();
expect("\\underrightarrow{x}^2").toBuild();
expect("\\underrightarrow{x}_2").toBuild();
expect("\\underrightarrow{x}_2^2").toBuild();
});
it("should produce mords", function() {
expect(getBuilt("\\underrightarrow x")[0].classes).toContain("mord");
expect(getBuilt("\\underrightarrow +")[0].classes).toContain("mord");
expect(getBuilt("\\underrightarrow +")[0].classes).not.toContain("mbin");
expect(getBuilt("\\underrightarrow )^2")[0].classes).toContain("mord");
expect(getBuilt("\\underrightarrow )^2")[0].classes).not.toContain("mclose");
});
});
describe("An extensible arrow parser", function() {
it("should not fail", function() {
expect("\\xrightarrow{x}").toParse();
expect("\\xrightarrow{x^2}").toParse();
expect("\\xrightarrow{x}^2").toParse();
expect("\\xrightarrow x").toParse();
expect("\\xrightarrow[under]{over}").toParse();
});
it("should produce xArrow", function() {
const parse = getParsed("\\xrightarrow x")[0];
expect(parse.type).toEqual("xArrow");
});
it("should be grouped more tightly than supsubs", function() {
const parse = getParsed("\\xrightarrow x^2")[0];
expect(parse.type).toEqual("supsub");
});
});
describe("An extensible arrow builder", function() {
it("should not fail", function() {
expect("\\xrightarrow{x}").toBuild();
expect("\\xrightarrow{x}^2").toBuild();
expect("\\xrightarrow{x}_2").toBuild();
expect("\\xrightarrow{x}_2^2").toBuild();
expect("\\xrightarrow[under]{over}").toBuild();
});
it("should produce mrell", function() {
expect(getBuilt("\\xrightarrow x")[0].classes).toContain("mrel");
expect(getBuilt("\\xrightarrow [under]{over}")[0].classes).toContain("mrel");
expect(getBuilt("\\xrightarrow +")[0].classes).toContain("mrel");
expect(getBuilt("\\xrightarrow +")[0].classes).not.toContain("mbin");
expect(getBuilt("\\xrightarrow )^2")[0].classes).toContain("mrel");
expect(getBuilt("\\xrightarrow )^2")[0].classes).not.toContain("mclose");
});
});
describe("A horizontal brace parser", function() {
it("should not fail", function() {
expect`\overbrace{x}`.toParse();
expect`\overbrace{x^2}`.toParse();
expect`\overbrace{x}^2`.toParse();
expect`\overbrace x`.toParse();
expect("\\underbrace{x}_2").toParse();
expect("\\underbrace{x}_2^2").toParse();
});
it("should produce horizBrace", function() {
const parse = getParsed`\overbrace x`[0];
expect(parse.type).toEqual("horizBrace");
});
it("should be grouped more tightly than supsubs", function() {
const parse = getParsed`\overbrace x^2`[0];
expect(parse.type).toEqual("supsub");
});
});
describe("A horizontal brace builder", function() {
it("should not fail", function() {
expect`\overbrace{x}`.toBuild();
expect`\overbrace{x}^2`.toBuild();
expect("\\underbrace{x}_2").toBuild();
expect("\\underbrace{x}_2^2").toBuild();
});
it("should produce mords", function() {
expect(getBuilt`\overbrace x`[0].classes).toContain("mord");
expect(getBuilt`\overbrace{x}^2`[0].classes).toContain("mord");
expect(getBuilt`\overbrace +`[0].classes).toContain("mord");
expect(getBuilt`\overbrace +`[0].classes).not.toContain("mbin");
expect(getBuilt`\overbrace )^2`[0].classes).toContain("mord");
expect(getBuilt`\overbrace )^2`[0].classes).not.toContain("mclose");
});
});
describe("A boxed parser", function() {
it("should not fail", function() {
expect`\boxed{x}`.toParse();
expect`\boxed{x^2}`.toParse();
expect`\boxed{x}^2`.toParse();
expect`\boxed x`.toParse();
});
it("should produce enclose", function() {
const parse = getParsed`\boxed x`[0];
expect(parse.type).toEqual("enclose");
});
});
describe("A boxed builder", function() {
it("should not fail", function() {
expect`\boxed{x}`.toBuild();
expect`\boxed{x}^2`.toBuild();
expect`\boxed{x}_2`.toBuild();
expect`\boxed{x}_2^2`.toBuild();
});
it("should produce mords", function() {
expect(getBuilt`\boxed x`[0].classes).toContain("mord");
expect(getBuilt`\boxed +`[0].classes).toContain("mord");
expect(getBuilt`\boxed +`[0].classes).not.toContain("mbin");
expect(getBuilt`\boxed )^2`[0].classes).toContain("mord");
expect(getBuilt`\boxed )^2`[0].classes).not.toContain("mclose");
});
});
describe("An fbox parser, unlike a boxed parser,", function() {
it("should fail when given math", function() {
expect`\fbox{\frac a b}`.not.toParse();
});
});
describe("A colorbox parser", function() {
it("should not fail, given a text argument", function() {
expect`\colorbox{red}{a b}`.toParse();
expect`\colorbox{red}{x}^2`.toParse();
expect`\colorbox{red} x`.toParse();
});
it("should fail, given a math argument", function() {
expect`\colorbox{red}{\alpha}`.not.toParse();
expect`\colorbox{red}{\frac{a}{b}}`.not.toParse();
});
it("should parse a color", function() {
expect`\colorbox{red}{a b}`.toParse();
expect`\colorbox{#197}{a b}`.toParse();
expect`\colorbox{#1a9b7c}{a b}`.toParse();
});
it("should produce enclose", function() {
const parse = getParsed`\colorbox{red} x`[0];
expect(parse.type).toEqual("enclose");
});
});
describe("A colorbox builder", function() {
it("should not fail", function() {
expect`\colorbox{red}{a b}`.toBuild();
expect`\colorbox{red}{a b}^2`.toBuild();
expect`\colorbox{red} x`.toBuild();
});
it("should produce mords", function() {
expect(getBuilt`\colorbox{red}{a b}`[0].classes).toContain("mord");
});
});
describe("An fcolorbox parser", function() {
it("should not fail, given a text argument", function() {
expect`\fcolorbox{blue}{yellow}{a b}`.toParse();
expect`\fcolorbox{blue}{yellow}{x}^2`.toParse();
expect`\fcolorbox{blue}{yellow} x`.toParse();
});
it("should fail, given a math argument", function() {
expect`\fcolorbox{blue}{yellow}{\alpha}`.not.toParse();
expect`\fcolorbox{blue}{yellow}{\frac{a}{b}}`.not.toParse();
});
it("should parse a color", function() {
expect`\fcolorbox{blue}{yellow}{a b}`.toParse();
expect`\fcolorbox{blue}{#197}{a b}`.toParse();
expect`\fcolorbox{blue}{#1a9b7c}{a b}`.toParse();
});
it("should produce enclose", function() {
const parse = getParsed`\fcolorbox{blue}{yellow} x`[0];
expect(parse.type).toEqual("enclose");
});
});
describe("A fcolorbox builder", function() {
it("should not fail", function() {
expect`\fcolorbox{blue}{yellow}{a b}`.toBuild();
expect`\fcolorbox{blue}{yellow}{a b}^2`.toBuild();
expect`\fcolorbox{blue}{yellow} x`.toBuild();
});
it("should produce mords", function() {
expect(getBuilt`\colorbox{red}{a b}`[0].classes).toContain("mord");
});
});
describe("A strike-through parser", function() {
it("should not fail", function() {
expect`\cancel{x}`.toParse();
expect`\cancel{x^2}`.toParse();
expect`\cancel{x}^2`.toParse();
expect`\cancel x`.toParse();
});
it("should produce enclose", function() {
const parse = getParsed`\cancel x`[0];
expect(parse.type).toEqual("enclose");
});
it("should be grouped more tightly than supsubs", function() {
const parse = getParsed`\cancel x^2`[0];
expect(parse.type).toEqual("supsub");
});
});
describe("A strike-through builder", function() {
it("should not fail", function() {
expect`\cancel{x}`.toBuild();
expect`\cancel{x}^2`.toBuild();
expect`\cancel{x}_2`.toBuild();
expect`\cancel{x}_2^2`.toBuild();
expect`\sout{x}`.toBuild();
expect`\sout{x}^2`.toBuild();
expect`\sout{x}_2`.toBuild();
expect`\sout{x}_2^2`.toBuild();
});
it("should produce mords", function() {
expect(getBuilt`\cancel x`[0].classes).toContain("mord");
expect(getBuilt`\cancel +`[0].classes).toContain("mord");
expect(getBuilt`\cancel +`[0].classes).not.toContain("mbin");
expect(getBuilt`\cancel )^2`[0].classes).toContain("mord");
expect(getBuilt`\cancel )^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();
expect`\hphantom{x}`.toParse();
expect`\hphantom{x^2}`.toParse();
expect`\hphantom{x}^2`.toParse();
expect`\hphantom x`.toParse();
});
it("should build a phantom node", function() {
const parse = getParsed`\phantom{x}`[0];
expect(parse.type).toEqual("phantom");
expect(parse.body).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();
expect`\hphantom{x}`.toBuild();
expect`\hphantom{x^2}`.toBuild();
expect`\hphantom{x}^2`.toBuild();
expect`\hphantom x`.toBuild();
});
it("should make the children transparent", function() {
const children = getBuilt`\phantom{x+1}`;
expect(children[0].style.color).toBe("transparent");
expect(children[2].style.color).toBe("transparent");
expect(children[4].style.color).toBe("transparent");
});
it("should make all descendants transparent", function() {
const children = getBuilt`\phantom{x+\blue{1}}`;
expect(children[0].style.color).toBe("transparent");
expect(children[2].style.color).toBe("transparent");
expect(children[4].style.color).toBe("transparent");
});
});
describe("A smash parser", function() {
it("should not fail", function() {
expect`\smash{x}`.toParse();
expect`\smash{x^2}`.toParse();
expect`\smash{x}^2`.toParse();
expect`\smash x`.toParse();
expect`\smash[b]{x}`.toParse();
expect`\smash[b]{x^2}`.toParse();
expect`\smash[b]{x}^2`.toParse();
expect`\smash[b] x`.toParse();
expect`\smash[]{x}`.toParse();
expect`\smash[]{x^2}`.toParse();
expect`\smash[]{x}^2`.toParse();
expect`\smash[] x`.toParse();
});
it("should build a smash node", function() {
const parse = getParsed`\smash{x}`[0];
expect(parse.type).toEqual("smash");
});
});
describe("A smash builder", function() {
it("should not fail", function() {
expect`\smash{x}`.toBuild(nonstrictSettings);
expect`\smash{x^2}`.toBuild(nonstrictSettings);
expect`\smash{x}^2`.toBuild(nonstrictSettings);
expect`\smash x`.toBuild(nonstrictSettings);
expect`\smash[b]{x}`.toBuild(nonstrictSettings);
expect`\smash[b]{x^2}`.toBuild(nonstrictSettings);
expect`\smash[b]{x}^2`.toBuild(nonstrictSettings);
expect`\smash[b] x`.toBuild(nonstrictSettings);
});
});
describe("A document fragment", function() {
it("should have paddings applied inside an extensible arrow", function() {
const markup = katex.renderToString("\\tiny\\xrightarrow\\textcolor{red}{x}");
expect(markup).toContain("x-arrow-pad");
});
it("should have paddings applied inside an enclose", function() {
const markup = katex.renderToString(r`\fbox\textcolor{red}{x}`);
expect(markup).toContain("boxpad");
});
it("should have paddings applied inside a square root", function() {
const markup = katex.renderToString(r`\sqrt\textcolor{red}{x}`);
expect(markup).toContain("padding-left");
});
});
describe("A parser error", function() {
it("should report the position of an error", function() {
try {
parseTree(r`\sqrt}`, new Settings());
} 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}`.not.toParse();
});
it("should not work if the optional argument isn't closed", function() {
expect`\sqrt[`.not.toParse();
});
});
describe("An array environment", function() {
it("should accept a single alignment character", function() {
const parse = getParsed`\begin{array}r1\\20\end{array}`;
expect(parse[0].type).toBe("array");
expect(parse[0].cols).toEqual([
{type: "align", align: "r"},
]);
});
it("should accept vertical separators", function() {
const parse = getParsed`\begin{array}{|l||c:r::}\end{array}`;
expect(parse[0].type).toBe("array");
expect(parse[0].cols).toEqual([
{type: "separator", separator: "|"},
{type: "align", align: "l"},
{type: "separator", separator: "|"},
{type: "separator", separator: "|"},
{type: "align", align: "c"},
{type: "separator", separator: ":"},
{type: "align", align: "r"},
{type: "separator", separator: ":"},
{type: "separator", separator: ":"},
]);
});
});
describe("A subarray environment", function() {
it("should accept only a single alignment character", function() {
const parse = getParsed`\begin{subarray}{c}a \\ b\end{subarray}`;
expect(parse[0].type).toBe("array");
expect(parse[0].cols).toEqual([
{type: "align", align: "c"},
]);
expect`\begin{subarray}{cc}a \\ b\end{subarray}`.not.toParse();
expect`\begin{subarray}{c}a & b \\ c & d\end{subarray}`.not.toParse();
expect`\begin{subarray}{c}a \\ b\end{subarray}`.toBuild();
});
});
describe("A substack function", function() {
it("should build", function() {
expect`\sum_{\substack{ 0<i<m \\ 0<j<n }} P(i,j)`.toBuild();
});
it("should accommodate spaces in the argument", function() {
expect`\sum_{\substack{ 0<i<m \\ 0<j<n }} P(i,j)`.toBuild();
});
it("should accommodate macros in the argument", function() {
expect`\sum_{\substack{ 0<i<\varPi \\ 0<j<\pi }} P(i,j)`.toBuild();
});
});
describe("A smallmatrix environment", function() {
it("should build", function() {
expect`\begin{smallmatrix} a & b \\ c & d \end{smallmatrix}`.toBuild();
});
});
describe("A cases environment", function() {
it("should parse its input", function() {
expect`f(a,b)=\begin{cases}a+1&\text{if }b\text{ is odd}\\a&\text{if }b=0\\a-1&\text{otherwise}\end{cases}`
.toParse();
});
});
describe("An rcases environment", function() {
it("should build", function() {
expect`\begin{rcases} a &\text{if } b \\ c &\text{if } d \end{rcases}⇒…`
.toBuild();
});
});
describe("An aligned environment", function() {
it("should parse its input", function() {
expect`\begin{aligned}a&=b&c&=d\\e&=f\end{aligned}`.toParse();
});
it("should allow cells in brackets", function() {
expect`\begin{aligned}[a]&[b]\\ [c]&[d]\end{aligned}`.toParse();
});
it("should forbid cells in brackets without space", function() {
expect`\begin{aligned}[a]&[b]\\[c]&[d]\end{aligned}`.not.toParse();
});
it("should not eat the last row when its first cell is empty", function() {
const ae = getParsed`\begin{aligned}&E_1 & (1)\\&E_2 & (2)\\&E_3 & (3)\end{aligned}`[0];
expect(ae.body).toHaveLength(3);
});
});
describe("operatorname support", function() {
it("should not fail", function() {
expect("\\operatorname{x*Π∑\\Pi\\sum\\frac a b}").toBuild();
expect("\\operatorname*{x*Π∑\\Pi\\sum\\frac a b}").toBuild();
expect("\\operatorname*{x*Π∑\\Pi\\sum\\frac a b}_y x").toBuild();
expect("\\operatorname*{x*Π∑\\Pi\\sum\\frac a b}\\limits_y x").toBuild();
});
});
describe("href and url commands", function() {
// We can't use raw strings for \url because \u is for Unicode escapes.
it("should parse its input", function() {
expect`\href{http://example.com/}{\sin}`.toBuild(trustSettings);
expect("\\url{http://example.com/}").toBuild(trustSettings);
});
it("should allow empty URLs", function() {
expect`\href{}{example here}`.toBuild(trustSettings);
expect("\\url{}").toBuild(trustSettings);
});
it("should allow single-character URLs", () => {
expect`\href%end`.toParseLike("\\href{%}end", trustSettings);
expect("\\url%end").toParseLike("\\url{%}end", trustSettings);
expect("\\url%%end\n").toParseLike("\\url{%}", trustSettings);
expect("\\url end").toParseLike("\\url{e}nd", trustSettings);
expect("\\url%end").toParseLike("\\url {%}end", trustSettings);
});
it("should allow spaces single-character URLs", () => {
expect`\href %end`.toParseLike("\\href{%}end", trustSettings);
expect("\\url %end").toParseLike("\\url{%}end", trustSettings);
});
it("should allow letters [#$%&~_^] without escaping", function() {
const url = "http://example.org/~bar/#top?foo=$foo&bar=ba^r_boo%20baz";
const parsed1 = getParsed(`\\href{${url}}{\\alpha}`, trustSettings)[0];
expect(parsed1.href).toBe(url);
const parsed2 = getParsed(`\\url{${url}}`, trustSettings)[0];
expect(parsed2.href).toBe(url);
});
it("should allow balanced braces in url", function() {
const url = "http://example.org/{{}t{oo}}";
const parsed1 = getParsed(`\\href{${url}}{\\alpha}`, trustSettings)[0];
expect(parsed1.href).toBe(url);
const parsed2 = getParsed(`\\url{${url}}`, trustSettings)[0];
expect(parsed2.href).toBe(url);
});
it("should not allow unbalanced brace(s) in url", function() {
expect`\href{http://example.com/{a}{bar}`.not.toParse();
expect`\href{http://example.com/}a}{bar}`.not.toParse();
expect`\\url{http://example.com/{a}`.not.toParse();
expect`\\url{http://example.com/}a}`.not.toParse();
});
it("should allow escape for letters [#$%&~_^{}]", function() {
const url = "http://example.org/~bar/#top?foo=$}foo{&bar=bar^r_boo%20baz";
const input = url.replace(/([#$%&~_^{}])/g, '\\$1');
const parsed1 = getParsed(`\\href{${input}}{\\alpha}`, trustSettings)[0];
expect(parsed1.href).toBe(url);
const parsed2 = getParsed(`\\url{${input}}`, trustSettings)[0];
expect(parsed2.href).toBe(url);
});
it("should allow comments after URLs", function() {
expect("\\url{http://example.com/}%comment\n").toBuild();
});
it("should be marked up correctly", function() {
const markup = katex.renderToString(r`\href{http://example.com/}{example here}`, {trust: true});
expect(markup).toContain("<a href=\"http://example.com/\">");
});
it("should not affect spacing around", function() {
const built = getBuilt("a\\href{http://example.com/}{+b}", trustSettings);
expect(built).toMatchSnapshot();
});
it("should forbid relative URLs when trust option is false", () => {
const parsed = getParsed("\\href{relative}{foo}");
expect(parsed).toMatchSnapshot();
});
it("should allow explicitly allowed protocols", () => {
const parsed = getParsed(
"\\href{ftp://x}{foo}",
new Settings({trust: (context) => context.protocol === "ftp"}),
);
expect(parsed).toMatchSnapshot();
});
it("should allow all protocols when trust option is true", () => {
const parsed = getParsed("\\href{ftp://x}{foo}", trustSettings);
expect(parsed).toMatchSnapshot();
});
it("should not allow explicitly disallow protocols", () => {
const parsed = getParsed(
"\\href{javascript:alert('x')}{foo}",
new Settings({trust: context => context.protocol !== "javascript"}),
);
expect(parsed).toMatchSnapshot();
});
});
describe("A raw text parser", function() {
it("should not not parse a mal-formed string", function() {
// In the next line, the first character passed to \includegraphics is a
// Unicode combining character. So this is a test that the parser will catch a bad string.
expect("\\includegraphics[\u030aheight=0.8em, totalheight=0.9em, width=0.9em]{" + "https://cdn.kastatic.org/images/apple-touch-icon-57x57-precomposed.new.png}").not.toParse();
});
it("should return null for a omitted optional string", function() {
expect("\\includegraphics{https://cdn.kastatic.org/images/apple-touch-icon-57x57-precomposed.new.png}").toParse();
});
});
describe("A parser that does not throw on unsupported commands", function() {
// The parser breaks on unsupported commands unless it is explicitly
// told not to
const errorColor = "#933";
const noThrowSettings = new Settings({
throwOnError: false,
errorColor: errorColor,
});
it("should still parse on unrecognized control sequences", function() {
expect`\error`.toParse(noThrowSettings);
});
describe("should allow unrecognized controls sequences anywhere, including", function() {
it("in superscripts and subscripts", function() {
expect`2_\error`.toBuild(noThrowSettings);
expect`3^{\error}_\error`.toBuild(noThrowSettings);
expect`\int\nolimits^\error_\error`.toBuild(noThrowSettings);
});
it("in fractions", function() {
expect`\frac{345}{\error}`.toBuild(noThrowSettings);
expect`\frac\error{\error}`.toBuild(noThrowSettings);
});
it("in square roots", function() {
expect`\sqrt\error`.toBuild(noThrowSettings);
expect`\sqrt{234\error}`.toBuild(noThrowSettings);
});
it("in text boxes", function() {
expect`\text{\error}`.toBuild(noThrowSettings);
});
});
it("should produce color nodes with a color value given by errorColor", function() {
const parsedInput = getParsed(r`\error`, noThrowSettings);
expect(parsedInput[0].type).toBe("color");
expect(parsedInput[0].color).toBe(errorColor);
});
it("should build katex-error span for other type of KaTeX error", function() {
const built = getBuilt("2^2^2", noThrowSettings);
expect(built).toMatchSnapshot();
});
it("should properly escape LaTeX in errors", function() {
const html = katex.renderToString("2^&\"<>", noThrowSettings);
expect(html).toMatchSnapshot();
});
});
describe("The symbol table integrity", function() {
it("should treat certain symbols as synonyms", function() {
expect`<`.toBuildLike`\lt`;
expect`>`.toBuildLike`\gt`;
expect`\left<\frac{1}{x}\right>`.toBuildLike`\left\lt\frac{1}{x}\right\gt`;
});
});
describe("Symbols", function() {
it("should support AMS symbols in both text and math mode", function() {
// These text+math symbols are from Section 6 of
// http://mirrors.ctan.org/fonts/amsfonts/doc/amsfonts.pdf
const symbols = r`\yen\checkmark\circledR\maltese`;
expect(symbols).toBuild();
expect(`\\text{${symbols}}`).toBuild(strictSettings);
});
});
describe("A macro expander", function() {
it("should produce individual tokens", function() {
expect`e^\foo`.toParseLike("e^1 23",
new Settings({macros: {"\\foo": "123"}}));
});
it("should preserve leading spaces inside macro definition", function() {
expect`\text{\foo}`.toParseLike(r`\text{ x}`,
new Settings({macros: {"\\foo": " x"}}));
});
it("should preserve leading spaces inside macro argument", function() {
expect`\text{\foo{ x}}`.toParseLike(r`\text{ x}`,
new Settings({macros: {"\\foo": "#1"}}));
});
it("should ignore expanded spaces in math mode", function() {
expect`\foo`.toParseLike("x", new Settings({macros: {"\\foo": " x"}}));
});
it("should consume spaces after control-word macro", function() {
expect`\text{\foo }`.toParseLike(r`\text{x}`,
new Settings({macros: {"\\foo": "x"}}));
});
it("should consume spaces after macro with \\relax", function() {
expect`\text{\foo }`.toParseLike(r`\text{}`,
new Settings({macros: {"\\foo": "\\relax"}}));
});
it("should not consume spaces after control-word expansion", function() {
expect`\text{\\ }`.toParseLike(r`\text{ }`,
new Settings({macros: {"\\\\": "\\relax"}}));
});
it("should consume spaces after \\relax", function() {
expect`\text{\relax }`.toParseLike`\text{}`;
});
it("should consume spaces after control-word function", function() {
expect`\text{\KaTeX }`.toParseLike`\text{\KaTeX}`;
});
it("should preserve spaces after control-symbol macro", function() {
expect`\text{\% y}`.toParseLike(r`\text{x y}`,
new Settings({macros: {"\\%": "x"}}));
});
it("should preserve spaces after control-symbol function", function() {
expect`\text{\' }`.toParse();
});
it("should consume spaces between arguments", function() {
expect`\text{\foo 1 2}`.toParseLike(r`\text{12end}`,
new Settings({macros: {"\\foo": "#1#2end"}}));
expect`\text{\foo {1} {2}}`.toParseLike(r`\text{12end}`,
new Settings({macros: {"\\foo": "#1#2end"}}));
});
it("should allow for multiple expansion", function() {
expect`1\foo2`.toParseLike("1aa2", new Settings({macros: {
"\\foo": "\\bar\\bar",
"\\bar": "a",
}}));
});
it("should allow for multiple expansion with argument", function() {
expect`1\foo2`.toParseLike("12222", new Settings({macros: {
"\\foo": "\\bar{#1}\\bar{#1}",
"\\bar": "#1#1",
}}));
});
it("should allow for macro argument", function() {
expect`\foo\bar`.toParseLike("(x)", new Settings({macros: {
"\\foo": "(#1)",
"\\bar": "x",
}}));
});
it("should allow for space macro argument (text version)", function() {
expect`\text{\foo\bar}`.toParseLike(r`\text{( )}`, new Settings({macros: {
"\\foo": "(#1)",
"\\bar": " ",
}}));
});
it("should allow for space macro argument (math version)", function() {
expect`\foo\bar`.toParseLike("()", new Settings({macros: {
"\\foo": "(#1)",
"\\bar": " ",
}}));
});
it("should allow for space second argument (text version)", function() {
expect`\text{\foo\bar\bar}`.toParseLike(r`\text{( , )}`, new Settings({macros: {
"\\foo": "(#1,#2)",
"\\bar": " ",
}}));
});
it("should allow for space second argument (math version)", function() {
expect`\foo\bar\bar`.toParseLike("(,)", new Settings({macros: {
"\\foo": "(#1,#2)",
"\\bar": " ",
}}));
});
it("should allow for empty macro argument", function() {
expect`\foo\bar`.toParseLike("()", new Settings({macros: {
"\\foo": "(#1)",
"\\bar": "",
}}));
});
// TODO: The following is not currently possible to get working, given that
// functions and macros are dealt with separately.
/*
it("should allow for space function arguments", function() {
expect`\frac\bar\bar`.toParseLike(r`\frac{}{}`, new Settings({macros: {
"\\bar": " ",
}}));
});
*/
it("should build \\overset and \\underset", function() {
expect`\overset{f}{\rightarrow} Y`.toBuild();
expect("\\underset{f}{\\rightarrow} Y").toBuild();
});
it("should build \\iff, \\implies, \\impliedby", function() {
expect`X \iff Y`.toBuild();
expect`X \implies Y`.toBuild();
expect`X \impliedby Y`.toBuild();
});
it("should allow aliasing characters", function() {
expect`x=c`.toParseLike("x'=c", new Settings({macros: {
"": "'",
}}));
});
it("\\@firstoftwo should consume both, and avoid errors", function() {
expect`\@firstoftwo{yes}{no}`.toParseLike`yes`;
expect`\@firstoftwo{yes}{1'_2^3}`.toParseLike`yes`;
});
it("\\@ifstar should consume star but nothing else", function() {
expect`\@ifstar{yes}{no}*!`.toParseLike`yes!`;
expect`\@ifstar{yes}{no}?!`.toParseLike`no?!`;
});
it("\\@ifnextchar should not consume nonspaces", function() {
expect`\@ifnextchar!{yes}{no}!!`.toParseLike`yes!!`;
expect`\@ifnextchar!{yes}{no}?!`.toParseLike`no?!`;
});
it("\\@ifnextchar should consume spaces", function() {
expect`\def\x#1{\@ifnextchar x{yes}{no}}\x{}x\x{} x`
.toParseLike`yesxyesx`;
});
it("\\@ifstar should consume star but nothing else", function() {
expect`\@ifstar{yes}{no}*!`.toParseLike`yes!`;
expect`\@ifstar{yes}{no}?!`.toParseLike`no?!`;
});
it("\\TextOrMath should work immediately", function() {
expect`\TextOrMath{text}{math}`.toParseLike`math`;
});
it("\\TextOrMath should work after other math", function() {
expect`x+\TextOrMath{text}{math}`.toParseLike`x+math`;
});
it("\\TextOrMath should work immediately after \\text", function() {
expect`\text{\TextOrMath{text}{math}}`.toParseLike`\text{text}`;
});
it("\\TextOrMath should work later after \\text", function() {
expect`\text{hello \TextOrMath{text}{math}}`.toParseLike`\text{hello text}`;
});
it("\\TextOrMath should work immediately after \\text ends", function() {
expect`\text{\TextOrMath{text}{math}}\TextOrMath{text}{math}`
.toParseLike`\text{text}math`;
});
it("\\TextOrMath should work immediately after $", function() {
expect`\text{$\TextOrMath{text}{math}$}`.toParseLike`\text{$math$}`;
});
it("\\TextOrMath should work later after $", function() {
expect`\text{$x+\TextOrMath{text}{math}$}`.toParseLike`\text{$x+math$}`;
});
it("\\TextOrMath should work immediately after $ ends", function() {
expect`\text{$\TextOrMath{text}{math}$\TextOrMath{text}{math}}`
.toParseLike`\text{$math$text}`;
});
it("\\TextOrMath should work in a macro", function() {
expect`\mode\text{\mode$\mode$\mode}\mode`
.toParseLike(r`math\text{text$math$text}math`, new Settings({macros: {
"\\mode": "\\TextOrMath{text}{math}",
}}));
});
it("\\TextOrMath should work in a macro passed to \\text", function() {
expect`\text\mode`.toParseLike(r`\text t`, new Settings({macros:
{"\\mode": "\\TextOrMath{t}{m}"}}));
});
it("\\char produces literal characters", () => {
expect("\\char`a").toParseLike("\\char`\\a");
expect("\\char`\\%").toParseLike("\\char37");
expect("\\char`\\%").toParseLike("\\char'45");
expect("\\char`\\%").toParseLike('\\char"25');
expect("\\char").not.toParse();
expect("\\char`").not.toParse();
expect("\\char'").not.toParse();
expect('\\char"').not.toParse();
expect("\\char'a").not.toParse();
expect('\\char"g').not.toParse();
expect('\\char"g').not.toParse();
});
it("should build Unicode private area characters", function() {
expect`\gvertneqq\lvertneqq\ngeqq\ngeqslant\nleqq`.toBuild();
expect`\nleqslant\nshortmid\nshortparallel\varsubsetneq`.toBuild();
expect`\varsubsetneqq\varsupsetneq\varsupsetneqq`.toBuild();
});
// TODO(edemaine): This doesn't work yet. Parses like `\text text`,
// which doesn't treat all four letters as an argument.
//it("\\TextOrMath should work in a macro passed to \\text", function() {
// expect`\text\mode`.toParseLike(r`\text{text}`, new Settings({macros:
// {"\\mode": "\\TextOrMath{text}{math}"}});
//});
it("\\gdef defines macros", function() {
expect`\gdef\foo{x^2}\foo+\foo`.toParseLike`x^2+x^2`;
expect`\gdef{\foo}{x^2}\foo+\foo`.toParseLike`x^2+x^2`;
expect`\gdef\foo{hi}\foo+\text{\foo}`.toParseLike`hi+\text{hi}`;
expect`\gdef\foo#1{hi #1}\text{\foo{Alice}, \foo{Bob}}`
.toParseLike`\text{hi Alice, hi Bob}`;
expect`\gdef\foo#1#2{(#1,#2)}\foo 1 2+\foo 3 4`.toParseLike`(1,2)+(3,4)`;
expect`\gdef\foo#2{}`.not.toParse();
expect`\gdef\foo#1#3{}`.not.toParse();
expect`\gdef\foo#1#2#3#4#5#6#7#8#9{}`.toParse();
expect`\gdef\foo#1#2#3#4#5#6#7#8#9#10{}`.not.toParse();
expect`\gdef\foo#{}`.not.toParse();
expect`\gdef\foo\bar`.toParse();
expect`\gdef{\foo\bar}{}`.not.toParse();
expect`\gdef{}{}`.not.toParse();
// TODO: These shouldn't work, but `1` and `{1}` are currently treated
// the same, as are `\foo` and `{\foo}`.
//expect`\gdef\foo1`.not.toParse();
//expect`\gdef{\foo}{}`.not.toParse();
});
it("\\def works locally", () => {
expect("\\def\\x{1}\\x{\\def\\x{2}\\x{\\def\\x{3}\\x}\\x}\\x")
.toParseLike`1{2{3}2}1`;
expect("\\def\\x{1}\\x\\def\\x{2}\\x{\\def\\x{3}\\x\\def\\x{4}\\x}\\x")
.toParseLike`12{34}2`;
});
it("\\gdef overrides at all levels", () => {
expect("\\def\\x{1}\\x{\\def\\x{2}\\x{\\gdef\\x{3}\\x}\\x}\\x")
.toParseLike`1{2{3}3}3`;
expect("\\def\\x{1}\\x{\\def\\x{2}\\x{\\global\\def\\x{3}\\x}\\x}\\x")
.toParseLike`1{2{3}3}3`;
expect("\\def\\x{1}\\x{\\def\\x{2}\\x{\\gdef\\x{3}\\x\\def\\x{4}\\x}" +
"\\x\\def\\x{5}\\x}\\x").toParseLike`1{2{34}35}3`;
});
it("\\global needs to followed by \\def", () => {
expect`\global\def\foo{}\foo`.toParseLike``;
// TODO: This doesn't work yet; \global needs to expand argument.
//expect`\def\DEF{\def}\global\DEF\foo{}\foo`.toParseLike``;
expect`\global\foo`.not.toParse();
expect`\global\bar x`.not.toParse();
});
it("Macro arguments do not generate groups", () => {
expect("\\def\\x{1}\\x\\def\\foo#1{#1}\\foo{\\x\\def\\x{2}\\x}\\x")
.toParseLike`1122`;
});
it("\\textbf arguments do generate groups", () => {
expect("\\def\\x{1}\\x\\textbf{\\x\\def\\x{2}\\x}\\x")
.toParseLike`1\textbf{12}1`;
});
it("\\sqrt optional arguments generate groups", () => {
expect("\\def\\x{1}\\def\\y{1}\\x\\y" +
"\\sqrt[\\def\\x{2}\\x]{\\def\\y{2}\\y}\\x\\y")
.toParseLike`11\sqrt[2]{2}11`;
});
it("array cells generate groups", () => {
expect`\def\x{1}\begin{matrix}\x&\def\x{2}\x&\x\end{matrix}\x`
.toParseLike`\begin{matrix}1&2&1\end{matrix}1`;
expect`\def\x{1}\begin{matrix}\def\x{2}\x&\x\end{matrix}\x`
.toParseLike`\begin{matrix}2&1\end{matrix}1`;
});
it("\\gdef changes settings.macros", () => {
const macros = {};
expect`\gdef\foo{1}`.toParse(new Settings({macros}));
expect(macros["\\foo"]).toBeTruthy();
});
it("\\def doesn't change settings.macros", () => {
const macros = {};
expect`\def\foo{1}`.toParse(new Settings({macros}));
expect(macros["\\foo"]).toBeFalsy();
});
it("\\def changes settings.macros with globalGroup", () => {
const macros = {};
expect`\gdef\foo{1}`.toParse(new Settings({macros, globalGroup: true}));
expect(macros["\\foo"]).toBeTruthy();
});
it("\\newcommand doesn't change settings.macros", () => {
const macros = {};
expect`\newcommand\foo{x^2}\foo+\foo`.toParse(new Settings({macros}));
expect(macros["\\foo"]).toBeFalsy();
});
it("\\newcommand changes settings.macros with globalGroup", () => {
const macros = {};
expect`\newcommand\foo{x^2}\foo+\foo`.toParse(
new Settings({macros, globalGroup: true}));
expect(macros["\\foo"]).toBeTruthy();
});
it("\\newcommand defines new macros", () => {
expect`\newcommand\foo{x^2}\foo+\foo`.toParseLike`x^2+x^2`;
expect`\newcommand{\foo}{x^2}\foo+\foo`.toParseLike`x^2+x^2`;
// Function detection
expect`\newcommand\bar{x^2}\bar+\bar`.not.toParse();
expect`\newcommand{\bar}{x^2}\bar+\bar`.not.toParse();
// Symbol detection
expect`\newcommand\lambda{x^2}\lambda`.not.toParse();
expect`\newcommand\textdollar{x^2}\textdollar`.not.toParse();
// Macro detection
expect`\newcommand{\foo}{1}\foo\newcommand{\foo}{2}\foo`.not.toParse();
// Implicit detection
expect`\newcommand\limits{}`.not.toParse();
});
it("\\renewcommand redefines macros", () => {
expect`\renewcommand\foo{x^2}\foo+\foo`.not.toParse();
expect`\renewcommand{\foo}{x^2}\foo+\foo`.not.toParse();
expect`\renewcommand\bar{x^2}\bar+\bar`.toParseLike`x^2+x^2`;
expect`\renewcommand{\bar}{x^2}\bar+\bar`.toParseLike`x^2+x^2`;
expect`\newcommand{\foo}{1}\foo\renewcommand{\foo}{2}\foo`.toParseLike`12`;
});
it("\\providecommand (re)defines macros", () => {
expect`\providecommand\foo{x^2}\foo+\foo`.toParseLike`x^2+x^2`;
expect`\providecommand{\foo}{x^2}\foo+\foo`.toParseLike`x^2+x^2`;
expect`\providecommand\bar{x^2}\bar+\bar`.toParseLike`x^2+x^2`;
expect`\providecommand{\bar}{x^2}\bar+\bar`.toParseLike`x^2+x^2`;
expect`\newcommand{\foo}{1}\foo\providecommand{\foo}{2}\foo`
.toParseLike`12`;
expect`\providecommand{\foo}{1}\foo\renewcommand{\foo}{2}\foo`
.toParseLike`12`;
expect`\providecommand{\foo}{1}\foo\providecommand{\foo}{2}\foo`
.toParseLike`12`;
});
it("\\newcommand is local", () => {
expect`\newcommand\foo{1}\foo{\renewcommand\foo{2}\foo}\foo`
.toParseLike`1{2}1`;
});
it("\\newcommand accepts number of arguments", () => {
expect`\newcommand\foo[1]{#1^2}\foo x+\foo{y}`.toParseLike`x^2+y^2`;
expect`\newcommand\foo[10]{#1^2}\foo 0123456789`.toParseLike`0^2`;
expect`\newcommand\foo[x]{}`.not.toParse();
expect`\newcommand\foo[1.5]{}`.not.toParse();
});
// This may change in the future, if we support the extra features of
// \hspace.
it("should treat \\hspace, \\hskip like \\kern", function() {
expect`\hspace{1em}`.toParseLike`\kern1em`;
expect`\hskip{1em}`.toParseLike`\kern1em`;
});
it("should expand \\limsup as expected", () => {
expect`\limsup`.toParseLike`\operatorname*{lim\,sup}`;
});
it("should expand \\liminf as expected", () => {
expect`\liminf`.toParseLike`\operatorname*{lim\,inf}`;
});
it("should expand \\plim as expected", () => {
expect`\plim`.toParseLike`\mathop{\operatorname{plim}}\limits`;
});
it("should expand \\argmin as expected", () => {
expect`\argmin`.toParseLike`\operatorname*{arg\,min}`;
});
it("should expand \\argmax as expected", () => {
expect`\argmax`.toParseLike`\operatorname*{arg\,max}`;
});
});
describe("\\tag support", function() {
const displayMode = new Settings({displayMode: true});
it("should fail outside display mode", () => {
expect`\tag{hi}x+y`.not.toParse();
});
it("should fail with multiple tags", () => {
expect`\tag{1}\tag{2}x+y`.not.toParse(displayMode);
});
it("should build", () => {
expect`\tag{hi}x+y`.toBuild(displayMode);
});
it("should ignore location of \\tag", () => {
expect`\tag{hi}x+y`.toParseLike(r`x+y\tag{hi}`, displayMode);
});
it("should handle \\tag* like \\tag", () => {
expect`\tag{hi}x+y`.toParseLike(r`\tag*{({hi})}x+y`, displayMode);
});
});
describe("leqno and fleqn rendering options", () => {
const expr = r`\tag{hi}x+y`;
for (const opt of ["leqno", "fleqn"]) {
it(`should not add ${opt} class by default`, () => {
const settings = new Settings({displayMode: true});
const built = katex.__renderToDomTree(expr, settings);
expect(built.classes).not.toContain(opt);
});
it(`should not add ${opt} class when false`, () => {
const settings = new Settings({displayMode: true});
settings[opt] = false;
const built = katex.__renderToDomTree(expr, settings);
expect(built.classes).not.toContain(opt);
});
it(`should add ${opt} class when true`, () => {
const settings = new Settings({displayMode: true});
settings[opt] = true;
const built = katex.__renderToDomTree(expr, settings);
expect(built.classes).toContain(opt);
});
}
});
describe("\\@binrel automatic bin/rel/ord", () => {
it("should generate proper class", () => {
expect("L\\@binrel+xR").toParseLike("L\\mathbin xR");
expect("L\\@binrel=xR").toParseLike("L\\mathrel xR");
expect("L\\@binrel xxR").toParseLike("L\\mathord xR");
expect("L\\@binrel{+}{x}R").toParseLike("L\\mathbin{{x}}R");
expect("L\\@binrel{=}{x}R").toParseLike("L\\mathrel{{x}}R");
expect("L\\@binrel{x}{x}R").toParseLike("L\\mathord{{x}}R");
});
it("should base on just first character in group", () => {
expect("L\\@binrel{+x}xR").toParseLike("L\\mathbin xR");
expect("L\\@binrel{=x}xR").toParseLike("L\\mathrel xR");
expect("L\\@binrel{xx}xR").toParseLike("L\\mathord xR");
});
});
describe("A parser taking String objects", function() {
it("should not fail on an empty String object", function() {
expect(new String("")).toParse();
});
it("should parse the same as a regular string", function() {
expect(new String("xy")).toParseLike`xy`;
expect(new String(r`\div`)).toParseLike`\div`;
expect(new String(r`\frac 1 2`)).toParseLike`\frac 1 2`;
});
});
describe("Unicode accents", function() {
it("should parse Latin-1 letters in math mode", function() {
// TODO(edemaine): Unsupported Latin-1 letters in math: ÇÐÞçðþ
expect`ÀÁÂÃÄÅÈÉÊËÌÍÎÏÑÒÓÔÕÖÙÚÛÜÝàáâãäåèéêëìíîïñòóôõöùúûüýÿ`
.toParseLike(
r`\grave A\acute A\hat A\tilde A\ddot A\mathring A` +
r`\grave E\acute E\hat E\ddot E` +
r`\grave I\acute I\hat I\ddot I` +
r`\tilde N` +
r`\grave O\acute O\hat O\tilde O\ddot O` +
r`\grave U\acute U\hat U\ddot U` +
r`\acute Y` +
r`\grave a\acute a\hat a\tilde a\ddot a\mathring a` +
r`\grave e\acute e\hat e\ddot e` +
r`\grave ı\acute ı\hat ı\ddot ı` +
r`\tilde n` +
r`\grave o\acute o\hat o\tilde o\ddot o` +
r`\grave u\acute u\hat u\ddot u` +
r`\acute y\ddot y`, nonstrictSettings);
});
it("should parse Latin-1 letters in text mode", function() {
// TODO(edemaine): Unsupported Latin-1 letters in text: ÇÐÞçðþ
expect`\text{ÀÁÂÃÄÅÈÉÊËÌÍÎÏÑÒÓÔÕÖÙÚÛÜÝàáâãäåèéêëìíîïñòóôõöùúûüýÿ}`
.toParseLike(
r`\text{\`A\'A\^A\~A\"A\r A` +
r`\`E\'E\^E\"E` +
r`\`I\'I\^I\"I` +
r`\~N` +
r`\`O\'O\^O\~O\"O` +
r`\`U\'U\^U\"U` +
r`\'Y` +
r`\`a\'a\^a\~a\"a\r a` +
r`\`e\'e\^e\"e` +
r`\`ı\'ı\^ı\"ı` +
r`\~n` +
r`\`o\'o\^o\~o\"o` +
r`\`u\'u\^u\"u` +
r`\'y\"y}`, strictSettings);
});
it("should support \\aa in text mode", function() {
expect`\text{\aa\AA}`.toParseLike(r`\text{\r a\r A}`, strictSettings);
expect`\aa`.not.toParse(strictSettings);
expect`\Aa`.not.toParse(strictSettings);
});
it("should parse combining characters", function() {
expect("A\u0301C\u0301").toParseLike(r`Á\acute C`, nonstrictSettings);
expect("\\text{A\u0301C\u0301}").toParseLike(r`\text{Á\'C}`, strictSettings);
});
it("should parse multi-accented characters", function() {
expect`ấā́ắ\text{ấā́ắ}`.toParse(nonstrictSettings);
// Doesn't parse quite the same as
// "\\text{\\'{\\^a}\\'{\\=a}\\'{\\u a}}" because of the ordgroups.
});
it("should parse accented i's and j's", function() {
expect`íȷ́`.toParseLike(r`\acute ı\acute ȷ`, nonstrictSettings);
expect`ấā́ắ\text{ấā́ắ}`.toParse(nonstrictSettings);
});
});
describe("Unicode", function() {
it("should parse negated relations", function() {
expect`∉∤∦≁≆≠≨≩≮≯≰≱⊀⊁⊈⊉⊊⊋⊬⊭⊮⊯⋠⋡⋦⋧⋨⋩⋬⋭⪇⪈⪉⪊⪵⪶⪹⪺⫋⫌`.toParse(strictSettings);
});
it("should build relations", function() {
expect`∈∋∝∼∽≂≃≅≈≊≍≎≏≐≑≒≓≖≗≜≡≤≥≦≧≪≫≬≳≷≺≻≼≽≾≿∴∵∣≔≕⩴⋘⋙⟂⊨∌`.toBuild(strictSettings);
});
it("should build big operators", function() {
expect`∏∐∑∫∬∭∮⋀⋁⋂⋃⨀⨁⨂⨄⨆`.toBuild(strictSettings);
});
it("should build more relations", function() {
expect`⊂⊃⊆⊇⊏⊐⊑⊒⊢⊣⊩⊪⊸⋈⋍⋐⋑⋔⋛⋞⋟⌢⌣⩾⪆⪌⪕⪖⪯⪰⪷⪸⫅⫆≘≙≚≛≝≞≟≲⩽⪅≶⋚⪋`.toBuild(strictSettings);
});
it("should parse symbols", function() {
expect("£¥ℂℍℑℎℓℕ℘ℙℚℜℝℤℲℵðℶℷℸ⅁∀∁∂∃∇∞∠∡∢♠♡♢♣♭♮♯✓°¬‼⋮\u00B7\u00A9").toBuild(strictSettings);
expect("\\text{£¥ℂℍℎ\u00A9\u00AE\uFE0F}").toBuild(strictSettings);
});
it("should build Greek capital letters", function() {
expect("\u0391\u0392\u0395\u0396\u0397\u0399\u039A\u039C\u039D" +
"\u039F\u03A1\u03A4\u03A7\u03DD").toBuild(strictSettings);
});
it("should build arrows", function() {
expect`←↑→↓↔↕↖↗↘↙↚↛↞↠↢↣↦↩↪↫↬↭↮↰↱↶↷↼↽↾↾↿⇀⇁⇂⇃⇄⇆⇇⇈⇉`.toBuild(strictSettings);
});
it("should build more arrows", function() {
expect`⇊⇋⇌⇍⇎⇏⇐⇑⇒⇓⇔⇕⇚⇛⇝⟵⟶⟷⟸⟹⟺⟼`.toBuild(strictSettings);
});
it("should build binary operators", function() {
expect("±×÷∓∔∧∨∩∪≀⊎⊓⊔⊕⊖⊗⊘⊙⊚⊛⊝⊞⊟⊠⊡⊺⊻⊼⋇⋉⋊⋋⋌⋎⋏⋒⋓⩞\u22C5").toBuild(strictSettings);
});
it("should build delimiters", function() {
expect("\\left\u230A\\frac{a}{b}\\right\u230B").toBuild();
expect("\\left\u2308\\frac{a}{b}\\right\u2308").toBuild();
expect("\\left\u27ee\\frac{a}{b}\\right\u27ef").toBuild();
expect("\\left\u27e8\\frac{a}{b}\\right\u27e9").toBuild();
expect("\\left\u23b0\\frac{a}{b}\\right\u23b1").toBuild();
expect`┌x┐ └x┘`.toBuild();
expect("\u231Cx\u231D \u231Ex\u231F").toBuild();
expect("\u27E6x\u27E7").toBuild();
expect("\\llbracket \\rrbracket").toBuild();
expect("\\lBrace \\rBrace").toBuild();
});
it("should build some surrogate pairs", function() {
let wideCharStr = "";
wideCharStr += String.fromCharCode(0xD835, 0xDC00); // bold A
wideCharStr += String.fromCharCode(0xD835, 0xDC68); // bold italic A
wideCharStr += String.fromCharCode(0xD835, 0xDD04); // Fraktur A
wideCharStr += String.fromCharCode(0xD835, 0xDD38); // double-struck
wideCharStr += String.fromCharCode(0xD835, 0xDC9C); // script A
wideCharStr += String.fromCharCode(0xD835, 0xDDA0); // sans serif A
wideCharStr += String.fromCharCode(0xD835, 0xDDD4); // bold sans A
wideCharStr += String.fromCharCode(0xD835, 0xDE08); // italic sans A
wideCharStr += String.fromCharCode(0xD835, 0xDE70); // monospace A
wideCharStr += String.fromCharCode(0xD835, 0xDFCE); // bold zero
wideCharStr += String.fromCharCode(0xD835, 0xDFE2); // sans serif zero
wideCharStr += String.fromCharCode(0xD835, 0xDFEC); // bold sans zero
wideCharStr += String.fromCharCode(0xD835, 0xDFF6); // monospace zero
expect(wideCharStr).toBuild(strictSettings);
let wideCharText = "\text{";
wideCharText += String.fromCharCode(0xD835, 0xDC00); // bold A
wideCharText += String.fromCharCode(0xD835, 0xDC68); // bold italic A
wideCharText += String.fromCharCode(0xD835, 0xDD04); // Fraktur A
wideCharText += String.fromCharCode(0xD835, 0xDD38); // double-struck
wideCharText += String.fromCharCode(0xD835, 0xDC9C); // script A
wideCharText += String.fromCharCode(0xD835, 0xDDA0); // sans serif A
wideCharText += String.fromCharCode(0xD835, 0xDDD4); // bold sans A
wideCharText += String.fromCharCode(0xD835, 0xDE08); // italic sans A
wideCharText += String.fromCharCode(0xD835, 0xDE70); // monospace A
wideCharText += String.fromCharCode(0xD835, 0xDFCE); // bold zero
wideCharText += String.fromCharCode(0xD835, 0xDFE2); // sans serif zero
wideCharText += String.fromCharCode(0xD835, 0xDFEC); // bold sans zero
wideCharText += String.fromCharCode(0xD835, 0xDFF6); // monospace zero
wideCharText += "}";
expect(wideCharText).toBuild(strictSettings);
});
});
describe("The maxSize setting", function() {
const rule = r`\rule{999em}{999em}`;
it("should clamp size when set", function() {
const built = getBuilt(rule, new Settings({maxSize: 5}))[0];
expect(built.style.borderRightWidth).toEqual("5em");
expect(built.style.borderTopWidth).toEqual("5em");
});
it("should not clamp size when not set", function() {
const built = getBuilt(rule)[0];
expect(built.style.borderRightWidth).toEqual("999em");
expect(built.style.borderTopWidth).toEqual("999em");
});
it("should make zero-width rules if a negative maxSize is passed", function() {
const built = getBuilt(rule, new Settings({maxSize: -5}))[0];
expect(built.style.borderRightWidth).toEqual("0em");
expect(built.style.borderTopWidth).toEqual("0em");
});
});
describe("The maxExpand setting", () => {
it("should prevent expansion", () => {
expect`\gdef\foo{1}\foo`.toParse();
expect`\gdef\foo{1}\foo`.toParse(new Settings({maxExpand: 2}));
expect`\gdef\foo{1}\foo`.not.toParse(new Settings({maxExpand: 1}));
expect`\gdef\foo{1}\foo`.not.toParse(new Settings({maxExpand: 0}));
});
it("should prevent infinite loops", () => {
expect`\gdef\foo{\foo}\foo`.not.toParse(
new Settings({maxExpand: 10}));
});
});
describe("The \\mathchoice function", function() {
const cmd = r`\sum_{k = 0}^{\infty} x^k`;
it("should render as if there is nothing other in display math", function() {
expect(`\\displaystyle\\mathchoice{${cmd}}{T}{S}{SS}`)
.toBuildLike(`\\displaystyle${cmd}`);
});
it("should render as if there is nothing other in text", function() {
expect(`\\mathchoice{D}{${cmd}}{S}{SS}`).toBuildLike(cmd);
});
it("should render as if there is nothing other in scriptstyle", function() {
expect(`x_{\\mathchoice{D}{T}{${cmd}}{SS}}`).toBuildLike(`x_{${cmd}}`);
});
it("should render as if there is nothing other in scriptscriptstyle", function() {
expect(`x_{y_{\\mathchoice{D}{T}{S}{${cmd}}}}`).toBuildLike(`x_{y_{${cmd}}}`);
});
});
describe("Newlines via \\\\ and \\newline", function() {
it("should build \\\\ and \\newline the same", () => {
expect`hello \\ world`.toBuildLike`hello \newline world`;
expect`hello \\[1ex] world`.toBuildLike(
"hello \\newline[1ex] world");
});
it("should not allow \\cr at top level", () => {
expect`hello \cr world`.not.toBuild();
});
it("array redefines and resets \\\\", () => {
expect`a\\b\begin{matrix}x&y\\z&w\end{matrix}\\c`
.toParseLike`a\newline b\begin{matrix}x&y\cr z&w\end{matrix}\newline c`;
});
it("\\\\ causes newline, even after mrel and mop", () => {
const markup = katex.renderToString(r`M = \\ a + \\ b \\ c`);
// Ensure newlines appear outside base spans (because, in this regexp,
// base span occurs immediately after each newline span).
expect(markup).toMatch(
/(<span class="base">.*?<\/span><span class="mspace newline"><\/span>){3}<span class="base">/);
expect(markup).toMatchSnapshot();
});
});
describe("Symbols", function() {
it("should parse \\text{\\i\\j}", () => {
expect`\text{\i\j}`.toBuild(strictSettings);
});
it("should parse spacing functions in math or text mode", () => {
expect`A\;B\,C\nobreakspace \text{A\;B\,C\nobreakspace}`.toBuild(strictSettings);
});
it("should render ligature commands like their unicode characters", () => {
expect`\text{\ae\AE\oe\OE\o\O\ss}`.toBuildLike(r`\text{æÆœŒøØß}`, strictSettings);
});
});
describe("strict setting", function() {
it("should allow unicode text when not strict", () => {
expect`é`.toParse(new Settings(nonstrictSettings));
expect``.toParse(new Settings(nonstrictSettings));
expect`é`.toParse(new Settings({strict: "ignore"}));
expect``.toParse(new Settings({strict: "ignore"}));
expect`é`.toParse(new Settings({strict: () => false}));
expect``.toParse(new Settings({strict: () => false}));
expect`é`.toParse(new Settings({strict: () => "ignore"}));
expect``.toParse(new Settings({strict: () => "ignore"}));
});
it("should forbid unicode text when strict", () => {
expect`é`.not.toParse(new Settings({strict: true}));
expect``.not.toParse(new Settings({strict: true}));
expect`é`.not.toParse(new Settings({strict: "error"}));
expect``.not.toParse(new Settings({strict: "error"}));
expect`é`.not.toParse(new Settings({strict: () => true}));
expect``.not.toParse(new Settings({strict: () => true}));
expect`é`.not.toParse(new Settings({strict: () => "error"}));
expect``.not.toParse(new Settings({strict: () => "error"}));
});
it("should warn about unicode text when default", () => {
expect`é`.toWarn(new Settings());
expect``.toWarn(new Settings());
});
it("should always allow unicode text in text mode", () => {
expect`\text{é試}`.toParse(nonstrictSettings);
expect`\text{é試}`.toParse(strictSettings);
expect`\text{é試}`.toParse();
});
it("should warn about top-level \\newline in display mode", () => {
expect`x\\y`.toWarn(new Settings({displayMode: true}));
expect`x\\y`.toParse(new Settings({displayMode: false}));
});
});
describe("Internal __* interface", function() {
const latex = r`\sum_{k = 0}^{\infty} x^k`;
const rendered = katex.renderToString(latex);
it("__parse renders same as renderToString", () => {
const parsed = katex.__parse(latex);
expect(buildTree(parsed, latex, new Settings()).toMarkup()).toEqual(rendered);
});
it("__renderToDomTree renders same as renderToString", () => {
const tree = katex.__renderToDomTree(latex);
expect(tree.toMarkup()).toEqual(rendered);
});
it("__renderToHTMLTree renders same as renderToString sans MathML", () => {
const tree = katex.__renderToHTMLTree(latex);
const renderedSansMathML = rendered.replace(
/<span class="katex-mathml">.*?<\/span>/, '');
expect(tree.toMarkup()).toEqual(renderedSansMathML);
});
});
describe("Extending katex by new fonts and symbols", function() {
beforeAll(() => {
const fontName = "mockEasternArabicFont";
// add eastern arabic numbers to symbols table
// these symbols are ۰۱۲۳۴۵۶۷۸۹ and ٠١٢٣٤٥٦٧٨٩
for (let number = 0; number <= 9; number++) {
const persianNum = String.fromCharCode(0x0660 + number);
katex.__defineSymbol(
"math", fontName, "textord", persianNum, persianNum);
const arabicNum = String.fromCharCode(0x06F0 + number);
katex.__defineSymbol(
"math", fontName, "textord", arabicNum, arabicNum);
}
});
it("should throw on rendering new symbols with no font metrics", () => {
// Lets parse 99^11 in eastern arabic
const errorMessage = "Font metrics not found for font: mockEasternArabicFont-Regular.";
expect(() => {
katex.__renderToDomTree("۹۹^{۱۱}", strictSettings);
}).toThrow(errorMessage);
});
it("should add font metrics to metrics map and render successfully", () => {
const mockMetrics = {};
// mock font metrics for the symbols that we added previously
for (let number = 0; number <= 9; number++) {
mockMetrics[0x0660 + number] = [-0.00244140625, 0.6875, 0, 0];
mockMetrics[0x06F0 + number] = [-0.00244140625, 0.6875, 0, 0];
}
katex.__setFontMetrics('mockEasternArabicFont-Regular', mockMetrics);
expect`۹۹^{۱۱}`.toBuild();
});
it("Add new font class to new extended symbols", () => {
expect(katex.renderToString("۹۹^{۱۱}")).toMatchSnapshot();
});
});