
See issue #278. Although the official definition makes use of @{…} notation, we use custom spacing instead, since that seems easier for now.
1314 lines
49 KiB
JavaScript
1314 lines
49 KiB
JavaScript
/**
|
|
* 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 Options = require("./Options");
|
|
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;
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
var buildExpression = function(expression, options, prev) {
|
|
var groups = [];
|
|
for (var i = 0; i < expression.length; i++) {
|
|
var group = expression[i];
|
|
groups.push(buildGroup(group, options, prev));
|
|
prev = group;
|
|
}
|
|
return groups;
|
|
};
|
|
|
|
// List of types used by getTypeOfGroup
|
|
var groupToType = {
|
|
mathord: "mord",
|
|
textord: "mord",
|
|
bin: "mbin",
|
|
rel: "mrel",
|
|
text: "mord",
|
|
open: "mopen",
|
|
close: "mclose",
|
|
inner: "minner",
|
|
genfrac: "minner",
|
|
array: "minner",
|
|
spacing: "mord",
|
|
punct: "mpunct",
|
|
ordgroup: "mord",
|
|
op: "mop",
|
|
katex: "mord",
|
|
overline: "mord",
|
|
rule: "mord",
|
|
leftright: "minner",
|
|
sqrt: "mord",
|
|
accent: "mord"
|
|
};
|
|
|
|
/**
|
|
* Gets the final math type of an expression, given its group type. This type is
|
|
* used to determine spacing between elements, and affects bin elements by
|
|
* causing them to change depending on what types are around them. This type
|
|
* must be attached to the outermost node of an element as a CSS class so that
|
|
* spacing with its surrounding elements works correctly.
|
|
*
|
|
* Some elements can be mapped one-to-one from group type to math type, and
|
|
* those are listed in the `groupToType` table.
|
|
*
|
|
* Others (usually elements that wrap around other elements) often have
|
|
* recursive definitions, and thus call `getTypeOfGroup` on their inner
|
|
* elements.
|
|
*/
|
|
var getTypeOfGroup = function(group) {
|
|
if (group == null) {
|
|
// Like when typesetting $^3$
|
|
return groupToType.mathord;
|
|
} else if (group.type === "supsub") {
|
|
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 === "delimsizing") {
|
|
return groupToType[group.value.delimType];
|
|
} else {
|
|
return groupToType[group.type];
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 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;
|
|
} 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 {
|
|
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";
|
|
};
|
|
|
|
/**
|
|
* 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 = {
|
|
mathord: function(group, options, prev) {
|
|
return buildCommon.mathit(
|
|
group.value, group.mode, options.getColor(), ["mord"]);
|
|
},
|
|
|
|
textord: function(group, options, prev) {
|
|
return buildCommon.mathrm(
|
|
group.value, group.mode, options.getColor(), ["mord"]);
|
|
},
|
|
|
|
bin: function(group, options, prev) {
|
|
var className = "mbin";
|
|
// Pull out the most recent element. Do some special handling to find
|
|
// things at the end of a \color group. Note that we don't use the same
|
|
// logic for ordgroups (which count as ords).
|
|
var prevAtom = prev;
|
|
while (prevAtom && prevAtom.type === "color") {
|
|
var atoms = prevAtom.value.value;
|
|
prevAtom = atoms[atoms.length - 1];
|
|
}
|
|
// See TeXbook pg. 442-446, Rules 5 and 6, and the text before Rule 19.
|
|
// Here, we determine whether the bin should turn into an ord. We
|
|
// currently only apply Rule 5.
|
|
if (!prev || utils.contains(["mbin", "mopen", "mrel", "mop", "mpunct"],
|
|
getTypeOfGroup(prevAtom))) {
|
|
group.type = "textord";
|
|
className = "mord";
|
|
}
|
|
|
|
return buildCommon.mathrm(
|
|
group.value, group.mode, options.getColor(), [className]);
|
|
},
|
|
|
|
rel: function(group, options, prev) {
|
|
return buildCommon.mathrm(
|
|
group.value, group.mode, options.getColor(), ["mrel"]);
|
|
},
|
|
|
|
open: function(group, options, prev) {
|
|
return buildCommon.mathrm(
|
|
group.value, group.mode, options.getColor(), ["mopen"]);
|
|
},
|
|
|
|
close: function(group, options, prev) {
|
|
return buildCommon.mathrm(
|
|
group.value, group.mode, options.getColor(), ["mclose"]);
|
|
},
|
|
|
|
inner: function(group, options, prev) {
|
|
return buildCommon.mathrm(
|
|
group.value, group.mode, options.getColor(), ["minner"]);
|
|
},
|
|
|
|
punct: function(group, options, prev) {
|
|
return buildCommon.mathrm(
|
|
group.value, group.mode, options.getColor(), ["mpunct"]);
|
|
},
|
|
|
|
ordgroup: function(group, options, prev) {
|
|
return makeSpan(
|
|
["mord", options.style.cls()],
|
|
buildExpression(group.value, options.reset())
|
|
);
|
|
},
|
|
|
|
text: function(group, options, prev) {
|
|
return makeSpan(["text", "mord", options.style.cls()],
|
|
buildExpression(group.value.body, options.reset()));
|
|
},
|
|
|
|
color: function(group, options, prev) {
|
|
var elements = buildExpression(
|
|
group.value.value,
|
|
options.withColor(group.value.color),
|
|
prev
|
|
);
|
|
|
|
// \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);
|
|
},
|
|
|
|
supsub: function(group, options, prev) {
|
|
// 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, prev);
|
|
}
|
|
|
|
var base = buildGroup(group.value.base, options.reset());
|
|
var supmid, submid, sup, sub;
|
|
|
|
if (group.value.sup) {
|
|
sup = buildGroup(group.value.sup,
|
|
options.withStyle(options.style.sup()));
|
|
supmid = makeSpan(
|
|
[options.style.reset(), options.style.sup().cls()], [sup]);
|
|
}
|
|
|
|
if (group.value.sub) {
|
|
sub = buildGroup(group.value.sub,
|
|
options.withStyle(options.style.sub()));
|
|
submid = makeSpan(
|
|
[options.style.reset(), options.style.sub().cls()], [sub]);
|
|
}
|
|
|
|
// Rule 18a
|
|
var supShift, subShift;
|
|
if (isCharacterBox(group.value.base)) {
|
|
supShift = 0;
|
|
subShift = 0;
|
|
} else {
|
|
supShift = base.height - fontMetrics.metrics.supDrop;
|
|
subShift = base.depth + fontMetrics.metrics.subDrop;
|
|
}
|
|
|
|
// Rule 18c
|
|
var minSupShift;
|
|
if (options.style === Style.DISPLAY) {
|
|
minSupShift = fontMetrics.metrics.sup1;
|
|
} else if (options.style.cramped) {
|
|
minSupShift = fontMetrics.metrics.sup3;
|
|
} else {
|
|
minSupShift = fontMetrics.metrics.sup2;
|
|
}
|
|
|
|
// scriptspace is a font-size-independent size, so scale it
|
|
// appropriately
|
|
var multiplier = Style.TEXT.sizeMultiplier *
|
|
options.style.sizeMultiplier;
|
|
var scriptspace =
|
|
(0.5 / fontMetrics.metrics.ptPerEm) / multiplier + "em";
|
|
|
|
var supsub;
|
|
if (!group.value.sup) {
|
|
// Rule 18b
|
|
subShift = Math.max(
|
|
subShift, fontMetrics.metrics.sub1,
|
|
sub.height - 0.8 * fontMetrics.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 * fontMetrics.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 * fontMetrics.metrics.xHeight);
|
|
subShift = Math.max(subShift, fontMetrics.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 * fontMetrics.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;
|
|
}
|
|
|
|
return makeSpan([getTypeOfGroup(group.value.base)],
|
|
[base, supsub]);
|
|
},
|
|
|
|
genfrac: function(group, options, prev) {
|
|
// 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 fstyle = options.style;
|
|
if (group.value.size === "display") {
|
|
fstyle = Style.DISPLAY;
|
|
} else if (group.value.size === "text") {
|
|
fstyle = Style.TEXT;
|
|
}
|
|
|
|
var nstyle = fstyle.fracNum();
|
|
var dstyle = fstyle.fracDen();
|
|
|
|
var numer = buildGroup(group.value.numer, options.withStyle(nstyle));
|
|
var numerreset = makeSpan([fstyle.reset(), nstyle.cls()], [numer]);
|
|
|
|
var denom = buildGroup(group.value.denom, options.withStyle(dstyle));
|
|
var denomreset = makeSpan([fstyle.reset(), dstyle.cls()], [denom]);
|
|
|
|
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 (fstyle.size === Style.DISPLAY.size) {
|
|
numShift = fontMetrics.metrics.num1;
|
|
if (ruleWidth > 0) {
|
|
clearance = 3 * ruleWidth;
|
|
} else {
|
|
clearance = 7 * fontMetrics.metrics.defaultRuleThickness;
|
|
}
|
|
denomShift = fontMetrics.metrics.denom1;
|
|
} else {
|
|
if (ruleWidth > 0) {
|
|
numShift = fontMetrics.metrics.num2;
|
|
clearance = ruleWidth;
|
|
} else {
|
|
numShift = fontMetrics.metrics.num3;
|
|
clearance = 3 * fontMetrics.metrics.defaultRuleThickness;
|
|
}
|
|
denomShift = fontMetrics.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 = fontMetrics.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 *= fstyle.sizeMultiplier / options.style.sizeMultiplier;
|
|
frac.depth *= fstyle.sizeMultiplier / options.style.sizeMultiplier;
|
|
|
|
// Rule 15e
|
|
var innerChildren = [makeSpan(["mfrac"], [frac])];
|
|
|
|
var delimSize;
|
|
if (fstyle.size === Style.DISPLAY.size) {
|
|
delimSize = fontMetrics.metrics.delim1;
|
|
} else {
|
|
delimSize = fontMetrics.metrics.getDelim2(fstyle);
|
|
}
|
|
|
|
if (group.value.leftDelim != null) {
|
|
innerChildren.unshift(
|
|
delimiter.customSizedDelim(
|
|
group.value.leftDelim, delimSize, true,
|
|
options.withStyle(fstyle), group.mode)
|
|
);
|
|
}
|
|
if (group.value.rightDelim != null) {
|
|
innerChildren.push(
|
|
delimiter.customSizedDelim(
|
|
group.value.rightDelim, delimSize, true,
|
|
options.withStyle(fstyle), group.mode)
|
|
);
|
|
}
|
|
|
|
return makeSpan(
|
|
["minner", options.style.reset(), fstyle.cls()],
|
|
innerChildren,
|
|
options.getColor());
|
|
},
|
|
|
|
array: function(group, options, prev) {
|
|
var r, c;
|
|
var nr = group.value.body.length;
|
|
var nc = 0;
|
|
var body = new Array(nr);
|
|
|
|
// 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 * fontMetrics.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 + fontMetrics.metrics.axisHeight;
|
|
var coldescriptions = group.value.cols || [];
|
|
var cols = [];
|
|
var colsep;
|
|
for (c = 0; c < nc; ++c) {
|
|
var coldescr = coldescriptions[c] || {};
|
|
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(["minner"], [body], options.getColor());
|
|
},
|
|
|
|
spacing: function(group, options, prev) {
|
|
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(
|
|
["mord", "mspace"],
|
|
[buildCommon.mathrm(group.value, group.mode)]
|
|
);
|
|
} else {
|
|
// Other kinds of spaces are of arbitrary width. We use CSS to
|
|
// generate these.
|
|
return makeSpan(
|
|
["mord", "mspace",
|
|
buildCommon.spacingFunctions[group.value].className]);
|
|
}
|
|
},
|
|
|
|
llap: function(group, options, prev) {
|
|
var inner = makeSpan(
|
|
["inner"], [buildGroup(group.value.body, options.reset())]);
|
|
var fix = makeSpan(["fix"], []);
|
|
return makeSpan(
|
|
["llap", options.style.cls()], [inner, fix]);
|
|
},
|
|
|
|
rlap: function(group, options, prev) {
|
|
var inner = makeSpan(
|
|
["inner"], [buildGroup(group.value.body, options.reset())]);
|
|
var fix = makeSpan(["fix"], []);
|
|
return makeSpan(
|
|
["rlap", options.style.cls()], [inner, fix]);
|
|
},
|
|
|
|
op: function(group, options, prev) {
|
|
// 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;
|
|
}
|
|
|
|
// Most operators have a large successor symbol, but these don't.
|
|
var noSuccessor = [
|
|
"\\smallint"
|
|
];
|
|
|
|
var large = false;
|
|
if (options.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 style = large ? "Size2-Regular" : "Size1-Regular";
|
|
base = buildCommon.makeSymbol(
|
|
group.value.body, style, "math", options.getColor(),
|
|
["op-symbol", large ? "large-op" : "small-op", "mop"]);
|
|
|
|
// 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 -
|
|
fontMetrics.metrics.axisHeight *
|
|
options.style.sizeMultiplier;
|
|
|
|
// The slant of the symbol is just its italic correction.
|
|
slant = base.italic;
|
|
} 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.mathrm(group.value.body[i], group.mode));
|
|
}
|
|
base = makeSpan(["mop"], output, options.getColor());
|
|
}
|
|
|
|
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, supKern, submid, subKern;
|
|
// 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(options.style.sup()));
|
|
supmid = makeSpan(
|
|
[options.style.reset(), options.style.sup().cls()], [sup]);
|
|
|
|
supKern = Math.max(
|
|
fontMetrics.metrics.bigOpSpacing1,
|
|
fontMetrics.metrics.bigOpSpacing3 - sup.depth);
|
|
}
|
|
|
|
if (subGroup) {
|
|
var sub = buildGroup(
|
|
subGroup, options.withStyle(options.style.sub()));
|
|
submid = makeSpan(
|
|
[options.style.reset(), options.style.sub().cls()],
|
|
[sub]);
|
|
|
|
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, top, 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]);
|
|
} else {
|
|
if (group.value.symbol) {
|
|
base.style.top = baseShift + "em";
|
|
}
|
|
|
|
return base;
|
|
}
|
|
},
|
|
|
|
katex: function(group, options, prev) {
|
|
// 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.mathrm("K", group.mode)]);
|
|
var a = makeSpan(
|
|
["a"], [buildCommon.mathrm("A", group.mode)]);
|
|
|
|
a.height = (a.height + 0.2) * 0.75;
|
|
a.depth = (a.height - 0.2) * 0.75;
|
|
|
|
var t = makeSpan(
|
|
["t"], [buildCommon.mathrm("T", group.mode)]);
|
|
var e = makeSpan(
|
|
["e"], [buildCommon.mathrm("E", group.mode)]);
|
|
|
|
e.height = (e.height - 0.2155);
|
|
e.depth = (e.depth + 0.2155);
|
|
|
|
var x = makeSpan(
|
|
["x"], [buildCommon.mathrm("X", group.mode)]);
|
|
|
|
return makeSpan(
|
|
["katex-logo"], [k, a, t, e, x], options.getColor());
|
|
},
|
|
|
|
overline: function(group, options, prev) {
|
|
// Overlines are handled in the TeXbook pg 443, Rule 9.
|
|
|
|
// Build the inner group in the cramped style.
|
|
var innerGroup = buildGroup(group.value.body,
|
|
options.withStyle(options.style.cramp()));
|
|
|
|
var ruleWidth = fontMetrics.metrics.defaultRuleThickness /
|
|
options.style.sizeMultiplier;
|
|
|
|
// Create the line above the body
|
|
var line = makeSpan(
|
|
[options.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(["overline", "mord"], [vlist], options.getColor());
|
|
},
|
|
|
|
sqrt: function(group, options, prev) {
|
|
// Square roots are handled in the TeXbook pg. 443, Rule 11.
|
|
|
|
// First, we do the same steps as in overline to build the inner group
|
|
// and line
|
|
var inner = buildGroup(group.value.body,
|
|
options.withStyle(options.style.cramp()));
|
|
|
|
var ruleWidth = fontMetrics.metrics.defaultRuleThickness /
|
|
options.style.sizeMultiplier;
|
|
|
|
var line = makeSpan(
|
|
[options.style.reset(), Style.TEXT.cls(), "sqrt-line"], [],
|
|
options.getColor());
|
|
line.height = ruleWidth;
|
|
line.maxFontSize = 1.0;
|
|
|
|
var phi = ruleWidth;
|
|
if (options.style.id < Style.TEXT.id) {
|
|
phi = fontMetrics.metrics.xHeight;
|
|
}
|
|
|
|
// Calculate the clearance between the body and line
|
|
var lineClearance = ruleWidth + phi / 4;
|
|
|
|
var innerHeight =
|
|
(inner.height + inner.depth) * options.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.getColor());
|
|
|
|
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(["sqrt", "mord"], [delim, body]);
|
|
} else {
|
|
// Handle the optional root index
|
|
|
|
// The index is always in scriptscript style
|
|
var root = buildGroup(
|
|
group.value.index,
|
|
options.withStyle(Style.SCRIPTSCRIPT));
|
|
var rootWrap = makeSpan(
|
|
[options.style.reset(), Style.SCRIPTSCRIPT.cls()],
|
|
[root]);
|
|
|
|
// 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(["sqrt", "mord"], [rootVListWrap, delim, body]);
|
|
}
|
|
},
|
|
|
|
sizing: function(group, options, prev) {
|
|
// 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), prev);
|
|
|
|
var span = makeSpan(["mord"],
|
|
[makeSpan(["sizing", "reset-" + options.size, group.value.size,
|
|
options.style.cls()],
|
|
inner)]);
|
|
|
|
// Calculate the correct maxFontSize manually
|
|
var fontSize = buildCommon.sizingMultiplier[group.value.size];
|
|
span.maxFontSize = fontSize * options.style.sizeMultiplier;
|
|
|
|
return span;
|
|
},
|
|
|
|
styling: function(group, options, prev) {
|
|
// Style changes are handled in the TeXbook on pg. 442, Rule 3.
|
|
|
|
// Figure out what style we're changing to.
|
|
var style = {
|
|
"display": Style.DISPLAY,
|
|
"text": Style.TEXT,
|
|
"script": Style.SCRIPT,
|
|
"scriptscript": Style.SCRIPTSCRIPT
|
|
};
|
|
|
|
var newStyle = style[group.value.style];
|
|
|
|
// Build the inner expression in the new style.
|
|
var inner = buildExpression(
|
|
group.value.value, options.withStyle(newStyle), prev);
|
|
|
|
return makeSpan([options.style.reset(), newStyle.cls()], inner);
|
|
},
|
|
|
|
delimsizing: function(group, options, prev) {
|
|
var delim = group.value.value;
|
|
|
|
if (delim === ".") {
|
|
// Empty delimiters still count as elements, even though they don't
|
|
// show anything.
|
|
return makeSpan([groupToType[group.value.delimType]]);
|
|
}
|
|
|
|
// Use delimiter.sizedDelim to generate the delimiter.
|
|
return makeSpan(
|
|
[groupToType[group.value.delimType]],
|
|
[delimiter.sizedDelim(
|
|
delim, group.value.size, options, group.mode)]);
|
|
},
|
|
|
|
leftright: function(group, options, prev) {
|
|
// Build the inner expression
|
|
var inner = buildExpression(group.value.body, options.reset());
|
|
|
|
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);
|
|
}
|
|
|
|
// 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 *= options.style.sizeMultiplier;
|
|
innerDepth *= options.style.sizeMultiplier;
|
|
|
|
var leftDelim;
|
|
if (group.value.left === ".") {
|
|
// Empty delimiters in \left and \right make null delimiter spaces.
|
|
leftDelim = makeSpan(["nulldelimiter"]);
|
|
} else {
|
|
// Otherwise, use leftRightDelim to generate the correct sized
|
|
// delimiter.
|
|
leftDelim = delimiter.leftRightDelim(
|
|
group.value.left, innerHeight, innerDepth, options,
|
|
group.mode);
|
|
}
|
|
// Add it to the beginning of the expression
|
|
inner.unshift(leftDelim);
|
|
|
|
var rightDelim;
|
|
// Same for the right delimiter
|
|
if (group.value.right === ".") {
|
|
rightDelim = makeSpan(["nulldelimiter"]);
|
|
} else {
|
|
rightDelim = delimiter.leftRightDelim(
|
|
group.value.right, innerHeight, innerDepth, options,
|
|
group.mode);
|
|
}
|
|
// Add it to the end of the expression.
|
|
inner.push(rightDelim);
|
|
|
|
return makeSpan(
|
|
["minner", options.style.cls()], inner, options.getColor());
|
|
},
|
|
|
|
rule: function(group, options, prev) {
|
|
// Make an empty span for the rule
|
|
var rule = makeSpan(["mord", "rule"], [], options.getColor());
|
|
|
|
// 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 *= fontMetrics.metrics.xHeight;
|
|
}
|
|
}
|
|
|
|
var width = group.value.width.number;
|
|
if (group.value.width.unit === "ex") {
|
|
width *= fontMetrics.metrics.xHeight;
|
|
}
|
|
|
|
var height = group.value.height.number;
|
|
if (group.value.height.unit === "ex") {
|
|
height *= fontMetrics.metrics.xHeight;
|
|
}
|
|
|
|
// The sizes of rules are absolute, so make it larger if we are in a
|
|
// smaller style.
|
|
shift /= options.style.sizeMultiplier;
|
|
width /= options.style.sizeMultiplier;
|
|
height /= options.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;
|
|
},
|
|
|
|
accent: function(group, options, prev) {
|
|
// Accents are handled in the TeXbook pg. 443, rule 12.
|
|
var base = group.value.base;
|
|
|
|
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(), prev);
|
|
}
|
|
|
|
// Build the base group
|
|
var body = buildGroup(
|
|
base, options.withStyle(options.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(options.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, fontMetrics.metrics.xHeight);
|
|
|
|
// Build the accent
|
|
var accent = buildCommon.makeSymbol(
|
|
group.value.accent, "Main-Regular", "math", options.getColor());
|
|
// 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]);
|
|
|
|
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;
|
|
}
|
|
},
|
|
|
|
phantom: function(group, options, prev) {
|
|
var elements = buildExpression(
|
|
group.value.value,
|
|
options.withPhantom(),
|
|
prev
|
|
);
|
|
|
|
// \phantom isn't supposed to affect the elements it contains.
|
|
// See "color" for more details.
|
|
return new buildCommon.makeFragment(elements);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 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, prev) {
|
|
if (!group) {
|
|
return makeSpan();
|
|
}
|
|
|
|
if (groupTypes[group.type]) {
|
|
// Call the groupTypes function
|
|
var groupNode = groupTypes[group.type](group, options, prev);
|
|
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, settings) {
|
|
// 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));
|
|
|
|
var startStyle = Style.TEXT;
|
|
if (settings.displayMode) {
|
|
startStyle = Style.DISPLAY;
|
|
}
|
|
|
|
// Setup the default options
|
|
var options = new Options({
|
|
style: startStyle,
|
|
size: "size5"
|
|
});
|
|
|
|
// Build the expression contained in the tree
|
|
var expression = buildExpression(tree, options);
|
|
var body = makeSpan(["base", options.style.cls()], expression);
|
|
|
|
// 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;
|