-
Notifications
You must be signed in to change notification settings - Fork 285
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Added search capability via lunr (#165)
Fixes #163 * feat: Added search capability via lunr * doc: Fixed typo * chore: Updated resource files of the example site * chore: Remove unnecessary div wrapping search results * chore: Reverted change of the config documentation * chore: Ordered config entries correctly * fix: Update document title when displaying search results * fix: Handle a 404 error when downloading search index * Use menu variable for icon * Adjust search icon's style * fix: Make back button work for restoring previous page state * fix: Respect search state even when coming back from another page or site * fix: Support non-English languages * chore: port config.toml to zh-cn * i18n: add zh, pt-br translations * Increase `postWidth` and `listWidth` to `39em` Otherwise search box will warp to a new line. * Update screenshots * Update resources * fix: Update URL when searching Co-authored-by: reuixiy <reuixiy@gmail.com>
- Loading branch information
Showing
20 changed files
with
426 additions
and
26 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,193 @@ | ||
(function initLunr() { | ||
let index = null; | ||
let lookup = null; | ||
let queuedTerm = null; | ||
let queuedDoNotAddState = false; | ||
let origContent = null; | ||
|
||
const form = document.getElementById("search"); | ||
const input = document.getElementById("search-input"); | ||
form.addEventListener("submit", function(event) { | ||
event.preventDefault(); | ||
|
||
let term = input.value.trim(); | ||
if (!term) { | ||
return; | ||
} | ||
startSearch(term, false); | ||
}, false); | ||
|
||
window.addEventListener("load", function(event) { | ||
if (history.state && history.state.type == "search") { | ||
startSearch(history.state.term, true); | ||
} | ||
}); | ||
|
||
window.addEventListener("popstate", function(event) { | ||
if (event.state && event.state.type == "search") { | ||
startSearch(event.state.term, true); | ||
} | ||
else if (!event.state && origContent) { | ||
let target = document.querySelector(".main-inner"); | ||
while (target.firstChild) { | ||
target.removeChild(target.firstChild); | ||
} | ||
|
||
for (let node of origContent) { | ||
target.appendChild(node); | ||
} | ||
origContent = null; | ||
} | ||
}, false); | ||
|
||
function startSearch(term, doNotAddState) { | ||
input.value = term; | ||
form.setAttribute("data-running", "true"); | ||
if (index) { | ||
search(term, doNotAddState); | ||
} | ||
else if (queuedTerm) { | ||
queuedTerm = term; | ||
queuedDoNotAddState = doNotAddState; | ||
} | ||
else { | ||
queuedTerm = term; | ||
queuedDoNotAddState = doNotAddState; | ||
initIndex(); | ||
} | ||
} | ||
|
||
function searchDone() { | ||
form.removeAttribute("data-running"); | ||
queuedTerm = null; | ||
queuedDoNotAddState = false; | ||
} | ||
|
||
function initIndex() { | ||
let request = new XMLHttpRequest(); | ||
request.open("GET", "{{ ((.Site.GetPage "").OutputFormats.Get "SearchIndex").RelPermalink }}"); | ||
request.responseType = "json"; | ||
request.addEventListener("load", function(event) { | ||
let documents = request.response; | ||
if (!documents) | ||
{ | ||
console.error("Search index could not be downloaded, was it generated?"); | ||
searchDone(); | ||
return; | ||
} | ||
|
||
lookup = {}; | ||
index = lunr(function() { | ||
const language = "{{ .Site.Language.Lang }}"; | ||
if (language != "en" && lunr.hasOwnProperty(language)) { | ||
this.use(lunr[language]); | ||
} | ||
|
||
this.ref("uri"); | ||
this.field("title"); | ||
this.field("subtitle"); | ||
this.field("content"); | ||
this.field("description"); | ||
this.field("categories"); | ||
this.field("tags"); | ||
|
||
for (let document of documents) { | ||
this.add(document); | ||
lookup[document.uri] = document; | ||
} | ||
}); | ||
|
||
search(queuedTerm, queuedDoNotAddState); | ||
}, false); | ||
request.addEventListener("error", searchDone, false); | ||
request.send(null); | ||
} | ||
|
||
function search(term, doNotAddState) { | ||
try { | ||
let results = index.search(term); | ||
|
||
let target = document.querySelector(".main-inner"); | ||
let replaced = []; | ||
while (target.firstChild) { | ||
replaced.push(target.firstChild); | ||
target.removeChild(target.firstChild); | ||
} | ||
if (!origContent) { | ||
origContent = replaced; | ||
} | ||
|
||
let title = document.createElement("h1"); | ||
title.id = "search-results"; | ||
title.className = "list-title"; | ||
|
||
// This is an overly simple pluralization scheme, it will only work | ||
// for some languages. | ||
if (results.length == 0) { | ||
title.textContent = "{{ i18n "searchResultsNone" (dict "Term" "{}") }}".replace("{}", term); | ||
} | ||
else if (results.length == 1) { | ||
title.textContent = "{{ i18n "searchResultsTitle" (dict "Count" 1 "Term" "{}") }}".replace("{}", term); | ||
} | ||
else { | ||
title.textContent = "{{ i18n "searchResultsTitle" (dict "Count" 13579 "Term" "{}") }}".replace("{}", term).replace("13579", results.length); | ||
} | ||
target.appendChild(title); | ||
document.title = title.textContent; | ||
|
||
let template = document.getElementById("search-result"); | ||
for (let result of results) { | ||
let doc = lookup[result.ref]; | ||
|
||
let element = template.content.cloneNode(true); | ||
element.querySelector(".summary-title-link").href = element.querySelector(".read-more-link").href = doc.uri; | ||
element.querySelector(".summary-title-link").textContent = doc.title; | ||
element.querySelector(".summary").textContent = truncateToEndOfSentence(doc.content, 70); | ||
target.appendChild(element); | ||
} | ||
title.scrollIntoView(true); | ||
|
||
if (!doNotAddState) { | ||
history.pushState({type: "search", term: term}, title.textContent, "#search=" + encodeURIComponent(term)); | ||
} | ||
|
||
{{ if .Site.Params.enableNavToggle }} | ||
if (navToggleLabel.classList.contains("open")) { | ||
document.getElementById(navToggleLabel.getAttribute("for")).click(); | ||
} | ||
{{ end }} | ||
} | ||
finally { | ||
searchDone(); | ||
} | ||
} | ||
|
||
// This matches Hugo's own summary logic: | ||
// https://github.com/gohugoio/hugo/blob/b5f39d23b86f9cb83c51da9fe4abb4c19c01c3b7/helpers/content.go#L543 | ||
function truncateToEndOfSentence(text, minWords) | ||
{ | ||
let match; | ||
let result = ""; | ||
let wordCount = 0; | ||
let regexp = /(\S+)(\s*)/g; | ||
while (match = regexp.exec(text)) { | ||
wordCount++; | ||
if (wordCount <= minWords) { | ||
result += match[0]; | ||
} | ||
else | ||
{ | ||
let char1 = match[1][match[1].length - 1]; | ||
let char2 = match[2][0]; | ||
if (/[.?!"]/.test(char1) || char2 == "\n") { | ||
result += match[1]; | ||
break; | ||
} | ||
else { | ||
result += match[0]; | ||
} | ||
} | ||
} | ||
return result; | ||
} | ||
})(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
@keyframes spin { | ||
100% { | ||
transform: rotateY(360deg); | ||
} | ||
} | ||
|
||
.search { | ||
display: flex; | ||
justify-content: center; | ||
border: 1px solid var(--color-contrast-medium); | ||
min-width: 1em; | ||
height: 1em; | ||
line-height: 1; | ||
border-radius: 0.75em; | ||
padding: 0.25em; | ||
|
||
.search-icon { | ||
cursor: pointer; | ||
width: 1em; | ||
height: 1em; | ||
margin: 0; | ||
vertical-align: bottom; | ||
|
||
color: var(--color-contrast-medium); | ||
transition: color $duration; | ||
&:hover { | ||
color: var(--color-primary); | ||
} | ||
} | ||
|
||
&[data-running] .search-icon { | ||
animation: spin 1.5s linear infinite; | ||
} | ||
|
||
.search-input { | ||
-moz-appearance: textfield; | ||
-webkit-appearance: textfield; | ||
appearance: textfield; | ||
border-width: 0; | ||
padding: 0; | ||
margin: 0; | ||
width: 0; | ||
outline: none; | ||
background: transparent; | ||
color: var(--color-contrast-higher); | ||
transition: width $duration; | ||
|
||
&:focus | ||
{ | ||
margin-left: 0.5em; | ||
width: 10em; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.