PageSourceSearch

https://pkl-lang.org/_/js/search-ui.js

js pkl-lang.org collected 2026-09-24 18:03:37 UTC 8,795 bytes, 244 lines download raw bytes

1// ===----------------------------------------------------------------------===//
2// Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8//	https://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15// ===----------------------------------------------------------------------===//
16;(function (globalScope) {
17  /* eslint-disable no-var */
18  const config = document.getElementById('search-ui-script').dataset;
19  const snippetLength = parseInt(config.snippetLength || 100, 10);
20  const siteRootPath = config.siteRootPath || '';
21  appendStylesheet(config.stylesheet)
22  const searchInput = document.getElementById('search-input');
23  const searchResult = document.createElement('div');
24  searchResult.classList.add('search-result-dropdown-menu')
25  searchInput.parentNode.appendChild(searchResult)
26
27  function appendStylesheet (href) {
28    if (!href) return
29    document.head.appendChild(Object.assign(document.createElement('link'), { rel: 'stylesheet', href: href }))
30  }
31
32  function highlightText (doc, position) {
33    const hits = [];
34    const start = position[0];
35    const length = position[1];
36
37    const text = doc.text;
38    const highlightSpan = document.createElement('span');
39    highlightSpan.classList.add('search-result-highlight')
40    highlightSpan.innerText = text.substr(start, length)
41
42    const end = start + length;
43    const textEnd = text.length - 1;
44    const contextAfter = end + snippetLength > textEnd ? textEnd : end + snippetLength;
45    const contextBefore = start - snippetLength < 0 ? 0 : start - snippetLength;
46    if (start === 0 && end === textEnd) {
47      hits.push(highlightSpan)
48    } else if (start === 0) {
49      hits.push(highlightSpan)
50      hits.push(document.createTextNode(text.substr(end, contextAfter)))
51    } else if (end === textEnd) {
52      hits.push(document.createTextNode(text.substr(0, start)))
53      hits.push(highlightSpan)
54    } else {
55      hits.push(document.createTextNode('...' + text.substr(contextBefore, start - contextBefore)))
56      hits.push(highlightSpan)
57      hits.push(document.createTextNode(text.substr(end, contextAfter - end) + '...'))
58    }
59    return hits
60  }
61
62  function highlightTitle (hash, doc, position) {
63    const hits = [];
64    const start = position[0];
65    const length = position[1];
66
67    const highlightSpan = document.createElement('span');
68    highlightSpan.classList.add('search-result-highlight')
69    let title;
70    if (hash) {
71      title = doc.titles.filter(function (item) {
72        return item.id === hash
73      })[0].text
74    } else {
75      title = doc.title
76    }
77    highlightSpan.innerText = title.substr(start, length)
78
79    const end = start + length;
80    const titleEnd = title.length - 1;
81    if (start === 0 && end === titleEnd) {
82      hits.push(highlightSpan)
83    } else if (start === 0) {
84      hits.push(highlightSpan)
85      hits.push(document.createTextNode(title.substr(length, titleEnd)))
86    } else if (end === titleEnd) {
87      hits.push(document.createTextNode(title.substr(0, start)))
88      hits.push(highlightSpan)
89    } else {
90      hits.push(document.createTextNode(title.substr(0, start)))
91      hits.push(highlightSpan)
92      hits.push(document.createTextNode(title.substr(end, titleEnd)))
93    }
94    return hits
95  }
96
97  function highlightHit (metadata, hash, doc) {
98    let hits = [];
99    for (const token in metadata) {
100      const fields = metadata[token];
101      for (const field in fields) {
102        const positions = fields[field];
103        if (positions.position) {
104          const position = positions.position[0]; // only higlight the first match
105          if (field === 'title') {
106            hits = highlightTitle(hash, doc, position)
107          } else if (field === 'text') {
108            hits = highlightText(doc, position)
109          }
110        }
111      }
112    }
113    return hits
114  }
115
116  function createSearchResult (result, store, searchResultDataset) {
117    result.forEach(function (item) {
118      let url = item.ref;
119      let hash;
120      if (url.includes('#')) {
121        hash = url.substring(url.indexOf('#') + 1)
122        url = url.replace('#' + hash, '')
123      }
124      const doc = store[url];
125      const metadata = item.matchData.metadata;
126      const hits = highlightHit(metadata, hash, doc);
127      searchResultDataset.appendChild(createSearchResultItem(doc, item, hits))
128    })
129  }
130
131  function createSearchResultItem (doc, item, hits) {
132    const documentTitle = document.createElement('div');
133    documentTitle.classList.add('search-result-document-title')
134    documentTitle.innerText = doc.title
135    const documentHit = document.createElement('div');
136    documentHit.classList.add('search-result-document-hit')
137    const documentHitLink = document.createElement('a');
138    documentHitLink.href = siteRootPath + item.ref
139    documentHit.appendChild(documentHitLink)
140    hits.forEach(function (hit) {
141      documentHitLink.appendChild(hit)
142    })
143    const searchResultItem = document.createElement('div');
144    searchResultItem.classList.add('search-result-item')
145    searchResultItem.appendChild(documentTitle)
146    searchResultItem.appendChild(documentHit)
147    searchResultItem.addEventListener('mousedown', function (e) {
148      e.preventDefault()
149    })
150    return searchResultItem
151  }
152
153  function createNoResult (text) {
154    const searchResultItem = document.createElement('div');
155    searchResultItem.classList.add('search-result-item')
156    const documentHit = document.createElement('div');
157    documentHit.classList.add('search-result-document-hit')
158    const message = document.createElement('strong');
159    message.innerText = 'No results found for query "' + text + '"'
160    documentHit.appendChild(message)
161    searchResultItem.appendChild(documentHit)
162    return searchResultItem
163  }
164
165  function clearSearchResults (reset) {
166    if (reset === true) searchInput.value = ''
167    searchResult.innerHTML = ''
168  }
169
170  function search (index, text) {
171    // execute an exact match search
172    let result = index.search(text);
173    if (result.length > 0) {
174      return result
175    }
176    // no result, use a begins with search
177    result = index.search(text + '*')
178    if (result.length > 0) {
179      return result
180    }
181    // no result, use a contains search
182    result = index.search('*' + text + '*')
183    return result
184  }
185
186  function searchIndex (index, store, text) {
187    clearSearchResults(false)
188    if (text.trim() === '') {
189      return
190    }
191    const result = search(index, text);
192    const searchResultDataset = document.createElement('div');
193    searchResultDataset.classList.add('search-result-dataset')
194    searchResult.appendChild(searchResultDataset)
195    if (result.length > 0) {
196      createSearchResult(result, store, searchResultDataset)
197    } else {
198      searchResultDataset.appendChild(createNoResult(text))
199    }
200  }
201
202  function confineEvent (e) {
203    e.stopPropagation()
204  }
205
206  function debounce (func, wait, immediate) {
207    let timeout;
208    return function () {
209      const context = this;
210      const args = arguments;
211      const later = function () {
212        timeout = null
213        if (!immediate) func.apply(context, args)
214      };
215      const callNow = immediate && !timeout;
216      clearTimeout(timeout)
217      timeout = setTimeout(later, wait)
218      if (callNow) func.apply(context, args)
219    }
220  }
221
222  function initSearch (lunr, data) {
223    const index = Object.assign({index: lunr.Index.load(data.index), store: data.store});
224    const debug = 'URLSearchParams' in globalScope && new URLSearchParams(globalScope.location.search).has('lunr-debug');
225    searchInput.addEventListener(
226      'keydown',
227      debounce(function (e) {
228        const query = searchInput.value;
229        if (e.key === 'Escape' || e.key === 'Esc') return clearSearchResults(true)
230        try {
231          if (!query) return clearSearchResults()
232          searchIndex(index.index, index.store, searchInput.value)
233        } catch (err) {
234          if (debug) console.debug('Invalid search query: ' + query + ' (' + err.message + ')')
235        }
236      }, 100)
237    )
238    searchInput.addEventListener('click', confineEvent)
239    searchResult.addEventListener('click', confineEvent)
240    document.documentElement.addEventListener('click', clearSearchResults)
241  }
242
243  globalScope.initSearch = initSearch
244})(typeof globalThis !== 'undefined' ? globalThis : window)

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.