import React from "react"; type MarkdownMessageProps = { content: string; }; type MarkdownBlock = | { type: "paragraph"; content: string } | { type: "heading"; level: number; content: string } | { type: "unordered-list"; items: string[] } | { type: "ordered-list"; items: string[] } | { type: "blockquote"; content: string } | { type: "code"; language: string; content: string }; export default function MarkdownMessage({ content }: MarkdownMessageProps) { const blocks = parseMarkdown(content); return (
{blocks.map((block, index) => renderBlock(block, index))}
); } function parseMarkdown(source: string): MarkdownBlock[] { const normalized = source.replace(/\r\n/g, "\n"); const lines = normalized.split("\n"); const blocks: MarkdownBlock[] = []; let paragraphLines: string[] = []; const flushParagraph = () => { if (!paragraphLines.length) return; blocks.push({ type: "paragraph", content: paragraphLines.join("\n").trim(), }); paragraphLines = []; }; for (let index = 0; index < lines.length; index += 1) { const line = lines[index]; const trimmed = line.trim(); if (!trimmed) { flushParagraph(); continue; } const codeFenceMatch = trimmed.match(/^```([^`]*)$/); if (codeFenceMatch) { flushParagraph(); const codeLines: string[] = []; let cursor = index + 1; while (cursor < lines.length && !lines[cursor].trim().startsWith("```")) { codeLines.push(lines[cursor]); cursor += 1; } blocks.push({ type: "code", language: codeFenceMatch[1].trim(), content: codeLines.join("\n"), }); index = cursor; continue; } const headingMatch = line.match(/^(#{1,6})\s+(.+)$/); if (headingMatch) { flushParagraph(); blocks.push({ type: "heading", level: headingMatch[1].length, content: headingMatch[2].trim(), }); continue; } const blockquoteMatch = line.match(/^>\s?(.*)$/); if (blockquoteMatch) { flushParagraph(); const quoteLines = [blockquoteMatch[1]]; let cursor = index + 1; while (cursor < lines.length) { const nextMatch = lines[cursor].match(/^>\s?(.*)$/); if (!nextMatch) break; quoteLines.push(nextMatch[1]); cursor += 1; } blocks.push({ type: "blockquote", content: quoteLines.join("\n").trim(), }); index = cursor - 1; continue; } const unorderedMatch = line.match(/^[-*+]\s+(.+)$/); if (unorderedMatch) { flushParagraph(); const items = [unorderedMatch[1].trim()]; let cursor = index + 1; while (cursor < lines.length) { const nextMatch = lines[cursor].match(/^[-*+]\s+(.+)$/); if (!nextMatch) break; items.push(nextMatch[1].trim()); cursor += 1; } blocks.push({ type: "unordered-list", items }); index = cursor - 1; continue; } const orderedMatch = line.match(/^\d+\.\s+(.+)$/); if (orderedMatch) { flushParagraph(); const items = [orderedMatch[1].trim()]; let cursor = index + 1; while (cursor < lines.length) { const nextMatch = lines[cursor].match(/^\d+\.\s+(.+)$/); if (!nextMatch) break; items.push(nextMatch[1].trim()); cursor += 1; } blocks.push({ type: "ordered-list", items }); index = cursor - 1; continue; } paragraphLines.push(line); } flushParagraph(); return blocks; } function renderBlock(block: MarkdownBlock, index: number) { if (block.type === "paragraph") { return (

{renderInline(block.content, `p-${index}`)}

); } if (block.type === "heading") { const sizeClass = block.level === 1 ? "text-xl font-bold" : block.level === 2 ? "text-lg font-bold" : "text-base font-semibold"; return (
{renderInline(block.content, `h-${index}`)}
); } if (block.type === "unordered-list") { return ( ); } if (block.type === "ordered-list") { return (
    {block.items.map((item, itemIndex) => (
  1. {renderInline(item, `ol-${index}-${itemIndex}`)}
  2. ))}
); } if (block.type === "blockquote") { return (
{renderInline(block.content, `q-${index}`)}
); } return (
{block.language && (
{block.language}
)}
        {block.content}
      
); } function renderInline(text: string, keyPrefix: string): React.ReactNode[] { const pattern = /(\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)|`([^`]+)`|\*\*([^*]+)\*\*|__([^_]+)__|\*([^*]+)\*|_([^_]+)_)/g; const nodes: React.ReactNode[] = []; let lastIndex = 0; let matchIndex = 0; let match: RegExpExecArray | null; while ((match = pattern.exec(text)) !== null) { const plainText = text.slice(lastIndex, match.index); if (plainText) { nodes.push(...renderPlainText(plainText, `${keyPrefix}-plain-${matchIndex}`)); } if (match[2] && match[3]) { nodes.push( {match[2]} , ); } else if (match[4]) { nodes.push( {match[4]} , ); } else if (match[5] || match[6]) { nodes.push( {match[5] ?? match[6]} , ); } else if (match[7] || match[8]) { nodes.push( {match[7] ?? match[8]} , ); } lastIndex = match.index + match[0].length; matchIndex += 1; } const trailingText = text.slice(lastIndex); if (trailingText) { nodes.push(...renderPlainText(trailingText, `${keyPrefix}-tail`)); } return nodes; } function renderPlainText(text: string, keyPrefix: string): React.ReactNode[] { return text.split("\n").flatMap((segment, index, array) => { const nodes: React.ReactNode[] = [ {segment}, ]; if (index < array.length - 1) { nodes.push(
); } return nodes; }); }