Org-mode: Index dynamique de documents Org-mode

Dans un précédent billet, on a vue comment utiliser les blocs dynamiques d'Org-mode et comment créer un nouveau type de bloc.

Maintenant, je vais montrer comment utiliser tout ça pour créer un nouveau type de bloc dynamique: index. Ce type de bloc génère un indexe de documents Org-mode.

Avant de commencer

Dans ce billet de blog, je vais analyser du code. Si tu n'es pas intéressé par cette analyse, mais que tu souhaite seulement utiliser ce nouveau type de bloc pour créer des indexes: Tu peux sauter à la section TL;DR.

Pour comprendre l'analyse, il est important:

  • D'avoir lu le précédent billet sur les blocs dynamiques
  • D'avoir quelques notions de programmation
  • Connaitre un minimum le Emacs-Lisp (Elisp), notamment comment est déclaré une fonction et comment elle peut retourner une valeur

Bonne lecture.

Objectif

Imagine: Tu as un dossier dans lequel se trouve des documents Org-mode et tu voudrais avoir, dans un autre document, un index de ce dossier.

Pour y arriver, on va créer un nouveau type de bloc:

  • Nommé index
  • Il va généré une liste, au format Org-mode, de tout les fichiers Org-mode présents dans un dossier
  • Le dossier parcouru pourra être indiqué avec le paramètre :directory, sinon le dossier courant sera utilisé
  • Le formatage de chaque élément de la liste pourra être personnalisé avec le paramètre :format
  • Optionnellement, les fichiers trouvés pourront être triés selon leur mot-clé DATE, du plus récent au plus ancien, avec le paramètre :sort-recent

Formatage des éléments de la liste

Pour le formatage des éléments de la liste, on pourra indiquer une chaine de caractère avec le paramètre :format. Cette chaine sera utilisée pour chacun des éléments.

Dans cette chaine de caractère, on aura accès à des gabarits, comme le titre, la description ou le chemin vers le fichier. Certains de ces gabarits reprennent la valeur des mots-clés présents dans les documents Org-mode trouvés.

Les gabarits suivants seront utilisable:

gabarit Mot-clé Org-mode description
%t TITLE Titre
%d DATE Date
%a AUTHOR Auteur ou autrice
%c CREATOR Créateur ou créatrice
%D DESCRIPTION Description
%p   Chemin vers le fichier
%P   Chemin absolue vers le fichier
%l LANGUAGE Langue
%o OPTIONS Options

Exemple de chaine de formatage:

"%d: [[%p][%t]]"

Et voici ce que donnerait cette exemple une fois formaté:

Exemple d'utilisation d'un bloc index

Voici un exemple d'utilisation d'un bloc de type index:

#+BEGIN index :directory "posts/"

#+END

Et voici un exemple de ce que le bloc va générer:

#+BEGIN: index :directory "posts/"
- 2023.07.05 : Org-mode: Dynamic block
- 2023.07.03 : Org-mode: Dupliquer une note
- 2023.06.06 : Fedora: OSTree Native Container
- 2023.05.09 : Org-noter
- 2023.04.30 : Ouverture
#+end

TL;DR

Pour créer le nouveau type de bloc dynamique index, il faut copier ce code dans ~/.emacs.d/init.el:

(defun my/org-index-extract-infos (file-path)
  "Extract, from one file, all the info needed for building an index"
  (with-temp-buffer
    (insert-file-contents file-path)
    (append
     (org-element-map (org-element-parse-buffer) 'keyword
       (lambda (keyword)
         (list
          (org-element-property :key keyword)
          (org-element-property :value keyword))))
     (list (list "FILE-PATH" file-path)
           (list "FILE-FULL-PATH" (expand-file-name file-path))))))



(defun my/org-index-list-files-path (directory)
  "Get relative path of all Org-mode document of a directory"
  (mapcar
   (lambda (file-name)
     (concat directory file-name))
   (directory-files directory nil ".org$")))


(defun my/org-index-sort-documents (documents-list)
  "Sort an Org-documents list by its date"
  (sort
   documents-list
   (lambda (file-a file-b)
     (string>
      (car (cdr (assoc '"DATE" file-a)))
      (car (cdr (assoc '"DATE" file-b)))))))


(defun my/org-index-format-element (document-infos fmt)
  "Generate a string with document infos"
  (format-spec
   fmt
   (list (cons ?t (car (cdr (assoc '"TITLE" document-infos))))
         (cons ?d (car (cdr (assoc '"DATE" document-infos))))
         (cons ?a (car (cdr (assoc '"AUTHOR" document-infos))))
         (cons ?c (car (cdr (assoc '"CREATOR" document-infos))))
         (cons ?D (car (cdr (assoc '"DESCRIPTION" document-infos))))
         (cons ?p (car (cdr (assoc '"FILE-PATH" document-infos))))
         (cons ?P (car (cdr (assoc '"FILE-FULL-PATH" document-infos))))
         (cons ?l (car (cdr (assoc '"LANGUAGE" document-infos))))
         (cons ?o (car (cdr (assoc '"OPTIONS" document-infos)))))))


(defun org-dblock-write:index (params)
  "Generate an index of a director, as a list"
  (let ((directory (or
                    (plist-get params :directory )
                    (file-name-directory (buffer-file-name))))
        (sort-recent (plist-get params :sort-recent))
        (fmt (or (plist-get params :format) "%d: [[file:%p][%t]]"))
        (documents-infos nil))
    (if sort-recent
        (setq documents-infos (my/org-index-sort-documents
                               (mapcar
                                'my/org-index-extract-infos
                                (my/org-index-list-files-path directory))))
      (setq documents-infos (mapcar
                             'my/org-index-extract-infos
                             (my/org-index-list-files-path directory))))
    (mapcar
     (lambda (one-document-infos)
       (insert
        "- "
        (my/org-index-format-element one-document-infos fmt)
        "\n"))
     documents-infos)))


(defun my/org-index-insert-dblock (directory)
  "Insert a new dynamic bloc of type index"
  (interactive
   (list (string-replace
          (file-name-directory (buffer-file-name))
          ""
          (expand-file-name (read-directory-name "Dirictory to build index on: ")))))
  (org-create-dblock
   (list :name "index"
         :directory directory)))

(org-dynamic-block-define "Index" 'my/org-index-insert-dblock)

Fonctionnement interne du bloc

Dans la section "Objectifs", on a passé en revue le fonctionnement du nouveau type de bloc du point de vue extérieur. On a définit quel texte il devait générer, quels paramètres il devait accepter et quels effets auront ces paramètres.

Maintenant, on doit définir son fonctionnement interne. Et pour commencer: De quels informations, en plus des paramètres, notre nouveau type de bloc aura besoin.

Stockage des informations de chaque document Org-mode trouvé

Pour chaque fichiers trouvés dans le dossier parcouru, il faudra récupérer les mots-clés ainsi que le chemin vers le fichier. On doit donc générer, pour chaque document Org-mode, une liste d'informations.

Ici, j'ai choisi le format associated list, ou alist. Il s'agit d'une liste, où chaque élément est lui-même une liste dans laquelle on trouve 2 éléments. Le premier élément sert de clé, le second de valeur associée. D'où le nom associated list.

Voici un exemple de alist:

'(("TITLE" "Titre de mon document")
  ("DATE" "2023.07.08"))

Je vais donc créer une première fonction Emacs-Lisp, dont le but sera d'extraire les informations d'un seul fichier. Cette fonction nous retournera ces informations sous la forme d'une alist. Sa signature sera: (my/org-index-extract-infos file-path). Son seul paramètre d'entrée, file-path, est une chaine de caractère représentant le chemin vers le fichier.

  • Fonction: (my/org-index-extract-infos file-path)
  • Paramètres:
    • file-path: Chemin vers le fichier à analyser
  • Retour:
    • Une alist de toutes les informations extraites du document analysé

Lister les documents Org-mode trouvés

Pour la deuxième fonction, je vais en créer une qui retournera la liste des fichiers Org-mode trouvés dans un dossier. Sa signature est: (my/org-index-get-file-paths-list directory). Son seul paramètre, directory, est le chemin vers le dossier à parcourir. Sous forme de chaine de caractère.

  • Fonction: (my/org-index-get-file-paths-list directory)
  • Paramètres:
    • directory: Chemin vers le dossier à parcourir
  • Retour:
    • Une liste de chemins, sous la forme d'une chaine de caractère

Cette fonction ne donnera que le chemin vers chaque document Org-mode. Mais je pourrais utiliser la fonction (mapcar), pour exécuter la fonction d'extraction sur chaque chemin retourné par la fonction de parcours.

Trie selon la date

Il me faudra aussi une fonction capable de trier la liste de plusieurs alist, selon le critère de trie définit dans la section "Objectif" de ce billet.

  • Fonction: (my/org-index-sort-documents documents-list)
  • Paramètres:
    • documents-list: La liste des documents Org-mode, sous la forme d'une liste de plusieurs alist
  • Retour:
    • La liste des documents Org-mode, triés selon la date

Formatage du contenu du bloc

Pour rappelle, le bloc dynamique devra générer une liste, au format Org-mode, de tout les documents Org-mode trouvés.

Mais chaque éléments de cette liste devra être formaté en utilisant une chaine de caractère. (Voir section Formatage des éléments de la liste).

Pour ça, je dois créer une fonction nommée (my/org-index-format-element document-alist format-str) qui s'occupera de formater une chaine de caractère. Le premier argument est une alist de toutes les informations sur un seul document. Le second argument est la chaine de caractère utilisée pour le formatage.

  • Fonction: (my/org-index-format-element document-infos fmt)
  • Paramètres:
    • document-infos: Les information d'un document Org-mode trouvé, au format alist
    • fmt: La chaine de caractère utilisée pour le formatage
  • Retour:
    • Une chaine de caractère formatée, à insérer dans le bloc dynamique

Le bloc dynamique

Ensuite, je vais créer la fonction (org-dblock-write:index params), qui va insérer le contenu du bloc dynamique. Elle utilisera les fonctions précédemment écrites, pour récupérer toutes les informations nécessaires sur chacun des fichiers Org-mode trouvés dans le dossier parcouru. Elle utilisera ensuite les fonctions (mapcar), (insert) et (format-spec) pour générer le contenue du block, à partir des informations extraites et de la chaine de formatage.

En enfin, j'intégrerai le nouveau type de bloc avec la fonction org-dynamic-block-insert-dblock.

Préparations

Pour cette article, je t'invite à créer un dossier de travail afin de pouvoir y tester ton code.

Crée simplement un dossier, nommé par exemple index-test/. Dans ce dossier, créer un fichier index-test.el ainsi qu'un dossier posts/. Le code du nouveau type de bloc sera écrit dans le fichier .el. Et dans le dossier posts/, on va placer 2 fichiers Org-mode.

Dans le dossier posts/, crée 2 fichiers Org-mode auxquels tu aura définit quelques mots-clés. Définit au moins les mots clés suivants: TITLE, DATE et AUTHOR. Par exemple comme ceci:

#+TITLE:      Article de test 1
#+AUTHOR:     Moi
#+DATE:       2023.06.06

Pour la date, je conseil d'utiliser le format AAAA.MM.DD, où AAAA est l'année, MM le mois sur 2 nombres et DD le jour sur 2 nombres aussi.

Tu peux maintenant ouvrir le fichier index-test.el avec Emacs et commencer l'écriture du code.

Écriture du nouveau type de bloc

Dans cette section, je vais suivre le format suivant: D’abord j'écris le code qu'il faut recopier dans index-test.el, puis je explique son fonctionnement partie par partie.

Et là première ligne de code qu'on va ajouter au fichier index-test.el est la suivante:

(require 'org-element)

La fonction (require) va simplement charger une fonctionnalité si elle n'est pas déjà chargée. Ici on charge org-element, qui nous fourni une API pour manipuler des fichiers Org-mode.

Récupération des mots-clés d'un fichier

Voici la fonction, (my/org-index-extract-infos), qui va récupérer toutes les informations utiles d'un seul fichier Org-mode:

(defun my/org-index-extract-infos (file-path)
  "Extract, from one file, all the info needed for building an index"
  (with-temp-buffer
    (insert-file-contents file-path)
    (append
     (org-element-map (org-element-parse-buffer) 'keyword
       (lambda (keyword)
         (list
          (org-element-property :key keyword)
          (org-element-property :value keyword))))
     (list (list "FILE-PATH" file-path)
           (list "FILE-FULL-PATH" (expand-file-name file-path))))))

Le paramètre d'entrée est file-path, le chemin vers le fichier à traiter. Le retour est une alist représentant toutes les informations extraites du fichier.

Il faut l'écrire dans le fichier index-test.el.

Maintenant, une explication du code.

Exécution dans un buffer temporaire

Pour commencer, notre fonction va appeler la fonction (with-temp-buffer). Cette dernière va simplement créer un buffer Emacs temporaire. Toutes les instructions Elisp donnés en paramètre à (with-temp-buffer) seront exécutées depuis ce buffer.

Son utilisation est:

(with-temp-buffer
  (call_1)
  (call_2)
  …)

Il faut remplacer call_1 et call_2 par des fonction qu'on souhaite appeler.

Remplissage du buffer temporaire

La première fonction exécutée dans ce buffer temporaire est (insert-file-contents). Elle va remplir le buffer courant avec le contenu du fichier indiqué par le premier paramètre. Ici, on lui donne le chemin reçu par le paramètre file-path. Après exécution, le buffer temporaire contiendra tout ce qui se trouve dans le fichier qu'on traite.

Concaténation de 2 listes

La deuxième fonction est (append), qui nous sert à concaténer 2 alist. La première contient tous les mots-clés Org-mode extraits à partir du contenu. La seconde contient le chemin relatif vers le fichier et le chemin absolue.

Extraction des mots clés du document Org-mode

La liste des mots clés est obtenu avec ce code:

(org-element-map (org-element-parse-buffer) 'keyword
  (lambda (keyword)
    (list
     (org-element-property :key keyword)
     (org-element-property :value keyword))))

Dans ce bout de code, on utilise la fonction (org-element-parse-buffer). Cette fonction analyse le buffer courant et retourne un arbre de tout les éléments trouvés.

On utilise également la fonction (org-element-map). Cette fonction sert à exécuté une fonction sur chaque élément d'un certain type présent dans un arbre d'analyse. On lui passe 3 arguments:

  • Un arbre d'analyse de notre document Org-mode, obtenu par l'appel à (org-element-parse-buffer)
  • Le type d'élément pour lequel on souhaite effectuer un traitement, ici 'keyword
  • Une fonction anonyme (lambda), qui sera exécutée pour chaque élément trouvé

La fonction anonyme (lambda) reçoit un seul paramètre. Il s'agit d'une property-list (plist) dans laquelle se trouvent les informations sur l’élément actuellement traité.

Si dans le documents Org-mode se trouvent 4 mots-clés (keywords), alors notre fonction anonyme sera appelée 4 fois.

Voici un exemple de plist que la fonction reçois:

(:key "Keyword_name" :value "Keyword_value")

Notre fonction anonyme va simplement récupérer les 2 valeurs de la plist et reconstruire une cellule de alist avec.

Par exemple, si elle reçoit ceci:

(:key "AUTHOR" :value "Moi")

Elle va retourner ceci:

("AUTHOR" "Moi")

Tous les retours des appelles à la fonction anonymes seront ajoutés à une liste, qui sera retournée par la fonction (org-element-map) après exécution.

Appel à cette fonction

Pour tester cette nouvelle fonction, tu peux l'exécuter comme ceci:

(my/org-index-extract-infos "posts/article1.org")

Il faut remplacer "posts/article1.org" par le chemin relatif à un de tes document Org-mode présent dans le dossier posts/.

Si tout fonctionne, elle devrait te retourner une alist avec tout les mots clés présent dans ton document de test, ainsi que le chemin relatif et absolue vers document.

Si tu veux tester la fonction, le chemin que tu indique est relatif au dossier de travail d'Emacs. Et le dossier de travail d'Emacs dépends du buffer sur lequel tu travail.

Récupération de la liste des fichiers org-mode d'un dossier

Voici la fonction, (my/org-index-list-files-path), qui va trouver tous les documents Org-mode d'un dossier:

(defun my/org-index-list-files-path (directory)
  "Get relative path of all Org-mode document of a directory"
  (mapcar
   (lambda (file-name)
     (concat directory file-name))
   (directory-files directory nil ".org$")))

Cette fonction reçoit comme paramètre le chemin vers le dossier à parcourir. Elle retourne une liste de chemins, un vers chaque document Org-mode trouvé.

Il faut l'écrire dans le fichier index-test.el.

Cette fonction va appeler (directory-files) en lui indiquant:

  • Le dossier à parcourir (premier paramètre)
  • De ne pas retourner des chemins absolues (deuxième paramètre)
  • La regex à utiliser pour trouver les fichiers qui nous intéressent (troisième paramètre)

La fonction (directiory-files) retourne les noms des fichiers trouvés, sous la forme d'une liste de chaines de caractères. Mais on souhaite avoir une liste de chemins vers chaque documents, pas uniquement les noms des fichiers.

C'est pour cela qu'on utilise (mapcar): Pour chaque nom de fichier trouvés par (directory-files), on exécute la fonction anonyme suivante:

(lambda (file-name)
  (concat directory file-name))

Cette fonction va simplement concaténer le chemin vers le dossier parcourus et le nom du fichier trouvé.

Le résultat de chaque exécution de la fonction anonyme est ajouté à une liste retournée par (mapcar). Et c'est cette liste que notre fonction retourne.

Trie en fonction de la date

Voici la fonction de trie, (my/org-index-sort-documents documents-list):

(defun my/org-index-sort-documents (documents-list)
  "Sort an Org-documents list by its date"
  (sort
   documents-list
   (lambda (file-a file-b)
     (string>
      (car (cdr (assoc '"DATE" file-a)))
      (car (cdr (assoc '"DATE" file-b)))))))

Elle accepte une liste de alist et retourne la liste triée. Elle utilise la fonction (sort), à laquelle on a passé 2 paramètres: La liste à trier et une fonction utilisée pour faire la comparaison entre 2 éléments à trier.

Il faut l'écrire dans le fichier index-test.el.

La fonction de trie est une fonction anonyme et ressemble à ceci:

(lambda (file-a file-b)
  (string>
   (car (cdr (assoc '"DATE" file-a)))
   (car (cdr (assoc '"DATE" file-b)))))

Elle accepte comme paramètres les 2 éléments à comparer et utilise la fonction (string>) pour comparer les dates. Ici, pour des raisons de simplification, on va traiter les dates comme de simples chaines de caractères.

Pour extraire la date de file-a et file-b, qui sont des alist, on utilise la fonction (assoc). En indiquant la clé '"DATE", (assoc) nous renvoie la cellule dont le premier élément est "DATE". Ce qui nous donne, par exemple: ("DATE" "2023.07.08"). On utilise ensuite (cdr) pour extraire le deuxième élément de la cellule.

(cdr) retourne toujours la liste qu'on lui donne, sans le premier élément. Dans le cas d'une liste de 2 éléments, il nous retourne donc une liste d'un seul élément. C'est pour ça qu'on utilise (car), pour extraire ce seul élément de la liste.

Formatage d'un élément de la liste

Voici le code de la fonction (my/org-index-format-element document-infos fmt):

(defun my/org-index-format-element (document-infos fmt)
  "Generate a string with document infos"
  (format-spec
   fmt
   (list (cons ?t (car (cdr (assoc '"TITLE" document-infos))))
         (cons ?d (car (cdr (assoc '"DATE" document-infos))))
         (cons ?a (car (cdr (assoc '"AUTHOR" document-infos))))
         (cons ?c (car (cdr (assoc '"CREATOR" document-infos))))
         (cons ?D (car (cdr (assoc '"DESCRIPTION" document-infos))))
         (cons ?p (car (cdr (assoc '"FILE-PATH" document-infos))))
         (cons ?P (car (cdr (assoc '"FILE-FULL-PATH" document-infos))))
         (cons ?l (car (cdr (assoc '"LANGUAGE" document-infos))))
         (cons ?o (car (cdr (assoc '"OPTIONS" document-infos)))))))

Pour rappelle, cette fonction accepte 2 paramètres: Les informations sur un document (document-infos), au format alist, et la chaine de caractère servant au formatage. Elle retourne la chaine de caractère formatée.

Il faut l'écrire dans le fichier index-test.el.

Pour le formatage, on utilise la fonction (format-spec) avec 2 paramètres: La chaine de caractère de formatage et la liste des gabarits.

La liste de gabarits est une alist, où les cellules utilisent un caractère . pour séparer la clé et la valeur.

Exemple de liste de gabarit :

'((?t . "Title of document")
  (?a . "Name of the author"))

On doit construire cette liste et associer chaque informations extraites du document avec une lettre de gabarit.

Construction de la liste des gabarits

Pour rappelle, la liste de gabarits doit ressembler à ceci:

'((?t . "Title of document")
  (?a . "Name of the author"))

On doit remplacer les chaines de caractère de cette exemple par les informations extraites du document Org-mode. Ces informations se trouvent dans la variable document-infos.

Pour chaque gabarit, on construit une cellule de alist en utilisant la fonction (cons). Cette fonction accepte 2 paramètres: La clé et la valeur de la cellule.

Exemple, si on exécute ceci:

(cons "Author" "Me")

On obtiens cette cellule en retour:

("Author" . "Me")

Dans notre liste de gabarit, on va donc associer une lettre avec une valeur issue des informations sur le document. Pour extraire une information de la variable document-infos, on utilise (assoc), (cdr) et (car). Je te renvoie à la section Trie en fonction de la date pour le détail de leur utilisation.

Chaque cellule construite est ajoutée à une liste en utilisant la fonction (list).

Le code du bloc dynamique

On arrive presque à la fin. Accroche toi, c'est bientôt terminé.

Voici le code de la fonction (org-dblock-write:index), qui sert à écrire le contenu d'un bloc de type index:

(defun org-dblock-write:index (params)
  "Generate an index of a director, as a list"
  (let ((directory (or
                    (plist-get params :directory )
                    (file-name-directory (buffer-file-name))))
        (sort-recent (plist-get params :sort-recent))
        (fmt (or (plist-get params :format) "%d: [[file:%p][%t]]"))
        (documents-infos nil))
    (if sort-recent
        (setq documents-infos (my/org-index-sort-documents
                               (mapcar
                                'my/org-index-extract-infos
                                (my/org-index-list-files-path directory))))
      (setq documents-infos (mapcar
                             'my/org-index-extract-infos
                             (my/org-index-list-files-path directory))))
    (mapcar
     (lambda (one-document-infos)
       (insert
        "- "
        (my/org-index-format-element one-document-infos fmt)
        "\n"))
     documents-infos)))

Cette fonction sera appelée directement par Org-mode, quand on demandera à générer le contenu d'un bloc de type index. Elle recevra tout les paramètres du bloc avec le paramètre params et elle va simplement insérer du texte: La liste, au format Org-mode, représentant tous les documents Org-mode trouvés.

Il faut l'écrire dans le fichier index-test.el.

Cette fonction est séparée en 3 parties: Définition des variables locales, récupération des informations sur chaque documents Org-mode et génération de la liste au format Org-mode.

Variables locales

Pour définir des variables locales, on utilise la fonction (let). Son première argument est une alist de toutes les variables locales. Tous les autres arguments sont du code qui aura accès à ces variables locales.

La alist des variables locale sera transformée en liste automatiquement. Donc, si pour la valeur d'une variable on écrit du code, ce code sera exécuté et son résultat sera assigné à la valeur.

Exemple simple d'utilisation de (let):

(let ((var_1 11)
      (var_2 22))
  (message var_1)
  (+ var_1 var_2))

Dans cet exemple, on définit 2 variables: var_1 et var_2. On va ensuite appeler les fonctions (message) et (+). Ces 2 appelles doivent êtres passé en paramètres à (let) pour qu'elles aient accès aux variables locales.

Pour revenir à notre code, on définit 3 variables locales:

  • directory, le dossier à parcourir
  • fmt, la chaine de caractère de formatage
  • sort-recent, indique si on doit trier les documents selon leur mot clés DATE
  • documents-infos, la liste des informations de tout les documents trouvés, pour le moment vide

La variable directory reprends le paramètre de bloc :directory. Si ce paramètre n'est pas définit, on utilise le dossier courant obtenu avec (file-name-directory (buffer-file-name)).

La variable fmt reprends le paramètre de bloc :format. Si ce paramètre n'est pas définit, on utilise "%d: [[file:%p][%t]]".

La variable sort-recent reprends le paramètre de bloc :sort-recent. Si ce paramètre n'est pas définit, on utilise nil qui est retourné par (plist-get params :sort-recent).

Récupération des informations

Pour cette partie là, on utilise la fonction (mapcar) pour exécuter (my/org-index-extract-infos) sur chaque chemins trouvés par (my/org-index-list-files-path).

Le résultat est stocké dans la variable documents-infos. Si, pour le bloc dynamique, le paramètre :sort-recent a été définit à t, alors la liste des documents sera triée avec la fonction (my/org-index-sort-documents).

(if sort-recent
    (setq documents-infos (my/org-index-sort-documents
                           (mapcar
                            'my/org-index-extract-infos
                            (my/org-index-list-files-path directory))))
  (setq documents-infos (mapcar
                         'my/org-index-extract-infos
                         (my/org-index-list-files-path directory))))

Génération de la liste au format Org-mode

Cette dernière partie va utiliser la fonction (mapcar), pour traiter chaque documents présents dans la variable documents-infos:

(mapcar
 (lambda (one-document-infos)
   (insert
    "- "
    (my/org-index-format-element one-document-infos fmt)
    "\n"))
 documents-infos)

La fonction anonyme exécutée par chaque document va faire appelle à la fonction (insert) en lui passant 3 chaines de caractères:

  • Le "- ", qui indique le début d'une élément de liste Org-mode
  • La chaine de caractère formatée par l'appelle à (my/org-index-format-element)
  • Un retour à la ligne

Création automatique d'un bloc de type index

Avec le code décrit jusque là, on a tout ce qu'il faut pour avoir un bloc de type index fonctionnel. Mais on aimerait aussi pouvoir demander à Emacs de créer un bloc de type index à notre place, après avoir demandé le chemin vers le dossier à parcourir.

Pour ça, ajoutez ce code au fichier index-test.el:

(defun my/org-index-insert-dblock (directory)
  "Insert a new dynamic bloc of type index"
  (interactive
   (list (string-replace
          (file-name-directory (buffer-file-name))
          ""
          (expand-file-name (read-directory-name "Dirictory to build index on: ")))))
  (org-create-dblock
   (list :name "index"
         :directory directory)))

(org-dynamic-block-define "Index" 'my/org-index-insert-dblock)

Ici, on déclare une nouvelle fonction interactive, nommée (my/org-index-insert-dblock). Elle accepte un paramètre: directory. Ce paramètre sera répété, comme paramètre du bloc écrit par cette fonction.

On peut distinguer 2 parties dans cette fonction:

  • La partie que demande à l'utilisateur/utilisatrice quel dossier parcourir
  • La partie qui crée le bloc dynamique

Interactivité de la fonction

Pour rappelle, une fonction interactive est une fonction elisp qui peut être appelée directement par un utilisateur ou une utilisatrice.

Une fonction devient interactive si l'instruction (interactive) est indiquée juste après la documentation. Cette instruction sert également à définir comment Emacs va récupérer les paramètres de la fonction. Cette indication peut avoir 2 formes: Une chaine de caractère ou une liste. Ici, j'ai choisi la liste, où chaque élément correspond à un des paramètres.

Pour récupérer le chemin relatif vers le dossier à parcourir, j'utilise ceci:

(string-replace
 (file-name-directory (buffer-file-name))
 ""
 (expand-file-name (read-directory-name "Dirictory to build index on: ")))

J'appelle (read-directory-name) pour demander à l'utilisateur/utilisatrice quel dossier parcourir. Cette fonction retourne un chemin absolue. (expand-file-name) va ensuite remplacer le ~/ par le chemin complet vers le dossier de l'utilisateur/utilisatrice. Enfin, (string-replace) va enlever le dossier courant du chemin pour qu'il ne reste que le chemin relatif. (file-name-directory (buffer-file-name)) nous servent à obtenir le dossier courant.

Création du bloc dynamique

Pour insérer un nouveau bloc de type index, ma fonction va appeler (org-create-dblock), en indiquant le type de bloc et le paramètre :directory à utiliser.

Enregistrement de la nouvelle fonction

L'appelle à (org-dynamic-block-define) va enregistrer ma fonction de création de bloc à la liste des fonctions disponibles.

Il est maintenant possible d'appeler, depuis un document Org-mode, la fonction interactive (org-dynamic-block-insert-dblock). Quand Emacs demande quel type de bloc dynamique insérer, on peut choisir Index puis indiquer le dossier à parcourir.

Utilisation

Si on évalue tout le code qu'on a écrit dans index-test.el, nous pouvons maintenant insérer des blocs dynamiques de type index.

Pour tester, on va créer un fichier index.org à la racine de notre dossier de test.

Insérer un bloc de type index

Pour insérer un bloc de type index dans un document Org-mode, on doit:

  • Ouvrir le document
  • Placer le curseur d'écriture là où on souhaite insérer le bloc
  • Appeler org-dynamic-block-insert-dblock (raccourcis C-c C-x x)
  • Choisir Index
  • Indiquer le dossier à parcourir

Emacs va ensuite insérer un nouveau bloc:

#+BEGIN: index :directory "posts/"

#+END:

On peut également écrire sois-même le bloc, ou appeler la fonction interactive (my/org-index-insert-dblock).

Générer le contenu

Pour générer le contenu, on place le curseur d'écriture sur la ligne #+BEGIN: et on appelle le raccourcis clavier C-c C-c.

Voici le résultat de notre exemple:

#+BEGIN: index :directory "posts/"
- 2023.06.08 : Article 1
- 2023.07.04 : Article 2

#+END:

Si on ajoute le paramètre :sort-recent t au bloc et qu'on génère à nouveau le contenu:

#+BEGIN: index :directory "posts/" :sort-recent t
- 2023.07.04 : Article 2
- 2023.06.08 : Article 1

#+END:

Installation permanent du type de bloc dans Emacs

Pour pouvoir utiliser notre nouveau type de bloc, index, après un redémarrage d'Emacs, il faut copier dans ~/.emacs.d/init.el tout le code qu'on a écrit dans index-test.el.

Conclusion

On a put voir comment fonctionnait un type de bloc dynamique qui construit l'index d'un dossier. Pour des questions de simplicité, j'ai séparé son fonctionnement en plusieurs fonctions. Mais il aurait été possible de tout condenser en une seule.

Un index généré automatiquement peut servir à organiser ses notes, mais également à créer la page d'index d'un blog.

Si tu utilise la fonction de publication d'Org-mode, pour publier un site web, il est possible de demande à Org-mode de:

  • Générer automatiquement un fichier index.org
  • D'y écrire un index des autres documents Org-mode convertit
  • De convertir également index.org vers un fichier index.html

Mais avec cette solution, le fichier index.org est ré-écrit à chaque exportation. Ce qui empêche de construire un fichier index personnalisé.

Là, avec un type de bloc dynamique index, on peut créer sois-même le document index.org, le personnaliser et garder une construction automatique de la liste des articles. On peut également avoir plusieurs indexes dans le même document.

Pour plus d'information, je t'invite à lire la documentation sur les blocs l'API org-element, ainsi que la documentation des différentes fonctions qu'on a utilisé.