Skip to content

Commit

Permalink
Merge pull request #777 from mathjax/lazy-update
Browse files Browse the repository at this point in the history
Lazy extension update
  • Loading branch information
dpvc authored Apr 6, 2022
2 parents ccb4b4c + a422353 commit f173491
Show file tree
Hide file tree
Showing 3 changed files with 201 additions and 26 deletions.
2 changes: 1 addition & 1 deletion ts/core/DOMAdaptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ export interface DOMAdaptor<N, T, D> {
getElements(nodes: (string | N | N[])[], document: D): N[];

/**
* Determine if a container node contains a given node is somewhere in its DOM tree
* Determine if a container node contains a given node somewhere in its DOM tree
*
* @param {N} container The container to search
* @param {N|T} node The node to look for
Expand Down
9 changes: 0 additions & 9 deletions ts/core/MathDocument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -436,15 +436,6 @@ export interface MathDocument<N, T, D> {
*/
state(state: number, restore?: boolean): MathDocument<N, T, D>;

/**
* Rerender the MathItems on the page
*
* @param {number=} start The state to start rerendering at
* @param {number=} end The state to end rerendering at
* @return {MathDocument} The math document instance
*/
rerender(start?: number, end?: number): MathDocument<N, T, D>;

/**
* Clear the processed values so that the document can be reprocessed
*
Expand Down
216 changes: 200 additions & 16 deletions ts/ui/lazy/LazyHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,22 @@
*/

import {MathDocumentConstructor, ContainerList} from '../../core/MathDocument.js';
import {MathItem, STATE} from '../../core/MathItem.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;
};
};

/**
Expand Down Expand Up @@ -97,6 +102,8 @@ export class LazyList<N, T, D> {

/*==========================================================================*/

newState('LAZYALWAYS', STATE.FINDMATH + 3);

/**
* The attribute to use for the ID on the marker node
*/
Expand Down Expand Up @@ -244,16 +251,6 @@ export function LazyMathItemMixin<N, T, D, B extends Constructor<HTMLMathItem<N,
}
}

/**
* @override
*/
public state(state: number = undefined, restore: boolean = false) {
//
// don't set the state if we are lazy processing
//
return (restore === null ? this._state : super.state(state, restore));
}

};

}
Expand All @@ -279,6 +276,21 @@ export interface LazyMathDocument<N, T, D> extends HTMLDocument<N, T, D> {
*/
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;

}

/**
Expand All @@ -299,6 +311,19 @@ B extends MathDocumentConstructor<HTMLDocument<N, T, D>>>(

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
*/
Expand All @@ -309,6 +334,16 @@ B extends MathDocumentConstructor<HTMLDocument<N, T, D>>>(
*/
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
*/
Expand All @@ -334,21 +369,155 @@ B extends MathDocumentConstructor<HTMLDocument<N, T, D>>>(
* 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.
* 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);
this.lazyObserver = new IntersectionObserver(this.lazyObserve.bind(this));
//
// 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 = (typeof window !== 'undefined' && window.requestIdleCallback ?
() => window.requestIdleCallback(callback) :
() => setTimeout(callback, 10));
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;
});
}

/**
Expand Down Expand Up @@ -495,6 +664,21 @@ B extends MathDocumentConstructor<HTMLDocument<N, T, D>>>(
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;
}

};

}
Expand Down

0 comments on commit f173491

Please sign in to comment.