Implement documentation search (#8937)
* Implement documentation search Signed-off-by: jolheiser <john.olheiser@gmail.com> Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com>
This commit is contained in:
		
							parent
							
								
									afe50873a5
								
							
						
					
					
						commit
						3b0303a4fc
					
				
					 13 changed files with 362 additions and 4 deletions
				
			
		
							
								
								
									
										1
									
								
								docs/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								docs/.gitignore
									
									
									
									
										vendored
									
									
								
							|  | @ -1,3 +1,4 @@ | |||
| public/ | ||||
| templates/swagger/v1_json.tmpl | ||||
| themes/ | ||||
| resources/ | ||||
|  |  | |||
							
								
								
									
										176
									
								
								docs/assets/js/search.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										176
									
								
								docs/assets/js/search.js
									
									
									
									
									
										Normal file
									
								
							|  | @ -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; | ||||
| } | ||||
|  | @ -20,6 +20,12 @@ params: | |||
|   website: https://docs.gitea.io | ||||
|   version: 1.9.5 | ||||
| 
 | ||||
| outputs: | ||||
|   home: | ||||
|     - HTML | ||||
|     - RSS | ||||
|     - JSON | ||||
| 
 | ||||
| menu: | ||||
|   page: | ||||
|     - name: Website | ||||
|  |  | |||
|  | @ -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" | ||||
| --- | ||||
|  |  | |||
							
								
								
									
										13
									
								
								docs/content/doc/help.fr-fr.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								docs/content/doc/help.fr-fr.md
									
									
									
									
									
										Normal file
									
								
							|  | @ -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" | ||||
| --- | ||||
|  | @ -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" | ||||
| --- | ||||
|  |  | |||
							
								
								
									
										13
									
								
								docs/content/doc/help.zh-tw.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								docs/content/doc/help.zh-tw.md
									
									
									
									
									
										Normal file
									
								
							|  | @ -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" | ||||
| --- | ||||
							
								
								
									
										25
									
								
								docs/content/doc/help/search.en-us.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								docs/content/doc/help/search.en-us.md
									
									
									
									
									
										Normal file
									
								
							|  | @ -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. | ||||
| 
 | ||||
							
								
								
									
										25
									
								
								docs/content/doc/help/search.fr-fr.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								docs/content/doc/help/search.fr-fr.md
									
									
									
									
									
										Normal file
									
								
							|  | @ -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. | ||||
| 
 | ||||
							
								
								
									
										25
									
								
								docs/content/doc/help/search.zh-cn.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								docs/content/doc/help/search.zh-cn.md
									
									
									
									
									
										Normal file
									
								
							|  | @ -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. | ||||
| 
 | ||||
							
								
								
									
										25
									
								
								docs/content/doc/help/search.zh-tw.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								docs/content/doc/help/search.zh-tw.md
									
									
									
									
									
										Normal file
									
								
							|  | @ -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. | ||||
| 
 | ||||
							
								
								
									
										5
									
								
								docs/layouts/_default/index.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								docs/layouts/_default/index.json
									
									
									
									
									
										Normal file
									
								
							|  | @ -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 -}} | ||||
							
								
								
									
										44
									
								
								docs/layouts/doc/search.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								docs/layouts/doc/search.html
									
									
									
									
									
										Normal file
									
								
							|  | @ -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" . }} | ||||
		Loading…
	
		Reference in a new issue