diff --git a/src/components/Article.tsx b/src/components/Article.tsx index f72ed0e..81a2ad1 100644 --- a/src/components/Article.tsx +++ b/src/components/Article.tsx @@ -4,10 +4,56 @@ import { fetchData } from '../data-loader'; export interface ArticleProps { src: string; + section?: string; // 指定要显示的标题(不含 #) onLoaded?: () => void; onError?: (error: Error) => void; } +/** + * 从 markdown 内容中提取指定标题下的内容 + */ +function extractSection(content: string, sectionTitle: string): string { + // 匹配标题(支持 1-6 级标题) + const sectionRegex = new RegExp( + `^(#{1,6})\\s*${escapeRegExp(sectionTitle)}\\s*$`, + 'im' + ); + + const match = content.match(sectionRegex); + if (!match) { + // 没有找到指定标题,返回空字符串 + return ''; + } + + const headerLevel = match[1].length; + const startIndex = match.index!; + + // 查找下一个同级或更高级别的标题 + const nextHeaderRegex = new RegExp( + `^#{1,${headerLevel}}\\s+.+$`, + 'gm' + ); + + // 从标题后开始搜索 + nextHeaderRegex.lastIndex = startIndex + match[0].length; + const nextMatch = nextHeaderRegex.exec(content); + + if (nextMatch) { + // 返回从当前标题到下一个同级/更高级标题之间的内容 + return content.slice(startIndex, nextMatch.index).trim(); + } + + // 返回从当前标题到文件末尾的内容 + return content.slice(startIndex).trim(); +} + +/** + * 转义正则表达式特殊字符 + */ +function escapeRegExp(str: string): string { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + /** * Article 组件 * 用于将特定 src 位置的 md 文件显示为 markdown 文章 @@ -23,7 +69,11 @@ export const Article: Component = (props) => { setLoading(true); try { const text = await fetchData(props.src); - setContent(text); + // 如果指定了 section,提取对应内容 + const finalContent = props.section + ? extractSection(text, props.section) + : text; + setContent(finalContent); setLoading(false); props.onLoaded?.(); } catch (err) { diff --git a/src/components/md-link.tsx b/src/components/md-link.tsx index da01269..8d9dc0b 100644 --- a/src/components/md-link.tsx +++ b/src/components/md-link.tsx @@ -10,9 +10,14 @@ customElement("md-link", {}, (props, { element }) => { let articleContainer: HTMLDivElement | undefined; let disposeArticle: (() => void) | null = null; - // 从 element 的 textContent 获取链接目标 + // 从 element 的 textContent 获取链接目标(支持 path#section 语法) const rawLinkSrc = element?.textContent?.trim() || ""; + // 解析 section(支持 path#section 语法) + const hashIndex = rawLinkSrc.indexOf('#'); + const path = hashIndex >= 0 ? rawLinkSrc.slice(0, hashIndex) : rawLinkSrc; + const section = hashIndex >= 0 ? rawLinkSrc.slice(hashIndex + 1) : undefined; + // 隐藏原始文本内容 if (element) { element.textContent = ""; @@ -31,7 +36,7 @@ customElement("md-link", {}, (props, { element }) => { return baseDir + relative; }; - const linkSrc = resolvePath(articlePath, rawLinkSrc); + const linkSrc = resolvePath(articlePath, path); // 查找包含此元素的 p 标签 const parentP = element?.closest("p"); @@ -61,6 +66,7 @@ customElement("md-link", {}, (props, { element }) => { disposeArticle = render(() => (
console.log("Article loaded:", linkSrc)} onError={(err) => console.error("Article error:", err)} />