From 7f5b1f14f1890d4cb62553c4a57aa2e28f581474 Mon Sep 17 00:00:00 2001
From: Silvio Giebl <silvio.giebl@hivemq.com>
Date: Sun, 29 Dec 2019 19:19:00 +0100
Subject: [PATCH] Search for sections (configurable via search.heading_level)
 Added more search configurations Display "no results found"

---
 _config.yml                     |  5 ++
 _layouts/default.html           |  4 ++
 _sass/search.scss               | 47 ++++++++++++++---
 assets/js/just-the-docs.js      | 91 +++++++++++++++++++++++----------
 assets/js/zzzz-search-data.json | 55 ++++++++++++++++++--
 5 files changed, 161 insertions(+), 41 deletions(-)

diff --git a/_config.yml b/_config.yml
index a0906708..c021b357 100644
--- a/_config.yml
+++ b/_config.yml
@@ -26,6 +26,11 @@ exclude: ["node_modules/", "*.gemspec", "*.gem", "Gemfile", "Gemfile.lock", "pac
 
 # Enable or disable the site search
 search_enabled: true
+search:
+  heading_level: 2
+  preview_words_before: 5
+  preview_words_after: 10
+  rel_url: false
 
 # Set the search token separator for hyphenated-word search:
 search_tokenizer_separator: /[\s/]+/
diff --git a/_layouts/default.html b/_layouts/default.html
index 4a66b45e..81dd721d 100644
--- a/_layouts/default.html
+++ b/_layouts/default.html
@@ -24,6 +24,10 @@ layout: table_wrappers
       <title>Expand</title>
       <path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/><path d="M0 0h24v24H0z" fill="none"/>
     </symbol>
+    <symbol id="svg-doc" viewBox="0 0 24 24">
+      <title>Document</title>
+      <path fill="none" d="M0 0h24v24H0V0z"/><path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zM6 20V4h7v5h5v11H6z"/>
+    </symbol>
   </svg>
 
   <div class="side-bar">
diff --git a/_sass/search.scss b/_sass/search.scss
index 6d476098..54f3211b 100644
--- a/_sass/search.scss
+++ b/_sass/search.scss
@@ -131,29 +131,50 @@
   &.active {
     background-color: $feedback-color;
   }
-
-  @include mq(md) {
-    padding-right: $sp-4;
-    padding-left: $sp-4;
-  }
 }
 
 .search-result-title {
   display: block;
   padding-top: $sp-2;
-  padding-right: $sp-4;
   padding-bottom: $sp-2;
 
   @include mq(sm) {
     display: inline-block;
     width: 40%;
+    padding-right: $sp-2;
     word-wrap: break-word;
     vertical-align: top;
   }
 }
 
+.search-result-doc {
+  display: flex;
+  align-items: center;
+
+  &.search-result-doc-parent {
+    opacity: 0.5;
+    @include fs-3;
+
+    @include mq(md) {
+      @include fs-2;
+    }
+  }
+
+  .search-result-icon {
+    width: $sp-4;
+    height: $sp-4;
+    margin-right: $sp-2;
+    fill: $link-color;
+  }
+}
+
+.search-result-section {
+  margin-left: #{$sp-4 + $sp-2};
+}
+
 .search-result-rel-url {
   display: block;
+  margin-left: #{$sp-4 + $sp-2};
   overflow: hidden;
   color: $search-result-preview-color;
   text-overflow: ellipsis;
@@ -166,6 +187,7 @@
   padding-top: $sp-2;
   padding-bottom: $sp-2;
   padding-left: $sp-4;
+  margin-left: $sp-2;
   color: $search-result-preview-color;
   border-left: $border;
   border-left-color: $border-color;
@@ -174,13 +196,22 @@
   @include mq(sm) {
     display: inline-block;
     width: 60%;
+    padding-left: $sp-2;
+    margin-left: 0;
     vertical-align: top;
   }
 }
 
 .search-result-highlight {
   font-weight: bold;
-  color: $link-color;
+}
+
+.search-no-result {
+  padding-top: $sp-2;
+  padding-right: $sp-3;
+  padding-bottom: $sp-2;
+  padding-left: $sp-3;
+  @include fs-3;
 }
 
 .search-button {
@@ -205,7 +236,7 @@
   z-index: 1;
   width: 0;
   height: 0;
-  background-color: rgba(0, 0, 0, 0.2);
+  background-color: rgba(0, 0, 0, 0.3);
   opacity: 0;
   transition: opacity ease $transition-duration, width 0s $transition-duration, height 0s $transition-duration;
 }
diff --git a/assets/js/just-the-docs.js b/assets/js/just-the-docs.js
index 49479f6b..0d2593ba 100644
--- a/assets/js/just-the-docs.js
+++ b/assets/js/just-the-docs.js
@@ -81,7 +81,9 @@ function initSearch() {
         this.ref('id');
         this.field('title', { boost: 200 });
         this.field('content', { boost: 2 });
-        this.field('url');
+        {% if site.search.rel_url != false -%}
+        this.field('relUrl');
+        {%- endif %}
         this.metadataWhitelist = ['position']
 
         for (var i in docs) {
@@ -89,7 +91,9 @@ function initSearch() {
             id: i,
             title: docs[i].title,
             content: docs[i].content,
-            url: docs[i].url
+            {% if site.search.rel_url != false -%}
+            relUrl: docs[i].relUrl
+            {%- endif %}
           });
         }
       });
@@ -112,20 +116,29 @@ function initSearch() {
     var searchInput = document.getElementById('search-input');
     var searchResults = document.getElementById('search-results');
     var mainHeader = document.getElementById('main-header');
+    var currentInput;
 
-    function clearResults() {
-      searchResults.innerHTML = '';
-      hideResults();
+    function showSearch() {
+      document.documentElement.classList.add('search-active');
     }
 
-    function hideResults() {
+    function hideSearch() {
       document.documentElement.classList.remove('search-active');
     }
 
     function update() {
-      clearResults();
-
       var input = searchInput.value;
+      if (input === '') {
+        hideSearch();
+      } else {
+        showSearch();
+        window.scroll(0, window.scrollY + mainHeader.getBoundingClientRect().top);
+      }
+      if (input === currentInput) {
+        return;
+      }
+      currentInput = input;
+      searchResults.innerHTML = '';
       if (input === '') {
         return;
       }
@@ -140,10 +153,13 @@ function initSearch() {
         });
       });
 
-      if (results.length > 0) {
-        window.scroll(0, window.scrollY + mainHeader.getBoundingClientRect().top);
-        document.documentElement.classList.add('search-active');
+      if (results.length == 0) {
+        var noResultsDiv = document.createElement('div');
+        noResultsDiv.classList.add('search-no-result');
+        noResultsDiv.innerText = 'No results found';
+        searchResults.appendChild(noResultsDiv);
 
+      } else {
         var resultsList = document.createElement('ul');
         resultsList.classList.add('search-results-list');
         searchResults.appendChild(resultsList);
@@ -163,13 +179,26 @@ function initSearch() {
 
           var resultTitle = document.createElement('div');
           resultTitle.classList.add('search-result-title');
-          resultTitle.innerText = doc.title;
           resultLink.appendChild(resultTitle);
 
-          var resultRelUrl = document.createElement('span');
-          resultRelUrl.classList.add('search-result-rel-url');
-          resultRelUrl.innerText = doc.relUrl;
-          resultTitle.appendChild(resultRelUrl);
+          var resultDoc = document.createElement('div');
+          resultDoc.classList.add('search-result-doc');
+          resultDoc.innerHTML = '<svg viewBox="0 0 24 24" class="search-result-icon"><use xlink:href="#svg-doc"></use></svg>';
+          resultTitle.appendChild(resultDoc);
+
+          var resultDocSpan = document.createElement('span');
+          resultDocSpan.innerText = doc.doc;
+          resultDoc.appendChild(resultDocSpan);
+          var resultDocOrSection = resultDocSpan;
+
+          if (doc.doc != doc.title) {
+            resultDoc.classList.add('search-result-doc-parent');
+            var resultSection = document.createElement('div');
+            resultSection.classList.add('search-result-section');
+            resultSection.innerText = doc.title;
+            resultTitle.appendChild(resultSection);
+            resultDocOrSection = resultSection;
+          }
 
           var metadata = result.matchData.metadata;
           var contentFound = false;
@@ -178,9 +207,9 @@ function initSearch() {
               var position = metadata[j].title.position[0];
               var start = position[0];
               var end = position[0] + position[1];
-              resultTitle.innerHTML = doc.title.substring(0, start) + '<span class="search-result-highlight">' + doc.title.substring(start, end) + '</span>' + doc.title.substring(end, doc.title.length)+'<span class="search-result-rel-url">'+doc.relUrl+'</span>';
-
-            } else if (metadata[j].content && !contentFound) {
+              resultDocOrSection.innerHTML = doc.title.substring(0, start) + '<span class="search-result-highlight">' + doc.title.substring(start, end) + '</span>' + doc.title.substring(end, doc.title.length);
+            }
+            if (metadata[j].content && !contentFound) {
               contentFound = true;
 
               var position = metadata[j].content.position[0];
@@ -190,10 +219,10 @@ function initSearch() {
               var previewEnd = end;
               var ellipsesBefore = true;
               var ellipsesAfter = true;
-              for (var k = 0; k < 3; k++) {
+              for (var k = 0; k < {{ site.search.preview_words_before | default: 5 }}; k++) {
                 var nextSpace = doc.content.lastIndexOf(' ', previewStart - 2);
-                var nextDot = doc.content.lastIndexOf('.', previewStart - 2);
-                if ((nextDot > 0) && (nextDot > nextSpace)) {
+                var nextDot = doc.content.lastIndexOf('. ', previewStart - 2);
+                if ((nextDot >= 0) && (nextDot > nextSpace)) {
                   previewStart = nextDot + 1;
                   ellipsesBefore = false;
                   break;
@@ -205,10 +234,10 @@ function initSearch() {
                 }
                 previewStart = nextSpace + 1;
               }
-              for (var k = 0; k < 10; k++) {
+              for (var k = 0; k < {{ site.search.preview_words_after | default: 10 }}; k++) {
                 var nextSpace = doc.content.indexOf(' ', previewEnd + 1);
-                var nextDot = doc.content.indexOf('.', previewEnd + 1);
-                if ((nextDot > 0) && (nextDot < nextSpace)) {
+                var nextDot = doc.content.indexOf('. ', previewEnd + 1);
+                if ((nextDot >= 0) && (nextDot < nextSpace)) {
                   previewEnd = nextDot;
                   ellipsesAfter = false;
                   break;
@@ -236,6 +265,13 @@ function initSearch() {
               resultLink.appendChild(resultPreview);
             }
           }
+
+          {% if site.search.rel_url != false -%}
+          var resultRelUrl = document.createElement('span');
+          resultRelUrl.classList.add('search-result-rel-url');
+          resultRelUrl.innerText = doc.relUrl;
+          resultTitle.appendChild(resultRelUrl);
+          {%- endif %}
         }
       }
     }
@@ -247,9 +283,8 @@ function initSearch() {
     jtd.addEvent(searchInput, 'keyup', function(e){
       switch (e.keyCode) {
         case 27: // When esc key is pressed, hide the results and clear the field
-          clearResults();
           searchInput.value = '';
-          return;
+          break;
         case 38: // arrow up
         case 40: // arrow down
         case 13: // enter
@@ -305,7 +340,7 @@ function initSearch() {
 
     jtd.addEvent(document, 'click', function(e){
       if (e.target != searchInput) {
-        hideResults();
+        hideSearch();
       }
     });
   }
diff --git a/assets/js/zzzz-search-data.json b/assets/js/zzzz-search-data.json
index d38a42f3..d336e8c5 100644
--- a/assets/js/zzzz-search-data.json
+++ b/assets/js/zzzz-search-data.json
@@ -2,12 +2,57 @@
 permalink: /assets/js/search-data.json
 ---
 {
-  {% assign comma = false %}
-  {% for page in site.html_pages %}{% if page.search_exclude != true %}{% if comma == true%},{% endif %}"{{ forloop.index0 }}": {
+  {%- assign i = 0 -%}
+  {% for page in site.html_pages %}
+    {%- if page.title and page.search_exclude != true -%}
+      {%- assign page_content = page.content -%}
+      {%- assign heading_level = site.search.heading_level | default: 1 -%}
+      {%- for j in (2..heading_level) -%}
+        {%- assign tag = '<h' | append: j -%}
+        {%- assign closing_tag = '</h' | append: j -%}
+        {%- assign page_content = page_content | replace: tag, '<h1' | replace: closing_tag, '</h1' -%}
+      {%- endfor -%}
+      {%- assign parts = page_content | split: '<h1' -%}
+      {%- assign title_found = false -%}
+      {% for part in parts offset: 1 %}
+        {%- assign titleAndContent = part | split: '</h1>' -%}
+        {%- assign title = titleAndContent[0] | replace_first: '>', '<h1>' | split: '<h1>' -%}
+        {%- assign title = title[1] | strip_html -%}
+        {%- assign content = titleAndContent[1] -%}
+        {%- assign url = page.url -%}
+        {%- if title == page.title and parts[0] == '' -%}
+          {%- assign title_found = true -%}
+        {%- else -%}
+          {%- assign id = titleAndContent[0] -%}
+          {%- assign id = id | split: 'id="' -%}
+          {%- if id.size == 2 -%}
+            {%- assign id = id[1] -%}
+            {%- assign id = id | split: '"' -%}
+            {%- assign id = id[0] -%}
+            {%- capture url -%}{{ url | append: '#' | append: id }}{%- endcapture -%}
+          {%- endif -%}
+        {%- endif -%}
+  {%- unless i == 0 -%},{%- endunless -%}
+  "{{ i }}": {
+    "doc": "{{ page.title | escape_once }}",
+    "title": "{{ title | escape_once }}",
+    "content": "{{ content | replace: '</h', ' . </h' | replace: '<hr', ' . <hr' | replace: '</p', ' . </p' | replace: '<ul', ' . <ul' | replace: '</ul', ' . </ul' | replace: '<ol', ' . <ol' | replace: '</ol', ' . </ol' | replace: '</tr', ' . </tr' | replace: '<li', ' | <li' | replace: '</li', ' | </li' | replace: '</td', ' | </td' | replace: '<td', ' | <td' | replace: '</th', ' | </th' | replace: '<th', ' | <th' | strip_html | escape_once | remove: 'Table of contents' | replace: '\', '&bsol;' | replace: ' .  .  . ', ' . ' | replace: ' .  . ', ' . ' | normalize_whitespace | replace: '| |', '|' }}",
+    "url": "{{ url | absolute_url }}",
+    "relUrl": "{{ url }}"
+  }
+        {%- assign i = i | plus: 1 -%}
+      {%- endfor -%}
+      {%- unless title_found -%}
+  {%- unless i == 0 -%},{%- endunless -%}
+  "{{ i }}": {
+    "doc": "{{ page.title | escape_once }}",
     "title": "{{ page.title | escape_once }}",
-    "content": "{{ page.content | replace: '</h', ' . </h' | replace: '<hr', ' . <hr' | replace: '</p', ' . </p' | replace: '</ul', ' . </ul' | replace: '</tr', ' . </tr' | replace: '</li', ' | </li' | replace: '</td', ' | </td' | strip_html | escape_once | remove: 'Table of contents' | replace: '\', ' ' | replace: ' .  .  . ', ' . ' | replace: ' .  . ', ' . ' | normalize_whitespace }}",
+    "content": "{{ parts[0] | replace: '</h', ' . </h' | replace: '<hr', ' . <hr' | replace: '</p', ' . </p' | replace: '<ul', ' . <ul' | replace: '</ul', ' . </ul' | replace: '<ol', ' . <ol' | replace: '</ol', ' . </ol' | replace: '</tr', ' . </tr' | replace: '<li', ' | <li' | replace: '</li', ' | </li' | replace: '</td', ' | </td' | replace: '<td', ' | <td' | replace: '</th', ' | </th' | replace: '<th', ' | <th' | strip_html | escape_once | remove: 'Table of contents' | replace: '\', '&bsol;' | replace: ' .  .  . ', ' . ' | replace: ' .  . ', ' . ' | normalize_whitespace | replace: '| |', '|' }}",
     "url": "{{ page.url | absolute_url }}",
     "relUrl": "{{ page.url }}"
-  }{% assign comma = true %}
-  {% endif %}{% endfor %}
+  }
+        {%- assign i = i | plus: 1 -%}
+      {%- endunless -%}
+    {%- endif -%}
+  {% endfor %}
 }
\ No newline at end of file
-- 
GitLab