import React, { useState, useEffect, useMemo, type ReactNode } from 'react'; import { unified } from 'unified'; import remarkParse from 'remark-parse'; import remarkMath from 'remark-math'; import remarkRehype from 'remark-rehype'; import rehypeMathjax from 'rehype-mathjax'; import rehypeReact, { type Options } from 'rehype-react'; import rehypeHighlight from 'rehype-highlight'; import * as prod from 'react/jsx-runtime'; import { notifications } from '@mantine/notifications'; import { remarkWikiLinks } from '../../utils/remarkWikiLinks'; import { useWorkspace } from '../../hooks/useWorkspace'; import { useHighlightTheme } from '../../hooks/useHighlightTheme'; interface MarkdownPreviewProps { content: string; handleFileSelect: (filePath: string | null) => Promise; } interface MarkdownImageProps { src: string; alt?: string; [key: string]: unknown; } interface MarkdownLinkProps { href: string; children: ReactNode; [key: string]: unknown; } const MarkdownPreview: React.FC = ({ content, handleFileSelect, }) => { const [processedContent, setProcessedContent] = useState( null ); const baseUrl = window.API_BASE_URL; const { currentWorkspace, colorScheme } = useWorkspace(); // Use the highlight theme hook useHighlightTheme(colorScheme === 'auto' ? 'light' : colorScheme); const processor = useMemo(() => { const handleLinkClick = ( e: React.MouseEvent, href: string ): void => { e.preventDefault(); if (href.startsWith(`${baseUrl}/internal/`)) { // For existing files, extract the path and directly select it const [filePath] = decodeURIComponent( href.replace(`${baseUrl}/internal/`, '') ).split('#'); if (filePath) { void handleFileSelect(filePath); } } else if (href.startsWith(`${baseUrl}/notfound/`)) { // For non-existent files, show a notification const fileName = decodeURIComponent( href.replace(`${baseUrl}/notfound/`, '') ); notifications.show({ title: 'File Not Found', message: `The file "${fileName}" does not exist.`, color: 'red', }); } }; // Only create the processor if we have a workspace name if (!currentWorkspace?.name) { return unified(); } return unified() .use(remarkParse) .use(remarkWikiLinks, currentWorkspace.name) .use(remarkMath) .use(remarkRehype) .use(rehypeMathjax) .use(rehypeHighlight) .use(rehypeReact, { jsx: prod.jsx, jsxs: prod.jsxs, Fragment: prod.Fragment, development: false, elementAttributeNameCase: 'react', stylePropertyNameCase: 'dom', components: { img: ({ src, alt, ...props }: MarkdownImageProps) => ( {alt { console.error('Failed to load image:', event.currentTarget.src); event.currentTarget.alt = 'Failed to load image'; }} {...props} /> ), a: ({ href, children, ...props }: MarkdownLinkProps) => ( handleLinkClick(e, href)} {...props}> {children} ), }, } as Options); }, [currentWorkspace?.name, baseUrl, handleFileSelect]); useEffect(() => { const processContent = async (): Promise => { if (!currentWorkspace) { return; } try { const result = await processor.process(content); setProcessedContent(result.result as ReactNode); } catch (error) { console.error('Error processing markdown:', error); } }; void processContent(); }, [content, processor, currentWorkspace]); return (
{processedContent}
); }; export default MarkdownPreview;