MD2PDF
Aplicação web progressiva (PWA) de edição Markdown com exportação direta para PDF através do motor de renderização do navegador.
Projetei este sistema para funcionar como uma aplicação de página única (SPA) incorporada ao ambiente estático do Astro, oferecendo armazenamento local persistente, sincronização de rolagem, suporte a aplicação web progressiva (PWA) e um fluxo contínuo de exportação direta para PDF que utiliza o motor de renderização nativo do navegador.
Contexto Operacional e Utilidade do Sistema
Meu objetivo principal ao construir esta aplicação foi resolver o atrito comum entre a redação técnica e a publicação de documentos, sem depender de infraestruturas em nuvem ou bancos de dados de terceiros. Ofereço um ambiente privado por padrão, garantindo que dados sensíveis —como rascunhos de documentação ou diagramas internos de empresas— permaneçam isolados na máquina local do usuário.
O fluxo de trabalho principal funciona da seguinte forma:
- Inicialização: Ao carregar, recupero a lista de documentos e o estado da aplicação do IndexedDB usando Dexie.js. Se não houver documentos, inicializo o armazenamento de dados com um exemplo estruturado.
- Entrada e Manipulação de Dados: O usuário interage com a interface do Monaco Editor, que intercepta nativamente eventos de teclado, a estrutura de formatação e até imagens coladas da área de transferência.
- Otimização em Memória: Intercepto as imagens coladas antes da propagação do evento, redimensiono-as usando um canvas interno para limitar a largura máxima a 800 pixels, recodifico-as para maior eficiência e as salvo de forma assíncrona no sistema de armazenamento do navegador (IndexedDB) com os tipos MIME apropriados (como WebP ou JPEG).
- Renderização em Tempo Real: À medida que o modelo de texto muda, utilizo o Zustand (o gerenciador de estado) para disparar atualizações na pré-visualização do documento. O React-Markdown processa a Árvore de Sintaxe Abstrata (AST) para renderizar fórmulas matemáticas (KaTeX), realce de sintaxe (highlight.js) e diagramas vetoriais (Mermaid.js).
- Exportação Nativa: Em vez de depender de motores pesados de renderização de PDF no servidor, ajusto explicitamente a árvore de estilos do DOM do documento para a visualização de impressão, injeto configurações CSS que ocultam a interface do usuário e invoco o comando
window.print(), aproveitando a alta fidelidade do motor de PDF nativo do navegador.
Análise da Arquitetura Geral
Estruturei o projeto seguindo uma arquitetura híbrida altamente assimétrica, na qual a camada de servidor ou de build é responsável apenas por fornecer o shell, delegando 100% do processamento de negócio ao cliente.
architecture-beta
group client(cloud)["Cliente / Navegador Web"]
service astro(server)["Shell Astro"] in client
service react(server)["SPA React"] in client
service storage(database)["IndexedDB / Dexie"] in client
astro:L -- R:react
react:B -- T:storage
Padrões Estruturais
- Astro como Host do Shell: Utilizo a camada Astro (
src/pages/index.astroe layouts) para fornecer pontos de entrada estáticos, a configuração global de PWA (vite-plugin-pwa) e a integração do SDK do Vercel Analytics para métricas, evitando assim a hidratação de frameworks pesados na inicialização estática. - Micro-Frontend React Interno: O contêiner raiz do React (
src/components/App.tsx) atua como o núcleo da interatividade. - Fluxo de Estado Unidirecional: Seguindo padrões análogos à Clean Architecture aplicada ao frontend, gerencio o estado com um repositório de fonte única da verdade (Zustand), no qual a visualização (React) apenas reflete as mudanças de estado, e os eventos do usuário disparam mutações (ações) que consolido e insiro na camada de persistência (Dexie.js).
Modelagem de Dados e Lógica de Eventos
Obtenho a persistência usando a API do IndexedDB, orquestrada por meio do wrapper Dexie.js para obter promises assíncronas confiáveis.
erDiagram
DOCUMENT {
number id PK
string title
string content
date updatedAt
}
IMAGE {
number id PK
blob blobData
string mimeType
date createdAt
}
DOCUMENT ||--o{ IMAGE : "referencia via markdown local-image://{id}"
Entidades e Fluxo de Relacionamentos
- Entidade
Document: Serve como repositório de texto bruto em formato Markdown. - Entidade
Image: Atua como um Blob Store binário local. Em vez de incorporar base64 diretamente no markdown (o que tornaria a AST do editor criticamente mais lenta), intercepto a colagem de dados binários no Monaco, reduzo suas dimensões por meio da Canvas API, insiro-os no IndexedDB e gero uma URI interna personalizada (por exemplo,local-image://12). - Interceptação do Ciclo de Vida Visual: Um componente wrapper (
MarkdownImageemsrc/components/Preview.tsx) intercepta as requisições de imagem geradas pelo parser de Markdown e, se o prefixo corresponder alocal-image://, solicita de forma assíncrona o blob ao IndexedDB e cria uma URL temporária usandoURL.createObjectURL(). O objeto é revogado quando o componente é desmontado, protegendo assim o gerenciamento de memória do navegador.
Sincronização de Componentes
sequenceDiagram
participant User as Usuário
participant Editor as Monaco Editor
participant Zustand as Store
participant Dexie as IndexedDB
participant Preview as Painel de Pré-visualização
User->>Editor: Digita Markdown
Editor->>Zustand: updateCurrentDocument(content)
Zustand->>Dexie: put(updatedDoc)
Zustand-->>Preview: Mudança de Estado React
Preview->>Preview: Analisa a AST do Markdown
Preview-->>User: Renderiza a Interface
Stack Tecnológico e Papel das Ferramentas
Minha seleção do stack tecnológico prioriza o desempenho puro da thread principal, o isolamento do cliente e a padronização tipográfica.
| Tecnologia | Categoria | Papel Específico |
|---|---|---|
| Astro | Framework Shell / PWA | Constrói o ambiente estático do site base e orquestra o suporte local a PWA por meio do Vite. |
| React 19 | Camada de Interface e UI | Gerencia a reatividade do editor e do painel de pré-visualização, incluindo ref-hooks para virtualização e controle do DOM. |
| TypeScript | Linguagem Principal | Contratos de interface para o modelo de banco de dados e o estado global. |
| Zustand | Gerenciamento de Estado | Permite inscrever componentes em fatias específicas do estado para minimizar re-renderizações e hidrata a configuração usando o LocalStorage como fallback inicial. |
| Dexie.js | Persistência de Dados | Fornece uma interface baseada em promises para o IndexedDB com controle de esquema relacional. |
| Monaco Editor | Motor de Edição | Motor de texto extraído do VS Code, com suporte a sintaxe, minimapa e eventos avançados. |
| React-Markdown | Compilador AST | Transforma o texto bruto, com suporte aos plugins remark-gfm (tabelas) e remark-math (suporte a KaTeX). |
| Mermaid.js | Diagram-as-Code | Analisa e renderiza blocos de sintaxe específica como gráficos SVG no contêiner de pré-visualização, acoplado ao tema. |
| Tailwind CSS v4 | Motor de Estilos | Gerencia o layout e aplica .prose de @tailwindcss/typography, manipulado condicionalmente na visualização de impressão com regras print:!w-full. |
| BiomeJS | Linter do Toolchain | Garante a consistência sintática, substituindo o Prettier/ESLint na fase de análise de código. |
Impacto Arquitetural e Design para Impressão
O principal acerto desta arquitetura está no design de seu modo de impressão (Print Mode Override). Em vez de depender da dependência padrão do setor (react-to-print), assumo o controle do ciclo de vida do React por um breve período:
Ao invocar a exportação, injeto um estado síncrono "light" no Store, forçando o React e o Mermaid.js a renderizar toda a árvore do DOM em contraste puro, sem fundos escuros. Um setTimeout calcula o ciclo exato de repintura do DOM (aprox. 1500ms), invoco a caixa de diálogo window.print() (que pausa temporariamente o motor JS) e, ao recuperar o controle, restauro imediatamente o tema escuro, caso estivesse ativado. Isso, combinado com a redefinição das margens por meio de folhas de estilo @media print, permite exportações de PDF nativas, limpas e precisas, sem bordas duplicadas.
Arraste para mover o diagrama. Use + e − para dar zoom, as setas para movê-lo e 0 para redefinir a visualização.
Vamos trabalhar juntos
Engenheiro de Software e desenvolvedor Senior Full-Stack, trabalho remotamente a partir de Santa Cruz, Bolívia. Aberto a novas oportunidades.