LazyHandler.ts 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718
  1. /*************************************************************
  2. *
  3. * Copyright (c) 2021-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 Mixin that implements lazy typesetting
  19. *
  20. * @author dpvc@mathjax.org (Davide Cervone)
  21. */
  22. import {MathDocumentConstructor, ContainerList} from '../../core/MathDocument.js';
  23. import {MathItem, STATE, newState} from '../../core/MathItem.js';
  24. import {HTMLMathItem} from '../../handlers/html/HTMLMathItem.js';
  25. import {HTMLDocument} from '../../handlers/html/HTMLDocument.js';
  26. import {HTMLHandler} from '../../handlers/html/HTMLHandler.js';
  27. import {handleRetriesFor} from '../../util/Retries.js';
  28. import {OptionList} from '../../util/Options.js';
  29. /**
  30. * Add the needed function to the window object.
  31. */
  32. declare const window: {
  33. requestIdleCallback: (callback: () => void) => void;
  34. addEventListener: ((type: string, handler: (event: Event) => void) => void);
  35. matchMedia: (type: string) => {
  36. addListener: (handler: (event: Event) => void) => void;
  37. };
  38. };
  39. /**
  40. * Generic constructor for Mixins
  41. */
  42. export type Constructor<T> = new(...args: any[]) => T;
  43. /**
  44. * A set of lazy MathItems
  45. */
  46. export type LazySet = Set<string>;
  47. /*==========================================================================*/
  48. /**
  49. * The data to map expression marker IDs back to their MathItem.
  50. */
  51. export class LazyList<N, T, D> {
  52. /**
  53. * The next ID to use
  54. */
  55. protected id: number = 0;
  56. /**
  57. * The map from IDs to MathItems
  58. */
  59. protected items: Map<string, LazyMathItem<N, T, D>> = new Map();
  60. /**
  61. * Add a MathItem to the list and return its ID
  62. *
  63. * @param {LazyMathItem} math The item to add
  64. * @return {string} The id for the newly added item
  65. */
  66. public add(math: LazyMathItem<N, T, D>): string {
  67. const id = String(this.id++);
  68. this.items.set(id, math);
  69. return id;
  70. }
  71. /**
  72. * Get the MathItem with the given ID
  73. *
  74. * @param {string} id The ID of the MathItem to get
  75. * @return {LazyMathItem} The MathItem having that ID (if any)
  76. */
  77. public get(id: string): LazyMathItem<N, T, D> {
  78. return this.items.get(id);
  79. }
  80. /**
  81. * Remove an item from the map
  82. *
  83. * @param {string} id The ID of the MathItem to remove
  84. */
  85. public delete(id: string) {
  86. return this.items.delete(id);
  87. }
  88. }
  89. /*==========================================================================*/
  90. newState('LAZYALWAYS', STATE.FINDMATH + 3);
  91. /**
  92. * The attribute to use for the ID on the marker node
  93. */
  94. export const LAZYID = 'data-mjx-lazy';
  95. /**
  96. * The properties added to MathItem for lazy typesetting
  97. *
  98. * @template N The HTMLElement node class
  99. * @template T The Text node class
  100. * @template D The Document class
  101. */
  102. export interface LazyMathItem<N, T, D> extends MathItem<N, T, D> {
  103. /**
  104. * True when the MathItem needs to be lazy compiled
  105. */
  106. lazyCompile: boolean;
  107. /**
  108. * True when the MathItem needs to be lazy displayed
  109. */
  110. lazyTypeset: boolean;
  111. /**
  112. * The DOM node used to mark the location of the math to be lazy typeset
  113. */
  114. lazyMarker: N;
  115. /**
  116. * True if this item is a TeX MathItem
  117. */
  118. lazyTex: boolean;
  119. }
  120. /**
  121. * The mixin for adding lazy typesetting to MathItems
  122. *
  123. * @param {B} BaseMathItem The MathItem class to be extended
  124. * @return {AssistiveMathItem} The augmented MathItem class
  125. *
  126. * @template N The HTMLElement node class
  127. * @template T The Text node class
  128. * @template D The Document class
  129. * @template B The MathItem class to extend
  130. */
  131. export function LazyMathItemMixin<N, T, D, B extends Constructor<HTMLMathItem<N, T, D>>>(
  132. BaseMathItem: B
  133. ): Constructor<LazyMathItem<N, T, D>> & B {
  134. return class extends BaseMathItem {
  135. /**
  136. * True when this item should be skipped during compilation
  137. * (i.e., it is not showing on screen, and has not needed to be
  138. * compiled for a later TeX expression that is showing).
  139. */
  140. public lazyCompile: boolean = true;
  141. /**
  142. * True when this item should be skipped during typesetting
  143. * (i.e., it has not yet appeared on screen).
  144. */
  145. public lazyTypeset: boolean = true;
  146. /**
  147. * The marker DOM node for this item.
  148. */
  149. public lazyMarker: N;
  150. /**
  151. * True if this is a TeX expression.
  152. */
  153. public lazyTex: boolean = false;
  154. /**
  155. * @override
  156. */
  157. constructor(...args: any[]) {
  158. super(...args);
  159. if (!this.end.node) {
  160. //
  161. // This is a MathItem that isn't in the document
  162. // (so either from semantic enrich, or from convert())
  163. // and should be typeset as usual.
  164. //
  165. this.lazyCompile = this.lazyTypeset = false;
  166. }
  167. }
  168. /**
  169. * Initially don't compile math, just use an empty math item,
  170. * then when the math comes into view (or is before something
  171. * that comes into view), compile it properly and mark the item
  172. * as only needing to be typeset.
  173. *
  174. * @override
  175. */
  176. public compile(document: LazyMathDocument<N, T, D>) {
  177. if (!this.lazyCompile) {
  178. super.compile(document);
  179. return;
  180. }
  181. if (this.state() < STATE.COMPILED) {
  182. this.lazyTex = (this.inputJax.name === 'TeX');
  183. this.root = document.mmlFactory.create('math');
  184. this.state(STATE.COMPILED);
  185. }
  186. }
  187. /**
  188. * Only update the state if restore is true or false (null is used for setting lazy states)
  189. * @override
  190. */
  191. public state(state: number = null, restore: boolean = false) {
  192. if (restore !== null) super.state(state, restore);
  193. return super.state();
  194. }
  195. /**
  196. * Initially, just insert a marker for where the math will go, and
  197. * track it in the lazy list. Then, when it comes into view,
  198. * typeset it properly.
  199. *
  200. * @override
  201. */
  202. public typeset(document: LazyMathDocument<N, T, D>) {
  203. if (!this.lazyTypeset) {
  204. super.typeset(document);
  205. return;
  206. }
  207. if (this.state() < STATE.TYPESET) {
  208. const adaptor = document.adaptor;
  209. if (!this.lazyMarker) {
  210. const id = document.lazyList.add(this);
  211. this.lazyMarker = adaptor.node('mjx-lazy', {[LAZYID]: id});
  212. this.typesetRoot = adaptor.node('mjx-container', {}, [this.lazyMarker]);
  213. }
  214. this.state(STATE.TYPESET);
  215. }
  216. }
  217. /**
  218. * When the MathItem is added to the page, set the observer to watch
  219. * for it coming into view so that it can be typeset.
  220. *
  221. * @override
  222. */
  223. public updateDocument(document: LazyMathDocument<N, T, D>) {
  224. super.updateDocument(document);
  225. if (this.lazyTypeset) {
  226. document.lazyObserver.observe(this.lazyMarker as any as Element);
  227. }
  228. }
  229. };
  230. }
  231. /*==========================================================================*/
  232. /**
  233. * The properties added to MathDocument for lazy typesetting
  234. *
  235. * @template N The HTMLElement node class
  236. * @template T The Text node class
  237. * @template D The Document class
  238. */
  239. export interface LazyMathDocument<N, T, D> extends HTMLDocument<N, T, D> {
  240. /**
  241. * The Intersection Observer used to track the appearance of the expression markers
  242. */
  243. lazyObserver: IntersectionObserver;
  244. /**
  245. * The mapping of markers to MathItems
  246. */
  247. lazyList: LazyList<N, T, D>;
  248. /**
  249. * The containers whose contents should always be typeset
  250. */
  251. lazyAlwaysContainers: N[];
  252. /**
  253. * A function that will typeset all the remaining expressions (e.g., for printing)
  254. */
  255. lazyTypesetAll(): Promise<void>;
  256. /**
  257. * Mark the math items that are to be always typeset
  258. */
  259. lazyAlways(): void;
  260. }
  261. /**
  262. * The mixin for adding lazy typesetting to MathDocuments
  263. *
  264. * @param {B} BaseDocument The MathDocument class to be extended
  265. * @return {LazyMathDocument} The Lazy MathDocument class
  266. *
  267. * @template N The HTMLElement node class
  268. * @template T The Text node class
  269. * @template D The Document class
  270. * @template B The MathDocument class to extend
  271. */
  272. export function LazyMathDocumentMixin<N, T, D,
  273. B extends MathDocumentConstructor<HTMLDocument<N, T, D>>>(
  274. BaseDocument: B
  275. ): MathDocumentConstructor<HTMLDocument<N, T, D>> & B {
  276. return class BaseClass extends BaseDocument {
  277. /**
  278. * @override
  279. */
  280. public static OPTIONS: OptionList = {
  281. ...BaseDocument.OPTIONS,
  282. lazyMargin: '200px',
  283. lazyAlwaysTypeset: null,
  284. renderActions: {
  285. ...BaseDocument.OPTIONS.renderActions,
  286. lazyAlways: [STATE.LAZYALWAYS, 'lazyAlways', '', false]
  287. }
  288. };
  289. /**
  290. * The Intersection Observer used to track the appearance of the expression markers
  291. */
  292. public lazyObserver: IntersectionObserver;
  293. /**
  294. * The mapping of markers to MathItems
  295. */
  296. public lazyList: LazyList<N, T, D>;
  297. /**
  298. * The containers whose contents should always be typeset
  299. */
  300. public lazyAlwaysContainers: N[] = null;
  301. /**
  302. * Index of last container where math was found in lazyAlwaysContainers
  303. */
  304. public lazyAlwaysIndex: number = 0;
  305. /**
  306. * A promise to make sure our compiling/typesetting is sequential
  307. */
  308. protected lazyPromise: Promise<void> = Promise.resolve();
  309. /**
  310. * The function used to typeset a set of lazy MathItems.
  311. * (uses requestIdleCallback if available, or setTimeout otherwise)
  312. */
  313. protected lazyProcessSet: () => void;
  314. /**
  315. * True when a set of MathItems is queued for being processed
  316. */
  317. protected lazyIdle: boolean = false;
  318. /**
  319. * The set of items that have come into view
  320. */
  321. protected lazySet: LazySet = new Set();
  322. /**
  323. * Augment the MathItem class used for this MathDocument,
  324. * then create the intersection observer and lazy list,
  325. * and bind the lazyProcessSet function to this instance
  326. * so it can be used as a callback more easily. Add the
  327. * event listeners to typeset everything before printing.
  328. *
  329. * @override
  330. * @constructor
  331. */
  332. constructor(...args: any[]) {
  333. super(...args);
  334. //
  335. // Use the LazyMathItem for math items
  336. //
  337. this.options.MathItem =
  338. LazyMathItemMixin<N, T, D, Constructor<HTMLMathItem<N, T, D>>>(this.options.MathItem);
  339. //
  340. // Allocate a process bit for lazyAlways
  341. //
  342. const ProcessBits = (this.constructor as typeof HTMLDocument).ProcessBits;
  343. !ProcessBits.has('lazyAlways') && ProcessBits.allocate('lazyAlways');
  344. //
  345. // Set up the lazy observer and other needed data
  346. //
  347. this.lazyObserver = new IntersectionObserver(this.lazyObserve.bind(this), {rootMargin: this.options.lazyMargin});
  348. this.lazyList = new LazyList<N, T, D>();
  349. const callback = this.lazyHandleSet.bind(this);
  350. this.lazyProcessSet = (window && window.requestIdleCallback ?
  351. () => window.requestIdleCallback(callback) :
  352. () => setTimeout(callback, 10));
  353. //
  354. // Install print listeners to typeset the rest of the document before printing
  355. //
  356. if (window) {
  357. let done = false;
  358. const handler = () => {
  359. !done && this.lazyTypesetAll();
  360. done = true;
  361. };
  362. window.matchMedia('print').addListener(handler); // for Safari
  363. window.addEventListener('beforeprint', handler); // for everyone else
  364. }
  365. }
  366. /**
  367. * Check all math items for those that should always be typeset
  368. */
  369. public lazyAlways() {
  370. if (!this.lazyAlwaysContainers || this.processed.isSet('lazyAlways')) return;
  371. for (const item of this.math) {
  372. const math = item as LazyMathItem<N, T, D>;
  373. if (math.lazyTypeset && this.lazyIsAlways(math)) {
  374. math.lazyCompile = math.lazyTypeset = false;
  375. }
  376. }
  377. this.processed.set('lazyAlways');
  378. }
  379. /**
  380. * Check if the MathItem is in one of the containers to always typeset.
  381. * (start looking using the last container where math was found,
  382. * in case the next math is in the same container).
  383. *
  384. * @param {LazyMathItem<N,T,D>} math The MathItem to test
  385. * @return {boolean} True if one of the document's containers holds the MathItem
  386. */
  387. protected lazyIsAlways(math: LazyMathItem<N, T, D>): boolean {
  388. if (math.state() < STATE.LAZYALWAYS) {
  389. math.state(STATE.LAZYALWAYS);
  390. const node = math.start.node;
  391. const adaptor = this.adaptor;
  392. const start = this.lazyAlwaysIndex;
  393. const end = this.lazyAlwaysContainers.length;
  394. do {
  395. const container = this.lazyAlwaysContainers[this.lazyAlwaysIndex];
  396. if (adaptor.contains(container, node)) return true;
  397. if (++this.lazyAlwaysIndex >= end) {
  398. this.lazyAlwaysIndex = 0;
  399. }
  400. } while (this.lazyAlwaysIndex !== start);
  401. }
  402. return false;
  403. }
  404. /**
  405. * @override
  406. */
  407. public state(state: number, restore: boolean = false) {
  408. super.state(state, restore);
  409. if (state < STATE.LAZYALWAYS) {
  410. this.processed.clear('lazyAlways');
  411. }
  412. return this;
  413. }
  414. /**
  415. * Function to typeset all remaining expressions (for printing, etc.)
  416. *
  417. * @return {Promise} Promise that is resolved after the typesetting completes.
  418. */
  419. public async lazyTypesetAll(): Promise<void> {
  420. //
  421. // The state we need to go back to (COMPILED or TYPESET).
  422. //
  423. let state = STATE.LAST;
  424. //
  425. // Loop through all the math...
  426. //
  427. for (const item of this.math) {
  428. const math = item as LazyMathItem<N, T, D>;
  429. //
  430. // If it is not lazy compile or typeset, skip it.
  431. //
  432. if (!math.lazyCompile && !math.lazyTypeset) continue;
  433. //
  434. // Mark the state that we need to start at.
  435. //
  436. if (math.lazyCompile) {
  437. math.state(STATE.COMPILED - 1);
  438. state = STATE.COMPILED;
  439. } else {
  440. math.state(STATE.TYPESET - 1);
  441. if (STATE.TYPESET < state) state = STATE.TYPESET;
  442. }
  443. //
  444. // Mark it as not lazy and remove it from the observer.
  445. //
  446. math.lazyCompile = math.lazyTypeset = false;
  447. math.lazyMarker && this.lazyObserver.unobserve(math.lazyMarker as any as Element);
  448. }
  449. //
  450. // If something needs updating
  451. //
  452. if (state === STATE.LAST) return Promise.resolve();
  453. //
  454. // Reset the document state to the starting state that we need.
  455. //
  456. this.state(state - 1, null);
  457. //
  458. // Save the SVG font cache and set it to "none" temporarily
  459. // (needed by Firefox, which doesn't seem to process the
  460. // xlinks otherwise).
  461. //
  462. const fontCache = this.outputJax.options.fontCache;
  463. if (fontCache) this.outputJax.options.fontCache = 'none';
  464. //
  465. // Typeset the math and put back the font cache when done.
  466. //
  467. this.reset();
  468. return handleRetriesFor(() => this.render()).then(() => {
  469. if (fontCache) this.outputJax.options.fontCache = fontCache;
  470. });
  471. }
  472. /**
  473. * The function used by the IntersectionObserver to monitor the markers coming into view.
  474. * When one (or more) does, add its math item to or remove it from the set to be processed, and
  475. * if added to the set, queue an idle task, if one isn't already pending.
  476. *
  477. * The idea is, if you start scrolling and reveal an expression marker, we add it into
  478. * the set and queue an idle task. But if you keep scrolling and the idle task hasn't run
  479. * yet, the mark may move out of view, and we don't want to waste time typesetting it, so
  480. * we remove it from the set. When you stop scrolling and the idle task finally runs, it
  481. * takes the expressions still in the set (those should be the ones still in view) and
  482. * starts typesetting them after having created a new set to add expressions to. If one
  483. * of the expressions loads an extension and the idle task has to pause, you can add new
  484. * expressions into the new list (or remove them from there), but can't affect the
  485. * current idle-task list. Those will be typeset even if they scroll out of view at that
  486. * point.
  487. *
  488. * Note that it is possible that an expression is added to the set and then removed
  489. * before the idle task runs, and it could be that the set is empty when it does. Then
  490. * the idle task does nothing, and new expressions are added to the new set to be
  491. * processed by the next idle task.
  492. *
  493. * @param {IntersectionObserverEntry[]} entries The markers that have come into or out of view.
  494. */
  495. protected lazyObserve(entries: IntersectionObserverEntry[]) {
  496. for (const entry of entries) {
  497. const id = this.adaptor.getAttribute(entry.target as any as N, LAZYID);
  498. const math = this.lazyList.get(id);
  499. if (!math) continue;
  500. if (!entry.isIntersecting) {
  501. this.lazySet.delete(id);
  502. continue;
  503. }
  504. this.lazySet.add(id);
  505. if (!this.lazyIdle) {
  506. this.lazyIdle = true;
  507. this.lazyProcessSet();
  508. }
  509. }
  510. }
  511. /**
  512. * Mark the MathItems in the set as needing compiling or typesetting,
  513. * and for TeX items, make sure the earlier TeX items are typeset
  514. * (in case they have automatic numbers, or define macros, etc.).
  515. * Then rerender the page to update the visible equations.
  516. */
  517. protected lazyHandleSet() {
  518. const set = this.lazySet;
  519. this.lazySet = new Set();
  520. this.lazyPromise = this.lazyPromise.then(() => {
  521. let state = this.compileEarlierItems(set) ? STATE.COMPILED : STATE.TYPESET;
  522. state = this.resetStates(set, state);
  523. this.state(state - 1, null); // reset processed bits to allow reprocessing
  524. return handleRetriesFor(() => {
  525. this.render();
  526. this.lazyIdle = false;
  527. });
  528. });
  529. }
  530. /**
  531. * Set the states of the MathItems in the set, depending on
  532. * whether they need compiling or just typesetting, and
  533. * update the state needed for the page rerendering.
  534. *
  535. * @param {LazySet} set The set of math items to update
  536. * @param {number} state The state needed for the items
  537. * @return {number} The updated state based on the items
  538. */
  539. protected resetStates(set: LazySet, state: number): number {
  540. for (const id of set.values()) {
  541. const math = this.lazyList.get(id);
  542. if (math.lazyCompile) {
  543. math.state(STATE.COMPILED - 1);
  544. state = STATE.COMPILED;
  545. } else {
  546. math.state(STATE.TYPESET - 1);
  547. }
  548. math.lazyCompile = math.lazyTypeset = false;
  549. math.lazyMarker && this.lazyObserver.unobserve(math.lazyMarker as any as Element);
  550. }
  551. return state;
  552. }
  553. /**
  554. * Mark any TeX items (earlier than the ones in the set) to be compiled.
  555. *
  556. * @param {LazySet} set The set of items that are newly visible
  557. * @return {boolean} True if there are TeX items to be typeset
  558. */
  559. protected compileEarlierItems(set: LazySet): boolean {
  560. let math = this.earliestTex(set);
  561. if (!math) return false;
  562. let compile = false;
  563. for (const item of this.math) {
  564. const earlier = item as LazyMathItem<N, T, D>;
  565. if (earlier === math || !earlier?.lazyCompile || !earlier.lazyTex) {
  566. break;
  567. }
  568. earlier.lazyCompile = false;
  569. earlier.lazyMarker && this.lazyObserver.unobserve(earlier.lazyMarker as any as Element);
  570. earlier.state(STATE.COMPILED - 1);
  571. compile = true;
  572. }
  573. return compile;
  574. }
  575. /**
  576. * Find the earliest TeX math item in the set, if any.
  577. *
  578. * @param {LazySet} set The set of newly visble math items
  579. * @return {LazyMathItem} The earliest TeX math item in the set, if any
  580. */
  581. protected earliestTex(set: LazySet): LazyMathItem<N, T, D> {
  582. let min: number = null;
  583. let minMath = null;
  584. for (const id of set.values()) {
  585. const math = this.lazyList.get(id);
  586. if (!math.lazyTex) continue;
  587. if (min === null || parseInt(id) < min) {
  588. min = parseInt(id);
  589. minMath = math;
  590. }
  591. }
  592. return minMath;
  593. }
  594. /**
  595. * If any of the removed items are observed or in the lazy list, remove them.
  596. *
  597. * @override
  598. */
  599. public clearMathItemsWithin(containers: ContainerList<N>) {
  600. const items = super.clearMathItemsWithin(containers) as LazyMathItem<N, T, D>[];
  601. for (const math of items) {
  602. const marker = math.lazyMarker;
  603. if (marker) {
  604. this.lazyObserver.unobserve(marker as any as Element);
  605. this.lazyList.delete(this.adaptor.getAttribute(marker, LAZYID));
  606. }
  607. }
  608. return items;
  609. }
  610. /**
  611. * @override
  612. */
  613. public render() {
  614. //
  615. // Get the containers whose content should always be typeset
  616. //
  617. const always = this.options.lazyAlwaysTypeset;
  618. this.lazyAlwaysContainers = !always ? null :
  619. this.adaptor.getElements(Array.isArray(always) ? always : [always], this.document);
  620. this.lazyAlwaysIndex = 0;
  621. super.render();
  622. return this;
  623. }
  624. };
  625. }
  626. /*==========================================================================*/
  627. /**
  628. * Add lazy typesetting support to a Handler instance
  629. *
  630. * @param {Handler} handler The Handler instance to enhance
  631. * @return {Handler} The handler that was modified (for purposes of chaining extensions)
  632. *
  633. * @template N The HTMLElement node class
  634. * @template T The Text node class
  635. * @template D The Document class
  636. */
  637. export function LazyHandler<N, T, D>(handler: HTMLHandler<N, T, D>): HTMLHandler<N, T, D> {
  638. //
  639. // Only update the document class if we can handle IntersectionObservers
  640. //
  641. if (typeof IntersectionObserver !== 'undefined') {
  642. handler.documentClass =
  643. LazyMathDocumentMixin<N, T, D, MathDocumentConstructor<HTMLDocument<N, T, D>>>(
  644. handler.documentClass
  645. ) as typeof HTMLDocument;
  646. }
  647. return handler;
  648. }