KaTeX/src/buildHTML.js
Eddie Kohler 6bb62b11b4 Support \mathop, \mathrel, \mathbin, \mathpunct, etc. class commands.
These commands set their arguments in a given TeX math class. Use
the existing "op" type for \mathop (to support \limits); introduce
a new "mclass" type for the other classes.

Fixes #482. Tests borrowed from #485 (cbreeden).
2016-12-05 15:48:05 -05:00

1490 lines
50 KiB
JavaScript

/* eslint no-console:0 */
/**
* This file does the main work of building a domTree structure from a parse
* tree. The entry point is the `buildHTML` function, which takes a parse tree.
* Then, the buildExpression, buildGroup, and various groupTypes functions are
* called, to produce a final HTML tree.
*/
var ParseError = require("./ParseError");
var Style = require("./Style");
var buildCommon = require("./buildCommon");
var delimiter = require("./delimiter");
var domTree = require("./domTree");
var fontMetrics = require("./fontMetrics");
var utils = require("./utils");
var makeSpan = buildCommon.makeSpan;
var isSpace = function(node) {
return node instanceof domTree.span && node.classes[0] === "mspace";
};
// Binary atoms (first class `mbin`) change into ordinary atoms (`mord`)
// depending on their surroundings. See TeXbook pg. 442-446, Rules 5 and 6,
// and the text before Rule 19.
var isBin = function(node) {
return node && node.classes[0] === "mbin";
};
var isBinLeftCanceller = function(node, isRealGroup) {
// TODO: This code assumes that a node's math class is the first element
// of its `classes` array. A later cleanup should ensure this, for
// instance by changing the signature of `makeSpan`.
if (node) {
return utils.contains(["mbin", "mopen", "mrel", "mop", "mpunct"],
node.classes[0]);
} else {
return isRealGroup;
}
};
var isBinRightCanceller = function(node, isRealGroup) {
if (node) {
return utils.contains(["mrel", "mclose", "mpunct"], node.classes[0]);
} else {
return isRealGroup;
}
};
/**
* Take a list of nodes, build them in order, and return a list of the built
* nodes. documentFragments are flattened into their contents, so the
* returned list contains no fragments. `isRealGroup` is true if `expression`
* is a real group (no atoms will be added on either side), as opposed to
* a partial group (e.g. one created by \color).
*/
var buildExpression = function(expression, options, isRealGroup) {
// Parse expressions into `groups`.
var groups = [];
for (var i = 0; i < expression.length; i++) {
var group = expression[i];
var output = buildGroup(group, options);
if (output instanceof domTree.documentFragment) {
Array.prototype.push.apply(groups, output.children);
} else {
groups.push(output);
}
}
// 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);
}
// Binary operators change to ordinary symbols in some contexts.
for (i = 0; i < groups.length; i++) {
if (isBin(groups[i])
&& (isBinLeftCanceller(groups[i - 1], isRealGroup)
|| isBinRightCanceller(groups[i + 1], isRealGroup))) {
groups[i].classes[0] = "mord";
}
}
return groups;
};
// Return math atom class (mclass) of a domTree.
var getTypeOfDomTree = function(node) {
if (node instanceof domTree.documentFragment) {
if (node.children.length) {
return getTypeOfDomTree(
node.children[node.children.length - 1]);
}
} else {
if (utils.contains(["mord", "mop", "mbin", "mrel", "mopen", "mclose",
"mpunct", "minner"], node.classes[0])) {
return node.classes[0];
}
}
return null;
};
/**
* Sometimes, groups perform special rules when they have superscripts or
* subscripts attached to them. This function lets the `supsub` group know that
* its inner element should handle the superscripts and subscripts instead of
* handling them itself.
*/
var shouldHandleSupSub = function(group, options) {
if (!group) {
return false;
} else if (group.type === "op") {
// Operators handle supsubs differently when they have limits
// (e.g. `\displaystyle\sum_2^3`)
return group.value.limits &&
(options.style.size === Style.DISPLAY.size ||
group.value.alwaysHandleSupSub);
} else if (group.type === "accent") {
return isCharacterBox(group.value.base);
} else {
return null;
}
};
/**
* Sometimes we want to pull out the innermost element of a group. In most
* cases, this will just be the group itself, but when ordgroups and colors have
* a single element, we want to pull that out.
*/
var getBaseElem = function(group) {
if (!group) {
return false;
} else if (group.type === "ordgroup") {
if (group.value.length === 1) {
return getBaseElem(group.value[0]);
} else {
return group;
}
} else if (group.type === "color") {
if (group.value.value.length === 1) {
return getBaseElem(group.value.value[0]);
} else {
return group;
}
} else if (group.type === "font") {
return getBaseElem(group.value.body);
} else {
return group;
}
};
/**
* TeXbook algorithms often reference "character boxes", which are simply groups
* with a single character in them. To decide if something is a character box,
* we find its innermost group, and see if it is a single character.
*/
var isCharacterBox = function(group) {
var baseElem = getBaseElem(group);
// These are all they types of groups which hold single characters
return baseElem.type === "mathord" ||
baseElem.type === "textord" ||
baseElem.type === "bin" ||
baseElem.type === "rel" ||
baseElem.type === "inner" ||
baseElem.type === "open" ||
baseElem.type === "close" ||
baseElem.type === "punct";
};
var makeNullDelimiter = function(options, classes) {
return makeSpan(classes.concat([
"sizing", "reset-" + options.size, "size5",
options.style.reset(), Style.TEXT.cls(),
"nulldelimiter"]));
};
/**
* This is a map of group types to the function used to handle that type.
* Simpler types come at the beginning, while complicated types come afterwards.
*/
var groupTypes = {};
groupTypes.mathord = function(group, options) {
return buildCommon.makeOrd(group, options, "mathord");
};
groupTypes.textord = function(group, options) {
return buildCommon.makeOrd(group, options, "textord");
};
groupTypes.bin = function(group, options) {
return buildCommon.mathsym(
group.value, group.mode, options, ["mbin"]);
};
groupTypes.rel = function(group, options) {
return buildCommon.mathsym(
group.value, group.mode, options, ["mrel"]);
};
groupTypes.open = function(group, options) {
return buildCommon.mathsym(
group.value, group.mode, options, ["mopen"]);
};
groupTypes.close = function(group, options) {
return buildCommon.mathsym(
group.value, group.mode, options, ["mclose"]);
};
groupTypes.inner = function(group, options) {
return buildCommon.mathsym(
group.value, group.mode, options, ["minner"]);
};
groupTypes.punct = function(group, options) {
return buildCommon.mathsym(
group.value, group.mode, options, ["mpunct"]);
};
groupTypes.ordgroup = function(group, options) {
return makeSpan(
["mord", options.style.cls()],
buildExpression(group.value, options.reset(), true),
options
);
};
groupTypes.text = function(group, options) {
return makeSpan(["mord", "text", options.style.cls()],
buildExpression(group.value.body, options.reset(), true),
options);
};
groupTypes.color = function(group, options) {
var elements = buildExpression(
group.value.value,
options.withColor(group.value.color),
false
);
// \color isn't supposed to affect the type of the elements it contains.
// To accomplish this, we wrap the results in a fragment, so the inner
// elements will be able to directly interact with their neighbors. For
// example, `\color{red}{2 +} 3` has the same spacing as `2 + 3`
return new buildCommon.makeFragment(elements);
};
groupTypes.supsub = function(group, options) {
// Superscript and subscripts are handled in the TeXbook on page
// 445-446, rules 18(a-f).
// Here is where we defer to the inner group if it should handle
// superscripts and subscripts itself.
if (shouldHandleSupSub(group.value.base, options)) {
return groupTypes[group.value.base.type](group, options);
}
var base = buildGroup(group.value.base, options.reset());
var supmid;
var submid;
var sup;
var sub;
var style = options.style;
var newOptions;
if (group.value.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) {
newOptions = options.withStyle(style.sub());
sub = buildGroup(group.value.sub, newOptions);
submid = makeSpan([style.reset(), style.sub().cls()],
[sub], newOptions);
}
// Rule 18a
var supShift;
var subShift;
if (isCharacterBox(group.value.base)) {
supShift = 0;
subShift = 0;
} else {
supShift = base.height - style.metrics.supDrop;
subShift = base.depth + style.metrics.subDrop;
}
// Rule 18c
var minSupShift;
if (style === Style.DISPLAY) {
minSupShift = style.metrics.sup1;
} else if (style.cramped) {
minSupShift = style.metrics.sup3;
} else {
minSupShift = style.metrics.sup2;
}
// scriptspace is a font-size-independent size, so scale it
// appropriately
var multiplier = Style.TEXT.sizeMultiplier *
style.sizeMultiplier;
var scriptspace =
(0.5 / fontMetrics.metrics.ptPerEm) / multiplier + "em";
var supsub;
if (!group.value.sup) {
// Rule 18b
subShift = Math.max(
subShift, style.metrics.sub1,
sub.height - 0.8 * style.metrics.xHeight);
supsub = buildCommon.makeVList([
{type: "elem", elem: submid},
], "shift", subShift, options);
supsub.children[0].style.marginRight = scriptspace;
// Subscripts shouldn't be shifted by the base's italic correction.
// Account for that by shifting the subscript back the appropriate
// amount. Note we only do this when the base is a single symbol.
if (base instanceof domTree.symbolNode) {
supsub.children[0].style.marginLeft = -base.italic + "em";
}
} else if (!group.value.sub) {
// Rule 18c, d
supShift = Math.max(supShift, minSupShift,
sup.depth + 0.25 * style.metrics.xHeight);
supsub = buildCommon.makeVList([
{type: "elem", elem: supmid},
], "shift", -supShift, options);
supsub.children[0].style.marginRight = scriptspace;
} else {
supShift = Math.max(
supShift, minSupShift, sup.depth + 0.25 * style.metrics.xHeight);
subShift = Math.max(subShift, style.metrics.sub2);
var ruleWidth = fontMetrics.metrics.defaultRuleThickness;
// Rule 18e
if ((supShift - sup.depth) - (sub.height - subShift) <
4 * ruleWidth) {
subShift = 4 * ruleWidth - (supShift - sup.depth) + sub.height;
var psi = 0.8 * style.metrics.xHeight - (supShift - sup.depth);
if (psi > 0) {
supShift += psi;
subShift -= psi;
}
}
supsub = buildCommon.makeVList([
{type: "elem", elem: submid, shift: subShift},
{type: "elem", elem: supmid, shift: -supShift},
], "individualShift", null, options);
// See comment above about subscripts not being shifted
if (base instanceof domTree.symbolNode) {
supsub.children[0].style.marginLeft = -base.italic + "em";
}
supsub.children[0].style.marginRight = scriptspace;
supsub.children[1].style.marginRight = scriptspace;
}
// We ensure to wrap the supsub vlist in a span.msupsub to reset text-align
var mclass = getTypeOfDomTree(base) || "mord";
return makeSpan([mclass],
[base, makeSpan(["msupsub"], [supsub])],
options);
};
groupTypes.genfrac = function(group, options) {
// Fractions are handled in the TeXbook on pages 444-445, rules 15(a-e).
// Figure out what style this fraction should be in based on the
// function used
var style = options.style;
if (group.value.size === "display") {
style = Style.DISPLAY;
} else if (group.value.size === "text") {
style = Style.TEXT;
}
var nstyle = style.fracNum();
var dstyle = style.fracDen();
var newOptions;
newOptions = options.withStyle(nstyle);
var numer = buildGroup(group.value.numer, newOptions);
var numerreset = makeSpan([style.reset(), nstyle.cls()],
[numer], newOptions);
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) {
ruleWidth = fontMetrics.metrics.defaultRuleThickness /
options.style.sizeMultiplier;
} else {
ruleWidth = 0;
}
// Rule 15b
var numShift;
var clearance;
var denomShift;
if (style.size === Style.DISPLAY.size) {
numShift = style.metrics.num1;
if (ruleWidth > 0) {
clearance = 3 * ruleWidth;
} else {
clearance = 7 * fontMetrics.metrics.defaultRuleThickness;
}
denomShift = style.metrics.denom1;
} else {
if (ruleWidth > 0) {
numShift = style.metrics.num2;
clearance = ruleWidth;
} else {
numShift = style.metrics.num3;
clearance = 3 * fontMetrics.metrics.defaultRuleThickness;
}
denomShift = style.metrics.denom2;
}
var frac;
if (ruleWidth === 0) {
// Rule 15c
var candiateClearance =
(numShift - numer.depth) - (denom.height - denomShift);
if (candiateClearance < clearance) {
numShift += 0.5 * (clearance - candiateClearance);
denomShift += 0.5 * (clearance - candiateClearance);
}
frac = buildCommon.makeVList([
{type: "elem", elem: denomreset, shift: denomShift},
{type: "elem", elem: numerreset, shift: -numShift},
], "individualShift", null, options);
} else {
// Rule 15d
var axisHeight = style.metrics.axisHeight;
if ((numShift - numer.depth) - (axisHeight + 0.5 * ruleWidth) <
clearance) {
numShift +=
clearance - ((numShift - numer.depth) -
(axisHeight + 0.5 * ruleWidth));
}
if ((axisHeight - 0.5 * ruleWidth) - (denom.height - denomShift) <
clearance) {
denomShift +=
clearance - ((axisHeight - 0.5 * ruleWidth) -
(denom.height - denomShift));
}
var mid = makeSpan(
[options.style.reset(), Style.TEXT.cls(), "frac-line"]);
// Manually set the height of the line because its height is
// created in CSS
mid.height = ruleWidth;
var midShift = -(axisHeight - 0.5 * ruleWidth);
frac = buildCommon.makeVList([
{type: "elem", elem: denomreset, shift: denomShift},
{type: "elem", elem: mid, shift: midShift},
{type: "elem", elem: numerreset, shift: -numShift},
], "individualShift", null, options);
}
// Since we manually change the style sometimes (with \dfrac or \tfrac),
// account for the possible size change here.
frac.height *= style.sizeMultiplier / options.style.sizeMultiplier;
frac.depth *= style.sizeMultiplier / options.style.sizeMultiplier;
// Rule 15e
var delimSize;
if (style.size === Style.DISPLAY.size) {
delimSize = style.metrics.delim1;
} else {
delimSize = style.metrics.delim2;
}
var leftDelim;
var rightDelim;
if (group.value.leftDelim == null) {
leftDelim = makeNullDelimiter(options, ["mopen"]);
} else {
leftDelim = delimiter.customSizedDelim(
group.value.leftDelim, delimSize, true,
options.withStyle(style), group.mode, ["mopen"]);
}
if (group.value.rightDelim == null) {
rightDelim = makeNullDelimiter(options, ["mclose"]);
} else {
rightDelim = delimiter.customSizedDelim(
group.value.rightDelim, delimSize, true,
options.withStyle(style), group.mode, ["mclose"]);
}
return makeSpan(
["mord", options.style.reset(), style.cls()],
[leftDelim, makeSpan(["mfrac"], [frac]), rightDelim],
options);
};
groupTypes.array = function(group, options) {
var r;
var c;
var nr = group.value.body.length;
var nc = 0;
var body = new Array(nr);
var style = options.style;
// Horizontal spacing
var pt = 1 / fontMetrics.metrics.ptPerEm;
var arraycolsep = 5 * pt; // \arraycolsep in article.cls
// Vertical spacing
var baselineskip = 12 * pt; // see size10.clo
// Default \arraystretch from lttab.dtx
// TODO(gagern): may get redefined once we have user-defined macros
var arraystretch = utils.deflt(group.value.arraystretch, 1);
var arrayskip = arraystretch * baselineskip;
var arstrutHeight = 0.7 * arrayskip; // \strutbox in ltfsstrc.dtx and
var arstrutDepth = 0.3 * arrayskip; // \@arstrutbox in lttab.dtx
var totalHeight = 0;
for (r = 0; r < group.value.body.length; ++r) {
var inrow = group.value.body[r];
var height = arstrutHeight; // \@array adds an \@arstrut
var depth = arstrutDepth; // to each tow (via the template)
if (nc < inrow.length) {
nc = inrow.length;
}
var outrow = new Array(inrow.length);
for (c = 0; c < inrow.length; ++c) {
var elt = buildGroup(inrow[c], options);
if (depth < elt.depth) {
depth = elt.depth;
}
if (height < elt.height) {
height = elt.height;
}
outrow[c] = elt;
}
var gap = 0;
if (group.value.rowGaps[r]) {
gap = group.value.rowGaps[r].value;
switch (gap.unit) {
case "em":
gap = gap.number;
break;
case "ex":
gap = gap.number * style.metrics.emPerEx;
break;
default:
console.error("Can't handle unit " + gap.unit);
gap = 0;
}
if (gap > 0) { // \@argarraycr
gap += arstrutDepth;
if (depth < gap) {
depth = gap; // \@xargarraycr
}
gap = 0;
}
}
outrow.height = height;
outrow.depth = depth;
totalHeight += height;
outrow.pos = totalHeight;
totalHeight += depth + gap; // \@yargarraycr
body[r] = outrow;
}
var offset = totalHeight / 2 + style.metrics.axisHeight;
var colDescriptions = group.value.cols || [];
var cols = [];
var colSep;
var colDescrNum;
for (c = 0, colDescrNum = 0;
// Continue while either there are more columns or more column
// descriptions, so trailing separators don't get lost.
c < nc || colDescrNum < colDescriptions.length;
++c, ++colDescrNum) {
var colDescr = colDescriptions[colDescrNum] || {};
var firstSeparator = true;
while (colDescr.type === "separator") {
// If there is more than one separator in a row, add a space
// between them.
if (!firstSeparator) {
colSep = makeSpan(["arraycolsep"], []);
colSep.style.width =
fontMetrics.metrics.doubleRuleSep + "em";
cols.push(colSep);
}
if (colDescr.separator === "|") {
var separator = makeSpan(
["vertical-separator"],
[]);
separator.style.height = totalHeight + "em";
separator.style.verticalAlign =
-(totalHeight - offset) + "em";
cols.push(separator);
} else {
throw new ParseError(
"Invalid separator type: " + colDescr.separator);
}
colDescrNum++;
colDescr = colDescriptions[colDescrNum] || {};
firstSeparator = false;
}
if (c >= nc) {
continue;
}
var sepwidth;
if (c > 0 || group.value.hskipBeforeAndAfter) {
sepwidth = utils.deflt(colDescr.pregap, arraycolsep);
if (sepwidth !== 0) {
colSep = makeSpan(["arraycolsep"], []);
colSep.style.width = sepwidth + "em";
cols.push(colSep);
}
}
var col = [];
for (r = 0; r < nr; ++r) {
var row = body[r];
var elem = row[c];
if (!elem) {
continue;
}
var shift = row.pos - offset;
elem.depth = row.depth;
elem.height = row.height;
col.push({type: "elem", elem: elem, shift: shift});
}
col = buildCommon.makeVList(col, "individualShift", null, options);
col = makeSpan(
["col-align-" + (colDescr.align || "c")],
[col]);
cols.push(col);
if (c < nc - 1 || group.value.hskipBeforeAndAfter) {
sepwidth = utils.deflt(colDescr.postgap, arraycolsep);
if (sepwidth !== 0) {
colSep = makeSpan(["arraycolsep"], []);
colSep.style.width = sepwidth + "em";
cols.push(colSep);
}
}
}
body = makeSpan(["mtable"], cols);
return makeSpan(["mord"], [body], options);
};
groupTypes.spacing = function(group, options) {
if (group.value === "\\ " || group.value === "\\space" ||
group.value === " " || group.value === "~") {
// Spaces are generated by adding an actual space. Each of these
// things has an entry in the symbols table, so these will be turned
// into appropriate outputs.
return makeSpan(
["mspace"],
[buildCommon.mathsym(group.value, group.mode)]
);
} else {
// Other kinds of spaces are of arbitrary width. We use CSS to
// generate these.
return makeSpan(
["mspace",
buildCommon.spacingFunctions[group.value].className]);
}
};
groupTypes.llap = function(group, options) {
var inner = makeSpan(
["inner"], [buildGroup(group.value.body, options.reset())]);
var fix = makeSpan(["fix"], []);
return makeSpan(
["mord", "llap", options.style.cls()], [inner, fix], options);
};
groupTypes.rlap = function(group, options) {
var inner = makeSpan(
["inner"], [buildGroup(group.value.body, options.reset())]);
var fix = makeSpan(["fix"], []);
return makeSpan(
["mord", "rlap", options.style.cls()], [inner, fix], options);
};
groupTypes.op = function(group, options) {
// Operators are handled in the TeXbook pg. 443-444, rule 13(a).
var supGroup;
var subGroup;
var hasLimits = false;
if (group.type === "supsub") {
// If we have limits, supsub will pass us its group to handle. Pull
// out the superscript and subscript and set the group to the op in
// its base.
supGroup = group.value.sup;
subGroup = group.value.sub;
group = group.value.base;
hasLimits = true;
}
var style = options.style;
// Most operators have a large successor symbol, but these don't.
var noSuccessor = [
"\\smallint",
];
var large = false;
if (style.size === Style.DISPLAY.size &&
group.value.symbol &&
!utils.contains(noSuccessor, group.value.body)) {
// Most symbol operators get larger in displaystyle (rule 13)
large = true;
}
var base;
var baseShift = 0;
var slant = 0;
if (group.value.symbol) {
// If this is a symbol, create the symbol.
var fontName = large ? "Size2-Regular" : "Size1-Regular";
base = buildCommon.makeSymbol(
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
// almost on the axis, so these numbers are very small. Note we
// don't actually apply this here, but instead it is used either in
// the vlist creation or separately when there are no limits.
baseShift = (base.height - base.depth) / 2 -
style.metrics.axisHeight * style.sizeMultiplier;
// The slant of the symbol is just its italic correction.
slant = base.italic;
} else if (group.value.value) {
// If this is a list, compose that list.
var inner = buildExpression(group.value.value, options, true);
base = makeSpan(["mop"], inner, options);
} else {
// Otherwise, this is a text operator. Build the text from the
// operator's name.
// TODO(emily): Add a space in the middle of some of these
// operators, like \limsup
var output = [];
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);
}
if (hasLimits) {
// IE 8 clips \int if it is in a display: inline-block. We wrap it
// in a new span so it is an inline, and works.
base = makeSpan([], [base]);
var supmid;
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) {
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,
fontMetrics.metrics.bigOpSpacing3 - sup.depth);
}
if (subGroup) {
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,
fontMetrics.metrics.bigOpSpacing4 - sub.height);
}
// Build the final group as a vlist of the possible subscript, base,
// and possible superscript.
var finalGroup;
var top;
var bottom;
if (!supGroup) {
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);
// Here, we shift the limits by the slant of the symbol. Note
// that we are supposed to shift the limits by 1/2 of the slant,
// but since we are centering the limits adding a full slant of
// margin will shift by 1/2 that.
finalGroup.children[0].style.marginLeft = -slant + "em";
} else if (!subGroup) {
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);
// See comment above about slants
finalGroup.children[1].style.marginLeft = slant + "em";
} else if (!supGroup && !subGroup) {
// This case probably shouldn't occur (this would mean the
// supsub was sending us a group with no superscript or
// subscript) but be safe.
return base;
} else {
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);
// See comment above about slants
finalGroup.children[0].style.marginLeft = -slant + "em";
finalGroup.children[2].style.marginLeft = slant + "em";
}
return makeSpan(["mop", "op-limits"], [finalGroup], options);
} else {
if (group.value.symbol) {
base.style.top = baseShift + "em";
}
return base;
}
};
groupTypes.katex = function(group, options) {
// The KaTeX logo. The offsets for the K and a were chosen to look
// 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)], options);
var a = makeSpan(
["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)], options);
var e = makeSpan(
["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)], options);
return makeSpan(
["mord", "katex-logo"], [k, a, t, e, x], options);
};
groupTypes.overline = function(group, options) {
// Overlines are handled in the TeXbook pg 443, Rule 9.
var style = options.style;
// Build the inner group in the cramped style.
var innerGroup = buildGroup(group.value.body,
options.withStyle(style.cramp()));
var ruleWidth = fontMetrics.metrics.defaultRuleThickness /
style.sizeMultiplier;
// Create the line above the body
var line = makeSpan(
[style.reset(), Style.TEXT.cls(), "overline-line"]);
line.height = ruleWidth;
line.maxFontSize = 1.0;
// Generate the vlist, with the appropriate kerns
var vlist = buildCommon.makeVList([
{type: "elem", elem: innerGroup},
{type: "kern", size: 3 * ruleWidth},
{type: "elem", elem: line},
{type: "kern", size: ruleWidth},
], "firstBaseline", null, options);
return makeSpan(["mord", "overline"], [vlist], options);
};
groupTypes.underline = function(group, options) {
// Underlines are handled in the TeXbook pg 443, Rule 10.
var style = options.style;
// Build the inner group.
var innerGroup = buildGroup(group.value.body, options);
var ruleWidth = fontMetrics.metrics.defaultRuleThickness /
style.sizeMultiplier;
// Create the line above the body
var line = makeSpan([style.reset(), Style.TEXT.cls(), "underline-line"]);
line.height = ruleWidth;
line.maxFontSize = 1.0;
// Generate the vlist, with the appropriate kerns
var vlist = buildCommon.makeVList([
{type: "kern", size: ruleWidth},
{type: "elem", elem: line},
{type: "kern", size: 3 * ruleWidth},
{type: "elem", elem: innerGroup},
], "top", innerGroup.height, options);
return makeSpan(["mord", "underline"], [vlist], options);
};
groupTypes.sqrt = function(group, options) {
// Square roots are handled in the TeXbook pg. 443, Rule 11.
var style = options.style;
// First, we do the same steps as in overline to build the inner group
// and line
var inner = buildGroup(group.value.body, options.withStyle(style.cramp()));
var ruleWidth = fontMetrics.metrics.defaultRuleThickness /
style.sizeMultiplier;
var line = makeSpan(
[style.reset(), Style.TEXT.cls(), "sqrt-line"], [],
options);
line.height = ruleWidth;
line.maxFontSize = 1.0;
var phi = ruleWidth;
if (style.id < Style.TEXT.id) {
phi = style.metrics.xHeight;
}
// Calculate the clearance between the body and line
var lineClearance = ruleWidth + phi / 4;
var innerHeight = (inner.height + inner.depth) * style.sizeMultiplier;
var minDelimiterHeight = innerHeight + lineClearance + ruleWidth;
// Create a \surd delimiter of the required minimum size
var delim = makeSpan(["sqrt-sign"], [
delimiter.customSizedDelim("\\surd", minDelimiterHeight,
false, options, group.mode)],
options);
var delimDepth = (delim.height + delim.depth) - ruleWidth;
// Adjust the clearance based on the delimiter size
if (delimDepth > inner.height + inner.depth + lineClearance) {
lineClearance =
(lineClearance + delimDepth - inner.height - inner.depth) / 2;
}
// Shift the delimiter so that its top lines up with the top of the line
var delimShift = -(inner.height + lineClearance + ruleWidth) + delim.height;
delim.style.top = delimShift + "em";
delim.height -= delimShift;
delim.depth += delimShift;
// We add a special case here, because even when `inner` is empty, we
// still get a line. So, we use a simple heuristic to decide if we
// should omit the body entirely. (note this doesn't work for something
// like `\sqrt{\rlap{x}}`, but if someone is doing that they deserve for
// it not to work.
var body;
if (inner.height === 0 && inner.depth === 0) {
body = makeSpan();
} else {
body = buildCommon.makeVList([
{type: "elem", elem: inner},
{type: "kern", size: lineClearance},
{type: "elem", elem: line},
{type: "kern", size: ruleWidth},
], "firstBaseline", null, options);
}
if (!group.value.index) {
return makeSpan(["mord", "sqrt"], [delim, body], options);
} else {
// Handle the optional root index
// The index is always in scriptscript style
var newOptions = options.withStyle(Style.SCRIPTSCRIPT);
var root = buildGroup(group.value.index, newOptions);
var rootWrap = makeSpan(
[style.reset(), Style.SCRIPTSCRIPT.cls()],
[root],
newOptions);
// Figure out the height and depth of the inner part
var innerRootHeight = Math.max(delim.height, body.height);
var innerRootDepth = Math.max(delim.depth, body.depth);
// The amount the index is shifted by. This is taken from the TeX
// source, in the definition of `\r@@t`.
var toShift = 0.6 * (innerRootHeight - innerRootDepth);
// Build a VList with the superscript shifted up correctly
var rootVList = buildCommon.makeVList(
[{type: "elem", elem: rootWrap}],
"shift", -toShift, options);
// Add a class surrounding it so we can add on the appropriate
// kerning
var rootVListWrap = makeSpan(["root"], [rootVList]);
return makeSpan(["mord", "sqrt"],
[rootVListWrap, delim, body], options);
}
};
groupTypes.sizing = function(group, options) {
// Handle sizing operators like \Huge. Real TeX doesn't actually allow
// these functions inside of math expressions, so we do some special
// handling.
var inner = buildExpression(group.value.value,
options.withSize(group.value.size), false);
// Compute the correct maxFontSize.
var style = options.style;
var fontSize = buildCommon.sizingMultiplier[group.value.size];
fontSize = fontSize * style.sizeMultiplier;
// 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) {
// Style changes are handled in the TeXbook on pg. 442, Rule 3.
// Figure out what style we're changing to.
var styleMap = {
"display": Style.DISPLAY,
"text": Style.TEXT,
"script": Style.SCRIPT,
"scriptscript": Style.SCRIPTSCRIPT,
};
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, newOptions, false);
// 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) {
var font = group.value.font;
return buildGroup(group.value.body, options.withFont(font));
};
groupTypes.delimsizing = function(group, options) {
var delim = group.value.value;
if (delim === ".") {
// Empty delimiters still count as elements, even though they don't
// show anything.
return makeSpan([group.value.mclass]);
}
// Use delimiter.sizedDelim to generate the delimiter.
return delimiter.sizedDelim(
delim, group.value.size, options, group.mode,
[group.value.mclass]);
};
groupTypes.leftright = function(group, options) {
// Build the inner expression
var inner = buildExpression(group.value.body, options.reset(), true);
var innerHeight = 0;
var innerDepth = 0;
// Calculate its height and depth
for (var i = 0; i < inner.length; i++) {
innerHeight = Math.max(inner[i].height, innerHeight);
innerDepth = Math.max(inner[i].depth, innerDepth);
}
var style = options.style;
// The size of delimiters is the same, regardless of what style we are
// in. Thus, to correctly calculate the size of delimiter we need around
// a group, we scale down the inner size based on the size.
innerHeight *= style.sizeMultiplier;
innerDepth *= style.sizeMultiplier;
var leftDelim;
if (group.value.left === ".") {
// Empty delimiters in \left and \right make null delimiter spaces.
leftDelim = makeNullDelimiter(options, ["mopen"]);
} else {
// Otherwise, use leftRightDelim to generate the correct sized
// delimiter.
leftDelim = delimiter.leftRightDelim(
group.value.left, innerHeight, innerDepth, options,
group.mode, ["mopen"]);
}
// Add it to the beginning of the expression
inner.unshift(leftDelim);
var rightDelim;
// Same for the right delimiter
if (group.value.right === ".") {
rightDelim = makeNullDelimiter(options, ["mclose"]);
} else {
rightDelim = delimiter.leftRightDelim(
group.value.right, innerHeight, innerDepth, options,
group.mode, ["mclose"]);
}
// Add it to the end of the expression.
inner.push(rightDelim);
return makeSpan(
["minner", style.cls()], inner, options);
};
groupTypes.rule = function(group, options) {
// Make an empty span for the rule
var rule = makeSpan(["mord", "rule"], [], options);
var style = options.style;
// Calculate the shift, width, and height of the rule, and account for units
var shift = 0;
if (group.value.shift) {
shift = group.value.shift.number;
if (group.value.shift.unit === "ex") {
shift *= style.metrics.xHeight;
}
}
var width = group.value.width.number;
if (group.value.width.unit === "ex") {
width *= style.metrics.xHeight;
}
var height = group.value.height.number;
if (group.value.height.unit === "ex") {
height *= style.metrics.xHeight;
}
// The sizes of rules are absolute, so make it larger if we are in a
// smaller style.
shift /= style.sizeMultiplier;
width /= style.sizeMultiplier;
height /= style.sizeMultiplier;
// Style the rule to the right size
rule.style.borderRightWidth = width + "em";
rule.style.borderTopWidth = height + "em";
rule.style.bottom = shift + "em";
// Record the height and width
rule.width = width;
rule.height = height + shift;
rule.depth = -shift;
return rule;
};
groupTypes.kern = function(group, options) {
// Make an empty span for the rule
var rule = makeSpan(["mord", "rule"], [], options);
var style = options.style;
var dimension = 0;
if (group.value.dimension) {
dimension = group.value.dimension.number;
if (group.value.dimension.unit === "ex") {
dimension *= style.metrics.xHeight;
}
}
dimension /= style.sizeMultiplier;
rule.style.marginLeft = dimension + "em";
return rule;
};
groupTypes.accent = function(group, options) {
// Accents are handled in the TeXbook pg. 443, rule 12.
var base = group.value.base;
var style = options.style;
var supsubGroup;
if (group.type === "supsub") {
// If our base is a character box, and we have superscripts and
// subscripts, the supsub will defer to us. In particular, we want
// to attach the superscripts and subscripts to the inner body (so
// that the position of the superscripts and subscripts won't be
// affected by the height of the accent). We accomplish this by
// sticking the base of the accent into the base of the supsub, and
// rendering that, while keeping track of where the accent is.
// The supsub group is the group that was passed in
var supsub = group;
// The real accent group is the base of the supsub group
group = supsub.value.base;
// The character box is the base of the accent group
base = group.value.base;
// Stick the character box into the base of the supsub group
supsub.value.base = base;
// Rerender the supsub group with its new base, and store that
// result.
supsubGroup = buildGroup(
supsub, options.reset());
}
// Build the base group
var body = buildGroup(
base, options.withStyle(style.cramp()));
// Calculate the skew of the accent. This is based on the line "If the
// nucleus is not a single character, let s = 0; otherwise set s to the
// kern amount for the nucleus followed by the \skewchar of its font."
// Note that our skew metrics are just the kern between each character
// and the skewchar.
var skew;
if (isCharacterBox(base)) {
// If the base is a character box, then we want the skew of the
// innermost character. To do that, we find the innermost character:
var baseChar = getBaseElem(base);
// Then, we render its group to get the symbol inside it
var baseGroup = buildGroup(
baseChar, options.withStyle(style.cramp()));
// Finally, we pull the skew off of the symbol.
skew = baseGroup.skew;
// Note that we now throw away baseGroup, because the layers we
// removed with getBaseElem might contain things like \color which
// we can't get rid of.
// TODO(emily): Find a better way to get the skew
} else {
skew = 0;
}
// calculate the amount of space between the body and the accent
var clearance = Math.min(
body.height,
style.metrics.xHeight);
// Build the accent
var accent = buildCommon.makeSymbol(
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;
// The \vec character that the fonts use is a combining character, and
// thus shows up much too far to the left. To account for this, we add a
// specific class which shifts the accent over to where we want it.
// TODO(emily): Fix this in a better way, like by changing the font
var vecClass = group.value.accent === "\\vec" ? "accent-vec" : null;
var accentBody = makeSpan(["accent-body", vecClass], [
makeSpan([], [accent])]);
accentBody = buildCommon.makeVList([
{type: "elem", elem: body},
{type: "kern", size: -clearance},
{type: "elem", elem: accentBody},
], "firstBaseline", null, options);
// Shift the accent over by the skew. Note we shift by twice the skew
// because we are centering the accent, so by adding 2*skew to the left,
// we shift it to the right by 1*skew.
accentBody.children[1].style.marginLeft = 2 * skew + "em";
var accentWrap = makeSpan(["mord", "accent"], [accentBody], options);
if (supsubGroup) {
// Here, we replace the "base" child of the supsub with our newly
// generated accent.
supsubGroup.children[0] = accentWrap;
// Since we don't rerun the height calculation after replacing the
// accent, we manually recalculate height.
supsubGroup.height = Math.max(accentWrap.height, supsubGroup.height);
// Accents should always be ords, even when their innards are not.
supsubGroup.classes[0] = "mord";
return supsubGroup;
} else {
return accentWrap;
}
};
groupTypes.phantom = function(group, options) {
var elements = buildExpression(
group.value.value,
options.withPhantom(),
false
);
// \phantom isn't supposed to affect the elements it contains.
// See "color" for more details.
return new buildCommon.makeFragment(elements);
};
groupTypes.mclass = function(group, options) {
var elements = buildExpression(group.value.value, options, true);
return makeSpan([group.value.mclass], elements, options);
};
/**
* buildGroup is the function that takes a group and calls the correct groupType
* function for it. It also handles the interaction of size and style changes
* between parents and children.
*/
var buildGroup = function(group, options) {
if (!group) {
return makeSpan();
}
if (groupTypes[group.type]) {
// Call the groupTypes function
var groupNode = groupTypes[group.type](group, options);
var multiplier;
// If the style changed between the parent and the current group,
// account for the size difference
if (options.style !== options.parentStyle) {
multiplier = options.style.sizeMultiplier /
options.parentStyle.sizeMultiplier;
groupNode.height *= multiplier;
groupNode.depth *= multiplier;
}
// If the size changed between the parent and the current group, account
// for that size difference.
if (options.size !== options.parentSize) {
multiplier = buildCommon.sizingMultiplier[options.size] /
buildCommon.sizingMultiplier[options.parentSize];
groupNode.height *= multiplier;
groupNode.depth *= multiplier;
}
return groupNode;
} else {
throw new ParseError(
"Got group of unknown type: '" + group.type + "'");
}
};
/**
* Take an entire parse tree, and build it into an appropriate set of HTML
* nodes.
*/
var buildHTML = function(tree, options) {
// buildExpression is destructive, so we need to make a clone
// of the incoming tree so that it isn't accidentally changed
tree = JSON.parse(JSON.stringify(tree));
// Build the expression contained in the tree
var expression = buildExpression(tree, options, true);
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
// depth of the expression.
var topStrut = makeSpan(["strut"]);
var bottomStrut = makeSpan(["strut", "bottom"]);
topStrut.style.height = body.height + "em";
bottomStrut.style.height = (body.height + body.depth) + "em";
// We'd like to use `vertical-align: top` but in IE 9 this lowers the
// baseline of the box to the bottom of this strut (instead staying in the
// normal place) so we use an absolute value for vertical-align instead
bottomStrut.style.verticalAlign = -body.depth + "em";
// Wrap the struts and body together
var htmlNode = makeSpan(["katex-html"], [topStrut, bottomStrut, body]);
htmlNode.setAttribute("aria-hidden", "true");
return htmlNode;
};
module.exports = buildHTML;