import * as React from 'react' import ReactMarkdown, { type Components } from 'react-markdown' import rehypeRaw from 'rehype-raw' import remarkGfm from 'remark-gfm' import { cn } from '@/lib/utils' import { CodeBlock, InlineCode } from './CodeBlock' import { preprocessLinks } from './linkify' /** * Render modes for markdown content: * * - 'terminal': Raw output with minimal formatting, control chars visible * Best for: Debug output, raw logs, when you want to see exactly what's there * * - 'minimal': Clean rendering with syntax highlighting but no extra chrome * Best for: Chat messages, inline content, when you want readability without clutter * * - 'full': Rich rendering with beautiful tables, styled code blocks, proper typography * Best for: Documentation, long-form content, when presentation matters */ export type RenderMode = 'terminal' | 'minimal' | 'full' export interface MarkdownProps { children: string /** * Render mode controlling formatting level * @default 'minimal' */ mode?: RenderMode className?: string /** * Message ID for memoization (optional) * When provided, memoizes parsed blocks to avoid re-parsing during streaming */ id?: string /** * Callback when a URL is clicked */ onUrlClick?: (url: string) => void /** * Callback when a file path is clicked */ onFileClick?: (path: string) => void } // File path detection regex - matches paths starting with /, ~/, or ./ const FILE_PATH_REGEX = /^(?:\/|~\/|\.\/)[\w\-./@]+\.(?:ts|tsx|js|jsx|mjs|cjs|md|json|yaml|yml|py|go|rs|css|scss|less|html|htm|txt|log|sh|bash|zsh|swift|kt|java|c|cpp|h|hpp|rb|php|xml|toml|ini|cfg|conf|env|sql|graphql|vue|svelte|astro|prisma)$/i /** * Create custom components based on render mode */ function createComponents( mode: RenderMode, onUrlClick?: (url: string) => void, onFileClick?: (path: string) => void ): Partial { const baseComponents: Partial = { // Links: Make clickable with callbacks a: ({ href, children }) => { const handleClick = (e: React.MouseEvent): void => { e.preventDefault() if (href) { // Check if it's a file path if (FILE_PATH_REGEX.test(href) && onFileClick) { onFileClick(href) } else if (onUrlClick) { onUrlClick(href) } else { // Default: open in new window window.open(href, '_blank', 'noopener,noreferrer') } } } return ( {children} ) } } // Terminal mode: minimal formatting if (mode === 'terminal') { return { ...baseComponents, // No special code handling - just monospace code: ({ children }) => {children}, pre: ({ children }) =>
{children}
, // Minimal paragraph spacing p: ({ children }) =>

{children}

, // Simple lists ul: ({ children }) => , ol: ({ children }) =>
    {children}
, li: ({ children }) =>
  • {children}
  • , // Plain tables table: ({ children }) => {children}
    , th: ({ children }) => {children}, td: ({ children }) => {children} } } // Minimal mode: clean with syntax highlighting if (mode === 'minimal') { return { ...baseComponents, // Inline code code: ({ className, children, ...props }) => { const match = /language-(\w+)/.exec(className || '') const isBlock = 'node' in props && props.node?.position?.start.line !== props.node?.position?.end.line // Block code - use CodeBlock with full mode if (match || isBlock) { const code = String(children).replace(/\n$/, '') return } // Inline code return {children} }, pre: ({ children }) => <>{children}, // Comfortable paragraph spacing p: ({ children }) =>

    {children}

    , // Styled lists ul: ({ children }) => (
      {children}
    ), ol: ({ children }) =>
      {children}
    , li: ({ children }) =>
  • {children}
  • , // Clean tables table: ({ children }) => (
    {children}
    ), thead: ({ children }) => {children}, th: ({ children }) => ( {children} ), td: ({ children }) => {children}, // Headings - H1/H2 same size, differentiated by weight h1: ({ children }) =>

    {children}

    , h2: ({ children }) => (

    {children}

    ), h3: ({ children }) => (

    {children}

    ), // Blockquotes blockquote: ({ children }) => (
    {children}
    ), // Horizontal rules hr: () =>
    , // Strong/emphasis strong: ({ children }) => {children}, em: ({ children }) => {children} } } // Full mode: rich styling return { ...baseComponents, // Full code blocks with copy button code: ({ className, children, ...props }) => { const match = /language-(\w+)/.exec(className || '') const isBlock = 'node' in props && props.node?.position?.start.line !== props.node?.position?.end.line if (match || isBlock) { const code = String(children).replace(/\n$/, '') return } return {children} }, pre: ({ children }) => <>{children}, // Rich paragraph spacing p: ({ children }) =>

    {children}

    , // Styled lists ul: ({ children }) => (
      {children}
    ), ol: ({ children }) =>
      {children}
    , li: ({ children }) =>
  • {children}
  • , // Beautiful tables table: ({ children }) => (
    {children}
    ), thead: ({ children }) => {children}, tbody: ({ children }) => {children}, th: ({ children }) => {children}, td: ({ children }) => {children}, tr: ({ children }) => {children}, // Rich headings h1: ({ children }) =>

    {children}

    , h2: ({ children }) => (

    {children}

    ), h3: ({ children }) =>

    {children}

    , h4: ({ children }) =>

    {children}

    , // Styled blockquotes blockquote: ({ children }) => (
    {children}
    ), // Task lists (GFM) input: ({ type, checked }) => { if (type === 'checkbox') { return ( ) } return }, // Horizontal rules hr: () =>
    , // Strong/emphasis strong: ({ children }) => {children}, em: ({ children }) => {children}, del: ({ children }) => {children} } } /** * Markdown - Customizable markdown renderer with multiple render modes * * Features: * - Three render modes: terminal, minimal, full * - Syntax highlighting via Shiki * - GFM support (tables, task lists, strikethrough) * - Clickable links and file paths * - Memoization for streaming performance */ export function Markdown({ children, mode = 'minimal', className, onUrlClick, onFileClick }: MarkdownProps): React.JSX.Element { const components = React.useMemo( () => createComponents(mode, onUrlClick, onFileClick), [mode, onUrlClick, onFileClick] ) // Preprocess to convert raw URLs and file paths to markdown links const processedContent = React.useMemo(() => preprocessLinks(children), [children]) return (
    {processedContent}
    ) } /** * MemoizedMarkdown - Optimized for streaming scenarios * * Splits content into blocks and memoizes each block separately, * so only new/changed blocks re-render during streaming. */ export const MemoizedMarkdown = React.memo(Markdown, (prevProps, nextProps) => { // If id is provided, use it for memoization if (prevProps.id && nextProps.id) { return ( prevProps.id === nextProps.id && prevProps.children === nextProps.children && prevProps.mode === nextProps.mode ) } // Otherwise compare content and mode return prevProps.children === nextProps.children && prevProps.mode === nextProps.mode }) MemoizedMarkdown.displayName = 'MemoizedMarkdown'