From b0430e4915785c266f6f8c3e7037926b8ee56552 Mon Sep 17 00:00:00 2001
From: Torey <torey.scheer@solo.io>
Date: Mon, 10 Jun 2024 09:18:06 -0400
Subject: [PATCH 1/4] Fixed flexsearch freezing when using `tokenize:full`

---
 layouts/partials/docs/footer/flexsearch.html | 310 ++++++++++---------
 1 file changed, 169 insertions(+), 141 deletions(-)

diff --git a/layouts/partials/docs/footer/flexsearch.html b/layouts/partials/docs/footer/flexsearch.html
index 3d252ff1..3d477475 100644
--- a/layouts/partials/docs/footer/flexsearch.html
+++ b/layouts/partials/docs/footer/flexsearch.html
@@ -39,12 +39,11 @@
   };
 
   document.addEventListener('click', function(event) {
+    var isClickInsideElement = suggestions.contains(event.target);
 
-  var isClickInsideElement = suggestions.contains(event.target);
-
-  if (!isClickInsideElement) {
+    if (!isClickInsideElement) {
       suggestions.classList.add('d-none');
-  }
+    }
 
   });
 
@@ -82,29 +81,61 @@
   - https://github.com/nextapps-de/flexsearch#index-documents-field-search
   - https://raw.githack.com/nextapps-de/flexsearch/master/demo/autocomplete.html
   */
-
   (function(){
-
-      var indexSectionMap = {};
-      function getIndex(section) {
-          if (!indexSectionMap[section]) {
-              indexSectionMap[section] = new FlexSearch.Document({
-                  // charset: "latin:default",
-                  tokenize: {{ .Site.Params.flexsearch.tokenize | default "forward" }},
-                  minlength: {{ .Site.Params.flexsearch.minQueryChar | default 0}},
-                  cache: {{ .Site.Params.flexsearch.cache | default 100 }},
-                  optimize: {{ .Site.Params.flexsearch.optimize | default true }},
-                  document: {
-                  id: 'id',
-                  store: [
-                      "href", "title", "description"
-                  ],
-                  index: ["title", "description", "content"]
-                  }
-              });
+    const siteFlexJson = JSON.parse({{ .Site.Params.flexsearch | jsonify }});
+    const siteFlexConfig = {
+      enabled: siteFlexJson.enabled ?? true,
+      tokenize: siteFlexJson.tokenize ?? "full",
+      minLength: siteFlexJson.minquerychar ?? 0,
+      cache: siteFlexJson.cache ?? 100,
+      maxResult: siteFlexJson.maxresult ?? 5,
+      searchSectionsIndex: siteFlexJson.searchsectionsindex ?? [],
+    };
+
+    var indexSectionMap = {};
+    function getIndex(section) {
+      if (!indexSectionMap[section]) {
+        indexSectionMap[section] = new FlexSearch.Document({
+          // charset: "latin:default",
+          tokenize: siteFlexConfig.tokenize,
+          minlength: siteFlexConfig.minLength,
+          cache: siteFlexConfig.cache,
+          optimize: siteFlexConfig.optimize,
+          document: {
+            id: 'id',
+            store: ["href", "title", "description"],
+            index: ["title", "description", "content"]
           }
-          return indexSectionMap[section];
+        });
       }
+      return indexSectionMap[section];
+    }
+
+    /////////////////////////
+    // Index ID Gen
+    /////////////////////////
+    const versionSubversionMap = {};
+    {{ if .Site.Params.versions }}
+        {{ range .Site.Params.versions }}
+            {{ $version := .linkVersion }}
+            versionSubversionMap[{{$version}}] = [];
+            {{ if .subversions }}
+                {{ range .subversions }}
+                    versionSubversionMap[{{$version}}].push({{.id}});
+                {{ end }}
+            {{ end }}
+        {{ end }}
+    {{ end }}
+
+    function getIndexIdOfCurrentPage() {
+      // There doesn't seem to be a perfect way to detect "Section" via JS, but
+      // this should work by finding any cases where something in url exists in search index key
+      const curSection = window.location.pathname.replace(/^\//, '').split('/')
+          .find(part=>!!versionSubversionMap[part]) || "{{ .Site.Params.version }}" || 'main';
+      const curSubversion = window.location.pathname.replace(/^\//, '').split('/')
+          .find(part=>!!versionSubversionMap[curSection].includes(part)) || "";
+      return curSection+'-'+curSubversion;
+    }
 
 
   // Not yet supported: https://github.com/nextapps-de/flexsearch#complex-documents
@@ -112,142 +143,139 @@
 
   // var docs = [
   //     {{ range $index, $page := (where .Site.Pages "Section" "docs") -}}
-  //     {
-  //         id: {{ $index }},
-  //         href: {{ .Permalink }},
-  //         title: {{ .Title }},
-  //         description: {{ .Params.description }},
-  //         content: {{ .Content }}
-  //     },
+  //        { id: {{ $index }}, href: {{ .Permalink }}, title: {{ .Title }}, description: {{ .Params.description }}, content: {{ .Content }} },
   //     {{ end -}}
   // ];
 
-
-  // https://discourse.gohugo.io/t/range-length-or-last-element/3803/2
-
-  // {{ $list := slice }}
-  // {{- if and (isset .Site.Params.flexsearch "searchsectionsindex") (not (eq (len .Site.Params.flexsearch.searchSectionsIndex) 0)) }}
-  //     {{- if eq .Site.Params.docs.searchSectionsIndex "ALL" }}
-  //         {{- $list = .Site.Pages }}
-  //     {{- else }}
-  //         {{- $list = (where .Site.Pages "Type" "in" .Site.Params.flexsearch.searchSectionsIndex) }}
-  //     {{- if (in .Site.Params.flexsearch.searchSectionsIndex "HomePage") }}
-  //         {{ $list = $list | append .Site.Home }}
-  //     {{- end }}
-  //     {{- end }}
-  // {{- else }}
-  //     {{ $version := .Site.Params.version }}
-  //     {{- $list = (where .Site.Pages "Section" $version) }}
-  // {{- end }}
-
-  {{ $list := .Site.Pages }}
-
-    {{ range $index, $element := $list -}}
-        {{ $subversion := or .Params.Params.subversion "" }}
-        {{ range .Ancestors -}}
-            {{ if .Params.Params.subversion }}
-                {{ $subversion = .Params.Params.subversion }}
-            {{ end }}
-        {{ end -}}
-        getIndex("{{ .Section }}-{{ $subversion }}").add(
-            {
-                id: {{ $index }},
-                href: "{{ .RelPermalink }}",
-                title: {{ .Title }},
-                {{ with .Description -}}
-                    description: {{ . }},
-                {{ else -}}
-                    description: {{ .Summary | htmlUnescape | plainify }},
-                {{ end -}}
-                {{ $keywords := "" -}}
-                {{ with .Keywords -}}
-                    {{ $keywords = delimit . ", " }}
-                {{ end -}}
-                content: "{{ $keywords }}" 
-                // content: {{ .Content | htmlUnescape | plainify }}
-            }
-        );
-    {{ end -}}
-
-  const versionSubversionMap = {};
-  {{ if .Site.Params.versions }}
-      {{ range .Site.Params.versions }}
-          {{ $version := .linkVersion }}
-          versionSubversionMap[{{$version}}] = [];
-          {{ if .subversions }}
-              {{ range .subversions }}
-                  versionSubversionMap[{{$version}}].push({{.id}});
+  function populateIndex() {
+    // https://discourse.gohugo.io/t/range-length-or-last-element/3803/2
+
+    // {{ $list := slice }}
+    // {{- if and (isset .Site.Params.flexsearch "searchsectionsindex") (not (eq (len .Site.Params.flexsearch.searchSectionsIndex) 0)) }}
+    //     {{- if eq .Site.Params.docs.searchSectionsIndex "ALL" }}
+    //         {{- $list = .Site.Pages }}
+    //     {{- else }}
+    //         {{- $list = (where .Site.Pages "Type" "in" .Site.Params.flexsearch.searchSectionsIndex) }}
+    //     {{- if (in .Site.Params.flexsearch.searchSectionsIndex "HomePage") }}
+    //         {{ $list = $list | append .Site.Home }}
+    //     {{- end }}
+    //     {{- end }}
+    // {{- else }}
+    //     {{ $version := .Site.Params.version }}
+    //     {{- $list = (where .Site.Pages "Section" $version) }}
+    // {{- end }}
+
+      const indexDocItemList = [];
+
+      {{ $list := .Site.Pages }}
+      {{ range $index, $element := $list -}}
+          {{ $subversion := or .Params.Params.subversion "" }}
+          {{ range .Ancestors -}}
+              {{ if .Params.Params.subversion }}
+                  {{ $subversion = .Params.Params.subversion }}
               {{ end }}
-          {{ end }}
-      {{ end }}
-  {{ end }}
+          {{ end -}}
+
+          {{ $description := "" }}
+          {{ with .Description -}}
+              {{ $description = . }}
+          {{ else -}}
+              {{ $description = .Summary | htmlUnescape | plainify }}
+          {{ end -}}
+
+          {{ $keywords := "" -}}
+          {{ with .Keywords -}}
+              {{ $keywords = delimit . ", " }}
+          {{ end -}}
+
+          indexDocItemList.push([
+              "{{ .Section }}-{{ $subversion }}",
+              {
+                  id: {{ $index }},
+                  href: "{{ .RelPermalink }}",
+                  title: {{ .Title }},
+                  description: {{ $description }},
+                  // content: "{{ $keywords }}"
+                  content: {{ .Content | htmlUnescape | plainify }}
+              }
+          ]);
+      {{ end -}}
+
+      const currentPageIndexId = getIndexIdOfCurrentPage();
+      for(const [indexId, doc] of indexDocItemList) {
+        if(indexId != currentPageIndexId) continue;
+        // Words that are to long cause flexsearch to freeze, so remove them from text we give to index
+        doc.content = doc.content.replaceAll(/\w{40,}/g, '-');
+        getIndex(indexId).add(doc);
+      }
+  }
+  populateIndex();
 
-  search.addEventListener('input', show_results, true);
+  search.addEventListener('input', find_results, true);
 
-  function show_results(){
-      const maxResult = {{ .Site.Params.flexsearch.maxResult | default 5}};
-      const minlength = {{ .Site.Params.flexsearch.minQueryChar | default 0}};
-      var searchQuery = this.value;
-      // There doesn't seem to be a perfect way to detect "Section" via JS, but
-      // this should work by finding any cases where something in url exists in search index key
-      const curSection = window.location.pathname.replace(/^\//, '').split('/')
-          .find(part=>!!versionSubversionMap[part]) || "{{ .Site.Params.version }}" || 'main';
-      const curSubversion = window.location.pathname.replace(/^\//, '').split('/')
-          .find(part=>!!versionSubversionMap[curSection].includes(part)) || "";
-      var index = getIndex(curSection+'-'+curSubversion);
-      var results = index.search(searchQuery, {limit: maxResult, enrich: true});
+  function find_results(){
+    var searchQuery = this.value;
+
+    suggestions.innerHTML = "";
+    suggestions.classList.remove('d-none');
 
-      // flatten results since index.search() returns results for each indexed field
-      const flatResults = new Map(); // keyed by href to dedupe results
-      for (const result of results.flatMap(r => r.result)) {
+    // inform user of search query minimum character requirement
+    const minlength = siteFlexConfig.minLength;
+    if (searchQuery.length < minlength) {
+      const minCharMessage = document.createElement('div')
+      minCharMessage.innerHTML = `Please type at least <strong>${minlength}</strong> characters`
+      minCharMessage.classList.add("suggestion__no-results");
+      suggestions.appendChild(minCharMessage);
+      return;
+    }
+
+    var index = getIndex(getIndexIdOfCurrentPage());
+    var results = index.search(searchQuery, { limit: siteFlexConfig.maxResult, enrich: true });
+    show_results(results, searchQuery);
+  }
+  function show_results(results, searchQuery){
+    // flatten results since index.search() returns results for each indexed field
+    const flatResults = new Map(); // keyed by href to dedupe results
+    for (const result of results.flatMap(r => r.result)) {
       if (flatResults.has(result.doc.href)) continue;
       flatResults.set(result.doc.href, result.doc);
-      }
+    }
 
-      suggestions.innerHTML = "";
-      suggestions.classList.remove('d-none');
+    suggestions.innerHTML = "";
+    suggestions.classList.remove('d-none');
 
-      // inform user of search query minimum character requirement
-      if (searchQuery.length < minlength) {
-          const minCharMessage = document.createElement('div')
-          minCharMessage.innerHTML = `Please type at least <strong>${minlength}</strong> characters`
-          minCharMessage.classList.add("suggestion__no-results");
-          suggestions.appendChild(minCharMessage);
+      // inform user that no results were found
+      if (flatResults.size === 0 && searchQuery) {
+          const noResultsMessage = document.createElement('div')
+          noResultsMessage.innerHTML = {{ i18n "search_no_results" | default "No results for" }} + ` "<strong>${searchQuery}</strong>"`
+          noResultsMessage.classList.add("suggestion__no-results");
+          suggestions.appendChild(noResultsMessage);
           return;
-      } else {
-          // inform user that no results were found
-          if (flatResults.size === 0 && searchQuery) {
-              const noResultsMessage = document.createElement('div')
-              noResultsMessage.innerHTML = {{ i18n "search_no_results" | default "No results for" }} + ` "<strong>${searchQuery}</strong>"`
-              noResultsMessage.classList.add("suggestion__no-results");
-              suggestions.appendChild(noResultsMessage);
-              return;
-          }
       }
 
-      // construct a list of suggestions
-      for(const [href, doc] of flatResults) {
-          const entry = document.createElement('div');
-          suggestions.appendChild(entry);
+    // construct a list of suggestions
+    for(const [href, doc] of flatResults) {
+      const entry = document.createElement('div');
+      suggestions.appendChild(entry);
 
-          const a = document.createElement('a');
-          a.href = href;
-          entry.appendChild(a);
+      const a = document.createElement('a');
+      a.href = href;
+      entry.appendChild(a);
 
-          const title = document.createElement('span');
-          title.textContent = doc.title;
-          title.classList.add("suggestion__title");
-          a.appendChild(title);
+      const title = document.createElement('span');
+      title.textContent = doc.title;
+      title.classList.add("suggestion__title");
+      a.appendChild(title);
 
-          const description = document.createElement('span');
-          description.textContent = doc.description;
-          description.classList.add("suggestion__description");
-          a.appendChild(description);
+      const description = document.createElement('span');
+      description.textContent = doc.description;
+      description.classList.add("suggestion__description");
+      a.appendChild(description);
 
-          suggestions.appendChild(entry);
+      suggestions.appendChild(entry);
 
-          if(suggestions.childElementCount == maxResult) break;
-      }
+      if(suggestions.childElementCount == siteFlexConfig.maxResult) break;
+    }
   }
   }());
 </script>

From 3495d128f2c38f94abb0514aaeb1b31e9b074da6 Mon Sep 17 00:00:00 2001
From: Torey <torey.scheer@solo.io>
Date: Mon, 10 Jun 2024 15:31:34 -0400
Subject: [PATCH 2/4] flexsearch entry description now contains search term,
 bolded

---
 .../plugins/flexsearch/_flexsearch.scss       |   1 +
 layouts/partials/docs/footer/flexsearch.html  | 253 +++++++++++-------
 2 files changed, 162 insertions(+), 92 deletions(-)

diff --git a/assets/docs/scss/custom/plugins/flexsearch/_flexsearch.scss b/assets/docs/scss/custom/plugins/flexsearch/_flexsearch.scss
index 678961c5..b275f606 100644
--- a/assets/docs/scss/custom/plugins/flexsearch/_flexsearch.scss
+++ b/assets/docs/scss/custom/plugins/flexsearch/_flexsearch.scss
@@ -231,6 +231,7 @@
 
 .suggestion__description,
 .suggestion__no-results {
+    display: block;
     color: var(--flexsearch-suggestion-desc-color);
 }
 
diff --git a/layouts/partials/docs/footer/flexsearch.html b/layouts/partials/docs/footer/flexsearch.html
index 3d477475..7c28f965 100644
--- a/layouts/partials/docs/footer/flexsearch.html
+++ b/layouts/partials/docs/footer/flexsearch.html
@@ -92,6 +92,9 @@
       searchSectionsIndex: siteFlexJson.searchsectionsindex ?? [],
     };
 
+    /////////////////////////
+    // Create Index
+    /////////////////////////
     var indexSectionMap = {};
     function getIndex(section) {
       if (!indexSectionMap[section]) {
@@ -103,8 +106,8 @@
           optimize: siteFlexConfig.optimize,
           document: {
             id: 'id',
-            store: ["href", "title", "description"],
-            index: ["title", "description", "content"]
+            store: ["href", "title", "description", "content"],
+            index: ["title", "description", "keywords", "content"]
           }
         });
       }
@@ -137,17 +140,10 @@
       return curSection+'-'+curSubversion;
     }
 
+    /////////////////////////
+    // Populate Index
+    /////////////////////////
 
-  // Not yet supported: https://github.com/nextapps-de/flexsearch#complex-documents
-
-
-  // var docs = [
-  //     {{ range $index, $page := (where .Site.Pages "Section" "docs") -}}
-  //        { id: {{ $index }}, href: {{ .Permalink }}, title: {{ .Title }}, description: {{ .Params.description }}, content: {{ .Content }} },
-  //     {{ end -}}
-  // ];
-
-  function populateIndex() {
     // https://discourse.gohugo.io/t/range-length-or-last-element/3803/2
 
     // {{ $list := slice }}
@@ -165,98 +161,106 @@
     //     {{- $list = (where .Site.Pages "Section" $version) }}
     // {{- end }}
 
-      const indexDocItemList = [];
-
-      {{ $list := .Site.Pages }}
-      {{ range $index, $element := $list -}}
-          {{ $subversion := or .Params.Params.subversion "" }}
-          {{ range .Ancestors -}}
-              {{ if .Params.Params.subversion }}
-                  {{ $subversion = .Params.Params.subversion }}
-              {{ end }}
-          {{ end -}}
-
-          {{ $description := "" }}
-          {{ with .Description -}}
-              {{ $description = . }}
-          {{ else -}}
-              {{ $description = .Summary | htmlUnescape | plainify }}
-          {{ end -}}
-
-          {{ $keywords := "" -}}
-          {{ with .Keywords -}}
-              {{ $keywords = delimit . ", " }}
-          {{ end -}}
-
-          indexDocItemList.push([
-              "{{ .Section }}-{{ $subversion }}",
-              {
-                  id: {{ $index }},
-                  href: "{{ .RelPermalink }}",
-                  title: {{ .Title }},
-                  description: {{ $description }},
-                  // content: "{{ $keywords }}"
-                  content: {{ .Content | htmlUnescape | plainify }}
-              }
-          ]);
-      {{ end -}}
-
-      const currentPageIndexId = getIndexIdOfCurrentPage();
-      for(const [indexId, doc] of indexDocItemList) {
-        if(indexId != currentPageIndexId) continue;
-        // Words that are to long cause flexsearch to freeze, so remove them from text we give to index
-        doc.content = doc.content.replaceAll(/\w{40,}/g, '-');
-        getIndex(indexId).add(doc);
-      }
-  }
-  populateIndex();
-
-  search.addEventListener('input', find_results, true);
+    const indexDocItemList = [];
 
-  function find_results(){
-    var searchQuery = this.value;
+    {{ $list := .Site.Pages }}
+    {{ range $index, $element := $list -}}
+        {{ $subversion := or .Params.Params.subversion "" }}
+        {{ range .Ancestors -}}
+            {{ if .Params.Params.subversion }}
+                {{ $subversion = .Params.Params.subversion }}
+            {{ end }}
+        {{ end -}}
+
+        {{ $description := "" }}
+        {{ with .Description -}}
+            {{ $description = . }}
+        {{ else -}}
+            {{ $description = .Summary | htmlUnescape | plainify }}
+        {{ end -}}
+
+        {{ $keywords := "" -}}
+        {{ with .Keywords -}}
+            {{ $keywords = delimit . ", " }}
+        {{ end -}}
+
+        indexDocItemList.push([
+            "{{ .Section }}-{{ $subversion }}",
+            {
+                id: {{ $index }},
+                href: "{{ .RelPermalink }}",
+                title: {{ .Title }},
+                description: {{ $description }},
+                keywords: "{{ $keywords }}",
+                content: {{ .Content | htmlUnescape | plainify }}
+            }
+        ]);
+    {{ end -}}
+
+    const currentPageIndexId = getIndexIdOfCurrentPage();
+    for(const [indexId, doc] of indexDocItemList) {
+      if(indexId != currentPageIndexId) continue;
+      // Words that are to long cause flexsearch to freeze, so remove them from text we give to index
+      doc.content = doc.content.replaceAll(/\w{40,}/g, '-');
+      getIndex(indexId).add(doc);
+    }
 
-    suggestions.innerHTML = "";
-    suggestions.classList.remove('d-none');
+    /////////////////////////
+    // Search Logic
+    /////////////////////////
+    search.addEventListener('input', find_results, true);
+
+    function find_results(){
+      var searchQuery = this.value;
+
+      suggestions.innerHTML = "";
+      suggestions.classList.remove('d-none');
+
+      // inform user of search query minimum character requirement
+      const minlength = siteFlexConfig.minLength;
+      if (searchQuery.length < minlength) {
+        const minCharMessage = document.createElement('div')
+        minCharMessage.innerHTML = `Please type at least <strong>${minlength}</strong> characters`
+        minCharMessage.classList.add("suggestion__no-results");
+        suggestions.appendChild(minCharMessage);
+        return;
+      }
 
-    // inform user of search query minimum character requirement
-    const minlength = siteFlexConfig.minLength;
-    if (searchQuery.length < minlength) {
-      const minCharMessage = document.createElement('div')
-      minCharMessage.innerHTML = `Please type at least <strong>${minlength}</strong> characters`
-      minCharMessage.classList.add("suggestion__no-results");
-      suggestions.appendChild(minCharMessage);
-      return;
+      var index = getIndex(getIndexIdOfCurrentPage());
+      var results = index.search(searchQuery, { limit: siteFlexConfig.maxResult, enrich: true });
+      show_results(results, searchQuery);
     }
 
-    var index = getIndex(getIndexIdOfCurrentPage());
-    var results = index.search(searchQuery, { limit: siteFlexConfig.maxResult, enrich: true });
-    show_results(results, searchQuery);
-  }
-  function show_results(results, searchQuery){
-    // flatten results since index.search() returns results for each indexed field
-    const flatResults = new Map(); // keyed by href to dedupe results
-    for (const result of results.flatMap(r => r.result)) {
-      if (flatResults.has(result.doc.href)) continue;
-      flatResults.set(result.doc.href, result.doc);
-    }
+    function show_results(results, searchQuery){
+      // flatten results since index.search() returns results for each indexed field
+      const flatResults = new Map(); // keyed by href to dedupe results
+      for (const result of results.flatMap(r => r.result)) {
+        if (flatResults.has(result.doc.href)) continue;
+        flatResults.set(result.doc.href, result.doc);
+      }
 
-    suggestions.innerHTML = "";
-    suggestions.classList.remove('d-none');
+      suggestions.innerHTML = "";
+      suggestions.classList.remove('d-none');
 
       // inform user that no results were found
       if (flatResults.size === 0 && searchQuery) {
           const noResultsMessage = document.createElement('div')
-          noResultsMessage.innerHTML = {{ i18n "search_no_results" | default "No results for" }} + ` "<strong>${searchQuery}</strong>"`
+          noResultsMessage.innerHTML = getNoResultsMessage(searchQuery);
           noResultsMessage.classList.add("suggestion__no-results");
           suggestions.appendChild(noResultsMessage);
           return;
       }
 
-    // construct a list of suggestions
-    for(const [href, doc] of flatResults) {
+      // construct a list of suggestions
+      for(const [href, doc] of flatResults) {
+        suggestions.appendChild( createSuggestion(doc, href, searchQuery) );
+        if(suggestions.childElementCount == siteFlexConfig.maxResult) break;
+      }
+    }
+
+    function createSuggestion(doc, href, searchQuery) {
+      const queryWords = searchQuery.trim().split(' ');
       const entry = document.createElement('div');
-      suggestions.appendChild(entry);
 
       const a = document.createElement('a');
       a.href = href;
@@ -268,14 +272,79 @@
       a.appendChild(title);
 
       const description = document.createElement('span');
-      description.textContent = doc.description;
+      description.innerHTML = getSuggestionDescription(doc, queryWords);
       description.classList.add("suggestion__description");
       a.appendChild(description);
 
-      suggestions.appendChild(entry);
+      return entry;
+    }
 
-      if(suggestions.childElementCount == siteFlexConfig.maxResult) break;
+    function getSuggestionDescription(doc, queryWords) {
+      // For the description, we should do the following:
+      // - If the `doc` description contains all search terms, show that (and highlight them all in that message)
+      // - otherwise try to find them in body, and highlight setence fragment of first instance when found
+      // - else if none found in body (perhaps word only in title/keywords, or regex didn't find it), fallback to description (and highlight any that are found)
+      let desc = doc.description;
+      if(stringContainsAllFuzzyWords(doc.description, queryWords)) {
+        desc = highlightFuzzyWordsInString(doc.description, queryWords);
+      } else {
+        // Find indiviudal segments that contain the search terms
+        const matchedSegments = queryWords.map(queryWord=>{
+          const match = doc.content.match(new RegExp(`(\\W?)(\\w*${queryWord}\\w*)(\\W?)`, 'i'));
+          if(match) {
+            try {
+              let string = '';
+              const { [0]:fullmatch, index } = match;
+              if(index > 0) {
+                // we want words before the matched word - ignore the earliest one though, since it will likely not be a full word.
+                string += doc.content.substring(index-30, index).trim().split(' ').slice(1).join(' ');
+              }
+              string += fullmatch;
+    
+              const indexAfter = index + fullmatch.length;
+              // we want words after the matched word - ignore the last one though, since it will likely not be a full word.
+              string += doc.content.substring(indexAfter, indexAfter+30).trim().split(' ').slice(0, -1).join(' ');
+    
+              return string.trim();
+            }
+            catch(e){}
+          }
+          return null;
+        }).filter(s=>!!s);
+
+        // See if one of the returned segments contains all search terms, and if so return only that one with all words highlighted in it
+        const singleSegmentIfOneExists = matchedSegments.find(segment=>stringContainsAllFuzzyWords(segment, queryWords));
+        if(!matchedSegments.length) {
+          desc = highlightFuzzyWordsInString(doc.description, queryWords);
+        }
+        else if(singleSegmentIfOneExists) {
+          desc = highlightFuzzyWordsInString(singleSegmentIfOneExists, queryWords);
+        }
+        else {
+          desc = matchedSegments.map(str=>highlightFuzzyWordsInString(str, queryWords)).join('<hr />');
+        }
+      }
+      return desc;
+    }
+
+    function stringContainsAllFuzzyWords(string, words) {
+      string = string.toLowerCase();
+      return words.every(w=>string.includes(w.toLowerCase()))
+    }
+
+    function highlightFuzzyWordsInString(string, words) {
+      words.forEach(queryWord=>{
+        const match = string.match(new RegExp(`(\\W?)(\\w*${queryWord}\\w*)(\\W?)`, 'i'));
+        if(match) {
+          const { [0]:fullmatch, [1]:pre, [2]:word, [3]:suff, index } = match;
+          string = string.substring(0, index) + `${pre}<b>${word}</b>${suff}` + string.substring(index+fullmatch.length);
+        }
+      });
+      return string;
+    }
+
+    function getNoResultsMessage(searchQuery) {
+      return {{ i18n "search_no_results" | default "No results for" }} + ` "<strong>${searchQuery}</strong>"`;
     }
-  }
   }());
 </script>

From b2542ed119ed1f1dce070f012ee2a7e2dedd9fc0 Mon Sep 17 00:00:00 2001
From: Torey <torey.scheer@solo.io>
Date: Mon, 10 Jun 2024 15:50:18 -0400
Subject: [PATCH 3/4] fixing bug where search text remains even after
 suggestions cleared

---
 layouts/partials/docs/footer/flexsearch.html | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/layouts/partials/docs/footer/flexsearch.html b/layouts/partials/docs/footer/flexsearch.html
index 7c28f965..479f1db5 100644
--- a/layouts/partials/docs/footer/flexsearch.html
+++ b/layouts/partials/docs/footer/flexsearch.html
@@ -14,7 +14,7 @@
       flexsearchContainer.addEventListener('shown.bs.collapse', function () {
           search.focus();
       });
-      // hide search collapse containder by clicking outside (except top header)
+      // hide search collapse container by clicking outside (except top header)
       var topHeader = document.getElementById("top-header");
       document.addEventListener('click', function(elem) {
           if (!flexsearchContainer.contains(elem.target) && !topHeader.contains(elem.target))
@@ -38,13 +38,13 @@
       }
   };
 
+  // Clears suggestion when clicking out
   document.addEventListener('click', function(event) {
-    var isClickInsideElement = suggestions.contains(event.target);
-
+    var isClickInsideElement = suggestions.contains(event.target) || flexsearchContainer.contains(event.target);
     if (!isClickInsideElement) {
       suggestions.classList.add('d-none');
+      search.value = '';
     }
-
   });
 
   /*

From 0ce90e5b0c6df004888e606383b9e723a3f9ae98 Mon Sep 17 00:00:00 2001
From: Torey <torey.scheer@solo.io>
Date: Mon, 10 Jun 2024 16:26:05 -0400
Subject: [PATCH 4/4] ellipsis

---
 layouts/partials/docs/footer/flexsearch.html | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/layouts/partials/docs/footer/flexsearch.html b/layouts/partials/docs/footer/flexsearch.html
index 479f1db5..abde5995 100644
--- a/layouts/partials/docs/footer/flexsearch.html
+++ b/layouts/partials/docs/footer/flexsearch.html
@@ -305,7 +305,7 @@
               // we want words after the matched word - ignore the last one though, since it will likely not be a full word.
               string += doc.content.substring(indexAfter, indexAfter+30).trim().split(' ').slice(0, -1).join(' ');
     
-              return string.trim();
+              return '…' + string.trim() + '…';
             }
             catch(e){}
           }