Show tag matches in search results + hide search highlights
authorAdam Dullage <redacted>
Mon, 5 Sep 2022 12:43:12 +0000 (13:43 +0100)
committerAdam Dullage <redacted>
Mon, 5 Sep 2022 12:43:12 +0000 (13:43 +0100)
flatnotes/flatnotes.py
flatnotes/models.py
flatnotes/src/classes.js
flatnotes/src/components/SearchResults.vue
flatnotes/src/constants.js
flatnotes/src/helpers.js

index 609f415a6f23aea84c79506ac1826c72e8f753f7..70fc4893f27502fb7de57c210e45209089788fd1 100644 (file)
@@ -12,6 +12,7 @@ from whoosh.analysis import CharsetFilter, KeywordAnalyzer, StemmingAnalyzer
 from whoosh.fields import ID, STORED, TEXT, SchemaClass
 from whoosh.index import Index
 from whoosh.qparser import MultifieldParser
+from whoosh.query import Every
 from whoosh.searching import Hit
 from whoosh.support.charset import accent_map
 
@@ -67,6 +68,7 @@ class Note:
 
     @title.setter
     def title(self, new_title):
+        new_title = new_title.strip()
         if not self._is_valid_title(new_title):
             raise InvalidTitleError
         new_filepath = os.path.join(
@@ -101,10 +103,26 @@ class Note:
 class SearchResult(Note):
     def __init__(self, flatnotes: "Flatnotes", hit: Hit) -> None:
         super().__init__(flatnotes, strip_ext(hit["filename"]))
-        self._title_highlights = hit.highlights("title", text=self.title)
-        self._content_highlights = hit.highlights(
-            "content",
-            text=self.content,
+
+        self._matched_fields = self._get_matched_fields(hit.matched_terms())
+
+        self._title_highlights = (
+            hit.highlights("title", text=self.title)
+            if "title" in self._matched_fields
+            else None
+        )
+        self._content_highlights = (
+            hit.highlights(
+                "content",
+                text=self.content,
+            )
+            if "content" in self._matched_fields
+            else None
+        )
+        self._tag_matches = (
+            [field[1] for field in hit.matched_terms() if field[0] == "tags"]
+            if "tags" in self._matched_fields
+            else None
         )
 
     @property
@@ -115,6 +133,16 @@ class SearchResult(Note):
     def content_highlights(self):
         return self._content_highlights
 
+    @property
+    def tag_matches(self):
+        return self._tag_matches
+
+    @staticmethod
+    def _get_matched_fields(matched_terms):
+        """Return a set of matched fields from a set of ('field', 'term') "
+        "tuples generated by whoosh.searching.Hit.matched_terms()."""
+        return set([matched_term[0] for matched_term in matched_terms])
+
 
 class Flatnotes(object):
     TAG_SECTION_RE = re.compile(r"(?:\s+#\w+)+$")
@@ -250,8 +278,11 @@ class Flatnotes(object):
         """Search the index for the given term."""
         self.update_index_debounced()
         with self.index.searcher() as searcher:
-            query = MultifieldParser(
-                ["title", "content", "tags"], self.index.schema
-            ).parse(term)
-            results = searcher.search(query, limit=None)
+            if term == "*":
+                query = Every()
+            else:
+                query = MultifieldParser(
+                    ["title", "content", "tags"], self.index.schema
+                ).parse(term)
+            results = searcher.search(query, limit=None, terms=True)
             return tuple(SearchResult(self, hit) for hit in results)
index ccb2b2da5a322671ec713f3b9dbeff45fd1eb353..9798a52e67de3ca11626b543df61c17cedc8e291 100644 (file)
@@ -1,4 +1,4 @@
-from typing import Dict, Optional
+from typing import Dict, List, Optional
 
 from helpers import CamelCaseBaseModel
 
@@ -34,6 +34,7 @@ class SearchResultModel(CamelCaseBaseModel):
     last_modified: int
     title_highlights: Optional[str]
     content_highlights: Optional[str]
+    tag_matches: Optional[List[str]]
 
     @classmethod
     def dump(self, search_result: SearchResult) -> Dict:
@@ -42,4 +43,5 @@ class SearchResultModel(CamelCaseBaseModel):
             "lastModified": search_result.last_modified,
             "titleHighlights": search_result.title_highlights,
             "contentHighlights": search_result.content_highlights,
+            "tagMatches": search_result.tag_matches,
         }
index 14ce77c94347ba2cd24b972714af88bd7de4288a..ecff15a5b09597cb6cb2d3a896ca6e5beb624beb 100644 (file)
@@ -21,10 +21,11 @@ class Note {
 }
 
 class SearchResult extends Note {
-  constructor(title, lastModified, titleHighlights, contentHighlights) {
+  constructor(title, lastModified, titleHighlights, contentHighlights, tagMatches) {
     super(title, lastModified);
     this.titleHighlights = titleHighlights;
     this.contentHighlights = contentHighlights;
+    this.tagMatches = tagMatches;
   }
 
   get titleHighlightsOrTitle() {
index 3c1d13cc891cc5660f3c321fda79176d2f4e9d70..016ae8cdbf636076c7b5f7982333fa73aa09fade 100644 (file)
 
     <!-- Search Results Loaded -->
     <div v-else>
+      <button type="button" class="bttn mb-3" @click="toggleHighlights">
+        <b-icon :icon="showHighlights ? 'eye-slash' : 'eye'"></b-icon>
+        {{ showHighlights ? "Hide" : "Show" }} Highlights
+      </button>
       <div
         v-for="result in searchResults"
         :key="result.title"
-        class="bttn result mb-3"
+        class="bttn result mb-2"
       >
         <a :href="result.href" @click.prevent="openNote(result.href)">
+          <div class="d-flex align-items-center">
+            <p
+              class="result-title"
+              v-html="
+                showHighlights ? result.titleHighlightsOrTitle : result.title
+              "
+            ></p>
+          </div>
           <p
-            class="font-weight-bold mb-0"
-            v-html="result.titleHighlightsOrTitle"
+            v-show="showHighlights"
+            class="result-contents"
+            v-html="result.contentHighlights"
           ></p>
-          <p class="result-contents" v-html="result.contentHighlights"></p>
+          <div v-show="showHighlights">
+            <span v-for="tag in result.tagMatches" :key="tag" class="tag mr-2"
+              >#{{ tag }}</span
+            >
+          </div>
         </a>
       </div>
     </div>
   margin: 0;
 }
 
+.result-title {
+  color: $text;
+}
+
 .result-contents {
   color: $muted-text;
 }
   font-weight: bold;
   color: $logo-key-colour;
 }
+
+.tag {
+  color: white;
+  font-size: 14px;
+  background-color: $logo-key-colour;
+  padding: 2px 6px;
+  border-radius: 4px;
+}
 </style>
 
 <script>
+import * as constants from "../constants";
+import * as helpers from "../helpers";
+
 import EventBus from "../eventBus";
 import LoadingIndicator from "./LoadingIndicator";
 import { SearchResult } from "../classes";
@@ -73,6 +105,7 @@ export default {
       searchFailedMessage: "Failed to load Search Results",
       searchFailedIcon: null,
       searchResults: null,
+      showHighlights: true,
     };
   },
 
@@ -101,7 +134,8 @@ export default {
                   result.title,
                   result.lastModified,
                   result.titleHighlights,
-                  result.contentHighlights
+                  result.contentHighlights,
+                  result.tagMatches
                 )
               );
             });
@@ -119,6 +153,14 @@ export default {
       EventBus.$emit("navigate", href);
     },
 
+    toggleHighlights: function () {
+      this.showHighlights = !this.showHighlights;
+      helpers.setSearchParam(
+        constants.params.showHighlights,
+        this.showHighlights
+      );
+    },
+
     init: function () {
       this.getSearchResults();
     },
@@ -126,6 +168,15 @@ export default {
 
   created: function () {
     this.init();
+
+    let showHighlightsParam = helpers.getSearchParam(
+      constants.params.showHighlights
+    );
+    if (typeof showHighlightsParam == "string") {
+      this.showHighlights = showHighlightsParam === "true";
+    } else {
+      this.showHighlights = true;
+    }
   },
 };
 </script>
index 4733166b7387faddb225560ec385cdacc2d3bcf4..858eafc5d0146680dba360d4685a98a1b0ce8127 100644 (file)
@@ -9,7 +9,11 @@ export const basePaths = {
 };
 
 // Params
-export const params = { searchTerm: "term", redirect: "redirect" };
+export const params = {
+  searchTerm: "term",
+  redirect: "redirect",
+  showHighlights: "showHighlights",
+};
 
 // Other
 export const alphabet = [
index 8e9bb7dc6a7f800d7fc0f1dd861027b4f7a9c016..cc0763a0685bb784108acf6270019d55b59444cf 100644 (file)
@@ -2,3 +2,11 @@ export function getSearchParam(paramName) {
   let urlSearchParams = new URLSearchParams(window.location.search);
   return urlSearchParams.get(paramName);
 }
+
+export function setSearchParam(paramName, value) {
+  let url = new URL(window.location.href);
+  let urlSearchParams = new URLSearchParams(url.search);
+  urlSearchParams.set(paramName, value);
+  url.search = urlSearchParams.toString();
+  window.history.replaceState({}, "", url.toString());
+}
git clone https://git.99rst.org/PROJECT