From 39f5bcb04289c87fb5f0826078c0fb583df307e0 Mon Sep 17 00:00:00 2001 From: Kevin Barabash Date: Fri, 13 Mar 2015 16:24:04 -0600 Subject: [PATCH] Add support for \phantom Summary: Using \phantom with non-phantom math in Perseus doesn't render to be the same size because \phantom uses MathJax and the non-phantom math uses KaTeX. Implementing \phantom in KaTeX should solve this alignment issue. Test Plan: [x] write (and run) unit tests [x] create (and run) screenshotter tests Reviewers: emily Reviewed By: emily Differential Revision: https://phabricator.khanacademy.org/D16720 --- src/Options.js | 76 ++++++++++++++---- src/buildHTML.js | 27 +++++-- src/buildMathML.js | 5 ++ src/functions.js | 17 ++++ test/katex-spec.js | 45 ++++++++++- test/screenshotter/images/Phantom-firefox.png | Bin 0 -> 11171 bytes test/screenshotter/ss_data.json | 1 + 7 files changed, 148 insertions(+), 23 deletions(-) create mode 100644 test/screenshotter/images/Phantom-firefox.png diff --git a/src/Options.js b/src/Options.js index 00dcf4f67..a3febb45b 100644 --- a/src/Options.js +++ b/src/Options.js @@ -14,41 +14,82 @@ * as the parentStyle and parentSize of the new options class, so parent * handling is taken care of automatically. */ -function Options(style, size, color, parentStyle, parentSize) { - this.style = style; - this.color = color; - this.size = size; +function Options(data) { + this.style = data.style; + this.color = data.color; + this.size = data.size; + this.phantom = data.phantom; - if (parentStyle === undefined) { - parentStyle = style; + if (data.parentStyle === undefined) { + this.parentStyle = data.style; + } else { + this.parentStyle = data.parentStyle; } - this.parentStyle = parentStyle; - if (parentSize === undefined) { - parentSize = size; + if (data.parentSize === undefined) { + this.parentSize = data.size; + } else { + this.parentSize = data.parentSize; } - this.parentSize = parentSize; } +/** + * Returns a new options object with the same properties as "this". Properties + * from "extension" will be copied to the new options object. + */ +Options.prototype.extend = function(extension) { + var data = { + style: this.style, + size: this.size, + color: this.color, + parentStyle: this.style, + parentSize: this.size, + phantom: this.phantom + }; + + for (var key in extension) { + if (extension.hasOwnProperty(key)) { + data[key] = extension[key]; + } + } + + return new Options(data); +}; + /** * Create a new options object with the given style. */ Options.prototype.withStyle = function(style) { - return new Options(style, this.size, this.color, this.style, this.size); + return this.extend({ + style: style + }); }; /** * Create a new options object with the given size. */ Options.prototype.withSize = function(size) { - return new Options(this.style, size, this.color, this.style, this.size); + return this.extend({ + size: size + }); }; /** * Create a new options object with the given color. */ Options.prototype.withColor = function(color) { - return new Options(this.style, this.size, color, this.style, this.size); + return this.extend({ + color: color + }); +}; + +/** + * Create a new options object with "phantom" set to true. + */ +Options.prototype.withPhantom = function() { + return this.extend({ + phantom: true + }); }; /** @@ -56,8 +97,7 @@ Options.prototype.withColor = function(color) { * used so that parent style and size changes are handled correctly. */ Options.prototype.reset = function() { - return new Options( - this.style, this.size, this.color, this.style, this.size); + return this.extend({}); }; /** @@ -79,7 +119,11 @@ var colorMap = { * `colorMap`. */ Options.prototype.getColor = function() { - return colorMap[this.color] || this.color; + if (this.phantom) { + return "transparent"; + } else { + return colorMap[this.color] || this.color; + } }; module.exports = Options; diff --git a/src/buildHTML.js b/src/buildHTML.js index 0512767b7..8a4fe9e4a 100644 --- a/src/buildHTML.js +++ b/src/buildHTML.js @@ -175,7 +175,7 @@ var groupTypes = { // things at the end of a \color group. Note that we don't use the same // logic for ordgroups (which count as ords). var prevAtom = prev; - while (prevAtom && prevAtom.type == "color") { + while (prevAtom && prevAtom.type === "color") { var atoms = prevAtom.value.value; prevAtom = atoms[atoms.length - 1]; } @@ -433,15 +433,15 @@ var groupTypes = { // Rule 15d var axisHeight = fontMetrics.metrics.axisHeight; - if ((numShift - numer.depth) - (axisHeight + 0.5 * ruleWidth) - < clearance) { + if ((numShift - numer.depth) - (axisHeight + 0.5 * ruleWidth) < + clearance) { numShift += clearance - ((numShift - numer.depth) - (axisHeight + 0.5 * ruleWidth)); } - if ((axisHeight - 0.5 * ruleWidth) - (denom.height - denomShift) - < clearance) { + if ((axisHeight - 0.5 * ruleWidth) - (denom.height - denomShift) < + clearance) { denomShift += clearance - ((axisHeight - 0.5 * ruleWidth) - (denom.height - denomShift)); @@ -1065,6 +1065,18 @@ var groupTypes = { } else { return accentWrap; } + }, + + phantom: function(group, options, prev) { + var elements = buildExpression( + group.value.value, + options.withPhantom(), + prev + ); + + // \phantom isn't supposed to affect the elements it contains. + // See "color" for more details. + return new buildCommon.makeFragment(elements); } }; @@ -1121,7 +1133,10 @@ var buildHTML = function(tree, settings) { } // Setup the default options - var options = new Options(startStyle, "size5", ""); + var options = new Options({ + style: startStyle, + size: "size5" + }); // Build the expression contained in the tree var expression = buildExpression(tree, options); diff --git a/src/buildMathML.js b/src/buildMathML.js index 3fb89b047..129b26d60 100644 --- a/src/buildMathML.js +++ b/src/buildMathML.js @@ -374,6 +374,11 @@ var groupTypes = { node.setAttribute("width", "0px"); return node; + }, + + phantom: function(group, options, prev) { + var inner = buildExpression(group.value.value); + return new mathMLTree.MathNode("mphantom", inner); } }; diff --git a/src/functions.js b/src/functions.js index 1c3e9b0de..aa8b8eb53 100644 --- a/src/functions.js +++ b/src/functions.js @@ -164,6 +164,23 @@ var functions = { type: "katex" }; } + }, + + "\\phantom": { + numArgs: 1, + handler: function(func, body) { + var inner; + if (body.type === "ordgroup") { + inner = body.value; + } else { + inner = [body]; + } + + return { + type: "phantom", + value: inner + }; + } } }; diff --git a/test/katex-spec.js b/test/katex-spec.js index 6aab20d7d..2042db983 100644 --- a/test/katex-spec.js +++ b/test/katex-spec.js @@ -569,7 +569,6 @@ describe("An over parser", function() { describe("A sizing parser", function() { var sizeExpression = "\\Huge{x}\\small{x}"; - var nestedSizeExpression = "\\Huge{\\small{x}}"; it("should not fail", function() { expect(sizeExpression).toParse(); @@ -1146,6 +1145,45 @@ describe("An accent builder", function() { }); }); +describe("A phantom parser", function() { + it("should not fail", function() { + expect("\\phantom{x}").toParse(); + expect("\\phantom{x^2}").toParse(); + expect("\\phantom{x}^2").toParse(); + expect("\\phantom x").toParse(); + }); + + it("should build a phantom node", function() { + var parse = getParsed("\\phantom{x}")[0]; + + expect(parse.type).toMatch("phantom"); + expect(parse.value.value).toBeDefined(); + }); +}); + +describe("A phantom builder", function() { + it("should not fail", function() { + expect("\\phantom{x}").toBuild(); + expect("\\phantom{x^2}").toBuild(); + expect("\\phantom{x}^2").toBuild(); + expect("\\phantom x").toBuild(); + }); + + it("should make the children transparent", function() { + var children = getBuilt("\\phantom{x+1}")[0].children; + expect(children[0].style.color).toBe("transparent"); + expect(children[1].style.color).toBe("transparent"); + expect(children[2].style.color).toBe("transparent"); + }); + + it("should make all descendants transparent", function() { + var children = getBuilt("\\phantom{x+\\blue{1}}")[0].children; + expect(children[0].style.color).toBe("transparent"); + expect(children[1].style.color).toBe("transparent"); + expect(children[2].children[0].style.color).toBe("transparent"); + }); +}); + describe("A parser error", function () { it("should report the position of an error", function () { try { @@ -1218,4 +1256,9 @@ describe("A MathML builder", function() { var textop = getMathML("\\sin").children[0].children[0]; expect(textop.children[0].type).toEqual("mi"); }); + + it("should generate a node for \\phantom", function() { + var phantom = getMathML("\\phantom{x}").children[0].children[0]; + expect(phantom.children[0].type).toEqual("mphantom"); + }); }); diff --git a/test/screenshotter/images/Phantom-firefox.png b/test/screenshotter/images/Phantom-firefox.png new file mode 100644 index 0000000000000000000000000000000000000000..363cd2463facc4d9db9f339b9f54f18ef54e19a4 GIT binary patch literal 11171 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 literal 0 HcmV?d00001 diff --git a/test/screenshotter/ss_data.json b/test/screenshotter/ss_data.json index 54cac70ae..62ff0cb68 100644 --- a/test/screenshotter/ss_data.json +++ b/test/screenshotter/ss_data.json @@ -21,6 +21,7 @@ "NullDelimiterInteraction": "http://localhost:7936/test/screenshotter/test.html?m=a \\bigl. + 2 \\quad \\left. + a \\right)", "OpLimits": "http://localhost:7936/test/screenshotter/test.html?m={\\sin_2^2 \\lim_2^2 \\int_2^2 \\sum_2^2}{\\displaystyle \\lim_2^2 \\int_2^2 \\intop_2^2 \\sum_2^2}", "Overline": "http://localhost:7936/test/screenshotter/test.html?m=\\overline{x}\\overline{x}\\overline{x^{x^{x^x}}} \\blue{\\overline{y}}", + "Phantom": "http://localhost:7936/test/screenshotter/test.html?m=\\dfrac{1+\\phantom{x^{\\blue{2}}} = x}{1+x^{\\blue{2}} = x}", "PrimeSpacing": "http://localhost:7936/test/screenshotter/test.html?m=f'+f_2'+f^{f'}", "RlapBug": "http://localhost:7936/test/screenshotter/test.html?m=\\frac{\\rlap{x}}{2}", "Rule": "http://localhost:7936/test/screenshotter/test.html?m=\\rule{1em}{0.5em}\\rule{1ex}{2ex}\\rule{1em}{1ex}\\rule{1em}{0.431ex}",