www

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

commit f52c84c18766587f0c735a9b234bde8022ae3efc
parent 29b00ee6b7bfa02ae57e47b57167f339f8bb8bda
Author: Emily Eisenberg <emily@khanacademy.org>
Date:   Fri, 12 Sep 2014 14:58:58 -0700

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

Diffstat:
MbuildCommon.js | 41+++++++++++++++++++++--------------------
MbuildTree.js | 205+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
Mdelimiter.js | 6+++---
MdomTree.js | 56++++++++++++++++++++++++++++++++++++++++++++------------
Mfunctions.js | 67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mstatic/katex.less | 21+++++++++++++++++++--
Msymbols.js | 90+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/huxley/Huxleyfile.json | 12++++++++++++
Atest/huxley/OpLimits.hux/firefox-1.png | 0
Atest/huxley/OpLimits.hux/record.json | 5+++++
Mtest/huxley/PrimeSpacing.hux/firefox-1.png | 0
Mtest/huxley/SupSubCharacterBox.hux/firefox-1.png | 0
Atest/huxley/SupSubOffsets.hux/firefox-1.png | 0
Atest/huxley/SupSubOffsets.hux/record.json | 5+++++
14 files changed, 427 insertions(+), 81 deletions(-)

diff --git a/buildCommon.js b/buildCommon.js @@ -2,40 +2,41 @@ var domTree = require("./domTree"); var fontMetrics = require("./fontMetrics"); 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) { value = symbols[mode][value].replace; } var metrics = fontMetrics.getCharacterMetrics(value, style); + var symbolNode; if (metrics) { - var textNode = new domTree.textNode(value, metrics.height, - metrics.depth); - if (metrics.italic > 0) { - var span = makeSpan([], [textNode]); - span.style.marginRight = metrics.italic + "em"; - - return span; - } else { - return textNode; - } + symbolNode = new domTree.symbolNode( + value, metrics.height, metrics.depth, metrics.italic, classes); } else { console && console.warn("No character metrics for '" + value + "' 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) { - return makeSpan(["mathit"], [makeText(value, "Math-Italic", mode)]); +var mathit = function(value, mode, color, classes) { + 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") { - return makeText(value, "Main-Regular", mode); + return makeSymbol(value, "Main-Regular", mode, color, classes); } 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 fontSizeInner = makeSpan([], [new domTree.textNode("\u200b")]); + var fontSizeInner = makeSpan([], [new domTree.symbolNode("\u200b")]); fontSizeInner.style.fontSize = (fontSize / options.style.sizeMultiplier) + "em"; 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 // baselines in some browsers (namely IE, sometimes safari) var baselineFix = makeSpan( - ["baseline-fix"], [fontSizer, new domTree.textNode("\u00a0")]); + ["baseline-fix"], [fontSizer, new domTree.symbolNode("\u200b")]); realChildren.push(baselineFix); var vlist = makeSpan(["vlist"], realChildren); @@ -222,7 +223,7 @@ var makeVList = function(children, positionType, positionData, options) { }; module.exports = { - makeText: makeText, + makeSymbol: makeSymbol, mathit: mathit, mathrm: mathrm, makeSpan: makeSpan, diff --git a/buildTree.js b/buildTree.js @@ -34,7 +34,7 @@ var groupToType = { spacing: "mord", punct: "mpunct", ordgroup: "mord", - namedfn: "mop", + op: "mop", katex: "mord", overline: "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 = { mathord: function(group, options, prev) { - return makeSpan( - ["mord"], - [buildCommon.mathit(group.value, group.mode)], - options.getColor() - ); + return buildCommon.mathit( + group.value, group.mode, options.getColor(), ["mord"]); }, textord: function(group, options, prev) { - return makeSpan( - ["mord"], - [buildCommon.mathrm(group.value, group.mode)], - options.getColor() - ); + return buildCommon.mathrm( + group.value, group.mode, options.getColor(), ["mord"]); }, bin: function(group, options, prev) { @@ -110,27 +114,28 @@ var groupTypes = { group.type = "ord"; className = "mord"; } - return makeSpan( - [className], - [buildCommon.mathrm(group.value, group.mode)], - options.getColor() - ); + + return buildCommon.mathrm( + group.value, group.mode, options.getColor(), [className]); }, rel: function(group, options, prev) { - return makeSpan( - ["mrel"], - [buildCommon.mathrm(group.value, group.mode)], - options.getColor() - ); + return buildCommon.mathrm( + group.value, group.mode, options.getColor(), ["mrel"]); }, 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())); }, 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()); if (group.value.sup) { @@ -181,6 +186,10 @@ var groupTypes = { ], "shift", v, options); supsub.children[0].style.marginRight = scriptspace; + + if (base instanceof domTree.symbolNode) { + supsub.children[0].style.marginLeft = -base.italic + "em"; + } } else if (!group.value.sub) { u = Math.max(u, p, sup.depth + 0.25 * fontMetrics.metrics.xHeight); @@ -211,6 +220,11 @@ var groupTypes = { {type: "elem", elem: supmid, shift: -u} ], "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[1].style.marginRight = scriptspace; } @@ -220,19 +234,13 @@ var groupTypes = { }, open: function(group, options, prev) { - return makeSpan( - ["mopen"], - [buildCommon.mathrm(group.value, group.mode)], - options.getColor() - ); + return buildCommon.mathrm( + group.value, group.mode, options.getColor(), ["mopen"]); }, close: function(group, options, prev) { - return makeSpan( - ["mclose"], - [buildCommon.mathrm(group.value, group.mode)], - options.getColor() - ); + return buildCommon.mathrm( + group.value, group.mode, options.getColor(), ["mclose"]); }, frac: function(group, options, prev) { @@ -344,11 +352,8 @@ var groupTypes = { }, punct: function(group, options, prev) { - return makeSpan( - ["mpunct"], - [buildCommon.mathrm(group.value, group.mode)], - options.getColor() - ); + return buildCommon.mathrm( + group.value, group.mode, options.getColor(), ["mpunct"]); }, ordgroup: function(group, options, prev) { @@ -358,13 +363,129 @@ var groupTypes = { ); }, - namedfn: function(group, options, prev) { - var chars = []; - for (var i = 1; i < group.value.body.length; i++) { - chars.push(buildCommon.mathrm(group.value.body[i], group.mode)); + op: function(group, options, prev) { + var supGroup; + var subGroup; + var hasLimits = false; + if (group.type === "supsub" ) { + supGroup = group.value.sup; + subGroup = group.value.sub; + group = group.value.base; + hasLimits = true; + } + + // 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; } - return makeSpan(["mop"], chars, options.getColor()); + 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) { diff --git a/delimiter.js b/delimiter.js @@ -23,7 +23,7 @@ var getMetrics = function(symbol, font) { }; 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) { @@ -39,7 +39,7 @@ var styleWrap = function(delim, toStyle, options) { }; 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); @@ -87,7 +87,7 @@ var makeInner = function(symbol, font, mode) { var inner = makeSpan( ["delimsizinginner", sizeClass], - [makeSpan([], [buildCommon.makeText(symbol, font, mode)])]); + [makeSpan([], [buildCommon.makeSymbol(symbol, font, mode)])]); return {type: "elem", elem: inner}; }; diff --git a/domTree.js b/domTree.js @@ -3,6 +3,17 @@ // function. They are useful for both storing extra properties on the nodes, as // 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) { this.classes = classes || []; this.children = children || []; @@ -15,14 +26,7 @@ function span(classes, children, height, depth, maxFontSize, style) { span.prototype.toDOM = function() { var span = document.createElement("span"); - var classes = this.classes.slice(); - for (var i = classes.length - 1; i >= 0; i--) { - if (!classes[i]) { - classes.splice(i, 1); - } - } - - span.className = classes.join(" "); + span.className = createClass(this.classes); for (var style in this.style) { if (this.style.hasOwnProperty(style)) { @@ -54,18 +58,46 @@ documentFragment.prototype.toDOM = function() { return frag; }; -function textNode(value, height, depth) { +function symbolNode(value, height, depth, italic, classes, style) { this.value = value || ""; this.height = height || 0; this.depth = depth || 0; + this.italic = italic || 0; + this.classes = classes || []; + this.style = style || {}; } -textNode.prototype.toDOM = function() { - return document.createTextNode(this.value); +symbolNode.prototype.toDOM = function() { + 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 = { span: span, documentFragment: documentFragment, - textNode: textNode + symbolNode: symbolNode }; diff --git a/functions.js b/functions.js @@ -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: [ "\\arcsin", "\\arccos", "\\arctan", "\\arg", "\\cos", "\\cosh", @@ -233,7 +237,66 @@ var duplicatedFunctions = [ numArgs: 0, handler: function(func) { 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 }; } diff --git a/static/katex.less b/static/katex.less @@ -385,10 +385,10 @@ big parens &.mult { .delim-size1 > span { - font-family: Katex_Size1; + font-family: KaTeX_Size1; } .delim-size4 > span { - font-family: Katex_Size4; + font-family: KaTeX_Size4; } } } @@ -397,4 +397,21 @@ big parens display: inline-block; 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; + } + } } diff --git a/symbols.js b/symbols.js @@ -673,6 +673,96 @@ var symbols = { font: "main", group: "textord", 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": { diff --git a/test/huxley/Huxleyfile.json b/test/huxley/Huxleyfile.json @@ -165,5 +165,17 @@ "name": "DisplayStyle", "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}" + }, + + { + "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" } ] diff --git a/test/huxley/OpLimits.hux/firefox-1.png b/test/huxley/OpLimits.hux/firefox-1.png Binary files differ. diff --git a/test/huxley/OpLimits.hux/record.json b/test/huxley/OpLimits.hux/record.json @@ -0,0 +1,5 @@ +[ + { + "action": "screenshot" + } +] diff --git a/test/huxley/PrimeSpacing.hux/firefox-1.png b/test/huxley/PrimeSpacing.hux/firefox-1.png Binary files differ. diff --git a/test/huxley/SupSubCharacterBox.hux/firefox-1.png b/test/huxley/SupSubCharacterBox.hux/firefox-1.png Binary files differ. diff --git a/test/huxley/SupSubOffsets.hux/firefox-1.png b/test/huxley/SupSubOffsets.hux/firefox-1.png Binary files differ. diff --git a/test/huxley/SupSubOffsets.hux/record.json b/test/huxley/SupSubOffsets.hux/record.json @@ -0,0 +1,5 @@ +[ + { + "action": "screenshot" + } +]