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;