diff --git a/src/buildHTML.js b/src/buildHTML.js index 631f5f19..e9556ec8 100644 --- a/src/buildHTML.js +++ b/src/buildHTML.js @@ -1533,43 +1533,63 @@ groupTypes.accentUnder = function(group, options) { }; groupTypes.enclose = function(group, options) { - // \cancel, \bcancel, \xcancel, \sout, \fbox + // \cancel, \bcancel, \xcancel, \sout, \fbox, \colorbox, \fcolorbox const inner = buildGroup(group.value.body, options); const label = group.value.label.substr(1); const scale = options.sizeMultiplier; let img; - let pad = 0; let imgShift = 0; + const isColorbox = /color/.test(label); if (label === "sout") { img = makeSpan(["stretchy", "sout"]); img.height = options.fontMetrics().defaultRuleThickness / scale; imgShift = -0.5 * options.fontMetrics().xHeight; + } else { // Add horizontal padding - inner.classes.push((label === "fbox" ? "boxpad" : "cancel-pad")); + inner.classes.push(/cancel/.test(label) ? "cancel-pad" : "boxpad"); // Add vertical padding - const isCharBox = (isCharacterBox(group.value.body)); + let vertPad = 0; // ref: LaTeX source2e: \fboxsep = 3pt; \fboxrule = .4pt // ref: cancel package: \advance\totalheight2\p@ % "+2" - pad = (label === "fbox" ? 0.34 : (isCharBox ? 0.2 : 0)); - imgShift = inner.depth + pad; + if (/box/.test(label)) { + vertPad = label === "colorbox" ? 0.3 : 0.34; + } else { + vertPad = isCharacterBox(group.value.body) ? 0.2 : 0; + } - img = stretchy.encloseSpan(inner, label, pad, options); + img = stretchy.encloseSpan(inner, label, vertPad, options); + imgShift = inner.depth + vertPad; + + if (isColorbox) { + img.style.backgroundColor = group.value.backgroundColor.value; + if (label === "fcolorbox") { + img.style.borderColor = group.value.borderColor.value; + } + } } - const vlist = buildCommon.makeVList([ - {type: "elem", elem: inner, shift: 0}, - {type: "elem", elem: img, shift: imgShift}, - ], "individualShift", null, options); - - if (label !== "fbox") { - vlist.children[0].children[0].children[1].classes.push("svg-align"); + let vlist; + if (isColorbox) { + vlist = buildCommon.makeVList([ + // Put the color background behind inner; + {type: "elem", elem: img, shift: imgShift}, + {type: "elem", elem: inner, shift: 0}, + ], "individualShift", null, options); + } else { + vlist = buildCommon.makeVList([ + // Write the \cancel stroke on top of inner. + {type: "elem", elem: inner, shift: 0}, + {type: "elem", elem: img, shift: imgShift}, + ], "individualShift", null, options); } if (/cancel/.test(label)) { + vlist.children[0].children[0].children[1].classes.push("svg-align"); + // cancel does not create horiz space for its line extension. // That is, not when adjacent to a mord. return makeSpan(["mord", "cancel-lap"], [vlist], options); diff --git a/src/buildMathML.js b/src/buildMathML.js index c2607b13..c632b93c 100644 --- a/src/buildMathML.js +++ b/src/buildMathML.js @@ -505,21 +505,33 @@ groupTypes.accentUnder = function(group, options) { groupTypes.enclose = function(group, options) { const node = new mathMLTree.MathNode( "menclose", [buildGroup(group.value.body, options)]); - let notation = ""; switch (group.value.label) { + case "\\cancel": + node.setAttribute("notation", "updiagonalstrike"); + break; case "\\bcancel": - notation = "downdiagonalstrike"; + node.setAttribute("notation", "downdiagonalstrike"); break; case "\\sout": - notation = "horizontalstrike"; + node.setAttribute("notation", "horizontalstrike"); break; case "\\fbox": - notation = "box"; + node.setAttribute("notation", "box"); + break; + case "\\colorbox": + node.setAttribute("mathbackground", + group.value.backgroundColor.value); + break; + case "\\fcolorbox": + node.setAttribute("mathbackground", + group.value.backgroundColor.value); + // TODO(ron): I don't know any way to set the border color. + node.setAttribute("notation", "box"); break; default: - notation = "updiagonalstrike"; + // xcancel + node.setAttribute("notation", "updiagonalstrike downdiagonalstrike"); } - node.setAttribute("notation", notation); return node; }; diff --git a/src/functions.js b/src/functions.js index 91c6a542..fc293e7d 100644 --- a/src/functions.js +++ b/src/functions.js @@ -88,6 +88,42 @@ defineFunction(["\\color"], { argTypes: ["color"], }, null); +// colorbox +defineFunction(["\\colorbox"], { + numArgs: 2, + allowedInText: true, + greediness: 3, + argTypes: ["color", "text"], +}, function(context, args) { + const color = args[0]; + const body = args[1]; + return { + type: "enclose", + label: context.funcName, + backgroundColor: color, + body: body, + }; +}); + +// fcolorbox +defineFunction(["\\fcolorbox"], { + numArgs: 3, + allowedInText: true, + greediness: 3, + argTypes: ["color", "color", "text"], +}, function(context, args) { + const borderColor = args[0]; + const backgroundColor = args[1]; + const body = args[2]; + return { + type: "enclose", + label: context.funcName, + backgroundColor: backgroundColor, + borderColor: borderColor, + body: body, + }; +}); + // An overline defineFunction(["\\overline"], { numArgs: 1, diff --git a/src/stretchy.js b/src/stretchy.js index 3e2e16f1..7a3e3c21 100644 --- a/src/stretchy.js +++ b/src/stretchy.js @@ -254,11 +254,13 @@ const encloseSpan = function(inner, label, pad, options) { let img; const totalHeight = inner.height + inner.depth + 2 * pad; - if (label === "fbox") { + if (/(fbox)|(color)/.test(label)) { img = buildCommon.makeSpan(["stretchy", label], [], options); - if (options.color) { + + if (label === "fbox" && options.color) { img.style.borderColor = options.getColor(); } + } else { // \cancel, \bcancel, or \xcancel // Since \cancel's SVG is inline and it omits the viewBox attribute, diff --git a/static/katex.less b/static/katex.less index b009b7bf..f7fad196 100644 --- a/static/katex.less +++ b/static/katex.less @@ -593,6 +593,10 @@ box-sizing: border-box; border: 0.04em solid black; // \fboxrule = 0.4pt } + .fcolorbox { + box-sizing: border-box; + border: 0.04em solid; // \fboxrule = 0.4pt + } .cancel-pad { padding: 0 0.2em 0 0.2em; // ref: cancel package \advance\dimen@ 2\p@ % "+2" } diff --git a/test/__snapshots__/mathml-spec.js.snap b/test/__snapshots__/mathml-spec.js.snap index 2b52ca14..3edca120 100644 --- a/test/__snapshots__/mathml-spec.js.snap +++ b/test/__snapshots__/mathml-spec.js.snap @@ -74,6 +74,27 @@ exports[`A MathML builder should make prime operators into nodes 1`] = ` `; +exports[`A MathML builder should use for colorbox 1`] = ` + + + + + + + + b + + + + + + \\colorbox{red}{b} + + + + +`; + exports[`A MathML builder should use for raisebox 1`] = ` diff --git a/test/katex-spec.js b/test/katex-spec.js index feb5d2bf..882a452c 100644 --- a/test/katex-spec.js +++ b/test/katex-spec.js @@ -2046,6 +2046,78 @@ describe("A boxed builder", function() { }); }); +describe("A colorbox parser", function() { + it("should not fail, given a text argument", function() { + expect("\\colorbox{red}{a b}").toParse(); + expect("\\colorbox{red}{x}^2").toParse(); + expect("\\colorbox{red} x").toParse(); + }); + + it("should fail, given a math argument", function() { + expect("\\colorbox{red}{\\alpha}").toNotParse(); + expect("\\colorbox{red}{\\frac{a}{b}}").toNotParse(); + }); + + it("should parse a color", function() { + expect("\\colorbox{red}{a b}").toParse(); + expect("\\colorbox{#197}{a b}").toParse(); + expect("\\colorbox{#1a9b7c}{a b}").toParse(); + }); + + it("should produce enclose", function() { + const parse = getParsed("\\colorbox{red} x")[0]; + expect(parse.type).toEqual("enclose"); + }); +}); + +describe("A colorbox builder", function() { + it("should not fail", function() { + expect("\\colorbox{red}{a b}").toBuild(); + expect("\\colorbox{red}{a b}^2").toBuild(); + expect("\\colorbox{red} x").toBuild(); + }); + + it("should produce mords", function() { + expect(getBuilt("\\colorbox{red}{a b}")[0].classes).toContain("mord"); + }); +}); + +describe("An fcolorbox parser", function() { + it("should not fail, given a text argument", function() { + expect("\\fcolorbox{blue}{yellow}{a b}").toParse(); + expect("\\fcolorbox{blue}{yellow}{x}^2").toParse(); + expect("\\fcolorbox{blue}{yellow} x").toParse(); + }); + + it("should fail, given a math argument", function() { + expect("\\fcolorbox{blue}{yellow}{\\alpha}").toNotParse(); + expect("\\fcolorbox{blue}{yellow}{\\frac{a}{b}}").toNotParse(); + }); + + it("should parse a color", function() { + expect("\\fcolorbox{blue}{yellow}{a b}").toParse(); + expect("\\fcolorbox{blue}{#197}{a b}").toParse(); + expect("\\fcolorbox{blue}{#1a9b7c}{a b}").toParse(); + }); + + it("should produce enclose", function() { + const parse = getParsed("\\fcolorbox{blue}{yellow} x")[0]; + expect(parse.type).toEqual("enclose"); + }); +}); + +describe("A fcolorbox builder", function() { + it("should not fail", function() { + expect("\\fcolorbox{blue}{yellow}{a b}").toBuild(); + expect("\\fcolorbox{blue}{yellow}{a b}^2").toBuild(); + expect("\\fcolorbox{blue}{yellow} x").toBuild(); + }); + + it("should produce mords", function() { + expect(getBuilt("\\colorbox{red}{a b}")[0].classes).toContain("mord"); + }); +}); + describe("A strike-through parser", function() { it("should not fail", function() { expect("\\cancel{x}").toParse(); diff --git a/test/mathml-spec.js b/test/mathml-spec.js index 6565c099..5c6c61fc 100644 --- a/test/mathml-spec.js +++ b/test/mathml-spec.js @@ -54,4 +54,8 @@ describe("A MathML builder", function() { it('should use for raisebox', () => { expect(getMathML("\\raisebox{0.25em}{b}")).toMatchSnapshot(); }); + + it('should use for colorbox', () => { + expect(getMathML("\\colorbox{red}{b}")).toMatchSnapshot(); + }); }); diff --git a/test/screenshotter/images/Colorbox-chrome.png b/test/screenshotter/images/Colorbox-chrome.png new file mode 100644 index 00000000..6e68c395 Binary files /dev/null and b/test/screenshotter/images/Colorbox-chrome.png differ diff --git a/test/screenshotter/images/Colorbox-firefox.png b/test/screenshotter/images/Colorbox-firefox.png new file mode 100644 index 00000000..bf732174 Binary files /dev/null and b/test/screenshotter/images/Colorbox-firefox.png differ diff --git a/test/screenshotter/ss_data.yaml b/test/screenshotter/ss_data.yaml index 70ce4bb8..e676c829 100644 --- a/test/screenshotter/ss_data.yaml +++ b/test/screenshotter/ss_data.yaml @@ -59,6 +59,7 @@ Colors: nolatex: different syntax and different scope ColorImplicit: bl{ack\color{red}red\textcolor{green}{green}red\color{blue}blue}black ColorSpacing: \textcolor{red}{\displaystyle \int x} + 1 +Colorbox: a \colorbox{teal} B \fcolorbox{blue}{red}{C} e+\colorbox{teal}x DashesAndQuotes: \text{``a'' b---c -- d----`e'-{-}-f}--``x'' DeepFontSizing: tex: |