PageSourceSearch

https://docs.strongswan.org/_/js/search-ui.js

js strongswan.org collected 2026-09-24 08:26:28 UTC 15,578 bytes, 448 lines download raw bytes

1(function (global, factory) {
2  typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
3  typeof define === 'function' && define.amd ? define(['exports'], factory) :
4  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.antoraSearch = {}));
5})(this, (function (exports) { 'use strict';
6
7  function buildHighlightedText (text, positions, snippetLength) {
8    const textLength = text.length;
9    const validPositions = positions
10      .filter((position) => position.length > 0 && position.start + position.length <= textLength);
11
12    if (validPositions.length === 0) {
13      return [
14        {
15          type: 'text',
16          text: text.slice(0, snippetLength >= textLength ? textLength : snippetLength) + (snippetLength < textLength ? '...' : ''),
17        },
18      ]
19    }
20
21    const orderedPositions = validPositions.sort((p1, p2) => p1.start - p2.start);
22    const range = {
23      start: 0,
24      end: textLength,
25    };
26    const firstPosition = orderedPositions[0];
27    if (snippetLength && text.length > snippetLength) {
28      const firstPositionStart = firstPosition.start;
29      const firstPositionLength = firstPosition.length;
30      const firstPositionEnd = firstPositionStart + firstPositionLength;
31
32      range.start = firstPositionStart - snippetLength < 0 ? 0 : firstPositionStart - snippetLength;
33      range.end = firstPositionEnd + snippetLength > textLength ? textLength : firstPositionEnd + snippetLength;
34    }
35    const nodes = [];
36    if (firstPosition.start > 0) {
37      nodes.push({
38        type: 'text',
39        text: (range.start > 0 ? '...' : '') + text.slice(range.start, firstPosition.start),
40      });
41    }
42    let lastEndPosition = 0;
43    const positionsWithinRange = orderedPositions
44      .filter((position) => position.start >= range.start && position.start + position.length <= range.end);
45
46    for (const position of positionsWithinRange) {
47      const start = position.start;
48      const length = position.length;
49      const end = start + length;
50      if (lastEndPosition > 0) {
51        // create text Node from the last end position to the start of the current position
52        nodes.push({
53          type: 'text',
54          text: text.slice(lastEndPosition, start),
55        });
56      }
57      nodes.push({
58        type: 'mark',
59        text: text.slice(start, end),
60      });
61      lastEndPosition = end;
62    }
63    if (lastEndPosition < range.end) {
64      nodes.push({
65        type: 'text',
66        text: text.slice(lastEndPosition, range.end) + (range.end < textLength ? '...' : ''),
67      });
68    }
69
70    return nodes
71  }
72
73  /**
74   * Taken and adapted from: https://github.com/olivernn/lunr.js/blob/aa5a878f62a6bba1e8e5b95714899e17e8150b38/lib/tokenizer.js#L24-L67
75   * @param lunr
76   * @param text
77   * @param term
78   * @return {{start: number, length: number}}
79   */
80  function findTermPosition (lunr, term, text) {
81    const str = text.toLowerCase();
82    const len = str.length;
83
84    for (let sliceEnd = 0, sliceStart = 0; sliceEnd <= len; sliceEnd++) {
85      const char = str.charAt(sliceEnd);
86      const sliceLength = sliceEnd - sliceStart;
87
88      if ((char.match(lunr.tokenizer.separator) || sliceEnd === len)) {
89        if (sliceLength > 0) {
90          const value = str.slice(sliceStart, sliceEnd);
91          // QUESTION: if we get an exact match without running the pipeline should we stop?
92          if (value.includes(term)) {
93            // returns the first match
94            return {
95              start: sliceStart,
96              length: value.length,
97            }
98          }
99        }
100        sliceStart = sliceEnd + 1;
101      }
102    }
103
104    // not found!
105    return {
106      start: 0,
107      length: 0,
108    }
109  }
110
111  /* global CustomEvent, globalThis */
112
113  const config = document.getElementById('search-ui-script').dataset;
114  const snippetLength = parseInt(config.snippetLength || 100, 10);
115  const siteRootPath = config.siteRootPath || '';
116  appendStylesheet(config.stylesheet);
117  const searchInput = document.getElementById('search-input');
118  const searchResultContainer = document.createElement('div');
119  searchResultContainer.classList.add('search-result-dropdown-menu');
120  searchInput.parentNode.appendChild(searchResultContainer);
121  const facetFilterInput = document.querySelector('#search-field input[type=checkbox][data-facet-filter]');
122
123  function appendStylesheet (href) {
124    if (!href) return
125    const link = document.createElement('link');
126    link.rel = 'stylesheet';
127    link.href = href;
128    document.head.appendChild(link);
129  }
130
131  function highlightPageTitle (title, terms) {
132    const positions = getTermPosition(title, terms);
133    return buildHighlightedText(title, positions, snippetLength)
134  }
135
136  function highlightSectionTitle (sectionTitle, terms) {
137    if (sectionTitle) {
138      const text = sectionTitle.text;
139      const positions = getTermPosition(text, terms);
140      return buildHighlightedText(text, positions, snippetLength)
141    }
142    return []
143  }
144
145  function highlightText (doc, terms) {
146    const text = doc.text;
147    const positions = getTermPosition(text, terms);
148    return buildHighlightedText(text, positions, snippetLength)
149  }
150
151  function getTermPosition (text, terms) {
152    const positions = terms
153      .map((term) => findTermPosition(globalThis.lunr, term, text))
154      .filter((position) => position.length > 0)
155      .sort((p1, p2) => p1.start - p2.start);
156
157    if (positions.length === 0) {
158      return []
159    }
160    return positions
161  }
162
163  function highlightHit (searchMetadata, sectionTitle, doc) {
164    const terms = {};
165    for (const term in searchMetadata) {
166      const fields = searchMetadata[term];
167      for (const field in fields) {
168        terms[field] = [...(terms[field] || []), term];
169      }
170    }
171    return {
172      pageTitleNodes: highlightPageTitle(doc.title, terms.title || []),
173      sectionTitleNodes: highlightSectionTitle(sectionTitle, terms.title || []),
174      pageContentNodes: highlightText(doc, terms.text || []),
175    }
176  }
177
178  function createSearchResult (result, store, searchResultDataset) {
179    let currentComponent;
180    result.forEach(function (item) {
181      const ids = item.ref.split('-');
182      const docId = ids[0];
183      const doc = store.documents[docId];
184      let sectionTitle;
185      if (ids.length > 1) {
186        const titleId = ids[1];
187        sectionTitle = doc.titles.filter(function (item) {
188          return String(item.id) === titleId
189        })[0];
190      }
191      const metadata = item.matchData.metadata;
192      const highlightingResult = highlightHit(metadata, sectionTitle, doc);
193      const componentVersion = store.componentVersions[`${doc.component}/${doc.version}`];
194      if (componentVersion !== undefined && currentComponent !== componentVersion) {
195        const searchResultComponentHeader = document.createElement('div');
196        searchResultComponentHeader.classList.add('search-result-component-header');
197        const { title, displayVersion } = componentVersion;
198        const componentVersionText = `${title}${doc.version && displayVersion ? ` ${displayVersion}` : ''}`;
199        searchResultComponentHeader.appendChild(document.createTextNode(componentVersionText));
200        searchResultDataset.appendChild(searchResultComponentHeader);
201        currentComponent = componentVersion;
202      }
203      searchResultDataset.appendChild(createSearchResultItem(doc, sectionTitle, item, highlightingResult));
204    });
205  }
206
207  function createSearchResultItem (doc, sectionTitle, item, highlightingResult) {
208    const documentTitle = document.createElement('div');
209    documentTitle.classList.add('search-result-document-title');
210    highlightingResult.pageTitleNodes.forEach(function (node) {
211      let element;
212      if (node.type === 'text') {
213        element = document.createTextNode(node.text);
214      } else {
215        element = document.createElement('span');
216        element.classList.add('search-result-highlight');
217        element.innerText = node.text;
218      }
219      documentTitle.appendChild(element);
220    });
221    const documentHit = document.createElement('div');
222    documentHit.classList.add('search-result-document-hit');
223    const documentHitLink = document.createElement('a');
224    documentHitLink.href = siteRootPath + doc.url + (sectionTitle ? '#' + sectionTitle.hash : '');
225    documentHit.appendChild(documentHitLink);
226    if (highlightingResult.sectionTitleNodes.length > 0) {
227      const documentSectionTitle = document.createElement('div');
228      documentSectionTitle.classList.add('search-result-section-title');
229      documentHitLink.appendChild(documentSectionTitle);
230      highlightingResult.sectionTitleNodes.forEach(function (node) {
231        let element;
232        if (node.type === 'text') {
233          element = document.createTextNode(node.text);
234        } else {
235          element = document.createElement('span');
236          element.classList.add('search-result-highlight');
237          element.innerText = node.text;
238        }
239        documentSectionTitle.appendChild(element);
240      });
241    }
242    highlightingResult.pageContentNodes.forEach(function (node) {
243      let element;
244      if (node.type === 'text') {
245        element = document.createTextNode(node.text);
246      } else {
247        element = document.createElement('span');
248        element.classList.add('search-result-highlight');
249        element.innerText = node.text;
250      }
251      documentHitLink.appendChild(element);
252    });
253    const searchResultItem = document.createElement('div');
254    searchResultItem.classList.add('search-result-item');
255    searchResultItem.appendChild(documentTitle);
256    searchResultItem.appendChild(documentHit);
257    searchResultItem.addEventListener('mousedown', function (e) {
258      e.preventDefault();
259    });
260    return searchResultItem
261  }
262
263  function createNoResult (text) {
264    const searchResultItem = document.createElement('div');
265    searchResultItem.classList.add('search-result-item');
266    const documentHit = document.createElement('div');
267    documentHit.classList.add('search-result-document-hit');
268    const message = document.createElement('strong');
269    message.innerText = 'No results found for query "' + text + '"';
270    documentHit.appendChild(message);
271    searchResultItem.appendChild(documentHit);
272    return searchResultItem
273  }
274
275  function clearSearchResults (reset) {
276    if (reset === true) searchInput.value = '';
277    searchResultContainer.innerHTML = '';
278  }
279
280  function filter (result, documents) {
281    const facetFilter = facetFilterInput && facetFilterInput.checked && facetFilterInput.dataset.facetFilter;
282    if (facetFilter) {
283      const [field, value] = facetFilter.split(':');
284      return result.filter((item) => {
285        const ids = item.ref.split('-');
286        const docId = ids[0];
287        const doc = documents[docId];
288        return field in doc && doc[field] === value
289      })
290    }
291    return result
292  }
293
294  function search (index, documents, queryString) {
295    // execute an exact match search
296    let query;
297    let result = filter(
298      index.query(function (lunrQuery) {
299        const parser = new globalThis.lunr.QueryParser(queryString, lunrQuery);
300        parser.parse();
301        query = lunrQuery;
302      }),
303      documents
304    );
305    if (result.length > 0) {
306      return result
307    }
308    // no result, use a begins with search
309    result = filter(
310      index.query(function (lunrQuery) {
311        lunrQuery.clauses = query.clauses.map((clause) => {
312          if (clause.presence !== globalThis.lunr.Query.presence.PROHIBITED) {
313            clause.term = clause.term + '*';
314            clause.wildcard = globalThis.lunr.Query.wildcard.TRAILING;
315            clause.usePipeline = false;
316          }
317          return clause
318        });
319      }),
320      documents
321    );
322    if (result.length > 0) {
323      return result
324    }
325    // no result, use a contains search
326    result = filter(
327      index.query(function (lunrQuery) {
328        lunrQuery.clauses = query.clauses.map((clause) => {
329          if (clause.presence !== globalThis.lunr.Query.presence.PROHIBITED) {
330            clause.term = '*' + clause.term + '*';
331            clause.wildcard = globalThis.lunr.Query.wildcard.LEADING | globalThis.lunr.Query.wildcard.TRAILING;
332            clause.usePipeline = false;
333          }
334          return clause
335        });
336      }),
337      documents
338    );
339    return result
340  }
341
342  function searchIndex (index, store, text) {
343    clearSearchResults(false);
344    if (text.trim() === '') {
345      return
346    }
347    const result = search(index, store.documents, text);
348    const searchResultDataset = document.createElement('div');
349    searchResultDataset.classList.add('search-result-dataset');
350    searchResultContainer.appendChild(searchResultDataset);
351    if (result.length > 0) {
352      createSearchResult(result, store, searchResultDataset);
353    } else {
354      searchResultDataset.appendChild(createNoResult(text));
355    }
356  }
357
358  function confineEvent (e) {
359    e.stopPropagation();
360  }
361
362  function debounce (func, wait, immediate) {
363    let timeout;
364    return function () {
365      const context = this;
366      const args = arguments;
367      const later = function () {
368        timeout = null;
369        if (!immediate) func.apply(context, args);
370      };
371      const callNow = immediate && !timeout;
372      clearTimeout(timeout);
373      timeout = setTimeout(later, wait);
374      if (callNow) func.apply(context, args);
375    }
376  }
377
378  function enableSearchInput (enabled) {
379    if (facetFilterInput) {
380      facetFilterInput.disabled = !enabled;
381    }
382    searchInput.disabled = !enabled;
383    searchInput.title = enabled ? '' : 'Loading index...';
384  }
385
386  function isClosed () {
387    return searchResultContainer.childElementCount === 0
388  }
389
390  function executeSearch (index) {
391    const debug = 'URLSearchParams' in globalThis && new URLSearchParams(globalThis.location.search).has('lunr-debug');
392    const query = searchInput.value;
393    try {
394      if (!query) return clearSearchResults()
395      searchIndex(index.index, index.store, query);
396    } catch (err) {
397      if (err instanceof globalThis.lunr.QueryParseError) {
398        if (debug) {
399          console.debug('Invalid search query: ' + query + ' (' + err.message + ')');
400        }
401      } else {
402        console.error('Something went wrong while searching', err);
403      }
404    }
405  }
406
407  function toggleFilter (e, index) {
408    searchInput.focus();
409    if (!isClosed()) {
410      executeSearch(index);
411    }
412  }
413
414  function initSearch (lunr, data) {
415    const start = performance.now();
416    const index = { index: lunr.Index.load(data.index), store: data.store };
417    enableSearchInput(true);
418    searchInput.dispatchEvent(
419      new CustomEvent('loadedindex', {
420        detail: {
421          took: performance.now() - start,
422        },
423      })
424    );
425    searchInput.addEventListener(
426      'keydown',
427      debounce(function (e) {
428        if (e.key === 'Escape' || e.key === 'Esc') return clearSearchResults(true)
429        executeSearch(index);
430      }, 100)
431    );
432    searchInput.addEventListener('click', confineEvent);
433    searchResultContainer.addEventListener('click', confineEvent);
434    if (facetFilterInput) {
435      facetFilterInput.parentElement.addEventListener('click', confineEvent);
436      facetFilterInput.addEventListener('change', (e) => toggleFilter(e, index));
437    }
438    document.documentElement.addEventListener('click', clearSearchResults);
439  }
440
441  // disable the search input until the index is loaded
442  enableSearchInput(false);
443
444  exports.initSearch = initSearch;
445
446  Object.defineProperty(exports, '__esModule', { value: true });
447
448}));

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.