Spaces:
Running
on
T4
Running
on
T4
/* 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; | |
/** | |
* 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, | |
// see https://github.com/Khan/KaTeX/wiki/Examining-TeX#group-types | |
var groupToType = { | |
mathord: "mord", | |
textord: "mord", | |
bin: "mbin", | |
rel: "mrel", | |
text: "mord", | |
open: "mopen", | |
close: "mclose", | |
inner: "minner", | |
genfrac: "mord", | |
array: "mord", | |
spacing: "mord", | |
punct: "mpunct", | |
ordgroup: "mord", | |
op: "mop", | |
katex: "mord", | |
overline: "mord", | |
underline: "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 || | |
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 { | |
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) { | |
return makeSpan([ | |
"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, prev) { | |
return buildCommon.makeOrd(group, options, "mathord"); | |
}; | |
groupTypes.textord = function(group, options, prev) { | |
return buildCommon.makeOrd(group, options, "textord"); | |
}; | |
groupTypes.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.mathsym( | |
group.value, group.mode, options.getColor(), [className]); | |
}; | |
groupTypes.rel = function(group, options, prev) { | |
return buildCommon.mathsym( | |
group.value, group.mode, options.getColor(), ["mrel"]); | |
}; | |
groupTypes.open = function(group, options, prev) { | |
return buildCommon.mathsym( | |
group.value, group.mode, options.getColor(), ["mopen"]); | |
}; | |
groupTypes.close = function(group, options, prev) { | |
return buildCommon.mathsym( | |
group.value, group.mode, options.getColor(), ["mclose"]); | |
}; | |
groupTypes.inner = function(group, options, prev) { | |
return buildCommon.mathsym( | |
group.value, group.mode, options.getColor(), ["minner"]); | |
}; | |
groupTypes.punct = function(group, options, prev) { | |
return buildCommon.mathsym( | |
group.value, group.mode, options.getColor(), ["mpunct"]); | |
}; | |
groupTypes.ordgroup = function(group, options, prev) { | |
return makeSpan( | |
["mord", options.style.cls()], | |
buildExpression(group.value, options.reset()) | |
); | |
}; | |
groupTypes.text = function(group, options, prev) { | |
return makeSpan(["text", "mord", options.style.cls()], | |
buildExpression(group.value.body, options.reset())); | |
}; | |
groupTypes.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); | |
}; | |
groupTypes.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; | |
var submid; | |
var sup; | |
var 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; | |
var 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]); | |
}; | |
groupTypes.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 delimSize; | |
if (fstyle.size === Style.DISPLAY.size) { | |
delimSize = fontMetrics.metrics.delim1; | |
} else { | |
delimSize = fontMetrics.metrics.getDelim2(fstyle); | |
} | |
var leftDelim; | |
var rightDelim; | |
if (group.value.leftDelim == null) { | |
leftDelim = makeNullDelimiter(options); | |
} else { | |
leftDelim = delimiter.customSizedDelim( | |
group.value.leftDelim, delimSize, true, | |
options.withStyle(fstyle), group.mode); | |
} | |
if (group.value.rightDelim == null) { | |
rightDelim = makeNullDelimiter(options); | |
} else { | |
rightDelim = delimiter.customSizedDelim( | |
group.value.rightDelim, delimSize, true, | |
options.withStyle(fstyle), group.mode); | |
} | |
return makeSpan( | |
["mord", options.style.reset(), fstyle.cls()], | |
[leftDelim, makeSpan(["mfrac"], [frac]), rightDelim], | |
options.getColor()); | |
}; | |
groupTypes.array = function(group, options, prev) { | |
var r; | |
var 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; | |
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.getColor()); | |
}; | |
groupTypes.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.mathsym(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]); | |
} | |
}; | |
groupTypes.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]); | |
}; | |
groupTypes.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]); | |
}; | |
groupTypes.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.mathsym(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; | |
var supKern; | |
var submid; | |
var 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; | |
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]); | |
} else { | |
if (group.value.symbol) { | |
base.style.top = baseShift + "em"; | |
} | |
return base; | |
} | |
}; | |
groupTypes.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.mathsym("K", group.mode)]); | |
var a = makeSpan( | |
["a"], [buildCommon.mathsym("A", group.mode)]); | |
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)]); | |
var e = makeSpan( | |
["e"], [buildCommon.mathsym("E", group.mode)]); | |
e.height = (e.height - 0.2155); | |
e.depth = (e.depth + 0.2155); | |
var x = makeSpan( | |
["x"], [buildCommon.mathsym("X", group.mode)]); | |
return makeSpan( | |
["katex-logo", "mord"], [k, a, t, e, x], options.getColor()); | |
}; | |
groupTypes.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()); | |
}; | |
groupTypes.underline = function(group, options, prev) { | |
// Underlines are handled in the TeXbook pg 443, Rule 10. | |
// Build the inner group. | |
var innerGroup = buildGroup(group.value.body, options); | |
var ruleWidth = fontMetrics.metrics.defaultRuleThickness / | |
options.style.sizeMultiplier; | |
// Create the line above the body | |
var line = makeSpan( | |
[options.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(["underline", "mord"], [vlist], options.getColor()); | |
}; | |
groupTypes.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]); | |
} | |
}; | |
groupTypes.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; | |
}; | |
groupTypes.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); | |
}; | |
groupTypes.font = function(group, options, prev) { | |
var font = group.value.font; | |
return buildGroup(group.value.body, options.withFont(font), prev); | |
}; | |
groupTypes.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)]); | |
}; | |
groupTypes.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 = makeNullDelimiter(options); | |
} 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 = makeNullDelimiter(options); | |
} 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()); | |
}; | |
groupTypes.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; | |
}; | |
groupTypes.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; | |
} | |
}; | |
groupTypes.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, 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); | |
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; | |