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 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 letitle:AGS
après le contenu de l’article auquel cas si l’article parle lui même detitle:AGS
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 dutitle:AGS
, 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 "%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. 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 :
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 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 "^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 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 "^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 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 :
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.
-
du moins pour commencer, peut-être que j’en ferai d’autres à l’avenir ↩
-
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 :) ↩
-
Peut-être qu’on pourrait le factoriser pour éviter d’avoir à le modifier à au moins deux endroits. ↩
-
J’ai choisi la balise
<title>
parce que c’est celle qui me permet le plus facilement d’identifier le contenu d’un article ↩