import { Component, ParentProps, createContext, useContext, createEffect, onCleanup, Show, createResource, createSignal, } from "solid-js"; import { parseMarkdown } from "../markdown"; import { extractSection } from "../data-loader"; import mermaid from "mermaid"; import { getIndexedData } from "../data-loader/file-index"; import { useNavigateWithParams } from "./useNavigateWithParams"; export interface ArticleProps { src: string; section?: string; // 指定要显示的标题(不含 #) onLoaded?: () => void; onError?: (error: Error) => void; class?: string; // 额外的 class 用于样式控制 scrollToHash?: boolean; // 是否自动滚动到 hash } // --------------------------------------------------------------------------- // Article DOM context – lets child components react to the content container // --------------------------------------------------------------------------- const ArticleDomCtx = createContext<() => HTMLDivElement | undefined>(); /** Access the article's content DOM element from a child component. */ export function useArticleDom(): () => HTMLDivElement | undefined { const ctx = useContext(ArticleDomCtx); if (!ctx) throw new Error("useArticleDom must be used inside an
"); return ctx; } async function fetchArticleContent(params: { src: string; section?: string; }): Promise { const text = await getIndexedData(params.src); // 如果指定了 section,提取对应内容 return params.section ? extractSection(text, params.section) : text; } /** * 滚动到指定的 hash 元素 */ function scrollToHash(hash: string) { if (!hash) return; // 移除 # 前缀 const id = hash.startsWith("#") ? hash.slice(1) : hash; if (!id) return; // 使用 decodeURIComponent 解码 ID(处理中文等特殊字符) const decodedId = decodeURIComponent(id); // 尝试查找元素 const element = document.getElementById(decodedId); if (element) { // 使用 scrollIntoView 滚动到元素 element.scrollIntoView({ behavior: "instant", block: "start" }); return true; } return false; } /** * Article 组件 * 用于将特定 src 位置的 md 文件显示为 markdown 文章 */ export const Article: Component = (props) => { const navigate = useNavigateWithParams(); const [content, { refetch }] = createResource( () => ({ src: props.src, section: props.section }), fetchArticleContent, ); const [contentDom, setContentDom] = createSignal(); createEffect(() => { const data = content(); if (data) { props.onLoaded?.(); // 内容加载完成后,渲染 mermaid 图表 void mermaid.run(); // 内容渲染后检查 hash 并滚动 scrollToHash(window.location.hash); } }); // Intercept markdown links to use client-side navigation with // preserved URL search params. createEffect(() => { const dom = contentDom(); if (!dom) return; const onClick = (e: MouseEvent) => { const anchor = (e.target as HTMLElement).closest("a[href]"); if (!anchor) return; const href = anchor.getAttribute("href"); if (!href) return; // Only intercept same-origin navigation links (not external, not anchors) if (href.startsWith("http") || href.startsWith("//")) return; if (href.startsWith("#")) return; e.preventDefault(); navigate(href); }; dom.addEventListener("click", onClick); onCleanup(() => dom.removeEventListener("click", onClick)); }); onCleanup(() => { // 清理时清空内容,触发内部组件的销毁 }); return (
加载中...
加载失败:{content.error?.message}
{props.children}
); }; export default Article;