commit e1c221273c1ac74f7a7cd98658a842e66993dfd4
parent 4be3931cb5b685c38e868eb2436522563d6f7de2
Author: Jeff Everett <everett.jeff.w@gmail.com>
Date: Mon, 27 Jul 2015 03:45:19 -0600
Added support for visual depiction of unsupported commands
Diffstat:
9 files changed, 152 insertions(+), 25 deletions(-)
diff --git a/README.md b/README.md
@@ -44,6 +44,9 @@ Make sure to include the CSS and font files, but there is no need to include the
You can provide an object of options as the last argument to `katex.render` and `katex.renderToString`. Available options are:
- `displayMode`: `boolean`. If `true` the math will be rendered in display mode, which will put the math in display style (so `\int` and `\sum` are large, for example), and will center the math on the page on its own line. If `false` the math will be rendered in inline mode. (default: `false`)
+- `breakOnUnsupportedCmds`: `boolean`. If `true`, KaTeX will generate a `ParseError` when it encounters an unsupported command. If `false`, KaTeX will render the command as text
+in the color given by `errorColor`. (default: `true`)
+- `errorColor`: `string`. A color string given in the format `"#XXX"` or `"#XXXXXX"`. This option determines the color which unsupported commands are rendered in. (default: `#cc0000`)
For example:
diff --git a/src/Parser.js b/src/Parser.js
@@ -129,6 +129,14 @@ Parser.prototype.parseExpression = function(pos, mode, breakOnInfix, breakOnToke
}
var atom = this.parseAtom(pos, mode);
if (!atom) {
+ if (!this.settings.breakOnUnsupportedCmds && lex.text[0] === "\\") {
+ var errorNode = this.handleUnsupportedCmd(lex.text, mode);
+ body.push(errorNode);
+
+ pos = lex.position;
+ continue;
+ }
+
break;
}
if (breakOnInfix && atom.result.type === "infix") {
@@ -204,8 +212,16 @@ Parser.prototype.handleSupSubscript = function(pos, mode, symbol, name) {
var group = this.parseGroup(pos, mode);
if (!group) {
- throw new ParseError(
- "Expected group after '" + symbol + "'", this.lexer, pos);
+ var lex = this.lexer.lex(pos, mode);
+
+ if (!this.settings.breakOnUnsupportedCmds && lex.text[0] === "\\") {
+ return new ParseResult(
+ this.handleUnsupportedCmd(lex.text, mode),
+ lex.position);
+ } else {
+ throw new ParseError(
+ "Expected group after '" + symbol + "'", this.lexer, pos);
+ }
} else if (group.isFunction) {
// ^ and _ have a greediness, so handle interactions with functions'
// greediness
@@ -224,6 +240,37 @@ Parser.prototype.handleSupSubscript = function(pos, mode, symbol, name) {
};
/**
+ * Converts the textual input of an unsupported command into a text node
+ * contained within a color node whose color is determined by errorColor
+ */
+ Parser.prototype.handleUnsupportedCmd = function(text, mode) {
+ var textordArray = [];
+
+ for (var i = 0; i < text.length; i++) {
+ textordArray.push(new ParseNode("textord", text[i], "text"));
+ }
+
+ var textNode = new ParseNode(
+ "text",
+ {
+ body: textordArray,
+ type: "text"
+ },
+ mode);
+
+ var colorNode = new ParseNode(
+ "color",
+ {
+ color: this.settings.errorColor,
+ value: [textNode],
+ type: "color"
+ },
+ mode);
+
+ return colorNode;
+ };
+
+/**
* Parses a group with optional super/subscripts.
*
* @return {?ParseResult}
@@ -498,9 +545,18 @@ Parser.prototype.parseArguments = function(pos, mode, func, funcData, args) {
arg = this.parseGroup(newPos, mode);
}
if (!arg) {
- throw new ParseError(
- "Expected group after '" + func + "'",
- this.lexer, newPos);
+ var lex = this.lexer.lex(newPos, mode);
+
+ if (!this.settings.breakOnUnsupportedCmds && lex.text[0] === "\\") {
+ arg = new ParseFuncOrArgument(
+ new ParseResult(
+ this.handleUnsupportedCmd(lex.text, mode),
+ lex.position),
+ false);
+ } else {
+ throw new ParseError(
+ "Expected group after '" + func + "'", this.lexer, pos);
+ }
}
}
var argNode;
diff --git a/src/Settings.js b/src/Settings.js
@@ -21,6 +21,8 @@ function Settings(options) {
// allow null options
options = options || {};
this.displayMode = get(options.displayMode, false);
+ this.breakOnUnsupportedCmds = get(options.breakOnUnsupportedCmds, true);
+ this.errorColor = get(options.errorColor, "#cc0000");
}
module.exports = Settings;
diff --git a/src/buildCommon.js b/src/buildCommon.js
@@ -54,7 +54,11 @@ var mathit = function(value, mode, color, classes) {
var mathrm = function(value, mode, color, classes) {
// Decide what font to render the symbol in by its entry in the symbols
// table.
- if (symbols[mode][value].font === "main") {
+ // Have a special case for when the value = \ because the \ is used as a
+ // textord in unsupported command errors but cannot be parsed as a regular
+ // text ordinal and is therefore not present as a symbol in the symbols
+ // table for text
+ if (value === "\\" || symbols[mode][value].font === "main") {
return makeSymbol(value, "Main-Regular", mode, color, classes);
} else {
return makeSymbol(
diff --git a/test/katex-spec.js b/test/katex-spec.js
@@ -13,33 +13,39 @@ var Settings = require("../src/Settings");
var defaultSettings = new Settings({});
-var getBuilt = function(expr) {
- expect(expr).toBuild();
+var getBuilt = function(expr, settings) {
+ var usedSettings = settings ? settings : defaultSettings;
- var built = buildHTML(parseTree(expr), defaultSettings);
+ expect(expr).toBuild(usedSettings);
+
+ var parsedTree = parseTree(expr, usedSettings);
+ var built = buildHTML(parsedTree, usedSettings);
// Remove the outer .katex and .katex-inner layers
return built.children[2].children;
};
-var getParsed = function(expr) {
- expect(expr).toParse();
+var getParsed = function(expr, settings) {
+ var usedSettings = settings ? settings : defaultSettings;
- return parseTree(expr, defaultSettings);
+ expect(expr).toParse(usedSettings);
+ return parseTree(expr, usedSettings);
};
beforeEach(function() {
jasmine.addMatchers({
toParse: function() {
return {
- compare: function(actual) {
+ compare: function(actual, settings) {
+ var usedSettings = settings ? settings : defaultSettings;
+
var result = {
pass: true,
message: "'" + actual + "' succeeded parsing"
};
try {
- parseTree(actual, defaultSettings);
+ parseTree(actual, usedSettings);
} catch (e) {
result.pass = false;
if (e instanceof ParseError) {
@@ -58,7 +64,9 @@ beforeEach(function() {
toNotParse: function() {
return {
- compare: function(actual) {
+ compare: function(actual, settings) {
+ var usedSettings = settings ? settings : defaultSettings;
+
var result = {
pass: false,
message: "Expected '" + actual + "' to fail " +
@@ -66,7 +74,7 @@ beforeEach(function() {
};
try {
- parseTree(actual, defaultSettings);
+ parseTree(actual, usedSettings);
} catch (e) {
if (e instanceof ParseError) {
result.pass = true;
@@ -85,16 +93,18 @@ beforeEach(function() {
toBuild: function() {
return {
- compare: function(actual) {
+ compare: function(actual, settings) {
+ var usedSettings = settings ? settings : defaultSettings;
+
var result = {
pass: true,
message: "'" + actual + "' succeeded in building"
};
- expect(actual).toParse();
+ expect(actual).toParse(usedSettings);
try {
- buildHTML(parseTree(actual), defaultSettings);
+ buildHTML(parseTree(actual, usedSettings), usedSettings);
} catch (e) {
result.pass = false;
if (e instanceof ParseError) {
@@ -1359,10 +1369,12 @@ describe("A cases environment", function() {
});
-var getMathML = function(expr) {
- expect(expr).toParse();
+var getMathML = function(expr, settings) {
+ var usedSettings = settings ? settings : defaultSettings;
+
+ expect(expr).toParse(usedSettings);
- var built = buildMathML(parseTree(expr));
+ var built = buildMathML(parseTree(expr, usedSettings), expr, usedSettings);
// Strip off the surrounding <span>
return built.children[0];
@@ -1400,3 +1412,45 @@ describe("A MathML builder", function() {
expect(phantom.children[0].type).toEqual("mphantom");
});
});
+
+describe("A parser that does not break on unsupported commands", function() {
+ // The parser breaks on unsupported commands unless it is explicitly
+ // told not to
+ var errorColor = "#933";
+ var doNotBreakSettings = new Settings({
+ breakOnUnsupportedCmds: false,
+ errorColor: errorColor
+ });
+
+ it("should still parse on unrecognized control sequences", function() {
+ expect("\\error").toParse(doNotBreakSettings);
+ });
+
+ describe("should allow unrecognized controls sequences anywhere, including", function() {
+ it("in superscripts and subscripts", function() {
+ expect("2_\\error").toBuild(doNotBreakSettings);
+ expect("3^{\\error}_\\error").toBuild(doNotBreakSettings);
+ expect("\\int\\nolimits^\\error_\\error").toBuild(doNotBreakSettings);
+ });
+
+ it("in fractions", function() {
+ expect("\\frac{345}{\\error}").toBuild(doNotBreakSettings);
+ expect("\\frac\\error{\\error}").toBuild(doNotBreakSettings);
+ });
+
+ it("in square roots", function() {
+ expect("\\sqrt\\error").toBuild(doNotBreakSettings);
+ expect("\\sqrt{234\\error}").toBuild(doNotBreakSettings);
+ });
+
+ it("in text boxes", function() {
+ expect("\\text{\\error}").toBuild(doNotBreakSettings);
+ });
+ });
+
+ it("should produce color nodes with a color value given by errorColor", function() {
+ var parsedInput = getParsed("\\error", doNotBreakSettings);
+ expect(parsedInput[0].type).toBe("color");
+ expect(parsedInput[0].value.color).toBe(errorColor);
+ });
+});
diff --git a/test/screenshotter/images/UnsupportedCmds-chrome.png b/test/screenshotter/images/UnsupportedCmds-chrome.png
Binary files differ.
diff --git a/test/screenshotter/images/UnsupportedCmds-firefox.png b/test/screenshotter/images/UnsupportedCmds-firefox.png
Binary files differ.
diff --git a/test/screenshotter/ss_data.json b/test/screenshotter/ss_data.json
@@ -38,5 +38,6 @@
"SupSubHorizSpacing": "http://localhost:7936/test/screenshotter/test.html?m=x^{x^{x}}\\Big|x_{x_{x_{x_{x}}}}\\bigg|x^{x^{x_{x_{x_{x_{x}}}}}}\\bigg|",
"SupSubOffsets": "http://localhost:7936/test/screenshotter/test.html?m=\\displaystyle \\int_{2+3}x f^{2+3}+3\\lim_{2+3+4+5}f",
"Text": "http://localhost:7936/test/screenshotter/test.html?m=\\frac{a}{b}\\text{c~ {ab} \\ e}+fg",
+ "UnsupportedCmds": "http://localhost:7936/test/screenshotter/test.html?m=\\err\\,\\frac\\fracerr3\\,2^\\superr_\\suberr\\,\\sqrt\\sqrterr&doNotBreak=1&errorColor=%23dd4c4c",
"VerticalSpacing": "http://localhost:7936/test/screenshotter/test.html?pre=potato<br>blah&post=<br>moo&m=x^{\\Huge y}z"
}
diff --git a/test/screenshotter/test.html b/test/screenshotter/test.html
@@ -25,9 +25,16 @@
query[match[1]] = decodeURIComponent(match[2]);
}
var mathNode = document.getElementById("math");
- katex.render(query["m"], mathNode, {
- displayMode: !!query["display"]
- });
+
+ var settings = {
+ displayMode: !!query["display"],
+ breakOnUnsupportedCmds: !query["doNotBreak"]
+ };
+ if (query["errorColor"]) {
+ settings.errorColor = query["errorColor"];
+ }
+
+ katex.render(query["m"], mathNode, settings);
document.getElementById("pre").innerHTML = query["pre"] || "";
document.getElementById("post").innerHTML = query["post"] || "";
</script>