]> git.proxmox.com Git - rustc.git/blame - vendor/mdbook/src/theme/searcher/searcher.js
New upstream version 1.49.0+dfsg1
[rustc.git] / vendor / mdbook / src / theme / searcher / searcher.js
CommitLineData
83c7162d
XL
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 }
9fa01778
XL
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
83c7162d
XL
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,
9fa01778
XL
30 doc_urls = [],
31 results_options = {
83c7162d
XL
32 teaser_word_count: 30,
33 limit_results: 30,
34 },
9fa01778 35 search_options = {
83c7162d
XL
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 '&': '&amp;',
117 '<': '&lt;',
118 '>': '&gt;',
119 '"': '&#34;',
120 "'": '&#39;'
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
9fa01778 143 var url = doc_urls[result.ref].split("#");
83c7162d
XL
144 if (url.length == 1) { // no anchor found
145 url.push("");
146 }
147
9fa01778 148 return '<a href="' + path_to_root + url[0] + '?' + URL_MARK_PARAM + '=' + searchterms + '#' + url[1]
83c7162d
XL
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 = [];
9fa01778 200 var window_size = Math.min(weighted.length, results_options.teaser_word_count);
83c7162d
XL
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
9fa01778
XL
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);
83c7162d
XL
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) {
72b1a166 314 if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey || e.target.type === 'textarea' || e.target.type === 'text') { return; }
83c7162d
XL
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
9fa01778
XL
446 var results = searchindex.search(searchterm, search_options);
447 var resultcount = Math.min(results.length, results_options.limit_results);
83c7162d
XL
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
9fa01778
XL
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
83c7162d
XL
475 // Exported functions
476 search.hasFocus = hasFocus;
477})(window.search);