'use client'; import React from 'react'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import { cn } from '@/lib/utils'; import { normalizeContent } from './content-normalizer'; import { ExternalLink } from 'lucide-react'; import { useRouter } from 'next/navigation'; // Helper to generate slug from heading text function slugify(text: string): string { return text .toString() .toLowerCase() .trim() .replace(/\s+/g, '-') // Replace spaces with - .replace(/[^\w\-]+/g, '') // Remove non-word chars .replace(/\-\-+/g, '-') // Replace multiple - with single - .replace(/^-+/, '') // Trim - from start .replace(/-+$/, ''); // Trim - from end } interface MarkdownRendererProps { content: string; className?: string; skipNormalization?: boolean; } export function MarkdownRenderer({ content, className, skipNormalization = false }: MarkdownRendererProps) { const router = useRouter(); // Normalize content to fix common LLM formatting issues const processedContent = skipNormalization ? content : normalizeContent(content); // Pre-calculate all heading data from the markdown // This runs once per content change and is stable across re-renders const headingData = React.useMemo(() => { const lines = processedContent.split('\n'); const headings: Array<{ level: number; text: string; index: number }> = []; let index = 0; for (const line of lines) { // Match H2, H3, H4 (skip H1 as it's not in TOC) const match = line.match(/^(#{2,4})\s+(.+)$/); if (match) { headings.push({ level: match[1].length, text: match[2].trim(), index: index++, }); } } return headings; }, [processedContent]); // Create a map from heading text to index for quick lookup during render const headingIndexMap = React.useMemo(() => { const map = new Map(); headingData.forEach(h => { const key = `${h.level}-${h.text}`; if (!map.has(key)) { map.set(key, h.index); } }); return map; }, [headingData]); return (
{ const text = children?.toString() || ''; const id = slugify(text); return

{children}

; }, h2: ({ children }) => { const text = children?.toString() || ''; const id = slugify(text); const key = `2-${text}`; const index = headingIndexMap.get(key); return (

{children}

); }, h3: ({ children }) => { const text = children?.toString() || ''; const id = slugify(text); const key = `3-${text}`; const index = headingIndexMap.get(key); return (

{children}

); }, h4: ({ children }) => { const text = children?.toString() || ''; const id = slugify(text); const key = `4-${text}`; const index = headingIndexMap.get(key); return (

{children}

); }, p: ({ children }) =>

{children}

, ul: ({ children }) =>
    {children}
, ol: ({ children }) =>
    {children}
, li: ({ children }) =>
  • {children}
  • , pre: ({ children, ...props }) => { // Extract language from code block if present const codeElement = React.Children.toArray(children).find( (child) => React.isValidElement(child) && child.type === 'code' ) as React.ReactElement<{ className?: string }> | undefined; const className = codeElement?.props?.className || ''; const match = /language-(\w+)/.exec(className); const language = match ? match[1] : null; return (
    {language && (
    {language}
    )}
                    {children}
                  
    ); }, code: ({ className, children, ...props }) => { const match = /language-(\w+)/.exec(className || ''); const isInline = !match; if (isInline) { return ( {children} ); } // Fenced code block with language return ( {children} ); }, blockquote: ({ children }) => (
    {children}
    ), a: ({ href, children }) => { if (!href) return {children}; // Internal doc links (?doc=...) const isInternalDoc = href.startsWith('?doc='); // Internal navigation links (?nav=...) const isNavLink = href.startsWith('?nav='); // Anchor links within page (#...) const isAnchorLink = href.startsWith('#'); // External links (http://, https://) const isExternal = href.startsWith('http://') || href.startsWith('https://'); // Internal links (doc links, nav links, or anchors) stay in same tab const shouldOpenNewTab = isExternal; // Handle special link types with router navigation const handleClick = (e: React.MouseEvent) => { if (isInternalDoc) { e.preventDefault(); router.push(`/${href}`); } else if (isNavLink) { e.preventDefault(); const view = href.replace('?nav=', ''); const isServerMode = process.env.NEXT_PUBLIC_SERVER_MODE === 'true'; if (isServerMode) { router.push(`/admin/${view}`); } else { // Browser mode - dispatch event for navigation window.dispatchEvent(new CustomEvent('nav-to-view', { detail: { view } })); router.push('/'); } } }; return ( {children} {isExternal && } ); }, strong: ({ children }) => {children}, em: ({ children }) => {children}, hr: () =>
    , table: ({ children }) => (
    {children}
    ), thead: ({ children }) => {children}, tbody: ({ children }) => {children}, tr: ({ children }) => {children}, th: ({ children }) => {children}, td: ({ children }) => {children}, }} > {processedContent}
    ); }