Écrire un filtre Lua pour Pandoc : ajouter des marques personnalisées aux titres convertis depuis Word
Lorsqu'un document Word devient du Markdown, les styles se transforment en structure : les paragraphes de style Titre 1 deviennent des en-têtes de premier niveau, Titre 2 de deuxième niveau, et ainsi de suite. Parfois, cela ne suffit pas. Vous voudrez peut-être que chaque titre issu de l'import Word porte un marqueur que votre modèle de site ou votre pipeline de build puisse reconnaître, et modifier à la main des centaines de titres n'est pas une option.
Pandoc a une réponse intégrée : les filtres Lua, de petits scripts qui transforment le document pendant qu'il traverse le convertisseur. Ce guide explique leur fonctionnement, en construit un étape par étape précisément pour cette tâche de marquage, et couvre son exécution et son débogage.
Qu'est-ce qu'un filtre Lua, et pourquoi Pandoc a choisi Lua
Pandoc analyse chaque entrée en un arbre de syntaxe abstrait commun, une représentation interne d'éléments tels que les en-têtes, les paragraphes et les tableaux, avant d'écrire la moindre sortie. Un filtre est du code qui parcourt cet arbre et le réécrit. Comme le filtre opère sur l'arbre normalisé, il fonctionne de la même façon, que l'entrée provienne de docx, HTML ou Markdown.
Pandoc embarque un interpréteur Lua, actuellement Lua 5.4, si bien que les filtres s'exécutent dans le processus Pandoc, sans runtime séparé ni extension compilée à construire. Vous écrivez un fichier texte, vous le remettez à Pandoc avec une option de ligne de commande, et la transformation a lieu en pleine conversion. Les filtres écrits ainsi évitent aussi l'allers-retour JSON que paient les filtres externes, ce qui garde le surcoût minime.
L'anatomie d'un filtre
Un fichier de filtre définit de simples fonctions nommées d'après les types d'éléments de l'AST. Une fonction nommée d'après le type Header s'exécute une fois par en-tête du document, une nommée d'après le type Para une fois par paragraphe. Si le fichier ne renvoie pas de table de filtre explicite, Pandoc collecte ces fonctions de premier niveau par leur nom et les utilise automatiquement.
La valeur de retour décide de ce qui remplace l'élément dans l'arbre. Renvoyer nil, ce qui inclut le fait d'oublier de renvoyer quoi que ce soit, laisse l'élément inchangé : une fonction étourdie est donc inoffensive. Renvoyer l'élément, modifié ou non, le remplace par la nouvelle version. Renvoyer une liste insère ces éléments à la place de l'original, et renvoyer une liste vide est la manière de supprimer un élément du document.
L'élément Header et ses attributs
Chaque Header dans l'AST de Pandoc porte trois choses : un niveau sous forme de nombre, une liste de contenu en ligne et un ensemble d'attributs. L'ensemble d'attributs compte trois parties : un identifiant utilisé pour les ancres, une liste de noms de classes et des attributs clé-valeur arbitraires. C'est le même trio qui apparaît en sortie sous forme d'id et de class sur la balise HTML d'un titre.
Où ces attributs aboutissent dépend du format de sortie. Le Markdown de Pandoc peut les rendre sous forme d'annotations entre accolades après le texte du titre, et la sortie HTML les place sur la balise elle-même. Un export GFM pur n'a pas de syntaxe pour les attributs, les classes y seraient donc perdues, ce qu'il vaut mieux savoir avant de choisir un format cible.
La tâche : marquer chaque titre Word
L'objectif est un filtre qui inspecte chaque Header et ajoute une classe personnalisée quand il correspond aux critères, par exemple tous les en-têtes, ou seulement ceux de deuxième niveau. La logique lit le champ level, décide, et ajoute une chaîne à la liste classes, soit avec la méthode insert de la liste, soit avec un simple table.insert. Rien d'autre ne change.
Les conditions le rendent utile en pratique. Vous pouvez donner une classe aux en-têtes de premier niveau et une autre aux plus profonds, limiter le marquage aux en-têtes dont le texte commence par un mot donné, ou sauter ceux qui portent déjà la classe afin que le filtre reste idempotent et puisse tourner deux fois sans dupliquer les marques.
Parcourir la logique du code
Écrit noir sur blanc, le filtre tient en quatre étapes courtes. Définissez une fonction nommée d'après le type Header pour que Pandoc lui achemine chaque titre. À l'intérieur, testez le champ level par rapport au seuil qui vous intéresse. S'il correspond, ajoutez le nom de la classe à la liste classes et, éventuellement, définissez un attribut clé-valeur. Renvoyez l'élément à la fin.
Ce return final mérite votre attention : modifier l'élément sur place et le renvoyer est le motif documenté, mais un return manquant signifie que Pandoc conserve l'original non modifié, le changement est donc calculé puis silencieusement ignoré. Avec le return en place, les titres ressortent avec la nouvelle classe, visible sur les balises HTML ou dans les attributs entre accolades du Markdown de Pandoc.
Exécuter le filtre
En ligne de commande, Pandoc accepte une option --lua-filter pointant vers votre fichier, et l'option peut être répétée pour chaîner plusieurs filtres dans l'ordre. Les filtres s'exécutent entre la lecture et l'écriture, la transformation s'applique donc quel que soit le format de sortie choisi.
Sur ce site, les options Pro incluent l'envoi d'un filtre Lua : remettez le même fichier .lua à côté de votre document Word, et le worker du navigateur le transmet au même moteur Pandoc. Les classes aboutissent dans la sortie exactement comme en local, sans que rien ne soit envoyé à un serveur.
Techniques de débogage
L'outil le plus rapide est la fonction print : la sortie imprimée depuis un filtre va vers stderr, elle ne contamine donc jamais le document converti, et vous pouvez afficher le niveau, les classes ou le texte d'un en-tête pour voir ce que le filtre a réellement reçu. Pandoc fournit aussi un module de journalisation, pandoc.log, dont les fonctions info et warning étiquettent les messages avec une sévérité.
Une bonne habitude consiste à développer sur un tout petit fichier. Construisez un docx avec un titre à chaque niveau, exécutez le filtre dessus et inspectez la sortie avant de le pointer vers de vraies archives. La plupart des échecs sont des fautes de frappe dans les noms de champs, et le canal d'erreur les révèle immédiatement.
Extensions et performances
Le même motif s'étend bien au-delà des titres : supprimer les paragraphes qui ne contiennent aucun texte, générer des identifiants à partir du texte des titres pour des ancres stables, ou envelopper les tableaux dans un Div pour qu'un modèle de site puisse les styliser en groupe. Tout cela tient en une fonction par type d'élément dans le même fichier.
Les performances sont rarement un problème. Les filtres s'exécutent par élément au sein du processus de conversion, et le parcours reste rapide même sur de très gros documents. La seule chose à éviter est le travail lourd répété par élément, comme lire un fichier à l'intérieur de la fonction, qui a sa place en tête du script, où il ne s'exécute qu'une fois.