Public
Edited
Apr 20, 2024
Fork of Indented ToC
Importers
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
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;

// Allow skip to be specified as a string or an array
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(headers)
document.querySelectorAll(selector)
).filter((d) => skipArr.indexOf(String(d.textContent)) === -1);

// CHeck if there's anything to render
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();
});
}
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more