🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

staticsearch

Package Overview
Dependencies
Maintainers
1
Versions
11
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

staticsearch - npm Package Compare versions

Comparing version
0.1.1
to
0.2.0
+1
-1
dist/js/staticsearch-bind.js

@@ -1,1 +0,1 @@

import{staticsearch as y}from"./__SSDIR__/staticsearch.js";var f="q",g=0,v=500;(()=>{let t=document.getElementById("staticsearch_search"),r=document.getElementById("staticsearch_result");r&&C(r,r.getAttribute("minscore"),r.getAttribute("maxresults")),t&&t.tagName==="INPUT"&&m(t)})();function S(){return new URLSearchParams(location.search).get(f)||""}function b(t,r){let n=new URL(location);t?n.searchParams.set(f,t):n.searchParams.delete(f),n.hash=r||"",history.replaceState({},"",n.href)}function m(t){t.name&&(f=t.name);let r=S();r&&(t.value=r);let n=t.value;n&&a(n);let p;t.addEventListener("input",c=>{let s=c.target.value;clearTimeout(p),p=setTimeout(()=>a(s),v)},!1);function a(c){c.length<2||y.find(c).then(s=>{})}}function C(t,r,n,p,a,c){let s="";r=parseFloat(r)||0,n=parseFloat(n)||0,a=a||document.getElementById("staticsearch_resultmessage"),a||(a=document.createElement("template"),a.innerHTML='<p part="resultmessage"><span part="resultcount">0</span> found for <span part="searchterm"></span>&hellip;</p>'),c=c||document.getElementById("staticsearch_item"),c||(c=document.createElement("template"),c.innerHTML='<li part="item"><a part="link"><h2 part="title"></h2><p part="description"></p></a></li>'),t.addEventListener("click",i=>{let l=i.target.closest("a");l&&s&&b(s,l.id)}),document.addEventListener("staticsearch:result",i=>{s=i.detail.search;let l=i.detail.result;t.innerHTML="";let o=document.createElement(p||"ol");o.setAttribute("part","searchresult"),l.forEach((e,E)=>{if(r&&e.relevancy<r||n&&E>=n)return;let u=c.content.cloneNode(!0);h(u,"link",null,{href:e.url,id:`staticsearchresult-${e.id}`}),h(u,"title",e.title),h(u,"description",e.description),o.appendChild(u)});let d=a.content.cloneNode(!0);if(h(d,"resultcount",o.childElementCount),h(d,"searchterm",s),t.appendChild(d),t.appendChild(o),!g&&location.hash){let e=t.querySelector(location.hash);e&&(e.focus(),e.scrollIntoView({behavior:"smooth",block:"nearest"}))}else t.scrollTop=0;g++},!1);function h(i,l,o,d={}){i.querySelectorAll(`[part="${l}"]`).forEach(e=>{for(let[E,u]of Object.entries(d))e.setAttribute(E,u);if(o){for(;e.firstElementChild;)e=e.firstElementChild;e.textContent=o}})}}export{m as staticSearchInput,S as staticSearchQuery,C as staticSearchResult,b as staticSearchSetQuery};
import{staticsearch as m}from"./__SSDIR__/staticsearch.js";var f="q",S=0,y="__AGENT__",E=Symbol(y),b=500;(()=>{let t=document.getElementById("staticsearch_search"),r=document.getElementById("staticsearch_result");r&&L(r,r.getAttribute("minscore"),r.getAttribute("maxresults")),t&&t.tagName==="INPUT"&&C(t)})();function v(){return new URLSearchParams(location.search).get(f)||""}function I(t,r){let n=new URL(location);t?n.searchParams.set(f,t):n.searchParams.delete(f),n.hash=r||"",history.replaceState({},"",n.href)}function C(t){if(t[E])return;t[E]=!0,t.name&&(f=t.name);let r=v()||sessionStorage.getItem(y)||"";r&&(t.value=r);let n=t.value;n&&a(n);let p;t.addEventListener("input",s=>{let c=s.target.value;clearTimeout(p),p=setTimeout(()=>a(c),b)},!1);function a(s){sessionStorage.setItem(y,s),!(s.length<2)&&m.find(s).then(c=>{})}}function L(t,r,n,p,a,s){if(t[E])return;t[E]=!0;let c="";r=parseFloat(r)||0,n=parseFloat(n)||0,a=a||document.getElementById("staticsearch_resultmessage"),a||(a=document.createElement("template"),a.innerHTML='<p part="resultmessage"><span part="resultcount">0</span> found for <span part="searchterm"></span>&hellip;</p>'),s=s||document.getElementById("staticsearch_item"),s||(s=document.createElement("template"),s.innerHTML='<li part="item"><a part="link"><h2 part="title"></h2><p part="description"></p></a></li>'),t.addEventListener("click",i=>{let l=i.target.closest("a");l&&c&&I(c,l.id)}),document.addEventListener("staticsearch:result",i=>{c=i.detail.search;let l=i.detail.result;t.innerHTML="";let o=document.createElement(p||"ol");o.setAttribute("part","searchresult"),l.forEach((e,g)=>{if(r&&e.relevancy<r||n&&g>=n)return;let u=s.content.cloneNode(!0);h(u,"link",null,{href:e.url,id:`staticsearchresult-${e.id}`}),h(u,"title",e.title),h(u,"description",e.description),o.appendChild(u)});let d=a.content.cloneNode(!0);if(h(d,"resultcount",o.childElementCount),h(d,"searchterm",c),t.appendChild(d),t.appendChild(o),!S&&location.hash){let e=t.querySelector(location.hash);e&&(e.focus(),e.scrollIntoView({behavior:"smooth",block:"nearest"}))}else t.scrollTop=0;S++},!1);function h(i,l,o,d={}){i.querySelectorAll(`[part="${l}"]`).forEach(e=>{for(let[g,u]of Object.entries(d))e.setAttribute(g,u);if(o){for(;e.firstElementChild;)e=e.firstElementChild;e.textContent=o}})}}export{C as staticSearchInput,v as staticSearchQuery,L as staticSearchResult,I as staticSearchSetQuery};

@@ -1,2 +0,2 @@

import{staticSearchQuery as h,staticSearchSetQuery as l,staticSearchInput as o,staticSearchResult as n}from"./__SSDIR__/staticsearch-bind.js";var a=class r extends HTMLElement{static path=new URL(import.meta.url).pathname.replace("__FILENAME__","");#t=null;#e=null;#i=null;constructor(){super()}connectedCallback(){let t=this.firstElementChild;t.setAttribute("part","startsearch"),this.attachShadow({mode:"open"});let s=document.createElement("link");s.rel="stylesheet",s.href=`${r.path}css/component.css`,this.shadowRoot.appendChild(s);let i=this.shadowRoot.appendChild(t);this.#r(),i.style.cursor="pointer",i.addEventListener("click",e=>{e.preventDefault(),this.#s()}),window.addEventListener("keydown",e=>{e.key==="k"&&(e.ctrlKey||e.metaKey)&&(e.preventDefault(),this.#s())}),h()&&this.#s()}#s(){this.#t.open?this.#a():(this.#t.showModal(),this.setAttribute("aria-expanded","true"),this.#e.focus())}#a(){this.#t.close(),this.removeAttribute("aria-expanded"),l()}#r(){let t=document.createElement("dialog");t.setAttribute("closedby","any"),t.setAttribute("part","dialog"),t.innerHTML=`
import{staticSearchQuery as h,staticSearchSetQuery as l,staticSearchInput as o,staticSearchResult as n}from"./__SSDIR__/staticsearch-bind.js";var a=class r extends HTMLElement{static path=new URL(import.meta.url).pathname.replace("__FILENAME__","");#t=null;#e=null;#i=null;constructor(){super()}connectedCallback(){let t=this.firstElementChild;if(this.attachShadow({mode:"open"}),!t)return;t.setAttribute("part","startsearch");let s=document.createElement("link");s.rel="stylesheet",s.href=`${r.path}css/component.css`,this.shadowRoot.appendChild(s);let i=this.shadowRoot.appendChild(t);this.#r(),i.style.cursor="pointer",i.addEventListener("click",e=>{e.preventDefault(),this.#s()}),window.addEventListener("keydown",e=>{e.key==="k"&&(e.ctrlKey||e.metaKey)&&(e.preventDefault(),this.#s())}),h()&&this.#s()}#s(){this.#t.open?this.#a():(this.#t.showModal(),this.setAttribute("aria-expanded","true"),this.#e.focus())}#a(){this.#t.close(),this.removeAttribute("aria-expanded"),l()}#r(){let t=document.createElement("dialog");t.setAttribute("closedby","any"),t.setAttribute("part","dialog"),t.innerHTML=`
<form method="dialog"><button part="close"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path xmlns="http://www.w3.org/2000/svg" fill="currentColor" d="M5.3 5.3a1 1 0 0 1 1.4 0l5.3 5.3 5.3-5.3a1 1 0 1 1 1.4 1.4L13.4 12l5.3 5.3a1 1 0 0 1-1.4 1.4L12 13.4l-5.3 5.3a1 1 0 0 1-1.4-1.4l5.3-5.3-5.3-5.3a1 1 0 0 1 0-1.4Z"/></svg></button></form>

@@ -3,0 +3,0 @@ <search part="search">

@@ -1,1 +0,1 @@

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}#i(o,e,t){return new Promise((r,s)=>{let{transaction:a,store:i}=this.#a(o,null,!0);a.oncomplete=()=>r(),a.onerror=n=>{s(new Error(n.target.error.message,{cause:n}))},e=Array.isArray(e)?e:[e],e.forEach(n=>{t?i.put(n):i.add(n)}),a.commit()})}add({store:o,item:e=[]}={}){return this.#i(o,e,!1)}put({store:o,item:e=[]}={}){return this.#i(o,e,!0)}#o(o,e,t,r){return new Promise((s,a)=>{r=Array.isArray(r)?r:[r];let i=t==="delete"||t==="clear",n=this.#a(o,e,i).store[t](...r);n.onsuccess=()=>s(n.result),n.onerror=()=>a(n.error??!1)})}count({store:o,index:e,lowerBound:t,upperBound:r}={}){return this.#o(o,e,"count",this.#n(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.#n(t,r),s])}getAllKeys({store:o,index:e,lowerBound:t,upperBound:r,count:s}={}){return this.#o(o,e,"getAllKeys",[this.#n(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.#n(t,r)])}clear({store:o}={}){return this.#o(o,null,"clear")}getCursor({store:o,index:e,lowerBound:t,upperBound:r,direction:s="next",callback:a}={}){return new Promise((i,n)=>{let l=this.#a(o,e).store.openCursor(this.#n(t,r),s);l.onsuccess=()=>{let u=l.result;u?u.advance(a&&a(u)||1):i(!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}}#n(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,a)=>{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.#i("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,description:r.d}))}),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)];if(!r.length)return this.#s("result",{search:e,result:t}),t;await Promise.allSettled(r.map(i=>this.loadIndex(i)));let s={};(await Promise.allSettled(r.map(i=>this.#e.get({store:"index",key:i})))).forEach(i=>{if(i.value?.page)for(let n in i.value.page)s[n]=s[n]||0,s[n]+=i.value.page[n]});let a=[];for(let i in s)a.push({id:i,relevancy:s[i]});return a.sort((i,n)=>n.relevancy-i.relevancy),(await Promise.allSettled(a.map(i=>this.#e.get({store:"page",key:parseFloat(i.id)})))).forEach((i,n)=>{i.value&&(t[n]=i.value,t[n].relevancy=a[n].relevancy)}),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(i=>!i.loaded);if(!r.length)return;let s=[],a=[];(await Promise.allSettled(r.map(i=>this.#i(`data/${i.id}.json`)))).map((i,n)=>{i.value&&(s.push({id:r[n].id,loaded:!0}),a.push(...Object.getOwnPropertyNames(i.value).map(l=>({word:l,page:i.value[l]}))))}),await Promise.allSettled([this.#e.put({store:"file",item:s}),this.#e.put({store:"index",item:a})])}#s(e="unknown",t={}){document.dispatchEvent(new CustomEvent("staticsearch:"+e,{detail:t}))}async#i(e){let t=o.path+e;try{let r=new AbortController,s=setTimeout(()=>r.abort(),this.fetchTimeout),a=await fetch(t,{signal:r.signal});if(clearTimeout(s),!a.ok)throw new Error(`Fetch failed ${t}: ${a.status}`);return await a.json()}catch{return null}}},g=new c;await g.init();export{g as staticsearch};
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}#i(o,e,t){return new Promise((r,s)=>{let{transaction:a,store:i}=this.#a(o,null,!0);a.oncomplete=()=>r(),a.onerror=n=>{s(new Error(n.target.error.message,{cause:n}))},e=Array.isArray(e)?e:[e],e.forEach(n=>{t?i.put(n):i.add(n)}),a.commit()})}add({store:o,item:e=[]}={}){return this.#i(o,e,!1)}put({store:o,item:e=[]}={}){return this.#i(o,e,!0)}#o(o,e,t,r){return new Promise((s,a)=>{r=Array.isArray(r)?r:[r];let i=t==="delete"||t==="clear",n=this.#a(o,e,i).store[t](...r);n.onsuccess=()=>s(n.result),n.onerror=()=>a(n.error??!1)})}count({store:o,index:e,lowerBound:t,upperBound:r}={}){return this.#o(o,e,"count",this.#n(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.#n(t,r),s])}getAllKeys({store:o,index:e,lowerBound:t,upperBound:r,count:s}={}){return this.#o(o,e,"getAllKeys",[this.#n(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.#n(t,r)])}clear({store:o}={}){return this.#o(o,null,"clear")}getCursor({store:o,index:e,lowerBound:t,upperBound:r,direction:s="next",callback:a}={}){return new Promise((i,n)=>{let l=this.#a(o,e).store.openCursor(this.#n(t,r),s);l.onsuccess=()=>{let u=l.result;u?u.advance(a&&a(u)||1):i(!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}}#n(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,a)=>{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.#i("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,description:r.d}))}),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)];if(!r.length)return this.#s("result",{search:e,result:t}),t;await Promise.allSettled(r.map(i=>this.loadIndex(i)));let s={};(await Promise.allSettled(r.map(i=>this.#e.get({store:"index",key:i})))).forEach(i=>{if(i.value?.page)for(let n in i.value.page)s[n]=s[n]||0,s[n]+=i.value.page[n]});let a=[];for(let i in s)a.push({id:i,relevancy:s[i]});return a.sort((i,n)=>n.relevancy-i.relevancy),(await Promise.allSettled(a.map(i=>this.#e.get({store:"page",key:parseFloat(i.id)})))).forEach((i,n)=>{i.value&&(t[n]=i.value,t[n].relevancy=a[n].relevancy)}),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(i=>!i.loaded);if(!r.length)return;let s=[],a=[];(await Promise.allSettled(r.map(i=>this.#i(`data/${i.id}.json`)))).map((i,n)=>{i.value&&(s.push({id:r[n].id,loaded:!0}),a.push(...Object.getOwnPropertyNames(i.value).map(l=>({word:l,page:i.value[l]}))))}),await Promise.allSettled([this.#e.put({store:"file",item:s}),this.#e.put({store:"index",item:a})])}#s(e="unknown",t={}){document.dispatchEvent(new CustomEvent("staticsearch:"+e,{detail:t}))}async#i(e){let t=o.path+e;try{let r=new AbortController,s=setTimeout(()=>r.abort(),this.fetchTimeout),a=await fetch(t,{signal:r.signal});if(clearTimeout(s),!a.ok)throw new Error(`Fetch failed ${t}: ${a.status}`);return await a.json()}catch{return null}}},g=new c;await g.init();export{g as staticsearch};

@@ -18,4 +18,4 @@ #!/usr/bin/env node

{ env: 'SITE_INDEXFILE', cli: 'indexfile', clis: 'i', prop: 'siteIndexFile', type: 'name', default: 'index.html', help: 'default index file' },
{ env: 'SITE_PARSEROBOTSFILE', cli: 'robotfile', clis: 'f', prop: 'siteParseRobotsFile', type: 'true|false', default: true, help: 'parse robot.txt Disallows' },
{ env: 'SITE_PARSEROBOTSMETA', cli: 'robotmeta', clis: 'm', prop: 'siteParseRobotsMeta', type: 'true|false', default: true, help: 'parse robot meta noindex' },
{ env: 'SITE_PARSEROBOTSFILE', cli: 'ignorerobotfile', clis: 'f', prop: 'siteParseRobotsFile', type: 'true|false', default: true, help: 'parse robot.txt Disallows' },
{ env: 'SITE_PARSEROBOTSMETA', cli: 'ignorerobotmeta', clis: 'm', prop: 'siteParseRobotsMeta', type: 'true|false', default: true, help: 'parse robot meta noindex' },
{ env: 'PAGE_DOMSELECTORS', cli: 'dom', clis: 'D', prop: 'pageDOMSelectors', type: 'nodelist', default: 'main', help: 'comma-separated content DOM nodes' },

@@ -104,3 +104,3 @@ { env: 'PAGE_DOMEXCLUDE', cli: 'domx', clis: 'X', prop: 'pageDOMExclude', type: 'nodelist', default: 'nav', help: 'comma-separated DOM nodes to exclude' },

.filter(c => c.cli)
.map(c => ` ${ (c.clis ? `-${ c.clis }, ` : ' ') }--${ c.cli.padEnd(14) }${ styleText(['dim'], (c.type ? ' <' + c.type + '>' : '').padEnd(13)) } ${ c.help } ${ c.default ? styleText(['dim'], `(${ c.default })`) : '' }`)
.map(c => ` ${ (c.clis ? `-${ c.clis }, ` : ' ') }--${ c.cli.padEnd(15) }${ styleText(['dim'], (c.type && c.type !== 'true|false' ? ' <' + c.type + '>' : '').padEnd(13)) } ${ c.help } ${ c.default ? styleText(['dim'], `(${ c.default })`) : '' }`)
.join('\n')

@@ -224,4 +224,2 @@ }

value = value.trim();
switch (c.type) {

@@ -235,4 +233,3 @@

case 'true|false':
value = value.toLowerCase();
value = value !== 'false' && value !== '0';
if (c.cli.startsWith('ignore')) value = !value;
break;

@@ -239,0 +236,0 @@

import process from 'node:process';
import { readdir, readFile, cp } from 'node:fs/promises';
import { join, resolve, dirname, extname, sep } from 'node:path';
import { performance } from 'node:perf_hooks';
import crypto from 'node:crypto';
import { PerfPro } from 'perfpro';
import { ConCol } from 'concol';
import { parseRobotsTxt, parseHTML } from './lib/parser.js';
import { writePath, deletePath } from './lib/file.js';
import { stemFilename, stemFunction, stopWords } from './lib/lang.js';
import { log } from './lib/log.js';
// performance handler
const perf = new PerfPro('StaticSearch');
// console logger
export const concol = new ConCol('StaticSearch', 'cyan');
// search indexer

@@ -52,4 +61,2 @@ class StaticSearch {

this.#recPerf('total indexing time');
// resolved working directories

@@ -84,5 +91,10 @@ const

// find all HTML files and slugs
// find all HTML files
let buildFile = (await readdir(workingBuildDir, { recursive: true }))
.filter(f => extname(f).toLowerCase().includes('.htm'))
.filter(f => extname(f).toLowerCase().includes('.htm'));
// record total number of HTML files
const totalHTMLfiles = buildFile.length;
buildFile = buildFile
.map(file => {

@@ -116,3 +128,3 @@

// <meta name="staticsearch" content="noindex">
this.#recPerf('parse HTML files');
perf.mark('HTML file parsing');
const robotRe = new RegExp(`<meta.*name=.*(robots|${ this.#agent }).*noindex`, 'i');

@@ -142,3 +154,3 @@

else {
log(`Unable to parse HTML in ${ buildFile[idx].file }`, 'warn');
concol.warn(`Unable to parse HTML in ${ buildFile[idx].file }`);
}

@@ -154,3 +166,3 @@

if (!buildFile.length) {
log(`no files available for indexing at ${ workingBuildDir }`);
concol.warn(`no files available for indexing at ${ workingBuildDir }`);
return;

@@ -162,4 +174,4 @@ }

this.#recPerf('parse HTML files', true);
this.#recPerf('word scoring');
perf.mark('HTML file parsing');
perf.mark('word score calculations');

@@ -171,3 +183,3 @@ const

// create search data stores
// create search word indexes
buildFile.forEach((page, idx) => {

@@ -219,4 +231,4 @@

this.#recPerf('word scoring', true);
this.#recPerf('write index files');
perf.mark('word score calculations');
perf.mark('index file writing');

@@ -230,3 +242,3 @@ // output index files

let curFile = null, wordFile = [];
let curFile = null, wordFile = [], wordHash = '';

@@ -240,3 +252,4 @@ while (wordList.length) {

// get next item
wordFile.push( wordList.shift() );
const w = wordList.shift();
wordFile.push( w );

@@ -258,3 +271,5 @@ }

await writePath(join(workingSearchDir, './data/', curFile + '.json'), JSON.stringify( wordOut, null, this.#JSONspacing ));
const out = JSON.stringify( wordOut, null, this.#JSONspacing );
await writePath(join(workingSearchDir, './data/', curFile + '.json'), out);
wordHash += out;

@@ -270,2 +285,5 @@ }

// generate index version cache
const version = crypto.createHash('sha1').update(wordHash).digest('hex');
// page indexes

@@ -298,3 +316,3 @@ await writePath(join(workingSearchDir, 'index.json'), JSON.stringify(

.replaceAll('__FILENAME__', jsFile)
.replaceAll('__VERSION__', Math.ceil( +new Date() / 60000))
.replaceAll('__VERSION__', version)
.replaceAll('__WORDCROP__', this.wordCrop);

@@ -310,46 +328,24 @@

this.#recPerf('write index files', true);
perf.mark('index file writing');
this.#recPerf('total indexing time', true);
concol.log(
this.#showMetrics();
[
'StaticSearch indexing complete\n',
log(`indexing complete
pages indexed: ${ String(pageIndex.length).padStart(5, ' ') }
unique words: ${ String(wordIndex.size).padStart(5, ' ') }
index files: ${ String(wordFileList.length + 1).padStart(5, ' ') }
`,
'info');
}
[ 'HTML files found', totalHTMLfiles ],
[ 'HTML files excluded', totalHTMLfiles - pageIndex.length ],
[ 'HTML files indexed', pageIndex.length ],
[ 'unique words indexed', wordIndex.size ],
[ 'index files created', wordFileList.length + 1 ],
'',
// record performance metric
#recPerf(name, done = false) {
performance.mark(`[${ this.#agent }] ${ name }:${ done ? 1 : 0 }`);
}
[ 'total indexing time', perf.now(), ' ms' ],
...perf.allDurations().map(p => [ p.name, p.duration, ' ms']),
]
// log performance metrics
#showMetrics() {
const metrics = new Set(
[...performance.getEntries()]
.filter( p => p.name.startsWith(`[${ this.#agent }] `))
.map( p => p.name.replace(/:.$/i, '') )
);
let msg = '';
metrics.forEach(m => {
const p = Math.ceil( performance.measure(m, m + ':0', m + ':1').duration );
msg += m.slice( this.#agent.length + 3 ).padStart(20,' ') + ':' + String(p).padStart(6, ' ') + 'ms\n';
performance.clearMarks(m + ':0');
performance.clearMarks(m + ':1');
performance.clearMeasures(m);
});
log(msg, 'info');
}

@@ -356,0 +352,0 @@

@@ -6,3 +6,3 @@ // file utilities

import { log } from './log.js';
import { concol } from '../indexer.js';

@@ -57,3 +57,3 @@ // get file information

catch (e) {
log(`Unable to write file: ${ filename }\n,${ e }`, 'error');
concol.error(`Unable to write file: ${ filename }\n,${ e }`);
return false;

@@ -76,5 +76,5 @@ }

catch (e) {
log(`Unable to delete path: ${ path }\n${ e }`, 'warn');
concol.warn(`Unable to delete path: ${ path }\n${ e }`);
}
}
{
"name": "staticsearch",
"version": "0.1.1",
"version": "0.2.0",
"description": "Simple static site search system.",

@@ -50,3 +50,5 @@ "type": "module",

"dependencies": {
"node-html-parser": "7.0.1"
"concol": "2.1.0",
"node-html-parser": "7.0.1",
"perfpro": "0.2.0"
},

@@ -53,0 +55,0 @@ "devDependencies": {

@@ -86,4 +86,4 @@ # StaticSearch

|`indexfile`|`SITE_INDEXFILE`|`.siteIndexFile`|default index file (`index.html`)|
|`robotfile`|`SITE_PARSEROBOTSFILE`|`.siteParseRobotsFile`|parse robot.txt Disallows (`true`)|
|`robotmeta`|`SITE_PARSEROBOTSMETA`|`.siteParseRobotsMeta`|parse robot meta noindex (`true`)|
|`ignorerobotfile`|`SITE_PARSEROBOTSFILE`|`.siteParseRobotsFile`|parse robot.txt Disallows (`true`)|
|`ignorerobotmeta`|`SITE_PARSEROBOTSMETA`|`.siteParseRobotsMeta`|parse robot meta noindex (`true`)|
|`dom`|`PAGE_DOMSELECTORS`|`.pageDOMSelectors`|comma-separated content DOM nodes (`main`)|

@@ -123,5 +123,5 @@ |`domx`|`PAGE_DOMEXCLUDE`|`.pageDOMExclude`|comma-separated DOM nodes to exclude (`nav`)|

If `robotfile`|`SITE_PARSEROBOTSFILE`|`.siteParseRobotsFile` is `true`, StaticSearch parses the `robots.txt` file in the root of the build directory. It will not index any HTML file matching a `Disallow: /some-directory/` line below `User-agent: *` or `User-agent: staticsearch`.
StaticSearch parses the `robots.txt` file in the root of the build directory and will not index any HTML file matching a `Disallow: /some-directory/` line below `User-agent: *` or `User-agent: staticsearch`. This can be disabled by setting `--ignorerobotfile`|`SITE_PARSEROBOTSFILE=false`|`.siteParseRobotsFile=false`.
If `robotmeta`|`SITE_PARSEROBOTSMETA`|`.siteParseRobotsMeta` is `true`, StaticSearch will parse HTML `meta` tags. It will not index any HTML file where `content="noindex"` in `<meta name="robots">` or `<meta name="staticsearch">`.
StaticSearch parses HTML `meta` tags and will not index any HTML file where `content="noindex"` in `<meta name="robots">` or `<meta name="staticsearch">`. This can be disabled by setting `--ignorerobotmeta`|`SITE_PARSEROBOTSMETA=false`|`.siteParseRobotsMeta=false`.

@@ -281,3 +281,3 @@

* `label="<string>"` - the label on the search `<input>`
* `minscore="<num>"` - only show pages with total relevency scores of this or above on results
* `minscore="<num>"` - only show pages with total relevancy scores of this or above on results
* `maxresults="<num>"` - show up to this number of pages on the results

@@ -410,3 +410,3 @@

* input deboucing
* input debouncing
* results output

@@ -496,3 +496,3 @@ * URL querystring and hash functionality when clicking a link and clicking back.

Finally, you can directly use the StaticSearch JavaScript API to return an array of results for a specific search term:
You can implement whatever input and output functionality or styling you require by directly using the StaticSearch JavaScript API. The `.find()` method returns an array of results for a specific search term:

@@ -531,2 +531,23 @@ ```js

You can therefore implement whatever input and output functionality or styling you require.
### StaticSearch events
However a search is initiated, `staticsearch:` events are triggered on the `document` property:
```js
// search started
document.addEventListener('staticsearch:find', e => {
// get search term
const { search } = e.detail;
});
// search result available
document.addEventListener('staticsearch:result', e => {
// get search term and result array
const { search, result } = e.detail;
});
```
// custom logging
import { styleText } from 'node:util';
export function log(msg, type = 'log', prefix = '[StaticSearch] ') {
const preSpace = ' '.repeat(prefix.length);
prefix = styleText(['dim', 'cyan'], prefix);
if (!console[type]) {
msg = 'unknown logging type: "${ type }"\n${ msg }';
}
msg
.split('\n')
.forEach( (m, i) => {
const nv = m.split(':');
if (nv.length > 1) {
m = '';
nv.forEach((v, j) => {
if (j + 1 < nv.length) {
m += v + ':';
}
else {
m += styleText(['cyanBright'], v);
}
});
}
else {
m = styleText(['whiteBright'], m);
}
console[type]((i ? preSpace : prefix) + m);
});
}