function toc(options = {}) {
if (typeof options === "string") {
options = {
headers: options
};
}
const {
headers = "h1,h2,h3",
exclude = "p",
hideStartingFrom = null,
title = "Table of Contents",
skip = []
} = options;
const skipArr = typeof skip === "string" ? [skip] : skip;
const selector = headers
.split(",")
.map((header) => `${header}:not(${exclude})`);
return Generators.observe((notify) => {
let previousHeadings = [];
let renderedEmptyToC = false;
function observed() {
const currentHeadings = Array.from(
document.querySelectorAll(selector)
).filter((d) => skipArr.indexOf(String(d.textContent)) === -1);
if (!currentHeadings.length) {
if (!renderedEmptyToC) {
notify(html`Unable to generate ToC: no headings found`);
renderedEmptyToC = true;
}
return;
}
// Check if anything changed from the previous render, and if not, bail
if (
currentHeadings.length === previousHeadings.length &&
!currentHeadings.some((h, i) => previousHeadings[i] !== h)
) {
return;
}
renderedEmptyToC = false;
// The start indentation specifies the top-most header tag that will
// be "unindented" in the ToC, and is effective the "2" in "h2"
let startIndentation = headers
.split(",")
.map((h) => parseInt(h.replace(/h/g, "")))
.sort()[0];
// The current indentation tracks what level of indentation we're at,
// so we can add <ul> and </ul> tags as needed to get the ToC to
// indend/unindent properly
let currentIndentation;
previousHeadings = currentHeadings;
const entries = [];
for (const h of Array.from(previousHeadings)) {
if (hideStartingFrom && h.textContent === hideStartingFrom) {
break;
}
let nodeIndentiation = parseInt(h.tagName[1], 10);
if (typeof currentIndentation === "undefined") {
// Add indentations as needed in case the initial header tag
// isn't the top-level specified for this ToC
currentIndentation = startIndentation;
while (nodeIndentiation > currentIndentation) {
currentIndentation++;
entries.push("<ul>");
}
} else {
while (currentIndentation < nodeIndentiation) {
entries.push("<ul>");
currentIndentation++;
}
while (currentIndentation > nodeIndentiation) {
entries.push("</ul>");
currentIndentation--;
}
}
entries.push(
Object.assign(
html`<li><a href="#">${DOM.text(h.textContent)}</a></li>`,
{
onclick: (e) => {
e.preventDefault();
h.scrollIntoView();
}
}
)
);
}
while (currentIndentation > startIndentation) {
entries.push("</ul>");
currentIndentation--;
}
let content;
if (title) {
content = html`<b>${DOM.text(title)}</b><ul>${entries}`;
} else {
content = html`<ul>${entries}`;
}
notify(content);
}
const observer = new MutationObserver(observed);
observer.observe(document.body, { childList: true, subtree: true });
observed();
return () => observer.disconnect();
});
}