1// Client-side docs search + TOC scroll-spy 2(function() { 3 'use strict'; 4 5 // === Search === 6 var input = document.getElementById('searchInput'); 7 var resultsBox = document.getElementById('searchResults'); 8 var index = null; 9 var indexPromise = null; 10 var activeIdx = -1; 11 12 function loadIndex() { 13 if (!indexPromise) { 14 indexPromise = fetch('/search-index.json') 15 .then(function(r) { return r.json(); }) 16 .then(function(data) { index = data; return data; }) 17 .catch(function() { index = []; return []; }); 18 } 19 return indexPromise; 20 } 21 22 function esc(s) { 23 return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); 24 } 25 26 function highlight(text, query) { 27 var lower = text.toLowerCase(); 28 var q = query.toLowerCase(); 29 var pos = lower.indexOf(q); 30 if (pos === -1) return esc(text); 31 return esc(text.slice(0, pos)) + '<mark>' + esc(text.slice(pos, pos + query.length)) + '</mark>' + esc(text.slice(pos + query.length)); 32 } 33 34 function search(query) { 35 if (!index) return []; 36 var q = query.trim().toLowerCase(); 37 if (!q) return []; 38 var titleHits = []; 39 var headingHits = []; 40 var excerptHits = []; 41 for (var i = 0; i < index.length; i++) { 42 var p = index[i]; 43 p._matchedHeading = null; 44 var title = (p.title || '').toLowerCase(); 45 var headingList = p.headings || []; 46 var matchedHeading = null; 47 for (var j = 0; j < headingList.length; j++) { 48 if (headingList[j].toLowerCase().indexOf(q) !== -1) { matchedHeading = headingList[j]; break; } 49 } 50 var excerpt = (p.excerpt || '').toLowerCase(); 51 var keywordMatch = (p.keywords || []).some(function(keyword) { return keyword.toLowerCase().indexOf(q) !== -1; }); 52 if (title.indexOf(q) !== -1) { 53 titleHits.push(p); 54 } else if (matchedHeading) { 55 p._matchedHeading = matchedHeading; 56 headingHits.push(p); 57 } else if (excerpt.indexOf(q) !== -1 || keywordMatch) { 58 excerptHits.push(p); 59 } 60 if (titleHits.length >= 8 && headingHits.length >= 8 && excerptHits.length >= 6) break; 61 } 62 return titleHits.concat(headingHits).concat(excerptHits).slice(0, 12); 63 } 64 65 function render(query) { 66 var q = query.trim(); 67 if (!q) { 68 resultsBox.hidden = true; 69 resultsBox.innerHTML = ''; 70 activeIdx = -1; 71 return; 72 } 73 var hits = search(q); 74 if (!hits.length) { 75 resultsBox.innerHTML = '<div class="search-empty">æªæ¾å°ä¸ã' + esc(q) + 'ãç¸å ³ç页é¢</div>'; 76 resultsBox.hidden = false; 77 activeIdx = -1; 78 return; 79 } 80 var html = ''; 81 for (var i = 0; i < hits.length; i++) { 82 var p = hits[i]; 83 var sub = p._matchedHeading ? 'ç« èï¼' + p._matchedHeading : (p.excerpt || '').slice(0, 60); 84 html += '<a class="search-result-item" data-idx="' + i + '" href="' + p.url + '">' + 85 '<span class="search-result-title">' + highlight(p.title || p.slug, q) + '</span>' + 86 '<span class="search-result-sub">' + esc(sub) + '</span>' + 87 '</a>'; 88 } 89 resultsBox.innerHTML = html; 90 resultsBox.hidden = false; 91 activeIdx = -1; 92 } 93 94 function moveActive(delta) { 95 var items = resultsBox.querySelectorAll('.search-result-item'); 96 if (!items.length) return; 97 activeIdx = (activeIdx + delta + items.length) % items.length; 98 items.forEach(function(it, i) { it.classList.toggle('active', i === activeIdx); }); 99 items[activeIdx].scrollIntoView({ block: 'nearest' }); 100 } 101 102 if (input && resultsBox) { 103 var debounce = null; 104 input.addEventListener('input', function() { 105 clearTimeout(debounce); 106 var q = this.value; 107 debounce = setTimeout(function() { 108 loadIndex().then(function() { render(q); }); 109 }, 120); 110 }); 111 input.addEventListener('focus', function() { 112 loadIndex(); 113 if (this.value.trim()) render(this.value); 114 }); 115 input.addEventListener('keydown', function(e) { 116 if (e.key === 'ArrowDown') { e.preventDefault(); moveActive(1); } 117 else if (e.key === 'ArrowUp') { e.preventDefault(); moveActive(-1); } 118 else if (e.key === 'Enter') { 119 var items = resultsBox.querySelectorAll('.search-result-item'); 120 var target = activeIdx >= 0 ? items[activeIdx] : items[0]; 121 if (target) { window.location.href = target.getAttribute('href'); } 122 } else if (e.key === 'Escape') { 123 resultsBox.hidden = true; 124 this.blur(); 125 } 126 }); 127 document.addEventListener('click', function(e) { 128 if (!e.target.closest('.sidebar-search') && !e.target.closest('.search-results')) { 129 resultsBox.hidden = true; 130 } 131 }); 132 // "/" shortcut focuses search 133 document.addEventListener('keydown', function(e) { 134 if (e.key === '/' && !e.target.closest('input, textarea, select')) { 135 e.preventDefault(); 136 input.focus(); 137 // On mobile the sidebar may be closed 138 var sidebar = document.getElementById('sidebar'); 139 if (sidebar && window.innerWidth <= 1024 && !sidebar.classList.contains('open')) { 140 var toggle = document.getElementById('sidebarToggle'); 141 if (toggle) toggle.click(); 142 } 143 } 144 }); 145 } 146 147 // === TOC scroll-spy === 148 var tocNav = document.getElementById('tocNav'); 149 if (tocNav) { 150 var links = tocNav.querySelectorAll('.toc-link'); 151 var headings = []; 152 links.forEach(function(link) { 153 var el = document.getElementById(link.getAttribute('data-toc-id')); 154 if (el) headings.push({ el: el, link: link }); 155 }); 156 157 if (headings.length) { 158 var currentLink = null; 159 function setActive(link) { 160 if (currentLink === link) return; 161 if (currentLink) currentLink.classList.remove('active'); 162 if (link) link.classList.add('active'); 163 currentLink = link; 164 } 165 166 var observer = new IntersectionObserver(function(entries) {
167 entries.forEach(function(entry) { 168 if (entry.isIntersecting) { 169 for (var i = 0; i < headings.length; i++) { 170 if (headings[i].el === entry.target) { setActive(headings[i].link); break; } 171 } 172 } 173 }); 174 }, { rootMargin: '-80px 0px -70% 0px', threshold: 0 }); 175 176 headings.forEach(function(h) { observer.observe(h.el); }); 177 178 // Smooth-scroll on click 179 tocNav.addEventListener('click', function(e) { 180 var link = e.target.closest('.toc-link'); 181 if (!link) return; 182 e.preventDefault(); 183 var el = document.getElementById(link.getAttribute('data-toc-id')); 184 if (el) { 185 var y = el.getBoundingClientRect().top + window.pageYOffset - 90; 186 window.scrollTo({ top: y, behavior: 'smooth' }); 187 history.replaceState(null, '', '#' + link.getAttribute('data-toc-id')); 188 setActive(link); 189 } 190 }); 191 } 192 } 193})();
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.