diff --git a/buildCommon.js b/buildCommon.js index 115b830da..3adbfa7c5 100644 --- 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 index fcaae5c42..c6b376eeb 100644 --- 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; } - 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) { diff --git a/delimiter.js b/delimiter.js index 343c040ac..928ccb72d 100644 --- 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 index 09de80c31..fa890a42b 100644 --- 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 index 0d6e8cac9..b7bcfca27 100644 --- 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 index bcd529f25..118d0168e 100644 --- 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 index 377dafa5e..3ed120313 100644 --- 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 index 8b8f01337..52e69ca9d 100644 --- 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 new file mode 100644 index 000000000..c0e0ca664 Binary files /dev/null and b/test/huxley/OpLimits.hux/firefox-1.png differ diff --git a/test/huxley/OpLimits.hux/record.json b/test/huxley/OpLimits.hux/record.json new file mode 100644 index 000000000..3cae6ac65 --- /dev/null +++ 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 index 4248acba9..c75177ea1 100644 Binary files a/test/huxley/PrimeSpacing.hux/firefox-1.png and b/test/huxley/PrimeSpacing.hux/firefox-1.png differ diff --git a/test/huxley/SupSubCharacterBox.hux/firefox-1.png b/test/huxley/SupSubCharacterBox.hux/firefox-1.png index dd54285d5..43c7771f5 100644 Binary files a/test/huxley/SupSubCharacterBox.hux/firefox-1.png and b/test/huxley/SupSubCharacterBox.hux/firefox-1.png differ diff --git a/test/huxley/SupSubOffsets.hux/firefox-1.png b/test/huxley/SupSubOffsets.hux/firefox-1.png new file mode 100644 index 000000000..dd30c0805 Binary files /dev/null and b/test/huxley/SupSubOffsets.hux/firefox-1.png differ diff --git a/test/huxley/SupSubOffsets.hux/record.json b/test/huxley/SupSubOffsets.hux/record.json new file mode 100644 index 000000000..3cae6ac65 --- /dev/null +++ b/test/huxley/SupSubOffsets.hux/record.json @@ -0,0 +1,5 @@ +[ + { + "action": "screenshot" + } +]