/** * Terminal markdown renderer. * Converts markdown text to chalk-formatted terminal output. */ import chalk from "chalk" interface RenderOptions { maxWidth?: number indent?: string } export function renderMarkdown(text: string, options?: RenderOptions): string { const lines = text.split("\n") const result: string[] = [] for (const line of lines) { result.push(renderLine(line, options)) } return result.join("\n") } function renderLine(line: string, options?: RenderOptions): string { const trimmed = line.trimStart() // Headings if (trimmed.startsWith("###### ")) return chalk.bold.cyan(trimmed.slice(7)) if (trimmed.startsWith("##### ")) return chalk.bold.cyan(trimmed.slice(6)) if (trimmed.startsWith("#### ")) return chalk.bold.cyan(trimmed.slice(5)) if (trimmed.startsWith("### ")) return chalk.bold.cyan(trimmed.slice(4)) if (trimmed.startsWith("## ")) return chalk.bold.blue(trimmed.slice(3)) if (trimmed.startsWith("# ")) return chalk.bold.white(trimmed.slice(2)) // Code blocks if (trimmed.startsWith("```")) { return chalk.dim(trimmed) } // Blockquotes if (trimmed.startsWith("> ")) { return chalk.dim("│ " + renderInline(trimmed.slice(2))) } // Lists if (/^[-*+]\s/.test(trimmed)) { return chalk.cyan("•") + " " + renderInline(trimmed.slice(2)) } if (/^\d+\.\s/.test(trimmed)) { const match = trimmed.match(/^(\d+\.)\s(.*)$/) if (match) return chalk.cyan(match[1]) + " " + renderInline(match[2]) } // Horizontal rule if (/^[-*_]{3,}\s*$/.test(trimmed)) { return chalk.dim("─".repeat(options?.maxWidth ?? 60)) } return renderInline(line) } function renderInline(text: string): string { // Bold text = text.replace(/\*\*(.+?)\*\*/g, (_, content) => chalk.bold(content)) // Italic text = text.replace(/(? chalk.italic(content)) // Inline code text = text.replace(/`([^`]+)`/g, (_, content) => chalk.bgBlackBright(content)) // Links text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, url) => chalk.blue.underline(label) + chalk.dim(` (${url})`)) return text } export function renderCodeBlock(code: string, language?: string): string { const header = language ? chalk.dim(`─ ${language} ─`) : "" const lines = code.split("\n").map((line, i) => { const num = String(i + 1).padStart(4) return chalk.dim(`${num} │`) + ` ${line}` }) return header + (header ? "\n" : "") + lines.join("\n") } export function renderTable(headers: string[], rows: string[][]): string { const colWidths = headers.map((h, i) => { const maxDataLen = Math.max(...rows.map((r) => (r[i] ?? "").length)) return Math.max(h.length, maxDataLen) + 2 }) const headerLine = headers.map((h, i) => chalk.bold(h.padEnd(colWidths[i]))).join("") const separator = colWidths.map((w) => chalk.dim("─".repeat(w))).join(chalk.dim("─")) const dataLines = rows.map((row) => row.map((cell, i) => (cell ?? "").padEnd(colWidths[i])).join(""), ) return [headerLine, separator, ...dataLines].join("\n") }