MD2PDF
Aplicación para la edición de Markdown con exportación PDF directo a través del motor de renderizado del navegador.
Diseñé este sistema para actuar como una aplicación de página única (SPA) embebida dentro del entorno estático de Astro, proporcionando almacenamiento local persistente, sincronización de desplazamiento, soporte de aplicaciones web progresivas (PWA) y un flujo fluido de exportación a PDF directo a través del motor de renderizado del navegador.
Contexto Operativo y Utilidad del Sistema
Mi objetivo principal al construir esta aplicación fue resolver la fricción habitual entre la escritura técnica y la publicación de documentos sin depender de infraestructuras en la nube o bases de datos de terceros. Proporciono un entorno privado por defecto, lo que garantiza que los datos sensibles, como borradores de documentación o diagramas internos de una compañía, permanezcan aislados en la máquina local del usuario.
El flujo de trabajo central (Workflow) opera de la siguiente manera:
- Inicialización: Al cargar, recupero la lista de documentos y el estado de la aplicación desde IndexedDB mediante Dexie.js. Si no hay documentos, inicializo el almacén de datos con un ejemplo estructurado.
- Ingreso y Manipulación de Datos: El usuario interactúa con la interfaz de Mónaco (Monaco Editor), que intercepta de manera nativa los eventos del teclado, la estructura de formato e incluso las imágenes pegadas desde el portapapeles.
- Optimización In-Memory: Intercepto las imágenes pegadas antes de la propagación del evento, las redimensiono a través de un
canvasinterno limitando el ancho máximo a 800 píxeles, las recodifico para eficiencia y las guardo asíncronamente en el sistema de almacenamiento del navegador (IndexedDB) usando tipos MIME (como WebP o JPEG) apropiados. - Renderizado en Tiempo Real: Mientras el modelo de texto cambia, utilizo Zustand (el gestor de estado) para disparar actualizaciones para la previsualización del documento. React-Markdown procesa el árbol de sintaxis abstracta (AST) para renderizar código matemático (KaTeX), resaltado de sintaxis (highlight.js) y gráficos vectoriales (Mermaid.js).
- Exportación Nativa: En lugar de depender de motores pesados de renderización de PDF en el servidor, ajusto explícitamente el árbol de estilos DOM del documento para la vista de impresión, inyecto configuraciones CSS que ocultan la interfaz de usuario, y emito la orden
window.print()aprovechando la alta fidelidad del motor PDF nativo del navegador.
Análisis de la Arquitectura General
Estructuré el proyecto siguiendo una arquitectura híbrida altamente asimétrica, donde el servidor o capa de construcción se encarga simplemente de proveer el “caparazón” (Shell), delegando el 100% del procesamiento de negocio al cliente.
architecture-beta
group client(cloud)["Cliente / Navegador Web"]
service astro(server)["Astro Shell"] in client
service react(server)["React SPA"] in client
service storage(database)["IndexedDB / Dexie"] in client
astro:L -- R:react
react:B -- T:storage
Patrones Estructurales
- Astro como Shell Host: Utilizo la capa de Astro (
src/pages/index.astroy layouts) para proporcionar la entrada estática, la configuración global del PWA (vite-plugin-pwa) y la integración del SDK de Vercel Analytics para métricas, evitando de este modo la hidratación de frameworks pesados al inicio estático. - React Micro-Frontend Interno: El contenedor raíz de React (
src/components/App.tsx) actúa como el núcleo de interactividad. - Flujo Unidireccional del Estado: Siguiendo patrones análogos a la Clean Architecture aplicada al frontend, manejo el estado con un repositorio de única fuente de verdad (Zustand), desde donde la vista (React) se limita a reflejar cambios del estado, y los eventos del usuario despachan mutaciones (acciones) que consolido e inserto en la capa de persistencia (Dexie.js).
Modelado de Datos y Lógica de Eventos
Logro la persistencia empleando la API de IndexedDB, orquestada a través del wrapper Dexie.js para conseguir promesas asíncronas fiables.
erDiagram
DOCUMENT {
number id PK
string title
string content
date updatedAt
}
IMAGE {
number id PK
blob blobData
string mimeType
date createdAt
}
DOCUMENT ||--o{ IMAGE : "references via markdown local-image://{id}"
Entidades y Flujo de Relación
- Entidad
Document: Sirve como repositorio de texto puro en formato Markdown. - Entidad
Image: Actúa como un Blob Store local binario. En lugar de embeber base64 directamente en el markdown (lo que ralentizaría críticamente el AST del editor), intercepto el pegado de datos binarios en Mónaco, reduzco sus dimensiones vía Canvas API, lo inyecto en IndexedDB, y genero un URI interno personalizado (ej.local-image://12). - Interceptación del ciclo de vida visual: Un componente envolvente (
MarkdownImageensrc/components/Preview.tsx) intercepta las solicitudes de imágenes generadas por el analizador de Markdown, y si el prefijo corresponde alocal-image://, solicita asíncronamente el blob de IndexedDB y crea una URL temporal conURL.createObjectURL(). El objeto es revocado una vez que el componente se desmonta, protegiendo así la gestión de la memoria del navegador.
Sincronización de Componentes
sequenceDiagram
participant User
participant Editor as Monaco Editor
participant Zustand as Store
participant Dexie as IndexedDB
participant Preview as Preview Panel
User->>Editor: Typifica Markdown
Editor->>Zustand: updateCurrentDocument(content)
Zustand->>Dexie: put(updatedDoc)
Zustand-->>Preview: React State Change
Preview->>Preview: Parsea Markdown AST
Preview-->>User: Renderiza Interfaz
Stack Tecnológico y Rol de Herramientas
Mi selección del entorno tecnológico prioriza el rendimiento puro del hilo principal, el aislamiento del cliente, y la estandarización tipográfica.
| Tecnología | Categoría | Rol Específico |
|---|---|---|
| Astro | Framework Shell / PWA | Construye el entorno estático del sitio web base y orquesta el soporte PWA local a través de Vite. |
| React 19 | Capa de Interfaz y UI | Gestiona la reactividad del editor y panel de vista previa, incluyendo ref-hooks para virtualización y control del DOM. |
| TypeScript | Lenguaje Principal | Contratos de interfaces del modelo de BD y estado global. |
| Zustand | Gestión de Estado | Permite suscribir componentes a fracciones específicas del estado minimizando re-renders, e hidrata la configuración usando LocalStorage como fallback inicial. |
| Dexie.js | Persistencia de Datos | Proporciona una interfaz basada en promesas para IndexedDB con control de esquemas relacionales. |
| Monaco Editor | Motor de Edición | Motor de texto extraído de VS Code que soporta sintaxis, minimapa, y eventos profundos. |
| React-Markdown | Compilador AST | Transforma el texto en bruto, soportando los plugins remark-gfm (tablas) y remark-math (soporte de KaTeX). |
| Mermaid.js | Diagramas como Código | Analiza y renderiza bloques de sintaxis específica a gráficos SVG en el contenedor de vista previa, acoplado al tema. |
| Tailwind CSS v4 | Motor de Estilos | Maneja el layout y aplica .prose de @tailwindcss/typography, manipulado condicionalmente en vista de impresión con reglas print:!w-full. |
| BiomeJS | Toolchain Linter | Garantiza la coherencia sintáctica reemplazando a Prettier/ESLint en la fase de análisis de código. |
Impacto Arquitectónico y Diseño de Imprimibilidad
El éxito fundamental de esta arquitectura recae en el diseño de su modo de impresión (Print Mode Override). En lugar de depender de la dependencia estándar en la industria (react-to-print), secuestro el ciclo de vida de React por un breve lapso:
Al invocar la exportación, inyecto un estado síncrono de "light" en el Store, forzando a React y a Mermaid.js a renderizar el árbol DOM entero en contrastes puros sin fondos oscuros. Un setTimeout calcula el ciclo exacto del repintado del DOM (aprox 1500ms), invoco el cuadro de diálogo window.print() (que detiene el motor JS temporalmente), y al recibir control de vuelta, restauro inmediatamente el tema oscuro si este estaba habilitado. Esto junto con el reseteo de márgenes vía hojas de estilo @media print permite exportaciones PDF nativas limpias, precisas y sin dobles bordes.