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 // encodeURIComponent escapes all chars that could allow an XSS except 149 // for '. Due to that we also manually replace ' with its url-encoded 150 // representation (%27). 151 var searchterms = encodeURIComponent(searchterms.join(" ")).replace(/\'/g, "%27"); 152 153 return '<a href="' + path_to_root + url[0] + '?' + URL_MARK_PARAM + '=' + searchterms + '#' + url[1] 154 + '" aria-details="teaser_' + teaser_count + '">' + result.doc.breadcrumbs + '</a>' 155 + '<span class="teaser" id="teaser_' + teaser_count + '" aria-label="Search Result Teaser">' 156 + teaser + '</span>'; 157 } 158 159 function makeTeaser(body, searchterms) { 160 // The strategy is as follows: 161 // First, assign a value to each word in the document: 162 // Words that correspond to search terms (stemmer aware): 40 163 // Normal words: 2 164 // First word in a sentence: 8 165 // Then use a sliding window with a constant number of words and count the 166 // sum of the values of the words within the window. Then use the window that got the 167 // maximum sum. If there are multiple maximas, then get the last one. 168 // Enclose the terms in <em>. 169 var stemmed_searchterms = searchterms.map(function(w) { 170 return elasticlunr.stemmer(w.toLowerCase()); 171 }); 172 var searchterm_weight = 40; 173 var weighted = []; // contains elements of ["word", weight, index_in_document] 174 // split in sentences, then words 175 var sentences = body.toLowerCase().split('. '); 176 var index = 0; 177 var value = 0; 178 var searchterm_found = false;
179 for (var sentenceindex in sentences) { 180 var words = sentences[sentenceindex].split(' '); 181 value = 8; 182 for (var wordindex in words) { 183 var word = words[wordindex]; 184 if (word.length > 0) { 185 for (var searchtermindex in stemmed_searchterms) { 186 if (elasticlunr.stemmer(word).startsWith(stemmed_searchterms[searchtermindex])) { 187 value = searchterm_weight; 188 searchterm_found = true; 189 } 190 }; 191 weighted.push([word, value, index]); 192 value = 2; 193 } 194 index += word.length; 195 index += 1; // ' ' or '.' if last word in sentence 196 }; 197 index += 1; // because we split at a two-char boundary '. ' 198 }; 199 200 if (weighted.length == 0) { 201 return body; 202 } 203 204 var window_weight = []; 205 var window_size = Math.min(weighted.length, results_options.teaser_word_count); 206 207 var cur_sum = 0; 208 for (var wordindex = 0; wordindex < window_size; wordindex++) { 209 cur_sum += weighted[wordindex][1]; 210 }; 211 window_weight.push(cur_sum); 212 for (var wordindex = 0; wordindex < weighted.length - window_size; wordindex++) { 213 cur_sum -= weighted[wordindex][1]; 214 cur_sum += weighted[wordindex + window_size][1]; 215 window_weight.push(cur_sum); 216 }; 217 218 if (searchterm_found) { 219 var max_sum = 0; 220 var max_sum_window_index = 0; 221 // backwards 222 for (var i = window_weight.length - 1; i >= 0; i--) { 223 if (window_weight[i] > max_sum) { 224 max_sum = window_weight[i]; 225 max_sum_window_index = i; 226 } 227 }; 228 } else { 229 max_sum_window_index = 0; 230 } 231 232 // add <em/> around searchterms 233 var teaser_split = []; 234 var index = weighted[max_sum_window_index][2]; 235 for (var i = max_sum_window_index; i < max_sum_window_index+window_size; i++) { 236 var word = weighted[i]; 237 if (index < word[2]) { 238 // missing text from index to start of `word` 239 teaser_split.push(body.substring(index, word[2])); 240 index = word[2]; 241 } 242 if (word[1] == searchterm_weight) { 243 teaser_split.push("<em>") 244 } 245 index = word[2] + word[0].length; 246 teaser_split.push(body.substring(word[2], index)); 247 if (word[1] == searchterm_weight) { 248 teaser_split.push("</em>") 249 } 250 }; 251 252 return teaser_split.join(''); 253 } 254 255 function init(config) { 256 results_options = config.results_options; 257 search_options = config.search_options; 258 searchbar_outer = config.searchbar_outer; 259 doc_urls = config.doc_urls; 260 searchindex = elasticlunr.Index.load(config.index); 261 262 // Set up events 263 searchicon.addEventListener('click', function(e) { searchIconClickHandler(); }, false); 264 searchbar.addEventListener('keyup', function(e) { searchbarKeyUpHandler(); }, false); 265 document.addEventListener('keydown', function(e) { globalKeyHandler(e); }, false); 266 // If the user uses the browser buttons, do the same as if a reload happened 267 window.onpopstate = function(e) { doSearchOrMarkFromUrl(); }; 268 // Suppress "submit" events so the page doesn't reload when the user presses Enter 269 document.addEventListener('submit', function(e) { e.preventDefault(); }, false); 270 271 // If reloaded, do the search or mark again, depending on the current url parameters 272 doSearchOrMarkFromUrl(); 273 } 274 275 function unfocusSearchbar() { 276 // hacky, but just focusing a div only works once 277 var tmp = document.createElement('input'); 278 tmp.setAttribute('style', 'position: absolute; opacity: 0;'); 279 searchicon.appendChild(tmp); 280 tmp.focus(); 281 tmp.remove(); 282 } 283 284 // On reload or browser history backwards/forwards events, parse the url and do search or mark 285 function doSearchOrMarkFromUrl() { 286 // Check current URL for search request 287 var url = parseURL(window.location.href); 288 if (url.params.hasOwnProperty(URL_SEARCH_PARAM) 289 && url.params[URL_SEARCH_PARAM] != "") { 290 showSearch(true); 291 searchbar.value = decodeURIComponent( 292 (url.params[URL_SEARCH_PARAM]+'').replace(/\+/g, '%20')); 293 searchbarKeyUpHandler(); // -> doSearch() 294 } else { 295 showSearch(false); 296 } 297 298 if (url.params.hasOwnProperty(URL_MARK_PARAM)) { 299 var words = decodeURIComponent(url.params[URL_MARK_PARAM]).split(' '); 300 marker.mark(words, { 301 exclude: mark_exclude 302 }); 303 304 var markers = document.querySelectorAll("mark"); 305 function hide() { 306 for (var i = 0; i < markers.length; i++) { 307 markers[i].classList.add("fade-out"); 308 window.setTimeout(function(e) { marker.unmark(); }, 300); 309 } 310 } 311 for (var i = 0; i < markers.length; i++) { 312 markers[i].addEventListener('click', hide); 313 } 314 } 315 } 316 317 // Eventhandler for keyevents on `document` 318 function globalKeyHandler(e) { 319 if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey || e.target.type === 'textarea' || e.target.type === 'text') { return; } 320 321 if (e.keyCode === ESCAPE_KEYCODE) { 322 e.preventDefault(); 323 searchbar.classList.remove("active"); 324 setSearchUrlParameters("", 325 (searchbar.value.trim() !== "") ? "push" : "replace"); 326 if (hasFocus()) { 327 unfocusSearchbar(); 328 } 329 showSearch(false); 330 marker.unmark(); 331 } else if (!hasFocus() && e.keyCode === SEARCH_HOTKEY_KEYCODE) { 332 e.preventDefault(); 333 showSearch(true); 334 window.scrollTo(0, 0); 335 searchbar.select(); 336 } else if (hasFocus() && e.keyCode === DOWN_KEYCODE) { 337 e.preventDefault(); 338 unfocusSearchbar(); 339 searchresults.firstElementChild.classList.add("focus"); 340 } else if (!hasFocus() && (e.keyCode === DOWN_KEYCODE 341 || e.keyCode === UP_KEYCODE 342 || e.keyCode === SELECT_KEYCODE)) { 343 // not `:focus` because browser does annoying scrolling 344 var focused = searchresults.querySelector("li.focus"); 345 if (!focused) return; 346 e.preventDefault(); 347 if (e.keyCode === DOWN_KEYCODE) { 348 var next = focused.nextElementSibling; 349 if (next) { 350 focused.classList.remove("focus"); 351 next.classList.add("focus"); 352 } 353 } else if (e.keyCode === UP_KEYCODE) { 354 focused.classList.remove("focus"); 355 var prev = focused.previousElementSibling; 356 if (prev) { 357 prev.classList.add("focus"); 358 } else { 359 searchbar.select(); 360 } 361 } else { // SELECT_KEYCODE 362 window.location.assign(focused.querySelector('a')); 363 } 364 } 365 } 366 367 function showSearch(yes) { 368 if (yes) { 369 search_wrap.classList.remove('hidden'); 370 searchicon.setAttribute('aria-expanded', 'true'); 371 } else { 372 search_wrap.classList.add('hidden'); 373 searchicon.setAttribute('aria-expanded', 'false'); 374 var results = searchresults.children; 375 for (var i = 0; i < results.length; i++) { 376 results[i].classList.remove("focus"); 377 } 378 } 379 } 380 381 function showResults(yes) { 382 if (yes) { 383 searchresults_outer.classList.remove('hidden'); 384 } else { 385 searchresults_outer.classList.add('hidden'); 386 } 387 } 388 389 // Eventhandler for search icon 390 function searchIconClickHandler() { 391 if (search_wrap.classList.contains('hidden')) { 392 showSearch(true); 393 window.scrollTo(0, 0); 394 searchbar.select(); 395 } else { 396 showSearch(false); 397 } 398 } 399 400 // Eventhandler for keyevents while the searchbar is focused 401 function searchbarKeyUpHandler() { 402 var searchterm = searchbar.value.trim(); 403 if (searchterm != "") { 404 searchbar.classList.add("active"); 405 doSearch(searchterm); 406 } else { 407 searchbar.classList.remove("active"); 408 showResults(false); 409 removeChildren(searchresults); 410 } 411 412 setSearchUrlParameters(searchterm, "push_if_new_search_else_replace"); 413 414 // Remove marks 415 marker.unmark(); 416 } 417 418 // Update current url with ?URL_SEARCH_PARAM= parameter, remove ?URL_MARK_PARAM and #heading-anchor . 419 // `action` can be one of "push", "replace", "push_if_new_search_else_replace" 420 // and replaces or pushes a new browser history item. 421 // "push_if_new_search_else_replace" pushes if there is no `?URL_SEARCH_PARAM=abc` yet. 422 function setSearchUrlParameters(searchterm, action) { 423 var url = parseURL(window.location.href); 424 var first_search = ! url.params.hasOwnProperty(URL_SEARCH_PARAM); 425 if (searchterm != "" || action == "push_if_new_search_else_replace") { 426 url.params[URL_SEARCH_PARAM] = searchterm; 427 delete url.params[URL_MARK_PARAM]; 428 url.hash = ""; 429 } else { 430 delete url.params[URL_MARK_PARAM]; 431 delete url.params[URL_SEARCH_PARAM]; 432 } 433 // A new search will also add a new history item, so the user can go back 434 // to the page prior to searching. A updated search term will only replace 435 // the url. 436 if (action == "push" || (action == "push_if_new_search_else_replace" && first_search) ) { 437 history.pushState({}, document.title, renderURL(url)); 438 } else if (action == "replace" || (action == "push_if_new_search_else_replace" && !first_search) ) { 439 history.replaceState({}, document.title, renderURL(url)); 440 } 441 } 442 443 function doSearch(searchterm) { 444 445 // Don't search the same twice 446 if (current_searchterm == searchterm) { return; } 447 else { current_searchterm = searchterm; } 448 449 if (searchindex == null) { return; } 450 451 // Do the actual search 452 var results = searchindex.search(searchterm, search_options); 453 var resultcount = Math.min(results.length, results_options.limit_results); 454 455 // Display search metrics 456 searchresults_header.innerText = formatSearchMetric(resultcount, searchterm); 457 458 // Clear and insert results 459 var searchterms = searchterm.split(' '); 460 removeChildren(searchresults); 461 for(var i = 0; i < resultcount ; i++){ 462 var resultElem = document.createElement('li'); 463 resultElem.innerHTML = formatSearchResult(results[i], searchterms); 464 searchresults.appendChild(resultElem); 465 } 466 467 // Display results 468 showResults(true); 469 } 470 471 fetch(path_to_root + 'searchindex.json') 472 .then(response => response.json()) 473 .then(json => init(json)) 474 .catch(error => { // Try to load searchindex.js if fetch failed 475 var script = document.createElement('script'); 476 script.src = path_to_root + 'searchindex.js'; 477 script.onload = () => init(window.search); 478 document.head.appendChild(script); 479 }); 480 481 // Exported functions 482 search.hasFocus = hasFocus; 483})(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.