UNPKG

trace.ai-cli

Version:

A powerful AI-powered CLI tool

188 lines (161 loc) 5.69 kB
// markdown.js // Minimal Markdown renderer (safe): supports **bold**, `code`, bullet lists, and fenced code blocks ```lang // Additionally provides a terminal-friendly ANSI renderer for Node CLI. function escapeHtml(s) { return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); } // Terminal-friendly Markdown to ANSI using chalk (Node only) function markdownToAnsi(md) { if (!md) return ''; const chalk = (typeof window === 'undefined') ? require('chalk') : null; if (!chalk) return md; // Fallback if chalk not available const lines = md.replace(/\r\n?/g, '\n').split('\n'); let out = ''; let inCode = false; let codeLines = []; const flushCode = () => { if (!inCode) return; const codeText = codeLines.join('\n'); out += '\n' + chalk.gray('┌─ code ───────────────────────────────') + '\n'; out += chalk.white(codeText) + '\n'; out += chalk.gray('└──────────────────────────────────────') + '\n'; inCode = false; codeLines = []; }; const processInlineFormatting = (text) => { return text .replace(/\*\*(.+?)\*\*/g, (_, t) => chalk.bold(t)) .replace(/`([^`]+)`/g, (_, t) => chalk.black.bgYellow(` ${t} `)); }; for (let raw of lines) { const line = raw.trimEnd(); const fenceMatch = /^```/.test(line); if (fenceMatch) { if (!inCode) { inCode = true; codeLines = []; continue; } else { flushCode(); continue; } } if (inCode) { codeLines.push(raw); continue; } // Handle bullet lists const bullet = /^\s*([*-])\s+(.+)$/.exec(line); if (bullet) { const item = processInlineFormatting(bullet[2]); out += chalk.blue('• ') + item + '\n'; continue; } // Handle empty lines if (line.trim() === '') { out += '\n'; continue; } // Handle regular paragraphs const para = processInlineFormatting(line); out += para + '\n'; } flushCode(); return out; } function markdownToHtml(md) { if (!md) return ''; // Normalize line endings md = md.replace(/\r\n?/g, '\n'); const lines = md.split('\n'); let html = ''; let inList = false; let inCode = false; let codeLang = ''; let codeLines = []; const flushList = () => { if (inList) { html += '</ul>'; inList = false; } }; for (let raw of lines) { let line = raw.trimEnd(); // Fenced code blocks const fenceMatch = /^```\s*([A-Za-z0-9_+-]*)\s*$/.exec(line); if (fenceMatch) { if (!inCode) { // starting code block flushList(); inCode = true; codeLang = fenceMatch[1] || ''; codeLines = []; } else { // ending code block -> flush code block const codeText = codeLines.join('\n'); const codeEsc = escapeHtml(codeText); const langClass = codeLang ? `language-${codeLang}` : ''; html += `<div class="code-block">\n<button class="copy-btn" title="Copy code" aria-label="Copy code">Copy</button><pre><code class="${langClass}">${codeEsc}</code></pre></div>`; inCode = false; codeLang = ''; codeLines = []; } continue; } if (inCode) { codeLines.push(raw); // preserve exact indentation inside code continue; } const bulletMatch = /^\s*([*-])\s+(.+)$/.exec(line); if (bulletMatch) { if (!inList) { html += '<ul>'; inList = true; } const item = bulletMatch[2]; const itemEsc = escapeHtml(item) .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>') .replace(/`([^`]+)`/g, '<code>$1</code>'); html += `<li>${itemEsc}</li>`; continue; } // Empty line -> paragraph break if (line.trim() === '') { flushList(); html += '<br/>'; continue; } // Normal paragraph line flushList(); const para = escapeHtml(line) .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>') .replace(/`([^`]+)`/g, '<code>$1</code>'); html += `<p>${para}</p>`; } flushList(); // If code block was never closed, flush it anyway if (inCode) { const codeText = codeLines.join('\n'); const codeEsc = escapeHtml(codeText); const langClass = codeLang ? `language-${codeLang}` : ''; html += `<div class="code-block"><button class="copy-btn" title="Copy code" aria-label="Copy code">📋</button><pre><code class="${langClass}">${codeEsc}</code></pre></div>`; } return html; } // Optional: centralize copy button handler so renderer.js stays clean function initMarkdownCopyHandler() { document.addEventListener('click', async (e) => { const btn = e.target.closest('.copy-btn'); if (!btn) return; try { const wrapper = btn.closest('.code-block'); const codeEl = wrapper ? wrapper.querySelector('code') : null; const codeText = codeEl ? codeEl.textContent : ''; if (!codeText) return; await navigator.clipboard.writeText(codeText); const prev = btn.textContent; btn.textContent = 'Copied!'; setTimeout(() => { btn.textContent = prev; }, 1200); } catch (_) { } }); } // Expose to browser global when available if (typeof window !== 'undefined') { window.markdownToHtml = markdownToHtml; window.initMarkdownCopyHandler = initMarkdownCopyHandler; } // Support CommonJS environments if available (harmless in extension builds) if (typeof module !== 'undefined' && module.exports) { module.exports = { markdownToHtml, initMarkdownCopyHandler, markdownToAnsi }; }