Cuaderno de GNUtas

Mostrar numeración de versos en un bloque VERSE de Org Mode

Gnu Emacs (como cualquier otro editor de texto que se precie, por otra parte) puede mostrar u ocultar los números de línea lógicos del documento. Esto tiene su utilidad para programación, y yo suelo usar, si ando en esos menesteres, el display-line-numbers-mode, cuya activación o desactivación tengo anclada a una secuencia de teclas. Hace bien su trabajo y no le pido más. Por supuesto, hay muchos modos más para numeración de líneas, incluso numeración relativa, pero ninguno (ni siquiera display-line-numbers) me resulta satisfactorio si de lo que se trata es de numerar los versos de un poema. Y ante esta carencia, me dediqué hace no mucho a escribir esta función, con la idea de que realice lo siguiente:

  1. Si estamos en un bloque verse de Org Mode (el lugar más idóneo para poner un poema en Emacs) y llamamos a la función, los versos se numerarán en el margen (mediante overlays temporales, que es el mismo procedimiento que emplean los otros modos para numerar las líneas) en secuencia de cinco, pero sin contar el primer verso. Y
  2. Es importante que en el cómputo de versos no se tenga en cuenta las líneas blancas que separan las estrofas. Ahí es una de las cosas en que la numeración «tradicional» de líneas en Emacs me falla, ya que numera todas las líneas reales, ya sean blancas o con contenido.

Como se ve, es la numeración clásica de los versos, algo que en LaTeX se puede conseguir fácilmente. De hecho, en esta GNUta explico cómo exporto versos desde Org a LaTeX y que queden en su destino, automáticamente, numerados. Pero mientras trabajo en Emacs, en el paraíso del texto plano, también me resulta práctico de cuando en vez contar con ese tipo de numeración, especialmente en mi traducción de la Odisea, work in progress. Echemos un vistazo rápido a cómo quedó, finalmente, mi código.

Función para mostrar u ocultar la numeración de versos

Primero, necesité definir antes esta pequeña función que avanza cuatro líneas pero ignorando las blancas. Como no me servía el comando forward-line (que no ignora las bancas), me inventé uno nuevo ad hoc, que repite cuatro veces una búsqueda con expresión regular:

(defun cuatro-versos-no-vacia ()
    (dotimes (numero 4)
      (re-search-forward "^.+" nil t)))

Nuestra función para numerar los versos, que expongo a continuación, tiene un esquema muy simple: se define un contador como una variable local, se avanza líneas y cada avance se añade un overlay en el margen:

(defun numera-versos ()
  (interactive)
  (setq left-margin-width 4)
  (set-window-buffer (selected-window) (current-buffer))
  (save-excursion
    (save-restriction
      (org-narrow-to-block)
      (goto-char (point-min))
      (end-of-line)
      (let
          ((numverso 0))
        (save-excursion
          (while
              (re-search-forward "^.+" nil t)
            (cuatro-versos-no-vacia)
            (let
                ((ov (make-overlay (point) (point))))
              (overlay-put ov 'overlay-num-verso t)
               ;; necesario para evitar el overlay en la línea que
               ;; cierra el bloque
              (unless (looking-back "#\\+end_verse\\|#\\+END_VERSE")
                (overlay-put ov 'before-string
                             (propertize " "
                                         'display
                                         `((margin left-margin)
                                           ,(propertize
                                             (format "%s" (number-to-string (setf numverso (+ numverso 5))))
                                             'face 'linum))))))
            (forward-line 1)))))))

Ya sólo nos quedaría definir esta otra función que oculte los números de verso y restituya los márgenes normales:

(defun elimina-num-versos ()
    (interactive)
    (remove-overlays nil nil 'overlay-num-verso t)
    (setq left-margin-width 0)
    (set-window-buffer (selected-window) (current-buffer)))

Y podemos ya anclar las funciones a un atajo de teclado (por ejemplo, M-j o el que se nos antoje). Si, además, queremos guardar la cortesía con nosotros mismos, y que se nos recuerde que este código sólo tiene sentido usarlo en un bloque verse, podemos incluir un condicional y un mensaje de error:

(global-set-key (kbd "M-j")
                  '(lambda ()
                           (interactive)
                           (save-excursion
                             (save-restriction
                               (org-narrow-to-element)
                               (goto-char (point-min))
                               (if (not (looking-at-p "#\\+begin_verse"))
                                   (error "No es un bloque VERSE..."))
                               (numera-versos))))

num-versos-org.gif

¿Y si queremos saltar a un verso concreto?

Pues, como bonus track, y ya venido arriba, también se me ocurrió esta otra función, muy útil, que nos preguntará en el minibúfer a qué número queremos saltar:

(defun localiza-verso ()
    (interactive)
    (save-restriction
      (org-narrow-to-block)
      (let
          ((numero (read-number "ir a verso...")))
        (goto-char (point-min))
        (end-of-line)
        (dotimes (x numero)
          (re-search-forward "^." nil t)))))

Actualización de 25/08/20: un modo menor

Aprovechando las funciones anteriores, podemos definir un modo menor para que los versos se vayan numerando a medida que escribimos. Primero, definimos esta función, que elimina la numeración y la vuelve a mostrar para tenerla actualizada:

(defun inserta-numero-versos-org ()
  (save-excursion
    (save-restriction
      (org-narrow-to-element)
      (goto-char (point-min))
      (if (not (looking-at-p "\s*#\\+begin_verse\\|\s*#\\+BEGIN_VERSE"))
          (error "No es un bloque VERSE...")
        (elimina-num-versos)
        (numera-versos)))))

Y la idea es que con nuestro modo menor la función anterior se evalúe cada vez que introducimos una pulsación de teclado:

(define-minor-mode num-versos-mode
  "Inserta y actualiza números de verso de cinco en cinco en un
bloque `verse' de Org, sin contar las líneas blancas"
  :init-value nil
  :lighter ("versos")
  (if num-versos-mode
      (add-hook 'post-self-insert-hook #'inserta-numero-versos-org nil 'local)
    (remove-hook 'post-self-insert-hook #'inserta-numero-versos-org 'local)
    (elimina-num-versos)))

Por último, para muestra un botón, con un soneto de Sor Juana:

Publicado: 22/10/2019

Ú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