From fbf0ea788f582b169366bd550b227078b6678250 Mon Sep 17 00:00:00 2001 From: Mimi <1119186082@qq.com> Date: Mon, 13 Sep 2021 15:07:58 +0800 Subject: [PATCH] Move local-search.js to hexo-generator-searchdb (#369) --- _vendors.yml | 7 + layout/_third-party/search/localsearch.njk | 3 +- source/js/third-party/search/local-search.js | 236 ++----------------- 3 files changed, 23 insertions(+), 223 deletions(-) diff --git a/_vendors.yml b/_vendors.yml index 43d23c7..d931222 100644 --- a/_vendors.yml +++ b/_vendors.yml @@ -138,6 +138,13 @@ instant_search: version: 4.28.0 file: dist/instantsearch.production.min.js integrity: sha256-x+1kTZNP+yy5U5ncci4pes6ALSaVss1UY5hnGy+mfqI= +local_search: + name: hexo-generator-searchdb + version: 1.4.0 + file: dist/search.js + unavailable: + - cdnjs + integrity: sha256-vXZMYLEqsROAXkEw93GGIvaB2ab+QW6w3+1ahD9nXXA= pdfobject: name: pdfobject version: 2.2.6 diff --git a/layout/_third-party/search/localsearch.njk b/layout/_third-party/search/localsearch.njk index e018cc4..8a8d527 100644 --- a/layout/_third-party/search/localsearch.njk +++ b/layout/_third-party/search/localsearch.njk @@ -1 +1,2 @@ -{{- next_js('third-party/search/local-search.js') }} +{{ next_vendors('local_search') }} +{{ next_js('third-party/search/local-search.js') }} diff --git a/source/js/third-party/search/local-search.js b/source/js/third-party/search/local-search.js index 62c9202..a3e5847 100644 --- a/source/js/third-party/search/local-search.js +++ b/source/js/third-party/search/local-search.js @@ -1,4 +1,4 @@ -/* global CONFIG, pjax */ +/* global CONFIG, pjax, LocalSearch */ document.addEventListener('DOMContentLoaded', () => { if (!CONFIG.path) { @@ -6,170 +6,23 @@ document.addEventListener('DOMContentLoaded', () => { console.warn('`hexo-generator-searchdb` plugin is not installed!'); return; } - // Popup Window - let isfetched = false; - let datas; + const localSearch = new LocalSearch({ + path : CONFIG.path, + top_n_per_article: CONFIG.localsearch.top_n_per_article, + unescape : CONFIG.localsearch.unescape + }); + const input = document.querySelector('.search-input'); - const getIndexByWord = (words, text, caseSensitive = false) => { - const index = []; - const included = new Set(); - words.forEach(word => { - if (CONFIG.localsearch.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) { - text = text.toLowerCase(); - 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 - const 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 - const 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; - }; - - const getResultItems = keywords => { - const resultItems = []; - datas.forEach(({ title, content, url }) => { - // The number of different keywords included in the article. - const [indexOfTitle, keysOfTitle] = getIndexByWord(keywords, title); - const [indexOfContent, keysOfContent] = 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(mergeIntoSlice(0, title.length, indexOfTitle)); - } - - let slicesOfContent = []; - while (indexOfContent.length !== 0) { - const item = indexOfContent[0]; - const { position } = item; - // Cut out 100 characters. The maxlength of .search-input is 80. - const start = Math.max(0, position - 20); - const end = Math.min(content.length, position + 80); - slicesOfContent.push(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(CONFIG.localsearch.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 += `
  • ${highlightKeyword(title, slicesOfTitle[0])}`; - } else { - resultItem += `
  • ${title}`; - } - - slicesOfContent.forEach(slice => { - resultItem += `

    ${highlightKeyword(content, slice)}...

    `; - }); - - resultItem += '
  • '; - resultItems.push({ - item: resultItem, - id : resultItems.length, - hitCount, - includedCount - }); - }); - return resultItems; - }; - const inputEventFunction = () => { - if (!isfetched) return; + if (!localSearch.isfetched) return; const searchText = input.value.trim().toLowerCase(); const keywords = searchText.split(/[-\s]+/); const container = document.querySelector('.search-result-container'); let resultItems = []; if (searchText.length > 0) { // Perform local searching - resultItems = getResultItems(keywords); + resultItems = localSearch.getResultItems(keywords); } if (keywords.length === 1 && keywords[0] === '') { container.classList.add('no-result'); @@ -196,71 +49,9 @@ document.addEventListener('DOMContentLoaded', () => { } }; - const fetchData = () => { - const isXml = !CONFIG.path.endsWith('json'); - fetch(CONFIG.path) - .then(response => response.text()) - .then(res => { - // Get the contents from search data - isfetched = true; - datas = isXml ? [...new DOMParser().parseFromString(res, 'text/xml').querySelectorAll('entry')].map(element => ({ - title : element.querySelector('title').textContent, - content: element.querySelector('content').textContent, - url : element.querySelector('url').textContent - })) : JSON.parse(res); - // Only match articles with non-empty titles - datas = datas.filter(data => data.title).map(data => { - data.title = data.title.trim(); - data.content = data.content ? data.content.trim().replace(/<[^>]+>/g, '') : ''; - data.url = decodeURIComponent(data.url).replace(/\/{2,}/g, '/'); - return data; - }); - // Remove loading animation - inputEventFunction(); - }); - }; - - // Highlight by wrapping node in mark elements with the given class name - const highlightText = (node, slice, className) => { - const val = node.nodeValue; - let index = slice.start; - const children = []; - for (const { position, length } of slice.hits) { - const text = document.createTextNode(val.substring(index, position)); - index = position + length; - const mark = document.createElement('mark'); - mark.className = className; - mark.appendChild(document.createTextNode(val.substr(position, length))); - children.push(text, mark); - } - node.nodeValue = val.substring(index, slice.end); - children.forEach(element => { - node.parentNode.insertBefore(element, node); - }); - }; - - // Highlight the search words provided in the url in the text - const highlightSearchWords = () => { - const params = new URL(location.href).searchParams.get('highlight'); - const keywords = params ? params.split(' ') : []; - const body = document.querySelector('.post-body'); - if (!keywords.length || !body) return; - const walk = document.createTreeWalker(body, NodeFilter.SHOW_TEXT, null); - const allNodes = []; - while (walk.nextNode()) { - if (!walk.currentNode.parentNode.matches('button, select, textarea')) allNodes.push(walk.currentNode); - } - allNodes.forEach(node => { - const [indexOfNode] = getIndexByWord(keywords, node.nodeValue); - if (!indexOfNode.length) return; - const slice = mergeIntoSlice(0, node.nodeValue.length, indexOfNode); - highlightText(node, slice, 'search-keyword'); - }); - }; - - highlightSearchWords(); + localSearch.highlightSearchWords(document.querySelector('.post-body')); if (CONFIG.localsearch.preload) { - fetchData(); + localSearch.fetchData(); } if (CONFIG.localsearch.trigger === 'auto') { @@ -273,6 +64,7 @@ document.addEventListener('DOMContentLoaded', () => { } }); } + window.addEventListener('search:loaded', inputEventFunction); // Handle and trigger popup window document.querySelectorAll('.popup-trigger').forEach(element => { @@ -280,7 +72,7 @@ document.addEventListener('DOMContentLoaded', () => { document.body.classList.add('search-active'); // Wait for search-popup animation to complete setTimeout(() => input.focus(), 500); - if (!isfetched) fetchData(); + if (!localSearch.isfetched) localSearch.fetchData(); }); }); @@ -296,7 +88,7 @@ document.addEventListener('DOMContentLoaded', () => { }); document.querySelector('.popup-btn-close').addEventListener('click', onPopupClose); document.addEventListener('pjax:success', () => { - highlightSearchWords(); + localSearch.highlightSearchWords(document.querySelector('.post-body')); onPopupClose(); }); window.addEventListener('keyup', event => {