From cfde89ecfc09e7611f59104ee2b35876008ad171 Mon Sep 17 00:00:00 2001 From: Jimmy Cai Date: Sun, 24 Mar 2024 23:44:14 +0100 Subject: [PATCH] refactor: remove `search` page layout --- assets/ts/search.tsx | 326 ---------------------------- layouts/page/search.html | 33 --- layouts/page/search.json | 23 -- layouts/partials/widget/search.html | 4 +- 4 files changed, 2 insertions(+), 384 deletions(-) delete mode 100644 assets/ts/search.tsx delete mode 100644 layouts/page/search.html delete mode 100644 layouts/page/search.json diff --git a/assets/ts/search.tsx b/assets/ts/search.tsx deleted file mode 100644 index 856b48d..0000000 --- a/assets/ts/search.tsx +++ /dev/null @@ -1,326 +0,0 @@ -interface pageData { - title: string, - date: string, - permalink: string, - content: string, - image?: string, - preview: string, - matchCount: number -} - -interface match { - start: number, - end: number -} - -/** - * Escape HTML tags as HTML entities - * Edited from: - * @link https://stackoverflow.com/a/5499821 - */ -const tagsToReplace = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - '…': '…' -}; - -function replaceTag(tag) { - return tagsToReplace[tag] || tag; -} - -function replaceHTMLEnt(str) { - return str.replace(/[&<>"]/g, replaceTag); -} - -function escapeRegExp(string) { - return string.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&'); -} - -class Search { - private data: pageData[]; - private form: HTMLFormElement; - private input: HTMLInputElement; - private list: HTMLDivElement; - private resultTitle: HTMLHeadElement; - private resultTitleTemplate: 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 - */ - private static processMatches(str: string, matches: match[], ellipsis: boolean = true, charLimit = 140, offset = 20): string { - matches.sort((a, b) => { - return a.start - b.start; - }); - - let i = 0, - lastIndex = 0, - charCount = 0; - - const resultArray: string[] = []; - - while (i < matches.length) { - const item = 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; - } - - let j = 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(`${replaceHTMLEnt(str.substring(item.start, end))}`); - charCount += end - item.start; - - i = j; - lastIndex = end; - - if (ellipsis && charCount > charLimit) break; - } - - /// Add the rest of the string - if (lastIndex < str.length) { - let end = str.length; - if (ellipsis) end = Math.min(end, lastIndex + offset); - - resultArray.push(`${replaceHTMLEnt(str.substring(lastIndex, end))}`); - - if (ellipsis && end != str.length) { - resultArray.push(` [...]`); - } - } - - return resultArray.join(''); - } - - private async searchKeywords(keywords: string[]) { - const rawData = await this.getData(); - const results: pageData[] = []; - - const regex = new RegExp(keywords.filter((v, index, arr) => { - arr[index] = escapeRegExp(v); - return v.trim() !== ''; - }).join('|'), 'gi'); - - for (const item of rawData) { - const titleMatches: match[] = [], - contentMatches: match[] = []; - - let result = { - ...item, - preview: '', - matchCount: 0 - } - - const contentMatchAll = item.content.matchAll(regex); - for (const match of Array.from(contentMatchAll)) { - contentMatches.push({ - start: match.index, - end: match.index + match[0].length - }); - } - - const titleMatchAll = item.title.matchAll(regex); - for (const match of Array.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 - return results.sort((a, b) => { - return b.matchCount - a.matchCount; - }); - } - - private async doSearch(keywords: string[]) { - const startTime = performance.now(); - - const results = await this.searchKeywords(keywords); - this.clear(); - - for (const item of results) { - this.list.append(Search.render(item)); - } - - const endTime = performance.now(); - - this.resultTitle.innerText = this.generateResultTitle(results.length, ((endTime - startTime) / 1000).toPrecision(1)); - } - - private generateResultTitle(resultLen, time) { - return this.resultTitleTemplate.replace("#PAGES_COUNT", resultLen).replace("#TIME_SECONDS", time); - } - - public async getData() { - if (!this.data) { - /// Not fetched yet - const jsonURL = this.form.dataset.json; - this.data = await fetch(jsonURL).then(res => res.json()); - const parser = new DOMParser(); - - for (const item of this.data) { - item.content = parser.parseFromString(item.content, 'text/html').body.innerText; - } - } - - return this.data; - } - - private bindSearchForm() { - let lastSearch = ''; - - const eventHandler = (e) => { - e.preventDefault(); - const keywords = this.input.value.trim(); - - Search.updateQueryString(keywords, true); - - if (keywords === '') { - lastSearch = ''; - return this.clear(); - } - - if (lastSearch === keywords) return; - lastSearch = keywords; - - this.doSearch(keywords.split(' ')); - } - - this.input.addEventListener('input', eventHandler); - this.input.addEventListener('compositionend', eventHandler); - } - - private clear() { - this.list.innerHTML = ''; - this.resultTitle.innerText = ''; - } - - private bindQueryStringChange() { - window.addEventListener('popstate', (e) => { - this.handleQueryString() - }) - } - - private handleQueryString() { - const pageURL = new URL(window.location.toString()); - const keywords = pageURL.searchParams.get('keyword'); - this.input.value = keywords; - - if (keywords) { - this.doSearch(keywords.split(' ')); - } - else { - this.clear() - } - } - - private static updateQueryString(keywords: string, replaceState = false) { - const pageURL = new URL(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()); - } - } - - public static render(item: pageData) { - return
- -
-

-
-
- {item.image && -
- -
- } -
-
; - } -} - -declare global { - interface Window { - searchResultTitleTemplate: string; - } -} - -window.addEventListener('load', () => { - setTimeout(function () { - const searchForm = document.querySelector('.search-form') as HTMLFormElement, - searchInput = searchForm.querySelector('input') as HTMLInputElement, - searchResultList = document.querySelector('.search-result--list') as HTMLDivElement, - searchResultTitle = document.querySelector('.search-result--title') as HTMLHeadingElement; - - new Search({ - form: searchForm, - input: searchInput, - list: searchResultList, - resultTitle: searchResultTitle, - resultTitleTemplate: window.searchResultTitleTemplate - }); - }, 0); -}) - -export default Search; \ No newline at end of file diff --git a/layouts/page/search.html b/layouts/page/search.html deleted file mode 100644 index fbfb74d..0000000 --- a/layouts/page/search.html +++ /dev/null @@ -1,33 +0,0 @@ -{{ define "body-class" }}template-search{{ end }} -{{ define "head" }} - {{- with .OutputFormats.Get "json" -}} - - {{- end -}} -{{ end }} -{{ define "main" }} -
-

- - -

- - -
- -
-

-
-
- - - -{{- $opts := dict "minify" hugo.IsProduction "JSXFactory" "createElement" -}} -{{- $searchScript := resources.Get "ts/search.tsx" | js.Build $opts -}} - - -{{ partialCached "footer/footer" . }} -{{ end }} \ No newline at end of file diff --git a/layouts/page/search.json b/layouts/page/search.json deleted file mode 100644 index a0f5184..0000000 --- a/layouts/page/search.json +++ /dev/null @@ -1,23 +0,0 @@ -{{- $pages := where .Site.RegularPages "Type" "in" .Site.Params.mainSections -}} -{{- $result := slice -}} - -{{- range $pages -}} - {{- $data := dict "title" .Title "date" .Date "permalink" .Permalink "content" (.Plain) -}} - - {{- $image := partialCached "helper/image" (dict "Context" . "Type" "articleList") .RelPermalink "articleList" -}} - {{- if $image.exists -}} - {{- $imagePermalink := "" -}} - {{- if and $image.resource .Page.Site.Params.imageProcessing.cover.enabled -}} - {{- $thumbnail := $image.resource.Fill "120x120" -}} - {{- $imagePermalink = (absURL $thumbnail.Permalink) -}} - {{- else -}} - {{- $imagePermalink = $image.permalink -}} - {{- end -}} - - {{- $data = merge $data (dict "image" (absURL $imagePermalink)) -}} - {{- end -}} - - {{- $result = $result | append $data -}} -{{- end -}} - -{{ jsonify $result }} \ No newline at end of file diff --git a/layouts/partials/widget/search.html b/layouts/partials/widget/search.html index 7b0fc73..e7a391d 100644 --- a/layouts/partials/widget/search.html +++ b/layouts/partials/widget/search.html @@ -1,4 +1,4 @@ -{{- $query := first 1 (where .Context.Site.Pages "Layout" "==" "search") -}} +{{- $query := first 1 (where .Context.Site.Pages "Layout" "==" "archives") -}} {{- if $query -}} {{- $searchPage := index $query 0 -}}
@@ -12,5 +12,5 @@

{{- else -}} - {{- warnf "Search page not found. Create a page with layout: search." -}} + {{- warnf "Archives page not found. Create a page with layout: archives." -}} {{- end -}} \ No newline at end of file