123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612 |
- // @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 svgGeometry from "./svgGeometry";
- import type Options from "./Options";
- import {DocumentFragment} from "./tree";
- 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;
- };
- /**
- * 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)) {
- 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 += `</${tagName}>`;
- 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,
- borderRightWidth: string,
- borderTopWidth: string,
- bottom: string,
- color: string,
- height: string,
- left: string,
- marginLeft: string,
- marginRight: string,
- marginTop: string,
- minWidth: string,
- paddingLeft: string,
- position: 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<HtmlDomNode>;
- // Span wrapping an SVG node.
- export type SvgSpan = Span<SvgNode>;
- export type SvgChildNode = PathNode | LineNode;
- export type documentFragment = DocumentFragment<HtmlDomNode>;
- /**
- * 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<ChildType: VirtualNode> 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 (<a>) 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 (<img>) 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 = `<img src='${this.src} 'alt='${this.alt}' `;
- // Add the styles, after hyphenation
- let styles = "";
- 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)}"`;
- }
- markup += "'/>";
- return markup;
- }
- }
- const iCombinations = {
- 'î': '\u0131\u0302',
- 'ï': '\u0131\u0308',
- 'í': '\u0131\u0301',
- // 'ī': '\u0131\u0304', // enable when we add Extended Latin
- 'ì': '\u0131\u0300',
- };
- /**
- * A symbol node contains information about a single symbol. It either renders
- * 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.
- */
- export class SymbolNode implements HtmlDomNode {
- text: string;
- height: number;
- depth: number;
- italic: number;
- skew: number;
- width: number;
- maxFontSize: number;
- classes: string[];
- style: CssStyle;
- constructor(
- text: string,
- height?: number,
- depth?: number,
- italic?: number,
- skew?: number,
- width?: number,
- classes?: string[],
- style?: CssStyle,
- ) {
- this.text = text;
- this.height = height || 0;
- this.depth = depth || 0;
- this.italic = italic || 0;
- this.skew = skew || 0;
- this.width = width || 0;
- this.classes = classes || [];
- this.style = style || {};
- this.maxFontSize = 0;
- // Mark text from non-Latin scripts with specific classes so that we
- // can specify which 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.
- // We use CSS class names like cjk_fallback, hangul_fallback and
- // brahmic_fallback. See ./unicodeScripts.js for the set of possible
- // script names
- const script = scriptFromCodepoint(this.text.charCodeAt(0));
- if (script) {
- this.classes.push(script + "_fallback");
- }
- if (/[îïíì]/.test(this.text)) { // add ī when we add Extended Latin
- this.text = iCombinations[this.text];
- }
- }
- hasClass(className: string): boolean {
- return utils.contains(this.classes, className);
- }
- /**
- * Creates a text node or span from a symbol node. Note that a span is only
- * created if it is needed.
- */
- toNode(): Node {
- const node = document.createTextNode(this.text);
- let span = null;
- if (this.italic > 0) {
- span = document.createElement("span");
- span.style.marginRight = this.italic + "em";
- }
- 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 = "<span";
- if (this.classes.length) {
- needsSpan = true;
- markup += " class=\"";
- markup += utils.escape(createClass(this.classes));
- markup += "\"";
- }
- let styles = "";
- if (this.italic > 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 += "</span>";
- 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 = "<svg";
- // Apply attributes
- for (const attr in this.attributes) {
- if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) {
- markup += ` ${attr}='${this.attributes[attr]}'`;
- }
- }
- markup += ">";
- for (let i = 0; i < this.children.length; i++) {
- markup += this.children[i].toMarkup();
- }
- markup += "</svg>";
- return markup;
- }
- }
- export class PathNode implements VirtualNode {
- pathName: string;
- alternate: ?string;
- constructor(pathName: string, alternate?: string) {
- this.pathName = pathName;
- this.alternate = alternate; // Used only for tall \sqrt
- }
- toNode(): Node {
- const svgNS = "http://www.w3.org/2000/svg";
- const node = document.createElementNS(svgNS, "path");
- if (this.alternate) {
- node.setAttribute("d", this.alternate);
- } else {
- node.setAttribute("d", svgGeometry.path[this.pathName]);
- }
- return node;
- }
- toMarkup(): string {
- if (this.alternate) {
- return `<path d='${this.alternate}'/>`;
- } else {
- return `<path d='${svgGeometry.path[this.pathName]}'/>`;
- }
- }
- }
- 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 = "<line";
- for (const attr in this.attributes) {
- if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) {
- markup += ` ${attr}='${this.attributes[attr]}'`;
- }
- }
- markup += "/>";
- return markup;
- }
- }
- export function assertSymbolDomNode(
- group: HtmlDomNode,
- ): SymbolNode {
- if (group instanceof SymbolNode) {
- return group;
- } else {
- throw new Error(`Expected symbolNode but got ${String(group)}.`);
- }
- }
- export function assertSpan(
- group: HtmlDomNode,
- ): Span<HtmlDomNode> {
- if (group instanceof Span) {
- return group;
- } else {
- throw new Error(`Expected span<HtmlDomNode> but got ${String(group)}.`);
- }
- }
|