Escribir un filtro Lua para Pandoc: añadir marcas personalizadas a los títulos convertidos desde Word
Cuando un documento de Word se convierte en Markdown, los estilos se trasladan como estructura: los párrafos con Título 1 se convierten en encabezados de primer nivel, los de Título 2 en segundo nivel, y así sucesivamente. A veces eso no basta. Quizá quieras que cada título importado de Word lleve una marca que tu plantilla de sitio o tu pipeline de compilación pueda reconocer, y editar a mano cientos de títulos no es un plan.
Pandoc tiene una respuesta integrada: los filtros Lua, pequeños scripts que transforman el documento mientras pasa por el conversor. Esta guía explica cómo funcionan, construye uno paso a paso exactamente para esta tarea de marcado y cubre cómo ejecutarlo y depurarlo.
Qué es un filtro Lua y por qué Pandoc eligió Lua
Pandoc analiza cada entrada en un árbol de sintaxis abstracta común, una representación interna de elementos como encabezados, párrafos y tablas, antes de escribir cualquier salida. Un filtro es código que recorre ese árbol y lo reescribe. Como el filtro opera sobre el árbol normalizado, funciona igual tanto si la entrada viene de docx, HTML o Markdown.
Pandoc incorpora un intérprete de Lua, actualmente Lua 5.4, así que los filtros se ejecutan dentro del proceso de Pandoc sin un runtime aparte ni extensiones compiladas. Escribes un archivo de texto, se lo pasas a Pandoc con una opción de línea de comandos y la transformación ocurre a mitad de la conversión. Los filtros escritos así también evitan el viaje de ida y vuelta por JSON que pagan los filtros externos, lo que mantiene la sobrecarga pequeña.
La anatomía de un filtro
Un archivo de filtro define funciones simples con el nombre de los tipos de elemento del AST. Una función llamada Header se ejecuta una vez por encabezado del documento; una llamada Para, una vez por párrafo. Si el archivo no devuelve una tabla de filtro explícita, Pandoc recoge esas funciones de nivel superior por su nombre y las usa automáticamente.
El valor de retorno decide qué reemplaza al elemento en el árbol. Devolver nil, lo que incluye olvidarse de devolver cualquier cosa, deja el elemento sin cambios, así que una función olvidadiza es inofensiva. Devolver el elemento, modificado o no, lo reemplaza por la nueva versión. Devolver una lista inserta esos elementos en lugar del original, y devolver una lista vacía es la forma de eliminar un elemento del documento.
El elemento Header y sus atributos
Cada Header en el AST de Pandoc lleva tres cosas: un nivel como número, una lista de contenido en línea y un conjunto de atributos. El conjunto de atributos tiene tres partes: un identificador usado para anclas, una lista de nombres de clase y atributos arbitrarios de clave-valor. Es el mismo trío que aparece en la salida como id y class en la etiqueta HTML de un encabezado.
Dónde acaban esos atributos depende del formato de salida. El Markdown de Pandoc puede representarlos como anotaciones entre llaves tras el texto del título, y la salida HTML los coloca en la propia etiqueta. Una exportación GFM pura no tiene sintaxis para atributos, así que las clases se perderían ahí; conviene saberlo antes de elegir un formato de destino.
La tarea: marcar cada título de Word
El objetivo es un filtro que inspecciona cada Header y añade una clase personalizada cuando cumple los criterios, por ejemplo en todos los encabezados o solo en los de segundo nivel. La lógica lee el campo level, decide y añade una cadena a la lista classes, ya sea con el método insert de la lista o con un simple table.insert. Nada más cambia.
Los condicionales lo hacen útil en la práctica. Puedes dar a los encabezados de primer nivel una clase y a los más profundos otra, limitar el marcado a encabezados cuyo texto empiece por una palabra dada, o saltarte los que ya llevan la clase para que el filtro siga siendo idempotente y pueda ejecutarse dos veces sin duplicar marcas.
Recorriendo la lógica del código
Escrito, el filtro son cuatro pasos cortos. Define una función con el nombre del tipo Header para que Pandoc le envíe cada encabezado. Dentro, compara el campo level con el umbral que te interesa. Si coincide, añade el nombre de la clase a la lista classes y, opcionalmente, establece un atributo clave-valor. Devuelve el elemento al final.
Ese return final merece atención: modificar el elemento in situ y devolverlo es el patrón documentado, pero un return ausente significa que Pandoc conserva el original sin modificar, así que el cambio se calcula y luego se descarta en silencio. Con el return en su sitio, los títulos salen con la nueva clase, visible en las etiquetas HTML o en los atributos entre llaves del Markdown de Pandoc.
Ejecutar el filtro
En la línea de comandos, Pandoc acepta una opción --lua-filter que apunta a tu archivo, y la opción puede repetirse para encadenar varios filtros en orden. Los filtros se ejecutan entre la lectura y la escritura, así que la transformación se aplica elijas el formato de salida que elijas.
En este sitio, las opciones Pro incluyen la subida de un filtro Lua: entrega el mismo archivo .lua junto a tu documento de Word y el worker del navegador lo pasa al mismo motor de Pandoc. Las clases llegan a la salida exactamente como lo harían en local, sin que nada se envíe a un servidor.
Técnicas de depuración
La herramienta más rápida es la función print: lo que imprime un filtro va a stderr, así que nunca contamina el documento convertido, y puedes volcar el nivel, las clases o el texto de un encabezado para ver qué recibió realmente el filtro. Pandoc también incluye un módulo de registro, pandoc.log, cuyas funciones info y warning etiquetan los mensajes con su gravedad.
Un buen hábito es desarrollar con un archivo diminuto. Crea un docx con un encabezado de cada nivel, ejecuta el filtro sobre él e inspecciona la salida antes de apuntarlo a archivos reales. La mayoría de los fallos son erratas en los nombres de campo, y el canal de errores los revela al instante.
Extensiones y rendimiento
El mismo patrón va mucho más allá de los encabezados: eliminar párrafos que no contienen texto, generar identificadores a partir del texto del título para tener anclas estables, o envolver tablas en un Div para que una plantilla del sitio las estilice como grupo. Todo eso es una función por tipo de elemento en el mismo archivo.
El rendimiento rara vez es un problema. Los filtros se ejecutan por elemento dentro del proceso de conversión, y el recorrido es rápido incluso en documentos muy grandes. Lo único que hay que evitar es el trabajo pesado repetido por elemento, como leer un archivo dentro de la función, que pertenece a la parte superior del script, donde se ejecuta una sola vez.