diff --git a/README.md b/README.md index cf245540..c3c8905f 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ You can provide an object of options as the last argument to `katex.render` and - `throwOnError`: `boolean`. If `true`, KaTeX will throw a `ParseError` when it encounters an unsupported command. If `false`, KaTeX will render the unsupported 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`) - `macros`: `object`. A collection of custom macros. Each macro is a property with a name like `\name` (written `"\\name"` in JavaScript) which maps to a string that describes the expansion of the macro. +- `colorIsTextColor`: `boolean`. If `true`, `\color` will work like LaTeX's `\textcolor`, and take two arguments (e.g., `\color{blue}{hello}`), which restores the old behavior of KaTeX (pre-0.8.0). If `false` (the default), `\color` will work like LaTeX's `\color`, and take one argument (e.g., `\color{blue}hello`). In both cases, `\textcolor` works as in LaTeX (e.g., `\textcolor{blue}{hello}`). For example: diff --git a/src/Parser.js b/src/Parser.js index 4f56b911..2cfb2989 100644 --- a/src/Parser.js +++ b/src/Parser.js @@ -50,6 +50,11 @@ function Parser(input, settings) { // Create a new macro expander (gullet) and (indirectly via that) also a // new lexer (mouth) for this parser (stomach, in the language of TeX) this.gullet = new MacroExpander(input, settings.macros); + // Use old \color behavior (same as LaTeX's \textcolor) if requested. + // We do this after the macros object has been copied by MacroExpander. + if (settings.colorIsTextColor) { + this.gullet.macros["\\color"] = "\\textcolor"; + } // Store the settings for use in parsing this.settings = settings; // Count leftright depth (for \middle errors) @@ -507,6 +512,18 @@ Parser.prototype.parseImplicitGroup = function() { body: new ParseNode("ordgroup", body, this.mode), }, this.mode); } + } else if (func === "\\color") { + // If we see a styling function, parse out the implicit body + const color = this.parseColorGroup(false); + if (!color) { + throw new ParseError("\\color not followed by color"); + } + const body = this.parseExpression(true); + return new ParseNode("color", { + type: "color", + color: color.result.value, + value: body, + }, this.mode); } else if (func === "$") { if (this.mode === "math") { throw new ParseError("$ within math mode"); diff --git a/src/Settings.js b/src/Settings.js index d38d77dc..bd494886 100644 --- a/src/Settings.js +++ b/src/Settings.js @@ -22,6 +22,7 @@ function Settings(options) { this.throwOnError = utils.deflt(options.throwOnError, true); this.errorColor = utils.deflt(options.errorColor, "#cc0000"); this.macros = options.macros || {}; + this.colorIsTextColor = utils.deflt(options.colorIsTextColor, false); } module.exports = Settings; diff --git a/src/functions.js b/src/functions.js index 1cec871c..5d064d45 100644 --- a/src/functions.js +++ b/src/functions.js @@ -23,9 +23,9 @@ const ParseNode = parseData.ParseNode; * - "color": An html color, like "#abc" or "blue" * - "original": The same type as the environment that the * function being parsed is in (e.g. used for the - * bodies of functions like \color where the first - * argument is special and the second argument is - * parsed normally) + * bodies of functions like \textcolor where the + * first argument is special and the second + * argument is parsed normally) * Other possible types (probably shouldn't be used) * - "text": Text-like (e.g. \text) * - "math": Normal math @@ -151,7 +151,7 @@ defineFunction([ }); // A two-argument custom color -defineFunction("\\color", { +defineFunction("\\textcolor", { numArgs: 2, allowedInText: true, greediness: 3, @@ -166,6 +166,14 @@ defineFunction("\\color", { }; }); +// \color is handled in Parser.js's parseImplicitGroup +defineFunction("\\color", { + numArgs: 1, + allowedInText: true, + greediness: 3, + argTypes: ["color"], +}, null); + // An overline defineFunction("\\overline", { numArgs: 1, diff --git a/test/errors-spec.js b/test/errors-spec.js index 190e75c5..fa2818d3 100644 --- a/test/errors-spec.js +++ b/test/errors-spec.js @@ -224,9 +224,9 @@ describe("Parser.expect calls:", function() { describe("#parseSpecialGroup expecting braces", function() { it("complains about missing { for color", function() { - expect("\\color#ffffff{text}").toFailWithParseError( - "Expected '{', got '#' at position 7:" + - " \\color#̲ffffff{text}"); + expect("\\textcolor#ffffff{text}").toFailWithParseError( + "Expected '{', got '#' at position 11:" + + " \\textcolor#̲ffffff{text}"); }); it("complains about missing { for size", function() { expect("\\rule{1em}[2em]").toFailWithParseError( @@ -234,9 +234,9 @@ describe("Parser.expect calls:", function() { }); // Can't test for the [ of an optional group since it's optional it("complains about missing } for color", function() { - expect("\\color{#ffffff{text}").toFailWithParseError( - "Invalid color: '#ffffff{text' at position 8:" + - " \\color{#̲f̲f̲f̲f̲f̲f̲{̲t̲e̲x̲t̲}"); + expect("\\textcolor{#ffffff{text}").toFailWithParseError( + "Invalid color: '#ffffff{text' at position 12:" + + " \\textcolor{#̲f̲f̲f̲f̲f̲f̲{̲t̲e̲x̲t̲}"); }); it("complains about missing ] for size", function() { expect("\\rule[1em{2em}{3em}").toFailWithParseError( @@ -249,9 +249,9 @@ describe("Parser.expect calls:", function() { " at position 7: \\rule[1̲e̲m̲"); }); it("complains about missing } for color at end of input", function() { - expect("\\color{#123456").toFailWithParseError( + expect("\\textcolor{#123456").toFailWithParseError( "Unexpected end of input in color" + - " at position 8: \\color{#̲1̲2̲3̲4̲5̲6̲"); + " at position 12: \\textcolor{#̲1̲2̲3̲4̲5̲6̲"); }); }); @@ -341,9 +341,9 @@ describe("Lexer:", function() { describe("#_innerLexColor", function() { it("reject hex notation without #", function() { - expect("\\color{1a2b3c}{foo}").toFailWithParseError( + expect("\\textcolor{1a2b3c}{foo}").toFailWithParseError( "Invalid color: '1a2b3c'" + - " at position 8: \\color{1̲a̲2̲b̲3̲c̲}{foo}"); + " at position 12: \\textcolor{1̲a̲2̲b̲3̲c̲}{foo}"); }); }); diff --git a/test/katex-spec.js b/test/katex-spec.js index 33c2f5fc..c0bd2347 100644 --- a/test/katex-spec.js +++ b/test/katex-spec.js @@ -166,18 +166,22 @@ beforeEach(function() { toParseLike: function(util, baton) { return { - compare: function(actual, expected) { + compare: function(actual, expected, settings) { + const usedSettings = settings ? settings : defaultSettings; + const result = { pass: true, message: "Parse trees of '" + actual + "' and '" + expected + "' are equivalent", }; - const actualTree = parseAndSetResult(actual, result); + const actualTree = parseAndSetResult(actual, result, + usedSettings); if (!actualTree) { return result; } - const expectedTree = parseAndSetResult(expected, result); + const expectedTree = parseAndSetResult(expected, result, + usedSettings); if (!expectedTree) { return result; } @@ -726,7 +730,7 @@ describe("A text parser", function() { const textExpression = "\\text{a b}"; const noBraceTextExpression = "\\text x"; const nestedTextExpression = - "\\text{a {b} \\blue{c} \\color{#fff}{x} \\llap{x}}"; + "\\text{a {b} \\blue{c} \\textcolor{#fff}{x} \\llap{x}}"; const spaceTextExpression = "\\text{ a \\ }"; const leadingSpaceTextExpression = "\\text {moo}"; const badTextExpression = "\\text{a b%}"; @@ -799,8 +803,9 @@ describe("A text parser", function() { describe("A color parser", function() { const colorExpression = "\\blue{x}"; const newColorExpression = "\\redA{x}"; - const customColorExpression = "\\color{#fA6}{x}"; - const badCustomColorExpression = "\\color{bad-color}{x}"; + const customColorExpression = "\\textcolor{#fA6}{x}"; + const badCustomColorExpression = "\\textcolor{bad-color}{x}"; + const oldColorExpression = "\\color{#fA6}xy"; it("should not fail", function() { expect(colorExpression).toParse(); @@ -833,10 +838,26 @@ describe("A color parser", function() { }); it("should have correct greediness", function() { - expect("\\color{red}a").toParse(); - expect("\\color{red}{\\text{a}}").toParse(); - expect("\\color{red}\\text{a}").toNotParse(); - expect("\\color{red}\\frac12").toNotParse(); + expect("\\textcolor{red}a").toParse(); + expect("\\textcolor{red}{\\text{a}}").toParse(); + expect("\\textcolor{red}\\text{a}").toNotParse(); + expect("\\textcolor{red}\\frac12").toNotParse(); + }); + + 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("\\textcolor{#fA6}{xy}", { + colorIsTextColor: false, + }); + }); + + it("should use two-argument \\color if requested", function() { + expect(oldColorExpression).toParseLike("\\textcolor{#fA6}{x}y", { + colorIsTextColor: true, + }); }); }); @@ -1178,7 +1199,7 @@ describe("A TeX-compliant parser", function() { it("should fail if there are not enough arguments", function() { const missingGroups = [ "\\frac{x}", - "\\color{#fff}", + "\\textcolor{#fff}", "\\rule{1em}", "\\llap", "\\bigl", @@ -1396,8 +1417,8 @@ describe("A font parser", function() { expect(bbBody[2].value.type).toEqual("font"); }); - it("should work with \\color", function() { - const colorMathbbParse = getParsed("\\color{blue}{\\mathbb R}")[0]; + it("should work with \\textcolor", function() { + const colorMathbbParse = getParsed("\\textcolor{blue}{\\mathbb R}")[0]; expect(colorMathbbParse.value.type).toEqual("color"); expect(colorMathbbParse.value.color).toEqual("blue"); const body = colorMathbbParse.value.value; @@ -1485,11 +1506,11 @@ describe("An HTML font tree-builder", function() { }); it("should render a combination of font and color changes", function() { - let markup = katex.renderToString("\\color{blue}{\\mathbb R}"); + let markup = katex.renderToString("\\textcolor{blue}{\\mathbb R}"); let span = "R"; expect(markup).toContain(span); - markup = katex.renderToString("\\mathbb{\\color{blue}{R}}"); + markup = katex.renderToString("\\mathbb{\\textcolor{blue}{R}}"); span = "R"; expect(markup).toContain(span); }); @@ -1649,7 +1670,7 @@ describe("A MathML font tree-builder", function() { }); it("should render a combination of font and color changes", function() { - let tex = "\\color{blue}{\\mathbb R}"; + let tex = "\\textcolor{blue}{\\mathbb R}"; let tree = getParsed(tex); let markup = buildMathML(tree, tex, defaultOptions).toMarkup(); let node = "" + @@ -1658,7 +1679,7 @@ describe("A MathML font tree-builder", function() { expect(markup).toContain(node); // reverse the order of the commands - tex = "\\mathbb{\\color{blue}{R}}"; + tex = "\\mathbb{\\textcolor{blue}{R}}"; tree = getParsed(tex); markup = buildMathML(tree, tex, defaultOptions).toMarkup(); node = "" + diff --git a/test/screenshotter/images/ColorImplicit-chrome.png b/test/screenshotter/images/ColorImplicit-chrome.png new file mode 100644 index 00000000..1b153a81 Binary files /dev/null and b/test/screenshotter/images/ColorImplicit-chrome.png differ diff --git a/test/screenshotter/images/ColorImplicit-firefox.png b/test/screenshotter/images/ColorImplicit-firefox.png new file mode 100644 index 00000000..b136ee3a Binary files /dev/null and b/test/screenshotter/images/ColorImplicit-firefox.png differ diff --git a/test/screenshotter/ss_data.yaml b/test/screenshotter/ss_data.yaml index b03e5a6c..54add76a 100644 --- a/test/screenshotter/ss_data.yaml +++ b/test/screenshotter/ss_data.yaml @@ -49,9 +49,10 @@ Cases: | a-1&\text{otherwise} \end{cases} Colors: - tex: \blue{a}\color{#0f0}{b}\color{red}{c} + tex: \blue{a}\textcolor{#0f0}{b}\textcolor{red}{c} nolatex: different syntax and different scope -ColorSpacing: \color{red}{\displaystyle \int x} + 1 +ColorSpacing: \textcolor{red}{\displaystyle \int x} + 1 +ColorImplicit: bl{ack\color{red}red\textcolor{green}{green}red\color{blue}blue}black DashesAndQuotes: \text{``a'' b---c -- d----`e'-{-}-f}--``x'' DeepFontSizing: tex: |