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