É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 danssrc
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. Lecut
permet de se débarrasser du%TAGS
, letr
d’avoir un tag par ligne et lesort -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. Lexargs printf
permet d’insérer le résultat dans une chaîne de caractère passée en premier argument deprintf
. En l’occurence faut bien échapper le%
avec un autre. Le second argument duprintf
(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 avectr
, on les double avecsed
puis on a à nouveau recours à la technique duxargs
+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 commandeprintf
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 desed
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 sedp
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 desed
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 bouclefor
. Le contenu du heredoc est mis dans le fichiersrc/tags/...md
dont on modifie les droits d’exécution. Pour récupérer les articles concernés on fait ungrep
sur la présence du tag en cours dans les fichiers desrc
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 comboxargs 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) desed
pour arranger les résultat comment on le veut, encore unxargs
+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 comboxargs
+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.