www

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

buildCommon.js (16128B)


      1 /* eslint no-console:0 */
      2 /**
      3  * This module contains general functions that can be used for building
      4  * different kinds of domTree nodes in a consistent manner.
      5  */
      6 
      7 const domTree = require("./domTree");
      8 const fontMetrics = require("./fontMetrics");
      9 const symbols = require("./symbols");
     10 const utils = require("./utils");
     11 
     12 // The following have to be loaded from Main-Italic font, using class mainit
     13 const mainitLetters = [
     14     "\\imath",   // dotless i
     15     "\\jmath",   // dotless j
     16     "\\pounds",  // pounds symbol
     17 ];
     18 
     19 /**
     20  * Looks up the given symbol in fontMetrics, after applying any symbol
     21  * replacements defined in symbol.js
     22  */
     23 const lookupSymbol = function(value, fontFamily, mode) {
     24     // Replace the value with its replaced value from symbol.js
     25     if (symbols[mode][value] && symbols[mode][value].replace) {
     26         value = symbols[mode][value].replace;
     27     }
     28     return {
     29         value: value,
     30         metrics: fontMetrics.getCharacterMetrics(value, fontFamily),
     31     };
     32 };
     33 
     34 /**
     35  * Makes a symbolNode after translation via the list of symbols in symbols.js.
     36  * Correctly pulls out metrics for the character, and optionally takes a list of
     37  * classes to be attached to the node.
     38  *
     39  * TODO: make argument order closer to makeSpan
     40  * TODO: add a separate argument for math class (e.g. `mop`, `mbin`), which
     41  * should if present come first in `classes`.
     42  */
     43 const makeSymbol = function(value, fontFamily, mode, options, classes) {
     44     const lookup = lookupSymbol(value, fontFamily, mode);
     45     const metrics = lookup.metrics;
     46     value = lookup.value;
     47 
     48     let symbolNode;
     49     if (metrics) {
     50         let italic = metrics.italic;
     51         if (mode === "text") {
     52             italic = 0;
     53         }
     54         symbolNode = new domTree.symbolNode(
     55             value, metrics.height, metrics.depth, italic, metrics.skew,
     56             classes);
     57     } else {
     58         // TODO(emily): Figure out a good way to only print this in development
     59         typeof console !== "undefined" && console.warn(
     60             "No character metrics for '" + value + "' in style '" +
     61                 fontFamily + "'");
     62         symbolNode = new domTree.symbolNode(value, 0, 0, 0, 0, classes);
     63     }
     64 
     65     if (options) {
     66         if (options.style.isTight()) {
     67             symbolNode.classes.push("mtight");
     68         }
     69         if (options.getColor()) {
     70             symbolNode.style.color = options.getColor();
     71         }
     72     }
     73 
     74     return symbolNode;
     75 };
     76 
     77 /**
     78  * Makes a symbol in Main-Regular or AMS-Regular.
     79  * Used for rel, bin, open, close, inner, and punct.
     80  */
     81 const mathsym = function(value, mode, options, classes) {
     82     // Decide what font to render the symbol in by its entry in the symbols
     83     // table.
     84     // Have a special case for when the value = \ because the \ is used as a
     85     // textord in unsupported command errors but cannot be parsed as a regular
     86     // text ordinal and is therefore not present as a symbol in the symbols
     87     // table for text
     88     if (value === "\\" || symbols[mode][value].font === "main") {
     89         return makeSymbol(value, "Main-Regular", mode, options, classes);
     90     } else {
     91         return makeSymbol(
     92             value, "AMS-Regular", mode, options, classes.concat(["amsrm"]));
     93     }
     94 };
     95 
     96 /**
     97  * Makes a symbol in the default font for mathords and textords.
     98  */
     99 const mathDefault = function(value, mode, options, classes, type) {
    100     if (type === "mathord") {
    101         const fontLookup = mathit(value, mode, options, classes);
    102         return makeSymbol(value, fontLookup.fontName, mode, options,
    103             classes.concat([fontLookup.fontClass]));
    104     } else if (type === "textord") {
    105         const font = symbols[mode][value] && symbols[mode][value].font;
    106         if (font === "ams") {
    107             return makeSymbol(
    108                 value, "AMS-Regular", mode, options, classes.concat(["amsrm"]));
    109         } else { // if (font === "main") {
    110             return makeSymbol(
    111                 value, "Main-Regular", mode, options,
    112                 classes.concat(["mathrm"]));
    113         }
    114     } else {
    115         throw new Error("unexpected type: " + type + " in mathDefault");
    116     }
    117 };
    118 
    119 /**
    120  * Determines which of the two font names (Main-Italic and Math-Italic) and
    121  * corresponding style tags (mainit or mathit) to use for font "mathit",
    122  * depending on the symbol.  Use this function instead of fontMap for font
    123  * "mathit".
    124  */
    125 const mathit = function(value, mode, options, classes) {
    126     if (/[0-9]/.test(value.charAt(0)) ||
    127             // glyphs for \imath and \jmath do not exist in Math-Italic so we
    128             // need to use Main-Italic instead
    129             utils.contains(mainitLetters, value)) {
    130         return {
    131             fontName: "Main-Italic",
    132             fontClass: "mainit",
    133         };
    134     } else {
    135         return {
    136             fontName: "Math-Italic",
    137             fontClass: "mathit",
    138         };
    139     }
    140 };
    141 
    142 /**
    143  * Makes either a mathord or textord in the correct font and color.
    144  */
    145 const makeOrd = function(group, options, type) {
    146     const mode = group.mode;
    147     const value = group.value;
    148 
    149     const classes = ["mord"];
    150 
    151     const font = options.font;
    152     if (font) {
    153         let fontLookup;
    154         if (font === "mathit" || utils.contains(mainitLetters, value)) {
    155             fontLookup = mathit(value, mode, options, classes);
    156         } else {
    157             fontLookup = fontMap[font];
    158         }
    159         if (lookupSymbol(value, fontLookup.fontName, mode).metrics) {
    160             return makeSymbol(value, fontLookup.fontName, mode, options,
    161                 classes.concat([fontLookup.fontClass || font]));
    162         } else {
    163             return mathDefault(value, mode, options, classes, type);
    164         }
    165     } else {
    166         return mathDefault(value, mode, options, classes, type);
    167     }
    168 };
    169 
    170 /**
    171  * Calculate the height, depth, and maxFontSize of an element based on its
    172  * children.
    173  */
    174 const sizeElementFromChildren = function(elem) {
    175     let height = 0;
    176     let depth = 0;
    177     let maxFontSize = 0;
    178 
    179     if (elem.children) {
    180         for (let i = 0; i < elem.children.length; i++) {
    181             if (elem.children[i].height > height) {
    182                 height = elem.children[i].height;
    183             }
    184             if (elem.children[i].depth > depth) {
    185                 depth = elem.children[i].depth;
    186             }
    187             if (elem.children[i].maxFontSize > maxFontSize) {
    188                 maxFontSize = elem.children[i].maxFontSize;
    189             }
    190         }
    191     }
    192 
    193     elem.height = height;
    194     elem.depth = depth;
    195     elem.maxFontSize = maxFontSize;
    196 };
    197 
    198 /**
    199  * Makes a span with the given list of classes, list of children, and options.
    200  *
    201  * TODO: Ensure that `options` is always provided (currently some call sites
    202  * don't pass it).
    203  * TODO: add a separate argument for math class (e.g. `mop`, `mbin`), which
    204  * should if present come first in `classes`.
    205  */
    206 const makeSpan = function(classes, children, options) {
    207     const span = new domTree.span(classes, children, options);
    208 
    209     sizeElementFromChildren(span);
    210 
    211     return span;
    212 };
    213 
    214 /**
    215  * Prepends the given children to the given span, updating height, depth, and
    216  * maxFontSize.
    217  */
    218 const prependChildren = function(span, children) {
    219     span.children = children.concat(span.children);
    220 
    221     sizeElementFromChildren(span);
    222 };
    223 
    224 /**
    225  * Makes a document fragment with the given list of children.
    226  */
    227 const makeFragment = function(children) {
    228     const fragment = new domTree.documentFragment(children);
    229 
    230     sizeElementFromChildren(fragment);
    231 
    232     return fragment;
    233 };
    234 
    235 /**
    236  * Makes an element placed in each of the vlist elements to ensure that each
    237  * element has the same max font size. To do this, we create a zero-width space
    238  * with the correct font size.
    239  */
    240 const makeFontSizer = function(options, fontSize) {
    241     const fontSizeInner = makeSpan([], [new domTree.symbolNode("\u200b")]);
    242     fontSizeInner.style.fontSize =
    243         (fontSize / options.style.sizeMultiplier) + "em";
    244 
    245     const fontSizer = makeSpan(
    246         ["fontsize-ensurer", "reset-" + options.size, "size5"],
    247         [fontSizeInner]);
    248 
    249     return fontSizer;
    250 };
    251 
    252 /**
    253  * Makes a vertical list by stacking elements and kerns on top of each other.
    254  * Allows for many different ways of specifying the positioning method.
    255  *
    256  * Arguments:
    257  *  - children: A list of child or kern nodes to be stacked on top of each other
    258  *              (i.e. the first element will be at the bottom, and the last at
    259  *              the top). Element nodes are specified as
    260  *                {type: "elem", elem: node}
    261  *              while kern nodes are specified as
    262  *                {type: "kern", size: size}
    263  *  - positionType: The method by which the vlist should be positioned. Valid
    264  *                  values are:
    265  *                   - "individualShift": The children list only contains elem
    266  *                                        nodes, and each node contains an extra
    267  *                                        "shift" value of how much it should be
    268  *                                        shifted (note that shifting is always
    269  *                                        moving downwards). positionData is
    270  *                                        ignored.
    271  *                   - "top": The positionData specifies the topmost point of
    272  *                            the vlist (note this is expected to be a height,
    273  *                            so positive values move up)
    274  *                   - "bottom": The positionData specifies the bottommost point
    275  *                               of the vlist (note this is expected to be a
    276  *                               depth, so positive values move down
    277  *                   - "shift": The vlist will be positioned such that its
    278  *                              baseline is positionData away from the baseline
    279  *                              of the first child. Positive values move
    280  *                              downwards.
    281  *                   - "firstBaseline": The vlist will be positioned such that
    282  *                                      its baseline is aligned with the
    283  *                                      baseline of the first child.
    284  *                                      positionData is ignored. (this is
    285  *                                      equivalent to "shift" with
    286  *                                      positionData=0)
    287  *  - positionData: Data used in different ways depending on positionType
    288  *  - options: An Options object
    289  *
    290  */
    291 const makeVList = function(children, positionType, positionData, options) {
    292     let depth;
    293     let currPos;
    294     let i;
    295     if (positionType === "individualShift") {
    296         const oldChildren = children;
    297         children = [oldChildren[0]];
    298 
    299         // Add in kerns to the list of children to get each element to be
    300         // shifted to the correct specified shift
    301         depth = -oldChildren[0].shift - oldChildren[0].elem.depth;
    302         currPos = depth;
    303         for (i = 1; i < oldChildren.length; i++) {
    304             const diff = -oldChildren[i].shift - currPos -
    305                 oldChildren[i].elem.depth;
    306             const size = diff -
    307                 (oldChildren[i - 1].elem.height +
    308                  oldChildren[i - 1].elem.depth);
    309 
    310             currPos = currPos + diff;
    311 
    312             children.push({type: "kern", size: size});
    313             children.push(oldChildren[i]);
    314         }
    315     } else if (positionType === "top") {
    316         // We always start at the bottom, so calculate the bottom by adding up
    317         // all the sizes
    318         let bottom = positionData;
    319         for (i = 0; i < children.length; i++) {
    320             if (children[i].type === "kern") {
    321                 bottom -= children[i].size;
    322             } else {
    323                 bottom -= children[i].elem.height + children[i].elem.depth;
    324             }
    325         }
    326         depth = bottom;
    327     } else if (positionType === "bottom") {
    328         depth = -positionData;
    329     } else if (positionType === "shift") {
    330         depth = -children[0].elem.depth - positionData;
    331     } else if (positionType === "firstBaseline") {
    332         depth = -children[0].elem.depth;
    333     } else {
    334         depth = 0;
    335     }
    336 
    337     // Make the fontSizer
    338     let maxFontSize = 0;
    339     for (i = 0; i < children.length; i++) {
    340         if (children[i].type === "elem") {
    341             maxFontSize = Math.max(maxFontSize, children[i].elem.maxFontSize);
    342         }
    343     }
    344     const fontSizer = makeFontSizer(options, maxFontSize);
    345 
    346     // Create a new list of actual children at the correct offsets
    347     const realChildren = [];
    348     currPos = depth;
    349     for (i = 0; i < children.length; i++) {
    350         if (children[i].type === "kern") {
    351             currPos += children[i].size;
    352         } else {
    353             const child = children[i].elem;
    354 
    355             const shift = -child.depth - currPos;
    356             currPos += child.height + child.depth;
    357 
    358             const childWrap = makeSpan([], [fontSizer, child]);
    359             childWrap.height -= shift;
    360             childWrap.depth += shift;
    361             childWrap.style.top = shift + "em";
    362 
    363             realChildren.push(childWrap);
    364         }
    365     }
    366 
    367     // Add in an element at the end with no offset to fix the calculation of
    368     // baselines in some browsers (namely IE, sometimes safari)
    369     const baselineFix = makeSpan(
    370         ["baseline-fix"], [fontSizer, new domTree.symbolNode("\u200b")]);
    371     realChildren.push(baselineFix);
    372 
    373     const vlist = makeSpan(["vlist"], realChildren);
    374     // Fix the final height and depth, in case there were kerns at the ends
    375     // since the makeSpan calculation won't take that in to account.
    376     vlist.height = Math.max(currPos, vlist.height);
    377     vlist.depth = Math.max(-depth, vlist.depth);
    378     return vlist;
    379 };
    380 
    381 // A table of size -> font size for the different sizing functions
    382 const sizingMultiplier = {
    383     size1: 0.5,
    384     size2: 0.7,
    385     size3: 0.8,
    386     size4: 0.9,
    387     size5: 1.0,
    388     size6: 1.2,
    389     size7: 1.44,
    390     size8: 1.73,
    391     size9: 2.07,
    392     size10: 2.49,
    393 };
    394 
    395 // A map of spacing functions to their attributes, like size and corresponding
    396 // CSS class
    397 const spacingFunctions = {
    398     "\\qquad": {
    399         size: "2em",
    400         className: "qquad",
    401     },
    402     "\\quad": {
    403         size: "1em",
    404         className: "quad",
    405     },
    406     "\\enspace": {
    407         size: "0.5em",
    408         className: "enspace",
    409     },
    410     "\\;": {
    411         size: "0.277778em",
    412         className: "thickspace",
    413     },
    414     "\\:": {
    415         size: "0.22222em",
    416         className: "mediumspace",
    417     },
    418     "\\,": {
    419         size: "0.16667em",
    420         className: "thinspace",
    421     },
    422     "\\!": {
    423         size: "-0.16667em",
    424         className: "negativethinspace",
    425     },
    426 };
    427 
    428 /**
    429  * Maps TeX font commands to objects containing:
    430  * - variant: string used for "mathvariant" attribute in buildMathML.js
    431  * - fontName: the "style" parameter to fontMetrics.getCharacterMetrics
    432  */
    433 // A map between tex font commands an MathML mathvariant attribute values
    434 const fontMap = {
    435     // styles
    436     "mathbf": {
    437         variant: "bold",
    438         fontName: "Main-Bold",
    439     },
    440     "mathrm": {
    441         variant: "normal",
    442         fontName: "Main-Regular",
    443     },
    444     "textit": {
    445         variant: "italic",
    446         fontName: "Main-Italic",
    447     },
    448 
    449     // "mathit" is missing because it requires the use of two fonts: Main-Italic
    450     // and Math-Italic.  This is handled by a special case in makeOrd which ends
    451     // up calling mathit.
    452 
    453     // families
    454     "mathbb": {
    455         variant: "double-struck",
    456         fontName: "AMS-Regular",
    457     },
    458     "mathcal": {
    459         variant: "script",
    460         fontName: "Caligraphic-Regular",
    461     },
    462     "mathfrak": {
    463         variant: "fraktur",
    464         fontName: "Fraktur-Regular",
    465     },
    466     "mathscr": {
    467         variant: "script",
    468         fontName: "Script-Regular",
    469     },
    470     "mathsf": {
    471         variant: "sans-serif",
    472         fontName: "SansSerif-Regular",
    473     },
    474     "mathtt": {
    475         variant: "monospace",
    476         fontName: "Typewriter-Regular",
    477     },
    478 };
    479 
    480 module.exports = {
    481     fontMap: fontMap,
    482     makeSymbol: makeSymbol,
    483     mathsym: mathsym,
    484     makeSpan: makeSpan,
    485     makeFragment: makeFragment,
    486     makeVList: makeVList,
    487     makeOrd: makeOrd,
    488     prependChildren: prependChildren,
    489     sizingMultiplier: sizingMultiplier,
    490     spacingFunctions: spacingFunctions,
    491 };