import React from "react"; /** * Parse inline markup: **bold** and *italic*. * Returns React elements safe from XSS. */ function parseInline(text: string, keyPrefix: string): React.ReactNode[] { const parts: React.ReactNode[] = []; const regex = /(\*\*(.+?)\*\*|\*(.+?)\*)/g; let lastIndex = 0; let match: RegExpExecArray | null; let key = 0; while ((match = regex.exec(text)) !== null) { if (match.index > lastIndex) { parts.push(text.slice(lastIndex, match.index)); } if (match[2]) { parts.push({match[2]}); } else if (match[3]) { parts.push({match[3]}); } lastIndex = match.index + match[0].length; } if (lastIndex < text.length) { parts.push(text.slice(lastIndex)); } return parts; } /** * Simple markup renderer. * Supports: **bold**, *italic*, ## headings, — bullet points, blank lines for paragraphs. */ export function formatMarkup(text: string): React.ReactNode { if (!text) return null; const lines = text.split("\n"); const elements: React.ReactNode[] = []; let key = 0; for (const line of lines) { const trimmed = line.trimStart(); // ## Heading if (trimmed.startsWith("## ")) { elements.push( {parseInline(trimmed.slice(3), `h${key}`)} ); continue; } // — Bullet point if (trimmed.startsWith("— ") || trimmed.startsWith("- ") || trimmed.startsWith("🤍 ")) { const content = trimmed.startsWith("🤍 ") ? trimmed.slice(3) : trimmed.slice(2); elements.push( {parseInline(content, `li${key}`)} ); continue; } // Empty line = paragraph break if (trimmed === "") { elements.push(); continue; } // Regular line with inline markup elements.push( {parseInline(line, `p${key}`)} ); } return <>{elements}; }