diff --git a/demos/html/viewer/index.html b/demos/html/viewer/index.html index 01ba832ae..482ecdfbf 100644 --- a/demos/html/viewer/index.html +++ b/demos/html/viewer/index.html @@ -38,6 +38,14 @@

Commit hash:

value="https://www.wiris.net/demo/plugins/app/" /> + +
@@ -62,6 +70,13 @@

Commit hash:

Language: + +
diff --git a/demos/html/viewer/src/app.js b/demos/html/viewer/src/app.js index 79a3aa4b9..d35bb5c6b 100644 --- a/demos/html/viewer/src/app.js +++ b/demos/html/viewer/src/app.js @@ -30,10 +30,12 @@ document.getElementById("viewer_form").addEventListener("submit", () => { lang: lang.value, viewer: viewer.value, zoom: zoom.value, + vieweroffset:vieweroffset.value, + simultaneousmml:simultaneousmml.value, + simultaneouslatex:simultaneouslatex.value, }; window.viewer.properties.config = config; document.getElementsByTagName("main")[0].innerHTML = document.getElementById("content").value; - window.com.wiris.js.JsPluginViewer.parseElement(document.getElementsByTagName("main")[0]); }); diff --git a/demos/html/viewer/src/static/style.css b/demos/html/viewer/src/static/style.css index e12aaaea9..75bbc1891 100644 --- a/demos/html/viewer/src/static/style.css +++ b/demos/html/viewer/src/static/style.css @@ -3,6 +3,7 @@ body { } input[type="text"], +input[type="number"], select, textarea { width: 100%; diff --git a/packages/viewer/docs/spec.md b/packages/viewer/docs/spec.md index d7c314ff9..6698585e2 100644 --- a/packages/viewer/docs/spec.md +++ b/packages/viewer/docs/spec.md @@ -112,6 +112,9 @@ The following table contains a specification of each of the properties. | lang | The language for the alt text. | Frontend | | en | | dpi | Resolution in dots per inch of the generated image. This feature scales the formula with a factor of dpi/96. | Frontend | Positive integer | 96 | | zoom | The scale of the generated image. | Frontend | Positive floating point number | 1 | +| vieweroffset | We render only the formulas on screen. This parameter defines the number of pixels to render in advance. | Frontend | Positive floating point number | 200 | +| simultaneousmml | Max number of possible simultaneous MathML rendering petitions at the same time. | Frontend | Positive floating point number | 50 | +| simultaneouslatex | Max number of possible simultaneous LaTeX rendering petitions at the same time. | Frontend | Positive floating point number | 50 | ## API diff --git a/packages/viewer/src/app.ts b/packages/viewer/src/app.ts index bfbc5ee69..1b12e57ed 100644 --- a/packages/viewer/src/app.ts +++ b/packages/viewer/src/app.ts @@ -1,6 +1,6 @@ import { Properties } from "./properties"; import { renderLatex } from "./latex"; -import { renderMathML } from "./mathml"; +import { renderAMathML } from "./mathml"; import { bypassEncapsulation } from "./retro"; import packageInformation from "../../../node_modules/@wiris/mathtype-viewer/package.json"; @@ -41,49 +41,85 @@ async function main(w: Window): Promise { const document = w.document; - /** - * Parse the DOM looking for LaTeX and elements. - * Replaces them with the corresponding rendered images within the given element. - * @param {Document} document - The DOM document in which to search for the rendering root. - * @param {MutationObserver} observer - Mutation observer to activate or reactivate every time the rendering root changes. - */ - properties.render = async () => { - const element: HTMLElement = document.querySelector(properties.element); - if (element) { - await renderLatex(properties, element); - await renderMathML(properties, element); - } - }; - // Initial function to call once document is loaded // Renders formulas and sets observer const start = async () => { - // Check if the viewer is alredy loaded + // Check if the viewer is already loaded if (w.viewer.isLoaded) return; w.viewer.isLoaded = true; - // First render - properties.render(); + // Interaction Observer + const mathobserver = new IntersectionObserver(async (entries, observer) => { + + let latexPromises = []; + + const maxMmlPetition = properties.simultaneousmml; + let mmlPromises = []; + for (const entry of entries) { + + if (entry.isIntersecting && entry.target instanceof MathMLElement && entry.target.matches('math')) { + const promise = renderAMathML(properties, entry.target); + mmlPromises.push(promise); + observer.unobserve(entry.target); + // To not saturate service, not make all render petition at once + if (mmlPromises.length >= maxMmlPetition) { + await Promise.all(mmlPromises); + mmlPromises = []; + } + } + + if (entry.isIntersecting && entry.target instanceof HTMLElement) { + const promise = renderLatex(properties, entry.target); + latexPromises.push(promise); + observer.unobserve(entry.target); + } + } + await Promise.all(latexPromises); + await Promise.all(mmlPromises); + }, { + rootMargin: `${properties.vieweroffset}px` + }); + + const allElements = document.querySelectorAll('*'); + window.addEventListener("load", function () { + allElements.forEach(function (element) { + mathobserver.observe(element); + }); + }); + + //MutationObserver // Callback called every time there is a mutation in the watched DOM element - // Feature temporarily disabled due to KB-37834 - // new MutationObserver(async (mutationList, observer) => { - // for (const mutation of mutationList) { - // for (const node of mutation.addedNodes) { - // if (node instanceof HTMLElement) { - // await properties.render(); - // } - // } - // } - // }) - // // We need to watch over the whole document, in case the Properties.element is inserted - // // e.g. we set Properties.element = '#renderArea' and then we append
$$2+2=4$$
to the document - // .observe(document, { - // attributes: true, // In case an attribute is changed in a node, for instance - // childList: true, // In case a new or $$latex$$ node is added, for instance - // subtree: true, // In case a node is added as a descendant of the observed element, for instance - // }); + new MutationObserver(async (mutationList) => { + for (const mutation of mutationList) { + for (const node of mutation.addedNodes) { + // If was a htmlelement, decompose to child element to find posible latex or mathml + if (node instanceof HTMLElement) { + const allElements = node.querySelectorAll('*'); + + allElements.forEach(function (element) { + mathobserver.observe(element); + }); + } + // Observe mathml + else if (node instanceof MathMLElement) { + mathobserver.observe(node); + } + // If it was a pure text, observe their parent in order to find posible latex + else if (node.nodeType == Node.TEXT_NODE) { + mathobserver.observe(node.parentElement); + } + } + } + }) + // We need to watch over the whole document, in case the Properties.element is inserted + // e.g. we set Properties.element = '#renderArea' and then we append
$$2+2=4$$
to the document + .observe(document, { + attributes: true, // In case an attribute is changed in a node, for instance + childList: true, // In case a new or $$latex$$ node is added, for instance + subtree: true, // In case a node is added as a descendant of the observed element, for instance + }); }; // https://developer.mozilla.org/en-US/docs/Web/API/Document/DOMContentLoaded_event#checking_whether_loading_is_already_complete diff --git a/packages/viewer/src/latex.ts b/packages/viewer/src/latex.ts index 94cba0197..f74692098 100644 --- a/packages/viewer/src/latex.ts +++ b/packages/viewer/src/latex.ts @@ -18,9 +18,21 @@ export async function renderLatex(properties: Properties, root: HTMLElement) { const latexNodes = findLatexTextNodes(root); + const maxLatexPetition = properties.simultaneouslatex; + + let promises = []; + for (const latexNode of latexNodes) { - await replaceLatexInTextNode(properties, latexNode); + promises.push(replaceLatexInTextNode(properties, latexNode)); + + // To not saturate service, not make all render petition at once + if (promises.length >= maxLatexPetition) { + await Promise.all(promises); + promises = []; + } } + + await Promise.all(promises); } /** diff --git a/packages/viewer/src/mathml.ts b/packages/viewer/src/mathml.ts index 7f5344df7..fc56f8759 100644 --- a/packages/viewer/src/mathml.ts +++ b/packages/viewer/src/mathml.ts @@ -48,6 +48,41 @@ function decodeSafeMathML(root: HTMLElement) { } } +export async function renderAMathML(properties: Properties, mathElement: MathMLElement): Promise { + if (properties.viewer !== "image" && properties.viewer !== "mathml") { + return; + } + + const mml = serializeHtmlToXml(mathElement.outerHTML); + + try { + let imgSource; + + // Transform mml to img. + if (properties.wirispluginperformance === "true") { + imgSource = await showImage( + mml, + properties.lang, + properties.editorServicesRoot, + properties.editorServicesExtension, + ); + } else { + imgSource = await createImage( + mml, + properties.lang, + properties.editorServicesRoot, + properties.editorServicesExtension, + ); + } + + // Set img properties. + const img = await setImageProperties(properties, imgSource, mml); + // Replace the MathML for the generated formula image. + mathElement.parentNode?.replaceChild(img, mathElement); + } catch { + console.error(`Cannot render ${mml}: invalid MathML format.`); + } +} /** * Parse the DOM looking for elements and replace them with the corresponding rendered images within the given element. * @param {Properties} properties - Properties of the viewer. @@ -60,37 +95,44 @@ export async function renderMathML(properties: Properties, root: HTMLElement): P decodeSafeMathML(root); + const promises = []; + for (const mathElement of [...root.getElementsByTagName("math")]) { const mml = serializeHtmlToXml(mathElement.outerHTML); - try { - let imgSource; - - // Transform mml to img. - if (properties.wirispluginperformance === "true") { - imgSource = await showImage( - mml, - properties.lang, - properties.editorServicesRoot, - properties.editorServicesExtension, - ); - } else { - imgSource = await createImage( - mml, - properties.lang, - properties.editorServicesRoot, - properties.editorServicesExtension, - ); + const promise = (async () => { + try { + let imgSource; + + // Transform mml to img. + if (properties.wirispluginperformance === "true") { + imgSource = await showImage( + mml, + properties.lang, + properties.editorServicesRoot, + properties.editorServicesExtension, + ); + } else { + imgSource = await createImage( + mml, + properties.lang, + properties.editorServicesRoot, + properties.editorServicesExtension, + ); + } + + // Set img properties. + const img = await setImageProperties(properties, imgSource, mml); + // Replace the MathML for the generated formula image. + mathElement.parentNode?.replaceChild(img, mathElement); + } catch { + console.error(`Cannot render ${mml}: invalid MathML format.`); } + })(); - // Set img properties. - const img = await setImageProperties(properties, imgSource, mml); - // Replace the MathML for the generated formula image. - mathElement.parentNode?.replaceChild(img, mathElement); - } catch { - console.error(`Cannot render ${mml}: invalid MathML format.`); - continue; - } + promises.push(promise); } + + await Promise.all(promises); } /** diff --git a/packages/viewer/src/properties.ts b/packages/viewer/src/properties.ts index 6d43c4bb8..526d4aa32 100644 --- a/packages/viewer/src/properties.ts +++ b/packages/viewer/src/properties.ts @@ -21,6 +21,9 @@ export type Config = { lang?: string; viewer?: Viewer; zoom?: number; + vieweroffset?: number, + simultaneousmml?: number; + simultaneouslatex?: number; }; /** @@ -39,6 +42,9 @@ const defaultValues: Config = { lang: "en", viewer: "none", zoom: 1, + vieweroffset: 200, + simultaneousmml: 50, + simultaneouslatex: 50, }; /** @@ -47,7 +53,7 @@ const defaultValues: Config = { export class Properties { private static instance: Properties | null = null; - render: () => Promise = async () => {}; + render: () => Promise = async () => { }; // Get URL properties (retrocompatibility). config: Config = defaultValues; @@ -56,7 +62,6 @@ export class Properties { * Do not use this method. Instead, use {@link Properties.generate}. * Constructors cannot be async so we make it private and force instantiation through an alternative static method. */ - private new() {} static getInstance(): Properties { if (!Properties.instance) { @@ -94,6 +99,17 @@ export class Properties { if (urlParams.get("zoom") !== null && urlParams.get("zoom") !== undefined) { Properties.instance.config.zoom = +urlParams.get("zoom"); } + if (urlParams.get("viewer-offset") !== null && urlParams.get("viewer-offset") !== undefined) { + Properties.instance.config.vieweroffset = +urlParams.get("viewer-offset"); + } + if (urlParams.get("simultaneous-mml") !== null && urlParams.get("simultaneous-mml") !== undefined) { + Properties.instance.config.simultaneousmml = +urlParams.get("simultaneous-mml"); + Math.min(Properties.instance.config.simultaneousmml, 50); + } + if (urlParams.get("simultaneous-latex") !== null && urlParams.get("simultaneous-latex") !== undefined) { + Properties.instance.config.simultaneouslatex = +urlParams.get("simultaneous-latex"); + Math.min(Properties.instance.config.simultaneouslatex, 50); + } } Properties.instance.checkServices(); @@ -266,4 +282,37 @@ export class Properties { this.config.backendConfig.wiriseditormathmlattribute = wiriseditormathmlattribute; this.render(); } + + /** + * Return the max number of simultaneous MathML rendering petition can make at the same time. + */ + get simultaneousmml(): number { + return this.config.simultaneousmml || defaultValues.simultaneousmml; + } + + set simultaneousmml(simultaneousmml: number) { + this.config.simultaneousmml = simultaneousmml; + this.render(); + } + /** + * Return the max number of simultaneous LaTeX rendering petition can make at the same time. + */ + get simultaneouslatex(): number { + return this.config.simultaneouslatex || defaultValues.simultaneouslatex; + } + set simultaneouslatex(simultaneouslatex: number) { + this.config.simultaneouslatex = simultaneouslatex; + this.render(); + } + + /** + * Return value serves to grow or shrink each side of the root element's bounding box before computing intersections. + */ + get vieweroffset(): number { + return this.config.vieweroffset || defaultValues.vieweroffset; + } + set vieweroffset(vieweroffset: number) { + this.config.vieweroffset = vieweroffset; + this.render(); + } }