HTMLDocument.ts 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. /*************************************************************
  2. *
  3. * Copyright (c) 2017-2022 The MathJax Consortium
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. /**
  18. * @fileoverview Implements the HTMLDocument class
  19. *
  20. * @author dpvc@mathjax.org (Davide Cervone)
  21. */
  22. import {AbstractMathDocument} from '../../core/MathDocument.js';
  23. import {userOptions, separateOptions, OptionList, expandable} from '../../util/Options.js';
  24. import {HTMLMathItem} from './HTMLMathItem.js';
  25. import {HTMLMathList} from './HTMLMathList.js';
  26. import {HTMLDomStrings} from './HTMLDomStrings.js';
  27. import {DOMAdaptor} from '../../core/DOMAdaptor.js';
  28. import {InputJax} from '../../core/InputJax.js';
  29. import {STATE, ProtoItem, Location} from '../../core/MathItem.js';
  30. import {StyleList} from '../../util/StyleList.js';
  31. /*****************************************************************/
  32. /**
  33. * List of Lists of pairs consisting of a DOM node and its text length
  34. *
  35. * These represent the Text elements that make up a single
  36. * string in the list of strings to be searched for math
  37. * (multiple consecutive Text nodes can form a single string).
  38. *
  39. * @template N The HTMLElement node class
  40. * @template T The Text node class
  41. */
  42. export type HTMLNodeArray<N, T> = [N | T, number][][];
  43. /*****************************************************************/
  44. /**
  45. * The HTMLDocument class (extends AbstractMathDocument)
  46. *
  47. * @template N The HTMLElement node class
  48. * @template T The Text node class
  49. * @template D The Document class
  50. */
  51. export class HTMLDocument<N, T, D> extends AbstractMathDocument<N, T, D> {
  52. /**
  53. * The kind of document
  54. */
  55. public static KIND: string = 'HTML';
  56. /**
  57. * The default options for HTMLDocument
  58. */
  59. public static OPTIONS: OptionList = {
  60. ...AbstractMathDocument.OPTIONS,
  61. renderActions: expandable({
  62. ...AbstractMathDocument.OPTIONS.renderActions,
  63. styles: [STATE.INSERTED + 1, '', 'updateStyleSheet', false] // update styles on a rerender() call
  64. }),
  65. MathList: HTMLMathList, // Use the HTMLMathList for MathLists
  66. MathItem: HTMLMathItem, // Use the HTMLMathItem for MathItem
  67. DomStrings: null // Use the default DomString parser
  68. };
  69. /**
  70. * Extra styles to be included in the document's stylesheet (added by extensions)
  71. */
  72. protected styles: StyleList[];
  73. /**
  74. * The DomString parser for locating the text in DOM trees
  75. */
  76. public domStrings: HTMLDomStrings<N, T, D>;
  77. /**
  78. * @override
  79. * @constructor
  80. * @extends {AbstractMathDocument}
  81. */
  82. constructor(document: any, adaptor: DOMAdaptor<N, T, D>, options: OptionList) {
  83. let [html, dom] = separateOptions(options, HTMLDomStrings.OPTIONS);
  84. super(document, adaptor, html);
  85. this.domStrings = this.options['DomStrings'] || new HTMLDomStrings<N, T, D>(dom);
  86. this.domStrings.adaptor = adaptor;
  87. this.styles = [];
  88. }
  89. /**
  90. * Creates a Location object for a delimiter at the position given by index in the N's string
  91. * of the array of strings searched for math, recovering the original DOM node where the delimiter
  92. * was found.
  93. *
  94. * @param {number} N The index of the string in the string array
  95. * @param {number} index The position within the N's string that needs to be found
  96. * @param {string} delim The delimiter for this position
  97. * @param {HTMLNodeArray} nodes The list of node lists representing the string array
  98. * @return {Location} The Location object for the position of the delimiter in the document
  99. */
  100. protected findPosition(N: number, index: number, delim: string, nodes: HTMLNodeArray<N, T>): Location<N, T> {
  101. const adaptor = this.adaptor;
  102. for (const list of nodes[N]) {
  103. let [node, n] = list;
  104. if (index <= n && adaptor.kind(node) === '#text') {
  105. return {node: node, n: Math.max(index, 0), delim: delim};
  106. }
  107. index -= n;
  108. }
  109. return {node: null, n: 0, delim: delim};
  110. }
  111. /**
  112. * Convert a ProtoItem to a MathItem (i.e., determine the actual Location
  113. * objects for its start and end)
  114. *
  115. * @param {ProtoItem} item The proto math item to turn into an actual MathItem
  116. * @param {InputJax} jax The input jax to use for the MathItem
  117. * @param {HTMLNodeArray} nodes The array of node lists that produced the string array
  118. * @return {HTMLMathItem} The MathItem for the given proto item
  119. */
  120. protected mathItem(item: ProtoItem<N, T>, jax: InputJax<N, T, D>,
  121. nodes: HTMLNodeArray<N, T>): HTMLMathItem<N, T, D> {
  122. let math = item.math;
  123. let start = this.findPosition(item.n, item.start.n, item.open, nodes);
  124. let end = this.findPosition(item.n, item.end.n, item.close, nodes);
  125. return new this.options.MathItem(math, jax, item.display, start, end) as HTMLMathItem<N, T, D>;
  126. }
  127. /**
  128. * Find math within the document:
  129. * Get the list of containers (default is document.body), and for each:
  130. * For each input jax:
  131. * Make a new MathList to store the located math
  132. * If the input jax processes strings:
  133. * If we haven't already made the string array and corresponding node list, do so
  134. * Ask the jax to find the math in the string array, and
  135. * for each one, push it onto the math list
  136. * Otherwise (the jax processes DOM nodes):
  137. * Ask the jax to find the math in the container, and
  138. * for each one, make the result into a MathItem, and push it on the list
  139. * Merge the new math list into the document's math list
  140. * (we use merge to maintain a sorted list of MathItems)
  141. *
  142. * @override
  143. */
  144. public findMath(options: OptionList) {
  145. if (!this.processed.isSet('findMath')) {
  146. this.adaptor.document = this.document;
  147. options = userOptions({elements: this.options.elements || [this.adaptor.body(this.document)]}, options);
  148. for (const container of this.adaptor.getElements(options['elements'], this.document)) {
  149. let [strings, nodes] = [null, null] as [string[], HTMLNodeArray<N, T>];
  150. for (const jax of this.inputJax) {
  151. let list = new (this.options['MathList'])();
  152. if (jax.processStrings) {
  153. if (strings === null) {
  154. [strings, nodes] = this.domStrings.find(container);
  155. }
  156. for (const math of jax.findMath(strings)) {
  157. list.push(this.mathItem(math, jax, nodes));
  158. }
  159. } else {
  160. for (const math of jax.findMath(container)) {
  161. let item: HTMLMathItem<N, T, D> =
  162. new this.options.MathItem(math.math, jax, math.display, math.start, math.end);
  163. list.push(item);
  164. }
  165. }
  166. this.math.merge(list);
  167. }
  168. }
  169. this.processed.set('findMath');
  170. }
  171. return this;
  172. }
  173. /**
  174. * @override
  175. */
  176. public updateDocument() {
  177. if (!this.processed.isSet('updateDocument')) {
  178. this.addPageElements();
  179. this.addStyleSheet();
  180. super.updateDocument();
  181. this.processed.set('updateDocument');
  182. }
  183. return this;
  184. }
  185. /**
  186. * Add any elements needed for the document
  187. */
  188. protected addPageElements() {
  189. const body = this.adaptor.body(this.document);
  190. const node = this.documentPageElements();
  191. if (node) {
  192. this.adaptor.append(body, node);
  193. }
  194. }
  195. /**
  196. * Add the stylesheet to the document
  197. */
  198. public addStyleSheet() {
  199. const sheet = this.documentStyleSheet();
  200. const adaptor = this.adaptor;
  201. if (sheet && !adaptor.parent(sheet)) {
  202. const head = adaptor.head(this.document);
  203. let styles = this.findSheet(head, adaptor.getAttribute(sheet, 'id'));
  204. if (styles) {
  205. adaptor.replace(sheet, styles);
  206. } else {
  207. adaptor.append(head, sheet);
  208. }
  209. }
  210. }
  211. /**
  212. * @param {N} head The document <head>
  213. * @param {string} id The id of the stylesheet to find
  214. * @param {N|null} The stylesheet with the given ID
  215. */
  216. protected findSheet(head: N, id: string) {
  217. if (id) {
  218. for (const sheet of this.adaptor.tags(head, 'style')) {
  219. if (this.adaptor.getAttribute(sheet, 'id') === id) {
  220. return sheet;
  221. }
  222. }
  223. }
  224. return null as N;
  225. }
  226. /**
  227. * @override
  228. */
  229. public removeFromDocument(restore: boolean = false) {
  230. if (this.processed.isSet('updateDocument')) {
  231. for (const math of this.math) {
  232. if (math.state() >= STATE.INSERTED) {
  233. math.state(STATE.TYPESET, restore);
  234. }
  235. }
  236. }
  237. this.processed.clear('updateDocument');
  238. return this;
  239. }
  240. /**
  241. * @override
  242. */
  243. public documentStyleSheet() {
  244. return this.outputJax.styleSheet(this);
  245. }
  246. /**
  247. * @override
  248. */
  249. public documentPageElements() {
  250. return this.outputJax.pageElements(this);
  251. }
  252. /**
  253. * Add styles to be included in the document's stylesheet
  254. *
  255. * @param {StyleList} styles The styles to include
  256. */
  257. public addStyles(styles: StyleList) {
  258. this.styles.push(styles);
  259. }
  260. /**
  261. * Get the array of document-specific styles
  262. */
  263. public getStyles() {
  264. return this.styles;
  265. }
  266. }