// @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 * `toNode` function or HTML markup using `toMarkup`. They are useful for both * storing extra properties on the nodes, as well as providing a way to easily * work with the DOM. * * Similar functions for working with MathML nodes exist in mathMLTree.js. * * TODO: refactor `span` and `anchor` into common superclass when * target environments support class inheritance */ import {scriptFromCodepoint} from "./unicodeScripts"; import utils from "./utils"; import {path} from "./svgGeometry"; import type Options from "./Options"; import {DocumentFragment} from "./tree"; import {makeEm} from "./units"; import ParseError from "./ParseError"; import type {VirtualNode} from "./tree"; /** * Create an HTML className based on a list of classes. In addition to joining * with spaces, we also remove empty classes. */ export const createClass = function(classes: string[]): string { return classes.filter(cls => cls).join(" "); }; const initNode = function( classes?: string[], options?: Options, style?: CssStyle, ) { this.classes = classes || []; this.attributes = {}; this.height = 0; this.depth = 0; this.maxFontSize = 0; this.style = style || {}; if (options) { if (options.style.isTight()) { this.classes.push("mtight"); } const color = options.getColor(); if (color) { this.style.color = color; } } }; /** * Convert into an HTML node */ const toNode = function(tagName: string): HTMLElement { const node = document.createElement(tagName); // Apply the class node.className = createClass(this.classes); // Apply inline styles for (const style in this.style) { if (this.style.hasOwnProperty(style)) { // $FlowFixMe Flow doesn't seem to understand span.style's type. node.style[style] = this.style[style]; } } // Apply attributes for (const attr in this.attributes) { if (this.attributes.hasOwnProperty(attr)) { node.setAttribute(attr, this.attributes[attr]); } } // Append the children, also as HTML nodes for (let i = 0; i < this.children.length; i++) { node.appendChild(this.children[i].toNode()); } return node; }; /** * https://w3c.github.io/html-reference/syntax.html#syntax-attributes * * > Attribute Names must consist of one or more characters * other than the space characters, U+0000 NULL, * '"', "'", ">", "/", "=", the control characters, * and any characters that are not defined by Unicode. */ const invalidAttributeNameRegex = /[\s"'>/=\x00-\x1f]/; /** * Convert into an HTML markup string */ const toMarkup = function(tagName: string): string { let markup = `<${tagName}`; // Add the class if (this.classes.length) { markup += ` class="${utils.escape(createClass(this.classes))}"`; } let styles = ""; // Add the styles, after hyphenation for (const style in this.style) { if (this.style.hasOwnProperty(style)) { styles += `${utils.hyphenate(style)}:${this.style[style]};`; } } if (styles) { markup += ` style="${utils.escape(styles)}"`; } // Add the attributes for (const attr in this.attributes) { if (this.attributes.hasOwnProperty(attr)) { if (invalidAttributeNameRegex.test(attr)) { throw new ParseError(`Invalid attribute name '${attr}'`); } markup += ` ${attr}="${utils.escape(this.attributes[attr])}"`; } } markup += ">"; // Add the markup of the children, also as markup for (let i = 0; i < this.children.length; i++) { markup += this.children[i].toMarkup(); } markup += ``; return markup; }; // Making the type below exact with all optional fields doesn't work due to // - https://github.com/facebook/flow/issues/4582 // - https://github.com/facebook/flow/issues/5688 // However, since *all* fields are optional, $Shape<> works as suggested in 5688 // above. // This type does not include all CSS properties. Additional properties should // be added as needed. export type CssStyle = $Shape<{ backgroundColor: string, borderBottomWidth: string, borderColor: string, borderRightStyle: string, borderRightWidth: string, borderTopWidth: string, borderStyle: string; borderWidth: string, bottom: string, color: string, height: string, left: string, margin: string, marginLeft: string, marginRight: string, marginTop: string, minWidth: string, paddingLeft: string, position: string, textShadow: string, top: string, width: string, verticalAlign: string, }> & {}; export interface HtmlDomNode extends VirtualNode { classes: string[]; height: number; depth: number; maxFontSize: number; style: CssStyle; hasClass(className: string): boolean; } // Span wrapping other DOM nodes. export type DomSpan = Span; // Span wrapping an SVG node. export type SvgSpan = Span; export type SvgChildNode = PathNode | LineNode; export type documentFragment = DocumentFragment; /** * 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. * * Represents two types with different uses: SvgSpan to wrap an SVG and DomSpan * otherwise. This typesafety is important when HTML builders access a span's * children. */ export class Span implements HtmlDomNode { children: ChildType[]; attributes: {[string]: string}; classes: string[]; height: number; depth: number; width: ?number; maxFontSize: number; style: CssStyle; constructor( classes?: string[], children?: ChildType[], options?: Options, style?: CssStyle, ) { initNode.call(this, classes, options, style); this.children = children || []; } /** * Sets an arbitrary attribute on the span. Warning: use this wisely. Not * all browsers support attributes the same, and having too many custom * attributes is probably bad. */ setAttribute(attribute: string, value: string) { this.attributes[attribute] = value; } hasClass(className: string): boolean { return utils.contains(this.classes, className); } toNode(): HTMLElement { return toNode.call(this, "span"); } toMarkup(): string { return toMarkup.call(this, "span"); } } /** * This node represents an anchor () element with a hyperlink. See `span` * for further details. */ export class Anchor implements HtmlDomNode { children: HtmlDomNode[]; attributes: {[string]: string}; classes: string[]; height: number; depth: number; maxFontSize: number; style: CssStyle; constructor( href: string, classes: string[], children: HtmlDomNode[], options: Options, ) { initNode.call(this, classes, options); this.children = children || []; this.setAttribute('href', href); } setAttribute(attribute: string, value: string) { this.attributes[attribute] = value; } hasClass(className: string): boolean { return utils.contains(this.classes, className); } toNode(): HTMLElement { return toNode.call(this, "a"); } toMarkup(): string { return toMarkup.call(this, "a"); } } /** * This node represents an image embed () element. */ export class Img implements VirtualNode { src: string; alt: string; classes: string[]; height: number; depth: number; maxFontSize: number; style: CssStyle; constructor( src: string, alt: string, style: CssStyle, ) { this.alt = alt; this.src = src; this.classes = ["mord"]; this.style = style; } hasClass(className: string): boolean { return utils.contains(this.classes, className); } toNode(): Node { const node = document.createElement("img"); node.src = this.src; node.alt = this.alt; node.className = "mord"; // Apply inline styles for (const style in this.style) { if (this.style.hasOwnProperty(style)) { // $FlowFixMe node.style[style] = this.style[style]; } } return node; } toMarkup(): string { let markup = `${utils.escape(this.alt)} 0) { span = document.createElement("span"); span.style.marginRight = makeEm(this.italic); } if (this.classes.length > 0) { span = span || document.createElement("span"); span.className = createClass(this.classes); } 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]; } } if (span) { span.appendChild(node); return span; } else { return node; } } /** * Creates markup for a symbol node. */ toMarkup(): string { // TODO(alpert): More duplication than I'd like from // span.prototype.toMarkup and symbolNode.prototype.toNode... let needsSpan = false; let markup = " 0) { styles += "margin-right:" + this.italic + "em;"; } for (const style in this.style) { if (this.style.hasOwnProperty(style)) { styles += utils.hyphenate(style) + ":" + this.style[style] + ";"; } } if (styles) { needsSpan = true; markup += " style=\"" + utils.escape(styles) + "\""; } const escaped = utils.escape(this.text); if (needsSpan) { markup += ">"; markup += escaped; markup += ""; return markup; } else { return escaped; } } } /** * SVG nodes are used to render stretchy wide elements. */ export class SvgNode implements VirtualNode { children: SvgChildNode[]; attributes: {[string]: string}; constructor(children?: SvgChildNode[], attributes?: {[string]: string}) { this.children = children || []; this.attributes = attributes || {}; } toNode(): Node { const svgNS = "http://www.w3.org/2000/svg"; const node = document.createElementNS(svgNS, "svg"); // Apply attributes 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++) { node.appendChild(this.children[i].toNode()); } return node; } toMarkup(): string { let markup = ``; } else { return ``; } } } export class LineNode implements VirtualNode { attributes: {[string]: string}; constructor(attributes?: {[string]: string}) { this.attributes = attributes || {}; } toNode(): Node { const svgNS = "http://www.w3.org/2000/svg"; const node = document.createElementNS(svgNS, "line"); // Apply attributes for (const attr in this.attributes) { if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) { node.setAttribute(attr, this.attributes[attr]); } } return node; } toMarkup(): string { let markup = " { if (group instanceof Span) { return group; } else { throw new Error(`Expected span but got ${String(group)}.`); } }