(function () { const navLinks = document.querySelectorAll('.wiki-nav a[href^="#"]'); const sections = Array.from(navLinks).map((link) => { const id = link.getAttribute('href').slice(1); return { link, el: document.getElementById(id) }; }).filter((s) => s.el); function setActive(id) { navLinks.forEach((a) => { a.classList.toggle('active', a.getAttribute('href') === '#' + id); }); } function scrollToTarget(id, el) { const target = el || document.getElementById(id); if (!target) return; target.scrollIntoView({ behavior: 'smooth', block: 'start' }); history.replaceState(null, '', '#' + id); const section = target.closest('section') || (target.matches && target.matches('section') ? target : null); if (section) setActive(section.id); } navLinks.forEach((link) => { link.addEventListener('click', (e) => { e.preventDefault(); const id = link.getAttribute('href').slice(1); scrollToTarget(id); }); }); if ('IntersectionObserver' in window && sections.length) { const observer = new IntersectionObserver( (entries) => { const visible = entries .filter((e) => e.isIntersecting) .sort((a, b) => b.intersectionRatio - a.intersectionRatio)[0]; if (visible) setActive(visible.target.id); }, { rootMargin: '-20% 0px -60% 0px', threshold: [0, 0.25, 0.5] } ); sections.forEach((s) => observer.observe(s.el)); } const hash = window.location.hash.slice(1); if (hash && document.getElementById(hash)) { setActive(hash); const section = document.getElementById(hash).closest('section'); if (section) setActive(section.id); } else if (sections.length) { setActive(sections[0].el.id); } /* ── Search ── */ const searchInput = document.getElementById('wiki-search-input'); const searchResults = document.getElementById('wiki-search-results'); const HIGHLIGHT_CLASS = 'wiki-search-highlight'; let activeHighlights = []; function stripText(el) { return (el.textContent || '').replace(/\s+/g, ' ').trim(); } function buildSearchIndex() { const entries = []; document.querySelectorAll('.wiki-content section').forEach((section) => { const sectionId = section.id; const sectionTitle = stripText(section.querySelector('h2') || section); section.querySelectorAll('h3, h4').forEach((heading) => { const headingId = heading.id || sectionId; entries.push({ id: headingId, sectionId, title: stripText(heading), sectionTitle, text: stripText(heading), el: heading, }); }); section.querySelectorAll('p, li, td').forEach((block) => { const text = stripText(block); if (text.length < 12) return; entries.push({ id: sectionId, sectionId, title: sectionTitle, sectionTitle, text, el: block, }); }); }); return entries; } const searchIndex = buildSearchIndex(); function clearHighlights() { activeHighlights.forEach((mark) => { const parent = mark.parentNode; if (!parent) return; parent.replaceChild(document.createTextNode(mark.textContent), mark); parent.normalize(); }); activeHighlights = []; } function escapeRegExp(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } function highlightMatches(el, query) { clearHighlights(); if (!el || !query) return; const terms = query.toLowerCase().split(/\s+/).filter((t) => t.length > 1); if (!terms.length) return; const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT); const textNodes = []; while (walker.nextNode()) textNodes.push(walker.currentNode); const pattern = new RegExp('(' + terms.map(escapeRegExp).join('|') + ')', 'gi'); textNodes.forEach((node) => { const val = node.nodeValue; if (!val || !pattern.test(val)) return; pattern.lastIndex = 0; const frag = document.createDocumentFragment(); let last = 0; val.replace(pattern, (match, _g, offset) => { if (offset > last) { frag.appendChild(document.createTextNode(val.slice(last, offset))); } const mark = document.createElement('mark'); mark.className = HIGHLIGHT_CLASS; mark.textContent = match; frag.appendChild(mark); activeHighlights.push(mark); last = offset + match.length; return match; }); if (last < val.length) { frag.appendChild(document.createTextNode(val.slice(last))); } node.parentNode.replaceChild(frag, node); }); } function scoreEntry(entry, terms) { const title = entry.title.toLowerCase(); const text = entry.text.toLowerCase(); let score = 0; terms.forEach((term) => { if (title.includes(term)) score += 10; if (text.includes(term)) score += 3; if (title.startsWith(term)) score += 5; }); return score; } function snippet(text, terms, maxLen) { const lower = text.toLowerCase(); let idx = -1; for (const term of terms) { const i = lower.indexOf(term); if (i !== -1 && (idx === -1 || i < idx)) idx = i; } if (idx === -1) return text.slice(0, maxLen) + (text.length > maxLen ? '…' : ''); const start = Math.max(0, idx - 30); const slice = text.slice(start, start + maxLen); return (start > 0 ? '…' : '') + slice + (start + maxLen < text.length ? '…' : ''); } function renderSearchResults(query) { if (!searchResults) return; const terms = query.toLowerCase().split(/\s+/).filter((t) => t.length > 1); searchResults.innerHTML = ''; if (!terms.length) { searchResults.hidden = true; clearHighlights(); return; } const hits = searchIndex .map((entry) => ({ entry, score: scoreEntry(entry, terms) })) .filter((h) => h.score > 0) .sort((a, b) => b.score - a.score) .slice(0, 12); if (!hits.length) { const li = document.createElement('li'); li.className = 'wiki-search-empty'; li.textContent = 'No matches'; searchResults.appendChild(li); searchResults.hidden = false; return; } hits.forEach(({ entry }) => { const li = document.createElement('li'); const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'wiki-search-hit'; const title = document.createElement('span'); title.className = 'wiki-search-hit-title'; title.textContent = entry.title; const preview = document.createElement('span'); preview.className = 'wiki-search-hit-preview'; preview.textContent = snippet(entry.text, terms, 80); btn.appendChild(title); btn.appendChild(preview); btn.addEventListener('click', () => { clearHighlights(); const scrollEl = entry.el.id ? entry.el : document.getElementById(entry.id); scrollToTarget(entry.id, scrollEl); const highlightRoot = entry.el.closest('section') || entry.el; highlightMatches(highlightRoot, query); searchResults.hidden = true; searchInput.blur(); }); li.appendChild(btn); searchResults.appendChild(li); }); searchResults.hidden = false; } if (searchInput && searchResults) { let debounceTimer; searchInput.addEventListener('input', () => { clearTimeout(debounceTimer); debounceTimer = setTimeout(() => renderSearchResults(searchInput.value.trim()), 120); }); searchInput.addEventListener('keydown', (e) => { if (e.key === 'Escape') { searchInput.value = ''; searchResults.hidden = true; clearHighlights(); } }); document.addEventListener('click', (e) => { if (!e.target.closest('.wiki-search')) { searchResults.hidden = true; } }); } })();