legacy/outils/rexams/main.js

import $ from 'jquery'

import { j3pAddElt, j3pAjouteCaseCoche, j3pAjouteZoneTexte, j3pBarre, j3pBoutonRadio, j3pBoutonRadioChecked, j3pDiv, j3pElement, j3pFocus, j3pGetRandomElt, j3pGetRandomInt, j3pImporteAnnexe, j3pMathquillXcas, j3pPaletteMathquill, j3pShowError, j3pValeurde } from 'src/legacy/core/functions'
import loadWebXcas from 'src/legacy/outils/xcas/loadWebXcas'
import textesGeneriques from 'src/lib/core/textes'
import { j3pAffiche } from 'src/lib/mathquill/functions'

const { cBien, cFaux } = textesGeneriques

// Les types valides
// ATTENTION, au 2023-02-01 seul le type singleChoice a été testé (une seule section utilise rexams et elle l’impose)
const typesExos = ['mathquill', 'num', 'multipleChoice', 'singleChoice']

// la fct d’évaluation des expressions par webXcas
let xcas

/**
 * Intégration de R-exams dans J3P par Patrick Raffinat 20 Mai 2020
 * @module legacy/outils/rexams/main
 */

export function enonceReset (_parcours) {
  _parcours.videLesZones()
  initTypeExo(_parcours)
  enonceComposants(_parcours)
  enonceQuestions(_parcours)
}

export function correction (_parcours) {
  if (!correctionAvecReponse(_parcours)) {
    _parcours.reponseManquante('correction')
  } else {
    const cbon = correctionTestReponse(_parcours)
    if (cbon) {
      correctionOk(_parcours)
    } else {
      correctionKo(_parcours)
    }
    _parcours.finCorrection()
  }
}

export function navigation (_parcours) {
  if (!_parcours.sectionTerminee()) {
    _parcours.etat = 'enonce'
  }
  _parcours.finNavigation()
}

function enonceComposants (_parcours) {
  const { typeExo, extol } = _parcours.storage
  j3pDiv(_parcours.zonesElts.MG,
    {
      id: 'div_reponse',
      contenu: '',
      coord: [10, 10],
      style: { width: '95%' }
    }
  )
  let coordEnonce = 10
  if (typeExo === 'mathquill') {
    coordEnonce = 120
    j3pElement('div_reponse').innerHTML = '<span style="color:blue">Réponse : </span>'
    // var latex = "e^{2.65 x} \\cdot x^2 \\cdot (3 + 2.5 x)";
    j3pAffiche('div_reponse', 'rexams_', '&1&',
      {
        inputmq1: { texte: '' }
      }
    )
    j3pDiv('div_reponse', 'Palette', '')
    j3pAddElt('Palette', 'br')
    j3pPaletteMathquill('Palette', 'rexams_inputmq1', { liste: ['racine', 'exp', 'fraction', 'puissance'] })
    j3pAddElt('Palette', 'br')
    j3pElement('rexams_inputmq1').focus()
  } else if (typeExo === 'num') {
    coordEnonce = 60
    let txt = 'Réponse (à ' + extol + ' près) : '
    txt = '<span style="color:blue">' + txt + '</span>'
    j3pElement('div_reponse').innerHTML = txt
    j3pAjouteZoneTexte('div_reponse', { id: 'rexams_rep', maxchars: '10', restrict: /[0-9.]/, texte: '', tailletexte: 20, width: 100 })
    j3pFocus('rexams_rep')
  }
  j3pDiv(_parcours.zonesElts.MG,
    {
      id: 'div_enonce',
      contenu: '',
      coord: [10, coordEnonce],
      style: { width: '95%' }
    }
  )
  j3pDiv(_parcours.zonesElts.MD,
    {
      id: 'explications',
      contenu: '',
      coord: [10, 60],
      style: {}
    }
  )
  j3pDiv(_parcours.zonesElts.MD,
    {
      id: 'correction',
      contenu: '',
      coord: [10, 10],
      style: _parcours.styles.petit.correction
    }
  )
}

function enonceQuestions (_parcours) {
  const { typeExo, fichiersHtmlAnnexes } = _parcours.storage
  const n = j3pGetRandomInt(0, fichiersHtmlAnnexes.length - 1) // La question est choisie parmi celles qui n’ont pas encore été posées
  const fichier = fichiersHtmlAnnexes[n]
  fichiersHtmlAnnexes.splice(n, 1) // On la retire des questions à poser la prochaine fois
  j3pImporteAnnexe(`qcmRexams/${fichier}`)
    .then(html => {
      const stor = _parcours.storage
      const divEnonce = j3pElement('div_enonce')
      const liste = html.split('\n')
      let nbChoix = 0
      const listeEnonce = []
      const listeSolution = []
      let dansEnonce = false
      let dansSolution = false
      let dansChoix = false
      for (let i = 0; i < liste.length - 1; i++) {
        if (liste[i].indexOf('<br/>') === 0) {
          liste[i] = ''
          continue
        }
        if (liste[i].indexOf('<h4>Question</h4>') >= 0) {
          listeEnonce.push(liste[i])
          dansEnonce = true
          continue
        }
        if (!dansEnonce && !dansSolution) {
          liste[i] = ''
          continue
        }
        if (dansEnonce) {
          if (liste[i].indexOf('<ol ') === 0) {
            if (typeExo === 'multipleChoice' || typeExo === 'singleChoice') {
              divEnonce.innerHTML += '!ol!'
            }
          } else if (liste[i].indexOf('</ol>') === 0) {
            if (typeExo === 'multipleChoice' || typeExo === 'singleChoice') {
              divEnonce.innerHTML += '!/ol!'
            }
          } else if (liste[i].indexOf('<li>') === 0) {
            dansChoix = true
          } else if (liste[i].indexOf('</li>') === 0) {
            dansChoix = false
          } else if (liste[i].indexOf('<h4>Solution</h4>') === 0) {
            dansEnonce = false
            dansSolution = true
            listeSolution.push('<h4>Explications</h4>')
            nbChoix = 0
          } else if (dansChoix) {
            if (typeExo === 'singleChoice') {
              nbChoix = nbChoix + 1
              const id = 'rexams_rep_' + nbChoix
              divEnonce.innerHTML += '!li!'
              j3pBoutonRadio('div_enonce', id, 'ensemble', String(nbChoix), liste[i])
              divEnonce.innerHTML += '!/li!'
            } else if (typeExo === 'multipleChoice') {
              nbChoix = nbChoix + 1
              const id = 'rexams_rep_' + nbChoix
              divEnonce.innerHTML += '!li!'
              j3pAjouteCaseCoche('div_enonce', { id })
              divEnonce.innerHTML += liste[i]
              divEnonce.innerHTML += '!/li!'
            }
          } else {
            listeEnonce.push(liste[i])
          }
        } else if (dansSolution) {
          if (liste[i].indexOf('<ol') === 0) {
            if (typeExo !== 'singleChoice') listeSolution.push(liste[i])
          } else if (liste[i].indexOf('</ol>') === 0) {
            dansSolution = false
            if (typeExo !== 'singleChoice') listeSolution.push(liste[i])
          } else if (liste[i].indexOf('<li>') === 0) {
            nbChoix = nbChoix + 1
            if (typeExo !== 'singleChoice') listeSolution.push(liste[i])
          } else if (liste[i].indexOf('</li>') === 0) {
            if (typeExo !== 'singleChoice') listeSolution.push(liste[i])
          } else if ((liste[i].indexOf('False') === 0)) {
            if (typeExo === 'multipleChoice') {
              listeSolution.push(liste[i])
              if (nbChoix === 1) stor.solution = []
              stor.solution.push(false)
            }
          } else if ((liste[i].indexOf('True') === 0)) {
            if (typeExo === 'singleChoice') {
              stor.solution = nbChoix
            }
            if (typeExo === 'multipleChoice') {
              listeSolution.push(liste[i])
              if (nbChoix === 1) stor.solution = []
              stor.solution.push(true)
            }
          } else if (liste[i].indexOf('!!!') >= 0 || liste[i].indexOf('<mi>!</mi>') >= 0) {
            const i1 = liste[i].indexOf('!!!')
            const i2 = liste[i].indexOf('!!!', i1 + 3)
            stor.solution = liste[i].substring(i1 + 3, i2) // rep
            listeSolution.push(liste[i])
          } else {
            listeSolution.push(liste[i])
          }
        }
      }
      j3pElement('correction').innerHTML = ''
      let codeChoix = divEnonce.innerHTML
      codeChoix = codeChoix.split('!li!').join('<li style="color:blue">')
      codeChoix = codeChoix.split('!/li!').join('</li>')
      codeChoix = codeChoix.split('!ol!').join('<ol type="a" style="color:blue">')
      codeChoix = codeChoix.split('!/ol!').join('</ol>')
      if (typeExo === 'multipleChoice') { // à cause de j3pCoche
        codeChoix = codeChoix.split('<br>').join('')
        codeChoix = codeChoix.split('j3pGestionCoche(this)').join('')
      }
      const codeEnonce = listeEnonce.join('\n')
      divEnonce.innerHTML = '\n' + codeEnonce
      divEnonce.innerHTML += codeChoix
      // parade pb MathJax Chrome : on met les explications et on les masque
      let codeSolution = listeSolution.join('\n')
      codeSolution = codeSolution.split('True.').join('Vrai.')
      codeSolution = codeSolution.split('False.').join('Faux.')
      codeSolution = codeSolution.split('!!!').join('')
      codeSolution = codeSolution.split('<mi>!</mi>').join('')
      j3pElement('explications').innerHTML = codeSolution
      j3pElement('explications').style.display = 'none'
      reloadMathjax()
      return loadWebXcas()
    })
    .then((_xcas) => {
      xcas = _xcas
      _parcours.finEnonce()
    })
    .catch(error => {
      j3pShowError(error, { message: 'Impossible de charger le contenu de cet exercice' })
    })
} // enonceQuestions

function correctionAvecReponse (_parcours) {
  const { typeExo, solution } = _parcours.storage
  let cbon = true
  if (typeExo === 'singleChoice') {
    const repEleve = j3pBoutonRadioChecked('ensemble')
    cbon = (repEleve[0] >= 0)
  } else if (typeExo === 'multipleChoice') {
    cbon = false
    const nbChoix = solution.length
    for (let i = 1; i <= nbChoix; i++) {
      if (document.getElementById('rexams_rep_' + i).checked) cbon = true
    }
    if (!cbon) {
      // eslint-disable-next-line no-alert
      cbon = confirm('confirmez-vous ne vouloir cocher aucun choix ?')
    }
  } else if (typeExo === 'num') {
    const repEleve = j3pValeurde('rexams_rep')
    if (!repEleve.trim()) return false
  } else if (typeExo === 'mathquill') {
    // en 2026-01 on a aucune section dans ce cas
    if (typeof window.jQuery?.fn.mathquill !== 'function') {
      throw Error('Mathquill n’a pas été chargé')
    }
    const repEleve = $(document.getElementById('rexams_inputmq1')).mathquill('latex')
    if (!repEleve.trim()) return false
  }
  return cbon
}

function correctionOk (_parcours) {
  _parcours.score++
  j3pElement('correction').style.color = _parcours.styles.cbien
  j3pElement('correction').innerHTML = cBien
  // parade pb MathJax Chrome : on réaffiche les explications
  j3pElement('explications').style.display = 'inline' // j3pElement("explications").innerHTML = stor.solutionTxt;
  _parcours.etat = 'navigation'
  _parcours.sectionCourante()
}

function correctionKo (_parcours) {
  const { typeExo } = _parcours.storage
  j3pElement('correction').style.color = _parcours.styles.cfaux
  j3pElement('correction').innerHTML = cFaux
  if (_parcours.essaiCourant >= _parcours.donneesSection.nbchances) {
    // Erreur au dernier essai
    // parade pb MathJax Chrome : on réaffiche les explications
    j3pElement('explications').style.display = 'inline' // j3pElement("explications").innerHTML = stor.solutionTxt;
    const stor = _parcours.storage
    const repOk = stor.solution
    if (typeExo === 'multipleChoice') {
      const nbChoix = stor.solution.length
      let note = 0
      for (let i = 1; i <= nbChoix; i++) {
        const repEleve = document.getElementById('rexams_rep_' + i)
        if (repEleve.checked !== repOk[i - 1]) {
          repEleve.parentNode.style.color = 'red'
        } else {
          note += 1 / nbChoix
        }
      }
      _parcours.score += note
    } else if (typeExo === 'singleChoice') {
      document.getElementById('rexams_rep_' + repOk).parentNode.style.color = 'red'
      document.getElementById('rexams_rep_' + repOk).parentNode.style.backgroundColor = 'yellow'
      let i = 1
      while (!document.getElementById('rexams_rep_' + i).checked) {
        i = i + 1
      }
      document.getElementById('rexams_rep_' + i).parentNode.style.color = 'red'
      // document.getElementById("rexams_rep_"+i).parentNode.style.textDecoration = "underline overline";
    } else if (typeExo === 'num') {
      j3pElement('rexams_rep').style.color = 'red'
      j3pBarre('rexams_rep')
    } else if (typeExo === 'mathquill') {
      j3pElement('rexams_inputmq1').style.color = 'red'
      j3pBarre('rexams_inputmq1')
    }
    _parcours.etat = 'navigation'
    _parcours.sectionCourante()
  }
}

function correctionTestReponse (_parcours) {
  const { typeExo, extol } = _parcours.storage
  const { solution } = _parcours.storage
  if (typeExo === 'singleChoice') {
    return Boolean(document.getElementById('rexams_rep_' + solution).checked)
  }
  if (typeExo === 'multipleChoice') {
    return solution.entries().every(([i, reponse]) => document.getElementById('rexams_rep_' + (i + 1)).checked === reponse)
  }
  if (typeExo === 'num') {
    let tolerance = extol
    if (!Number.isFinite(extol)) {
      console.error(Error('Paramétrage incorrect, il manque la tolérance (extol n’est pas défini)'))
      tolerance = solution / 20
    }
    const repEleve = Number(document.getElementById('rexams_rep').value)
    return (Math.abs(solution - repEleve) < tolerance)
  }
  if (typeExo === 'mathquill') {
    const repOk = solution.split('\\cdot').join('\\times')
    const repOkXcas = j3pMathquillXcas(repOk)
    const repEleve = $('#rexams_inputmq1').mathquill('latex')
    const repEleveXcas = j3pMathquillXcas(repEleve)
    const diff = xcas('simplify(normal(' + repEleveXcas + '-(' + repOkXcas + ')))')
    return diff === 0
  }
  throw Error(`type d’exercice ${typeExo} non géré`)
}

function reloadMathjax () {
  if (navigator.appName.indexOf('irefox') >= 0) return
  const src = 'https://mathjax.rstudio.com/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML'
  try {
    // $('script[src="' + src + '"]').remove();
    $('script[src]').remove()
    window.MathJax = null
  } catch (err1) {
    console.error(err1)
  }
  try {
    $('<script>').attr('src', src).appendTo('head')
  } catch (err2) {
    console.error(err2)
  }
}

function initTypeExo (_parcours) {
  const stor = _parcours.storage
  const ds = _parcours.donneesSection
  if (typesExos.includes(stor.typeExo)) return // un seul type ok, on le garde tel quel
  if (typesExos.includes(ds.typeExo)) {
    // un seul type ok, en param on le garde tel quel en le mettant dans stor
    stor.typeExo = ds.typeExo
    return
  }

  // on peut nous passer une liste en param (choisi par l’utilisateur) ou dans stor (déterminé par la section)
  let typesExosVoulus = ds.typesExos || stor.typesExos

  // si y’a pas de storage.typeExo, on peut fournir une liste dans donneesSection.typesExos
  if (typeof typesExosVoulus === 'string') {
    typesExosVoulus = typesExosVoulus
      .replace(/[[\] ]/g, '') // vire les éventuels crochets et espaces
      .split(/[,;]/g) // découpe sur le séparateur , ou ;
    // on ne vire pas les inconnus, c’est fait juste en dessous avec un warning
  }
  let typesExosOk
  if (Array.isArray(typesExosVoulus)) {
    // on vérifie les types fournis
    typesExosOk = typesExosVoulus.filter(type => {
      if (!typesExos.includes(type)) {
        console.error(Error(`Le type ${type} n’est pas un type valide pour rexams (pas dans ${typesExos.join('|')})`))
        return false
      }
      return true
    })
  }
  if (!typesExosOk?.length) {
    console.error(Error('Aucun type fourni (ni typeExo ni typesExos en paramètre)'))
    typesExosOk = typesExos
  }
  // et on init le type (qui sera le même pour toutes les répétitions)
  stor.typeExo = j3pGetRandomElt(typesExosOk)
}