Skip to content

Commit f173491

Browse files
authored
Merge pull request #777 from mathjax/lazy-update
Lazy extension update
2 parents ccb4b4c + a422353 commit f173491

File tree

3 files changed

+201
-26
lines changed

3 files changed

+201
-26
lines changed

ts/core/DOMAdaptor.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ export interface DOMAdaptor<N, T, D> {
121121
getElements(nodes: (string | N | N[])[], document: D): N[];
122122

123123
/**
124-
* Determine if a container node contains a given node is somewhere in its DOM tree
124+
* Determine if a container node contains a given node somewhere in its DOM tree
125125
*
126126
* @param {N} container The container to search
127127
* @param {N|T} node The node to look for

ts/core/MathDocument.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -436,15 +436,6 @@ export interface MathDocument<N, T, D> {
436436
*/
437437
state(state: number, restore?: boolean): MathDocument<N, T, D>;
438438

439-
/**
440-
* Rerender the MathItems on the page
441-
*
442-
* @param {number=} start The state to start rerendering at
443-
* @param {number=} end The state to end rerendering at
444-
* @return {MathDocument} The math document instance
445-
*/
446-
rerender(start?: number, end?: number): MathDocument<N, T, D>;
447-
448439
/**
449440
* Clear the processed values so that the document can be reprocessed
450441
*

ts/ui/lazy/LazyHandler.ts

Lines changed: 200 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,22 @@
2222
*/
2323

2424
import {MathDocumentConstructor, ContainerList} from '../../core/MathDocument.js';
25-
import {MathItem, STATE} from '../../core/MathItem.js';
25+
import {MathItem, STATE, newState} from '../../core/MathItem.js';
2626
import {HTMLMathItem} from '../../handlers/html/HTMLMathItem.js';
2727
import {HTMLDocument} from '../../handlers/html/HTMLDocument.js';
2828
import {HTMLHandler} from '../../handlers/html/HTMLHandler.js';
2929
import {handleRetriesFor} from '../../util/Retries.js';
30+
import {OptionList} from '../../util/Options.js';
3031

3132
/**
3233
* Add the needed function to the window object.
3334
*/
3435
declare const window: {
3536
requestIdleCallback: (callback: () => void) => void;
37+
addEventListener: ((type: string, handler: (event: Event) => void) => void);
38+
matchMedia: (type: string) => {
39+
addListener: (handler: (event: Event) => void) => void;
40+
};
3641
};
3742

3843
/**
@@ -97,6 +102,8 @@ export class LazyList<N, T, D> {
97102

98103
/*==========================================================================*/
99104

105+
newState('LAZYALWAYS', STATE.FINDMATH + 3);
106+
100107
/**
101108
* The attribute to use for the ID on the marker node
102109
*/
@@ -244,16 +251,6 @@ export function LazyMathItemMixin<N, T, D, B extends Constructor<HTMLMathItem<N,
244251
}
245252
}
246253

247-
/**
248-
* @override
249-
*/
250-
public state(state: number = undefined, restore: boolean = false) {
251-
//
252-
// don't set the state if we are lazy processing
253-
//
254-
return (restore === null ? this._state : super.state(state, restore));
255-
}
256-
257254
};
258255

259256
}
@@ -279,6 +276,21 @@ export interface LazyMathDocument<N, T, D> extends HTMLDocument<N, T, D> {
279276
*/
280277
lazyList: LazyList<N, T, D>;
281278

279+
/**
280+
* The containers whose contents should always be typeset
281+
*/
282+
lazyAlwaysContainers: N[];
283+
284+
/**
285+
* A function that will typeset all the remaining expressions (e.g., for printing)
286+
*/
287+
lazyTypesetAll(): Promise<void>;
288+
289+
/**
290+
* Mark the math items that are to be always typeset
291+
*/
292+
lazyAlways(): void;
293+
282294
}
283295

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

300312
return class BaseClass extends BaseDocument {
301313

314+
/**
315+
* @override
316+
*/
317+
public static OPTIONS: OptionList = {
318+
...BaseDocument.OPTIONS,
319+
lazyMargin: '200px',
320+
lazyAlwaysTypeset: null,
321+
renderActions: {
322+
...BaseDocument.OPTIONS.renderActions,
323+
lazyAlways: [STATE.LAZYALWAYS, 'lazyAlways', '', false]
324+
}
325+
};
326+
302327
/**
303328
* The Intersection Observer used to track the appearance of the expression markers
304329
*/
@@ -309,6 +334,16 @@ B extends MathDocumentConstructor<HTMLDocument<N, T, D>>>(
309334
*/
310335
public lazyList: LazyList<N, T, D>;
311336

337+
/**
338+
* The containers whose contents should always be typeset
339+
*/
340+
public lazyAlwaysContainers: N[] = null;
341+
342+
/**
343+
* Index of last container where math was found in lazyAlwaysContainers
344+
*/
345+
public lazyAlwaysIndex: number = 0;
346+
312347
/**
313348
* A promise to make sure our compiling/typesetting is sequential
314349
*/
@@ -334,21 +369,155 @@ B extends MathDocumentConstructor<HTMLDocument<N, T, D>>>(
334369
* Augment the MathItem class used for this MathDocument,
335370
* then create the intersection observer and lazy list,
336371
* and bind the lazyProcessSet function to this instance
337-
* so it can be used as a callback more easily.
372+
* so it can be used as a callback more easily. Add the
373+
* event listeners to typeset everything before printing.
338374
*
339375
* @override
340376
* @constructor
341377
*/
342378
constructor(...args: any[]) {
343379
super(...args);
380+
//
381+
// Use the LazyMathItem for math items
382+
//
344383
this.options.MathItem =
345384
LazyMathItemMixin<N, T, D, Constructor<HTMLMathItem<N, T, D>>>(this.options.MathItem);
346-
this.lazyObserver = new IntersectionObserver(this.lazyObserve.bind(this));
385+
//
386+
// Allocate a process bit for lazyAlways
387+
//
388+
const ProcessBits = (this.constructor as typeof HTMLDocument).ProcessBits;
389+
!ProcessBits.has('lazyAlways') && ProcessBits.allocate('lazyAlways');
390+
//
391+
// Set up the lazy observer and other needed data
392+
//
393+
this.lazyObserver = new IntersectionObserver(this.lazyObserve.bind(this), {rootMargin: this.options.lazyMargin});
347394
this.lazyList = new LazyList<N, T, D>();
348395
const callback = this.lazyHandleSet.bind(this);
349-
this.lazyProcessSet = (typeof window !== 'undefined' && window.requestIdleCallback ?
350-
() => window.requestIdleCallback(callback) :
351-
() => setTimeout(callback, 10));
396+
this.lazyProcessSet = (window && window.requestIdleCallback ?
397+
() => window.requestIdleCallback(callback) :
398+
() => setTimeout(callback, 10));
399+
//
400+
// Install print listeners to typeset the rest of the document before printing
401+
//
402+
if (window) {
403+
let done = false;
404+
const handler = () => {
405+
!done && this.lazyTypesetAll();
406+
done = true;
407+
};
408+
window.matchMedia('print').addListener(handler); // for Safari
409+
window.addEventListener('beforeprint', handler); // for everyone else
410+
}
411+
}
412+
413+
/**
414+
* Check all math items for those that should always be typeset
415+
*/
416+
public lazyAlways() {
417+
if (!this.lazyAlwaysContainers || this.processed.isSet('lazyAlways')) return;
418+
for (const item of this.math) {
419+
const math = item as LazyMathItem<N, T, D>;
420+
if (math.lazyTypeset && this.lazyIsAlways(math)) {
421+
math.lazyCompile = math.lazyTypeset = false;
422+
}
423+
}
424+
this.processed.set('lazyAlways');
425+
}
426+
427+
/**
428+
* Check if the MathItem is in one of the containers to always typeset.
429+
* (start looking using the last container where math was found,
430+
* in case the next math is in the same container).
431+
*
432+
* @param {LazyMathItem<N,T,D>} math The MathItem to test
433+
* @return {boolean} True if one of the document's containers holds the MathItem
434+
*/
435+
protected lazyIsAlways(math: LazyMathItem<N, T, D>): boolean {
436+
if (math.state() < STATE.LAZYALWAYS) {
437+
math.state(STATE.LAZYALWAYS);
438+
const node = math.start.node;
439+
const adaptor = this.adaptor;
440+
const start = this.lazyAlwaysIndex;
441+
const end = this.lazyAlwaysContainers.length;
442+
do {
443+
const container = this.lazyAlwaysContainers[this.lazyAlwaysIndex];
444+
if (adaptor.contains(container, node)) return true;
445+
if (++this.lazyAlwaysIndex >= end) {
446+
this.lazyAlwaysIndex = 0;
447+
}
448+
} while (this.lazyAlwaysIndex !== start);
449+
}
450+
return false;
451+
}
452+
453+
/**
454+
* @override
455+
*/
456+
public state(state: number, restore: boolean = false) {
457+
super.state(state, restore);
458+
if (state < STATE.LAZYALWAYS) {
459+
this.processed.clear('lazyAlways');
460+
}
461+
return this;
462+
}
463+
464+
/**
465+
* Function to typeset all remaining expressions (for printing, etc.)
466+
*
467+
* @return {Promise} Promise that is resolved after the typesetting completes.
468+
*/
469+
public async lazyTypesetAll(): Promise<void> {
470+
//
471+
// The state we need to go back to (COMPILED or TYPESET).
472+
//
473+
let state = STATE.LAST;
474+
//
475+
// Loop through all the math...
476+
//
477+
for (const item of this.math) {
478+
const math = item as LazyMathItem<N, T, D>;
479+
//
480+
// If it is not lazy compile or typeset, skip it.
481+
//
482+
if (!math.lazyCompile && !math.lazyTypeset) continue;
483+
//
484+
// Mark the state that we need to start at.
485+
//
486+
if (math.lazyCompile) {
487+
math.state(STATE.COMPILED - 1);
488+
state = STATE.COMPILED;
489+
} else {
490+
math.state(STATE.TYPESET - 1);
491+
if (STATE.TYPESET < state) state = STATE.TYPESET;
492+
}
493+
//
494+
// Mark it as not lazy and remove it from the observer.
495+
//
496+
math.lazyCompile = math.lazyTypeset = false;
497+
math.lazyMarker && this.lazyObserver.unobserve(math.lazyMarker as any as Element);
498+
}
499+
//
500+
// If something needs updating
501+
//
502+
if (state === STATE.LAST) return Promise.resolve();
503+
//
504+
// Reset the document state to the starting state that we need.
505+
//
506+
this.state(state - 1, null);
507+
//
508+
// Save the SVG font cache and set it to "none" temporarily
509+
// (needed by Firefox, which doesn't seem to process the
510+
// xlinks otherwise).
511+
//
512+
const fontCache = this.outputJax.options.fontCache;
513+
if (fontCache) this.outputJax.options.fontCache = 'none';
514+
//
515+
// Typeset the math and put back the font cache when done.
516+
//
517+
this.reset();
518+
return handleRetriesFor(() => this.render()).then(() => {
519+
if (fontCache) this.outputJax.options.fontCache = fontCache;
520+
});
352521
}
353522

354523
/**
@@ -495,6 +664,21 @@ B extends MathDocumentConstructor<HTMLDocument<N, T, D>>>(
495664
return items;
496665
}
497666

667+
/**
668+
* @override
669+
*/
670+
public render() {
671+
//
672+
// Get the containers whose content should always be typeset
673+
//
674+
const always = this.options.lazyAlwaysTypeset;
675+
this.lazyAlwaysContainers = !always ? null :
676+
this.adaptor.getElements(Array.isArray(always) ? always : [always], this.document);
677+
this.lazyAlwaysIndex = 0;
678+
super.render();
679+
return this;
680+
}
681+
498682
};
499683

500684
}

0 commit comments

Comments
 (0)