UNPKG

remark-flexible-toc

Version:

Remark plugin to expose the table of contents via Vfile.data or via an option reference

116 lines 4.65 kB
import { visit, CONTINUE } from "unist-util-visit"; import GithubSlugger from "github-slugger"; import { toString } from "mdast-util-to-string"; const DEFAULT_SETTINGS = { tocName: "toc", tocRef: [], maxDepth: 6, skipLevels: [1], skipParents: [], }; /** * adds numberings to the TOC items. * why "number[]"? It is because up to you joining with dot or dash or slicing the first number (reserved for h1) * * [1] * [1,1] * [1,2] * [1,2,1] */ function addNumbering(arr) { for (let i = 0; i < arr.length; i++) { const tocItem = arr[i]; const depth = tocItem.depth; let numbering; const prevObj = i > 0 ? arr[i - 1] : undefined; const prevDepth = prevObj ? prevObj.depth : undefined; const prevNumbering = prevObj ? prevObj.numbering : undefined; if (!prevNumbering || !prevDepth) { numbering = Array.from({ length: depth }, () => 1); } else if (depth === prevDepth) { numbering = [...prevNumbering]; numbering[depth - 1]++; } else if (depth > prevDepth) { numbering = [ ...prevNumbering, ...Array.from({ length: depth - prevDepth }, // if depth is more bigger than prevDepth, put more "1" inside the array () => 1), ]; } else { // if (depth < prevDepth) numbering = prevNumbering.slice(0, depth); numbering[depth - 1]++; } tocItem.numbering = numbering; } } const RemarkFlexibleToc = (options) => { const settings = Object.assign({}, DEFAULT_SETTINGS, options); const exludeRegexFilter = settings.exclude && (Array.isArray(settings.exclude) ? new RegExp("^(" + settings.exclude.join("|") + ")$", "i") : new RegExp("^(" + settings.exclude + ")$", "i")); return (tree, file) => { const slugger = new GithubSlugger(); const tocItems = []; visit(tree, "heading", (_node, _index, _parent) => { /* v8 ignore next -- @preserve */ if (!_parent || typeof _index === "undefined") return; const depth = _node.depth; const value = toString(_node, { includeImageAlt: false }); let href = `#${settings.prefix ?? ""}${slugger.slug(value)}`; const parent = _parent.type; // maxDepth check if (depth > settings.maxDepth) return CONTINUE; // skipLevels check if (settings.skipLevels.includes(depth)) return CONTINUE; // skipParents check if (parent !== "root" && settings.skipParents.includes(parent)) return CONTINUE; // exclude check if (exludeRegexFilter && exludeRegexFilter.test(value)) return CONTINUE; // Other remark plugins can store custom data in node.data.hProperties // I omitted node.data.hName and node.data.hChildren since not related with toc // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore, vscode resolves the type but tsc build raises an error awkwardly, saying "hProperties" does not exist in "HeadingData". const hProperties = _node.data?.hProperties; const data = hProperties ? { ...hProperties } : undefined; if (data?.["id"]) href = `#${data["id"]}`; tocItems.push({ value, href, depth, numbering: [], parent, ...(data && { data }), }); return CONTINUE; }); addNumbering(tocItems); // it is allowed to modify the TOC in the callback settings.callback?.(tocItems); // method - 1 for exposing the data via vfile.data ************************** // other plugins are not allowed to mutate the exposed TOC // The spreading is slower than push but need to fresh copy file.data[settings.tocName] = [...tocItems]; // method - 2 for exposing the data via reference array ********************* if (options?.tocRef) { // prevent dublication if the plugin is called more than once settings.tocRef.length = 0; tocItems.forEach((tocItem) => { // the tocRef is not allowed to mutate the vfile.data.toc settings.tocRef.push(tocItem); }); } }; }; export default RemarkFlexibleToc; //# sourceMappingURL=index.js.map