PageSourceSearch

https://premluczak.work/assets/js/table-of-contents.js?v=Sud8JSI_83vWSIiD

js premluczak.work collected 2026-10-04 08:04:27 UTC 8,517 bytes, 282 lines download raw bytes

1(function() { 'use strict';
2
3function initTableOfContents() {
4  const tocContainer = document.querySelector('.post-toc-list');
5  const tocNav = document.querySelector('.post-toc-nav');
6  const contentArea = document.querySelector('.gh-content');
7
8  if (!tocContainer || !contentArea) {
9    return;
10  }
11
12  function generateSlug(text) {
13    return 'toc-' + text
14      .toLowerCase()
15      .trim()
16      .replace(/[^\w\s-]/g, '')
17      .replace(/\s+/g, '-')
18      .replace(/-+/g, '-');
19  }
20
21  function createControls() {
22    const controlsDiv = document.createElement('div');
23    controlsDiv.className = 'post-toc-controls';
24
25    const themeIcon = document.querySelector('.theme-icon-sun');
26    let versionParam = '';
27    if (themeIcon && themeIcon.src) {
28      const match = themeIcon.src.match(/\?v=[^&]+/);
29      if (match) versionParam = match[0];
30    }
31
32    const prevBtn = document.createElement('button');
33    prevBtn.className = 'post-toc-nav-btn post-toc-nav-prev';
34    prevBtn.setAttribute('aria-label', 'Previous project');
35    prevBtn.style.display = 'flex';
36    prevBtn.style.visibility = 'visible';
37    prevBtn.style.opacity = '1';
38
39    const prevImg = document.createElement('img');
40    prevImg.src = '/assets/icons/arrow-left.svg' + versionParam;
41    prevImg.alt = 'Previous';
42    prevBtn.appendChild(prevImg);
43
44    const prevTooltip = document.createElement('span');
45    prevTooltip.className = 'post-toc-tooltip';
46    prevTooltip.textContent = 'Previous project';
47    prevBtn.appendChild(prevTooltip);
48
49    try {
50      prevBtn.addEventListener('click', async () => {
51      const article = document.querySelector('article.post');
52      const currentId = article ? article.getAttribute('data-post-id') : null;
53      const tagsStr = article ? article.getAttribute('data-post-tags') : null;
54
55      if (currentId && tagsStr) {
56        const tags = tagsStr
57          .split(',')
58          .map(t => t.trim())
59          .filter(t => t && !t.startsWith('hash-'));
60
61        const tagSlug = tags.length > 0 ? tags[0] : null;
62
63        if (!tagSlug) {
64          const prevLink = document.querySelector('a.post-nav-prev');
65          if (prevLink && prevLink.offsetParent !== null) {
66            prevLink.click();
67          }
68          return;
69        }
70
71        try {
72          const posts = await window.fetchPostsByTag(tagSlug);
73
74          const currentIndex = posts.findIndex(p => p.id === currentId);
75          if (currentIndex >= 0) {
76            const prevIndex = currentIndex === 0 ? posts.length - 1 : currentIndex - 1;
77            window.location.href = posts[prevIndex].url;
78          }
79        } catch (e) {
80          //
81        }
82      } else {
83        const prevLink = document.querySelector('a.post-nav-prev');
84        if (prevLink && prevLink.offsetParent !== null) {
85          prevLink.click();
86        }
87      }
88      });
89    } catch (e) {
90      console.error('Error setting up prev button listener:', e);
91    }
92
93    const nextBtn = document.createElement('button');
94    nextBtn.className = 'post-toc-nav-btn post-toc-nav-next';
95    nextBtn.setAttribute('aria-label', 'Next project');
96    nextBtn.style.display = 'flex';
97    nextBtn.style.visibility = 'visible';
98    nextBtn.style.opacity = '1';
99
100    const nextImg = document.createElement('img');
101    nextImg.src = '/assets/icons/arrow-right.svg' + versionParam;
102    nextImg.alt = 'Next';
103    nextBtn.appendChild(nextImg);
104
105    const nextTooltip = document.createElement('span');
106    nextTooltip.className = 'post-toc-tooltip';
107    nextTooltip.textContent = 'Next project';
108    nextBtn.appendChild(nextTooltip);
109
110    try {
111      nextBtn.addEventListener('click', async () => {
112      const article = document.querySelector('article.post');
113      const currentId = article ? article.getAttribute('data-post-id') : null;
114      const tagsStr = article ? article.getAttribute('data-post-tags') : null;
115
116      if (currentId && tagsStr) {
117        const tags = tagsStr
118          .split(',')
119          .map(t => t.trim())
120          .filter(t => t && !t.startsWith('hash-'));
121
122        const tagSlug = tags.length > 0 ? tags[0] : null;
123
124        if (!tagSlug) {
125          const nextLink = document.querySelector('a.post-nav-next');
126          if (nextLink && nextLink.offsetParent !== null) {
127            nextLink.click();
128          }
129          return;
130        }
131
132        try {
133          const posts = await window.fetchPostsByTag(tagSlug);
134
135          const currentIndex = posts.findIndex(p => p.id === currentId);
136          if (currentIndex >= 0) {
137            const nextIndex = currentIndex === posts.length - 1 ? 0 : currentIndex + 1;
138            window.location.href = posts[nextIndex].url;
139          }
140        } catch (e) {
141          //
142        }
143      } else {
144        const nextLink = document.querySelector('a.post-nav-next');
145        if (nextLink && nextLink.offsetParent !== null) {
146          nextLink.click();
147        }
148      }
149      });
150    } catch (e) {
151      console.error('Error setting up next button listener:', e);
152    }
153
154    controlsDiv.appendChild(prevBtn);
155    controlsDiv.appendChild(nextBtn);
156
157    return controlsDiv;
158  }
159
160  function addStartItem() {
161    const startLi = document.createElement('li');
162    startLi.className = 'post-toc-item post-toc-item-start';
163
164    const startLink = document.createElement('a');
165    startLink.href = '#start';
166    startLink.className = 'post-toc-link';
167    startLink.textContent = 'Start';
168
169    startLink.addEventListener('click', (e) => {
170      e.preventDefault();
171      window.scrollTo({ top: 0, behavior: 'smooth' });
172      updateActiveHeader(null);
173    });
174
175    startLi.appendChild(startLink);
176    tocContainer.appendChild(startLi);
177  }
178
179  const controls = createControls();
180  const controlsLi = document.createElement('li');
181  controlsLi.className = 'post-toc-controls-item';
182  controlsLi.appendChild(controls);
183  tocContainer.appendChild(controlsLi);
184
185  addStartItem();
186
187  const headers = contentArea.querySelectorAll('h2');
188  if (headers.length === 0) {
189    const testLi = document.createElement('li');
190    testLi.className = 'post-toc-item';
191    const testLink = document.createElement('a');
192    testLink.href = '#';
193    testLink.className = 'post-toc-link';
194    testLink.textContent = 'Test';
195    testLi.appendChild(testLink);
196    tocContainer.appendChild(testLi);
197
198    const testLi2 = document.createElement('li');
199    testLi2.className = 'post-toc-item post-toc-item--h3';
200    const testLink2 = document.createElement('a');
201    testLink2.href = '#';
202    testLink2.className = 'post-toc-link';
203    testLink2.textContent = 'Sub';
204    testLi2.appendChild(testLink2);
205    tocContainer.appendChild(testLi2);
206    return;
207  }
208
209  headers.forEach(header => {
210    if (!header.id) {
211      header.id = generateSlug(header.textContent);
212    }
213
214    const li = document.createElement('li');
215    li.className = 'post-toc-item';
216
217    const link = document.createElement('a');
218    link.href = `#${header.id}`;
219    link.className = 'post-toc-link';
220    link.textContent = header.textContent;
221
222    link.addEventListener('click', (e) => {
223      e.preventDefault();
224      header.scrollIntoView({ behavior: 'smooth' });
225      updateActiveHeader(header);
226    });
227
228    li.appendChild(link);
229    tocContainer.appendChild(li);
230  });
231
232  function trackScrollPosition() {
233    const scrollTop = window.scrollY;
234
235    if (scrollTop < 100) {
236      updateActiveHeader(null);
237    }
238  }
239
240  const observerOptions = {
241    rootMargin: '-100px 0px -66% 0px'
242  };
243
244  const observer = new IntersectionObserver((entries) => {
245    entries.forEach(entry => {
246      if (entry.isIntersecting) {
247        updateActiveHeader(entry.target);
248      }
249    });
250  }, observerOptions);
251
252  headers.forEach(header => observer.observe(header));
253
254  document.addEventListener('scroll', trackScrollPosition, { passive: true });
255
256  function updateActiveHeader(activeHeader) {
257    document.querySelectorAll('.post-toc-item--active').forEach(item => {
258      item.classList.remove('post-toc-item--active');
259    });
260
261    if (activeHeader === null) {
262      const startItem = tocContainer.querySelector('.post-toc-item-start');
263      if (startItem) {
264        startItem.classList.add('post-toc-item--active');
265      }
266    } else {
267      const activeItem = tocContainer.querySelector(
268        `a[href="#${activeHeader.id}"]`
269      )?.closest('.post-toc-item');
270
271      if (activeItem) {
272        activeItem.classList.add('post-toc-item--active');
273      }
274    }
275  }
276}
277
278if (typeof window !== 'undefined') {
279  window.initTableOfContents = initTableOfContents;
280}
281
282})();

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.