import { QuartzTransformerPlugin } from "../types" import { Root } from "mdast" import { visit } from "unist-util-visit" import { toString } from "mdast-util-to-string" import Slugger from "github-slugger" import { wikilinkRegex } from "./ofm" export interface Options { maxDepth: 1 | 2 | 3 | 4 | 5 | 6 minEntries: 1 showByDefault: boolean collapseByDefault: boolean } const defaultOptions: Options = { maxDepth: 3, minEntries: 1, showByDefault: true, collapseByDefault: false, } interface TocEntry { depth: number text: string slug: string // this is just the anchor (#some-slug), not the canonical slug } const regexMdLinks = new RegExp(/\[([^\[]+)\](\(.*\))/, "g") const slugAnchor = new Slugger() export const TableOfContents: QuartzTransformerPlugin | undefined> = ( userOpts, ) => { const opts = { ...defaultOptions, ...userOpts } return { name: "TableOfContents", markdownPlugins() { return [ () => { return async (tree: Root, file) => { const display = file.data.frontmatter?.enableToc ?? opts.showByDefault if (display) { slugAnchor.reset() const toc: TocEntry[] = [] let highestDepth: number = opts.maxDepth visit(tree, "heading", (node) => { if (node.depth <= opts.maxDepth) { let text = toString(node) // strip link formatting from toc entries text = text.replace(wikilinkRegex, (_, rawFp, __, rawAlias) => { const fp = rawFp?.trim() ?? "" const alias = rawAlias?.slice(1).trim() return alias ?? fp }) text = text.replace(regexMdLinks, "$1") highestDepth = Math.min(highestDepth, node.depth) toc.push({ depth: node.depth, text, slug: slugAnchor.slug(text), }) } }) if (toc.length > opts.minEntries) { file.data.toc = toc.map((entry) => ({ ...entry, depth: entry.depth - highestDepth, })) file.data.collapseToc = opts.collapseByDefault } } } }, ] }, } } declare module "vfile" { interface DataMap { toc: TocEntry[] collapseToc: boolean } }