import React from 'react'; import { toast } from 'sonner'; import { copyToClipboard } from '@/utils/clipboard'; interface MarkdownLiteProps { children: string; className?: string; stopPropagation?: boolean; } // Supports [text](url), `code`, and line breaks (both \n and actual newlines) const MarkdownLite: React.FC = ({ children, className, stopPropagation = false, }) => { if (!children) return null; // Handle both literal \n strings and actual newlines let lines: string[]; if (children.includes('\\n')) { lines = children.split('\\n'); } else { lines = children.split('\n'); } // Group consecutive bullet points together const processedLines: React.ReactNode[] = []; let i = 0; while (i < lines.length) { const line = lines[i]; const trimmed = line.trim(); const headingMatch = trimmed.match(/^(#{1,3})\s+(.+)$/); const isBulletPoint = trimmed.startsWith('* ') && !trimmed.startsWith('**'); if (headingMatch) { const level = headingMatch[1].length; const text = headingMatch[2]; const headingClasses = [ 'text-base font-bold mt-3 mb-1', 'text-sm font-semibold mt-2.5 mb-1', 'text-sm font-medium mt-2 mb-0.5', ]; const tag = level === 1 ? 'h1' : level === 2 ? 'h2' : 'h3'; processedLines.push( React.createElement( tag, { key: `h-${i}`, className: headingClasses[level - 1] }, processInlineMarkdown(text, stopPropagation) ) ); i++; } else if (isBulletPoint) { // Collect all consecutive bullet points const bulletPoints: string[] = []; while ( i < lines.length && lines[i].trim().startsWith('* ') && !lines[i].trim().startsWith('**') ) { bulletPoints.push(lines[i]); i++; } // Render as a single list processedLines.push( ); } else { // Regular line if (i > 0) { processedLines.push(
); } processedLines.push(processLine(line, stopPropagation)); i++; } } return {processedLines}; }; // Helper function to process a single line for markdown function processLine(text: string, stopPropagation: boolean) { // Process regular line with inline markdown return processInlineMarkdown(text, stopPropagation); } // Helper function to process inline markdown (bold, italic, code, links) function processInlineMarkdown(text: string, stopPropagation: boolean) { // Regex for **bold**, *italic*, `code`, and [text](url) const regex = /(\*\*[^*]+\*\*|\*[^*]+\*|`[^`]+`|\[[^\]]+\]\([^\)]+\))/g; const parts = text.split(regex); const matches = text.match(regex) || []; let matchIdx = 0; return parts.map((part, i) => { if (i === parts.length - 1 && part === '') return null; if (i % 2 === 0) { // Plain text return part; } else { const match = matches[matchIdx++]; if (!match) return null; if (match.startsWith('**') && match.endsWith('**')) { // Bold text return ( {match.slice(2, -2)} ); } else if ( match.startsWith('*') && match.endsWith('*') && !match.startsWith('**') ) { // Italic text return ( {match.slice(1, -1)} ); } else if (match.startsWith('`') && match.endsWith('`')) { // Inline code return ( { if (stopPropagation) { e.stopPropagation(); } await copyToClipboard(match.slice(1, -1), { onSuccess: () => toast.success('Copied to clipboard'), onError: () => toast.error('Failed to copy to clipboard'), }); }} className="bg-muted px-1 py-0.5 rounded text-[--brand] font-mono text-xs break-all cursor-pointer" > {match.slice(1, -1)} ); } else { // Link const linkMatch = match.match(/\[([^\]]+)\]\(([^\)]+)\)/); if (linkMatch) { const [, text, url] = linkMatch; return ( stopPropagation && e.stopPropagation()} > {text} ); } } return match; } }); } export default MarkdownLite;