mirror of
https://github.com/Smaug123/KaTeX
synced 2025-10-07 04:08:43 +00:00
* 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
477 lines
16 KiB
JavaScript
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,
|
|
};
|