Enseignement et automatisation avec Pandoc

2020-10-19

Comment créer des versions au contenu différent à partir d’un seul fichier avec Pandoc et Markdown. Tutoriel qui s’appuie sur un exemple pédagogique (créer simultanément un fascicule d’exercices avec et sans correction) et dans lequel j’explique pas-à-pas deux fonctionnalités géniales de Pandoc : la syntaxe native qui permet de créer des conteneurs génériques (équivalents aux éléments div du langage HTML), et les filtres.

L’idée est la suivante : à partir d’un unique document « source » au format texte, générer automatiquement plusieurs versions dont le contenu diffère en fonction de certaines conditions. Par exemple, on prépare des exercices et leur correction dans un fichier rédigé en Markdown, et on utilise Pandoc pour fabriquer deux versions PDF de ce texte : une qui n’affiche que les exercices et une qui affiche les exercices avec leur correction.

Pré-requis

Ce tutoriel requiert Pandoc 2.0 (2017-10-29) ou plus récent, car il repose sur la fonctionnalité des filtres Lua introduite dans cette version.

Les conteneurs génériques

On rédige ici en Pandoc Markdown, c’est-à-dire la variante de Markdown spécifique à Pandoc. Pensée par un chercheur (John MacFarlane) pour l’écriture scientifique, cette variante ajoute plusieurs fonctionnalités emblématiques comme la gestion automatique des citations, sous une forme relativement accessible et légère.

Le Pandoc Markdown ajoute également des fonctionnalités plus expertes qui poussent Markdown au-delà de son périmètre initial, pour en faire un langage aux capacités plus proches de celles de reStructuredText ou AsciiDoc reStructuredText et AsciiDoc sont à XML ce que Markdown est à HTML. Ils sont généralement utilisés pour la documentation technique et les contenus à la structure complexe.
. La simplicité de Markdown, gage d’efficacité, est aussi ce qui fait ses limites : c’est une forme raccourcie de HTML mais qui n’en possède pas certains atouts, en particulier les éléments div et span. Or ce sont précisément ces éléments dont il va être question ici.

Ces conteneurs génériques représentent un peu l’équivalent textuel d’une sélection à main levée : ils permettent de grouper facilement des éléments pour leur appliquer en bloc des paramètres divers et variés. Leur utilisation n’est pas exempte de problèmes : on parle de « divite » pour désigner le mal dont souffrent certains sites web au code boursouflé et globalement peu accessibles, conséquence d’une certaine paresse sémantique qui consiste à utiliser systématiquement des div et span génériques plutôt que des éléments spécifiques. Pour autant, ce sont des éléments essentiels, car ils contribuent à faire du web un environnement de publication capable, riche de possibilités éditoriales.

En Pandoc Markdown, un mécanisme similaire existe, qui permet de délimiter du contenu et de lui appliquer des paramètres définis. Le plus simple pour l’expliquer est de montrer des exemples de cette syntaxe puis l’équivalent en HTML, mais notez bien que le principe fonctionne en fait pour tous les formats d’exports de Pandoc, et n’est pas seulement lié aux possibilités de HTML. C’est ce qui va nous permettre de parler de PDF un peu plus bas.

Si on veut écrire « CSS est fun ! » avec le nom CSS en petites capitales, on peut écrire en Pandoc Markdown :

[css]{.smallcaps} est fun !

Cette notation avec crochets et accolades est la syntaxe native de Pandoc pour créer un équivalent de l’élément HTML span. Cela remplit la même fonction : appliquer des paramètres arbitraires à une sélection à l’intérieur d’un texte. Ici, c’est une classe CSS qui modifie la police de caractères des éléments auxquels elle s’applique. Quand on convertit ce texte en HTML via Pandoc, cela donne :

<p><span class="smallcaps">css</span> est fun !</p>

La classe smallcaps est prédéfinie dans les modèles de document HTML par défaut de Pandoc :

.smallcaps { font-variant: small-caps }

Mais cela ne signifie pas qu’on ne peut utiliser cette syntaxe que pour passer de Markdown à HTML. Par exemple, le module de Pandoc qui gère la conversion en LaTeX est codé pour reconnaître un tel span avec la classe smallcaps ; si on convertit le même fragment de Pandoc Markdown en LaTeX, cela donne :

\textsc{css} est fun !

Dans la même logique, on peut utiliser un équivalent de div pour appliquer des paramètres à une section arbitraire d’un fichier rédigé en Pandoc Markdown :

::: {.important}
Deux principes guident CSS :

- la cascade (*cascade*) ;
- l'héritage (*inheritance*).
:::

Converti en HTML via Pandoc, cela donnera :

<div class="important">
  <p>Deux principes guident CSS :</p>
  <ul>
    <li>la cascade (<em>cascade</em>) ;</li>
    <li>l’héritage (<em>inheritance</em>).</li>
  </ul>
</div>

Ici, important n’est pas une classe prédéfinie dans les modèles de documents par défaut de Pandoc : si je veux que ce paragraphe soit encadré en rouge par exemple, je devrai définir moi-même ces paramètres de mise en forme pour chaque format dans lequel je souhaite qu’ils s’appliquent.

Notez que les astérisques ont été correctement interprétées comme de l’emphase en Markdown et donc converties en éléments HTML em (généralement mis en forme par de l’italique). En effet, cette syntaxe est dite native car elle permet de continuer à écrire en Markdown à l’intérieur de ce qu’elle délimite. C’est un gros avantage sur le Markdown classique, dans lequel on peut inclure du HTML et notamment des div, mais avec la contrainte que le contenu de ces div devra lui-même être en HTML.

Comme je le disais plus haut, si ce mécanisme mime le fonctionnement des éléments div et span de HTML, il est en fait indépendant d’un format particulier. Et il se combine parfaitement avec une autre fonctionnalité experte de Pandoc : les filtres.

Concoctons un filtre

Comme l’explique John MacFarlane dans le manuel de Pandoc, ce dernier a un design modulaire. Lorsqu’on convertit un document d’un format à un autre avec Pandoc, le programme crée en fait un intermédiaire invisible, une représentation abstraite du document sous forme d’arbre hiérarchique (abstract syntax tree ou AST). Les filtres sont des programmes que les utilisateurs de Pandoc peuvent écrire, partager et réutiliser, afin d’intervenir sur le document dans cet état intermédiaire pour le modifier.

On peut rajouter autant de filtres que nécessaire, et bricoler son document de façon plus ou moins complexe. Généralement, cela sert à préserver l’idée du processus de travail basé sur un document source unique : en effet, on peut utiliser les filtres pour implémenter des fonctionnalités similaires dans plusieurs formats d’export ; ceci permet de conserver une syntaxe homogène quel que soit le format visé.

Certains filtres sont déjà anciens et apportent des fonctionnalités extrêmement utiles. On peut mentionner par exemple le filtre pandoc-crossref, qui permet d’étiqueter et de numéroter différents éléments d’un document (comme les figures et les tableaux) pour faire des références croisées. Le filtre pandoc-citeproc est probablement le plus connu des auteurs scientifiques : c’est lui qui permet de traiter les citations faites au fil du texte pour générer des notes ou appels bibliographiques ; ce filtre est si emblématique de l’esprit de Pandoc qu’il a fini par être rapatrié dans le programme sous la forme d’une option native avec des performances accrues (dans la version 2.11 parue ce mois-ci).

Concrètement, un filtre Pandoc consiste en une série d’instructions rédigées dans un langage de programmation. Cela représente un plafond de verre pour certains, dont moi. Même avec un peu de formation à la programmation, le fonctionnement des filtres n’est pas intuitif et leur documentation ressemble à un jeu de piste. Depuis la version 2.0, Pandoc peut interpréter nativement le langage Lua : c’est une simplification, car cela permet d’écrire des filtres en Lua sans avoir à installer quoi que soit de supplémentaire à Pandoc. Lua n’est pas plus simple ou plus compliqué que les autres langages dans lesquels les filtres sont exprimables (notamment Python). Les éléments les plus utiles pour apprendre à créer des filtres sont probablement les exemples donnés dans le manuel.

Après un peu de tâtonnements, j’ai fini par comprendre comment fonctionnent les filtres. Je vous laisse consulter d’autres tutoriels spécifiques à cette fonctionnalité si vous souhaitez un guide plus complet Il n’existe malheureusement pas beaucoup de ressources sur les filtres Pandoc en français…
mais voici tout de suite un exemple qui explique les principes dont on a besoin ici.

Voici quelques lignes en Pandoc Markdown, situées à la fin d’un article scientifique :

# Conclusion

Une conclusion frappante.

# Bibliographie

::: {#refs}
:::

Le conteneur avec l’identifiant Comme en CSS, un identifiant commence par un croisillon #, alors qu’une classe commence par un point.
prédéfini refs est utilisé par Pandoc pour positionner la bibliographie. Par défaut, ce conteneur est implicite et positionné à la fin du document. Il n’a pas besoin d’être inclus explicitement dans le document pour que mon exemple fonctionne, mais je l’ai fait pour mettre en lumière le rôle clé de l’identifiant.

Imaginons la situation suivante : on voudrait modifier cette bibliographie lorsque le document est transformé en PDF, par exemple avec une macro LaTeX intitulée fullwidth, mais on ne veut pas « polluer » le fichier source avec une syntaxe étrangère à Markdown. La solution est d’insérer la commande via un filtre pendant la conversion.

Pour créer ce filtre, on va commencer par quelque chose de classique en programmation : on va créer une fonction (function), c’est-à-dire une série d’instructions, à laquelle on va donner un nom quelconque mais généralement assez descriptif (ici MaFonction) et qu’on va appliquer à quelque chose (objet).

function MaFonction(objet)

Ce quelque chose, c’est le document, morceau par morceau. Dans un filtre Pandoc, la fonction marche de la façon suivante : elle inspecte le document du début à la fin, prend chaque élément et le « retourne » (de l’anglais return), c’est-à-dire le remet à sa place. Imaginez un employé de bureau auquel on aurait confié la fonction d’inspecter le contenu d’un meuble à tiroirs, avec une liste d’actions à effectuer en fonction de ce qu’il trouve dedans. Ici on va demander à la fonction de vérifier si l’objet a un identifiant d’une certaine valeur (refs). Tout ceci repose sur un mécanisme classique de la programmation, à savoir le fait de prendre une décision basée sur une condition (ifthen).

if objet.identifier == "refs" then

Then… what? Eh bien, si la fonction lit un identifiant refs, cela déclenche une instruction qui consiste à remettre l’objet en place mais en insérant avant et après un bloc de code brut (pandoc.RawBlock) en LaTeX (latex) qui correspond à la macro qu’on souhaite ajouter.

return {
  pandoc.RawBlock('latex', '\\begin{fullwidth}'),
  objet,
  pandoc.RawBlock('latex', '\\end{fullwidth}'),
}

Évidemment, il faut prévoir le cas alternatif (else), c’est-à-dire quand l’objet examiné n’a pas d’identifiant refs – ici, c’est littéralement tout le reste du document. C’est relativement simple, on se contente de remettre à sa place l’objet inchangé :

else
  return objet

Il ne reste qu’à mettre fin à la boucle conditionnelle (if, then, else) et à la fonction de manière globale, avec end. Mis bout à bout, cela donne :

function MaFonction(objet)
  if objet.identifier == "refs" then
  return {
    pandoc.RawBlock('latex', '\\begin{fullwidth}'),
    objet,
    pandoc.RawBlock('latex', '\\end{fullwidth}'),
  }
  else
    return objet
  end
end

Simple, non ? En fait, si je m’éloigne de cet exemple, ça devient vite très compliqué. Mais avançons modestement et utilisons cette compréhension basique pour arriver à l’objectif de ce tutoriel : combiner des conteneurs génériques et un filtre, pour faire un peu de sorcellerie documentaire.

Synthèse : affichage conditionnel du contenu

L’association des conteneurs génériques et des filtres permet de réaliser des choses qui relèvent de la « programmation éditoriale » (voir un billet précédent). Le cas pratique que je propose ici consiste à délimiter certains contenus dans un fichier rédigé en Pandoc Markdown, leur attribuer une classe (sorte d’étiquette de groupe) et appliquer un filtre durant la conversion pour masquer ces contenus dans le document généré à la sortie.

Voici un exemple d’exercice dans un fascicule de TP :

# Exercice

Voici un exercice en apparence très difficile.

::: solution
Voici la solution, en fait très simple.
:::

Notez que je n’ai pas écrit {.solution} mais solution. En effet, on peut se passer des accolades et du point si le délimiteur de début n’est suivi que d’un seul mot : Pandoc interprètera ce dernier comme un nom de classe. Ceci permet une écriture plus économe.

Je peux convertir ce fichier en PDF avec Pandoc :

pandoc tp.md -o tp-corrigé.pdf

Les lignes ::: solution et ::: disparaissent, car Pandoc les a reconnues comme formant un div dans sa syntaxe native et cet élément est « invisible ». Mais le texte entre les deux (la solution) s’affiche, car aucune instruction particulière n’est liée à ce conteneur. D’où le nom que j’ai donné au fichier : dans cette version, on voit le corrigé.

Examinons maintenant ce filtre :

function Div(d)
  if d.classes[1] == "solution" then 
    return {}
  else
    return d
  end
end

Il est très similaire à celui dont je me suis servi plus haut pour expliquer le fonctionnement des filtres. La fonction s’appelle Div au lieu de MaFonction mais cela n’affecte pas le filtre, seulement ma propre compréhension. Au lieu du mot objet j’ai utilisé d mais cela désigne toujours de manière générique un élément du document examiné à un instant t par la fonction. Au lieu de chercher un identifiant, la fonction vérifie cette fois si l’objet possède une classe intitulée solution ; on précise entre crochets qu’on vérifie la première classe de l’objet, s’il en a plusieurs ([1]).

Si la condition est vérifiée, alors on retourne… rien du tout ! En effet, écrire return {} revient à escamoter l’élément d du document sur le point d’être généré. Et bien sûr, pour tous les autres cas (else), on ne touche à rien (return d).

Il faut enregistrer ce filtre sous une forme à la fois conventionnelle pour Pandoc et compréhensible pour vous, par exemple cachersolution.lua. Je recommande de mettre tous vos filtres à un emplacement pratique comme $HOME/.pandoc/filters/, c’est-à-dire utiliser le répertoire utilisateur de Pandoc. Créez-le si besoin.

On peut alors créer une version sans correction du fascicule d’exercices avec la commande suivante :

pandoc tp.md --lua-filter=cachersolution.lua -o tp.pdf

Mieux, on peut créer un fichier texte pandoc-tp.sh contenant les deux commandes :

#!/bin/bash
pandoc tp.md -o tp-corrigé.pdf
pandoc tp.md  --lua-filter=cachersolution.lua -o tp.pdf

Ceci permet de lancer les deux conversions avec une seule commande :

./pandoc-tp.sh

Pour fluidifier encore ce processus, on peut mettre ce script dans un répertoire de fichiers exécutables connu du système d’exploitation pour pouvoir le lancer depuis n’importe où avec la commande pandoc-tp. Il faut alors adapter un peu le script pour qu’il sache où trouver le fichier à convertir. On peut aussi utiliser un bon éditeur scriptable comme BBEdit pour appeler le script avec un raccourci clavier.

Mais je commence à dépasser le périmètre initial de ce tutoriel. Restons-en donc là, et concluons par un mot sur les outils choisis (PDF via Pandoc).

A11y Golightly

Ce tutoriel est issu de ma pratique du moment : j’enseigne, et j’ai notamment besoin de créer des fascicules de TP de manière rapide et efficace. J’ai choisi le PDF plutôt qu’un environnement web pour plusieurs raisons.

D’abord, je souhaitais garantir un accès hors-ligne au document et le PDF était la solution la plus simple pour moi à court terme. Ensuite, les TP en question nécessitent généralement un ordinateur, ce qui diminue l’intérêt d’un support web avec mise en page adaptative. Enfin, même si les outils comme MkDocs permettent de créer facilement des sites à partir de trois fois rien, je suis plus rapide avec Pandoc et tous les étudiants peuvent s’approprier un bon vieux PDF.

Cependant, cela ne va pas sans quelques difficultés. L’accessibilité et LaTeX, c’est une problématique compliquée.

Certaines conventions graphiques simples, comme le fait de colorer et souligner les liens hypertexte, sont difficiles à réaliser en LaTeX sans mettre les mains dans le cambouis. J’ai préféré utiliser l’option links-as-notes de Pandoc, qui transforme les liens en notes de bas de page cliquables ; cela peut sembler bizarre quand on est très habitué au web, mais l’accessibilité est meilleure et ça prend une ligne.

Il est également difficile de rendre correctement copiables les extraits de code dans le PDF, notamment l’indentation, cet espacement invisible en début de ligne qui est parfois crucial. Ce problème complique la tâche des étudiants sur les TP les plus techniques ; j’ai fini par fournir certains extraits de code sous forme de fichiers texte autonomes en plus des fascicules.

Quel que soit le support choisi, tout ceci nécessite donc de l’attention. C’est ici que je recommande encore plus chaudement Pandoc, car il fait gagner beaucoup de temps. Je suis très amateur de personnalisation typographique mais c’est dangereusement improductif quand le travail d’écriture doit passer par une phase quasiment industrielle.

Pandoc inclut des templates très robustes pour tous ses formats d’exports, et ils réagissent à des options qu’on peut déclarer en ligne de commande mais aussi directement dans son fichier Markdown. Ainsi, on peut très bien obtenir un bon rendu PDF sans jamais ouvrir un fichier LaTeX, en écrivant ce genre de chose en début de document :

---
title: Sérialisation 03 TP
author: Arthur Perret (Université Bordeaux Montaigne)
date: DUT Infonum 2020-2021
documentclass: scrartcl
fontfamily: plex-otf
links-as-notes: true
lang: fr-FR
...

Ceci est un fascicule de TP.

# Exercice 1

Avec quelques métadonnées de configuration, on obtient très vite un résultat impeccable, en une commande Pandoc et sans aucun bidouillage.

Exemple de fascicule de TP avec énoncé à gauche et corrigé à droite.

Cette possibilité, combinée au système que j’ai expliqué dans ce tutoriel, me permet de proposer des fascicules de TP complets et corrects à mes étudiants, sans abandonner mes outils habituels et en m’aidant à gérer la charge de travail inhérente à la préparation des cours.

J’espère que ce billet vous aura donné envie de tester Pandoc pour votre travail. Ce que je présente ici me sert à faire des versions avec ou sans corrigé de mes fascicules de TP, mais certains d’entre vous entrevoient peut-être déjà d’autres applications. Le principe de l’affichage conditionnel est un premier pas vers le principe du One Document Does it all (ODD), dans lequel chacun est libre de remplacer document, it et all en fonction de sa problématique.