|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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, |
|
}; |
|
|