/
table-of-contents.ts
56 lines (49 loc) · 1.39 KB
/
table-of-contents.ts
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
import { type Root } from 'mdast'
import { toc } from 'mdast-util-toc'
import { remark } from 'remark'
import remarkGfm from 'remark-gfm'
import type { Rules } from 'remark-mdat'
import { z } from 'zod'
export default {
'table-of-contents': {
// Apply towards the end so any generated headings are available
applicationOrder: 1,
// eslint-disable-next-line @typescript-eslint/require-await
async content(options, tree) {
const validOptions = z
.object({
depth: z
.union([
z.literal(1),
z.literal(2),
z.literal(3),
z.literal(4),
z.literal(5),
z.literal(6),
])
.optional(),
})
.optional()
.parse(options)
const result = toc(tree, {
// eslint-disable-next-line unicorn/no-null
heading: null,
maxDepth: validOptions?.depth ?? 3,
tight: true,
})
const heading = `## Table of contents`
if (result.map === undefined) {
throw new Error('Could not generate table of contents')
}
const rootWrapper: Root = {
children: result.map.children,
type: 'root',
}
// The "tight" option set above on toc() doesn't seem to work unless TOC has nested
// items, so we strip out blank lines ourselves for non-tested TOCs
const tocString = remark().use(remarkGfm).stringify(rootWrapper).replaceAll('\n\n', '\n')
return [heading, tocString].join('\n')
},
order: 6,
},
} satisfies Rules