/** * Refer to hexo-generator-searchdb * https://github.com/next-theme/hexo-generator-searchdb/blob/main/dist/search.js * Modified by hexo-theme-butterfly */ class LocalSearch { constructor ({ path = '', unescape = false, top_n_per_article = 1 }) { this.path = path this.unescape = unescape this.top_n_per_article = top_n_per_article this.isfetched = false this.datas = null } getIndexByWord (words, text, caseSensitive = false) { const index = [] const included = new Set() if (!caseSensitive) { text = text.toLowerCase() } words.forEach(word => { if (this.unescape) { const div = document.createElement('div') div.innerText = word word = div.innerHTML } const wordLen = word.length if (wordLen === 0) return let startPosition = 0 let position = -1 if (!caseSensitive) { word = word.toLowerCase() } while ((position = text.indexOf(word, startPosition)) > -1) { index.push({ position, word }) included.add(word) startPosition = position + wordLen } }) // Sort index by position of keyword index.sort((left, right) => { if (left.position !== right.position) { return left.position - right.position } return right.word.length - left.word.length }) return [index, included] } // Merge hits into slices mergeIntoSlice (start, end, index) { let item = index[0] let { position, word } = item const hits = [] const count = new Set() while (position + word.length <= end && index.length !== 0) { count.add(word) hits.push({ position, length: word.length }) const wordEnd = position + word.length // Move to next position of hit index.shift() while (index.length !== 0) { item = index[0] position = item.position word = item.word if (wordEnd > position) { index.shift() } else { break } } } return { hits, start, end, count: count.size } } // Highlight title and content highlightKeyword (val, slice) { let result = '' let index = slice.start for (const { position, length } of slice.hits) { result += val.substring(index, position) index = position + length result += `${val.substr(position, length)}` } result += val.substring(index, slice.end) return result } getResultItems (keywords) { const resultItems = [] this.datas.forEach(({ title, content, url }) => { // The number of different keywords included in the article. const [indexOfTitle, keysOfTitle] = this.getIndexByWord(keywords, title) const [indexOfContent, keysOfContent] = this.getIndexByWord(keywords, content) const includedCount = new Set([...keysOfTitle, ...keysOfContent]).size // Show search results const hitCount = indexOfTitle.length + indexOfContent.length if (hitCount === 0) return const slicesOfTitle = [] if (indexOfTitle.length !== 0) { slicesOfTitle.push(this.mergeIntoSlice(0, title.length, indexOfTitle)) } let slicesOfContent = [] while (indexOfContent.length !== 0) { const item = indexOfContent[0] const { position } = item // Cut out 120 characters. The maxlength of .search-input is 80. const start = Math.max(0, position - 20) const end = Math.min(content.length, position + 100) slicesOfContent.push(this.mergeIntoSlice(start, end, indexOfContent)) } // Sort slices in content by included keywords' count and hits' count slicesOfContent.sort((left, right) => { if (left.count !== right.count) { return right.count - left.count } else if (left.hits.length !== right.hits.length) { return right.hits.length - left.hits.length } return left.start - right.start }) // Select top N slices in content const upperBound = parseInt(this.top_n_per_article, 10) if (upperBound >= 0) { slicesOfContent = slicesOfContent.slice(0, upperBound) } let resultItem = '' url = new URL(url, location.origin) url.searchParams.append('highlight', keywords.join(' ')) if (slicesOfTitle.length !== 0) { resultItem += `
${this.highlightKeyword(content, slice)}...
` }) resultItem += '