mirror of
https://github.com/Smaug123/KaTeX
synced 2025-10-05 03:08:40 +00:00
Port domTree and buildCommon to @flow. (#938)
* Port domTree to @flow. * Port buildCommon to @flow. * Change domTree attribute arrays to attribute objects.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
// @flow
|
||||
/* eslint no-console:0 */
|
||||
/**
|
||||
* This module contains general functions that can be used for building
|
||||
@@ -9,6 +10,12 @@ import fontMetrics from "./fontMetrics";
|
||||
import symbols from "./symbols";
|
||||
import utils from "./utils";
|
||||
|
||||
import type Options from "./Options";
|
||||
import type ParseNode from "./ParseNode";
|
||||
import type {CharacterMetrics} from "./fontMetrics";
|
||||
import type {Mode} from "./types";
|
||||
import type {DomChildNode, CombinableDomNode} from "./domTree";
|
||||
|
||||
// The following have to be loaded from Main-Italic font, using class mainit
|
||||
const mainitLetters = [
|
||||
"\\imath", // dotless i
|
||||
@@ -20,7 +27,12 @@ const mainitLetters = [
|
||||
* Looks up the given symbol in fontMetrics, after applying any symbol
|
||||
* replacements defined in symbol.js
|
||||
*/
|
||||
const lookupSymbol = function(value, fontFamily, mode) {
|
||||
const lookupSymbol = function(
|
||||
value: string,
|
||||
// TODO(#963): Use a union type for this.
|
||||
fontFamily: 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;
|
||||
@@ -39,8 +51,15 @@ const lookupSymbol = function(value, fontFamily, mode) {
|
||||
* 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, fontFamily, mode, options, classes) {
|
||||
const makeSymbol = function(
|
||||
value: string,
|
||||
fontFamily: string,
|
||||
mode: Mode,
|
||||
options?: Options,
|
||||
classes?: string[],
|
||||
): domTree.symbolNode {
|
||||
const lookup = lookupSymbol(value, fontFamily, mode);
|
||||
const metrics = lookup.metrics;
|
||||
value = lookup.value;
|
||||
@@ -67,8 +86,9 @@ const makeSymbol = function(value, fontFamily, mode, options, classes) {
|
||||
if (options.style.isTight()) {
|
||||
symbolNode.classes.push("mtight");
|
||||
}
|
||||
if (options.getColor()) {
|
||||
symbolNode.style.color = options.getColor();
|
||||
const color = options.getColor();
|
||||
if (color) {
|
||||
symbolNode.style.color = color;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,8 +98,15 @@ const makeSymbol = function(value, fontFamily, mode, options, classes) {
|
||||
/**
|
||||
* Makes a symbol in Main-Regular or AMS-Regular.
|
||||
* Used for rel, bin, open, close, inner, and punct.
|
||||
*
|
||||
* TODO(#953): Make `options` mandatory and always pass it in.
|
||||
*/
|
||||
const mathsym = function(value, mode, options, classes) {
|
||||
const mathsym = function(
|
||||
value: string,
|
||||
mode: Mode,
|
||||
options?: Options,
|
||||
classes?: string[] = [],
|
||||
): domTree.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
|
||||
@@ -97,7 +124,13 @@ const mathsym = function(value, mode, options, classes) {
|
||||
/**
|
||||
* Makes a symbol in the default font for mathords and textords.
|
||||
*/
|
||||
const mathDefault = function(value, mode, options, classes, type) {
|
||||
const mathDefault = function(
|
||||
value: string,
|
||||
mode: Mode,
|
||||
options: Options,
|
||||
classes: string[],
|
||||
type: string, // TODO(#892): Use ParseNode type here.
|
||||
): domTree.symbolNode {
|
||||
if (type === "mathord") {
|
||||
const fontLookup = mathit(value, mode, options, classes);
|
||||
return makeSymbol(value, fontLookup.fontName, mode, options,
|
||||
@@ -123,7 +156,12 @@ const mathDefault = function(value, mode, options, classes, type) {
|
||||
* depending on the symbol. Use this function instead of fontMap for font
|
||||
* "mathit".
|
||||
*/
|
||||
const mathit = function(value, mode, options, classes) {
|
||||
const mathit = function(
|
||||
value: string,
|
||||
mode: Mode,
|
||||
options: Options,
|
||||
classes: string[],
|
||||
): {| fontName: string, fontClass: string |} {
|
||||
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
|
||||
@@ -143,7 +181,11 @@ const mathit = function(value, mode, options, classes) {
|
||||
/**
|
||||
* Makes either a mathord or textord in the correct font and color.
|
||||
*/
|
||||
const makeOrd = function(group, options, type) {
|
||||
const makeOrd = function(
|
||||
group: ParseNode,
|
||||
options: Options,
|
||||
type: string, // TODO(#892): Use ParseNode type here.
|
||||
): domTree.symbolNode {
|
||||
const mode = group.mode;
|
||||
const value = group.value;
|
||||
|
||||
@@ -172,7 +214,9 @@ const makeOrd = function(group, options, type) {
|
||||
* Combine as many characters as possible in the given array of characters
|
||||
* via their tryCombine method.
|
||||
*/
|
||||
const tryCombineChars = function(chars) {
|
||||
const tryCombineChars = function(
|
||||
chars: CombinableDomNode[],
|
||||
): CombinableDomNode[] {
|
||||
for (let i = 0; i < chars.length - 1; i++) {
|
||||
if (chars[i].tryCombine(chars[i + 1])) {
|
||||
chars.splice(i + 1, 1);
|
||||
@@ -186,22 +230,22 @@ const tryCombineChars = function(chars) {
|
||||
* Calculate the height, depth, and maxFontSize of an element based on its
|
||||
* children.
|
||||
*/
|
||||
const sizeElementFromChildren = function(elem) {
|
||||
const sizeElementFromChildren = function(
|
||||
elem: domTree.span | domTree.anchor | domTree.documentFragment,
|
||||
) {
|
||||
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;
|
||||
}
|
||||
for (const child of elem.children) {
|
||||
if (child.height > height) {
|
||||
height = child.height;
|
||||
}
|
||||
if (child.depth > depth) {
|
||||
depth = child.depth;
|
||||
}
|
||||
if (child.maxFontSize > maxFontSize) {
|
||||
maxFontSize = child.maxFontSize;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,12 +257,16 @@ const sizeElementFromChildren = function(elem) {
|
||||
/**
|
||||
* 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(#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, children, options) {
|
||||
const makeSpan = function(
|
||||
classes?: string[],
|
||||
children?: DomChildNode[],
|
||||
options?: Options,
|
||||
): domTree.span {
|
||||
const span = new domTree.span(classes, children, options);
|
||||
|
||||
sizeElementFromChildren(span);
|
||||
@@ -230,7 +278,12 @@ const makeSpan = function(classes, children, options) {
|
||||
* Makes an anchor with the given href, list of classes, list of children,
|
||||
* and options.
|
||||
*/
|
||||
const makeAnchor = function(href, classes, children, options) {
|
||||
const makeAnchor = function(
|
||||
href: string,
|
||||
classes: string[],
|
||||
children: DomChildNode[],
|
||||
options: Options,
|
||||
) {
|
||||
const anchor = new domTree.anchor(href, classes, children, options);
|
||||
|
||||
sizeElementFromChildren(anchor);
|
||||
@@ -242,7 +295,10 @@ const makeAnchor = function(href, classes, children, options) {
|
||||
* Prepends the given children to the given span, updating height, depth, and
|
||||
* maxFontSize.
|
||||
*/
|
||||
const prependChildren = function(span, children) {
|
||||
const prependChildren = function(
|
||||
span: domTree.span,
|
||||
children: DomChildNode[],
|
||||
) {
|
||||
span.children = children.concat(span.children);
|
||||
|
||||
sizeElementFromChildren(span);
|
||||
@@ -251,7 +307,9 @@ const prependChildren = function(span, children) {
|
||||
/**
|
||||
* Makes a document fragment with the given list of children.
|
||||
*/
|
||||
const makeFragment = function(children) {
|
||||
const makeFragment = function(
|
||||
children: DomChildNode[],
|
||||
): domTree.documentFragment {
|
||||
const fragment = new domTree.documentFragment(children);
|
||||
|
||||
sizeElementFromChildren(fragment);
|
||||
@@ -260,11 +318,21 @@ const makeFragment = function(children) {
|
||||
};
|
||||
|
||||
|
||||
// TODO(#939): Uncomment and use VListParam as the type of makeVList's first param.
|
||||
/*
|
||||
type VListElem =
|
||||
{type: "elem", elem: DomChildNode, marginLeft?: string, marginRight?: string};
|
||||
type VListKern = {type: "kern", size: number};
|
||||
// These are exact object types to catch typos in the names of the optional fields.
|
||||
type VListElem = {|
|
||||
type: "elem",
|
||||
elem: DomChildNode,
|
||||
marginLeft?: string,
|
||||
marginRight?: string,
|
||||
|};
|
||||
type VListElemAndShift = {|
|
||||
type: "elem",
|
||||
elem: DomChildNode,
|
||||
shift: number,
|
||||
marginLeft?: string,
|
||||
marginRight?: string,
|
||||
|};
|
||||
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).
|
||||
@@ -273,45 +341,44 @@ type VListChild = VListElem | VListKern;
|
||||
type VListParam = {|
|
||||
// Each child contains how much it should be shifted downward.
|
||||
positionType: "individualShift",
|
||||
children: (VListElem & {shift: number})[],
|
||||
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. Positive values move
|
||||
// downwards.
|
||||
// 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. This is equivalent to "shift" with positionData=0.
|
||||
// of the first child which MUST be an "elem". This is equivalent to "shift"
|
||||
// with positionData=0.
|
||||
positionType: "firstBaseline",
|
||||
children: VListChild[],
|
||||
|};
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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 parameter documentation on the type documentation above.
|
||||
*/
|
||||
const makeVList = function({positionType, positionData, children}, 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
|
||||
// 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
|
||||
depth = -oldChildren[0].shift - oldChildren[0].elem.depth;
|
||||
currPos = depth;
|
||||
for (i = 1; i < oldChildren.length; i++) {
|
||||
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 -
|
||||
@@ -320,30 +387,50 @@ const makeVList = function({positionType, positionData, children}, options) {
|
||||
|
||||
currPos = currPos + diff;
|
||||
|
||||
children.push({type: "kern", size: size});
|
||||
children.push({type: "kern", size});
|
||||
children.push(oldChildren[i]);
|
||||
}
|
||||
} else if (positionType === "top") {
|
||||
|
||||
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 = 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;
|
||||
}
|
||||
let bottom = params.positionData;
|
||||
for (const child of params.children) {
|
||||
bottom -= child.type === "kern"
|
||||
? child.size
|
||||
: child.elem.height + child.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 if (params.positionType === "bottom") {
|
||||
depth = -params.positionData;
|
||||
} else {
|
||||
depth = 0;
|
||||
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): domTree.span {
|
||||
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
|
||||
@@ -353,10 +440,10 @@ const makeVList = function({positionType, positionData, children}, options) {
|
||||
// be positioned precisely without worrying about font ascent and
|
||||
// line-height.
|
||||
let pstrutSize = 0;
|
||||
for (i = 0; i < children.length; i++) {
|
||||
if (children[i].type === "elem") {
|
||||
const child = children[i].elem;
|
||||
pstrutSize = Math.max(pstrutSize, child.maxFontSize, child.height);
|
||||
for (const child of children) {
|
||||
if (child.type === "elem") {
|
||||
const elem = child.elem;
|
||||
pstrutSize = Math.max(pstrutSize, elem.maxFontSize, elem.height);
|
||||
}
|
||||
}
|
||||
pstrutSize += 2;
|
||||
@@ -367,24 +454,24 @@ const makeVList = function({positionType, positionData, children}, options) {
|
||||
const realChildren = [];
|
||||
let minPos = depth;
|
||||
let maxPos = depth;
|
||||
currPos = depth;
|
||||
for (i = 0; i < children.length; i++) {
|
||||
if (children[i].type === "kern") {
|
||||
currPos += children[i].size;
|
||||
let currPos = depth;
|
||||
for (const child of children) {
|
||||
if (child.type === "kern") {
|
||||
currPos += child.size;
|
||||
} else {
|
||||
const child = children[i].elem;
|
||||
const elem = child.elem;
|
||||
|
||||
const childWrap = makeSpan([], [pstrut, child]);
|
||||
childWrap.style.top = (-pstrutSize - currPos - child.depth) + "em";
|
||||
if (children[i].marginLeft) {
|
||||
childWrap.style.marginLeft = children[i].marginLeft;
|
||||
const childWrap = makeSpan([], [pstrut, elem]);
|
||||
childWrap.style.top = (-pstrutSize - currPos - elem.depth) + "em";
|
||||
if (child.marginLeft) {
|
||||
childWrap.style.marginLeft = child.marginLeft;
|
||||
}
|
||||
if (children[i].marginRight) {
|
||||
childWrap.style.marginRight = children[i].marginRight;
|
||||
if (child.marginRight) {
|
||||
childWrap.style.marginRight = child.marginRight;
|
||||
}
|
||||
|
||||
realChildren.push(childWrap);
|
||||
currPos += child.height + child.depth;
|
||||
currPos += elem.height + elem.depth;
|
||||
}
|
||||
minPos = Math.min(minPos, currPos);
|
||||
maxPos = Math.max(maxPos, currPos);
|
||||
@@ -422,7 +509,9 @@ const makeVList = function({positionType, positionData, children}, options) {
|
||||
};
|
||||
|
||||
// Converts verb group into body string, dealing with \verb* form
|
||||
const makeVerb = function(group, options) {
|
||||
const makeVerb = function(group: ParseNode, options: Options): string {
|
||||
// TODO(#892): Make ParseNode type-safe and confirm `group.type` to guarantee
|
||||
// that `group.value.body` is of type string.
|
||||
let text = group.value.body;
|
||||
if (group.value.star) {
|
||||
text = text.replace(/ /g, '\u2423'); // Open Box
|
||||
@@ -435,7 +524,7 @@ const makeVerb = function(group, options) {
|
||||
|
||||
// A map of spacing functions to their attributes, like size and corresponding
|
||||
// CSS class
|
||||
const spacingFunctions = {
|
||||
const spacingFunctions: {[string]: {| size: string, className: string |}} = {
|
||||
"\\qquad": {
|
||||
size: "2em",
|
||||
className: "qquad",
|
||||
@@ -472,7 +561,7 @@ const spacingFunctions = {
|
||||
* - fontName: the "style" parameter to fontMetrics.getCharacterMetrics
|
||||
*/
|
||||
// A map between tex font commands an MathML mathvariant attribute values
|
||||
const fontMap = {
|
||||
const fontMap: {[string]: {| variant: string, fontName: string |}} = {
|
||||
// styles
|
||||
"mathbf": {
|
||||
variant: "bold",
|
||||
@@ -519,16 +608,16 @@ const fontMap = {
|
||||
};
|
||||
|
||||
export default {
|
||||
fontMap: fontMap,
|
||||
makeSymbol: makeSymbol,
|
||||
mathsym: mathsym,
|
||||
makeSpan: makeSpan,
|
||||
makeAnchor: makeAnchor,
|
||||
makeFragment: makeFragment,
|
||||
makeVList: makeVList,
|
||||
makeOrd: makeOrd,
|
||||
makeVerb: makeVerb,
|
||||
tryCombineChars: tryCombineChars,
|
||||
prependChildren: prependChildren,
|
||||
spacingFunctions: spacingFunctions,
|
||||
fontMap,
|
||||
makeSymbol,
|
||||
mathsym,
|
||||
makeSpan,
|
||||
makeAnchor,
|
||||
makeFragment,
|
||||
makeVList,
|
||||
makeOrd,
|
||||
makeVerb,
|
||||
tryCombineChars,
|
||||
prependChildren,
|
||||
spacingFunctions,
|
||||
};
|
||||
|
@@ -333,11 +333,13 @@ const sqrtSvg = function(sqrtName, height, viewBoxHeight, options) {
|
||||
}
|
||||
const pathNode = new domTree.pathNode(sqrtName, alternate);
|
||||
|
||||
// Note: 1000:1 ratio of viewBox to document em width.
|
||||
const attributes = [["width", "400em"], ["height", height + "em"]];
|
||||
attributes.push(["viewBox", "0 0 400000 " + viewBoxHeight]);
|
||||
attributes.push(["preserveAspectRatio", "xMinYMin slice"]);
|
||||
const svg = new domTree.svgNode([pathNode], attributes);
|
||||
const svg = new domTree.svgNode([pathNode], {
|
||||
// Note: 1000:1 ratio of viewBox to document em width.
|
||||
"width": "400em",
|
||||
"height": height + "em",
|
||||
"viewBox": "0 0 400000 " + viewBoxHeight,
|
||||
"preserveAspectRatio": "xMinYMin slice",
|
||||
});
|
||||
|
||||
return buildCommon.makeSpan(["hide-tail"], [svg], options);
|
||||
};
|
||||
|
245
src/domTree.js
245
src/domTree.js
@@ -1,3 +1,4 @@
|
||||
// @flow
|
||||
/**
|
||||
* These objects store the data about the DOM nodes we create, as well as some
|
||||
* extra data. They can then be transformed into real DOM nodes with the
|
||||
@@ -10,12 +11,13 @@
|
||||
import {cjkRegex, hangulRegex} from "./unicodeRegexes";
|
||||
import utils from "./utils";
|
||||
import svgGeometry from "./svgGeometry";
|
||||
import type Options from "./Options";
|
||||
|
||||
/**
|
||||
* Create an HTML className based on a list of classes. In addition to joining
|
||||
* with spaces, we also remove null or empty classes.
|
||||
*/
|
||||
const createClass = function(classes) {
|
||||
const createClass = function(classes: string[]): string {
|
||||
classes = classes.slice();
|
||||
for (let i = classes.length - 1; i >= 0; i--) {
|
||||
if (!classes[i]) {
|
||||
@@ -26,13 +28,46 @@ const createClass = function(classes) {
|
||||
return classes.join(" ");
|
||||
};
|
||||
|
||||
// To ensure that all nodes have compatible signatures for these methods.
|
||||
interface VirtualDomNode {
|
||||
toNode(): Node;
|
||||
toMarkup(): string;
|
||||
}
|
||||
|
||||
export interface CombinableDomNode extends VirtualDomNode {
|
||||
tryCombine(sibling: CombinableDomNode): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* All `DomChildNode`s MUST have `height`, `depth`, and `maxFontSize` numeric
|
||||
* fields.
|
||||
*
|
||||
* `DomChildNode` is not defined as an interface since `documentFragment` also
|
||||
* has these fields but should not be considered a `DomChildNode`.
|
||||
*/
|
||||
export type DomChildNode = span | anchor | svgNode | symbolNode;
|
||||
|
||||
export type SvgChildNode = pathNode | lineNode;
|
||||
|
||||
/**
|
||||
* This node represents a span node, with a className, a list of children, and
|
||||
* an inline style. It also contains information about its height, depth, and
|
||||
* maxFontSize.
|
||||
*/
|
||||
class span {
|
||||
constructor(classes, children, options) {
|
||||
class span implements CombinableDomNode {
|
||||
classes: string[];
|
||||
children: DomChildNode[];
|
||||
height: number;
|
||||
depth: number;
|
||||
maxFontSize: number;
|
||||
style: {[string]: string};
|
||||
attributes: {[string]: string};
|
||||
|
||||
constructor(
|
||||
classes?: string[],
|
||||
children?: DomChildNode[],
|
||||
options?: Options,
|
||||
) {
|
||||
this.classes = classes || [];
|
||||
this.children = children || [];
|
||||
this.height = 0;
|
||||
@@ -44,8 +79,9 @@ class span {
|
||||
if (options.style.isTight()) {
|
||||
this.classes.push("mtight");
|
||||
}
|
||||
if (options.getColor()) {
|
||||
this.style.color = options.getColor();
|
||||
const color = options.getColor();
|
||||
if (color) {
|
||||
this.style.color = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,18 +91,18 @@ class span {
|
||||
* browsers support attributes the same, and having too many custom attributes
|
||||
* is probably bad.
|
||||
*/
|
||||
setAttribute(attribute, value) {
|
||||
setAttribute(attribute: string, value: string) {
|
||||
this.attributes[attribute] = value;
|
||||
}
|
||||
|
||||
tryCombine(sibling) {
|
||||
tryCombine(sibling: CombinableDomNode): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the span into an HTML node
|
||||
*/
|
||||
toNode() {
|
||||
toNode(): HTMLSpanElement {
|
||||
const span = document.createElement("span");
|
||||
|
||||
// Apply the class
|
||||
@@ -75,6 +111,7 @@ class span {
|
||||
// Apply inline styles
|
||||
for (const style in this.style) {
|
||||
if (Object.prototype.hasOwnProperty.call(this.style, style)) {
|
||||
// $FlowFixMe Flow doesn't seem to understand span.style's type.
|
||||
span.style[style] = this.style[style];
|
||||
}
|
||||
}
|
||||
@@ -97,7 +134,7 @@ class span {
|
||||
/**
|
||||
* Convert the span into an HTML markup string
|
||||
*/
|
||||
toMarkup() {
|
||||
toMarkup(): string {
|
||||
let markup = "<span";
|
||||
|
||||
// Add the class
|
||||
@@ -147,23 +184,36 @@ class span {
|
||||
* a list of children, and an inline style. It also contains information about its
|
||||
* height, depth, and maxFontSize.
|
||||
*/
|
||||
class anchor {
|
||||
constructor(href, classes, children, options) {
|
||||
this.href = href || "";
|
||||
this.classes = classes || [];
|
||||
this.children = children || [];
|
||||
class anchor implements CombinableDomNode {
|
||||
href: string;
|
||||
classes: string[];
|
||||
children: DomChildNode[];
|
||||
height: number;
|
||||
depth: number;
|
||||
maxFontSize: number;
|
||||
style: {[string]: string};
|
||||
attributes: {[string]: string};
|
||||
|
||||
constructor(
|
||||
href: string,
|
||||
classes: string[],
|
||||
children: DomChildNode[],
|
||||
options: Options,
|
||||
) {
|
||||
this.href = href;
|
||||
this.classes = classes;
|
||||
this.children = children;
|
||||
this.height = 0;
|
||||
this.depth = 0;
|
||||
this.maxFontSize = 0;
|
||||
this.style = {};
|
||||
this.attributes = {};
|
||||
if (options) {
|
||||
if (options.style.isTight()) {
|
||||
this.classes.push("mtight");
|
||||
}
|
||||
if (options.getColor()) {
|
||||
this.style.color = options.getColor();
|
||||
}
|
||||
if (options.style.isTight()) {
|
||||
this.classes.push("mtight");
|
||||
}
|
||||
const color = options.getColor();
|
||||
if (color) {
|
||||
this.style.color = color;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,18 +222,18 @@ class anchor {
|
||||
* browsers support attributes the same, and having too many custom attributes
|
||||
* is probably bad.
|
||||
*/
|
||||
setAttribute(attribute, value) {
|
||||
setAttribute(attribute: string, value: string) {
|
||||
this.attributes[attribute] = value;
|
||||
}
|
||||
|
||||
tryCombine(sibling) {
|
||||
tryCombine(sibling: CombinableDomNode): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the anchor into an HTML node
|
||||
*/
|
||||
toNode() {
|
||||
toNode(): HTMLAnchorElement {
|
||||
const a = document.createElement("a");
|
||||
|
||||
// Apply the href
|
||||
@@ -197,6 +247,7 @@ class anchor {
|
||||
// Apply inline styles
|
||||
for (const style in this.style) {
|
||||
if (Object.prototype.hasOwnProperty.call(this.style, style)) {
|
||||
// $FlowFixMe Flow doesn't seem to understand a.style's type.
|
||||
a.style[style] = this.style[style];
|
||||
}
|
||||
}
|
||||
@@ -219,7 +270,7 @@ class anchor {
|
||||
/**
|
||||
* Convert the a into an HTML markup string
|
||||
*/
|
||||
toMarkup() {
|
||||
toMarkup(): string {
|
||||
let markup = "<a";
|
||||
|
||||
// Add the href
|
||||
@@ -269,8 +320,13 @@ class anchor {
|
||||
* contains children and doesn't have any HTML properties. It also keeps track
|
||||
* of a height, depth, and maxFontSize.
|
||||
*/
|
||||
class documentFragment {
|
||||
constructor(children) {
|
||||
class documentFragment implements VirtualDomNode {
|
||||
children: DomChildNode[];
|
||||
height: number;
|
||||
depth: number;
|
||||
maxFontSize: number;
|
||||
|
||||
constructor(children?: DomChildNode[]) {
|
||||
this.children = children || [];
|
||||
this.height = 0;
|
||||
this.depth = 0;
|
||||
@@ -280,7 +336,7 @@ class documentFragment {
|
||||
/**
|
||||
* Convert the fragment into a node
|
||||
*/
|
||||
toNode() {
|
||||
toNode(): Node {
|
||||
// Create a fragment
|
||||
const frag = document.createDocumentFragment();
|
||||
|
||||
@@ -295,7 +351,7 @@ class documentFragment {
|
||||
/**
|
||||
* Convert the fragment into HTML markup
|
||||
*/
|
||||
toMarkup() {
|
||||
toMarkup(): string {
|
||||
let markup = "";
|
||||
|
||||
// Simply concatenate the markup for the children together
|
||||
@@ -320,9 +376,26 @@ const iCombinations = {
|
||||
* to a single text node, or a span with a single text node in it, depending on
|
||||
* whether it has CSS classes, styles, or needs italic correction.
|
||||
*/
|
||||
class symbolNode {
|
||||
constructor(value, height, depth, italic, skew, classes, style) {
|
||||
this.value = value || "";
|
||||
class symbolNode implements CombinableDomNode {
|
||||
value: string;
|
||||
height: number;
|
||||
depth: number;
|
||||
italic: number;
|
||||
skew: number;
|
||||
maxFontSize: number;
|
||||
classes: string[];
|
||||
style: {[string]: string};
|
||||
|
||||
constructor(
|
||||
value: string,
|
||||
height?: number,
|
||||
depth?: number,
|
||||
italic?: number,
|
||||
skew?: number,
|
||||
classes?: string[],
|
||||
style?: {[string]: string},
|
||||
) {
|
||||
this.value = value;
|
||||
this.height = height || 0;
|
||||
this.depth = depth || 0;
|
||||
this.italic = italic || 0;
|
||||
@@ -335,11 +408,11 @@ class symbolNode {
|
||||
// fonts to use. This allows us to render these characters with a serif
|
||||
// font in situations where the browser would either default to a sans serif
|
||||
// or render a placeholder character.
|
||||
if (cjkRegex.test(value)) {
|
||||
if (cjkRegex.test(this.value)) {
|
||||
// I couldn't find any fonts that contained Hangul as well as all of
|
||||
// the other characters we wanted to test there for it gets its own
|
||||
// CSS class.
|
||||
if (hangulRegex.test(value)) {
|
||||
if (hangulRegex.test(this.value)) {
|
||||
this.classes.push('hangul_fallback');
|
||||
} else {
|
||||
this.classes.push('cjk_fallback');
|
||||
@@ -351,7 +424,7 @@ class symbolNode {
|
||||
}
|
||||
}
|
||||
|
||||
tryCombine(sibling) {
|
||||
tryCombine(sibling: CombinableDomNode): boolean {
|
||||
if (!sibling
|
||||
|| !(sibling instanceof symbolNode)
|
||||
|| this.italic > 0
|
||||
@@ -383,7 +456,7 @@ class symbolNode {
|
||||
* Creates a text node or span from a symbol node. Note that a span is only
|
||||
* created if it is needed.
|
||||
*/
|
||||
toNode() {
|
||||
toNode(): Node {
|
||||
const node = document.createTextNode(this.value);
|
||||
let span = null;
|
||||
|
||||
@@ -400,6 +473,7 @@ class symbolNode {
|
||||
for (const style in this.style) {
|
||||
if (this.style.hasOwnProperty(style)) {
|
||||
span = span || document.createElement("span");
|
||||
// $FlowFixMe Flow doesn't seem to understand span.style's type.
|
||||
span.style[style] = this.style[style];
|
||||
}
|
||||
}
|
||||
@@ -415,7 +489,7 @@ class symbolNode {
|
||||
/**
|
||||
* Creates markup for a symbol node.
|
||||
*/
|
||||
toMarkup() {
|
||||
toMarkup(): string {
|
||||
// TODO(alpert): More duplication than I'd like from
|
||||
// span.prototype.toMarkup and symbolNode.prototype.toNode...
|
||||
let needsSpan = false;
|
||||
@@ -460,20 +534,31 @@ class symbolNode {
|
||||
/**
|
||||
* SVG nodes are used to render stretchy wide elements.
|
||||
*/
|
||||
class svgNode {
|
||||
constructor(children, attributes) {
|
||||
class svgNode implements VirtualDomNode {
|
||||
children: SvgChildNode[];
|
||||
attributes: {[string]: string};
|
||||
// Required for all `DomChildNode`s. Are always 0 for svgNode.
|
||||
height: number;
|
||||
depth: number;
|
||||
maxFontSize: number;
|
||||
|
||||
constructor(children?: SvgChildNode[], attributes?: {[string]: string}) {
|
||||
this.children = children || [];
|
||||
this.attributes = attributes || [];
|
||||
this.attributes = attributes || {};
|
||||
this.height = 0;
|
||||
this.depth = 0;
|
||||
this.maxFontSize = 0;
|
||||
}
|
||||
|
||||
toNode() {
|
||||
toNode(): Node {
|
||||
const svgNS = "http://www.w3.org/2000/svg";
|
||||
const node = document.createElementNS(svgNS, "svg");
|
||||
|
||||
// Apply attributes
|
||||
for (let i = 0; i < this.attributes.length; i++) {
|
||||
const [name, value] = this.attributes[i];
|
||||
node.setAttribute(name, value);
|
||||
for (const attr in this.attributes) {
|
||||
if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) {
|
||||
node.setAttribute(attr, this.attributes[attr]);
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.children.length; i++) {
|
||||
@@ -482,13 +567,14 @@ class svgNode {
|
||||
return node;
|
||||
}
|
||||
|
||||
toMarkup() {
|
||||
toMarkup(): string {
|
||||
let markup = "<svg";
|
||||
|
||||
// Apply attributes
|
||||
for (let i = 0; i < this.attributes.length; i++) {
|
||||
const [name, value] = this.attributes[i];
|
||||
markup += ` ${name}='${value}'`;
|
||||
for (const attr in this.attributes) {
|
||||
if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) {
|
||||
markup += ` ${attr}='${this.attributes[attr]}'`;
|
||||
}
|
||||
}
|
||||
|
||||
markup += ">";
|
||||
@@ -504,58 +590,65 @@ class svgNode {
|
||||
}
|
||||
}
|
||||
|
||||
class pathNode {
|
||||
constructor(pathName, alternate) {
|
||||
class pathNode implements VirtualDomNode {
|
||||
pathName: string;
|
||||
alternate: ?string;
|
||||
|
||||
constructor(pathName: string, alternate?: string) {
|
||||
this.pathName = pathName;
|
||||
this.alternate = alternate; // Used only for tall \sqrt
|
||||
}
|
||||
|
||||
toNode() {
|
||||
toNode(): Node {
|
||||
const svgNS = "http://www.w3.org/2000/svg";
|
||||
const node = document.createElementNS(svgNS, "path");
|
||||
|
||||
if (this.pathName !== "sqrtTall") {
|
||||
node.setAttribute("d", svgGeometry.path[this.pathName]);
|
||||
} else {
|
||||
if (this.alternate) {
|
||||
node.setAttribute("d", this.alternate);
|
||||
} else {
|
||||
node.setAttribute("d", svgGeometry.path[this.pathName]);
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
toMarkup() {
|
||||
if (this.pathName !== "sqrtTall") {
|
||||
return `<path d='${svgGeometry.path[this.pathName]}'/>`;
|
||||
} else {
|
||||
toMarkup(): string {
|
||||
if (this.alternate) {
|
||||
return `<path d='${this.alternate}'/>`;
|
||||
} else {
|
||||
return `<path d='${svgGeometry.path[this.pathName]}'/>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class lineNode {
|
||||
constructor(attributes) {
|
||||
this.attributes = attributes || [];
|
||||
class lineNode implements VirtualDomNode {
|
||||
attributes: {[string]: string};
|
||||
|
||||
constructor(attributes?: {[string]: string}) {
|
||||
this.attributes = attributes || {};
|
||||
}
|
||||
|
||||
toNode() {
|
||||
toNode(): Node {
|
||||
const svgNS = "http://www.w3.org/2000/svg";
|
||||
const node = document.createElementNS(svgNS, "line");
|
||||
|
||||
// Apply attributes
|
||||
for (let i = 0; i < this.attributes.length; i++) {
|
||||
const [name, value] = this.attributes[i];
|
||||
node.setAttribute(name, value);
|
||||
for (const attr in this.attributes) {
|
||||
if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) {
|
||||
node.setAttribute(attr, this.attributes[attr]);
|
||||
}
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
toMarkup() {
|
||||
toMarkup(): string {
|
||||
let markup = "<line";
|
||||
|
||||
for (let i = 0; i < this.attributes.length; i++) {
|
||||
const [name, value] = this.attributes[i];
|
||||
markup += ` ${name}='${value}'`;
|
||||
for (const attr in this.attributes) {
|
||||
if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) {
|
||||
markup += ` ${attr}='${this.attributes[attr]}'`;
|
||||
}
|
||||
}
|
||||
|
||||
markup += "/>";
|
||||
@@ -565,11 +658,11 @@ class lineNode {
|
||||
}
|
||||
|
||||
export default {
|
||||
span: span,
|
||||
anchor: anchor,
|
||||
documentFragment: documentFragment,
|
||||
symbolNode: symbolNode,
|
||||
svgNode: svgNode,
|
||||
pathNode: pathNode,
|
||||
lineNode: lineNode,
|
||||
span,
|
||||
anchor,
|
||||
documentFragment,
|
||||
symbolNode,
|
||||
svgNode,
|
||||
pathNode,
|
||||
lineNode,
|
||||
};
|
||||
|
@@ -166,6 +166,7 @@ defineFunction({
|
||||
positionType: "individualShift",
|
||||
children: [
|
||||
{type: "elem", elem: denomm, shift: denomShift},
|
||||
// $FlowFixMe `rule` cannot be `null` here.
|
||||
{type: "elem", elem: rule, shift: midShift},
|
||||
{type: "elem", elem: numerm, shift: -numShift},
|
||||
],
|
||||
|
@@ -90,38 +90,58 @@ const htmlBuilder = (group, options) => {
|
||||
// in a new span so it is an inline, and works.
|
||||
base = buildCommon.makeSpan([], [base]);
|
||||
|
||||
let supm;
|
||||
let supKern;
|
||||
let subm = {height: 0, depth: 0}; // Make flow happy
|
||||
let subKern;
|
||||
let newOptions;
|
||||
let sub;
|
||||
let sup;
|
||||
// We manually have to handle the superscripts and subscripts. This,
|
||||
// aside from the kern calculations, is copied from supsub.
|
||||
if (supGroup) {
|
||||
newOptions = options.havingStyle(style.sup());
|
||||
supm = html.buildGroup(supGroup, newOptions, options);
|
||||
const elem = html.buildGroup(
|
||||
supGroup, options.havingStyle(style.sup()), options);
|
||||
|
||||
supKern = Math.max(
|
||||
options.fontMetrics().bigOpSpacing1,
|
||||
options.fontMetrics().bigOpSpacing3 - supm.depth);
|
||||
sup = {
|
||||
elem,
|
||||
kern: Math.max(
|
||||
options.fontMetrics().bigOpSpacing1,
|
||||
options.fontMetrics().bigOpSpacing3 - elem.depth),
|
||||
};
|
||||
}
|
||||
|
||||
if (subGroup) {
|
||||
newOptions = options.havingStyle(style.sub());
|
||||
subm = html.buildGroup(subGroup, newOptions, options);
|
||||
const elem = html.buildGroup(
|
||||
subGroup, options.havingStyle(style.sub()), options);
|
||||
|
||||
subKern = Math.max(
|
||||
options.fontMetrics().bigOpSpacing2,
|
||||
options.fontMetrics().bigOpSpacing4 - subm.height);
|
||||
sub = {
|
||||
elem,
|
||||
kern: Math.max(
|
||||
options.fontMetrics().bigOpSpacing2,
|
||||
options.fontMetrics().bigOpSpacing4 - elem.height),
|
||||
};
|
||||
}
|
||||
|
||||
// Build the final group as a vlist of the possible subscript, base,
|
||||
// and possible superscript.
|
||||
let finalGroup;
|
||||
let top;
|
||||
let bottom;
|
||||
if (!supGroup) {
|
||||
top = base.height - baseShift;
|
||||
if (sup && sub) {
|
||||
const bottom = options.fontMetrics().bigOpSpacing5 +
|
||||
sub.elem.height + sub.elem.depth +
|
||||
sub.kern +
|
||||
base.depth + baseShift;
|
||||
|
||||
finalGroup = buildCommon.makeVList({
|
||||
positionType: "bottom",
|
||||
positionData: bottom,
|
||||
children: [
|
||||
{type: "kern", size: options.fontMetrics().bigOpSpacing5},
|
||||
{type: "elem", elem: sub.elem, marginLeft: -slant + "em"},
|
||||
{type: "kern", size: sub.kern},
|
||||
{type: "elem", elem: base},
|
||||
{type: "kern", size: sup.kern},
|
||||
{type: "elem", elem: sup.elem, marginLeft: slant + "em"},
|
||||
{type: "kern", size: options.fontMetrics().bigOpSpacing5},
|
||||
],
|
||||
}, options);
|
||||
} else if (sub) {
|
||||
const top = base.height - baseShift;
|
||||
|
||||
// Shift the limits by the slant of the symbol. Note
|
||||
// that we are supposed to shift the limits by 1/2 of the slant,
|
||||
@@ -132,48 +152,29 @@ const htmlBuilder = (group, options) => {
|
||||
positionData: top,
|
||||
children: [
|
||||
{type: "kern", size: options.fontMetrics().bigOpSpacing5},
|
||||
{type: "elem", elem: subm, marginLeft: -slant + "em"},
|
||||
{type: "kern", size: subKern},
|
||||
{type: "elem", elem: sub.elem, marginLeft: -slant + "em"},
|
||||
{type: "kern", size: sub.kern},
|
||||
{type: "elem", elem: base},
|
||||
],
|
||||
}, options);
|
||||
} else if (!subGroup) {
|
||||
bottom = base.depth + baseShift;
|
||||
} else if (sup) {
|
||||
const bottom = base.depth + baseShift;
|
||||
|
||||
finalGroup = buildCommon.makeVList({
|
||||
positionType: "bottom",
|
||||
positionData: bottom,
|
||||
children: [
|
||||
{type: "elem", elem: base},
|
||||
{type: "kern", size: supKern},
|
||||
{type: "elem", elem: supm, marginLeft: slant + "em"},
|
||||
{type: "kern", size: sup.kern},
|
||||
{type: "elem", elem: sup.elem, marginLeft: slant + "em"},
|
||||
{type: "kern", size: options.fontMetrics().bigOpSpacing5},
|
||||
],
|
||||
}, options);
|
||||
} else if (!supGroup && !subGroup) {
|
||||
} else {
|
||||
// This case probably shouldn't occur (this would mean the
|
||||
// supsub was sending us a group with no superscript or
|
||||
// subscript) but be safe.
|
||||
return base;
|
||||
} else {
|
||||
bottom = options.fontMetrics().bigOpSpacing5 +
|
||||
subm.height + subm.depth +
|
||||
subKern +
|
||||
base.depth + baseShift;
|
||||
|
||||
finalGroup = buildCommon.makeVList({
|
||||
positionType: "bottom",
|
||||
positionData: bottom,
|
||||
children: [
|
||||
{type: "kern", size: options.fontMetrics().bigOpSpacing5},
|
||||
{type: "elem", elem: subm, marginLeft: -slant + "em"},
|
||||
{type: "kern", size: subKern},
|
||||
{type: "elem", elem: base},
|
||||
{type: "kern", size: supKern},
|
||||
{type: "elem", elem: supm, marginLeft: slant + "em"},
|
||||
{type: "kern", size: options.fontMetrics().bigOpSpacing5},
|
||||
],
|
||||
}, options);
|
||||
}
|
||||
|
||||
return buildCommon.makeSpan(
|
||||
|
236
src/stretchy.js
236
src/stretchy.js
@@ -12,7 +12,6 @@ import utils from "./utils";
|
||||
|
||||
import type Options from "./Options";
|
||||
import type ParseNode from "./ParseNode";
|
||||
import type {span} from "./domTree";
|
||||
|
||||
const stretchyCodePoint: {[string]: string} = {
|
||||
widehat: "^",
|
||||
@@ -154,98 +153,107 @@ const groupLength = function(arg: ParseNode): number {
|
||||
}
|
||||
};
|
||||
|
||||
const svgSpan = function(group: ParseNode, options: Options): span {
|
||||
const svgSpan = function(group: ParseNode, options: Options): domTree.span {
|
||||
// Create a span with inline SVG for the element.
|
||||
const label = group.value.label.substr(1);
|
||||
let attributes = [];
|
||||
let height;
|
||||
let viewBoxWidth = 400000; // default
|
||||
let viewBoxHeight = 0;
|
||||
let minWidth = 0;
|
||||
let path;
|
||||
let paths;
|
||||
let pathName;
|
||||
let svgNode;
|
||||
let span;
|
||||
function buildSvgSpan_(): {
|
||||
span: domTree.span,
|
||||
minWidth: number,
|
||||
height: number,
|
||||
} {
|
||||
let viewBoxWidth = 400000; // default
|
||||
const label = group.value.label.substr(1);
|
||||
if (utils.contains(["widehat", "widetilde", "utilde"], label)) {
|
||||
// There are four SVG images available for each function.
|
||||
// Choose a taller image when there are more characters.
|
||||
const numChars = groupLength(group.value.base);
|
||||
let viewBoxHeight;
|
||||
let pathName;
|
||||
let height;
|
||||
|
||||
if (utils.contains(["widehat", "widetilde", "utilde"], label)) {
|
||||
// There are four SVG images available for each function.
|
||||
// Choose a taller image when there are more characters.
|
||||
const numChars = groupLength(group.value.base);
|
||||
let viewBoxHeight;
|
||||
|
||||
if (numChars > 5) {
|
||||
viewBoxHeight = (label === "widehat" ? 420 : 312);
|
||||
viewBoxWidth = (label === "widehat" ? 2364 : 2340);
|
||||
// Next get the span height, in 1000 ems
|
||||
height = (label === "widehat" ? 0.42 : 0.34);
|
||||
pathName = (label === "widehat" ? "widehat" : "tilde") + "4";
|
||||
if (numChars > 5) {
|
||||
viewBoxHeight = (label === "widehat" ? 420 : 312);
|
||||
viewBoxWidth = (label === "widehat" ? 2364 : 2340);
|
||||
// Next get the span height, in 1000 ems
|
||||
height = (label === "widehat" ? 0.42 : 0.34);
|
||||
pathName = (label === "widehat" ? "widehat" : "tilde") + "4";
|
||||
} else {
|
||||
const imgIndex = [1, 1, 2, 2, 3, 3][numChars];
|
||||
if (label === "widehat") {
|
||||
viewBoxWidth = [0, 1062, 2364, 2364, 2364][imgIndex];
|
||||
viewBoxHeight = [0, 239, 300, 360, 420][imgIndex];
|
||||
height = [0, 0.24, 0.3, 0.3, 0.36, 0.42][imgIndex];
|
||||
pathName = "widehat" + imgIndex;
|
||||
} else {
|
||||
viewBoxWidth = [0, 600, 1033, 2339, 2340][imgIndex];
|
||||
viewBoxHeight = [0, 260, 286, 306, 312][imgIndex];
|
||||
height = [0, 0.26, 0.286, 0.3, 0.306, 0.34][imgIndex];
|
||||
pathName = "tilde" + imgIndex;
|
||||
}
|
||||
}
|
||||
const path = new domTree.pathNode(pathName);
|
||||
const svgNode = new domTree.svgNode([path], {
|
||||
"width": "100%",
|
||||
"height": height + "em",
|
||||
"viewBox": `0 0 ${viewBoxWidth} ${viewBoxHeight}`,
|
||||
"preserveAspectRatio": "none",
|
||||
});
|
||||
return {
|
||||
span: buildCommon.makeSpan([], [svgNode], options),
|
||||
minWidth: 0,
|
||||
height,
|
||||
};
|
||||
} else {
|
||||
const imgIndex = [1, 1, 2, 2, 3, 3][numChars];
|
||||
if (label === "widehat") {
|
||||
viewBoxWidth = [0, 1062, 2364, 2364, 2364][imgIndex];
|
||||
viewBoxHeight = [0, 239, 300, 360, 420][imgIndex];
|
||||
height = [0, 0.24, 0.3, 0.3, 0.36, 0.42][imgIndex];
|
||||
pathName = "widehat" + imgIndex;
|
||||
} else {
|
||||
viewBoxWidth = [0, 600, 1033, 2339, 2340][imgIndex];
|
||||
viewBoxHeight = [0, 260, 286, 306, 312][imgIndex];
|
||||
height = [0, 0.26, 0.286, 0.3, 0.306, 0.34][imgIndex];
|
||||
pathName = "tilde" + imgIndex;
|
||||
}
|
||||
}
|
||||
path = new domTree.pathNode(pathName);
|
||||
attributes.push(["width", "100%"]);
|
||||
attributes.push(["height", height + "em"]);
|
||||
attributes.push(["viewBox", `0 0 ${viewBoxWidth} ${viewBoxHeight}`]);
|
||||
attributes.push(["preserveAspectRatio", "none"]);
|
||||
const spans = [];
|
||||
|
||||
svgNode = new domTree.svgNode([path], attributes);
|
||||
span = buildCommon.makeSpan([], [svgNode], options);
|
||||
|
||||
} else {
|
||||
let widthClass;
|
||||
let align;
|
||||
const spans = [];
|
||||
|
||||
[paths, minWidth, viewBoxHeight, align] = katexImagesData[label];
|
||||
const numSvgChildren = paths.length;
|
||||
if (1 > numSvgChildren || numSvgChildren > 3) {
|
||||
throw new Error(
|
||||
`Correct katexImagesData or update code below to support
|
||||
${numSvgChildren} children.`);
|
||||
}
|
||||
height = viewBoxHeight / 1000;
|
||||
|
||||
for (let i = 0; i < numSvgChildren; i++) {
|
||||
path = new domTree.pathNode(paths[i]);
|
||||
|
||||
attributes = [["width", "400em"], ["height", height + "em"]];
|
||||
attributes.push(["viewBox", `0 0 ${viewBoxWidth} ${viewBoxHeight}`]);
|
||||
|
||||
if (numSvgChildren === 2) {
|
||||
widthClass = ["halfarrow-left", "halfarrow-right"][i];
|
||||
align = ["xMinYMin", "xMaxYMin"][i];
|
||||
} else if (numSvgChildren === 3) {
|
||||
widthClass = ["brace-left", "brace-center", "brace-right"][i];
|
||||
align = ["xMinYMin", "xMidYMin", "xMaxYMin"][i];
|
||||
}
|
||||
|
||||
attributes.push(["preserveAspectRatio", align + " slice"]);
|
||||
svgNode = new domTree.svgNode([path], attributes);
|
||||
const [paths, minWidth, viewBoxHeight, align1] = katexImagesData[label];
|
||||
const height = viewBoxHeight / 1000;
|
||||
|
||||
const numSvgChildren = paths.length;
|
||||
let widthClasses;
|
||||
let aligns;
|
||||
if (numSvgChildren === 1) {
|
||||
spans.push(buildCommon.makeSpan(["hide-tail"], [svgNode], options));
|
||||
widthClasses = ["hide-tail"];
|
||||
aligns = [align1];
|
||||
} else if (numSvgChildren === 2) {
|
||||
widthClasses = ["halfarrow-left", "halfarrow-right"];
|
||||
aligns = ["xMinYMin", "xMaxYMin"];
|
||||
} else if (numSvgChildren === 3) {
|
||||
widthClasses = ["brace-left", "brace-center", "brace-right"];
|
||||
aligns = ["xMinYMin", "xMidYMin", "xMaxYMin"];
|
||||
} else {
|
||||
const span = buildCommon.makeSpan([widthClass], [svgNode], options);
|
||||
span.style.height = height + "em";
|
||||
spans.push(span);
|
||||
throw new Error(
|
||||
`Correct katexImagesData or update code here to support
|
||||
${numSvgChildren} children.`);
|
||||
}
|
||||
}
|
||||
|
||||
span = numSvgChildren === 1 ? spans[0] :
|
||||
buildCommon.makeSpan(["stretchy"], spans, options);
|
||||
}
|
||||
for (let i = 0; i < numSvgChildren; i++) {
|
||||
const path = new domTree.pathNode(paths[i]);
|
||||
|
||||
const svgNode = new domTree.svgNode([path], {
|
||||
"width": "400em",
|
||||
"height": height + "em",
|
||||
"viewBox": `0 0 ${viewBoxWidth} ${viewBoxHeight}`,
|
||||
"preserveAspectRatio": aligns[i] + " slice",
|
||||
});
|
||||
|
||||
const span =
|
||||
buildCommon.makeSpan([widthClasses[i]], [svgNode], options);
|
||||
if (numSvgChildren === 1) {
|
||||
return {span, minWidth, height};
|
||||
} else {
|
||||
span.style.height = height + "em";
|
||||
spans.push(span);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
span: buildCommon.makeSpan(["stretchy"], spans, options),
|
||||
minWidth,
|
||||
height,
|
||||
};
|
||||
}
|
||||
} // buildSvgSpan_()
|
||||
const {span, minWidth, height} = buildSvgSpan_();
|
||||
|
||||
// Note that we are returning span.depth = 0.
|
||||
// Any adjustments relative to the baseline must be done in buildHTML.
|
||||
@@ -259,11 +267,11 @@ const svgSpan = function(group: ParseNode, options: Options): span {
|
||||
};
|
||||
|
||||
const encloseSpan = function(
|
||||
inner: span,
|
||||
inner: domTree.span,
|
||||
label: string,
|
||||
pad: number,
|
||||
options: Options,
|
||||
): span {
|
||||
): domTree.span {
|
||||
// Return an image span for \cancel, \bcancel, \xcancel, or \fbox
|
||||
let img;
|
||||
const totalHeight = inner.height + inner.depth + 2 * pad;
|
||||
@@ -271,8 +279,11 @@ const encloseSpan = function(
|
||||
if (/(fbox)|(color)/.test(label)) {
|
||||
img = buildCommon.makeSpan(["stretchy", label], [], options);
|
||||
|
||||
if (label === "fbox" && options.color) {
|
||||
img.style.borderColor = options.getColor();
|
||||
if (label === "fbox") {
|
||||
const color = options.color && options.getColor();
|
||||
if (color) {
|
||||
img.style.borderColor = color;
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
@@ -280,31 +291,33 @@ const encloseSpan = function(
|
||||
// Since \cancel's SVG is inline and it omits the viewBox attribute,
|
||||
// its stroke-width will not vary with span area.
|
||||
|
||||
let attributes = [["x1", "0"]];
|
||||
let attributes: {[string]: string} = {"x1": "0"};
|
||||
const lines = [];
|
||||
|
||||
if (label !== "cancel") {
|
||||
attributes.push(["y1", "0"]);
|
||||
attributes.push(["x2", "100%"]);
|
||||
attributes.push(["y2", "100%"]);
|
||||
attributes.push(["stroke-width", "0.046em"]);
|
||||
attributes["y1"] = "0";
|
||||
attributes["x2"] = "100%";
|
||||
attributes["y2"] = "100%";
|
||||
attributes["stroke-width"] = "0.046em";
|
||||
lines.push(new domTree.lineNode(attributes));
|
||||
}
|
||||
|
||||
if (label === "xcancel") {
|
||||
attributes = [["x1", "0"]]; // start a second line.
|
||||
attributes = {"x1": "0"}; // start a second line.
|
||||
}
|
||||
|
||||
if (label !== "bcancel") {
|
||||
attributes.push(["y1", "100%"]);
|
||||
attributes.push(["x2", "100%"]);
|
||||
attributes.push(["y2", "0"]);
|
||||
attributes.push(["stroke-width", "0.046em"]);
|
||||
attributes["y1"] = "100%";
|
||||
attributes["x2"] = "100%";
|
||||
attributes["y2"] = "0";
|
||||
attributes["stroke-width"] = "0.046em";
|
||||
lines.push(new domTree.lineNode(attributes));
|
||||
}
|
||||
|
||||
attributes = [["width", "100%"], ["height", totalHeight + "em"]];
|
||||
const svgNode = new domTree.svgNode(lines, attributes);
|
||||
const svgNode = new domTree.svgNode(lines, {
|
||||
"width": "100%",
|
||||
"height": totalHeight + "em",
|
||||
});
|
||||
|
||||
img = buildCommon.makeSpan([], [svgNode], options);
|
||||
}
|
||||
@@ -315,22 +328,21 @@ const encloseSpan = function(
|
||||
return img;
|
||||
};
|
||||
|
||||
const ruleSpan = function(className: string, options: Options): span {
|
||||
const ruleSpan = function(className: string, options: Options): domTree.span {
|
||||
// Get a big square image. The parent span will hide the overflow.
|
||||
const pathNode = new domTree.pathNode('bigRule');
|
||||
const attributes = [
|
||||
["width", "400em"],
|
||||
["height", "400em"],
|
||||
["viewBox", "0 0 400000 400000"],
|
||||
["preserveAspectRatio", "xMinYMin slice"],
|
||||
];
|
||||
const svg = new domTree.svgNode([pathNode], attributes);
|
||||
const svg = new domTree.svgNode([pathNode], {
|
||||
"width": "400em",
|
||||
"height": "400em",
|
||||
"viewBox": "0 0 400000 400000",
|
||||
"preserveAspectRatio": "xMinYMin slice",
|
||||
});
|
||||
return buildCommon.makeSpan([className, "hide-tail"], [svg], options);
|
||||
};
|
||||
|
||||
export default {
|
||||
encloseSpan: encloseSpan,
|
||||
mathMLnode: mathMLnode,
|
||||
ruleSpan: ruleSpan,
|
||||
svgSpan: svgSpan,
|
||||
encloseSpan,
|
||||
mathMLnode,
|
||||
ruleSpan,
|
||||
svgSpan,
|
||||
};
|
||||
|
Reference in New Issue
Block a user