Add limit operators

Summary:
Add support for all of the other operators, including the ones with symbols and
limits. This also fixes the bug where subscripts were shifted the same amount as
subscripts.

To accomplish this, the domTree.textNode has been repurposed into symbolNode
which is no longer an actual text node, but instead represents an element with a
single symbol in it. This lets us access properties like the italic correction
of a symbol in a reasonable manner without having to recursively look through
children of spans.

Depends on D13082

Fixes #8

Test Plan:
 - Make sure tests work
 - Make sure huxley screenshots didn't change much, and new screenshot looks good

Reviewers: alpert

Reviewed By: alpert

Differential Revision: http://phabricator.khanacademy.org/D13122
This commit is contained in:
Emily Eisenberg 2014-09-12 14:58:58 -07:00
parent 29b00ee6b7
commit f52c84c187
14 changed files with 427 additions and 81 deletions

View File

@ -2,40 +2,41 @@ var domTree = require("./domTree");
var fontMetrics = require("./fontMetrics"); var fontMetrics = require("./fontMetrics");
var symbols = require("./symbols"); var symbols = require("./symbols");
var makeText = function(value, style, mode) { var makeSymbol = function(value, style, mode, color, classes) {
if (symbols[mode][value] && symbols[mode][value].replace) { if (symbols[mode][value] && symbols[mode][value].replace) {
value = symbols[mode][value].replace; value = symbols[mode][value].replace;
} }
var metrics = fontMetrics.getCharacterMetrics(value, style); var metrics = fontMetrics.getCharacterMetrics(value, style);
var symbolNode;
if (metrics) { if (metrics) {
var textNode = new domTree.textNode(value, metrics.height, symbolNode = new domTree.symbolNode(
metrics.depth); value, metrics.height, metrics.depth, metrics.italic, classes);
if (metrics.italic > 0) {
var span = makeSpan([], [textNode]);
span.style.marginRight = metrics.italic + "em";
return span;
} else {
return textNode;
}
} else { } else {
console && console.warn("No character metrics for '" + value + console && console.warn("No character metrics for '" + value +
"' in style '" + style + "'"); "' in style '" + style + "'");
return new domTree.textNode(value, 0, 0); symbolNode = new domTree.symbolNode(value, 0, 0, 0, classes);
} }
if (color) {
symbolNode.style.color = color;
}
return symbolNode;
}; };
var mathit = function(value, mode) { var mathit = function(value, mode, color, classes) {
return makeSpan(["mathit"], [makeText(value, "Math-Italic", mode)]); return makeSymbol(
value, "Math-Italic", mode, color, classes.concat(["mathit"]));
}; };
var mathrm = function(value, mode) { var mathrm = function(value, mode, color, classes) {
if (symbols[mode][value].font === "main") { if (symbols[mode][value].font === "main") {
return makeText(value, "Main-Regular", mode); return makeSymbol(value, "Main-Regular", mode, color, classes);
} else { } else {
return makeSpan(["amsrm"], [makeText(value, "AMS-Regular", mode)]); return makeSymbol(
value, "AMS-Regular", mode, color, classes.concat(["amsrm"]));
} }
}; };
@ -84,7 +85,7 @@ var makeFragment = function(children) {
}; };
var makeFontSizer = function(options, fontSize) { var makeFontSizer = function(options, fontSize) {
var fontSizeInner = makeSpan([], [new domTree.textNode("\u200b")]); var fontSizeInner = makeSpan([], [new domTree.symbolNode("\u200b")]);
fontSizeInner.style.fontSize = (fontSize / options.style.sizeMultiplier) + "em"; fontSizeInner.style.fontSize = (fontSize / options.style.sizeMultiplier) + "em";
var fontSizer = makeSpan( var fontSizer = makeSpan(
@ -210,7 +211,7 @@ var makeVList = function(children, positionType, positionData, options) {
// Add in an element at the end with no offset to fix the calculation of // Add in an element at the end with no offset to fix the calculation of
// baselines in some browsers (namely IE, sometimes safari) // baselines in some browsers (namely IE, sometimes safari)
var baselineFix = makeSpan( var baselineFix = makeSpan(
["baseline-fix"], [fontSizer, new domTree.textNode("\u00a0")]); ["baseline-fix"], [fontSizer, new domTree.symbolNode("\u200b")]);
realChildren.push(baselineFix); realChildren.push(baselineFix);
var vlist = makeSpan(["vlist"], realChildren); var vlist = makeSpan(["vlist"], realChildren);
@ -222,7 +223,7 @@ var makeVList = function(children, positionType, positionData, options) {
}; };
module.exports = { module.exports = {
makeText: makeText, makeSymbol: makeSymbol,
mathit: mathit, mathit: mathit,
mathrm: mathrm, mathrm: mathrm,
makeSpan: makeSpan, makeSpan: makeSpan,

View File

@ -34,7 +34,7 @@ var groupToType = {
spacing: "mord", spacing: "mord",
punct: "mpunct", punct: "mpunct",
ordgroup: "mord", ordgroup: "mord",
namedfn: "mop", op: "mop",
katex: "mord", katex: "mord",
overline: "mord", overline: "mord",
rule: "mord", rule: "mord",
@ -81,21 +81,25 @@ var isCharacterBox = function(group) {
} }
}; };
var shouldHandleSupSub = function(group, options) {
if (group == null) {
return false;
} else if (group.type === "op") {
return group.value.limits && options.style.id === Style.DISPLAY.id;
} else {
return null;
}
};
var groupTypes = { var groupTypes = {
mathord: function(group, options, prev) { mathord: function(group, options, prev) {
return makeSpan( return buildCommon.mathit(
["mord"], group.value, group.mode, options.getColor(), ["mord"]);
[buildCommon.mathit(group.value, group.mode)],
options.getColor()
);
}, },
textord: function(group, options, prev) { textord: function(group, options, prev) {
return makeSpan( return buildCommon.mathrm(
["mord"], group.value, group.mode, options.getColor(), ["mord"]);
[buildCommon.mathrm(group.value, group.mode)],
options.getColor()
);
}, },
bin: function(group, options, prev) { bin: function(group, options, prev) {
@ -110,27 +114,28 @@ var groupTypes = {
group.type = "ord"; group.type = "ord";
className = "mord"; className = "mord";
} }
return makeSpan(
[className], return buildCommon.mathrm(
[buildCommon.mathrm(group.value, group.mode)], group.value, group.mode, options.getColor(), [className]);
options.getColor()
);
}, },
rel: function(group, options, prev) { rel: function(group, options, prev) {
return makeSpan( return buildCommon.mathrm(
["mrel"], group.value, group.mode, options.getColor(), ["mrel"]);
[buildCommon.mathrm(group.value, group.mode)],
options.getColor()
);
}, },
text: function(group, options, prev) { text: function(group, options, prev) {
return makeSpan(["text mord", options.style.cls()], return makeSpan(["text", "mord", options.style.cls()],
buildExpression(group.value.body, options.reset())); buildExpression(group.value.body, options.reset()));
}, },
supsub: function(group, options, prev) { supsub: function(group, options, prev) {
var baseGroup = group.value.base;
if (shouldHandleSupSub(group.value.base, options)) {
return groupTypes[group.value.base.type](group, options, prev);
}
var base = buildGroup(group.value.base, options.reset()); var base = buildGroup(group.value.base, options.reset());
if (group.value.sup) { if (group.value.sup) {
@ -181,6 +186,10 @@ var groupTypes = {
], "shift", v, options); ], "shift", v, options);
supsub.children[0].style.marginRight = scriptspace; supsub.children[0].style.marginRight = scriptspace;
if (base instanceof domTree.symbolNode) {
supsub.children[0].style.marginLeft = -base.italic + "em";
}
} else if (!group.value.sub) { } else if (!group.value.sub) {
u = Math.max(u, p, u = Math.max(u, p,
sup.depth + 0.25 * fontMetrics.metrics.xHeight); sup.depth + 0.25 * fontMetrics.metrics.xHeight);
@ -211,6 +220,11 @@ var groupTypes = {
{type: "elem", elem: supmid, shift: -u} {type: "elem", elem: supmid, shift: -u}
], "individualShift", null, options); ], "individualShift", null, options);
if (base instanceof domTree.symbolNode) {
supsub.children[1].style.marginLeft = base.italic + "em";
base.italic = 0;
}
supsub.children[0].style.marginRight = scriptspace; supsub.children[0].style.marginRight = scriptspace;
supsub.children[1].style.marginRight = scriptspace; supsub.children[1].style.marginRight = scriptspace;
} }
@ -220,19 +234,13 @@ var groupTypes = {
}, },
open: function(group, options, prev) { open: function(group, options, prev) {
return makeSpan( return buildCommon.mathrm(
["mopen"], group.value, group.mode, options.getColor(), ["mopen"]);
[buildCommon.mathrm(group.value, group.mode)],
options.getColor()
);
}, },
close: function(group, options, prev) { close: function(group, options, prev) {
return makeSpan( return buildCommon.mathrm(
["mclose"], group.value, group.mode, options.getColor(), ["mclose"]);
[buildCommon.mathrm(group.value, group.mode)],
options.getColor()
);
}, },
frac: function(group, options, prev) { frac: function(group, options, prev) {
@ -344,11 +352,8 @@ var groupTypes = {
}, },
punct: function(group, options, prev) { punct: function(group, options, prev) {
return makeSpan( return buildCommon.mathrm(
["mpunct"], group.value, group.mode, options.getColor(), ["mpunct"]);
[buildCommon.mathrm(group.value, group.mode)],
options.getColor()
);
}, },
ordgroup: function(group, options, prev) { ordgroup: function(group, options, prev) {
@ -358,13 +363,129 @@ var groupTypes = {
); );
}, },
namedfn: function(group, options, prev) { op: function(group, options, prev) {
var chars = []; var supGroup;
for (var i = 1; i < group.value.body.length; i++) { var subGroup;
chars.push(buildCommon.mathrm(group.value.body[i], group.mode)); var hasLimits = false;
if (group.type === "supsub" ) {
supGroup = group.value.sup;
subGroup = group.value.sub;
group = group.value.base;
hasLimits = true;
} }
return makeSpan(["mop"], chars, options.getColor()); // Most operators have a large successor symbol, but these don't.
var noSuccessor = [
"\\smallint"
];
var large = false;
if (options.style.id === Style.DISPLAY.id &&
group.value.symbol &&
!utils.contains(noSuccessor, group.value.body)) {
// Make symbols larger in displaystyle, except for smallint
large = true;
}
var base;
var baseShift = 0;
var delta = 0;
if (group.value.symbol) {
var style = large ? "Size2-Regular" : "Size1-Regular";
base = buildCommon.makeSymbol(
group.value.body, style, "math", options.getColor(),
["op-symbol", large ? "large-op" : "small-op", "mop"]);
baseShift = (base.height - base.depth) / 2 -
fontMetrics.metrics.axisHeight *
options.style.sizeMultiplier;
delta = base.italic;
} else {
var output = [];
for (var i = 1; i < group.value.body.length; i++) {
output.push(buildCommon.mathrm(group.value.body[i], group.mode));
}
base = makeSpan(["mop"], output, options.getColor());
}
if (hasLimits) {
if (supGroup) {
var sup = buildGroup(supGroup,
options.withStyle(options.style.sup()));
var supmid = makeSpan(
[options.style.reset(), options.style.sup().cls()], [sup]);
var supKern = Math.max(
fontMetrics.metrics.bigOpSpacing1,
fontMetrics.metrics.bigOpSpacing3 - sup.depth);
}
if (subGroup) {
var sub = buildGroup(subGroup,
options.withStyle(options.style.sub()));
var submid = makeSpan(
[options.style.reset(), options.style.sub().cls()], [sub]);
var subKern = Math.max(
fontMetrics.metrics.bigOpSpacing2,
fontMetrics.metrics.bigOpSpacing4 - sub.height);
}
var finalGroup;
if (!supGroup) {
var top = base.height - baseShift;
finalGroup = buildCommon.makeVList([
{type: "kern", size: fontMetrics.metrics.bigOpSpacing5},
{type: "elem", elem: submid},
{type: "kern", size: subKern},
{type: "elem", elem: base}
], "top", top, options);
finalGroup.children[0].style.marginLeft = -delta + "em";
} else if (!subGroup) {
var bottom = base.depth + baseShift;
finalGroup = buildCommon.makeVList([
{type: "elem", elem: base},
{type: "kern", size: supKern},
{type: "elem", elem: supmid},
{type: "kern", size: fontMetrics.metrics.bigOpSpacing5}
], "bottom", bottom, options);
finalGroup.children[1].style.marginLeft = delta + "em";
} else if (!supGroup && !subGroup) {
return base;
} else {
var bottom = fontMetrics.metrics.bigOpSpacing5 +
submid.height + submid.depth +
subKern +
base.depth + baseShift;
finalGroup = buildCommon.makeVList([
{type: "kern", size: fontMetrics.metrics.bigOpSpacing5},
{type: "elem", elem: submid},
{type: "kern", size: subKern},
{type: "elem", elem: base},
{type: "kern", size: supKern},
{type: "elem", elem: supmid},
{type: "kern", size: fontMetrics.metrics.bigOpSpacing5}
], "bottom", bottom, options);
finalGroup.children[0].style.marginLeft = -delta + "em";
finalGroup.children[2].style.marginLeft = delta + "em";
}
return makeSpan(["mop", "op-limits"], [finalGroup]);
} else {
if (group.value.symbol) {
base.style.top = baseShift + "em";
}
return base;
}
}, },
katex: function(group, options, prev) { katex: function(group, options, prev) {

View File

@ -23,7 +23,7 @@ var getMetrics = function(symbol, font) {
}; };
var mathrmSize = function(value, size, mode) { var mathrmSize = function(value, size, mode) {
return buildCommon.makeText(value, "Size" + size + "-Regular", mode); return buildCommon.makeSymbol(value, "Size" + size + "-Regular", mode);
}; };
var styleWrap = function(delim, toStyle, options) { var styleWrap = function(delim, toStyle, options) {
@ -39,7 +39,7 @@ var styleWrap = function(delim, toStyle, options) {
}; };
var makeSmallDelim = function(delim, style, center, options, mode) { var makeSmallDelim = function(delim, style, center, options, mode) {
var text = buildCommon.makeText(delim, "Main-Regular", mode); var text = buildCommon.makeSymbol(delim, "Main-Regular", mode);
var span = styleWrap(text, style, options); var span = styleWrap(text, style, options);
@ -87,7 +87,7 @@ var makeInner = function(symbol, font, mode) {
var inner = makeSpan( var inner = makeSpan(
["delimsizinginner", sizeClass], ["delimsizinginner", sizeClass],
[makeSpan([], [buildCommon.makeText(symbol, font, mode)])]); [makeSpan([], [buildCommon.makeSymbol(symbol, font, mode)])]);
return {type: "elem", elem: inner}; return {type: "elem", elem: inner};
}; };

View File

@ -3,6 +3,17 @@
// function. They are useful for both storing extra properties on the nodes, as // function. They are useful for both storing extra properties on the nodes, as
// well as providing a way to easily work with the DOM. // well as providing a way to easily work with the DOM.
var createClass = function(classes) {
classes = classes.slice();
for (var i = classes.length - 1; i >= 0; i--) {
if (!classes[i]) {
classes.splice(i, 1);
}
}
return classes.join(" ");
};
function span(classes, children, height, depth, maxFontSize, style) { function span(classes, children, height, depth, maxFontSize, style) {
this.classes = classes || []; this.classes = classes || [];
this.children = children || []; this.children = children || [];
@ -15,14 +26,7 @@ function span(classes, children, height, depth, maxFontSize, style) {
span.prototype.toDOM = function() { span.prototype.toDOM = function() {
var span = document.createElement("span"); var span = document.createElement("span");
var classes = this.classes.slice(); span.className = createClass(this.classes);
for (var i = classes.length - 1; i >= 0; i--) {
if (!classes[i]) {
classes.splice(i, 1);
}
}
span.className = classes.join(" ");
for (var style in this.style) { for (var style in this.style) {
if (this.style.hasOwnProperty(style)) { if (this.style.hasOwnProperty(style)) {
@ -54,18 +58,46 @@ documentFragment.prototype.toDOM = function() {
return frag; return frag;
}; };
function textNode(value, height, depth) { function symbolNode(value, height, depth, italic, classes, style) {
this.value = value || ""; this.value = value || "";
this.height = height || 0; this.height = height || 0;
this.depth = depth || 0; this.depth = depth || 0;
this.italic = italic || 0;
this.classes = classes || [];
this.style = style || {};
} }
textNode.prototype.toDOM = function() { symbolNode.prototype.toDOM = function() {
return document.createTextNode(this.value); var node = document.createTextNode(this.value);
var span = null;
if (this.italic > 0) {
span = document.createElement("span");
span.style.marginRight = this.italic + "em";
}
if (this.classes.length > 0) {
span = span || document.createElement("span");
span.className = createClass(this.classes);
}
for (var style in this.style) {
if (this.style.hasOwnProperty(style)) {
span = span || document.createElement("span");
span.style[style] = this.style[style];
}
}
if (span) {
span.appendChild(node);
return span;
} else {
return node;
}
}; };
module.exports = { module.exports = {
span: span, span: span,
documentFragment: documentFragment, documentFragment: documentFragment,
textNode: textNode symbolNode: symbolNode
}; };

View File

@ -221,7 +221,11 @@ var duplicatedFunctions = [
} }
}, },
// No-argument mathy operators // There are 2 flags for operators; whether they produce limits in
// displaystyle, and whether they are symbols and should grow in
// displaystyle. These four groups cover the four possible choices.
// No limits, not symbols
{ {
funcs: [ funcs: [
"\\arcsin", "\\arccos", "\\arctan", "\\arg", "\\cos", "\\cosh", "\\arcsin", "\\arccos", "\\arctan", "\\arg", "\\cos", "\\cosh",
@ -233,7 +237,66 @@ var duplicatedFunctions = [
numArgs: 0, numArgs: 0,
handler: function(func) { handler: function(func) {
return { return {
type: "namedfn", type: "op",
limits: false,
symbol: false,
body: func
};
}
}
},
// Limits, not symbols
{
funcs: [
"\\det", "\\gcd", "\\inf", "\\lim", "\\liminf", "\\limsup", "\\max",
"\\min", "\\Pr", "\\sup"
],
data: {
numArgs: 0,
handler: function(func) {
return {
type: "op",
limits: true,
symbol: false,
body: func
};
}
}
},
// No limits, symbols
{
funcs: [
"\\int", "\\iint", "\\iiint", "\\oint"
],
data: {
numArgs: 0,
handler: function(func) {
return {
type: "op",
limits: false,
symbol: true,
body: func
};
}
}
},
// Limits, symbols
{
funcs: [
"\\coprod", "\\bigvee", "\\bigwedge", "\\biguplus", "\\bigcap",
"\\bigcup", "\\intop", "\\prod", "\\sum", "\\bigotimes",
"\\bigoplus", "\\bigodot", "\\bigsqcup", "\\smallint"
],
data: {
numArgs: 0,
handler: function(func) {
return {
type: "op",
limits: true,
symbol: true,
body: func body: func
}; };
} }

View File

@ -385,10 +385,10 @@ big parens
&.mult { &.mult {
.delim-size1 > span { .delim-size1 > span {
font-family: Katex_Size1; font-family: KaTeX_Size1;
} }
.delim-size4 > span { .delim-size4 > span {
font-family: Katex_Size4; font-family: KaTeX_Size4;
} }
} }
} }
@ -397,4 +397,21 @@ big parens
display: inline-block; display: inline-block;
width: @nulldelimiterspace; width: @nulldelimiterspace;
} }
.op-symbol {
position: relative;
&.small-op {
font-family: KaTeX_Size1;
}
&.large-op {
font-family: KaTeX_Size2;
}
}
.op-limits {
> .vlist > span {
text-align: center;
}
}
} }

View File

@ -673,6 +673,96 @@ var symbols = {
font: "main", font: "main",
group: "textord", group: "textord",
replace: "\u21d5" replace: "\u21d5"
},
"\\coprod": {
font: "math",
group: "op",
replace: "\u2210"
},
"\\bigvee": {
font: "math",
group: "op",
replace: "\u22c1"
},
"\\bigwedge": {
font: "math",
group: "op",
replace: "\u22c0"
},
"\\biguplus": {
font: "math",
group: "op",
replace: "\u2a04"
},
"\\bigcap": {
font: "math",
group: "op",
replace: "\u22c2"
},
"\\bigcup": {
font: "math",
group: "op",
replace: "\u22c3"
},
"\\int": {
font: "math",
group: "op",
replace: "\u222b"
},
"\\intop": {
font: "math",
group: "op",
replace: "\u222b"
},
"\\iint": {
font: "math",
group: "op",
replace: "\u222c"
},
"\\iiint": {
font: "math",
group: "op",
replace: "\u222d"
},
"\\prod": {
font: "math",
group: "op",
replace: "\u220f"
},
"\\sum": {
font: "math",
group: "op",
replace: "\u2211"
},
"\\bigotimes": {
font: "math",
group: "op",
replace: "\u2a02"
},
"\\bigoplus": {
font: "math",
group: "op",
replace: "\u2a01"
},
"\\bigodot": {
font: "math",
group: "op",
replace: "\u2a00"
},
"\\oint": {
font: "math",
group: "op",
replace: "\u222e"
},
"\\bigsqcup": {
font: "math",
group: "op",
replace: "\u2a06"
},
"\\smallint": {
font: "math",
group: "op",
replace: "\u222b"
} }
}, },
"text": { "text": {

View File

@ -165,5 +165,17 @@
"name": "DisplayStyle", "name": "DisplayStyle",
"screenSize": [1024, 768], "screenSize": [1024, 768],
"url": "http://localhost:7936/test/huxley/test.html?m={\\displaystyle\\sqrt{x}}{\\sqrt{x}}{\\displaystyle \\frac12}{\\frac12}{\\displaystyle x^1_2}{x^1_2}" "url": "http://localhost:7936/test/huxley/test.html?m={\\displaystyle\\sqrt{x}}{\\sqrt{x}}{\\displaystyle \\frac12}{\\frac12}{\\displaystyle x^1_2}{x^1_2}"
},
{
"name": "OpLimits",
"screenSize": [1024, 768],
"url": "http://localhost:7936/test/huxley/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}"
},
{
"name": "SupSubOffsets",
"screenSize": [1024, 768],
"url": "http://localhost:7936/test/huxley/test.html?m=\\displaystyle \\int_{2+3}x f^{2+3}+3\\lim_{2+3+4+5}f"
} }
] ]

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View File

@ -0,0 +1,5 @@
[
{
"action": "screenshot"
}
]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

@ -0,0 +1,5 @@
[
{
"action": "screenshot"
}
]