legacy/outils/algo/algo_python.js

/**
 * Regroupe des fonctions utilisées dans les sections python (hors basthon)
 * @module legacy/outils/algo/algo_python
 */

// ça plante avec cette version
// import Sk from 'skulpt'
// => faut prendre la vieille qui existe dans docroot/externals (ou utiliser plutôt basthon)
// Il faut donc que la section déclare l’outil skulpt ou appelle notre fct init

import { j3pElement, j3pEmpty, j3pShowError } from 'src/legacy/core/functions'
import { loadJs } from 'sesajs/dom'

import { j3pBaseUrl } from 'src/lib/core/constantes'

let Sk = window.Sk

export async function init () {
  try {
    if (Sk) return
    const basePath = j3pBaseUrl + 'externals/editeurs/'
    await loadJs(basePath + 'skulpt/skulpt.min.js')
    await loadJs(basePath + 'skulpt/skulpt-stdlib.js')
    Sk = window.Sk
    if (!window.ace) await loadJs(basePath + 'ace/ace.js')
  } catch (error) {
    j3pShowError(error, { message: 'Le chargement de l’interpréteur python a échoué', mustNotify: true })
  }
}

function ensureSk () {
  if (!Sk) {
    if (!window.Sk) throw Error('Skulpt n’est pas chargé (il faut appeler init avant)')
    Sk = window.Sk
  }
}

function getCleanCode (code) {
  // skulpt plante si on lui passe un caractère non ascii…
  return code
    // .replace(/\t/g, '    ')
    // .replace(/ {4}/g, '\t')
    .replace(/[áàâ]/g, 'a')
    .replace(/ç/g, 'c')
    .replace(/[éèëê]/g, 'e')
    .replace(/[îï]/g, 'i')
    .replace(/[ô]/g, 'o')
    .replace(/[ûüù]/g, 'u')
    .replace(/’/g, '\'')
    // et on remplace tous les autres caractères non ascii (hors de la plage 32-126) par une espace
    .replace(/[^\t\n !"#$%&'()*+,\-./0-9:;<=>?@A-Z[\\\]^_`a-z{|}~]/g, ' ')
}

function outf (text) {
  const mypre = document.getElementById('output')
  mypre.innerHTML = mypre.innerHTML + text
}

function builtinRead (x) {
  ensureSk()
  if (Sk.builtinFiles === undefined || Sk.builtinFiles.files[x] === undefined) {
    throw Error('File not found: "' + x + '"')
  }
  return Sk.builtinFiles.files[x]
}

export function runPython (editor, laConsole, ajoutPrint) {
  ensureSk()
  // ajoutPrint est un booléen (optionnel) qui vaut true par défaut
  // lorsqu’il vaut false, le programme n’ajoute pas de print à la validation pour être testé
  if (typeof ajoutPrint !== 'boolean') ajoutPrint = true
  effacerOutput()
  let prog = editor.getValue()
  if (prog.lastIndexOf('\n') !== prog.length - 1) {
    // c’est que je n’ai pas de retour à la ligne à la fin du programme, donc il me faut l’ajouter
    prog += '\n'
  }
  if (laConsole) {
    const contenuConsole = laConsole.getValue()
    if (contenuConsole.indexOf('print') === 0 || !ajoutPrint) {
      prog += laConsole.getValue()
    } else {
      prog += 'print ' + laConsole.getValue()
    }
  }
  prog = getCleanCode(prog)

  const mypre = document.getElementById('output')
  mypre.innerHTML = ''
  Sk.canvas = 'mycanvas'
  Sk.pre = 'output'
  Sk.configure({ output: outf, read: builtinRead, python3: true })
  try {
    // eslint-disable-next-line no-eval
    eval(Sk.importMainWithBody('<stdin>', false, prog))
  } catch (pythonError) {
    console.error('erreur python', pythonError)
    Println(pythonError.toString())
    try {
      const lig = pythonError.lineno || pythonError.traceback[0].lineno
      Println('erreur ligne ' + lig)
      editor.gotoLine(lig)
    } catch (error) {
      console.error('erreur lors de l’affichage de l’erreur python', error)
    }
  }
}

export function runPythonCode (prog) {
  ensureSk()
  prog = getCleanCode(prog)
  const mypre = document.getElementById('output')
  mypre.innerHTML = ''
  Sk.canvas = 'mycanvas'
  Sk.pre = 'output'
  Sk.configure({ output: outf, read: builtinRead, python3: true })
  try {
    // eslint-disable-next-line no-eval
    eval(Sk.importMainWithBody('<stdin>', false, prog))
  } catch (e) {
    console.error('erreur python', e)
    Println(e.toString())
  }
}

// ---------------------------------------------------------------------------
// Rajouté pour J3P
// ---------------------------------------------------------------------------
// getText ne semble pas être utilisée, mais pourquoi est-elle là alors ???
// eslint-disable-next-line no-unused-vars
function getText () {
  // return editor.getValue();
  let code = document.getElementById('pre_editor').innerHTML
  code = code.split('<BR>').join('\n')
  code = code.split('<br>').join('\n')
  code = code.split('&nbsp;').join('')
  // confirm(code);
  return code
}

// setText ne semble pas être utilisée, mais pourquoi est-elle là alors ???
// eslint-disable-next-line no-unused-vars
function setText (texte) {
  // editor.setValue(texte, -1);
  document.getElementById('pre_editor').innerHTML = texte
}

export function getTextAce (editor) {
  return editor.getValue()
}

export function setTextAce (editor, texte) {
  editor.setValue(texte, -1)
}

// Print ne semble pas être utilisée, mais pourquoi est-elle là alors ???
// eslint-disable-next-line no-unused-vars
function Print (texte) {
  const txt = (typeof texte !== 'undefined') ? texte : ''
  const mypre = document.getElementById('output')
  mypre.innerHTML = mypre.innerHTML + txt
}

function Println (texte) {
  const txt = (typeof texte !== 'undefined') ? texte : ''
  const mypre = document.getElementById('output')
  mypre.innerHTML = mypre.innerHTML + txt + '\n'
}

function effacerOutput () {
  const mypre = document.getElementById('output')
  mypre.innerHTML = ''
}

export function desactiverEditeur (editeur, nomDivEditor, codePython) {
  // editeur est le nom de l’éditeur
  // nomDivEditor est le nom de la div contenant l’éditeur
  // codePython est le dernier code présent dans l’éditeur
  try {
    const divEditor = (typeof nomDivEditor === 'string') ? j3pElement(nomDivEditor) : nomDivEditor
    divEditor.addEventListener('input', function () { setTextAce(editeur, codePython) })
    divEditor.addEventListener('keyup', function (e) {
      const keynum = (window.event) ? e.keyCode : e.which
      if (keynum === 46 || keynum === 8) {
        setTextAce(editeur, codePython)
      }
    })
  } catch (error) {
    console.error(error)
  }
}

/**
 * Vérifie que le code contient bien un : en fin de chaque ligne contenant les mots clés qui le nécessitent
 * @param {string} algo
 * @return {{numLigne: number, presents: boolean}}
 */

export function verifDeuxPoints (algo) {
  // algo est l’algo au format python
  // on vérifie qu'à la fin de chaque ligne comportant un mot clé on ait les deux-points
  const motsCles = ['def', 'for', 'if', 'while', 'elif', 'else']
  const lignes = algo.split('\n')
  let deuxPointsPresents = true
  let numLigne = -1
  for (let i = lignes.length - 1; i >= 0; i--) {
    let txt = lignes[i].replace(/\t/g, '')
    for (let k = 0; k < motsCles.length; k++) {
      if (txt.indexOf(motsCles[k]) === 0) {
        // je vérifie qu'à la fin j’ai bien deux points (attention aux espaces)
        txt = txt.replace(/\s*/g, '')
        if (txt.charAt(txt.length - 1) !== ':') {
          deuxPointsPresents = false
          numLigne = i
        }
      }
    }
  }
  return { presents: deuxPointsPresents, numLigne: numLigne + 1 }
}

/**
 * Vérifie que les lignes débutant avec def|for|if|while|elif|else se terminent par : (et pas les autres)
 * En cas de pb (presents est à false) numLigne donne le n° de la ligne qui pose pb
 * @param {string} algo
 * @return {{numLigne: number, presents: boolean}}
 */
export function verifDeuxPointsBis (algo) {
  // algo est l’algo au format python
  // on vérifie qu'à la fin de chaque ligne comportant un mot clé on ait les deux-points à la fin, et pas les autres
  let numLigne
  const deuxPointsPresents = algo.split('\n').every(function (line, lineIndex) {
    numLigne = lineIndex + 1
    // si on trouve un de ces mots clés faut : en fin de ligne
    if (/^[\t ]*(def|for|if|while|elif|else)[\t ]/.test(line)) {
      // y’a le mot clé, éventuellement précédé d’une espace|tabulation, et suivi d’une espace|tabulation
      return /: *$/.test(line) // ok si y’a les deux points à la fin (on teste pas entre les deux)
    } else {
      // faut pas les 2 points
      return !(/: *$/.test(line))
    }
  })
  return { presents: deuxPointsPresents, numLigne }
}

/**
 * Vérifie que les lignes débutant avec def|for|if|while|elif|else se terminent par : (et pas les autres)
 * En cas de pb (presents est à false) numLigne donne le n° de la ligne qui pose pb
 * @param {string} algo
 * @return {{numLigne: number, presents: boolean}}
 * /
function getFirstLineWithColonPb (algo) {
  // algo est l’algo au format python
  // on vérifie qu'à la fin de chaque ligne comportant un mot clé on ait les deux-points à la fin, et pas les autres
  let numLigne = 0
  // avec every la boucle s’arrête au 1er false retourné
  algo.split('\n').every(function (line, lineIndex) {
    // si on trouve un de ces mots clés faut : en fin de ligne
    if (/^[\t ]*(def|for|if|while|elif|else)[\t ]/.test(line)) {
      // y’a le mot clé, éventuellement précédé d’une espace|tabulation, et suivi d’une espace|tabulation, on regarde la fin
      if (!(/:[\t ]*$/.test(line))) numLigne = lineIndex + 1
    } else {
      // faut pas les 2 points
      if (!(/: *$/.test(line))) numLigne = lineIndex + 1
    }
    // si on a affecté numLigne ça renverra false et la boucle s’arrêtera là
    return numLigne === 0
  })
  return numLigne
} /* */

/**
 * Vérifie que les lignes contiennent def|for|if|while|elif|else ou bien se terminent par :
 * @param {string[]} algo
 * @return {boolean}
 */
export function deuxPointsPresents (algo) {
  return algo.split('\n').every(function (line) {
    // si on trouve un de ces mots on considère ça ok
    if (/(def|for|if|while|elif|else)/.test(line)) return true
    // @todo mettre une regex un peu plus stricte, une par instruction, par ex
    // if (/^ *def [a-zA-Z0-9_-]+ *$/.test(line)) return true
    return /: */.test(line)
  })
}

// verifPython ne semble pas être utilisée, mais pourquoi est-elle là alors ???
// eslint-disable-next-line no-unused-vars
function verifPython (algoEleve, algoSecret, entrees) {
  const outputElt = j3pElement('output')
  // on ajoute les entrees à tester
  let codeEntrees = ''
  Object.keys(entrees).forEach(function (cle) {
    codeEntrees += cle + ' = ' + entrees[cle] + '\n'
  })
  // on ote les saisies de l’algo eleve (on garde les lignes sans "input(")
  const cleanedAlgoEleve = algoEleve.split('\n').filter(function (line) {
    if (!line) return false
    return !(/input\(/.test(line))
  }).join('\n')
  // idem pour l’algo secret
  const cleanedAlgoSecret = algoSecret.split('\n').filter(function (line) {
    if (!line) return false
    return !(/input\(/.test(line))
  }).join('\n')
  // on exécute données + algoSecret
  runPythonCode(codeEntrees + cleanedAlgoSecret)
  const sortieSecret = outputElt.innerHTML
  // on exécute données + algoEleve
  runPythonCode(codeEntrees + cleanedAlgoEleve)
  const sortieEleve = outputElt.innerHTML
  // on s’arrête si le test élève sort une erreur
  if (sortieEleve.includes('Error')) {
    outputElt.innerHTML = 'Ton programme a échoué avec\n' + codeEntrees + '\n' + outputElt.innerHTML
    return false
  }
  // on compare les 2 exécutions (on vire \n et <br> avant comparaison)
  if (sortieSecret.replace(/(\n|<br>)/g, '') === sortieEleve.replace(/(\n|<br>)/g, '')) {
    j3pEmpty(outputElt)
    return true
  }
  // on explique l’échec
  outputElt.innerHTML = 'Résultat faux pour ' + codeEntrees
  outputElt.innerHTML += '\nta réponse : ' + sortieEleve
  outputElt.innerHTML += '\nla bonne réponse : ' + sortieSecret
  return false
}