thomas.touhey.fr

Utiliser et adapter docutils

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.

Décoder et produire des documents

docutils a une documentation très incomplete, mais j'ai complété ma recherche au travers des éléments suivants :

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 :

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 :

Les composants de base pour étendre docutils

Afin que thblog tire le meilleur de docutils, il y a plusieurs niveaux auxquels on peut ajouter ou mettre à jour des éléments :

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.

Connecter les documents ensembles

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 :

default:

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.

post:

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 :

page:

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:.

file:

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.

static:

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:.

pep:

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.

Hacker les substitutions

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 !

Ajouter un avertissement "TODO"

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

Ajouter un writer GMI

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 :

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 :

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é.

Conclusions

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 :

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. :-)