master
1---
2import type { MarkdownHeading } from 'astro';
3
4interface Props {
5 class?: string;
6 headings: MarkdownHeading[];
7}
8
9const { headings = [], class: className } = Astro.props;
10const minDepth = Math.min(...headings.map((h) => h.depth));
11const maxLevel = 3;
12
13// 构建嵌套的 TOC 结构
14function buildTocTree(headings: MarkdownHeading[]) {
15 const filteredHeadings = headings.filter((h) => h.depth < minDepth + maxLevel);
16 const result: (MarkdownHeading & { children: typeof result })[] = [];
17 const stack: { node: (typeof result)[0]; depth: number }[] = [];
18
19 for (const heading of filteredHeadings) {
20 const node = { ...heading, children: [] };
21
22 while (stack.length > 0 && stack[stack.length - 1].depth >= heading.depth) {
23 stack.pop();
24 }
25
26 if (stack.length === 0) {
27 result.push(node);
28 } else {
29 stack[stack.length - 1].node.children.push(node);
30 }
31
32 stack.push({ node, depth: heading.depth });
33 }
34
35 return result;
36}
37
38const tocTree = buildTocTree(headings);
39---
40
41<div id="toc" class:list={['card border-base-300 bg-base-200 border', className]}>
42 <div class="card-body max-h-96 overflow-y-auto p-2">
43 <ul>
44 {
45 // 递归渲染 TOC 组件
46 (() => {
47 // eslint-disable-next-line @typescript-eslint/no-explicit-any
48 function renderTocItem(item: MarkdownHeading & { children: any[] }) {
49 return (
50 <li>
51 <a class:list={[`level-${item.depth - minDepth + 1}`]} href={`#${item.slug}`}>
52 {item.text}
53 </a>
54 {item.children.length > 0 && (
55 <ul>{item.children.map((child) => renderTocItem(child))}</ul>
56 )}
57 </li>
58 );
59 }
60 return tocTree.map((heading) => renderTocItem(heading));
61 })()
62 }
63 </ul>
64 </div>
65</div>
66
67<style>
68 ul {
69 list-style: none;
70 padding: 0;
71 margin: 0;
72 }
73
74 ul ul {
75 padding-left: 1rem;
76 }
77
78 a {
79 display: block;
80 padding: 0.5rem 1rem;
81
82 @apply duration-200 hover:scale-105 active:scale-95;
83 }
84</style>