Cuaderno de GNUtas

Sobre archivos largos en Org, documentos maestros y subdocumentos (un apunte y un apaño)

En este nuevo flujo de trabajo a que estoy adaptando la composición tipográfica, donde la fuente la preparo en Org Mode y dejo a LATEX en segundo plano, un problema habitual suele ser el de la compilación de libros extensos: de 900 págs. en adelante. Con este tipo de documentos lo más práctico es usar el comando en línea de Org #+INCLUDE:, cuyo cometido es muy similar al de las típicas macros de LATEX \input o \include. Se parte de un documento maestro (en este caso, claro, de Org), que soporta toda la configuración general —el código Elisp, las macros, la estructura de primer nivel, etc.—, y se incluyen las partes y capítulos como archivos de Org más pequeños. Al igual que sucede con LATEX, esto nos da más control para la depuración y trabajo del texto y nos permite poder compilar/exportar sólo partes del libro a voluntad. Ahora bien, la diferencia principal de este método con el análogo latexiano es que, en el momento de la exportación, Org genera un único documento *.tex donde añade, uno tras otro, todos los subdocumentos incluidos en el documento maestro. Si además, como es mi caso, hay mucho código Elisp para evaluar en el pre- y postproceso, antes de que se nos entregue el documento *.tex, el proceso de exportación puede eternizarse cansinamente.

Lo ideal sería contar con una alternativa al método de Org, sin renunciar a él (porque también es útil para navegar cómodamente por los archivos hijos). Más exactamente: poder exportar los subdocumentos individualmente a subdocumentos *.tex y añadir a nuestro archivo maestro los correspondientes comandos \input como líneas a exportar de LATEX. Así, lo que obtenemos con la exportación del fichero maestro de Org es un fichero maestro de LATEX, pero no un documento *.tex monstruosamente grande, con todo el contenido del libro explícitamente ahí metido. Esta alternativa la podemos ir ejecutando individualmente con cada subdocumento que ya demos por definitivo. Cuando luego exportemos el documento maestro el tiempo de evaluación del código Lisp será muy poco, pues la mayor parte ya se habrá evaluado en la exportación de los archivos individuales, y estarán almacenados como documentos *.tex. Por el contrario, si aplicamos el método de Org para todo el libro, el exportador no reutilizará los subdocumentos, sino que lo vuelve a evaluar todo de nuevo para generar un nuevo e inmenso documento *.tex.

Bien, si ya tenemos la idea, podemos ensayar una función en Elisp que haga lo siguiente:

  1. Extrae la ruta y el nombre del archivo que tenemos en el comando de Org #+INCLUDE:
  2. Exporte a LATEX el contenido de dicho archivo, pero únicamente el contenido: nada de preámbulos, \begin{document} o \end{document}
  3. Guarde ese archivo exportado en la ruta indicada
  4. Añada una línea de LATEX en nuestro documento maestro, justo bajo la anterior de #+INCLUDE:, con el comando \input y la ruta antes extraída. Por supuesto, esta línea estará temporalmente comentada.

Para el paso 2 podríamos correr el exportador con las opciones visible-only y body-only. Pero hay un problema. Nos lo entenderá como un documento nuevo, no dependiente del documento maestro, y no aplicará nuestro código Elisp ni el resto de la configuración. Por otra parte, si hemos incluido el archivo con una orden del tipo :minlevel para forzar el nivel de las secciones, la ignorará y todas las secciones las entenderá como de primer nivel. No queda otra que dar un rodeo y hacer un pequeño apaño. A saber.

En primer lugar definimos una macro de sustitución que indicará unos límites dentro de los cuales se incluirá el contenido. La macro será simplemente para introducir cualquier símbolo (el que nos inventemos) comentado como para un fichero *.tex:

#+MACRO: inc @@latex:% <===>@@

Entonces, supongamos que tenemos una línea #+INCLUDE: de la siguiente forma:

#+INCLUDE: "/ruta/hacia/subdocumento.org" :minlevel 3

Lo encerraremos todo entre las dos macros que hemos definido:

{{{inc}}}

#+INCLUDE: "/ruta/hacia/subdocumento.org" :minlevel 3

{{{inc}}}

Nuestra función exportará todo el documento a LATEX, pero ya tendremos marcado desde dónde hasta dónde extraer el contenido.

Y ya por fin, ésta fue la función que escribí:

(defun include-org-include-latex ()
 "Exporta un documento de Org añadido con la macro `#+include' a
 LaTeX, pero sólo el cuerpo del documento. Lo guarda en la carpeta
 correspondiente con la expernsión `.tex' y añade una línea para
 exportar como LaTeX con el comando `\input', comentada, con la
 ruta del archivo guardado"
 (interactive)
 (let*
     ((elemento (org-element-at-point))
      (propiedad (replace-regexp-in-string "\"" "" (org-element-property :value elemento)))
      (ruta (replace-regexp-in-string "\s*:.+" "" propiedad))
      (ruta-tex (replace-regexp-in-string "\.org" ".tex" ruta))
      (buf (file-name-sans-extension (buffer-name))))
   (org-latex-export-to-latex nil nil nil t nil)
   (shell-command (concat "touch " ruta-tex))
   (save-window-excursion
     (find-file (concat buf ".tex"))
     (goto-char (point-min))
     (re-search-forward "% <===>" nil t)
     (set-mark-command nil)
     (re-search-forward "% <===>" nil t)
     (beginning-of-line)
     (narrow-to-region (region-beginning) (region-end))
     (setq contenido-contr (buffer-string))
     (widen)
     (deactivate-mark)
     (kill-buffer)
     (write-region contenido-contr nil ruta-tex))
   (unless
       (re-search-forward ruta-tex nil t)
     (forward-line 2)
     (end-of-line)
     (newline 2)
     (insert (concat
              "# @@latex:\\input{"
              (replace-regexp-in-string "\"" "" ruta-tex)
              "}@@")))))

Como se ve, para poder extraer la ruta del archivo me son fundamentales las funciones org-element-at-point, que devuelve el tipo de elemento Org sobre el que tenemos el cursor, junto con su lista de propiedades; y org-element-property, que nos extrae la propiedad que queremos, es decir, la ruta. Bueno, no exactamente la ruta, sino más bien todo lo que está después de #+INCLUDE:. Para obtener la ruta, habrá que limpiarlo un poco, de ahí lo del replace-regexp-in-string, para deshacerse de otros elementos y de las comillas.

Y poco más. Ya sólo resta probarla. Si tenemos el cursor sobre la línea #+INCLUDE: que queremos exportar, llamamos a nuestra función, se exporta el contenido del subdocumento a subdocumento LATEX y (en caso de que no esté ya añadida) escribe más abajo la orden latexiana \input. Quedando algo parecido a esto:

  {{{inc}}}

  #+INCLUDE: "/ruta/hacia/subdocumento.org" :minlevel 3

  {{{inc}}}

# @@latex:\input{/ruta/hacia/subdocumento.tex}@@

Recuérdese que la línea está comentada, pues de momento no la necesitamos. Llegado el caso, comentamos el #+INCLUDE: de Org y descomentamos el \input de LATEX.

Addenda

  1. Hay otra posibilidad, que aún no he tenido ocasión de poner en práctica, y que es la de usar org-publish para producir, por un lado todos los documetos individuales (con la opción body-only), y por otro el documeto maestro. Puedo dar fe de que org-publish es una verdadera maravilla para generar y publicar páginas estáticas en HTML (de hecho, es lo que uso para este blog y mis otros sitios en la red), por lo que suele asociarse más con esa actividad. Pero la salida a PDF (incluso a más de un formato a la vez) es una opción interesante, golosa y poco conocida. Será cosa de echarle un ojo algún día. Daremos cuenta.
  2. La función anterior podemos mejorarla un poco, si añadimos a la línea del \input de LATEX la fecha y la hora de la última exportación. Quedaría el final, por tanto, algo como esto:

    ;; ...
    (save-excursion
      (unless
          (re-search-forward ruta-tex nil t)
        (forward-line 2)
        (end-of-line)
        (newline 2)
        (insert (concat
                 "# @@latex:\\input{"
                 (replace-regexp-in-string "\"" "" ruta-tex)
                 "}@@\n# Última exp.: ")))
      (re-search-forward "Última exp.: " nil t)
      (kill-line)
      (insert
       (format-time-string "%d/%m/%y - %H:%M")))
    ;; ...
    

Publicado: 15/06/20

Última actualización: 21/01/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