import fs from 'node:fs/promises'; import path from 'node:path'; import { ready, parse, ParseFlags } from './index.ts'; /** Options accepted by a template function. */ interface TemplateOptions { title?: string; toc?: string; } /** A template renders the converted HTML content into a full document. */ type Template = (content: string, options?: TemplateOptions) => string; /** * Available HTML templates for wrapping markdown output */ export const templates: Record = { default: (content, options = {}) => ` ${options.title || 'Document'}
${options.toc ? `` : ''} ${content}
`, minimal: content => ` Document ${content} `, plain: content => content, }; interface Heading { level: number; text: string; id: string; } /** Result of {@link generateTableOfContents} when headings were found. */ interface TableOfContents { toc: string; html: string; } /** * Extract headings from HTML content to generate a table of contents * * @param html HTML content * @param minLevel Minimum heading level (2-6) * @param maxLevel Maximum heading level (2-6) * @returns TOC + rewritten HTML (with heading ids), or an empty string when no headings matched. */ export function generateTableOfContents( html: string, minLevel = 2, maxLevel = 4 ): TableOfContents | '' { const headingRegex = /]*)?>(.+?)<\/h\1>/g; const headings: Heading[] = []; let match: RegExpExecArray | null; while ((match = headingRegex.exec(html)) !== null) { const level = parseInt(match[1]); if (level >= minLevel && level <= maxLevel) { const text = match[2].replace(/<[^>]+>/g, ''); // Remove any HTML tags const id = `heading-${headings.length}`; headings.push({ level, text, id }); } } if (headings.length === 0) { return ''; } // Replace heading IDs in HTML let modifiedHtml = html; headings.forEach(h => { const regex = new RegExp( `]*)>([^<]*${h.text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[^<]*)`, 'i' ); modifiedHtml = modifiedHtml.replace(regex, match => match.replace(` { while (currentLevel < h.level) { tocHtml += '
    '; currentLevel++; } while (currentLevel > h.level) { tocHtml += '
'; currentLevel--; } tocHtml += `
  • ${h.text}
  • `; }); tocHtml += ''.repeat(currentLevel - minLevel + 1); return { toc: tocHtml, html: modifiedHtml }; } /** Options accepted by the `md2html` command, as parsed by commander. */ export interface MarkdownConversionOptions { output?: string; template?: string; tocMin?: string | number; tocMax?: string | number; html?: boolean; } /** * Handle markdown conversion command */ export async function handleMarkdownConversion( input: string | undefined, options: MarkdownConversionOptions ): Promise { try { // Initialize the WebAssembly module await ready(); let markdown: string; // Read from stdin if no input file provided if (!input) { // Read from stdin const chunks: string[] = []; process.stdin.setEncoding('utf-8'); for await (const chunk of process.stdin) { chunks.push(chunk); } markdown = chunks.join(''); } else { // Read from file const inputPath = path.resolve(input); markdown = await fs.readFile(inputPath, 'utf-8'); } // Parse flags let parseFlags = ParseFlags.DEFAULT; // Allow HTML if specified if (!options.html) { parseFlags |= ParseFlags.NO_HTML_BLOCKS | ParseFlags.NO_HTML_SPANS; } // Convert markdown to HTML let html = parse(markdown, { parseFlags, verbatimEntities: true, xhtml: true, }) as string; // Generate table of contents if requested const tocMinLevel = parseInt(String(options.tocMin)) || 2; const tocMaxLevel = parseInt(String(options.tocMax)) || 4; let toc = ''; if (tocMinLevel <= tocMaxLevel) { const result = generateTableOfContents(html, tocMinLevel, tocMaxLevel); if (result && result.toc) { html = result.html; toc = result.toc; } } // Select template const templateFn = templates[options.template ?? ''] || templates.default; // Generate final HTML const title = input ? path.basename(input, path.extname(input)) : 'Document'; const finalHtml = templateFn(html, { title, toc: toc || undefined }); // Output result if (options.output) { const outputPath = path.resolve(options.output); await fs.writeFile(outputPath, finalHtml, 'utf-8'); console.log(`✓ Successfully converted to: ${outputPath}`); } else { // Output to stdout console.log(finalHtml); } } catch (error) { console.error(`Error: ${error instanceof Error ? error.message : error}`); process.exit(1); } } /** * Show available templates */ export async function showAvailableTemplates(): Promise { console.log('Available templates:\n'); Object.keys(templates).forEach(name => { console.log(` • ${name}`); }); console.log( '\nUse with -t or --template option. Example: md2html input.md -t minimal' ); }