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