|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 type {VirtualNode} from "./tree"; |
|
|
|
|
|
|
|
|
|
|
|
|
|
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; |
|
} |
|
} |
|
}; |
|
|
|
|
|
|
|
|
|
const toNode = function(tagName: string): HTMLElement { |
|
const node = document.createElement(tagName); |
|
|
|
|
|
node.className = createClass(this.classes); |
|
|
|
|
|
for (const style in this.style) { |
|
if (this.style.hasOwnProperty(style)) { |
|
|
|
node.style[style] = this.style[style]; |
|
} |
|
} |
|
|
|
|
|
for (const attr in this.attributes) { |
|
if (this.attributes.hasOwnProperty(attr)) { |
|
node.setAttribute(attr, this.attributes[attr]); |
|
} |
|
} |
|
|
|
|
|
for (let i = 0; i < this.children.length; i++) { |
|
node.appendChild(this.children[i].toNode()); |
|
} |
|
|
|
return node; |
|
}; |
|
|
|
|
|
|
|
|
|
const toMarkup = function(tagName: string): string { |
|
let markup = `<${tagName}`; |
|
|
|
|
|
if (this.classes.length) { |
|
markup += ` class="${utils.escape(createClass(this.classes))}"`; |
|
} |
|
|
|
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)}"`; |
|
} |
|
|
|
|
|
for (const attr in this.attributes) { |
|
if (this.attributes.hasOwnProperty(attr)) { |
|
markup += ` ${attr}="${utils.escape(this.attributes[attr])}"`; |
|
} |
|
} |
|
|
|
markup += ">"; |
|
|
|
|
|
for (let i = 0; i < this.children.length; i++) { |
|
markup += this.children[i].toMarkup(); |
|
} |
|
|
|
markup += `</${tagName}>`; |
|
|
|
return markup; |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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; |
|
} |
|
|
|
|
|
export type DomSpan = Span<HtmlDomNode>; |
|
|
|
export type SvgSpan = Span<SvgNode>; |
|
|
|
export type SvgChildNode = PathNode | LineNode; |
|
export type documentFragment = DocumentFragment<HtmlDomNode>; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 || []; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
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"); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
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"); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
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"; |
|
|
|
|
|
for (const style in this.style) { |
|
if (this.style.hasOwnProperty(style)) { |
|
|
|
node.style[style] = this.style[style]; |
|
} |
|
} |
|
|
|
return node; |
|
} |
|
|
|
toMarkup(): string { |
|
let markup = `<img src="${utils.escape(this.src)}"` + |
|
` alt="${utils.escape(this.alt)}"`; |
|
|
|
|
|
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\u0300', |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
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; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const script = scriptFromCodepoint(this.text.charCodeAt(0)); |
|
if (script) { |
|
this.classes.push(script + "_fallback"); |
|
} |
|
|
|
if (/[îïíì]/.test(this.text)) { |
|
this.text = iCombinations[this.text]; |
|
} |
|
} |
|
|
|
hasClass(className: string): boolean { |
|
return utils.contains(this.classes, className); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
toNode(): Node { |
|
const node = document.createTextNode(this.text); |
|
let span = null; |
|
|
|
if (this.italic > 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"); |
|
|
|
span.style[style] = this.style[style]; |
|
} |
|
} |
|
|
|
if (span) { |
|
span.appendChild(node); |
|
return span; |
|
} else { |
|
return node; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
toMarkup(): string { |
|
|
|
|
|
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; |
|
} |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
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"); |
|
|
|
|
|
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 xmlns="http://www.w3.org/2000/svg"`; |
|
|
|
|
|
for (const attr in this.attributes) { |
|
if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) { |
|
markup += ` ${attr}="${utils.escape(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; |
|
} |
|
|
|
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", path[this.pathName]); |
|
} |
|
|
|
return node; |
|
} |
|
|
|
toMarkup(): string { |
|
if (this.alternate) { |
|
return `<path d="${utils.escape(this.alternate)}"/>`; |
|
} else { |
|
return `<path d="${utils.escape(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"); |
|
|
|
|
|
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}="${utils.escape(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)}.`); |
|
} |
|
} |
|
|