From f730e36d34a0c3c61100944f1799ef0277c731f7 Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Sat, 30 Nov 2024 20:50:31 +0700 Subject: [PATCH] [Add] search (WIP) --- config.yml | 6 ++ content/search.md | 6 ++ layouts/_default/index.json | 5 ++ layouts/_default/search.html | 24 +++++++ static/js/search.js | 136 +++++++++++++++++++++++++++++++++++ 5 files changed, 177 insertions(+) create mode 100644 content/search.md create mode 100644 layouts/_default/index.json create mode 100644 layouts/_default/search.html create mode 100644 static/js/search.js diff --git a/config.yml b/config.yml index 1bf77d1..1d403ed 100644 --- a/config.yml +++ b/config.yml @@ -112,3 +112,9 @@ languages: url: https://tiennm99.github.io/webcv - name: PDF CV url: https://tiennm99.github.io/cv/miti99.pdf + +outputs: + home: + - html + - rss + - json diff --git a/content/search.md b/content/search.md new file mode 100644 index 0000000..17b7150 --- /dev/null +++ b/content/search.md @@ -0,0 +1,6 @@ +--- +title: "Search" +sitemap: + priority : 0.1 +layout: "search" +--- diff --git a/layouts/_default/index.json b/layouts/_default/index.json new file mode 100644 index 0000000..c93f805 --- /dev/null +++ b/layouts/_default/index.json @@ -0,0 +1,5 @@ +{{- $.Scratch.Add "index" slice -}} +{{- range .Site.RegularPages -}} + {{- $.Scratch.Add "index" (dict "title" .Title "tags" .Params.tags "categories" .Params.categories "contents" .Plain "permalink" .Permalink) -}} +{{- end -}} +{{- $.Scratch.Get "index" | jsonify -}} diff --git a/layouts/_default/search.html b/layouts/_default/search.html new file mode 100644 index 0000000..7ba203d --- /dev/null +++ b/layouts/_default/search.html @@ -0,0 +1,24 @@ +{{ define "main" }} + +
+
+
Loading...
+ + + + + + +
+ +{{ end }} diff --git a/static/js/search.js b/static/js/search.js new file mode 100644 index 0000000..6d1595d --- /dev/null +++ b/static/js/search.js @@ -0,0 +1,136 @@ +var summaryInclude = 180; +var fuseOptions = { + shouldSort: true, + includeMatches: true, + includeScore: true, + tokenize: true, + location: 0, + distance: 100, + minMatchCharLength: 1, + keys: [ + {name: "title", weight: 0.45}, + {name: "contents", weight: 0.4}, + {name: "tags", weight: 0.1}, + {name: "categories", weight: 0.05} + ] +}; + +// ============================= +// Search +// ============================= + +var inputBox = document.getElementById('search-query'); +if (inputBox !== null) { + var searchQuery = param("q"); + if (searchQuery) { + inputBox.value = searchQuery || ""; + executeSearch(searchQuery, false); + } else { + document.getElementById('search-results').innerHTML = '

Please enter a word or phrase above, or see all tags.

'; + } +} + +function executeSearch(searchQuery) { + + show(document.querySelector('.search-loading')); + + fetch('/index.json').then(function (response) { + if (response.status !== 200) { + console.log('Looks like there was a problem. Status Code: ' + response.status); + return; + } + // Examine the text in the response + response.json().then(function (pages) { + var fuse = new Fuse(pages, fuseOptions); + var result = fuse.search(searchQuery); + if (result.length > 0) { + populateResults(result); + } else { + document.getElementById('search-results').innerHTML = '

No matches found

'; + } + hide(document.querySelector('.search-loading')); + }) + .catch(function (err) { + console.log('Fetch Error :-S', err); + }); + }); +} + +function populateResults(results) { + + var searchQuery = document.getElementById("search-query").value; + var searchResults = document.getElementById("search-results"); + + // pull template from hugo template definition + var templateDefinition = document.getElementById("search-result-template").innerHTML; + + results.forEach(function (value, key) { + + var contents = value.item.contents; + var snippet = ""; + var snippetHighlights = []; + + snippetHighlights.push(searchQuery); + snippet = contents.substring(0, summaryInclude * 2) + '…'; + + //replace values + var tags = "" + if (value.item.tags) { + value.item.tags.forEach(function (element) { + tags = tags + "" + "#" + element + " " + }); + } + + var output = render(templateDefinition, { + key: key, + title: value.item.title, + link: value.item.permalink, + tags: tags, + categories: value.item.categories, + snippet: snippet + }); + searchResults.innerHTML += output; + + snippetHighlights.forEach(function (snipvalue, snipkey) { + var instance = new Mark(document.getElementById('summary-' + key)); + instance.mark(snipvalue); + }); + + }); +} + +function render(templateString, data) { + var conditionalMatches, conditionalPattern, copy; + conditionalPattern = /\$\{\s*isset ([a-zA-Z]*) \s*\}(.*)\$\{\s*end\s*}/g; + //since loop below depends on re.lastInxdex, we use a copy to capture any manipulations whilst inside the loop + copy = templateString; + while ((conditionalMatches = conditionalPattern.exec(templateString)) !== null) { + if (data[conditionalMatches[1]]) { + //valid key, remove conditionals, leave contents. + copy = copy.replace(conditionalMatches[0], conditionalMatches[2]); + } else { + //not valid, remove entire section + copy = copy.replace(conditionalMatches[0], ''); + } + } + templateString = copy; + //now any conditionals removed we can do simple substitution + var key, find, re; + for (key in data) { + find = '\\$\\{\\s*' + key + '\\s*\\}'; + re = new RegExp(find, 'g'); + templateString = templateString.replace(re, data[key]); + } + return templateString; +} + +// Helper Functions +function show(elem) { + elem.style.display = 'block'; +} +function hide(elem) { + elem.style.display = 'none'; +} +function param(name) { + return decodeURIComponent((location.search.split(name + '=')[1] || '').split('&')[0]).replace(/\+/g, ' '); +}