www

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

buildHTML.js (55826B)


      1 /* eslint no-console:0 */
      2 /**
      3  * This file does the main work of building a domTree structure from a parse
      4  * tree. The entry point is the `buildHTML` function, which takes a parse tree.
      5  * Then, the buildExpression, buildGroup, and various groupTypes functions are
      6  * called, to produce a final HTML tree.
      7  */
      8 
      9 const ParseError = require("./ParseError");
     10 const Style = require("./Style");
     11 
     12 const buildCommon = require("./buildCommon");
     13 const delimiter = require("./delimiter");
     14 const domTree = require("./domTree");
     15 const fontMetrics = require("./fontMetrics");
     16 const utils = require("./utils");
     17 
     18 const makeSpan = buildCommon.makeSpan;
     19 
     20 const isSpace = function(node) {
     21     return node instanceof domTree.span && node.classes[0] === "mspace";
     22 };
     23 
     24 // Binary atoms (first class `mbin`) change into ordinary atoms (`mord`)
     25 // depending on their surroundings. See TeXbook pg. 442-446, Rules 5 and 6,
     26 // and the text before Rule 19.
     27 
     28 const isBin = function(node) {
     29     return node && node.classes[0] === "mbin";
     30 };
     31 
     32 const isBinLeftCanceller = function(node, isRealGroup) {
     33     // TODO: This code assumes that a node's math class is the first element
     34     // of its `classes` array. A later cleanup should ensure this, for
     35     // instance by changing the signature of `makeSpan`.
     36     if (node) {
     37         return utils.contains(["mbin", "mopen", "mrel", "mop", "mpunct"],
     38                               node.classes[0]);
     39     } else {
     40         return isRealGroup;
     41     }
     42 };
     43 
     44 const isBinRightCanceller = function(node, isRealGroup) {
     45     if (node) {
     46         return utils.contains(["mrel", "mclose", "mpunct"], node.classes[0]);
     47     } else {
     48         return isRealGroup;
     49     }
     50 };
     51 
     52 /**
     53  * Splice out any spaces from `children` starting at position `i`, and return
     54  * the spliced-out array. Returns null if `children[i]` does not exist or is not
     55  * a space.
     56  */
     57 const spliceSpaces = function(children, i) {
     58     let j = i;
     59     while (j < children.length && isSpace(children[j])) {
     60         j++;
     61     }
     62     if (j === i) {
     63         return null;
     64     } else {
     65         return children.splice(i, j - i);
     66     }
     67 };
     68 
     69 /**
     70  * Take a list of nodes, build them in order, and return a list of the built
     71  * nodes. documentFragments are flattened into their contents, so the
     72  * returned list contains no fragments. `isRealGroup` is true if `expression`
     73  * is a real group (no atoms will be added on either side), as opposed to
     74  * a partial group (e.g. one created by \color).
     75  */
     76 const buildExpression = function(expression, options, isRealGroup) {
     77     // Parse expressions into `groups`.
     78     const groups = [];
     79     for (let i = 0; i < expression.length; i++) {
     80         const group = expression[i];
     81         const output = buildGroup(group, options);
     82         if (output instanceof domTree.documentFragment) {
     83             Array.prototype.push.apply(groups, output.children);
     84         } else {
     85             groups.push(output);
     86         }
     87     }
     88     // At this point `groups` consists entirely of `symbolNode`s and `span`s.
     89 
     90     // Explicit spaces (e.g., \;, \,) should be ignored with respect to atom
     91     // spacing (e.g., "add thick space between mord and mrel"). Since CSS
     92     // adjacency rules implement atom spacing, spaces should be invisible to
     93     // CSS. So we splice them out of `groups` and into the atoms themselves.
     94     for (let i = 0; i < groups.length; i++) {
     95         const spaces = spliceSpaces(groups, i);
     96         if (spaces) {
     97             // Splicing of spaces may have removed all remaining groups.
     98             if (i < groups.length) {
     99                 // If there is a following group, move space within it.
    100                 if (groups[i] instanceof domTree.symbolNode) {
    101                     groups[i] = makeSpan([].concat(groups[i].classes),
    102                         [groups[i]]);
    103                 }
    104                 buildCommon.prependChildren(groups[i], spaces);
    105             } else {
    106                 // Otherwise, put any spaces back at the end of the groups.
    107                 Array.prototype.push.apply(groups, spaces);
    108                 break;
    109             }
    110         }
    111     }
    112 
    113     // Binary operators change to ordinary symbols in some contexts.
    114     for (let i = 0; i < groups.length; i++) {
    115         if (isBin(groups[i])
    116             && (isBinLeftCanceller(groups[i - 1], isRealGroup)
    117                 || isBinRightCanceller(groups[i + 1], isRealGroup))) {
    118             groups[i].classes[0] = "mord";
    119         }
    120     }
    121 
    122     return groups;
    123 };
    124 
    125 // Return math atom class (mclass) of a domTree.
    126 const getTypeOfDomTree = function(node) {
    127     if (node instanceof domTree.documentFragment) {
    128         if (node.children.length) {
    129             return getTypeOfDomTree(
    130                 node.children[node.children.length - 1]);
    131         }
    132     } else {
    133         if (utils.contains([
    134             "mord", "mop", "mbin", "mrel", "mopen", "mclose",
    135             "mpunct", "minner",
    136         ], node.classes[0])) {
    137             return node.classes[0];
    138         }
    139     }
    140     return null;
    141 };
    142 
    143 /**
    144  * Sometimes, groups perform special rules when they have superscripts or
    145  * subscripts attached to them. This function lets the `supsub` group know that
    146  * its inner element should handle the superscripts and subscripts instead of
    147  * handling them itself.
    148  */
    149 const shouldHandleSupSub = function(group, options) {
    150     if (!group) {
    151         return false;
    152     } else if (group.type === "op") {
    153         // Operators handle supsubs differently when they have limits
    154         // (e.g. `\displaystyle\sum_2^3`)
    155         return group.value.limits &&
    156             (options.style.size === Style.DISPLAY.size ||
    157             group.value.alwaysHandleSupSub);
    158     } else if (group.type === "accent") {
    159         return isCharacterBox(group.value.base);
    160     } else {
    161         return null;
    162     }
    163 };
    164 
    165 /**
    166  * Sometimes we want to pull out the innermost element of a group. In most
    167  * cases, this will just be the group itself, but when ordgroups and colors have
    168  * a single element, we want to pull that out.
    169  */
    170 const getBaseElem = function(group) {
    171     if (!group) {
    172         return false;
    173     } else if (group.type === "ordgroup") {
    174         if (group.value.length === 1) {
    175             return getBaseElem(group.value[0]);
    176         } else {
    177             return group;
    178         }
    179     } else if (group.type === "color") {
    180         if (group.value.value.length === 1) {
    181             return getBaseElem(group.value.value[0]);
    182         } else {
    183             return group;
    184         }
    185     } else if (group.type === "font") {
    186         return getBaseElem(group.value.body);
    187     } else {
    188         return group;
    189     }
    190 };
    191 
    192 /**
    193  * TeXbook algorithms often reference "character boxes", which are simply groups
    194  * with a single character in them. To decide if something is a character box,
    195  * we find its innermost group, and see if it is a single character.
    196  */
    197 const isCharacterBox = function(group) {
    198     const baseElem = getBaseElem(group);
    199 
    200     // These are all they types of groups which hold single characters
    201     return baseElem.type === "mathord" ||
    202         baseElem.type === "textord" ||
    203         baseElem.type === "bin" ||
    204         baseElem.type === "rel" ||
    205         baseElem.type === "inner" ||
    206         baseElem.type === "open" ||
    207         baseElem.type === "close" ||
    208         baseElem.type === "punct";
    209 };
    210 
    211 const makeNullDelimiter = function(options, classes) {
    212     return makeSpan(classes.concat([
    213         "sizing", "reset-" + options.size, "size5",
    214         options.style.reset(), Style.TEXT.cls(),
    215         "nulldelimiter"]));
    216 };
    217 
    218 /**
    219  * This is a map of group types to the function used to handle that type.
    220  * Simpler types come at the beginning, while complicated types come afterwards.
    221  */
    222 const groupTypes = {};
    223 
    224 groupTypes.mathord = function(group, options) {
    225     return buildCommon.makeOrd(group, options, "mathord");
    226 };
    227 
    228 groupTypes.textord = function(group, options) {
    229     return buildCommon.makeOrd(group, options, "textord");
    230 };
    231 
    232 groupTypes.bin = function(group, options) {
    233     return buildCommon.mathsym(
    234         group.value, group.mode, options, ["mbin"]);
    235 };
    236 
    237 groupTypes.rel = function(group, options) {
    238     return buildCommon.mathsym(
    239         group.value, group.mode, options, ["mrel"]);
    240 };
    241 
    242 groupTypes.open = function(group, options) {
    243     return buildCommon.mathsym(
    244         group.value, group.mode, options, ["mopen"]);
    245 };
    246 
    247 groupTypes.close = function(group, options) {
    248     return buildCommon.mathsym(
    249         group.value, group.mode, options, ["mclose"]);
    250 };
    251 
    252 groupTypes.inner = function(group, options) {
    253     return buildCommon.mathsym(
    254         group.value, group.mode, options, ["minner"]);
    255 };
    256 
    257 groupTypes.punct = function(group, options) {
    258     return buildCommon.mathsym(
    259         group.value, group.mode, options, ["mpunct"]);
    260 };
    261 
    262 groupTypes.ordgroup = function(group, options) {
    263     return makeSpan(
    264         ["mord", options.style.cls()],
    265         buildExpression(group.value, options.reset(), true),
    266         options
    267     );
    268 };
    269 
    270 groupTypes.text = function(group, options) {
    271     const newOptions = options.withFont(group.value.style);
    272     const inner = buildExpression(group.value.body, newOptions, true);
    273     for (let i = 0; i < inner.length - 1; i++) {
    274         if (inner[i].tryCombine(inner[i + 1])) {
    275             inner.splice(i + 1, 1);
    276             i--;
    277         }
    278     }
    279     return makeSpan(["mord", "text", newOptions.style.cls()],
    280         inner, newOptions);
    281 };
    282 
    283 groupTypes.color = function(group, options) {
    284     const elements = buildExpression(
    285         group.value.value,
    286         options.withColor(group.value.color),
    287         false
    288     );
    289 
    290     // \color isn't supposed to affect the type of the elements it contains.
    291     // To accomplish this, we wrap the results in a fragment, so the inner
    292     // elements will be able to directly interact with their neighbors. For
    293     // example, `\color{red}{2 +} 3` has the same spacing as `2 + 3`
    294     return new buildCommon.makeFragment(elements);
    295 };
    296 
    297 groupTypes.supsub = function(group, options) {
    298     // Superscript and subscripts are handled in the TeXbook on page
    299     // 445-446, rules 18(a-f).
    300 
    301     // Here is where we defer to the inner group if it should handle
    302     // superscripts and subscripts itself.
    303     if (shouldHandleSupSub(group.value.base, options)) {
    304         return groupTypes[group.value.base.type](group, options);
    305     }
    306 
    307     const base = buildGroup(group.value.base, options.reset());
    308     let supmid;
    309     let submid;
    310     let sup;
    311     let sub;
    312 
    313     const style = options.style;
    314     let newOptions;
    315 
    316     if (group.value.sup) {
    317         newOptions = options.withStyle(style.sup());
    318         sup = buildGroup(group.value.sup, newOptions);
    319         supmid = makeSpan([style.reset(), style.sup().cls()],
    320             [sup], newOptions);
    321     }
    322 
    323     if (group.value.sub) {
    324         newOptions = options.withStyle(style.sub());
    325         sub = buildGroup(group.value.sub, newOptions);
    326         submid = makeSpan([style.reset(), style.sub().cls()],
    327             [sub], newOptions);
    328     }
    329 
    330     // Rule 18a
    331     let supShift;
    332     let subShift;
    333     if (isCharacterBox(group.value.base)) {
    334         supShift = 0;
    335         subShift = 0;
    336     } else {
    337         supShift = base.height - style.metrics.supDrop;
    338         subShift = base.depth + style.metrics.subDrop;
    339     }
    340 
    341     // Rule 18c
    342     let minSupShift;
    343     if (style === Style.DISPLAY) {
    344         minSupShift = style.metrics.sup1;
    345     } else if (style.cramped) {
    346         minSupShift = style.metrics.sup3;
    347     } else {
    348         minSupShift = style.metrics.sup2;
    349     }
    350 
    351     // scriptspace is a font-size-independent size, so scale it
    352     // appropriately
    353     const multiplier = Style.TEXT.sizeMultiplier *
    354             style.sizeMultiplier;
    355     const scriptspace =
    356         (0.5 / fontMetrics.metrics.ptPerEm) / multiplier + "em";
    357 
    358     let supsub;
    359     if (!group.value.sup) {
    360         // Rule 18b
    361         subShift = Math.max(
    362             subShift, style.metrics.sub1,
    363             sub.height - 0.8 * style.metrics.xHeight);
    364 
    365         supsub = buildCommon.makeVList([
    366             {type: "elem", elem: submid},
    367         ], "shift", subShift, options);
    368 
    369         supsub.children[0].style.marginRight = scriptspace;
    370 
    371         // Subscripts shouldn't be shifted by the base's italic correction.
    372         // Account for that by shifting the subscript back the appropriate
    373         // amount. Note we only do this when the base is a single symbol.
    374         if (base instanceof domTree.symbolNode) {
    375             supsub.children[0].style.marginLeft = -base.italic + "em";
    376         }
    377     } else if (!group.value.sub) {
    378         // Rule 18c, d
    379         supShift = Math.max(supShift, minSupShift,
    380             sup.depth + 0.25 * style.metrics.xHeight);
    381 
    382         supsub = buildCommon.makeVList([
    383             {type: "elem", elem: supmid},
    384         ], "shift", -supShift, options);
    385 
    386         supsub.children[0].style.marginRight = scriptspace;
    387     } else {
    388         supShift = Math.max(
    389             supShift, minSupShift, sup.depth + 0.25 * style.metrics.xHeight);
    390         subShift = Math.max(subShift, style.metrics.sub2);
    391 
    392         const ruleWidth = fontMetrics.metrics.defaultRuleThickness;
    393 
    394         // Rule 18e
    395         if ((supShift - sup.depth) - (sub.height - subShift) <
    396                 4 * ruleWidth) {
    397             subShift = 4 * ruleWidth - (supShift - sup.depth) + sub.height;
    398             const psi = 0.8 * style.metrics.xHeight - (supShift - sup.depth);
    399             if (psi > 0) {
    400                 supShift += psi;
    401                 subShift -= psi;
    402             }
    403         }
    404 
    405         supsub = buildCommon.makeVList([
    406             {type: "elem", elem: submid, shift: subShift},
    407             {type: "elem", elem: supmid, shift: -supShift},
    408         ], "individualShift", null, options);
    409 
    410         // See comment above about subscripts not being shifted
    411         if (base instanceof domTree.symbolNode) {
    412             supsub.children[0].style.marginLeft = -base.italic + "em";
    413         }
    414 
    415         supsub.children[0].style.marginRight = scriptspace;
    416         supsub.children[1].style.marginRight = scriptspace;
    417     }
    418 
    419     // We ensure to wrap the supsub vlist in a span.msupsub to reset text-align
    420     const mclass = getTypeOfDomTree(base) || "mord";
    421     return makeSpan([mclass],
    422         [base, makeSpan(["msupsub"], [supsub])],
    423         options);
    424 };
    425 
    426 groupTypes.genfrac = function(group, options) {
    427     // Fractions are handled in the TeXbook on pages 444-445, rules 15(a-e).
    428     // Figure out what style this fraction should be in based on the
    429     // function used
    430     let style = options.style;
    431     if (group.value.size === "display") {
    432         style = Style.DISPLAY;
    433     } else if (group.value.size === "text") {
    434         style = Style.TEXT;
    435     }
    436 
    437     const nstyle = style.fracNum();
    438     const dstyle = style.fracDen();
    439     let newOptions;
    440 
    441     newOptions = options.withStyle(nstyle);
    442     const numer = buildGroup(group.value.numer, newOptions);
    443     const numerreset = makeSpan([style.reset(), nstyle.cls()],
    444         [numer], newOptions);
    445 
    446     newOptions = options.withStyle(dstyle);
    447     const denom = buildGroup(group.value.denom, newOptions);
    448     const denomreset = makeSpan([style.reset(), dstyle.cls()],
    449         [denom], newOptions);
    450 
    451     let ruleWidth;
    452     if (group.value.hasBarLine) {
    453         ruleWidth = fontMetrics.metrics.defaultRuleThickness /
    454             options.style.sizeMultiplier;
    455     } else {
    456         ruleWidth = 0;
    457     }
    458 
    459     // Rule 15b
    460     let numShift;
    461     let clearance;
    462     let denomShift;
    463     if (style.size === Style.DISPLAY.size) {
    464         numShift = style.metrics.num1;
    465         if (ruleWidth > 0) {
    466             clearance = 3 * ruleWidth;
    467         } else {
    468             clearance = 7 * fontMetrics.metrics.defaultRuleThickness;
    469         }
    470         denomShift = style.metrics.denom1;
    471     } else {
    472         if (ruleWidth > 0) {
    473             numShift = style.metrics.num2;
    474             clearance = ruleWidth;
    475         } else {
    476             numShift = style.metrics.num3;
    477             clearance = 3 * fontMetrics.metrics.defaultRuleThickness;
    478         }
    479         denomShift = style.metrics.denom2;
    480     }
    481 
    482     let frac;
    483     if (ruleWidth === 0) {
    484         // Rule 15c
    485         const candidateClearance =
    486             (numShift - numer.depth) - (denom.height - denomShift);
    487         if (candidateClearance < clearance) {
    488             numShift += 0.5 * (clearance - candidateClearance);
    489             denomShift += 0.5 * (clearance - candidateClearance);
    490         }
    491 
    492         frac = buildCommon.makeVList([
    493             {type: "elem", elem: denomreset, shift: denomShift},
    494             {type: "elem", elem: numerreset, shift: -numShift},
    495         ], "individualShift", null, options);
    496     } else {
    497         // Rule 15d
    498         const axisHeight = style.metrics.axisHeight;
    499 
    500         if ((numShift - numer.depth) - (axisHeight + 0.5 * ruleWidth) <
    501                 clearance) {
    502             numShift +=
    503                 clearance - ((numShift - numer.depth) -
    504                              (axisHeight + 0.5 * ruleWidth));
    505         }
    506 
    507         if ((axisHeight - 0.5 * ruleWidth) - (denom.height - denomShift) <
    508                 clearance) {
    509             denomShift +=
    510                 clearance - ((axisHeight - 0.5 * ruleWidth) -
    511                              (denom.height - denomShift));
    512         }
    513 
    514         const mid = makeSpan(
    515             [options.style.reset(), Style.TEXT.cls(), "frac-line"]);
    516         // Manually set the height of the line because its height is
    517         // created in CSS
    518         mid.height = ruleWidth;
    519 
    520         const midShift = -(axisHeight - 0.5 * ruleWidth);
    521 
    522         frac = buildCommon.makeVList([
    523             {type: "elem", elem: denomreset, shift: denomShift},
    524             {type: "elem", elem: mid,        shift: midShift},
    525             {type: "elem", elem: numerreset, shift: -numShift},
    526         ], "individualShift", null, options);
    527     }
    528 
    529     // Since we manually change the style sometimes (with \dfrac or \tfrac),
    530     // account for the possible size change here.
    531     frac.height *= style.sizeMultiplier / options.style.sizeMultiplier;
    532     frac.depth *= style.sizeMultiplier / options.style.sizeMultiplier;
    533 
    534     // Rule 15e
    535     let delimSize;
    536     if (style.size === Style.DISPLAY.size) {
    537         delimSize = style.metrics.delim1;
    538     } else {
    539         delimSize = style.metrics.delim2;
    540     }
    541 
    542     let leftDelim;
    543     let rightDelim;
    544     if (group.value.leftDelim == null) {
    545         leftDelim = makeNullDelimiter(options, ["mopen"]);
    546     } else {
    547         leftDelim = delimiter.customSizedDelim(
    548             group.value.leftDelim, delimSize, true,
    549             options.withStyle(style), group.mode, ["mopen"]);
    550     }
    551     if (group.value.rightDelim == null) {
    552         rightDelim = makeNullDelimiter(options, ["mclose"]);
    553     } else {
    554         rightDelim = delimiter.customSizedDelim(
    555             group.value.rightDelim, delimSize, true,
    556             options.withStyle(style), group.mode, ["mclose"]);
    557     }
    558 
    559     return makeSpan(
    560         ["mord", options.style.reset(), style.cls()],
    561         [leftDelim, makeSpan(["mfrac"], [frac]), rightDelim],
    562         options);
    563 };
    564 
    565 const calculateSize = function(sizeValue, style) {
    566     let x = sizeValue.number;
    567     if (sizeValue.unit === "ex") {
    568         x *= style.metrics.emPerEx;
    569     } else if (sizeValue.unit === "mu") {
    570         x /= 18;
    571     }
    572     return x;
    573 };
    574 
    575 groupTypes.array = function(group, options) {
    576     let r;
    577     let c;
    578     const nr = group.value.body.length;
    579     let nc = 0;
    580     let body = new Array(nr);
    581 
    582     const style = options.style;
    583 
    584     // Horizontal spacing
    585     const pt = 1 / fontMetrics.metrics.ptPerEm;
    586     const arraycolsep = 5 * pt; // \arraycolsep in article.cls
    587 
    588     // Vertical spacing
    589     const baselineskip = 12 * pt; // see size10.clo
    590     // Default \arraystretch from lttab.dtx
    591     // TODO(gagern): may get redefined once we have user-defined macros
    592     const arraystretch = utils.deflt(group.value.arraystretch, 1);
    593     const arrayskip = arraystretch * baselineskip;
    594     const arstrutHeight = 0.7 * arrayskip; // \strutbox in ltfsstrc.dtx and
    595     const arstrutDepth = 0.3 * arrayskip;  // \@arstrutbox in lttab.dtx
    596 
    597     let totalHeight = 0;
    598     for (r = 0; r < group.value.body.length; ++r) {
    599         const inrow = group.value.body[r];
    600         let height = arstrutHeight; // \@array adds an \@arstrut
    601         let depth = arstrutDepth;   // to each tow (via the template)
    602 
    603         if (nc < inrow.length) {
    604             nc = inrow.length;
    605         }
    606 
    607         const outrow = new Array(inrow.length);
    608         for (c = 0; c < inrow.length; ++c) {
    609             const elt = buildGroup(inrow[c], options);
    610             if (depth < elt.depth) {
    611                 depth = elt.depth;
    612             }
    613             if (height < elt.height) {
    614                 height = elt.height;
    615             }
    616             outrow[c] = elt;
    617         }
    618 
    619         let gap = 0;
    620         if (group.value.rowGaps[r]) {
    621             gap = calculateSize(group.value.rowGaps[r].value, style);
    622             if (gap > 0) { // \@argarraycr
    623                 gap += arstrutDepth;
    624                 if (depth < gap) {
    625                     depth = gap; // \@xargarraycr
    626                 }
    627                 gap = 0;
    628             }
    629         }
    630 
    631         outrow.height = height;
    632         outrow.depth = depth;
    633         totalHeight += height;
    634         outrow.pos = totalHeight;
    635         totalHeight += depth + gap; // \@yargarraycr
    636         body[r] = outrow;
    637     }
    638 
    639     const offset = totalHeight / 2 + style.metrics.axisHeight;
    640     const colDescriptions = group.value.cols || [];
    641     const cols = [];
    642     let colSep;
    643     let colDescrNum;
    644     for (c = 0, colDescrNum = 0;
    645          // Continue while either there are more columns or more column
    646          // descriptions, so trailing separators don't get lost.
    647          c < nc || colDescrNum < colDescriptions.length;
    648          ++c, ++colDescrNum) {
    649 
    650         let colDescr = colDescriptions[colDescrNum] || {};
    651 
    652         let firstSeparator = true;
    653         while (colDescr.type === "separator") {
    654             // If there is more than one separator in a row, add a space
    655             // between them.
    656             if (!firstSeparator) {
    657                 colSep = makeSpan(["arraycolsep"], []);
    658                 colSep.style.width =
    659                     fontMetrics.metrics.doubleRuleSep + "em";
    660                 cols.push(colSep);
    661             }
    662 
    663             if (colDescr.separator === "|") {
    664                 const separator = makeSpan(
    665                     ["vertical-separator"],
    666                     []);
    667                 separator.style.height = totalHeight + "em";
    668                 separator.style.verticalAlign =
    669                     -(totalHeight - offset) + "em";
    670 
    671                 cols.push(separator);
    672             } else {
    673                 throw new ParseError(
    674                     "Invalid separator type: " + colDescr.separator);
    675             }
    676 
    677             colDescrNum++;
    678             colDescr = colDescriptions[colDescrNum] || {};
    679             firstSeparator = false;
    680         }
    681 
    682         if (c >= nc) {
    683             continue;
    684         }
    685 
    686         let sepwidth;
    687         if (c > 0 || group.value.hskipBeforeAndAfter) {
    688             sepwidth = utils.deflt(colDescr.pregap, arraycolsep);
    689             if (sepwidth !== 0) {
    690                 colSep = makeSpan(["arraycolsep"], []);
    691                 colSep.style.width = sepwidth + "em";
    692                 cols.push(colSep);
    693             }
    694         }
    695 
    696         let col = [];
    697         for (r = 0; r < nr; ++r) {
    698             const row = body[r];
    699             const elem = row[c];
    700             if (!elem) {
    701                 continue;
    702             }
    703             const shift = row.pos - offset;
    704             elem.depth = row.depth;
    705             elem.height = row.height;
    706             col.push({type: "elem", elem: elem, shift: shift});
    707         }
    708 
    709         col = buildCommon.makeVList(col, "individualShift", null, options);
    710         col = makeSpan(
    711             ["col-align-" + (colDescr.align || "c")],
    712             [col]);
    713         cols.push(col);
    714 
    715         if (c < nc - 1 || group.value.hskipBeforeAndAfter) {
    716             sepwidth = utils.deflt(colDescr.postgap, arraycolsep);
    717             if (sepwidth !== 0) {
    718                 colSep = makeSpan(["arraycolsep"], []);
    719                 colSep.style.width = sepwidth + "em";
    720                 cols.push(colSep);
    721             }
    722         }
    723     }
    724     body = makeSpan(["mtable"], cols);
    725     return makeSpan(["mord"], [body], options);
    726 };
    727 
    728 groupTypes.spacing = function(group, options) {
    729     if (group.value === "\\ " || group.value === "\\space" ||
    730         group.value === " " || group.value === "~") {
    731         // Spaces are generated by adding an actual space. Each of these
    732         // things has an entry in the symbols table, so these will be turned
    733         // into appropriate outputs.
    734         if (group.mode === "text") {
    735             return buildCommon.makeOrd(group, options, "textord");
    736         } else {
    737             return makeSpan(["mspace"],
    738                 [buildCommon.mathsym(group.value, group.mode, options)],
    739                 options);
    740         }
    741     } else {
    742         // Other kinds of spaces are of arbitrary width. We use CSS to
    743         // generate these.
    744         return makeSpan(
    745             ["mspace", buildCommon.spacingFunctions[group.value].className],
    746             [], options);
    747     }
    748 };
    749 
    750 groupTypes.llap = function(group, options) {
    751     const inner = makeSpan(
    752         ["inner"], [buildGroup(group.value.body, options.reset())]);
    753     const fix = makeSpan(["fix"], []);
    754     return makeSpan(
    755         ["mord", "llap", options.style.cls()], [inner, fix], options);
    756 };
    757 
    758 groupTypes.rlap = function(group, options) {
    759     const inner = makeSpan(
    760         ["inner"], [buildGroup(group.value.body, options.reset())]);
    761     const fix = makeSpan(["fix"], []);
    762     return makeSpan(
    763         ["mord", "rlap", options.style.cls()], [inner, fix], options);
    764 };
    765 
    766 groupTypes.op = function(group, options) {
    767     // Operators are handled in the TeXbook pg. 443-444, rule 13(a).
    768     let supGroup;
    769     let subGroup;
    770     let hasLimits = false;
    771     if (group.type === "supsub") {
    772         // If we have limits, supsub will pass us its group to handle. Pull
    773         // out the superscript and subscript and set the group to the op in
    774         // its base.
    775         supGroup = group.value.sup;
    776         subGroup = group.value.sub;
    777         group = group.value.base;
    778         hasLimits = true;
    779     }
    780 
    781     const style = options.style;
    782 
    783     // Most operators have a large successor symbol, but these don't.
    784     const noSuccessor = [
    785         "\\smallint",
    786     ];
    787 
    788     let large = false;
    789     if (style.size === Style.DISPLAY.size &&
    790         group.value.symbol &&
    791         !utils.contains(noSuccessor, group.value.body)) {
    792 
    793         // Most symbol operators get larger in displaystyle (rule 13)
    794         large = true;
    795     }
    796 
    797     let base;
    798     let baseShift = 0;
    799     let slant = 0;
    800     if (group.value.symbol) {
    801         // If this is a symbol, create the symbol.
    802         const fontName = large ? "Size2-Regular" : "Size1-Regular";
    803         base = buildCommon.makeSymbol(
    804             group.value.body, fontName, "math", options,
    805             ["mop", "op-symbol", large ? "large-op" : "small-op"]);
    806 
    807         // Shift the symbol so its center lies on the axis (rule 13). It
    808         // appears that our fonts have the centers of the symbols already
    809         // almost on the axis, so these numbers are very small. Note we
    810         // don't actually apply this here, but instead it is used either in
    811         // the vlist creation or separately when there are no limits.
    812         baseShift = (base.height - base.depth) / 2 -
    813             style.metrics.axisHeight * style.sizeMultiplier;
    814 
    815         // The slant of the symbol is just its italic correction.
    816         slant = base.italic;
    817     } else if (group.value.value) {
    818         // If this is a list, compose that list.
    819         const inner = buildExpression(group.value.value, options, true);
    820 
    821         base = makeSpan(["mop"], inner, options);
    822     } else {
    823         // Otherwise, this is a text operator. Build the text from the
    824         // operator's name.
    825         // TODO(emily): Add a space in the middle of some of these
    826         // operators, like \limsup
    827         const output = [];
    828         for (let i = 1; i < group.value.body.length; i++) {
    829             output.push(buildCommon.mathsym(group.value.body[i], group.mode));
    830         }
    831         base = makeSpan(["mop"], output, options);
    832     }
    833 
    834     if (hasLimits) {
    835         // IE 8 clips \int if it is in a display: inline-block. We wrap it
    836         // in a new span so it is an inline, and works.
    837         base = makeSpan([], [base]);
    838 
    839         let supmid;
    840         let supKern;
    841         let submid;
    842         let subKern;
    843         let newOptions;
    844         // We manually have to handle the superscripts and subscripts. This,
    845         // aside from the kern calculations, is copied from supsub.
    846         if (supGroup) {
    847             newOptions = options.withStyle(style.sup());
    848             const sup = buildGroup(supGroup, newOptions);
    849             supmid = makeSpan([style.reset(), style.sup().cls()],
    850                 [sup], newOptions);
    851 
    852             supKern = Math.max(
    853                 fontMetrics.metrics.bigOpSpacing1,
    854                 fontMetrics.metrics.bigOpSpacing3 - sup.depth);
    855         }
    856 
    857         if (subGroup) {
    858             newOptions = options.withStyle(style.sub());
    859             const sub = buildGroup(subGroup, newOptions);
    860             submid = makeSpan([style.reset(), style.sub().cls()],
    861                 [sub], newOptions);
    862 
    863             subKern = Math.max(
    864                 fontMetrics.metrics.bigOpSpacing2,
    865                 fontMetrics.metrics.bigOpSpacing4 - sub.height);
    866         }
    867 
    868         // Build the final group as a vlist of the possible subscript, base,
    869         // and possible superscript.
    870         let finalGroup;
    871         let top;
    872         let bottom;
    873         if (!supGroup) {
    874             top = base.height - baseShift;
    875 
    876             finalGroup = buildCommon.makeVList([
    877                 {type: "kern", size: fontMetrics.metrics.bigOpSpacing5},
    878                 {type: "elem", elem: submid},
    879                 {type: "kern", size: subKern},
    880                 {type: "elem", elem: base},
    881             ], "top", top, options);
    882 
    883             // Here, we shift the limits by the slant of the symbol. Note
    884             // that we are supposed to shift the limits by 1/2 of the slant,
    885             // but since we are centering the limits adding a full slant of
    886             // margin will shift by 1/2 that.
    887             finalGroup.children[0].style.marginLeft = -slant + "em";
    888         } else if (!subGroup) {
    889             bottom = base.depth + baseShift;
    890 
    891             finalGroup = buildCommon.makeVList([
    892                 {type: "elem", elem: base},
    893                 {type: "kern", size: supKern},
    894                 {type: "elem", elem: supmid},
    895                 {type: "kern", size: fontMetrics.metrics.bigOpSpacing5},
    896             ], "bottom", bottom, options);
    897 
    898             // See comment above about slants
    899             finalGroup.children[1].style.marginLeft = slant + "em";
    900         } else if (!supGroup && !subGroup) {
    901             // This case probably shouldn't occur (this would mean the
    902             // supsub was sending us a group with no superscript or
    903             // subscript) but be safe.
    904             return base;
    905         } else {
    906             bottom = fontMetrics.metrics.bigOpSpacing5 +
    907                 submid.height + submid.depth +
    908                 subKern +
    909                 base.depth + baseShift;
    910 
    911             finalGroup = buildCommon.makeVList([
    912                 {type: "kern", size: fontMetrics.metrics.bigOpSpacing5},
    913                 {type: "elem", elem: submid},
    914                 {type: "kern", size: subKern},
    915                 {type: "elem", elem: base},
    916                 {type: "kern", size: supKern},
    917                 {type: "elem", elem: supmid},
    918                 {type: "kern", size: fontMetrics.metrics.bigOpSpacing5},
    919             ], "bottom", bottom, options);
    920 
    921             // See comment above about slants
    922             finalGroup.children[0].style.marginLeft = -slant + "em";
    923             finalGroup.children[2].style.marginLeft = slant + "em";
    924         }
    925 
    926         return makeSpan(["mop", "op-limits"], [finalGroup], options);
    927     } else {
    928         if (group.value.symbol) {
    929             base.style.top = baseShift + "em";
    930         }
    931 
    932         return base;
    933     }
    934 };
    935 
    936 groupTypes.mod = function(group, options) {
    937     const inner = [];
    938 
    939     if (group.value.modType === "bmod") {
    940         // “\nonscript\mskip-\medmuskip\mkern5mu”
    941         if (!options.style.isTight()) {
    942             inner.push(makeSpan(
    943                 ["mspace", "negativemediumspace"], [], options));
    944         }
    945         inner.push(makeSpan(["mspace", "thickspace"], [], options));
    946     } else if (options.style.size === Style.DISPLAY.size) {
    947         inner.push(makeSpan(["mspace", "quad"], [], options));
    948     } else if (group.value.modType === "mod") {
    949         inner.push(makeSpan(["mspace", "twelvemuspace"], [], options));
    950     } else {
    951         inner.push(makeSpan(["mspace", "eightmuspace"], [], options));
    952     }
    953 
    954     if (group.value.modType === "pod" || group.value.modType === "pmod") {
    955         inner.push(buildCommon.mathsym("(", group.mode));
    956     }
    957 
    958     if (group.value.modType !== "pod") {
    959         const modInner = [
    960             buildCommon.mathsym("m", group.mode),
    961             buildCommon.mathsym("o", group.mode),
    962             buildCommon.mathsym("d", group.mode)];
    963         if (group.value.modType === "bmod") {
    964             inner.push(makeSpan(["mbin"], modInner, options));
    965             // “\mkern5mu\nonscript\mskip-\medmuskip”
    966             inner.push(makeSpan(["mspace", "thickspace"], [], options));
    967             if (!options.style.isTight()) {
    968                 inner.push(makeSpan(
    969                     ["mspace", "negativemediumspace"], [], options));
    970             }
    971         } else {
    972             Array.prototype.push.apply(inner, modInner);
    973             inner.push(makeSpan(["mspace", "sixmuspace"], [], options));
    974         }
    975     }
    976 
    977     if (group.value.value) {
    978         Array.prototype.push.apply(inner,
    979             buildExpression(group.value.value, options, false));
    980     }
    981 
    982     if (group.value.modType === "pod" || group.value.modType === "pmod") {
    983         inner.push(buildCommon.mathsym(")", group.mode));
    984     }
    985 
    986     return buildCommon.makeFragment(inner);
    987 };
    988 
    989 groupTypes.katex = function(group, options) {
    990     // The KaTeX logo. The offsets for the K and a were chosen to look
    991     // good, but the offsets for the T, E, and X were taken from the
    992     // definition of \TeX in TeX (see TeXbook pg. 356)
    993     const k = makeSpan(
    994         ["k"], [buildCommon.mathsym("K", group.mode)], options);
    995     const a = makeSpan(
    996         ["a"], [buildCommon.mathsym("A", group.mode)], options);
    997 
    998     a.height = (a.height + 0.2) * 0.75;
    999     a.depth = (a.height - 0.2) * 0.75;
   1000 
   1001     const t = makeSpan(
   1002         ["t"], [buildCommon.mathsym("T", group.mode)], options);
   1003     const e = makeSpan(
   1004         ["e"], [buildCommon.mathsym("E", group.mode)], options);
   1005 
   1006     e.height = (e.height - 0.2155);
   1007     e.depth = (e.depth + 0.2155);
   1008 
   1009     const x = makeSpan(
   1010         ["x"], [buildCommon.mathsym("X", group.mode)], options);
   1011 
   1012     return makeSpan(
   1013         ["mord", "katex-logo"], [k, a, t, e, x], options);
   1014 };
   1015 
   1016 groupTypes.overline = function(group, options) {
   1017     // Overlines are handled in the TeXbook pg 443, Rule 9.
   1018     const style = options.style;
   1019 
   1020     // Build the inner group in the cramped style.
   1021     const innerGroup = buildGroup(group.value.body,
   1022             options.withStyle(style.cramp()));
   1023 
   1024     const ruleWidth = fontMetrics.metrics.defaultRuleThickness /
   1025         style.sizeMultiplier;
   1026 
   1027     // Create the line above the body
   1028     const line = makeSpan(
   1029         [style.reset(), Style.TEXT.cls(), "overline-line"]);
   1030     line.height = ruleWidth;
   1031     line.maxFontSize = 1.0;
   1032 
   1033     // Generate the vlist, with the appropriate kerns
   1034     const vlist = buildCommon.makeVList([
   1035         {type: "elem", elem: innerGroup},
   1036         {type: "kern", size: 3 * ruleWidth},
   1037         {type: "elem", elem: line},
   1038         {type: "kern", size: ruleWidth},
   1039     ], "firstBaseline", null, options);
   1040 
   1041     return makeSpan(["mord", "overline"], [vlist], options);
   1042 };
   1043 
   1044 groupTypes.underline = function(group, options) {
   1045     // Underlines are handled in the TeXbook pg 443, Rule 10.
   1046     const style = options.style;
   1047 
   1048     // Build the inner group.
   1049     const innerGroup = buildGroup(group.value.body, options);
   1050 
   1051     const ruleWidth = fontMetrics.metrics.defaultRuleThickness /
   1052         style.sizeMultiplier;
   1053 
   1054     // Create the line above the body
   1055     const line = makeSpan([style.reset(), Style.TEXT.cls(), "underline-line"]);
   1056     line.height = ruleWidth;
   1057     line.maxFontSize = 1.0;
   1058 
   1059     // Generate the vlist, with the appropriate kerns
   1060     const vlist = buildCommon.makeVList([
   1061         {type: "kern", size: ruleWidth},
   1062         {type: "elem", elem: line},
   1063         {type: "kern", size: 3 * ruleWidth},
   1064         {type: "elem", elem: innerGroup},
   1065     ], "top", innerGroup.height, options);
   1066 
   1067     return makeSpan(["mord", "underline"], [vlist], options);
   1068 };
   1069 
   1070 groupTypes.sqrt = function(group, options) {
   1071     // Square roots are handled in the TeXbook pg. 443, Rule 11.
   1072     const style = options.style;
   1073 
   1074     // First, we do the same steps as in overline to build the inner group
   1075     // and line
   1076     const inner = buildGroup(
   1077         group.value.body, options.withStyle(style.cramp()));
   1078 
   1079     const ruleWidth = fontMetrics.metrics.defaultRuleThickness /
   1080         style.sizeMultiplier;
   1081 
   1082     const line = makeSpan(
   1083         [style.reset(), Style.TEXT.cls(), "sqrt-line"], [],
   1084         options);
   1085     line.height = ruleWidth;
   1086     line.maxFontSize = 1.0;
   1087 
   1088     let phi = ruleWidth;
   1089     if (style.id < Style.TEXT.id) {
   1090         phi = style.metrics.xHeight;
   1091     }
   1092 
   1093     // Calculate the clearance between the body and line
   1094     let lineClearance = ruleWidth + phi / 4;
   1095 
   1096     const innerHeight = (inner.height + inner.depth) * style.sizeMultiplier;
   1097     const minDelimiterHeight = innerHeight + lineClearance + ruleWidth;
   1098 
   1099     // Create a \surd delimiter of the required minimum size
   1100     const delim = makeSpan(["sqrt-sign"], [
   1101         delimiter.customSizedDelim("\\surd", minDelimiterHeight,
   1102                                    false, options, group.mode)],
   1103                          options);
   1104 
   1105     const delimDepth = (delim.height + delim.depth) - ruleWidth;
   1106 
   1107     // Adjust the clearance based on the delimiter size
   1108     if (delimDepth > inner.height + inner.depth + lineClearance) {
   1109         lineClearance =
   1110             (lineClearance + delimDepth - inner.height - inner.depth) / 2;
   1111     }
   1112 
   1113     // Shift the delimiter so that its top lines up with the top of the line
   1114     const delimShift = -(inner.height + lineClearance + ruleWidth) +
   1115           delim.height;
   1116     delim.style.top = delimShift + "em";
   1117     delim.height -= delimShift;
   1118     delim.depth += delimShift;
   1119 
   1120     // We add a special case here, because even when `inner` is empty, we
   1121     // still get a line. So, we use a simple heuristic to decide if we
   1122     // should omit the body entirely. (note this doesn't work for something
   1123     // like `\sqrt{\rlap{x}}`, but if someone is doing that they deserve for
   1124     // it not to work.
   1125     let body;
   1126     if (inner.height === 0 && inner.depth === 0) {
   1127         body = makeSpan();
   1128     } else {
   1129         body = buildCommon.makeVList([
   1130             {type: "elem", elem: inner},
   1131             {type: "kern", size: lineClearance},
   1132             {type: "elem", elem: line},
   1133             {type: "kern", size: ruleWidth},
   1134         ], "firstBaseline", null, options);
   1135     }
   1136 
   1137     if (!group.value.index) {
   1138         return makeSpan(["mord", "sqrt"], [delim, body], options);
   1139     } else {
   1140         // Handle the optional root index
   1141 
   1142         // The index is always in scriptscript style
   1143         const newOptions = options.withStyle(Style.SCRIPTSCRIPT);
   1144         const root = buildGroup(group.value.index, newOptions);
   1145         const rootWrap = makeSpan(
   1146             [style.reset(), Style.SCRIPTSCRIPT.cls()],
   1147             [root],
   1148             newOptions);
   1149 
   1150         // Figure out the height and depth of the inner part
   1151         const innerRootHeight = Math.max(delim.height, body.height);
   1152         const innerRootDepth = Math.max(delim.depth, body.depth);
   1153 
   1154         // The amount the index is shifted by. This is taken from the TeX
   1155         // source, in the definition of `\r@@t`.
   1156         const toShift = 0.6 * (innerRootHeight - innerRootDepth);
   1157 
   1158         // Build a VList with the superscript shifted up correctly
   1159         const rootVList = buildCommon.makeVList(
   1160             [{type: "elem", elem: rootWrap}],
   1161             "shift", -toShift, options);
   1162         // Add a class surrounding it so we can add on the appropriate
   1163         // kerning
   1164         const rootVListWrap = makeSpan(["root"], [rootVList]);
   1165 
   1166         return makeSpan(["mord", "sqrt"],
   1167             [rootVListWrap, delim, body], options);
   1168     }
   1169 };
   1170 
   1171 groupTypes.sizing = function(group, options) {
   1172     // Handle sizing operators like \Huge. Real TeX doesn't actually allow
   1173     // these functions inside of math expressions, so we do some special
   1174     // handling.
   1175     const inner = buildExpression(group.value.value,
   1176             options.withSize(group.value.size), false);
   1177 
   1178     // Compute the correct maxFontSize.
   1179     const style = options.style;
   1180     const fontSize = buildCommon.sizingMultiplier[group.value.size] *
   1181           style.sizeMultiplier;
   1182 
   1183     // Add size-resetting classes to the inner list and set maxFontSize
   1184     // manually. Handle nested size changes.
   1185     for (let i = 0; i < inner.length; i++) {
   1186         const pos = utils.indexOf(inner[i].classes, "sizing");
   1187         if (pos < 0) {
   1188             inner[i].classes.push("sizing", "reset-" + options.size,
   1189                                   group.value.size, style.cls());
   1190             inner[i].maxFontSize = fontSize;
   1191         } else if (inner[i].classes[pos + 1] === "reset-" + group.value.size) {
   1192             // This is a nested size change: e.g., inner[i] is the "b" in
   1193             // `\Huge a \small b`. Override the old size (the `reset-` class)
   1194             // but not the new size.
   1195             inner[i].classes[pos + 1] = "reset-" + options.size;
   1196         }
   1197     }
   1198 
   1199     return buildCommon.makeFragment(inner);
   1200 };
   1201 
   1202 groupTypes.styling = function(group, options) {
   1203     // Style changes are handled in the TeXbook on pg. 442, Rule 3.
   1204 
   1205     // Figure out what style we're changing to.
   1206     const styleMap = {
   1207         "display": Style.DISPLAY,
   1208         "text": Style.TEXT,
   1209         "script": Style.SCRIPT,
   1210         "scriptscript": Style.SCRIPTSCRIPT,
   1211     };
   1212 
   1213     const newStyle = styleMap[group.value.style];
   1214     const newOptions = options.withStyle(newStyle);
   1215 
   1216     // Build the inner expression in the new style.
   1217     const inner = buildExpression(
   1218         group.value.value, newOptions, false);
   1219 
   1220     // Add style-resetting classes to the inner list. Handle nested changes.
   1221     for (let i = 0; i < inner.length; i++) {
   1222         const pos = utils.indexOf(inner[i].classes, newStyle.reset());
   1223         if (pos < 0) {
   1224             inner[i].classes.push(options.style.reset(), newStyle.cls());
   1225         } else {
   1226             // This is a nested style change, as `\textstyle a\scriptstyle b`.
   1227             // Only override the old style (the reset class).
   1228             inner[i].classes[pos] = options.style.reset();
   1229         }
   1230     }
   1231 
   1232     return new buildCommon.makeFragment(inner);
   1233 };
   1234 
   1235 groupTypes.font = function(group, options) {
   1236     const font = group.value.font;
   1237     return buildGroup(group.value.body, options.withFont(font));
   1238 };
   1239 
   1240 groupTypes.delimsizing = function(group, options) {
   1241     const delim = group.value.value;
   1242 
   1243     if (delim === ".") {
   1244         // Empty delimiters still count as elements, even though they don't
   1245         // show anything.
   1246         return makeSpan([group.value.mclass]);
   1247     }
   1248 
   1249     // Use delimiter.sizedDelim to generate the delimiter.
   1250     return delimiter.sizedDelim(
   1251             delim, group.value.size, options, group.mode,
   1252             [group.value.mclass]);
   1253 };
   1254 
   1255 groupTypes.leftright = function(group, options) {
   1256     // Build the inner expression
   1257     const inner = buildExpression(group.value.body, options.reset(), true);
   1258 
   1259     let innerHeight = 0;
   1260     let innerDepth = 0;
   1261     let hadMiddle = false;
   1262 
   1263     // Calculate its height and depth
   1264     for (let i = 0; i < inner.length; i++) {
   1265         if (inner[i].isMiddle) {
   1266             hadMiddle = true;
   1267         } else {
   1268             innerHeight = Math.max(inner[i].height, innerHeight);
   1269             innerDepth = Math.max(inner[i].depth, innerDepth);
   1270         }
   1271     }
   1272 
   1273     const style = options.style;
   1274 
   1275     // The size of delimiters is the same, regardless of what style we are
   1276     // in. Thus, to correctly calculate the size of delimiter we need around
   1277     // a group, we scale down the inner size based on the size.
   1278     innerHeight *= style.sizeMultiplier;
   1279     innerDepth *= style.sizeMultiplier;
   1280 
   1281     let leftDelim;
   1282     if (group.value.left === ".") {
   1283         // Empty delimiters in \left and \right make null delimiter spaces.
   1284         leftDelim = makeNullDelimiter(options, ["mopen"]);
   1285     } else {
   1286         // Otherwise, use leftRightDelim to generate the correct sized
   1287         // delimiter.
   1288         leftDelim = delimiter.leftRightDelim(
   1289             group.value.left, innerHeight, innerDepth, options,
   1290             group.mode, ["mopen"]);
   1291     }
   1292     // Add it to the beginning of the expression
   1293     inner.unshift(leftDelim);
   1294 
   1295     // Handle middle delimiters
   1296     if (hadMiddle) {
   1297         for (let i = 1; i < inner.length; i++) {
   1298             const middleDelim = inner[i];
   1299             if (middleDelim.isMiddle) {
   1300                 // Apply the options that were active when \middle was called
   1301                 inner[i] = delimiter.leftRightDelim(
   1302                     middleDelim.isMiddle.value, innerHeight, innerDepth,
   1303                     middleDelim.isMiddle.options, group.mode, []);
   1304                 // Add back spaces shifted into the delimiter
   1305                 const spaces = spliceSpaces(middleDelim.children, 0);
   1306                 if (spaces) {
   1307                     buildCommon.prependChildren(inner[i], spaces);
   1308                 }
   1309             }
   1310         }
   1311     }
   1312 
   1313     let rightDelim;
   1314     // Same for the right delimiter
   1315     if (group.value.right === ".") {
   1316         rightDelim = makeNullDelimiter(options, ["mclose"]);
   1317     } else {
   1318         rightDelim = delimiter.leftRightDelim(
   1319             group.value.right, innerHeight, innerDepth, options,
   1320             group.mode, ["mclose"]);
   1321     }
   1322     // Add it to the end of the expression.
   1323     inner.push(rightDelim);
   1324 
   1325     return makeSpan(
   1326         ["minner", style.cls()], inner, options);
   1327 };
   1328 
   1329 groupTypes.middle = function(group, options) {
   1330     let middleDelim;
   1331     if (group.value.value === ".") {
   1332         middleDelim = makeNullDelimiter(options, []);
   1333     } else {
   1334         middleDelim = delimiter.sizedDelim(
   1335             group.value.value, 1, options,
   1336             group.mode, []);
   1337         middleDelim.isMiddle = {value: group.value.value, options: options};
   1338     }
   1339     return middleDelim;
   1340 };
   1341 
   1342 groupTypes.rule = function(group, options) {
   1343     // Make an empty span for the rule
   1344     const rule = makeSpan(["mord", "rule"], [], options);
   1345     const style = options.style;
   1346 
   1347     // Calculate the shift, width, and height of the rule, and account for units
   1348     let shift = 0;
   1349     if (group.value.shift) {
   1350         shift = calculateSize(group.value.shift, style);
   1351     }
   1352 
   1353     let width = calculateSize(group.value.width, style);
   1354     let height = calculateSize(group.value.height, style);
   1355 
   1356     // The sizes of rules are absolute, so make it larger if we are in a
   1357     // smaller style.
   1358     shift /= style.sizeMultiplier;
   1359     width /= style.sizeMultiplier;
   1360     height /= style.sizeMultiplier;
   1361 
   1362     // Style the rule to the right size
   1363     rule.style.borderRightWidth = width + "em";
   1364     rule.style.borderTopWidth = height + "em";
   1365     rule.style.bottom = shift + "em";
   1366 
   1367     // Record the height and width
   1368     rule.width = width;
   1369     rule.height = height + shift;
   1370     rule.depth = -shift;
   1371 
   1372     return rule;
   1373 };
   1374 
   1375 groupTypes.kern = function(group, options) {
   1376     // Make an empty span for the rule
   1377     const rule = makeSpan(["mord", "rule"], [], options);
   1378     const style = options.style;
   1379 
   1380     let dimension = 0;
   1381     if (group.value.dimension) {
   1382         dimension = calculateSize(group.value.dimension, style);
   1383     }
   1384 
   1385     dimension /= style.sizeMultiplier;
   1386 
   1387     rule.style.marginLeft = dimension + "em";
   1388 
   1389     return rule;
   1390 };
   1391 
   1392 groupTypes.accent = function(group, options) {
   1393     // Accents are handled in the TeXbook pg. 443, rule 12.
   1394     let base = group.value.base;
   1395     const style = options.style;
   1396 
   1397     let supsubGroup;
   1398     if (group.type === "supsub") {
   1399         // If our base is a character box, and we have superscripts and
   1400         // subscripts, the supsub will defer to us. In particular, we want
   1401         // to attach the superscripts and subscripts to the inner body (so
   1402         // that the position of the superscripts and subscripts won't be
   1403         // affected by the height of the accent). We accomplish this by
   1404         // sticking the base of the accent into the base of the supsub, and
   1405         // rendering that, while keeping track of where the accent is.
   1406 
   1407         // The supsub group is the group that was passed in
   1408         const supsub = group;
   1409         // The real accent group is the base of the supsub group
   1410         group = supsub.value.base;
   1411         // The character box is the base of the accent group
   1412         base = group.value.base;
   1413         // Stick the character box into the base of the supsub group
   1414         supsub.value.base = base;
   1415 
   1416         // Rerender the supsub group with its new base, and store that
   1417         // result.
   1418         supsubGroup = buildGroup(
   1419             supsub, options.reset());
   1420     }
   1421 
   1422     // Build the base group
   1423     const body = buildGroup(
   1424         base, options.withStyle(style.cramp()));
   1425 
   1426     // Calculate the skew of the accent. This is based on the line "If the
   1427     // nucleus is not a single character, let s = 0; otherwise set s to the
   1428     // kern amount for the nucleus followed by the \skewchar of its font."
   1429     // Note that our skew metrics are just the kern between each character
   1430     // and the skewchar.
   1431     let skew = 0;
   1432     if (isCharacterBox(base)) {
   1433         // If the base is a character box, then we want the skew of the
   1434         // innermost character. To do that, we find the innermost character:
   1435         const baseChar = getBaseElem(base);
   1436         // Then, we render its group to get the symbol inside it
   1437         const baseGroup = buildGroup(
   1438             baseChar, options.withStyle(style.cramp()));
   1439         // Finally, we pull the skew off of the symbol.
   1440         skew = baseGroup.skew;
   1441         // Note that we now throw away baseGroup, because the layers we
   1442         // removed with getBaseElem might contain things like \color which
   1443         // we can't get rid of.
   1444         // TODO(emily): Find a better way to get the skew
   1445     }
   1446 
   1447     // calculate the amount of space between the body and the accent
   1448     const clearance = Math.min(
   1449         body.height,
   1450         style.metrics.xHeight);
   1451 
   1452     // Build the accent
   1453     const accent = buildCommon.makeSymbol(
   1454         group.value.accent, "Main-Regular", "math", options);
   1455     // Remove the italic correction of the accent, because it only serves to
   1456     // shift the accent over to a place we don't want.
   1457     accent.italic = 0;
   1458 
   1459     // The \vec character that the fonts use is a combining character, and
   1460     // thus shows up much too far to the left. To account for this, we add a
   1461     // specific class which shifts the accent over to where we want it.
   1462     // TODO(emily): Fix this in a better way, like by changing the font
   1463     const vecClass = group.value.accent === "\\vec" ? "accent-vec" : null;
   1464 
   1465     let accentBody = makeSpan(["accent-body", vecClass], [
   1466         makeSpan([], [accent])]);
   1467 
   1468     accentBody = buildCommon.makeVList([
   1469         {type: "elem", elem: body},
   1470         {type: "kern", size: -clearance},
   1471         {type: "elem", elem: accentBody},
   1472     ], "firstBaseline", null, options);
   1473 
   1474     // Shift the accent over by the skew. Note we shift by twice the skew
   1475     // because we are centering the accent, so by adding 2*skew to the left,
   1476     // we shift it to the right by 1*skew.
   1477     accentBody.children[1].style.marginLeft = 2 * skew + "em";
   1478 
   1479     const accentWrap = makeSpan(["mord", "accent"], [accentBody], options);
   1480 
   1481     if (supsubGroup) {
   1482         // Here, we replace the "base" child of the supsub with our newly
   1483         // generated accent.
   1484         supsubGroup.children[0] = accentWrap;
   1485 
   1486         // Since we don't rerun the height calculation after replacing the
   1487         // accent, we manually recalculate height.
   1488         supsubGroup.height = Math.max(accentWrap.height, supsubGroup.height);
   1489 
   1490         // Accents should always be ords, even when their innards are not.
   1491         supsubGroup.classes[0] = "mord";
   1492 
   1493         return supsubGroup;
   1494     } else {
   1495         return accentWrap;
   1496     }
   1497 };
   1498 
   1499 groupTypes.phantom = function(group, options) {
   1500     const elements = buildExpression(
   1501         group.value.value,
   1502         options.withPhantom(),
   1503         false
   1504     );
   1505 
   1506     // \phantom isn't supposed to affect the elements it contains.
   1507     // See "color" for more details.
   1508     return new buildCommon.makeFragment(elements);
   1509 };
   1510 
   1511 groupTypes.mclass = function(group, options) {
   1512     const elements = buildExpression(group.value.value, options, true);
   1513 
   1514     return makeSpan([group.value.mclass], elements, options);
   1515 };
   1516 
   1517 /**
   1518  * buildGroup is the function that takes a group and calls the correct groupType
   1519  * function for it. It also handles the interaction of size and style changes
   1520  * between parents and children.
   1521  */
   1522 const buildGroup = function(group, options) {
   1523     if (!group) {
   1524         return makeSpan();
   1525     }
   1526 
   1527     if (groupTypes[group.type]) {
   1528         // Call the groupTypes function
   1529         const groupNode = groupTypes[group.type](group, options);
   1530         let multiplier;
   1531 
   1532         // If the style changed between the parent and the current group,
   1533         // account for the size difference
   1534         if (options.style !== options.parentStyle) {
   1535             multiplier = options.style.sizeMultiplier /
   1536                     options.parentStyle.sizeMultiplier;
   1537 
   1538             groupNode.height *= multiplier;
   1539             groupNode.depth *= multiplier;
   1540         }
   1541 
   1542         // If the size changed between the parent and the current group, account
   1543         // for that size difference.
   1544         if (options.size !== options.parentSize) {
   1545             multiplier = buildCommon.sizingMultiplier[options.size] /
   1546                     buildCommon.sizingMultiplier[options.parentSize];
   1547 
   1548             groupNode.height *= multiplier;
   1549             groupNode.depth *= multiplier;
   1550         }
   1551 
   1552         return groupNode;
   1553     } else {
   1554         throw new ParseError(
   1555             "Got group of unknown type: '" + group.type + "'");
   1556     }
   1557 };
   1558 
   1559 /**
   1560  * Take an entire parse tree, and build it into an appropriate set of HTML
   1561  * nodes.
   1562  */
   1563 const buildHTML = function(tree, options) {
   1564     // buildExpression is destructive, so we need to make a clone
   1565     // of the incoming tree so that it isn't accidentally changed
   1566     tree = JSON.parse(JSON.stringify(tree));
   1567 
   1568     // Build the expression contained in the tree
   1569     const expression = buildExpression(tree, options, true);
   1570     const body = makeSpan(["base", options.style.cls()], expression, options);
   1571 
   1572     // Add struts, which ensure that the top of the HTML element falls at the
   1573     // height of the expression, and the bottom of the HTML element falls at the
   1574     // depth of the expression.
   1575     const topStrut = makeSpan(["strut"]);
   1576     const bottomStrut = makeSpan(["strut", "bottom"]);
   1577 
   1578     topStrut.style.height = body.height + "em";
   1579     bottomStrut.style.height = (body.height + body.depth) + "em";
   1580     // We'd like to use `vertical-align: top` but in IE 9 this lowers the
   1581     // baseline of the box to the bottom of this strut (instead staying in the
   1582     // normal place) so we use an absolute value for vertical-align instead
   1583     bottomStrut.style.verticalAlign = -body.depth + "em";
   1584 
   1585     // Wrap the struts and body together
   1586     const htmlNode = makeSpan(["katex-html"], [topStrut, bottomStrut, body]);
   1587 
   1588     htmlNode.setAttribute("aria-hidden", "true");
   1589 
   1590     return htmlNode;
   1591 };
   1592 
   1593 module.exports = buildHTML;