1function debounce(func, wait) { 2 var timeout; 3 4 return function () { 5 var context = this; 6 var args = arguments; 7 clearTimeout(timeout); 8 9 timeout = setTimeout(function () { 10 timeout = null; 11 func.apply(context, args); 12 }, wait); 13 }; 14} 15 16// Taken from mdbook 17// The strategy is as follows: 18// First, assign a value to each word in the document: 19// Words that correspond to search terms (stemmer aware): 40 20// Normal words: 2 21// First word in a sentence: 8 22// Then use a sliding window with a constant number of words and count the 23// sum of the values of the words within the window. Then use the window that got the 24// maximum sum. If there are multiple maximas, then get the last one. 25// Enclose the terms in <b>. 26function makeTeaser(body, terms) { 27 var TERM_WEIGHT = 40; 28 var NORMAL_WORD_WEIGHT = 2; 29 var FIRST_WORD_WEIGHT = 8; 30 var TEASER_MAX_WORDS = 30; 31 32 var stemmedTerms = terms.map(function (w) { 33 return elasticlunr.stemmer(w.toLowerCase()); 34 }); 35 var termFound = false; 36 var index = 0; 37 var weighted = []; // contains elements of ["word", weight, index_in_document] 38 39 // split in sentences, then words 40 var sentences = body.toLowerCase().split(". "); 41 42 for (var i in sentences) { 43 var words = sentences[i].split(" "); 44 var value = FIRST_WORD_WEIGHT; 45 46 for (var j in words) { 47 var word = words[j]; 48 49 if (word.length > 0) { 50 for (var k in stemmedTerms) { 51 if (elasticlunr.stemmer(word).startsWith(stemmedTerms[k])) { 52 value = TERM_WEIGHT; 53 termFound = true; 54 } 55 } 56 weighted.push([word, value, index]); 57 value = NORMAL_WORD_WEIGHT; 58 } 59 60 index += word.length; 61 index += 1; // ' ' or '.' if last word in sentence 62 } 63 64 index += 1; // because we split at a two-char boundary '. ' 65 } 66 67 if (weighted.length === 0) { 68 return body; 69 } 70 71 var windowWeights = []; 72 var windowSize = Math.min(weighted.length, TEASER_MAX_WORDS); 73 // We add a window with all the weights first 74 var curSum = 0; 75 for (var i = 0; i < windowSize; i++) { 76 curSum += weighted[i][1]; 77 } 78 windowWeights.push(curSum); 79 80 for (var i = 0; i < weighted.length - windowSize; i++) { 81 curSum -= weighted[i][1]; 82 curSum += weighted[i + windowSize][1]; 83 windowWeights.push(curSum); 84 } 85 86 // If we didn't find the term, just pick the first window 87 var maxSumIndex = 0; 88 if (termFound) { 89 var maxFound = 0; 90 // backwards 91 for (var i = windowWeights.length - 1; i >= 0; i--) { 92 if (windowWeights[i] > maxFound) { 93 maxFound = windowWeights[i]; 94 maxSumIndex = i; 95 } 96 } 97 } 98 99 var teaser = []; 100 var startIndex = weighted[maxSumIndex][2]; 101 for (var i = maxSumIndex; i < maxSumIndex + windowSize; i++) { 102 var word = weighted[i]; 103 if (startIndex < word[2]) { 104 // missing text from index to start of `word` 105 teaser.push(body.substring(startIndex, word[2])); 106 startIndex = word[2]; 107 } 108 109 // add <em/> around search terms 110 if (word[1] === TERM_WEIGHT) { 111 teaser.push("<b>"); 112 } 113 startIndex = word[2] + word[0].length; 114 teaser.push(body.substring(word[2], startIndex)); 115 116 if (word[1] === TERM_WEIGHT) { 117 teaser.push("</b>"); 118 } 119 } 120 teaser.push("â¦"); 121 return teaser.join(""); 122} 123 124function formatSearchResultItem(item, terms) { 125 return '<div class="search-results__item">' 126 + `<a href="${item.ref}">${item.doc.title}</a>` 127 + `<div>${makeTeaser(item.doc.body, terms)}</div>` 128 + '</div>'; 129} 130 131function initSearch() { 132 var $searchInput = document.getElementById("search"); 133 var $searchResults = document.querySelector(".search-results"); 134 var $searchResultsItems = document.querySelector(".search-results__items"); 135 var MAX_ITEMS = 10; 136 137 var options = { 138 bool: "AND", 139 fields: { 140 title: {boost: 2}, 141 body: {boost: 1}, 142 } 143 }; 144 var currentTerm = ""; 145 var index; 146 147 var initIndex = async function () { 148 if (index === undefined) { 149 index = fetch("/search_index.en.json") 150 .then( 151 async function(response) { 152 return await elasticlunr.Index.load(await response.json()); 153 } 154 ); 155 } 156 let res = await index; 157 return res; 158 } 159 160 $searchInput.addEventListener("keyup", debounce(async function() { 161 var term = $searchInput.value.trim(); 162 if (term === currentTerm) { 163 return; 164 } 165 $searchResults.style.display = term === "" ? "none" : "block"; 166 $searchResultsItems.innerHTML = ""; 167 currentTerm = term; 168 if (term === "") { 169 return; 170 } 171 172 var results = (await initIndex()).search(term, options); 173 if (results.length === 0) { 174 $searchResults.style.display = "none"; 175 return; 176 } 177 178 for (var i = 0; i < Math.min(results.length, MAX_ITEMS); i++) { 179 var item = document.createElement("li"); 180 item.innerHTML = formatSearchResultItem(results[i], term.split(" ")); 181 $searchResultsItems.appendChild(item); 182 } 183 }, 150)); 184 185 window.addEventListener('click', function(e) { 186 if ($searchResults.style.display == "block" && !$searchResults.contains(e.target)) { 187 $searchResults.style.display = "none"; 188 } 189 }); 190} 191 192 193if (document.readyState === "complete" || 194 (document.readyState !== "loading" && !document.documentElement.doScroll) 195) { 196 initSearch(); 197} else { 198 document.addEventListener("DOMContentLoaded", initSearch); 199}
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.