PageSourceSearch

https://ekssecurity.netlify.app/searcher.js

js ekssecurity.netlify.app collected 2026-10-03 10:41:24 UTC 18,097 bytes, 477 lines download raw bytes

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            '&': '&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
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.