domTree.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612
  1. // @flow
  2. /**
  3. * These objects store the data about the DOM nodes we create, as well as some
  4. * extra data. They can then be transformed into real DOM nodes with the
  5. * `toNode` function or HTML markup using `toMarkup`. They are useful for both
  6. * storing extra properties on the nodes, as well as providing a way to easily
  7. * work with the DOM.
  8. *
  9. * Similar functions for working with MathML nodes exist in mathMLTree.js.
  10. *
  11. * TODO: refactor `span` and `anchor` into common superclass when
  12. * target environments support class inheritance
  13. */
  14. import {scriptFromCodepoint} from "./unicodeScripts";
  15. import utils from "./utils";
  16. import svgGeometry from "./svgGeometry";
  17. import type Options from "./Options";
  18. import {DocumentFragment} from "./tree";
  19. import type {VirtualNode} from "./tree";
  20. /**
  21. * Create an HTML className based on a list of classes. In addition to joining
  22. * with spaces, we also remove empty classes.
  23. */
  24. export const createClass = function(classes: string[]): string {
  25. return classes.filter(cls => cls).join(" ");
  26. };
  27. const initNode = function(
  28. classes?: string[],
  29. options?: Options,
  30. style?: CssStyle,
  31. ) {
  32. this.classes = classes || [];
  33. this.attributes = {};
  34. this.height = 0;
  35. this.depth = 0;
  36. this.maxFontSize = 0;
  37. this.style = style || {};
  38. if (options) {
  39. if (options.style.isTight()) {
  40. this.classes.push("mtight");
  41. }
  42. const color = options.getColor();
  43. if (color) {
  44. this.style.color = color;
  45. }
  46. }
  47. };
  48. /**
  49. * Convert into an HTML node
  50. */
  51. const toNode = function(tagName: string): HTMLElement {
  52. const node = document.createElement(tagName);
  53. // Apply the class
  54. node.className = createClass(this.classes);
  55. // Apply inline styles
  56. for (const style in this.style) {
  57. if (this.style.hasOwnProperty(style)) {
  58. // $FlowFixMe Flow doesn't seem to understand span.style's type.
  59. node.style[style] = this.style[style];
  60. }
  61. }
  62. // Apply attributes
  63. for (const attr in this.attributes) {
  64. if (this.attributes.hasOwnProperty(attr)) {
  65. node.setAttribute(attr, this.attributes[attr]);
  66. }
  67. }
  68. // Append the children, also as HTML nodes
  69. for (let i = 0; i < this.children.length; i++) {
  70. node.appendChild(this.children[i].toNode());
  71. }
  72. return node;
  73. };
  74. /**
  75. * Convert into an HTML markup string
  76. */
  77. const toMarkup = function(tagName: string): string {
  78. let markup = `<${tagName}`;
  79. // Add the class
  80. if (this.classes.length) {
  81. markup += ` class="${utils.escape(createClass(this.classes))}"`;
  82. }
  83. let styles = "";
  84. // Add the styles, after hyphenation
  85. for (const style in this.style) {
  86. if (this.style.hasOwnProperty(style)) {
  87. styles += `${utils.hyphenate(style)}:${this.style[style]};`;
  88. }
  89. }
  90. if (styles) {
  91. markup += ` style="${utils.escape(styles)}"`;
  92. }
  93. // Add the attributes
  94. for (const attr in this.attributes) {
  95. if (this.attributes.hasOwnProperty(attr)) {
  96. markup += ` ${attr}="${utils.escape(this.attributes[attr])}"`;
  97. }
  98. }
  99. markup += ">";
  100. // Add the markup of the children, also as markup
  101. for (let i = 0; i < this.children.length; i++) {
  102. markup += this.children[i].toMarkup();
  103. }
  104. markup += `</${tagName}>`;
  105. return markup;
  106. };
  107. // Making the type below exact with all optional fields doesn't work due to
  108. // - https://github.com/facebook/flow/issues/4582
  109. // - https://github.com/facebook/flow/issues/5688
  110. // However, since *all* fields are optional, $Shape<> works as suggested in 5688
  111. // above.
  112. // This type does not include all CSS properties. Additional properties should
  113. // be added as needed.
  114. export type CssStyle = $Shape<{
  115. backgroundColor: string,
  116. borderBottomWidth: string,
  117. borderColor: string,
  118. borderRightWidth: string,
  119. borderTopWidth: string,
  120. bottom: string,
  121. color: string,
  122. height: string,
  123. left: string,
  124. marginLeft: string,
  125. marginRight: string,
  126. marginTop: string,
  127. minWidth: string,
  128. paddingLeft: string,
  129. position: string,
  130. top: string,
  131. width: string,
  132. verticalAlign: string,
  133. }> & {};
  134. export interface HtmlDomNode extends VirtualNode {
  135. classes: string[];
  136. height: number;
  137. depth: number;
  138. maxFontSize: number;
  139. style: CssStyle;
  140. hasClass(className: string): boolean;
  141. }
  142. // Span wrapping other DOM nodes.
  143. export type DomSpan = Span<HtmlDomNode>;
  144. // Span wrapping an SVG node.
  145. export type SvgSpan = Span<SvgNode>;
  146. export type SvgChildNode = PathNode | LineNode;
  147. export type documentFragment = DocumentFragment<HtmlDomNode>;
  148. /**
  149. * This node represents a span node, with a className, a list of children, and
  150. * an inline style. It also contains information about its height, depth, and
  151. * maxFontSize.
  152. *
  153. * Represents two types with different uses: SvgSpan to wrap an SVG and DomSpan
  154. * otherwise. This typesafety is important when HTML builders access a span's
  155. * children.
  156. */
  157. export class Span<ChildType: VirtualNode> implements HtmlDomNode {
  158. children: ChildType[];
  159. attributes: {[string]: string};
  160. classes: string[];
  161. height: number;
  162. depth: number;
  163. width: ?number;
  164. maxFontSize: number;
  165. style: CssStyle;
  166. constructor(
  167. classes?: string[],
  168. children?: ChildType[],
  169. options?: Options,
  170. style?: CssStyle,
  171. ) {
  172. initNode.call(this, classes, options, style);
  173. this.children = children || [];
  174. }
  175. /**
  176. * Sets an arbitrary attribute on the span. Warning: use this wisely. Not
  177. * all browsers support attributes the same, and having too many custom
  178. * attributes is probably bad.
  179. */
  180. setAttribute(attribute: string, value: string) {
  181. this.attributes[attribute] = value;
  182. }
  183. hasClass(className: string): boolean {
  184. return utils.contains(this.classes, className);
  185. }
  186. toNode(): HTMLElement {
  187. return toNode.call(this, "span");
  188. }
  189. toMarkup(): string {
  190. return toMarkup.call(this, "span");
  191. }
  192. }
  193. /**
  194. * This node represents an anchor (<a>) element with a hyperlink. See `span`
  195. * for further details.
  196. */
  197. export class Anchor implements HtmlDomNode {
  198. children: HtmlDomNode[];
  199. attributes: {[string]: string};
  200. classes: string[];
  201. height: number;
  202. depth: number;
  203. maxFontSize: number;
  204. style: CssStyle;
  205. constructor(
  206. href: string,
  207. classes: string[],
  208. children: HtmlDomNode[],
  209. options: Options,
  210. ) {
  211. initNode.call(this, classes, options);
  212. this.children = children || [];
  213. this.setAttribute('href', href);
  214. }
  215. setAttribute(attribute: string, value: string) {
  216. this.attributes[attribute] = value;
  217. }
  218. hasClass(className: string): boolean {
  219. return utils.contains(this.classes, className);
  220. }
  221. toNode(): HTMLElement {
  222. return toNode.call(this, "a");
  223. }
  224. toMarkup(): string {
  225. return toMarkup.call(this, "a");
  226. }
  227. }
  228. /**
  229. * This node represents an image embed (<img>) element.
  230. */
  231. export class Img implements VirtualNode {
  232. src: string;
  233. alt: string;
  234. classes: string[];
  235. height: number;
  236. depth: number;
  237. maxFontSize: number;
  238. style: CssStyle;
  239. constructor(
  240. src: string,
  241. alt: string,
  242. style: CssStyle,
  243. ) {
  244. this.alt = alt;
  245. this.src = src;
  246. this.classes = ["mord"];
  247. this.style = style;
  248. }
  249. hasClass(className: string): boolean {
  250. return utils.contains(this.classes, className);
  251. }
  252. toNode(): Node {
  253. const node = document.createElement("img");
  254. node.src = this.src;
  255. node.alt = this.alt;
  256. node.className = "mord";
  257. // Apply inline styles
  258. for (const style in this.style) {
  259. if (this.style.hasOwnProperty(style)) {
  260. // $FlowFixMe
  261. node.style[style] = this.style[style];
  262. }
  263. }
  264. return node;
  265. }
  266. toMarkup(): string {
  267. let markup = `<img src='${this.src} 'alt='${this.alt}' `;
  268. // Add the styles, after hyphenation
  269. let styles = "";
  270. for (const style in this.style) {
  271. if (this.style.hasOwnProperty(style)) {
  272. styles += `${utils.hyphenate(style)}:${this.style[style]};`;
  273. }
  274. }
  275. if (styles) {
  276. markup += ` style="${utils.escape(styles)}"`;
  277. }
  278. markup += "'/>";
  279. return markup;
  280. }
  281. }
  282. const iCombinations = {
  283. 'î': '\u0131\u0302',
  284. 'ï': '\u0131\u0308',
  285. 'í': '\u0131\u0301',
  286. // 'ī': '\u0131\u0304', // enable when we add Extended Latin
  287. 'ì': '\u0131\u0300',
  288. };
  289. /**
  290. * A symbol node contains information about a single symbol. It either renders
  291. * to a single text node, or a span with a single text node in it, depending on
  292. * whether it has CSS classes, styles, or needs italic correction.
  293. */
  294. export class SymbolNode implements HtmlDomNode {
  295. text: string;
  296. height: number;
  297. depth: number;
  298. italic: number;
  299. skew: number;
  300. width: number;
  301. maxFontSize: number;
  302. classes: string[];
  303. style: CssStyle;
  304. constructor(
  305. text: string,
  306. height?: number,
  307. depth?: number,
  308. italic?: number,
  309. skew?: number,
  310. width?: number,
  311. classes?: string[],
  312. style?: CssStyle,
  313. ) {
  314. this.text = text;
  315. this.height = height || 0;
  316. this.depth = depth || 0;
  317. this.italic = italic || 0;
  318. this.skew = skew || 0;
  319. this.width = width || 0;
  320. this.classes = classes || [];
  321. this.style = style || {};
  322. this.maxFontSize = 0;
  323. // Mark text from non-Latin scripts with specific classes so that we
  324. // can specify which fonts to use. This allows us to render these
  325. // characters with a serif font in situations where the browser would
  326. // either default to a sans serif or render a placeholder character.
  327. // We use CSS class names like cjk_fallback, hangul_fallback and
  328. // brahmic_fallback. See ./unicodeScripts.js for the set of possible
  329. // script names
  330. const script = scriptFromCodepoint(this.text.charCodeAt(0));
  331. if (script) {
  332. this.classes.push(script + "_fallback");
  333. }
  334. if (/[îïíì]/.test(this.text)) { // add ī when we add Extended Latin
  335. this.text = iCombinations[this.text];
  336. }
  337. }
  338. hasClass(className: string): boolean {
  339. return utils.contains(this.classes, className);
  340. }
  341. /**
  342. * Creates a text node or span from a symbol node. Note that a span is only
  343. * created if it is needed.
  344. */
  345. toNode(): Node {
  346. const node = document.createTextNode(this.text);
  347. let span = null;
  348. if (this.italic > 0) {
  349. span = document.createElement("span");
  350. span.style.marginRight = this.italic + "em";
  351. }
  352. if (this.classes.length > 0) {
  353. span = span || document.createElement("span");
  354. span.className = createClass(this.classes);
  355. }
  356. for (const style in this.style) {
  357. if (this.style.hasOwnProperty(style)) {
  358. span = span || document.createElement("span");
  359. // $FlowFixMe Flow doesn't seem to understand span.style's type.
  360. span.style[style] = this.style[style];
  361. }
  362. }
  363. if (span) {
  364. span.appendChild(node);
  365. return span;
  366. } else {
  367. return node;
  368. }
  369. }
  370. /**
  371. * Creates markup for a symbol node.
  372. */
  373. toMarkup(): string {
  374. // TODO(alpert): More duplication than I'd like from
  375. // span.prototype.toMarkup and symbolNode.prototype.toNode...
  376. let needsSpan = false;
  377. let markup = "<span";
  378. if (this.classes.length) {
  379. needsSpan = true;
  380. markup += " class=\"";
  381. markup += utils.escape(createClass(this.classes));
  382. markup += "\"";
  383. }
  384. let styles = "";
  385. if (this.italic > 0) {
  386. styles += "margin-right:" + this.italic + "em;";
  387. }
  388. for (const style in this.style) {
  389. if (this.style.hasOwnProperty(style)) {
  390. styles += utils.hyphenate(style) + ":" + this.style[style] + ";";
  391. }
  392. }
  393. if (styles) {
  394. needsSpan = true;
  395. markup += " style=\"" + utils.escape(styles) + "\"";
  396. }
  397. const escaped = utils.escape(this.text);
  398. if (needsSpan) {
  399. markup += ">";
  400. markup += escaped;
  401. markup += "</span>";
  402. return markup;
  403. } else {
  404. return escaped;
  405. }
  406. }
  407. }
  408. /**
  409. * SVG nodes are used to render stretchy wide elements.
  410. */
  411. export class SvgNode implements VirtualNode {
  412. children: SvgChildNode[];
  413. attributes: {[string]: string};
  414. constructor(children?: SvgChildNode[], attributes?: {[string]: string}) {
  415. this.children = children || [];
  416. this.attributes = attributes || {};
  417. }
  418. toNode(): Node {
  419. const svgNS = "http://www.w3.org/2000/svg";
  420. const node = document.createElementNS(svgNS, "svg");
  421. // Apply attributes
  422. for (const attr in this.attributes) {
  423. if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) {
  424. node.setAttribute(attr, this.attributes[attr]);
  425. }
  426. }
  427. for (let i = 0; i < this.children.length; i++) {
  428. node.appendChild(this.children[i].toNode());
  429. }
  430. return node;
  431. }
  432. toMarkup(): string {
  433. let markup = "<svg";
  434. // Apply attributes
  435. for (const attr in this.attributes) {
  436. if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) {
  437. markup += ` ${attr}='${this.attributes[attr]}'`;
  438. }
  439. }
  440. markup += ">";
  441. for (let i = 0; i < this.children.length; i++) {
  442. markup += this.children[i].toMarkup();
  443. }
  444. markup += "</svg>";
  445. return markup;
  446. }
  447. }
  448. export class PathNode implements VirtualNode {
  449. pathName: string;
  450. alternate: ?string;
  451. constructor(pathName: string, alternate?: string) {
  452. this.pathName = pathName;
  453. this.alternate = alternate; // Used only for tall \sqrt
  454. }
  455. toNode(): Node {
  456. const svgNS = "http://www.w3.org/2000/svg";
  457. const node = document.createElementNS(svgNS, "path");
  458. if (this.alternate) {
  459. node.setAttribute("d", this.alternate);
  460. } else {
  461. node.setAttribute("d", svgGeometry.path[this.pathName]);
  462. }
  463. return node;
  464. }
  465. toMarkup(): string {
  466. if (this.alternate) {
  467. return `<path d='${this.alternate}'/>`;
  468. } else {
  469. return `<path d='${svgGeometry.path[this.pathName]}'/>`;
  470. }
  471. }
  472. }
  473. export class LineNode implements VirtualNode {
  474. attributes: {[string]: string};
  475. constructor(attributes?: {[string]: string}) {
  476. this.attributes = attributes || {};
  477. }
  478. toNode(): Node {
  479. const svgNS = "http://www.w3.org/2000/svg";
  480. const node = document.createElementNS(svgNS, "line");
  481. // Apply attributes
  482. for (const attr in this.attributes) {
  483. if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) {
  484. node.setAttribute(attr, this.attributes[attr]);
  485. }
  486. }
  487. return node;
  488. }
  489. toMarkup(): string {
  490. let markup = "<line";
  491. for (const attr in this.attributes) {
  492. if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) {
  493. markup += ` ${attr}='${this.attributes[attr]}'`;
  494. }
  495. }
  496. markup += "/>";
  497. return markup;
  498. }
  499. }
  500. export function assertSymbolDomNode(
  501. group: HtmlDomNode,
  502. ): SymbolNode {
  503. if (group instanceof SymbolNode) {
  504. return group;
  505. } else {
  506. throw new Error(`Expected symbolNode but got ${String(group)}.`);
  507. }
  508. }
  509. export function assertSpan(
  510. group: HtmlDomNode,
  511. ): Span<HtmlDomNode> {
  512. if (group instanceof Span) {
  513. return group;
  514. } else {
  515. throw new Error(`Expected span<HtmlDomNode> but got ${String(group)}.`);
  516. }
  517. }