commit 39f5bcb04289c87fb5f0826078c0fb583df307e0
parent 51d751f96dfd88ffca2a39d6cfd25ece811be88a
Author: Kevin Barabash <kevinb@khanacademy.org>
Date: Fri, 13 Mar 2015 16:24:04 -0600
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
Diffstat:
7 files changed, 148 insertions(+), 23 deletions(-)
diff --git 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
@@ -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
@@ -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
@@ -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
@@ -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 <mphantom> 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
Binary files differ.
diff --git 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}",