1"use strict"; 2window.search = window.search || {}; 3(function search(search) { 4 // Search functionality 5 // 6 // You can use !hasFocus() to prevent keyhandling in your key 7 // event handlers while the user is typing their search. 8 9 if (!Mark || !elasticlunr) { 10 return; 11 } 12 13 //IE 11 Compatibility from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith 14 if (!String.prototype.startsWith) { 15 String.prototype.startsWith = function(search, pos) { 16 return this.substr(!pos || pos < 0 ? 0 : +pos, search.length) === search; 17 }; 18 } 19 20 var search_wrap = document.getElementById('search-wrapper'), 21 searchbar = document.getElementById('searchbar'), 22 searchbar_outer = document.getElementById('searchbar-outer'), 23 searchresults = document.getElementById('searchresults'), 24 searchresults_outer = document.getElementById('searchresults-outer'), 25 searchresults_header = document.getElementById('searchresults-header'), 26 searchicon = document.getElementById('search-toggle'), 27 content = document.getElementById('content'), 28 29 searchindex = null, 30 doc_urls = [], 31 results_options = { 32 teaser_word_count: 30, 33 limit_results: 30, 34 }, 35 search_options = { 36 bool: "AND", 37 expand: true, 38 fields: { 39 title: {boost: 1}, 40 body: {boost: 1}, 41 breadcrumbs: {boost: 0} 42 } 43 }, 44 mark_exclude = [], 45 marker = new Mark(content), 46 current_searchterm = "", 47 URL_SEARCH_PARAM = 'search', 48 URL_MARK_PARAM = 'highlight', 49 teaser_count = 0, 50 51 SEARCH_HOTKEY_KEYCODE = 83, 52 ESCAPE_KEYCODE = 27, 53 DOWN_KEYCODE = 40, 54 UP_KEYCODE = 38, 55 SELECT_KEYCODE = 13; 56 57 function hasFocus() { 58 return searchbar === document.activeElement; 59 } 60 61 function removeChildren(elem) { 62 while (elem.firstChild) { 63 elem.removeChild(elem.firstChild); 64 } 65 } 66 67 // Helper to parse a url into its building blocks. 68 function parseURL(url) { 69 var a = document.createElement('a'); 70 a.href = url; 71 return { 72 source: url, 73 protocol: a.protocol.replace(':',''), 74 host: a.hostname, 75 port: a.port, 76 params: (function(){ 77 var ret = {}; 78 var seg = a.search.replace(/^\?/,'').split('&'); 79 var len = seg.length, i = 0, s; 80 for (;i<len;i++) { 81 if (!seg[i]) { continue; } 82 s = seg[i].split('='); 83 ret[s[0]] = s[1]; 84 } 85 return ret; 86 })(), 87 file: (a.pathname.match(/\/([^/?#]+)$/i) || [,''])[1], 88 hash: a.hash.replace('#',''), 89 path: a.pathname.replace(/^([^/])/,'/$1') 90 }; 91 } 92 93 // Helper to recreate a url string from its building blocks. 94 function renderURL(urlobject) { 95 var url = urlobject.protocol + "://" + urlobject.host; 96 if (urlobject.port != "") { 97 url += ":" + urlobject.port; 98 } 99 url += urlobject.path; 100 var joiner = "?"; 101 for(var prop in urlobject.params) { 102 if(urlobject.params.hasOwnProperty(prop)) { 103 url += joiner + prop + "=" + urlobject.params[prop]; 104 joiner = "&"; 105 } 106 } 107 if (urlobject.hash != "") { 108 url += "#" + urlobject.hash; 109 } 110 return url; 111 } 112 113 // Helper to escape html special chars for displaying the teasers 114 var escapeHTML = (function() { 115 var MAP = { 116 '&': '&', 117 '<': '<', 118 '>': '>', 119 '"': '"', 120 "'": ''' 121 }; 122 var repl = function(c) { return MAP[c]; }; 123 return function(s) { 124 return s.replace(/[&<>'"]/g, repl); 125 }; 126 })(); 127 128 function formatSearchMetric(count, searchterm) { 129 if (count == 1) { 130 return count + " search result for '" + searchterm + "':"; 131 } else if (count == 0) { 132 return "No search results for '" + searchterm + "'."; 133 } else { 134 return count + " search results for '" + searchterm + "':"; 135 } 136 } 137 138 function formatSearchResult(result, searchterms) { 139 var teaser = makeTeaser(escapeHTML(result.doc.body), searchterms); 140 teaser_count++; 141 142 // The ?URL_MARK_PARAM= parameter belongs inbetween the page and the #heading-anchor 143 var url = doc_urls[result.ref].split("#"); 144 if (url.length == 1) { // no anchor found 145 url.push(""); 146 } 147 148 return '<a href="' + path_to_root + url[0] + '?' + URL_MARK_PARAM + '=' + searchterms + '#' + url[1] 149 + '" aria-details="teaser_' + teaser_count + '">' + result.doc.breadcrumbs + '</a>' 150 + '<span class="teaser" id="teaser_' + teaser_count + '" aria-label="Search Result Teaser">' 151 + teaser + '</span>'; 152 } 153 154 function makeTeaser(body, searchterms) { 155 // The strategy is as follows: 156 // First, assign a value to each word in the document: 157 // Words that correspond to search terms (stemmer aware): 40 158 // Normal words: 2 159 // First word in a sentence: 8 160 // Then use a sliding window with a constant number of words and count the 161 // sum of the values of the words within the window. Then use the window that got the 162 // maximum sum. If there are multiple maximas, then get the last one. 163 // Enclose the terms in <em>. 164 var stemmed_searchterms = searchterms.map(function(w) { 165 return elasticlunr.stemmer(w.toLowerCase()); 166 }); 167 var searchterm_weight = 40; 168 var weighted = []; // contains elements of ["word", weight, index_in_document] 169 // split in sentences, then words 170 var sentences = body.toLowerCase().split('. '); 171 var index = 0; 172 var value = 0; 173 var searchterm_found = false;
174 for (var sentenceindex in sentences) { 175 var words = sentences[sentenceindex].split(' '); 176 value = 8; 177 for (var wordindex in words) { 178 var word = words[wordindex]; 179 if (word.length > 0) { 180 for (var searchtermindex in stemmed_searchterms) { 181 if (elasticlunr.stemmer(word).startsWith(stemmed_searchterms[searchtermindex])) { 182 value = searchterm_weight; 183 searchterm_found = true; 184 } 185 }; 186 weighted.push([word, value, index]); 187 value = 2; 188 } 189 index += word.length; 190 index += 1; // ' ' or '.' if last word in sentence 191 }; 192 index += 1; // because we split at a two-char boundary '. ' 193 }; 194 195 if (weighted.length == 0) { 196 return body; 197 } 198 199 var window_weight = []; 200 var window_size = Math.min(weighted.length, results_options.teaser_word_count); 201 202 var cur_sum = 0; 203 for (var wordindex = 0; wordindex < window_size; wordindex++) { 204 cur_sum += weighted[wordindex][1]; 205 }; 206 window_weight.push(cur_sum); 207 for (var wordindex = 0; wordindex < weighted.length - window_size; wordindex++) { 208 cur_sum -= weighted[wordindex][1]; 209 cur_sum += weighted[wordindex + window_size][1]; 210 window_weight.push(cur_sum); 211 }; 212 213 if (searchterm_found) { 214 var max_sum = 0; 215 var max_sum_window_index = 0; 216 // backwards 217 for (var i = window_weight.length - 1; i >= 0; i--) { 218 if (window_weight[i] > max_sum) { 219 max_sum = window_weight[i]; 220 max_sum_window_index = i; 221 } 222 }; 223 } else { 224 max_sum_window_index = 0; 225 } 226 227 // add <em/> around searchterms 228 var teaser_split = []; 229 var index = weighted[max_sum_window_index][2]; 230 for (var i = max_sum_window_index; i < max_sum_window_index+window_size; i++) { 231 var word = weighted[i]; 232 if (index < word[2]) { 233 // missing text from index to start of `word` 234 teaser_split.push(body.substring(index, word[2])); 235 index = word[2]; 236 } 237 if (word[1] == searchterm_weight) { 238 teaser_split.push("<em>") 239 } 240 index = word[2] + word[0].length; 241 teaser_split.push(body.substring(word[2], index)); 242 if (word[1] == searchterm_weight) { 243 teaser_split.push("</em>") 244 } 245 }; 246 247 return teaser_split.join(''); 248 } 249 250 function init(config) { 251 results_options = config.results_options; 252 search_options = config.search_options; 253 searchbar_outer = config.searchbar_outer; 254 doc_urls = config.doc_urls; 255 searchindex = elasticlunr.Index.load(config.index); 256 257 // Set up events 258 searchicon.addEventListener('click', function(e) { searchIconClickHandler(); }, false); 259 searchbar.addEventListener('keyup', function(e) { searchbarKeyUpHandler(); }, false); 260 document.addEventListener('keydown', function(e) { globalKeyHandler(e); }, false); 261 // If the user uses the browser buttons, do the same as if a reload happened 262 window.onpopstate = function(e) { doSearchOrMarkFromUrl(); }; 263 // Suppress "submit" events so the page doesn't reload when the user presses Enter 264 document.addEventListener('submit', function(e) { e.preventDefault(); }, false); 265 266 // If reloaded, do the search or mark again, depending on the current url parameters 267 doSearchOrMarkFromUrl(); 268 } 269 270 function unfocusSearchbar() { 271 // hacky, but just focusing a div only works once 272 var tmp = document.createElement('input'); 273 tmp.setAttribute('style', 'position: absolute; opacity: 0;'); 274 searchicon.appendChild(tmp); 275 tmp.focus(); 276 tmp.remove(); 277 } 278 279 // On reload or browser history backwards/forwards events, parse the url and do search or mark 280 function doSearchOrMarkFromUrl() { 281 // Check current URL for search request 282 var url = parseURL(window.location.href); 283 if (url.params.hasOwnProperty(URL_SEARCH_PARAM) 284 && url.params[URL_SEARCH_PARAM] != "") { 285 showSearch(true); 286 searchbar.value = decodeURIComponent( 287 (url.params[URL_SEARCH_PARAM]+'').replace(/\+/g, '%20')); 288 searchbarKeyUpHandler(); // -> doSearch() 289 } else { 290 showSearch(false); 291 } 292 293 if (url.params.hasOwnProperty(URL_MARK_PARAM)) { 294 var words = url.params[URL_MARK_PARAM].split(' '); 295 marker.mark(words, { 296 exclude: mark_exclude 297 }); 298 299 var markers = document.querySelectorAll("mark"); 300 function hide() { 301 for (var i = 0; i < markers.length; i++) { 302 markers[i].classList.add("fade-out"); 303 window.setTimeout(function(e) { marker.unmark(); }, 300); 304 } 305 } 306 for (var i = 0; i < markers.length; i++) { 307 markers[i].addEventListener('click', hide); 308 } 309 } 310 } 311 312 // Eventhandler for keyevents on `document` 313 function globalKeyHandler(e) { 314 if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey || e.target.type === 'textarea') { return; } 315 316 if (e.keyCode === ESCAPE_KEYCODE) { 317 e.preventDefault(); 318 searchbar.classList.remove("active"); 319 setSearchUrlParameters("", 320 (searchbar.value.trim() !== "") ? "push" : "replace"); 321 if (hasFocus()) { 322 unfocusSearchbar(); 323 } 324 showSearch(false); 325 marker.unmark(); 326 } else if (!hasFocus() && e.keyCode === SEARCH_HOTKEY_KEYCODE) { 327 e.preventDefault(); 328 showSearch(true); 329 window.scrollTo(0, 0); 330 searchbar.select(); 331 } else if (hasFocus() && e.keyCode === DOWN_KEYCODE) { 332 e.preventDefault(); 333 unfocusSearchbar(); 334 searchresults.firstElementChild.classList.add("focus"); 335 } else if (!hasFocus() && (e.keyCode === DOWN_KEYCODE 336 || e.keyCode === UP_KEYCODE 337 || e.keyCode === SELECT_KEYCODE)) { 338 // not `:focus` because browser does annoying scrolling 339 var focused = searchresults.querySelector("li.focus"); 340 if (!focused) return; 341 e.preventDefault(); 342 if (e.keyCode === DOWN_KEYCODE) { 343 var next = focused.nextElementSibling; 344 if (next) { 345 focused.classList.remove("focus"); 346 next.classList.add("focus"); 347 } 348 } else if (e.keyCode === UP_KEYCODE) { 349 focused.classList.remove("focus"); 350 var prev = focused.previousElementSibling; 351 if (prev) { 352 prev.classList.add("focus"); 353 } else { 354 searchbar.select(); 355 } 356 } else { // SELECT_KEYCODE 357 window.location.assign(focused.querySelector('a')); 358 } 359 } 360 } 361 362 function showSearch(yes) { 363 if (yes) { 364 search_wrap.classList.remove('hidden'); 365 searchicon.setAttribute('aria-expanded', 'true'); 366 } else { 367 search_wrap.classList.add('hidden'); 368 searchicon.setAttribute('aria-expanded', 'false'); 369 var results = searchresults.children; 370 for (var i = 0; i < results.length; i++) { 371 results[i].classList.remove("focus"); 372 } 373 } 374 } 375 376 function showResults(yes) { 377 if (yes) { 378 searchresults_outer.classList.remove('hidden'); 379 } else { 380 searchresults_outer.classList.add('hidden'); 381 } 382 } 383 384 // Eventhandler for search icon 385 function searchIconClickHandler() { 386 if (search_wrap.classList.contains('hidden')) { 387 showSearch(true); 388 window.scrollTo(0, 0); 389 searchbar.select(); 390 } else { 391 showSearch(false); 392 } 393 } 394 395 // Eventhandler for keyevents while the searchbar is focused 396 function searchbarKeyUpHandler() { 397 var searchterm = searchbar.value.trim(); 398 if (searchterm != "") { 399 searchbar.classList.add("active"); 400 doSearch(searchterm); 401 } else { 402 searchbar.classList.remove("active"); 403 showResults(false); 404 removeChildren(searchresults); 405 } 406 407 setSearchUrlParameters(searchterm, "push_if_new_search_else_replace"); 408 409 // Remove marks 410 marker.unmark(); 411 } 412 413 // Update current url with ?URL_SEARCH_PARAM= parameter, remove ?URL_MARK_PARAM and #heading-anchor . 414 // `action` can be one of "push", "replace", "push_if_new_search_else_replace" 415 // and replaces or pushes a new browser history item. 416 // "push_if_new_search_else_replace" pushes if there is no `?URL_SEARCH_PARAM=abc` yet. 417 function setSearchUrlParameters(searchterm, action) { 418 var url = parseURL(window.location.href); 419 var first_search = ! url.params.hasOwnProperty(URL_SEARCH_PARAM); 420 if (searchterm != "" || action == "push_if_new_search_else_replace") { 421 url.params[URL_SEARCH_PARAM] = searchterm; 422 delete url.params[URL_MARK_PARAM]; 423 url.hash = ""; 424 } else { 425 delete url.params[URL_SEARCH_PARAM]; 426 } 427 // A new search will also add a new history item, so the user can go back 428 // to the page prior to searching. A updated search term will only replace 429 // the url. 430 if (action == "push" || (action == "push_if_new_search_else_replace" && first_search) ) { 431 history.pushState({}, document.title, renderURL(url)); 432 } else if (action == "replace" || (action == "push_if_new_search_else_replace" && !first_search) ) { 433 history.replaceState({}, document.title, renderURL(url)); 434 } 435 } 436 437 function doSearch(searchterm) { 438 439 // Don't search the same twice 440 if (current_searchterm == searchterm) { return; } 441 else { current_searchterm = searchterm; } 442 443 if (searchindex == null) { return; } 444 445 // Do the actual search 446 var results = searchindex.search(searchterm, search_options); 447 var resultcount = Math.min(results.length, results_options.limit_results); 448 449 // Display search metrics 450 searchresults_header.innerText = formatSearchMetric(resultcount, searchterm); 451 452 // Clear and insert results 453 var searchterms = searchterm.split(' '); 454 removeChildren(searchresults); 455 for(var i = 0; i < resultcount ; i++){ 456 var resultElem = document.createElement('li'); 457 resultElem.innerHTML = formatSearchResult(results[i], searchterms); 458 searchresults.appendChild(resultElem); 459 } 460 461 // Display results 462 showResults(true); 463 } 464 465 fetch(path_to_root + 'searchindex.json') 466 .then(response => response.json()) 467 .then(json => init(json)) 468 .catch(error => { // Try to load searchindex.js if fetch failed 469 var script = document.createElement('script'); 470 script.src = path_to_root + 'searchindex.js'; 471 script.onload = () => init(window.search); 472 document.head.appendChild(script); 473 }); 474 475 // Exported functions 476 search.hasFocus = hasFocus; 477})(window.search);
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.