diff --git a/src/App.tsx b/src/App.tsx index 7700558..4d4225d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,9 @@ import { Component, createEffect, createMemo, createSignal } from "solid-js"; import { useLocation } from "@solidjs/router"; -import { useJournalStream } from "./components/stores/journalStream"; +import { + addRevealedClasses, + useJournalStream, +} from "./components/stores/journalStream"; // 导入组件以注册自定义元素 import "./components"; @@ -123,6 +126,9 @@ const App: Component = () => {
+ addRevealedClasses(dom, stream.revealedPaths[currentPath()]) + } /> diff --git a/src/components/Article.tsx b/src/components/Article.tsx index 3403d29..9d69b45 100644 --- a/src/components/Article.tsx +++ b/src/components/Article.tsx @@ -21,6 +21,7 @@ export interface ArticleProps { onError?: (error: Error) => void; class?: string; // 额外的 class 用于样式控制 scrollToHash?: boolean; // 是否自动滚动到 hash + onDom?: (dom: HTMLDivElement) => void; } async function fetchArticleContent(params: { @@ -64,6 +65,7 @@ export const Article: Component = (props) => { () => ({ src: props.src, section: props.section }), fetchArticleContent, ); + let innerDiv!: HTMLDivElement; // 解析 iconPath,默认为 "./assets",空字符串表示禁用 const iconPrefix = createMemo(() => { @@ -80,6 +82,8 @@ export const Article: Component = (props) => { // 内容渲染后检查 hash 并滚动 scrollToHash(location.hash); + + props.onDom?.(innerDiv); } }); @@ -101,6 +105,7 @@ export const Article: Component = (props) => {
diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 3edae62..1d3c889 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -3,6 +3,7 @@ import { generateToc, type FileNode, type TocNode } from "../data-loader"; import { useLocation } from "@solidjs/router"; import { FileTreeNode, HeadingNode } from "./FileTree"; import { isPathRevealed } from "./stores/journalStream"; +import { createEffect } from "solid-js"; export interface SidebarProps { isOpen: boolean; @@ -43,8 +44,8 @@ const SidebarContent: Component = (props) => { } for (const node of currentFileHeadings()) { traverse(node, (anode) => { - if (isPathRevealed(pathname, anode.title)) { - traverse(anode, (each) => set.add(each.title)); + if (isPathRevealed(pathname, anode.id ?? anode.title)) { + traverse(anode, (each) => set.add(anode.id ?? anode.title)); } }); } @@ -56,10 +57,13 @@ const SidebarContent: Component = (props) => { return true; }; const isHeadingHidden = (node: TocNode): boolean => { - if (revealedHeadings().has(node.title)) return false; + if (revealedHeadings().has(node.id ?? node.title)) return false; if (node.children?.some((child) => !isHeadingHidden(child))) return false; return true; }; + createEffect(() => { + console.log(currentFileHeadings(), revealedHeadings()); + }); return (
diff --git a/src/components/stores/journalStream.ts b/src/components/stores/journalStream.ts index 2c99058..6e22a39 100644 --- a/src/components/stores/journalStream.ts +++ b/src/components/stores/journalStream.ts @@ -627,6 +627,54 @@ export function isPathRevealed(path: string, section?: string): boolean { return revealed.size > 0; } +/** + * Walk every element in `root` and tag it as revealed or concealed. + * + * Headings (h1–h6) update a current-heading tracker. When a higher-level + * heading is encountered (e.g. h2 after h1), all sub-headings at deeper + * levels are cleared. Each non-heading element receives a `revealed` or + * `concealed` CSS class depending on whether any of its current heading + * ancestors is present in the `revealed` set. + */ +export function addRevealedClasses( + root: HTMLDivElement, + revealed: Set, +) { + if (!revealed) return; + // Current heading text for each level (h1..h6, index 1..6) + const cur: Record = {}; + + const HEADING_TAGS = new Set(["H1", "H2", "H3", "H4", "H5", "H6"]); + + const walk = (el: Element) => { + const tag = el.tagName.toUpperCase(); + + if (HEADING_TAGS.has(tag)) { + const level = Number(tag.charAt(1)); // 1-6 + const text = el.textContent?.trim() ?? ""; + + // Update current heading at this level + cur[level] = text; + // Clear all deeper heading levels + for (let l = level + 1; l <= 6; l++) delete cur[l]; + } else { + // Non-heading: check if any current heading ancestor is revealed + const isRevealed = Object.values(cur).some((h) => revealed.has(h)); + el.classList.add(isRevealed ? "revealed" : "concealed"); + } + + // Recurse into children + for (const child of el.children) { + walk(child); + } + }; + + // Start walk from root's children (skip the container itself) + for (const child of root.children) { + walk(child); + } +} + /** Normalize a path for lookup: strip .md, leading ./ or / */ function normalizePath(p: string): string { return p.replace(/^\.?\//, "").replace(/\.md$/, "");