You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
782 lines
26 KiB
JavaScript
782 lines
26 KiB
JavaScript
// @flow
|
|
/* 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 {SymbolNode, Anchor, Span, PathNode, SvgNode, createClass} from "./domTree";
|
|
import {getCharacterMetrics} from "./fontMetrics";
|
|
import symbols, {ligatures} from "./symbols";
|
|
import {wideCharacterFont} from "./wide-character";
|
|
import {calculateSize, makeEm} from "./units";
|
|
import {DocumentFragment} from "./tree";
|
|
|
|
import type Options from "./Options";
|
|
import type {ParseNode} from "./parseNode";
|
|
import type {CharacterMetrics} from "./fontMetrics";
|
|
import type {FontVariant, Mode} from "./types";
|
|
import type {documentFragment as HtmlDocumentFragment} from "./domTree";
|
|
import type {HtmlDomNode, DomSpan, SvgSpan, CssStyle} from "./domTree";
|
|
import type {Measurement} from "./units";
|
|
|
|
/**
|
|
* Looks up the given symbol in fontMetrics, after applying any symbol
|
|
* replacements defined in symbol.js
|
|
*/
|
|
const lookupSymbol = function(
|
|
value: string,
|
|
// TODO(#963): Use a union type for this.
|
|
fontName: string,
|
|
mode: Mode,
|
|
): {value: string, metrics: ?CharacterMetrics} {
|
|
// 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: getCharacterMetrics(value, fontName, mode),
|
|
};
|
|
};
|
|
|
|
/**
|
|
* 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`.
|
|
* TODO(#953): Make `options` mandatory and always pass it in.
|
|
*/
|
|
const makeSymbol = function(
|
|
value: string,
|
|
fontName: string,
|
|
mode: Mode,
|
|
options?: Options,
|
|
classes?: string[],
|
|
): SymbolNode {
|
|
const lookup = lookupSymbol(value, fontName, mode);
|
|
const metrics = lookup.metrics;
|
|
value = lookup.value;
|
|
|
|
let symbolNode;
|
|
if (metrics) {
|
|
let italic = metrics.italic;
|
|
if (mode === "text" || (options && options.font === "mathit")) {
|
|
italic = 0;
|
|
}
|
|
symbolNode = new SymbolNode(
|
|
value, metrics.height, metrics.depth, italic, metrics.skew,
|
|
metrics.width, 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 '${fontName}' and mode '${mode}'`);
|
|
symbolNode = new SymbolNode(value, 0, 0, 0, 0, 0, classes);
|
|
}
|
|
|
|
if (options) {
|
|
symbolNode.maxFontSize = options.sizeMultiplier;
|
|
if (options.style.isTight()) {
|
|
symbolNode.classes.push("mtight");
|
|
}
|
|
const color = options.getColor();
|
|
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.
|
|
*/
|
|
const mathsym = function(
|
|
value: string,
|
|
mode: Mode,
|
|
options: Options,
|
|
classes?: string[] = [],
|
|
): SymbolNode {
|
|
// 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, as well as a special case for boldsymbol because it
|
|
// can be used for bold + and -
|
|
if (options.font === "boldsymbol" &&
|
|
lookupSymbol(value, "Main-Bold", mode).metrics) {
|
|
return makeSymbol(value, "Main-Bold", mode, options,
|
|
classes.concat(["mathbf"]));
|
|
} else 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"]));
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Determines which of the two font names (Main-Bold and Math-BoldItalic) and
|
|
* corresponding style tags (mathbf or boldsymbol) to use for font "boldsymbol",
|
|
* depending on the symbol. Use this function instead of fontMap for font
|
|
* "boldsymbol".
|
|
*/
|
|
const boldsymbol = function(
|
|
value: string,
|
|
mode: Mode,
|
|
options: Options,
|
|
classes: string[],
|
|
type: "mathord" | "textord",
|
|
): {| fontName: string, fontClass: string |} {
|
|
if (type !== "textord" &&
|
|
lookupSymbol(value, "Math-BoldItalic", mode).metrics) {
|
|
return {
|
|
fontName: "Math-BoldItalic",
|
|
fontClass: "boldsymbol",
|
|
};
|
|
} else {
|
|
// Some glyphs do not exist in Math-BoldItalic so we need to use
|
|
// Main-Bold instead.
|
|
return {
|
|
fontName: "Main-Bold",
|
|
fontClass: "mathbf",
|
|
};
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Makes either a mathord or textord in the correct font and color.
|
|
*/
|
|
const makeOrd = function<NODETYPE: "spacing" | "mathord" | "textord">(
|
|
group: ParseNode<NODETYPE>,
|
|
options: Options,
|
|
type: "mathord" | "textord",
|
|
): HtmlDocumentFragment | SymbolNode {
|
|
const mode = group.mode;
|
|
const text = group.text;
|
|
|
|
const classes = ["mord"];
|
|
|
|
// Math mode or Old font (i.e. \rm)
|
|
const isFont = mode === "math" || (mode === "text" && options.font);
|
|
const fontOrFamily = isFont ? options.font : options.fontFamily;
|
|
let wideFontName = "";
|
|
let wideFontClass = "";
|
|
if (text.charCodeAt(0) === 0xD835) {
|
|
[wideFontName, wideFontClass] = wideCharacterFont(text, mode);
|
|
}
|
|
if (wideFontName.length > 0) {
|
|
// surrogate pairs get special treatment
|
|
return makeSymbol(text, wideFontName, mode, options,
|
|
classes.concat(wideFontClass));
|
|
} else if (fontOrFamily) {
|
|
let fontName;
|
|
let fontClasses;
|
|
if (fontOrFamily === "boldsymbol") {
|
|
const fontData = boldsymbol(text, mode, options, classes, type);
|
|
fontName = fontData.fontName;
|
|
fontClasses = [fontData.fontClass];
|
|
} else if (isFont) {
|
|
fontName = fontMap[fontOrFamily].fontName;
|
|
fontClasses = [fontOrFamily];
|
|
} else {
|
|
fontName = retrieveTextFontName(fontOrFamily, options.fontWeight,
|
|
options.fontShape);
|
|
fontClasses = [fontOrFamily, options.fontWeight, options.fontShape];
|
|
}
|
|
|
|
if (lookupSymbol(text, fontName, mode).metrics) {
|
|
return makeSymbol(text, fontName, mode, options,
|
|
classes.concat(fontClasses));
|
|
} else if (ligatures.hasOwnProperty(text) &&
|
|
fontName.slice(0, 10) === "Typewriter") {
|
|
// Deconstruct ligatures in monospace fonts (\texttt, \tt).
|
|
const parts = [];
|
|
for (let i = 0; i < text.length; i++) {
|
|
parts.push(makeSymbol(text[i], fontName, mode, options,
|
|
classes.concat(fontClasses)));
|
|
}
|
|
return makeFragment(parts);
|
|
}
|
|
}
|
|
|
|
// Makes a symbol in the default font for mathords and textords.
|
|
if (type === "mathord") {
|
|
return makeSymbol(text, "Math-Italic", mode, options,
|
|
classes.concat(["mathnormal"]));
|
|
} else if (type === "textord") {
|
|
const font = symbols[mode][text] && symbols[mode][text].font;
|
|
if (font === "ams") {
|
|
const fontName = retrieveTextFontName("amsrm", options.fontWeight,
|
|
options.fontShape);
|
|
return makeSymbol(
|
|
text, fontName, mode, options,
|
|
classes.concat("amsrm", options.fontWeight, options.fontShape));
|
|
} else if (font === "main" || !font) {
|
|
const fontName = retrieveTextFontName("textrm", options.fontWeight,
|
|
options.fontShape);
|
|
return makeSymbol(
|
|
text, fontName, mode, options,
|
|
classes.concat(options.fontWeight, options.fontShape));
|
|
} else { // fonts added by plugins
|
|
const fontName = retrieveTextFontName(font, options.fontWeight,
|
|
options.fontShape);
|
|
// We add font name as a css class
|
|
return makeSymbol(
|
|
text, fontName, mode, options,
|
|
classes.concat(fontName, options.fontWeight, options.fontShape));
|
|
}
|
|
} else {
|
|
throw new Error("unexpected type: " + type + " in makeOrd");
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Returns true if subsequent symbolNodes have the same classes, skew, maxFont,
|
|
* and styles.
|
|
*/
|
|
const canCombine = (prev: SymbolNode, next: SymbolNode) => {
|
|
if (createClass(prev.classes) !== createClass(next.classes)
|
|
|| prev.skew !== next.skew
|
|
|| prev.maxFontSize !== next.maxFontSize) {
|
|
return false;
|
|
}
|
|
|
|
// If prev and next both are just "mbin"s or "mord"s we don't combine them
|
|
// so that the proper spacing can be preserved.
|
|
if (prev.classes.length === 1) {
|
|
const cls = prev.classes[0];
|
|
if (cls === "mbin" || cls === "mord") {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
for (const style in prev.style) {
|
|
if (prev.style.hasOwnProperty(style)
|
|
&& prev.style[style] !== next.style[style]) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
for (const style in next.style) {
|
|
if (next.style.hasOwnProperty(style)
|
|
&& prev.style[style] !== next.style[style]) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* Combine consecutive domTree.symbolNodes into a single symbolNode.
|
|
* Note: this function mutates the argument.
|
|
*/
|
|
const tryCombineChars = (chars: HtmlDomNode[]): HtmlDomNode[] => {
|
|
for (let i = 0; i < chars.length - 1; i++) {
|
|
const prev = chars[i];
|
|
const next = chars[i + 1];
|
|
if (prev instanceof SymbolNode
|
|
&& next instanceof SymbolNode
|
|
&& canCombine(prev, next)) {
|
|
|
|
prev.text += next.text;
|
|
prev.height = Math.max(prev.height, next.height);
|
|
prev.depth = Math.max(prev.depth, next.depth);
|
|
// Use the last character's italic correction since we use
|
|
// it to add padding to the right of the span created from
|
|
// the combined characters.
|
|
prev.italic = next.italic;
|
|
chars.splice(i + 1, 1);
|
|
i--;
|
|
}
|
|
}
|
|
return chars;
|
|
};
|
|
|
|
/**
|
|
* Calculate the height, depth, and maxFontSize of an element based on its
|
|
* children.
|
|
*/
|
|
const sizeElementFromChildren = function(
|
|
elem: DomSpan | Anchor | HtmlDocumentFragment,
|
|
) {
|
|
let height = 0;
|
|
let depth = 0;
|
|
let maxFontSize = 0;
|
|
|
|
for (let i = 0; i < elem.children.length; i++) {
|
|
const child = elem.children[i];
|
|
if (child.height > height) {
|
|
height = child.height;
|
|
}
|
|
if (child.depth > depth) {
|
|
depth = child.depth;
|
|
}
|
|
if (child.maxFontSize > maxFontSize) {
|
|
maxFontSize = child.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(#953): Ensure that `options` is always provided (currently some call
|
|
* sites don't pass it) and make the type below mandatory.
|
|
* TODO: add a separate argument for math class (e.g. `mop`, `mbin`), which
|
|
* should if present come first in `classes`.
|
|
*/
|
|
const makeSpan = function(
|
|
classes?: string[],
|
|
children?: HtmlDomNode[],
|
|
options?: Options,
|
|
style?: CssStyle,
|
|
): DomSpan {
|
|
const span = new Span(classes, children, options, style);
|
|
|
|
sizeElementFromChildren(span);
|
|
|
|
return span;
|
|
};
|
|
|
|
// SVG one is simpler -- doesn't require height, depth, max-font setting.
|
|
// This is also a separate method for typesafety.
|
|
const makeSvgSpan = (
|
|
classes?: string[],
|
|
children?: SvgNode[],
|
|
options?: Options,
|
|
style?: CssStyle,
|
|
): SvgSpan => new Span(classes, children, options, style);
|
|
|
|
const makeLineSpan = function(
|
|
className: string,
|
|
options: Options,
|
|
thickness?: number,
|
|
): DomSpan {
|
|
const line = makeSpan([className], [], options);
|
|
line.height = Math.max(
|
|
thickness || options.fontMetrics().defaultRuleThickness,
|
|
options.minRuleThickness,
|
|
);
|
|
line.style.borderBottomWidth = makeEm(line.height);
|
|
line.maxFontSize = 1.0;
|
|
return line;
|
|
};
|
|
|
|
/**
|
|
* Makes an anchor with the given href, list of classes, list of children,
|
|
* and options.
|
|
*/
|
|
const makeAnchor = function(
|
|
href: string,
|
|
classes: string[],
|
|
children: HtmlDomNode[],
|
|
options: Options,
|
|
): Anchor {
|
|
const anchor = new Anchor(href, classes, children, options);
|
|
|
|
sizeElementFromChildren(anchor);
|
|
|
|
return anchor;
|
|
};
|
|
|
|
/**
|
|
* Makes a document fragment with the given list of children.
|
|
*/
|
|
const makeFragment = function(
|
|
children: HtmlDomNode[],
|
|
): HtmlDocumentFragment {
|
|
const fragment = new DocumentFragment(children);
|
|
|
|
sizeElementFromChildren(fragment);
|
|
|
|
return fragment;
|
|
};
|
|
|
|
/**
|
|
* Wraps group in a span if it's a document fragment, allowing to apply classes
|
|
* and styles
|
|
*/
|
|
const wrapFragment = function(
|
|
group: HtmlDomNode,
|
|
options: Options,
|
|
): HtmlDomNode {
|
|
if (group instanceof DocumentFragment) {
|
|
return makeSpan([], [group], options);
|
|
}
|
|
return group;
|
|
};
|
|
|
|
|
|
// These are exact object types to catch typos in the names of the optional fields.
|
|
export type VListElem = {|
|
|
type: "elem",
|
|
elem: HtmlDomNode,
|
|
marginLeft?: ?string,
|
|
marginRight?: string,
|
|
wrapperClasses?: string[],
|
|
wrapperStyle?: CssStyle,
|
|
|};
|
|
type VListElemAndShift = {|
|
|
type: "elem",
|
|
elem: HtmlDomNode,
|
|
shift: number,
|
|
marginLeft?: ?string,
|
|
marginRight?: string,
|
|
wrapperClasses?: string[],
|
|
wrapperStyle?: CssStyle,
|
|
|};
|
|
type VListKern = {| type: "kern", size: number |};
|
|
|
|
// 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).
|
|
type VListChild = VListElem | VListKern;
|
|
|
|
type VListParam = {|
|
|
// Each child contains how much it should be shifted downward.
|
|
positionType: "individualShift",
|
|
children: VListElemAndShift[],
|
|
|} | {|
|
|
// "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 which MUST be an
|
|
// "elem". Positive values move downwards.
|
|
positionType: "top" | "bottom" | "shift",
|
|
positionData: number,
|
|
children: VListChild[],
|
|
|} | {|
|
|
// The vlist is positioned so that its baseline is aligned with the baseline
|
|
// of the first child which MUST be an "elem". This is equivalent to "shift"
|
|
// with positionData=0.
|
|
positionType: "firstBaseline",
|
|
children: VListChild[],
|
|
|};
|
|
|
|
|
|
// Computes the updated `children` list and the overall depth.
|
|
//
|
|
// This helper function for makeVList makes it easier to enforce type safety by
|
|
// allowing early exits (returns) in the logic.
|
|
const getVListChildrenAndDepth = function(params: VListParam): {
|
|
children: (VListChild | VListElemAndShift)[] | VListChild[],
|
|
depth: number,
|
|
} {
|
|
if (params.positionType === "individualShift") {
|
|
const oldChildren = params.children;
|
|
const children: (VListChild | VListElemAndShift)[] = [oldChildren[0]];
|
|
|
|
// Add in kerns to the list of params.children to get each element to be
|
|
// shifted to the correct specified shift
|
|
const depth = -oldChildren[0].shift - oldChildren[0].elem.depth;
|
|
let currPos = depth;
|
|
for (let 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});
|
|
children.push(oldChildren[i]);
|
|
}
|
|
|
|
return {children, depth};
|
|
}
|
|
|
|
let depth;
|
|
if (params.positionType === "top") {
|
|
// We always start at the bottom, so calculate the bottom by adding up
|
|
// all the sizes
|
|
let bottom = params.positionData;
|
|
for (let i = 0; i < params.children.length; i++) {
|
|
const child = params.children[i];
|
|
bottom -= child.type === "kern"
|
|
? child.size
|
|
: child.elem.height + child.elem.depth;
|
|
}
|
|
depth = bottom;
|
|
} else if (params.positionType === "bottom") {
|
|
depth = -params.positionData;
|
|
} else {
|
|
const firstChild = params.children[0];
|
|
if (firstChild.type !== "elem") {
|
|
throw new Error('First child must have type "elem".');
|
|
}
|
|
if (params.positionType === "shift") {
|
|
depth = -firstChild.elem.depth - params.positionData;
|
|
} else if (params.positionType === "firstBaseline") {
|
|
depth = -firstChild.elem.depth;
|
|
} else {
|
|
throw new Error(`Invalid positionType ${params.positionType}.`);
|
|
}
|
|
}
|
|
return {children: params.children, depth};
|
|
};
|
|
|
|
/**
|
|
* Makes a vertical list by stacking elements and kerns on top of each other.
|
|
* Allows for many different ways of specifying the positioning method.
|
|
*
|
|
* See VListParam documentation above.
|
|
*/
|
|
const makeVList = function(params: VListParam, options: Options): DomSpan {
|
|
const {children, depth} = getVListChildrenAndDepth(params);
|
|
|
|
// Create a strut that is taller than any list item. The strut is added to
|
|
// each item, where it will determine the item's baseline. Since it has
|
|
// `overflow:hidden`, the strut's top edge will sit on the item's line box's
|
|
// top edge and the strut's bottom edge will sit on the item's baseline,
|
|
// with no additional line-height spacing. This allows the item baseline to
|
|
// be positioned precisely without worrying about font ascent and
|
|
// line-height.
|
|
let pstrutSize = 0;
|
|
for (let i = 0; i < children.length; i++) {
|
|
const child = children[i];
|
|
if (child.type === "elem") {
|
|
const elem = child.elem;
|
|
pstrutSize = Math.max(pstrutSize, elem.maxFontSize, elem.height);
|
|
}
|
|
}
|
|
pstrutSize += 2;
|
|
const pstrut = makeSpan(["pstrut"], []);
|
|
pstrut.style.height = makeEm(pstrutSize);
|
|
|
|
// Create a new list of actual children at the correct offsets
|
|
const realChildren = [];
|
|
let minPos = depth;
|
|
let maxPos = depth;
|
|
let currPos = depth;
|
|
for (let i = 0; i < children.length; i++) {
|
|
const child = children[i];
|
|
if (child.type === "kern") {
|
|
currPos += child.size;
|
|
} else {
|
|
const elem = child.elem;
|
|
const classes = child.wrapperClasses || [];
|
|
const style = child.wrapperStyle || {};
|
|
|
|
const childWrap = makeSpan(classes, [pstrut, elem], undefined, style);
|
|
childWrap.style.top = makeEm(-pstrutSize - currPos - elem.depth);
|
|
if (child.marginLeft) {
|
|
childWrap.style.marginLeft = child.marginLeft;
|
|
}
|
|
if (child.marginRight) {
|
|
childWrap.style.marginRight = child.marginRight;
|
|
}
|
|
|
|
realChildren.push(childWrap);
|
|
currPos += elem.height + elem.depth;
|
|
}
|
|
minPos = Math.min(minPos, currPos);
|
|
maxPos = Math.max(maxPos, currPos);
|
|
}
|
|
|
|
// The vlist contents go in a table-cell with `vertical-align:bottom`.
|
|
// This cell's bottom edge will determine the containing table's baseline
|
|
// without overly expanding the containing line-box.
|
|
const vlist = makeSpan(["vlist"], realChildren);
|
|
vlist.style.height = makeEm(maxPos);
|
|
|
|
// A second row is used if necessary to represent the vlist's depth.
|
|
let rows;
|
|
if (minPos < 0) {
|
|
// We will define depth in an empty span with display: table-cell.
|
|
// It should render with the height that we define. But Chrome, in
|
|
// contenteditable mode only, treats that span as if it contains some
|
|
// text content. And that min-height over-rides our desired height.
|
|
// So we put another empty span inside the depth strut span.
|
|
const emptySpan = makeSpan([], []);
|
|
const depthStrut = makeSpan(["vlist"], [emptySpan]);
|
|
depthStrut.style.height = makeEm(-minPos);
|
|
|
|
// Safari wants the first row to have inline content; otherwise it
|
|
// puts the bottom of the *second* row on the baseline.
|
|
const topStrut = makeSpan(["vlist-s"], [new SymbolNode("\u200b")]);
|
|
|
|
rows = [makeSpan(["vlist-r"], [vlist, topStrut]),
|
|
makeSpan(["vlist-r"], [depthStrut])];
|
|
} else {
|
|
rows = [makeSpan(["vlist-r"], [vlist])];
|
|
}
|
|
|
|
const vtable = makeSpan(["vlist-t"], rows);
|
|
if (rows.length === 2) {
|
|
vtable.classes.push("vlist-t2");
|
|
}
|
|
vtable.height = maxPos;
|
|
vtable.depth = -minPos;
|
|
return vtable;
|
|
};
|
|
|
|
// Glue is a concept from TeX which is a flexible space between elements in
|
|
// either a vertical or horizontal list. In KaTeX, at least for now, it's
|
|
// static space between elements in a horizontal layout.
|
|
const makeGlue = (measurement: Measurement, options: Options): DomSpan => {
|
|
// Make an empty span for the space
|
|
const rule = makeSpan(["mspace"], [], options);
|
|
const size = calculateSize(measurement, options);
|
|
rule.style.marginRight = makeEm(size);
|
|
return rule;
|
|
};
|
|
|
|
// Takes font options, and returns the appropriate fontLookup name
|
|
const retrieveTextFontName = function(
|
|
fontFamily: string,
|
|
fontWeight: string,
|
|
fontShape: string,
|
|
): string {
|
|
let baseFontName = "";
|
|
switch (fontFamily) {
|
|
case "amsrm":
|
|
baseFontName = "AMS";
|
|
break;
|
|
case "textrm":
|
|
baseFontName = "Main";
|
|
break;
|
|
case "textsf":
|
|
baseFontName = "SansSerif";
|
|
break;
|
|
case "texttt":
|
|
baseFontName = "Typewriter";
|
|
break;
|
|
default:
|
|
baseFontName = fontFamily; // use fonts added by a plugin
|
|
}
|
|
|
|
let fontStylesName;
|
|
if (fontWeight === "textbf" && fontShape === "textit") {
|
|
fontStylesName = "BoldItalic";
|
|
} else if (fontWeight === "textbf") {
|
|
fontStylesName = "Bold";
|
|
} else if (fontWeight === "textit") {
|
|
fontStylesName = "Italic";
|
|
} else {
|
|
fontStylesName = "Regular";
|
|
}
|
|
|
|
return `${baseFontName}-${fontStylesName}`;
|
|
};
|
|
|
|
/**
|
|
* 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: {[string]: {| variant: FontVariant, fontName: string |}} = {
|
|
// styles
|
|
"mathbf": {
|
|
variant: "bold",
|
|
fontName: "Main-Bold",
|
|
},
|
|
"mathrm": {
|
|
variant: "normal",
|
|
fontName: "Main-Regular",
|
|
},
|
|
"textit": {
|
|
variant: "italic",
|
|
fontName: "Main-Italic",
|
|
},
|
|
"mathit": {
|
|
variant: "italic",
|
|
fontName: "Main-Italic",
|
|
},
|
|
"mathnormal": {
|
|
variant: "italic",
|
|
fontName: "Math-Italic",
|
|
},
|
|
|
|
// "boldsymbol" is missing because they require the use of multiple fonts:
|
|
// Math-BoldItalic and Main-Bold. This is handled by a special case in
|
|
// makeOrd which ends up calling boldsymbol.
|
|
|
|
// 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",
|
|
},
|
|
};
|
|
|
|
const svgData: {
|
|
[string]: ([string, number, number])
|
|
} = {
|
|
// path, width, height
|
|
vec: ["vec", 0.471, 0.714], // values from the font glyph
|
|
oiintSize1: ["oiintSize1", 0.957, 0.499], // oval to overlay the integrand
|
|
oiintSize2: ["oiintSize2", 1.472, 0.659],
|
|
oiiintSize1: ["oiiintSize1", 1.304, 0.499],
|
|
oiiintSize2: ["oiiintSize2", 1.98, 0.659],
|
|
};
|
|
|
|
const staticSvg = function(value: string, options: Options): SvgSpan {
|
|
// Create a span with inline SVG for the element.
|
|
const [pathName, width, height] = svgData[value];
|
|
const path = new PathNode(pathName);
|
|
const svgNode = new SvgNode([path], {
|
|
"width": makeEm(width),
|
|
"height": makeEm(height),
|
|
// Override CSS rule `.katex svg { width: 100% }`
|
|
"style": "width:" + makeEm(width),
|
|
"viewBox": "0 0 " + 1000 * width + " " + 1000 * height,
|
|
"preserveAspectRatio": "xMinYMin",
|
|
});
|
|
const span = makeSvgSpan(["overlay"], [svgNode], options);
|
|
span.height = height;
|
|
span.style.height = makeEm(height);
|
|
span.style.width = makeEm(width);
|
|
return span;
|
|
};
|
|
|
|
export default {
|
|
fontMap,
|
|
makeSymbol,
|
|
mathsym,
|
|
makeSpan,
|
|
makeSvgSpan,
|
|
makeLineSpan,
|
|
makeAnchor,
|
|
makeFragment,
|
|
wrapFragment,
|
|
makeVList,
|
|
makeOrd,
|
|
makeGlue,
|
|
staticSvg,
|
|
svgData,
|
|
tryCombineChars,
|
|
};
|