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>