/** * Markdown Rendering Utility * Converts markdown text to styled React components */ import React from 'react'; /** * Parse inline markdown syntax (code, bold, italic, links) * Converts inline markdown elements within a text string to React components */ function parseInlineMarkdown(text: string): React.ReactNode { const parts: React.ReactNode[] = []; let remaining = text; let keyIndex = 0; while (remaining.length > 0) { // Inline code const codeMatch = remaining.match(/^`([^`]+)`/); if (codeMatch) { parts.push( {codeMatch[1]} ); remaining = remaining.slice(codeMatch[0].length); continue; } // Bold const boldMatch = remaining.match(/^\*\*([^*]+)\*\*/); if (boldMatch) { parts.push({boldMatch[1]}); remaining = remaining.slice(boldMatch[0].length); continue; } // Italic const italicMatch = remaining.match(/^\*([^*]+)\*/); if (italicMatch) { parts.push({italicMatch[1]}); remaining = remaining.slice(italicMatch[0].length); continue; } // Links const linkMatch = remaining.match(/^\[([^\]]+)\]\(([^)]+)\)/); if (linkMatch) { parts.push( {linkMatch[1]} ); remaining = remaining.slice(linkMatch[0].length); continue; } // Regular character const nextSpecial = remaining.search(/[`*\[]/); if (nextSpecial === -1) { parts.push(remaining); break; } else if (nextSpecial === 0) { parts.push(remaining[0]); remaining = remaining.slice(1); } else { parts.push(remaining.slice(0, nextSpecial)); remaining = remaining.slice(nextSpecial); } } return parts.length === 1 ? parts[0] : <>{parts}; } /** * Render markdown content to React components * Supports headers, lists, code blocks, tables, blockquotes, and inline formatting */ export function renderMarkdown(content: string): React.ReactNode { const lines = content.split('\n'); const elements: React.ReactNode[] = []; let i = 0; let listItems: { type: 'ul' | 'ol'; items: React.ReactNode[] } | null = null; let codeBlock: { language: string; lines: string[] } | null = null; let tableRows: string[][] | null = null; const flushList = () => { if (listItems) { if (listItems.type === 'ul') { elements.push( ); } else { elements.push(
    {listItems.items}
); } listItems = null; } }; const flushTable = () => { if (tableRows && tableRows.length > 0) { const header = tableRows[0]; const body = tableRows.slice(2); // Skip header separator row elements.push(
{header.map((cell, idx) => ( ))} {body.map((row, rowIdx) => ( {row.map((cell, cellIdx) => ( ))} ))}
{cell.trim()}
{cell.trim()}
); tableRows = null; } }; while (i < lines.length) { const line = lines[i]; // Code block start/end if (line.startsWith('```')) { if (codeBlock) { // End code block elements.push(
            
              {codeBlock.lines.join('\n')}
            
          
); codeBlock = null; } else { // Start code block flushList(); flushTable(); const language = line.slice(3).trim(); codeBlock = { language, lines: [] }; } i++; continue; } // Inside code block if (codeBlock) { codeBlock.lines.push(line); i++; continue; } // Table row if (line.startsWith('|') && line.endsWith('|')) { flushList(); const cells = line.slice(1, -1).split('|'); if (!tableRows) { tableRows = []; } tableRows.push(cells); i++; continue; } else if (tableRows) { flushTable(); } // Headers if (line.startsWith('#### ')) { flushList(); elements.push(

{parseInlineMarkdown(line.slice(5))}

); i++; continue; } if (line.startsWith('### ')) { flushList(); elements.push(

{parseInlineMarkdown(line.slice(4))}

); i++; continue; } if (line.startsWith('## ')) { flushList(); elements.push(

{parseInlineMarkdown(line.slice(3))}

); i++; continue; } if (line.startsWith('# ')) { flushList(); elements.push(

{parseInlineMarkdown(line.slice(2))}

); i++; continue; } // Unordered list items if (line.match(/^[-*]\s/)) { if (!listItems || listItems.type !== 'ul') { flushList(); listItems = { type: 'ul', items: [] }; } listItems.items.push(
  • {parseInlineMarkdown(line.replace(/^[-*]\s/, ''))}
  • ); i++; continue; } // Ordered list items if (line.match(/^\d+\.\s/)) { if (!listItems || listItems.type !== 'ol') { flushList(); listItems = { type: 'ol', items: [] }; } listItems.items.push(
  • {parseInlineMarkdown(line.replace(/^\d+\.\s/, ''))}
  • ); i++; continue; } // Checkbox list items if (line.match(/^[-*]\s\[[ x]\]/i)) { if (!listItems || listItems.type !== 'ul') { flushList(); listItems = { type: 'ul', items: [] }; } const checked = line.match(/^[-*]\s\[x\]/i); const itemContent = line.replace(/^[-*]\s\[[ x]\]\s?/i, ''); listItems.items.push(
  • {checked ? '✓' : '○'} {parseInlineMarkdown(itemContent)}
  • ); i++; continue; } // Horizontal rule if (line.match(/^[-*_]{3,}$/)) { flushList(); elements.push(
    ); i++; continue; } // Blockquote if (line.startsWith('> ')) { flushList(); elements.push(
    {parseInlineMarkdown(line.slice(2))}
    ); i++; continue; } // Empty line if (!line.trim()) { flushList(); i++; continue; } // Regular paragraph flushList(); elements.push(

    {parseInlineMarkdown(line)}

    ); i++; } // Flush any remaining items flushList(); flushTable(); return
    {elements}
    ; }