import { SecondaryPageLink, useSecondaryPage } from '@/PageManager' import ImageWithLightbox from '@/components/ImageWithLightbox' import ImageCarousel from '@/components/ImageCarousel/ImageCarousel' import { getLongFormArticleMetadataFromEvent } from '@/lib/event-metadata' import { toNote, toNoteList, toProfile } from '@/lib/link' import { extractAllImagesFromEvent } from '@/lib/image-extraction' import { ExternalLink, ChevronDown, ChevronRight } from 'lucide-react' import { Event, kinds } from 'nostr-tools' import React, { useMemo, useEffect, useRef, useState } from 'react' import Markdown from 'react-markdown' import remarkGfm from 'remark-gfm' import remarkMath from 'remark-math' import rehypeKatex from 'rehype-katex' import 'katex/dist/katex.min.css' import NostrNode from './NostrNode' import { remarkNostr } from './remarkNostr' import { Components } from './types' import { Button } from '@/components/ui/button' import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible' export default function MarkdownArticle({ event, className }: { event: Event className?: string }) { const { push } = useSecondaryPage() const metadata = useMemo(() => getLongFormArticleMetadataFromEvent(event), [event]) const [isImagesOpen, setIsImagesOpen] = useState(false) // Extract all images from the event const allImages = useMemo(() => extractAllImagesFromEvent(event), [event]) const contentRef = useRef(null) // Initialize highlight.js for syntax highlighting useEffect(() => { const initHighlight = async () => { if (typeof window !== 'undefined') { const hljs = await import('highlight.js') if (contentRef.current) { contentRef.current.querySelectorAll('pre code').forEach((block) => { // Ensure text color is visible before highlighting const element = block as HTMLElement element.style.color = 'inherit' element.classList.add('text-gray-900', 'dark:text-gray-100') hljs.default.highlightElement(element) // Ensure text color remains visible after highlighting element.style.color = 'inherit' }) } } } // Run highlight after a short delay to ensure content is rendered const timeoutId = setTimeout(initHighlight, 100) return () => clearTimeout(timeoutId) }, [event.content]) const components = useMemo( () => ({ nostr: ({ rawText, bech32Id }) => , a: ({ href, children, ...props }) => { if (!href) { return } if (href.startsWith('note1') || href.startsWith('nevent1') || href.startsWith('naddr1')) { return ( {children} ) } if (href.startsWith('npub1') || href.startsWith('nprofile1')) { return ( {children} ) } return ( {children} ) }, p: (props) => { // Check if the paragraph contains only an image const children = props.children if (React.Children.count(children) === 1 && React.isValidElement(children)) { const child = children as React.ReactElement if (child.type === ImageWithLightbox) { // Render image outside paragraph context return
} } return

}, div: (props) =>

, code: ({ className, children, ...props }: any) => { const match = /language-(\w+)/.exec(className || '') const isInline = !match return !isInline && match ? (
              
                {children}
              
            
) : ( {children} ) }, text: ({ children }) => { // Handle hashtags in text if (typeof children === 'string') { const hashtagRegex = /#(\w+)/g const parts = [] let lastIndex = 0 let match while ((match = hashtagRegex.exec(children)) !== null) { // Add text before the hashtag if (match.index > lastIndex) { parts.push(children.slice(lastIndex, match.index)) } // Add the hashtag as a clickable link const hashtag = match[1] parts.push( #{hashtag} ) lastIndex = match.index + match[0].length } // Add remaining text if (lastIndex < children.length) { parts.push(children.slice(lastIndex)) } return <>{parts} } return <>{children} }, img: () => { // Don't render inline images - they'll be shown in the carousel return null } }) as Components, [] ) return ( <>
{metadata.title &&

{metadata.title}

} {metadata.summary && (

{metadata.summary}

)} {metadata.image && ( )} { if (url.startsWith('nostr:')) { return url.slice(6) // Remove 'nostr:' prefix for rendering } return url }} components={components} > {event.content} {/* Image Carousel - Collapsible */} {allImages.length > 0 && ( )} {metadata.tags.length > 0 && (
{metadata.tags.map((tag) => (
{ e.stopPropagation() push(toNoteList({ hashtag: tag, kinds: [kinds.LongFormArticle] })) }} > #{tag}
))}
)}
) }