lib/outils/mathgraph/index.js

import { j3pGetUrlParams } from 'src/legacy/core/functions'
import { loadJs } from 'sesajs/dom'

import FormulaComparator from './FormulaComparator'

/** @module lib/outils/mathgraph/index */

// pour ne lancer le chargement qu’une seule fois, et surtout utiliser
// le mtgLoad mis en global par notre loader pour éviter tout conflit
// (si la section déclare mathgraph comme outil ET utilise ces fcts, ça déconne)
let mtgLoad

const mtgLanguages = ['fr', 'ar', 'de', 'es', 'en']
const defaultMtgLanguage = 'fr'

const languages = {
  ara: 'ar',
  deu: 'de',
  eng: 'en',
  fra: 'fr',
  spa: 'es',
}

/**
 * Initialise mtgOptions avec language fr et decimalDot false s'ils n'étaient pas précisés
 * @param {MtgOptions} mtgOptions
 */
function normalizeMtgOptions (mtgOptions) {
  // @todo mettre cette conversion dans le mtgLoad de mathgraph
  if (mtgOptions.language?.length === 3) {
    mtgOptions.language = languages[mtgOptions.language]
  }
  mtgOptions.language = mtgLanguages.includes(mtgOptions.language) ? mtgOptions.language : defaultMtgLanguage
  if (mtgOptions.decimalDot == null) {
    // virgule si langue ≠ anglais (valable pour fr|de|es en 2025-06)
    mtgOptions.decimalDot = mtgOptions.language === 'en'
  }
}

/**
 * Retourne la fct mtgLoad
 * @return {Promise<function>} Résolue avec la fonction mtgLoad
 */
export async function getMtgLoad (mtgUrl) {
  if (mtgLoad) return mtgLoad
  if (typeof window.mtgLoad !== 'function') {
    const isDev = /^(localhost|.+\.sesamath.dev|.+\.local)$/.test(window.location.hostname)
    const url = mtgUrl || j3pGetUrlParams('mtgUrl') || window.mtgUrl || (isDev ? 'https://dev.mathgraph32.org/js/mtgLoad/mtgLoad.js' : 'https://www.mathgraph32.org/js/mtgLoad/mtgLoad.min.js')
    const opts = { timeout: 60 }
    // si on charge mtg sur son pnpm start, par ex avec http://localhost:8081/#mtgUrl=http://localhost:8082/src/mtgLoad.global.js&…
    // faut le faire en type module
    if (/local(host(:8082)?)?\/src\/mtgLoad/.test(url)) opts.type = 'module'
    await loadJs(url, opts)
    if (typeof window.mtgLoad !== 'function') throw Error('Le chargement de Mathgraph a échoué')
  } else {
    console.warn('il y a déjà un mtgLoad mis en global par j3pLoad, parce qu’une des sections de ce graphe a déclaré l’outil mathgraph => pas de nouveau chargement')
  }
  // après chargement de ce js, window.mtgLoad est un preloader qui veut une callback, ça ne retourne jamais de promesse, on wrappe ça
  mtgLoad = function (container, svgOptions, mtgOptions, cb) {
    if (cb) return window.mtgLoad(container, svgOptions, mtgOptions, cb)
    return new Promise((resolve, reject) => {
      window.mtgLoad(container, svgOptions, mtgOptions, function (error, mtgApp) {
        if (error) return reject(error)
        resolve(mtgApp)
      })
    })
  }
  return mtgLoad
}

/**
 * Retourne une promesse qui sera résolue avec une instance de MtgApp (l’éditeur mathgraph, ou le player si mtgOptions.isEditable est mis à false)
 * ou rejetée une erreur (ne pas oublier de la traiter, avec à minima un `.catch(j3pShowError)` après le then, qui dans ce cas va aussi
 * capturer les erreurs du code mis dans le then, pour dissocier mettre ce catch avant le then puis un autre catch pour les erreurs éventuelles du then)
 * Cf la doc {@link https://www.mathgraph32.org/documentation/loading/global.html#mtgLoad} pour les options possibles
 * avec des exemples {@link https://www.mathgraph32.org/documentation/loading/tutorial-loadPlayer.html} pour le player
 * ou {@link https://www.mathgraph32.org/documentation/loading/tutorial-loadEditor.html} pour l’éditeur
 * @param {HTMLElement|string} container
 * @param {SvgOptions} svgOptions
 * @param {MtgOptions} mtgOptions Si vous précisez loadCoreOnly, il vaut mieux utiliser `getMtgCore()`,
 *              et si c’est loadCoreWithMathJax alors utilisez `getMtgCore({withMathjax : true })`
 *              Ça retournera un MtgAppLecteur (en passant ces options ici aussi, mais le typage de ce qui est retourné
 *              va correspondre à un objet MtgApp alors que vous aurez un MtgAppLecteur)
 * @param {boolean} [mtgOptions.isEditable=true] passer false pour récupérer un MtgAppLecteur à la place d’un MtgApp
 * @param {boolean} [mtgOptions.loadApi=false] passer true pour avoir les méthodes de l’api
 * @return {Promise<MtgApp|MtgAppLecteur|MtgAppApi|MtgAppLecteurApi>} suivant les options loadCoreOnly|loadCoreWithMathJax|isEditable
 */
export async function getMtgApp (container, svgOptions, mtgOptions) {
  const mtgLoad = await getMtgLoad()
  normalizeMtgOptions(mtgOptions)
  const mtgApp = await mtgLoad(container, svgOptions, mtgOptions)
  if (!mtgApp) throw Error('Le chargeur de mathgraph n’a pas retourné d’application mathgraph sous la forme attendue')
  return mtgApp
}

/**
 * Retourne une promesse qui sera résolue avec une instance de MtgAppLecteur (le player
 * ou rejetée une erreur (ne pas oublier de la traiter, avec à minima un `.catch(j3pShowError)` après le then, qui dans ce cas va aussi
 * capturer les erreurs du code mis dans le then, pour dissocier mettre ce catch avant le then puis un autre catch pour les erreurs éventuelles du then)
 * Cf la doc {@link https://www.mathgraph32.org/documentation/loading/global.html#mtgLoad} pour les options possibles
 * avec des exemples {@link https://www.mathgraph32.org/documentation/loading/tutorial-loadPlayer.html}
 * @param {HTMLElement|string} container
 * @param {SvgOptions} [svgOptions]
 * @param {MtgOptions} [mtgOptions] Si vous précisez loadCoreOnly, il vaut mieux utiliser `getMtgCore()`,
 *              et si c’est loadCoreWithMathJax alors utilisez `getMtgCore({withMathjax : true })`
 * @param {boolean} [mtgOptions.isEditable=true] passer false pour récupérer un MtgAppLecteur à la place d’un MtgApp
 * @param {boolean} [mtgOptions.loadApi=false] passer true pour avoir les méthodes de l’api
 * @return {Promise<MtgApp|MtgAppLecteur|MtgAppApi|MtgAppLecteurApi>} suivant les options loadCoreOnly|loadCoreWithMathJax|isEditable
 */
export async function getMtgAppLecteur (container, svgOptions = {}, mtgOptions = {}) {
  mtgOptions.isEditable = false
  return getMtgApp(container, svgOptions, mtgOptions)
}

/**
 * Charge un MtgAppLecteurApi
 * @param container
 * @param svgOptions
 * @param mtgOptions
 * @return {Promise<MtgAppLecteurApi>}
 */
export async function getMtgAppLecteurApi (container, svgOptions, mtgOptions) {
  normalizeMtgOptions(mtgOptions)
  // on impose le MtgAppLecteurApi en sortie
  mtgOptions.loadApi = true
  mtgOptions.isEditable = false
  return getMtgApp(container, svgOptions, mtgOptions)
}

/**
 * Retourne une promesse qui sera résolue avec une instance du moteur de mathgraph (un MtgAppLecteur, même s’il n’y a pas de figure à afficher) ou une erreur
 * Cf la doc {@link https://www.mathgraph32.org/documentation/loading/tutorial-loadCore.html} pour l’utiliser ensuite
 * @param {Object} [options]
 * @param {boolean} [options.withMathjax=false] passer true pour charger Mathjax dès le départ (sinon il sera chargé si besoin seulement, d’après ce qu’il aura à traiter)
 * @param {boolean} [options.withApi=false] passer true pour récupérer un MtgAppLecteurApi (MtgAppLecteur avec les méthodes de l’api mathgraph synchrone)
 * @param {boolean} [options.withApiPromise=false] passer true pour récupérer un MtgAppLecteurApi (MtgAppLecteur avec les méthodes de l’api mathgraph qui retournent des promesses résolues lorsque les objets sont affichés)
 * @return {Promise<MtgAppLecteur|MtgAppLecteurApi>}
 */
export function getMtgCore ({ withMathjax = false, withApi = false, withApiPromise = false } = {}) {
  // player only, sans figure initiale ni svg (qui devra être créé par la section si besoin)
  const mtgOptions = {}
  normalizeMtgOptions(mtgOptions)
  if (withMathjax) mtgOptions.loadCoreWithMathJax = true
  else mtgOptions.loadCoreOnly = true
  if (withApi || withApiPromise) {
    mtgOptions.loadApi = true
    if (withApiPromise) mtgOptions.isPromiseMode = true
  }
  return getMtgApp('', {}, mtgOptions)
}

// notre singleton qui sera retourné par getComparator
/**
 * @typedef compareFormula
 * @type {function}
 * @param solution
 * @param reponse
 * @param {Object} [options]
 * @param {string} [options.varNames=xyztab] noms des variables formelles utilisées (6 max)
 * @param {boolean} [options.equivalenceFracDec=false] Passer true pour que…
 * @returns {number} -1 en cas d’erreur de syntaxe (-2 en cas d’erreur de syntaxe sur la solution), 1 si équivalent et 0 sinon
 * @throws {SyntaxError} en cas d’erreur de syntaxe dans la solution ou la réponse
 */
let compareFormula

/**
 * Retourne la fonction qui permettra de tester des équivalences d’expressions algébriques
 * @returns {Promise<compareFormula>}
 */
export async function getComparator () {
  // on crée le comparateur au 1er appel
  if (!compareFormula) {
    const mtgAppLecteur = await getMtgCore()
    const formulaComparator = new FormulaComparator(mtgAppLecteur)
    compareFormula = formulaComparator.compare.bind(formulaComparator)
  }
  return compareFormula
}