import ParseError from "./ParseError";
import Style from "./Style";
import {PathNode, SvgNode, SymbolNode} from "./domTree";
import {sqrtPath, innerPath, tallDelim} from "./svgGeometry";
import buildCommon from "./buildCommon";
import {getCharacterMetrics} from "./fontMetrics";
import symbols from "./symbols";
import utils from "./utils";
import {makeEm} from "./units";
import fontMetricsData from "./fontMetricsData";
import type Options from "./Options";
import type {CharacterMetrics} from "./fontMetrics";
import type {HtmlDomNode, DomSpan, SvgSpan} from "./domTree";
import type {Mode} from "./types";
import type {StyleInterface} from "./Style";
import type {VListElem} from "./buildCommon";
const getMetrics = function(
symbol: string,
font: string,
mode: Mode,
): CharacterMetrics {
const replace = symbols.math[symbol] && symbols.math[symbol].replace;
const metrics =
getCharacterMetrics(replace || symbol, font, mode);
if (!metrics) {
throw new Error(`Unsupported symbol ${symbol} and font size ${font}.`);
}
return metrics;
};
const styleWrap = function(
delim: HtmlDomNode,
toStyle: StyleInterface,
options: Options,
classes: string[],
): DomSpan {
const newOptions = options.havingBaseStyle(toStyle);
const span = buildCommon.makeSpan(
classes.concat(newOptions.sizingClasses(options)),
[delim], options);
const delimSizeMultiplier =
newOptions.sizeMultiplier / options.sizeMultiplier;
span.height *= delimSizeMultiplier;
span.depth *= delimSizeMultiplier;
span.maxFontSize = newOptions.sizeMultiplier;
return span;
};
const centerSpan = function(
span: DomSpan,
options: Options,
style: StyleInterface,
) {
const newOptions = options.havingBaseStyle(style);
const shift =
(1 - options.sizeMultiplier / newOptions.sizeMultiplier) *
options.fontMetrics().axisHeight;
span.classes.push("delimcenter");
span.style.top = makeEm(shift);
span.height -= shift;
span.depth += shift;
};
const makeSmallDelim = function(
delim: string,
style: StyleInterface,
center: boolean,
options: Options,
mode: Mode,
classes: string[],
): DomSpan {
const text = buildCommon.makeSymbol(delim, "Main-Regular", mode, options);
const span = styleWrap(text, style, options, classes);
if (center) {
centerSpan(span, options, style);
}
return span;
};
const mathrmSize = function(
value: string,
size: number,
mode: Mode,
options: Options,
): SymbolNode {
return buildCommon.makeSymbol(value, "Size" + size + "-Regular",
mode, options);
};
const makeLargeDelim = function(delim,
size: number,
center: boolean,
options: Options,
mode: Mode,
classes: string[],
): DomSpan {
const inner = mathrmSize(delim, size, mode, options);
const span = styleWrap(
buildCommon.makeSpan(["delimsizing", "size" + size], [inner], options),
Style.TEXT, options, classes);
if (center) {
centerSpan(span, options, Style.TEXT);
}
return span;
};
const makeGlyphSpan = function(
symbol: string,
font: "Size1-Regular" | "Size4-Regular",
mode: Mode,
): VListElem {
let sizeClass;
if (font === "Size1-Regular") {
sizeClass = "delim-size1";
} else {
sizeClass = "delim-size4";
}
const corner = buildCommon.makeSpan(
["delimsizinginner", sizeClass],
[buildCommon.makeSpan([], [buildCommon.makeSymbol(symbol, font, mode)])]);
return {type: "elem", elem: corner};
};
const makeInner = function(
ch: string,
height: number,
options: Options
): VListElem {
const width = fontMetricsData['Size4-Regular'][ch.charCodeAt(0)]
? fontMetricsData['Size4-Regular'][ch.charCodeAt(0)][4]
: fontMetricsData['Size1-Regular'][ch.charCodeAt(0)][4];
const path = new PathNode("inner", innerPath(ch, Math.round(1000 * height)));
const svgNode = new SvgNode([path], {
"width": makeEm(width),
"height": makeEm(height),
"style": "width:" + makeEm(width),
"viewBox": "0 0 " + 1000 * width + " " + Math.round(1000 * height),
"preserveAspectRatio": "xMinYMin",
});
const span = buildCommon.makeSvgSpan([], [svgNode], options);
span.height = height;
span.style.height = makeEm(height);
span.style.width = makeEm(width);
return {type: "elem", elem: span};
};
const lapInEms = 0.008;
const lap = {type: "kern", size: -1 * lapInEms};
const verts = ["|", "\\lvert", "\\rvert", "\\vert"];
const doubleVerts = ["\\|", "\\lVert", "\\rVert", "\\Vert"];
const makeStackedDelim = function(
delim: string,
heightTotal: number,
center: boolean,
options: Options,
mode: Mode,
classes: string[],
): DomSpan {
let top;
let middle;
let repeat;
let bottom;
let svgLabel = "";
let viewBoxWidth = 0;
top = repeat = bottom = delim;
middle = null;
let font = "Size1-Regular";
if (delim === "\\uparrow") {
repeat = bottom = "\u23d0";
} else if (delim === "\\Uparrow") {
repeat = bottom = "\u2016";
} else if (delim === "\\downarrow") {
top = repeat = "\u23d0";
} else if (delim === "\\Downarrow") {
top = repeat = "\u2016";
} else if (delim === "\\updownarrow") {
top = "\\uparrow";
repeat = "\u23d0";
bottom = "\\downarrow";
} else if (delim === "\\Updownarrow") {
top = "\\Uparrow";
repeat = "\u2016";
bottom = "\\Downarrow";
} else if (utils.contains(verts, delim)) {
repeat = "\u2223";
svgLabel = "vert";
viewBoxWidth = 333;
} else if (utils.contains(doubleVerts, delim)) {
repeat = "\u2225";
svgLabel = "doublevert";
viewBoxWidth = 556;
} else if (delim === "[" || delim === "\\lbrack") {
top = "\u23a1";
repeat = "\u23a2";
bottom = "\u23a3";
font = "Size4-Regular";
svgLabel = "lbrack";
viewBoxWidth = 667;
} else if (delim === "]" || delim === "\\rbrack") {
top = "\u23a4";
repeat = "\u23a5";
bottom = "\u23a6";
font = "Size4-Regular";
svgLabel = "rbrack";
viewBoxWidth = 667;
} else if (delim === "\\lfloor" || delim === "\u230a") {
repeat = top = "\u23a2";
bottom = "\u23a3";
font = "Size4-Regular";
svgLabel = "lfloor";
viewBoxWidth = 667;
} else if (delim === "\\lceil" || delim === "\u2308") {
top = "\u23a1";
repeat = bottom = "\u23a2";
font = "Size4-Regular";
svgLabel = "lceil";
viewBoxWidth = 667;
} else if (delim === "\\rfloor" || delim === "\u230b") {
repeat = top = "\u23a5";
bottom = "\u23a6";
font = "Size4-Regular";
svgLabel = "rfloor";
viewBoxWidth = 667;
} else if (delim === "\\rceil" || delim === "\u2309") {
top = "\u23a4";
repeat = bottom = "\u23a5";
font = "Size4-Regular";
svgLabel = "rceil";
viewBoxWidth = 667;
} else if (delim === "(" || delim === "\\lparen") {
top = "\u239b";
repeat = "\u239c";
bottom = "\u239d";
font = "Size4-Regular";
svgLabel = "lparen";
viewBoxWidth = 875;
} else if (delim === ")" || delim === "\\rparen") {
top = "\u239e";
repeat = "\u239f";
bottom = "\u23a0";
font = "Size4-Regular";
svgLabel = "rparen";
viewBoxWidth = 875;
} else if (delim === "\\{" || delim === "\\lbrace") {
top = "\u23a7";
middle = "\u23a8";
bottom = "\u23a9";
repeat = "\u23aa";
font = "Size4-Regular";
} else if (delim === "\\}" || delim === "\\rbrace") {
top = "\u23ab";
middle = "\u23ac";
bottom = "\u23ad";
repeat = "\u23aa";
font = "Size4-Regular";
} else if (delim === "\\lgroup" || delim === "\u27ee") {
top = "\u23a7";
bottom = "\u23a9";
repeat = "\u23aa";
font = "Size4-Regular";
} else if (delim === "\\rgroup" || delim === "\u27ef") {
top = "\u23ab";
bottom = "\u23ad";
repeat = "\u23aa";
font = "Size4-Regular";
} else if (delim === "\\lmoustache" || delim === "\u23b0") {
top = "\u23a7";
bottom = "\u23ad";
repeat = "\u23aa";
font = "Size4-Regular";
} else if (delim === "\\rmoustache" || delim === "\u23b1") {
top = "\u23ab";
bottom = "\u23a9";
repeat = "\u23aa";
font = "Size4-Regular";
}
const topMetrics = getMetrics(top, font, mode);
const topHeightTotal = topMetrics.height + topMetrics.depth;
const repeatMetrics = getMetrics(repeat, font, mode);
const repeatHeightTotal = repeatMetrics.height + repeatMetrics.depth;
const bottomMetrics = getMetrics(bottom, font, mode);
const bottomHeightTotal = bottomMetrics.height + bottomMetrics.depth;
let middleHeightTotal = 0;
let middleFactor = 1;
if (middle !== null) {
const middleMetrics = getMetrics(middle, font, mode);
middleHeightTotal = middleMetrics.height + middleMetrics.depth;
middleFactor = 2; }
const minHeight = topHeightTotal + bottomHeightTotal + middleHeightTotal;
const repeatCount = Math.max(0, Math.ceil(
(heightTotal - minHeight) / (middleFactor * repeatHeightTotal)));
const realHeightTotal =
minHeight + repeatCount * middleFactor * repeatHeightTotal;
let axisHeight = options.fontMetrics().axisHeight;
if (center) {
axisHeight *= options.sizeMultiplier;
}
const depth = realHeightTotal / 2 - axisHeight;
const stack = [];
if (svgLabel.length > 0) {
const midHeight = realHeightTotal - topHeightTotal - bottomHeightTotal;
const viewBoxHeight = Math.round(realHeightTotal * 1000);
const pathStr = tallDelim(svgLabel, Math.round(midHeight * 1000));
const path = new PathNode(svgLabel, pathStr);
const width = (viewBoxWidth / 1000).toFixed(3) + "em";
const height = (viewBoxHeight / 1000).toFixed(3) + "em";
const svg = new SvgNode([path], {
"width": width,
"height": height,
"viewBox": `0 0 ${viewBoxWidth} ${viewBoxHeight}`,
});
const wrapper = buildCommon.makeSvgSpan([], [svg], options);
wrapper.height = viewBoxHeight / 1000;
wrapper.style.width = width;
wrapper.style.height = height;
stack.push({type: "elem", elem: wrapper});
} else {
stack.push(makeGlyphSpan(bottom, font, mode));
stack.push(lap);
if (middle === null) {
const innerHeight = realHeightTotal - topHeightTotal - bottomHeightTotal
+ 2 * lapInEms;
stack.push(makeInner(repeat, innerHeight, options));
} else {
const innerHeight = (realHeightTotal - topHeightTotal -
bottomHeightTotal - middleHeightTotal) / 2 + 2 * lapInEms;
stack.push(makeInner(repeat, innerHeight, options));
stack.push(lap);
stack.push(makeGlyphSpan(middle, font, mode));
stack.push(lap);
stack.push(makeInner(repeat, innerHeight, options));
}
stack.push(lap);
stack.push(makeGlyphSpan(top, font, mode));
}
const newOptions = options.havingBaseStyle(Style.TEXT);
const inner = buildCommon.makeVList({
positionType: "bottom",
positionData: depth,
children: stack,
}, newOptions);
return styleWrap(
buildCommon.makeSpan(["delimsizing", "mult"], [inner], newOptions),
Style.TEXT, options, classes);
};
const vbPad = 80; const emPad = 0.08;
const sqrtSvg = function(
sqrtName: string,
height: number,
viewBoxHeight: number,
extraVinculum: number,
options: Options,
): SvgSpan {
const path = sqrtPath(sqrtName, extraVinculum, viewBoxHeight);
const pathNode = new PathNode(sqrtName, path);
const svg = new SvgNode([pathNode], {
"width": "400em",
"height": makeEm(height),
"viewBox": "0 0 400000 " + viewBoxHeight,
"preserveAspectRatio": "xMinYMin slice",
});
return buildCommon.makeSvgSpan(["hide-tail"], [svg], options);
};
const makeSqrtImage = function(
height: number,
options: Options,
): {
span: SvgSpan,
ruleWidth: number,
advanceWidth: number,
} {
const newOptions = options.havingBaseSizing();
const delim = traverseSequence("\\surd", height * newOptions.sizeMultiplier,
stackLargeDelimiterSequence, newOptions);
let sizeMultiplier = newOptions.sizeMultiplier;
const extraVinculum = Math.max(0,
options.minRuleThickness - options.fontMetrics().sqrtRuleThickness);
let span;
let spanHeight = 0;
let texHeight = 0;
let viewBoxHeight = 0;
let advanceWidth;
if (delim.type === "small") {
viewBoxHeight = 1000 + 1000 * extraVinculum + vbPad;
if (height < 1.0) {
sizeMultiplier = 1.0; } else if (height < 1.4) {
sizeMultiplier = 0.7; }
spanHeight = (1.0 + extraVinculum + emPad) / sizeMultiplier;
texHeight = (1.00 + extraVinculum) / sizeMultiplier;
span = sqrtSvg("sqrtMain", spanHeight, viewBoxHeight, extraVinculum,
options);
span.style.minWidth = "0.853em";
advanceWidth = 0.833 / sizeMultiplier;
} else if (delim.type === "large") {
viewBoxHeight = (1000 + vbPad) * sizeToMaxHeight[delim.size];
texHeight = (sizeToMaxHeight[delim.size] + extraVinculum) / sizeMultiplier;
spanHeight = (sizeToMaxHeight[delim.size] + extraVinculum + emPad)
/ sizeMultiplier;
span = sqrtSvg("sqrtSize" + delim.size, spanHeight, viewBoxHeight,
extraVinculum, options);
span.style.minWidth = "1.02em";
advanceWidth = 1.0 / sizeMultiplier;
} else {
spanHeight = height + extraVinculum + emPad;
texHeight = height + extraVinculum;
viewBoxHeight = Math.floor(1000 * height + extraVinculum) + vbPad;
span = sqrtSvg("sqrtTall", spanHeight, viewBoxHeight, extraVinculum,
options);
span.style.minWidth = "0.742em";
advanceWidth = 1.056;
}
span.height = texHeight;
span.style.height = makeEm(spanHeight);
return {
span,
advanceWidth,
ruleWidth: (options.fontMetrics().sqrtRuleThickness + extraVinculum)
* sizeMultiplier,
};
};
const stackLargeDelimiters = [
"(", "\\lparen", ")", "\\rparen",
"[", "\\lbrack", "]", "\\rbrack",
"\\{", "\\lbrace", "\\}", "\\rbrace",
"\\lfloor", "\\rfloor", "\u230a", "\u230b",
"\\lceil", "\\rceil", "\u2308", "\u2309",
"\\surd",
];
const stackAlwaysDelimiters = [
"\\uparrow", "\\downarrow", "\\updownarrow",
"\\Uparrow", "\\Downarrow", "\\Updownarrow",
"|", "\\|", "\\vert", "\\Vert",
"\\lvert", "\\rvert", "\\lVert", "\\rVert",
"\\lgroup", "\\rgroup", "\u27ee", "\u27ef",
"\\lmoustache", "\\rmoustache", "\u23b0", "\u23b1",
];
const stackNeverDelimiters = [
"<", ">", "\\langle", "\\rangle", "/", "\\backslash", "\\lt", "\\gt",
];
const sizeToMaxHeight = [0, 1.2, 1.8, 2.4, 3.0];
const makeSizedDelim = function(
delim: string,
size: number,
options: Options,
mode: Mode,
classes: string[],
): DomSpan {
if (delim === "<" || delim === "\\lt" || delim === "\u27e8") {
delim = "\\langle";
} else if (delim === ">" || delim === "\\gt" || delim === "\u27e9") {
delim = "\\rangle";
}
if (utils.contains(stackLargeDelimiters, delim) ||
utils.contains(stackNeverDelimiters, delim)) {
return makeLargeDelim(delim, size, false, options, mode, classes);
} else if (utils.contains(stackAlwaysDelimiters, delim)) {
return makeStackedDelim(
delim, sizeToMaxHeight[size], false, options, mode, classes);
} else {
throw new ParseError("Illegal delimiter: '" + delim + "'");
}
};
type Delimiter =
{type: "small", style: StyleInterface} |
{type: "large", size: 1 | 2 | 3 | 4} |
{type: "stack"};
const stackNeverDelimiterSequence = [
{type: "small", style: Style.SCRIPTSCRIPT},
{type: "small", style: Style.SCRIPT},
{type: "small", style: Style.TEXT},
{type: "large", size: 1},
{type: "large", size: 2},
{type: "large", size: 3},
{type: "large", size: 4},
];
const stackAlwaysDelimiterSequence = [
{type: "small", style: Style.SCRIPTSCRIPT},
{type: "small", style: Style.SCRIPT},
{type: "small", style: Style.TEXT},
{type: "stack"},
];
const stackLargeDelimiterSequence = [
{type: "small", style: Style.SCRIPTSCRIPT},
{type: "small", style: Style.SCRIPT},
{type: "small", style: Style.TEXT},
{type: "large", size: 1},
{type: "large", size: 2},
{type: "large", size: 3},
{type: "large", size: 4},
{type: "stack"},
];
const delimTypeToFont = function(type: Delimiter): string {
if (type.type === "small") {
return "Main-Regular";
} else if (type.type === "large") {
return "Size" + type.size + "-Regular";
} else if (type.type === "stack") {
return "Size4-Regular";
} else {
throw new Error(`Add support for delim type '${type.type}' here.`);
}
};
const traverseSequence = function(
delim: string,
height: number,
sequence: Delimiter[],
options: Options,
): Delimiter {
const start = Math.min(2, 3 - options.style.size);
for (let i = start; i < sequence.length; i++) {
if (sequence[i].type === "stack") {
break;
}
const metrics = getMetrics(delim, delimTypeToFont(sequence[i]), "math");
let heightDepth = metrics.height + metrics.depth;
if (sequence[i].type === "small") {
const newOptions = options.havingBaseStyle(sequence[i].style);
heightDepth *= newOptions.sizeMultiplier;
}
if (heightDepth > height) {
return sequence[i];
}
}
return sequence[sequence.length - 1];
};
const makeCustomSizedDelim = function(
delim: string,
height: number,
center: boolean,
options: Options,
mode: Mode,
classes: string[],
): DomSpan {
if (delim === "<" || delim === "\\lt" || delim === "\u27e8") {
delim = "\\langle";
} else if (delim === ">" || delim === "\\gt" || delim === "\u27e9") {
delim = "\\rangle";
}
let sequence;
if (utils.contains(stackNeverDelimiters, delim)) {
sequence = stackNeverDelimiterSequence;
} else if (utils.contains(stackLargeDelimiters, delim)) {
sequence = stackLargeDelimiterSequence;
} else {
sequence = stackAlwaysDelimiterSequence;
}
const delimType = traverseSequence(delim, height, sequence, options);
if (delimType.type === "small") {
return makeSmallDelim(delim, delimType.style, center, options,
mode, classes);
} else if (delimType.type === "large") {
return makeLargeDelim(delim, delimType.size, center, options, mode,
classes);
} else {
return makeStackedDelim(delim, height, center, options, mode,
classes);
}
};
const makeLeftRightDelim = function(
delim: string,
height: number,
depth: number,
options: Options,
mode: Mode,
classes: string[],
): DomSpan {
const axisHeight =
options.fontMetrics().axisHeight * options.sizeMultiplier;
const delimiterFactor = 901;
const delimiterExtend = 5.0 / options.fontMetrics().ptPerEm;
const maxDistFromAxis = Math.max(
height - axisHeight, depth + axisHeight);
const totalHeight = Math.max(
maxDistFromAxis / 500 * delimiterFactor,
2 * maxDistFromAxis - delimiterExtend);
return makeCustomSizedDelim(delim, totalHeight, true, options, mode, classes);
};
export default {
sqrtImage: makeSqrtImage,
sizedDelim: makeSizedDelim,
sizeToMaxHeight: sizeToMaxHeight,
customSizedDelim: makeCustomSizedDelim,
leftRightDelim: makeLeftRightDelim,
};