www

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

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 };