From 98af2772f5e8371f38676e1a617055c862ea5f8e Mon Sep 17 00:00:00 2001 From: Jan Britz Date: Mon, 29 Sep 2025 18:22:40 +0200 Subject: [PATCH] feat: add MathJax v4 and remove v2 --- ts/src/mathjax/base.ts | 29 ++++++++++---------- ts/src/mathjax/index.ts | 13 ++++----- ts/src/mathjax/v2.ts | 60 ----------------------------------------- ts/src/mathjax/v3.ts | 8 +++--- ts/src/mathjax/v4.ts | 46 +++++++++++++++++++++++++++++++ 5 files changed, 73 insertions(+), 83 deletions(-) delete mode 100644 ts/src/mathjax/v2.ts create mode 100644 ts/src/mathjax/v4.ts diff --git a/ts/src/mathjax/base.ts b/ts/src/mathjax/base.ts index eedabd9..882c20b 100644 --- a/ts/src/mathjax/base.ts +++ b/ts/src/mathjax/base.ts @@ -1,10 +1,9 @@ -/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable */ export abstract class BaseMathJaxHelper { protected abstract cdnUrl: string; protected mathjax: any; - - private renderQueue: Promise = Promise.resolve(); + private loadPromise: Promise | null = null; /** * MathJax helper. @@ -16,12 +15,16 @@ export abstract class BaseMathJaxHelper { } /** - * Loads MathJax from a CDN. + * Loads MathJax from a CDN and initializes it. * * @protected */ protected loadFromCdn(): Promise { - return new Promise((resolve, reject) => { + if (this.loadPromise) { + return this.loadPromise; + } + + return this.loadPromise = new Promise((resolve, reject) => { const script = document.createElement("script"); script.type = "text/javascript"; script.src = this.cdnUrl; @@ -30,7 +33,9 @@ export abstract class BaseMathJaxHelper { script.onload = () => { // @ts-expect-error After loading the script, window.MathJax will exist. this.mathjax = window.MathJax; - resolve(); + + // We return the startup promise to ensure that MathJax is fully initialized before using it. + resolve(this.mathjax.startup.promise); }; script.onerror = () => reject(new Error(`Failed to load ${this.cdnUrl}.`)); @@ -49,13 +54,9 @@ export abstract class BaseMathJaxHelper { * Renders LaTeX inside an element. Loads MathJax from a CDN if necessary. */ render(element: Element, inline: boolean): Promise { - // We do this to chain every render call. This also ensures that we only load once from the CDN: - // https://docs.mathjax.org/en/v3.2-latest/web/typeset.html#handling-asynchronous-typesetting - return (this.renderQueue = this.renderQueue.then(() => { - if (this.mathjax === undefined) { - return this.loadFromCdn().then(() => this._render(element, inline)); - } - return this._render(element, inline); - })); + if (this.mathjax === undefined) { + return this.loadFromCdn().then(() => this._render(element, inline)); + } + return this._render(element, inline); } } diff --git a/ts/src/mathjax/index.ts b/ts/src/mathjax/index.ts index b1d90f3..2720710 100644 --- a/ts/src/mathjax/index.ts +++ b/ts/src/mathjax/index.ts @@ -1,29 +1,30 @@ /* eslint-disable */ import { BaseMathJaxHelper } from "./base"; -import { MathJax2Helper } from "./v2"; import { MathJax3Helper } from "./v3"; +import { MathJax4Helper } from "./v4"; let mathJaxHelper: BaseMathJaxHelper | null = null; /** * Uses MathJax to render LaTeX inside the given element. * - * If MathJax is not available it will be loaded from a CDN. + * If MathJax is not available, it will be loaded from a CDN. */ export function renderLaTeX(element: Element, inline: boolean = true): Promise { if (mathJaxHelper === null) { // @ts-expect-error We need to check for the existence of MathJax. const mathjax: any = window.MathJax; if (typeof mathjax === "object") { - if (mathjax.version.startsWith("2.")) { - mathJaxHelper = new MathJax2Helper(mathjax); - } else if (mathjax.version.startsWith("3.")) { + if (mathjax.version.startsWith("3.")) { mathJaxHelper = new MathJax3Helper(mathjax); + } else if (mathjax.version.startsWith("4.")) { + mathJaxHelper = new MathJax4Helper(mathjax); } else { - return Promise.reject(new Error("Only MathJax 2.x and 3.x are supported.")); + return Promise.reject(new Error("Only MathJax 3.x and 4.x are supported.")); } } else { + // We should in theory switch to MathJax 4 as the default, but we might need to change our UI. mathJaxHelper = new MathJax3Helper(); } } diff --git a/ts/src/mathjax/v2.ts b/ts/src/mathjax/v2.ts deleted file mode 100644 index 3b13bf0..0000000 --- a/ts/src/mathjax/v2.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* eslint-disable */ -import { BaseMathJaxHelper } from "./base"; - -/** - * Uses MathJax 2 to render LaTeX. - * - * Docs: https://docs.mathjax.org/en/v2.7-latest/ - */ -export class MathJax2Helper extends BaseMathJaxHelper { - protected cdnUrl = "https://cdn.jsdelivr.net/npm/mathjax@2/MathJax.js"; - - protected loadFromCdn() { - // Configure MathJax. - const script = document.createElement("script"); - script.type = "text/x-mathjax-config"; - script.text = ` - MathJax.Hub.Config({ - jax: ["input/TeX","output/CommonHTML"], - extensions: ["tex2jax.js", "Safe.js"], - tex2jax: { - inlineMath: [["\\\\(", "\\\\)"]], - displayMath: [["\\\\[", "\\\\]"]], - processEscapes: true, - }, - skipStartupTypeset: true, - errorSettings: {message: ["!"]}, - messageStyle: "none", - }); - `; - document.head.appendChild(script); - return super.loadFromCdn(); - } - - protected _render(element: Element, inline: boolean): Promise { - return new Promise((resolve, reject) => { - try { - // Ensure that MathJax is fully initialized. - this.mathjax.Hub.Register.StartupHook("End", () => { - // We require the 'tex2jax' extension. - if (this.mathjax.Extension.tex2jax === undefined) { - throw new Error("The 'tex2jax' extension is required."); - } - - // Get the delimiters. - const inlineDelimiters = this.mathjax.Extension.tex2jax.config.inlineMath[0]; - const displayDelimiters = this.mathjax.Extension.tex2jax.config.displayMath[0]; - const [openingDelimiter, closingDelimiter] = inline ? inlineDelimiters : displayDelimiters; - - // Add the delimiters. - element.textContent = `${openingDelimiter} ${element.textContent} ${closingDelimiter}`; - - // Perform the rendering. - this.mathjax.Hub.Queue(["Typeset", this.mathjax.Hub, element], [resolve]); - }); - } catch (error) { - reject(error); - } - }); - } -} diff --git a/ts/src/mathjax/v3.ts b/ts/src/mathjax/v3.ts index 271826a..a42049b 100644 --- a/ts/src/mathjax/v3.ts +++ b/ts/src/mathjax/v3.ts @@ -4,10 +4,11 @@ import { BaseMathJaxHelper } from "./base"; /** * Uses MathJax 3 to render LaTeX. * - * Docs: https://docs.mathjax.org/en/v3.2-latest/ + * Docs: https://docs.mathjax.org/en/v3.2/ */ export class MathJax3Helper extends BaseMathJaxHelper { protected cdnUrl = "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js"; + private queue: Promise = Promise.resolve(); protected loadFromCdn() { // Configure MathJax. @@ -34,8 +35,9 @@ export class MathJax3Helper extends BaseMathJaxHelper { } protected _render(element: Element, inline: boolean): Promise { - // Ensure that MathJax is fully initialized. - return this.mathjax.startup.promise.then(() => { + // We need to manually chain every render call. + // https://docs.mathjax.org/en/v3.2/web/typeset.html#handling-asynchronous-typesetting + return this.queue = this.queue.then(() => { // Get the delimiters. const inlineDelimiters = this.mathjax.config.tex?.inlineMath?.[0] ?? ["\\(", "\\)"]; const displayDelimiters = this.mathjax.config.tex?.displayMath?.[0] ?? ["\\[", "\\]"]; diff --git a/ts/src/mathjax/v4.ts b/ts/src/mathjax/v4.ts new file mode 100644 index 0000000..b6a6c13 --- /dev/null +++ b/ts/src/mathjax/v4.ts @@ -0,0 +1,46 @@ +/* eslint-disable */ +import { BaseMathJaxHelper } from "./base"; + +/** + * Uses MathJax 4 to render LaTeX. + * + * Docs: https://docs.mathjax.org/en/v4.0/ + */ +export class MathJax4Helper extends BaseMathJaxHelper { + protected cdnUrl = "https://cdn.jsdelivr.net/npm/mathjax@4/tex-chtml.js"; + + protected loadFromCdn(): Promise { + // Configure MathJax. + // @ts-ignore + window.MathJax = { + tex: { + inlineMath: [["\\\\(", "\\\\)"]], + displayMath: [["\\\\[", "\\\\]"]], + processEscapes: true, + packages: { "[+]": ["noerrors"] }, + }, + startup: { + typeset: false, + }, + options: { + ignoreHtmlClass: "tex2jax_ignore", + processHtmlClass: "tex2jax_process", + }, + loader: { + load: ["[tex]/noerrors", "ui/safe"], + }, + }; + return super.loadFromCdn(); + } + + protected _render(element: Element, inline: boolean): Promise { + const inlineDelimiters = this.mathjax.config.tex?.inlineMath?.[0] ?? ["\\(", "\\)"]; + const displayDelimiters = this.mathjax.config.tex?.displayMath?.[0] ?? ["\\[", "\\]"]; + const [openingDelimiter, closingDelimiter] = inline ? inlineDelimiters : displayDelimiters; + + // Add the delimiters. + element.textContent = `${openingDelimiter} ${element.textContent} ${closingDelimiter}`; + + return this.mathjax.typesetPromise([element]) + } +}