mirror of
https://github.com/Smaug123/KaTeX
synced 2025-10-10 05:28:41 +00:00
Implicit \color, explicitly grouped \textcolor (#619)
This commit is contained in:
committed by
Kevin Barabash
parent
ad0abbd4e9
commit
25dde7f841
@@ -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:
|
||||
|
||||
|
@@ -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");
|
||||
|
@@ -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;
|
||||
|
@@ -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,
|
||||
|
@@ -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}");
|
||||
});
|
||||
});
|
||||
|
||||
|
@@ -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 = "<span class=\"mord mathbb\" style=\"color:blue;\">R</span>";
|
||||
expect(markup).toContain(span);
|
||||
|
||||
markup = katex.renderToString("\\mathbb{\\color{blue}{R}}");
|
||||
markup = katex.renderToString("\\mathbb{\\textcolor{blue}{R}}");
|
||||
span = "<span class=\"mord mathbb\" style=\"color:blue;\">R</span>";
|
||||
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 = "<mstyle mathcolor=\"blue\">" +
|
||||
@@ -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 = "<mstyle mathcolor=\"blue\">" +
|
||||
|
BIN
test/screenshotter/images/ColorImplicit-chrome.png
Normal file
BIN
test/screenshotter/images/ColorImplicit-chrome.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
BIN
test/screenshotter/images/ColorImplicit-firefox.png
Normal file
BIN
test/screenshotter/images/ColorImplicit-firefox.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
@@ -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: |
|
||||
|
Reference in New Issue
Block a user