stacker.news/lib/remark-toc.js
Edward Kung 4998041d73
Automatically generate table of contents in text (#2213)
* automatic toc generation in markdown

* don't open hash links in new tab

* only process toc for top level items

---------

Co-authored-by: ekzyis <ek@stacker.news>
2025-08-26 09:42:01 -05:00

62 lines
1.7 KiB
JavaScript

import { SKIP, visit } from 'unist-util-visit'
import { extractHeadings } from './toc'
export default function remarkToc () {
return function transformer (tree) {
const headings = extractHeadings(tree)
visit(tree, 'paragraph', (node, index, parent) => {
if (
node.children?.length === 1 &&
node.children[0].type === 'text' &&
node.children[0].value.trim() === '{:toc}'
) {
parent.children.splice(index, 1, buildToc(headings))
return [SKIP, index]
}
})
}
}
function buildToc (headings) {
const root = { type: 'list', ordered: false, spread: false, children: [] }
const stack = [{ depth: 0, node: root }] // holds the current chain of parents
for (const { heading, slug, depth } of headings) {
// walk up the stack to find the parent of the current heading
while (stack.length && depth <= stack[stack.length - 1].depth) {
stack.pop()
}
let parent = stack[stack.length - 1].node
// if the parent is a li, gets its child ul
if (parent.type === 'listItem') {
let ul = parent.children.find(c => c.type === 'list')
if (!ul) {
ul = { type: 'list', ordered: false, spread: false, children: [] }
parent.children.push(ul)
}
parent = ul
}
// build the li from the current heading
const listItem = {
type: 'listItem',
spread: false,
children: [{
type: 'paragraph',
children: [{
type: 'link',
url: `#${slug}`,
children: [{ type: 'text', value: heading }]
}]
}]
}
parent.children.push(listItem)
stack.push({ depth, node: listItem })
}
return root
}