PageSourceSearch

https://sebastianraschka.com/js/article-toc.js

js sebastianraschka.com collected 2026-10-01 08:44:27 UTC 10,937 bytes, 441 lines download raw bytes

1(function (window, document) {
2  'use strict';
3
4  var ARTICLE_SELECTOR = 'article.post-content';
5  var HEADING_SELECTOR = 'h1, h2, h3, h4, h5, h6';
6  var DEFAULT_MIN_HEADING_COUNT = 4;
7  var BLOG_MIN_HEADING_COUNT = 2;
8  var MANUAL_TOC_MIN_LINK_COUNT = 4;
9  var hasInitialized = false;
10
11  function normalizePath(path) {
12    if (!path) {
13      return '/';
14    }
15    return path.toLowerCase();
16  }
17
18  function isHomePage(path) {
19    return path === '/' || path === '/index.html';
20  }
21
22  function isBlogArticle(path) {
23    return path.indexOf('/blog/') === 0 && path !== '/blog/' && path !== '/blog/index.html';
24  }
25
26  function getMinimumHeadingCount(path) {
27    if (isBlogArticle(path)) {
28      return BLOG_MIN_HEADING_COUNT;
29    }
30    return DEFAULT_MIN_HEADING_COUNT;
31  }
32
33  function normalizeText(text) {
34    return (text || '').replace(/\s+/g, ' ').trim();
35  }
36
37  function getHeadingText(node) {
38    var clone;
39    var removable;
40    var i;
41
42    if (!node) {
43      return '';
44    }
45
46    clone = node.cloneNode(true);
47    removable = clone.querySelectorAll(
48      '.heading-anchor-link, .sr-only, a.anchor, a.headerlink, a.anchorjs-link'
49    );
50
51    for (i = 0; i < removable.length; i += 1) {
52      removeNode(removable[i]);
53    }
54
55    return normalizeText(clone.textContent);
56  }
57
58  function headingLevel(node) {
59    var level = parseInt(node.tagName.substring(1), 10);
60    if (isNaN(level)) {
61      return 6;
62    }
63    return Math.max(1, Math.min(6, level));
64  }
65
66  function parseLevel(value) {
67    var level = parseInt(value, 10);
68    if (isNaN(level)) {
69      return null;
70    }
71    return Math.max(1, Math.min(6, level));
72  }
73
74  function isTruthy(value) {
75    if (typeof value !== 'string') {
76      return false;
77    }
78    value = value.toLowerCase();
79    return value === '1' || value === 'true' || value === 'yes';
80  }
81
82  function filterHeadingsByLevel(headings, article) {
83    var highestLevel = 6;
84    var minLevel = parseLevel(article.getAttribute('data-toc-min-level'));
85    var maxLevel = parseLevel(article.getAttribute('data-toc-max-level'));
86    var highestOnly = isTruthy(article.getAttribute('data-toc-highest-only'));
87    var filtered = [];
88    var i;
89    var level;
90
91    if (!headings.length) {
92      return headings;
93    }
94
95    for (i = 0; i < headings.length; i += 1) {
96      level = headingLevel(headings[i]);
97      if (level < highestLevel) {
98        highestLevel = level;
99      }
100    }
101
102    if (highestOnly) {
103      minLevel = highestLevel;
104      maxLevel = highestLevel;
105    } else {
106      if (minLevel === null) {
107        minLevel = 1;
108      }
109      if (maxLevel === null) {
110        maxLevel = 6;
111      }
112      if (minLevel > maxLevel) {
113        level = minLevel;
114        minLevel = maxLevel;
115        maxLevel = level;
116      }
117    }
118
119    for (i = 0; i < headings.length; i += 1) {
120      level = headingLevel(headings[i]);
121      if (level >= minLevel && level <= maxLevel) {
122        filtered.push(headings[i]);
123      }
124    }
125
126    return filtered;
127  }
128
129  function isManualTocHeading(node) {
130    var text = getHeadingText(node).toLowerCase();
131    return text === 'table of contents' || text === 'contents' || text === 'sections';
132  }
133
134  function isInternalAnchorList(node) {
135    var links;
136    var i;
137    var href;
138
139    if (!node || (node.tagName !== 'UL' && node.tagName !== 'OL')) {
140      return false;
141    }
142
143    links = node.querySelectorAll('a[href]');
144    if (links.length < MANUAL_TOC_MIN_LINK_COUNT) {
145      return false;
146    }
147
148    for (i = 0; i < links.length; i += 1) {
149      href = links[i].getAttribute('href') || '';
150      if (href.charAt(0) !== '#') {
151        return false;
152      }
153    }
154
155    return true;
156  }
157
158  function removeNode(node) {
159    if (node && node.parentNode) {
160      node.parentNode.removeChild(node);
161    }
162  }
163
164  function cleanupEmptyParagraph(node) {
165    if (!node || node.tagName !== 'P') {
166      return;
167    }
168    if (!normalizeText(node.textContent) && !node.querySelector('img, video, iframe, table, pre, code')) {
169      removeNode(node);
170    }
171  }
172
173  function removeHardcodedToc(article) {
174    var i;
175    var anchor;
176    var heading;
177    var next;
178    var markers = article.querySelectorAll('ul#markdown-toc, ol#markdown-toc, details.llms-toc, details.rfs-toc');
179    var tocHeadings = article.querySelectorAll(HEADING_SELECTOR);
180    var children = article.children;
181    var firstHeading = article.querySelector(HEADING_SELECTOR);
182
183    for (i = 0; i < markers.length; i += 1) {
184      removeNode(markers[i]);
185    }
186
187    anchor = article.querySelector('a#table-of-contents');
188    if (anchor) {
189      heading = anchor.parentNode;
190      removeNode(anchor);
191      cleanupEmptyParagraph(heading);
192    }
193
194    for (i = 0; i < tocHeadings.length; i += 1) {
195      heading = tocHeadings[i];
196      if (!isManualTocHeading(heading)) {
197        continue;
198      }
199      next = heading.nextElementSibling;
200      if (isInternalAnchorList(next)) {
201        removeNode(next);
202        removeNode(heading);
203      }
204    }
205
206    for (i = 0; i < children.length; i += 1) {
207      if (firstHeading && children[i] === firstHeading) {
208        break;
209      }
210      if (isInternalAnchorList(children[i])) {
211        removeNode(children[i]);
212        break;
213      }
214    }
215  }
216
217  function slugify(text) {
218    var slug = normalizeText(text)
219      .toLowerCase()
220      .replace(/[^a-z0-9\s-]/g, '')
221      .replace(/\s+/g, '-')
222      .replace(/-+/g, '-')
223      .replace(/^-|-$/g, '');
224    return slug || 'section';
225  }
226
227  function listContains(nodes, node) {
228    var i;
229    for (i = 0; i < nodes.length; i += 1) {
230      if (nodes[i] === node) {
231        return true;
232      }
233    }
234    return false;
235  }
236
237  function collectHeadings(article) {
238    var nodes = article.querySelectorAll(HEADING_SELECTOR);
239    var headings = [];
240    var i;
241
242    for (i = 0; i < nodes.length; i += 1) {
243      if (nodes[i].closest && nodes[i].closest('[data-generated-toc]')) {
244        continue;
245      }
246      if (isManualTocHeading(nodes[i])) {
247        continue;
248      }
249      if (!getHeadingText(nodes[i])) {
250        continue;
251      }
252      headings.push(nodes[i]);
253    }
254
255    return headings;
256  }
257
258  function usedIdsOutsideHeadings(headings) {
259    var ids = Object.create(null);
260    var nodes = document.querySelectorAll('[id]');
261    var i;
262    var nodeId;
263
264    for (i = 0; i < nodes.length; i += 1) {
265      if (listContains(headings, nodes[i])) {
266        continue;
267      }
268      nodeId = nodes[i].getAttribute('id');
269      if (nodeId) {
270        ids[nodeId] = true;
271      }
272    }
273
274    return ids;
275  }
276
277  function ensureHeadingIds(headings) {
278    var ids = usedIdsOutsideHeadings(headings);
279    var i;
280    var id;
281    var base;
282    var candidate;
283    var counter;
284
285    for (i = 0; i < headings.length; i += 1) {
286      id = headings[i].getAttribute('id');
287      if (id && !ids[id]) {
288        ids[id] = true;
289        continue;
290      }
291
292      base = slugify(getHeadingText(headings[i]));
293      candidate = base;
294      counter = 2;
295
296      while (ids[candidate]) {
297        candidate = base + '-' + counter;
298        counter += 1;
299      }
300
301      headings[i].setAttribute('id', candidate);
302      ids[candidate] = true;
303    }
304  }
305
306  function shouldCollapseTocByDefault() {
307    return true;
308  }
309
310  function createToc(article, headings, collapseByDefault) {
311    var details = document.createElement('details');
312    var summary = document.createElement('summary');
313    var title = document.createElement('span');
314    var nav = document.createElement('nav');
315    var list = document.createElement('ol');
316    var minLevel = 6;
317    var i;
318    var level;
319    var item;
320    var link;
321
322    details.className = 'article-toc';
323    details.setAttribute('data-generated-toc', 'true');
324    if (!document.getElementById('table-of-contents')) {
325      details.id = 'table-of-contents';
326    }
327    if (
328      !collapseByDefault &&
329      !(window.matchMedia && window.matchMedia('(max-width: 860px)').matches)
330    ) {
331      details.open = true;
332    }
333
334    summary.className = 'article-toc__summary';
335    title.className = 'article-toc__title';
336    title.textContent = 'Table of contents';
337    summary.appendChild(title);
338    details.appendChild(summary);
339
340    nav.className = 'article-toc__nav';
341    nav.setAttribute('aria-label', 'Table of contents');
342    list.className = 'article-toc__list';
343
344    for (i = 0; i < headings.length; i += 1) {
345      level = headingLevel(headings[i]);
346      if (level < minLevel) {
347        minLevel = level;
348      }
349    }
350
351    for (i = 0; i < headings.length; i += 1) {
352      level = headingLevel(headings[i]);
353      item = document.createElement('li');
354      item.className = 'article-toc__item article-toc__item--level-' + level;
355      item.style.marginLeft = Math.max(0, (level - minLevel) * 14) + 'px';
356
357      link = document.createElement('a');
358      link.className = 'article-toc__link';
359      link.setAttribute('href', '#' + headings[i].id);
360      link.textContent = getHeadingText(headings[i]);
361
362      item.appendChild(link);
363      list.appendChild(item);
364    }
365
366    nav.appendChild(list);
367    details.appendChild(nav);
368    article.insertBefore(details, article.firstChild);
369
370    return details;
371  }
372
373  function updateActiveLink(details) {
374    var hash = window.location.hash ? decodeURIComponent(window.location.hash.slice(1)) : '';
375    var links = details.querySelectorAll('.article-toc__link');
376    var i;
377    var hrefId;
378
379    for (i = 0; i < links.length; i += 1) {
380      hrefId = decodeURIComponent((links[i].getAttribute('href') || '').replace(/^#/, ''));
381      if (hash && hrefId === hash) {
382        links[i].classList.add('is-active');
383      } else {
384        links[i].classList.remove('is-active');
385      }
386    }
387  }
388
389  function init() {
390    if (hasInitialized) {
391      return;
392    }
393    hasInitialized = true;
394
395    var articles = document.querySelectorAll(ARTICLE_SELECTOR);
396    var path = normalizePath(window.location.pathname);
397    var minimumHeadingCount = getMinimumHeadingCount(path);
398    var i;
399    var headings;
400    var collapseByDefault;
401    var toc;
402
403    if (isHomePage(path)) {
404      return;
405    }
406
407    for (i = 0; i < articles.length; i += 1) {
408      if (articles[i].querySelector('[data-generated-toc]')) {
409        continue;
410      }
411
412      removeHardcodedToc(articles[i]);
413      headings = collectHeadings(articles[i]);
414      headings = filterHeadingsByLevel(headings, articles[i]);
415      if (headings.length < minimumHeadingCount) {
416        continue;
417      }
418
419      ensureHeadingIds(headings);
420      collapseByDefault = shouldCollapseTocByDefault();
421      toc = createToc(articles[i], headings, collapseByDefault);
422      updateActiveLink(toc);
423
424      window.addEventListener('hashchange', (function (node) {
425        return function () {
426          updateActiveLink(node);
427        };
428      })(toc));
429    }
430  }
431
432  window.ArticleToc = {
433    init: init
434  };
435
436  if (document.readyState === 'loading') {
437    document.addEventListener('DOMContentLoaded', init);
438  } else {
439    init();
440  }
441})(window, document);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.