Cuaderno de GNUtas

Trasteando con la API de org-element

La API de la biblioteca de org-element ofrece un muy completo juego de herramientas para poder manipular a conveniencia los distintos elementos de Org Mode. De entre todos esos recursos vamos a centrarnos aquí en la polivalente función org-element-map, que sirve tanto para un roto como para un descosido. Lo que viene a hacer esta expresión, dicho con algo de trazo grueso, es aplicar de forma iterativa una función (generalmente una función anónima) a un elemento de Org dado. El argumento obligatorio que deberá tomar la función aplicada será siempre ese elemento de Org. Y el campo de acción, una estructura de datos, como la que se obtiene mediante la función org-element-parse-buffer. Por ejemplo, si evaluamos lo siguiente:

(org-element-map (org-element-parse-buffer) 'keyword 'identity)

nos devolverá una lista en bruto de las directivas de Org que tenemos en el búfer, o sea, de las típicas palabras clave que comienzan por "#+...".

Otra posibilidad muy golosa es aplicar esta función en un filtro personalizado de exportación y añadirlo a la lista org-export-filter-parse-tree-functions, bien de manera local mediante una directiva #+BIND1 o de forma global en nuestro ~/.emacs. Un filtro de este tipo habrá de tener tres argumentos: la estructura de datos a manipular, el backend o salida de exportación (si es a LATEX, a HTML, a odt, etc.) y el canal de comunicación, que es una lista de propiedades. Empecemos por un ejemplo muy básico, que no resultará demasiado útil pero que nos ayudará a hacernos una idea de cómo funciona el asunto. Imaginemos que en nuestro documento de Org tenemos una directiva personalizada (por ejemplo #+directiva:) que tiene un valor determinado y queremos cambiarlo en todos los casos. Nuestro filtro quedaría entonces así (dirigiendo el condicional a la exportación a HTML):

#+BIND: org-export-filter-parse-tree-functions (filtro-ejemplo-uno)

#+begin_src emacs-lisp :exports results :results none
  (defun filtro-ejemplo-uno (datos backend info)
    (when (org-export-derived-backend-p backend 'html)
      (org-element-map datos 'keyword
        (lambda (k)
          (when
              (org-element-put-property k :value "un valor cualquiera")))
        info)
      datos))
#+end_src

Partiendo de esta base podemos embarcarnos en cosas más prácticas. Por ejemplo, operar una simple sustitución de cadenas dentro de los sub-árboles, pero sólo en aquellos que tengas la etiqueta :sust:

#+BIND: org-export-filter-parse-tree-functions (filtro-ejemplo-dos)

#+begin_src emacs-lisp :exports results :results none
(defun filtro-ejemplo-dos (datos backend info)
    (when (org-export-derived-backend-p backend 'latex)
      (org-element-map datos 'headline
        (lambda (hl)
          (when (member "sust" (org-element-property :tags hl))
            (let* ((contenido (org-element-interpret-data (org-element-contents hl)))
                   (contennido-bis (replace-regexp-in-string "lorem" "ipsum" contents)))
              (org-element-set-contents hl (with-temp-buffer
                                             (insert contenido-bis)
                                             (org-element-parse-buffer))))))
        info)
      datos))
#+end_src

Un par de cosas que conviene explicar de esta función. La variable local contenido recoge el contenido completo del encabezado (es decir, el argumento de la función lambda: hl) pero rehecho con sintaxis de Org (a través de org-element-interpret-data). Luego, mediante org-element-set-contents aplicamos al encabezado un nuevo contenido (contenido-bis, que ya incluye la sustitución de cadenas), pero de nuevo «parseado» a través de org-element-parse-buffer.

Continuando en esta veta, marchemos hacia un tercer y último ejemplo, algo más ambicioso. Ahora lo que pretendemos es que aquellos encabezados que incluyan la propiedad font con un valor que ha de ser por fuerza el nombre de una familia de fuentes seguido de (opcionalmente) una serie de propiedades OpenType, todo con la sintaxis del paquete de LATEX fontspec2. Nuestro precioso filtro quedaría entonces tal que así (y el resultado de la exportación puede verse en la fig. 1):

#+BIND: org-export-filter-parse-tree-functions (filtro-ejemplo-tres)

#+begin_src emacs-lisp :exports results :results none
(defun filtro-ejemplo-tres (datos backend info)
  (when (org-export-derived-backend-p backend 'latex)
    (org-element-map datos 'headline
      (lambda (hl)
        (when (org-element-property :FONT hl)
          (let* ((font (org-element-property :FONT hl))
                 (contenido (org-element-interpret-data (org-element-contents hl)))
                 (contenido-bis (concat
                                "@@latex:{\\fontspec{@@"
                                (replace-regexp-in-string "\s*\\(\\[.+\\]\\)\s*" "" font)
                                "@@latex:}%@@\n"
                                (if (string-match "\\(\\[.+\\]\\)" font)
                                    (concat "@@latex:" (match-string 1 font) "%@@\n\n")
                                  "\n")
                                contenido
                                "\n@@latex:}@@")))
            (org-element-set-contents hl (with-temp-buffer
                                           (insert contenido-bis)
                                           (org-element-parse-buffer))))))
      info)
    datos))
#+end_src

fuentes-org-element.png

Figura 1: Un ejemplo exportando a LATEX con org-element-map (click en la imagen para ampliar)

Actualización de 22/05/21

Después de unas cuantas pruebas con la función anterior, encontré un problema importante en que no había reparado antes: el filtro funciona bien cuando los encabezados son todos del mismo nivel. Con secciones de niveles distintos, es decir, con subárboles y subsubárboles, mi código no entregaba el resultado esperado. Nicolás Goaziou me hizo una serie de sugerencias en la lista de correo de Org Mode, y a partir de ellas pude ensayar la segunda versión de la función, que copio más abajo. Y, ya que el uso de dos propiedades me resultaba también un tanto antieconómico y redundante, aproveché también para simplificarlo todo en una sola propiedad, llamada :fontspec:, de la cual se extrae tanto el comando \addfontfeature como la orden \fontspec, mediante un simple juego de marcas textuales. A saber:

Propiedad / valor resultado en LATEX
:fontspec: font ! (opcional: features) \fontspec{font}[features]
:fontspec: > (add) features \addfontfeature{features}

En resumen, la nueva función quedaría como sigue3 (resultado de la exportación en la fig. 2, click en la imagen para ampliar):

(defun my-custom-filters/fontspec-headline-mod (tree backend info)
  (when (org-export-derived-backend-p backend 'latex)
    (org-element-map tree 'headline
      (lambda (headline)
        (when (org-export-get-node-property :FONTSPEC headline t)
          (let* ((value (org-export-get-node-property :FONTSPEC headline t))
                 (font (when (string-match "\\([^!]+\\)\s+!\s*\\(.*\\)\s*" value)
                         (concat "{\\fontspec{"
                                 (match-string 1 value)
                                 "}["
                                 (if (string-match-p "!\s+[[:graph:]]+" value)
                                     (match-string 2 value)
                                   "")
                                 "]\n")))
                 (fontfeature (when (string-match ">\s+\\(.+\\)\s*" value)
                                (concat "{\\addfontfeature{" (match-string 1 value) "}\n")))
                 (fontspec (cond (font font)
                                 (fontfeature fontfeature)))
                 ;; sugerencia de Nicolas Goaziou:
                 (create-export-snippet
                  (lambda (v)
                    (org-element-create 'export-snippet (list :back-end "latex" :value v)))))
            (apply #'org-element-set-contents
                   headline
                   (append (list (funcall create-export-snippet fontspec))
                           (org-element-contents headline)
                           (list (funcall create-export-snippet "}\n")))))))
      info)
    tree))

fuentes-org-element2.png

Figura 2: Resultado de la exportación/compilación con la nueva versión de la función, que ya entrega el resultado esperado con encabezados de distinto nivel

Publicado: 14/05/21

Última actualización: 16/08/23


Índice general

Acerca de...

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

Notas:

1

Recuérdese que para que la directiva #+BIND: sea leída en la exportación necesitamos configurar la variable org-export-allow-bind-keywords como non-nil.

2

Véase https://www.ctan.org/pkg/fontspec. Por cierto, para insertar cómodamente los nombres de fuentes instaladas en nuestro sistema a partir de una lista autocompletiva, tengo escrita esta extensión para Helm.

3

Por cierto, Nicolas Goaziou comentó también que la nueva rama experimental org-cite-wip incluye una función específica para evitar (funcall create-export-snippet fontspec). Podremos añadir en su lugar (org-export-raw-string fontspec).

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