remark plugin to generate a table of contents (TOC)
npx @tessl/cli install tessl/npm-remark-toc@9.0.0remark-toc is a remark plugin that automatically generates table of contents (TOC) for markdown documents. It searches for headings that match a configurable pattern (default: 'Contents' or 'TOC'), removes existing content under that heading, and replaces it with a nested list of links to all subsequent headings in the document.
npm install remark-tocimport remarkToc from 'remark-toc'For Deno:
import remarkToc from 'https://esm.sh/remark-toc@9'import {remark} from 'remark'
import remarkToc from 'remark-toc'
import {read} from 'to-vfile'
const file = await remark()
.use(remarkToc)
.process(await read('example.md'))
console.log(String(file))Input markdown:
# My Document
Intro content here.
## Contents
## Chapter 1
Content...
### Subsection
More content...
## Chapter 2
Final content...Output markdown:
# My Document
Intro content here.
## Contents
* [Chapter 1](#chapter-1)
* [Subsection](#subsection)
* [Chapter 2](#chapter-2)
## Chapter 1
Content...
### Subsection
More content...
## Chapter 2
Final content...Generates a table of contents by finding a heading that matches a configurable pattern and replacing its content with a structured list of all subsequent headings.
/**
* Generate a table of contents (TOC).
*
* Looks for the first heading matching options.heading (case insensitive),
* removes everything between it and an equal or higher next heading, and
* replaces that with a list representing the rest of the document structure,
* linking to all further headings.
*/
function remarkToc(options?: Readonly<Options> | null | undefined): Transform;Usage with options:
import {remark} from 'remark'
import remarkToc from 'remark-toc'
// Custom heading pattern
const processor = remark().use(remarkToc, {
heading: 'table of contents|overview',
maxDepth: 3,
tight: false,
ordered: true
})Advanced configuration:
import {remark} from 'remark'
import remarkToc from 'remark-toc'
const processor = remark().use(remarkToc, {
heading: 'structure',
minDepth: 2,
maxDepth: 3,
skip: 'example|notes?',
prefix: 'user-content-',
parents: ['listItem', 'root'],
tight: false,
ordered: true
})interface Options {
/**
* Heading to look for, wrapped in new RegExp('^(' + value + ')$', 'i')
* @default '(table[ -]of[ -])?contents?|toc'
*/
heading?: string;
/**
* Max heading depth to include in the table of contents
* This is inclusive: when set to 3, level three headings are included (###)
* @default 6
*/
maxDepth?: number;
/**
* Min heading depth to include in the table of contents
* This is inclusive: when set to 2, only level two headings and deeper are included (##, ###, etc.)
* @default 1
*/
minDepth?: number;
/**
* Headings to skip, wrapped in new RegExp('^(' + value + ')$', 'i')
* Any heading matching this expression will not be present in the table of contents
*/
skip?: string;
/**
* Allow headings to be children of certain node types
* @default tree (root node)
*/
parents?: Test;
/**
* Whether to compile list items tightly, otherwise space is added around items
* @default true
*/
tight?: boolean;
/**
* Whether to compile list items as an ordered list, otherwise they are unordered
* @default false
*/
ordered?: boolean;
/**
* Add a prefix to links to headings in the table of contents
* Useful when later going from markdown to HTML and sanitizing with rehype-sanitize
* @example 'user-content-'
*/
prefix?: string;
}
type Test = string | Function | Object | Array<string | Function | Object>;
type Transform = (tree: Root) => undefined;
interface Root {
type: 'root';
children: Array<any>;
}import {remark} from 'remark'
import remarkToc from 'remark-toc'
const processor = remark().use(remarkToc, {
heading: 'structure'
})Searches for headings containing "structure" (case-insensitive).
import {remark} from 'remark'
import remarkToc from 'remark-toc'
const processor = remark().use(remarkToc, {
ordered: true,
tight: false
})Generates:
1. [History](#history)
1. [Discovery](#discovery)
2. [Name and symbol](#name-and-symbol)
3. [Planet X disproved](#planet-x-disproved)
2. [Orbit](#orbit)import {remark} from 'remark'
import remarkToc from 'remark-toc'
const processor = remark().use(remarkToc, {
maxDepth: 3,
parents: ['listItem', 'root'],
skip: 'delta'
})import {remark} from 'remark'
import remarkToc from 'remark-toc'
const processor = remark().use(remarkToc, {
minDepth: 2,
maxDepth: 4
})import {remark} from 'remark'
import remarkToc from 'remark-toc'
const processor = remark().use(remarkToc, {
prefix: 'user-content-'
})Generates:
* [History](#user-content-history)
* [Discovery](#user-content-discovery)
* [Name and symbol](#user-content-name-and-symbol)
* [Orbit](#user-content-orbit)remark-toc copies existing nodes into the table of contents, which can include script tags or other HTML content. When transforming to HTML, ensure proper sanitization:
# Contents
## Bravo<script>alert(1)</script>Becomes:
# Contents
- [Bravo<script>alert(1)</script>](#bravoscriptalert1script)
## Bravo<script>alert(1)</script>Use rehype-sanitize when converting to HTML.