import React, { type PropsWithChildren } from 'react' import ReactMarkdown from 'react-markdown' import remarkGfm from 'remark-gfm' import rehypeSanitize from 'rehype-sanitize' import type { Components } from 'react-markdown' interface MarkdownContentProps { content: string className?: string } const BLOCK_LEVEL_TAGS = new Set([ 'pre', 'table', 'blockquote', 'ul', 'ol', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'div', 'img', 'hr', ]) // Generate heading ID from text function generateHeadingId(text: string): string { return text .toString() .toLowerCase() .trim() .replace(/\s+/g, '-') // Replace spaces with - .replace(/[^\w\-\u4e00-\u9fa5]+/g, '') // Remove non-word chars except Chinese .replace(/\-\-+/g, '-') // Replace multiple - with single - .replace(/^-+/, '') // Trim - from start .replace(/-+$/, '') // Trim - from end } // Custom GitHub-style components const components: Components = { // Headings with anchor links h1: ({ children, ...props }: PropsWithChildren) => { const id = generateHeadingId(typeof children === 'string' ? children : '') return (

{children} #

) }, h2: ({ children, ...props }: PropsWithChildren) => { const id = generateHeadingId(typeof children === 'string' ? children : '') return (

{children} #

) }, h3: ({ children, ...props }: PropsWithChildren) => { const id = generateHeadingId(typeof children === 'string' ? children : '') return (

{children} #

) }, h4: ({ children, ...props }) => (

{children}

), // Paragraphs - skip wrapping if contains block-level elements like pre p: ({ children, node, ...props }) => { // Check if the paragraph node contains block-level elements in its children // This uses the AST node data from react-markdown const hasBlockElement = node?.children?.some((child) => { const tagName = 'tagName' in child ? child.tagName : undefined // Check if any direct child is a block-level element return typeof tagName === 'string' && BLOCK_LEVEL_TAGS.has(tagName) }) if (hasBlockElement) { return <>{children} } return (

{children}

) }, // Lists ul: ({ children, className, ...props }) => (
    {children}
), ol: ({ children, className, ...props }) => (
    {children}
), li: ({ children, className, ...props }) => (
  • {children}
  • ), // Code blocks (pre element wrapper) pre: ({ children, ...props }) => { return (
            {children}
          
    ) }, // Code elements (both inline and in code blocks) code: ({ className, children, ...props }) => { // If there's no language class and no newlines, treat as inline code. const hasLanguageClass = className && typeof className === 'string' && className.startsWith('language-') const childStr = String(children) const hasNewlines = childStr.includes('\n') const isInline = !hasLanguageClass && !hasNewlines if (isInline) { return ( {children} ) } // For code blocks, just return the code element - the pre wrapper will be added by react-markdown return ( {children} ) }, // Blockquotes blockquote: ({ children, ...props }) => (
    {children}
    ), // Tables table: ({ children, ...props }) => (
    {children}
    ), thead: ({ children, ...props }) => ( {children} ), th: ({ children, ...props }) => ( {children} ), td: ({ children, ...props }) => ( {children} ), // Links a: ({ children, href, ...props }) => ( {children} ), // Images - wrapped in container for size control img: ({ src, alt, ...props }) => (
    {/* External markdown images use arbitrary remote hosts; Next/Image is not suitable here. */} {/* eslint-disable-next-line @next/next/no-img-element */} {alt}
    ), // Horizontal rule hr: ({ ...props }) => (
    ), // Task lists input: ({ type, checked, ...props }) => type === 'checkbox' ? ( ) : null, } export function MarkdownContent({ content, className = '' }: MarkdownContentProps) { if (!content || content.trim() === '') { return (

    No content available.

    ) } return (
    {content}
    ) }