epicora-pdf 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,167 @@
1
+ # epicora-pdf
2
+
3
+ > Converta documentos Markdown em PDFs com identidade visual Epicora — em um único comando.
4
+
5
+ CLI que transforma arquivos `.md` em PDFs profissionais, com tipografia, paleta de cores, cabeçalho com logo e rodapé de paginação padronizados pela marca Epicora. Gerado via Playwright/Chromium para máxima fidelidade visual.
6
+
7
+ ---
8
+
9
+ ## Instalação
10
+
11
+ ```bash
12
+ npm install -g epicora-pdf
13
+ ```
14
+
15
+ Após instalar, verifique se o Chromium (usado internamente pelo Playwright) está disponível:
16
+
17
+ ```bash
18
+ npx playwright install chromium
19
+ ```
20
+
21
+ ---
22
+
23
+ ## Uso
24
+
25
+ ```bash
26
+ epicora-pdf <arquivo.md> [opções]
27
+ ```
28
+
29
+ ### Exemplos
30
+
31
+ ```bash
32
+ # Gera proposta.pdf no mesmo diretório do .md
33
+ epicora-pdf proposta.md
34
+
35
+ # Define o caminho de saída manualmente
36
+ epicora-pdf proposta.md --output documentos/proposta-cliente.pdf
37
+
38
+ # Gera e abre o PDF imediatamente
39
+ epicora-pdf proposta.md --open
40
+ ```
41
+
42
+ ### Opções
43
+
44
+ | Flag | Alias | Descrição |
45
+ |------|-------|-----------|
46
+ | `--output <caminho>` | `-o` | Caminho de saída do PDF. Padrão: mesmo nome e diretório do `.md` |
47
+ | `--open` | — | Abre o PDF gerado automaticamente após a conversão |
48
+ | `--version` | `-V` | Exibe a versão instalada |
49
+ | `--help` | `-h` | Exibe as opções disponíveis |
50
+
51
+ ---
52
+
53
+ ## Saída
54
+
55
+ O PDF gerado segue o padrão visual da Epicora:
56
+
57
+ - **Formato:** A4 com margens de 88px (topo/base) × 56px (laterais)
58
+ - **Cabeçalho:** Logo da Epicora + URL `epicora.com.br` com faixa de gradiente colorida
59
+ - **Rodapé:** Número de página em Violeta
60
+ - **Tipografia:** Fonte [Alexandria](https://fonts.google.com/specimen/Alexandria) via Google Fonts
61
+ - **Syntax highlighting:** Paleta neon no tema escuro para blocos de código
62
+
63
+ ### Paleta de marca
64
+
65
+ | Nome | Hex | Uso |
66
+ |------|-----|-----|
67
+ | Violeta | `#6100ff` | Cor primária, links, numeração, H3 |
68
+ | Grafite | `#383838` | Texto principal, H1, H2 |
69
+ | Amarelo | `#daff19` | Números e literais em código |
70
+ | Verde-água | `#14ffb9` | Strings em código, gradiente |
71
+ | Vermelho | `#fd4950` | Built-ins em código |
72
+
73
+ ---
74
+
75
+ ## Markdown suportado
76
+
77
+ O conversor suporta toda a especificação **GFM (GitHub Flavored Markdown)**, incluindo:
78
+
79
+ - Títulos `H1` a `H6` com estilos distintos por nível
80
+ - **Negrito**, _itálico_, ~~tachado~~, `código inline` e ==marcação==
81
+ - Blocos de código com syntax highlighting automático para 180+ linguagens (via [highlight.js](https://highlightjs.org/))
82
+ - Diagramas [Mermaid](https://mermaid.js.org/) — fluxogramas, sequências, Gantt, etc.
83
+ - Tabelas com cabeçalho estilizado
84
+ - Blockquotes com acento visual
85
+ - Listas ordenadas, não-ordenadas e task lists (`- [ ]`)
86
+ - Imagens com bordas arredondadas e sombra
87
+ - Links clicáveis
88
+ - Régua horizontal (`---`)
89
+
90
+ ### Quebras de página automáticas
91
+
92
+ Cada `H1` (exceto o primeiro, que funciona como título de capa) dispara automaticamente uma nova página no PDF. Isso permite estruturar documentos longos apenas com headings, sem nenhuma configuração extra.
93
+
94
+ ---
95
+
96
+ ## Estrutura do projeto
97
+
98
+ ```
99
+ epicora-pdf/
100
+ ├── src/
101
+ │ ├── cli.ts # Ponto de entrada — CLI com Commander.js
102
+ │ ├── renderer.ts # Orquestrador — pipeline MD → PDF via Playwright
103
+ │ ├── markdown.ts # Conversão MD → HTML (marked + highlight.js)
104
+ │ ├── template.ts # HTML5 completo com Google Fonts e mermaid.js
105
+ │ └── styles.ts # CSS completo com paleta de marca Epicora
106
+ ├── assets/
107
+ │ └── logo.svg # Logo embedded como base64 no cabeçalho
108
+ ├── bin/
109
+ │ └── epicora-pdf # Shebang para execução global via npm
110
+ └── dist/ # JavaScript compilado (gerado pelo build)
111
+ ```
112
+
113
+ ### Pipeline de conversão
114
+
115
+ ```
116
+ arquivo.md
117
+ → CLI valida entrada e saída
118
+ → marked converte MD em HTML (GFM + highlight.js)
119
+ → template.ts monta HTML5 completo com fonts e estilos
120
+ → Playwright/Chromium renderiza o HTML
121
+ → Playwright aguarda renderização de diagramas Mermaid
122
+ → PDF A4 exportado com cabeçalho/rodapé
123
+ ```
124
+
125
+ ---
126
+
127
+ ## Requisitos
128
+
129
+ - **Node.js** >= 18.0.0
130
+ - **Chromium** — instalado via `npx playwright install chromium`
131
+ - Conexão com internet no primeiro uso (carrega Google Fonts e Mermaid via CDN)
132
+
133
+ ---
134
+
135
+ ## Desenvolvimento
136
+
137
+ Este é um repositório privado. Clone com acesso autorizado:
138
+
139
+ ```bash
140
+ git clone https://github.com/Epicora-Software-House/epicora-pdf.git
141
+ cd epicora-pdf
142
+ npm install
143
+ npx playwright install chromium
144
+ ```
145
+
146
+ ### Comandos disponíveis
147
+
148
+ ```bash
149
+ npm run dev -- input.md --output output.pdf --open # Executa via ts-node (sem build)
150
+ npm run build # Compila TypeScript → dist/
151
+ npx tsc --noEmit # Type-check sem emitir arquivos
152
+ node dist/cli.js input.md --output output.pdf # Executa o build compilado
153
+ ```
154
+
155
+ ### Modificando estilos
156
+
157
+ Toda a aparência visual está centralizada em `src/styles.ts`. As variáveis CSS no topo do arquivo controlam a paleta completa. Após alterações, rode `npm run build` para recompilar.
158
+
159
+ ### Modificando o cabeçalho ou rodapé
160
+
161
+ O cabeçalho e rodapé são gerados em `src/renderer.ts` pelas funções `buildHeaderTemplate` e `buildFooterTemplate`. Eles utilizam inline styles (requisito do Playwright para templates de cabeçalho/rodapé em PDF).
162
+
163
+ ---
164
+
165
+ ## Licença
166
+
167
+ MIT © [Epicora](https://epicora.com.br)
package/README.pdf ADDED
Binary file
Binary file
package/dist/cli.js ADDED
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ var __importDefault = (this && this.__importDefault) || function (mod) {
37
+ return (mod && mod.__esModule) ? mod : { "default": mod };
38
+ };
39
+ Object.defineProperty(exports, "__esModule", { value: true });
40
+ const commander_1 = require("commander");
41
+ const path_1 = __importDefault(require("path"));
42
+ const fs_1 = __importDefault(require("fs"));
43
+ const renderer_1 = require("./renderer");
44
+ const program = new commander_1.Command();
45
+ program
46
+ .name('epicora-pdf')
47
+ .description('Converte Markdown em PDF com identidade visual Epicora')
48
+ .version('1.0.0')
49
+ .argument('<input>', 'Arquivo Markdown de entrada (.md)')
50
+ .option('-o, --output <path>', 'Caminho de saída do PDF')
51
+ .option('--open', 'Abre o PDF após gerar')
52
+ .action(async (input, options) => {
53
+ const inputPath = path_1.default.resolve(input);
54
+ if (!fs_1.default.existsSync(inputPath)) {
55
+ console.error(`Erro: arquivo não encontrado — ${inputPath}`);
56
+ process.exit(1);
57
+ }
58
+ if (!inputPath.endsWith('.md')) {
59
+ console.error('Erro: o arquivo de entrada deve ser um .md');
60
+ process.exit(1);
61
+ }
62
+ const outputPath = options.output
63
+ ? path_1.default.resolve(options.output)
64
+ : inputPath.replace(/\.md$/, '.pdf');
65
+ fs_1.default.mkdirSync(path_1.default.dirname(outputPath), { recursive: true });
66
+ console.log(`Gerando PDF...`);
67
+ console.log(` Entrada: ${inputPath}`);
68
+ console.log(` Saída: ${outputPath}`);
69
+ try {
70
+ await (0, renderer_1.renderPdf)(inputPath, outputPath);
71
+ console.log(`PDF gerado: ${outputPath}`);
72
+ if (options.open) {
73
+ const { exec } = await Promise.resolve().then(() => __importStar(require('child_process')));
74
+ const cmd = process.platform === 'win32'
75
+ ? `start "" "${outputPath}"`
76
+ : process.platform === 'darwin'
77
+ ? `open "${outputPath}"`
78
+ : `xdg-open "${outputPath}"`;
79
+ exec(cmd);
80
+ }
81
+ }
82
+ catch (err) {
83
+ console.error('Erro ao gerar PDF:', err);
84
+ process.exit(1);
85
+ }
86
+ });
87
+ program.parse();
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.convertMarkdown = convertMarkdown;
7
+ const marked_1 = require("marked");
8
+ const marked_highlight_1 = require("marked-highlight");
9
+ const highlight_js_1 = __importDefault(require("highlight.js"));
10
+ marked_1.marked.use((0, marked_highlight_1.markedHighlight)({
11
+ langPrefix: 'hljs language-',
12
+ highlight(code, lang) {
13
+ const language = highlight_js_1.default.getLanguage(lang) ? lang : 'plaintext';
14
+ return highlight_js_1.default.highlight(code, { language }).value;
15
+ },
16
+ }));
17
+ marked_1.marked.use({
18
+ gfm: true,
19
+ breaks: false,
20
+ });
21
+ // Mermaid blocks bypass syntax highlighting and are rendered client-side
22
+ marked_1.marked.use({
23
+ renderer: {
24
+ code(code, infostring) {
25
+ if (infostring === 'mermaid') {
26
+ return `<div class="mermaid">${code}</div>\n`;
27
+ }
28
+ return false;
29
+ },
30
+ },
31
+ });
32
+ function convertMarkdown(md) {
33
+ const result = marked_1.marked.parse(md);
34
+ return typeof result === 'string' ? result : '';
35
+ }
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.renderPdf = renderPdf;
7
+ const path_1 = __importDefault(require("path"));
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const playwright_1 = require("playwright");
10
+ const markdown_1 = require("./markdown");
11
+ const template_1 = require("./template");
12
+ const BRAND = {
13
+ violeta: '#6100ff',
14
+ verdeAgua: '#14ffb9',
15
+ amarelo: '#daff19',
16
+ grafite: '#383838',
17
+ gray200: '#e4e4e4',
18
+ gray300: '#cccccc',
19
+ gray400: '#999999',
20
+ };
21
+ async function renderPdf(inputPath, outputPath) {
22
+ const md = fs_1.default.readFileSync(inputPath, 'utf-8');
23
+ const filename = path_1.default.basename(inputPath, path_1.default.extname(inputPath));
24
+ const htmlBody = (0, markdown_1.convertMarkdown)(md);
25
+ const fullHtml = (0, template_1.buildHtml)(htmlBody, filename);
26
+ const logoPath = path_1.default.join(__dirname, '..', 'assets', 'epicora.png');
27
+ const logoBase64 = fs_1.default.readFileSync(logoPath).toString('base64');
28
+ const logoDataUri = `data:image/png;base64,${logoBase64}`;
29
+ const browser = await playwright_1.chromium.launch();
30
+ const page = await browser.newPage();
31
+ await page.setContent(fullHtml, { waitUntil: 'networkidle' });
32
+ // Wait for mermaid diagrams to finish rendering
33
+ const hasMermaid = await page.evaluate(() => document.querySelectorAll('.mermaid').length > 0);
34
+ if (hasMermaid) {
35
+ await page.waitForFunction(() => window.__mermaidDone === true, { timeout: 30000 });
36
+ }
37
+ await page.pdf({
38
+ path: outputPath,
39
+ format: 'A4',
40
+ printBackground: true,
41
+ displayHeaderFooter: true,
42
+ headerTemplate: buildHeaderTemplate(logoDataUri),
43
+ footerTemplate: buildFooterTemplate(filename),
44
+ margin: {
45
+ top: '88px',
46
+ bottom: '62px',
47
+ left: '56px',
48
+ right: '56px',
49
+ },
50
+ });
51
+ await browser.close();
52
+ }
53
+ function buildHeaderTemplate(logoDataUri) {
54
+ return `
55
+ <div style="
56
+ width: 100%;
57
+ font-family: -apple-system, Helvetica, Arial, sans-serif;
58
+ box-sizing: border-box;
59
+ position: relative;
60
+ ">
61
+ <div style="
62
+ height: 4px;
63
+ background: linear-gradient(90deg, ${BRAND.violeta} 0%, ${BRAND.verdeAgua} 60%, ${BRAND.amarelo} 100%);
64
+ width: 100%;
65
+ "></div>
66
+ <div style="
67
+ padding: 14px 56px 12px 56px;
68
+ display: flex;
69
+ align-items: center;
70
+ justify-content: space-between;
71
+ box-sizing: border-box;
72
+ ">
73
+ <img
74
+ src="${logoDataUri}"
75
+ style="height: 26px; width: auto; display: block;"
76
+ />
77
+ <span style="
78
+ font-size: 6.5pt;
79
+ color: ${BRAND.gray400};
80
+ letter-spacing: 0.9px;
81
+ font-weight: 500;
82
+ text-transform: uppercase;
83
+ line-height: 1;
84
+ ">epicora.com.br</span>
85
+ </div>
86
+ </div>
87
+ `;
88
+ }
89
+ function buildFooterTemplate(_filename) {
90
+ return `
91
+ <div style="
92
+ width: 100%;
93
+ padding: 0 56px;
94
+ display: flex;
95
+ align-items: center;
96
+ justify-content: flex-end;
97
+ font-family: -apple-system, Helvetica, Arial, sans-serif;
98
+ box-sizing: border-box;
99
+ position: relative;
100
+ ">
101
+ <span class="pageNumber" style="
102
+ color: ${BRAND.violeta};
103
+ font-weight: 700;
104
+ font-size: 9pt;
105
+ line-height: 1;
106
+ "></span>
107
+ </div>
108
+ `;
109
+ }
package/dist/styles.js ADDED
@@ -0,0 +1,456 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getStyles = getStyles;
4
+ function getStyles() {
5
+ return `
6
+ /* ============================================================
7
+ EPICORA PDF — Brand Stylesheet v2.0
8
+ Palette:
9
+ Grafite #383838
10
+ Amarelo #daff19
11
+ Verde-água #14ffb9
12
+ Violeta #6100ff
13
+ Vermelho #fd4950
14
+ Branco #ffffff
15
+ ============================================================ */
16
+
17
+ @import url('https://fonts.googleapis.com/css2?family=Alexandria:ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;1,300;1,400&display=swap');
18
+
19
+ *, *::before, *::after {
20
+ box-sizing: border-box;
21
+ }
22
+
23
+ :root {
24
+ --grafite: #383838;
25
+ --grafite-90: #2e2e2e;
26
+ --amarelo: #daff19;
27
+ --verde-agua: #14ffb9;
28
+ --violeta: #6100ff;
29
+ --vermelho: #fd4950;
30
+ --branco: #ffffff;
31
+ --gray-50: #fafafa;
32
+ --gray-100: #f3f3f3;
33
+ --gray-200: #e4e4e4;
34
+ --gray-300: #cccccc;
35
+ --gray-400: #999999;
36
+ --gray-600: #555555;
37
+ --violeta-06: rgba(97, 0, 255, 0.06);
38
+ --violeta-12: rgba(97, 0, 255, 0.12);
39
+ --violeta-22: rgba(97, 0, 255, 0.22);
40
+ }
41
+
42
+ @page {
43
+ size: A4;
44
+ margin: 88px 56px 64px 56px;
45
+ }
46
+
47
+ html, body {
48
+ margin: 0;
49
+ padding: 0;
50
+ font-family: 'Alexandria', -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
51
+ font-size: 10.5pt;
52
+ line-height: 1.78;
53
+ color: var(--grafite);
54
+ background-color: #ffffff;
55
+ background-image: radial-gradient(circle, rgba(97, 0, 255, 0.055) 1.5px, transparent 1.5px);
56
+ background-size: 28px 28px;
57
+ -webkit-print-color-adjust: exact;
58
+ print-color-adjust: exact;
59
+ }
60
+
61
+
62
+ .content {
63
+ max-width: 100%;
64
+ }
65
+
66
+ /* ================================================================
67
+ COVER TITLE — First H1: full-bleed dark block
68
+ ================================================================ */
69
+
70
+ .content > h1:first-child {
71
+ font-size: 29pt;
72
+ font-weight: 800;
73
+ color: var(--grafite);
74
+ margin: 0 0 16px 0;
75
+ padding-bottom: 12px;
76
+ border-bottom: 3px solid var(--violeta);
77
+ line-height: 1.15;
78
+ letter-spacing: -0.8px;
79
+ page-break-after: avoid;
80
+ }
81
+
82
+ /* Lead paragraph — first <p> right after the cover title */
83
+ .content > h1:first-child + p {
84
+ font-size: 12pt;
85
+ font-weight: 300;
86
+ color: var(--gray-600);
87
+ line-height: 1.85;
88
+ margin-bottom: 32px;
89
+ }
90
+
91
+ /* ================================================================
92
+ H1 — Section break (every H1 after the first)
93
+ ================================================================ */
94
+
95
+ h1 {
96
+ font-size: 21pt;
97
+ font-weight: 800;
98
+ color: var(--grafite);
99
+ margin: 0 0 24px 0;
100
+ padding-bottom: 12px;
101
+ border-bottom: 3px solid var(--violeta);
102
+ line-height: 1.2;
103
+ letter-spacing: -0.4px;
104
+ page-break-after: avoid;
105
+ }
106
+
107
+ h1 ~ h1 {
108
+ page-break-before: always;
109
+ margin-top: 0;
110
+ }
111
+
112
+ /* ================================================================
113
+ H2 — Section headings
114
+ ================================================================ */
115
+
116
+ h2 {
117
+ font-size: 14pt;
118
+ font-weight: 700;
119
+ color: var(--grafite);
120
+ margin: 40px 0 14px 0;
121
+ padding: 7px 16px 7px 16px;
122
+ border-left: 4px solid var(--violeta);
123
+ background: var(--violeta-06);
124
+ border-radius: 0 6px 6px 0;
125
+ line-height: 1.3;
126
+ page-break-after: avoid;
127
+ }
128
+
129
+ /* ================================================================
130
+ H3 — Sub-section headings
131
+ ================================================================ */
132
+
133
+ h3 {
134
+ font-size: 11.5pt;
135
+ font-weight: 700;
136
+ color: var(--violeta);
137
+ margin: 30px 0 8px 0;
138
+ line-height: 1.4;
139
+ page-break-after: avoid;
140
+ }
141
+
142
+ /* ================================================================
143
+ H4 / H5 / H6
144
+ ================================================================ */
145
+
146
+ h4, h5, h6 {
147
+ font-size: 8.5pt;
148
+ font-weight: 700;
149
+ color: var(--gray-400);
150
+ margin: 22px 0 6px 0;
151
+ text-transform: uppercase;
152
+ letter-spacing: 1.2px;
153
+ page-break-after: avoid;
154
+ }
155
+
156
+ /* Avoid orphan headings */
157
+ h1, h2, h3, h4, h5, h6 {
158
+ orphans: 3;
159
+ widows: 3;
160
+ }
161
+
162
+ /* ================================================================
163
+ PARAGRAPHS
164
+ ================================================================ */
165
+
166
+ p {
167
+ margin: 0 0 14px 0;
168
+ orphans: 3;
169
+ widows: 3;
170
+ }
171
+
172
+ /* ================================================================
173
+ LINKS
174
+ ================================================================ */
175
+
176
+ a {
177
+ color: var(--violeta);
178
+ text-decoration: none;
179
+ border-bottom: 1px solid var(--violeta-22);
180
+ }
181
+
182
+ /* ================================================================
183
+ TABLES
184
+ ================================================================ */
185
+
186
+ table {
187
+ width: 100%;
188
+ border-collapse: separate;
189
+ border-spacing: 0;
190
+ margin: 16px 0 28px 0;
191
+ font-size: 9.5pt;
192
+ page-break-inside: avoid;
193
+ border: 1px solid var(--gray-200);
194
+ border-radius: 8px;
195
+ overflow: hidden;
196
+ }
197
+
198
+ thead tr {
199
+ background: var(--grafite);
200
+ color: var(--branco);
201
+ }
202
+
203
+ thead th {
204
+ padding: 11px 14px;
205
+ text-align: left;
206
+ font-weight: 700;
207
+ font-size: 8pt;
208
+ letter-spacing: 0.7px;
209
+ text-transform: uppercase;
210
+ white-space: nowrap;
211
+ }
212
+
213
+ tbody tr:nth-child(odd) {
214
+ background: var(--gray-50);
215
+ }
216
+
217
+ tbody tr:nth-child(even) {
218
+ background: var(--branco);
219
+ }
220
+
221
+ tbody td {
222
+ padding: 9px 14px;
223
+ border-bottom: 1px solid var(--gray-200);
224
+ vertical-align: top;
225
+ line-height: 1.55;
226
+ }
227
+
228
+ tbody tr:last-child td {
229
+ border-bottom: none;
230
+ }
231
+
232
+ /* First column slightly bolder */
233
+ tbody td:first-child {
234
+ font-weight: 500;
235
+ }
236
+
237
+ /* ================================================================
238
+ BLOCKQUOTES
239
+ ================================================================ */
240
+
241
+ blockquote {
242
+ margin: 20px 0;
243
+ padding: 18px 48px 18px 20px;
244
+ border-left: 4px solid var(--violeta);
245
+ background: var(--violeta-06);
246
+ border-radius: 0 8px 8px 0;
247
+ page-break-inside: avoid;
248
+ position: relative;
249
+ }
250
+
251
+ blockquote::after {
252
+ content: '\\201C';
253
+ position: absolute;
254
+ top: 4px;
255
+ right: 14px;
256
+ font-size: 44pt;
257
+ font-weight: 800;
258
+ color: var(--violeta-22);
259
+ line-height: 1;
260
+ font-family: Georgia, serif;
261
+ pointer-events: none;
262
+ }
263
+
264
+ blockquote p {
265
+ margin: 0;
266
+ font-size: 10pt;
267
+ line-height: 1.72;
268
+ }
269
+
270
+ blockquote p:not(:last-child) {
271
+ margin-bottom: 8px;
272
+ }
273
+
274
+ blockquote strong {
275
+ color: var(--violeta);
276
+ font-weight: 700;
277
+ }
278
+
279
+ blockquote em {
280
+ color: var(--gray-600);
281
+ }
282
+
283
+ /* ================================================================
284
+ INLINE CODE
285
+ ================================================================ */
286
+
287
+ code {
288
+ font-family: 'Fira Code', 'Cascadia Code', 'Consolas', 'Courier New', monospace;
289
+ font-size: 8.5pt;
290
+ background: var(--violeta-06);
291
+ color: var(--violeta);
292
+ padding: 2px 7px;
293
+ border-radius: 4px;
294
+ border: 1px solid var(--violeta-12);
295
+ font-weight: 500;
296
+ }
297
+
298
+ /* ================================================================
299
+ CODE BLOCKS
300
+ ================================================================ */
301
+
302
+ pre {
303
+ background: #1a1b2e;
304
+ border-radius: 10px;
305
+ margin: 16px 0 24px 0;
306
+ page-break-inside: avoid;
307
+ overflow: hidden;
308
+ border: 1px solid rgba(255, 255, 255, 0.06);
309
+ }
310
+
311
+ /* Gradient accent stripe at the top of code blocks */
312
+ pre::before {
313
+ content: '';
314
+ display: block;
315
+ height: 3px;
316
+ background: linear-gradient(90deg, var(--violeta), var(--verde-agua));
317
+ }
318
+
319
+ pre code {
320
+ display: block;
321
+ background: transparent;
322
+ color: #cdd6f4;
323
+ padding: 16px 20px 22px 20px;
324
+ border: none;
325
+ font-size: 9pt;
326
+ line-height: 1.72;
327
+ font-weight: 400;
328
+ }
329
+
330
+ /* Syntax highlighting — Epicora neon palette on dark */
331
+ .hljs-keyword,
332
+ .hljs-selector-tag,
333
+ .hljs-tag { color: #c792ea; }
334
+ .hljs-string,
335
+ .hljs-attr-value { color: #14ffb9; } /* verde-agua */
336
+ .hljs-number,
337
+ .hljs-literal { color: #daff19; } /* amarelo */
338
+ .hljs-comment,
339
+ .hljs-quote { color: #5c6086; font-style: italic; }
340
+ .hljs-function,
341
+ .hljs-title { color: #89b4fa; }
342
+ .hljs-attr,
343
+ .hljs-name { color: #fab387; }
344
+ .hljs-built_in { color: #fd4950; } /* vermelho */
345
+ .hljs-type { color: #a6e3a1; }
346
+ .hljs-variable { color: #cdd6f4; }
347
+ .hljs-params { color: #f2cdcd; }
348
+ .hljs-operator { color: #89dceb; }
349
+ .hljs-punctuation { color: #9399b2; }
350
+ .hljs-property { color: #89dceb; }
351
+ .hljs-class .hljs-title { color: #a6e3a1; font-weight: bold; }
352
+
353
+ /* ================================================================
354
+ LISTS
355
+ ================================================================ */
356
+
357
+ ul, ol {
358
+ margin: 0 0 14px 0;
359
+ padding-left: 24px;
360
+ }
361
+
362
+ li {
363
+ margin-bottom: 5px;
364
+ line-height: 1.65;
365
+ }
366
+
367
+ li > ul,
368
+ li > ol {
369
+ margin: 4px 0 4px 0;
370
+ }
371
+
372
+ ul li::marker {
373
+ color: var(--violeta);
374
+ font-size: 0.85em;
375
+ }
376
+
377
+ ol li::marker {
378
+ color: var(--violeta);
379
+ font-weight: 700;
380
+ font-size: 0.9em;
381
+ }
382
+
383
+ li input[type="checkbox"] {
384
+ margin-right: 6px;
385
+ accent-color: var(--violeta);
386
+ }
387
+
388
+ /* ================================================================
389
+ HORIZONTAL RULE
390
+ ================================================================ */
391
+
392
+ hr {
393
+ border: none;
394
+ height: 2px;
395
+ margin: 36px 0;
396
+ background: linear-gradient(90deg,
397
+ var(--violeta) 0%,
398
+ var(--verde-agua) 50%,
399
+ transparent 100%
400
+ );
401
+ border-radius: 2px;
402
+ }
403
+
404
+ /* ================================================================
405
+ IMAGES
406
+ ================================================================ */
407
+
408
+ img {
409
+ max-width: 100%;
410
+ height: auto;
411
+ border-radius: 8px;
412
+ page-break-inside: avoid;
413
+ display: block;
414
+ margin: 20px auto;
415
+ box-shadow: 0 2px 14px rgba(0, 0, 0, 0.09);
416
+ }
417
+
418
+ /* ================================================================
419
+ INLINE TEXT
420
+ ================================================================ */
421
+
422
+ strong {
423
+ font-weight: 700;
424
+ }
425
+
426
+ em {
427
+ font-style: italic;
428
+ color: var(--gray-600);
429
+ }
430
+
431
+ del {
432
+ color: var(--gray-300);
433
+ text-decoration: line-through;
434
+ }
435
+
436
+ mark {
437
+ background: rgba(218, 255, 25, 0.38);
438
+ color: var(--grafite);
439
+ padding: 1px 4px;
440
+ border-radius: 3px;
441
+ }
442
+
443
+ /* ================================================================
444
+ PRINT UTILITIES
445
+ ================================================================ */
446
+
447
+ .page-break {
448
+ page-break-before: always;
449
+ }
450
+
451
+ p, li, blockquote {
452
+ orphans: 3;
453
+ widows: 3;
454
+ }
455
+ `;
456
+ }
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildHtml = buildHtml;
4
+ const styles_1 = require("./styles");
5
+ function buildHtml(body, title) {
6
+ return `<!DOCTYPE html>
7
+ <html lang="pt-BR">
8
+ <head>
9
+ <meta charset="UTF-8">
10
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
11
+ <title>${escapeHtml(title)}</title>
12
+ <link rel="preconnect" href="https://fonts.googleapis.com">
13
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
14
+ <link href="https://fonts.googleapis.com/css2?family=Alexandria:ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;1,400&display=swap" rel="stylesheet">
15
+ <style>${(0, styles_1.getStyles)()}</style>
16
+ </head>
17
+ <body>
18
+ <div class="content">
19
+ ${body}
20
+ </div>
21
+ <script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
22
+ <script>
23
+ mermaid.initialize({ startOnLoad: false, theme: 'neutral' });
24
+ mermaid.run({ querySelector: '.mermaid' }).then(() => {
25
+ window.__mermaidDone = true;
26
+ }).catch(() => {
27
+ window.__mermaidDone = true;
28
+ });
29
+ </script>
30
+ </body>
31
+ </html>`;
32
+ }
33
+ function escapeHtml(text) {
34
+ return text
35
+ .replace(/&/g, '&amp;')
36
+ .replace(/</g, '&lt;')
37
+ .replace(/>/g, '&gt;');
38
+ }
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "epicora-pdf",
3
+ "version": "1.0.0",
4
+ "description": "Converte documentos Markdown em PDF com identidade visual Epicora",
5
+ "main": "dist/cli.js",
6
+ "bin": {
7
+ "epicora-pdf": "./bin/epicora-pdf"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "dev": "ts-node src/cli.ts",
12
+ "prepare": "tsc"
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "bin",
17
+ "assets"
18
+ ],
19
+ "keywords": [
20
+ "pdf",
21
+ "markdown",
22
+ "cli",
23
+ "converter",
24
+ "epicora",
25
+ "html-to-pdf",
26
+ "playwright"
27
+ ],
28
+ "author": {
29
+ "name": "Epicora",
30
+ "email": "pedro.cunha@epicora.com.br",
31
+ "url": "https://epicora.com.br"
32
+ },
33
+ "license": "MIT",
34
+ "engines": {
35
+ "node": ">=18.0.0"
36
+ },
37
+ "dependencies": {
38
+ "commander": "^12.1.0",
39
+ "highlight.js": "^11.10.0",
40
+ "marked": "^12.0.0",
41
+ "marked-highlight": "^2.1.4",
42
+ "playwright": "^1.47.0"
43
+ },
44
+ "devDependencies": {
45
+ "@types/node": "^22.0.0",
46
+ "ts-node": "^10.9.2",
47
+ "typescript": "^5.5.0"
48
+ }
49
+ }