Merge pull request #317 from JeffEverett/unsupported_commands

Added support for visual depiction of unsupported commands
This commit is contained in:
Kevin Barabash 2015-07-29 10:55:04 -07:00
commit b2d2df9bef
9 changed files with 152 additions and 25 deletions

View File

@ -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:

View File

@ -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
@ -223,6 +239,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.
*
@ -499,9 +546,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;

View File

@ -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;

View File

@ -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(

View File

@ -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;
var built = buildMathML(parseTree(expr));
expect(expr).toParse(usedSettings);
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);
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -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"
}

View File

@ -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>