diff --git a/src/buildCommon.js b/src/buildCommon.js index 2a6a142b..35c9719e 100644 --- a/src/buildCommon.js +++ b/src/buildCommon.js @@ -11,9 +11,11 @@ import symbols, {ligatures} from "./symbols"; import utils from "./utils"; import {wideCharacterFont} from "./wide-character"; import {calculateSize} from "./units"; +import * as tree from "./tree"; import type Options from "./Options"; import type ParseNode from "./ParseNode"; +import type {documentFragment as HtmlDocumentFragment} from "./domTree"; import type {NodeType} from "./ParseNode"; import type {CharacterMetrics} from "./fontMetrics"; import type {Mode} from "./types"; @@ -233,7 +235,7 @@ const makeOrd = function( group: ParseNode, options: Options, type: "mathord" | "textord", -): domTree.symbolNode | domTree.documentFragment { +): HtmlDocumentFragment | domTree.symbolNode { const mode = group.mode; const value = group.value; @@ -307,7 +309,7 @@ const tryCombineChars = function(chars: HtmlDomNode[]): HtmlDomNode[] { * children. */ const sizeElementFromChildren = function( - elem: DomSpan | domTree.anchor | domTree.documentFragment, + elem: DomSpan | domTree.anchor | HtmlDocumentFragment, ) { let height = 0; let depth = 0; @@ -394,8 +396,8 @@ const makeAnchor = function( */ const makeFragment = function( children: HtmlDomNode[], -): domTree.documentFragment { - const fragment = new domTree.documentFragment(children); +): HtmlDocumentFragment { + const fragment = new tree.documentFragment(children); sizeElementFromChildren(fragment); diff --git a/src/buildHTML.js b/src/buildHTML.js index 4bb6359a..30b5e202 100644 --- a/src/buildHTML.js +++ b/src/buildHTML.js @@ -14,6 +14,7 @@ import utils, {assert} from "./utils"; import {checkNodeType} from "./ParseNode"; import {spacings, tightSpacings} from "./spacingData"; import {_htmlGroupBuilders as groupBuilders} from "./defineFunction"; +import * as tree from "./tree"; import type Options from "./Options"; import type {AnyParseNode} from "./ParseNode"; @@ -90,7 +91,7 @@ export const buildExpression = function( const rawGroups: HtmlDomNode[] = []; for (let i = 0; i < expression.length; i++) { const output = buildGroup(expression[i], options); - if (output instanceof domTree.documentFragment) { + if (output instanceof tree.documentFragment) { const children: HtmlDomNode[] = output.children; rawGroups.push(...children); } else { @@ -203,7 +204,7 @@ const getOutermostNode = function( node: HtmlDomNode, side: Side, ): HtmlDomNode { - if (node instanceof domTree.documentFragment || + if (node instanceof tree.documentFragment || node instanceof domTree.anchor) { const children = node.children; if (children.length) { diff --git a/src/defineFunction.js b/src/defineFunction.js index 72be10e7..2aca6c1e 100644 --- a/src/defineFunction.js +++ b/src/defineFunction.js @@ -1,6 +1,5 @@ // @flow import {checkNodeType} from "./ParseNode"; -import domTree from "./domTree"; import type Parser from "./Parser"; 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 {HtmlDomNode} from "./domTree"; import type {Token} from "./Token"; -import type {MathNodeClass} from "./mathMLTree"; +import type {MathDomNode} from "./mathMLTree"; /** Context provided to function handlers for error messages. */ export type FunctionContext = {| @@ -28,7 +27,7 @@ export type HtmlBuilder = (ParseNode, Options) => HtmlDomNod export type MathMLBuilder = ( group: ParseNode, options: Options, -) => MathNodeClass | domTree.documentFragment; +) => MathDomNode; // More general version of `HtmlBuilder` for nodes (e.g. \sum, accent types) // whose presence impacts super/subscripting. In this case, ParseNode<"supsub"> @@ -119,8 +118,6 @@ type FunctionDefSpec = {| // This should not modify the `ParseNode`. htmlBuilder?: HtmlBuilder, - // 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 // created when rendering the defined LaTeX function. // This should not modify the `ParseNode`. diff --git a/src/domTree.js b/src/domTree.js index b67d3710..e56944fa 100644 --- a/src/domTree.js +++ b/src/domTree.js @@ -12,6 +12,10 @@ import {scriptFromCodepoint} from "./unicodeScripts"; import utils from "./utils"; import svgGeometry from "./svgGeometry"; 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 @@ -23,13 +27,7 @@ const createClass = function(classes: string[]): string { export type CssStyle = {[name: string]: string}; -// To ensure that all nodes have compatible signatures for these methods. -interface VirtualNodeInterface { - toNode(): Node; - toMarkup(): string; -} - -export interface HtmlDomNode extends VirtualNodeInterface { +export interface HtmlDomNode extends VirtualNode { classes: string[]; height: number; depth: number; @@ -46,9 +44,10 @@ export type DomSpan = span; export type SvgSpan = span; export type SvgChildNode = pathNode | lineNode; +export type documentFragment = tree.documentFragment; -export class HtmlDomContainer +export class HtmlDomContainer implements HtmlDomNode { children: ChildType[]; attributes: {[string]: string}; @@ -196,7 +195,7 @@ export class HtmlDomContainer * otherwise. This typesafety is important when HTML builders access a span's * children. */ -class span extends HtmlDomContainer { +class span extends HtmlDomContainer { constructor( classes?: string[], children?: ChildType[], @@ -234,67 +233,6 @@ class anchor extends HtmlDomContainer { } } -/** - * 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 = { 'î': '\u0131\u0302', 'ï': '\u0131\u0308', @@ -470,7 +408,7 @@ class symbolNode implements HtmlDomNode { /** * SVG nodes are used to render stretchy wide elements. */ -class svgNode implements VirtualNodeInterface { +class svgNode implements VirtualNode { children: SvgChildNode[]; attributes: {[string]: string}; @@ -519,7 +457,7 @@ class svgNode implements VirtualNodeInterface { } } -class pathNode implements VirtualNodeInterface { +class pathNode implements VirtualNode { pathName: string; alternate: ?string; @@ -550,7 +488,7 @@ class pathNode implements VirtualNodeInterface { } } -class lineNode implements VirtualNodeInterface { +class lineNode implements VirtualNode { attributes: {[string]: string}; constructor(attributes?: {[string]: string}) { @@ -609,7 +547,6 @@ export function assertDomContainer( export default { span, anchor, - documentFragment, symbolNode, svgNode, pathNode, diff --git a/src/functions/mclass.js b/src/functions/mclass.js index 0aeb62ea..82e4030d 100644 --- a/src/functions/mclass.js +++ b/src/functions/mclass.js @@ -1,7 +1,7 @@ // @flow import defineFunction, {ordargument} from "../defineFunction"; import buildCommon from "../buildCommon"; -import domTree from "../domTree"; +import mathMLTree from "../mathMLTree"; import ParseNode from "../ParseNode"; import * as html from "../buildHTML"; @@ -16,7 +16,7 @@ function htmlBuilder(group, options) { function mathmlBuilder(group, options) { const inner = mml.buildExpression(group.value.value, options); - return new domTree.documentFragment(inner); + return mathMLTree.newDocumentFragment(inner); } // Math class commands except \mathop diff --git a/src/functions/op.js b/src/functions/op.js index 71822420..63fe1ae2 100644 --- a/src/functions/op.js +++ b/src/functions/op.js @@ -3,7 +3,7 @@ import defineFunction, {ordargument} from "../defineFunction"; import buildCommon from "../buildCommon"; import domTree from "../domTree"; -import mathMLTree from "../mathMLTree"; +import * as mathMLTree from "../mathMLTree"; import utils from "../utils"; import Style from "../Style"; import ParseNode, {assertNodeType, checkNodeType} from "../ParseNode"; @@ -258,11 +258,7 @@ const mathmlBuilder: MathMLBuilder<"op"> = (group, options) => { const operator = new mathMLTree.MathNode("mo", [mml.makeText("\u2061", "text")]); - // TODO: Refactor to not return an HTML DOM object from MathML builder - // 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 mathMLTree.newDocumentFragment([node, operator]); } return node; diff --git a/src/functions/operatorname.js b/src/functions/operatorname.js index 0f2f7ca4..c961cccb 100644 --- a/src/functions/operatorname.js +++ b/src/functions/operatorname.js @@ -90,6 +90,6 @@ defineFunction({ const operator = new mathMLTree.MathNode("mo", [mml.makeText("\u2061", "text")]); - return new domTree.documentFragment([identifier, operator]); + return mathMLTree.newDocumentFragment([identifier, operator]); }, }); diff --git a/src/functions/sqrt.js b/src/functions/sqrt.js index dcd43868..8706f8da 100644 --- a/src/functions/sqrt.js +++ b/src/functions/sqrt.js @@ -1,12 +1,12 @@ // @flow import defineFunction from "../defineFunction"; import buildCommon from "../buildCommon"; -import domTree from "../domTree"; import mathMLTree from "../mathMLTree"; import delimiter from "../delimiter"; import Style from "../Style"; import ParseNode from "../ParseNode"; +import * as tree from "../tree"; import * as html from "../buildHTML"; import * as mml from "../buildMathML"; @@ -39,7 +39,7 @@ defineFunction({ // Some groups can return document fragments. Handle those by wrapping // them in a span. - if (inner instanceof domTree.documentFragment) { + if (inner instanceof tree.documentFragment) { inner = buildCommon.makeSpan([], [inner], options); } diff --git a/src/functions/symbolsOrd.js b/src/functions/symbolsOrd.js index a818e4bf..3ff45867 100644 --- a/src/functions/symbolsOrd.js +++ b/src/functions/symbolsOrd.js @@ -8,7 +8,7 @@ import * as mml from "../buildMathML"; // "mathord" and "textord" ParseNodes created in Parser.js from symbol Groups in // src/symbols.js. -const defaultVariant = { +const defaultVariant: {[string]: string} = { "mi": "italic", "mn": "normal", "mtext": "normal", diff --git a/src/mathMLTree.js b/src/mathMLTree.js index b67bee7f..1f31e515 100644 --- a/src/mathMLTree.js +++ b/src/mathMLTree.js @@ -10,6 +10,9 @@ */ 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 @@ -24,19 +27,26 @@ export type MathNodeType = "mrow" | "menclose" | "mstyle" | "mpadded" | "mphantom"; -export type MathNodeClass = MathNode | TextNode | SpaceNode; +export interface MathDomNode extends VirtualNode { + toText(): string; +} + +export type documentFragment = tree.documentFragment; +export function newDocumentFragment(children: MathDomNode[]): documentFragment { + return new tree.documentFragment(children); +} /** * This node represents a general purpose MathML node of any type. The * constructor requires the type of node to create (for example, `"mo"` or * `"mspace"`, corresponding to `` and `` tags). */ -export class MathNode { +export class MathNode implements MathDomNode { type: MathNodeType; attributes: {[string]: string}; - children: (MathNode | TextNode)[]; + children: MathDomNode[]; - constructor(type: MathNodeType, children?: (MathNode | TextNode)[]) { + constructor(type: MathNodeType, children?: MathDomNode[]) { this.type = type; this.attributes = {}; this.children = children || []; @@ -114,7 +124,7 @@ export class MathNode { /** * This node represents a piece of text. */ -export class TextNode { +export class TextNode implements MathDomNode { text: string; needsEscape: boolean; @@ -151,7 +161,7 @@ export class TextNode { * This node represents a space, but may render as or as text, * depending on the width. */ -class SpaceNode { +class SpaceNode implements MathDomNode { width: number; character: ?string; @@ -226,4 +236,5 @@ export default { MathNode, TextNode, SpaceNode, + newDocumentFragment, }; diff --git a/src/tree.js b/src/tree.js new file mode 100644 index 00000000..221ed8d1 --- /dev/null +++ b/src/tree.js @@ -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 + 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(""); + } +}