diff --git a/katex.js b/katex.js index 7b9700e8..89073210 100644 --- a/katex.js +++ b/katex.js @@ -13,6 +13,8 @@ import Settings from "./src/Settings"; import { buildTree, buildHTMLTree } from "./src/buildTree"; import parseTree from "./src/parseTree"; +import buildCommon from "./src/buildCommon"; +import domTree from "./src/domTree"; import utils from "./src/utils"; import type {SettingsOptions} from "./src/Settings"; @@ -72,6 +74,26 @@ const generateParseTree = function( return parseTree(expression, settings); }; +/** + * If the given error is a KaTeX ParseError and options.throwOnError is false, + * renders the invalid LaTeX as a span with hover title giving the KaTeX + * error message. Otherwise, simply throws the error. + */ +const renderError = function( + error, + expression: string, + options: Settings, +) { + if (options.throwOnError || !(error instanceof ParseError)) { + throw error; + } + const node = buildCommon.makeSpan(["katex-error"], + [new domTree.symbolNode(expression)]); + node.setAttribute("title", error.toString()); + node.setAttribute("style", `color:${options.errorColor}`); + return node; +}; + /** * Generates and returns the katex build tree. This is used for advanced * use cases (like rendering to custom output). @@ -81,8 +103,12 @@ const renderToDomTree = function( options: SettingsOptions, ) { const settings = new Settings(options); - const tree = parseTree(expression, settings); - return buildTree(tree, expression, settings); + try { + const tree = parseTree(expression, settings); + return buildTree(tree, expression, settings); + } catch (error) { + return renderError(error, expression, settings); + } }; /** @@ -94,8 +120,12 @@ const renderToHTMLTree = function( options: SettingsOptions, ) { const settings = new Settings(options); - const tree = parseTree(expression, settings); - return buildHTMLTree(tree, expression, settings); + try { + const tree = parseTree(expression, settings); + return buildHTMLTree(tree, expression, settings); + } catch (error) { + return renderError(error, expression, settings); + } }; export default { diff --git a/static/main.js b/static/main.js index 896caa42..01e72df5 100644 --- a/static/main.js +++ b/static/main.js @@ -14,7 +14,7 @@ function init() { input.addEventListener("input", reprocess, false); permalink.addEventListener("click", setSearch); - const options = {displayMode: true, macros: {}}; + const options = {displayMode: true, throwOnError: false, macros: {}}; const query = queryString.parse(window.location.search); if (query.text) { diff --git a/test/__snapshots__/katex-spec.js.snap b/test/__snapshots__/katex-spec.js.snap index 2afc94dd..612fa342 100644 --- a/test/__snapshots__/katex-spec.js.snap +++ b/test/__snapshots__/katex-spec.js.snap @@ -1,5 +1,39 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`A parser that does not throw on unsupported commands should build katex-error span for other type of KaTeX error 1`] = ` +{ + "attributes": { + "style": "color:#933", + "title": "ParseError: KaTeX parse error: Double superscript at position 4: 2^2^̲2" + }, + "children": [ + { + "classes": [ + ], + "depth": 0, + "height": 0, + "italic": 0, + "maxFontSize": 0, + "skew": 0, + "style": { + }, + "value": "2^2^2", + "width": 0 + } + ], + "classes": [ + "katex-error" + ], + "depth": 0, + "height": 0, + "maxFontSize": 0, + "style": { + } +} +`; + +exports[`A parser that does not throw on unsupported commands should properly escape LaTeX in errors 1`] = `"2^&"<>"`; + exports[`An implicit group parser within optional groups should work style commands \\sqrt[\\textstyle 3]{x} 1`] = ` [ { diff --git a/test/katex-spec.js b/test/katex-spec.js index cae86e94..9c4c6fdb 100644 --- a/test/katex-spec.js +++ b/test/katex-spec.js @@ -44,8 +44,11 @@ const defaultOptions = new Options({ const _getBuilt = function(expr, settings) { const usedSettings = settings ? settings : defaultSettings; - const parsedTree = parseTree(expr, usedSettings); - const rootNode = buildTree(parsedTree, expr, usedSettings); + const rootNode = katex.__renderToDomTree(expr, usedSettings); + + if (rootNode.classes.indexOf('katex-error') >= 0) { + return rootNode; + } // grab the root node of the HTML rendering const builtHTML = rootNode.children[1]; @@ -98,10 +101,10 @@ const parseAndSetResult = function(expr, result, settings) { } catch (e) { result.pass = false; if (e instanceof ParseError) { - result.message = "'" + expr + "' failed " + + result.message = () => "'" + expr + "' failed " + "parsing with error: " + e.message; } else { - result.message = "'" + expr + "' failed " + + result.message = () => "'" + expr + "' failed " + "parsing with unknown error: " + e.message; } } @@ -113,10 +116,10 @@ const buildAndSetResult = function(expr, result, settings) { } catch (e) { result.pass = false; if (e instanceof ParseError) { - result.message = "'" + expr + "' failed " + + result.message = () => "'" + expr + "' failed " + "parsing with error: " + e.message; } else { - result.message = "'" + expr + "' failed " + + result.message = () => "'" + expr + "' failed " + "parsing with unknown error: " + e.message; } } @@ -149,10 +152,10 @@ beforeEach(function() { } catch (e) { if (e instanceof ParseError) { result.pass = true; - result.message = "'" + actual + "' correctly " + + result.message = () => "'" + actual + "' correctly " + "didn't parse with error: " + e.message; } else { - result.message = "'" + actual + "' failed " + + result.message = () => "'" + actual + "' failed " + "parsing with unknown error: " + e.message; } } @@ -175,10 +178,10 @@ beforeEach(function() { } catch (e) { result.pass = false; if (e instanceof ParseError) { - result.message = "'" + actual + "' failed to " + + result.message = () => "'" + actual + "' failed to " + "build with error: " + e.message; } else { - result.message = "'" + actual + "' failed " + + result.message = () => "'" + actual + "' failed " + "building with unknown error: " + e.message; } } @@ -2638,6 +2641,18 @@ describe("A parser that does not throw on unsupported commands", function() { expect(parsedInput[0].type).toBe("color"); expect(parsedInput[0].value.color).toBe(errorColor); }); + + it("should build katex-error span for other type of KaTeX error", function() { + // Use _getBuilt instead of getBuilt to avoid calling expect...toParse + // and thus throwing parse error + 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() {