aboutsummaryrefslogtreecommitdiff
path: root/174bg/handbook/source/table-of-contents.js
blob: dd873539661075d66aaed2159f5048abde392535 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
/**
 * table-of-contents.js
 *
 * Builds a nested table of contents from the page's sections and injects it
 * into the element with id "table-of-contents".
 *
 * How it works:
 *   1. Finds every `<article>` and `<section>` in `<main>` that has an `id`.
 *   2. Reads the first heading (h1–h6) in each to determine its title and
 *      nesting level.
 *   3. Builds a hierarchical <ul>/<li> tree using a stack to track the
 *      current nesting level, indenting nested levels with a left border.
 *   4. Each entry links to its section via an `#id` anchor.
 *
 * The TOC is only rendered if the container exists and at least one
 * qualifying section with a heading is found.
 */
document.addEventListener("DOMContentLoaded", () => {
  const container = document.getElementById("table-of-contents");
  if (!container) return;

  const sections = document.querySelectorAll(
    "main article[id], main section[id]",
  );

  const entries = [];
  sections.forEach((section) => {
    const heading = section.querySelector("h1, h2, h3, h4, h5, h6");
    if (!heading) return;
    entries.push({
      id: section.id,
      level: parseInt(heading.tagName.charAt(1), 10),
      text: heading.textContent.trim(),
    });
  });

  if (entries.length === 0) return;

  const baseLevel = Math.min(...entries.map((entry) => entry.level));

  const rootList = document.createElement("ul");
  rootList.className = "space-y-1 text-sm";
  const stack = [{ level: baseLevel - 1, list: rootList, item: null }];

  entries.forEach((entry) => {
    while (stack.length > 1 && entry.level <= stack[stack.length - 1].level) {
      stack.pop();
    }

    const parent = stack[stack.length - 1];

    let parentList = parent.list;
    if (!parentList) {
      parentList = document.createElement("ul");
      parentList.className =
        "mt-1 ml-3 space-y-1 border-l border-stone-200 pl-3 dark:border-stone-700";
      parent.item.appendChild(parentList);
      parent.list = parentList;
    }

    const listItem = document.createElement("li");
    const link = document.createElement("a");
    link.href = "#" + entry.id;
    link.textContent = entry.text;
    link.className =
      "block rounded px-2 py-1 text-stone-600 no-underline transition-colors hover:bg-stone-100 hover:text-stone-900 dark:text-stone-400 dark:hover:bg-stone-800 dark:hover:text-stone-100";
    listItem.appendChild(link);
    parentList.appendChild(listItem);

    stack.push({
      level: entry.level,
      list: null,
      item: listItem,
    });
  });

  container.appendChild(rootList);
});