PageSourceSearch

https://www.ruby-lang.org/javascripts/toc.js

js ruby-lang.org collected 2026-10-01 07:20:46 UTC 4,372 bytes, 149 lines download raw bytes

1// Table of Contents Generator
2(function() {
3  'use strict';
4
5  // Generate TOC from content headings
6  function generateTOC() {
7    const content = document.getElementById('content');
8    const tocContainers = document.querySelectorAll('.toc-content');
9
10    if (!content || tocContainers.length === 0) return;
11
12    // Find all h2 and h3 headings with IDs
13    const headings = content.querySelectorAll('h2[id], h3[id], h4[id], h5[id], h6[id]');
14
15    if (headings.length === 0) {
16      // Hide TOC wrappers if no headings found
17      document.querySelectorAll('.toc-wrapper').forEach(el => el.style.display = 'none');
18      return;
19    }
20
21    // Build TOC HTML
22    const tocHTML = buildTOCHTML(headings);
23
24    // Insert TOC into all containers
25    tocContainers.forEach(container => {
26      container.innerHTML = tocHTML;
27    });
28
29    // Set up scroll tracking
30    setupScrollTracking(headings);
31  }
32
33  function buildTOCHTML(headings) {
34    let html = '<ul class="space-y-3 text-sm list-disc pl-5 marker:text-stone-400 dark:marker:text-stone-500">';
35    let currentLevel = 2;
36
37    headings.forEach(heading => {
38      const level = parseInt(heading.tagName.substr(1));
39      const text = heading.textContent;
40      const id = heading.id;
41
42      if (level > currentLevel) {
43        html += '<ul class="space-y-3 pl-4 mt-2 list-disc marker:text-stone-400 dark:marker:text-stone-500">';
44      } else if (level < currentLevel) {
45        html += '</ul>';
46      }
47
48      html += `<li class="mb-3"><a href="#${id}" class="toc-link text-stone-700 dark:text-stone-300 no-underline transition-colors" data-heading-id="${id}">${text}</a></li>`;
49      currentLevel = level;
50    });
51
52    // Close any open ul tags
53    while (currentLevel > 2) {
54      html += '</ul>';
55      currentLevel--;
56    }
57
58    html += '</ul>';
59    return html;
60  }
61
62  // Set up scroll tracking
63  function setupScrollTracking(headings) {
64    // Convert NodeList to Array for easier manipulation
65    const headingsArray = Array.from(headings);
66
67    // Update active heading on scroll
68    function updateActiveHeading() {
69      // Threshold: top of viewport + small offset (e.g., 100px for header)
70      const scrollOffset = 100;
71
72      // Find the heading closest to the top of the viewport
73      let activeHeading = null;
74
75      for (let i = headingsArray.length - 1; i >= 0; i--) {
76        const heading = headingsArray[i];
77        const rect = heading.getBoundingClientRect();
78
79        // Check if heading is above or at the scroll offset
80        if (rect.top <= scrollOffset) {
81          activeHeading = heading;
82          break;
83        }
84      }
85
86      // If no heading is above the offset, use the first one if it's visible
87      if (!activeHeading && headingsArray.length > 0) {
88        const firstRect = headingsArray[0].getBoundingClientRect();
89        if (firstRect.top > scrollOffset) {
90          // We're above the first heading, no active heading
91          clearActiveLink();
92          return;
93        }
94      }
95
96      if (activeHeading) {
97        setActiveLink(activeHeading.id);
98      } else {
99        clearActiveLink();
100      }
101    }
102
103    // Update on scroll with throttling for performance
104    let ticking = false;
105    window.addEventListener('scroll', function() {
106      if (!ticking) {
107        window.requestAnimationFrame(function() {
108          updateActiveHeading();
109          ticking = false;
110        });
111        ticking = true;
112      }
113    });
114
115    // Update on page load and hash change
116    updateActiveHeading();
117    window.addEventListener('hashchange', function() {
118      setTimeout(updateActiveHeading, 100);
119    });
120  }
121
122  // Set active link in TOC
123  function setActiveLink(id) {
124    // Remove active class from all links first
125    document.querySelectorAll('.toc-link').forEach(link => {
126      link.classList.remove('toc-active');
127    });
128
129    // Add active class to current link
130    const links = document.querySelectorAll(`.toc-link[data-heading-id="${id}"]`);
131    links.forEach(link => {
132      link.classList.add('toc-active');
133    });
134  }
135
136  // Clear active link
137  function clearActiveLink() {
138    document.querySelectorAll('.toc-link').forEach(link => {
139      link.classList.remove('toc-active');
140    });
141  }
142
143  // Run on page load
144  if (document.readyState === 'loading') {
145    document.addEventListener('DOMContentLoaded', generateTOC);
146  } else {
147    generateTOC();
148  }
149})();

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.