legacy/outils/calculatrice/ValidationZones.js

import $ from 'jquery'
import { j3pBarre, j3pElement, j3pEnsureHtmlElement, j3pFocus, j3pFreezeElt, j3pMathquillXcas, j3pNombre, j3pPGCD, j3pSupprimeEspaces, j3pValeurde } from 'src/legacy/core/functions'
import { legacyStyles } from 'src/legacy/core/StylesJ3p'
import Tarbre from 'src/legacy/outils/calculatrice/Tarbre'
import { j3pDesactive } from 'src/lib/mathquill/functions'

class ValidationZones {
  /**
   * Constructeur des zones de saisie, qui retourne leur validateur
   * Il permet de créer les fonctions de validation que l’on pourra ensuite appeler pour valider les réponses données.
   * Cf le tutorial {@tutorial ValidationZones} (jsdoc/tutorials/ValidationZones.md)
   * @param {Object} zonesOptions
   * @param {HTMLInputElement[]|string[]} zonesOptions.zones La liste des zones (ou de leurs ids)
   * @param {HTMLInputElement[]|string[]} [zonesOptions.validePerso] La liste des zones dont la validation est gérée dans la section (et pas par ValidationZones)
   * @param {Parcours} zonesOptions.parcours Le parcours courant (pour accéder à donneesSection.debug et
   */
  constructor (zonesOptions) {
    if (!this || typeof this !== 'object') throw Error('Constructeur qui doit être appelé avec new')
    if (typeof zonesOptions !== 'object') throw Error('paramètre invalide')
    if (!Array.isArray(zonesOptions.zones)) throw Error('Paramètre zones invalide (pas un tableau)')
    /**
     * @private
     * @type {HTMLInputElement[]}
     */
    const inputs = []
    for (const zone of zonesOptions.zones) {
      try {
        const zoneSaisie = j3pEnsureHtmlElement(zone)
        // Pour connaître la position de la zone de saisie dans le tableau zone (utile pour valider ou colorer une zone unique)
        zoneSaisie.indexZone = inputs.length
        inputs.push(zoneSaisie)
      } catch (error) {
        console.error(`création de la zone ${zone} impossible, pas un id ni un élément html`, error)
      }
    }
    if (!Array.isArray(zonesOptions.validePerso)) {
      zonesOptions.validePerso = []
    }
    /**
     * @typedef ZonesProps
     * @property {HTMLInputElement[]} inputs Liste des inputs (ou zones mathquill)
     * @property {boolean[]} validePerso Liste de booléen
     * @property {boolean[]} aRepondu
     * @property {string[]} reponseSaisie Liste des réponses saisies par l’élève (en cas de sélection dans une liste ça contient l’index)
     * @property {boolean[]} bonneReponse
     * @property {boolean[]} reponseSimplifiee (chaque élément peut être un tableau de booléen)
     */
    /**
     * La liste des propriétés pour nos zones à valider
     * Il devrait rester privé (interne à cette classe), mais bcp de sections le manipule et affectent des valeurs
     * @type {ZonesProps}
     */
    this.zones = {
      // array d’elts html
      inputs,
      // liste de booléen, true si on a filé l’id (ou la zone de saisie) dans validePerso, false sinon
      // zonesOptions.validePerso peut contenir l’id ou l’élément html
      validePerso: inputs.map(input => zonesOptions.validePerso.includes(input) || zonesOptions.validePerso.includes(input.id)),
      aRepondu: [], // propriété aRepondu de chaque zone
      reponseSaisie: [], // on récupère la réponse saisie
      bonneReponse: inputs.map(() => false), // pour chaque zone (sans validation perso) on sait si la réponse est bonne
      reponseSimplifiee: inputs.map(() => [true, true]), // par défaut, on considère toutes les réponses simplifiées
      isActive: inputs.map(() => true) // par défaut, toutes les zones sont actives
    }
    // pour la compatibilité ascendante (inputs s’appelait id)
    Object.defineProperty(this.zones, 'id', {
      enumerable: true,
      get: () => {
        console.error(Error('Il ne faut plus utiliser la propriété id, elle est remplacée par la propriété inputs'))
        return this.zones.inputs
      },
      set: () => {
        throw Error('Il ne faut plus utiliser la propriété id, elle est remplacée par la propriété inputs, et elle ne doit pas être modifiée')
      }
    })
    /**
     * Le parcours courant
     * @type {Parcours|undefined}
     */
    this.parcours = zonesOptions.parcours
    /**
     * @type {boolean}
     * @private
     */
    this._isDebug = this.parcours?.isDebug
  }

  /**
   * Valide toutes les zones (et colorie)
   * @param {boolean} [options]
   * @param {boolean} [options.lastChance] Si le parcours n’a pas été fourni au constructeur, il faut passer un booléen (pour savoir si c’est le dernier essai et geler les réponses élève)
   * @param {boolean} [options.noFreeze] Passer true pour ne pas geler les réponses élève
   * @return {{detailsreponses: {}, aRepondu: boolean, bonneReponse: boolean}}
   */
  validationGlobale (opts = {}) {
    // pour compatibilité ascendante
    if (typeof opts === 'boolean') {
      opts = {
        lastChance: opts
      }
    }
    let { lastChance, noFreeze = false } = opts
    if (typeof lastChance !== 'boolean') {
      // pas fourni, on le déduit de parcours
      if (this.parcours) {
        lastChance = this.parcours.essaiCourant >= this.parcours.donneesSection.nbchances
      } else {
        console.warn('Sans avoir fourni de parcours au constructeur il faut passer l’argument lastChance à validationGlobale, pas de freeze possible sans lui')
      }
    }
    // On vérifie tout d’abord si l’élève a répondu à chaque zone
    const aRepondu = this.valideReponses()
    let bonneReponse = true
    const detailsreponses = {}
    if (aRepondu) {
      for (let i = this.zones.inputs.length - 1; i >= 0; i--) {
        // pour les zones dont la validation est perso, on ne les prend pas en compte (idem pour celle qui ne sont plus actives car déjà considérées comme bonnes)
        if (!this.zones.validePerso[i] && this.zones.isActive[i]) {
          const zoneSaisie = this.zones.inputs[i]
          const bilanReponseZone = this.valideUneZone(zoneSaisie, zoneSaisie.reponse)
          this.zones.bonneReponse[i] = bilanReponseZone.bonneReponse
          bonneReponse = (bonneReponse && this.zones.bonneReponse[i])
          detailsreponses[this.zones.inputs[i]] = {
            correct: this.zones.bonneReponse[i],
            valeur: this.zones.reponseSaisie[i],
            zonesimplifiee: this.zones.reponseSimplifiee[i],
            detailPolynome: bilanReponseZone.detailPolynome
          }
        } else {
          // ce qui suit évite d’avoir à le préciser dans la section dans le cas d’une bonne réponse'
          this.zones.bonneReponse[i] = true
        }
      }
      this.coloreLesZones({ noFreeze })
      // ce n'est pas ici qu'il faut freezer les input car on passe à côté de ceux qui sont gérés par
      if (lastChance && !noFreeze) this.#freezeAll()
    } else {
      bonneReponse = false
    }
    // console.log('validationGlobale avec lastChance', lastChance, 'va retourner', { aRepondu, bonneReponse, detailsreponses })
    return { aRepondu, bonneReponse, detailsreponses }
  }

  /**
   * Vérifie que toutes les zones ont bien été remplies (appelée par validationGlobale)
   * @return {boolean} True si toutes les zones ont une réponse
   */
  valideReponses () {
    let repEleve // cette variable récupère le contenu de la zone de saisie
    let aRepondu = true
    // On part de la dernière zone car si elle n’est pas complétée, alors on lui donne le focus.
    // Ainsi, la zone avec le focus est la première zone vide
    for (let i = this.zones.inputs.length - 1; i >= 0; i--) {
      this.zones.aRepondu[i] = true
      const elt = this.zones.inputs[i]
      if (elt) {
        if (!Array.isArray(elt.solutionSimplifiee)) {
          elt.solutionSimplifiee = ['valide et on accepte', 'valide et on accepte']
        }
        for (let k = 0; k <= 1; k++) {
          if ((elt.solutionSimplifiee[k] === 'reponse fausse') ||
            (elt.solutionSimplifiee[k] === 'reponsefausse') ||
            (elt.solutionSimplifiee[k] === 'réponse fausse') ||
            (elt.solutionSimplifiee[k] === 'reponseFausse') ||
            (elt.solutionSimplifiee[k] === 'fausse')) {
            elt.solutionSimplifiee[k] = 'reponse fausse'
          } else if ((elt.solutionSimplifiee[k] === 'non valide') ||
            (elt.solutionSimplifiee[k] === 'nonValide') ||
            (elt.solutionSimplifiee[k] === 'reponse non valide') ||
            (elt.solutionSimplifiee[k] === 'réponse non valide') ||
            (elt.solutionSimplifiee[k] === 'nonvalide') ||
            (elt.solutionSimplifiee[k] === 'reponsenonvalide')) {
            elt.solutionSimplifiee[k] = 'non valide'
          } else {
            elt.solutionSimplifiee[k] = 'valide et on accepte'
          }
        }
      }
      if (elt?.id.includes('input')) {
        // c’est une zone de saisie
        repEleve = j3pValeurde(elt)
      } else {
        // c’est une liste'
        repEleve = elt.selectedIndex
      }
      // on récupère la réponse de chaque zone de saisie
      this.zones.reponseSaisie[i] = repEleve
      if (elt?.id.includes('input')) {
        // la zone de saisie est-elle complétée
        // console.log('elt.id:', elt.id, repEleve, 'et elt:', elt, $(elt).mathquill('latex'), elt.innerHTML)
        if (repEleve === '') {
          this.zones.aRepondu[i] = false
          // à chaque fois, on redonne le focus à la zone concernée.
          // A la fin, c’est la première zone vide qui l’aura (d’où l’ordre dans lequel le for est construit)
          j3pFocus(elt)
        } else {
          const input = elt
          if (input.typeReponse === undefined) {
            input.typeReponse = ['texte']
          }
          if (input.typeReponse[0] === 'nombre') {
            this.zones.reponseSimplifiee[i] = this.reponseSimplifiee(elt)
          } else {
            this.zones.reponseSimplifiee[i] = true
          }
          for (let k = 0; k <= 1; k++) {
            if ((elt.solutionSimplifiee[k] === 'non valide') && !this.zones.reponseSimplifiee[i][k]) {
              // la réponse n’est pas simplifiée (somme ou racine restante pour k=0, fraction réductible pour k=1),
              // et on demande dans la section à ce que cela soit considéré comme une réponse non valide (l’élève doit proposer autre chose)
              this.zones.aRepondu[i] = false
            }
          }
        }
        if (!this.zones.aRepondu[i]) {
          // on redonne le focus à la zone si l’élève n’a pas répondu (ou avec une réponse non simplifiée si c’est pris en compte)
          j3pFocus(elt)
        }
      } else {
        if (elt.typeReponse === undefined) {
          elt.typeReponse = [true]
        }
        if (elt.typeReponse[0]) {
          // on veut dans ce cas que l’élève modifie la valeur par défaut de la liste
          // la liste est-elle complétée
          if (repEleve === 0) {
            this.zones.aRepondu[i] = false
            j3pFocus(elt)
          }
        } else {
          this.zones.aRepondu[i] = true
        }
      }
      aRepondu = (aRepondu && this.zones.aRepondu[i])
      // console.log('aRepondu ? zone', i, this.zones.aRepondu[i])
    }
    // console.log('valideReponses va retourner', aRepondu, 'pour', this.zones)
    return aRepondu
  }

  /**
   * Met en couleur les zones de saisie (suivant qu’elles soient bonne ou fausses)
   * Elle permet aussi de barrer la réponse à la fin et de désactiver la zone
   * Elle ne sera appelée que si toutes les réponses ont été données et n’agit pas sur les zones où la validaton est perso.
   * @param {Object} [options]
   * @param {boolean} [options.noFreeze] Passer true pour ne pas geler les réponses élève
   */
  coloreLesZones (options = {}) {
    for (let i = this.zones.inputs.length - 1; i >= 0; i--) {
      // si la réponse est bonne on la remet dans la couleur de l’énoncé
      // sinon, on la met en rouge
      if (!this.zones.validePerso[i] && this.zones.isActive[i]) {
        this.coloreUneZone(this.zones.inputs[i], options)
      }
    }
  }

  /**
   * Met en couleur une seule zone de saisie (suivant qu’elle soit bonne ou fausse)
   * Permet aussi de barrer la réponse à la fin et de désactiver la zone.
   * Elle est appelée par coloreLesZones, mais peut l'être de manière autonome (pour une validation perso)
   * Si la réponse est bonne on la remet dans la couleur de l’énoncé, sinon, on la met en rouge
   * @param {string|HTMLElement} zone zone de saisie qui peut être identifiée par son id
   * @param {Object} [options]
   * @param {boolean} [options.noFreeze] Passer true pour ne pas geler les réponses élève
   */
  coloreUneZone (zone, options = {}) {
    // nos deux listeners, this est l’élément html
    function onInput () {
      this.style.color = legacyStyles.petit.enonce.color
      this.removeEventListener('input', onInput)
      this.removeEventListener('keyup', onKeyup)
    }

    function onKeyup (e) {
      const keynum = (window.event) ? e.keyCode : e.which
      if (keynum === 46 || keynum === 8) {
        this.style.color = legacyStyles.petit.enonce.color
        this.removeEventListener('input', onInput)
        this.removeEventListener('keyup', onKeyup)
      }
    }

    let elt
    const { noFreeze = false } = options
    try {
      elt = j3pEnsureHtmlElement(zone)
    } catch (error) {
      console.error(error)
      return
    }
    // console.log('coloreUneZone', elt.indexZone, this.zones.isActive[elt.indexZone], elt)
    if (!this.zones.isActive[elt.indexZone]) {
      // la zone est déjà inactive => on ne fait rien (elle est probablement déjà gelée
      // avec l'original sorti du dom)
      return
    }

    if (this.zones.bonneReponse[elt.indexZone]) {
      // bonne réponse
      elt.style.color = legacyStyles.cbien
      j3pDesactive(elt)
      this.zones.isActive[elt.indexZone] = false
      // On gèle cette zone
      if (!noFreeze) j3pFreezeElt(elt)
      return
    }

    // mauvaise réponse
    elt.style.color = legacyStyles.cfaux
    // on désactive la zone si l’élève n’a plus de chance pour répondre
    // sinon on redonne le focus
    if (!this.parcours) console.error(Error('Il faut passer le parcours au constructeur ValidationZones pour pouvoir appeler coloreUneZone'))
    const parcours = this.parcours || window.j3p
    if (parcours.essaiCourant < parcours.donneesSection.nbchances) {
      j3pFocus(elt)
      // Je fais en sorte que lorsqu’on va de nouveau compléter la zone, le texte va revenir en noir
      // On pourrait ajouter l’objet {once:true} comme argument pour détruire l’écouteur dès sa première utilisation, mais ça ne fonctionne pas sur tous les navigateurs
      elt.addEventListener('input', onInput)
      elt.addEventListener('keyup', onKeyup)
      return
    }

    // faux au dernier essai
    j3pDesactive(elt)
    this.zones.isActive[elt.indexZone] = false
    if (elt.couleur) elt.style.color = elt.couleur
    if (!noFreeze && (elt.barrer === undefined || elt.barrer)) {
      // ça freeze après avoir barré
      j3pBarre(elt)
    }
  }

  /**
   * Valide une zone (et la colorie)
   * Vérifie juste si une seule zone est correctement remplie
   * zone est l’élément (input) ou l’id de la zone testée, que l’on suppose non vide (valideReponses doit avoir été appelée avant).
   * reponse est un tableau de réponse si zone.typeReponse[0] vaut "texte" ou undefined, sinon, c’est un nombre ou un polynôme
   * @param {HTMLElement|string} zone
   * @param {string[]|string|number} reponse
   * @return {{detailPolynome: {}, bonneReponse: boolean}}
   */
  valideUneZone (zone, reponse) {
    /**
     * @type {string}
     * @private
     */
    let idZone
    if (typeof zone === 'string') {
      idZone = zone
      zone = j3pElement(zone)
    } else {
      idZone = zone.id || '' // on assure d’avoir une string pour la suite du code
    }
    let puisPrecision
    let bonneReponse = false
    let repEleve = this.zones.reponseSaisie[zone.indexZone]
    let detailPolynome = {} // cela servira pour récupérer les informations sur la validation d’un polynôme (pour mieux cerner les erreurs)'
    if (idZone.includes('input')) {
      // on a une zone de saisie
      if (zone.typeReponse === undefined) {
        zone.typeReponse = ['texte']
      }
      if (zone.typeReponse[0] === 'texte') {
        // on veut un tableau de string pour simplifier la suite
        if (!Array.isArray(reponse)) {
          if (typeof reponse === 'number') {
            reponse = [String(reponse)]
          } else if (typeof reponse === 'string') {
            reponse = [reponse]
          } else {
            console.error(Error(`reponse attendue invalide : ${reponse} (${typeof reponse})`))
            reponse = ['']
          }
        }
        bonneReponse = reponse.includes(repEleve)
        if (bonneReponse) {
          // dans ce cas, on remplace la réponse par la première acceptée par le correcteur
          if (idZone.includes('inputmq')) {
            // pour une zone mathquill
            $(zone).mathquill('latex', reponse[0])
          } else {
            // pour un autre type de zone de saisie
            zone.value = reponse[0]

            // cette zone peut être dynamique donc il faut bricoler pour que la taille s’adapte au textes éventuellement modifiés
            let fontZone = zone.style.fontFamily
            let elParent = zone
            while (!fontZone) {
              elParent = elParent.parentNode
              if (elParent && elParent.style && elParent.style.fontFamily) {
                fontZone = elParent.style.fontFamily
              } else {
                // il n’y a plus d’élément parent (on est arrivé sur le document)
                fontZone = legacyStyles.petit.enonce.style.fontFamily
                break
              }
            }
            const tailleFontZone = zone.style.fontSize
            const texteZone = zone.value
            const longueur = $(zone).textWidth(texteZone, fontZone, tailleFontZone)
            zone.style.width = (longueur + 16) + 'px'
          }
        }
      } else if (zone.typeReponse[0] === 'polynome') {
        // la zone est nécessairement une zone mathquill dans ce cas
        const varPoly = zone.typeReponse[1]
        const unarbre1 = new Tarbre(reponse, [varPoly]) // pour le polynôme attendu

        // on interprète repEleve pour qu’il soit compris par la suite'
        const tabFrac = repEleve.split('frac')
        let irreductible = true // si des fractions sont présentes, sont-elles simplifiées ?
        let vraiPolynome = true // savoir si c’est bien un polynome (pas une fonction rationnelle par ex)
        let autreVariable = false // existe-t-il une autre variable que varPoly ?
        let signeDouble = false // pour contrôler si l’élève a écrit deux signes consécutifs'
        let egalitePolynome = true // cette variable pourra évoluer si vraiPolynome==true et autreVariable==false
        let estDeveloppe = true // savoir si on retrouve bien une forme développée (sans parenthèses)
        let estSimplifie = true // si le polynôme est développé, on vérifie qu’il est simplifié'
        let monomesDegresDifferents = true // si on a un polynôme développé et qu’il est égal à celui demandé, on vérifie si une même puissance de x 'apparaît pas deux fois.'
        let degreNonSimplifie = -1 // si le polynôme n’est pas simplifié, cette variable reçoit le degré du premier monôme non réduit (par puissance décroissantes)'
        let degreErrone = -1 // on cherche quel coefficient est inexacte (on renvoie le degré le plus élevé)
        const numFrac = []
        const denFrac = []
        const numFracInit = []
        const denFracInit = []
        const pgcdFrac = []
        const xFrac = [] // servira pour récupérer x s’il se trouve au numérateur ou au dénominateur'
        const puisFrac = []
        const varXFrac = [] // pour récupérer les puissances de x présentes dans les fractions

        // repEleveSimp sera le polynôme où les fractions auront été remplacée par leur valeur décimale approchée
        let repEleveSimp = repEleve
        const regX = new RegExp('(\\\\cdot)*(' + varPoly + ')(\\^)*(\\{?\\(?([0-9]+)\\)?)*', 'g')
        const regX2 = new RegExp('(\\\\cdot)*(' + varPoly + ')(\\^)*\\{?\\(?', 'g')
        // je regarde ici si toutes les éventuelles fractions sont bien réduites
        if (tabFrac.length > 1) {
          for (let j = 1; j < tabFrac.length; j++) {
            xFrac[j] = []
            numFrac[j] = tabFrac[j].split('}')[0].substring(1)
            numFracInit[j] = numFrac[j]
            xFrac[j][0] = numFrac[j].match(regX)
            numFrac[j] = numFrac[j].replace(regX, '')
            if (!numFrac[j]) {
              numFrac[j] = '1'
            }
            // j’ai récupéré le coef du numérateur et la puissance de x
            denFrac[j] = tabFrac[j].split('}')[1].split('}')[0].substring(1)
            denFracInit[j] = denFrac[j]
            xFrac[j][1] = denFrac[j].match(regX)
            denFrac[j] = denFrac[j].replace(regX, '')
            if (!denFrac[j]) {
              denFrac[j] = '1'
            }
            // j’ai récupéré le coef du dénominateur et la puissance de x
            pgcdFrac[j] = this.pgcd(Math.abs(numFrac[j]), Math.abs(denFrac[j]))
            if ((pgcdFrac[j] > 1) || (pgcdFrac[j] === 'pb')) {
              irreductible = false
            }
            puisFrac[j] = []

            // pour la suite, je simplifie les puissances de x (s’il y en a) et gère l’affichage de la puissance de x simplifiée'
            if (xFrac[j][0] != null) {
              if (xFrac[j][1] != null) {
                // x est présent au numérateur et au dénominateur
                puisFrac[j][0] = xFrac[j][0][0].replace(regX2, '')
                puisFrac[j][1] = xFrac[j][1][0].replace(regX2, '')
                if (!puisFrac[j][0]) {
                  if (!puisFrac[j][1]) {
                    varXFrac[j] = ''
                  } else {
                    if (Number(puisFrac[j][1]) === 2) {
                      varXFrac[j] = '(1)/(' + varPoly + ')'
                    } else {
                      varXFrac[j] = '(1)/(' + varPoly + '^' + String(Number(puisFrac[j][1] - 1)) + ')'
                    }
                  }
                } else {
                  if (!puisFrac[j][1]) {
                    if (Number(puisFrac[j][0]) === 2) {
                      varXFrac[j] = varPoly
                    } else {
                      varXFrac[j] = varPoly + '^' + String(Number(puisFrac[j][0] - 1))
                    }
                  } else {
                    if (Number(puisFrac[j][0]) > Number(puisFrac[j][1])) {
                      if (Number(puisFrac[j][0]) === Number(puisFrac[j][1]) + 1) {
                        varXFrac[j] = varPoly
                      } else {
                        varXFrac[j] = varPoly + '^' + String(Math.round(Number(puisFrac[j][0] - puisFrac[j][1])))
                      }
                    } else {
                      if (Number(puisFrac[j][1]) === Number(puisFrac[j][0]) + 1) {
                        varXFrac[j] = '(1)/(' + varPoly + ')'
                      } else {
                        varXFrac[j] = '(1)/(' + varPoly + '^' + String(Math.round(Number(puisFrac[j][1] - puisFrac[j][0]))) + ')'
                      }
                    }
                  }
                }
                irreductible = false
              } else {
                // on a x au numérateur et pas au dénominateur
                puisFrac[j][0] = xFrac[j][0][0].replace(regX2, '')
                if (!puisFrac[j][0]) {
                  varXFrac[j] = varPoly
                } else {
                  varXFrac[j] = varPoly + '^' + puisFrac[j][0]
                }
              }
            } else {
              if (xFrac[j][1] != null) {
                // on a x au dénominateur et pas au numérateur
                puisFrac[j][1] = xFrac[j][1][0].replace(regX2, '')
                if (!puisFrac[j][1]) {
                  varXFrac[j] = '(1)/(' + varPoly + ')'
                } else {
                  varXFrac[j] = '(1)/(' + varPoly + '^' + puisFrac[j][1] + ')'
                }
              } else {
                // sinon x n’est ni au numérateur, ni au dénominateur, on n’a donc pas à gérer ces "x"'
                varXFrac[j] = ''
              }
            }
            // console.log("varXFrac:"+varXFrac);
            // console.log("!!!!avant repEleveSimp:"+repEleveSimp)
            // je remplace alors chaque fraction par la valeur décimale et je mets aussi la puissance de x pour reconstruire le monome
            repEleveSimp = repEleveSimp.replace('\\frac{' + numFracInit[j] + '}}{' + denFracInit[j] + '}', String(numFrac[j] / denFrac[j]) + varXFrac[j])
            repEleveSimp = repEleveSimp.replace('\\frac{' + numFracInit[j] + '}}{' + denFracInit[j] + '}}', String(numFrac[j] / denFrac[j]) + varXFrac[j])
            repEleveSimp = repEleveSimp.replace('\\frac{' + numFracInit[j] + '}{' + denFracInit[j] + '}}', String(numFrac[j] / denFrac[j]) + varXFrac[j])
            repEleveSimp = repEleveSimp.replace('\\frac{' + numFracInit[j] + '}{' + denFracInit[j] + '}', String(numFrac[j] / denFrac[j]) + varXFrac[j])
            if (this._isDebug) console.debug('repEleveSimp:' + repEleveSimp)
          }
        }
        // on vérifie tout d’abord que 2 signes ne sont pas consécutifs
        // je dois prendre en compte les signes doubles
        const tabSigneDouble = ['++', '+-', '-+', '--', '*+', '*-', '**']
        const tabSigneEquiv = ['+', '-', '-', '+', '*', '*(-1)', '*']
        if (this._isDebug) console.debug('repEleveSimp avant signes :' + repEleveSimp)
        for (let j = 0; j < tabSigneDouble.length; j++) {
          while (repEleveSimp.includes(tabSigneDouble[j])) {
            // le signe double est présent
            signeDouble = true
            repEleveSimp = repEleveSimp.replace(tabSigneDouble[j], tabSigneEquiv[j])
          }
        }
        if (this._isDebug) console.debug('repEleveSimp:' + repEleveSimp)
        // ici je ne dois plus avoir de double signe
        let repEleveSuite = j3pMathquillXcas(repEleve)
        // on doit remplacer sqrt par racine pour l’évaluation'
        while (repEleveSuite.includes('sqrt')) {
          repEleveSuite = repEleveSuite.replace('sqrt', 'racine')
        }
        while (repEleveSimp.includes('sqrt')) {
          repEleveSimp = repEleveSimp.replace('sqrt', 'racine')
        }
        const regVarpol = new RegExp('(' + varPoly + ')', 'g')
        let repEleveSansFracetsqrt = repEleveSuite.replace(/(racine)/g, '') // racineTxt,"");
        repEleveSansFracetsqrt = repEleveSansFracetsqrt.replace(regVarpol, '')
        if (/[a-z]/g.test(repEleveSansFracetsqrt)) {
          autreVariable = true
        }
        let ordrePuissance // en lien avec puisPrecision;
        if (!autreVariable) {
          const unarbre2 = new Tarbre(repEleveSuite, [varPoly]) // pour le polynôme entré par l’élève'
          for (let j = 0; j <= 14; j++) {
            if (unarbre1.evalue([j]) === 0) {
              puisPrecision = 12
            } else {
              ordrePuissance = String(Math.abs(unarbre1.evalue([j]))).indexOf('.')
              // ordrePuissance = Math.floor(Math.log(Math.abs(unarbre1.evalue([j])))/Math.log(10))+1;
              puisPrecision = (ordrePuissance === '-1') ? 12 - String(Math.abs(reponse)).length : 12 - ordrePuissance
            }
            egalitePolynome = (egalitePolynome && (Math.abs(unarbre1.evalue([j]) - unarbre2.evalue([j])) < Math.pow(10, -puisPrecision)))
          }
          if (zone.typeReponse[2] === 'developpe') {
            // s’il reste des parenthèses, c’est que ce n’est pas vraiment développé'
            if ((repEleve.includes('(')) || (repEleve.includes(')'))) {
              estDeveloppe = false
            } else if (repEleveSimp.includes('/')) {
              vraiPolynome = false
              // ce n’est pas un polynôme (il reste après simplification 1/x^...)
            } else {
              const repSansSommeTab = []
              const decomp1 = repEleveSimp.split('+')
              const decomp2 = []
              let j2
              for (let j = 0; j < decomp1.length; j++) {
                if (decomp1[j] !== '') {
                  decomp2[j] = decomp1[j].split('-')
                  for (j2 = 0; j2 < decomp2[j].length; j2++) {
                    if (decomp2[j][j2] !== '') {
                      if (j2 > 0) {
                        repSansSommeTab.push('-' + decomp2[j][j2])
                      } else {
                        repSansSommeTab.push(decomp2[j][j2])
                      }
                    }
                  }
                }
              }
              if (this._isDebug) console.debug('repSansSommeTab:' + repSansSommeTab)
              // à cet instant repSansSommeTab est un tableau contenant les monomes
              // je reprends chaque élément de ce tableau et en extrait le coef qui est écrit aiinsi que le degré
              const degreRepTab = []
              const monomeReduit = []
              const passageDansMonome = [] // si on trouve plusieurs puissances de x dans un monome, c’est qu’il n’est pas réduit'
              let degMax = 0
              let degMaxTab
              const regDeg = /\^\{?\(?\d{1,3}\)?}?/g // new RegExp('(\\^\\{?\\(?[0-9]{1,3}\\)?\\}?)', 'g')
              const regDegNb = /\d/g // new RegExp('[0-9]', 'g')
              for (let j = 0; j < repSansSommeTab.length; j++) {
                degreRepTab[j] = 0
                monomeReduit[j] = true
                passageDansMonome[j] = 0
                // recherche du degré le plus élevé du monome
                degMaxTab = 1
                if (regDeg.test(repSansSommeTab[j])) {
                  degMaxTab = repSansSommeTab[j].match(regDeg)
                }
                for (let k = 0; k < degMaxTab.length; k++) {
                  degMax = Math.max(degMax, degMaxTab[k].match(regDegNb)[0])
                  if (this._isDebug) console.debug('j=' + j + '  degMax' + degMax)
                }
                // au max, on va dire que le degré peut être de 20
                j2 = degMax
                while (j2 >= 1) {
                  if (repSansSommeTab[j].includes(varPoly + '^' + j2)) {
                    // x^j2 est dans ce monome (x étant varPoly)
                    passageDansMonome[j]++
                    degreRepTab[j] += j2
                    repSansSommeTab[j] = repSansSommeTab[j].replace(varPoly + '^' + String(j2), '') // cela vire x^j2
                  } else {
                    j2--
                  }
                }
                while (repSansSommeTab[j].includes(varPoly)) {
                  // x est présent dans ce monome (x étant varPoly)
                  degreRepTab[j] += 1
                  passageDansMonome[j]++
                  repSansSommeTab[j] = repSansSommeTab[j].replace(varPoly, '') // cela vire x
                }
                if (passageDansMonome[j] > 1) {
                  monomeReduit[j] = false
                  degreNonSimplifie = Math.max(degreNonSimplifie, degreRepTab[j])
                }
                if (this._isDebug) {
                  console.debug('degreRepTab[' + j + ']:' + degreRepTab[j])
                  console.debug('degreNonSimplifie:' + degreNonSimplifie)
                }
                // console.log("repSansSommeTab[j]="+repSansSommeTab[j]);
                // s’il reste un signe * qui soit au début ou à la fin, on le vire.
                while (repSansSommeTab[j].includes('**')) {
                  repSansSommeTab[j] = repSansSommeTab[j].replace('**', '*')
                }
                // console.log("repSansSommeTab[j] sans ** ="+repSansSommeTab[j]);
                const lgCoefMonome = repSansSommeTab[j].length
                // console.log("index:"+repSansSommeTab[j].lastIndexOf("*")+"  lgCoefMonome="+lgCoefMonome)
                if (repSansSommeTab[j].lastIndexOf('*') === lgCoefMonome - 1) {
                  repSansSommeTab[j] = repSansSommeTab[j].substring(0, lgCoefMonome - 1) // on enlève les signes de multiplication
                }
                if (repSansSommeTab[j].lastIndexOf('\\cdot') === lgCoefMonome - 6) {
                  repSansSommeTab[j] = repSansSommeTab[j].substring(0, lgCoefMonome - 6) // on enlève les signes de multiplication
                }
                if (repSansSommeTab[j].indexOf('*') === 0) {
                  repSansSommeTab[j] = repSansSommeTab[j].slice(1) // on enlève les signes de multiplication
                }
                if (repSansSommeTab[j].indexOf('\\cdot') === 0) {
                  repSansSommeTab[j] = repSansSommeTab[j].slice(5) // on enlève les signes de multiplication
                }
                // j’ai un pb avec les valeur décimale'
                if (!repSansSommeTab[j]) {
                  repSansSommeTab[j] = '1'
                } else if (repSansSommeTab[j] === '-') {
                  repSansSommeTab[j] = '-1'
                }
                if (this._isDebug) console.debug('repSansSommeTab[' + j + ']=' + repSansSommeTab[j])
                if (Number.isNaN(Number(repSansSommeTab[j]))) {
                  if (this._isDebug) console.debug(repSansSommeTab[j] + ' n’est pas un nombre')
                  monomeReduit[j] = false
                } else if (Math.abs(repSansSommeTab[j]) < 1e-12) {
                  // dans ce cas est présent le monome 0x^... ce qui n’est pas réduit'
                  monomeReduit[j] = false
                }
                if (!monomeReduit[j]) {
                  estSimplifie = false
                }
              }

              // j’ai vérifié que le polynôme ne contenait plus de parenthèses
              // et si c’est le cas que tous les coefficients sont bien des nombres (pas 3*5 par ex)
              // pour que le polynôme soit bien développé, il suffit de vérifier que je n’ai pas 2 monomes de même degré'
              const tabDegreOrdonne = []
              for (let j = 0; j < degreRepTab.length; j++) {
                tabDegreOrdonne.push(degreRepTab[j])
              }
              tabDegreOrdonne.sort()
              if (this._isDebug) console.debug('degreRepTab:' + degreRepTab + '  tabDegreOrdonne:' + tabDegreOrdonne)
              // si un élément de ce tableau est égal à son suivant, alors on a deux monomes de même degré
              for (let j = 0; j < tabDegreOrdonne.length - 1; j++) {
                if (tabDegreOrdonne[j] === tabDegreOrdonne[j + 1]) {
                  monomesDegresDifferents = false
                }
              }
              // je contrôle enfin le coefficient de chaque monome, voir s’il est bon'
              // d’abord, si une puissance de x apparaît plusieurs fois'
              for (let j = tabDegreOrdonne.length - 1; j >= 1; j--) {
                if (tabDegreOrdonne[j] === tabDegreOrdonne[j - 1]) {
                  degreNonSimplifie = Math.max(degreNonSimplifie, tabDegreOrdonne[j])
                }
              }
              // je reprends la liste des monomes non réduits
              for (let j = 0; j < degreRepTab.length; j++) {
                if (!monomeReduit[j]) {
                  degreNonSimplifie = Math.max(degreNonSimplifie, degreRepTab[j])
                }
              }
              if (this._isDebug) console.debug('degreNonSimplifie 2 :' + degreNonSimplifie)
              // il faut aussi que je réordonne les coefficients dans l’ordre du degré du monôme'
              // le coefficient repSansSommeTab[j] du monome de degré degreRepTab[j] doit être égal à celui du tableau des réponses zone.typeReponse[3]
              const tabMonomePrisEnCompte = [] // ceci sert à vérifier que tous les monômes de la réponses sont comparés
              for (let j = 0; j < zone.typeReponse[3].length; j++) {
                tabMonomePrisEnCompte[j] = false
              }
              for (let j = 0; j < degreRepTab.length; j++) {
                tabMonomePrisEnCompte[degreRepTab[j]] = true // le monôme de degré degreRepTab[j] de la réponse attendu est vérifié
                if (Math.abs(repSansSommeTab[j] - zone.typeReponse[3][degreRepTab[j]]) > 1e-10) {
                  // Il y a un pb de coef dans le monôme de degré degreRepTab[j]
                  if (this._isDebug) console.debug(repSansSommeTab[j] + '  et ' + zone.typeReponse[3][degreRepTab[j]] + '  avec ' + degreRepTab[j])
                  degreErrone = Math.max(degreErrone, degreRepTab[j])
                  if (this._isDebug) console.debug('degreErrone:' + degreErrone)
                }
              }
              for (let j = 0; j < zone.typeReponse[3].length; j++) {
                if (!tabMonomePrisEnCompte[j] && (Math.abs(zone.typeReponse[3][j]) > 1e-12)) {
                  degreErrone = Math.max(degreErrone, j)
                }
              }
              if (degreNonSimplifie > -1) {
                estSimplifie = false
              }
            }
            if (this._isDebug) console.debug('vraiPolynome :' + vraiPolynome + '   autreVariable : ' + autreVariable + '  egalitePolynome:' + egalitePolynome + '  estDeveloppe:' + estDeveloppe + '   estSimplifie:' + estSimplifie + '  monomesDegresDifferents:' + monomesDegresDifferents + '  signeDouble:' + signeDouble + '   irreductible:' + irreductible + '   degreNonSimplifie=' + degreNonSimplifie + '  degreErrone:' + degreErrone)
            bonneReponse = (estDeveloppe && estSimplifie && monomesDegresDifferents && !signeDouble && irreductible)
            detailPolynome = {
              autreVariable,
              vraiPolynome,
              signeDouble,
              irreductible,
              estDeveloppe,
              estSimplifie,
              degreNonSimplifie,
              degreErrone,
              monomesDegresDifferents
            }
            // fin de la partie sur la forme développée
          }
          bonneReponse = (bonneReponse && egalitePolynome)
        } else {
          bonneReponse = false // car autrevariable==true
        }
      } else {
        // on a un nombre
        if (idZone.includes('inputmq')) {
          // pour une zone mathquill, je dois le convertir
          if (repEleve.includes('infty')) {
            // c’est +/-infini
            repEleve = repEleve.replace('\\infty', 'Infinity')
          } else {
            repEleve = this.#calculeValeur(repEleve)
          }
        } else {
          // Dans le cas d’un nombre entier, un copier-coller peut mettre des espaces donc il faudra se débarrasser. Cela por
          if (Number.isInteger(j3pNombre(j3pSupprimeEspaces(repEleve)))) repEleve = repEleve.replace(/\s([0-9]{3})/g, '$1')
          if (!Number.isNaN(j3pNombre(repEleve))) {
            repEleve = j3pNombre(repEleve)
          }
        }
        let ordrePuissance // en lien avec puisPrecision;
        if (zone.typeReponse[1] === 'exact') {
          if (reponse === 0) {
            puisPrecision = 15
          } else {
            ordrePuissance = String(Math.abs(reponse)).indexOf('.')
            puisPrecision = (ordrePuissance === '-1') ? 13 - String(Math.abs(reponse)).length : 13 - ordrePuissance
          }
          bonneReponse = (Math.abs(repEleve - reponse) < Math.pow(10, -puisPrecision))
        } else if (zone.typeReponse[1] === 'approche') {
          bonneReponse = (Math.abs(repEleve - reponse) <= zone.typeReponse[2])
        } else if (zone.typeReponse[1] === 'arrondi') {
          const nbRep1 = Math.floor(reponse / zone.typeReponse[2]) * zone.typeReponse[2]
          const nbRep2 = Math.ceil(reponse / zone.typeReponse[2]) * zone.typeReponse[2]
          // ordrePuissance de nbRep1 est le nombre n tel que 10^n<= nbRep1 < 10^{n+1}
          if (nbRep1 === 0) {
            puisPrecision = 15
          } else {
            ordrePuissance = String(Math.abs(reponse)).indexOf('.')
            // ordrePuissance = Math.floor(Math.log(Math.abs(nbRep1))/Math.log(10))+1;
            puisPrecision = (ordrePuissance === '-1') ? 15 - String(Math.abs(reponse)).length : 15 - ordrePuissance
          }
          let bonArrondi
          if (Math.round((reponse - nbRep1) * Math.pow(10, puisPrecision)) / Math.pow(10, puisPrecision) < zone.typeReponse[2] / 2) {
            bonArrondi = nbRep1
          } else {
            bonArrondi = nbRep2
          }

          // console.log(bonArrondi, puisPrecision, repEleve) // ne pas laisser ça en prod ! ça donne la solution en console
          bonneReponse = (Math.abs(repEleve - bonArrondi) < Math.pow(10, -puisPrecision))
        }
        if (bonneReponse) {
          for (let k = 0; k <= 1; k++) {
            if ((zone.solutionSimplifiee[k] === 'réponse fausse') && !this.zones.reponseSimplifiee[zone.indexZone][k]) {
              // la réponse n’est pas simplifiée (somme ou racine restante pour k=0, fraction réductible pour k=1),
              // et on demande dans la section à ce que cela soit considéré comme une réponse non valide (l’élève doit proposer autre chose)
              bonneReponse = false
            }
          }
        }
      }
    } else {
      // on a une liste
      bonneReponse = (repEleve === Number(reponse))
    }
    return { bonneReponse, detailPolynome }
  }

  /**
   * À documenter
   */
  redonneFocus () {
    // cette fonction permet de redonner le focus à la première zone fausse dans le cas d’une validation
    // où on aurait des zones gérées de manière perso'
    for (let i = this.zones.inputs.length - 1; i >= 0; i--) {
      if (!this.zones.bonneReponse[i]) {
        j3pFocus(this.zones.inputs[i], true)
      }
    }
  }

  /**
   * Utilisez plutôt {@link module:lib/utils/number.pgcd} si vous voulez un vrai pgcd
   * @deprecated
   * @param x
   * @param y
   * @return {string|number}
   */
  pgcd (x, y) {
    const pgcd = j3pPGCD(x, y)
    if (typeof pgcd !== 'number') return 'pb'
    return pgcd
  }

  /**
   * À documenter
   */
  reponseSimplifiee (laZone) {
    // cette fonction vérifie si laZone contient une expression simplifiée
    // elle renvoie un tableau contenant 2 valeurs : [0] si l’expression n’est pas une somme simplifiable
    // [1] pour savoir si la fraction éventuellement présente est bien simplifiée'
    let elt, idZone
    if (typeof laZone === 'string') {
      elt = j3pElement(laZone)
      idZone = laZone
    } else {
      elt = laZone
      idZone = laZone.id
    }
    let repEleve
    if (idZone.includes('inputmq')) {
      // c’est une zone mq
      repEleve = $(elt).mathquill('latex')
    } else if (idZone.includes('input')) {
      // c’est une zone de saisie non mq'
      repEleve = j3pValeurde(elt)
    } else {
      // c’est une liste'
      repEleve = elt.selectedIndex
    }
    if (idZone.includes('inputmq')) {
      // pour une zone mathquill, je dois le convertir
      repEleve = j3pMathquillXcas(repEleve)
      // on doit remplacer sqrt par racine pour l’évaluation'
      while (repEleve.includes('sqrt')) {
        repEleve = repEleve.replace('sqrt', 'racine')
      }
    } else {
      repEleve = j3pNombre(repEleve)
    }
    let simplification
    let fractionsSimplifiees = true
    const sqrtfracRegexp1 = /racine\(\(\d+\)\/\(\d+\)\)/ // new RegExp('racine\\(\\([0-9]{1,}\\)/\\([0-9]{1,}\\)\\)', 'i')
    const sqrtfracRegexp2 = /\(racine\(\d+\)\)\/\(\d+\)/ // new RegExp('\\(racine\\([0-9]{1,}\\)\\)/\\([0-9]{1,}\\)', 'i')
    const nbrestantRegexp = /-?[\d,.]+\*/ // new RegExp('\\-?[0-9\\,\\.]{1,}\\*', 'i')
    let reponseSimplifiee = true
    if (String(repEleve).includes('racine')) {
      // J’ai une racine carrée, je vérifie que le radical est simplifié
      if (sqrtfracRegexp1.test(repEleve)) {
        // de la forme sqrt(...)/... ou ...*sqrt(...)/...
        const tabSqrtfrac1 = repEleve.match(sqrtfracRegexp1)
        reponseSimplifiee = (reponseSimplifiee && ((tabSqrtfrac1.length === 1) && ((repEleve.replace(tabSqrtfrac1[0], '') === '') || (nbrestantRegexp.test(repEleve.replace(tabSqrtfrac1[0], ''))))))
        let fracSqrt = tabSqrtfrac1[0].substring(tabSqrtfrac1[0].indexOf('(') + 1, tabSqrtfrac1[0].length - 1)
        while ((fracSqrt.includes('(')) || (fracSqrt.includes(')'))) {
          fracSqrt = fracSqrt.replace('(', '')
          fracSqrt = fracSqrt.replace(')', '')
        }
        reponseSimplifiee = (reponseSimplifiee && this.estNbFrac(fracSqrt)[0])
      }
      if (sqrtfracRegexp2.test(repEleve)) {
        // de la forme sqrt(.../...) ou ...*sqrt(.../...)
        const tabSqrtfrac2 = repEleve.match(sqrtfracRegexp2)
        reponseSimplifiee = (reponseSimplifiee && ((tabSqrtfrac2.length === 1) && ((repEleve.replace(tabSqrtfrac2[0], '') === '') || (nbrestantRegexp.test(repEleve.replace(tabSqrtfrac2[0], ''))))))
      }
      const sqrtRegexp0 = /[+-]?\d*\*?racine\(\d+\)/g // new RegExp('(\\+|\\-)?[0-9]{0,}\\*?racine\\([0-9]{1,}\\)', 'ig')
      const sqrtRegexp1 = /racine\(\d+\)/g // new RegExp('racine\\([0-9]{1,}\\)', 'ig')
      if (sqrtRegexp0.test(repEleve)) {
        // on a donc une racine carrée dans la réponse de l’élève'
        // reponseSimplifiee = true;
        const tabSqrt1 = repEleve.match(sqrtRegexp1)
        // var tabSqrt0 = repEleve.match(sqrtRegexp0)
        for (let k0 = 0; k0 < tabSqrt1.length; k0++) {
          const arbreSqrt = new Tarbre(tabSqrt1[k0], [])
          if (Math.abs(Math.round(arbreSqrt.evalue([])) - arbreSqrt.evalue([])) < 1e-12) {
            // l’une des racine carrée est simplifiable'
            reponseSimplifiee = false
          }
          if (tabSqrt1.length > 1) {
            // cela signifie qu’il existe plus d’une racine carrée'
            for (let k1 = 0; k1 < tabSqrt1.length; k1++) {
              if (k0 !== k1) {
                const arbreDif01 = new Tarbre(tabSqrt1[k0] + '/(' + tabSqrt1[k1] + ')', [])
                const maValeur = arbreDif01.evalue([])
                if (Math.abs(Math.round(maValeur) - maValeur) < 1e-12) {
                  reponseSimplifiee = false
                }
              }
            }
          }
        }
      }
    } else if (!String(repEleve).includes('/')) {
      // on n’a pas de fraction
      const repValeur = this.#calculeValeur(repEleve)
      if (Math.abs(Math.round(1e8 * repValeur) / 1e8 - repValeur) < 1e-12) {
        // on a une valeur décimale
        // pour être écrite de manière simple, elle doit donc être décmale et ne pas comporter d’opération
        reponseSimplifiee = ((String(repEleve).indexOf('+') <= 0) && (String(repEleve).indexOf('-') <= 0) && (String(repEleve).indexOf('*') <= 0))
        fractionsSimplifiees = true
      }
    } else {
      try {
        simplification = this.estNbFrac(repEleve)
      } catch (e) {
        simplification = [false, false]
      }
      reponseSimplifiee = simplification[0]
      fractionsSimplifiees = simplification[1]
    }
    return [reponseSimplifiee, fractionsSimplifiees]
  }

  /**
   * À documenter
   * @param {string} texte
   * @return {boolean[]} Deux booléens, le premier si ??? et le 2e si ???
   */
  estNbFrac (texte) {
    // cette fonction teste si texte est juste un nombre ou juste une fraction (éventuellement sous la forme a*b/c)
    let repSimple = false
    let fractSimple = true
    let tabNum
    let num, den
    // on peut n’avoir qu’un nombre 'entier ou décimal'
    if (!isNaN(Number(j3pNombre(String(texte))))) {
      repSimple = true
    } else if (!texte.includes('/')) {
      // le nombre n’est pas une fraction
      repSimple = true
    }
    // on seulement une fraction ou bien un nb de la forme a*b/c
    const fractRegexp1 = /\(\d+\)\/\(\d+\)/ // new RegExp('\\([0-9]{1,}\\)/\\([0-9]{1,}\\)', 'i')
    const fractRegexp2 = /\(-\d+\)\/\(\d+\)/ // new RegExp('\\(\\-[0-9]{1,}\\)/\\([0-9]{1,}\\)', 'i')
    const fractRegexp3 = /-?[\d,.]+\*?\(-?\d+\)\/\(\d+\)/ // new RegExp('\\-?[0-9\\.\\,]{1,}\\*?\\(\\-?[0-9]{1,}\\)/\\([0-9]{1,}\\)', 'i')
    if (fractRegexp1.test(texte)) {
      const tabFrac1 = texte.match(fractRegexp1)
      repSimple = ((tabFrac1.length === 1) && ((texte.replace(tabFrac1[0], '') === '') || (texte.replace(tabFrac1[0], '') === '-') || (texte.replace(tabFrac1[0], '') === '+')))
      if (repSimple) {
        // c’est que je n’ai qu’une fraction de la forme -?(...)/(...)'
        // on vérifie que cette fraction est simplifiée
        tabNum = texte.split('/')
        num = j3pNombre(tabNum[0].substring(tabNum[0].indexOf('(') + 1, tabNum[0].length - 1))
        den = j3pNombre(tabNum[1].substring(1, tabNum[1].length - 1))
        fractSimple = ((this.pgcd(num, den) === 1) && (den !== 1))
      }
    }
    if (!repSimple) {
      if (fractRegexp2.test(texte)) {
        const tabFrac2 = texte.match(fractRegexp2)
        repSimple = ((tabFrac2.length === 1) && (texte.replace(tabFrac2[0], '') === ''))
        if (repSimple) {
          // c’est que je n’ai qu’une fraction de la forme (-...)/(...)'
          // on vérifie que cette fraction est simplifiée
          tabNum = texte.split('/')
          num = j3pNombre(tabNum[0].substring(2, tabNum[0].length - 1))
          den = j3pNombre(tabNum[1].substring(1, tabNum[1].length - 1))
          fractSimple = ((this.pgcd(num, den) === 1) && (den !== 1))
        }
      }
    }
    if (!repSimple) {
      if (fractRegexp3.test(texte)) {
        const tabFrac3 = texte.match(fractRegexp3)
        repSimple = ((tabFrac3.length === 1) && (texte.replace(tabFrac3[0], '') === ''))
        if (repSimple) {
          // c’est que je n’ai qu’une fraction de la forme ...*(-?...)/(...)'
          // on vérifie que cette fraction est simplifiée
          tabNum = texte.split('/')
          const posPar = tabNum[0].indexOf('(')
          if (tabNum[0].charAt(posPar - 1) === '*') {
            num = j3pNombre(tabNum[0].substring(0, posPar - 1)) * j3pNombre(tabNum[0].substring(posPar + 1, tabNum[0].length - 1))
          } else {
            num = j3pNombre(tabNum[0].substring(0, posPar)) * j3pNombre(tabNum[0].substring(posPar + 1, tabNum[0].length - 1))
          }
          den = j3pNombre(tabNum[1].substring(1, tabNum[1].length - 1))
          fractSimple = ((this.pgcd(num, den) === 1) && (den !== 1))
        }
      }
    }
    return [repSimple, fractSimple]
  }

  /**
   * À documenter
   * @param fDeX
   * @return {string}
   */
  transformeExp (fDeX) {
    // fDeX est une chaine de caractère correspondant à l’espression d’une fonction : f(x)
    // elle revoie la même chaine en remplaçant e^... par exp(...)
    // on appelle cette fonction après avoir utilisé j3pMathquillXcas, donc il n’y a plus frac{..}{..} mais (..)/(..)
    // Attention car e n’est pas seulement pour exp, il peut aussi l'être pour racine
    let newFDeX = ''
    let finFdex = fDeX
    while (finFdex.includes('ne')) {
      newFDeX += finFdex.substring(0, finFdex.indexOf('ne') + 2)
      finFdex = finFdex.substring(finFdex.indexOf('ne') + 2)
    }
    let posE = finFdex.indexOf('e')
    while (posE > -1) {
      // e est présent et ce n’est aps celui de racine
      newFDeX += finFdex.substring(0, posE)
      // on regarde ce qui suit e
      if ((finFdex.charAt(posE + 1) === 'x') && (finFdex.charAt(posE + 2) === 'p')) {
        // il est déjà écrit exp, donc on passe
        newFDeX += 'exp'
        finFdex = finFdex.substring(posE + 3)
      } else if ((finFdex.charAt(posE + 1) === '+') || (finFdex.charAt(posE + 1) === '-') || (finFdex.charAt(posE + 1) === '*') || (finFdex.charAt(posE + 1) === ')') || (finFdex.charAt(posE + 1) === 'x')) {
        // e est tout seul, je le remplace par exp(1)
        newFDeX += 'exp(1)'
        finFdex = finFdex.substring(posE + 1)
      } else {
        // ce qui suit e est ^. Il faut alors trouver les parenthèses qui encadrent ce dont on prend l’exponentielle
        if (finFdex.charAt(posE + 2) === '(') {
          // c’est de la forme e^(...)
          let parOuverte = 1
          let posParferme = posE + 3
          while (parOuverte > 0) {
            if (finFdex.charAt(posParferme) === '(') {
              parOuverte++
            } else if (finFdex.charAt(posParferme) === ')') {
              parOuverte--
            }
            posParferme++
          }
          newFDeX += 'exp(' + finFdex.substring(posE + 3, posParferme)
          finFdex = finFdex.substring(posParferme)
        } else {
          // c’est uste de la forme e^., cad que seul le caractère suivant est est en puissance
          newFDeX += 'exp(' + finFdex.substring(posE + 2, posE + 3) + ')'
          finFdex = finFdex.substring(posE + 3)
        }
      }
      posE = finFdex.indexOf('e')
    }
    newFDeX += finFdex
    // J’ai encore un souci car il ne reconnait pas 3e comme un produit, donc il faut le lui expliquer...
    newFDeX = newFDeX.replace(/(\d)e/g, '$1*e')
    return newFDeX
  }

  /**
   * À documenter
   * @param texteFonction
   * @return {string}
   * @private
   */
  #getEcriturePourCalcul (texteFonction) {
    // texte fonction est au format latex ou tout autre format intermédiaire
    // on transforme cette écriture pour avoir l’expression sous la forme permettant de créer un arbre et d’appliquer la fonction evalue
    // je convertis pour que ce soit plus facile à utiliser
    let newExpressionFct = j3pMathquillXcas(texteFonction)
    // gestion de la racine carrée
    // il faut que je transforme sqrt en racine
    while (newExpressionFct.includes('sqrt')) {
      newExpressionFct = newExpressionFct.replace('sqrt', 'racine')
    }
    // mais j’ai encore un problème avec l’exponentielle : je peux avoir e isolé, ou bien e^x ou bien e^{...}. Ceschoses ne sont pas comprises, il faut modifier ça et écrire sous la forme exp(...)
    newExpressionFct = this.transformeExp(newExpressionFct)
    while (newExpressionFct.includes('exp()')) {
      newExpressionFct = newExpressionFct.replace('exp()', 'exp(1)')
    }
    return newExpressionFct
  }

  /**
   * À documenter
   * @param chaine
   * @return {number}
   * @private
   */
  #calculeValeur (chaine) {
    // un nombre écrit sous forme d’une chaine de caractères va être calculé pour qu’on ait sa valeur décimale
    const newChaine = this.#getEcriturePourCalcul(chaine)
    const arbreChaine = new Tarbre(newChaine, [])
    return arbreChaine.evalue([])
  }

  /**
   * Gèle le DOM des zones d’input, pour empêcher toute modif du DOM via l’inspecteur d’élément
   * (contre la triche des élèves qui trafiquent la page avant de faire une capture)
   * @private
   */
  #freezeAll () {
    for (const input of this.zones.inputs) {
      if (this.zones.validePerso[input.indexZone] || input.frozen || !this.zones.isActive[input.indexZone]) continue
      j3pFreezeElt(input)
    }
  }
}

export default ValidationZones