interfacepageData{title: string,date: string,permalink: string,content: string,image?: string,preview: string,matchCount: number}interfacematch{start: number,end: number}/**
* Escape HTML tags as HTML entities
* Edited from:
* @link https://stackoverflow.com/a/5499821
*/consttagsToReplace={'&':'&','<':'<','>':'>','"':'"','…':'…'};functionreplaceTag(tag){returntagsToReplace[tag]||tag;}functionreplaceHTMLEnt(str){returnstr.replace(/[&<>"]/g,replaceTag);}functionescapeRegExp(string){returnstring.replace(/[.*+\-?^${}()|[\]\\]/g,'\\$&');}classSearch{privatedata: pageData[];privateform: HTMLFormElement;privateinput: HTMLInputElement;privatelist: HTMLDivElement;privateresultTitle: HTMLHeadElement;privateresultTitleTemplate: string;constructor({form,input,list,resultTitle,resultTitleTemplate}){this.form=form;this.input=input;this.list=list;this.resultTitle=resultTitle;this.resultTitleTemplate=resultTitleTemplate;this.handleQueryString();this.bindQueryStringChange();this.bindSearchForm();}/**
* Processes search matches
* @param str original text
* @param matches array of matches
* @param ellipsis whether to add ellipsis to the end of each match
* @param charLimit max length of preview string
* @param offset how many characters before and after the match to include in preview
* @returns preview string
*/privatestaticprocessMatches(str: string,matches: match[],ellipsis: boolean=true,charLimit=140,offset=20):string{matches.sort((a,b)=>{returna.start-b.start;});leti=0,lastIndex=0,charCount=0;constresultArray: string[]=[];while(i<matches.length){constitem=matches[i];/// item.start >= lastIndex (equal only for the first iteration)
/// because of the while loop that comes after, iterating over variable j
if(ellipsis&&item.start-offset>lastIndex){resultArray.push(`${replaceHTMLEnt(str.substring(lastIndex,lastIndex+offset))} [...] `);resultArray.push(`${replaceHTMLEnt(str.substring(item.start-offset,item.start))}`);charCount+=offset*2;}else{/// If the match is too close to the end of last match, don't add ellipsis
resultArray.push(replaceHTMLEnt(str.substring(lastIndex,item.start)));charCount+=item.start-lastIndex;}letj=i+1,end=item.end;/// Include as many matches as possible
/// [item.start, end] is the range of the match
while(j<matches.length&&matches[j].start<=end){end=Math.max(matches[j].end,end);++j;}resultArray.push(`<mark>${replaceHTMLEnt(str.substring(item.start,end))}</mark>`);charCount+=end-item.start;i=j;lastIndex=end;if(ellipsis&&charCount>charLimit)break;}/// Add the rest of the string
if(lastIndex<str.length){letend=str.length;if(ellipsis)end=Math.min(end,lastIndex+offset);resultArray.push(`${replaceHTMLEnt(str.substring(lastIndex,end))}`);if(ellipsis&&end!=str.length){resultArray.push(` [...]`);}}returnresultArray.join('');}privateasyncsearchKeywords(keywords: string[]){constrawData=awaitthis.getData();constresults: pageData[]=[];constregex=newRegExp(keywords.filter((v,index,arr)=>{arr[index]=escapeRegExp(v);returnv.trim()!=='';}).join('|'),'gi');for(constitemofrawData){consttitleMatches: match[]=[],contentMatches: match[]=[];letresult={...item,preview:'',matchCount: 0}constcontentMatchAll=item.content.matchAll(regex);for(constmatchofArray.from(contentMatchAll)){contentMatches.push({start: match.index,end: match.index+match[0].length});}consttitleMatchAll=item.title.matchAll(regex);for(constmatchofArray.from(titleMatchAll)){titleMatches.push({start: match.index,end: match.index+match[0].length});}if(titleMatches.length>0)result.title=Search.processMatches(result.title,titleMatches,false);if(contentMatches.length>0){result.preview=Search.processMatches(result.content,contentMatches);}else{/// If there are no matches in the content, use the first 140 characters as preview
result.preview=replaceHTMLEnt(result.content.substring(0,140));}result.matchCount=titleMatches.length+contentMatches.length;if(result.matchCount>0)results.push(result);}/// Result with more matches appears first
returnresults.sort((a,b)=>{returnb.matchCount-a.matchCount;});}privateasyncdoSearch(keywords: string[]){conststartTime=performance.now();constresults=awaitthis.searchKeywords(keywords);this.clear();for(constitemofresults){this.list.append(Search.render(item));}constendTime=performance.now();this.resultTitle.innerText=this.generateResultTitle(results.length,((endTime-startTime)/1000).toPrecision(1));pjax.refresh(document);}privategenerateResultTitle(resultLen,time){returnthis.resultTitleTemplate.replace("#PAGES_COUNT",resultLen).replace("#TIME_SECONDS",time);}publicasyncgetData() {if(!this.data){/// Not fetched yet
constjsonURL=this.form.dataset.json;this.data=awaitfetch(jsonURL).then(res=>res.json());constparser=newDOMParser();for(constitemofthis.data){item.content=parser.parseFromString(item.content,'text/html').body.innerText;}}returnthis.data;}privatebindSearchForm() {letlastSearch='';consteventHandler=(e)=>{e.preventDefault();constkeywords=this.input.value.trim();Search.updateQueryString(keywords,true);if(keywords===''){lastSearch='';returnthis.clear();}if(lastSearch===keywords)return;lastSearch=keywords;this.doSearch(keywords.split(' '));}this.input.addEventListener('input',eventHandler);this.input.addEventListener('compositionend',eventHandler);}privateclear() {this.list.innerHTML='';this.resultTitle.innerText='';}privatebindQueryStringChange() {window.addEventListener('popstate',(e)=>{this.handleQueryString()})}privatehandleQueryString() {constpageURL=newURL(window.location.toString());constkeywords=pageURL.searchParams.get('keyword');this.input.value=keywords;if(keywords){this.doSearch(keywords.split(' '));}else{this.clear()}}privatestaticupdateQueryString(keywords: string,replaceState=false){constpageURL=newURL(window.location.toString());if(keywords===''){pageURL.searchParams.delete('keyword')}else{pageURL.searchParams.set('keyword',keywords);}if(replaceState){window.history.replaceState('','',pageURL.toString());}else{window.history.pushState('','',pageURL.toString());}}publicstaticrender(item: pageData){return<article><ahref={item.permalink}><divclass="article-details"><h2class="article-title"dangerouslySetInnerHTML={{__html: item.title}}></h2><sectionclass="article-preview"dangerouslySetInnerHTML={{__html: item.preview}}></section></div>{item.image&&<divclass="article-image"><imgsrc={item.image}loading="lazy"/></div>}</a></article>;}}declareglobal{interfaceWindow{searchResultTitleTemplate: string;}}//window.addEventListener('load', () => {
functionsearchInit() {letsearch=document.querySelector('.search-result');if(search){constsearchForm=document.querySelector('.search-form')asHTMLFormElement,searchInput=searchForm.querySelector('input')asHTMLInputElement,searchResultList=document.querySelector('.search-result--list')asHTMLDivElement,searchResultTitle=document.querySelector('.search-result--title')asHTMLHeadingElement;newSearch({form: searchForm,input: searchInput,list: searchResultList,resultTitle: searchResultTitle,resultTitleTemplate: window.searchResultTitleTemplate});}}export{searchInit}exportdefaultSearch;