From f8fa1f2779153a549bfce436d7bb714d1157a6cd Mon Sep 17 00:00:00 2001 From: Mimi <1119186082@qq.com> Date: Tue, 28 Jul 2020 15:23:33 +0800 Subject: [PATCH 01/16] Update --- source/js/local-search.js | 267 ++++++++++++++++++++++++-------------- 1 file changed, 172 insertions(+), 95 deletions(-) diff --git a/source/js/local-search.js b/source/js/local-search.js index 5e5ae1550..05e88fac1 100644 --- a/source/js/local-search.js +++ b/source/js/local-search.js @@ -81,125 +81,131 @@ document.addEventListener('DOMContentLoaded', () => { slice.hits.forEach(hit => { result += text.substring(prevEnd, hit.position); const end = hit.position + hit.length; - result += `${text.substring(hit.position, end)}`; + result += `${text.substring(hit.position, end)}`; prevEnd = end; }); result += text.substring(prevEnd, slice.end); return result; }; - const inputEventFunction = () => { - if (!isfetched) return; - const searchText = input.value.trim().toLowerCase(); - const keywords = searchText.split(/[-\s]+/); - if (keywords.length > 1) { - keywords.push(searchText); - } + const getResultItems = (searchText, keywords) => { const resultItems = []; - if (searchText.length > 0) { - // Perform local searching - datas.forEach(({ title, content, url }) => { - const titleInLowerCase = title.toLowerCase(); - const contentInLowerCase = content.toLowerCase(); - let indexOfTitle = []; - let indexOfContent = []; - let searchTextCount = 0; - keywords.forEach(keyword => { - indexOfTitle = indexOfTitle.concat(getIndexByWord(keyword, titleInLowerCase, false)); - indexOfContent = indexOfContent.concat(getIndexByWord(keyword, contentInLowerCase, false)); - }); - - // Show search results - if (indexOfTitle.length > 0 || indexOfContent.length > 0) { - const hitCount = indexOfTitle.length + indexOfContent.length; - // Sort index by position of keyword - [indexOfTitle, indexOfContent].forEach(index => { - index.sort((itemLeft, itemRight) => { - if (itemRight.position !== itemLeft.position) { - return itemRight.position - itemLeft.position; - } - return itemLeft.word.length - itemRight.word.length; - }); - }); + console.log(searchText) + datas.forEach(({ title, content, url }) => { + const titleInLowerCase = title.toLowerCase(); + const contentInLowerCase = content.toLowerCase(); + let indexOfTitle = []; + let indexOfContent = []; + let searchTextCount = 0; + keywords.forEach(keyword => { + indexOfTitle = indexOfTitle.concat(getIndexByWord(keyword, titleInLowerCase, false)); + indexOfContent = indexOfContent.concat(getIndexByWord(keyword, contentInLowerCase, false)); + }); - const slicesOfTitle = []; - if (indexOfTitle.length !== 0) { - const tmp = mergeIntoSlice(0, title.length, indexOfTitle, searchText); - searchTextCount += tmp.searchTextCountInSlice; - slicesOfTitle.push(tmp); + // Show search results + if (indexOfTitle.length === 0 && indexOfContent.length === 0) return; + const hitCount = indexOfTitle.length + indexOfContent.length; + // Sort index by position of keyword + [indexOfTitle, indexOfContent].forEach(index => { + index.sort((left, right) => { + if (right.position !== left.position) { + return right.position - left.position; } + return left.word.length - right.word.length; + }); + }); - let slicesOfContent = []; - while (indexOfContent.length !== 0) { - const item = indexOfContent[indexOfContent.length - 1]; - const { position, word } = item; - // Cut out 100 characters - let start = position - 20; - let end = position + 80; - if (start < 0) { - start = 0; - } - if (end < position + word.length) { - end = position + word.length; - } - if (end > content.length) { - end = content.length; - } - const tmp = mergeIntoSlice(start, end, indexOfContent, searchText); - searchTextCount += tmp.searchTextCountInSlice; - slicesOfContent.push(tmp); - } + const slicesOfTitle = []; + if (indexOfTitle.length !== 0) { + const tmp = mergeIntoSlice(0, title.length, indexOfTitle, searchText); + searchTextCount += tmp.searchTextCountInSlice; + slicesOfTitle.push(tmp); + } - // Sort slices in content by search text's count and hits' count - slicesOfContent.sort((sliceLeft, sliceRight) => { - if (sliceLeft.searchTextCount !== sliceRight.searchTextCount) { - return sliceRight.searchTextCount - sliceLeft.searchTextCount; - } else if (sliceLeft.hits.length !== sliceRight.hits.length) { - return sliceRight.hits.length - sliceLeft.hits.length; - } - return sliceLeft.start - sliceRight.start; - }); + let slicesOfContent = []; + while (indexOfContent.length !== 0) { + const item = indexOfContent[indexOfContent.length - 1]; + const { position, word } = item; + // Cut out 100 characters + let start = position - 20; + let end = position + 80; + if (start < 0) { + start = 0; + } + if (end < position + word.length) { + end = position + word.length; + } + if (end > content.length) { + end = content.length; + } + const tmp = mergeIntoSlice(start, end, indexOfContent, searchText); + searchTextCount += tmp.searchTextCountInSlice; + slicesOfContent.push(tmp); + } - // Select top N slices in content - const upperBound = parseInt(CONFIG.localsearch.top_n_per_article, 10); - if (upperBound >= 0) { - slicesOfContent = slicesOfContent.slice(0, upperBound); - } + // Sort slices in content by search text's count and hits' count + slicesOfContent.sort((left, right) => { + if (left.searchTextCount !== right.searchTextCount) { + return right.searchTextCount - left.searchTextCount; + } else if (left.hits.length !== right.hits.length) { + return right.hits.length - left.hits.length; + } + return left.start - right.start; + }); - let resultItem = ''; + // Select top N slices in content + const upperBound = parseInt(CONFIG.localsearch.top_n_per_article, 10); + if (upperBound >= 0) { + slicesOfContent = slicesOfContent.slice(0, upperBound); + } - if (slicesOfTitle.length !== 0) { - resultItem += `
  • ${highlightKeyword(title, slicesOfTitle[0])}`; - } else { - resultItem += `
  • ${title}`; - } + let resultItem = ''; - slicesOfContent.forEach(slice => { - resultItem += `

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

    `; - }); + if (slicesOfTitle.length !== 0) { + resultItem += `
  • ${highlightKeyword(title, slicesOfTitle[0])}`; + } else { + resultItem += `
  • ${title}`; + } - resultItem += '
  • '; - resultItems.push({ - item: resultItem, - id : resultItems.length, - hitCount, - searchTextCount - }); - } + slicesOfContent.forEach(slice => { + resultItem += `

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

    `; + }); + + resultItem += ''; + resultItems.push({ + item: resultItem, + id : resultItems.length, + hitCount, + searchTextCount }); + }); + return resultItems; + } + + const inputEventFunction = () => { + if (!isfetched) return; + const searchText = input.value.trim().toLowerCase(); + const keywords = searchText.split(/[-\s]+/); + if (keywords.length > 1) { + keywords.push(searchText); + } + let resultItems = []; + if (searchText.length > 0) { + // Perform local searching + resultItems = getResultItems(searchText, keywords); } if (keywords.length === 1 && keywords[0] === '') { resultContent.innerHTML = '
    '; } else if (resultItems.length === 0) { resultContent.innerHTML = '
    '; } else { - resultItems.sort((resultLeft, resultRight) => { - if (resultLeft.searchTextCount !== resultRight.searchTextCount) { - return resultRight.searchTextCount - resultLeft.searchTextCount; - } else if (resultLeft.hitCount !== resultRight.hitCount) { - return resultRight.hitCount - resultLeft.hitCount; + resultItems.sort((left, right) => { + if (left.searchTextCount !== right.searchTextCount) { + return right.searchTextCount - left.searchTextCount; + } else if (left.hitCount !== right.hitCount) { + return right.hitCount - left.hitCount; } - return resultRight.id - resultLeft.id; + return right.id - left.id; }); resultContent.innerHTML = ``; window.pjax && window.pjax.refresh(resultContent); @@ -232,6 +238,77 @@ document.addEventListener('DOMContentLoaded', () => { }); }; + /** + * small helper function to urldecode strings + */ + const urldecode = x => { + return decodeURIComponent(x).replace(/\+/g, ' '); + }; + + /** + * This function returns the parsed url parameters of the + * current request. Multiple values per key are supported, + * it will always return arrays of strings for the value parts. + */ + const getQueryParameters = () => { + const parts = location.search.split('?')[1].split('&'); + const result = {}; + for (let part of parts) { + let [key, value] = part.split('=', 2); + key = urldecode(key); + value = urldecode(value); + if (key in result) { + result[key].push(value); + } else { + result[key] = [value]; + } + } + return result; + }; + + /** + * highlight a given string on a jquery object by wrapping it in + * span elements with the given class name. + */ + const highlightText = (element, terms, className) => { + let text; + terms.forEach(term => { + text = term.toLowerCase() + }); + const highlight = node => { + const val = node.nodeValue; + const pos = val.toLowerCase().indexOf(text); + if (pos >= 0) { + const span = document.createElement("mark"); + span.className = className; + span.appendChild(document.createTextNode(val.substr(pos, text.length))); + const next = document.createTextNode(val.substr(pos + text.length)); + node.parentNode.insertBefore(span, node.parentNode.insertBefore(next, node.nextSibling)); + node.nodeValue = val.substr(0, pos); + highlight(next); + } + } + if (!element.parentNode.matches("button, select, textarea")) highlight(element); + }; + + /** + * highlight the search words provided in the url in the text + */ + window.highlightSearchWords = () => { + const params = getQueryParameters(); + const terms = (params.highlight) ? params.highlight[0].split(/\s+/) : []; + const body = document.querySelector('.post-body'); + if (!terms.length || !body) return; + const walk = document.createTreeWalker(body, NodeFilter.SHOW_TEXT, null, false); + const allNodes = []; + while (walk.nextNode()) { + allNodes.push(walk.currentNode); + } + allNodes.forEach(node => { + highlightText(node, terms, 'search-keyword'); + }); + }; + if (CONFIG.localsearch.preload) { fetchData(); } From d30faa87b5c890afdb17088514d3938bb988149a Mon Sep 17 00:00:00 2001 From: Mimi <1119186082@qq.com> Date: Tue, 28 Jul 2020 15:24:12 +0800 Subject: [PATCH 02/16] Style --- .../css/_common/components/third-party/search.styl | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/source/css/_common/components/third-party/search.styl b/source/css/_common/components/third-party/search.styl index ff75552b7..0673cbb9c 100644 --- a/source/css/_common/components/third-party/search.styl +++ b/source/css/_common/components/third-party/search.styl @@ -154,12 +154,6 @@ if (hexo-config('local_search.enable')) { font-weight: bold; } - .search-keyword { - border-bottom: 1px dashed $red; - color: $red; - font-weight: bold; - } - #search-result { display: flex; height: calc(100% - 55px); @@ -172,4 +166,11 @@ if (hexo-config('local_search.enable')) { margin: auto; } } + + mark.search-keyword { + background: transparent; + border-bottom: 1px dashed $red; + color: $red; + font-weight: bold; + } } From 5adbf86822c1a694ec64b3a3d0f1eab4d262698f Mon Sep 17 00:00:00 2001 From: Mimi <1119186082@qq.com> Date: Tue, 28 Jul 2020 15:48:12 +0800 Subject: [PATCH 03/16] Fix --- source/js/local-search.js | 37 +++++++++++++++++-------------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/source/js/local-search.js b/source/js/local-search.js index 05e88fac1..f5b1417d7 100644 --- a/source/js/local-search.js +++ b/source/js/local-search.js @@ -24,7 +24,7 @@ document.addEventListener('DOMContentLoaded', () => { const wordLen = word.length; if (wordLen === 0) return []; let startPosition = 0; - let position = []; + let position = -1; const index = []; if (!caseSensitive) { text = text.toLowerCase(); @@ -42,10 +42,10 @@ document.addEventListener('DOMContentLoaded', () => { let item = index[index.length - 1]; let { position, word } = item; const hits = []; - let searchTextCountInSlice = 0; + let searchTextCount = 0; while (position + word.length <= end && index.length !== 0) { if (word === searchText) { - searchTextCountInSlice++; + searchTextCount++; } hits.push({ position, @@ -70,7 +70,7 @@ document.addEventListener('DOMContentLoaded', () => { hits, start, end, - searchTextCount: searchTextCountInSlice + searchTextCount }; }; @@ -90,35 +90,32 @@ document.addEventListener('DOMContentLoaded', () => { const getResultItems = (searchText, keywords) => { const resultItems = []; - console.log(searchText) datas.forEach(({ title, content, url }) => { - const titleInLowerCase = title.toLowerCase(); - const contentInLowerCase = content.toLowerCase(); let indexOfTitle = []; let indexOfContent = []; let searchTextCount = 0; keywords.forEach(keyword => { - indexOfTitle = indexOfTitle.concat(getIndexByWord(keyword, titleInLowerCase, false)); - indexOfContent = indexOfContent.concat(getIndexByWord(keyword, contentInLowerCase, false)); + indexOfTitle = indexOfTitle.concat(getIndexByWord(keyword, title, false)); + indexOfContent = indexOfContent.concat(getIndexByWord(keyword, content, false)); }); // Show search results - if (indexOfTitle.length === 0 && indexOfContent.length === 0) return; const hitCount = indexOfTitle.length + indexOfContent.length; + if (hitCount === 0) return; // Sort index by position of keyword - [indexOfTitle, indexOfContent].forEach(index => { - index.sort((left, right) => { - if (right.position !== left.position) { - return right.position - left.position; - } - return left.word.length - right.word.length; - }); - }); + const compare = (left, right) => { + if (right.position !== left.position) { + return right.position - left.position; + } + return left.word.length - right.word.length; + }; + indexOfTitle.sort(compare); + indexOfContent.sort(compare); const slicesOfTitle = []; if (indexOfTitle.length !== 0) { const tmp = mergeIntoSlice(0, title.length, indexOfTitle, searchText); - searchTextCount += tmp.searchTextCountInSlice; + searchTextCount += tmp.searchTextCount; slicesOfTitle.push(tmp); } @@ -139,7 +136,7 @@ document.addEventListener('DOMContentLoaded', () => { end = content.length; } const tmp = mergeIntoSlice(start, end, indexOfContent, searchText); - searchTextCount += tmp.searchTextCountInSlice; + searchTextCount += tmp.searchTextCount; slicesOfContent.push(tmp); } From 2501c710712308329c0351b0200357d3669eefd9 Mon Sep 17 00:00:00 2001 From: Mimi <1119186082@qq.com> Date: Tue, 28 Jul 2020 16:29:29 +0800 Subject: [PATCH 04/16] Rename to count --- source/js/local-search.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/source/js/local-search.js b/source/js/local-search.js index f5b1417d7..cf60d2136 100644 --- a/source/js/local-search.js +++ b/source/js/local-search.js @@ -42,10 +42,10 @@ document.addEventListener('DOMContentLoaded', () => { let item = index[index.length - 1]; let { position, word } = item; const hits = []; - let searchTextCount = 0; + let count = 0; while (position + word.length <= end && index.length !== 0) { if (word === searchText) { - searchTextCount++; + count++; } hits.push({ position, @@ -70,7 +70,7 @@ document.addEventListener('DOMContentLoaded', () => { hits, start, end, - searchTextCount + count }; }; @@ -115,7 +115,7 @@ document.addEventListener('DOMContentLoaded', () => { const slicesOfTitle = []; if (indexOfTitle.length !== 0) { const tmp = mergeIntoSlice(0, title.length, indexOfTitle, searchText); - searchTextCount += tmp.searchTextCount; + searchTextCount += tmp.count; slicesOfTitle.push(tmp); } @@ -136,14 +136,14 @@ document.addEventListener('DOMContentLoaded', () => { end = content.length; } const tmp = mergeIntoSlice(start, end, indexOfContent, searchText); - searchTextCount += tmp.searchTextCount; + searchTextCount += tmp.count; slicesOfContent.push(tmp); } // Sort slices in content by search text's count and hits' count slicesOfContent.sort((left, right) => { - if (left.searchTextCount !== right.searchTextCount) { - return right.searchTextCount - left.searchTextCount; + 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; } From ff01f29c179c32da739b05b7848479a44abc42e6 Mon Sep 17 00:00:00 2001 From: Mimi <1119186082@qq.com> Date: Tue, 28 Jul 2020 18:20:47 +0800 Subject: [PATCH 05/16] Reverse index --- source/js/local-search.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/source/js/local-search.js b/source/js/local-search.js index cf60d2136..889f015c7 100644 --- a/source/js/local-search.js +++ b/source/js/local-search.js @@ -39,7 +39,7 @@ document.addEventListener('DOMContentLoaded', () => { // Merge hits into slices const mergeIntoSlice = (start, end, index, searchText) => { - let item = index[index.length - 1]; + let item = index[0]; let { position, word } = item; const hits = []; let count = 0; @@ -54,13 +54,13 @@ document.addEventListener('DOMContentLoaded', () => { const wordEnd = position + word.length; // Move to next position of hit - index.pop(); + index.shift(); while (index.length !== 0) { - item = index[index.length - 1]; + item = index[0]; position = item.position; word = item.word; if (wordEnd > position) { - index.pop(); + index.shift(); } else { break; } @@ -104,10 +104,10 @@ document.addEventListener('DOMContentLoaded', () => { if (hitCount === 0) return; // Sort index by position of keyword const compare = (left, right) => { - if (right.position !== left.position) { - return right.position - left.position; + if (left.position !== right.position) { + return left.position - right.position; } - return left.word.length - right.word.length; + return right.word.length - left.word.length; }; indexOfTitle.sort(compare); indexOfContent.sort(compare); @@ -121,7 +121,7 @@ document.addEventListener('DOMContentLoaded', () => { let slicesOfContent = []; while (indexOfContent.length !== 0) { - const item = indexOfContent[indexOfContent.length - 1]; + const item = indexOfContent[0]; const { position, word } = item; // Cut out 100 characters let start = position - 20; From 86e7955caef2b2154bcbbba07fc8e56fae5c2c34 Mon Sep 17 00:00:00 2001 From: Mimi <1119186082@qq.com> Date: Tue, 28 Jul 2020 21:43:53 +0800 Subject: [PATCH 06/16] Sort by includedCount --- layout/_partials/search/localsearch.njk | 2 +- source/js/local-search.js | 57 +++++++++---------------- 2 files changed, 21 insertions(+), 38 deletions(-) diff --git a/layout/_partials/search/localsearch.njk b/layout/_partials/search/localsearch.njk index df81e95d3..26931e6c7 100644 --- a/layout/_partials/search/localsearch.njk +++ b/layout/_partials/search/localsearch.njk @@ -3,7 +3,7 @@
    -
    diff --git a/source/js/local-search.js b/source/js/local-search.js index 889f015c7..c2a6f3c35 100644 --- a/source/js/local-search.js +++ b/source/js/local-search.js @@ -38,15 +38,11 @@ document.addEventListener('DOMContentLoaded', () => { }; // Merge hits into slices - const mergeIntoSlice = (start, end, index, searchText) => { + const mergeIntoSlice = (start, end, index) => { let item = index[0]; let { position, word } = item; const hits = []; - let count = 0; while (position + word.length <= end && index.length !== 0) { - if (word === searchText) { - count++; - } hits.push({ position, length: word.length @@ -69,8 +65,7 @@ document.addEventListener('DOMContentLoaded', () => { return { hits, start, - end, - count + end }; }; @@ -88,15 +83,19 @@ document.addEventListener('DOMContentLoaded', () => { return result; }; - const getResultItems = (searchText, keywords) => { + const getResultItems = (keywords) => { const resultItems = []; datas.forEach(({ title, content, url }) => { let indexOfTitle = []; let indexOfContent = []; - let searchTextCount = 0; + // The number of different keywords included in the article. + let includedCount = 0; keywords.forEach(keyword => { - indexOfTitle = indexOfTitle.concat(getIndexByWord(keyword, title, false)); - indexOfContent = indexOfContent.concat(getIndexByWord(keyword, content, false)); + const titleIndex = getIndexByWord(keyword, title, false); + const contentIndex = getIndexByWord(keyword, content, false); + if (titleIndex.length + contentIndex.length > 0) includedCount++; + indexOfTitle = indexOfTitle.concat(titleIndex); + indexOfContent = indexOfContent.concat(contentIndex); }); // Show search results @@ -114,30 +113,17 @@ document.addEventListener('DOMContentLoaded', () => { const slicesOfTitle = []; if (indexOfTitle.length !== 0) { - const tmp = mergeIntoSlice(0, title.length, indexOfTitle, searchText); - searchTextCount += tmp.count; - slicesOfTitle.push(tmp); + slicesOfTitle.push(mergeIntoSlice(0, title.length, indexOfTitle)); } let slicesOfContent = []; while (indexOfContent.length !== 0) { const item = indexOfContent[0]; - const { position, word } = item; - // Cut out 100 characters - let start = position - 20; - let end = position + 80; - if (start < 0) { - start = 0; - } - if (end < position + word.length) { - end = position + word.length; - } - if (end > content.length) { - end = content.length; - } - const tmp = mergeIntoSlice(start, end, indexOfContent, searchText); - searchTextCount += tmp.count; - slicesOfContent.push(tmp); + 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 search text's count and hits' count @@ -173,7 +159,7 @@ document.addEventListener('DOMContentLoaded', () => { item: resultItem, id : resultItems.length, hitCount, - searchTextCount + includedCount }); }); return resultItems; @@ -183,13 +169,10 @@ document.addEventListener('DOMContentLoaded', () => { if (!isfetched) return; const searchText = input.value.trim().toLowerCase(); const keywords = searchText.split(/[-\s]+/); - if (keywords.length > 1) { - keywords.push(searchText); - } let resultItems = []; if (searchText.length > 0) { // Perform local searching - resultItems = getResultItems(searchText, keywords); + resultItems = getResultItems(keywords); } if (keywords.length === 1 && keywords[0] === '') { resultContent.innerHTML = '
    '; @@ -197,8 +180,8 @@ document.addEventListener('DOMContentLoaded', () => { resultContent.innerHTML = '
    '; } else { resultItems.sort((left, right) => { - if (left.searchTextCount !== right.searchTextCount) { - return right.searchTextCount - left.searchTextCount; + if (left.includedCount !== right.includedCount) { + return right.includedCount - left.includedCount; } else if (left.hitCount !== right.hitCount) { return right.hitCount - left.hitCount; } From bd2385767c85884f4a9454e55c9738ef40451482 Mon Sep 17 00:00:00 2001 From: Mimi <1119186082@qq.com> Date: Tue, 28 Jul 2020 22:27:12 +0800 Subject: [PATCH 07/16] Jobs done --- source/js/local-search.js | 64 +++++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 29 deletions(-) diff --git a/source/js/local-search.js b/source/js/local-search.js index c2a6f3c35..14f1de17c 100644 --- a/source/js/local-search.js +++ b/source/js/local-search.js @@ -37,6 +37,14 @@ document.addEventListener('DOMContentLoaded', () => { return index; }; + // Sort index by position of keyword + const compare = (left, right) => { + if (left.position !== right.position) { + return left.position - right.position; + } + return right.word.length - left.word.length; + }; + // Merge hits into slices const mergeIntoSlice = (start, end, index) => { let item = index[0]; @@ -101,13 +109,7 @@ document.addEventListener('DOMContentLoaded', () => { // Show search results const hitCount = indexOfTitle.length + indexOfContent.length; if (hitCount === 0) return; - // Sort index by position of keyword - const compare = (left, right) => { - if (left.position !== right.position) { - return left.position - right.position; - } - return right.word.length - left.word.length; - }; + indexOfTitle.sort(compare); indexOfContent.sort(compare); @@ -250,25 +252,22 @@ document.addEventListener('DOMContentLoaded', () => { * highlight a given string on a jquery object by wrapping it in * span elements with the given class name. */ - const highlightText = (element, terms, className) => { - let text; - terms.forEach(term => { - text = term.toLowerCase() - }); - const highlight = node => { - const val = node.nodeValue; - const pos = val.toLowerCase().indexOf(text); - if (pos >= 0) { - const span = document.createElement("mark"); - span.className = className; - span.appendChild(document.createTextNode(val.substr(pos, text.length))); - const next = document.createTextNode(val.substr(pos + text.length)); - node.parentNode.insertBefore(span, node.parentNode.insertBefore(next, node.nextSibling)); - node.nodeValue = val.substr(0, pos); - highlight(next); - } + const highlightText = (node, hits, className) => { + const val = node.nodeValue; + let index = 0; + const children = []; + for (let { position, length } of hits) { + const text = document.createTextNode(val.substr(index, position - index)); + index = position + length; + const mark = document.createElement("mark"); + mark.className = className; + mark.appendChild(document.createTextNode(val.substr(position, length))); + children.push(text, mark); } - if (!element.parentNode.matches("button, select, textarea")) highlight(element); + node.nodeValue = val.substr(index, val.length); + children.forEach(element => { + node.parentNode.insertBefore(element, node); + }); }; /** @@ -276,16 +275,23 @@ document.addEventListener('DOMContentLoaded', () => { */ window.highlightSearchWords = () => { const params = getQueryParameters(); - const terms = (params.highlight) ? params.highlight[0].split(/\s+/) : []; + const keywords = (params.highlight) ? params.highlight[0].split(/\s+/) : []; const body = document.querySelector('.post-body'); - if (!terms.length || !body) return; + if (!keywords.length || !body) return; const walk = document.createTreeWalker(body, NodeFilter.SHOW_TEXT, null, false); const allNodes = []; while (walk.nextNode()) { - allNodes.push(walk.currentNode); + if (!walk.currentNode.parentNode.matches("button, select, textarea")) allNodes.push(walk.currentNode); } allNodes.forEach(node => { - highlightText(node, terms, 'search-keyword'); + let indexOfNode = []; + keywords.forEach(keyword => { + const nodeIndex = getIndexByWord(keyword, node.nodeValue, false); + indexOfNode = indexOfNode.concat(nodeIndex); + }); + if (!indexOfNode.length) return; + const { hits } = mergeIntoSlice(0, node.nodeValue.length, indexOfNode); + highlightText(node, hits, 'search-keyword'); }); }; From 57998b56be2af3242571b4e79791d7e8a3c1e392 Mon Sep 17 00:00:00 2001 From: Mimi <1119186082@qq.com> Date: Tue, 28 Jul 2020 22:34:38 +0800 Subject: [PATCH 08/16] Fix eslint --- source/js/local-search.js | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/source/js/local-search.js b/source/js/local-search.js index 14f1de17c..26ef2830e 100644 --- a/source/js/local-search.js +++ b/source/js/local-search.js @@ -165,7 +165,7 @@ document.addEventListener('DOMContentLoaded', () => { }); }); return resultItems; - } + }; const inputEventFunction = () => { if (!isfetched) return; @@ -220,9 +220,7 @@ document.addEventListener('DOMContentLoaded', () => { }); }; - /** - * small helper function to urldecode strings - */ + // Small helper function to urldecode strings const urldecode = x => { return decodeURIComponent(x).replace(/\+/g, ' '); }; @@ -235,7 +233,7 @@ document.addEventListener('DOMContentLoaded', () => { const getQueryParameters = () => { const parts = location.search.split('?')[1].split('&'); const result = {}; - for (let part of parts) { + for (const part of parts) { let [key, value] = part.split('=', 2); key = urldecode(key); value = urldecode(value); @@ -248,18 +246,15 @@ document.addEventListener('DOMContentLoaded', () => { return result; }; - /** - * highlight a given string on a jquery object by wrapping it in - * span elements with the given class name. - */ + // Highlight by wrapping node in mark elements with the given class name const highlightText = (node, hits, className) => { const val = node.nodeValue; let index = 0; const children = []; - for (let { position, length } of hits) { + for (const { position, length } of hits) { const text = document.createTextNode(val.substr(index, position - index)); index = position + length; - const mark = document.createElement("mark"); + const mark = document.createElement('mark'); mark.className = className; mark.appendChild(document.createTextNode(val.substr(position, length))); children.push(text, mark); @@ -270,18 +265,16 @@ document.addEventListener('DOMContentLoaded', () => { }); }; - /** - * highlight the search words provided in the url in the text - */ + // Highlight the search words provided in the url in the text window.highlightSearchWords = () => { const params = getQueryParameters(); - const keywords = (params.highlight) ? params.highlight[0].split(/\s+/) : []; + const keywords = params.highlight ? params.highlight[0].split(/\s+/) : []; const body = document.querySelector('.post-body'); if (!keywords.length || !body) return; const walk = document.createTreeWalker(body, NodeFilter.SHOW_TEXT, null, false); const allNodes = []; while (walk.nextNode()) { - if (!walk.currentNode.parentNode.matches("button, select, textarea")) allNodes.push(walk.currentNode); + if (!walk.currentNode.parentNode.matches('button, select, textarea')) allNodes.push(walk.currentNode); } allNodes.forEach(node => { let indexOfNode = []; @@ -290,6 +283,7 @@ document.addEventListener('DOMContentLoaded', () => { indexOfNode = indexOfNode.concat(nodeIndex); }); if (!indexOfNode.length) return; + indexOfNode.sort(compare); const { hits } = mergeIntoSlice(0, node.nodeValue.length, indexOfNode); highlightText(node, hits, 'search-keyword'); }); From 90cc90b858744b5488e2f516f2de4cba60e9b5f8 Mon Sep 17 00:00:00 2001 From: Mimi <1119186082@qq.com> Date: Tue, 28 Jul 2020 23:12:49 +0800 Subject: [PATCH 09/16] Reuse --- source/js/local-search.js | 86 +++++++++++++++++---------------------- 1 file changed, 38 insertions(+), 48 deletions(-) diff --git a/source/js/local-search.js b/source/js/local-search.js index 26ef2830e..aaf02b730 100644 --- a/source/js/local-search.js +++ b/source/js/local-search.js @@ -15,34 +15,39 @@ document.addEventListener('DOMContentLoaded', () => { const input = document.querySelector('.search-input'); const resultContent = document.getElementById('search-result'); - const getIndexByWord = (word, text, caseSensitive) => { - 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; - const index = []; - if (!caseSensitive) { - text = text.toLowerCase(); - word = word.toLowerCase(); - } - while ((position = text.indexOf(word, startPosition)) > -1) { - index.push({ position, word }); - startPosition = position + wordLen; - } - return index; - }; + const getIndexByWord = (words, text, caseSensitive = false) => { + // Sort index by position of keyword + const compare = (left, right) => { + if (left.position !== right.position) { + return left.position - right.position; + } + return right.word.length - left.word.length; + }; - // Sort index by position of keyword - const compare = (left, right) => { - if (left.position !== right.position) { - return left.position - right.position; - } - return right.word.length - left.word.length; + 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; + } + }); + index.sort(compare); + return [index, included]; }; // Merge hits into slices @@ -94,25 +99,15 @@ document.addEventListener('DOMContentLoaded', () => { const getResultItems = (keywords) => { const resultItems = []; datas.forEach(({ title, content, url }) => { - let indexOfTitle = []; - let indexOfContent = []; // The number of different keywords included in the article. - let includedCount = 0; - keywords.forEach(keyword => { - const titleIndex = getIndexByWord(keyword, title, false); - const contentIndex = getIndexByWord(keyword, content, false); - if (titleIndex.length + contentIndex.length > 0) includedCount++; - indexOfTitle = indexOfTitle.concat(titleIndex); - indexOfContent = indexOfContent.concat(contentIndex); - }); + 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; - indexOfTitle.sort(compare); - indexOfContent.sort(compare); - const slicesOfTitle = []; if (indexOfTitle.length !== 0) { slicesOfTitle.push(mergeIntoSlice(0, title.length, indexOfTitle)); @@ -252,14 +247,14 @@ document.addEventListener('DOMContentLoaded', () => { let index = 0; const children = []; for (const { position, length } of hits) { - const text = document.createTextNode(val.substr(index, position - index)); + 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.substr(index, val.length); + node.nodeValue = val.substr(index); children.forEach(element => { node.parentNode.insertBefore(element, node); }); @@ -277,13 +272,8 @@ document.addEventListener('DOMContentLoaded', () => { if (!walk.currentNode.parentNode.matches('button, select, textarea')) allNodes.push(walk.currentNode); } allNodes.forEach(node => { - let indexOfNode = []; - keywords.forEach(keyword => { - const nodeIndex = getIndexByWord(keyword, node.nodeValue, false); - indexOfNode = indexOfNode.concat(nodeIndex); - }); + const [indexOfNode] = getIndexByWord(keywords, node.nodeValue); if (!indexOfNode.length) return; - indexOfNode.sort(compare); const { hits } = mergeIntoSlice(0, node.nodeValue.length, indexOfNode); highlightText(node, hits, 'search-keyword'); }); From 98d921be71782cdc3c281d8dad4912dcd62fb606 Mon Sep 17 00:00:00 2001 From: Mimi <1119186082@qq.com> Date: Tue, 28 Jul 2020 23:45:32 +0800 Subject: [PATCH 10/16] Parse URL --- source/js/local-search.js | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/source/js/local-search.js b/source/js/local-search.js index aaf02b730..5544b906c 100644 --- a/source/js/local-search.js +++ b/source/js/local-search.js @@ -96,7 +96,7 @@ document.addEventListener('DOMContentLoaded', () => { return result; }; - const getResultItems = (keywords) => { + const getResultItems = keywords => { const resultItems = []; datas.forEach(({ title, content, url }) => { // The number of different keywords included in the article. @@ -141,14 +141,17 @@ document.addEventListener('DOMContentLoaded', () => { let resultItem = ''; + url = new URL(url, location.origin); + url.searchParams.append('highlight', keywords.join(' ')); + if (slicesOfTitle.length !== 0) { - resultItem += `
  • ${highlightKeyword(title, slicesOfTitle[0])}`; + resultItem += `
  • ${highlightKeyword(title, slicesOfTitle[0])}`; } else { - resultItem += `
  • ${title}`; + resultItem += `
  • ${title}`; } slicesOfContent.forEach(slice => { - resultItem += `

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

    `; + resultItem += `

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

    `; }); resultItem += '
  • '; @@ -226,7 +229,8 @@ document.addEventListener('DOMContentLoaded', () => { * it will always return arrays of strings for the value parts. */ const getQueryParameters = () => { - const parts = location.search.split('?')[1].split('&'); + const s = location.search; + const parts = s.substr(s.indexOf('?') + 1).split('&'); const result = {}; for (const part of parts) { let [key, value] = part.split('=', 2); @@ -261,7 +265,7 @@ document.addEventListener('DOMContentLoaded', () => { }; // Highlight the search words provided in the url in the text - window.highlightSearchWords = () => { + const highlightSearchWords = () => { const params = getQueryParameters(); const keywords = params.highlight ? params.highlight[0].split(/\s+/) : []; const body = document.querySelector('.post-body'); @@ -315,7 +319,10 @@ document.addEventListener('DOMContentLoaded', () => { } }); document.querySelector('.popup-btn-close').addEventListener('click', onPopupClose); - document.addEventListener('pjax:success', onPopupClose); + document.addEventListener('pjax:success', () => { + highlightSearchWords(); + onPopupClose(); + }); window.addEventListener('keyup', event => { if (event.key === 'Escape') { onPopupClose(); From ee8bf7cabf4fcf2814178ff028f4f2c74441398b Mon Sep 17 00:00:00 2001 From: Mimi <1119186082@qq.com> Date: Wed, 29 Jul 2020 00:01:17 +0800 Subject: [PATCH 11/16] Update --- source/js/local-search.js | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/source/js/local-search.js b/source/js/local-search.js index 5544b906c..a320445ca 100644 --- a/source/js/local-search.js +++ b/source/js/local-search.js @@ -218,11 +218,6 @@ document.addEventListener('DOMContentLoaded', () => { }); }; - // Small helper function to urldecode strings - const urldecode = x => { - return decodeURIComponent(x).replace(/\+/g, ' '); - }; - /** * This function returns the parsed url parameters of the * current request. Multiple values per key are supported, @@ -233,9 +228,7 @@ document.addEventListener('DOMContentLoaded', () => { const parts = s.substr(s.indexOf('?') + 1).split('&'); const result = {}; for (const part of parts) { - let [key, value] = part.split('=', 2); - key = urldecode(key); - value = urldecode(value); + const [key, value] = part.split('=', 2); if (key in result) { result[key].push(value); } else { @@ -267,7 +260,7 @@ document.addEventListener('DOMContentLoaded', () => { // Highlight the search words provided in the url in the text const highlightSearchWords = () => { const params = getQueryParameters(); - const keywords = params.highlight ? params.highlight[0].split(/\s+/) : []; + const keywords = params.highlight ? params.highlight[0].split(/\+/).map(decodeURIComponent) : []; const body = document.querySelector('.post-body'); if (!keywords.length || !body) return; const walk = document.createTreeWalker(body, NodeFilter.SHOW_TEXT, null, false); From 63e7a284fd6ed7f9346ff44251782a188a11944f Mon Sep 17 00:00:00 2001 From: Mimi <1119186082@qq.com> Date: Wed, 29 Jul 2020 01:43:20 +0800 Subject: [PATCH 12/16] Detroit loading spinner --- layout/_partials/search/localsearch.njk | 12 ++++++++- .../components/third-party/search.styl | 25 +++++++++++++++++++ source/js/local-search.js | 1 - 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/layout/_partials/search/localsearch.njk b/layout/_partials/search/localsearch.njk index 26931e6c7..6a55af46c 100644 --- a/layout/_partials/search/localsearch.njk +++ b/layout/_partials/search/localsearch.njk @@ -13,6 +13,16 @@
    - +
    diff --git a/source/css/_common/components/third-party/search.styl b/source/css/_common/components/third-party/search.styl index 0673cbb9c..2518300ce 100644 --- a/source/css/_common/components/third-party/search.styl +++ b/source/css/_common/components/third-party/search.styl @@ -164,6 +164,31 @@ if (hexo-config('local_search.enable')) { #no-result { color: $grey-light; margin: auto; + + svg { + width: 6em; + + .spinner { + animation: fa-spin 5s linear alternate-reverse infinite; + opacity: .5; + transform-origin: 50%; + } + + use { + &:nth-of-type(1) { + animation-duration: 2s; + } + &:nth-of-type(2) { + animation-duration: 3s; + } + &:nth-of-type(3) { + animation-duration: 7s; + } + &:nth-of-type(4) { + animation-duration: 11s; + } + } + } } } diff --git a/source/js/local-search.js b/source/js/local-search.js index a320445ca..c8195c84d 100644 --- a/source/js/local-search.js +++ b/source/js/local-search.js @@ -213,7 +213,6 @@ document.addEventListener('DOMContentLoaded', () => { return data; }); // Remove loading animation - document.getElementById('no-result').innerHTML = ''; inputEventFunction(); }); }; From 9a63279d93e9d002212c11cd2068834f1584a0a5 Mon Sep 17 00:00:00 2001 From: Mimi <1119186082@qq.com> Date: Wed, 29 Jul 2020 07:31:02 +0800 Subject: [PATCH 13/16] Update --- scripts/helpers/next-config.js | 16 +++++++++------- source/js/local-search.js | 19 +++++++++---------- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/scripts/helpers/next-config.js b/scripts/helpers/next-config.js index cef8beaee..89d5b9aa2 100644 --- a/scripts/helpers/next-config.js +++ b/scripts/helpers/next-config.js @@ -9,7 +9,6 @@ const { parse } = require('url'); */ hexo.extend.helper.register('next_config', function() { const { config, theme, next_version } = this; - config.algolia = config.algolia || {}; const exportConfig = { hostname : parse(config.url).hostname || config.url, root : config.root, @@ -24,19 +23,22 @@ hexo.extend.helper.register('next_config', function() { lazyload : theme.lazyload, pangu : theme.pangu, comments : theme.comments, - algolia : { + motion : theme.motion, + prism : config.prismjs.enable && !config.prismjs.preprocess + }; + if (theme.algolia_search && theme.algolia_search.enable) { + config.algolia = config.algolia || {}; + exportConfig.algolia = { appID : config.algolia.applicationID, apiKey : config.algolia.apiKey, indexName: config.algolia.indexName, hits : theme.algolia_search.hits, labels : theme.algolia_search.labels - }, - localsearch: theme.local_search, - motion : theme.motion, - prism : config.prismjs.enable && !config.prismjs.preprocess - }; + }; + } if (config.search) { exportConfig.path = config.search.path; + exportConfig.localsearch = theme.local_search; } return `