report-to-pdf — De informe Markdown a PDF profesional
Renderizador determinista de Markdown a PDF, controlado por script. Analiza un subconjunto restringido de Markdown de "informe" con un pequeño analizador de bloques hecho a mano (sin biblioteca Markdown externa) y lo maqueta a través de fpdf2 con las mismas convenciones visuales que el generador build-user-guide.py del módulo ColeteOnline Magento: portada, encabezado/pie de página, TDC, barras de acento para encabezados de sección, bloques de código sombreados, tablas con bordes y rayas. La lógica de diseño nunca cambia entre ejecuciones con la misma entrada: mismo informe de entrada, mismo PDF de salida, siempre, sin LLM en el bucle de renderizado.
Cuándo usarlo
- "Convierte esta revisión en Markdown a un PDF para el mentor."
- "Exporta mi informe a un PDF profesional."
- "Quiero que cada vez se vea igual, sin que un LLM lo reformatee cada ejecución."
- Cualquier informe/escrito en
.mdque tenga encabezados, listas, bloques de código o tablas y necesite una versión PDF de calidad para cliente/mentor/marca.
Cómo ejecutar
python3 <directorio-del-skill>/scripts/build_report_pdf.py <entrada.md> [opciones]
(<directorio-del-skill> es el directorio base de este skill, la carpeta que contiene este SKILL.md).
Opciones:
-o / --output <ruta>— ruta del PDF de salida (por defecto:<entrada>.pdfjunto al archivo fuente).--logo <ruta>— imagen de logo opcional (PNG/WebP/JPEG, cualquier formato que pueda abrir Pillow) en la portada. Repite la bandera para varios logos, dibujados lado a lado en una fila (también configurable mediante frontmatterlogo: a.png, b.png, rutas relativas al archivo.md).--title,--subtitle,--version,--vendor,--author— sobrescribe los valores correspondientes del frontmatter (ver más abajo); las banderas de CLI tienen prioridad.--cover center|left— diseño de la portada. Todo el bloque de título (logo, título, subtítulo, autor, versión) se centra verticalmente en ambos modos;center(por defecto) también lo centra horizontalmente,leftlo alinea a la izquierda. También configurable mediante frontmattercover:.--no-header— omite el encabezado de página y el espacio que reserva, de modo que el contenido de cada página comience en el mismo margen superior (también configurable mediante frontmatterheader: false).--accent "R,G,B"— sobrescribe el color de acento usado para la barra superior de la portada y las barras de acento de los encabezados de sección (por defecto220,50,50, rojo ColeteOnline).--dark— renderiza con el tema oscuro en lugar del tema claro predeterminado (también configurable mediante frontmattertheme: dark;--darktiene prioridad si ambos están presentes). Ver "Tema oscuro" más abajo.
El script instala automáticamente fpdf2, Pillow y Pygments mediante pip install --user si falla alguna importación, reflejando la autoinstalación de Pillow del script de referencia. Los tres fueron verificados como importables y el script se ejecutó de principio a fin durante el desarrollo (ver Pruebas más abajo).
Convenciones de Markdown que espera
Bloque de frontmatter opcional al principio del archivo:
---
title: Título de mi informe
subtitle: Un subtítulo de una línea
version: 1.0
vendor: ColeteOnline SRL
accent: 220,50,50
---
Todas las claves son opcionales y tienen valores predeterminados sensatos:
title— se usa el primer encabezado# H1encontrado en el cuerpo, luego el nombre del archivo de entrada.subtitle— se omite (no se muestra en la portada).version— se usa la fecha de hoy (AAAA-MM-DD); estableceversion: nonepara ocultar la línea de versión.vendor— se omite (la línea de copyright se omite en el pie de página de la portada si no se establece).author— nombre(s) del autor(es), se muestra en la portada debajo del subtítulo (ej.author: Jane Doe, John Smith). Se omite si no se establece.cover—center(por defecto) oleft; ver--coverarriba.header—falseomite el encabezado de página; ver--no-header.justify—truejustifica el texto del cuerpo, los encabezados y las celdas de las tablas; el valor predeterminado es alineado a la izquierda (el texto justificado estira los espacios entre palabras en líneas envueltas cortas, ej. un encabezado de dos líneas).accent— se usa el rojo de marca ColeteOnline220,50,50.
Cuerpo Markdown y cómo se renderiza cada elemento:
| Markdown | Renderizado como |
|---|---|
# Título | El primer # en el archivo es solo el título de la portada (se consume, no se repite en el cuerpo). Cualquier # posterior es un encabezado principal real (ver más abajo). |
# Sección Principal (después del primer #) | Encabezado principal. Siempre comienza su propia página nueva (a menos que el cursor ya esté en la parte superior de una página en blanco), gran barra izquierda de color, negrita 16pt, texto negro — y obtiene una entrada en la Tabla de Contenido. Úsalo solo para divisiones de nivel superior genuinas; cada una cuesta una página. |
## Subsección | Subencabezado. Negrita 13pt, texto de color acento con una subrayado corto de color acento (visualmente distinto de los niveles superior e inferior) — no fuerza un salto de página, solo continúa el flujo. Aún obtiene una entrada en la TDC. Tiene su propia comprobación de "no dejar un encabezado al final de una página". |
### Sub-subsección (y ####+) | Encabezado terciario. Negrita 12pt, gris neutro, regla delgada de ancho completo, sin salto de página, sin entrada en la TDC (mantiene la TDC escaneable). |
| Texto de párrafo | Párrafo de cuerpo justificado a la izquierda, 10pt. |
- elemento / * elemento | Lista con viñetas (•). |
1. elemento | Lista numerada. |
```lenguaje ... ``` | Bloque de código monoespaciado sombreado y con bordes; la etiqueta de lenguaje (si está presente) se imprime como una pequeña etiqueta en cursiva sobre el bloque y dirige el resaltado de sintaxis real de Pygments — ver más abajo. |
| ` | a |
--- / *** / ___ en su propia línea | Espaciador vertical (no un salto de página). |
<br> en su propia línea | Una línea de cuerpo en blanco explícita (5.5 mm). Apila varias para más espacio. El renderizador no agrega espacios propios al inicio de una página, así que usa esto cuando quieras espacio extra. |
<!-- pagebreak --> en su propia línea | Iniciar una nueva página (no hace nada si ya está al inicio de una). |
 en su propia línea | Imagen centrada y con bordes, escalada para ajustarse; las rutas relativas se resuelven con respecto a la carpeta del archivo .md. PNG/JPEG/WebP y SVG (ver "Figuras, recuadros y matemáticas"). Una imagen rasterizada sin procesar de menos de 900 px de ancho tiene un tamaño máximo de ~130x105 mm; una figura (imagen seguida de un pie de foto), cualquier SVG y cualquier imagen de 900 px o más puede usar el ancho completo del texto (altura máxima 120 mm). Una figura que no cabe en el espacio restante de la página se reduce para que quepa (hasta el 70 % de su altura) en lugar de saltar a la página siguiente y dejar un espacio. |
*Figura: ...* (una línea completa en cursiva justo después de una imagen) | El pie de foto de la imagen: pequeño, centrado, en cursiva, mantenido en la misma página que la imagen. El texto alternativo nunca se imprime. |
> texto líneas | Recuadro de definición: un recuadro con bordes y ligeramente sombreado por cada blockquote (los términos en negrita y las matemáticas inline funcionan dentro; una línea > sin nada después comienza un nuevo párrafo en el mismo recuadro). |
$...$ | Matemáticas inline, convertidas a texto Unicode (letras griegas, ≤ ≥ × ·, sub/superíndices, \frac{a}{b} -> (a)/b, \sqrt, \text{}, acentos). Un precio como $5 no son matemáticas. |
$$...$$ (una línea, o en sus propias líneas) | Matemáticas en display, una imagen centrada del primer backend de matemáticas que funcione (ver "Figuras, recuadros y matemáticas"). |
| Bloque sangrado con 4 espacios después de una línea en blanco | Bloque de código sin etiqueta de lenguaje (las exportaciones de cuadernos imprimen la salida de celda de esta manera). No se aplica justo después de una lista. |
**negrita** | Texto en negrita real, a través del renderizador markdown=True incorporado de fpdf2. |
[texto](url) | Una anotación de enlace PDF real y clicable — solo se muestra el texto de visualización, no la URL sin procesar — con estilo en un color de enlace distinto. |
*cursiva* / _cursiva_ | Cursiva renderizada (convertida internamente a la forma nativa __italic__ de fpdf2). |
`código` | Las comillas invertidas de código inline se eliminan — no hay soporte monoespaciado inline, se renderiza como texto sin formato. |
Los encabezados, párrafos, viñetas, elementos numerados y celdas de tabla se renderizan a través de la ruta markdown=True de fpdf2, por lo que negrita y [texto](url) funcionan en todas partes donde aparece texto, incluso dentro de encabezados y celdas de tabla. Los bloques de código delimitados son la única excepción: su contenido se renderiza de manera completamente literal (sin análisis de markdown), por lo que una muestra de código que contenga **, __ o []() literales no se malinterpretará.
Figuras, recuadros y matemáticas
Escrito para que el acompañante markdown de un cuaderno (salida de nbconvert) se renderice directamente: report-to-pdf notebook.md reemplaza un paso de PDF basado en Chrome separado.
Imágenes SVG.  funciona. El dibujo SVG propio de fpdf2 omite las puntas de flecha <marker>, filtros y máscaras, dibuja texto en una fuente básica (sin griego) y colorea mal el texto sin fill, por lo que cada SVG se rasteriza a 2.5x y se incrusta como PNG, usando, en orden: un Chrome/Edge sin cabeza (se encuentra automáticamente; establece REPORT_PDF_BROWSER en una ruta de navegador para sobrescribir), luego cairosvg si es importable. Sin ninguno, el SVG se dibuja de forma nativa por fpdf2 y se muestra una advertencia de que puede estar incompleto. La rasterización se almacena en caché por ejecución.
Backends de matemáticas en display (--math auto|latex|mathtext|unicode, o frontmatter math:; por defecto auto). El script imprime qué backend se usó.
latex: LaTeX real cuandopdflatex,xelatexotectonicestán en PATH (máspdftocairo, PyMuPDF odvisvgmpara hacer la imagen), olatex+dvipng. Cada fórmula se compila constandalone,amsmath,amssymbyxcolora 12 pt, luego se recorta a un PNG transparente de 300 ppp en el color del texto. Instalación: MiKTeX (Windows), TeX Live (apt install texlive-latex-base texlive-latex-extra poppler-utils, macOSbrew install --cask mactex-no-gui), otectonic(binario único, descarga paquetes bajo demanda;conda install -c conda-forge tectonicoscoop install tectonic; probado de principio a fin en Windows con PyMuPDF como convertidor de imágenes).mathtext: subconjunto LaTeX de matplotlib (fracciones, sumas, griego,\text{}se asigna a\mathrm). No necesita TeX, solo matplotlib. Entornos comoalignedno son compatibles y fallan.unicode: la misma conversión de texto que las matemáticas inline, centrada.
Un backend que falta o falla (por ejemplo, una fórmula que TeX rechaza) cae silenciosamente al siguiente; --math latex aún se degrada de la misma manera.
Prueba: python tests/run_checks.py (conversión de matemáticas, analizador, enrutamiento SVG, retroceso de backend con PATH simulado, compilación de extremo a extremo de tests/fixture.md). Renderiza un acompañante: python scripts/build_report_pdf.py notebook.md -o notebook.pdf.
Resaltado de sintaxis
Los bloques de código delimitados se tokenizan con lexers reales de Pygments (no un resaltador de expresiones regulares hecho a mano — el código es exactamente el tipo de cosa que un analizador ad hoc pequeño puede hacer sutilmente mal: escapes de cadenas, comentarios anidados, literales de plantilla). La etiqueta de lenguaje en el delimitador (```typescript, ```sql, ```python, ...) selecciona el lexer; un puñado de alias comunes se normalizan (ts/tsx → typescript, js/jsx → javascript, sh/shell/zsh → bash, yml → yaml, prisma → graphql — el lexer incorporado más cercano, y una buena coincidencia en la práctica, ya que la sintaxis @attribute(...) de Prisma y la sintaxis @directive(...) de GraphQL se tokenizan casi de manera idéntica).
El resaltado es una ventaja, nunca un requisito estricto para renderizar un bloque de código: una etiqueta de lenguaje faltante, un lenguaje que Pygments no reconoce (incluso después de la tabla de alias), o Pygments mismo no siendo importable, todo cae al comportamiento exacto anterior al resaltado: todo el bloque en un solo color plano, sin fallos, sin contenido faltante.
Paleta de colores (elegida para legibilidad contra el fondo gris claro del bloque de código, no vinculada a ningún tema de Pygments en particular):
| Categoría de token | Color |
|---|---|
Palabras clave (interface, async, const, NOT NULL, palabras clave de tipo como string/number) | Magenta |
| Cadenas | Verde |
| Números | Marrón/naranja |
| Comentarios | Gris |
Nombres de funciones/clases, decoradores/atributos (@map, @relation) | Azul / marrón |
Incorporados (Promise, etc.) | Verde azulado |
| Todo lo demás (identificadores, puntuación, operadores) | El color base del código |
Un tipo de token sin una entrada exacta en la paleta se resuelve recorriendo la jerarquía de tipos de token de Pygments (por ejemplo, Keyword.Declaration no tiene entrada propia, pero su padre Keyword sí) hasta que se encuentra una coincidencia; la raíz Token siempre coincide, por lo que esto nunca falla en resolver a algún color.
La lógica de ajuste de escala monoespaciada (una línea de código demasiado ancha para la página reduce la fuente de todo el bloque de manera uniforme en lugar de desbordarse) no se ve afectada por el resaltado: mide anchos de caracteres sin formato, que no cambian según el color en el que se dibuje un token.
Tema oscuro
Pasa --dark, o establece theme: dark en el frontmatter, para renderizar todo el documento en un tema oscuro en lugar del tema claro predeterminado. Cada color en el documento cambia a su contraparte adecuada para oscuro, no solo los bloques de código:
- El fondo de la página se rellena de oscuro (fpdf2 no tiene una configuración nativa de "color de fondo de página", por lo que esto se hace rellenando la página completa en el gancho
header(), antes de que se dibuje cualquier otra cosa encima, incluida la portada). - El texto del cuerpo, los encabezados (los tres niveles), el texto atenuado/pie de página, las líneas de regla, las rayas de cebra de las tablas y el encabezado/pie de página usan un conjunto de colores de modo oscuro separado, no solo una versión atenuada del claro.
- El resaltado de sintaxis usa su propia paleta de modo oscuro (rosas/verdes/azules más brillantes elegidos para contrastar con el fondo oscuro del código), no los colores del modo claro atenuados.
- El color del enlace (tanto los enlaces markdown
[texto](url)como los enlaces de salto a página de la TDC) cambia a un azul más claro que sigue siendo legible sobre un fondo oscuro.
El color --accent (la barra superior, la barra izquierda del encabezado principal y el texto/subrayado del subencabezado) se usa tal cual en ambos temas: elige uno que funcione contra un fondo blanco y uno casi negro si planeas generar el mismo informe en ambos temas, o sobrescribe --accent por tema si no.
Tabla de Contenido
La TDC rellenada posteriormente (ver "Cómo se renderiza esto" arriba
…
[truncado para traducción]