Spaces:
Running
on
T4
Running
on
T4
/* eslint no-console:0 */ | |
/** | |
* This module contains general functions that can be used for building | |
* different kinds of domTree nodes in a consistent manner. | |
*/ | |
var domTree = require("./domTree"); | |
var fontMetrics = require("./fontMetrics"); | |
var symbols = require("./symbols"); | |
var utils = require("./utils"); | |
var greekCapitals = [ | |
"\\Gamma", | |
"\\Delta", | |
"\\Theta", | |
"\\Lambda", | |
"\\Xi", | |
"\\Pi", | |
"\\Sigma", | |
"\\Upsilon", | |
"\\Phi", | |
"\\Psi", | |
"\\Omega", | |
]; | |
var dotlessLetters = [ | |
"\u0131", // dotless i, \imath | |
"\u0237", // dotless j, \jmath | |
]; | |
/** | |
* Makes a symbolNode after translation via the list of symbols in symbols.js. | |
* Correctly pulls out metrics for the character, and optionally takes a list of | |
* classes to be attached to the node. | |
*/ | |
var makeSymbol = function(value, style, mode, color, classes) { | |
// Replace the value with its replaced value from symbol.js | |
if (symbols[mode][value] && symbols[mode][value].replace) { | |
value = symbols[mode][value].replace; | |
} | |
var metrics = fontMetrics.getCharacterMetrics(value, style); | |
var symbolNode; | |
if (metrics) { | |
symbolNode = new domTree.symbolNode( | |
value, metrics.height, metrics.depth, metrics.italic, metrics.skew, | |
classes); | |
} else { | |
// TODO(emily): Figure out a good way to only print this in development | |
typeof console !== "undefined" && console.warn( | |
"No character metrics for '" + value + "' in style '" + | |
style + "'"); | |
symbolNode = new domTree.symbolNode(value, 0, 0, 0, 0, classes); | |
} | |
if (color) { | |
symbolNode.style.color = color; | |
} | |
return symbolNode; | |
}; | |
/** | |
* Makes a symbol in Main-Regular or AMS-Regular. | |
* Used for rel, bin, open, close, inner, and punct. | |
*/ | |
var mathsym = function(value, mode, color, classes) { | |
// Decide what font to render the symbol in by its entry in the symbols | |
// table. | |
// Have a special case for when the value = \ because the \ is used as a | |
// textord in unsupported command errors but cannot be parsed as a regular | |
// text ordinal and is therefore not present as a symbol in the symbols | |
// table for text | |
if (value === "\\" || symbols[mode][value].font === "main") { | |
return makeSymbol(value, "Main-Regular", mode, color, classes); | |
} else { | |
return makeSymbol( | |
value, "AMS-Regular", mode, color, classes.concat(["amsrm"])); | |
} | |
}; | |
/** | |
* Makes a symbol in the default font for mathords and textords. | |
*/ | |
var mathDefault = function(value, mode, color, classes, type) { | |
if (type === "mathord") { | |
return mathit(value, mode, color, classes); | |
} else if (type === "textord") { | |
return makeSymbol( | |
value, "Main-Regular", mode, color, classes.concat(["mathrm"])); | |
} else { | |
throw new Error("unexpected type: " + type + " in mathDefault"); | |
} | |
}; | |
/** | |
* Makes a symbol in the italic math font. | |
*/ | |
var mathit = function(value, mode, color, classes) { | |
if (/[0-9]/.test(value.charAt(0)) || | |
// glyphs for \imath and \jmath do not exist in Math-Italic so we | |
// need to use Main-Italic instead | |
utils.contains(dotlessLetters, value) || | |
utils.contains(greekCapitals, value)) { | |
return makeSymbol( | |
value, "Main-Italic", mode, color, classes.concat(["mainit"])); | |
} else { | |
return makeSymbol( | |
value, "Math-Italic", mode, color, classes.concat(["mathit"])); | |
} | |
}; | |
/** | |
* Makes either a mathord or textord in the correct font and color. | |
*/ | |
var makeOrd = function(group, options, type) { | |
var mode = group.mode; | |
var value = group.value; | |
if (symbols[mode][value] && symbols[mode][value].replace) { | |
value = symbols[mode][value].replace; | |
} | |
var classes = ["mord"]; | |
var color = options.getColor(); | |
var font = options.font; | |
if (font) { | |
if (font === "mathit" || utils.contains(dotlessLetters, value)) { | |
return mathit(value, mode, color, classes); | |
} else { | |
var fontName = fontMap[font].fontName; | |
if (fontMetrics.getCharacterMetrics(value, fontName)) { | |
return makeSymbol( | |
value, fontName, mode, color, classes.concat([font])); | |
} else { | |
return mathDefault(value, mode, color, classes, type); | |
} | |
} | |
} else { | |
return mathDefault(value, mode, color, classes, type); | |
} | |
}; | |
/** | |
* Calculate the height, depth, and maxFontSize of an element based on its | |
* children. | |
*/ | |
var sizeElementFromChildren = function(elem) { | |
var height = 0; | |
var depth = 0; | |
var maxFontSize = 0; | |
if (elem.children) { | |
for (var i = 0; i < elem.children.length; i++) { | |
if (elem.children[i].height > height) { | |
height = elem.children[i].height; | |
} | |
if (elem.children[i].depth > depth) { | |
depth = elem.children[i].depth; | |
} | |
if (elem.children[i].maxFontSize > maxFontSize) { | |
maxFontSize = elem.children[i].maxFontSize; | |
} | |
} | |
} | |
elem.height = height; | |
elem.depth = depth; | |
elem.maxFontSize = maxFontSize; | |
}; | |
/** | |
* Makes a span with the given list of classes, list of children, and color. | |
*/ | |
var makeSpan = function(classes, children, color) { | |
var span = new domTree.span(classes, children); | |
sizeElementFromChildren(span); | |
if (color) { | |
span.style.color = color; | |
} | |
return span; | |
}; | |
/** | |
* Makes a document fragment with the given list of children. | |
*/ | |
var makeFragment = function(children) { | |
var fragment = new domTree.documentFragment(children); | |
sizeElementFromChildren(fragment); | |
return fragment; | |
}; | |
/** | |
* Makes an element placed in each of the vlist elements to ensure that each | |
* element has the same max font size. To do this, we create a zero-width space | |
* with the correct font size. | |
*/ | |
var makeFontSizer = function(options, fontSize) { | |
var fontSizeInner = makeSpan([], [new domTree.symbolNode("\u200b")]); | |
fontSizeInner.style.fontSize = | |
(fontSize / options.style.sizeMultiplier) + "em"; | |
var fontSizer = makeSpan( | |
["fontsize-ensurer", "reset-" + options.size, "size5"], | |
[fontSizeInner]); | |
return fontSizer; | |
}; | |
/** | |
* Makes a vertical list by stacking elements and kerns on top of each other. | |
* Allows for many different ways of specifying the positioning method. | |
* | |
* Arguments: | |
* - children: A list of child or kern nodes to be stacked on top of each other | |
* (i.e. the first element will be at the bottom, and the last at | |
* the top). Element nodes are specified as | |
* {type: "elem", elem: node} | |
* while kern nodes are specified as | |
* {type: "kern", size: size} | |
* - positionType: The method by which the vlist should be positioned. Valid | |
* values are: | |
* - "individualShift": The children list only contains elem | |
* nodes, and each node contains an extra | |
* "shift" value of how much it should be | |
* shifted (note that shifting is always | |
* moving downwards). positionData is | |
* ignored. | |
* - "top": The positionData specifies the topmost point of | |
* the vlist (note this is expected to be a height, | |
* so positive values move up) | |
* - "bottom": The positionData specifies the bottommost point | |
* of the vlist (note this is expected to be a | |
* depth, so positive values move down | |
* - "shift": The vlist will be positioned such that its | |
* baseline is positionData away from the baseline | |
* of the first child. Positive values move | |
* downwards. | |
* - "firstBaseline": The vlist will be positioned such that | |
* its baseline is aligned with the | |
* baseline of the first child. | |
* positionData is ignored. (this is | |
* equivalent to "shift" with | |
* positionData=0) | |
* - positionData: Data used in different ways depending on positionType | |
* - options: An Options object | |
* | |
*/ | |
var makeVList = function(children, positionType, positionData, options) { | |
var depth; | |
var currPos; | |
var i; | |
if (positionType === "individualShift") { | |
var oldChildren = children; | |
children = [oldChildren[0]]; | |
// Add in kerns to the list of children to get each element to be | |
// shifted to the correct specified shift | |
depth = -oldChildren[0].shift - oldChildren[0].elem.depth; | |
currPos = depth; | |
for (i = 1; i < oldChildren.length; i++) { | |
var diff = -oldChildren[i].shift - currPos - | |
oldChildren[i].elem.depth; | |
var size = diff - | |
(oldChildren[i - 1].elem.height + | |
oldChildren[i - 1].elem.depth); | |
currPos = currPos + diff; | |
children.push({type: "kern", size: size}); | |
children.push(oldChildren[i]); | |
} | |
} else if (positionType === "top") { | |
// We always start at the bottom, so calculate the bottom by adding up | |
// all the sizes | |
var bottom = positionData; | |
for (i = 0; i < children.length; i++) { | |
if (children[i].type === "kern") { | |
bottom -= children[i].size; | |
} else { | |
bottom -= children[i].elem.height + children[i].elem.depth; | |
} | |
} | |
depth = bottom; | |
} else if (positionType === "bottom") { | |
depth = -positionData; | |
} else if (positionType === "shift") { | |
depth = -children[0].elem.depth - positionData; | |
} else if (positionType === "firstBaseline") { | |
depth = -children[0].elem.depth; | |
} else { | |
depth = 0; | |
} | |
// Make the fontSizer | |
var maxFontSize = 0; | |
for (i = 0; i < children.length; i++) { | |
if (children[i].type === "elem") { | |
maxFontSize = Math.max(maxFontSize, children[i].elem.maxFontSize); | |
} | |
} | |
var fontSizer = makeFontSizer(options, maxFontSize); | |
// Create a new list of actual children at the correct offsets | |
var realChildren = []; | |
currPos = depth; | |
for (i = 0; i < children.length; i++) { | |
if (children[i].type === "kern") { | |
currPos += children[i].size; | |
} else { | |
var child = children[i].elem; | |
var shift = -child.depth - currPos; | |
currPos += child.height + child.depth; | |
var childWrap = makeSpan([], [fontSizer, child]); | |
childWrap.height -= shift; | |
childWrap.depth += shift; | |
childWrap.style.top = shift + "em"; | |
realChildren.push(childWrap); | |
} | |
} | |
// Add in an element at the end with no offset to fix the calculation of | |
// baselines in some browsers (namely IE, sometimes safari) | |
var baselineFix = makeSpan( | |
["baseline-fix"], [fontSizer, new domTree.symbolNode("\u200b")]); | |
realChildren.push(baselineFix); | |
var vlist = makeSpan(["vlist"], realChildren); | |
// Fix the final height and depth, in case there were kerns at the ends | |
// since the makeSpan calculation won't take that in to account. | |
vlist.height = Math.max(currPos, vlist.height); | |
vlist.depth = Math.max(-depth, vlist.depth); | |
return vlist; | |
}; | |
// A table of size -> font size for the different sizing functions | |
var sizingMultiplier = { | |
size1: 0.5, | |
size2: 0.7, | |
size3: 0.8, | |
size4: 0.9, | |
size5: 1.0, | |
size6: 1.2, | |
size7: 1.44, | |
size8: 1.73, | |
size9: 2.07, | |
size10: 2.49, | |
}; | |
// A map of spacing functions to their attributes, like size and corresponding | |
// CSS class | |
var spacingFunctions = { | |
"\\qquad": { | |
size: "2em", | |
className: "qquad", | |
}, | |
"\\quad": { | |
size: "1em", | |
className: "quad", | |
}, | |
"\\enspace": { | |
size: "0.5em", | |
className: "enspace", | |
}, | |
"\\;": { | |
size: "0.277778em", | |
className: "thickspace", | |
}, | |
"\\:": { | |
size: "0.22222em", | |
className: "mediumspace", | |
}, | |
"\\,": { | |
size: "0.16667em", | |
className: "thinspace", | |
}, | |
"\\!": { | |
size: "-0.16667em", | |
className: "negativethinspace", | |
}, | |
}; | |
/** | |
* Maps TeX font commands to objects containing: | |
* - variant: string used for "mathvariant" attribute in buildMathML.js | |
* - fontName: the "style" parameter to fontMetrics.getCharacterMetrics | |
*/ | |
// A map between tex font commands an MathML mathvariant attribute values | |
var fontMap = { | |
// styles | |
"mathbf": { | |
variant: "bold", | |
fontName: "Main-Bold", | |
}, | |
"mathrm": { | |
variant: "normal", | |
fontName: "Main-Regular", | |
}, | |
// "mathit" is missing because it requires the use of two fonts: Main-Italic | |
// and Math-Italic. This is handled by a special case in makeOrd which ends | |
// up calling mathit. | |
// families | |
"mathbb": { | |
variant: "double-struck", | |
fontName: "AMS-Regular", | |
}, | |
"mathcal": { | |
variant: "script", | |
fontName: "Caligraphic-Regular", | |
}, | |
"mathfrak": { | |
variant: "fraktur", | |
fontName: "Fraktur-Regular", | |
}, | |
"mathscr": { | |
variant: "script", | |
fontName: "Script-Regular", | |
}, | |
"mathsf": { | |
variant: "sans-serif", | |
fontName: "SansSerif-Regular", | |
}, | |
"mathtt": { | |
variant: "monospace", | |
fontName: "Typewriter-Regular", | |
}, | |
}; | |
module.exports = { | |
fontMap: fontMap, | |
makeSymbol: makeSymbol, | |
mathsym: mathsym, | |
makeSpan: makeSpan, | |
makeFragment: makeFragment, | |
makeVList: makeVList, | |
makeOrd: makeOrd, | |
sizingMultiplier: sizingMultiplier, | |
spacingFunctions: spacingFunctions, | |
}; | |