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 #+BIND
1 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):
#+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
:
#+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 fontspec
2. Nuestro precioso filtro quedaría entonces tal que así (y el
resultado de la exportación puede verse en la fig. 1):
#+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
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))
∞
Publicado: 14/05/21
Última actualización: 16/08/23
Esta obra está bajo una licencia de Creative Commons Reconocimiento-NoComercial 4.0 Internacional.
Notas:
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
.
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.
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)
.