import React, { useMemo, useState } from 'react'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import remarkMath from 'remark-math'; import remarkBreaks from 'remark-breaks'; import rehypeRaw from 'rehype-raw'; import rehypeKatex from 'rehype-katex'; import rehypeHighlight from 'rehype-highlight'; import { useApp } from '../context/AppContext'; import { generateBreadcrumbs, getFileTitle } from '../utils/fileUtils'; import './MarkdownRenderer.css'; import 'katex/dist/katex.min.css'; import 'highlight.js/styles/github.css'; // 下载Markdown文件功能 function downloadMarkdown(content, filename) { const blob = new Blob([content], { type: 'text/markdown;charset=utf-8' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = filename || 'document.md'; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); } // 复制到剪贴板功能 async function copyToClipboard(content) { try { await navigator.clipboard.writeText(content); return true; } catch (err) { // 降级方案:使用传统的复制方法 const textArea = document.createElement('textarea'); textArea.value = content; textArea.style.position = 'fixed'; textArea.style.left = '-999999px'; textArea.style.top = '-999999px'; document.body.appendChild(textArea); textArea.focus(); textArea.select(); try { document.execCommand('copy'); document.body.removeChild(textArea); return true; } catch (err) { document.body.removeChild(textArea); return false; } } } // 功能按钮组件 function ActionButtons({ content, filename }) { const [copyStatus, setCopyStatus] = useState(''); const [downloadStatus, setDownloadStatus] = useState(''); const handleDownload = () => { try { downloadMarkdown(content, filename); setDownloadStatus('success'); setTimeout(() => setDownloadStatus(''), 2000); } catch (error) { setDownloadStatus('error'); setTimeout(() => setDownloadStatus(''), 2000); } }; const handleCopy = async () => { const success = await copyToClipboard(content); setCopyStatus(success ? 'success' : 'error'); setTimeout(() => setCopyStatus(''), 2000); }; return (
); } // 字数统计工具函数 function countWords(markdownText) { if (!markdownText || typeof markdownText !== 'string') { return 0; } // 移除Markdown格式符号的正则表达式 let plainText = markdownText // 移除代码块 .replace(/```[\s\S]*?```/g, '') // 移除内联代码 .replace(/`[^`]*`/g, '') // 移除链接 [text](url) .replace(/\[([^\]]*)\]\([^)]*\)/g, '$1') // 移除图片 ![alt](url) .replace(/!\[([^\]]*)\]\([^)]*\)/g, '$1') // 移除标题标记 .replace(/^#{1,6}\s+/gm, '') // 移除粗体和斜体标记 .replace(/\*\*([^*]+)\*\*/g, '$1') .replace(/\*([^*]+)\*/g, '$1') .replace(/__([^_]+)__/g, '$1') .replace(/_([^_]+)_/g, '$1') // 移除删除线 .replace(/~~([^~]+)~~/g, '$1') // 移除引用标记 .replace(/^>\s*/gm, '') // 移除列表标记 .replace(/^[\s]*[-*+]\s+/gm, '') .replace(/^[\s]*\d+\.\s+/gm, '') // 移除水平分割线 .replace(/^[-*_]{3,}$/gm, '') // 移除HTML标签 .replace(/<[^>]*>/g, '') // 移除多余的空白字符 .replace(/\s+/g, ' ') .trim(); // 统计中文字符和英文单词 const chineseChars = (plainText.match(/[\u4e00-\u9fa5]/g) || []).length; const englishWords = plainText .replace(/[\u4e00-\u9fa5]/g, ' ') .split(/\s+/) .filter(word => word.length > 0 && /[a-zA-Z]/.test(word)).length; return chineseChars + englishWords; } // 字数统计显示组件 function WordCount({ content }) { const wordCount = useMemo(() => countWords(content), [content]); if (wordCount === 0) { return null; } return (
📊 全文共 {wordCount.toLocaleString()} 字
); } // 自定义插件:禁用内联代码解析 function remarkDisableInlineCode() { return (tree) => { // 移除所有内联代码节点,将其转换为普通文本 function visit(node, parent, index) { if (node.type === 'inlineCode') { // 将内联代码节点替换为文本节点 const textNode = { type: 'text', value: node.value }; if (parent && typeof index === 'number') { parent.children[index] = textNode; } return; } if (node.children) { for (let i = 0; i < node.children.length; i++) { visit(node.children[i], node, i); } } } visit(tree); }; } function Breadcrumbs({ filePath }) { const breadcrumbs = generateBreadcrumbs(filePath); if (breadcrumbs.length === 0) return null; return ( ); } function CodeBlock({ inline, className, children, ...props }) { const [copied, setCopied] = useState(false); if (inline) { // 不渲染为代码,直接返回普通文本 return {children}; } // 改进的文本提取逻辑,处理React元素和纯文本 const extractText = (node) => { if (typeof node === 'string') { return node; } if (typeof node === 'number') { return String(node); } if (React.isValidElement(node)) { return extractText(node.props.children); } if (Array.isArray(node)) { return node.map(extractText).join(''); } return ''; }; const codeText = extractText(children).replace(/\n$/, ''); const match = /language-(\w+)/.exec(className || ''); const language = match ? match[1] : 'text'; const buttonClass = 'code-copy-button' + (copied ? ' copied' : ''); const buttonLabel = copied ? '已复制' : '复制代码'; const handleCopy = async () => { if (typeof navigator === 'undefined' || !navigator.clipboard) { return; } try { await navigator.clipboard.writeText(codeText); setCopied(true); setTimeout(() => setCopied(false), 2200); } catch (error) { console.error('Failed to copy code block', error); } }; return (
{language}
        {children}
      
); } function CustomLink({ href, children, ...props }) { if (href && href.startsWith('[[') && href.endsWith(']]')) { const linkText = href.slice(2, -2); return ( {children || linkText} ); } const isExternal = href && /^(https?:)?\/\//.test(href); const linkClass = isExternal ? 'external-link' : 'internal-link'; return ( {children} {isExternal && 🔗} ); } function CustomTable({ children, ...props }) { return (
{children}
); } function createHeadingRenderer(tag) { return function HeadingRenderer({ children, ...props }) { const text = React.Children.toArray(children) .map((child) => { if (typeof child === 'string') return child; if (React.isValidElement(child) && typeof child.props.children === 'string') { return child.props.children; } return ''; }) .join(' ') .trim(); const id = text .toLowerCase() .replace(/[^a-z0-9\u00c0-\u024f\u4e00-\u9fa5\s-]/g, '') .replace(/\s+/g, '-'); const HeadingTag = tag; return ( # {children} ); }; } const headingComponents = { h1: createHeadingRenderer('h1'), h2: createHeadingRenderer('h2'), h3: createHeadingRenderer('h3'), h4: createHeadingRenderer('h4'), }; export default function MarkdownRenderer() { const { currentFile, currentContent, isLoading, sidebarOpen } = useApp(); const components = useMemo( () => ({ code: ({ inline, className, children, ...props }) => { if (inline) { // 内联代码直接返回普通文本,不做任何特殊处理 return {children}; } // 代码块使用原来的CodeBlock组件 return {children}; }, a: CustomLink, table: CustomTable, ...headingComponents, blockquote: ({ children, ...props }) => (
{children}
), img: ({ alt, ...props }) => (
{alt} {alt &&
{alt}
}
), }), [], ); const contentAreaClass = 'content-area' + (sidebarOpen ? ' with-sidebar' : ''); if (!currentFile) { return (

🌙 欢迎回来

从左侧目录选择任意 Markdown 笔记即可开始阅读。

📝 原汁原味的 Markdown 样式
💡 深色界面,夜间更护眼
代码高亮与复制一键搞定
); } const fileTitle = getFileTitle(currentFile.split('/').pop(), currentContent); const filename = currentFile.split('/').pop(); return (

{fileTitle}

{isLoading ? (
加载中...
) : (
{currentContent}
)}
); }