修改网站markdown解析格式

This commit is contained in:
2025-09-29 15:05:43 +08:00
parent df2f1bf3dd
commit c8f40f3616
11 changed files with 482 additions and 677 deletions

View File

@@ -1,4 +1,4 @@
import React, { useMemo, useState, useEffect, useCallback } from 'react';
import React, { useMemo, useState } from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
@@ -15,6 +15,7 @@ import 'highlight.js/styles/github.css';
function Breadcrumbs({ filePath }) {
const breadcrumbs = generateBreadcrumbs(filePath);
if (breadcrumbs.length === 0) return null;
return (
<nav className="breadcrumbs" aria-label="当前位置">
{breadcrumbs.map((crumb, index) => (
@@ -28,8 +29,7 @@ function Breadcrumbs({ filePath }) {
}
function CodeBlock({ inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '');
const language = match ? match[1] : '';
const [copied, setCopied] = useState(false);
if (inline) {
return (
@@ -39,13 +39,34 @@ function CodeBlock({ inline, className, children, ...props }) {
);
}
const codeText = React.Children.toArray(children).join('').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 (
<div className="code-block-wrapper">
{language && (
<div className="code-block-header">
<span className="code-language">{language}</span>
</div>
)}
<div className="code-block-header">
<span className="code-language">{language}</span>
<button type="button" className={buttonClass} onClick={handleCopy} aria-live="polite">
{buttonLabel}
</button>
</div>
<pre className={className} {...props}>
<code>{children}</code>
</pre>
@@ -57,19 +78,21 @@ function CustomLink({ href, children, ...props }) {
if (href && href.startsWith('[[') && href.endsWith(']]')) {
const linkText = href.slice(2, -2);
return (
<span className="internal-link" title={内部链接: }>
<span className="internal-link" title={`内部链接: ${linkText}`}>
{children || linkText}
</span>
);
}
const isExternal = href && /^(https?:)?\/\//.test(href);
const linkClass = isExternal ? 'external-link' : 'internal-link';
return (
<a
href={href}
target={isExternal ? '_blank' : '_self'}
rel={isExternal ? 'noopener noreferrer' : undefined}
className={isExternal ? 'external-link' : 'internal-link'}
className={linkClass}
{...props}
>
{children}
@@ -86,97 +109,8 @@ function CustomTable({ children, ...props }) {
);
}
function useHeadings(content) {
return useMemo(() => {
const headingRegex = /^(#{1,4})\s+(.+)$/gm;
const headings = [];
let match;
while ((match = headingRegex.exec(content)) !== null) {
const [, hashes, text] = match;
const level = hashes.length;
if (level > 4) continue;
const plainText = text.replace(/[*_~]/g, '').trim();
const id = plainText
.toLowerCase()
.replace(/[^a-z0-9\u00C0-\u024f\u4e00-\u9fa5\s-]/g, '')
.replace(/\s+/g, '-');
headings.push({ id, text: plainText, level });
}
return headings;
}, [content]);
}
function TableOfContents({ headings }) {
if (headings.length === 0) return null;
return (
<aside className="content-toc" aria-label="目录">
<h3>目录</h3>
<ul className="toc-list">
{headings.map((heading) => (
<li key={heading.id} className="toc-item">
<a
className={ oc-link level-}
href={#}
>
{heading.text}
</a>
</li>
))}
</ul>
</aside>
);
}
export default function MarkdownRenderer() {
const { currentFile, currentContent, isLoading, sidebarOpen } = useApp();
const [activeHeading, setActiveHeading] = useState(null);
const headings = useHeadings(currentContent);
useEffect(() => {
if (typeof window === 'undefined' || headings.length === 0) {
return undefined;
}
const observer = new IntersectionObserver(
(entries) => {
const visible = entries
.filter((entry) => entry.isIntersecting)
.sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top);
if (visible[0]) {
setActiveHeading(visible[0].target.id);
}
},
{
rootMargin: '-40% 0px -50% 0px',
threshold: [0, 1],
}
);
headings.forEach((heading) => {
const element = document.getElementById(heading.id);
if (element) {
observer.observe(element);
}
});
return () => {
headings.forEach((heading) => {
const element = document.getElementById(heading.id);
if (element) {
observer.unobserve(element);
}
});
observer.disconnect();
};
}, [headings]);
const renderHeading = useCallback((Tag) => ({ children, ...props }) => {
function createHeadingRenderer(tag) {
return function HeadingRenderer({ children, ...props }) {
const text = React.Children.toArray(children)
.map((child) => {
if (typeof child === 'string') return child;
@@ -190,111 +124,74 @@ export default function MarkdownRenderer() {
const id = text
.toLowerCase()
.replace(/[^a-z0-9\u00C0-\u024f\u4e00-\u9fa5\s-]/g, '')
.replace(/[^a-z0-9\u00c0-\u024f\u4e00-\u9fa5\s-]/g, '')
.replace(/\s+/g, '-');
const HeadingTag = tag;
return (
<Tag id={id} {...props}>
<a href={#} aria-hidden className="heading-anchor">
<HeadingTag id={id} {...props}>
<a href={`#${id}`} aria-hidden className="heading-anchor">
#
</a>
{children}
</Tag>
</HeadingTag>
);
}, []);
};
}
const components = useMemo(() => ({
code: CodeBlock,
a: CustomLink,
table: CustomTable,
h1: renderHeading('h1'),
h2: renderHeading('h2'),
h3: renderHeading('h3'),
h4: renderHeading('h4'),
blockquote: ({ children, ...props }) => {
const childText = React.Children.toArray(children)
.map((child) => {
if (typeof child === 'string') return child.trim();
if (
React.isValidElement(child) &&
typeof child.props.children === 'string'
) {
return child.props.children.trim();
}
return '';
})
.join(' ');
const headingComponents = {
h1: createHeadingRenderer('h1'),
h2: createHeadingRenderer('h2'),
h3: createHeadingRenderer('h3'),
h4: createHeadingRenderer('h4'),
};
const calloutMatch = childText.match(/^\s*\[(info|warning|danger|success)\]\s*/i);
const calloutType = calloutMatch ? calloutMatch[1].toLowerCase() : null;
export default function MarkdownRenderer() {
const { currentFile, currentContent, isLoading, sidebarOpen } = useApp();
return (
<blockquote
className={custom-blockquote}
{...props}
>
const components = useMemo(
() => ({
code: CodeBlock,
a: CustomLink,
table: CustomTable,
...headingComponents,
blockquote: ({ children, ...props }) => (
<blockquote className="custom-blockquote" {...props}>
{children}
</blockquote>
);
},
img: ({ alt, ...props }) => (
<figure className="markdown-image">
<img alt={alt} {...props} />
{alt && <figcaption>{alt}</figcaption>}
</figure>
),
ol: ({ ordered, ...props }) => <ol className="ordered-list" {...props} />,
ul: ({ ordered, ...props }) => <ul className="unordered-list" {...props} />,
li: ({ checked, children, ...props }) => (
<li
className={list-item}
{...props}
>
{children}
</li>
),
}), [renderHeading]);
),
img: ({ alt, ...props }) => (
<figure className="markdown-image">
<img alt={alt} {...props} />
{alt && <figcaption>{alt}</figcaption>}
</figure>
),
}),
[],
);
useEffect(() => {
if (!activeHeading) return undefined;
const tocLinks = document.querySelectorAll('.toc-link');
tocLinks.forEach((link) => {
if (link.getAttribute('href') === #) {
link.classList.add('active');
} else {
link.classList.remove('active');
}
});
return () => {
tocLinks.forEach((link) => link.classList.remove('active'));
};
}, [activeHeading, headings]);
const contentAreaClass = 'content-area' + (sidebarOpen ? ' with-sidebar' : '');
if (!currentFile) {
return (
<div className={content-area }>
<div className={contentAreaClass}>
<div className="welcome-message">
<div className="welcome-content">
<h1>🌱 欢迎来到萌芽笔记</h1>
<p>从左侧导航栏选择一个笔记文件开始阅读</p>
<h1>🌙 欢迎</h1>
<p>从左侧目录选择任意 Markdown 笔记即可开始阅读</p>
<div className="welcome-features">
<div className="feature-item">
<span className="feature-icon">📝</span>
<span>支持完整的Markdown语法</span>
<span>原汁原味的 Markdown 样式</span>
</div>
<div className="feature-item">
<span className="feature-icon">🎨</span>
<span>代码语法高亮</span>
<span className="feature-icon">💡</span>
<span>深色界面夜间更护眼</span>
</div>
<div className="feature-item">
<span className="feature-icon">📊</span>
<span>数学公式与图表</span>
</div>
<div className="feature-item">
<span className="feature-icon">📱</span>
<span>移动端友好布局</span>
<span className="feature-icon"></span>
<span>代码高亮与复制一键搞定</span>
</div>
</div>
</div>
@@ -304,16 +201,15 @@ export default function MarkdownRenderer() {
}
const fileTitle = getFileTitle(currentFile.split('/').pop(), currentContent);
const hasHeadings = headings.length > 0;
return (
<div className={content-area }>
<div className={contentAreaClass}>
<div className="content-header">
<Breadcrumbs filePath={currentFile} />
<h1 className="content-title">{fileTitle}</h1>
</div>
<div className={content-body }>
<div className="content-body">
<div className="markdown-pane">
{isLoading ? (
<div className="loading-content">
@@ -332,8 +228,6 @@ export default function MarkdownRenderer() {
</div>
)}
</div>
{hasHeadings && <TableOfContents headings={headings} />}
</div>
</div>
);