Mes outils pour rédiger ce blogue -- L'implantation
par Benoit, 2013-04-06

Dans la deuxième et dernière partie de cet article (la première ici), je décris la mise en oeuvre du script d’assemblage de ce blogue, permettant de convertir les sources au format Markdown en un ensemble de pages et de fils d’articles HTML.

Liens pertinents:

Traitement des sources Markdown

Comme mentionné dans la documentation, les articles qui composent ce blogue sont rédigés au format Markdown, avec des méta-informations traitables via l’outil erb, compris dans la librairie standard de Ruby. La première tâche du script d’assemblage du blogue consiste donc à lire chacun des fichiers d’extension .md présents dans le sous-répertoire post du projet. Ils sont traités à l’aide d’erb, dans l’environnement d’une instance de la classe PostBuilder, qui offre les méthodes titre(), date(), etiq(), et ainsi de suite, correspondant chacune aux directives de méta-informations définies. Ces méthodes collaborent à la construction d’une instance de Post, qui contient toutes les informations pertinentes à l’article: son titre, ses étiquettes, sa date de publication… ainsi que son texte principal et, facultativement, son résumé.

À ce stade, le texte et le résumé de l’article sont stockés dans l’instance de Post sous leur forme originale Markdown. La traduction en HTML n’est réalisée qu’au besoin, lors de l’appel d’une des méthodes summary_html(), text_html() ou post_html() (cette dernière ramène le résumé ou, à défaut, le texte). Pour la traduction du Markdown au HTML, on utilise la gemme Ruby kramdown (simple à installer: gem install kramdown). La traduction en soi ne nécessite qu’un simple énoncé, comme le suggère la fonction to_html() appelée par les méthodes mentionnées ci-haut:

require 'rubygems'
require 'kramdown'
# ...
def to_html(md)
  Kramdown::Document.new(md || "").to_html
end

Les pages d’article

Chaque fichier Markdown est converti en une page d’article, soit une page HTML qui contient le contenu entier de l’article. La génération de cette page ne nécessite que le traitement de la source Markdown, ainsi que d’une paire de gabarits RHTML: il est donc très simple de ne générer que les pages correspondant à une source Markdown nouvelle ou modifiée, en comparant le moment d’écriture de la source à celui du fichier HTML. Ce principe de reconstruction incrémentale fait d’ailleurs en sorte que si l’un des gabarits est modifié, toutes les pages d’articles sont regénérées.

Afin de faciliter le maintien d’un visuel cohérent entre chacun des éléments du blogue, j’utilise deux gabarits pour insérer le contenu. Le premier de ces gabarits, base.rhtml, décrit les éléments structurels communs à toutes les pages: l’en-tête avec le titre du blogue, le pied-de-page et la barre de navigation à droite de l’écran. Le contenu principal à gauche de la barre de navigation dépend du type de page visitée: il peut s’agit d’un article complet ou d’une liste d’articles abrégés et annotés. Dans le cas d’une page d’article, la disposition du contenu de l’article est décrite par le second gabarit page.rhtml.

Ces deux gabarits sont décrits au format RHTML: il s’agit simplement de code HTML comprenant des balises spéciales pour le traitement par erb. Ces balises permettent d’insérer divers éléments générés par l’exécution de routines Ruby sans une séquence fastidieuse d’énoncés d’écriture; un peu comme le langage PHP. En particulier, je m’en sers dans le cas d’une page d’articles pour inscrire les données d’une instance de Post, comme son titre ou son texte en format HTML. Donc, on traite d’abord le gabarit page.rhtml de manière à générer le contenu principal; on traite ensuite le gabarit base.rhtml, créant de ce fait la page HTML voulue.

Les fils d’articles

L’un des buts de la mise en oeuvre du script blog.rb consiste à faire en sorte que l’assemblage du blogue soit incrémental, c’est-à-dire qu’il ne génère les pages HTML que pour les sources modifiées ou nouvellement ajoutées. Cela n’est pas immédiat pour les fils d’articles. Le fil principal dépend des quelques fichiers Markdown les plus récents (évalué en termes de la date de publication spécifiée avec la directive d’en-tête date). Quant aux fils spécialisés, il faut pouvoir se rappeler à quelles étiquettes sont rattachés chaque article, de manière à savoir si un fil doit être regénéré, lorsqu’une étiquette est ajoutée ou retirée à un article. Par conséquent, le script maintient une banque de méta-informations persistente dans le fichier meta.yaml, et manipulée via la classe singleton MetaDB. Les instances de Post construites via PostBuilder sont donc transmises à MetaDB afin de déterminer pour quels fils il faut regénérer le code HTML.

En outre, la banque de méta-informations assiste le script dans la compilation des articles composant chacun des fils à regénérer. Cette liste d’articles, dûment ordonnée en ordre chronologique décroissant, est convertie en une page HTML à l’aide de deux gabarits RHTML, comme les pages d’article. Cette fois, le premier gabarit utilisé est post_list.rhtml, qui génère la suite d’articles abrégés, qui est ensuite insérée dans base.rhtml pour former la page finale.

L’index des étiquettes

Un processus similaire à la génération des fils est employée pour construire une liste de toutes les étiquettes servant à classifier les articles du blogue, de manière à explorer le blogue de manière thématique. L’ensemble complet des étiquettes est maintenu dans la banque de méta-informations (MetaDB). La liste au format HTML est donc aisément générée à l’aide du gabarit tags.rhtml, ce qui permet de construire la page complète en l’insérant dans le gabarit base.rhtml.

Le problème des accents

Je rédige ce blog en français et mes sources Markdown sont encodées UTF-8. En particulier, certaines des étiquettes associées aux articles comportent des caractères accentués: selon l’encodage UTF-8, ces caractères n’appartenant pas à l’ensemble ASCII inférieur doivent être décrits à l’aide de deux octets. Cela pose deux importants problèmes. Le premier est que chaque fil spécifique à une étiquette a un nom de fichier basé sur cette étiquette. Dans le cas des étiquettes comportant des caractères accentués (ou d’ailleurs composées de plusieurs mots), le nom du fichier HTML résultant est compliqué et l’URL pour l’atteindre, non conforme au standard. Le deuxième problème est que le tri alphabétique des étiquettes ne place pas un mot comme “Écoute” avec les autres étiquettes commençant par la lettre E.

Dans la version 1.8.7 de Ruby, la librairie coeur et la librairie standard ignorent joyeusement les questions d’encodage. Le meilleur outil que j’ai pu identifier pour manipuler les caractères encodés UTF-8 est la méthode String#unpack(): cette dernière permet de convertir une chaîne de caractères encodée UTF-8 en un tableau d’entiers correspondant respectivement à chacun des caractères. Il est donc possible d’utiliser une simple table de correspondance pour traduire les caractères accentués en leur correspondant sans accent. Le même truc peut être employé pour traduire les espaces, apostrophes et autres inévitables ponctuations en caractères adéquats pour faire partie d’un URL. Le tableau de caractères désaccentués et déponctués est aisément reconverti en chaîne par la méthode Array#pack(), symmétrique à String#unpack().

Le flux RSS

Bien qu’il aurait été aisé d’employer la technique d’instantiation de gabarits utilisée ci-haut pour la composition du flux RSS du site, Ruby offre la librairie standard rss/maker pour solutionner plus élégamment ce problème. Bien qu’il soit aisé de trouver des exemples sur le web, cette énorme et complexe librairie est plutôt mal documentée. Je rapporte ci-dessous le coeur de la fonction make_rss_feed(), où le code XML est généré à partir d’une instance nommée rss:

rss = RSS::Maker.make("2.0") { |maker|
  maker.channel.title = BLOG_TITLE
  maker.channel.link = BLOG_ROOT
  maker.channel.description = "Le blogue de Benoit Hamelin"
  maker.channel.lastBuildDate = Time.now

  metadb = MetaDB.instance
  # Obtenir le Post pour chaque fichier Markdown, puis
  # les trier en ordre chronologique décroissant.
  md_list.collect{|md| metadb.process(md)}.sort{|l,r| r.date <=> l.date}.each{|post|
    item = maker.items.new_item
    item.title = post.title
    item.link = post.full_url
    # Voir ci-dessous pour justifier root_local_links().
    item.description = root_local_links(post.post_html)
    item.author = post.author_address if post.author_address
    item.date = post.date
    item.guid.content = post.full_url
    item.guid.isPermaLink = true
    post.tags.sort.each { |tag|
      c = item.categories.new_category
      c.content = tag
    }
  }
}
open(path, "w") {|file|
  file.puts(rss.to_s)
}

L’objet RSS est relativement simple à construire lorsqu’on connaît quelque peu le standard, puisque les champs à évaluer correspondent aux éléments XML qui composent le standard. Deux exceptions viennent compliquer la situation:

  1. Pour chaque item devant être ajouté au channel, il faut invoquer maker.items.new_item, puis décrire l’item en utilisant les méthodes de l’instance renvoyée par new_item. Dans le standard, les éléments item sont enfants de l’élément channel, aussi m’aurait-il semblé plus simple de créer un item via maker.channel.new_item, plutôt.
  2. Similairement, au sens du standard, un item peut contenir plusieurs éléments category, lesquelles correspondent ici aux étiquettes de l’article. Pour ajouter une catégorie, il faut invoquer item.categories.new_category, ce qui prend à nouveau mon intuition à revers. Notons que mon approche à deux lignes ci-haut aurait alternativement pu être écrite:

    item.categories.new_category.content = tag
    

Le fil généré par cette approche est valide, dans la mesure où il est accepté par le validateur de flux mis en ligne par les auteurs du standard RSS. Cependant, ce fil a comme défaut qu’il associe à chaque item les éléments redondants pubDate et dc:date. Je n’ai pas encore réussi à faire retirer l’élément dc:date, qui ne fait pas partie du standard RSS 2.0 auquel mon flux est censé adhérer.

Commentaires