Compare commits

..
4 Commits
Author SHA1 Message Date
hykocx 39089e2f1d docs: simplify and clarify README content
Reword descriptions for conciseness, add a prerequisites section
for Node.js, and remove the dependencies table to reduce noise.
2026-04-15 11:40:09 -04:00
hykocx 0f09b30164 feat: add markdown-to-pdf converter with Puppeteer
Introduce a Node.js CLI tool that converts Markdown files to styled
PDFs using `marked` for parsing and `puppeteer` for rendering.

- Add `md-to-pdf.js` as the main conversion script with:
  - CLI argument handling for input/output file paths
  - Automatic output directory creation alongside the input file
  - Pre-processing to strip thematic breaks (`---`) before headings
  - Embedded CSS for clean, print-ready styling (headings, tables,
    code blocks, blockquotes, page breaks, etc.)
  - PDF generation via Puppeteer with A4 format and custom margins
- Add `.gitignore` to exclude `node_modules/` and `output/` directories
- Add `package-lock.json` with resolved dependencies (marked, puppeteer)
2026-04-15 11:28:16 -04:00
hykocx ffeaea1f38 docs: add full README with usage and feature documentation
Add comprehensive README in French covering installation, CLI usage
with optional output path, feature list (pagination, table styling,
code blocks, blockquotes), and a dependency table for `marked` and
`puppeteer`.
2026-04-15 11:28:01 -04:00
hykocx 76ddecb315 Initial commit 2026-04-15 10:20:25 -04:00
5 changed files with 1450 additions and 1 deletions
+2
View File
@@ -0,0 +1,2 @@
node_modules/
output/
+34 -1
View File
@@ -1,3 +1,36 @@
# markdown-to-pdf
Convertit un fichier Markdown en PDF bien mis en forme.
Convertit un fichier Markdown en PDF bien mis en forme. Fond blanc, typographie soignée, sans coupures de page au milieu des tableaux ou des blocs de code.
## Prérequis
Node.js installé.
## Installation
```bash
npm install
```
## Utilisation
```bash
node md-to-pdf.js <fichier.md>
```
Le PDF est généré dans `output/<fichier>.pdf`. Le dossier `output/` est créé automatiquement s'il n'existe pas.
Pour choisir un chemin de sortie différent :
```bash
node md-to-pdf.js rapport.md /tmp/rapport.pdf
```
## Ce que le script gère
- Tableaux, blocs de code, citations et images jamais coupés entre deux pages
- Les titres ne se retrouvent pas seuls en bas de page
- Lignes alternées sur les tableaux
- Code inline et en blocs avec police monospace et fond discret
- Citations avec bordure bleue à gauche
- Format A4 avec marges adaptées
+243
View File
@@ -0,0 +1,243 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const { marked } = require('marked');
const puppeteer = require('puppeteer');
const inputFile = process.argv[2];
if (!inputFile) {
console.error('Usage: node md-to-pdf.js <input.md> [output/custom.pdf]');
process.exit(1);
}
if (!fs.existsSync(inputFile)) {
console.error(`File not found: ${inputFile}`);
process.exit(1);
}
const outputDir = path.resolve(path.dirname(inputFile), 'output');
if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });
const baseName = path.basename(inputFile, path.extname(inputFile)) + '.pdf';
const outputFile = process.argv[3]
? path.resolve(process.argv[3])
: path.join(outputDir, baseName);
const rawMarkdown = fs.readFileSync(inputFile, 'utf8');
// Remove a thematic break (---) when it is immediately followed by a heading
// (with only blank lines in between), so it doesn't render as <hr>.
const markdown = rawMarkdown.replace(/^---[ \t]*$(\n[ \t]*)*(?=^#{1,6}\s)/gm, '');
const body = marked(markdown);
const html = `<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<style>
*, *::before, *::after {
box-sizing: border-box;
}
html, body {
margin: 0;
padding: 0;
background: #ffffff;
color: #1a1a1a;
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
font-size: 14px;
line-height: 1.7;
}
.page {
max-width: 860px;
margin: 0 auto;
padding: 40px 50px;
}
/* ---------- Headings ---------- */
h1, h2, h3, h4, h5, h6 {
font-weight: 700;
line-height: 1.3;
margin-top: 2em;
margin-bottom: 0.5em;
break-after: avoid;
}
.page > h1:first-child,
.page > h2:first-child,
.page > h3:first-child {
margin-top: 0;
}
h1 { font-size: 2em; border-bottom: 2px solid #e0e0e0; padding-bottom: 0.3em; }
h2 { font-size: 1.5em; border-bottom: 1px solid #e0e0e0; padding-bottom: 0.2em; }
h3 { font-size: 1.2em; }
h4 { font-size: 1em; }
/* ---------- Paragraphs & text ---------- */
p { margin: 0.6em 0; }
a { color: #0969da; text-decoration: none; }
a:hover { text-decoration: underline; }
strong { font-weight: 700; }
em { font-style: italic; }
/* ---------- Code ---------- */
code {
font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', Consolas, monospace;
font-size: 0.88em;
background: #f3f4f6;
border: 1px solid #e5e7eb;
border-radius: 4px;
padding: 0.15em 0.4em;
}
pre {
background: #f6f8fa;
border: 1px solid #d1d5db;
border-radius: 6px;
padding: 1em 1.2em;
overflow-x: auto;
break-inside: avoid;
margin: 1em 0;
}
pre code {
background: none;
border: none;
padding: 0;
font-size: 0.85em;
line-height: 1.6;
}
/* ---------- Blockquote ---------- */
blockquote {
margin: 1em 0;
padding: 0.6em 1.2em;
border-left: 4px solid #0969da;
background: #f0f6ff;
border-radius: 0 6px 6px 0;
break-inside: avoid;
color: #444;
}
blockquote p { margin: 0; }
/* ---------- Lists ---------- */
ul, ol {
margin: 0.6em 0;
padding-left: 1.8em;
}
li { margin: 0.25em 0; }
/* ---------- Horizontal rule ---------- */
hr {
border: none;
border-top: 1px solid #e0e0e0;
margin: 2em 0;
}
/* ---------- Tables ---------- */
table {
width: 100%;
border-collapse: collapse;
margin: 1em 0;
font-size: 0.92em;
break-inside: avoid;
}
thead {
background: #f0f1f3;
}
th, td {
border: 1px solid #d1d5db;
padding: 0.55em 0.9em;
text-align: left;
vertical-align: top;
}
th {
font-weight: 600;
color: #374151;
}
tbody tr:nth-child(even) {
background: #f9fafb;
}
/* ---------- Images ---------- */
img {
max-width: 100%;
height: auto;
border-radius: 6px;
break-inside: avoid;
}
/* ---------- Page-break hints ---------- */
p, li, blockquote, pre, img {
break-inside: avoid;
}
/* Avoid orphan headings at the bottom of a page */
h1 + *, h2 + *, h3 + *, h4 + *, h5 + *, h6 + * {
break-before: avoid;
}
</style>
</head>
<body>
<div class="page">
${body}
</div>
</body>
</html>`;
(async () => {
const browser = await puppeteer.launch({ args: ['--no-sandbox'] });
const page = await browser.newPage();
// Viewport très haut pour que getBoundingClientRect() retourne des coords absolues
await page.setViewport({ width: 794, height: 100000 });
await page.setContent(html, { waitUntil: 'networkidle0' });
// Si un h2 tombe dans la moitié basse d'une page A4, on le pousse à la page suivante.
// On simule la pagination avec un décalage cumulatif : chaque saut de page forcé
// avance les positions de tous les titres suivants, évitant l'effet cascade.
await page.evaluate(() => {
const MM_TO_PX = 96 / 25.4;
const PAGE_H = Math.round(297 * MM_TO_PX); // 1123px — hauteur brute A4
const MARGIN_TOP = Math.round(15 * MM_TO_PX); // 57px
const MARGIN_BOT = Math.round(15 * MM_TO_PX); // 57px
const CONTENT_H = PAGE_H - MARGIN_TOP - MARGIN_BOT; // ~1009px par page
let cumulativeShift = 0;
document.querySelectorAll('h2').forEach(h => {
// Position originale (layout écran, sans sauts de page CSS)
const originalY = h.getBoundingClientRect().top;
// Position effective après les sauts de page déjà décidés avant ce titre
const effectiveY = originalY + cumulativeShift;
const pageNum = Math.floor(effectiveY / CONTENT_H);
const posInPage = effectiveY % CONTENT_H;
if (posInPage > CONTENT_H / 2) {
h.style.pageBreakBefore = 'always';
// Ce titre est repoussé au début de la page suivante :
// on calcule le décalage ajouté et on l'accumule pour les titres suivants
const newEffectiveY = (pageNum + 1) * CONTENT_H;
cumulativeShift += newEffectiveY - effectiveY;
}
});
});
await page.pdf({
path: outputFile,
format: 'A4',
printBackground: true,
margin: { top: '15mm', bottom: '15mm', left: '10mm', right: '10mm' },
});
await browser.close();
console.log(`PDF generated: ${outputFile}`);
})();
+1150
View File
File diff suppressed because it is too large Load Diff
+21
View File
@@ -0,0 +1,21 @@
{
"name": "markdown-to-pdf",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "ssh://[email protected]/hykocx/markdown-to-pdf.git"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs",
"dependencies": {
"marked": "^18.0.0",
"puppeteer": "^24.40.0"
}
}