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.