Vous reprendrez bien un morceau ?
Le langage Ruby foisonne de méthodes diverses et variées pour manipuler des
chaînes de caractères, des nombres, des collections, et bien d’autres. Prenons
le cas des collections par exemple. Il en existe de plusieurs sortes : Array
,
Hash
ou encore Set
pour ne citer que les plus utilisées. Et qu’ont-elles
toutes en commun ? Ce sont des Enumerable
, c’est-à-dire qu’elles incluent ce
module et ainsi partagent quantité de méthodes fort utiles lorsque l’on manipule
des collections.
Parmi ces méthodes, certaines semblent extrêmement spécialisées, parfois obscures, à tel point qu’on peut avoir du mal à comprendre dans quelle situation elles peuvent s’avérer utiles. Jusqu’au jour où cette situation se présente, et là, c’est la révélation !
Cela m’est arrivé récemment alors que je travaillais à l’amélioration d’un outil développé en interne chez Synbioz. Son but est de nous faciliter la tâche lors de la conversion de nos articles de blog — tel que celui que vous lisez en ce moment — rédigés en Markdown, vers leur mise en page finale en HTML.
Cet outil se comporte comme se comporteraient de petits utilitaires Unix spécialisés que l’on chaîne pour aboutir au résultat final.
╭────────────────── Pipeline ─────────────────╮
│ │
╭──────────╮ │ ╭─────────╮ ╭─────────╮ ╭─────────╮ │ ╭──────╮
│ Markdown ├─────>│ Pipe #1 ├───>│ Pipe #2 ├───>│ Pipe #n ├─────>│ HTML │
╰──────────╯ │ ╰─────────╯ ╰─────────╯ ╰─────────╯ │ ╰──────╯
│ │
╰─────────────────────────────────────────────╯
Très vite, on se rend compte que toutes les lignes de texte de notre article n’ont pas la même portée et ne réclament pas le même traitement. Pour illustrer cela, on peut par exemple distinguer un paragraphe dans le corps de l’article, d’un bloc de code, ou encore du frontmatter, cet en-tête au format YAML qui déclare les méta-données.
C’est à ce moment précis que toute la lumière se fait sur Enumerable#chunk
!
Cette méthode méconnue va nous être d’un grand secours. Que fait-elle ? Elle
parcourt les éléments d’un Enumerable
en les regroupant en fonction de la
valeur de retour d’un bloc qu’on lui passe en argument. En d’autres termes, elle
découpe notre article en petits bouts homogènes selon un critère discriminent.
Pour le dire encore autrement, et si l’on m’autorise la comparaison, c’est un
peu l’équivalent de String#split
pour un Enumerable
.
Mettons-nous à table !
À l’instar d’une recette de cuisine nous enjoignant à nous emparer d’un œuf
pour en séparer le blanc du jaune, voyons comment Enumerable#chunk
peut nous
aider à séparer le frontmatter du corps de texte.
Le frontmatter se trouve nécessairement au tout début de notre fichier et est encadré de 3 tirets, comme ceci :
---
author: Jeff B. Cohen <jeff@example.com>
title: Titre de l'article
description: Elle sera utilisée dans la balise <meta name='description'>
---
Ici commence mon article.
Imaginons que l’extrait ci-dessus constitue l’intégralité d’un fichier nommé
article.md
et que nous tâchions d’en extraire le frontmatter. Voici comment
nous pourrions procéder. Tout d’abord, écrivons une méthode chunks
qui prendra
en argument le contenu de notre fichier sous la forme d’une liste de lignes.
SEPARATOR_REGEX = /\A---\z/.freeze
# Sépare des morceaux de données délimités par des lignes de 3 tirets
def chunks(lines, separator: SEPARATOR_REGEX)
lines.chunk do |line|
line.chomp !~ separator
end
end
Nous pouvons ensuite l’utiliser comme ceci :
lines = File.readlines('article.md')
chunks(lines).to_a
# [
# [false, ["---"]],
# [true, ["author: Jeff B. Cohen <jeff@example.com>",
# "title: Titre de l'article",
# "description: Elle sera utilisée dans la balise <meta name='description'>"]],
# [false, ["---"]],
# [true, ["",
# "Ici commence mon article."]]
# ]
Qu’avons-nous ? Une liste de listes, contenant chacune deux données : un booléen
et un Array
. Le booléen représente le résultat de l’expression que contient
notre bloc : false
si l’on se trouve face à notre séparateur, qui sera isolé
dans une liste rien que pour lui ; ou true
dans le cas contraire. Tant qu’on
ne rencontre pas de nouveau notre séparateur, toutes les lignes qui se
présenteront seront regroupées au sein du même Array
. On observe bien ici une
alternance, séparateur, frontmatter, séparateur, corps de l’article.
La singularité de notre exemple fait que nous ne nous retrouvons jamais avec
plusieurs lignes consécutives constituées de 3 tirets. Cela dit, si nous
voulions nous assurer que chaque séparateur se retrouve effectivement isolé, il
existe un mot-clé forçant ce comportement ! Il s’agit de :_alone
. Voyons cela
de plus près :
def chunks(lines, separator: SEPARATOR_REGEX)
lines.chunk do |line|
line.chomp !~ separator || :_alone
end
end
Le résultat de notre bloc sera ainsi true
ou :_alone
. Voici ce que l’on
obtient à présent :
lines = File.readlines('article.md')
chunks(lines).to_a
# [
# [:_alone, ["---"]],
# [true, ["author: Jeff B. Cohen <jeff@example.com>",
# "title: Titre de l'article",
# "description: Elle sera utilisée dans la balise <meta name='description'>"]],
# [:_alone, ["---"]],
# [true, ["",
# "Ici commence mon article."]]
# ]
Dans le cas qui nous occupe cependant, il ne nous est pas utile de récupérer le
séparateur. Tout ce qui nous intéresse, ce sont nos deux blocs. Ça tombe bien,
c’est prévu ! Les valeurs nil
ou :_separator
, au choix, indiquent que les
éléments doivent être ignorés. Adaptons notre méthode chunks
et voyons ce que
ça donne.
def chunks(lines, separator: SEPARATOR_REGEX)
lines.chunk do |line|
line.chomp !~ separator || nil
end
end
Le résultat de notre bloc sera cette fois true
ou nil
. Voici ce que l’on
obtient à présent :
lines = File.readlines('article.md')
chunks(lines).to_a
# [
# [true, ["author: Jeff B. Cohen <jeff@example.com>",
# "title: Titre de l'article",
# "description: Elle sera utilisée dans la balise <meta name='description'>"]],
# [true, ["",
# "Ici commence mon article."]]
# ]
Pour récupérer le frontmatter, il nous suffit cette fois d’extraire le
second élément du premier Array
de l’Enumerator
que nous renvoie notre
méthode chunks
:
lines = File.readlines('article.md')
_, header = chunks(lines).first
puts header
# ["author: Jeff B. Cohen <jeff@example.com>",
# "title: Titre de l'article",
# "description: Elle sera utilisée dans la balise <meta name='description'>"]
Un dernier petit morceau
Pour aller plus en finesse, sachez qu’il existe également une méthode
Enumerable#chunk_while
qui permet de décider où trancher en comparant chaque
élément avec le précédent. Faut-il encore en avoir l’usage… mais qui sait !