const http = require('http'); const fs = require('fs'); const path = require('path'); const { convert, buildIndex } = require('./converter'); const MIME_TYPES = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.svg': 'image/svg+xml', '.webp': 'image/webp', '.css': 'text/css', '.js': 'application/javascript', '.ico': 'image/x-icon', }; /** Recursively find all .md files under a directory. */ function findMarkdownFiles(dir) { const results = []; for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { if (entry.name.startsWith('.')) continue; const full = path.join(dir, entry.name); if (entry.isDirectory()) { results.push(...findMarkdownFiles(full)); } else if (/\.md$/i.test(entry.name)) { results.push(full); } } return results; } function notifyClients(clients) { const dead = []; clients.forEach((res) => { try { res.write('data: reload\n\n'); } catch { dead.push(res); } }); return clients.filter((c) => !dead.includes(c) && !c.writableEnded); } function startServer({ inputFile, inputDir, port }) { let clients = []; const isDir = !!inputDir; const baseDir = isDir ? path.resolve(inputDir) : path.dirname(path.resolve(inputFile)); // --- rebuild --- function rebuild(changedFile) { if (changedFile) { const rel = path.relative(baseDir, changedFile); console.log(` ↺ ${rel} — rechargement...`); } clients = notifyClients(clients); } // --- request handler --- const server = http.createServer((req, res) => { const url = new URL(req.url, `http://localhost:${port}`); const pathname = decodeURIComponent(url.pathname); // SSE endpoint if (pathname === '/events') { res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive', }); res.write(': keep-alive\n\n'); clients.push(res); const heartbeat = setInterval(() => { if (res.writableEnded) { clearInterval(heartbeat); return; } res.write(': ping\n\n'); }, 15000); req.on('close', () => { clearInterval(heartbeat); clients = clients.filter((c) => c !== res); }); return; } // ── SINGLE FILE MODE ────────────────────────────────────────────────────── if (!isDir) { if (pathname === '/' || pathname === '/index.html') { res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end(convert(inputFile, true)); return; } return serveStatic(req, res, pathname, baseDir, port); } // ── DIRECTORY MODE ──────────────────────────────────────────────────────── // Index page if (pathname === '/') { const files = findMarkdownFiles(baseDir); // Redirect to Home.md if it exists const homePage = files.find((f) => /^home\.md$/i.test(path.basename(f)) && path.dirname(f) === baseDir ); if (homePage) { const slug = path.relative(baseDir, homePage).replace(/\.md$/i, ''); res.writeHead(302, { Location: '/' + slug.split('/').map(encodeURIComponent).join('/') }); res.end(); return; } res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end(buildIndex(baseDir, files, true)); return; } // Try to serve a markdown page: /Some-Page → Some-Page.md const pagePath = pathname.replace(/^\//, ''); const candidates = [ path.join(baseDir, pagePath + '.md'), path.join(baseDir, pagePath + '.MD'), path.join(baseDir, pagePath), // already has extension path.join(baseDir, pagePath, 'index.md'), ]; for (const candidate of candidates) { // Security: block path traversal if (!candidate.startsWith(baseDir + path.sep) && candidate !== baseDir) continue; if (fs.existsSync(candidate) && /\.md$/i.test(candidate)) { try { const files = findMarkdownFiles(baseDir); const html = convert(candidate, true, { baseDir, files }); res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end(html); } catch (err) { res.writeHead(500); res.end(`Erreur de conversion : ${err.message}`); } return; } } // Fall back to serving a static asset serveStatic(req, res, pathname, baseDir, port); }); server.listen(port, () => { console.log(`\n🚀 Serveur démarré : http://localhost:${port}`); if (isDir) { console.log(`📂 Dossier : ${baseDir}`); } console.log('👁 Surveillance des modifications... (Ctrl+C pour arrêter)\n'); }); server.on('error', (err) => { if (err.code === 'EADDRINUSE') { console.error(`\n✗ Erreur : le port ${port} est déjà utilisé. Essayez --port `); process.exit(1); } throw err; }); return { server, rebuild }; } function serveStatic(req, res, pathname, baseDir, port) { const safePath = path.normalize(pathname).replace(/^(\.\.[/\\])+/, ''); const filePath = path.join(baseDir, safePath); if (!filePath.startsWith(baseDir + path.sep) && filePath !== baseDir) { res.writeHead(403); res.end('Forbidden'); return; } fs.readFile(filePath, (err, data) => { if (err) { res.writeHead(404); res.end('Not found'); return; } const ext = path.extname(filePath).toLowerCase(); res.writeHead(200, { 'Content-Type': MIME_TYPES[ext] || 'application/octet-stream' }); res.end(data); }); } module.exports = { startServer };