import fs from 'fs/promises'; import path from 'path'; import { Command } from 'commander'; import hljs from 'highlight.js'; import MarkdownIt from 'markdown-it'; import markdownItAnchor from 'markdown-it-anchor'; /** * Options for Markdown to HTML conversion */ export interface MarkdownHtmlAgentOptions { /** Document title displayed in header */ title: string; /** Markdown content as string */ markdown: string; /** Date string to display; defaults to system date */ date?: string; /** Path to logo SVG file */ logoPath?: string; } /** * Converts Markdown content to an HTML string with FTPROD branding */ export async function markdownHtmlAgent( opts: MarkdownHtmlAgentOptions ): Promise { const { title, markdown, date: dateOpt, logoPath } = opts; const dateString = dateOpt || new Date().toLocaleDateString('fr-FR'); let logoDataUri = ''; // Always include default FTPROD logo if no custom path provided const logoFilePath = logoPath ? (path.isAbsolute(logoPath) ? logoPath : path.resolve(process.cwd(), logoPath)) : path.resolve(__dirname, '../../assets/logo.svg'); try { const content = await fs.readFile(logoFilePath); const ext = path.extname(logoFilePath).toLowerCase().slice(1); const mime = ext === 'svg' ? 'image/svg+xml' : ext === 'png' ? 'image/png' : 'application/octet-stream'; logoDataUri = `data:${mime};base64,${content.toString('base64')}`; } catch { // ignore missing logo, proceed without image } const md = new MarkdownIt({ html: true, linkify: true, typographer: true, highlight: (str: string, lang: string): string => { if (lang && hljs.getLanguage(lang)) { return `
${hljs.highlight(str, { language: lang }).value}
`; } return `
${md.utils.escapeHtml(str)}
`; }, }); md.use(markdownItAnchor, { slugify: (s: string) => s.trim().toLowerCase().replace(/[^\w]+/g, '-').replace(/^-+|-+$/g, '') }); function generateTOCHTML(src: string): string { const lines = src.split(/\r?\n/); const heads: { level: number; text: string; slug: string }[] = []; for (const line of lines) { const m = line.match(/^(#{1,6})\s+(.*)/); if (m) { const level = m[1].length; const text = m[2].trim(); const slug = text.toLowerCase().replace(/[^\w]+/g, '-').replace(/^-+|-+$/g, ''); heads.push({ level, text, slug }); } } if (heads.length <= 3) { return ''; } let html = ''; return html; } // Remove first Markdown title from body since we use a cover page let bodyMarkdown = markdown; if (/^#\s/m.test(markdown.trimStart())) { const lines = markdown.split(/\r?\n/); lines.shift(); bodyMarkdown = lines.join('\n'); } const tocHtml = generateTOCHTML(bodyMarkdown); const contentHtml = md.render(bodyMarkdown); // Cover page for printed output const coverHtml = `
${logoDataUri ? `FTPROD logo` : ''}

${title}

${dateString}

`; const html = ` ${coverHtml} ${tocHtml}
${contentHtml} `; return html; } // CLI registration for markdownHtmlAgent export const cli = { command: 'markdown-to-html', description: 'Convertit un fichier Markdown en HTML avec branding FTPROD', builder: (cmd: Command) => cmd .argument('', 'Chemin vers le fichier Markdown') .argument('', 'Chemin de sortie du fichier HTML') .option('--title ', 'Titre du document', 'Document FTPROD') .option('--date <date>', 'Date à afficher (format libre)'), handler: async (markdownFile: string, outputFile: string, opts: any) => { try { const mdContent = await fs.readFile(markdownFile, 'utf-8'); // eslint-disable-next-line @typescript-eslint/no-var-requires const { markdownHtmlAgent: run } = require('./markdownHtmlAgent'); const html = await run({ title: opts.title, markdown: mdContent, date: opts.date }); await fs.writeFile(outputFile, html, 'utf-8'); console.log(`HTML généré: ${outputFile}`); } catch (err: unknown) { const msg = err instanceof Error ? err.message : String(err); console.error('Erreur génération HTML:', msg); process.exit(1); } }, };