|
4 | 4 | (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.antoraSearch = {})); |
5 | 5 | })(this, (function (exports) { 'use strict'; |
6 | 6 |
|
| 7 | + /** |
| 8 | + * Splitting the text by the given positions. |
| 9 | + * The text within the positions getting the type "mark", all other text gets the type "text". |
| 10 | + * @param {string} text |
| 11 | + * @param {Object[]} positions |
| 12 | + * @param {number} positions.start |
| 13 | + * @param {number} positions.length |
| 14 | + * @param {number} snippetLength Maximum text length for text in the result. |
| 15 | + * @returns {[{text: string, type: string}]} |
| 16 | + */ |
7 | 17 | function buildHighlightedText (text, positions, snippetLength) { |
8 | 18 | const textLength = text.length; |
9 | | - const validPositions = positions |
10 | | - .filter((position) => position.length > 0 && position.start + position.length <= textLength); |
| 19 | + const validPositions = positions.filter( |
| 20 | + (position) => position.length > 0 && position.start + position.length <= textLength |
| 21 | + ); |
11 | 22 |
|
12 | 23 | if (validPositions.length === 0) { |
13 | 24 | return [ |
14 | 25 | { |
15 | 26 | type: 'text', |
16 | | - text: text.slice(0, snippetLength >= textLength ? textLength : snippetLength) + (snippetLength < textLength ? '...' : ''), |
| 27 | + text: |
| 28 | + text.slice(0, snippetLength >= textLength ? textLength : snippetLength) + |
| 29 | + (snippetLength < textLength ? '...' : ''), |
17 | 30 | }, |
18 | 31 | ] |
19 | 32 | } |
|
40 | 53 | }); |
41 | 54 | } |
42 | 55 | let lastEndPosition = 0; |
43 | | - const positionsWithinRange = orderedPositions |
44 | | - .filter((position) => position.start >= range.start && position.start + position.length <= range.end); |
| 56 | + const positionsWithinRange = orderedPositions.filter( |
| 57 | + (position) => position.start >= range.start && position.start + position.length <= range.end |
| 58 | + ); |
45 | 59 |
|
46 | 60 | for (const position of positionsWithinRange) { |
47 | 61 | const start = position.start; |
|
79 | 93 | */ |
80 | 94 | function findTermPosition (lunr, term, text) { |
81 | 95 | 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 | | - } |
| 96 | + const index = str.indexOf(term); |
| 97 | + |
| 98 | + if (index >= 0) { |
| 99 | + // extend term until word boundary to return the entire word |
| 100 | + const boundaries = str.substr(index).match(/^[\p{Alpha}]+/u); |
| 101 | + if (boundaries !== null && boundaries.length >= 0) { |
| 102 | + return { |
| 103 | + start: index, |
| 104 | + length: boundaries[0].length, |
99 | 105 | } |
100 | | - sliceStart = sliceEnd + 1; |
101 | 106 | } |
102 | 107 | } |
103 | 108 |
|
104 | | - // not found! |
| 109 | + // Not found |
105 | 110 | return { |
106 | 111 | start: 0, |
107 | 112 | length: 0, |
|
142 | 147 | return [] |
143 | 148 | } |
144 | 149 |
|
| 150 | + function highlightKeyword (doc, terms) { |
| 151 | + const keyword = doc.keyword; |
| 152 | + if (keyword) { |
| 153 | + const positions = getTermPosition(keyword, terms); |
| 154 | + return buildHighlightedText(keyword, positions, snippetLength) |
| 155 | + } |
| 156 | + return [] |
| 157 | + } |
| 158 | + |
145 | 159 | function highlightText (doc, terms) { |
146 | 160 | const text = doc.text; |
147 | 161 | const positions = getTermPosition(text, terms); |
|
172 | 186 | pageTitleNodes: highlightPageTitle(doc.title, terms.title || []), |
173 | 187 | sectionTitleNodes: highlightSectionTitle(sectionTitle, terms.title || []), |
174 | 188 | pageContentNodes: highlightText(doc, terms.text || []), |
| 189 | + pageKeywordNodes: highlightKeyword(doc, terms.keyword || []), |
175 | 190 | } |
176 | 191 | } |
177 | 192 |
|
|
227 | 242 | const documentSectionTitle = document.createElement('div'); |
228 | 243 | documentSectionTitle.classList.add('search-result-section-title'); |
229 | 244 | 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 | | - }); |
| 245 | + highlightingResult.sectionTitleNodes.forEach((node) => createHighlightedText(node, documentSectionTitle)); |
| 246 | + } |
| 247 | + highlightingResult.pageContentNodes.forEach((node) => createHighlightedText(node, documentHitLink)); |
| 248 | + |
| 249 | + // only show keyword when we got a hit on them |
| 250 | + if (doc.keyword && highlightingResult.pageKeywordNodes.length > 1) { |
| 251 | + const documentKeywords = document.createElement('div'); |
| 252 | + documentKeywords.classList.add('search-result-keywords'); |
| 253 | + const documentKeywordsFieldLabel = document.createElement('span'); |
| 254 | + documentKeywordsFieldLabel.classList.add('search-result-keywords-field-label'); |
| 255 | + documentKeywordsFieldLabel.innerText = 'keywords: '; |
| 256 | + const documentKeywordsList = document.createElement('span'); |
| 257 | + documentKeywordsList.classList.add('search-result-keywords-list'); |
| 258 | + highlightingResult.pageKeywordNodes.forEach((node) => createHighlightedText(node, documentKeywordsList)); |
| 259 | + documentKeywords.appendChild(documentKeywordsFieldLabel); |
| 260 | + documentKeywords.appendChild(documentKeywordsList); |
| 261 | + documentHitLink.appendChild(documentKeywords); |
241 | 262 | } |
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 | 263 | const searchResultItem = document.createElement('div'); |
254 | 264 | searchResultItem.classList.add('search-result-item'); |
255 | 265 | searchResultItem.appendChild(documentTitle); |
|
260 | 270 | return searchResultItem |
261 | 271 | } |
262 | 272 |
|
| 273 | + /** |
| 274 | + * Creates an element from a highlightingResultNode and add it to the targetNode. |
| 275 | + * @param {Object} highlightingResultNode |
| 276 | + * @param {String} highlightingResultNode.type - type of the node |
| 277 | + * @param {String} highlightingResultNode.text |
| 278 | + * @param {Node} targetNode |
| 279 | + */ |
| 280 | + function createHighlightedText (highlightingResultNode, targetNode) { |
| 281 | + let element; |
| 282 | + if (highlightingResultNode.type === 'text') { |
| 283 | + element = document.createTextNode(highlightingResultNode.text); |
| 284 | + } else { |
| 285 | + element = document.createElement('span'); |
| 286 | + element.classList.add('search-result-highlight'); |
| 287 | + element.innerText = highlightingResultNode.text; |
| 288 | + } |
| 289 | + targetNode.appendChild(element); |
| 290 | + } |
| 291 | + |
263 | 292 | function createNoResult (text) { |
264 | 293 | const searchResultItem = document.createElement('div'); |
265 | 294 | searchResultItem.classList.add('search-result-item'); |
|
278 | 307 | } |
279 | 308 |
|
280 | 309 | function filter (result, documents) { |
281 | | - const facetFilter = facetFilterInput && facetFilterInput.checked && facetFilterInput.dataset.facetFilter; |
| 310 | + const facetFilter = facetFilterInput?.checked && facetFilterInput.dataset.facetFilter; |
282 | 311 | if (facetFilter) { |
283 | 312 | const [field, value] = facetFilter.split(':'); |
284 | 313 | return result.filter((item) => { |
|
0 commit comments