staticsearch
Advanced tools
@@ -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>…</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>…</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}; |
+4
-7
@@ -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 @@ |
+49
-53
| 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 @@ |
+3
-3
@@ -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 }`); | ||
| } | ||
| } |
+4
-2
| { | ||
| "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": { |
+29
-8
@@ -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; | ||
| }); | ||
| ``` |
-41
| // 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); | ||
| }); | ||
| } |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Debug access
Supply chain riskUses debug, reflection and dynamic code execution features.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
548
3.98%30
-3.23%82843
-0.33%3
200%18
-5.26%962
-3.41%+ Added
+ Added
+ Added
+ Added