PageSourceSearch

https://www.andromeda-prover.org/searcher-09f2665d.js

js andromeda-prover.org collected 2026-10-07 16:43:31 UTC 20,294 bytes, 558 lines download raw bytes

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