Cuaderno de GNUtas

Buscar entre los ítems de listas descriptivas de Org Mode

A lo largo de mi tradución de la Odisea (work in progress) voy escribiendo y ampliando una lista descriptiva de Org Mode para ir recolectando fórmulas homéricas, repeticiones, etc. junto a las estrategias de traducción que sigo y sus diversas variantes. Me ayuda a refrescar la memoria en un camino tan vasto (sí, ya sabemos que a Homero no le harían falta estos artificios, pero sus pobres traductores estamos indefensos en ese arduo equilibrio entre memoria y olvido); no obstante, como me daba pereza tener que consultar la lista de cuando en vez, se me ocurrió escribir una funcioncilla en Elisp que me extrajera de esa lista descriptiva una lista lispiana, para poder buscar allí a partir de un pasaje dado. La función, por tanto, es de un uso muy restringido. Pero también pensé que, una vez despojada de sus contextos y contingencias, podría ser de utilidad en otros escenarios. La idea más abstracta sería entonces poder buscar entre los ítems de una o varias listas descriptivas, y sólo aquellas que estén dentro de los nodos de Org marcados con una determinada etiqueta: siempre (es importante) una lista por nodo.

Por poner un ejemplo. Imaginemos que tenemos bajo este nodo (titulado «Lista 1» y marcado con la etiqueta makelist) la siguiente lista descriptiva:

* Lista 1                                                                              :makelist:

- Peras :: contenido hablando de esta sabrosa fruta
- agua mineral :: con gas o sin gas
- velocípedos :: las bicicletas con sabor de época

Pues, si en algún momento dado, buscamos por algún término, pasaje o expresión regular, algo así como «gas», nos aparecería en el minibúfer la cadena:

agua mineral :: con gas o sin gas

El código para conseguir estos rápidos recordatorios, en fin, es muy simple, y hace uso de la función, utilísima, org-map-entries, que nos permitirá extraer el contenido como lista de aquellos nodos etiquetados como makelist. Y, para poder obtener esas listas nos viene como anillo al dedo la función org-list-to-lisp, que convierte una lista de Org, en la posición del cursor, en una lista de Elisp. La lista obtenida tendrá dos elementos: el car será el tipo de lista; el cdr, la lista de elementos propiamente dicha, donde cada elemento será otra lista compuesta únicamente por una cadena. Para verlo, nos bastaría tan sólo con evaluar la función org-list-to-lisp en cualquier punto de la lista:

(descriptive
 ("Peras :: contenido hablando de esta sabrosa fruta")
 ("agua mineral :: con gas o sin gas")
 ("velocípedos :: las bicicletas con sabor de época"))

Ya con estos mimbres, pasamos a explicar la función. En primer lugar debemos definir una variable que almacenará la lista Elisp con cada búsqueda:

(setq mi-lista-desc nil)

Y comenzamos con la función. org-map-entries ejecutará una función anónima que añadirá cada elemento de la lista Elisp extraída (del cdr, se entiende) a nuestra variable:

(defun mi-org-busca-listas-desc ()
  (interactive)
  (org-map-entries
   (lambda ()
     (save-restriction
       (org-narrow-to-subtree)
       (when
           (re-search-forward "^- " nil t)
         (beginning-of-line)
         (mapc (lambda (el)
                 (add-to-list 'mi-lista-desc el))
               (cdr (org-list-to-lisp))))))

La función actuará sólo en los nodos que tengamos etiquetados como makelist:

"makelist")

El elemento a buscar en las listas puede ser una región marcada o un prompt en el minibúfer:

(let ((element-re (if (region-active-p)
                      (buffer-substring-no-properties (region-beginning) (region-end))
                    (read-from-minibuffer "Buscar en listas descriptivas: "))))

Y ya sólo nos queda buscar esa cadena y obtener el elemento completo, convertido también en cadena como mensaje en el minibúfer:

(setq mi-element-desc
      (assoc-if
       (lambda (x)
         (string-match-p element-re x))
       mi-lista-desc))
(message
 (mapconcat 'identity
            mi-element-desc ""))))

Por tanto, nuestra función quedaría así:

(defun mi-org-busca-listas-desc ()
  (interactive)
  (org-map-entries
   (lambda ()
     (save-restriction
       (org-narrow-to-subtree)
       (when
           (re-search-forward "^- " nil t)
         (beginning-of-line)
         (mapc (lambda (el)
                 (add-to-list 'mi-lista-desc el))
               (cdr (org-list-to-lisp))))))
"makelist")
(let ((element-re (if (region-active-p)
                      (buffer-substring-no-properties (region-beginning) (region-end))
                    (read-from-minibuffer "Buscar en listas descriptivas: "))))
(setq mi-element-desc
      (assoc-if
       (lambda (x)
         (string-match-p element-re x))
       mi-lista-desc))
(message
 (mapconcat 'identity
            mi-element-desc ""))))

La función se portará mejor cuanto más definidas sean las búsquedas, y así, de hecho, la he venido usando en mi trabajo con la Odisea. Pero si buscamos por un término que aparece más de una vez en la lista, nos devolverá únicamente la primera ocurrencia. Si uno no espera repeticiones de este estilo en su lista, puede conformarse sin problemas con el código antes descrito. De lo contrario, se podría ensayar esta otra posibilidad, que nos devuelve todas las ocurrencias:

 1: (defun mi-org-busca-listas-desc ()
 2:     (interactive)
 3:     (org-map-entries
 4:      (lambda ()
 5:        (save-restriction
 6:          (org-narrow-to-subtree)
 7:          (when
 8:              (re-search-forward "^- " nil t)
 9:            (beginning-of-line)
10:            (mapc (lambda (el)
11:                    (add-to-list 'mi-lista-desc el))
12:                  (cdr (org-list-to-lisp))))))
13:      "makelist")
14:     (let ((element-re (if (region-active-p)
15:                           (buffer-substring-no-properties (region-beginning) (region-end))
16:                         (read-from-minibuffer "Buscar en listas descriptivas: "))))
17:       (setq mi-element-desc
18:             (cl-remove-if
19:              (lambda (x)
20:                (not (string-match-p ".+" x)))
21:              (mapcar (lambda (k)
22:                        (if (string-match element-re (car k))
23:                            (car k) ""))
24:                      mi-lista-desc)))
25:       (message
26:        (mapconcat 'identity
27:                   mi-element-desc "\n"))))

Es decir, en lugar de usar assoc-if, que sólo devuelve la primera ocurrencia, buscamos por expresión regular en cada uno de los elementos de la lista (con mapcar, línea 21), con lo cual generaremos una nueva lista que contendrá los positivos junto a las búsquedas fallidas como cadenas «vacías». Estas últimas, que es espacio sobrante, las eliminaremos mediante un condicional cl-remove-if (línea 18). Solución algo sucia, pero que al cabo nos saca del apuro: ahora nuestra pesquisa podrá obtener más de una ocurrencia, en caso de que las haya. Por ejemplo, si en nuestra lista descriptiva anterior repetimos la palabra «pera» en dos posiciones (una en la parte a describir y otra en medio de la descripción de otro ítem):

* Lista 1                                                                              :makelist:

- Peras :: contenido hablando de esta sabrosa fruta
- agua mineral :: con gas o sin gas (y no olvidemos las peras de agua)
- velocípedos :: las bicicletas con sabor de época

Y buscamos la cadena «peras», obtendremos entonces en nuestro minibúfer:

agua mineral :: con gas o sin gas (y no olvidemos las peras de agua)
Peras :: contenido hablando de esta sabrosa fruta

Publicado: 22/11/21

Última actualización: 07/08/22


Índice general

Acerca de...

Esta obra está bajo una licencia de Creative Commons Reconocimiento-NoComercial 4.0 Internacional.

© Juan Manuel Macías
Creado con esmero en
GNU Emacs