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.