diff --git a/assets/ts/search_nohl.tsx b/assets/ts/search_nohl.tsx
new file mode 100644
index 0000000..2922fdd
--- /dev/null
+++ b/assets/ts/search_nohl.tsx
@@ -0,0 +1,252 @@
+interface pageData {
+ title: string,
+ date: string,
+ permalink: string,
+ content: string,
+ image?: string,
+ preview: string,
+ matchCount: 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();
+ }
+
+ private async searchKeywords(keywords: string[]) {
+ const rawData = await this.getData();
+ let results: pageData[] = [];
+
+ /// Sort keywords by their length
+ keywords.sort((a, b) => {
+ return b.length - a.length
+ });
+
+ for (const item of rawData) {
+ let result = {
+ ...item,
+ preview: '',
+ matchCount: 0
+ }
+
+ let matched = false;
+
+ for (const keyword of keywords) {
+ if (keyword === '') continue;
+
+ const regex = new RegExp(escapeRegExp(replaceHTMLEnt(keyword)), 'gi');
+
+ const contentMatch = regex.exec(result.content);
+ regex.lastIndex = 0; /// Reset regex
+
+ const titleMatch = regex.exec(result.title);
+ regex.lastIndex = 0; /// Reset regex
+
+ if (titleMatch || contentMatch) {
+ matched = true;
+ ++result.matchCount;
+
+ let start = 0,
+ end = 100;
+
+ if (contentMatch) {
+ start = contentMatch.index - 20;
+ end = contentMatch.index + 80
+
+ if (start < 0) start = 0;
+ }
+
+ if (result.preview.indexOf(keyword) == -1) {
+ if (start !== 0) result.preview += `[...] `;
+ result.preview += `${result.content.slice(start, end)} `;
+ }
+ }
+ }
+
+ if (matched) {
+ result.preview += '[...]';
+ 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());
+ }
+
+ return this.data;
+ }
+
+ private bindSearchForm() {
+ let lastSearch = '';
+
+ const eventHandler = (e) => {
+ e.preventDefault();
+ const keywords = this.input.value;
+
+ Search.updateQueryString(keywords, true);
+
+ if (keywords === '') {
+ 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;
diff --git a/config.yaml b/config.yaml
index 0e5283c..9d8f412 100644
--- a/config.yaml
+++ b/config.yaml
@@ -119,6 +119,10 @@ params:
tagCloud:
limit: 10
+ # search modes: original, nohighlight, wholeword, highlight
+ search:
+ mode: nohighlight
+
opengraph:
twitter:
# Your Twitter username
diff --git a/layouts/page/search.html b/layouts/page/search.html
index 935b384..a2e98c9 100644
--- a/layouts/page/search.html
+++ b/layouts/page/search.html
@@ -1,6 +1,6 @@
{{ define "body-class" }}template-search{{ end }}
{{ define "head" }}
- {{- with .OutputFormats.Get "json" -}}
+ {{- with .OutputFormats.Get "json" -}}
{{- end -}}
{{ end }}
@@ -23,9 +23,21 @@
window.searchResultTitleTemplate = "{{ T `search.resultTitle` }}"
+{{- $searchfunc := "" -}}
{{- $opts := dict "minify" hugo.IsProduction "JSXFactory" "createElement" -}}
-{{- $searchScript := resources.Get "ts/search.tsx" | js.Build $opts -}}
+{{- if eq .Site.Params.widgets.search.mode "original" -}}
+ {{- $searchfunc = "ts/search.tsx" -}}
+{{- else if eq .Site.Params.widgets.search.mode "nohighlight" -}}
+ {{- $searchfunc = "ts/search_nohl.tsx" -}}
+{{- else if eq .Site.Params.widgets.search.mode "wholeword" -}}
+ {{- $searchfunc = "ts/search_ww.tsx" -}}
+{{- else if eq .Site.Params.widgets.search.mode "highlight" -}}
+ {{- $searchfunc = "ts/search_hl.tsx" -}}
+{{- else -}}
+ {{ warnf "Searching function not defined" }}
+{{- end -}}
+{{- $searchScript := resources.Get $searchfunc | js.Build $opts -}}
{{ partialCached "footer/footer" . }}
-{{ end }}
\ No newline at end of file
+{{ end }}