| {{ define "main" }} |
| <!-- Source: https://makewithhugo.com/add-search-to-a-hugo-site/ --> |
| <main> |
| <div id="search-results"></div> |
| <div class="search-loading">Loading...</div> |
| |
| <script id="search-result-template" type="text/x-js-template"> |
| <div id="summary-${key}"> |
| <h3><a href="${link}">${title}</a></h3> |
| <p class="pb-0 mb-0">${snippet}</p> |
| <p class="opacity-50 pt-0 mt-0"><small>Score: ${score}</small></p> |
| <p> |
| <small> |
| ${ isset tags }Tags: ${tags}<br>${ end } |
| </small> |
| </p> |
| </div> |
| </script> |
| |
| <script src="/js/fuse.min.js" type="text/javascript" crossorigin="anonymous" referrerpolicy="no-referrer"></script> |
| <script src="/js/mark.min.js" type="text/javascript" crossorigin="anonymous" referrerpolicy="no-referrer"></script> |
| <script type="text/javascript"> |
| (function() { |
| const summaryInclude = 180; |
| // See: https://fusejs.io/api/options.html |
| const fuseOptions = { |
| // Indicates whether comparisons should be case sensitive. |
| isCaseSensitive: false, |
| // Whether the score should be included in the result set. |
| // A score of 0 indicates a perfect match, while a score of 1 indicates a complete mismatch. |
| includeScore: true, |
| // Whether the matches should be included in the result set. |
| // When true, each record in the result set will include the indices of the matched characters. |
| // These can consequently be used for highlighting purposes. |
| includeMatches: true, |
| // Only the matches whose length exceeds this value will be returned. |
| // (For instance, if you want to ignore single character matches in the result, set it to 2). |
| minMatchCharLength: 2, |
| // Whether to sort the result list, by score. |
| shouldSort: true, |
| // List of keys that will be searched. |
| // This supports nested paths, weighted search, searching in arrays of strings and objects. |
| keys: [ |
| {name: "title", weight: 0.8}, |
| {name: "contents", weight: 0.7}, |
| // {name: "tags", weight: 0.95}, |
| // {name: "categories", weight: 0.05} |
| ], |
| // --- Fuzzy Matching Options |
| // Determines approximately where in the text is the pattern expected to be found. |
| location: 0, |
| // At what point does the match algorithm give up. |
| // A threshold of 0.0 requires a perfect match (of both letters and location), |
| // a threshold of 1.0 would match anything. |
| threshold: 0.2, |
| // Determines how close the match must be to the fuzzy location (specified by location). |
| // An exact letter match which is distance characters away from the fuzzy location would |
| // score as a complete mismatch. A distance of 0 requires the match be at the exact |
| // location specified. A distance of 1000 would require a perfect match to be within 800 |
| // characters of the location to be found using a threshold of 0.8. |
| distance: 100, |
| // When true, search will ignore location and distance, so it won't matter where in |
| // the string the pattern appears. |
| // |
| // NOTE: These settings are used to calculate the Fuzziness Score (Bitap algorithm) in Fuse.js. |
| // It calculates threshold (default 0.6) * distance (default (100), which gives 60 by |
| // default, meaning it will search for the query-term within 60 characters from the location |
| // (default 0). Since Jena docs may have very long text that includes the query term anywhere |
| // we disable it with ignoreLocation: true. |
| // For more: https://fusejs.io/concepts/scoring-theory.html#scoring-theory |
| ignoreLocation: true, |
| }; |
| |
| // ============================= |
| // Search |
| // ============================= |
| |
| const inputBox = document.getElementById('search-query'); |
| if (inputBox !== null) { |
| const searchQuery = param("q"); |
| if (searchQuery) { |
| inputBox.value = searchQuery || ""; |
| executeSearch(searchQuery, false); |
| } else { |
| document.getElementById('search-results').innerHTML = '<p class="search-results-empty">Please enter a word or phrase above, or see <a href="/tags/">all tags</a>.</p>'; |
| } |
| } |
| |
| 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) { |
| const fuse = new Fuse(pages, fuseOptions); |
| const result = fuse.search(searchQuery); |
| if (result.length > 0) { |
| populateResults(result); |
| } else { |
| document.getElementById('search-results').innerHTML = '<p class=\"search-results-empty\">No matches found</p>'; |
| } |
| hide(document.querySelector('.search-loading')); |
| }) |
| .catch(function (err) { |
| console.log('Fetch Error :-S', err); |
| }); |
| }); |
| } |
| |
| function populateResults(results) { |
| |
| const searchQuery = document.getElementById("search-query").value; |
| const searchResults = document.getElementById("search-results"); |
| |
| searchResults.innerHTML += `<p>Search returned ${results.length} matches.</p>`; |
| |
| // pull template from hugo template definition |
| const templateDefinition = document.getElementById("search-result-template").innerHTML; |
| |
| results.forEach(function (value, key) { |
| const contents = value.item.contents; |
| let snippet = ""; |
| const snippetHighlights = []; |
| |
| snippetHighlights.push(searchQuery); |
| snippet = contents.substring(0, summaryInclude * 2) + '…'; |
| |
| //replace values |
| let tags = "" |
| if (value.item.tags) { |
| value.item.tags.forEach(function (element) { |
| tags = tags + "<a href='/tags/" + element + "'>" + "#" + element + "</a> " |
| }); |
| } |
| |
| const output = render(templateDefinition, { |
| key: key, |
| title: value.item.title, |
| link: value.item.permalink, |
| tags: tags, |
| categories: value.item.categories, |
| snippet: snippet, |
| score: value.score |
| }); |
| searchResults.innerHTML += output; |
| |
| snippetHighlights.forEach(function (snipvalue, snipkey) { |
| const instance = new Mark(document.getElementById('summary-' + key)); |
| instance.mark(snipvalue); |
| }); |
| |
| }); |
| } |
| |
| function render(templateString, data) { |
| let 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 |
| let 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, ' '); |
| } |
| |
| })() |
| </script> |
| </main> |
| |
| {{ end }} |