Files
KaTeX/src/buildCommon.js
Hossein Saniei a019f36f8a Upgrade the source to use ES6 syntax including classes, import and static properties (#679)
* Add babel transform-class-properties to have static class properties

* Upgrade Lexer and Parser files to use ES6 classes

* Update eslint max line length to 90 character (more indent because of using ES6 classes)

* Upgrade eslint and jasmin to support ES stage-2 features

* Use static properties to place constants near their functions

* Migrate all remaining sources to ES6 syntax

* Increase eslint max line length to 84

* Remove non-babelified endpoint in dev server.js

* Clean up server.js functions after removing browserified

* Make screenshotter not to use babel endpoint as we babelify everything now
2017-07-03 08:09:21 -04:00

477 lines
16 KiB
JavaScript

/* eslint no-console:0 */
/**
* This module contains general functions that can be used for building
* different kinds of domTree nodes in a consistent manner.
*/
import domTree from "./domTree";
import fontMetrics from "./fontMetrics";
import symbols from "./symbols";
import utils from "./utils";
// The following have to be loaded from Main-Italic font, using class mainit
const mainitLetters = [
"\\imath", // dotless i
"\\jmath", // dotless j
"\\pounds", // pounds symbol
];
/**
* Looks up the given symbol in fontMetrics, after applying any symbol
* replacements defined in symbol.js
*/
const lookupSymbol = function(value, fontFamily, mode) {
// Replace the value with its replaced value from symbol.js
if (symbols[mode][value] && symbols[mode][value].replace) {
value = symbols[mode][value].replace;
}
return {
value: value,
metrics: fontMetrics.getCharacterMetrics(value, fontFamily),
};
};
/**
* 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.
*
* TODO: make argument order closer to makeSpan
* TODO: add a separate argument for math class (e.g. `mop`, `mbin`), which
* should if present come first in `classes`.
*/
const makeSymbol = function(value, fontFamily, mode, options, classes) {
const lookup = lookupSymbol(value, fontFamily, mode);
const metrics = lookup.metrics;
value = lookup.value;
let symbolNode;
if (metrics) {
let italic = metrics.italic;
if (mode === "text") {
italic = 0;
}
symbolNode = new domTree.symbolNode(
value, metrics.height, metrics.depth, 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 '" +
fontFamily + "'");
symbolNode = new domTree.symbolNode(value, 0, 0, 0, 0, classes);
}
if (options) {
symbolNode.maxFontSize = options.sizeMultiplier;
if (options.style.isTight()) {
symbolNode.classes.push("mtight");
}
if (options.getColor()) {
symbolNode.style.color = options.getColor();
}
}
return symbolNode;
};
/**
* Makes a symbol in Main-Regular or AMS-Regular.
* Used for rel, bin, open, close, inner, and punct.
*/
const mathsym = function(value, mode, options, 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, options, classes);
} else {
return makeSymbol(
value, "AMS-Regular", mode, options, classes.concat(["amsrm"]));
}
};
/**
* Makes a symbol in the default font for mathords and textords.
*/
const mathDefault = function(value, mode, options, classes, type) {
if (type === "mathord") {
const fontLookup = mathit(value, mode, options, classes);
return makeSymbol(value, fontLookup.fontName, mode, options,
classes.concat([fontLookup.fontClass]));
} else if (type === "textord") {
const font = symbols[mode][value] && symbols[mode][value].font;
if (font === "ams") {
return makeSymbol(
value, "AMS-Regular", mode, options, classes.concat(["amsrm"]));
} else { // if (font === "main") {
return makeSymbol(
value, "Main-Regular", mode, options,
classes.concat(["mathrm"]));
}
} else {
throw new Error("unexpected type: " + type + " in mathDefault");
}
};
/**
* Determines which of the two font names (Main-Italic and Math-Italic) and
* corresponding style tags (mainit or mathit) to use for font "mathit",
* depending on the symbol. Use this function instead of fontMap for font
* "mathit".
*/
const mathit = function(value, mode, options, 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(mainitLetters, value)) {
return {
fontName: "Main-Italic",
fontClass: "mainit",
};
} else {
return {
fontName: "Math-Italic",
fontClass: "mathit",
};
}
};
/**
* Makes either a mathord or textord in the correct font and color.
*/
const makeOrd = function(group, options, type) {
const mode = group.mode;
const value = group.value;
const classes = ["mord"];
const font = options.font;
if (font) {
let fontLookup;
if (font === "mathit" || utils.contains(mainitLetters, value)) {
fontLookup = mathit(value, mode, options, classes);
} else {
fontLookup = fontMap[font];
}
if (lookupSymbol(value, fontLookup.fontName, mode).metrics) {
return makeSymbol(value, fontLookup.fontName, mode, options,
classes.concat([fontLookup.fontClass || font]));
} else {
return mathDefault(value, mode, options, classes, type);
}
} else {
return mathDefault(value, mode, options, classes, type);
}
};
/**
* Calculate the height, depth, and maxFontSize of an element based on its
* children.
*/
const sizeElementFromChildren = function(elem) {
let height = 0;
let depth = 0;
let maxFontSize = 0;
if (elem.children) {
for (let 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 options.
*
* TODO: Ensure that `options` is always provided (currently some call sites
* don't pass it).
* TODO: add a separate argument for math class (e.g. `mop`, `mbin`), which
* should if present come first in `classes`.
*/
const makeSpan = function(classes, children, options) {
const span = new domTree.span(classes, children, options);
sizeElementFromChildren(span);
return span;
};
/**
* Prepends the given children to the given span, updating height, depth, and
* maxFontSize.
*/
const prependChildren = function(span, children) {
span.children = children.concat(span.children);
sizeElementFromChildren(span);
};
/**
* Makes a document fragment with the given list of children.
*/
const makeFragment = function(children) {
const 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.
*/
const makeFontSizer = function(options, fontSize) {
const fontSizeInner = makeSpan([], [new domTree.symbolNode("\u200b")]);
fontSizeInner.style.fontSize = fontSize + "em";
const fontSizer = makeSpan(
["fontsize-ensurer"].concat(options.baseSizingClasses()),
[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
*
*/
const makeVList = function(children, positionType, positionData, options) {
let depth;
let currPos;
let i;
if (positionType === "individualShift") {
const 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++) {
const diff = -oldChildren[i].shift - currPos -
oldChildren[i].elem.depth;
const 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
let 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
let maxFontSize = 0;
for (i = 0; i < children.length; i++) {
if (children[i].type === "elem") {
maxFontSize = Math.max(maxFontSize, children[i].elem.maxFontSize);
}
}
const fontSizer = makeFontSizer(options, maxFontSize);
// Create a new list of actual children at the correct offsets
const realChildren = [];
currPos = depth;
for (i = 0; i < children.length; i++) {
if (children[i].type === "kern") {
currPos += children[i].size;
} else {
const child = children[i].elem;
const shift = -child.depth - currPos;
currPos += child.height + child.depth;
const 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)
const baselineFix = makeSpan(
["baseline-fix"], [fontSizer, new domTree.symbolNode("\u200b")]);
realChildren.push(baselineFix);
const 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 map of spacing functions to their attributes, like size and corresponding
// CSS class
const 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
const fontMap = {
// styles
"mathbf": {
variant: "bold",
fontName: "Main-Bold",
},
"mathrm": {
variant: "normal",
fontName: "Main-Regular",
},
"textit": {
variant: "italic",
fontName: "Main-Italic",
},
// "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,
prependChildren: prependChildren,
spacingFunctions: spacingFunctions,
};