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:
Ashish Myles
2017-11-24 14:31:49 -05:00
committed by GitHub
parent a02859033a
commit c8249c389f
6 changed files with 537 additions and 339 deletions

View File

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

View File

@@ -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);
};

View File

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

View File

@@ -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},
],

View File

@@ -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(

View File

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