Add support for Fuse.js search (fixes #101)

This commit is contained in:
daudix
2024-10-16 22:19:22 +03:00
parent 76849cd98e
commit 841ccc5fa5
8 changed files with 184 additions and 25 deletions

View File

@ -10,7 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- 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

View File

@ -35,7 +35,8 @@ smart_punctuation = true
bottom_footnotes = true
[search]
index_format = "elasticlunr_json"
# index_format = "elasticlunr_json"
index_format = "fuse_json"
[languages.ar]
title = "Duckquill"
@ -136,6 +137,8 @@ show_reading_time = true
# Whether to show a share button in articles.
# Uses https://shareopenly.org.
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.
# Note: This will make your page significantly heavier.
# Instead, consider enabling it per page/section.

View File

@ -398,10 +398,18 @@
}
span {
color: var(--fg-muted-5);
&:first-of-type,
&.more-matches {
margin-block-start: 0.5rem;
border-block-start: max(1px, 0.0625rem) solid var(--fg-muted-2);
padding-block-start: 0.25rem;
color: var(--fg-muted-5);
}
&.more-matches {
font-size: var(--font-size-small);
}
strong {
color: var(--fg-color);

9
static/fuse.js Normal file

File diff suppressed because one or more lines are too long

View File

@ -117,6 +117,17 @@
{{ page.content | safe }}
</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 -%}
{% include "partials/comments.html" %}
{%- endif -%}

View File

@ -92,8 +92,13 @@
{%- endif %}
{%- 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"]) %}
{%- elif config.search.index_format == "fuse_json" -%}
{%- include "partials/search_fuse.html" %}
{%- set scripts = scripts | concat(with=["fuse.js"]) %}
{%- endif -%}
{%- endif %}
{%- if config.extra.nav.show_theme_switcher %}

View File

@ -132,8 +132,9 @@
}
function initSearch() {
var $searchInput = document.getElementById("search-bar");
var $searchResults = document.getElementById("search-results");
var searchBar = document.getElementById("search-bar");
var searchContainer = document.getElementById("search-container");
var searchResults = document.getElementById("search-results");
var MAX_ITEMS = 10;
var options = {
@ -159,13 +160,13 @@
return res;
}
$searchInput.addEventListener("keyup", debounce(async function () {
var term = $searchInput.value.trim();
searchBar.addEventListener("keyup", debounce(async function () {
var term = searchBar.value.trim();
if (term === currentTerm) {
return;
}
$searchResults.style.display = term === "" ? "none" : "flex";
$searchResults.innerHTML = "";
searchResults.style.display = term === "" ? "none" : "flex";
searchResults.innerHTML = "";
currentTerm = term;
if (term === "") {
return;
@ -173,18 +174,19 @@
var results = (await initIndex()).search(term, options);
if (results.length === 0) {
$searchResults.style.display = "none";
searchResults.style.display = "none";
return;
}
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));
window.addEventListener('click', function (e) {
if ($searchResults.style.display == "flex" && !$searchResults.contains(e.target)) {
$searchResults.style.display = "none";
document.addEventListener("keydown", function(event) {
if (event.key === "/") {
event.preventDefault();
toggleSearch();
}
});
}
@ -197,13 +199,6 @@
searchBar.focus();
}
document.addEventListener("keydown", function(event) {
if (event.key === "/") {
event.preventDefault();
toggleSearch();
}
});
if (document.readyState === "complete" ||
(document.readyState !== "loading" && !document.documentElement.doScroll)
) {

View 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>