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
This commit is contained in:
Kevin Barabash 2015-03-13 16:24:04 -06:00
parent 51d751f96d
commit 39f5bcb042
7 changed files with 148 additions and 23 deletions

View File

@ -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;

View File

@ -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);

View File

@ -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);
}
};

View File

@ -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
};
}
}
};

View File

@ -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");
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -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}",