Add support for Fuse.js search (fixes #101)
This commit is contained in:
@ -10,7 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Add `classic-article-list` mod for returning the classic article list style.
|
- Add `classic-article-list` mod for returning the classic article list style.
|
||||||
- Support `fediverse:creator` meta tag.
|
- Add `fediverse:creator` meta tag.
|
||||||
|
- Add support for Fuse.js search (#101).
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
@ -35,7 +35,8 @@ smart_punctuation = true
|
|||||||
bottom_footnotes = true
|
bottom_footnotes = true
|
||||||
|
|
||||||
[search]
|
[search]
|
||||||
index_format = "elasticlunr_json"
|
# index_format = "elasticlunr_json"
|
||||||
|
index_format = "fuse_json"
|
||||||
|
|
||||||
[languages.ar]
|
[languages.ar]
|
||||||
title = "Duckquill"
|
title = "Duckquill"
|
||||||
@ -136,6 +137,8 @@ show_reading_time = true
|
|||||||
# Whether to show a share button in articles.
|
# Whether to show a share button in articles.
|
||||||
# Uses https://shareopenly.org.
|
# Uses https://shareopenly.org.
|
||||||
show_share_button = true
|
show_share_button = true
|
||||||
|
# Whether to show the "Read Also" section with articles linked to in the article
|
||||||
|
show_backlinks = true
|
||||||
# Whether to enable the KaTeX library for rendering LaTeX.
|
# Whether to enable the KaTeX library for rendering LaTeX.
|
||||||
# Note: This will make your page significantly heavier.
|
# Note: This will make your page significantly heavier.
|
||||||
# Instead, consider enabling it per page/section.
|
# Instead, consider enabling it per page/section.
|
||||||
|
@ -398,10 +398,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
span {
|
span {
|
||||||
|
color: var(--fg-muted-5);
|
||||||
|
|
||||||
|
&:first-of-type,
|
||||||
|
&.more-matches {
|
||||||
margin-block-start: 0.5rem;
|
margin-block-start: 0.5rem;
|
||||||
border-block-start: max(1px, 0.0625rem) solid var(--fg-muted-2);
|
border-block-start: max(1px, 0.0625rem) solid var(--fg-muted-2);
|
||||||
padding-block-start: 0.25rem;
|
padding-block-start: 0.25rem;
|
||||||
color: var(--fg-muted-5);
|
}
|
||||||
|
|
||||||
|
&.more-matches {
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
}
|
||||||
|
|
||||||
strong {
|
strong {
|
||||||
color: var(--fg-color);
|
color: var(--fg-color);
|
||||||
|
9
static/fuse.js
Normal file
9
static/fuse.js
Normal file
File diff suppressed because one or more lines are too long
@ -117,6 +117,17 @@
|
|||||||
{{ page.content | safe }}
|
{{ page.content | safe }}
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
|
{%- if config.extra.show_backlinks and page.backlinks | length > 0 -%}
|
||||||
|
<h2>{{ macros_translate::translate(key="backlinks", default="Backlinks", language_strings=language_strings) }}</h2>
|
||||||
|
<ul>
|
||||||
|
{%- for backlink in page.backlinks -%}
|
||||||
|
<li>
|
||||||
|
<a href="{{ backlink.permalink }}">{{ backlink.title }}</a>
|
||||||
|
</li>
|
||||||
|
{%- endfor -%}
|
||||||
|
</ul>
|
||||||
|
{%- endif -%}
|
||||||
|
|
||||||
{%- if page.extra.comments.id -%}
|
{%- if page.extra.comments.id -%}
|
||||||
{% include "partials/comments.html" %}
|
{% include "partials/comments.html" %}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
|
@ -92,8 +92,13 @@
|
|||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
|
||||||
{%- if config.build_search_index %}
|
{%- if config.build_search_index %}
|
||||||
{%- include "partials/search.html" %}
|
{%- if config.search.index_format == "elasticlunr_json" -%}
|
||||||
|
{%- include "partials/search_elasticlunr.html" %}
|
||||||
{%- set scripts = scripts | concat(with=["elasticlunr.min.js"]) %}
|
{%- set scripts = scripts | concat(with=["elasticlunr.min.js"]) %}
|
||||||
|
{%- elif config.search.index_format == "fuse_json" -%}
|
||||||
|
{%- include "partials/search_fuse.html" %}
|
||||||
|
{%- set scripts = scripts | concat(with=["fuse.js"]) %}
|
||||||
|
{%- endif -%}
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
|
||||||
{%- if config.extra.nav.show_theme_switcher %}
|
{%- if config.extra.nav.show_theme_switcher %}
|
||||||
|
@ -132,8 +132,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function initSearch() {
|
function initSearch() {
|
||||||
var $searchInput = document.getElementById("search-bar");
|
var searchBar = document.getElementById("search-bar");
|
||||||
var $searchResults = document.getElementById("search-results");
|
var searchContainer = document.getElementById("search-container");
|
||||||
|
var searchResults = document.getElementById("search-results");
|
||||||
var MAX_ITEMS = 10;
|
var MAX_ITEMS = 10;
|
||||||
|
|
||||||
var options = {
|
var options = {
|
||||||
@ -159,13 +160,13 @@
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
$searchInput.addEventListener("keyup", debounce(async function () {
|
searchBar.addEventListener("keyup", debounce(async function () {
|
||||||
var term = $searchInput.value.trim();
|
var term = searchBar.value.trim();
|
||||||
if (term === currentTerm) {
|
if (term === currentTerm) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$searchResults.style.display = term === "" ? "none" : "flex";
|
searchResults.style.display = term === "" ? "none" : "flex";
|
||||||
$searchResults.innerHTML = "";
|
searchResults.innerHTML = "";
|
||||||
currentTerm = term;
|
currentTerm = term;
|
||||||
if (term === "") {
|
if (term === "") {
|
||||||
return;
|
return;
|
||||||
@ -173,18 +174,19 @@
|
|||||||
|
|
||||||
var results = (await initIndex()).search(term, options);
|
var results = (await initIndex()).search(term, options);
|
||||||
if (results.length === 0) {
|
if (results.length === 0) {
|
||||||
$searchResults.style.display = "none";
|
searchResults.style.display = "none";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var i = 0; i < Math.min(results.length, MAX_ITEMS); i++) {
|
for (var i = 0; i < Math.min(results.length, MAX_ITEMS); i++) {
|
||||||
$searchResults.innerHTML += formatSearchResultItem(results[i], term.split(" "));
|
searchResults.innerHTML += formatSearchResultItem(results[i], term.split(" "));
|
||||||
}
|
}
|
||||||
}, 150));
|
}, 150));
|
||||||
|
|
||||||
window.addEventListener('click', function (e) {
|
document.addEventListener("keydown", function(event) {
|
||||||
if ($searchResults.style.display == "flex" && !$searchResults.contains(e.target)) {
|
if (event.key === "/") {
|
||||||
$searchResults.style.display = "none";
|
event.preventDefault();
|
||||||
|
toggleSearch();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -197,13 +199,6 @@
|
|||||||
searchBar.focus();
|
searchBar.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("keydown", function(event) {
|
|
||||||
if (event.key === "/") {
|
|
||||||
event.preventDefault();
|
|
||||||
toggleSearch();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (document.readyState === "complete" ||
|
if (document.readyState === "complete" ||
|
||||||
(document.readyState !== "loading" && !document.documentElement.doScroll)
|
(document.readyState !== "loading" && !document.documentElement.doScroll)
|
||||||
) {
|
) {
|
127
templates/partials/search_fuse.html
Normal file
127
templates/partials/search_fuse.html
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
{#- Based on https://codeberg.org/daudix/duckquill/issues/101#issuecomment-2377169 -#}
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
let searchSetup = false;
|
||||||
|
let fuse;
|
||||||
|
|
||||||
|
async function initIndex() {
|
||||||
|
if (searchSetup) return;
|
||||||
|
|
||||||
|
const url = "{{ get_url(path='/', lang=lang) }}/search_index.{{ config.default_language }}.json";
|
||||||
|
const response = await fetch(url);
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
includeScore: false,
|
||||||
|
includeMatches: true,
|
||||||
|
ignoreLocation: true,
|
||||||
|
threshold: 0.15,
|
||||||
|
keys: [
|
||||||
|
{ name: "title", weight: 3 },
|
||||||
|
{ name: "description", weight: 2 },
|
||||||
|
{ name: "body", weight: 1 }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
fuse = new Fuse(await response.json(), options);
|
||||||
|
searchSetup = true;
|
||||||
|
|
||||||
|
console.log("Search index initialized successfully");
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSearch() {
|
||||||
|
initIndex();
|
||||||
|
const searchBar = document.getElementById("search-bar");
|
||||||
|
const searchContainer = document.getElementById("search-container");
|
||||||
|
const searchResults = document.getElementById("search-results");
|
||||||
|
searchContainer.classList.toggle("active");
|
||||||
|
searchBar.toggleAttribute("disabled");
|
||||||
|
searchBar.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function debounce(actual_fn, wait) {
|
||||||
|
let timeoutId;
|
||||||
|
|
||||||
|
return (...args) => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
|
actual_fn(...args);
|
||||||
|
}, wait);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function initSearch() {
|
||||||
|
const searchBar = document.getElementById("search-bar");
|
||||||
|
const searchResults = document.getElementById("search-results");
|
||||||
|
const searchContainer = document.getElementById("search-container");
|
||||||
|
const MAX_ITEMS = 10;
|
||||||
|
const MAX_RESULTS = 4;
|
||||||
|
|
||||||
|
let currentTerm = "";
|
||||||
|
|
||||||
|
searchBar.addEventListener("keyup", (e) => {
|
||||||
|
const searchVal = searchBar.value.trim();
|
||||||
|
const results = fuse.search(searchVal, { limit: MAX_ITEMS });
|
||||||
|
|
||||||
|
let html = "";
|
||||||
|
for (const result of results) {
|
||||||
|
html += makeTeaser(result, searchVal);
|
||||||
|
}
|
||||||
|
searchResults.innerHTML = html;
|
||||||
|
|
||||||
|
if (html) {
|
||||||
|
searchResults.style.display = "flex";
|
||||||
|
} else {
|
||||||
|
searchResults.style.display = "none";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function makeTeaser(result, searchVal) {
|
||||||
|
const TEASER_SIZE = 25;
|
||||||
|
let output = `<div class="search-result item"><a class="result-title" href=${result.item.url}>${result.item.title}</a>`;
|
||||||
|
|
||||||
|
for (const match of result.matches) {
|
||||||
|
if (match.key === "title") continue;
|
||||||
|
|
||||||
|
const indices = match.indices.sort((a, b) => Math.abs(a[1] - a[0] - searchVal.length) - Math.abs(b[1] - b[0] - searchVal.length)).slice(0, MAX_RESULTS);
|
||||||
|
const value = match.value;
|
||||||
|
|
||||||
|
for (const ind of indices) {
|
||||||
|
const start = Math.max(0, ind[0] - TEASER_SIZE);
|
||||||
|
const end = Math.min(value.length - 1, ind[1] + TEASER_SIZE);
|
||||||
|
output += "<span>"
|
||||||
|
+ value.substring(start, ind[0])
|
||||||
|
+ `<strong>${value.substring(ind[0], ind[1] + 1)}</strong>`
|
||||||
|
+ value.substring(ind[1] + 1, end)
|
||||||
|
+ "</span>";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (match.indices.length > 4) {
|
||||||
|
output += `<span class="more-matches">{{ macros_translate::translate(key="more_matches", default="$MATCHES more matches", language_strings=language_strings) }}</span>`.replace("$MATCHES", `+${match.indices.length - MAX_RESULTS}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return output + "</div>";
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("click", function (event) {
|
||||||
|
if (searchSetup && searchBar.getAttribute("disabled") === null && !searchContainer.contains(event.target)) {
|
||||||
|
toggleSearch();
|
||||||
|
}
|
||||||
|
}, { passive: true });
|
||||||
|
|
||||||
|
document.addEventListener("keydown", function(event) {
|
||||||
|
if (event.key === "/") {
|
||||||
|
event.preventDefault();
|
||||||
|
toggleSearch();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === "complete" ||
|
||||||
|
(document.readyState !== "loading" && !document.documentElement.doScroll))
|
||||||
|
initSearch();
|
||||||
|
else
|
||||||
|
document.addEventListener("DOMContentLoaded", initSearch);
|
||||||
|
</script>
|
Reference in New Issue
Block a user