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.