delimiter.js (19514B)
1 /** 2 * This file deals with creating delimiters of various sizes. The TeXbook 3 * discusses these routines on page 441-442, in the "Another subroutine sets box 4 * x to a specified variable delimiter" paragraph. 5 * 6 * There are three main routines here. `makeSmallDelim` makes a delimiter in the 7 * normal font, but in either text, script, or scriptscript style. 8 * `makeLargeDelim` makes a delimiter in textstyle, but in one of the Size1, 9 * Size2, Size3, or Size4 fonts. `makeStackedDelim` makes a delimiter out of 10 * smaller pieces that are stacked on top of one another. 11 * 12 * The functions take a parameter `center`, which determines if the delimiter 13 * should be centered around the axis. 14 * 15 * Then, there are three exposed functions. `sizedDelim` makes a delimiter in 16 * one of the given sizes. This is used for things like `\bigl`. 17 * `customSizedDelim` makes a delimiter with a given total height+depth. It is 18 * called in places like `\sqrt`. `leftRightDelim` makes an appropriate 19 * delimiter which surrounds an expression of a given height an depth. It is 20 * used in `\left` and `\right`. 21 */ 22 23 const ParseError = require("./ParseError"); 24 const Style = require("./Style"); 25 26 const buildCommon = require("./buildCommon"); 27 const fontMetrics = require("./fontMetrics"); 28 const symbols = require("./symbols"); 29 const utils = require("./utils"); 30 31 const makeSpan = buildCommon.makeSpan; 32 33 /** 34 * Get the metrics for a given symbol and font, after transformation (i.e. 35 * after following replacement from symbols.js) 36 */ 37 const getMetrics = function(symbol, font) { 38 if (symbols.math[symbol] && symbols.math[symbol].replace) { 39 return fontMetrics.getCharacterMetrics( 40 symbols.math[symbol].replace, font); 41 } else { 42 return fontMetrics.getCharacterMetrics( 43 symbol, font); 44 } 45 }; 46 47 /** 48 * Builds a symbol in the given font size (note size is an integer) 49 */ 50 const mathrmSize = function(value, size, mode, options) { 51 return buildCommon.makeSymbol(value, "Size" + size + "-Regular", 52 mode, options); 53 }; 54 55 /** 56 * Puts a delimiter span in a given style, and adds appropriate height, depth, 57 * and maxFontSizes. 58 */ 59 const styleWrap = function(delim, toStyle, options, classes) { 60 classes = classes || []; 61 const span = makeSpan( 62 classes.concat(["style-wrap", options.style.reset(), toStyle.cls()]), 63 [delim], options); 64 65 const multiplier = toStyle.sizeMultiplier / options.style.sizeMultiplier; 66 67 span.height *= multiplier; 68 span.depth *= multiplier; 69 span.maxFontSize = toStyle.sizeMultiplier; 70 71 return span; 72 }; 73 74 /** 75 * Makes a small delimiter. This is a delimiter that comes in the Main-Regular 76 * font, but is restyled to either be in textstyle, scriptstyle, or 77 * scriptscriptstyle. 78 */ 79 const makeSmallDelim = function(delim, style, center, options, mode, classes) { 80 const text = buildCommon.makeSymbol(delim, "Main-Regular", mode, options); 81 82 const span = styleWrap(text, style, options, classes); 83 84 if (center) { 85 const shift = 86 (1 - options.style.sizeMultiplier / style.sizeMultiplier) * 87 options.style.metrics.axisHeight; 88 89 span.style.top = shift + "em"; 90 span.height -= shift; 91 span.depth += shift; 92 } 93 94 return span; 95 }; 96 97 /** 98 * Makes a large delimiter. This is a delimiter that comes in the Size1, Size2, 99 * Size3, or Size4 fonts. It is always rendered in textstyle. 100 */ 101 const makeLargeDelim = function(delim, size, center, options, mode, classes) { 102 const inner = mathrmSize(delim, size, mode, options); 103 104 const span = styleWrap( 105 makeSpan(["delimsizing", "size" + size], [inner], options), 106 Style.TEXT, options, classes); 107 108 if (center) { 109 const shift = (1 - options.style.sizeMultiplier) * 110 options.style.metrics.axisHeight; 111 112 span.style.top = shift + "em"; 113 span.height -= shift; 114 span.depth += shift; 115 } 116 117 return span; 118 }; 119 120 /** 121 * Make an inner span with the given offset and in the given font. This is used 122 * in `makeStackedDelim` to make the stacking pieces for the delimiter. 123 */ 124 const makeInner = function(symbol, font, mode) { 125 let sizeClass; 126 // Apply the correct CSS class to choose the right font. 127 if (font === "Size1-Regular") { 128 sizeClass = "delim-size1"; 129 } else if (font === "Size4-Regular") { 130 sizeClass = "delim-size4"; 131 } 132 133 const inner = makeSpan( 134 ["delimsizinginner", sizeClass], 135 [makeSpan([], [buildCommon.makeSymbol(symbol, font, mode)])]); 136 137 // Since this will be passed into `makeVList` in the end, wrap the element 138 // in the appropriate tag that VList uses. 139 return {type: "elem", elem: inner}; 140 }; 141 142 /** 143 * Make a stacked delimiter out of a given delimiter, with the total height at 144 * least `heightTotal`. This routine is mentioned on page 442 of the TeXbook. 145 */ 146 const makeStackedDelim = function(delim, heightTotal, center, options, mode, 147 classes) { 148 // There are four parts, the top, an optional middle, a repeated part, and a 149 // bottom. 150 let top; 151 let middle; 152 let repeat; 153 let bottom; 154 top = repeat = bottom = delim; 155 middle = null; 156 // Also keep track of what font the delimiters are in 157 let font = "Size1-Regular"; 158 159 // We set the parts and font based on the symbol. Note that we use 160 // '\u23d0' instead of '|' and '\u2016' instead of '\\|' for the 161 // repeats of the arrows 162 if (delim === "\\uparrow") { 163 repeat = bottom = "\u23d0"; 164 } else if (delim === "\\Uparrow") { 165 repeat = bottom = "\u2016"; 166 } else if (delim === "\\downarrow") { 167 top = repeat = "\u23d0"; 168 } else if (delim === "\\Downarrow") { 169 top = repeat = "\u2016"; 170 } else if (delim === "\\updownarrow") { 171 top = "\\uparrow"; 172 repeat = "\u23d0"; 173 bottom = "\\downarrow"; 174 } else if (delim === "\\Updownarrow") { 175 top = "\\Uparrow"; 176 repeat = "\u2016"; 177 bottom = "\\Downarrow"; 178 } else if (delim === "[" || delim === "\\lbrack") { 179 top = "\u23a1"; 180 repeat = "\u23a2"; 181 bottom = "\u23a3"; 182 font = "Size4-Regular"; 183 } else if (delim === "]" || delim === "\\rbrack") { 184 top = "\u23a4"; 185 repeat = "\u23a5"; 186 bottom = "\u23a6"; 187 font = "Size4-Regular"; 188 } else if (delim === "\\lfloor") { 189 repeat = top = "\u23a2"; 190 bottom = "\u23a3"; 191 font = "Size4-Regular"; 192 } else if (delim === "\\lceil") { 193 top = "\u23a1"; 194 repeat = bottom = "\u23a2"; 195 font = "Size4-Regular"; 196 } else if (delim === "\\rfloor") { 197 repeat = top = "\u23a5"; 198 bottom = "\u23a6"; 199 font = "Size4-Regular"; 200 } else if (delim === "\\rceil") { 201 top = "\u23a4"; 202 repeat = bottom = "\u23a5"; 203 font = "Size4-Regular"; 204 } else if (delim === "(") { 205 top = "\u239b"; 206 repeat = "\u239c"; 207 bottom = "\u239d"; 208 font = "Size4-Regular"; 209 } else if (delim === ")") { 210 top = "\u239e"; 211 repeat = "\u239f"; 212 bottom = "\u23a0"; 213 font = "Size4-Regular"; 214 } else if (delim === "\\{" || delim === "\\lbrace") { 215 top = "\u23a7"; 216 middle = "\u23a8"; 217 bottom = "\u23a9"; 218 repeat = "\u23aa"; 219 font = "Size4-Regular"; 220 } else if (delim === "\\}" || delim === "\\rbrace") { 221 top = "\u23ab"; 222 middle = "\u23ac"; 223 bottom = "\u23ad"; 224 repeat = "\u23aa"; 225 font = "Size4-Regular"; 226 } else if (delim === "\\lgroup") { 227 top = "\u23a7"; 228 bottom = "\u23a9"; 229 repeat = "\u23aa"; 230 font = "Size4-Regular"; 231 } else if (delim === "\\rgroup") { 232 top = "\u23ab"; 233 bottom = "\u23ad"; 234 repeat = "\u23aa"; 235 font = "Size4-Regular"; 236 } else if (delim === "\\lmoustache") { 237 top = "\u23a7"; 238 bottom = "\u23ad"; 239 repeat = "\u23aa"; 240 font = "Size4-Regular"; 241 } else if (delim === "\\rmoustache") { 242 top = "\u23ab"; 243 bottom = "\u23a9"; 244 repeat = "\u23aa"; 245 font = "Size4-Regular"; 246 } else if (delim === "\\surd") { 247 top = "\ue001"; 248 bottom = "\u23b7"; 249 repeat = "\ue000"; 250 font = "Size4-Regular"; 251 } 252 253 // Get the metrics of the four sections 254 const topMetrics = getMetrics(top, font); 255 const topHeightTotal = topMetrics.height + topMetrics.depth; 256 const repeatMetrics = getMetrics(repeat, font); 257 const repeatHeightTotal = repeatMetrics.height + repeatMetrics.depth; 258 const bottomMetrics = getMetrics(bottom, font); 259 const bottomHeightTotal = bottomMetrics.height + bottomMetrics.depth; 260 let middleHeightTotal = 0; 261 let middleFactor = 1; 262 if (middle !== null) { 263 const middleMetrics = getMetrics(middle, font); 264 middleHeightTotal = middleMetrics.height + middleMetrics.depth; 265 middleFactor = 2; // repeat symmetrically above and below middle 266 } 267 268 // Calcuate the minimal height that the delimiter can have. 269 // It is at least the size of the top, bottom, and optional middle combined. 270 const minHeight = topHeightTotal + bottomHeightTotal + middleHeightTotal; 271 272 // Compute the number of copies of the repeat symbol we will need 273 const repeatCount = Math.ceil( 274 (heightTotal - minHeight) / (middleFactor * repeatHeightTotal)); 275 276 // Compute the total height of the delimiter including all the symbols 277 const realHeightTotal = 278 minHeight + repeatCount * middleFactor * repeatHeightTotal; 279 280 // The center of the delimiter is placed at the center of the axis. Note 281 // that in this context, "center" means that the delimiter should be 282 // centered around the axis in the current style, while normally it is 283 // centered around the axis in textstyle. 284 let axisHeight = options.style.metrics.axisHeight; 285 if (center) { 286 axisHeight *= options.style.sizeMultiplier; 287 } 288 // Calculate the depth 289 const depth = realHeightTotal / 2 - axisHeight; 290 291 // Now, we start building the pieces that will go into the vlist 292 293 // Keep a list of the inner pieces 294 const inners = []; 295 296 // Add the bottom symbol 297 inners.push(makeInner(bottom, font, mode)); 298 299 if (middle === null) { 300 // Add that many symbols 301 for (let i = 0; i < repeatCount; i++) { 302 inners.push(makeInner(repeat, font, mode)); 303 } 304 } else { 305 // When there is a middle bit, we need the middle part and two repeated 306 // sections 307 for (let i = 0; i < repeatCount; i++) { 308 inners.push(makeInner(repeat, font, mode)); 309 } 310 inners.push(makeInner(middle, font, mode)); 311 for (let i = 0; i < repeatCount; i++) { 312 inners.push(makeInner(repeat, font, mode)); 313 } 314 } 315 316 // Add the top symbol 317 inners.push(makeInner(top, font, mode)); 318 319 // Finally, build the vlist 320 const inner = buildCommon.makeVList(inners, "bottom", depth, options); 321 322 return styleWrap( 323 makeSpan(["delimsizing", "mult"], [inner], options), 324 Style.TEXT, options, classes); 325 }; 326 327 // There are three kinds of delimiters, delimiters that stack when they become 328 // too large 329 const stackLargeDelimiters = [ 330 "(", ")", "[", "\\lbrack", "]", "\\rbrack", 331 "\\{", "\\lbrace", "\\}", "\\rbrace", 332 "\\lfloor", "\\rfloor", "\\lceil", "\\rceil", 333 "\\surd", 334 ]; 335 336 // delimiters that always stack 337 const stackAlwaysDelimiters = [ 338 "\\uparrow", "\\downarrow", "\\updownarrow", 339 "\\Uparrow", "\\Downarrow", "\\Updownarrow", 340 "|", "\\|", "\\vert", "\\Vert", 341 "\\lvert", "\\rvert", "\\lVert", "\\rVert", 342 "\\lgroup", "\\rgroup", "\\lmoustache", "\\rmoustache", 343 ]; 344 345 // and delimiters that never stack 346 const stackNeverDelimiters = [ 347 "<", ">", "\\langle", "\\rangle", "/", "\\backslash", "\\lt", "\\gt", 348 ]; 349 350 // Metrics of the different sizes. Found by looking at TeX's output of 351 // $\bigl| // \Bigl| \biggl| \Biggl| \showlists$ 352 // Used to create stacked delimiters of appropriate sizes in makeSizedDelim. 353 const sizeToMaxHeight = [0, 1.2, 1.8, 2.4, 3.0]; 354 355 /** 356 * Used to create a delimiter of a specific size, where `size` is 1, 2, 3, or 4. 357 */ 358 const makeSizedDelim = function(delim, size, options, mode, classes) { 359 // < and > turn into \langle and \rangle in delimiters 360 if (delim === "<" || delim === "\\lt") { 361 delim = "\\langle"; 362 } else if (delim === ">" || delim === "\\gt") { 363 delim = "\\rangle"; 364 } 365 366 // Sized delimiters are never centered. 367 if (utils.contains(stackLargeDelimiters, delim) || 368 utils.contains(stackNeverDelimiters, delim)) { 369 return makeLargeDelim(delim, size, false, options, mode, classes); 370 } else if (utils.contains(stackAlwaysDelimiters, delim)) { 371 return makeStackedDelim( 372 delim, sizeToMaxHeight[size], false, options, mode, classes); 373 } else { 374 throw new ParseError("Illegal delimiter: '" + delim + "'"); 375 } 376 }; 377 378 /** 379 * There are three different sequences of delimiter sizes that the delimiters 380 * follow depending on the kind of delimiter. This is used when creating custom 381 * sized delimiters to decide whether to create a small, large, or stacked 382 * delimiter. 383 * 384 * In real TeX, these sequences aren't explicitly defined, but are instead 385 * defined inside the font metrics. Since there are only three sequences that 386 * are possible for the delimiters that TeX defines, it is easier to just encode 387 * them explicitly here. 388 */ 389 390 // Delimiters that never stack try small delimiters and large delimiters only 391 const stackNeverDelimiterSequence = [ 392 {type: "small", style: Style.SCRIPTSCRIPT}, 393 {type: "small", style: Style.SCRIPT}, 394 {type: "small", style: Style.TEXT}, 395 {type: "large", size: 1}, 396 {type: "large", size: 2}, 397 {type: "large", size: 3}, 398 {type: "large", size: 4}, 399 ]; 400 401 // Delimiters that always stack try the small delimiters first, then stack 402 const stackAlwaysDelimiterSequence = [ 403 {type: "small", style: Style.SCRIPTSCRIPT}, 404 {type: "small", style: Style.SCRIPT}, 405 {type: "small", style: Style.TEXT}, 406 {type: "stack"}, 407 ]; 408 409 // Delimiters that stack when large try the small and then large delimiters, and 410 // stack afterwards 411 const stackLargeDelimiterSequence = [ 412 {type: "small", style: Style.SCRIPTSCRIPT}, 413 {type: "small", style: Style.SCRIPT}, 414 {type: "small", style: Style.TEXT}, 415 {type: "large", size: 1}, 416 {type: "large", size: 2}, 417 {type: "large", size: 3}, 418 {type: "large", size: 4}, 419 {type: "stack"}, 420 ]; 421 422 /** 423 * Get the font used in a delimiter based on what kind of delimiter it is. 424 */ 425 const delimTypeToFont = function(type) { 426 if (type.type === "small") { 427 return "Main-Regular"; 428 } else if (type.type === "large") { 429 return "Size" + type.size + "-Regular"; 430 } else if (type.type === "stack") { 431 return "Size4-Regular"; 432 } 433 }; 434 435 /** 436 * Traverse a sequence of types of delimiters to decide what kind of delimiter 437 * should be used to create a delimiter of the given height+depth. 438 */ 439 const traverseSequence = function(delim, height, sequence, options) { 440 // Here, we choose the index we should start at in the sequences. In smaller 441 // sizes (which correspond to larger numbers in style.size) we start earlier 442 // in the sequence. Thus, scriptscript starts at index 3-3=0, script starts 443 // at index 3-2=1, text starts at 3-1=2, and display starts at min(2,3-0)=2 444 const start = Math.min(2, 3 - options.style.size); 445 for (let i = start; i < sequence.length; i++) { 446 if (sequence[i].type === "stack") { 447 // This is always the last delimiter, so we just break the loop now. 448 break; 449 } 450 451 const metrics = getMetrics(delim, delimTypeToFont(sequence[i])); 452 let heightDepth = metrics.height + metrics.depth; 453 454 // Small delimiters are scaled down versions of the same font, so we 455 // account for the style change size. 456 457 if (sequence[i].type === "small") { 458 heightDepth *= sequence[i].style.sizeMultiplier; 459 } 460 461 // Check if the delimiter at this size works for the given height. 462 if (heightDepth > height) { 463 return sequence[i]; 464 } 465 } 466 467 // If we reached the end of the sequence, return the last sequence element. 468 return sequence[sequence.length - 1]; 469 }; 470 471 /** 472 * Make a delimiter of a given height+depth, with optional centering. Here, we 473 * traverse the sequences, and create a delimiter that the sequence tells us to. 474 */ 475 const makeCustomSizedDelim = function(delim, height, center, options, mode, 476 classes) { 477 if (delim === "<" || delim === "\\lt") { 478 delim = "\\langle"; 479 } else if (delim === ">" || delim === "\\gt") { 480 delim = "\\rangle"; 481 } 482 483 // Decide what sequence to use 484 let sequence; 485 if (utils.contains(stackNeverDelimiters, delim)) { 486 sequence = stackNeverDelimiterSequence; 487 } else if (utils.contains(stackLargeDelimiters, delim)) { 488 sequence = stackLargeDelimiterSequence; 489 } else { 490 sequence = stackAlwaysDelimiterSequence; 491 } 492 493 // Look through the sequence 494 const delimType = traverseSequence(delim, height, sequence, options); 495 496 // Depending on the sequence element we decided on, call the appropriate 497 // function. 498 if (delimType.type === "small") { 499 return makeSmallDelim(delim, delimType.style, center, options, mode, 500 classes); 501 } else if (delimType.type === "large") { 502 return makeLargeDelim(delim, delimType.size, center, options, mode, 503 classes); 504 } else if (delimType.type === "stack") { 505 return makeStackedDelim(delim, height, center, options, mode, classes); 506 } 507 }; 508 509 /** 510 * Make a delimiter for use with `\left` and `\right`, given a height and depth 511 * of an expression that the delimiters surround. 512 */ 513 const makeLeftRightDelim = function(delim, height, depth, options, mode, 514 classes) { 515 // We always center \left/\right delimiters, so the axis is always shifted 516 const axisHeight = 517 options.style.metrics.axisHeight * options.style.sizeMultiplier; 518 519 // Taken from TeX source, tex.web, function make_left_right 520 const delimiterFactor = 901; 521 const delimiterExtend = 5.0 / fontMetrics.metrics.ptPerEm; 522 523 const maxDistFromAxis = Math.max( 524 height - axisHeight, depth + axisHeight); 525 526 const totalHeight = Math.max( 527 // In real TeX, calculations are done using integral values which are 528 // 65536 per pt, or 655360 per em. So, the division here truncates in 529 // TeX but doesn't here, producing different results. If we wanted to 530 // exactly match TeX's calculation, we could do 531 // Math.floor(655360 * maxDistFromAxis / 500) * 532 // delimiterFactor / 655360 533 // (To see the difference, compare 534 // x^{x^{\left(\rule{0.1em}{0.68em}\right)}} 535 // in TeX and KaTeX) 536 maxDistFromAxis / 500 * delimiterFactor, 537 2 * maxDistFromAxis - delimiterExtend); 538 539 // Finally, we defer to `makeCustomSizedDelim` with our calculated total 540 // height 541 return makeCustomSizedDelim(delim, totalHeight, true, options, mode, 542 classes); 543 }; 544 545 module.exports = { 546 sizedDelim: makeSizedDelim, 547 customSizedDelim: makeCustomSizedDelim, 548 leftRightDelim: makeLeftRightDelim, 549 };