From 232aa4322e2d09324641f01e11aa9a2c8c453527 Mon Sep 17 00:00:00 2001 From: xaoxuu Date: Sun, 27 Nov 2022 17:16:38 +0800 Subject: [PATCH] [feat] add local search --- _config.yml | 11 +- _data/widgets.yml | 5 +- languages/en.yml | 5 + languages/zh-CN.yml | 5 + languages/zh-TW.yml | 5 + layout/_partial/scripts/index.ejs | 9 ++ layout/_partial/widgets/search.ejs | 31 ++++ source/css/_layout/widgets/search.styl | 3 + source/css/_layout/widgets/widgets.styl | 6 +- source/css/_plugins/index.styl | 4 + source/css/_plugins/search/local-search.styl | 70 ++++++++ source/js/main.js | 33 ++++ source/js/search/local-search.js | 161 +++++++++++++++++++ 13 files changed, 342 insertions(+), 6 deletions(-) create mode 100644 layout/_partial/widgets/search.ejs create mode 100644 source/css/_layout/widgets/search.styl create mode 100644 source/css/_plugins/search/local-search.styl create mode 100644 source/js/search/local-search.js diff --git a/_config.yml b/_config.yml index 230684a..b7521b0 100755 --- a/_config.yml +++ b/_config.yml @@ -25,10 +25,10 @@ sidebar: # about: '[关于](/about/)' # Sidebar widgets widgets: - index: welcome, recent, timeline # for home/wiki/categories/tags/archives/404 pages + index: welcome, search, recent, timeline # for home/wiki/categories/tags/archives/404 pages page: welcome, toc # for pages using 'layout:page' - post: toc, ghrepo, ghissues # for pages using 'layout:post' - wiki: ghrepo, toc, ghissues, related # for pages using 'layout:wiki' + post: toc, ghrepo, search, ghissues # for pages using 'layout:post' + wiki: search, ghrepo, toc, ghissues, related # for pages using 'layout:wiki' ######## Index ######## post-index: # 近期发布 分类 标签 归档 and ... @@ -61,6 +61,11 @@ article: max_count: 5 +search: + service: local_search # hexo, todo... + local_search: # npm i hexo-generator-search + + ######## Comments ######## comments: service: # beaudar, utterances, twikoo, waline diff --git a/_data/widgets.yml b/_data/widgets.yml index 8d19eb5..29e7ed9 100644 --- a/_data/widgets.yml +++ b/_data/widgets.yml @@ -2,7 +2,10 @@ # layout即组件布局,支持自定义的有: # - markdown: 渲染 md 文本 # - +search: + layout: search + filter: auto # auto or 'path' + ghrepo: layout: ghrepo related: diff --git a/languages/en.yml b/languages/en.yml index adc51da..a5f1333 100755 --- a/languages/en.yml +++ b/languages/en.yml @@ -41,6 +41,11 @@ page: why: The address may be entered incorrectly or the address has been deleted. action: Back to Home +search: + search: Search + search_in: Search in %s + no_results: No Results! + message: copied: Copied! diff --git a/languages/zh-CN.yml b/languages/zh-CN.yml index ce87ae3..49af285 100755 --- a/languages/zh-CN.yml +++ b/languages/zh-CN.yml @@ -41,6 +41,11 @@ page: why: 可能是输入地址有误或该地址已被删除 action: 返回主页 +search: + search: 站内搜索 + search_in: 在 %s 中搜索 + no_results: 没有找到内容! + message: copied: 复制成功 diff --git a/languages/zh-TW.yml b/languages/zh-TW.yml index c874176..f49a3dc 100755 --- a/languages/zh-TW.yml +++ b/languages/zh-TW.yml @@ -41,6 +41,11 @@ page: why: 可能是網址有誤或已經刪除 action: 返回首頁 +search: + search: 站內搜索 + search_in: 在 %s 中搜索 + no_results: 沒有找到內容! + message: copied: 複製成功 diff --git a/layout/_partial/scripts/index.ejs b/layout/_partial/scripts/index.ejs index 3dc91bb..7d848c3 100644 --- a/layout/_partial/scripts/index.ejs +++ b/layout/_partial/scripts/index.ejs @@ -107,6 +107,15 @@ jQuery: '<%- url_for(theme.plugins.jquery || "https://fastly.jsdelivr.net/npm/jquery@latest/dist/jquery.min.js") %>' }; + if ('<%- theme.search.service %>') { + stellar.search = {}; + stellar.search.service = '<%- theme.search.service %>'; + if (stellar.search.service == 'local_search') { + let service_obj = Object.assign({}, <%- JSON.stringify(config.search) %>); + stellar.search[stellar.search.service] = service_obj; + } + } + // stellar js stellar.plugins.stellar = Object.assign(<%- JSON.stringify(theme.plugins.stellar) %>); diff --git a/layout/_partial/widgets/search.ejs b/layout/_partial/widgets/search.ejs new file mode 100644 index 0000000..438f5ef --- /dev/null +++ b/layout/_partial/widgets/search.ejs @@ -0,0 +1,31 @@ +<% +function layoutDiv() { + var el = '' + el += '
' + el += '' + el += '
' + el += '
' + return el +} +%> +<%- layoutDiv() %> diff --git a/source/css/_layout/widgets/search.styl b/source/css/_layout/widgets/search.styl new file mode 100644 index 0000000..a6109b0 --- /dev/null +++ b/source/css/_layout/widgets/search.styl @@ -0,0 +1,3 @@ +.widgets .widget-wrapper.search + margin-top: 0 + margin-bottom: 0 diff --git a/source/css/_layout/widgets/widgets.styl b/source/css/_layout/widgets/widgets.styl index 41a2c77..ba4f9ce 100644 --- a/source/css/_layout/widgets/widgets.styl +++ b/source/css/_layout/widgets/widgets.styl @@ -10,8 +10,6 @@ z-index: 1 line-height: 1.2 .widget-wrapper - display: block - margin: 1rem 0 2rem .widget-header padding-left: var(--gap-l) padding-right: var(--gap-l) @@ -56,3 +54,7 @@ margin-top: 3rem .widget-wrapper+.widget-wrapper.toc .widget-header margin-top: 1rem + +.widget-wrapper + display: block + margin: 1rem 0 2rem \ No newline at end of file diff --git a/source/css/_plugins/index.styl b/source/css/_plugins/index.styl index 2a68111..c8db7cc 100644 --- a/source/css/_plugins/index.styl +++ b/source/css/_plugins/index.styl @@ -8,6 +8,10 @@ if hexo-config('plugins.scrollreveal.enable') if hexo-config('plugins.fancybox.enable') @import 'fancybox' +// 搜索 +if hexo-config('search.service') == 'local_search' + @import 'search/local-search' + // 评论 if hexo-config('comments.service') == 'beaudar' @import 'comments/beaudar' diff --git a/source/css/_plugins/search/local-search.styl b/source/css/_plugins/search/local-search.styl new file mode 100644 index 0000000..96e5685 --- /dev/null +++ b/source/css/_plugins/search/local-search.styl @@ -0,0 +1,70 @@ +.search-wrapper + width: 100% + >.search-form + position: sticky + top: 1rem + .search-input + width: 100% + padding: 0.75rem 1rem + line-height: 1 + box-sizing: border-box + border-radius: $border-block + background-color: var(--card) + color: var(--text-p0) + box-shadow: $boxshadow-button + trans1 box-shadow + &:hover + box-shadow: 0 0 2px 0px alpha($color-theme, 0.2), 0 0 8px 0px alpha($color-theme, 0.2) + &:focus + box-shadow: 0 0 2px 0px alpha($color-theme, 0.4), 0 0 8px 0px alpha($color-theme, 0.4) + + .search-no-result + display: none + margin: 1em auto + color: var(--text-p1) + text-align: center + font-size: $fs-14 + padding: 2rem + background: var(--block) + border-radius: $border-block + + #search-result + ul.search-result-list + padding: 0 + margin: 0 + list-style-type: none + li + margin: 1em auto + &:hover + .search-result-title + color: $color-hover + .search-result-title + background-image: none + color: var(--text-p1) + text-transform: capitalize + font-weight: bold + line-height: 1.2 + + .search-result-content + overflow: hidden + color: var(--text-p3) + margin: .4em auto + max-height: 13em + text-align: justify + font-size: $fs-12 + line-height: 1.2 + display: -webkit-box + -webkit-box-orient: vertical + overflow: hidden + -webkit-line-clamp: 3 + + .search-keyword + border-bottom: 1px dashed $color-hover + color: $color-hover + font-weight: bold + + + +.search-wrapper.noresult + .search-no-result + display: block \ No newline at end of file diff --git a/source/js/main.js b/source/js/main.js index f88f443..da3f3da 100644 --- a/source/js/main.js +++ b/source/js/main.js @@ -301,6 +301,39 @@ if (stellar.plugins.fancybox) { } } + +if (stellar.search.service) { + if (stellar.search.service == 'local_search') { + stellar.jQuery(() => { + stellar.loadScript('/js/search/local-search.js', { defer: true }).then(function () { + var $inputArea = $("input#search-input"); + var $resultArea = document.querySelector("div#search-result"); + $inputArea.focus(function() { + var path = stellar.search[stellar.search.service]?.path || '/search.xml'; + const filter = $inputArea.attr('data-filter') || ''; + searchFunc(path, filter, 'search-input', 'search-result'); + }); + $inputArea.keydown(function(e) { + if (e.which == 13) { + e.preventDefault(); + } + }); + var observer = new MutationObserver(function(mutationsList, observer) { + if (mutationsList.length == 1) { + if (mutationsList[0].addedNodes.length) { + $('.search-wrapper').removeClass('noresult'); + } else if (mutationsList[0].removedNodes.length) { + $('.search-wrapper').addClass('noresult'); + } + } + }); + observer.observe($resultArea, { childList: true }); + }); + }) + } +} + + // heti if (stellar.plugins.heti) { stellar.loadCSS(stellar.plugins.heti.css); diff --git a/source/js/search/local-search.js b/source/js/search/local-search.js new file mode 100644 index 0000000..0a32acd --- /dev/null +++ b/source/js/search/local-search.js @@ -0,0 +1,161 @@ +// A local search script with the help of +// [hexo-generator-search](https://github.com/PaicHyperionDev/hexo-generator-search) +// Copyright (C) 2015 +// Joseph Pan +// Shuhao Mao +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +// 02110-1301 USA +// +// Modified by: +// Pieter Robberechts + +/*exported searchFunc*/ +var searchFunc = function(path, filter, searchId, contentId) { + + function stripHtml(html) { + html = html.replace(//gi, ""); + html = html.replace(//gi, ""); + html = html.replace(//gi, ""); + html = html.replace(/<\/div>/ig, "\n"); + html = html.replace(/<\/li>/ig, "\n"); + html = html.replace(/
  • /ig, " * "); + html = html.replace(/<\/ul>/ig, "\n"); + html = html.replace(/<\/p>/ig, "\n"); + html = html.replace(//gi, "\n"); + html = html.replace(/<[^>]+>/ig, ""); + return html; + } + + function getAllCombinations(keywords) { + var i, j, result = []; + + for (i = 0; i < keywords.length; i++) { + for (j = i + 1; j < keywords.length + 1; j++) { + result.push(keywords.slice(i, j).join(" ")); + } + } + return result; + } + + $.ajax({ + url: path, + dataType: "xml", + success: function(xmlResponse) { + // get the contents from search data + var datas = $("entry", xmlResponse).map(function() { + return { + title: $("title", this).text(), + content: $("content", this).text(), + url: $("link", this).attr("href") + }; + }).get(); + + var $input = document.getElementById(searchId); + if (!$input) { return; } + var $resultContent = document.getElementById(contentId); + + $input.addEventListener("input", function(){ + var resultList = []; + var keywords = getAllCombinations(this.value.trim().toLowerCase().split(" ")) + .sort(function(a,b) { return b.split(" ").length - a.split(" ").length; }); + $resultContent.innerHTML = ""; + if (this.value.trim().length <= 0) { + return; + } + // perform local searching + datas.forEach(function(data) { + var matches = 0; + if (!data.title || data.title.trim() === "") { + return; + } + if (filter && !data.url.includes(filter)) { + return; + } + var dataTitle = data.title.trim().toLowerCase(); + var dataTitleLowerCase = dataTitle.toLowerCase(); + var dataContent = stripHtml(data.content.trim()); + var dataContentLowerCase = dataContent.toLowerCase(); + var dataUrl = data.url; + var indexTitle = -1; + var indexContent = -1; + var firstOccur = -1; + // only match artiles with not empty contents + if (dataContent !== "") { + keywords.forEach(function(keyword) { + indexTitle = dataTitleLowerCase.indexOf(keyword); + indexContent = dataContentLowerCase.indexOf(keyword); + + if( indexTitle >= 0 || indexContent >= 0 ){ + matches += 1; + if (indexContent < 0) { + indexContent = 0; + } + if (firstOccur < 0) { + firstOccur = indexContent; + } + } + }); + } + // show search results + if (matches > 0) { + var searchResult = {}; + searchResult.rank = matches; + searchResult.str = "
  • "+ dataTitle +""; + if (firstOccur >= 0) { + // cut out 100 characters + var start = firstOccur - 20; + var end = firstOccur + 80; + + if(start < 0){ + start = 0; + } + + if(start == 0){ + end = 100; + } + + if(end > dataContent.length){ + end = dataContent.length; + } + + var matchContent = dataContent.substring(start, end); + + // highlight all keywords + var regS = new RegExp(keywords.join("|"), "gi"); + matchContent = matchContent.replace(regS, function(keyword) { + return ""+keyword+""; + }); + + searchResult.str += "

    " + matchContent +"...

    "; + } + searchResult.str += "
  • "; + resultList.push(searchResult); + } + }); + if (resultList.length) { + resultList.sort(function(a, b) { + return b.rank - a.rank; + }); + var result ="
      "; + for (var i = 0; i < resultList.length; i++) { + result += resultList[i].str; + } + result += "
    "; + $resultContent.innerHTML = result; + } + }); + } + }); +};