diff --git a/config.yaml b/config.yaml index e469c20..9f9f92b 100644 --- a/config.yaml +++ b/config.yaml @@ -102,6 +102,14 @@ privacy: youtube: privacyEnhanced: true +# The search index, used by the search page (static/js/search.js) +outputFormats: + searchindex: + mediaType: application/json + baseName: index + isPlainText: true + notAlternative: true + taxonomies: opensource: "opensource" topic: "topics" diff --git a/content/english/search/index.md b/content/english/search/index.md index 5410598..33f6113 100644 --- a/content/english/search/index.md +++ b/content/english/search/index.md @@ -2,4 +2,8 @@ title: "Search" type: search layout: search +# The search index is generated as a separate JSON file (see config.yaml) +outputs: +- html +- searchindex --- diff --git a/content/french/search/index.md b/content/french/search/index.md index 190460d..d5c1cc0 100644 --- a/content/french/search/index.md +++ b/content/french/search/index.md @@ -2,4 +2,8 @@ title: "Recherche" type: search layout: search +# The search index is generated as a separate JSON file (see config.yaml) +outputs: +- html +- searchindex --- diff --git a/layouts/_partials/head.html b/layouts/_partials/head.html index 0e56a8e..f2935f1 100644 --- a/layouts/_partials/head.html +++ b/layouts/_partials/head.html @@ -59,15 +59,6 @@ {{ partial "opengraph.html" . }} {{ partial "twitter_cards.html" . }} - -{{ if eq .Type "search" }} - - -{{ end }} - {{- if .Site.Params.enablePlausible -}} {{- end -}} diff --git a/layouts/search.html b/layouts/search.html index e1e1855..ba7b43b 100644 --- a/layouts/search.html +++ b/layouts/search.html @@ -19,40 +19,13 @@ {{- end }} {{- /* - -- The search data are embedded in a JSON data block, used by /js/search.js + -- The search index is a separate JSON file (search.searchindex.json), + -- loaded by /js/search.js. Only the URL of the index and the translated + -- strings are embedded in the page, in a JSON data block (no inline + -- script, see the Content Security Policy). */ -}} - {{- /* Taxonomies */ -}} - {{- $.Store.Set "taxonomies" dict -}} - {{- range $name, $taxonomy := .Site.Taxonomies -}} - {{- $.Store.Set "taxonomy" dict -}} - {{- range (index $.Site.Taxonomies $name) -}} - {{- $.Store.SetInMap "taxonomy" .Page.Title (dict "href" .Page.RelPermalink "title" .Page.Title) -}} - {{- end -}} - {{- $.Store.SetInMap "taxonomies" $name ($.Store.Get "taxonomy") -}} - {{- end }} - - {{- /* Search index */ -}} - {{- $.Store.Set "index" slice -}} - {{- range .Site.RegularPages -}} - {{- $.Store.Set "reading_time" nil -}} - {{- if eq .Type "blog" -}} - {{- $minutes := .ReadingTime -}} - {{- if gt $minutes 0 -}} - {{- $.Store.Set "reading_time" (i18n "minutes" $minutes) -}} - {{- end -}} - {{- end -}} - - {{- $.Store.Set "date" nil -}} - {{- if ne .Type "page" -}} - {{- $.Store.Set "date" (.Date.Format (i18n "date_format")) -}} - {{- end -}} - - {{ .Store.Set "en_article_in_fr_site" false }} - {{- partial "lang-detection.html" . -}} - {{- $.Store.Add "index" (dict "title" .Title "opensource" .Params.opensource "topics" .Params.topics "permalink" .Permalink "hasEnFlag" (.Store.Get "en_article_in_fr_site") "type" .Type "type_label" (i18n (print "type_" .Type)) "date" ($.Store.Get "date") "reading_time" ($.Store.Get "reading_time") "summary" (.Summary | plainify | htmlUnescape)) -}} - {{- end }} - {{- $data := dict "taxonomies" ($.Store.Get "taxonomies") "index" ($.Store.Get "index") "i18n" (dict "published_on" (i18n "published_on") "continue_reading" (i18n "continue_reading")) }} - - + {{- $config := dict "indexUrl" (.OutputFormats.Get "searchindex").RelPermalink "i18n" (dict "published_on" (i18n "published_on") "continue_reading" (i18n "continue_reading")) }} + + {{ end }} diff --git a/layouts/search.searchindex.json b/layouts/search.searchindex.json new file mode 100644 index 0000000..b4d6717 --- /dev/null +++ b/layouts/search.searchindex.json @@ -0,0 +1,34 @@ +{{- /* + -- The search index of the "search" page, loaded by /js/search.js. + */ -}} +{{- /* Taxonomies */ -}} +{{- $.Store.Set "taxonomies" dict -}} +{{- range $name, $taxonomy := .Site.Taxonomies -}} +{{- $.Store.Set "taxonomy" dict -}} +{{- range (index $.Site.Taxonomies $name) -}} +{{- $.Store.SetInMap "taxonomy" .Page.Title (dict "href" .Page.RelPermalink "title" .Page.Title) -}} +{{- end -}} +{{- $.Store.SetInMap "taxonomies" $name ($.Store.Get "taxonomy") -}} +{{- end -}} + +{{- /* Search index */ -}} +{{- $.Store.Set "index" slice -}} +{{- range .Site.RegularPages -}} + {{- $.Store.Set "reading_time" nil -}} + {{- if eq .Type "blog" -}} + {{- $minutes := .ReadingTime -}} + {{- if gt $minutes 0 -}} + {{- $.Store.Set "reading_time" (i18n "minutes" $minutes) -}} + {{- end -}} + {{- end -}} + + {{- $.Store.Set "date" nil -}} + {{- if ne .Type "page" -}} + {{- $.Store.Set "date" (.Date.Format (i18n "date_format")) -}} + {{- end -}} + + {{- .Store.Set "en_article_in_fr_site" false -}} + {{- partial "lang-detection.html" . -}} + {{- $.Store.Add "index" (dict "title" .Title "opensource" .Params.opensource "topics" .Params.topics "permalink" .Permalink "hasEnFlag" (.Store.Get "en_article_in_fr_site") "type" .Type "type_label" (i18n (print "type_" .Type)) "date" ($.Store.Get "date") "reading_time" ($.Store.Get "reading_time") "summary" (.Summary | plainify | htmlUnescape)) -}} +{{- end -}} +{{- dict "taxonomies" ($.Store.Get "taxonomies") "index" ($.Store.Get "index") | jsonify -}} diff --git a/static/js/fuse.basic.min.js b/static/js/fuse.basic.min.js new file mode 100644 index 0000000..181c26b --- /dev/null +++ b/static/js/fuse.basic.min.js @@ -0,0 +1,9 @@ +/** + * Fuse.js v7.5.0 - Lightweight fuzzy-search (http://fusejs.io) + * + * Copyright (c) 2026 Kiro Risk (http://kiro.me) + * All Rights Reserved. Apache Software License 2.0 + * + * http://www.apache.org/licenses/LICENSE-2.0 + */ +function e(e){return Array.isArray?Array.isArray(e):u(e)===`[object Array]`}function t(e){if(typeof e==`string`)return e;if(typeof e==`bigint`)return e.toString();let t=e+``;return t==`0`&&1/e==-1/0?`-0`:t}function n(e){return e==null?``:t(e)}function r(e){return typeof e==`string`}function i(e){return typeof e==`number`}function a(e){return e===!0||e===!1||s(e)&&u(e)==`[object Boolean]`}function o(e){return typeof e==`object`}function s(e){return o(e)&&e!==null}function c(e){return e!=null}function l(e){return!e.trim().length}function u(e){return e==null?e===void 0?`[object Undefined]`:`[object Null]`:Object.prototype.toString.call(e)}const d=`Invalid doc index: must be a non-negative integer within the bounds of the docs array`,f=e=>`Pattern length exceeds max of ${e}.`,p=e=>`Missing ${e} property in key`,m=e=>`Property 'weight' in key '${e}' must be a positive integer`,h=Object.prototype.hasOwnProperty;var g=class{constructor(e){this._keys=[],this._keyMap={};let t=0;e.forEach(e=>{let n=_(e);this._keys.push(n),this._keyMap[n.id]=n,t+=n.weight}),this._keys.forEach(e=>{e.weight/=t})}get(e){return this._keyMap[e]}keys(){return this._keys}toJSON(){return JSON.stringify(this._keys)}};function _(t){let n=null,i=null,a=null,o=1,s=null;if(r(t)||e(t))a=t,n=v(t),i=y(t);else{if(!h.call(t,`name`))throw Error(p(`name`));let e=t.name;if(a=e,h.call(t,`weight`)&&t.weight!==void 0&&(o=t.weight,o<=0))throw Error(m(y(e)));n=v(e),i=y(e),s=t.getFn??null}return{path:n,id:i,weight:o,src:a,getFn:s}}function v(t){return e(t)?t:t.split(`.`)}function y(t){return e(t)?t.join(`.`):t}function b(t,o){let s=[],l=!1,u=(t,o,d,f)=>{if(c(t))if(!o[d])s.push(f===void 0?t:{v:t,i:f});else{let p=t[o[d]];if(!c(p))return;if(d===o.length-1&&(r(p)||i(p)||a(p)||typeof p==`bigint`))s.push(f===void 0?n(p):{v:n(p),i:f});else if(e(p)){l=!0;for(let e=0,t=p.length;ee.score===t.score?e.idx=9&&e<=13||e===32||e===160}function D(e=1,t=3){let n=new Map,r=10**t;return{get(t){let i=0,a=!1;for(let e=0;e{this._keysMap[e.id]=t})}create(){if(this.isCreated||!this.docs.length)return;this.isCreated=!0;let e=this.docs.length;this.records=Array(e);let t=0;if(r(this.docs[0]))for(let n=0;ne&&--this.records[t].i}removeAll(e){let t=new Set;for(let n of e)Number.isInteger(n)&&n>=0&&t.add(n);if(t.size===0)return;this.records=this.records.filter(e=>!t.has(e.i));let n=Array.from(t).sort((e,t)=>e-t);for(let e of this.records){let t=0,r=n.length;for(;t>>1;n[i]t),records:this.records}}};function k(e,t,{getFn:n=T.getFn,fieldNormWeight:r=T.fieldNormWeight}={}){let i=new O({getFn:n,fieldNormWeight:r});return i.setKeys(e.map(_)),i.setSources(t),i.create(),i}function A(e,{getFn:t=T.getFn,fieldNormWeight:n=T.fieldNormWeight}={}){let{keys:r,records:i}=e,a=new O({getFn:t,fieldNormWeight:n});return a.setKeys(r),a.setIndexRecords(i),a}function j(e=[],t=T.minMatchCharLength){let n=[],r=-1,i=-1,a=0;for(let o=e.length;a=t&&n.push([r,i]),r=-1)}return e[a-1]&&a-r>=t&&n.push([r,a-1]),n}function M(e,t,n,{location:r=T.location,distance:i=T.distance,threshold:a=T.threshold,findAllMatches:o=T.findAllMatches,minMatchCharLength:s=T.minMatchCharLength,includeMatches:c=T.includeMatches,ignoreLocation:l=T.ignoreLocation}={}){if(t.length>32)throw Error(f(32));let u=t.length,d=e.length,p=Math.max(0,Math.min(r,d)),m=a,h=p,g=(e,t)=>{let n=e/u;if(l)return n;let r=Math.abs(p-t);return i?n+r/i:r?1:n},_=s>1||c,v=_?Array(d):[],y;for(;(y=e.indexOf(t,h))>-1;){let e=g(0,y);if(m=Math.min(e,m),h=y+u,_){let e=0;for(;e=a;--r){let i=r-1,o=n[e[i]];if(c[r]=(c[r+1]<<1|1)&o,t&&(c[r]|=(b[r+1]|b[r])<<1|1|b[r+1]),c[r]&w&&(x=g(t,i),x<=m)){if(m=x,h=i,S=t,h<=p)break;a=Math.max(1,2*p-h)}}if(g(t+1,p)>m)break;b=c}if(_&&h>=0){let t=Math.min(d-1,h+u-1+S);for(let r=h;r<=t;r+=1)n[e[r]]&&(v[r]=1)}let E={isMatch:h>=0,score:Math.max(.001,x)};if(_){let e=j(v,s);e.length?c&&(E.indices=e):E.isMatch=!1}return E}function N(e){let t={};for(let n=0,r=e.length;ne[0]-t[0]||e[1]-t[1]);let t=[e[0]];for(let n=1,r=e.length;ne.normalize(`NFD`).replace(/[\u0300-\u036F\u0483-\u0489\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u07FD\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u08D3-\u08E1\u08E3-\u0903\u093A-\u093C\u093E-\u094F\u0951-\u0957\u0962\u0963\u0981-\u0983\u09BC\u09BE-\u09C4\u09C7\u09C8\u09CB-\u09CD\u09D7\u09E2\u09E3\u09FE\u0A01-\u0A03\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A70\u0A71\u0A75\u0A81-\u0A83\u0ABC\u0ABE-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AE2\u0AE3\u0AFA-\u0AFF\u0B01-\u0B03\u0B3C\u0B3E-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B62\u0B63\u0B82\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD7\u0C00-\u0C04\u0C3E-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C81-\u0C83\u0CBC\u0CBE-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CE2\u0CE3\u0D00-\u0D03\u0D3B\u0D3C\u0D3E-\u0D44\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0D62\u0D63\u0D82\u0D83\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DF2\u0DF3\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9\u0EBB\u0EBC\u0EC8-\u0ECD\u0F18\u0F19\u0F35\u0F37\u0F39\u0F3E\u0F3F\u0F71-\u0F84\u0F86\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102B-\u103E\u1056-\u1059\u105E-\u1060\u1062-\u1064\u1067-\u106D\u1071-\u1074\u1082-\u108D\u108F\u109A-\u109D\u135D-\u135F\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17B4-\u17D3\u17DD\u180B-\u180D\u1885\u1886\u18A9\u1920-\u192B\u1930-\u193B\u1A17-\u1A1B\u1A55-\u1A5E\u1A60-\u1A7C\u1A7F\u1AB0-\u1ABE\u1B00-\u1B04\u1B34-\u1B44\u1B6B-\u1B73\u1B80-\u1B82\u1BA1-\u1BAD\u1BE6-\u1BF3\u1C24-\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE8\u1CED\u1CF2-\u1CF4\u1CF7-\u1CF9\u1DC0-\u1DF9\u1DFB-\u1DFF\u20D0-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302F\u3099\u309A\uA66F-\uA672\uA674-\uA67D\uA69E\uA69F\uA6F0\uA6F1\uA802\uA806\uA80B\uA823-\uA827\uA880\uA881\uA8B4-\uA8C5\uA8E0-\uA8F1\uA8FF\uA926-\uA92D\uA947-\uA953\uA980-\uA983\uA9B3-\uA9C0\uA9E5\uAA29-\uAA36\uAA43\uAA4C\uAA4D\uAA7B-\uAA7D\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAEB-\uAAEF\uAAF5\uAAF6\uABE3-\uABEA\uABEC\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F]/g,``).replace(I,e=>F[e]):e=>e;var R=class{constructor(e,{location:t=T.location,threshold:n=T.threshold,distance:r=T.distance,includeMatches:i=T.includeMatches,findAllMatches:a=T.findAllMatches,minMatchCharLength:o=T.minMatchCharLength,isCaseSensitive:s=T.isCaseSensitive,ignoreDiacritics:c=T.ignoreDiacritics,ignoreLocation:l=T.ignoreLocation}={}){if(this.options={location:t,threshold:n,distance:r,includeMatches:i,findAllMatches:a,minMatchCharLength:o,isCaseSensitive:s,ignoreDiacritics:c,ignoreLocation:l},e=s?e:e.toLowerCase(),e=c?L(e):e,this.pattern=e,this.chunks=[],!this.pattern.length)return;let u=(e,t)=>{this.chunks.push({pattern:e,alphabet:N(e),startIndex:t})},d=this.pattern.length;if(d>32){let e=0,t=d%32,n=d-t;for(;e{let{isMatch:m,score:h,indices:g}=M(e,t,n,{location:i+p,distance:a,threshold:o,findAllMatches:s,minMatchCharLength:c,includeMatches:r,ignoreLocation:l});m&&(f=!0),d+=h,m&&g&&u.push(...g)});let p={isMatch:f,score:f?d/this.chunks.length:1};return f&&r&&(p.indices=P(u)),p}};const z=[];function B(...e){z.push(...e)}function V(e,t){for(let n=0,r=z.length;n{let a=e?e.weight:null;n*=(i===0&&a?2**-52:i)**+((a||1)*(t?1:r))}),n}function U(e,{ignoreFieldNorm:t=T.ignoreFieldNorm}){e.forEach(e=>{e.score=H(e.matches,{ignoreFieldNorm:t})})}var W=class{constructor(e,t){this.limit=e,this.heap=[],this.comparator=t}get size(){return this.heap.length}insert(e){this.size0;){let n=e-1>>1;if(this.comparator(t[e],t[n])<=0)break;let r=t[e];t[e]=t[n],t[n]=r,e=n}}_sinkDown(e){let t=this.heap,n=t.length,r=e;do{e=r;let i=2*e+1,a=2*e+2;if(i0&&(r=i),a0&&(r=a),r!==e){let n=t[e];t[e]=t[r],t[r]=n}}while(r!==e)}};function G(e){let t=[];return e.matches.forEach(e=>{if(!c(e.indices)||!e.indices.length)return;let n={indices:e.indices,value:e.value};e.key&&(n.key=e.key.id),e.idx>-1&&(n.refIndex=e.idx),t.push(n)}),t}function K(e,t,{includeMatches:n=T.includeMatches,includeScore:r=T.includeScore}={}){return e.map(e=>{let{idx:i}=e,a={item:t[i],refIndex:i};return n&&(a.matches=G(e)),r&&(a.score=e.score),a})}const q=/[\p{L}\p{M}\p{N}_]+/gu;function J(e){return typeof e==`function`?t=>e(t):e instanceof RegExp?(e.global,t=>t.match(e)||[]):e=>e.match(q)||[]}function Y({isCaseSensitive:e=!1,ignoreDiacritics:t=!1,tokenize:n}={}){let r=J(n);return{tokenize(n){return e||(n=n.toLowerCase()),t&&(n=L(n)),r(n)}}}function X(e,t,n,r){let i=r.tokenize(t);if(!i.length)return;e.fieldCount++,e.docFieldCount.set(n,(e.docFieldCount.get(n)||0)+1);let a=new Set(i),o=e.docTermFieldHits.get(n);o||(o=new Map,e.docTermFieldHits.set(n,o));for(let t of a)o.set(t,(o.get(t)||0)+1),e.df.set(t,(e.df.get(t)||0)+1)}function Z(e,t,n,r){let{i,v:a,$:o}=t;if(a!==void 0){X(e,a,i,r);return}if(o)for(let t=0;te-t);for(let t of n)ne(e,t);let r=e=>{let t=0,r=n.length;for(;t>>1;n[i]i?r(t):t,n);e.docFieldCount=a;let o=new Map;for(let[t,n]of e.docTermFieldHits)o.set(t>i?r(t):t,n);e.docTermFieldHits=o}var $=class{constructor(e,t,n){if(this.options={...T,...t},this.options.useExtendedSearch)throw Error(`Extended search is not available`);if(this.options.useTokenSearch)throw Error(`Token search is not available`);this._keyStore=new g(this.options.keys),this._docs=e,this._myIndex=null,this._invertedIndex=null,this.setCollection(e,n),this._lastQuery=null,this._lastSearcher=null}_getSearcher(e){if(this._lastQuery===e)return this._lastSearcher;let t=V(e,this._invertedIndex?{...this.options,_invertedIndex:this._invertedIndex}:this.options);return this._lastQuery=e,this._lastSearcher=t,t}setCollection(e,t){if(this._docs=e,t&&!(t instanceof O))throw Error(`Incorrect 'index' type`);if(this._myIndex=t||k(this.options.keys,this._docs,{getFn:this.options.getFn,fieldNormWeight:this.options.fieldNormWeight}),this.options.useTokenSearch){let e=Y({isCaseSensitive:this.options.isCaseSensitive,ignoreDiacritics:this.options.ignoreDiacritics,tokenize:this.options.tokenize});this._invertedIndex=ee(this._myIndex.records,this._myIndex.keys.length,e)}this._invalidateSearcherCache()}add(e){if(!c(e))return;this._docs.push(e);let t=this._myIndex.add(e,this._docs.length-1);if(this._invertedIndex&&t){let e=Y({isCaseSensitive:this.options.isCaseSensitive,ignoreDiacritics:this.options.ignoreDiacritics,tokenize:this.options.tokenize});te(this._invertedIndex,t,this._myIndex.keys.length,e)}this._invalidateSearcherCache()}remove(e=()=>!1){let t=[],n=[];for(let r=0,i=this._docs.length;r!e.has(n)),this._myIndex.removeAll(n),this._invalidateSearcherCache()}return t}removeAt(e){if(!Number.isInteger(e)||e<0||e>=this._docs.length)throw Error(d);this._invertedIndex&&Q(this._invertedIndex,[e]);let t=this._docs.splice(e,1)[0];return this._myIndex.removeAt(e),this._invalidateSearcherCache(),t}_invalidateSearcherCache(){this._lastQuery=null,this._lastSearcher=null}getIndex(){return this._myIndex}_normalizedKeys(){return this._myIndex.keys.map(e=>this._keyStore.get(e.id)||e)}search(e,t){let{limit:n=-1}=t||{},{includeMatches:a,includeScore:o,shouldSort:s,sortFn:c,ignoreFieldNorm:l}=this.options;if(r(e)&&!e.trim()){let e=this._docs.map((e,t)=>({item:e,refIndex:t}));return i(n)&&n>-1&&(e=e.slice(0,n)),e}let u=s&&i(n)&&n>0&&r(e),d=c,f=(e,t)=>d(e,t)||e.idx-t.idx,p;if(u){let t=new W(n,f);r(this._docs[0])?this._searchStringList(e,{heap:t,ignoreFieldNorm:l}):this._searchObjectList(e,{heap:t,ignoreFieldNorm:l}),p=t.extractSorted()}else p=r(e)?r(this._docs[0])?this._searchStringList(e):this._searchObjectList(e):this._searchLogical(e),U(p,{ignoreFieldNorm:l}),s&&p.sort(r(e)?f:d),i(n)&&n>-1&&(p=p.slice(0,n));return K(p,this._docs,{includeMatches:a,includeScore:o})}_searchStringList(e,{heap:t,ignoreFieldNorm:n}={}){let r=this._getSearcher(e),i=this.options.useTokenSearch&&this.options.tokenMatch===`all`,{records:a}=this._myIndex,o=t?null:[];return a.forEach(({v:e,i:a,n:s})=>{if(!c(e))return;let l=r.searchIn(e);if(l.isMatch){let r={score:l.score,value:e,norm:s,indices:l.indices};i&&(r.matchedMask=l.matchedMask,r.matchedTerms=l.matchedTerms,r.termCount=l.termCount);let c=[r];if(!i||this._coversAllTokens(c)){let r={item:e,idx:a,matches:c};t?(r.score=H(r.matches,{ignoreFieldNorm:n}),t.insert(r)):o.push(r)}}}),o}_searchLogical(e){throw Error(`Logical search is not available`)}_searchObjectList(e,{heap:t,ignoreFieldNorm:n}={}){let r=this._getSearcher(e),i=this.options.useTokenSearch&&this.options.tokenMatch===`all`,{records:a}=this._myIndex,o=this._normalizedKeys(),s=t?null:[];return a.forEach(({$:e,i:a})=>{if(!c(e))return;let l=[],u=!1,d=!1;if(o.forEach((t,n)=>{let i=this._findMatches({key:t,value:e[n],searcher:r});i.length?(l.push(...i),i[0].hasInverse&&(d=!0)):u=!0}),!(d&&u)&&l.length&&(!i||this._coversAllTokens(l))){let r={idx:a,item:e,matches:l};t?(r.score=H(r.matches,{ignoreFieldNorm:n}),t.insert(r)):s.push(r)}}),s}_findMatches({key:t,value:n,searcher:r}){if(!c(n))return[];let i=[];if(e(n))n.forEach(({v:e,i:n,n:a})=>{if(!c(e))return;let o=r.searchIn(e);if(o.isMatch){let r={score:o.score,key:t,value:e,idx:n,norm:a,indices:o.indices,hasInverse:o.hasInverse};o.termCount!==void 0&&(r.matchedMask=o.matchedMask,r.matchedTerms=o.matchedTerms,r.termCount=o.termCount),i.push(r)}});else{let{v:e,n:a}=n,o=r.searchIn(e);if(o.isMatch){let n={score:o.score,key:t,value:e,norm:a,indices:o.indices,hasInverse:o.hasInverse};o.termCount!==void 0&&(n.matchedMask=o.matchedMask,n.matchedTerms=o.matchedTerms,n.termCount=o.termCount),i.push(n)}}return i}_coversAllTokens(e){let t=e.length?e[0].termCount:void 0;if(t===void 0)return!0;if(t<=31){let n=0;for(let t=0;tB(e))};var re=$;export{re as default}; \ No newline at end of file diff --git a/static/js/fuse.js b/static/js/fuse.js deleted file mode 100644 index dabc9fb..0000000 --- a/static/js/fuse.js +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Fuse.js v6.4.3 - Lightweight fuzzy-search (http://fusejs.io) - * - * Copyright (c) 2020 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(t){return(e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(t)}function t(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function n(e,t){for(var n=0;ne.length)&&(t=e.length);for(var n=0,r=new Array(t);n0&&void 0!==arguments[0]?arguments[0]:3,t=new Map;return{get:function(n){var r=n.match(w).length;if(t.has(r))return t.get(r);var i=parseFloat((1/Math.sqrt(r)).toFixed(e));return t.set(r,i),i},clear:function(){t.clear()}}}var _=function(){function e(){var n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},r=n.getFn,i=void 0===r?x.getFn:r;t(this,e),this.norm=L(3),this.getFn=i,this.isCreated=!1,this.setIndexRecords()}return r(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?x.getFn:r,o=new _({getFn:i});return o.setKeys(e.map(b)),o.setSources(t),o.create(),o}function O(e,t){var n=e.matches;t.matches=[],f(n)&&n.forEach((function(e){if(f(e.indices)&&e.indices.length){var n={indices:e.indices,value:e.value};e.key&&(n.key=e.key.src),e.idx>-1&&(n.refIndex=e.idx),t.matches.push(n)}}))}function A(e,t){t.score=e.score}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,c=t.expectedLocation,a=void 0===c?0:c,s=t.distance,h=void 0===s?x.distance:s,u=t.ignoreLocation,l=void 0===u?x.ignoreLocation:u,d=r/e.length;if(l)return d;var f=Math.abs(a-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]:x.minMatchCharLength,n=[],r=-1,i=-1,o=0,c=e.length;o=t&&n.push([r,i]),r=-1)}return e[o-1]&&o-r>=t&&n.push([r,o-1]),n}function I(e){for(var t={},n=0,r=e.length;n1&&void 0!==arguments[1]?arguments[1]:{},o=i.location,c=void 0===o?x.location:o,a=i.threshold,s=void 0===a?x.threshold:a,h=i.distance,u=void 0===h?x.distance:h,l=i.includeMatches,d=void 0===l?x.includeMatches:l,f=i.findAllMatches,v=void 0===f?x.findAllMatches:f,y=i.minMatchCharLength,g=void 0===y?x.minMatchCharLength:y,p=i.isCaseSensitive,m=void 0===p?x.isCaseSensitive:p,b=i.ignoreLocation,k=void 0===b?x.ignoreLocation:b;if(t(this,e),this.options={location:c,threshold:s,distance:u,includeMatches:d,findAllMatches:v,minMatchCharLength:g,isCaseSensitive:m,ignoreLocation:k},this.pattern=m?n:n.toLowerCase(),this.chunks=[],this.pattern.length){var M=function(e,t){r.chunks.push({pattern:e,alphabet:I(e),startIndex:t})},w=this.pattern.length;if(w>32){for(var L=0,_=w%32,S=w-_;L3&&void 0!==arguments[3]?arguments[3]:{},i=r.location,o=void 0===i?x.location:i,c=r.distance,a=void 0===c?x.distance:c,s=r.threshold,h=void 0===s?x.threshold:s,u=r.findAllMatches,l=void 0===u?x.findAllMatches:u,d=r.minMatchCharLength,f=void 0===d?x.minMatchCharLength:d,v=r.includeMatches,y=void 0===v?x.includeMatches:v,p=r.ignoreLocation,m=void 0===p?x.ignoreLocation:p;if(t.length>32)throw new Error(g(32));for(var b,k=t.length,M=e.length,w=Math.max(0,Math.min(o,M)),L=h,_=w,S=f>1||y,O=S?Array(M):[];(b=e.indexOf(t,_))>-1;){var A=j(t,{currentLocation:b,expectedLocation:w,distance:a,ignoreLocation:m});if(L=Math.min(A,L),_=b+k,S)for(var I=0;I=J;U-=1){var q=U-1,B=n[e.charAt(q)];if(S&&(O[q]=+!!B),T[U]=(T[U+1]<<1|1)&B,$&&(T[U]|=(F[U+1]|F[U])<<1|1|F[U+1]),T[U]&N&&(C=j(t,{errors:$,currentLocation:q,expectedLocation:w,distance:a,ignoreLocation:m}))<=L){if(L=C,(_=q)<=w)break;J=Math.max(1,2*w-_)}}var V=j(t,{errors:$+1,currentLocation:w,expectedLocation:w,distance:a,ignoreLocation:m});if(V>L)break;F=T}var G={isMatch:_>=0,score:Math.max(.001,C)};if(S){var H=E(O,f);H.length?y&&(G.indices=H):G.isMatch=!1}return G}(e,n,i,{location:c+o,distance:s,threshold:h,findAllMatches:u,minMatchCharLength:l,includeMatches:r,ignoreLocation:d}),m=p.isMatch,b=p.score,k=p.indices;m&&(y=!0),v+=b,m&&k&&(f=[].concat(a(f),a(k)))}));var p={isMatch:y,score:y?v/this.chunks.length:1};return y&&r&&(p.indices=f),p}}]),e}(),C=[];function P(e,t){for(var n=0,r=C.length;n1&&void 0!==arguments[1]?arguments[1]:{},i=arguments.length>2?arguments[2]:void 0;if(t(this,e),this.options=c({},x,{},r),this.options.useExtendedSearch)throw new Error("Extended search is not available");this._keyStore=new m(this.options.keys),this.setCollection(n,i)}return r(e,[{key:"setCollection",value:function(e,t){if(this._docs=e,t&&!(t instanceof _))throw new Error("Incorrect 'index' type");this._myIndex=t||S(this.options.keys,this._docs,{getFn:this.options.getFn})}},{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,c=i.includeScore,a=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 $(d,{ignoreFieldNorm:h}),a&&d.sort(s),l(r)&&r>-1&&(d=d.slice(0,r)),D(d,this._docs,{includeMatches:o,includeScore:c})}},{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 c=t.searchIn(n),a=c.isMatch,s=c.score,h=c.indices;a&&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,c=[];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,a(t._findMatches({key:e,value:r[i],searcher:n})))})),s.length&&c.push({idx:o,item:r,matches:s})}})),c}},{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,c=e.n;if(f(n)){var a=r.searchIn(n),s=a.isMatch,h=a.score,u=a.indices;s&&i.push({score:h,key:t,value:n,idx:o,norm:c,indices:u})}}));else{var o=n.v,c=n.n,a=r.searchIn(o),s=a.isMatch,u=a.score,l=a.indices;s&&i.push({score:u,key:t,value:o,norm:c,indices:l})}return i}}]),e}();function $(e,t){var n=t.ignoreFieldNorm,r=void 0===n?x.ignoreFieldNorm:n;e.forEach((function(e){var t=1;e.matches.forEach((function(e){var n=e.key,i=e.norm,o=e.score,c=n?n.weight:null;t*=Math.pow(0===o&&c?Number.EPSILON:o,(c||1)*(r?1:i))})),e.score=t}))}function D(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},r=n.includeMatches,i=void 0===r?x.includeMatches:r,o=n.includeScore,c=void 0===o?x.includeScore:o,a=[];return i&&a.push(O),c&&a.push(A),e.map((function(e){var n=e.idx,r={item:t[n],refIndex:n};return a.length&&a.forEach((function(t){t(e,r)})),r}))}return N.version="6.4.3",N.createIndex=S,N.parseIndex=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=t.getFn,r=void 0===n?x.getFn:n,i=e.keys,o=e.records,c=new _({getFn:r});return c.setKeys(i),c.setIndexRecords(o),c},N.config=x,N},"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self).Fuse=t(); \ No newline at end of file diff --git a/static/js/search.js b/static/js/search.js index 3f706cc..3e2ad5b 100644 --- a/static/js/search.js +++ b/static/js/search.js @@ -1,13 +1,17 @@ // -// Search engine of the "search" page. +// Search engine of the "search" page (ES module). // -// The search index, the taxonomies and the translated strings are generated -// by Hugo in a JSON data block (see layouts/search.html), so that no inline +// The search index and the taxonomies are generated by Hugo as a separate +// JSON file (see layouts/search.searchindex.json), loaded when the page is +// displayed and cached by the browser. The URL of the index and the translated +// strings are embedded in the page in a JSON data block, so that no inline // script is needed (Content Security Policy). // -var searchData = JSON.parse(document.getElementById('search-data').textContent); -var taxonomies = searchData.taxonomies; -var searchDb = searchData.index; +import Fuse from './fuse.basic.min.js'; + +var searchConfig = JSON.parse(document.getElementById('search-config').textContent); +var taxonomies = {}; +var fuse = null; var searchResults = document.getElementById('searchResults'); // targets the
above var searchInput = document.getElementById('searchInput'); // input box for search @@ -50,7 +54,7 @@ function printSearchResult(result) { if (result.item.type != "page") { var div = tag("div", { 'class': 'post-metadata'}); var span_date = tag("span", { 'class': 'publication-date' }); - span_date.textContent = searchData.i18n.published_on + " " + result.item.date; + span_date.textContent = searchConfig.i18n.published_on + " " + result.item.date; div.appendChild(span_date); if (result.item.topics != null) { result.item.topics.forEach(function (v) { @@ -105,7 +109,7 @@ function printSearchResult(result) { var p = tag("p", {}); p.textContent = result.item.summary; var a = tag("a", { href: result.item.permalink, 'class': 'continue-reading' }); - a.textContent = searchData.i18n.continue_reading; + a.textContent = searchConfig.i18n.continue_reading; p.appendChild(a); section.appendChild(p); @@ -121,28 +125,40 @@ function clearSearchResults() { } // -// load the search index +// Fuse.js options; check fuse.js website for details // -function fuseInit(searchDb) { - var options = { // fuse.js options; check fuse.js website for details - shouldSort: true, - ignoreLocation: true, - threshold: 0.4, - minMatchCharLength: 3, - keys: [ - 'title', - 'permalink', - 'summary', - 'topics', - 'opensource' - ] - }; - fuse = new Fuse(searchDb, options); // build the index from the json file -} +var fuseOptions = { + shouldSort: true, + ignoreLocation: true, + threshold: 0.4, + minMatchCharLength: 3, + keys: [ + 'title', + 'permalink', + 'summary', + 'topics', + 'opensource' + ] +}; + +// +// Load the search index and initialize Fuse.js +// +var searchReady = fetch(searchConfig.indexUrl) + .then(function (response) { + if (!response.ok) { + throw new Error("Cannot load the search index: HTTP " + response.status); + } + return response.json(); + }) + .then(function (data) { + taxonomies = data.taxonomies; + fuse = new Fuse(data.index, fuseOptions); + }); -// -// using the index loaded previously, run a search query (for "term") -// every time a letter is typed in the search box +// +// Run a search query (for "term") every time a letter is typed in the +// search box // function executeSearch(term) { if (lastSearch == term) { @@ -156,12 +172,12 @@ function executeSearch(term) { lastSearch = term; } -// Initialize the Fuse.io framework with the Search DB -fuseInit(searchDb); - -// -// execute search as each character is typed -// -searchInput.onkeyup = function(e) { - executeSearch(this.value); -} +searchInput.addEventListener('input', function () { + var term = this.value; + searchReady.then(function () { + // Only display the results of the latest search terms + if (searchInput.value === term) { + executeSearch(term); + } + }); +});