toc
Jacky Zhao
2023-06-10 b8c011410d6bcd6837f4efd6a3948196a0f7aeea
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import { PluggableList } from "unified"
import { QuartzTransformerPlugin } from "../types"
import { Root } from "mdast"
import { visit } from "unist-util-visit"
import { toString } from "mdast-util-to-string"
import { slugAnchor } from "../../path"
 
export interface Options {
  maxDepth: 1 | 2 | 3 | 4 | 5 | 6,
  minEntries: 1,
  showByDefault: boolean
}
 
const defaultOptions: Options = {
  maxDepth: 3,
  minEntries: 1,
  showByDefault: true,
}
 
interface TocEntry {
  depth: number,
  text: string,
  slug: string
}
 
export class TableOfContents extends QuartzTransformerPlugin {
  name = "TableOfContents"
  opts: Options
 
  constructor(opts?: Partial<Options>) {
    super()
    this.opts = { ...defaultOptions, ...opts }
  }
 
  markdownPlugins(): PluggableList {
    return [() => {
      return async (tree: Root, file) => {
        const display = file.data.frontmatter?.enableToc ?? this.opts.showByDefault
        if (display) {
          const toc: TocEntry[] = []
          let highestDepth: number = this.opts.maxDepth
          visit(tree, 'heading', (node) => {
            if (node.depth <= this.opts.maxDepth) {
              const text = toString(node)
              highestDepth = Math.min(highestDepth, node.depth)
              toc.push({
                depth: node.depth,
                text,
                slug: slugAnchor.slug(text)
              })
            }
          })
 
          if (toc.length > this.opts.minEntries) {
            file.data.toc = toc.map(entry => ({ ...entry, depth: entry.depth - highestDepth }))
          }
        }
      }
    }]
  }
 
  htmlPlugins(): PluggableList {
    return []
  }
}
 
declare module 'vfile' {
  interface DataMap {
    toc: TocEntry[]
  }
}