Étendre Catium : Quelques exemples pratiques

Retour accueil


Catium ne comporte 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 générateur de sites statiques offrent 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 Catium, 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 title: 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 title:AGS. Les valeurs pourraient être délimitées par des virgules comme ceci :

title:AGS 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 "^title:AGS" 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 title:AGS après le contenu de l’article auquel cas si l’article parle lui même de title:AGS 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 title:AGS, 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 "%title:AGS %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 :

title:AGS 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 title:AGS 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 #! page 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 title:AGS avec :

#! /bin/sh

. ./common

alias title:AGS="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 "^title:AGS" src/* | cut -d' ' -f2 | tr ',' '\n' | sort -u)
do

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

sectionmd: main
# $tag

Articles concernés :
%

grep -lIrm1 '^title:AGS.*musique.*$' src/* |
    xargs grep -Hm1 '^title: ' |
    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 "^title:AGS" 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 :

publication: 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 '^title:\|^author:\|^publication:' |
    paste - - - |
    sed -Ee 's,src/(.*).md:title: "?([^"]*)"?   src/.*.md:author: (.*)  src/.*publication: (.*),* \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 '^title:\|^author:\|^publication:' |
    paste - - - |
    awk -F'\t' -v now="publication: "$(date -I) '{if(substr($3,length($3)-12,13)<=now){print $0}}' |
    sed -Ee 's,src/(.*).md:title: "?([^"]*)"?   src/.*.md:author: (.*)  src/.*publication: (.*),* \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.

Bilinguisme

J’ai eu à implémenter un site en version bilingue anglais<->français récemment, voilà comment j’ai fait. Le dépôt du site en question et son url.

L’idée est que les fichiers sources continennent dans leurs noms la langue de destination. Je crois que d’autres systèmes fonctionnent comme ça. Par exemple contact.fr.sh est la source de contact.html en français et contact.en.sh la source de contact.html en anglais.

Dans le makefile on récupère les fichiers sources et avec une petite commande sed on créer le chemin de la cible en prenant compte de la langue

sources      != find contents -type f -name '*.sh'
html         != echo "${sources}" | sed -E 's,contents/([^.]+)\.(en|fr)\.sh,public/\2/\1.html,g'

Ainsi on a

sources cibles
contents/contact.fr.sh public/fr/contact.html
contents/contact.en.sh public/en/contact.html
contents/apropos.fr.sh public/fr/apropos.html
contents/apropos.en.sh public/en/apropos.html

La commande sed est un peu moche et ne gère pas des langues arbitraires parce que j’ai eu la flemme d’écrire une expression régulière généraliste. Sans un système plus complexe il faudra toujours que la page s’appelle de la même manière dans les deux langues. Pas de “about.html” en anglais donc. Au pire cela empêche une personne qui parle anglais de deviner les noms des pages et de trouver toute seule site.com/en/about.html. Si les urls des sites étaient assez standardisée pourquoi pas mais en l’état ça ne me semble pas être un gros problème.

Il nous faut ensuite générer des liens sur les pages permettant de passer d’une langue vers une autre. Pour cela il nous faut, lorsque l’on génère une version, construire l’url de l’autre pour l’insérer dans la page. On peut modifier page de la sorte :

destination=$(echo "$@" |
    sed -E 's,contents/([^.]+)\.(en|fr)\.sh,public/\2/\1.html,g' |
    sed -E 's,^public,,')
echo "$@" | grep -q ".fr." && autre="en" || autre="fr"
destinationautre=$(echo "$destination" | sed "s,/../,/$autre/,")

On reproduit le traitement du makefile pour connaître l’url de destination3, on retire la partie public puis on met la langue opposée dans une variable puis on construit le chemin vers l’article de l’autre version en fonction de la langue récupérée.

Dans le layout on ajoute là où l’on souhaite voir le lien apparaître :

<a href="$destinationautre">$autre</a>

En l’état tout fonctionne bien sauf la page d’accueil servie par défaut, généralement index.html. En effet avec notre système toutes les pages html se retrouveront sous les dossiers fr ou en mais jamais à la racine.

Pour contourner ce problème on peut à nouveau modifier le makefile de façon à ce que le fichier index.fr.sh (ou index.en.sh en fonction de la langue par défaut de votre site) génère à la fois /fr/index.html et /index.html :

all: public/index.html ...

public/index.html : contents/index.fr.sh page common
    mkdir -p $(shell dirname $@)
    $< > $@

On ajoute l’index.html de la racine dans les dépendances de la règle all puis une règle personnalisée pour cette page qui ne dépend pas directement de son clône dans le dossier source mais de index.fr.sh ou index.en.sh selon la langue par défaut.

Notre site intègre une gestion basique du bilinguisme ! Il ne reste plus qu’à tout traduire !

S’auto-référencer dans un article

J’ai récemment entrepris la rédaction d’une longue page se faisant fréquemment référence à elle même. Pour renvoyer vers une autre page du site ou pire, renvoyer vers un titre spécifiquement, il faut faire preuve d’une mémoire colossale et d’une implémentation mentale de l’algorithme de génération des identifiants de titre de votre traducteur markdown -> html.

J’ai donc entrepris de créer un script permettant de lister toutes les options et d’en choisir une. Ce script a pour dépendance fzy pour l’interface de choix et ne fonctionnera qu’après une première construction du site puisqu’il va chercher les données dans l’html et non pas dans les sources.

Il faut d’abord mettre dans une variable la totalité des éléments ayant un attribut id et les titres de tous les articles4 :

link=$(find public -type f -iname '*.html' |
xargs grep -Er '(<title>|id=")' |
    cut -b7- |
    fzy)

find liste tous les fichiers html, xargs grep permet de chercher toutes les lignes contenant <title> ou id=" dans chacun de ces fichiers puis cut supprime le nom du dossier public. Le format de sortie est comme ceci :

/youtube/index.html:    <title>Guide de survie en territoire youtubesque</title>
/youtube/index.html:    <h2 id="pourquoi-">Pourquoi ?</h2>
/youtube/index.html:    <h2 id="pour-les-smartphones">Pour les smartphones</h2>

Ensuite, le traitement sera différent selon si notre sélection est un identifiant vers lequel renvoyer ou le titre d’une page entière. Pour tester cela on utilise des grep :

if $(echo "$link" | grep -q 'id='); then
    [...]
else $(echo "$link" | grep -q '<title>'); then
    [...]
fi

Si l’on veut faire un lien vers un titre en particulier page on choisit la ligne contenant la balise titre correspondante et le traitement est comme suit :

if $(echo "$link" | grep -q 'id='); then
    echo "$link" |
        sed -E 's,:.+id=",#,;s/index.html//' |
        cut -d'"' -f1
else $(echo "$link" | grep -q '<title>')
    [...]
fi

On remplace tout ce qui se trouve entre le chemin du fichier et la première apostrophe par un #, on retire l’index.html (optionnel) et on ne garde que ce qu’il y a à gauche de l’apostrophe restante. L’avant-après :

/youtube/index.html:    <h2 id="pour-les-smartphones">Pour les smartphones</h2>
/youtube/#pour-les-smartphones

Si l’on veut faire un lien vers une page, on choisit la ligne qui contient sa balise titre et le traitement sera le suivant :

if $(echo "$link" | grep -q 'id='); then
    [...]
else $(echo "$link" | grep -q '<title>'); then
    echo "$link" |
        cut -d':' -f1 |
        sed 's/index.html//'
fi

On ne garde que le chemin du fichier dans lequel on a trouvé la balise avec cut et on supprime index.html (toujours optionnel). L’avant-après :

/youtube/index.html:    <title>Guide de survie en territoire youtubesque</title>
/youtube/

Le script final :

link=$(find public -type f -iname '*.html' |
    xargs grep -Er '(<title>|id=")' |
        cut -b7- |
        fzy)

if $(echo "$link" | grep -q 'id='); then
    echo "$link" |
        sed -E 's,:.+id=",#,;s/index.html//' |
        cut -d'"' -f1
else $(echo "$link" | grep -q '<title>'); then
    echo "$link" |
        cut -d':' -f1 |
        sed 's/index.html//'
fi

Si on veut l’utiliser dans vim à la volée en mode insertion on peut utiliser cette macro :

inoremap <buffer> (-l <ESC>:r!./slug<CR>kgJEa

En tapant rapidement sur (-l en mode insertion fzy s’ouvrira, on fait son choix et hop on continue à écrire derrière le lien nouvellement inséré.

Conclusion

J’espère avoir démontré qu’étendre Catium 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 petite et gérable 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 les tags je dirais qu’il y a environ une trentaine de lignes importantes, une dizaine pour le bilinguisme. Ce n’est pas négligeable, surtout quand on considère la relative crypticité de certaines commandes mais 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 :) 

  3. Peut-être qu’on pourrait le factoriser pour éviter d’avoir à le modifier à au moins deux endroits. 

  4. J’ai choisi la balise <title> parce que c’est celle qui me permet le plus facilement d’identifier le contenu d’un article