Crear un árbol de Org (automáticamente) con entradas de Elfeed
Ahora que los blogs y demás publicaciones periódicas de Internet que pretenden juntar más
de un párrafo de lectura están de capa caída, en detrimento de las redes ego-sociales, es
cuando apetece más seguirlos. Para hacerlo desde GNU Emacs de una forma placentera
contamos con la excelente biblioteca elispiana y agregador de feeds Elfeed. Y entre los
feeds que tengo incluidos para consultar sus medios desde allí no sólo están los (pocos,
bien es cierto) blogs que sigo, sino algunos feeds de noticias más o menos interesantes o
intensos, como el que va dando cuenta de los nuevos paquetes añadidos al ecosistema TeX
del CTAN, que se halla en un estado de perpetuo crecimiento. Pero como esta consulta la
hago muy a menudo, se me ocurrió escribir algo de Elisp para poder generar automáticamente
una entrada en un documento de notas y apuntes varios de Org Mode que me recogiera los
últimos paquetes subidos en los últimos seis meses y que se actualizase a un golpe M-x
.
Código que detallaremos a continuación, por si fuese de utilidad a alguien. Verán que no
es difícil adaptarlo a cualquier otro tipo de feed periódico.
Figura 1: Lista de los últimos paquetes actualizados en CTAN mostrada por Elfeed
Comenzamos por definir esta esta primera función almacena en una lista cada línea de un texto marcado como elemento.
(defun lineas-a-lista () (if (region-active-p) (progn (narrow-to-region (region-beginning) (region-end)) (split-string (buffer-string) "\n" nil)) (error "ninguna región marcada")))
La siguiente función intercala cada línea de entradas de la página de resultados de Elfeed
con el contenido de cada entrada. Para ello nos resultará útil la expresión -interleave
del paquete dash
(aprovechando también una idea de la que ya hablaba aquí a cuento de
una función para intercalar líneas entre dos textos):
(defun intercala-elfeed () (setq lineas-b (lineas-a-lista)) (setq lineas-c (-interleave lineas-b entradas-elfeed)) (replace-regexp ".+" "" nil (region-beginning) (region-end)) (deactivate-mark) (insert (mapconcat 'identity lineas-c "\n")))
Y esta otra función que recoge una lista con el contenido de cada entrada de Elfeed. Usa un simple bucle que busca por líneas que comienzan por dígitos, ya que la página de resultados de Elfeed siempre muestra una fecha al comienzo de cada línea (véase fig. 1).
(defun mi-elfeed-almacena-entradas () "almacena los contenidos de cada entrada marcada en la lista de Elfeed" (setq entradas-elfeed nil) (save-excursion (goto-char (point-min)) (save-window-excursion (while (re-search-forward "\\(^[[:digit:]]+-\\)" nil t) (call-interactively 'elfeed-search-show-entry) (add-to-list 'entradas-elfeed (buffer-string) (switch-to-buffer "*elfeed-search*"))))))
Y ya, por último, la función que hace el resto del trabajo. Consistirá en lo siguiente:
insertar, en la posición del cursor un encabezado de primer nivel que contenga todas las
entradas que sigan el criterio de búsqueda de Elfeed (ver línea 12 la expresión
elfeed-search-set-filter
) como subárboles. Y cada vez que evaluemos la función a la
altura del primer nivel de ese árbol se actualizará todo. El resultado lo podemos ver en
la fig. 2.
(defun inserta-ctan-nuevo-elfeed () (interactive) (when (save-excursion (beginning-of-line) (looking-at-p "\\*+\s+Paquetes nuevos en CTAN")) (save-restriction (org-narrow-to-subtree) (delete-region (point-min) (point-max)))) (save-window-excursion (call-interactively 'elfeed) (elfeed-search-set-filter "@6-months-ago +TeX New on CTAN ") (setq entradas-marcadas (replace-regexp-in-string "\\([[:graph:]]+\\)\s\s+.+$" "\\1" (buffer-string))) (mi-elfeed-almacena-entradas) (kill-buffer)) (let* ((entradas (with-temp-buffer (insert entradas-marcadas) ;; un carácter `temporal' para evitar falsos positivos (reemplaza "\\(^[[:digit:]]+-\\)" "¡>¡\\1") (mark-whole-buffer) (intercala-elfeed) (deactivate-mark) (whitespace-cleanup) (buffer-string))) (final (with-temp-buffer (org-mode) (org-insert-heading) (insert (concat "Paquetes nuevos en CTAN (últimos 6 meses)" " -- " (format-time-string "%d/%m/%y"))) (save-restriction (org-narrow-to-subtree) (goto-char (point-max)) (insert entradas) (save-excursion (goto-char (point-min)) (while (re-search-forward "\\(^¡>¡\\)" nil t) (replace-match "" t nil) (beginning-of-line) (org-insert-heading)) (goto-char (point-min)) (forward-line) (while (re-search-forward "^* " nil t) (org-do-demote)))) (buffer-string)))) (insert (format "%s" final))))
Figura 2: Nuestra lista de entradas de Elfeed como un árbol de Org Mode
Estrambote: una versión para publicar en un blog la lista de entradas actualizada
Realmente esta función anterior es la segunda versión de otra anterior que escribí con el objeto de mantener (con actualización periódica y automática) una lista de paquetes nuevos de CTAN en mi blog tipográfico, hermano de éste, La lunotipia. A diferencia de la otra, lo que viene a hacer esta función es devolver el resultado como una cadena de texto:
(defun ctan-nuevo-elfeed () (interactive) (save-window-excursion (call-interactively 'elfeed) (elfeed-search-set-filter "@6-months-ago +TeX New on CTAN ") (setq entradas-marcadas (replace-regexp-in-string "\\([[:graph:]]+\\)\s\s+.+$" "\\1" (buffer-string))) (mi-elfeed-almacena-entradas) (kill-buffer)) (let* ((entradas (with-temp-buffer (insert entradas-marcadas) ;; un carácter temporal para evitar falsos positivos (reemplaza "\\(^[[:digit:]]+-\\)" "¡>¡\\1") (mark-whole-buffer) (intercala-elfeed) (deactivate-mark) (whitespace-cleanup) (buffer-string))) (final (with-temp-buffer (org-mode) (org-insert-heading) (insert (concat "Paquetes nuevos en CTAN (últimos 6 meses)" " -- " (format-time-string "%d/%m/%y"))) (save-restriction (org-narrow-to-subtree) (goto-char (point-max)) (insert entradas) (save-excursion (goto-char (point-min)) (while (re-search-forward "\\(^¡>¡\\)" nil t) (replace-match "" t nil) (beginning-of-line) (org-insert-heading)) (goto-char (point-min)) (forward-line) (while (re-search-forward "^* " nil t) (org-do-demote)))) (buffer-string)))) (format "%s" final)))
Y en La Lunotipia sólo tengo que añadir este bloque de código que se evalúa durante la exportación y exporta los resultados en bruto (resultado de la exportación en la fig. 3):
#+begin_src emacs-lisp :exports results :results raw (ctan-nuevo-elfeed) #+end_src
Figura 3: Lista de paquetes nuevos en CTAN en mi blog La Lunotipia
Actualización de 04/06/21
Se me ocurrió esta otra posibilidad bastante más elegante, que hace uso de
org-map-entries
. La ventaja evidente es que podemos crear varios árboles con distíntos
parámentros de Elfeed, cada uno añadido como valor de una propiedad, llamada :ELFEED:
.
En la siguiente función org-map-entries
sólo escaneará en el documento actual, y
únicamente en aquellas entradas que tengan dicha propiedad.
Definimos antes esta función, que es una versión de la primera, pero con algunas mejoras:
(defun mi-org/prepara-elfeed () (interactive) (let* ((element (org-element-at-point)) (prop (org-element-property :ELFEED element)) (level (org-element-property :level element))) (save-restriction (org-narrow-to-subtree) ;; este condicional es necesario para vaciar el contenido del ;; sub-árbol, y añadir una nueva actualización (when (re-search-forward "\\*+\s+Entradas" nil t) (save-restriction (org-narrow-to-subtree) (delete-region (point-min) (point-max))))) (save-restriction (org-narrow-to-subtree) (goto-char (point-max)) (save-window-excursion (elfeed-update) (call-interactively 'elfeed) (elfeed-search-set-filter prop) (setq entradas-marcadas (replace-regexp-in-string "\\([[:graph:]]+\\)\s\s+.+$" "\\1" (buffer-string))) (mi-elfeed-almacena-entradas) (kill-buffer)) (let* ((entradas (with-temp-buffer (insert entradas-marcadas) ;; un carácter temporal para evitar falsos positivos (reemplaza "\\(^[[:digit:]]+-\\)" "¡>¡\\1") (mark-whole-buffer) (intercala-elfeed) (deactivate-mark) ;; añadimos un carácter de espacio cero a ;; aquellas líneas que comienzan por un ;; asterisco, para que no se traten luego como ;; encabezados de Org (reemplaza "\\(^\\*\\)" "\u200B\\1") (whitespace-cleanup) (buffer-string))) (entradas-final (with-temp-buffer (org-mode) (org-insert-heading) (insert (concat "Entradas " " -- " "última actualización: " (format-time-string "%d/%m/%y"))) (save-restriction (org-narrow-to-subtree) (goto-char (point-max)) (insert entradas) (save-excursion (goto-char (point-min)) (while (re-search-forward "\\(^¡>¡\\)" nil t) (replace-match "" t nil) (beginning-of-line) (org-insert-heading)) (goto-char (point-min)) (forward-line) (while (re-search-forward "^\\* " nil t) (replace-match "** ")))) (buffer-substring (point-min) (point-max))))) (org-paste-subtree (1+ level) entradas-final)))))
Y, finalmente, la función con org-map-entries
:
(defun mi-org/insertar-entradas-elfeed () (interactive) (org-map-entries #'mi-org/prepara-elfeed "+ELFEED={.+}" 'file nil))
Y para expandir la lista de entradas en mi blog automáticamente, me basta con tener esta entrada así:
* Lista de paquetes nuevos en CTAN (últimos 6 meses) :PROPERTIES: :ELFEED: @6-months-ago +TeX New on CTAN :END: #+begin_src emacs-lisp :exports results :results silent (save-restriction (org-narrow-to-subtree) (goto-char (point-min)) (mi-org/insertar-entradas-elfeed)) #+end_src
∞
Publicado: 10/05/21
Última actualización: 16/08/23
Esta obra está bajo una licencia de Creative Commons Reconocimiento-NoComercial 4.0 Internacional.