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