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";
const lookupSymbol = function(
value: string,
fontName: string,
mode: Mode,
): {value: string, metrics: ?CharacterMetrics} {
if (symbols[mode][value] && symbols[mode][value].replace) {
value = symbols[mode][value].replace;
}
return {
value: value,
metrics: getCharacterMetrics(value, fontName, mode),
};
};
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 {
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;
};
const mathsym = function(
value: string,
mode: Mode,
options: Options,
classes?: string[] = [],
): SymbolNode {
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"]));
}
};
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 {
return {
fontName: "Main-Bold",
fontClass: "mathbf",
};
}
};
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"];
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) {
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") {
const parts = [];
for (let i = 0; i < text.length; i++) {
parts.push(makeSymbol(text[i], fontName, mode, options,
classes.concat(fontClasses)));
}
return makeFragment(parts);
}
}
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 { const fontName = retrieveTextFontName(font, options.fontWeight,
options.fontShape);
return makeSymbol(
text, fontName, mode, options,
classes.concat(fontName, options.fontWeight, options.fontShape));
}
} else {
throw new Error("unexpected type: " + type + " in makeOrd");
}
};
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.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;
};
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);
prev.italic = next.italic;
chars.splice(i + 1, 1);
i--;
}
}
return chars;
};
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;
};
const makeSpan = function(
classes?: string[],
children?: HtmlDomNode[],
options?: Options,
style?: CssStyle,
): DomSpan {
const span = new Span(classes, children, options, style);
sizeElementFromChildren(span);
return span;
};
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;
};
const makeAnchor = function(
href: string,
classes: string[],
children: HtmlDomNode[],
options: Options,
): Anchor {
const anchor = new Anchor(href, classes, children, options);
sizeElementFromChildren(anchor);
return anchor;
};
const makeFragment = function(
children: HtmlDomNode[],
): HtmlDocumentFragment {
const fragment = new DocumentFragment(children);
sizeElementFromChildren(fragment);
return fragment;
};
const wrapFragment = function(
group: HtmlDomNode,
options: Options,
): HtmlDomNode {
if (group instanceof DocumentFragment) {
return makeSpan([], [group], options);
}
return group;
};
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 |};
type VListChild = VListElem | VListKern;
type VListParam = {|
positionType: "individualShift",
children: VListElemAndShift[],
|} | {|
positionType: "top" | "bottom" | "shift",
positionData: number,
children: VListChild[],
|} | {|
positionType: "firstBaseline",
children: VListChild[],
|};
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]];
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") {
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};
};
const makeVList = function(params: VListParam, options: Options): DomSpan {
const {children, depth} = getVListChildrenAndDepth(params);
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);
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);
}
const vlist = makeSpan(["vlist"], realChildren);
vlist.style.height = makeEm(maxPos);
let rows;
if (minPos < 0) {
const emptySpan = makeSpan([], []);
const depthStrut = makeSpan(["vlist"], [emptySpan]);
depthStrut.style.height = makeEm(-minPos);
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;
};
const makeGlue = (measurement: Measurement, options: Options): DomSpan => {
const rule = makeSpan(["mspace"], [], options);
const size = calculateSize(measurement, options);
rule.style.marginRight = makeEm(size);
return rule;
};
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; }
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}`;
};
const fontMap: {[string]: {| variant: FontVariant, fontName: string |}} = {
"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",
},
"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])
} = {
vec: ["vec", 0.471, 0.714], oiintSize1: ["oiintSize1", 0.957, 0.499], 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 {
const [pathName, width, height] = svgData[value];
const path = new PathNode(pathName);
const svgNode = new SvgNode([path], {
"width": makeEm(width),
"height": makeEm(height),
"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,
};