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
JavaScript
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