import type { ReactNode } from "react"; export function renderPlainMarkdownText(text: string, keyPrefix: string): ReactNode[] { const nodes: ReactNode[] = []; const pattern = /https?:\/\/[^\s)]+/g; let cursor = 0; let match: RegExpExecArray | null; while ((match = pattern.exec(text)) !== null) { if (match.index > cursor) { nodes.push(text.slice(cursor, match.index)); } const url = match[0]; nodes.push( {url} , ); cursor = match.index + url.length; } if (cursor < text.length) { nodes.push(text.slice(cursor)); } return nodes.length > 0 ? nodes : [text]; } export function renderMarkdownInline(text: string, keyPrefix: string): ReactNode[] { const nodes: ReactNode[] = []; const pattern = /(`[^`\n]+`|\*\*[\s\S]+?\*\*|__[\s\S]+?__|~~[\s\S]+?~~|\[[^\]]+\]\(https?:\/\/[^)\s]+\)|https?:\/\/[^\s)]+)/g; let cursor = 0; let match: RegExpExecArray | null; while ((match = pattern.exec(text)) !== null) { const token = match[0]; if (match.index > cursor) { nodes.push(...renderPlainMarkdownText(text.slice(cursor, match.index), `${keyPrefix}-text-${cursor}`)); } if (token.startsWith("`") && token.endsWith("`")) { nodes.push({token.slice(1, -1)}); } else if ((token.startsWith("**") && token.endsWith("**")) || (token.startsWith("__") && token.endsWith("__"))) { nodes.push({renderMarkdownInline(token.slice(2, -2), `${keyPrefix}-strong-${match.index}`)}); } else if (token.startsWith("~~") && token.endsWith("~~")) { nodes.push({renderMarkdownInline(token.slice(2, -2), `${keyPrefix}-del-${match.index}`)}); } else if (token.startsWith("[") && token.includes("](") && token.endsWith(")")) { const labelEnd = token.indexOf("]("); const label = token.slice(1, labelEnd); const href = token.slice(labelEnd + 2, -1); nodes.push( {renderMarkdownInline(label, `${keyPrefix}-md-link-label-${match.index}`)} , ); } else { nodes.push( {token} , ); } cursor = match.index + token.length; } if (cursor < text.length) { nodes.push(...renderPlainMarkdownText(text.slice(cursor), `${keyPrefix}-text-${cursor}`)); } return nodes.length > 0 ? nodes : [text]; } export function splitMarkdownTableRow(line: string): string[] { return line .trim() .replace(/^\|/, "") .replace(/\|$/, "") .split("|") .map((cell) => cell.trim()); } export function isMarkdownTableDivider(line: string): boolean { return /^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?\s*$/.test(line); } export function isMarkdownBlockStart(line: string, nextLine?: string): boolean { const trimmed = line.trim(); return ( trimmed === "" || /^```/.test(trimmed) || /^#{1,4}\s+/.test(trimmed) || /^>\s?/.test(trimmed) || /^[-*+]\s+/.test(trimmed) || /^\d+[.)]\s+/.test(trimmed) || /^[-*_]{3,}$/.test(trimmed) || (line.includes("|") && !!nextLine && isMarkdownTableDivider(nextLine)) ); } export function renderMarkdownBlocks(text: string): ReactNode[] { const lines = text.replace(/\r\n?/g, "\n").trim().split("\n"); const blocks: ReactNode[] = []; let index = 0; while (index < lines.length) { const line = lines[index] ?? ""; const trimmed = line.trim(); if (!trimmed) { index += 1; continue; } const fenceMatch = trimmed.match(/^```(\S+)?/); if (fenceMatch) { const language = fenceMatch[1] || ""; const codeLines: string[] = []; index += 1; while (index < lines.length && !lines[index].trim().startsWith("```")) { codeLines.push(lines[index]); index += 1; } if (index < lines.length) index += 1; blocks.push(
{language ?
{language}
: null}
            {codeLines.join("\n")}
          
, ); continue; } const headingMatch = trimmed.match(/^(#{1,4})\s+(.+)$/); if (headingMatch) { const level = Math.min(headingMatch[1].length, 4); const Tag = `h${level}` as keyof JSX.IntrinsicElements; blocks.push({renderMarkdownInline(headingMatch[2], `heading-${index}`)}); index += 1; continue; } if (/^[-*_]{3,}$/.test(trimmed)) { blocks.push(
); index += 1; continue; } if (line.includes("|") && index + 1 < lines.length && isMarkdownTableDivider(lines[index + 1])) { const headers = splitMarkdownTableRow(line); const rows: string[][] = []; index += 2; while (index < lines.length && lines[index].includes("|") && lines[index].trim()) { rows.push(splitMarkdownTableRow(lines[index])); index += 1; } blocks.push(
{headers.map((header, cellIndex) => ( ))} {rows.map((row, rowIndex) => ( {headers.map((_, cellIndex) => ( ))} ))}
{renderMarkdownInline(header, `table-head-${cellIndex}`)}
{renderMarkdownInline(row[cellIndex] || "", `table-cell-${rowIndex}-${cellIndex}`)}
, ); continue; } if (/^>\s?/.test(trimmed)) { const quoteLines: string[] = []; while (index < lines.length && /^>\s?/.test(lines[index].trim())) { quoteLines.push(lines[index].trim().replace(/^>\s?/, "")); index += 1; } blocks.push(
{quoteLines.map((quoteLine, quoteIndex) => (

{renderMarkdownInline(quoteLine, `quote-${index}-${quoteIndex}`)}

))}
, ); continue; } const unorderedMatch = trimmed.match(/^[-*+]\s+(.+)$/); const orderedMatch = trimmed.match(/^\d+[.)]\s+(.+)$/); if (unorderedMatch || orderedMatch) { const ordered = Boolean(orderedMatch); const ListTag = ordered ? "ol" : "ul"; const items: string[] = []; while (index < lines.length) { const itemMatch = ordered ? lines[index].trim().match(/^\d+[.)]\s+(.+)$/) : lines[index].trim().match(/^[-*+]\s+(.+)$/); if (!itemMatch) break; items.push(itemMatch[1]); index += 1; } blocks.push( {items.map((item, itemIndex) => (
  • {renderMarkdownInline(item, `list-${index}-${itemIndex}`)}
  • ))}
    , ); continue; } const paragraphLines: string[] = [line]; index += 1; while (index < lines.length && !isMarkdownBlockStart(lines[index], lines[index + 1])) { paragraphLines.push(lines[index]); index += 1; } const paragraphText = paragraphLines.join("\n").trim(); blocks.push(

    {renderMarkdownInline(paragraphText, `paragraph-${index}`)}

    ); } return blocks; }