Comme décrit dans Bienvenue, thblog !, pour thblog, j'ai utilisé le langage reStructuredText pour les pages formattées, c'est-à-dire la majorité de mon contenu ici. Pour cela, j'ai utilisé le module standard de facto pour publier des documents écrits en reStructuredText : docutils, utilisé par une bonne partie des outils implémentant le format, dont Sphinx et Pelican.
Dans ce post, je parlerai comment thblog utilise docutils, mettrai l'accent sur les besoins spécifiques à thblog, et verrai où et comment je suis intervenu pour répondre à ces besoins.
docutils a une documentation très incomplete, mais j'ai complété ma recherche au travers des éléments suivants :
La PEP 258, nommée "Docutils Design Specification", qui couvre les décisions de design les plus importantes autour de docutils ;
Le docutils Hacker's Guide, qui explique plus en détail la structure de l'outil ;
Le code source de Docutils directement ;
L'usage de docutils faits dans d'autres projets supportant le reStructuredText, tels que Sphinx ou Pelican.
Au plus haut niveau d'abstraction, docutils est responsable de prendre une entrée, d'une chaîne de caractères, chaîne ou flux d'octets Python, et de retourner une chaîne de caractères ou d'octets formattés. Comme indiqué dans le docutils project model, ceci est implémenté au travers d'un Publisher, qui lance les étapes suivantes :
Lire le contenu depuis la source et en faire une liste de lignes ;
Passer le contenu au travers d'un Parser, dans notre cas le parser reStructuredText, et retourne un arbre de noeuds, dont le sommet est appelé un document ;
Appliquer les transformations (Transform) au document, afin de modifier l'arbre une fois le document décodé ;
Produire le document dans le format de sortie spécifié sur le flux de sortie, à l'aide d'un Writer.
Chaque site généré (e.g. thomas.touhey.fr ou thomas.touhey.uk) peut avoir plusieurs sorties, et certaines des opérations citées au-dessus requièrent que tous les documents soient lus et décodés pour que certaines métadonnées puissent en être ressorties. Par conséquent, la liste des opérations est en réalité scindée en deux :
Un moment pour détecter / décoder le contenu, ce qui produit un arbre de noeuds pour le document (aussi appelé "doctree"), appliquer les transformations, et extraire les métadonnées telles que le titre du document, l'auteur, ou la liste des ancres.
Ceci est implémenté dans une méthode appelée Docutils.parse ;
Un moment pour produire le contenu, en copiant l'arbre de noeuds pour le document, appliquant des post-transformations dessus (qui peuvent être spécifiques à une sortie, ou requérir des informations d'autres documents), et produisant le document dans le format spécifié (en général HTML ou GMI).
Ceci est implémenté dans une méthode appelée Docutils.write.
Afin que thblog tire le meilleur de docutils, il y a plusieurs niveaux auxquels on peut ajouter ou mettre à jour des éléments :
On peut ajouter des types de noeuds, comme sous-classes de docutils.nodes.Node et/ou docutils.nodes.Element, qui peuvent rester jusqu'à ce qu'ils soient pris en compte par un writer pour produire la sortie correspondante, ou peuvent être remplacés par d'autres par des transformations. Envisagez-les comme analogues aux tags HTML, e.g. <paragraph>.
Par exemple, nous ajoutons le tag <todo>, qui est affiché comme un avertissement ou une note ;
On peut ajouter ou mettre à jour des traducteurs HTML/GMI pour certains types de noeuds, i.e. connecter une fonction à "nous visitons actuellement ce noeud" et/ou "nous sommes en train de quitter ce noeud", pour ajouter du contenu dans les traducteurs associés aux writers ;
On peut ajouter ou mettre à jour des directives reStructuredText.
Par exemple, thblog ajoute .. post-list:: pour inclure une liste de tous les posts dans une page ;
On peut ajouter ou mettre à jour des rôles reStructuredText.
Par exemple, thblog ajoute :post:`2025-03-22-hello-thblog` pour référencer un post ;
On peut ajouter des transformations ;
On peut ajouter des post-transformations, qui sont des transformations exécutées au moment de produire du contenu, i.e. quand l'ensemble du contenu du site est chargé, plutôt que juste après que le contenu soit décodé.
Par exemple, thblog ajoute OutputFilter, qui s'assure que le contenu des noeuds <output_filter> n'est inclus que pour certaines sorties, par exemple <output_filter output='html'>.
Malheureusement, certaines de ces opérations nécessitent de manipuler des variables globales définies dans docutils ; une bonne partie du code qui nous permet de faire ceci est repris de Sphinx.
Par défaut, docutils ne peut traiter que les documents en isolation ; il requiert des extensions pour connecter les documents ensembles et au reste du site.
thblog permet les documents (incluant ceux non écrits en reStructuredText) de se référencer en utilisant des ancres, qui sont au format namespace:reference. Les espaces de noms (namespace) incluent actuellement :
L'espace de noms par défaut, utilisé pour les ancres définies directement dans les posts et les pages, par exemple avec la source suivante :
.. _my-custom-reference:
Un titre de section incroyable !
--------------------------------
Ici, le document présente l'ancre default:my-custom-reference pour faire un lien à cette section directement.
Les documents reStructuredText peuvent référencer ces ancres avec le rôle :ref, analogue au rôle :ref: de Sphinx.
L'espace de noms dans lequel les posts sont référencés utilise le format YYYY-MM-DD-slug, par exemple post:2025-03-22-hello-thblog.
Les documents reStructuredText peuvent référencer ceux-ci avec le rôle :post:, ou la directive .. post:: pour obtenir une carte telle que celle-ci :
Bienvenue, thblog !
Publié le 22 mars 2025 par Thomas Touhey.
Je passe mon blog sous mon propre générateur, migrant depuis Jekyll. Pourquoi ? J'en détaille les raisons dans cet article !
L'espace de noms dans lequel les pages sont référencées, par exemple page:projects.
Les documents reStructuredText peuvent référencer ceux-ci avec le rôle :page:.
L'espace de noms dans lequel les fichiers sont référencés, par exemple file:myage.rb.
Les documents reStructuredText peuvent référencer ceux-ci avec le rôle :download:, analogues au rôle :download: de Sphinx.
L'espace de noms dans lequel les fichiers statiques sont référencés, par exemple static:resume.pdf.
Les documents reStructuredText peuvent référencer ceux-ci avec le rôle :static:.
L'espace de noms pour référencer les Python Enhancement Proposals (PEP), e.g. avec le rôle :pep: dans les documents reStructuredText.
Des namespaces et rôles équivalents existent pour le RNCP (:rncp:) et les RFCs (:rfc:).
Afin d'implémenter ceux-ci, des rôles nommés "rôles de référence" (reference roles), produisent des noeuds de type pending_reference ou pending_reference_card, qui sont ensuite transformés par une post-transformation nommée PendingReferenceResolver en des noeuds reference et reference_card respectivement. Ce comportement reprend la logique du XRefRole de Sphinx, qui produit des noeuds de type pending_xref, qui sont ensuite transformés en noeuds de type reference par une post-transformation nommée ReferencesResolver.
Pour la page posts, j'ai également réalisé une autre directive nommée .. post-list::, traitée dans une post-transformation en de multiples noeuds de type pending_reference_card référençant les posts, qui peuvent ensuite être traités.
Comme pour mon précédent blog utilisant Jekyll, je voulais pouvoir afficher mon âge calculé sur À propos de moi. Après y avoir réfléchi un peu, j'ai décidé que je voulais l'implémenter avec des substitutions reStructuredText.
L'idée était que |Authors.Thomas.Age| puisse être traduit en mon âge comme un entier. Afin de pouvoir le faire, j'ai réalisé une classe nommée SubstitutionDefsMapping qui prend l'environnement et les définitions de substitutions du document original, puis remplace doctree.substitution_defs après le décodage du document, mais avant de réaliser les transformations.
Cette classe retourne les substitutions si elles existent dans le dictionnaire original, ou par défaut vérifie que la clé correspond à un pattern, par exemple Author.<Name>.<Property> ; si c'est le cas, je retourne la propriété correspondante à l'aide d'une méthode spécifique.
Cette méthode me permet également d'ajouter autant de clés que je le souhaite ; ainsi, |UTC.Date| et |UTC.Year| existent également, parce qu'après tout, pourquoi pas !
Lorsque j'écris des documentations Sphinx, j'ai tendance à beaucoup utiliser la directive .. todo::, et les configure pour apparaître dans la documentation produite pour permettre à mon lecteur ou ma lectrice de savoir si une information manque à un endroit.
Cette directive est définie dans une extension Sphinx (sphinx.ext.todo), et n'existe pas dans docutils de base ; j'ai dû l'ajouter. Heureusement, avec thblog, c'est assez facile à faire :
@extension.docutils.node
class todo(Admonition, Element):
"""Node signalling a work in progress."""
@extension.docutils.directive("todo")
class Todo(BaseAdmonition):
"""Directive signalling a work in progress."""
node_class = todo
L'un des challenges que j'ai pour thblog a été d'ajouter le support des multiples sorties pour chaque site, incluant la sortie HTTP. Une des possibilités que j'ai envisagé a été une sortie pour Gemini, un "slow web" alternatif inspiré par Gopher. Sur Gemini, le format d'hypertexte le plus courant est le Gemtext (avec l'extension .gmi).
Le Gemtext est un langage orienté ligne, et ne supporte pas de formattage au sein de chaque ligne (i.e. pas d'emphase ou de décoration de texte). Une ligne est soit :
Du texte simple, une ligne vide entrant dans cette catégorie ;
Un titre, du premier au troisième niveau d'importance (# à ###) ;
Une citation (ou une partie de citation), avec le préfixe > ;
Un élément de liste, avec le préfixe * (ou le fait de garder l'indentation pour une nouvelle ligne au sein de l'élément de liste), par exemple :
* Premier élément !
* Deuxième élément, qui a plusieurs lignes !
Ceci est une deuxième ligne du deuxième élément.
* Troisième élément !
Un lien à une page ou une image, avec le format => <url> <texte>.
Ces règles sont surchargées si l'on entre dans un bloc préformatté, avec trois backticks, jusqu'à ce que l'on en sorte avec trois backticks à nouveau.
Ce langage est très limité pour ce que le blog est capable de produire, et toute opération de rendu est "best effort" de la part de thblog. Les limitations suivantes sont connues :
Le markup inline est perdu, avec quelques exceptions ;
Les liens sont collectés, les doublons sont retirés, et le résultat est placé en fin de document ;
Les cartes de référence sont représentées comme un lien, une ligne de texte représentant l'auteur et la date de publication, ainsi qu'une citation contenant la description.
Cependant, la limitation la plus importante est l'absence de profondeur. Dans les posts et pages de mon blog, vous pouvez trouver des extraits de code dans des éléments de liste, des listes dans les éléments de liste, des extraits de code dans des avertissements et des citations (tous deux représentés par des citations en GMI), et caetera.
J'ai choisi d'ignorer cette limitation de profondeur, et de représenter des contenus intégrés comme le fait Markdown, e.g.:
* Coucou, ceci est une liste :
* Coucou, je suis une liste dans une liste !
N'est-ce pas incroyable ?
* Ça l'est !
* Un autre élément de liste, déjà ?
```
Ouah, celui-là a du code !
```
Ceci n'étant pas standard, ce n'est interprété par aucun client à ma connaissance ; cependant, même présentés dans leur forme brute, le rendu reste correct pour les personnes qui connaissent le Markdown ou autres langages similaires.
Et voilà, thblog a une sortie Gemini ! C'est loin d'être parfait, et il reste du travail pour améliorer les noms des liens et les alt des images, mais ça reste suffisant pour être utilisé.
Bien que docutils soit le standard pour décoder et rendre du reStructuredText en Python, je peux dire avec certitude que c'est un bazar à utiliser et étendre :
La documentation est dispersée quand elle existe (ce qui est assez ironique pour un langage fait pour écrire de la documentation !), ce qui mène à beaucoup d'exploration de code pour essayer de comprendre quels attributs sont utilisés avec quels éléments ;
Toute instance de classe ne doit être utilisée qu'une seule fois, e.g. l'on instancie un publisher pour utiliser avec un seul document, puis on doit en instancier un autre pour un autre document ;
Toute extension requière de modifier des variables globales à un point ou un autre (ce qui est problématique si thblog est lancé dans le même contexte que d'autres outils utilisant docutils) ;
Certaines classes sont un cauchemar à surcharger (le parser reStructuredText référence des fonctions dans des dictionnaires en attribut directement, donc surcharger les méthodes n'est pas assez, il faut également surcharger les dictionnaires pour référencer les nouvelles méthodes et classes !).
Cependant, une fois qu'une certaine couche d'abstraction est posée dans thblog, cela me permet de partager des concepts avec Sphinx et autres outils clients de docutils, et importer quelques extensions définies pour Sphinx si je les apprécie. L'autre sens est également vrai, je peux copier des éléments docutils définis pour thblog dans une extension Sphinx par exemple !
Utiliser docutils dans ce projet m'a permis de tester ses limites, et les concepts intéressants qu'il apporte ; mais à un moment, je me retrouverai peut-être à reconstruire un parser reStructuredText de zéro, tout en cherchant à éviter ces défauts. Pour le moment, en revanche, ça fonctionne, et je devrais sans doute déjà me concentrer sur le fait d'écrire des posts et d'autres contenus pour mon blog. :-)