diff --git a/src/Style.js b/src/Style.js index 96cf50d6..5cf2e6f5 100644 --- a/src/Style.js +++ b/src/Style.js @@ -87,6 +87,13 @@ Style.prototype.reset = function() { return resetNames[this.size]; }; +/** + * Return if this style is tightly spaced (scriptstyle/scriptscriptstyle) + */ +Style.prototype.isTight = function() { + return this.size >= 2; +}; + // IDs of the different styles var D = 0; var Dc = 1; diff --git a/src/buildCommon.js b/src/buildCommon.js index d793b5b3..c4ab8ba6 100644 --- a/src/buildCommon.js +++ b/src/buildCommon.js @@ -34,14 +34,16 @@ var mainitLetters = [ * Makes a symbolNode after translation via the list of symbols in symbols.js. * Correctly pulls out metrics for the character, and optionally takes a list of * classes to be attached to the node. + * + * TODO: make argument order closer to makeSpan */ -var makeSymbol = function(value, style, mode, color, classes) { +var makeSymbol = function(value, fontFamily, mode, options, classes) { // Replace the value with its replaced value from symbol.js if (symbols[mode][value] && symbols[mode][value].replace) { value = symbols[mode][value].replace; } - var metrics = fontMetrics.getCharacterMetrics(value, style); + var metrics = fontMetrics.getCharacterMetrics(value, fontFamily); var symbolNode; if (metrics) { @@ -52,12 +54,17 @@ var makeSymbol = function(value, style, mode, color, classes) { // TODO(emily): Figure out a good way to only print this in development typeof console !== "undefined" && console.warn( "No character metrics for '" + value + "' in style '" + - style + "'"); + fontFamily + "'"); symbolNode = new domTree.symbolNode(value, 0, 0, 0, 0, classes); } - if (color) { - symbolNode.style.color = color; + if (options) { + if (options.style.isTight()) { + symbolNode.classes.push("mtight"); + } + if (options.getColor()) { + symbolNode.style.color = options.getColor(); + } } return symbolNode; @@ -67,7 +74,7 @@ var makeSymbol = function(value, style, mode, color, classes) { * Makes a symbol in Main-Regular or AMS-Regular. * Used for rel, bin, open, close, inner, and punct. */ -var mathsym = function(value, mode, color, classes) { +var mathsym = function(value, mode, options, classes) { // Decide what font to render the symbol in by its entry in the symbols // table. // Have a special case for when the value = \ because the \ is used as a @@ -75,22 +82,22 @@ var mathsym = function(value, mode, color, classes) { // text ordinal and is therefore not present as a symbol in the symbols // table for text if (value === "\\" || symbols[mode][value].font === "main") { - return makeSymbol(value, "Main-Regular", mode, color, classes); + return makeSymbol(value, "Main-Regular", mode, options, classes); } else { return makeSymbol( - value, "AMS-Regular", mode, color, classes.concat(["amsrm"])); + value, "AMS-Regular", mode, options, classes.concat(["amsrm"])); } }; /** * Makes a symbol in the default font for mathords and textords. */ -var mathDefault = function(value, mode, color, classes, type) { +var mathDefault = function(value, mode, options, classes, type) { if (type === "mathord") { - return mathit(value, mode, color, classes); + return mathit(value, mode, options, classes); } else if (type === "textord") { return makeSymbol( - value, "Main-Regular", mode, color, classes.concat(["mathrm"])); + value, "Main-Regular", mode, options, classes.concat(["mathrm"])); } else { throw new Error("unexpected type: " + type + " in mathDefault"); } @@ -99,17 +106,17 @@ var mathDefault = function(value, mode, color, classes, type) { /** * Makes a symbol in the italic math font. */ -var mathit = function(value, mode, color, classes) { +var mathit = function(value, mode, options, classes) { if (/[0-9]/.test(value.charAt(0)) || // glyphs for \imath and \jmath do not exist in Math-Italic so we // need to use Main-Italic instead utils.contains(mainitLetters, value) || utils.contains(greekCapitals, value)) { return makeSymbol( - value, "Main-Italic", mode, color, classes.concat(["mainit"])); + value, "Main-Italic", mode, options, classes.concat(["mainit"])); } else { return makeSymbol( - value, "Math-Italic", mode, color, classes.concat(["mathit"])); + value, "Math-Italic", mode, options, classes.concat(["mathit"])); } }; @@ -124,23 +131,22 @@ var makeOrd = function(group, options, type) { } var classes = ["mord"]; - var color = options.getColor(); var font = options.font; if (font) { if (font === "mathit" || utils.contains(mainitLetters, value)) { - return mathit(value, mode, color, classes); + return mathit(value, mode, options, classes); } else { var fontName = fontMap[font].fontName; if (fontMetrics.getCharacterMetrics(value, fontName)) { return makeSymbol( - value, fontName, mode, color, classes.concat([font])); + value, fontName, mode, options, classes.concat([font])); } else { - return mathDefault(value, mode, color, classes, type); + return mathDefault(value, mode, options, classes, type); } } } else { - return mathDefault(value, mode, color, classes, type); + return mathDefault(value, mode, options, classes, type); } }; @@ -173,20 +179,29 @@ var sizeElementFromChildren = function(elem) { }; /** - * Makes a span with the given list of classes, list of children, and color. + * Makes a span with the given list of classes, list of children, and options. + * + * TODO: Ensure that `options` is always provided (currently some call sites + * don't pass it). */ -var makeSpan = function(classes, children, color) { - var span = new domTree.span(classes, children); +var makeSpan = function(classes, children, options) { + var span = new domTree.span(classes, children, options); sizeElementFromChildren(span); - if (color) { - span.style.color = color; - } - return span; }; +/** + * Prepends the given children to the given span, updating height, depth, and + * maxFontSize. + */ +var prependChildren = function(span, children) { + span.children = children.concat(span.children); + + sizeElementFromChildren(span); +}; + /** * Makes a document fragment with the given list of children. */ @@ -447,6 +462,7 @@ module.exports = { makeFragment: makeFragment, makeVList: makeVList, makeOrd: makeOrd, + prependChildren: prependChildren, sizingMultiplier: sizingMultiplier, spacingFunctions: spacingFunctions, }; diff --git a/src/buildHTML.js b/src/buildHTML.js index 9bc8c3a7..61ec1b7c 100644 --- a/src/buildHTML.js +++ b/src/buildHTML.js @@ -17,18 +17,57 @@ var utils = require("./utils"); var makeSpan = buildCommon.makeSpan; +var isSpace = function(node) { + return node instanceof domTree.span && node.classes[0] === "mspace"; +}; + /** * Take a list of nodes, build them in order, and return a list of the built * nodes. This function handles the `prev` node correctly, and passes the - * previous element from the list as the prev of the next element. + * previous element from the list as the prev of the next element, ignoring + * spaces. documentFragments are flattened into their contents, so the + * returned list contains no fragments. */ var buildExpression = function(expression, options, prev) { + // Parse expressions into `groups`. var groups = []; for (var i = 0; i < expression.length; i++) { var group = expression[i]; - groups.push(buildGroup(group, options, prev)); - prev = group; + var output = buildGroup(group, options, prev); + if (output instanceof domTree.documentFragment) { + Array.prototype.push.apply(groups, output.children); + } else { + groups.push(output); + } + if (!isSpace(output)) { + prev = group; + } } + // At this point `groups` consists entirely of `symbolNode`s and `span`s. + + // Explicit spaces (e.g., \;, \,) should be ignored with respect to atom + // spacing (e.g., "add thick space between mord and mrel"). Since CSS + // adjacency rules implement atom spacing, spaces should be invisible to + // CSS. So we splice them out of `groups` and into the atoms themselves. + var spaces = null; + for (i = 0; i < groups.length; i++) { + if (isSpace(groups[i])) { + spaces = spaces || []; + spaces.push(groups[i]); + groups.splice(i, 1); + i--; + } else if (spaces) { + if (groups[i] instanceof domTree.symbolNode) { + groups[i] = makeSpan(groups[i].classes, [groups[i]]); + } + buildCommon.prependChildren(groups[i], spaces); + spaces = null; + } + } + if (spaces) { + Array.prototype.push.apply(groups, spaces); + } + return groups; }; @@ -80,12 +119,13 @@ var getTypeOfGroup = function(group) { return getTypeOfGroup(group.value.base); } else if (group.type === "llap" || group.type === "rlap") { return getTypeOfGroup(group.value); - } else if (group.type === "color") { - return getTypeOfGroup(group.value.value); - } else if (group.type === "sizing") { - return getTypeOfGroup(group.value.value); - } else if (group.type === "styling") { - return getTypeOfGroup(group.value.value); + } else if (group.type === "color" || group.type === "sizing" + || group.type === "styling") { + // Return type of rightmost element of group. + var atoms = group.value.value; + return getTypeOfGroup(atoms[atoms.length - 1]); + } else if (group.type === "font") { + return getTypeOfGroup(group.value.body); } else if (group.type === "delimsizing") { return groupToType[group.value.delimType]; } else { @@ -203,44 +243,46 @@ groupTypes.bin = function(group, options, prev) { } return buildCommon.mathsym( - group.value, group.mode, options.getColor(), [className]); + group.value, group.mode, options, [className]); }; groupTypes.rel = function(group, options, prev) { return buildCommon.mathsym( - group.value, group.mode, options.getColor(), ["mrel"]); + group.value, group.mode, options, ["mrel"]); }; groupTypes.open = function(group, options, prev) { return buildCommon.mathsym( - group.value, group.mode, options.getColor(), ["mopen"]); + group.value, group.mode, options, ["mopen"]); }; groupTypes.close = function(group, options, prev) { return buildCommon.mathsym( - group.value, group.mode, options.getColor(), ["mclose"]); + group.value, group.mode, options, ["mclose"]); }; groupTypes.inner = function(group, options, prev) { return buildCommon.mathsym( - group.value, group.mode, options.getColor(), ["minner"]); + group.value, group.mode, options, ["minner"]); }; groupTypes.punct = function(group, options, prev) { return buildCommon.mathsym( - group.value, group.mode, options.getColor(), ["mpunct"]); + group.value, group.mode, options, ["mpunct"]); }; groupTypes.ordgroup = function(group, options, prev) { return makeSpan( ["mord", options.style.cls()], - buildExpression(group.value, options.reset()) + buildExpression(group.value, options.reset()), + options ); }; groupTypes.text = function(group, options, prev) { - return makeSpan(["text", "mord", options.style.cls()], - buildExpression(group.value.body, options.reset())); + return makeSpan(["mord", "text", options.style.cls()], + buildExpression(group.value.body, options.reset()), + options); }; groupTypes.color = function(group, options, prev) { @@ -274,15 +316,20 @@ groupTypes.supsub = function(group, options, prev) { var sub; var style = options.style; + var newOptions; if (group.value.sup) { - sup = buildGroup(group.value.sup, options.withStyle(style.sup())); - supmid = makeSpan([style.reset(), style.sup().cls()], [sup]); + newOptions = options.withStyle(style.sup()); + sup = buildGroup(group.value.sup, newOptions); + supmid = makeSpan([style.reset(), style.sup().cls()], + [sup], newOptions); } if (group.value.sub) { - sub = buildGroup(group.value.sub, options.withStyle(style.sub())); - submid = makeSpan([style.reset(), style.sub().cls()], [sub]); + newOptions = options.withStyle(style.sub()); + sub = buildGroup(group.value.sub, newOptions); + submid = makeSpan([style.reset(), style.sub().cls()], + [sub], newOptions); } // Rule 18a @@ -376,7 +423,8 @@ groupTypes.supsub = function(group, options, prev) { // We ensure to wrap the supsub vlist in a span.msupsub to reset text-align return makeSpan([getTypeOfGroup(group.value.base)], - [base, makeSpan(["msupsub"], [supsub])]); + [base, makeSpan(["msupsub"], [supsub])], + options); }; groupTypes.genfrac = function(group, options, prev) { @@ -392,12 +440,17 @@ groupTypes.genfrac = function(group, options, prev) { var nstyle = style.fracNum(); var dstyle = style.fracDen(); + var newOptions; - var numer = buildGroup(group.value.numer, options.withStyle(nstyle)); - var numerreset = makeSpan([style.reset(), nstyle.cls()], [numer]); + newOptions = options.withStyle(nstyle); + var numer = buildGroup(group.value.numer, newOptions); + var numerreset = makeSpan([style.reset(), nstyle.cls()], + [numer], newOptions); - var denom = buildGroup(group.value.denom, options.withStyle(dstyle)); - var denomreset = makeSpan([style.reset(), dstyle.cls()], [denom]); + newOptions = options.withStyle(dstyle); + var denom = buildGroup(group.value.denom, newOptions); + var denomreset = makeSpan([style.reset(), dstyle.cls()], + [denom], newOptions); var ruleWidth; if (group.value.hasBarLine) { @@ -510,7 +563,7 @@ groupTypes.genfrac = function(group, options, prev) { return makeSpan( ["mord", options.style.reset(), style.cls()], [leftDelim, makeSpan(["mfrac"], [frac]), rightDelim], - options.getColor()); + options); }; groupTypes.array = function(group, options, prev) { @@ -674,7 +727,7 @@ groupTypes.array = function(group, options, prev) { } } body = makeSpan(["mtable"], cols); - return makeSpan(["mord"], [body], options.getColor()); + return makeSpan(["mord"], [body], options); }; groupTypes.spacing = function(group, options, prev) { @@ -684,14 +737,14 @@ groupTypes.spacing = function(group, options, prev) { // things has an entry in the symbols table, so these will be turned // into appropriate outputs. return makeSpan( - ["mord", "mspace"], + ["mspace"], [buildCommon.mathsym(group.value, group.mode)] ); } else { // Other kinds of spaces are of arbitrary width. We use CSS to // generate these. return makeSpan( - ["mord", "mspace", + ["mspace", buildCommon.spacingFunctions[group.value].className]); } }; @@ -701,7 +754,7 @@ groupTypes.llap = function(group, options, prev) { ["inner"], [buildGroup(group.value.body, options.reset())]); var fix = makeSpan(["fix"], []); return makeSpan( - ["llap", options.style.cls()], [inner, fix]); + ["llap", options.style.cls()], [inner, fix], options); }; groupTypes.rlap = function(group, options, prev) { @@ -709,7 +762,7 @@ groupTypes.rlap = function(group, options, prev) { ["inner"], [buildGroup(group.value.body, options.reset())]); var fix = makeSpan(["fix"], []); return makeSpan( - ["rlap", options.style.cls()], [inner, fix]); + ["rlap", options.style.cls()], [inner, fix], options); }; groupTypes.op = function(group, options, prev) { @@ -750,8 +803,8 @@ groupTypes.op = function(group, options, prev) { // If this is a symbol, create the symbol. var fontName = large ? "Size2-Regular" : "Size1-Regular"; base = buildCommon.makeSymbol( - group.value.body, fontName, "math", options.getColor(), - ["op-symbol", large ? "large-op" : "small-op", "mop"]); + group.value.body, fontName, "math", options, + ["mop", "op-symbol", large ? "large-op" : "small-op"]); // Shift the symbol so its center lies on the axis (rule 13). It // appears that our fonts have the centers of the symbols already @@ -772,7 +825,7 @@ groupTypes.op = function(group, options, prev) { for (var i = 1; i < group.value.body.length; i++) { output.push(buildCommon.mathsym(group.value.body[i], group.mode)); } - base = makeSpan(["mop"], output, options.getColor()); + base = makeSpan(["mop"], output, options); } if (hasLimits) { @@ -784,11 +837,14 @@ groupTypes.op = function(group, options, prev) { var supKern; var submid; var subKern; + var newOptions; // We manually have to handle the superscripts and subscripts. This, // aside from the kern calculations, is copied from supsub. if (supGroup) { - var sup = buildGroup(supGroup, options.withStyle(style.sup())); - supmid = makeSpan([style.reset(), style.sup().cls()], [sup]); + newOptions = options.withStyle(style.sup()); + var sup = buildGroup(supGroup, newOptions); + supmid = makeSpan([style.reset(), style.sup().cls()], + [sup], newOptions); supKern = Math.max( fontMetrics.metrics.bigOpSpacing1, @@ -796,8 +852,10 @@ groupTypes.op = function(group, options, prev) { } if (subGroup) { - var sub = buildGroup(subGroup, options.withStyle(style.sub())); - submid = makeSpan([style.reset(), style.sub().cls()], [sub]); + newOptions = options.withStyle(style.sub()); + var sub = buildGroup(subGroup, newOptions); + submid = makeSpan([style.reset(), style.sub().cls()], + [sub], newOptions); subKern = Math.max( fontMetrics.metrics.bigOpSpacing2, @@ -862,7 +920,7 @@ groupTypes.op = function(group, options, prev) { finalGroup.children[2].style.marginLeft = slant + "em"; } - return makeSpan(["mop", "op-limits"], [finalGroup]); + return makeSpan(["mop", "op-limits"], [finalGroup], options); } else { if (group.value.symbol) { base.style.top = baseShift + "em"; @@ -877,26 +935,26 @@ groupTypes.katex = function(group, options, prev) { // good, but the offsets for the T, E, and X were taken from the // definition of \TeX in TeX (see TeXbook pg. 356) var k = makeSpan( - ["k"], [buildCommon.mathsym("K", group.mode)]); + ["k"], [buildCommon.mathsym("K", group.mode)], options); var a = makeSpan( - ["a"], [buildCommon.mathsym("A", group.mode)]); + ["a"], [buildCommon.mathsym("A", group.mode)], options); a.height = (a.height + 0.2) * 0.75; a.depth = (a.height - 0.2) * 0.75; var t = makeSpan( - ["t"], [buildCommon.mathsym("T", group.mode)]); + ["t"], [buildCommon.mathsym("T", group.mode)], options); var e = makeSpan( - ["e"], [buildCommon.mathsym("E", group.mode)]); + ["e"], [buildCommon.mathsym("E", group.mode)], options); e.height = (e.height - 0.2155); e.depth = (e.depth + 0.2155); var x = makeSpan( - ["x"], [buildCommon.mathsym("X", group.mode)]); + ["x"], [buildCommon.mathsym("X", group.mode)], options); return makeSpan( - ["katex-logo", "mord"], [k, a, t, e, x], options.getColor()); + ["mord", "katex-logo"], [k, a, t, e, x], options); }; groupTypes.overline = function(group, options, prev) { @@ -924,7 +982,7 @@ groupTypes.overline = function(group, options, prev) { {type: "kern", size: ruleWidth}, ], "firstBaseline", null, options); - return makeSpan(["overline", "mord"], [vlist], options.getColor()); + return makeSpan(["mord", "overline"], [vlist], options); }; groupTypes.underline = function(group, options, prev) { @@ -950,7 +1008,7 @@ groupTypes.underline = function(group, options, prev) { {type: "elem", elem: innerGroup}, ], "top", innerGroup.height, options); - return makeSpan(["underline", "mord"], [vlist], options.getColor()); + return makeSpan(["mord", "underline"], [vlist], options); }; groupTypes.sqrt = function(group, options, prev) { @@ -966,7 +1024,7 @@ groupTypes.sqrt = function(group, options, prev) { var line = makeSpan( [style.reset(), Style.TEXT.cls(), "sqrt-line"], [], - options.getColor()); + options); line.height = ruleWidth; line.maxFontSize = 1.0; @@ -985,7 +1043,7 @@ groupTypes.sqrt = function(group, options, prev) { var delim = makeSpan(["sqrt-sign"], [ delimiter.customSizedDelim("\\surd", minDelimiterHeight, false, options, group.mode)], - options.getColor()); + options); var delimDepth = (delim.height + delim.depth) - ruleWidth; @@ -1019,17 +1077,17 @@ groupTypes.sqrt = function(group, options, prev) { } if (!group.value.index) { - return makeSpan(["sqrt", "mord"], [delim, body]); + return makeSpan(["mord", "sqrt"], [delim, body], options); } else { // Handle the optional root index // The index is always in scriptscript style - var root = buildGroup( - group.value.index, - options.withStyle(Style.SCRIPTSCRIPT)); + var newOptions = options.withStyle(Style.SCRIPTSCRIPT); + var root = buildGroup(group.value.index, newOptions); var rootWrap = makeSpan( [style.reset(), Style.SCRIPTSCRIPT.cls()], - [root]); + [root], + newOptions); // Figure out the height and depth of the inner part var innerRootHeight = Math.max(delim.height, body.height); @@ -1047,7 +1105,8 @@ groupTypes.sqrt = function(group, options, prev) { // kerning var rootVListWrap = makeSpan(["root"], [rootVList]); - return makeSpan(["sqrt", "mord"], [rootVListWrap, delim, body]); + return makeSpan(["mord", "sqrt"], + [rootVListWrap, delim, body], options); } }; @@ -1058,17 +1117,28 @@ groupTypes.sizing = function(group, options, prev) { var inner = buildExpression(group.value.value, options.withSize(group.value.size), prev); + // Compute the correct maxFontSize. var style = options.style; - var span = makeSpan(["mord"], - [makeSpan(["sizing", "reset-" + options.size, group.value.size, - style.cls()], - inner)]); - - // Calculate the correct maxFontSize manually var fontSize = buildCommon.sizingMultiplier[group.value.size]; - span.maxFontSize = fontSize * style.sizeMultiplier; + fontSize = fontSize * style.sizeMultiplier; - return span; + // Add size-resetting classes to the inner list and set maxFontSize + // manually. Handle nested size changes. + for (var i = 0; i < inner.length; i++) { + var pos = utils.indexOf(inner[i].classes, "sizing"); + if (pos < 0) { + inner[i].classes.push("sizing", "reset-" + options.size, + group.value.size, style.cls()); + inner[i].maxFontSize = fontSize; + } else if (inner[i].classes[pos + 1] === "reset-" + group.value.size) { + // This is a nested size change: e.g., inner[i] is the "b" in + // `\Huge a \small b`. Override the old size (the `reset-` class) + // but not the new size. + inner[i].classes[pos + 1] = "reset-" + options.size; + } + } + + return buildCommon.makeFragment(inner); }; groupTypes.styling = function(group, options, prev) { @@ -1083,12 +1153,25 @@ groupTypes.styling = function(group, options, prev) { }; var newStyle = styleMap[group.value.style]; + var newOptions = options.withStyle(newStyle); // Build the inner expression in the new style. var inner = buildExpression( - group.value.value, options.withStyle(newStyle), prev); + group.value.value, newOptions, prev); - return makeSpan([options.style.reset(), newStyle.cls()], inner); + // Add style-resetting classes to the inner list. Handle nested changes. + for (var i = 0; i < inner.length; i++) { + var pos = utils.indexOf(inner[i].classes, newStyle.reset()); + if (pos < 0) { + inner[i].classes.push(options.style.reset(), newStyle.cls()); + } else { + // This is a nested style change, as `\textstyle a\scriptstyle b`. + // Only override the old style (the reset class). + inner[i].classes[pos] = options.style.reset(); + } + } + + return new buildCommon.makeFragment(inner); }; groupTypes.font = function(group, options, prev) { @@ -1109,7 +1192,8 @@ groupTypes.delimsizing = function(group, options, prev) { return makeSpan( [groupToType[group.value.delimType]], [delimiter.sizedDelim( - delim, group.value.size, options, group.mode)]); + delim, group.value.size, options, group.mode)], + options); }; groupTypes.leftright = function(group, options, prev) { @@ -1160,12 +1244,12 @@ groupTypes.leftright = function(group, options, prev) { inner.push(rightDelim); return makeSpan( - ["minner", style.cls()], inner, options.getColor()); + ["minner", style.cls()], inner, options); }; groupTypes.rule = function(group, options, prev) { // Make an empty span for the rule - var rule = makeSpan(["mord", "rule"], [], options.getColor()); + var rule = makeSpan(["mord", "rule"], [], options); var style = options.style; // Calculate the shift, width, and height of the rule, and account for units @@ -1208,7 +1292,7 @@ groupTypes.rule = function(group, options, prev) { groupTypes.kern = function(group, options, prev) { // Make an empty span for the rule - var rule = makeSpan(["mord", "rule"], [], options.getColor()); + var rule = makeSpan(["mord", "rule"], [], options); var style = options.style; var dimension = 0; @@ -1290,7 +1374,7 @@ groupTypes.accent = function(group, options, prev) { // Build the accent var accent = buildCommon.makeSymbol( - group.value.accent, "Main-Regular", "math", options.getColor()); + group.value.accent, "Main-Regular", "math", options); // Remove the italic correction of the accent, because it only serves to // shift the accent over to a place we don't want. accent.italic = 0; @@ -1315,7 +1399,7 @@ groupTypes.accent = function(group, options, prev) { // we shift it to the right by 1*skew. accentBody.children[1].style.marginLeft = 2 * skew + "em"; - var accentWrap = makeSpan(["mord", "accent"], [accentBody]); + var accentWrap = makeSpan(["mord", "accent"], [accentBody], options); if (supsubGroup) { // Here, we replace the "base" child of the supsub with our newly @@ -1400,7 +1484,7 @@ var buildHTML = function(tree, options) { // Build the expression contained in the tree var expression = buildExpression(tree, options); - var body = makeSpan(["base", options.style.cls()], expression); + var body = makeSpan(["base", options.style.cls()], expression, options); // Add struts, which ensure that the top of the HTML element falls at the // height of the expression, and the bottom of the HTML element falls at the diff --git a/src/delimiter.js b/src/delimiter.js index b8af478a..3b626210 100644 --- a/src/delimiter.js +++ b/src/delimiter.js @@ -47,8 +47,9 @@ var getMetrics = function(symbol, font) { /** * Builds a symbol in the given font size (note size is an integer) */ -var mathrmSize = function(value, size, mode) { - return buildCommon.makeSymbol(value, "Size" + size + "-Regular", mode); +var mathrmSize = function(value, size, mode, options) { + return buildCommon.makeSymbol(value, "Size" + size + "-Regular", + mode, options); }; /** @@ -57,7 +58,7 @@ var mathrmSize = function(value, size, mode) { */ var styleWrap = function(delim, toStyle, options) { var span = makeSpan( - ["style-wrap", options.style.reset(), toStyle.cls()], [delim]); + ["style-wrap", options.style.reset(), toStyle.cls()], [delim], options); var multiplier = toStyle.sizeMultiplier / options.style.sizeMultiplier; @@ -74,7 +75,7 @@ var styleWrap = function(delim, toStyle, options) { * scriptscriptstyle. */ var makeSmallDelim = function(delim, style, center, options, mode) { - var text = buildCommon.makeSymbol(delim, "Main-Regular", mode); + var text = buildCommon.makeSymbol(delim, "Main-Regular", mode, options); var span = styleWrap(text, style, options); @@ -96,11 +97,11 @@ var makeSmallDelim = function(delim, style, center, options, mode) { * Size3, or Size4 fonts. It is always rendered in textstyle. */ var makeLargeDelim = function(delim, size, center, options, mode) { - var inner = mathrmSize(delim, size, mode); + var inner = mathrmSize(delim, size, mode, options); var span = styleWrap( makeSpan(["delimsizing", "size" + size], - [inner], options.getColor()), + [inner], options), Style.TEXT, options); if (center) { @@ -318,7 +319,7 @@ var makeStackedDelim = function(delim, heightTotal, center, options, mode) { var inner = buildCommon.makeVList(inners, "bottom", depth, options); return styleWrap( - makeSpan(["delimsizing", "mult"], [inner], options.getColor()), + makeSpan(["delimsizing", "mult"], [inner], options), Style.TEXT, options); }; diff --git a/src/domTree.js b/src/domTree.js index 46b515ea..0517b812 100644 --- a/src/domTree.js +++ b/src/domTree.js @@ -30,14 +30,22 @@ var createClass = function(classes) { * an inline style. It also contains information about its height, depth, and * maxFontSize. */ -function span(classes, children, height, depth, maxFontSize, style) { +function span(classes, children, options) { this.classes = classes || []; this.children = children || []; - this.height = height || 0; - this.depth = depth || 0; - this.maxFontSize = maxFontSize || 0; - this.style = style || {}; + this.height = 0; + this.depth = 0; + this.maxFontSize = 0; + this.style = {}; this.attributes = {}; + if (options) { + if (options.style.isTight()) { + this.classes.push("mtight"); + } + if (options.getColor()) { + this.style.color = options.getColor(); + } + } } /** @@ -133,11 +141,11 @@ span.prototype.toMarkup = function() { * contains children and doesn't have any HTML properties. It also keeps track * of a height, depth, and maxFontSize. */ -function documentFragment(children, height, depth, maxFontSize) { +function documentFragment(children) { this.children = children || []; - this.height = height || 0; - this.depth = depth || 0; - this.maxFontSize = maxFontSize || 0; + this.height = 0; + this.depth = 0; + this.maxFontSize = 0; } /** diff --git a/static/katex.less b/static/katex.less index 8b11048a..7b8c293a 100644 --- a/static/katex.less +++ b/static/katex.less @@ -101,111 +101,120 @@ @mediumspace: 0.22222em; @thickspace: 0.27778em; - .textstyle { - > .mord { - & + .mord {} - & + .mop { margin-left: @thinspace; } - & + .mbin { margin-left: @mediumspace; } - & + .mrel { margin-left: @thickspace; } - & + .mopen {} - & + .mclose {} - & + .mpunct {} - & + .minner { margin-left: @thinspace; } - } - - > .mop { - & + .mord { margin-left: @thinspace; } - & + .mop { margin-left: @thinspace; } - & + .mbin {} - & + .mrel { margin-left: @thickspace; } - & + .mopen {} - & + .mclose {} - & + .mpunct {} - & + .minner { margin-left: @thinspace; } - } - - > .mbin { - & + .mord { margin-left: @mediumspace; } - & + .mop { margin-left: @mediumspace; } - & + .mbin {} - & + .mrel {} - & + .mopen { margin-left: @mediumspace; } - & + .mclose {} - & + .mpunct {} - & + .minner { margin-left: @mediumspace; } - } - - > .mrel { - & + .mord { margin-left: @thickspace; } - & + .mop { margin-left: @thickspace; } - & + .mbin {} - & + .mrel {} - & + .mopen { margin-left: @thickspace; } - & + .mclose {} - & + .mpunct {} - & + .minner { margin-left: @thickspace; } - } - - > .mopen { - & + .mord {} - & + .mop {} - & + .mbin {} - & + .mrel {} - & + .mopen {} - & + .mclose {} - & + .mpunct {} - & + .minner {} - } - - > .mclose { - & + .mord {} - & + .mop { margin-left: @thinspace; } - & + .mbin { margin-left: @mediumspace; } - & + .mrel { margin-left: @thickspace; } - & + .mopen {} - & + .mclose {} - & + .mpunct {} - & + .minner { margin-left: @thinspace; } - } - - > .mpunct { - & + .mord { margin-left: @thinspace; } - & + .mop { margin-left: @thinspace; } - & + .mbin {} - & + .mrel { margin-left: @thinspace; } - & + .mopen { margin-left: @thinspace; } - & + .mclose { margin-left: @thinspace; } - & + .mpunct { margin-left: @thinspace; } - & + .minner { margin-left: @thinspace; } - } - - > .minner { - & + .mord { margin-left: @thinspace; } - & + .mop { margin-left: @thinspace; } - & + .mbin { margin-left: @mediumspace; } - & + .mrel { margin-left: @thickspace; } - & + .mopen { margin-left: @thinspace; } - & + .mclose {} - & + .mpunct { margin-left: @thinspace; } - & + .minner { margin-left: @thinspace; } - } - } - + // These spacings apply in textstyle and displaystyle. .mord { + & + .mord {} & + .mop { margin-left: @thinspace; } + & + .mbin { margin-left: @mediumspace; } + & + .mrel { margin-left: @thickspace; } + & + .mopen {} + & + .mclose {} + & + .mpunct {} + & + .minner { margin-left: @thinspace; } } .mop { & + .mord { margin-left: @thinspace; } & + .mop { margin-left: @thinspace; } + & + .mbin {} + & + .mrel { margin-left: @thickspace; } + & + .mopen {} + & + .mclose {} + & + .mpunct {} + & + .minner { margin-left: @thinspace; } + } + + .mbin { + & + .mord { margin-left: @mediumspace; } + & + .mop { margin-left: @mediumspace; } + & + .mbin {} + & + .mrel {} + & + .mopen { margin-left: @mediumspace; } + & + .mclose {} + & + .mpunct {} + & + .minner { margin-left: @mediumspace; } + } + + .mrel { + & + .mord { margin-left: @thickspace; } + & + .mop { margin-left: @thickspace; } + & + .mbin {} + & + .mrel {} + & + .mopen { margin-left: @thickspace; } + & + .mclose {} + & + .mpunct {} + & + .minner { margin-left: @thickspace; } + } + + .mopen { + & + .mord {} + & + .mop {} + & + .mbin {} + & + .mrel {} + & + .mopen {} + & + .mclose {} + & + .mpunct {} + & + .minner {} } .mclose { + & + .mord {} & + .mop { margin-left: @thinspace; } + & + .mbin { margin-left: @mediumspace; } + & + .mrel { margin-left: @thickspace; } + & + .mopen {} + & + .mclose {} + & + .mpunct {} + & + .minner { margin-left: @thinspace; } + } + + .mpunct { + & + .mord { margin-left: @thinspace; } + & + .mop { margin-left: @thinspace; } + & + .mbin {} + & + .mrel { margin-left: @thinspace; } + & + .mopen { margin-left: @thinspace; } + & + .mclose { margin-left: @thinspace; } + & + .mpunct { margin-left: @thinspace; } + & + .minner { margin-left: @thinspace; } } .minner { + & + .mord { margin-left: @thinspace; } & + .mop { margin-left: @thinspace; } + & + .mbin { margin-left: @mediumspace; } + & + .mrel { margin-left: @thickspace; } + & + .mopen { margin-left: @thinspace; } + & + .mclose {} + & + .mpunct { margin-left: @thinspace; } + & + .minner { margin-left: @thinspace; } + } + + // These tighter spacings apply in scriptstyle and scriptscriptstyle. + .mord.mtight { margin-left: 0; } + .mop.mtight { margin-left: 0; } + .mbin.mtight { margin-left: 0; } + .mrel.mtight { margin-left: 0; } + .mopen.mtight { margin-left: 0; } + .mclose.mtight { margin-left: 0; } + .mpunct.mtight { margin-left: 0; } + .minner.mtight { margin-left: 0; } + + .mord { + & + .mop.mtight { margin-left: @thinspace; } + } + + .mop { + & + .mord.mtight { margin-left: @thinspace; } + & + .mop.mtight { margin-left: @thinspace; } + } + + .mclose { + & + .mop.mtight { margin-left: @thinspace; } + } + + .minner { + & + .mop.mtight { margin-left: @thinspace; } } .reset-textstyle.textstyle { font-size: 1em; } diff --git a/test/katex-spec.js b/test/katex-spec.js index 57aad4e5..122c8dbd 100644 --- a/test/katex-spec.js +++ b/test/katex-spec.js @@ -1581,7 +1581,8 @@ describe("A bin builder", function() { it("should correctly interact with color objects", function() { expect(getBuilt("\\blue{x}+y")[1].classes).toContain("mbin"); - expect(getBuilt("\\blue{x+}+y")[1].classes).toContain("mord"); + expect(getBuilt("\\blue{x+}+y")[1].classes).toContain("mbin"); + expect(getBuilt("\\blue{x+}+y")[2].classes).toContain("mord"); }); }); @@ -1695,17 +1696,17 @@ describe("A phantom builder", function() { }); it("should make the children transparent", function() { - var children = getBuilt("\\phantom{x+1}")[0].children; + var children = getBuilt("\\phantom{x+1}"); 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; + var children = getBuilt("\\phantom{x+\\blue{1}}"); expect(children[0].style.color).toBe("transparent"); expect(children[1].style.color).toBe("transparent"); - expect(children[2].children[0].style.color).toBe("transparent"); + expect(children[2].style.color).toBe("transparent"); }); }); diff --git a/test/screenshotter/images/BoldSpacing-chrome.png b/test/screenshotter/images/BoldSpacing-chrome.png new file mode 100644 index 00000000..443e149b Binary files /dev/null and b/test/screenshotter/images/BoldSpacing-chrome.png differ diff --git a/test/screenshotter/images/BoldSpacing-firefox.png b/test/screenshotter/images/BoldSpacing-firefox.png new file mode 100644 index 00000000..022f4ad4 Binary files /dev/null and b/test/screenshotter/images/BoldSpacing-firefox.png differ diff --git a/test/screenshotter/images/ColorSpacing-chrome.png b/test/screenshotter/images/ColorSpacing-chrome.png new file mode 100644 index 00000000..52005442 Binary files /dev/null and b/test/screenshotter/images/ColorSpacing-chrome.png differ diff --git a/test/screenshotter/images/ColorSpacing-firefox.png b/test/screenshotter/images/ColorSpacing-firefox.png new file mode 100644 index 00000000..5eb2d466 Binary files /dev/null and b/test/screenshotter/images/ColorSpacing-firefox.png differ diff --git a/test/screenshotter/images/LimitControls-chrome.png b/test/screenshotter/images/LimitControls-chrome.png index 40f2e63d..1d9a4196 100644 Binary files a/test/screenshotter/images/LimitControls-chrome.png and b/test/screenshotter/images/LimitControls-chrome.png differ diff --git a/test/screenshotter/images/LimitControls-firefox.png b/test/screenshotter/images/LimitControls-firefox.png index db98c978..8184ac23 100644 Binary files a/test/screenshotter/images/LimitControls-firefox.png and b/test/screenshotter/images/LimitControls-firefox.png differ diff --git a/test/screenshotter/images/NegativeSpaceBetweenRel-chrome.png b/test/screenshotter/images/NegativeSpaceBetweenRel-chrome.png new file mode 100644 index 00000000..5428d096 Binary files /dev/null and b/test/screenshotter/images/NegativeSpaceBetweenRel-chrome.png differ diff --git a/test/screenshotter/images/NegativeSpaceBetweenRel-firefox.png b/test/screenshotter/images/NegativeSpaceBetweenRel-firefox.png new file mode 100644 index 00000000..2617807f Binary files /dev/null and b/test/screenshotter/images/NegativeSpaceBetweenRel-firefox.png differ diff --git a/test/screenshotter/images/Sizing-chrome.png b/test/screenshotter/images/Sizing-chrome.png index 53668924..9f57ca86 100644 Binary files a/test/screenshotter/images/Sizing-chrome.png and b/test/screenshotter/images/Sizing-chrome.png differ diff --git a/test/screenshotter/images/Sizing-firefox.png b/test/screenshotter/images/Sizing-firefox.png index 2e36f1ee..7febe8cc 100644 Binary files a/test/screenshotter/images/Sizing-firefox.png and b/test/screenshotter/images/Sizing-firefox.png differ diff --git a/test/screenshotter/images/StyleSwitching-chrome.png b/test/screenshotter/images/StyleSwitching-chrome.png new file mode 100644 index 00000000..9774771e Binary files /dev/null and b/test/screenshotter/images/StyleSwitching-chrome.png differ diff --git a/test/screenshotter/images/StyleSwitching-firefox.png b/test/screenshotter/images/StyleSwitching-firefox.png new file mode 100644 index 00000000..60e45bfe Binary files /dev/null and b/test/screenshotter/images/StyleSwitching-firefox.png differ diff --git a/test/screenshotter/images/SupSubLeftAlignReset-chrome.png b/test/screenshotter/images/SupSubLeftAlignReset-chrome.png index 95fb8a4c..8e3edfe4 100644 Binary files a/test/screenshotter/images/SupSubLeftAlignReset-chrome.png and b/test/screenshotter/images/SupSubLeftAlignReset-chrome.png differ diff --git a/test/screenshotter/images/SupSubLeftAlignReset-firefox.png b/test/screenshotter/images/SupSubLeftAlignReset-firefox.png index 88e7ae73..97ccd63e 100644 Binary files a/test/screenshotter/images/SupSubLeftAlignReset-firefox.png and b/test/screenshotter/images/SupSubLeftAlignReset-firefox.png differ diff --git a/test/screenshotter/ss_data.yaml b/test/screenshotter/ss_data.yaml index 4897096d..ca069065 100644 --- a/test/screenshotter/ss_data.yaml +++ b/test/screenshotter/ss_data.yaml @@ -27,6 +27,7 @@ ArrayType: 1\begin{array}{c}2\\3\end{array}4 Baseline: a+b-c\cdot d/e BasicTest: a BinomTest: \dbinom{a}{b}\tbinom{a}{b}^{\binom{a}{b}+17} +BoldSpacing: \mathbf{A}^2+\mathbf{B}_3*\mathscr{C}' Cases: | f(a,b)=\begin{cases} a+1&\text{if }b\text{ is odd} \\ @@ -36,6 +37,7 @@ Cases: | Colors: tex: \blue{a}\color{#0f0}{b}\color{red}{c} nolatex: different syntax and different scope +ColorSpacing: \color{red}{\displaystyle \int x} + 1 DashesAndQuotes: \text{``a'' b---c -- d----`e'-{-}-f}--``x'' DeepFontSizing: tex: | @@ -82,6 +84,7 @@ MathRm: \mathrm{Ax2k\breve{a}\omega\Omega\imath+\KaTeX} MathSf: \mathsf{Ax2k\breve{a}\omega\Omega\imath+\KaTeX} MathScr: \mathscr{Ax2k\breve{a}\omega\Omega\imath+\KaTeX} MathTt: \mathtt{Ax2k\breve{a}\omega\Omega\imath+\KaTeX} +NegativeSpaceBetweenRel: A =\!= B NestedFractions: | \dfrac{\frac{a}{b}}{\frac{c}{d}}\dfrac{\dfrac{a}{b}} {\dfrac{c}{d}}\frac{\frac{a}{b}}{\frac{c}{d}} @@ -106,6 +109,7 @@ Sqrt: | ^{\sqrt{\sqrt{\sqrt{x}}}}} SqrtRoot: | 1+\sqrt[3]{2}+\sqrt[1923^234]{2^{2^{2^{2^{2^{2^{2^{2^{2^{2^{2^2}}}}}}}}}}} +StyleSwitching: a\cdot b\scriptstyle a\cdot ba\textstyle\cdot ba\scriptstyle\cdot b SupSubCharacterBox: a_2f_2{f}_2{aa}_2{af}_2\mathbf{y}_Ay_A SupSubHorizSpacing: | x^{x^{x}}\Big|x_{x_{x_{x_{x}}}}\bigg|x^{x^{x_{x_{x_{x_{x}}}}}}\bigg|