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')
// 移除图片 
.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 (
);
}
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}}
),
}),
[],
);
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 (
{isLoading ? (
) : (
{currentContent}
)}
);
}