Étendre Francium : Quelques exemples pratiques

Auteurice : Arthur Pons

Temps de lecture : ~13 minutes


Francium ne comporter pas beaucoup de fonctionnalités. La base de code est petite et faite de façon à “avoir la main” au maximum d’endroits possibles via la ligne de commande. L’idée sous-jacente est que cela permettrait d’étendre facilement les fonctionnalités comme souhaité. Dans cet article tentons d’implémenter plusieurs fonctionnalités pour vérifier ou infirmer cette hypothèse.

Tags

Beaucoup de blogs, et donc de générateur de sites statiques, il existe la possibilité de tagger certains articles. Cela permet de les regrouper par thème, de favoriser la navigation et la découvrabilité du contenu. On retrouve souvent les tags inscrits sur les articles, sur la page d’accueil et sur leur propres pages. Tentons, en partant d’une version minimale de Francium d’implémenter une telle fonctionnalité.

Il est à noter qu’il y aurait pleins de façons différentes de le faire, je n’en documente ici qu’une seule1 ayant ses avantages et ses désavantages.

Tagger un article

Admettons que nous avons des articles dans notre dossier src/articles :

./src/articles/article4.md
./src/articles/article3.md
./src/articles/article2.md
./src/articles/article1.md

Ces articles n’ont pour le moment qu’un seul type de “métadonnée”, le titre du document. Il est renseigné avec %T titre du document dans chacun des fichiers. Pour ajouter des tags nous pourrions faire le choix de les ajouter directement dans les fichiers, pour qu’ils se suffisent à eux même, en créant un nouveau type de métadonnée %TAGS. Les valeurs pourraient être délimitées par des virgules comme ceci :

%TAGS musique,youtube,sobriété

Et hop, nous avons taggué la page. La première chose qui devrait vous venir à l’esprit est que si l’on bosse sur un gros site nous voulons probablement réutiliser un tag déjà existant sans pour autant tous les avoir en tête. Pour aider à la tâche2 on peut imaginer un script listant tous les tags :

azdjazdazd

grep -Ihrm1 "^%TAGS" src/* |
    cut -d' ' -f2 |
    tr ',' '\n' | sort -u

Pour grep, -I empêche de scanner les fichiers binaires, -h permet de ne pas afficher les noms des fichiers même s’il y en a plusieurs, -r permet de faire une recherche récursive dans src et -m1 de s’arrêter sur le premier résultat plutôt que de scanner les fichiers dans leur totalité. Techniquement il est possible d’ajouter le %TAGS après le contenu de l’article auquel cas si l’article parle lui même de %TAGS et le mentionne en tout début de ligne le script pourrait casser mais cela paraît très très peu probable. Le cut permet de se débarrasser du %TAGS, le tr d’avoir un tag par ligne et le sort -u de supprimer les doublons.

Imaginons un autre script permettant de construire la ligne nécessaire dans les documents en se basant sur un sélection de tags :

paste -s -d',' | xargs printf "%%TAGS %s\n"

paste -s permet de “sérialiser” le collage. Autrement dit, avec une seule source de donnée (ici stdin), cela va coller toutes les lignes les une avec les autres. -d permet de choisir le délimiteur. Le xargs printf permet d’insérer le résultat dans une chaîne de caractère passée en premier argument de printf. En l’occurence faut bien échapper le % avec un autre. Le second argument du printf (c’est à dire note liste de tag) se placera là où on trouve le %s.’

Avec ces deux scripts à notre portée, si l’on est en train d’éditer un document on peut les exécuter dans vim et facilement le tagguer avec des tags dont on saura qu’ils existent déjà exactement sous cette forme à travers le site. La commande :r!commande1 insérera quelque chose du type :

data
musique
sobriété
youtube

Là où le curseur se trouve. On peut ensuite supprimer les tags que l’on ne souhaite pas. Finalement on sélectionne ce que l’on souhaite et on filtre avec commande2 pour obtenir :

%TAGS data,youtube

Si vous acceptez d’ajouter une petite dépendance à quelque chose comme fzy ou fzf on peut même avoir une jolie interface pour sélectionner ses tags en lançant :

:r!commande1 | fzy -m | commande2

On sélectionne ses tags avec la tabulation, on appuie sur entrée et hop voilà.

L’afficher quelque part

Si l’on tente de construire le site à ce stade on obtiendra comme quoi %TAGS n’est pas une commande. Il faut déclarer l’alias et choisir quoi lui faire faire dans le script qui gère ces pages. En plus du script de base common nous allons créé un script article pour gérer notre cas particulier. Cela fait sens si l’on ne tagguera que des articles. Pour la suite il faudra donc que chaque document que l’on souhaite tagguer commence par #! article pour que cela soit pris en compte. Dans article on appel common pour avoir les alias et fonctions communes à toutes les pages et on spécifie le petit nouveau %TAGS avec :

#! /bin/sh

. ./common

alias %TAGS="tags"
tags() tags="$*"

Ici on décide d’instancier la variable $tags contenant la liste des tags mais nous aurions pu faire n’importe quoi d’autre. Dorénavant nous avons deux choix pour les afficher sur un article. Soit on créer un nouveau layout html dans lequel on intègre les tags soit on les injecte dans le markdown juste avant qu’il soit traduit en html. Chaque méthode à ses avantages et inconvénients.

L’ajouter au layout est peut-être un peu plus “propre” dans le sens que le code s’exécute une seule fois et pas à chaque appel de save_md. Cependant impossible d’insérer les tags au milieu du contenu markdown écrit à la main.

Si l’on veut l’ajouter au layout on peut créer un nouveau layout en ajoutant par exemple :

<meta name="keywords" content="$tags" />
# Et plus loin
<div class="tags">
    <p>tags : $(echo "$tags" | tr ',' '\n' | sed 'p' | xargs printf "<a href='/tags/%s.html'>%s</a> - " | sed -z 's/ - $//')</p>
</div>

Dans $tags la liste des tags séprarés par des virgules. On les met tous sur une ligne différente avec tr, on les double avec sed puis on a à nouveau recours à la technique du xargs + printf pour générer les liens html. Finalement on retire le - qui traîne à la fin. Il a fallu doubler les lignes puisque que pour chaque tag on fait appel à son nom deux fois dans la commande printf qui créer le lien (voir les deux %s). Sachant que chaque %s “consomme” un argument, si l’on ne doublait pas les lignes on aurait des liens type <a href='tags/1.html'>2</a>. sed 'p' double les lignes parce que le comportement par défaut de sed est, après avoir exécuté toutes les commandes, d’imprimer ce qu’il a dans son “pattern space” (c’est à dire ce sur quoi il travail, généralement la ligne courante). La commande sed p imprime le pattern space. Cet appel à sed va donc, pour chaque ligne, l’imprimer puis, à la sortie du script pour la ligne courante, imprimer le pattern space. On se retrouvera donc avec un doublon de chaque ligne.

et en appelant le nouveau layout dans article :

. lib/htmltags

Alternativement on peut surcharger le save_md de common pour insérer, par exemple juste après le titre principal, la liste des tags :

save_md() {
    taglinks=$(echo "$tags" | tr ',' '\n' | sed 'p' | xargs printf "[%s](/tags/%s.html)\ - " | sed -z 's/ - $//')
    cat |
    sed -E "
/^# .+/ s+$+\
\n\
tags : $taglinks \n\
\n\
------------\n\
+" |
    lowdown >> "$the/$1"
}

On met dans la variable taglinks la même chose que ce que l’on avait généré dans le layout mais version markdown. Ensuite on fait un coup de sed qui, pour toutes les lignes commençant le titre principal, ajoute juste après le bloc qui suit. Ce n’est pas super lisible mais j’ai tenté de faire de mon mieux en échappant les nouvelles lignes avec un \ de façon à ce que ça ressemble au plus près à ce qui est réellement inséré dans le flux.

Un petit coup de make et hop on devrait voir les tags affichés sur les articles. J’ai à chaque fois créé des liens avec l’idée de créer ensuite des pages de tags.

Les pages des tags

Chaque tags pourrait avoir sa page, listant les articles concernés. Cette partie est la plus hasardeuse de mon exploration. Je ne la trouve pas satisfaisante et je ne sais pas comment faire autrement.

Il serait délicat de créer à la main chaque page, d’autant plus qu’elle n’a pas vocation à contenir du contenu écrit par des humain·e·s. Je propose donc d’avoir un script qui, basé sur les tags existant dans les articles, va créé les sources des pages des tags. Au prochain make ces pages seront convertie en html comme toutes les autres. Le script en question pourrait être :

#! /bin/sh

mkdir -p src/tags
for tag in $( grep -Ihrm1 "^%TAGS" src/* | cut -d' ' -f2 | tr ',' '\n' | sort -u)
do

<<. cat > src/tags/$tag.md ; chmod +x src/tags/$tag.md
#! page
%T 'Tag $tag'

%S main
# $tag

Articles concernés :
%

grep -lIrm1 '^%TAGS.*musique.*$' src/* |
    xargs grep -Hm1 '^%T ' |
    sed -E 's/([^:]*)[^ ]+ (.+)$/\2\n\1/' |
    sed 's/^"//;s/"$//;s/^src//;s/md$/html/' |
    xargs -d'\n' printf '  * [%s](%s)\n' |
    save_md main
.

done

On créé le dossier tags. On récupère la liste des tags (comme dans la commande1) et on boucle dessus avec une heredoc. Le heredoc contient le “template” du fichier permettant de générer la page des tags. On insère les noms des tags avec la variable $tag qui est celle récupérée par la boucle for. Le contenu du heredoc est mis dans le fichier src/tags/...md dont on modifie les droits d’exécution. Pour récupérer les articles concernés on fait un grep sur la présence du tag en cours dans les fichiers de src avec -l pour n’avoir que les noms des fichiers (pas besoin de la valeur des tags, juste de savoir qu’il y a celui qu’on veut), -I, -r et -m1 sont expliqués plus tôt dans l’article. Pour chacun des fichiers ayant matchés il nous faut son titre, on fait donc un combo xargs grep sur la métadonnée du titre en prenant bien soin de mettre un -H pour que le nom du fichier apparaisse même s’il n’y a qu’un seul résultat. Un peu (pas mal) de sed pour arranger les résultat comment on le veut, encore un xargs + printf pour créer les liens au format makrdown et on sauve tout ça dans la section main.

Le script fonctionne très bien, là où il ne m’offre pas satisfaction est l’intégration avec le makefile. En effet, les pages de tags étant toutes générées depuis le même script il n’est pas possible de créer des dépendances différentes. Si l’une doit être modifiée elles devront toutes l’être. De toute façon, les tags ne vivant que dans les articles eux même il ne serait de toute façon pas possible de savoir quelle page de tag remettre à jour à la modif d’un article puisque l’on ne sait pas qu’est-ce qui a été modifié dans l’article. Est-ce que c’était les tags ? Si oui, qu’est-ce qui a été supprimé / ajouté ? Dans le doute, la seule solution vaguement convenable serait de reconstruire ces pages à chaque build du site. Ce n’est pas forcément très coûteux mais c’est un peu bête d’utiliser make pour en arriver là. De plus, si l’on utilise la parallélisation des règles avec -j il est possible que make ne reconstruise pas certaines pages de tags s’il tente de déclencher ces règles avant que le script générant les fichiers sources ait terminé.

Peut-être qu’une solution serait de faire vivre les tags en dehors des articles, avec un fichier tags listant les tags et vivant à côté de son article :

src/articles/
└── article1
    ├── index.html
    └── tags

Auquel cas il serait possible de créer des règles ayant du sens. Ce système à le désavantage de devoir maintenir un lien entre les deux fichiers, que ce soit à travers leurs emplacements dans l’arborescence, leurs noms, éventuellement une entête dans le fichier tags etc. Ces liens semblent tous un peu plus délicats à maintenir, migrer, porter, faire évoluer que celui d’avoir les tags écrits à l’intérieur du document que l’on souhaite tagguer.

Et si l’on veut maintenant voir tous les tags, mettons sur la page d’accueil ?

Aperçu général des tags

Mettons que nous voulons voir la liste des tags avec le nombre d’articles associés à côté. On peut ajouter le script suivant au fichier :

grep -hrm1 "^%TAGS" src/* |
    cut -d' ' -f2 |
    tr ',' '\n' |
    sort | uniq -c |
    sed -E 's/^ *([0-9]+*) (.+)$/\2\n\2\n\1/' |
    xargs printf "  * [%s](/tags/%s.html) - %s articles\n" |
    save_md main

Toujours la même chose pour récupérer les tags, mais au lieu de retirer les doublons on les compte avec uniq -c. Avec sed on réarrange le contenu de façon à avoir sur deux lignes le nom du tag et la troisième le nombre d’articles associés. Finalement un dernier combo xargs + printf pour créer la liste comme on veut et hop on enregistre.

Programmation de la publication

Il y a plus d’un an de cela Derek voulait pousser un article en cours d’écriture sur le dépôt git pour le partager avec nous sans pour autant qu’il apparaisse sur la page d’accueil puisque non fini. Pour implémenter cela nous avons choisi d’ajouter une date de publication dans les articles.

Ajout de la donnée

Dans les articles, quelques chose sous la forme suivante pour publication le 13 juillet 2023 :

%P 2023-07-13

Condition d’apparition en fonction de la date

Le simple fait d’ajouter la donnée ne change rien mais on peut dorénavant l’utiliser pour conditionner l’apparition des pages en fonction de cette date. Par exemple, si l’on souhaite lister toutes les articles dans le dossier articles :

find src/articles/ -type f -name 'index.md' |
    xargs grep -Hm3 '^%T\|^%A\|^%P' |
    paste - - - |
    sed -Ee 's,src/(.*).md:%T "?([^"]*)"?   src/.*.md:%A (.*)   src/.*%P (.*),* \4 - [\2](\1.html),'\
         -e 's,([0-9]{4})-([0-9]{2})-([0-9]{2}),\1/\2/\3,' |
    sort -rn

alors il suffit d’ajouter une commande awk faisant comparant les dates au format yyyy-mm-dd quelque part dans le pipeline :

find src/articles/ -type f -name 'index.md' |
    xargs grep -Hm3 '^%T\|^%A\|^%P' |
    paste - - - |
    awk -F'\t' -v now="%P "$(date -I) '{if(substr($3,length($3)-12,13)<=now){print $0}}' |
    sed -Ee 's,src/(.*).md:%T "?([^"]*)"?   src/.*.md:%A (.*)   src/.*%P (.*),* \4 - [\2](\1.html),'\
         -e 's,([0-9]{4})-([0-9]{2})-([0-9]{2}),\1/\2/\3,' |
    sort -rn

pour ne voir apparaître que les articles dont la date de publication est antérieur à aujourd’hui. Le tour est joué en une seule ligne de code.

Limitations

Évidemment ce système permet uniquement de contrôler la présence ou non d’un article quelque part dans une liste. Cela n’empêche pas de pouvoir voir les sources dans le dépôt git si celui-ci est publique ni de tomber totalement par hasard dessus si l’on trouve l’url (très peu probable cependant). Pour empêcher ce second scénario il faudrait implémenter quelque chose au niveau de makefile.

Conclusion

J’espère avoir démontré qu’étendre Francium est raisonnablement facile pour une personne sachant développer. Le résultat final et surtout la base nécessaire à pouvoir permettre une telle implémentation me semble petits et gérables sur le long terme. Le compte du nombre de lignes de code est délicat, est-ce que l’on compte les parties “template”, est-ce que chaque pipe compte comme une ligne, chaque commande sed comme une autre ? En tout cas on peut remarquer qu’il est possible de factoriser une quantité non négligeable du code écrit (pour lister les tags par ex). Je dirais qu’il y a environ une trentaine de lignes importantes pour les tags. Cela dit, tout est devant vos yeux. La totalité de l’implémentation a été décrite ici.


  1. du moins pour commencer, peut-être que j’en ferai d’autres à l’avenir 

  2. et à condition d’utiliser vim mais vraiment essayez. Passez en atelier les mardi après-midi à l’atrium sur le campus de l’Unistra pour vous faire aider :)