UNPKG

mdast-util-toc

Version:

mdast utility to generate a table of contents from a tree

147 lines (134 loc) 4.21 kB
/** * @typedef {import('mdast').Heading} Heading * @typedef {import('mdast').Nodes} Nodes * @typedef {import('mdast').PhrasingContent} PhrasingContent * @typedef {import('unist-util-is').Test} Test */ /** * @typedef {Heading['depth']} Rank * */ /** * @typedef SearchOptions * Search configuration. * @property {Rank | null | undefined} [maxDepth=6] * Maximum heading depth to include in the table of contents (default: `6`). * * This is inclusive: when set to `3`, level three headings are included * (those with three hashes, `###`). * @property {Rank | null | undefined} [minDepth=1] * Minimum heading depth to include in the table of contents (default: `1`). * * This is inclusive: when set to `3`, level three headings are included * (those with three hashes, `###`). * @property {string | null | undefined} [skip] * Headings to skip, wrapped in `new RegExp('^(' + value + ')$', 'i')` * (default: `undefined`). * * Any heading matching this expression will not be present in the table of * contents. * @property {Test} [parents] * Allow headings to be children of certain node types (default: the to `toc` * given `tree`, to only allow top-level headings) (default: * `d => d === tree`). * * Internally, uses `unist-util-is` to check, so `parents` can be any * `is`-compatible test. * * @typedef SearchEntry * Entry. * @property {string} id * ID of entry. * @property {Array<PhrasingContent>} children * Contents of entry. * @property {Rank} depth * Rank of entry. * * @typedef SearchResult * Results. * @property {number} index * Where the contents section starts, if looking for a heading. * @property {number} endIndex * Where the contents section ends, if looking for a heading. * @property {Array<SearchEntry>} map * List of entries. */ import Slugger from 'github-slugger' import {toString} from 'mdast-util-to-string' import {convert} from 'unist-util-is' import {visit} from 'unist-util-visit' import {toExpression} from './to-expression.js' const slugs = new Slugger() /** * Search a node for a toc. * * @param {Nodes} root * @param {RegExp | undefined} expression * @param {SearchOptions} settings * @returns {SearchResult} */ export function search(root, expression, settings) { const max = 'children' in root ? root.children.length : 0 const skip = settings.skip ? toExpression(settings.skip) : undefined const parents = convert( settings.parents || function (d) { return d === root } ) /** @type {Array<SearchEntry>} */ const map = [] /** @type {number | undefined} */ let index /** @type {number | undefined} */ let endIndex /** @type {Heading | undefined} */ let opening slugs.reset() // Visit all headings in `root`. We `slug` all headings (to account for // duplicates), but only create a TOC from top-level headings (by default). visit(root, 'heading', function (node, position, parent) { const value = toString(node, {includeImageAlt: false}) /** @type {string} */ // @ts-expect-error `hProperties` from <https://github.com/syntax-tree/mdast-util-to-hast> const id = node.data && node.data.hProperties && node.data.hProperties.id const slug = slugs.slug(id || value) if (!parents(parent)) { return } // Our opening heading. if ( position !== undefined && expression && !index && expression.test(value) ) { index = position + 1 opening = node return } // Our closing heading. if ( position !== undefined && opening && !endIndex && node.depth <= opening.depth ) { endIndex = position } // A heading after the closing (if we were looking for one). if ( (endIndex || !expression) && (!settings.minDepth || node.depth >= settings.minDepth) && (!settings.maxDepth || node.depth <= settings.maxDepth) && (!skip || !skip.test(value)) ) { map.push({depth: node.depth, children: node.children, id: slug}) } }) return { index: index === undefined ? -1 : index, endIndex: index === undefined ? -1 : endIndex || max, map } }