staticsearch
Version:
Simple static site search system.
2 lines (1 loc) • 5.94 kB
JavaScript
import{stem as p}from"./stem/__STEMFILE__";function d(o,e,t=7,r=new Set){return new Set(o.normalize("NFD").replace(/[\u0300-\u036f]/g,"").replace(/[\u2018\u2019\u201A\u201B\u0027]/g,"").toLowerCase().replace(/[^a-z\s]/g," ").split(/\s+/g).map(s=>e(s.trim()).slice(0,t)).filter(s=>s.length>1&&!r.has(s)).sort())}var h=class{#t=null;#e=null;#r=null;constructor(o,e,t){return this.#e=o||"db",this.#r=e||1,this.#s(t)}connect(){return this.#s().then(()=>!0).catch(()=>!1)}close(){this.#t.close(),this.#t=null}#s(o){return new Promise((e,t)=>{if(!("indexedDB"in window)){t(new Error("No indexedDB support"));return}let r=indexedDB.open(this.#e,this.#r);r.onsuccess=()=>{this.#t=r.result,e(this)},r.onerror=s=>{t(new Error(`IndexedDB error ${s.target.errorCode}: ${s.target.message}`,{cause:s}))},o&&(r.onupgradeneeded=s=>{o(r.result,s.oldVersion,s.newVersion)})})}get isConnected(){return!!this.#t}get name(){return this.#e}get version(){return this.#r}#n(o,e,t){return new Promise((r,s)=>{let{transaction:i,store:a}=this.#a(o,null,!0);i.oncomplete=()=>r(),i.onerror=n=>{s(new Error(n.target.error.message,{cause:n}))},e=Array.isArray(e)?e:[e],e.forEach(n=>{t?a.put(n):a.add(n)}),i.commit()})}add({store:o,item:e=[]}={}){return this.#n(o,e,!1)}put({store:o,item:e=[]}={}){return this.#n(o,e,!0)}#o(o,e,t,r){return new Promise((s,i)=>{r=Array.isArray(r)?r:[r];let a=t==="delete"||t==="clear",n=this.#a(o,e,a).store[t](...r);n.onsuccess=()=>s(n.result),n.onerror=()=>i(n.error??!1)})}count({store:o,index:e,lowerBound:t,upperBound:r}={}){return this.#o(o,e,"count",this.#i(t,r))}get({store:o,index:e,key:t}={}){return this.#o(o,e,"get",t)}getAll({store:o,index:e,lowerBound:t,upperBound:r,count:s}={}){return this.#o(o,e,"getAll",[this.#i(t,r),s])}getAllKeys({store:o,index:e,lowerBound:t,upperBound:r,count:s}={}){return this.#o(o,e,"getAllKeys",[this.#i(t,r),s])}delete({store:o,key:e}={}){return this.#o(o,null,"delete",[e])}deleteAll({store:o,index:e,lowerBound:t,upperBound:r}={}){return this.#o(o,e,"delete",[this.#i(t,r)])}clear({store:o}={}){return this.#o(o,null,"clear")}getCursor({store:o,index:e,lowerBound:t,upperBound:r,direction:s="next",callback:i}={}){return new Promise((a,n)=>{let l=this.#a(o,e).store.openCursor(this.#i(t,r),s);l.onsuccess=()=>{let u=l.result;u?u.advance(i&&i(u)||1):a(!0)},l.onerror=()=>n(l.error)})}drop(){return new Promise((o,e)=>{this.close();let t=indexedDB.deleteDatabase(this.#e);t.onsuccess=()=>{this.#e=null,this.#r=null,o(!0)},t.onerror=()=>e(!1)})}#a(o,e,t){let r=this.#t.transaction(o,t?"readwrite":"readonly",{durability:t?"strict":"default"}),s=r.objectStore(o);return{transaction:r,store:e&&!t?s.index(e):s}}#i(o,e){let t;return o&&e?t=IDBKeyRange.bound(o,e):o?t=IDBKeyRange.lowerBound(o):e&&(t=IDBKeyRange.upperBound(e)),t}};var c=class o{static path=new URL(import.meta.url).pathname.replace("__FILENAME__","");static dbName="__AGENT__";static version="__VERSION__";static wordCrop=__WORDCROP__;#t=!1;#e=null;#r=null;fetchTimeout=5e3;constructor(){}async init(){if(this.#t)return!0;if(this.#e=this.#e||await new h(o.dbName,1,(r,s,i)=>{switch(s){case 0:r.createObjectStore("cfg",{keyPath:"name"}),r.createObjectStore("page",{keyPath:"id"}),r.createObjectStore("file",{keyPath:"id"}),r.createObjectStore("index",{keyPath:"word"})}}),!this.#e?.isConnected)return!1;if((await this.#e.get({store:"cfg",key:"version"}))?.value===o.version)return this.#r=new Set((await this.#e.get({store:"cfg",key:"stopword"}))?.value),this.#t=!0,!0;await Promise.allSettled([this.#e.put({store:"cfg",item:{name:"version",value:o.version}}),this.#e.clear({store:"page"}),this.#e.clear({store:"file"}),this.#e.clear({store:"index"})]);let t=await this.#n("index.json");return t===null?!1:(this.#r=new Set(t.stopword),await this.#e.put({store:"cfg",item:{name:"stopword",value:t.stopword}}),await this.#e.add({store:"page",item:t.page.map((r,s)=>({id:s,url:r.u,title:r.t||r.u,description:r.d||"",date:r.p||"",words:r.w||0}))}),await this.#e.add({store:"file",item:t.file.map(r=>({id:r,loaded:!1}))}),this.#t=!0,!0)}async find(e){if(!this.#t)throw new Error("StaticSearch failed to initialize");e=e.trim(),this.#s("find",{search:e});let t=[],r=[...this.wordList(e)],s=r.length;if(!s)return this.#s("result",{search:e,result:t}),t;await Promise.allSettled(r.map(n=>this.loadIndex(n)));let i={};(await Promise.allSettled(r.map(n=>this.#e.get({store:"index",key:n})))).forEach(n=>{if(n.value?.page)for(let l in n.value.page)i[l]=i[l]||{rel:0,wc:0},i[l].rel+=n.value.page[l],i[l].wc++});let a=[];for(let n in i)a.push({id:n,relevancy:i[n].rel,found:i[n].wc/s});return a.sort((n,l)=>l.relevancy-n.relevancy),(await Promise.allSettled(a.map(n=>this.#e.get({store:"page",key:parseFloat(n.id)})))).forEach((n,l)=>{n.value&&(t[l]=n.value,t[l].relevancy=a[l].relevancy,t[l].found=a[l].found)}),this.#s("result",{search:e,result:t}),t}wordList(e){return d(e,p,o.wordCrop,this.#r)}async loadIndex(e){if(!this.#t)throw new Error("StaticSearch failed to initialize");if(e=e.toLowerCase().replace(/[^a-z]/g,"").slice(0,2),!e)return;let t=e.slice(0,e.length-1)+String.fromCharCode(e.charCodeAt(e.length-1)+1),r=(await this.#e.getAll({store:"file",lowerBound:e,upperBound:t})).filter(a=>!a.loaded);if(!r.length)return;let s=[],i=[];(await Promise.allSettled(r.map(a=>this.#n(`data/${a.id}.json`)))).map((a,n)=>{a.value&&(s.push({id:r[n].id,loaded:!0}),i.push(...Object.getOwnPropertyNames(a.value).map(l=>({word:l,page:a.value[l]}))))}),await Promise.allSettled([this.#e.put({store:"file",item:s}),this.#e.put({store:"index",item:i})])}#s(e="unknown",t={}){document.dispatchEvent(new CustomEvent("staticsearch:"+e,{detail:t}))}async#n(e){let t=o.path+e;try{let r=new AbortController,s=setTimeout(()=>r.abort(),this.fetchTimeout),i=await fetch(t,{signal:r.signal});if(clearTimeout(s),!i.ok)throw new Error(`Fetch failed ${t}: ${i.status}`);return await i.json()}catch{return null}}},w=new c;await w.init();export{w as staticsearch};