123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718 |
- /*************************************************************
- *
- * Copyright (c) 2021-2022 The MathJax Consortium
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * @fileoverview Mixin that implements lazy typesetting
- *
- * @author dpvc@mathjax.org (Davide Cervone)
- */
- import {MathDocumentConstructor, ContainerList} from '../../core/MathDocument.js';
- import {MathItem, STATE, newState} from '../../core/MathItem.js';
- import {HTMLMathItem} from '../../handlers/html/HTMLMathItem.js';
- import {HTMLDocument} from '../../handlers/html/HTMLDocument.js';
- import {HTMLHandler} from '../../handlers/html/HTMLHandler.js';
- import {handleRetriesFor} from '../../util/Retries.js';
- import {OptionList} from '../../util/Options.js';
- /**
- * Add the needed function to the window object.
- */
- declare const window: {
- requestIdleCallback: (callback: () => void) => void;
- addEventListener: ((type: string, handler: (event: Event) => void) => void);
- matchMedia: (type: string) => {
- addListener: (handler: (event: Event) => void) => void;
- };
- };
- /**
- * Generic constructor for Mixins
- */
- export type Constructor<T> = new(...args: any[]) => T;
- /**
- * A set of lazy MathItems
- */
- export type LazySet = Set<string>;
- /*==========================================================================*/
- /**
- * The data to map expression marker IDs back to their MathItem.
- */
- export class LazyList<N, T, D> {
- /**
- * The next ID to use
- */
- protected id: number = 0;
- /**
- * The map from IDs to MathItems
- */
- protected items: Map<string, LazyMathItem<N, T, D>> = new Map();
- /**
- * Add a MathItem to the list and return its ID
- *
- * @param {LazyMathItem} math The item to add
- * @return {string} The id for the newly added item
- */
- public add(math: LazyMathItem<N, T, D>): string {
- const id = String(this.id++);
- this.items.set(id, math);
- return id;
- }
- /**
- * Get the MathItem with the given ID
- *
- * @param {string} id The ID of the MathItem to get
- * @return {LazyMathItem} The MathItem having that ID (if any)
- */
- public get(id: string): LazyMathItem<N, T, D> {
- return this.items.get(id);
- }
- /**
- * Remove an item from the map
- *
- * @param {string} id The ID of the MathItem to remove
- */
- public delete(id: string) {
- return this.items.delete(id);
- }
- }
- /*==========================================================================*/
- newState('LAZYALWAYS', STATE.FINDMATH + 3);
- /**
- * The attribute to use for the ID on the marker node
- */
- export const LAZYID = 'data-mjx-lazy';
- /**
- * The properties added to MathItem for lazy typesetting
- *
- * @template N The HTMLElement node class
- * @template T The Text node class
- * @template D The Document class
- */
- export interface LazyMathItem<N, T, D> extends MathItem<N, T, D> {
- /**
- * True when the MathItem needs to be lazy compiled
- */
- lazyCompile: boolean;
- /**
- * True when the MathItem needs to be lazy displayed
- */
- lazyTypeset: boolean;
- /**
- * The DOM node used to mark the location of the math to be lazy typeset
- */
- lazyMarker: N;
- /**
- * True if this item is a TeX MathItem
- */
- lazyTex: boolean;
- }
- /**
- * The mixin for adding lazy typesetting to MathItems
- *
- * @param {B} BaseMathItem The MathItem class to be extended
- * @return {AssistiveMathItem} The augmented MathItem class
- *
- * @template N The HTMLElement node class
- * @template T The Text node class
- * @template D The Document class
- * @template B The MathItem class to extend
- */
- export function LazyMathItemMixin<N, T, D, B extends Constructor<HTMLMathItem<N, T, D>>>(
- BaseMathItem: B
- ): Constructor<LazyMathItem<N, T, D>> & B {
- return class extends BaseMathItem {
- /**
- * True when this item should be skipped during compilation
- * (i.e., it is not showing on screen, and has not needed to be
- * compiled for a later TeX expression that is showing).
- */
- public lazyCompile: boolean = true;
- /**
- * True when this item should be skipped during typesetting
- * (i.e., it has not yet appeared on screen).
- */
- public lazyTypeset: boolean = true;
- /**
- * The marker DOM node for this item.
- */
- public lazyMarker: N;
- /**
- * True if this is a TeX expression.
- */
- public lazyTex: boolean = false;
- /**
- * @override
- */
- constructor(...args: any[]) {
- super(...args);
- if (!this.end.node) {
- //
- // This is a MathItem that isn't in the document
- // (so either from semantic enrich, or from convert())
- // and should be typeset as usual.
- //
- this.lazyCompile = this.lazyTypeset = false;
- }
- }
- /**
- * Initially don't compile math, just use an empty math item,
- * then when the math comes into view (or is before something
- * that comes into view), compile it properly and mark the item
- * as only needing to be typeset.
- *
- * @override
- */
- public compile(document: LazyMathDocument<N, T, D>) {
- if (!this.lazyCompile) {
- super.compile(document);
- return;
- }
- if (this.state() < STATE.COMPILED) {
- this.lazyTex = (this.inputJax.name === 'TeX');
- this.root = document.mmlFactory.create('math');
- this.state(STATE.COMPILED);
- }
- }
- /**
- * Only update the state if restore is true or false (null is used for setting lazy states)
- * @override
- */
- public state(state: number = null, restore: boolean = false) {
- if (restore !== null) super.state(state, restore);
- return super.state();
- }
- /**
- * Initially, just insert a marker for where the math will go, and
- * track it in the lazy list. Then, when it comes into view,
- * typeset it properly.
- *
- * @override
- */
- public typeset(document: LazyMathDocument<N, T, D>) {
- if (!this.lazyTypeset) {
- super.typeset(document);
- return;
- }
- if (this.state() < STATE.TYPESET) {
- const adaptor = document.adaptor;
- if (!this.lazyMarker) {
- const id = document.lazyList.add(this);
- this.lazyMarker = adaptor.node('mjx-lazy', {[LAZYID]: id});
- this.typesetRoot = adaptor.node('mjx-container', {}, [this.lazyMarker]);
- }
- this.state(STATE.TYPESET);
- }
- }
- /**
- * When the MathItem is added to the page, set the observer to watch
- * for it coming into view so that it can be typeset.
- *
- * @override
- */
- public updateDocument(document: LazyMathDocument<N, T, D>) {
- super.updateDocument(document);
- if (this.lazyTypeset) {
- document.lazyObserver.observe(this.lazyMarker as any as Element);
- }
- }
- };
- }
- /*==========================================================================*/
- /**
- * The properties added to MathDocument for lazy typesetting
- *
- * @template N The HTMLElement node class
- * @template T The Text node class
- * @template D The Document class
- */
- export interface LazyMathDocument<N, T, D> extends HTMLDocument<N, T, D> {
- /**
- * The Intersection Observer used to track the appearance of the expression markers
- */
- lazyObserver: IntersectionObserver;
- /**
- * The mapping of markers to MathItems
- */
- lazyList: LazyList<N, T, D>;
- /**
- * The containers whose contents should always be typeset
- */
- lazyAlwaysContainers: N[];
- /**
- * A function that will typeset all the remaining expressions (e.g., for printing)
- */
- lazyTypesetAll(): Promise<void>;
- /**
- * Mark the math items that are to be always typeset
- */
- lazyAlways(): void;
- }
- /**
- * The mixin for adding lazy typesetting to MathDocuments
- *
- * @param {B} BaseDocument The MathDocument class to be extended
- * @return {LazyMathDocument} The Lazy MathDocument class
- *
- * @template N The HTMLElement node class
- * @template T The Text node class
- * @template D The Document class
- * @template B The MathDocument class to extend
- */
- export function LazyMathDocumentMixin<N, T, D,
- B extends MathDocumentConstructor<HTMLDocument<N, T, D>>>(
- BaseDocument: B
- ): MathDocumentConstructor<HTMLDocument<N, T, D>> & B {
- return class BaseClass extends BaseDocument {
- /**
- * @override
- */
- public static OPTIONS: OptionList = {
- ...BaseDocument.OPTIONS,
- lazyMargin: '200px',
- lazyAlwaysTypeset: null,
- renderActions: {
- ...BaseDocument.OPTIONS.renderActions,
- lazyAlways: [STATE.LAZYALWAYS, 'lazyAlways', '', false]
- }
- };
- /**
- * The Intersection Observer used to track the appearance of the expression markers
- */
- public lazyObserver: IntersectionObserver;
- /**
- * The mapping of markers to MathItems
- */
- public lazyList: LazyList<N, T, D>;
- /**
- * The containers whose contents should always be typeset
- */
- public lazyAlwaysContainers: N[] = null;
- /**
- * Index of last container where math was found in lazyAlwaysContainers
- */
- public lazyAlwaysIndex: number = 0;
- /**
- * A promise to make sure our compiling/typesetting is sequential
- */
- protected lazyPromise: Promise<void> = Promise.resolve();
- /**
- * The function used to typeset a set of lazy MathItems.
- * (uses requestIdleCallback if available, or setTimeout otherwise)
- */
- protected lazyProcessSet: () => void;
- /**
- * True when a set of MathItems is queued for being processed
- */
- protected lazyIdle: boolean = false;
- /**
- * The set of items that have come into view
- */
- protected lazySet: LazySet = new Set();
- /**
- * Augment the MathItem class used for this MathDocument,
- * then create the intersection observer and lazy list,
- * and bind the lazyProcessSet function to this instance
- * so it can be used as a callback more easily. Add the
- * event listeners to typeset everything before printing.
- *
- * @override
- * @constructor
- */
- constructor(...args: any[]) {
- super(...args);
- //
- // Use the LazyMathItem for math items
- //
- this.options.MathItem =
- LazyMathItemMixin<N, T, D, Constructor<HTMLMathItem<N, T, D>>>(this.options.MathItem);
- //
- // Allocate a process bit for lazyAlways
- //
- const ProcessBits = (this.constructor as typeof HTMLDocument).ProcessBits;
- !ProcessBits.has('lazyAlways') && ProcessBits.allocate('lazyAlways');
- //
- // Set up the lazy observer and other needed data
- //
- this.lazyObserver = new IntersectionObserver(this.lazyObserve.bind(this), {rootMargin: this.options.lazyMargin});
- this.lazyList = new LazyList<N, T, D>();
- const callback = this.lazyHandleSet.bind(this);
- this.lazyProcessSet = (window && window.requestIdleCallback ?
- () => window.requestIdleCallback(callback) :
- () => setTimeout(callback, 10));
- //
- // Install print listeners to typeset the rest of the document before printing
- //
- if (window) {
- let done = false;
- const handler = () => {
- !done && this.lazyTypesetAll();
- done = true;
- };
- window.matchMedia('print').addListener(handler); // for Safari
- window.addEventListener('beforeprint', handler); // for everyone else
- }
- }
- /**
- * Check all math items for those that should always be typeset
- */
- public lazyAlways() {
- if (!this.lazyAlwaysContainers || this.processed.isSet('lazyAlways')) return;
- for (const item of this.math) {
- const math = item as LazyMathItem<N, T, D>;
- if (math.lazyTypeset && this.lazyIsAlways(math)) {
- math.lazyCompile = math.lazyTypeset = false;
- }
- }
- this.processed.set('lazyAlways');
- }
- /**
- * Check if the MathItem is in one of the containers to always typeset.
- * (start looking using the last container where math was found,
- * in case the next math is in the same container).
- *
- * @param {LazyMathItem<N,T,D>} math The MathItem to test
- * @return {boolean} True if one of the document's containers holds the MathItem
- */
- protected lazyIsAlways(math: LazyMathItem<N, T, D>): boolean {
- if (math.state() < STATE.LAZYALWAYS) {
- math.state(STATE.LAZYALWAYS);
- const node = math.start.node;
- const adaptor = this.adaptor;
- const start = this.lazyAlwaysIndex;
- const end = this.lazyAlwaysContainers.length;
- do {
- const container = this.lazyAlwaysContainers[this.lazyAlwaysIndex];
- if (adaptor.contains(container, node)) return true;
- if (++this.lazyAlwaysIndex >= end) {
- this.lazyAlwaysIndex = 0;
- }
- } while (this.lazyAlwaysIndex !== start);
- }
- return false;
- }
- /**
- * @override
- */
- public state(state: number, restore: boolean = false) {
- super.state(state, restore);
- if (state < STATE.LAZYALWAYS) {
- this.processed.clear('lazyAlways');
- }
- return this;
- }
- /**
- * Function to typeset all remaining expressions (for printing, etc.)
- *
- * @return {Promise} Promise that is resolved after the typesetting completes.
- */
- public async lazyTypesetAll(): Promise<void> {
- //
- // The state we need to go back to (COMPILED or TYPESET).
- //
- let state = STATE.LAST;
- //
- // Loop through all the math...
- //
- for (const item of this.math) {
- const math = item as LazyMathItem<N, T, D>;
- //
- // If it is not lazy compile or typeset, skip it.
- //
- if (!math.lazyCompile && !math.lazyTypeset) continue;
- //
- // Mark the state that we need to start at.
- //
- if (math.lazyCompile) {
- math.state(STATE.COMPILED - 1);
- state = STATE.COMPILED;
- } else {
- math.state(STATE.TYPESET - 1);
- if (STATE.TYPESET < state) state = STATE.TYPESET;
- }
- //
- // Mark it as not lazy and remove it from the observer.
- //
- math.lazyCompile = math.lazyTypeset = false;
- math.lazyMarker && this.lazyObserver.unobserve(math.lazyMarker as any as Element);
- }
- //
- // If something needs updating
- //
- if (state === STATE.LAST) return Promise.resolve();
- //
- // Reset the document state to the starting state that we need.
- //
- this.state(state - 1, null);
- //
- // Save the SVG font cache and set it to "none" temporarily
- // (needed by Firefox, which doesn't seem to process the
- // xlinks otherwise).
- //
- const fontCache = this.outputJax.options.fontCache;
- if (fontCache) this.outputJax.options.fontCache = 'none';
- //
- // Typeset the math and put back the font cache when done.
- //
- this.reset();
- return handleRetriesFor(() => this.render()).then(() => {
- if (fontCache) this.outputJax.options.fontCache = fontCache;
- });
- }
- /**
- * The function used by the IntersectionObserver to monitor the markers coming into view.
- * When one (or more) does, add its math item to or remove it from the set to be processed, and
- * if added to the set, queue an idle task, if one isn't already pending.
- *
- * The idea is, if you start scrolling and reveal an expression marker, we add it into
- * the set and queue an idle task. But if you keep scrolling and the idle task hasn't run
- * yet, the mark may move out of view, and we don't want to waste time typesetting it, so
- * we remove it from the set. When you stop scrolling and the idle task finally runs, it
- * takes the expressions still in the set (those should be the ones still in view) and
- * starts typesetting them after having created a new set to add expressions to. If one
- * of the expressions loads an extension and the idle task has to pause, you can add new
- * expressions into the new list (or remove them from there), but can't affect the
- * current idle-task list. Those will be typeset even if they scroll out of view at that
- * point.
- *
- * Note that it is possible that an expression is added to the set and then removed
- * before the idle task runs, and it could be that the set is empty when it does. Then
- * the idle task does nothing, and new expressions are added to the new set to be
- * processed by the next idle task.
- *
- * @param {IntersectionObserverEntry[]} entries The markers that have come into or out of view.
- */
- protected lazyObserve(entries: IntersectionObserverEntry[]) {
- for (const entry of entries) {
- const id = this.adaptor.getAttribute(entry.target as any as N, LAZYID);
- const math = this.lazyList.get(id);
- if (!math) continue;
- if (!entry.isIntersecting) {
- this.lazySet.delete(id);
- continue;
- }
- this.lazySet.add(id);
- if (!this.lazyIdle) {
- this.lazyIdle = true;
- this.lazyProcessSet();
- }
- }
- }
- /**
- * Mark the MathItems in the set as needing compiling or typesetting,
- * and for TeX items, make sure the earlier TeX items are typeset
- * (in case they have automatic numbers, or define macros, etc.).
- * Then rerender the page to update the visible equations.
- */
- protected lazyHandleSet() {
- const set = this.lazySet;
- this.lazySet = new Set();
- this.lazyPromise = this.lazyPromise.then(() => {
- let state = this.compileEarlierItems(set) ? STATE.COMPILED : STATE.TYPESET;
- state = this.resetStates(set, state);
- this.state(state - 1, null); // reset processed bits to allow reprocessing
- return handleRetriesFor(() => {
- this.render();
- this.lazyIdle = false;
- });
- });
- }
- /**
- * Set the states of the MathItems in the set, depending on
- * whether they need compiling or just typesetting, and
- * update the state needed for the page rerendering.
- *
- * @param {LazySet} set The set of math items to update
- * @param {number} state The state needed for the items
- * @return {number} The updated state based on the items
- */
- protected resetStates(set: LazySet, state: number): number {
- for (const id of set.values()) {
- const math = this.lazyList.get(id);
- if (math.lazyCompile) {
- math.state(STATE.COMPILED - 1);
- state = STATE.COMPILED;
- } else {
- math.state(STATE.TYPESET - 1);
- }
- math.lazyCompile = math.lazyTypeset = false;
- math.lazyMarker && this.lazyObserver.unobserve(math.lazyMarker as any as Element);
- }
- return state;
- }
- /**
- * Mark any TeX items (earlier than the ones in the set) to be compiled.
- *
- * @param {LazySet} set The set of items that are newly visible
- * @return {boolean} True if there are TeX items to be typeset
- */
- protected compileEarlierItems(set: LazySet): boolean {
- let math = this.earliestTex(set);
- if (!math) return false;
- let compile = false;
- for (const item of this.math) {
- const earlier = item as LazyMathItem<N, T, D>;
- if (earlier === math || !earlier?.lazyCompile || !earlier.lazyTex) {
- break;
- }
- earlier.lazyCompile = false;
- earlier.lazyMarker && this.lazyObserver.unobserve(earlier.lazyMarker as any as Element);
- earlier.state(STATE.COMPILED - 1);
- compile = true;
- }
- return compile;
- }
- /**
- * Find the earliest TeX math item in the set, if any.
- *
- * @param {LazySet} set The set of newly visble math items
- * @return {LazyMathItem} The earliest TeX math item in the set, if any
- */
- protected earliestTex(set: LazySet): LazyMathItem<N, T, D> {
- let min: number = null;
- let minMath = null;
- for (const id of set.values()) {
- const math = this.lazyList.get(id);
- if (!math.lazyTex) continue;
- if (min === null || parseInt(id) < min) {
- min = parseInt(id);
- minMath = math;
- }
- }
- return minMath;
- }
- /**
- * If any of the removed items are observed or in the lazy list, remove them.
- *
- * @override
- */
- public clearMathItemsWithin(containers: ContainerList<N>) {
- const items = super.clearMathItemsWithin(containers) as LazyMathItem<N, T, D>[];
- for (const math of items) {
- const marker = math.lazyMarker;
- if (marker) {
- this.lazyObserver.unobserve(marker as any as Element);
- this.lazyList.delete(this.adaptor.getAttribute(marker, LAZYID));
- }
- }
- return items;
- }
- /**
- * @override
- */
- public render() {
- //
- // Get the containers whose content should always be typeset
- //
- const always = this.options.lazyAlwaysTypeset;
- this.lazyAlwaysContainers = !always ? null :
- this.adaptor.getElements(Array.isArray(always) ? always : [always], this.document);
- this.lazyAlwaysIndex = 0;
- super.render();
- return this;
- }
- };
- }
- /*==========================================================================*/
- /**
- * Add lazy typesetting support to a Handler instance
- *
- * @param {Handler} handler The Handler instance to enhance
- * @return {Handler} The handler that was modified (for purposes of chaining extensions)
- *
- * @template N The HTMLElement node class
- * @template T The Text node class
- * @template D The Document class
- */
- export function LazyHandler<N, T, D>(handler: HTMLHandler<N, T, D>): HTMLHandler<N, T, D> {
- //
- // Only update the document class if we can handle IntersectionObservers
- //
- if (typeof IntersectionObserver !== 'undefined') {
- handler.documentClass =
- LazyMathDocumentMixin<N, T, D, MathDocumentConstructor<HTMLDocument<N, T, D>>>(
- handler.documentClass
- ) as typeof HTMLDocument;
- }
- return handler;
- }
|