You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jena.apache.org by ki...@apache.org on 2023/04/07 11:09:34 UTC

[jena-site] 01/03: Add basic search with Fuse.js (search engine), Mark.js (word highlighter) and Hugo (search index)

This is an automated email from the ASF dual-hosted git repository.

kinow pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/jena-site.git

commit 7905e130de1df841d6700c472fb0b4502da26db9
Author: Bruno P. Kinoshita <ki...@users.noreply.github.com>
AuthorDate: Sun Feb 12 09:34:25 2023 +0100

    Add basic search with Fuse.js (search engine), Mark.js (word highlighter) and Hugo (search index)
---
 config.toml                  |   3 +
 layouts/_default/baseof.html |   9 ++-
 layouts/_default/index.json  |   5 ++
 layouts/_default/search.html | 164 +++++++++++++++++++++++++++++++++++++++++++
 source/search/__index.md     |   7 ++
 static/js/fuse.min.js        |   9 +++
 static/js/mark.min.js        |   7 ++
 7 files changed, 203 insertions(+), 1 deletion(-)

diff --git a/config.toml b/config.toml
index ce2da183d..4ab62692b 100644
--- a/config.toml
+++ b/config.toml
@@ -44,3 +44,6 @@ repositorySourceBranch = "main"
 
 siteRepositoryUrl = "https://github.com/apache/jena-site"
 siteRepositorySourceBranch = "main"
+
+[outputs]
+home = ["HTML", "RSS", "JSON"]
diff --git a/layouts/_default/baseof.html b/layouts/_default/baseof.html
index 276a62ad6..497ee4b2c 100644
--- a/layouts/_default/baseof.html
+++ b/layouts/_default/baseof.html
@@ -101,7 +101,14 @@
                     </ul>
                 </li>
             </ul>
-
+            <form class="d-flex" role="search" action="/search" method="GET">
+                <div class="input-group">
+                    <input class="form-control border-end-0 border m-0" type="search" name="q" id="search-query" placeholder="Search...." aria-label="Search">
+                    <button class="btn btn-outline-secondary border-start-0 border" type="submit">
+                        <i class="bi-search"></i>
+                    </button>
+                </div>
+            </form>
             <ul class="navbar-nav ms-auto">
                 <li id="ask" class="nav-item"><a class="nav-link" href="/help_and_support/index.html"><span class="bi-patch-question"></span> Ask</a></li>
 
diff --git a/layouts/_default/index.json b/layouts/_default/index.json
new file mode 100644
index 000000000..ae08324d8
--- /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 000000000..79af2183c
--- /dev/null
+++ b/layouts/_default/search.html
@@ -0,0 +1,164 @@
+{{ 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>${snippet}</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;
+      const 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
+      // =============================
+
+      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");
+
+        // 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) + '&hellip;';
+
+          //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
+          });
+          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 }}
diff --git a/source/search/__index.md b/source/search/__index.md
new file mode 100644
index 000000000..ec17b86dc
--- /dev/null
+++ b/source/search/__index.md
@@ -0,0 +1,7 @@
+---
+title: "Search Results"
+sitemap:
+priority : 0.1
+layout: "search"
+slug: index
+---
diff --git a/static/js/fuse.min.js b/static/js/fuse.min.js
new file mode 100644
index 000000000..d9da0b3c0
--- /dev/null
+++ b/static/js/fuse.min.js
@@ -0,0 +1,9 @@
+/**
+ * Fuse.js v6.6.2 - Lightweight fuzzy-search (http://fusejs.io)
+ *
+ * Copyright (c) 2022 Kiro Risk (http://kiro.me)
+ * All Rights Reserved. Apache Software License 2.0
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ */
+var e,t;e=this,t=function(){"use strict";function e(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function t(t){for(var n=1;n<arguments.length;n++){var r=null!=arguments[n]?arguments[n]:{};n%2?e(Object(r),!0).forEach((function(e){c(t,e,r[e])})):Object.getOwnPropertyDescriptors?Object.defineProperties(t,Object.getOwnPropertyDe [...]
diff --git a/static/js/mark.min.js b/static/js/mark.min.js
new file mode 100644
index 000000000..1eea05338
--- /dev/null
+++ b/static/js/mark.min.js
@@ -0,0 +1,7 @@
+/*!***************************************************
+* mark.js v8.11.1
+* https://markjs.io/
+* Copyright (c) 2014–2018, Julian Kühnel
+* Released under the MIT license https://git.io/vwTVl
+*****************************************************/
+!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.Mark=t()}(this,function(){"use strict";var e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},t=function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")},n=function(){ [...]