]> source.dussan.org Git - gitea.git/commitdiff
Implement documentation search (#8937)
authorJohn Olheiser <42128690+jolheiser@users.noreply.github.com>
Wed, 13 Nov 2019 18:03:18 +0000 (12:03 -0600)
committerzeripath <art27@cantab.net>
Wed, 13 Nov 2019 18:03:18 +0000 (18:03 +0000)
* Implement documentation search

Signed-off-by: jolheiser <john.olheiser@gmail.com>
Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com>
13 files changed:
docs/.gitignore
docs/assets/js/search.js [new file with mode: 0644]
docs/config.yaml
docs/content/doc/help.en-us.md
docs/content/doc/help.fr-fr.md [new file with mode: 0644]
docs/content/doc/help.zh-cn.md
docs/content/doc/help.zh-tw.md [new file with mode: 0644]
docs/content/doc/help/search.en-us.md [new file with mode: 0644]
docs/content/doc/help/search.fr-fr.md [new file with mode: 0644]
docs/content/doc/help/search.zh-cn.md [new file with mode: 0644]
docs/content/doc/help/search.zh-tw.md [new file with mode: 0644]
docs/layouts/_default/index.json [new file with mode: 0644]
docs/layouts/doc/search.html [new file with mode: 0644]

index 55ec469a426cf33daf00c85bd3c48c925370bd11..9cd1408bd249fa82627b88695d26698ef9e86d39 100644 (file)
@@ -1,3 +1,4 @@
 public/
 templates/swagger/v1_json.tmpl
 themes/
+resources/
diff --git a/docs/assets/js/search.js b/docs/assets/js/search.js
new file mode 100644 (file)
index 0000000..72d94c9
--- /dev/null
@@ -0,0 +1,176 @@
+function ready(fn) {
+    if (document.readyState != 'loading') {
+        fn();
+    } else {
+        document.addEventListener('DOMContentLoaded', fn);
+    }
+}
+
+ready(doSearch);
+
+const summaryInclude = 60;
+const fuseOptions = {
+    shouldSort: true,
+    includeMatches: true,
+    matchAllTokens: true,
+    threshold: 0.0, // for parsing diacritics
+    tokenize: true,
+    location: 0,
+    distance: 100,
+    maxPatternLength: 32,
+    minMatchCharLength: 1,
+    keys: [{
+        name: "title",
+        weight: 0.8
+    },
+        {
+            name: "contents",
+            weight: 0.5
+        },
+        {
+            name: "tags",
+            weight: 0.3
+        },
+        {
+            name: "categories",
+            weight: 0.3
+        }
+    ]
+};
+
+function param(name) {
+    return decodeURIComponent((location.search.split(name + '=')[1] || '').split('&')[0]).replace(/\+/g, ' ');
+}
+
+let searchQuery = param("s");
+
+function doSearch() {
+    if (searchQuery) {
+        document.getElementById("search-query").value = searchQuery;
+        executeSearch(searchQuery);
+    } else {
+        const para = document.createElement("P");
+        para.innerText = "Please enter a word or phrase above";
+        document.getElementById("search-results").appendChild(para);
+    }
+}
+
+function getJSON(url, fn) {
+    const request = new XMLHttpRequest();
+    request.open('GET', url, true);
+    request.onload = function () {
+        if (request.status >= 200 && request.status < 400) {
+            const data = JSON.parse(request.responseText);
+            fn(data);
+        } else {
+            console.log("Target reached on " + url + " with error " + request.status);
+        }
+    };
+    request.onerror = function () {
+        console.log("Connection error " + request.status);
+    };
+    request.send();
+}
+
+function executeSearch(searchQuery) {
+    getJSON("/" + document.LANG + "/index.json", function (data) {
+        const pages = data;
+        const fuse = new Fuse(pages, fuseOptions);
+        const result = fuse.search(searchQuery);
+        console.log({
+            "matches": result
+        });
+        document.getElementById("search-results").innerHTML = "";
+        if (result.length > 0) {
+            populateResults(result);
+        } else {
+            const para = document.createElement("P");
+            para.innerText = "No matches found";
+            document.getElementById("search-results").appendChild(para);
+        }
+    });
+}
+
+function populateResults(result) {
+    result.forEach(function (value, key) {
+        const content = value.item.contents;
+        let snippet = "";
+        const snippetHighlights = [];
+        if (fuseOptions.tokenize) {
+            snippetHighlights.push(searchQuery);
+            value.matches.forEach(function (mvalue) {
+                if (mvalue.key === "tags" || mvalue.key === "categories") {
+                    snippetHighlights.push(mvalue.value);
+                } else if (mvalue.key === "contents") {
+                    const ind = content.toLowerCase().indexOf(searchQuery.toLowerCase());
+                    const start = ind - summaryInclude > 0 ? ind - summaryInclude : 0;
+                    const end = ind + searchQuery.length + summaryInclude < content.length ? ind + searchQuery.length + summaryInclude : content.length;
+                    snippet += content.substring(start, end);
+                    if (ind > -1) {
+                        snippetHighlights.push(content.substring(ind, ind + searchQuery.length))
+                    } else {
+                        snippetHighlights.push(mvalue.value.substring(mvalue.indices[0][0], mvalue.indices[0][1] - mvalue.indices[0][0] + 1));
+                    }
+                }
+            });
+        }
+
+        if (snippet.length < 1) {
+            snippet += content.substring(0, summaryInclude * 2);
+        }
+        //pull template from hugo templarte definition
+        const templateDefinition = document.getElementById("search-result-template").innerHTML;
+        //replace values
+        const output = render(templateDefinition, {
+            key: key,
+            title: value.item.title,
+            link: value.item.permalink,
+            tags: value.item.tags,
+            categories: value.item.categories,
+            snippet: snippet
+        });
+        document.getElementById("search-results").appendChild(htmlToElement(output));
+
+        snippetHighlights.forEach(function (snipvalue) {
+            new Mark(document.getElementById("summary-" + key)).mark(snipvalue);
+        });
+
+    });
+}
+
+function render(templateString, data) {
+    let conditionalMatches, copy;
+    const 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 content.
+            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;
+}
+
+/**
+ * By Mark Amery: https://stackoverflow.com/a/35385518
+ * @param {String} HTML representing a single element
+ * @return {Element}
+ */
+function htmlToElement(html) {
+    const template = document.createElement('template');
+    html = html.trim(); // Never return a text node of whitespace as the result
+    template.innerHTML = html;
+    return template.content.firstChild;
+}
index 23d1257337487b9b347c243ead743f28f2a9d6ab..039e2938fb1f67a8665895f1439829673fb55ad6 100644 (file)
@@ -20,6 +20,12 @@ params:
   website: https://docs.gitea.io
   version: 1.9.5
 
+outputs:
+  home:
+    - HTML
+    - RSS
+    - JSON
+
 menu:
   page:
     - name: Website
index 5ad1dd7f1edb3b78a3e3fba2113ecd5b10625dc4..635cb8931e342cf879f08e35264b2bdf431b2d7b 100644 (file)
@@ -2,12 +2,12 @@
 date: "2017-01-20T15:00:00+08:00"
 title: "Help"
 slug: "help"
-weight: 50
+weight: 5
 toc: false
 draft: false
 menu:
   sidebar:
     name: "Help"
-    weight: 50
+    weight: 5
     identifier: "help"
 ---
diff --git a/docs/content/doc/help.fr-fr.md b/docs/content/doc/help.fr-fr.md
new file mode 100644 (file)
index 0000000..ab0cedc
--- /dev/null
@@ -0,0 +1,13 @@
+---
+date: "2017-01-20T15:00:00+08:00"
+title: "Aide"
+slug: "help"
+weight: 5
+toc: false
+draft: false
+menu:
+  sidebar:
+    name: "Aide"
+    weight: 5
+    identifier: "help"
+---
index 6af7aa1719b99e35874b76911aff29b9ad4bbc68..9465cd5464cea1e85738539a6a66b529573afb84 100644 (file)
@@ -2,12 +2,12 @@
 date: "2017-01-20T15:00:00+08:00"
 title: "帮助"
 slug: "help"
-weight: 50
+weight: 5
 toc: false
 draft: false
 menu:
   sidebar:
     name: "帮助"
-    weight: 50
+    weight: 5
     identifier: "help"
 ---
diff --git a/docs/content/doc/help.zh-tw.md b/docs/content/doc/help.zh-tw.md
new file mode 100644 (file)
index 0000000..c9cd794
--- /dev/null
@@ -0,0 +1,13 @@
+---
+date: "2017-01-20T15:00:00+08:00"
+title: "救命"
+slug: "help"
+weight: 5
+toc: false
+draft: false
+menu:
+  sidebar:
+    name: "救命"
+    weight: 5
+    identifier: "help"
+---
diff --git a/docs/content/doc/help/search.en-us.md b/docs/content/doc/help/search.en-us.md
new file mode 100644 (file)
index 0000000..93c154b
--- /dev/null
@@ -0,0 +1,25 @@
+---
+date: "2019-11-12T16:00:00+02:00"
+title: "Search"
+slug: "search"
+weight: 4
+toc: true
+draft: false
+menu:
+  sidebar:
+    parent: "help"
+    name: "Search"
+    weight: 4
+    identifier: "search"
+sitemap:
+  priority : 0.1
+layout: "search"
+---
+
+
+This file exists solely to respond to /search URL with the related `search` layout template.
+
+No content shown here is rendered, all content is based in the template layouts/doc/search.html
+
+Setting a very low sitemap priority will tell search engines this is not important content.
+
diff --git a/docs/content/doc/help/search.fr-fr.md b/docs/content/doc/help/search.fr-fr.md
new file mode 100644 (file)
index 0000000..3507e9e
--- /dev/null
@@ -0,0 +1,25 @@
+---
+date: "2019-11-12T16:00:00+02:00"
+title: "Chercher"
+slug: "search"
+weight: 4
+toc: true
+draft: false
+menu:
+  sidebar:
+    parent: "help"
+    name: "Chercher"
+    weight: 4
+    identifier: "search"
+sitemap:
+  priority : 0.1
+layout: "search"
+---
+
+
+This file exists solely to respond to /search URL with the related `search` layout template.
+
+No content shown here is rendered, all content is based in the template layouts/doc/search.html
+
+Setting a very low sitemap priority will tell search engines this is not important content.
+
diff --git a/docs/content/doc/help/search.zh-cn.md b/docs/content/doc/help/search.zh-cn.md
new file mode 100644 (file)
index 0000000..a51860a
--- /dev/null
@@ -0,0 +1,25 @@
+---
+date: "2019-11-12T16:00:00+02:00"
+title: "搜索"
+slug: "search"
+weight: 4
+toc: true
+draft: false
+menu:
+  sidebar:
+    parent: "help"
+    name: "搜索"
+    weight: 4
+    identifier: "search"
+sitemap:
+  priority : 0.1
+layout: "search"
+---
+
+
+This file exists solely to respond to /search URL with the related `search` layout template.
+
+No content shown here is rendered, all content is based in the template layouts/doc/search.html
+
+Setting a very low sitemap priority will tell search engines this is not important content.
+
diff --git a/docs/content/doc/help/search.zh-tw.md b/docs/content/doc/help/search.zh-tw.md
new file mode 100644 (file)
index 0000000..a51860a
--- /dev/null
@@ -0,0 +1,25 @@
+---
+date: "2019-11-12T16:00:00+02:00"
+title: "搜索"
+slug: "search"
+weight: 4
+toc: true
+draft: false
+menu:
+  sidebar:
+    parent: "help"
+    name: "搜索"
+    weight: 4
+    identifier: "search"
+sitemap:
+  priority : 0.1
+layout: "search"
+---
+
+
+This file exists solely to respond to /search URL with the related `search` layout template.
+
+No content shown here is rendered, all content is based in the template layouts/doc/search.html
+
+Setting a very low sitemap priority will tell search engines this is not important content.
+
diff --git a/docs/layouts/_default/index.json b/docs/layouts/_default/index.json
new file mode 100644 (file)
index 0000000..ae08324
--- /dev/null
@@ -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/docs/layouts/doc/search.html b/docs/layouts/doc/search.html
new file mode 100644 (file)
index 0000000..736fcae
--- /dev/null
@@ -0,0 +1,44 @@
+{{ partial "header.html" . }}
+{{ partial "navbar.html" . }}
+
+<section class="section">
+       <div class="container is-centered page">
+               <div class="columns">
+                       <div class="column is-one-quarter">
+                {{ partial "menu" . }}
+                       </div>
+                       <div class="column">
+                               <div class=" content">
+                                       <section class="resume-section p-3 p-lg-5 d-flex flex-column">
+                                               <div class="my-auto" >
+                                                       <form action="{{ "search" | absLangURL }}">
+                                                               <label>Search:
+                                                                       <input id="search-query" name="s"/>
+                                                               </label>
+                                                       </form>
+                                                       <br/>
+                                                       <div id="search-results"></div>
+                                               </div>
+                                       </section>
+                                       <!-- this template is sucked in by search.js and appended to the search-results div above. So editing here will adjust style -->
+                                       <script id="search-result-template" type="text/x-js-template">
+                                               <div id="summary-${key}">
+                                                       <h4><a href="${link}">${title}</a></h4>
+                                                       <p>${snippet}</p>
+                                                       ${ isset tags }<p>Tags: ${tags}</p>${ end }
+                                                       ${ isset categories }<p>Categories: ${categories}</p>${ end }
+                                                       <hr/>
+                                               </div>
+                                       </script>
+                               </div>
+                       </div>
+               </div>
+       </div>
+</section>
+
+<script src="https://cdnjs.cloudflare.com/ajax/libs/fuse.js/3.4.5/fuse.min.js"></script>
+<script src="https://cdnjs.cloudflare.com/ajax/libs/mark.js/8.11.1/mark.min.js"></script>
+<script>document.LANG = "{{ .Language.Lang }}";</script>
+{{ $script := resources.Get "js/search.js" | minify | fingerprint -}}
+<script src="{{ $script.Permalink }}" {{ printf "integrity=%q" $script.Data.Integrity | safeHTMLAttr }}></script>
+{{ partial "footer.html" . }}