From ce0ce5be25d37af394f73a8efbbb2d9e182d353e Mon Sep 17 00:00:00 2001 From: Mivinci <1366723936@qq.com> Date: Fri, 12 May 2023 20:30:35 +0800 Subject: [PATCH] add search support using fusejs --- README.md | 23 ++++++++-------- assets/css/main.scss | 10 +++++++ assets/css/theme.scss | 2 ++ assets/js/min/fuse.basic.min.js | 9 +++++++ assets/js/search.js | 24 +++++++++++++++++ assets/js/selectable.js | 6 +++-- assets/js/theme.js | 10 ++++--- exampleSite/config.yaml | 42 ++++++++++++++++++++++++++--- exampleSite/content/search.md | 4 +++ exampleSite/content/search.zh-cn.md | 4 +++ layouts/_default/search.html | 16 +++++++++++ layouts/index.html | 2 ++ layouts/index.json | 7 +++++ layouts/partials/head.html | 23 ++++++++++++---- 14 files changed, 156 insertions(+), 26 deletions(-) create mode 100644 assets/js/min/fuse.basic.min.js create mode 100644 assets/js/search.js create mode 100644 exampleSite/content/search.md create mode 100644 exampleSite/content/search.zh-cn.md create mode 100644 layouts/_default/search.html create mode 100644 layouts/index.json diff --git a/README.md b/README.md index 39c8743..02c3e28 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,24 @@ # Minima -A clean and minimal Hugo theme porting from [Hexo Minima](https://github.com/adisaktijrs/hexo-theme-minima). Check out the [example site](https://mivinci.github.io/hugo-theme-minima). +Minima is a clean and minimal Hugo theme originally ported from [Hexo Minima](https://github.com/adisaktijrs/hexo-theme-minima). Check out the [example site](https://mivinci.github.io/hugo-theme-minima). ![screenshot](./images/tn.png) -> Note that the main branch is in development stage, UI or configuration may vary. +> Note that the main branch is in development phase, UI or configuration may vary. ## Features -- [x] Dark mode -- [x] Multilingual mode -- [x] Code highlighting - VSCode dark+ -- [x] Math - KaTeX -- [x] Flowcharts - Mermaid -- [x] Comment - Disqus, Utterances, Giscus -- [x] Google analytics -- [x] External link -- [x] RSS +- [x] ๐ŸŒ— Dark mode +- [x] ๐Ÿ“š Multilingual mode +- [x] ๐Ÿณ๏ธโ€๐ŸŒˆ Code highlighting - VSCode dark+ +- [x] ๐Ÿ”ข Math - KaTeX +- [x] ๐Ÿ’น Flowcharts - Mermaid +- [x] ๐Ÿง‘โ€๐Ÿ’ป Comment - Disqus, Utterances, Giscus +- [x] ๐Ÿ”Ž Search - FuseJS +- [x] ใ€ฝ๏ธ Google analytics +- [x] ๐Ÿ”— External link +- [x] โœ‰๏ธ RSS ## Usage diff --git a/assets/css/main.scss b/assets/css/main.scss index 46ce128..f17d305 100644 --- a/assets/css/main.scss +++ b/assets/css/main.scss @@ -44,6 +44,16 @@ main p a:hover { text-decoration: underline; } +main .search > input { + width: 100%; + padding: .5em; + font-size: large; + border: 2px solid var(--grid); + border-radius: 2px; + background-color: transparent; + outline: none; +} + @keyframes showup { from { opacity: 0; diff --git a/assets/css/theme.scss b/assets/css/theme.scss index 40f4411..9cc4561 100644 --- a/assets/css/theme.scss +++ b/assets/css/theme.scss @@ -46,6 +46,7 @@ --prime: #3170a7; --back: #e6dece; --text: #434343; + --grid: #555; --code-back: #dbd3c1be; --code-text: #24292f; @@ -58,6 +59,7 @@ --prime: #3170a7; --back: #ccc; --text: #434343; + --grid: #555; --code-back: #c1c1c1be; --code-text: #24292f; diff --git a/assets/js/min/fuse.basic.min.js b/assets/js/min/fuse.basic.min.js new file mode 100644 index 0000000..89477c6 --- /dev/null +++ b/assets/js/min/fuse.basic.min.js @@ -0,0 +1,9 @@ +/** + * Fuse.js v6.6.2 - Lightweight fuzzy-search (http://fusejs.io) + * + * Copyright (c) 2022 Kiro Risk (http://kiro.me) + * All Rights Reserved. Apache Software License 2.0 + * + * http://www.apache.org/licenses/LICENSE-2.0 + */ +var e,t;e=this,t=function(){"use strict";function e(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function t(t){for(var n=1;ne.length)&&(t=e.length);for(var n=0,r=new Array(t);n0&&void 0!==arguments[0]?arguments[0]:1,t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:3,n=new Map,r=Math.pow(10,t);return{get:function(t){var i=t.match(_).length;if(n.has(i))return n.get(i);var o=1/Math.pow(i,.5*e),a=parseFloat(Math.round(o*r)/r);return n.set(i,a),a},clear:function(){n.clear()}}}var O=function(){function e(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=t.getFn,i=void 0===n?L.getFn:n,o=t.fieldNormWeight,a=void 0===o?L.fieldNormWeight:o;r(this,e),this.norm=S(a,3),this.getFn=i,this.isCreated=!1,this.setIndexRecords()}return o(e,[{key:"setSources",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];this.docs=e}},{key:"setIndexRecords",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];this.records=e}},{key:"setKeys",value:function(){var e=this,t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];this.keys=t,this._keysMap={},t.forEach((function(t,n){e._keysMap[t.id]=n}))}},{key:"create",value:function(){var e=this;!this.isCreated&&this.docs.length&&(this.isCreated=!0,u(this.docs[0])?this.docs.forEach((function(t,n){e._addString(t,n)})):this.docs.forEach((function(t,n){e._addObject(t,n)})),this.norm.clear())}},{key:"add",value:function(e){var t=this.size();u(e)?this._addString(e,t):this._addObject(e,t)}},{key:"removeAt",value:function(e){this.records.splice(e,1);for(var t=e,n=this.size();t2&&void 0!==arguments[2]?arguments[2]:{},r=n.getFn,i=void 0===r?L.getFn:r,o=n.fieldNormWeight,a=void 0===o?L.fieldNormWeight:o,c=new O({getFn:i,fieldNormWeight:a});return c.setKeys(e.map(k)),c.setSources(t),c.create(),c}function j(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=t.errors,r=void 0===n?0:n,i=t.currentLocation,o=void 0===i?0:i,a=t.expectedLocation,c=void 0===a?0:a,s=t.distance,h=void 0===s?L.distance:s,u=t.ignoreLocation,l=void 0===u?L.ignoreLocation:u,d=r/e.length;if(l)return d;var f=Math.abs(c-o);return h?d+f/h:f?1:d}function E(){for(var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[],t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:L.minMatchCharLength,n=[],r=-1,i=-1,o=0,a=e.length;o=t&&n.push([r,i]),r=-1)}return e[o-1]&&o-r>=t&&n.push([r,o-1]),n}var I=32;function F(e){for(var t={},n=0,r=e.length;n1&&void 0!==arguments[1]?arguments[1]:{},o=i.location,a=void 0===o?L.location:o,c=i.threshold,s=void 0===c?L.threshold:c,h=i.distance,u=void 0===h?L.distance:h,l=i.includeMatches,d=void 0===l?L.includeMatches:l,f=i.findAllMatches,v=void 0===f?L.findAllMatches:f,g=i.minMatchCharLength,y=void 0===g?L.minMatchCharLength:g,p=i.isCaseSensitive,m=void 0===p?L.isCaseSensitive:p,b=i.ignoreLocation,k=void 0===b?L.ignoreLocation:b;if(r(this,e),this.options={location:a,threshold:s,distance:u,includeMatches:d,findAllMatches:v,minMatchCharLength:y,isCaseSensitive:m,ignoreLocation:k},this.pattern=m?t:t.toLowerCase(),this.chunks=[],this.pattern.length){var M=function(e,t){n.chunks.push({pattern:e,alphabet:F(e),startIndex:t})},w=this.pattern.length;if(w>I){for(var x=0,_=w%I,S=w-_;x3&&void 0!==arguments[3]?arguments[3]:{},i=r.location,o=void 0===i?L.location:i,a=r.distance,c=void 0===a?L.distance:a,s=r.threshold,h=void 0===s?L.threshold:s,u=r.findAllMatches,l=void 0===u?L.findAllMatches:u,d=r.minMatchCharLength,f=void 0===d?L.minMatchCharLength:d,v=r.includeMatches,g=void 0===v?L.includeMatches:v,y=r.ignoreLocation,m=void 0===y?L.ignoreLocation:y;if(t.length>I)throw new Error(p(I));for(var b,k=t.length,M=e.length,w=Math.max(0,Math.min(o,M)),x=h,_=w,S=f>1||g,O=S?Array(M):[];(b=e.indexOf(t,_))>-1;){var A=j(t,{currentLocation:b,expectedLocation:w,distance:c,ignoreLocation:m});if(x=Math.min(A,x),_=b+k,S)for(var F=0;F=T;R-=1){var U=R-1,B=n[e.charAt(U)];if(S&&(O[U]=+!!B),J[R]=(J[R+1]<<1|1)&B,$&&(J[R]|=(C[R+1]|C[R])<<1|1|C[R+1]),J[R]&W&&(N=j(t,{errors:$,currentLocation:U,expectedLocation:w,distance:c,ignoreLocation:m}))<=x){if(x=N,(_=U)<=w)break;T=Math.max(1,2*w-_)}}if(j(t,{errors:$+1,currentLocation:w,expectedLocation:w,distance:c,ignoreLocation:m})>x)break;C=J}var V={isMatch:_>=0,score:Math.max(.001,N)};if(S){var q=E(O,f);q.length?g&&(V.indices=q):V.isMatch=!1}return V}(e,n,i,{location:a+o,distance:s,threshold:h,findAllMatches:u,minMatchCharLength:l,includeMatches:r,ignoreLocation:d}),m=y.isMatch,b=y.score,k=y.indices;m&&(g=!0),v+=b,m&&k&&(f=[].concat(c(f),c(k)))}));var y={isMatch:g,score:g?v/this.chunks.length:1};return g&&r&&(y.indices=f),y}}]),e}(),N=[];function P(e,t){for(var n=0,r=N.length;n-1&&(n.refIndex=e.idx),t.matches.push(n)}}))}function D(e,t){t.score=e.score}function K(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},r=n.includeMatches,i=void 0===r?L.includeMatches:r,o=n.includeScore,a=void 0===o?L.includeScore:o,c=[];return i&&c.push($),a&&c.push(D),e.map((function(e){var n=e.idx,r={item:t[n],refIndex:n};return c.length&&c.forEach((function(t){t(e,r)})),r}))}var T=function(){function e(n){var i=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},o=arguments.length>2?arguments[2]:void 0;if(r(this,e),this.options=t(t({},L),i),this.options.useExtendedSearch)throw new Error(y);this._keyStore=new b(this.options.keys),this.setCollection(n,o)}return o(e,[{key:"setCollection",value:function(e,t){if(this._docs=e,t&&!(t instanceof O))throw new Error("Incorrect 'index' type");this._myIndex=t||A(this.options.keys,this._docs,{getFn:this.options.getFn,fieldNormWeight:this.options.fieldNormWeight})}},{key:"add",value:function(e){f(e)&&(this._docs.push(e),this._myIndex.add(e))}},{key:"remove",value:function(){for(var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:function(){return!1},t=[],n=0,r=this._docs.length;n1&&void 0!==arguments[1]?arguments[1]:{},n=t.limit,r=void 0===n?-1:n,i=this.options,o=i.includeMatches,a=i.includeScore,c=i.shouldSort,s=i.sortFn,h=i.ignoreFieldNorm,d=u(e)?u(this._docs[0])?this._searchStringList(e):this._searchObjectList(e):this._searchLogical(e);return W(d,{ignoreFieldNorm:h}),c&&d.sort(s),l(r)&&r>-1&&(d=d.slice(0,r)),K(d,this._docs,{includeMatches:o,includeScore:a})}},{key:"_searchStringList",value:function(e){var t=P(e,this.options),n=this._myIndex.records,r=[];return n.forEach((function(e){var n=e.v,i=e.i,o=e.n;if(f(n)){var a=t.searchIn(n),c=a.isMatch,s=a.score,h=a.indices;c&&r.push({item:n,idx:i,matches:[{score:s,value:n,norm:o,indices:h}]})}})),r}},{key:"_searchLogical",value:function(e){throw new Error("Logical search is not available")}},{key:"_searchObjectList",value:function(e){var t=this,n=P(e,this.options),r=this._myIndex,i=r.keys,o=r.records,a=[];return o.forEach((function(e){var r=e.$,o=e.i;if(f(r)){var s=[];i.forEach((function(e,i){s.push.apply(s,c(t._findMatches({key:e,value:r[i],searcher:n})))})),s.length&&a.push({idx:o,item:r,matches:s})}})),a}},{key:"_findMatches",value:function(e){var t=e.key,n=e.value,r=e.searcher;if(!f(n))return[];var i=[];if(h(n))n.forEach((function(e){var n=e.v,o=e.i,a=e.n;if(f(n)){var c=r.searchIn(n),s=c.isMatch,h=c.score,u=c.indices;s&&i.push({score:h,key:t,value:n,idx:o,norm:a,indices:u})}}));else{var o=n.v,a=n.n,c=r.searchIn(o),s=c.isMatch,u=c.score,l=c.indices;s&&i.push({score:u,key:t,value:o,norm:a,indices:l})}return i}}]),e}();return T.version="6.6.2",T.createIndex=A,T.parseIndex=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=t.getFn,r=void 0===n?L.getFn:n,i=t.fieldNormWeight,o=void 0===i?L.fieldNormWeight:i,a=e.keys,c=e.records,s=new O({getFn:r,fieldNormWeight:o});return s.setKeys(a),s.setIndexRecords(c),s},T.config=L,T},"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).Fuse=t(); \ No newline at end of file diff --git a/assets/js/search.js b/assets/js/search.js new file mode 100644 index 0000000..ba1455e --- /dev/null +++ b/assets/js/search.js @@ -0,0 +1,24 @@ +import * as params from '@params'; + +const search_input = document.querySelector("#search-input"); +const search_result = document.querySelector("#search-result"); + +let fuse; + +window.onload = async function() { + const data = await fetch("../index.json").then(res => res.json()); + const opts = params.search.fuse; + fuse = new Fuse(data, opts); +} + +search_input.addEventListener("input", function () { + if (!fuse) return; + const results = fuse.search(this.value.trim()); + let html = ''; + if (results.length > 0) { + for (const v of results) { + html += `
  • ${v.item.title}
  • `; + } + } + search_result.innerHTML = html; +}) diff --git a/assets/js/selectable.js b/assets/js/selectable.js index a801f89..74e1dc8 100644 --- a/assets/js/selectable.js +++ b/assets/js/selectable.js @@ -1,6 +1,8 @@ +import * as params from '@params'; + export function setup_selectable () { - const selectable = '{{ .Site.Params.selectable }}' - if (selectable === 'false') { + const selectable = params.selectable + if (!selectable) { document.documentElement.style = 'user-select:none' } } \ No newline at end of file diff --git a/assets/js/theme.js b/assets/js/theme.js index 61d5af8..f35d309 100644 --- a/assets/js/theme.js +++ b/assets/js/theme.js @@ -1,7 +1,9 @@ -const comment = '{{ .Site.Params.comment.provider }}' -const default_theme_config = '{{ .Site.Params.defaultTheme }}' -const icon_light = '{{ index .Site.Params.switch 1 }}' -const icon_dark = '{{ index .Site.Params.switch 0 }}' +import * as params from '@params'; + +const comment = params.comment.provider +const default_theme_config = params.defaulttheme +const icon_light = params.switch[1] +const icon_dark = params.switch[0] const THEME_LIGHT = default_theme_config === 'system' ? 'light' : default_theme_config const THEME_DARK = 'dark' diff --git a/exampleSite/config.yaml b/exampleSite/config.yaml index d78930a..430b3e7 100644 --- a/exampleSite/config.yaml +++ b/exampleSite/config.yaml @@ -9,7 +9,7 @@ paginate: 12 theme: hugo-theme-minima # defaultContentLanguage specifies the default language to use. defaultContentLanguage: en -# language.x setup +# language.xxx setup languages: en: languageName: EN # will be displayed in the navbar. @@ -97,15 +97,42 @@ params: reactions: true metadata: false + # search plugin + search: + enable: true + provider: fuse + title: Search + placeholder: Enter keywords + # check out https://fusejs.io + fuse: + keys: + - title + - permalink + - summary + - content + distance: 100 + location: 0 + threshold: 0.6 + ignoreLocation: false + isCaseSensitive: false + includeScore: false + includeMatches: false + minMatchCharLength: 1 + shouldSort: true + findAllMatches: false + # menu.main is an array containing what is used as the navigator. menu: main: - identifier: tags - name: "Tags" - weight: 2 + name: Tags + weight: 1 - identifier: series - name: "Series" + name: Series + weight: 2 + - identifier: search + name: ๐Ÿ” weight: 3 # taxonomies defines ways to classify yout posts. Below are some presets that @@ -115,6 +142,13 @@ taxonomies: tag: tags series: series +# outputs tells Hugo the kind of files to be rendered. +outputs: + home: + - HTML + - RSS + - JSON + # markup.highlight has two keys set to make sure that the syntax highlighting # in your posts are rendered correctly, so DO NOT edit them. markup: diff --git a/exampleSite/content/search.md b/exampleSite/content/search.md new file mode 100644 index 0000000..b6d35b5 --- /dev/null +++ b/exampleSite/content/search.md @@ -0,0 +1,4 @@ +--- +title: Search +layout: search +--- \ No newline at end of file diff --git a/exampleSite/content/search.zh-cn.md b/exampleSite/content/search.zh-cn.md new file mode 100644 index 0000000..f23353f --- /dev/null +++ b/exampleSite/content/search.zh-cn.md @@ -0,0 +1,4 @@ +--- +title: ๆœ็ดข +layout: search +--- \ No newline at end of file diff --git a/layouts/_default/search.html b/layouts/_default/search.html new file mode 100644 index 0000000..9217e8f --- /dev/null +++ b/layouts/_default/search.html @@ -0,0 +1,16 @@ +{{ define "main" }} +
    + {{- $title := .Site.Params.search.title | default .Title }} + {{- $placeholder := .Site.Params.search.placeholder | default .Title }} +

    {{ $title }}

    +

    Powered by fuse.js.

    + +
    +{{ end }} \ No newline at end of file diff --git a/layouts/index.html b/layouts/index.html index e51721c..bf01da8 100644 --- a/layouts/index.html +++ b/layouts/index.html @@ -11,8 +11,10 @@
    {{ $paginator := .Paginate (where .Site.RegularPages "Kind" "page") }} {{ range $paginator.Pages }} + {{ if ne .Page.Layout "search" }} {{ partial "item.html" . }} {{ end }} + {{ end }} {{ partial "paginator.html" . }}
    {{ if .Site.Params.friends.feeds }} diff --git a/layouts/index.json b/layouts/index.json new file mode 100644 index 0000000..5847bae --- /dev/null +++ b/layouts/index.json @@ -0,0 +1,7 @@ +{{- $.Scratch.Add "index" slice -}} +{{- range site.RegularPages -}} +{{- if ne .Layout "search" -}} + {{- $.Scratch.Add "index" (dict "title" .Title "permalink" .Permalink "summary" .Summary "content" .Plain) -}} +{{- end -}} +{{- end -}} +{{- $.Scratch.Get "index" | jsonify -}} \ No newline at end of file diff --git a/layouts/partials/head.html b/layouts/partials/head.html index 8ef7c53..c84a497 100644 --- a/layouts/partials/head.html +++ b/layouts/partials/head.html @@ -1,9 +1,11 @@ + {{ if hugo.IsProduction }} {{ template "_internal/opengraph.html" . }} {{ template "_internal/twitter_cards.html" . }} {{ template "_internal/google_analytics.html" . }} + {{ end }} @@ -13,14 +15,25 @@ {{ .Site.Title }} - {{ .Title }} {{ end }} + {{ $favicon := "favicon.ico" }} + {{ $options := (dict "targetPath" "minima.css" "outputStyle" "compressed" "enableSourceMap" true) }} - {{ $style := resources.Get "css/main.scss" | resources.ExecuteAsTemplate "main.scss" . | resources.ToCSS $options }} - - {{ $options = (dict "targetPath" "minima.js" "minify" true) }} - {{ $js := resources.Get "js/main.js" | js.Build $options | resources.ExecuteAsTemplate "minima.js" . }} - + {{ $style := resources.Get "css/main.scss" | resources.ExecuteAsTemplate "main.scss" . | resources.ToCSS $options | fingerprint }} + + + {{ $options = (dict "targetPath" "minima.js" "minify" true "params" site.Params) }} + {{ $script := resources.Get "js/main.js" | js.Build $options | fingerprint }} + + {{ if and .Site.Params.search.enable (eq .Layout "search") }} + {{ $options = (dict "minify" true "params" site.Params) }} + {{ $search := resources.Get "js/search.js" | js.Build $options}} + {{ $fusejs := resources.Get "js/min/fuse.basic.min.js" }} + {{ $script := (slice $fusejs $search) | resources.Concat "assets/js/search.js" | fingerprint }} + + {{ end }} + {{ if .IsTranslated }} {{ range .Translations }}