const fs = require('fs'); const path = require('path'); const { marked } = require('marked'); const { markedHighlight } = require('marked-highlight'); const hljs = require('highlight.js'); marked.use(markedHighlight({ langPrefix: 'hljs language-', highlight(code, lang) { const language = hljs.getLanguage(lang) ? lang : 'plaintext'; return hljs.highlight(code, { language }).value; }, })); // ───────────────────────────────────────────────────────────────────────────── // CSS — inspiré de https://github.com/PavelLaptev/markdown-theme-shell // ───────────────────────────────────────────────────────────────────────────── const STYLES = ` /* ── Reset ── */ *, *::before, *::after { box-sizing: border-box; } ::selection { background: #ffc3c3; } html, body { height: 100%; margin: 0; } body { font-family: 'Poppins', sans-serif; background: #fff; color: #222; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; overflow-x: hidden; } /* ── Sidebar ── */ .sidebar { position: fixed; top: 0; left: 0; width: 264px; height: 100vh; background: #1a1b2e; display: flex; flex-direction: column; z-index: 99; overflow: hidden; } .sidebar__title-block { margin: 48px 24px 8px; flex-shrink: 0; } .sidebar__title-block a { text-decoration: none; } .sidebar__title-block__title { color: #fff; font-size: 26px; font-weight: 700; margin: 0 0 4px; line-height: 1.25; pointer-events: none; } .sidebar__title-block__version { color: rgba(255,255,255,0.3); font-size: 14px; font-weight: 700; display: block; margin-bottom: 24px; } /* ── Sidebar menu ── */ .sidebar__menu { list-style: none; margin: 0; padding: 8px 0 48px 0; flex: 1; overflow-y: auto; } .sidebar__menu::-webkit-scrollbar { width: 0; } /* First-level = section group label (non-clickable) */ .sidebar__menu__group-label { display: block; padding: 16px 24px 6px; color: rgba(255,255,255,0.35); font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; } /* First-level links (root files or group header links) */ .sidebar__menu__first-level-title { margin: 0; } .sidebar__menu__first-level-title__link { display: block; padding: 7px 24px; color: rgba(255,255,255,0.85); text-decoration: none; font-size: 16px; font-weight: 400; line-height: 1.4; transition: color 0.2s; } .sidebar__menu__first-level-title__link:hover { color: #ff5577; } .sidebar__menu__first-level-title__link.active { color: #ff5577; font-weight: 500; } /* Second-level links (files inside a group) */ .sidebar__menu__second-level { list-style: none; padding: 0; margin: 0; } .sidebar__menu__second-level-title { margin: 0; } .sidebar__menu__second-level-title__link { display: block; padding: 5px 24px 5px 36px; color: rgba(255,255,255,0.55); text-decoration: none; font-size: 14px; font-weight: 400; line-height: 1.4; transition: color 0.2s; } .sidebar__menu__second-level-title__link:hover { color: #ff5577; } .sidebar__menu__second-level-title__link.active { color: #ff5577; opacity: 1; } /* ── Content ── */ .content { margin-left: 264px; padding: 48px 72px 96px; max-width: 1080px; min-height: 100vh; background: #fff; } /* ── Typography ── */ .content a { color: #0000ee; text-decoration: none; transition: color 0.16s; } .content a:hover { color: #ff5577; } .content a:visited { color: #551a8b; } .content a:visited:hover { color: #ff5577; } .content h1, .content h2, .content h3, .content h4, .content h5, .content h6 { color: #222; margin-top: 48px; margin-bottom: 12px; line-height: 1.3; } .content h1 { font-size: 2em; margin-top: 0; } .content h2 { font-size: 1.5em; } .content h3 { font-size: 1.25em; } .content hr { margin: 72px 0; border: none; border-top: 1px solid rgba(34,34,34,0.15); } .content p { color: #222; font-size: 1em; line-height: 1.64em; margin: 0 0 1em; } .content code { font-family: 'Roboto Mono', 'SFMono-Regular', Consolas, monospace; font-size: 85%; font-weight: 700; background: #f4f4f7; padding: 0.15em 0.4em; border-radius: 4px; line-height: 1.4; } .content pre { background: #f4f4f7; padding: 20px 24px; border-radius: 6px; overflow-x: auto; margin: 16px 0; } .content pre code { background: none; padding: 0; font-weight: 400; font-size: 13px; } .content blockquote { border-left: 3px solid rgba(34,34,34,0.25); margin: 24px 0; padding: 4px 24px; color: rgba(34,34,34,0.65); } .content img { max-width: 100%; margin: 8px 0; } .content table { width: 100%; border-spacing: 0; border-collapse: collapse; margin: 24px 0 48px; font-size: 14px; color: #222; } .content table th { border-bottom: 1px solid #ff5577; padding: 10px; text-align: left; font-weight: 600; } .content table td { border-bottom: 1px solid rgba(255,85,119,0.25); padding: 10px; } .content ul li, .content ol li { color: #222; font-size: 1em; line-height: 1.64em; margin-bottom: 8px; } .content ul li code, .content ol li code { font-weight: 700; } /* ── Responsive ── */ @media screen and (max-width: 1020px) { .sidebar { left: -300px; transition: left 0.2s cubic-bezier(0.09, 0.46, 0.45, 0.94); } .content { margin-left: 0; padding: 32px 24px 64px; } } @media screen and (max-width: 540px) { .content { padding: 24px 16px 48px; } }`; const SSE_SCRIPT = ` `; // ───────────────────────────────────────────────────────────────────────────── // Helpers // ───────────────────────────────────────────────────────────────────────────── function escapeHtml(str) { return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } /** Build the sidebar HTML from the list of all pages (Shell theme structure). */ function buildSidebar(baseDir, files, currentFile) { const currentRel = currentFile ? path.relative(baseDir, currentFile) : null; // Group by first-level subdirectory const groups = new Map(); // groupName → [rel, ...] files.forEach((f) => { const rel = path.relative(baseDir, f); const parts = rel.split(path.sep); const group = parts.length > 1 ? parts[0] : ''; if (!groups.has(group)) groups.set(group, []); groups.get(group).push(rel); }); const dirName = path.basename(baseDir); let html = ` `; return html; } // ───────────────────────────────────────────────────────────────────────────── // Shell // ───────────────────────────────────────────────────────────────────────────── function htmlShell({ title, sidebar, body, watchMode }) { const hasSidebar = !!sidebar; const noSidebarStyle = hasSidebar ? '' : ` `; return ` ${escapeHtml(title)} ${noSidebarStyle} ${hasSidebar ? sidebar : ''}
${body}
${watchMode ? SSE_SCRIPT : ''} `; } // ───────────────────────────────────────────────────────────────────────────── // Public API // ───────────────────────────────────────────────────────────────────────────── /** * Convert a single .md file to a full HTML page. * @param {string} inputFile Absolute path to the .md file * @param {boolean} watchMode Inject SSE live-reload script * @param {{ baseDir: string, files: string[] }} [nav] When set, render with sidebar */ function convert(inputFile, watchMode = false, nav = null) { const content = fs.readFileSync(inputFile, 'utf-8'); const body = marked.parse(content); const title = path.basename(inputFile, '.md'); const sidebar = nav ? buildSidebar(nav.baseDir, nav.files, inputFile) : null; return htmlShell({ title, sidebar, body, watchMode }); } /** * Build an index/home page listing all .md files. * Only used when no Home.md exists. */ function buildIndex(baseDir, files, watchMode = false) { const dirName = path.basename(baseDir); const sidebar = buildSidebar(baseDir, files, null); const groups = new Map(); files.forEach((f) => { const rel = path.relative(baseDir, f); const parts = rel.split(path.sep); const group = parts.length > 1 ? parts[0] : ''; if (!groups.has(group)) groups.set(group, []); groups.get(group).push(rel); }); const listHtml = [...groups.keys()].sort().map((group) => { const items = groups.get(group).sort().map((rel) => { const pagePath = rel.replace(/\.md$/i, ''); const label = path.basename(pagePath).replace(/-/g, ' '); const href = '/' + pagePath.split('/').map(encodeURIComponent).join('/'); return `
  • ${escapeHtml(label)}
  • `; }).join('\n'); if (group === '') return ``; return `

    ${escapeHtml(group)}

    \n`; }).join('\n'); const body = `

    📂 ${escapeHtml(dirName)}

    ${files.length} fichier${files.length > 1 ? 's' : ''} Markdown

    ${listHtml}`; return htmlShell({ title: dirName, sidebar, body, watchMode }); } module.exports = { convert, buildIndex };