Skip to content

Commit

Permalink
feat: Added search capability via lunr (#165)
Browse files Browse the repository at this point in the history
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
palant and reuixiy authored May 21, 2020
1 parent 46522c6 commit fb54c61
Show file tree
Hide file tree
Showing 20 changed files with 426 additions and 26 deletions.
193 changes: 193 additions & 0 deletions assets/js/lunr-search.js
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;
}
})();
10 changes: 10 additions & 0 deletions assets/scss/base/_responsive.scss
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,16 @@
#langs li {
width: auto;
}
@if ($enableLunrSearch) {
.search-item {
grid-column: 1 / -1;
}

.search .search-input {
margin-left: 0.5em;
flex: 1;
}
}
} @else {
.nav {
margin-right: 1em;
Expand Down
54 changes: 54 additions & 0 deletions assets/scss/components/_search.scss
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;
}
}
}
8 changes: 8 additions & 0 deletions assets/scss/main.scss
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,14 @@ $fofPoster: url({{ $fofPoster }});
{{ end }}
{{ end }}

// Lunr search

{{ if and (eq .Site.Params.headerLayout "flex") .Site.Params.enableLunrSearch }}
$enableLunrSearch: true;
@import "components/search";
{{ else }}
$enableLunrSearch: false;
{{ end }}

// Responsive

Expand Down
29 changes: 24 additions & 5 deletions config-examples/en/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -136,10 +136,15 @@ uglyURLs = false
mediaType = "application/rss+xml"
baseName = "rss"

# Search index for lunr.js
[outputFormats.SearchIndex]
mediaType = "application/json"
baseName = "search"

# Hugo’s output control
[outputs]
page = ["HTML"]
home = ["HTML", "SectionsAtom", "SectionsRSS"]
home = ["HTML", "SectionsAtom", "SectionsRSS", "SearchIndex"]
section = ["HTML"]
# Taxonomy
taxonomyTerm = ["HTML"]
Expand Down Expand Up @@ -182,10 +187,10 @@ uglyURLs = false
# is left empty("") or does
# not exist)
# identifier Icon’s class name
# (there are two speacial
# (there are three special
# values for header layout
# flex: `theme-switcher`,
# `lang-switcher`)
# `lang-switcher`, `search`)

[menu]
## Menu bar
Expand Down Expand Up @@ -225,6 +230,10 @@ uglyURLs = false
[[menu.main]]
weight = 7
identifier = "lang-switcher"
[[menu.main]]
weight = 8
identifier = "search"
post = "search"



Expand Down Expand Up @@ -412,7 +421,7 @@ uglyURLs = false
######################################
# List Page

listWidth = 30
listWidth = 39
# Note: you can leave it empty("") to
# fallback to the default value: 42
# Unit: em
Expand Down Expand Up @@ -705,7 +714,7 @@ uglyURLs = false
# supported

# The content width of the post
postWidth = 32
postWidth = 39
# Note: you can leave it empty("") to
# fallback to the default value: 42
# Unit: em
Expand Down Expand Up @@ -1287,6 +1296,16 @@ uglyURLs = false
# Note: https://github.com/instantpage/instant.page


######################################
# Lunr search

# Note: This requires SearchIndex
# output to be enabled.

enableLunrSearch = true
# Note: https://lunrjs.com/


######################################
# 404 Page

Expand Down
Loading

0 comments on commit fb54c61

Please sign in to comment.