diff options
author | Christopher Speller <crspeller@gmail.com> | 2016-03-14 08:50:46 -0400 |
---|---|---|
committer | Christopher Speller <crspeller@gmail.com> | 2016-03-16 18:02:55 -0400 |
commit | 12896bd23eeba79884245c1c29fdc568cf21a7fa (patch) | |
tree | 4e7f83d3e2564b9b89d669e9f7905ff11768b11a /webapp/non_npm_dependencies/katex/src/buildHTML.js | |
parent | 29fe6a3d13c9c7aa490fffebbe5d1b5fdf1e3090 (diff) | |
download | chat-12896bd23eeba79884245c1c29fdc568cf21a7fa.tar.gz chat-12896bd23eeba79884245c1c29fdc568cf21a7fa.tar.bz2 chat-12896bd23eeba79884245c1c29fdc568cf21a7fa.zip |
Converting to Webpack. Stage 1.
Diffstat (limited to 'webapp/non_npm_dependencies/katex/src/buildHTML.js')
-rw-r--r-- | webapp/non_npm_dependencies/katex/src/buildHTML.js | 1375 |
1 files changed, 1375 insertions, 0 deletions
diff --git a/webapp/non_npm_dependencies/katex/src/buildHTML.js b/webapp/non_npm_dependencies/katex/src/buildHTML.js new file mode 100644 index 000000000..dbd33a3fc --- /dev/null +++ b/webapp/non_npm_dependencies/katex/src/buildHTML.js @@ -0,0 +1,1375 @@ +/* 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", + 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.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; |