Aller au contenu

Terminal et IDE dans Mkdocs⚓︎

Introduction⚓︎

Pyodide-Mkdocs est une solution technique permettant de créer un cours interactif à partir d'un site généré par Mkdocs.

Il permet d'intégrer, dans le navigateur, côté client :

  • une console Python ;
  • un éditeur de code ;
  • un juge en ligne associé à des corrigés.

Garantie :

  • sans cookie
  • sans inscription
  • créé par un enseignant pour les enseignants
Solution

La technologie permettant ce tour de force s'appelle Pyodide.

Pyodide utilise WebAssembly pour faire le lien entre Python et Javascript et proposer un environnement permettant de manipuler le DOM avec Python, ou de manipuler Python avec Javascript.

Aperçu⚓︎

###
# testsbksl-nlbksl-nlassert dentiste("j'ai mal") == 'aia'bksl-nlassert dentiste("il fait chaud") == 'iaiau'bksl-nlassert dentiste("") == ''bksl-nlbksl-nlbksl-nl# pas d'autres testsbksl-nlbksl-nlassert dentiste("a"py-str20 + "b"py-str10 + "e") == 'a'py-str20 + 'e'bksl-nlassert dentiste("b"py-str10 + "e" + "a"py-str20) == 'e' + 'a'py-str20 bksl-nlassert dentiste("ab"py-str10) == 'a'py-str10bksl-nlassert dentiste("aeiouy"py-str10) == 'aeiouy'py-str10bksl-nlassert dentiste("z"py-str100 + 'y') == 'y'bksl-nlbksl-nl 5/5

voyelles = ['a', 'e', 'i', 'o', 'u', 'y']bksl-nlbksl-nldef dentiste(texte):bksl-nl passbksl-nlbksl-nlassert dentiste("j'ai mal") == 'aia'bksl-nlassert dentiste("il fait chaud") == 'iaiau'bksl-nlassert dentiste("") == ''bksl-nlbksl-nlvoyelles = ['a', 'e', 'i', 'o', 'u', 'y']bksl-nlbksl-nldef dentiste(texte):bksl-nl resultat = ''bksl-nl for lettre in texte:bksl-nl if lettre in voyelles:bksl-nl resultat = resultat + lettrebksl-nl return resultatbksl-nlbksl-nl

A

Une première remarque

Ceci est un exercice classique.

D'autres détails

On pourrait représenter la situation dans un tableau :

a b c
1 2 3

Z

Démarrage rapide⚓︎

Vous ne connaissez rien à Mkdocs et vous souhaitez vous y mettre ? Des mots comme yaml, javascript, Pyodide ou templates Jinja vous font peur ?

Commencez en douceur en partant d'un repo Mkdocs aux dernières normes en vigueur : Clonez le répertoire Git !

Installation⚓︎

On part d'une installation comme indiqué sur [https://ens-fr.gitlab.io/mkdocs/] avec le plugin macro, préalablement installé.

L'installation demande de rajouter à cette configuration les éléments suivants.

Modification :

  • fichier YML mkdocs.yml ;
  • fichier de macro main.py ;

Ajout :

  • un dossier my_theme_customizations/ à la racine du projet Mkdocs ;
  • un template HTML my_theme_customizations/main.html ;
  • un fichier CSS docs/xtra/stylesheets/pyoditeur.css ;
  • deux fichiers Javascript docs/xtra/javascripts/interpreter.js et my_theme_customizations/js/ide.js ;
  • deux fichiers Markdown docs/xtra/start.md et docs/xtra/end.md.

Fichier YML mkdocs.yml⚓︎

Ajoutez les lignes surlignées dans votre fichier mkdocs.yml .

    site_name: "Terminal et REPL dans Mkdocs"
    
    ...
    
    theme:
        name: material
        custom_dir: my_theme_customizations/
      
    ...
    
    extra_javascript:
      - xtra/javascripts/mathjax-config.js                    # MathJax
      - javascripts/config.js
      - https://polyfill.io/v3/polyfill.min.js?features=es6
      - https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js
      - xtra/javascripts/interpreter.js
    
    extra_css:
      - xtra/stylesheets/pyoditeur.css
      - xtra/stylesheets/ajustements.css                      # ajustements

Fichier macro Python main.py⚓︎

À votre fichier main.py, ajoutez les lignes du fichier main.py.

Création du dossier custom_dir⚓︎

N'oubliez pas de créer le dossier my_theme_customizations/ à la racine du projet Mkdocs.

Dans ce dossier, ajoutez le template Jinja main.html :

    {% extends "base.html" %}
    
    {% block content %}
    {{super()}}
    <script src="{{ base_url }}/js/ide.js"></script> 
    {% endblock %}
    
    {% block announce %}
    ⚠️ Pyodide-Mkdocs ne se développe pas tout seul ! Prof à 100%, offrez moi un <a href="https://www.buymeacoffee.com/profbouillot" target="_blank">café</a> ou des <a href="https://github.com/bouillotvincent" target="_blank">BAT</a> !
    {% endblock %}
    
    {% block extrahead %}
    
    {% if config.extra.ace_style %}
    {% if config.extra.ace_style['slate'] and config.extra.ace_style['default'] %}
    <input id="ace_palette" autocomplete="off" class="md-toggle" data-ace-dark-mode="{{config.extra.ace_style['slate']}}" data-ace-light-mode="{{config.extra.ace_style['default']}}" type="checkbox"></input>
    {% elif config.extra.ace_style['slate'] %}
    <input id="ace_palette" autocomplete="off" class="md-toggle" data-ace-dark-mode="{{config.extra.ace_style['slate']}}" data-ace-light-mode="crimson_editor" type="checkbox"></input>
    {% elif config.extra.ace_style['default'] %}
    <input id="ace_palette" autocomplete="off" class="md-toggle" data-ace-dark-mode="tomorrow_night_bright" data-ace-light-mode="{{config.extra.ace_style['default']}}" type="checkbox"></input>
    {% endif %}
    {% else %}
    <input id="ace_palette" autocomplete="off" class="md-toggle" data-ace-dark-mode="tomorrow_night_bright" data-ace-light-mode="crimson_editor" type="checkbox"></input>
    {% endif %}
    {% endblock %}
    
    {% block libs %}
        {{ super() }}
        <!-- favicons -->
        <link rel="shortcut icon" href="../assets/images/favicon.ico" type="image/x-icon">
        <link rel="icon" href="../assets/images/favicon_32x32.png" sizes="32x32">
        <link rel="icon" href="../assets/images/favicon_96x96.png" sizes="96x96">
        <link rel="icon" href="../assets/images/favicon_144x144.png" sizes="144x144">
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.5.0/styles/atom-one-light.min.css">
        <!-- Load CDNs : Pyodide (Python in WASM), Ace (Editor) and JQuery (Terminal) -->
        <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.5.0/highlight.min.js"></script>    
        <script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.12/ace.js" type="text/javascript" charset="utf-8"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.12/ext-language_tools.min.js" type="text/javascript" charset="utf-8"></script>
        <script src="https://cdn.jsdelivr.net/pyodide/v0.17.0/full/pyodide.js"></script>
        <script src="https://cdn.jsdelivr.net/pyodide/v0.17.0/full/pyodide.asm.js"></script>
        <script src="https://cdn.jsdelivr.net/npm/js-md5"></script>
        <script src="https://cdn.jsdelivr.net/npm/jquery"></script>
        <script src="https://cdn.jsdelivr.net/npm/jquery.terminal@2.23.0/js/jquery.terminal.min.js"></script>
        <link href="https://cdn.jsdelivr.net/npm/jquery.terminal@2.23.0/css/jquery.terminal.min.css" rel="stylesheet">
    {% endblock %}

Fichier CSS pyoditeur.css⚓︎

Afin de coller au thème du site, recopiez et ajoutez le fichier pyoditeur.css au dossier docs/xtra/stylesheets/.

Si vous avez opté pour d'autres couleurs, c'est là que vous pourrez faire les modifications de l'éditeur.

Fichiers javascripts interpreter.js et ide.js⚓︎

Deux fichiers Javascript interpreter.js et ide.js sont nécessaires :

  • interpreter.js doit être placé dans le dossier : docs/xtra/javascripts/ ;
  • ide.js doit être placé dans le dossier : my_theme_customizations/js/ide.js.

Fichiers start_REM.md et end_REM.md⚓︎

Pour la bonne gestion des fichiers de remarque, il faut également ajouter deux fichiers standardisés au format markdown : start_REM.md et end_REM.md

Images des boutons⚓︎

Les boutons doivent être placés à cette adresse : /docs/images/buttons/. Il existe six boutons que vous pouvez récupérer en téléchargeant l'archive.

Syntaxe et exemples⚓︎

Syntaxe Markdown⚓︎

La syntaxe

{{ terminal() }}
Création d'un terminal vide. L'auto-complétion avec Tab et le rappel de l'historique (avec CtrlR ) sont possibles.

{{ IDE() }}
Création d'un IDE (~ Thonny) vide. La flèche permet de lancer le code tapé dans la zone de saisie (avec les numéros de ligne). La zone de saisie se redimensionne automatiquement et autorise l'auto-complétion de type snippet avec Tab.

###

{{ IDEv() }}
Cette commande crée un IDE vide, avec division verticale. La flèche permet de lancer le code tapé dans la zone de saisie (avec les numéros de ligne). La zone de saisie se redimensionne automatiquement et autorise l'auto-complétion de type snippet avec Tab.

###

{{ IDE('foo/bar/nom_de_fichier', MAX = 8, SANS = 'max,min') }}
Cette commande charge le fichier nom_de_fichier.py dans un IDE. Le fichier doit être dans docs/scripts/foo/bar/. Ne pas oublier les guillemets.

MAX = 8 indique le nombre maximal de tentatives de validation que l'élève peut effectuer. MAX = 1000 permet de mettre ce nombre à l'infini. Valeur par défaut : MAX = 5 .

SANS = 'max,min' permet d'interdire l'utilisation des fonctions built-ins max et min.

Les IDE sont enregistrés à intervalle de temps régulier. Ils permettent également l'autocomplétion avec la combinaison de touches AltSpace.

###
benchmark = ['longueur([])==0', 'longueur([1,3,5,5])==4', 'longueur([0]py-str100)==100']bksl-nl ∞/∞
L = [5,3,4,1]bksl-nlbksl-nldef longueur(L: list) -> int:bksl-nl return bksl-nldef longueur(L: list) -> int:bksl-nl return len(L)bksl-nl

Remarque

Ceci est un exemple complexe de remarque.

La première ligne du fichier de remarque doit être vide

La syntaxe markdown est complètement préservée. Par exemple, un tableau :

a b c
1 2 3

Une admonition ?

Vous pouvez inclure des admonitions et des superfences dans vos remarques.

{{ IDEv('foo/bar/nom_de_fichier', MAX = 1000) }}
Cette commande charge le fichier nom_de_fichier dans un IDE avec division verticale. Le fichier doit être dans docs/scripts/foo/bar/.

###
benchmark = ['longueur([])==0', 'longueur([1,3,5,5])==4', 'longueur([0]py-str100)==100']bksl-nl 3/3
L = [5,3,4,1]bksl-nlbksl-nldef longueur(L: list) -> int:bksl-nl return bksl-nldef longueur(L: list) -> int:bksl-nl return len(L)bksl-nl

Remarque

Ceci est un exemple complexe de remarque.

La première ligne du fichier de remarque doit être vide

La syntaxe markdown est complètement préservée. Par exemple, un tableau :

a b c
1 2 3

Une admonition ?

Vous pouvez inclure des admonitions et des superfences dans vos remarques.

Détails techniques

Tous les IDE et les terminaux partagent le même namespace. On peut donc accéder à n'importe quelle fonction créée dans n'importe quel IDE ou terminal.

C'est un comportement qui a l'avantage de pouvoir proposer des exercices où l'on construit petit à petit un code complexe.

Exemples⚓︎

L'exemple ci-dessous, obtenu avec {{ IDEv('exo2') }}. N'hésitez pas à modifier le code pour calculer la moyenne, l'écart-type, afficher cela dans le terminal etc.

###
b1 = ['somme([]) == None', 'somme([1]) == 1', 'somme([1,2]) == 3', 'somme([-1,1]) == 0']bksl-nlb2 = ['sommation([1]) == 1', 'sommation([1,2]) == 3', 'sommation([-1,1]) == 0']bksl-nlbksl-nlbenchmark = (b1, b2)bksl-nl 5/5

def sommation(T: list) -> int:bksl-nl a = 0bksl-nl for nombre in T:bksl-nl a = a + nombrebksl-nl return abksl-nlbksl-nldef somme(L: list) -> None or int:bksl-nl return None if len(L) == 0 else sum(L)bksl-nldef somme(L: list[int]) -> int:bksl-nl return None if len(L) == 0 else sum(L)bksl-nl

A

Remarque sur la solution

C'est simple mais il faut être vigilant.

Une autre remarque est possible

Toujours simple mais toujours vigilant.

Z

L'exemple ci-dessous a été obtenu avec {{ IDE('algo_glouton') }}.

###

# dictionnaire :bksl-nl# - clé : nom de l'objetbksl-nl# - valeur : tableau [poids, prix]bksl-nlinventaire = {"A": [13,700], "B": [12,650], "C": [6,250], "D": [6,400],"E": [5, 100]}bksl-nlbksl-nl# Calcule la valeur massique en divisant la 2ème valeur du tableau par la premièrebksl-nl# on ajoute cela à la valeur du dictionnairebksl-nlfor objet, (poids, prix) in inventaire.items():bksl-nl inventaire[objet].append(prix/poids)bksl-nlbksl-nl# Trie le tableau en ordre décroissant suivant la valeur massique.bksl-nldef f(dico: dict, col = 2):bksl-nl tableaupy-undtrié = sorted(dico.items(), key = lambda a: a[1][col], reverse=True)bksl-nl return {clé:valeur for clé, valeur in tableaupy-undtrié}bksl-nlbksl-nlbksl-nlinventaire = f(inventaire, 2)bksl-nlbksl-nlpoidspy-undmax = 30bksl-nlbksl-nl# Algorithme gloutonbksl-nldef gloutonnerie(inventaire : dict, poidspy-undmax:int=30):bksl-nl sacpy-undapy-unddos = []bksl-nl poidspy-undsac = 0bksl-nl for objet, (poids, prix, vpy-undmassique) in inventaire.items():bksl-nl if poidspy-undsac + poids <= poidspy-undmax:bksl-nl sacpy-undapy-unddos.append(objet)bksl-nl poidspy-undsac += poidsbksl-nl return sacpy-undapy-unddos, poidspy-undsacbksl-nlbksl-nlprint(gloutonnerie(inventaire, poidspy-undmax))bksl-nlbksl-nlbksl-nl

A

Z