diff --git a/src/components/project/MarkdownContent.tsx b/src/components/project/MarkdownContent.tsx index 2f55ea6..9ca261a 100644 --- a/src/components/project/MarkdownContent.tsx +++ b/src/components/project/MarkdownContent.tsx @@ -1,10 +1,8 @@ -'use client' - +import React from 'react' import ReactMarkdown from 'react-markdown' import remarkGfm from 'remark-gfm' import rehypeRaw from 'rehype-raw' import rehypeSanitize from 'rehype-sanitize' -import { useState, useEffect } from 'react' import type { Components } from 'react-markdown' interface MarkdownContentProps { @@ -25,6 +23,14 @@ function generateHeadingId(text: string): string { .replace(/-+$/, '') // Trim - from end } +// Helper function to escape HTML +function escapeHtml(code: string): string { + return code + .replace(/&/g, '&') + .replace(//g, '>') +} + // Custom GitHub-style components const components: Components = { // Headings with anchor links @@ -81,12 +87,26 @@ const components: Components = { ), - // Paragraphs - p: ({ children, ...props }) => ( -

- {children} -

- ), + // Paragraphs - skip wrapping if contains block-level elements like pre + p: ({ children, node, ...props }: any) => { + // 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: any) => { + const tagName = child?.tagName + // Check if any direct child is a block-level element + return ['pre', 'table', 'blockquote', 'ul', 'ol', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'div', 'img', 'hr'].includes(tagName) + }) + + if (hasBlockElement) { + return <>{children} + } + + return ( +

+ {children} +

+ ) + }, // Lists ul: ({ children, className, ...props }) => ( @@ -111,9 +131,24 @@ const components: Components = { ), - // Code blocks with syntax highlighting + // Code blocks (pre element wrapper) + pre: ({ children, ...props }: any) => { + return ( +
+        {children}
+      
+ ) + }, + + // Code elements (both inline and in code blocks) code: ({ inline, className, children, ...props }: any) => { - if (inline) { + // If inline is explicitly true, or if there's no language class and no newlines, treat as inline + const hasLanguageClass = className && typeof className === 'string' && className.startsWith('language-') + const childStr = String(children) + const hasNewlines = childStr.includes('\n') + const isInline = inline === true || (!hasLanguageClass && !hasNewlines) + + if (isInline) { return ( + + {children} + ) }, @@ -202,44 +238,12 @@ const components: Components = { checked={checked} readOnly className="mr-2 h-4 w-4 rounded border-gray-300" + name="task-item" {...props} /> ) : null, } -// Syntax highlighter wrapper (will use Shiki for server-side, Prism for client) -function SyntaxHighlighterWrapper({ language, code }: { language: string; code: string }) { - const [highlighted, setHighlighted] = useState(null) - - useEffect(() => { - // For now, use simple highlighting with proper escaping - // In production, you'd want to use Shiki or Prism for better highlighting - const escaped = code - .replace(/&/g, '&') - .replace(//g, '>') - - setHighlighted(escaped) - }, [code]) - - if (!highlighted) { - return ( -
-        {code}
-      
- ) - } - - return ( -
-      
-    
- ) -} - export function MarkdownContent({ content, className = '' }: MarkdownContentProps) { if (!content || content.trim() === '') {