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 +167 -0
- package/README.pdf +0 -0
- package/assets/epicora.png +0 -0
- package/dist/cli.js +87 -0
- package/dist/markdown.js +35 -0
- package/dist/renderer.js +109 -0
- package/dist/styles.js +456 -0
- package/dist/template.js +38 -0
- package/package.json +49 -0
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();
|
package/dist/markdown.js
ADDED
|
@@ -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
|
+
}
|
package/dist/renderer.js
ADDED
|
@@ -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
|
+
}
|
package/dist/template.js
ADDED
|
@@ -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, '&')
|
|
36
|
+
.replace(/</g, '<')
|
|
37
|
+
.replace(/>/g, '>');
|
|
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
|
+
}
|