Markdown comme langage d'écriture d'un manuscrit

Retour d'expérience de mon utilisation de markdown et pandoc pour rédiger un manuscrit de thèse de doctorat.


J'ai récemment(au final ça commence presque à dater) écrit une thèse. C'est de loin le plus long document que j'ai écrit, environ 250 pages, et qui n'a rien à voir avec la rédaction d'article de recherche, généralement de 6 à 12 pages. Tous les gens passés par là vous le diront, ce n'est pas un exercice facile, la rédaction occupe généralement les 6 derniers mois du doctorat.

La rédaction de ce type de document requière notamment:

  • de nombreux retours avec l'encadrement pour relecture, de courts passages surtout,
  • beaucoup de déplacements de section d'un chapitre à l'autre,
  • beaucoup de schémas, d'image et de modifications "mineures", typiquement des textes, des flèches.

L'objectif de cet article est de décrire un ensemble d'outils qui m'a permis d'avoir l'environnement le plus productif. Batteries not included: je ne fournis que des pistes et des usages, je n'ai pas maintenu de script ou de conteneur docker prêt à l'emploi.

Premier constat

L'école doctorale demande de respecter une page de garde, un résumé en 4ème de couverture et suivant un certain thème. Idéalement il faut donc suivre un template word ou LaTeX.

En fonction du domaine de recherche, informatique pour moi, la production de diagrammes UML, de formules, la gestion de la bibliographie rendent l'utilisation de LaTeX obligatoire.

Second constat

Rédiger en LaTeX impose une lourdeur, la syntaxe même langage distrait du fond. Insérer une image, un bloc résumé, de l'emphase, impose de mélanger le fond de la forme au point de gêner un des points clés: être en mesure de rapidement se déplacer dans le document et localiser du contenu.

Les phases de relecture sont primordiales afin de vérifier la cohérence, les transitions entre sections ou tout simplement les accords au sein d'une phrase.

L'approche que j'ai privilégié est de rédiger en markdown pour retirer le plus de mise en forme possible puis d'utiliser pandoc pour générer du LaTeX reposant sur un template. Divers filtres pandoc sont utilisés afin de combler aux exigences de LaTeX.

Grossièrement, un ensemble de chapitres markdown sont compilés en LaTeX pour produire un fichier content.tex intégré dans un template à l'aide de la commande \input{content}.

Je vais détailler ici les éléments de la stack technique, quelques exemples de filtres et de document source et finir par mon ressentis général sur ce process d'écriture.

Stack technique

Étapes de compilation

  1. compilation dans l'ordre de chaque chapitre en LaTeX, a) intégration des schémas, b) appel de tous les filtres pour produire un document LaTeX compilable,
  2. compilation réelle du document LaTeX final avec son template,
  3. optionnellement vérification que l'ensemble d'un fichier de citation est bien utilisé.

Pas de solutions tout intégrées satisfaisantes

Beaucoup de solutions existent pour produire des livrables à partir de sources markdown (gitbook, voire même des CMS tel que grav peuvent correspondre), ces approches produise des sites web statiques mais plus rarement un format 'livre' ou 'rapport' acceptable.

J'ai donc composé moi même, avec mes outils habituels, sur mon archlinux de l'époque tout était disponible dans les repositories ou via l'AUR, sur une Ubuntu les paquets seraient les suivant:

pandoc pandoc-crossref # via cabal, tout n'est pas disponible dans les dépôts officiels
texlive-xetex # pour xelatex
texlive-luatex # pour checkcites
texlive-bibtex-extra # bibtex
texlive-extra-utils # pour pdfcrop
biber

Drawio fournit un éditeur WYSIWYG de diagrammes, sans formalisme particulier mais intégrant une bibliothèque pour gérer la plupart des standards UML. Sur un ordinateur de bureau l'intégration CLI ne pose pas de problème, en revanche en headless (WSL2 ou docker), la tache semble plus compliquée compte tenu du nombre d'issue github mentionnant des erreurs. Une tentative d'image docker semble toutefois fonctionnelle (coucou Erwan).

Le template final utilisé est produit par un doctorant pour correspondre aux attentes de l'école doctorale (https://gitlab.inria.fr/proman/mathstic-thesis-template). C'est une partie rédigée directement en LaTeX parce qu'elle ne correspond pas aux aller-retours de relecture décrits plus haut.

Pandoc

Pandoc permet d'intégrer des filtres lors de la compilation, ici ce qui nous intéresse c'est markdown vers LaTeX. Comme en compilation, le filtre donne accès à l'AST, la structure de représentation interne de tout type de document, le langage pivot en quelque sorte.

Un filtre peut prendre en entrée un paragraphe, peut le modifier et le retourner; chaque filtre fonctionne de manière isolée par rapport aux autres.

Exemples choisis

Pour mon manuscrit, j'ai rédigé 17 filtres pour pandoc, allant du simple remplacement de texte plus lisible en markdown mais incompatible en LaTeX à la compilation de diagramme pour intégrer un export pdf ou png.

Je présente ici quelques uns de ces filtres afin d'illustrer leur intégration au document source et leur complexité.

  1. n° pour LaTeX

Le filtre le plus simple, le symbole ° n'est pas supporté en unicode, il faut utiliser \no{}. Le filtre va donc s'intéresser uniquement à la compilation vers LaTeX, chercher le texte dans tous le contenu de tous les textes écrits et va le remplacer par la méthode LaTeX équivalent.

Le filtre que j'ai utilisé est le suivant:

function Str(ele)
    if FORMAT=="latex" then
        local w = ele.text
        if w:match('n°') then
            w = w:gsub('n°', '\\no{}')
            return pandoc.RawInline('tex', w)
        end
    end

end
  1. Utilisation de metadonnées pour un chapitre

J'ai choisi de structuré le cœur du manuscrit en chapitre en gardant un fichier par chapitre, 1 axe est étudié dans un chapitre, autant que cela se reflète dans le découpage des fichiers. L'intérêt est de pouvoir compiler uniquement un chapitre, l'envoyer par mail pour relecture ou encore intervertir deux chapitres entre eux.

J'ai choisi de mettre le résumé de chapitre en metadata du fichier markdown pour directement générer un bloc de texte intégré à la mise en page des chapitres.

En considérant le début de fichier suivant:

---
title: 'Contexte'
part: État de l'art
abstract: |
    La structure actuelle du réseau grille ne permet pas le développement de générateurs d'énergie réinjectant directement sur le réseau de manière massive. La solution est donc de consommer directement depuis la production locale. Cela nécessite l'ajout d'appareils de mesure, et par définition le développement d'une smartgrid. L'ajout de matériel est très vite limité par le besoin de connaissance des activités, de leurs consommations associées et de leur évolution. Le choix du matériel à installer est primordial et peut s'avérer compliqué à cause de la complexité de leur fonctionnement, notamment pour les moyens de stockage telles que les batteries.
---

L'attribut part est utilisé pour générer un changement de partie, puis le title et l'abstract sont repris dans des balises LaTeX séparées pour produire la mise en page souhaitée:

Rendu du document latex

Pour les chapitres nécessitant une mise en page spéciale des attributs différents peuvent être utilisés:

---
title: 'Remerciements'
no_abstract: true
number: false
---

Ici la page de remerciements ne doit pas utiliser la numérotation des chapitres et n'aura pas de bloc résumé.

  1. Mise en forme des résumés de fin de chapitre

Pour faciliter la compréhension générale, chaque chapitre se termine par un résumé des idées développées dans ce chapitre.

Pour ce filtre il est plus simple de reposer sur la notion de classe déjà présente, et de l'utiliser sur le délimiteur ::: supporté par Pandoc:

:::{.summary}
L'hétérogénéité des capteurs, services tiers et des algorithmes d'optimisation nécessite la mise en place d'un environnement dédié. Une telle architecture peut passer par un middleware en mesure de proposer une modularité afin de séparer le cœur de l'application de cette hétérogénéité.

Cette modularité liée à l'environnement d'un gestionnaire d'énergie doit s'articuler autour des activités réalisées. Nous choisissons une approche dirigée par les modèles pour permettre à un Expert de construire ses modèles à partir d'un langage dédié. Ces modèles sont ensuite chargés et servent de base à l'optimisation énergétique.
:::
local function contains(t, value)
    for _,v in ipairs(t) do
        if(v==value) then
            return true
        end
    end
    return false
end

function Div(el)
        if (contains(el.classes,"summary") and FORMAT=="latex" ) then
            local t = {}

            table.insert(t, pandoc.RawInline("latex", "\\sectionsummary{"))
            local content = el.content[1].content
            table.insert(t, pandoc.RawInline("latex", "\\textit{"))
            for k,v in pairs(content) do
                table.insert(t, v)
            end
            table.insert(t, pandoc.RawInline("latex", "}"))
            table.insert(t, pandoc.RawInline("latex", "}"))

            return pandoc.Para(t)
        end
end
  1. Intégration de fichiers externes

Vouloir citer un extrait de code source peut polluer le flux d'un paragraphe et polluer le correcteur grammatical.

La fonction peut s'aisément se rajouter dans notre environnement en mappant vers les fonctions de listings de LaTeX et l'ajout des bons attributs pour la coloration syntaxique et la construction de la table des listings.

Le filtre que j'ai utilisé est le suivant:

local open = io.open

local reguldsl_default_lstset = {}

local function read_file(path)
    local file = open(path, "rb") -- r read mode and b binary mode
    if not file then return nil end
    local content = file:read "*a" -- *a or *all reads the whole file
    file:close()
    return content
end

function CodeBlock(el)
    if el.classes[1] == "include" then
        -- look for a file attribute to know whether we need to replace the text
        local file = el.attr['attributes']['file']

        for k,v in pairs(reguldsl_default_lstset) do
            el.attr['attributes'][k] = v
        end

        el.attr['attributes']['file'] = nil
        el.attr['attributes']['label'] = 'lst:' .. file:gsub('/', ''):gsub('_', '')
        --el.attr['attributes']['language'] = 'reguldsl'
        el.attr['attributes']['caption'] = pandoc.utils.stringify(el.text)

        if file then
            el.text = read_file(file)
        end
        return el
    end
end
  1. Intégration de diagrammes drawio

Une des plus grosses pertes de temps que j'ai eu, et qui a motivé la mise en place de ce système de compilation depuis markdown est la manipulation des diagrammes construits dans un éditeur tiers, tel que drawio.

Drawio est un éditeur graphique de «formes libres» (comprendre qu'il ne respecte pas un formalisme en particulier tel que UML), les fichiers de travail sont en .drawio (basé sur xml) et les exports peuvent être au format pdf ou png.

Cela nécessite de garder un dossier pour les sources, le versionner, et garder un dossier dédié aux exports que le document LaTeX utilisera. Le cas échéant, les sources markdown peuvent pointer vers ces fichiers d'export, pour les intégrer dans la compilation LaTeX.

fichier .drawio -> export en png/pdf -> compilation LaTeX

J'ai donc écrit un filtre pour directement pouvoir citer le fichier source en markdown, compiler à la volé l'export stocké dans un dossier temporaire, y faire référence côté LaTeX pour directement intégrer l'export png dans le fichier pdf final.

  1. Pour aller plus loin

J'ai utilisé de nombreux autres filtres pour gérer différentes plaies de la mise en page: les tableaux, les légendes d'image, les groupes d'images (Figure 1.a et 1.b par exemple).

Correction orthographique avancée

En plus de la correction intégrée à n'importe quel éditeur, j'ai utilisé Antidote (à partir de 59€/an) selon moi un indispensable pour des documents de cette taille. Les fonctionnalités importantes non disponibles dans un correcteur classique:

  • cohérence des typographies: si un mot autorise plusieurs écritures, notamment les lettres 'y' ou 'i' Antidote s'assure que le choix est le même tout au long du document,
  • conjugaison: la détection du sujet d'une phrase est plutôt fiable, Antidote questionne certaines utilisations de conditionnels,
  • style: les répétitions, les phrases trop longues.

Antidote permet l'intégration aux outils de rédaction classique (LibreOffice, Word), pas adapté à mon utilisation de Vim ou VSCode. Pour permettre la correction dans Antidote j'ai du passer par l'export Pandoc en texte brut pour nettoyer tout le balisage et copier cela dans l'éditeur Antidote.

De plus, j'avais commencé à définir un dictionnaire personnalisé de mots techniques dans vim :spell qui n'est pas utilisé par Antidote.

J'ai donc du faire des copier coller chapitre par chapitre pour rapporter les corrections dans le document source, vraiment pas idéal.

Retour d'expérience

Je vais pas me le cacher, développer des outils informatiques autour de la rédaction même du manuscrit de ma thèse a rajouté un côté meta (dont ma thèse n'avait pas besoin) qui permet de se changer les idées.

De manière générale, je suis satisfait du workflow d'écriture que j'avais, j'ai gagné du temps là où le «tout LaTeX» aurait été fastidieux: la gestion des diagrammes hors Tikz et la lisibilité du texte.

En revanche, j'ai développé ces filtres et chaine de compilation pour mon poste, à un instant donné, sur une machine tournant sur Archlinux (distribution Linux très fréquemment mise à jour).

Pour garder un outil utilisable il aurait plus commode d'utiliser docker pour:

  • figer une version de drawio dont l'export pdf en ligne de commande semble casser une release sur deux,
  • mieux gérer les dépendances pour l'environnement LaTeX.

Pour conclure, je pense que Pandoc à de l'avenir, au moins pour ces milieux «de niche». Je vais continuer à l'utiliser, peut être pour m'amuser avec un export ebook pour ce manuscrit puisque pandoc gère le .epub ou plus simplement pour de la rédaction technique (markdown pour wiki gitlab et export pdf pour communication externe).