Refactor documentFragment and implement both HtmlDomNode and MathDomNode interfaces (stepping stone to port buildMathML to flow) (#1478)

* Make MathNodeClass include documentFragment for ergonomics.

* Separate out the HTML and MathML documentFragments.

These two documentFragments have different additional properties/methodsi
and limitations. This separation is needed for porting buildMathML to
flow.

* Coalesce the documentFragment subclasses to avoid subclassing polyfill.

* Make DomSpan and SvgSpan type aliases again instead of subclasses.

* Remove type MathNodeClass in favor of MathDomNode.

* Resolve $FlowFixMes by reordering variants of a union type.
This commit is contained in:
Ashish Myles
2018-07-16 02:41:27 -04:00
committed by ylemkimon
parent 48e9058a06
commit f0976ade26
11 changed files with 129 additions and 103 deletions

View File

@@ -11,9 +11,11 @@ import symbols, {ligatures} from "./symbols";
import utils from "./utils"; import utils from "./utils";
import {wideCharacterFont} from "./wide-character"; import {wideCharacterFont} from "./wide-character";
import {calculateSize} from "./units"; import {calculateSize} from "./units";
import * as tree from "./tree";
import type Options from "./Options"; import type Options from "./Options";
import type ParseNode from "./ParseNode"; import type ParseNode from "./ParseNode";
import type {documentFragment as HtmlDocumentFragment} from "./domTree";
import type {NodeType} from "./ParseNode"; import type {NodeType} from "./ParseNode";
import type {CharacterMetrics} from "./fontMetrics"; import type {CharacterMetrics} from "./fontMetrics";
import type {Mode} from "./types"; import type {Mode} from "./types";
@@ -233,7 +235,7 @@ const makeOrd = function<NODETYPE: "spacing" | "mathord" | "textord">(
group: ParseNode<NODETYPE>, group: ParseNode<NODETYPE>,
options: Options, options: Options,
type: "mathord" | "textord", type: "mathord" | "textord",
): domTree.symbolNode | domTree.documentFragment { ): HtmlDocumentFragment | domTree.symbolNode {
const mode = group.mode; const mode = group.mode;
const value = group.value; const value = group.value;
@@ -307,7 +309,7 @@ const tryCombineChars = function(chars: HtmlDomNode[]): HtmlDomNode[] {
* children. * children.
*/ */
const sizeElementFromChildren = function( const sizeElementFromChildren = function(
elem: DomSpan | domTree.anchor | domTree.documentFragment, elem: DomSpan | domTree.anchor | HtmlDocumentFragment,
) { ) {
let height = 0; let height = 0;
let depth = 0; let depth = 0;
@@ -394,8 +396,8 @@ const makeAnchor = function(
*/ */
const makeFragment = function( const makeFragment = function(
children: HtmlDomNode[], children: HtmlDomNode[],
): domTree.documentFragment { ): HtmlDocumentFragment {
const fragment = new domTree.documentFragment(children); const fragment = new tree.documentFragment(children);
sizeElementFromChildren(fragment); sizeElementFromChildren(fragment);

View File

@@ -14,6 +14,7 @@ import utils, {assert} from "./utils";
import {checkNodeType} from "./ParseNode"; import {checkNodeType} from "./ParseNode";
import {spacings, tightSpacings} from "./spacingData"; import {spacings, tightSpacings} from "./spacingData";
import {_htmlGroupBuilders as groupBuilders} from "./defineFunction"; import {_htmlGroupBuilders as groupBuilders} from "./defineFunction";
import * as tree from "./tree";
import type Options from "./Options"; import type Options from "./Options";
import type {AnyParseNode} from "./ParseNode"; import type {AnyParseNode} from "./ParseNode";
@@ -90,7 +91,7 @@ export const buildExpression = function(
const rawGroups: HtmlDomNode[] = []; const rawGroups: HtmlDomNode[] = [];
for (let i = 0; i < expression.length; i++) { for (let i = 0; i < expression.length; i++) {
const output = buildGroup(expression[i], options); const output = buildGroup(expression[i], options);
if (output instanceof domTree.documentFragment) { if (output instanceof tree.documentFragment) {
const children: HtmlDomNode[] = output.children; const children: HtmlDomNode[] = output.children;
rawGroups.push(...children); rawGroups.push(...children);
} else { } else {
@@ -203,7 +204,7 @@ const getOutermostNode = function(
node: HtmlDomNode, node: HtmlDomNode,
side: Side, side: Side,
): HtmlDomNode { ): HtmlDomNode {
if (node instanceof domTree.documentFragment || if (node instanceof tree.documentFragment ||
node instanceof domTree.anchor) { node instanceof domTree.anchor) {
const children = node.children; const children = node.children;
if (children.length) { if (children.length) {

View File

@@ -1,6 +1,5 @@
// @flow // @flow
import {checkNodeType} from "./ParseNode"; import {checkNodeType} from "./ParseNode";
import domTree from "./domTree";
import type Parser from "./Parser"; import type Parser from "./Parser";
import type ParseNode, {AnyParseNode, NodeType} from "./ParseNode"; import type ParseNode, {AnyParseNode, NodeType} from "./ParseNode";
@@ -8,7 +7,7 @@ import type Options from "./Options";
import type {ArgType, BreakToken, Mode} from "./types"; import type {ArgType, BreakToken, Mode} from "./types";
import type {HtmlDomNode} from "./domTree"; import type {HtmlDomNode} from "./domTree";
import type {Token} from "./Token"; import type {Token} from "./Token";
import type {MathNodeClass} from "./mathMLTree"; import type {MathDomNode} from "./mathMLTree";
/** Context provided to function handlers for error messages. */ /** Context provided to function handlers for error messages. */
export type FunctionContext = {| export type FunctionContext = {|
@@ -28,7 +27,7 @@ export type HtmlBuilder<NODETYPE> = (ParseNode<NODETYPE>, Options) => HtmlDomNod
export type MathMLBuilder<NODETYPE> = ( export type MathMLBuilder<NODETYPE> = (
group: ParseNode<NODETYPE>, group: ParseNode<NODETYPE>,
options: Options, options: Options,
) => MathNodeClass | domTree.documentFragment; ) => MathDomNode;
// More general version of `HtmlBuilder` for nodes (e.g. \sum, accent types) // More general version of `HtmlBuilder` for nodes (e.g. \sum, accent types)
// whose presence impacts super/subscripting. In this case, ParseNode<"supsub"> // whose presence impacts super/subscripting. In this case, ParseNode<"supsub">
@@ -119,8 +118,6 @@ type FunctionDefSpec<NODETYPE: NodeType> = {|
// This should not modify the `ParseNode`. // This should not modify the `ParseNode`.
htmlBuilder?: HtmlBuilder<NODETYPE>, htmlBuilder?: HtmlBuilder<NODETYPE>,
// TODO: Currently functions/op.js returns documentFragment. Refactor it
// and update the return type of this function.
// This function returns an object representing the MathML structure to be // This function returns an object representing the MathML structure to be
// created when rendering the defined LaTeX function. // created when rendering the defined LaTeX function.
// This should not modify the `ParseNode`. // This should not modify the `ParseNode`.

View File

@@ -12,6 +12,10 @@ import {scriptFromCodepoint} from "./unicodeScripts";
import utils from "./utils"; import utils from "./utils";
import svgGeometry from "./svgGeometry"; import svgGeometry from "./svgGeometry";
import type Options from "./Options"; import type Options from "./Options";
import * as tree from "./tree";
import type {VirtualNode} from "./tree";
/** /**
* Create an HTML className based on a list of classes. In addition to joining * Create an HTML className based on a list of classes. In addition to joining
@@ -23,13 +27,7 @@ const createClass = function(classes: string[]): string {
export type CssStyle = {[name: string]: string}; export type CssStyle = {[name: string]: string};
// To ensure that all nodes have compatible signatures for these methods. export interface HtmlDomNode extends VirtualNode {
interface VirtualNodeInterface {
toNode(): Node;
toMarkup(): string;
}
export interface HtmlDomNode extends VirtualNodeInterface {
classes: string[]; classes: string[];
height: number; height: number;
depth: number; depth: number;
@@ -46,9 +44,10 @@ export type DomSpan = span<HtmlDomNode>;
export type SvgSpan = span<svgNode>; export type SvgSpan = span<svgNode>;
export type SvgChildNode = pathNode | lineNode; export type SvgChildNode = pathNode | lineNode;
export type documentFragment = tree.documentFragment<HtmlDomNode>;
export class HtmlDomContainer<ChildType: VirtualNodeInterface> export class HtmlDomContainer<ChildType: VirtualNode>
implements HtmlDomNode { implements HtmlDomNode {
children: ChildType[]; children: ChildType[];
attributes: {[string]: string}; attributes: {[string]: string};
@@ -196,7 +195,7 @@ export class HtmlDomContainer<ChildType: VirtualNodeInterface>
* otherwise. This typesafety is important when HTML builders access a span's * otherwise. This typesafety is important when HTML builders access a span's
* children. * children.
*/ */
class span<ChildType: VirtualNodeInterface> extends HtmlDomContainer<ChildType> { class span<ChildType: VirtualNode> extends HtmlDomContainer<ChildType> {
constructor( constructor(
classes?: string[], classes?: string[],
children?: ChildType[], children?: ChildType[],
@@ -234,67 +233,6 @@ class anchor extends HtmlDomContainer<HtmlDomNode> {
} }
} }
/**
* This node represents a document fragment, which contains elements, but when
* placed into the DOM doesn't have any representation itself. Thus, it only
* contains children and doesn't have any HTML properties. It also keeps track
* of a height, depth, and maxFontSize.
*/
class documentFragment implements HtmlDomNode {
children: HtmlDomNode[];
classes: string[]; // Never used; needed for satisfying interface.
height: number;
depth: number;
maxFontSize: number;
style: CssStyle; // Never used; needed for satisfying interface.
constructor(children?: HtmlDomNode[]) {
this.children = children || [];
this.classes = [];
this.height = 0;
this.depth = 0;
this.maxFontSize = 0;
this.style = {};
}
hasClass(className: string): boolean {
return utils.contains(this.classes, className);
}
tryCombine(sibling: HtmlDomNode): boolean {
return false;
}
/**
* Convert the fragment into a node
*/
toNode(): Node {
// Create a fragment
const frag = document.createDocumentFragment();
// Append the children
for (let i = 0; i < this.children.length; i++) {
frag.appendChild(this.children[i].toNode());
}
return frag;
}
/**
* Convert the fragment into HTML markup
*/
toMarkup(): string {
let markup = "";
// Simply concatenate the markup for the children together
for (let i = 0; i < this.children.length; i++) {
markup += this.children[i].toMarkup();
}
return markup;
}
}
const iCombinations = { const iCombinations = {
'î': '\u0131\u0302', 'î': '\u0131\u0302',
'ï': '\u0131\u0308', 'ï': '\u0131\u0308',
@@ -470,7 +408,7 @@ class symbolNode implements HtmlDomNode {
/** /**
* SVG nodes are used to render stretchy wide elements. * SVG nodes are used to render stretchy wide elements.
*/ */
class svgNode implements VirtualNodeInterface { class svgNode implements VirtualNode {
children: SvgChildNode[]; children: SvgChildNode[];
attributes: {[string]: string}; attributes: {[string]: string};
@@ -519,7 +457,7 @@ class svgNode implements VirtualNodeInterface {
} }
} }
class pathNode implements VirtualNodeInterface { class pathNode implements VirtualNode {
pathName: string; pathName: string;
alternate: ?string; alternate: ?string;
@@ -550,7 +488,7 @@ class pathNode implements VirtualNodeInterface {
} }
} }
class lineNode implements VirtualNodeInterface { class lineNode implements VirtualNode {
attributes: {[string]: string}; attributes: {[string]: string};
constructor(attributes?: {[string]: string}) { constructor(attributes?: {[string]: string}) {
@@ -609,7 +547,6 @@ export function assertDomContainer(
export default { export default {
span, span,
anchor, anchor,
documentFragment,
symbolNode, symbolNode,
svgNode, svgNode,
pathNode, pathNode,

View File

@@ -1,7 +1,7 @@
// @flow // @flow
import defineFunction, {ordargument} from "../defineFunction"; import defineFunction, {ordargument} from "../defineFunction";
import buildCommon from "../buildCommon"; import buildCommon from "../buildCommon";
import domTree from "../domTree"; import mathMLTree from "../mathMLTree";
import ParseNode from "../ParseNode"; import ParseNode from "../ParseNode";
import * as html from "../buildHTML"; import * as html from "../buildHTML";
@@ -16,7 +16,7 @@ function htmlBuilder(group, options) {
function mathmlBuilder(group, options) { function mathmlBuilder(group, options) {
const inner = mml.buildExpression(group.value.value, options); const inner = mml.buildExpression(group.value.value, options);
return new domTree.documentFragment(inner); return mathMLTree.newDocumentFragment(inner);
} }
// Math class commands except \mathop // Math class commands except \mathop

View File

@@ -3,7 +3,7 @@
import defineFunction, {ordargument} from "../defineFunction"; import defineFunction, {ordargument} from "../defineFunction";
import buildCommon from "../buildCommon"; import buildCommon from "../buildCommon";
import domTree from "../domTree"; import domTree from "../domTree";
import mathMLTree from "../mathMLTree"; import * as mathMLTree from "../mathMLTree";
import utils from "../utils"; import utils from "../utils";
import Style from "../Style"; import Style from "../Style";
import ParseNode, {assertNodeType, checkNodeType} from "../ParseNode"; import ParseNode, {assertNodeType, checkNodeType} from "../ParseNode";
@@ -258,11 +258,7 @@ const mathmlBuilder: MathMLBuilder<"op"> = (group, options) => {
const operator = new mathMLTree.MathNode("mo", const operator = new mathMLTree.MathNode("mo",
[mml.makeText("\u2061", "text")]); [mml.makeText("\u2061", "text")]);
// TODO: Refactor to not return an HTML DOM object from MathML builder return mathMLTree.newDocumentFragment([node, operator]);
// or refactor documentFragment to be standalone and explicitly reusable
// for both HTML and MathML DOM operations. In either case, update the
// return type of `mathBuilder` in `defineFunction` to accommodate.
return new domTree.documentFragment([node, operator]);
} }
return node; return node;

View File

@@ -90,6 +90,6 @@ defineFunction({
const operator = new mathMLTree.MathNode("mo", const operator = new mathMLTree.MathNode("mo",
[mml.makeText("\u2061", "text")]); [mml.makeText("\u2061", "text")]);
return new domTree.documentFragment([identifier, operator]); return mathMLTree.newDocumentFragment([identifier, operator]);
}, },
}); });

View File

@@ -1,12 +1,12 @@
// @flow // @flow
import defineFunction from "../defineFunction"; import defineFunction from "../defineFunction";
import buildCommon from "../buildCommon"; import buildCommon from "../buildCommon";
import domTree from "../domTree";
import mathMLTree from "../mathMLTree"; import mathMLTree from "../mathMLTree";
import delimiter from "../delimiter"; import delimiter from "../delimiter";
import Style from "../Style"; import Style from "../Style";
import ParseNode from "../ParseNode"; import ParseNode from "../ParseNode";
import * as tree from "../tree";
import * as html from "../buildHTML"; import * as html from "../buildHTML";
import * as mml from "../buildMathML"; import * as mml from "../buildMathML";
@@ -39,7 +39,7 @@ defineFunction({
// Some groups can return document fragments. Handle those by wrapping // Some groups can return document fragments. Handle those by wrapping
// them in a span. // them in a span.
if (inner instanceof domTree.documentFragment) { if (inner instanceof tree.documentFragment) {
inner = buildCommon.makeSpan([], [inner], options); inner = buildCommon.makeSpan([], [inner], options);
} }

View File

@@ -8,7 +8,7 @@ import * as mml from "../buildMathML";
// "mathord" and "textord" ParseNodes created in Parser.js from symbol Groups in // "mathord" and "textord" ParseNodes created in Parser.js from symbol Groups in
// src/symbols.js. // src/symbols.js.
const defaultVariant = { const defaultVariant: {[string]: string} = {
"mi": "italic", "mi": "italic",
"mn": "normal", "mn": "normal",
"mtext": "normal", "mtext": "normal",

View File

@@ -10,6 +10,9 @@
*/ */
import utils from "./utils"; import utils from "./utils";
import * as tree from "./tree";
import type {VirtualNode} from "./tree";
/** /**
* MathML node types used in KaTeX. For a complete list of MathML nodes, see * MathML node types used in KaTeX. For a complete list of MathML nodes, see
@@ -24,19 +27,26 @@ export type MathNodeType =
"mrow" | "menclose" | "mrow" | "menclose" |
"mstyle" | "mpadded" | "mphantom"; "mstyle" | "mpadded" | "mphantom";
export type MathNodeClass = MathNode | TextNode | SpaceNode; export interface MathDomNode extends VirtualNode {
toText(): string;
}
export type documentFragment = tree.documentFragment<MathDomNode>;
export function newDocumentFragment(children: MathDomNode[]): documentFragment {
return new tree.documentFragment(children);
}
/** /**
* This node represents a general purpose MathML node of any type. The * This node represents a general purpose MathML node of any type. The
* constructor requires the type of node to create (for example, `"mo"` or * constructor requires the type of node to create (for example, `"mo"` or
* `"mspace"`, corresponding to `<mo>` and `<mspace>` tags). * `"mspace"`, corresponding to `<mo>` and `<mspace>` tags).
*/ */
export class MathNode { export class MathNode implements MathDomNode {
type: MathNodeType; type: MathNodeType;
attributes: {[string]: string}; attributes: {[string]: string};
children: (MathNode | TextNode)[]; children: MathDomNode[];
constructor(type: MathNodeType, children?: (MathNode | TextNode)[]) { constructor(type: MathNodeType, children?: MathDomNode[]) {
this.type = type; this.type = type;
this.attributes = {}; this.attributes = {};
this.children = children || []; this.children = children || [];
@@ -114,7 +124,7 @@ export class MathNode {
/** /**
* This node represents a piece of text. * This node represents a piece of text.
*/ */
export class TextNode { export class TextNode implements MathDomNode {
text: string; text: string;
needsEscape: boolean; needsEscape: boolean;
@@ -151,7 +161,7 @@ export class TextNode {
* This node represents a space, but may render as <mspace.../> or as text, * This node represents a space, but may render as <mspace.../> or as text,
* depending on the width. * depending on the width.
*/ */
class SpaceNode { class SpaceNode implements MathDomNode {
width: number; width: number;
character: ?string; character: ?string;
@@ -226,4 +236,5 @@ export default {
MathNode, MathNode,
TextNode, TextNode,
SpaceNode, SpaceNode,
newDocumentFragment,
}; };

82
src/tree.js Normal file
View File

@@ -0,0 +1,82 @@
// @flow
import utils from "./utils";
import type {CssStyle, HtmlDomNode} from "./domTree";
import type {MathDomNode} from "./mathMLTree";
// To ensure that all nodes have compatible signatures for these methods.
export interface VirtualNode {
toNode(): Node;
toMarkup(): string;
}
/**
* This node represents a document fragment, which contains elements, but when
* placed into the DOM doesn't have any representation itself. It only contains
* children and doesn't have any DOM node properties.
*/
export class documentFragment<ChildType: VirtualNode>
implements HtmlDomNode, MathDomNode {
children: ChildType[];
// HtmlDomNode
classes: string[];
height: number;
depth: number;
maxFontSize: number;
style: CssStyle; // Never used; needed for satisfying interface.
constructor(children: ChildType[]) {
this.children = children;
this.classes = [];
this.height = 0;
this.depth = 0;
this.maxFontSize = 0;
this.style = {};
}
hasClass(className: string): boolean {
return utils.contains(this.classes, className);
}
tryCombine(sibling: HtmlDomNode): boolean {
return false;
}
/** Convert the fragment into a node. */
toNode(): Node {
const frag = document.createDocumentFragment();
for (let i = 0; i < this.children.length; i++) {
frag.appendChild(this.children[i].toNode());
}
return frag;
}
/** Convert the fragment into HTML markup. */
toMarkup(): string {
let markup = "";
// Simply concatenate the markup for the children together.
for (let i = 0; i < this.children.length; i++) {
markup += this.children[i].toMarkup();
}
return markup;
}
/**
* Converts the math node into a string, similar to innerText. Applies to
* MathDomNode's only.
*/
toText(): string {
// To avoid this, we would subclass documentFragment separately for
// MathML, but polyfills for subclassing is expensive per PR 1469.
// $FlowFixMe: Only works for ChildType = MathDomNode.
const toText = (child: ChildType): string => child.toText();
return this.children.map(toText).join("");
}
}