kernel/loadMathJax.js

/*!
 * MathGraph32 Javascript : Software for animating online dynamic mathematics figures
 * https://www.mathgraph32.org/
 * @Author Yves Biton (yves.biton@sesamath.net)
 * @License: GNU AGPLv3 https://www.gnu.org/licenses/agpl-3.0.html
 * @version 5.7.0
 */

import { getMathjaxBase } from 'src/kernel/kernel'

export default loadMathJax

const loadDelay = 40 // en s (avec peu de débit ça peut être assez long)

// en local on impose l'url de notre pnpm start avec notre port par défaut, si on veut autre chose faudra
// que l'appelant le précise avec du window.mathjax3Base ou mtgOptions.mathjax3Base
const mathJax3Uri = 'es5/tex-svg.js'

const mathJaxConfig = {
  tex: {
    inlineMath: [
      ['$', '$'],
      ['\\(', '\\)']
    ],
    // packages: {'[+]': ['noerrors', 'color']}
    packages: { '[+]': ['color', 'colortbl'] }
  },
  svg: {
    // cf http://docs.mathjax.org/en/latest/options/output/svg.html#the-configuration-block
    mtextInheritFont: false,
    mtextFont: 'Roboto,"Noto Sans Arabic",sans-serif' // Deuxième police pour gérer l'arabe
  },
  options: {
    ignoreHtmlClass: 'tex2jax_ignore',
    processHtmlClass: 'tex2jax_process'
  },
  loader: {
    // la conversion donnait ça
    // load: ['input/tex', 'output/svg']
    // mais ça marchait pas, c'est bcp mieux avec
    load: ['[tex]/noerrors', '[tex]/color', '[tex]/colortbl']
  },
  // on ne veut pas de typeset du dom complet dès le chargement
  // (comportement par défaut de es5/tex-svg.js)
  startup: {
    typeset: false
  }
}

// si on fait deux appels très rapprochés à loadMathJax,
// on peut se retrouver dans le cas où le 2e retourne Promise.resolve() alors que MathJax n'est pas encore ready
let mathJaxLoadingPromise

/**
 * Charge mathjax dans le <head> de la page courante (appelé seulement par addLatex)
 * @param {string} [mathjax3Base] Un éventuel chemin vers le dossier de MathJax3 (qui devra contenir es5/tex-svg.js), sans slash de fin
 * @returns {Promise<undefined>}
 */
function loadMathJax (mathjax3Base) {
  // si on a déjà été appelé y'a rien à faire (c'est peut-être encore en cours ou déjà résolu)
  if (mathJaxLoadingPromise) return mathJaxLoadingPromise

  // faut le charger
  mathJaxLoadingPromise = new Promise((resolve, reject) => {
    // Depuis le 18/3/2025 on impose la fonte 'Roboto' sur tous les conteneurs de svg mtg32

    // si qqun d'autre a déjà chargé MathJax (iep dans un nœud j3p précédent par ex)
    // ça ne devrait pas être la peine de le refaire, mais c'est vraiment compliqué de s'assurer
    // qu'il a les bons packages avec la bonne config.
    // affecter `MathJax.config = mathJaxConfig` puis appeler `MathJax.startup.getComponents()`
    // fonctionne, sauf s'il manque des packages (dans ce cas il faudrait ajouter du
    // `\require(lePackage)` avant le code latex qui l'utilise)
    // Cf commit f1c029df pour une version avec ce fonctionnement
    // (ça fonctionnait pour les packages déjà chargé,
    //   mais l'ajout du package colortbl n'était pas pris en charge,
    //   ça donnait du `Undefined control sequence \columncolor`
    //   à la place d'afficher les commandes inconnues)
    // => on recharge toujours
    let base = mathjax3Base || getMathjaxBase()
    if (!base.endsWith('/')) base += '/'
    const mathJax3Url = base + mathJax3Uri

    if (typeof MathJax === 'object') {
      // si y'a déjà un script on le vire, sinon ça va pas forcément recharger
      for (const elt of document.getElementsByTagName('script')) {
        if (elt.src === mathJax3Url) {
          elt.remove()
        }
      }
    }

    // faut charger MathJax
    // on ajoute la résolution de la promesse quand il sera prêt
    // cf http://docs.mathjax.org/en/latest/web/configuration.html#performing-actions-during-startup
    const ready = () => {
      // on vire le timeout
      clearTimeout(timeout)
      MathJax.startup.defaultReady()
      MathJax.startup.promise.then(() => {
        resolve()
      })
    } // ready
    // on ne veut pas modifier notre objet mathJaxConfig
    window.MathJax = {
      ...mathJaxConfig,
      startup: { ...mathJaxConfig.startup, ready },
    }

    // chargement mathjax
    const eltScript = document.createElement('script')
    eltScript.type = 'text/javascript'
    // faut préciser ça sinon ff peut râler (il doit y avoir un bout de code mathjax qui veut lire les cookies)
    eltScript.crossOrigin = 'anonymous'
    eltScript.src = mathJax3Url
    const head = document.getElementsByTagName('head')[0]
    // Modifs Yves pour résoudre le pb de chargement de MathJax en dev
    eltScript.id = 'MathJax-script'
    eltScript.async = true
    // Fin essai Yves
    head.appendChild(eltScript)
    // on ajoute un timeout de chargement
    const timeout = setTimeout(() => {
      const error = Error(`Mathjax non chargé après ${loadDelay}s d’attente`)
      // on ajoute ça pour que celui qui chopera cette erreur puisse l'afficher à l'utilisateur (et éventuellement éviter que ça remonte jusqu'à bugsnag)
      error.userFriendly = true
      reject(error)
    }, loadDelay * 1000)
  })

  return mathJaxLoadingPromise
} // loadMathJax