Add client-side search using Lunr

This commit is contained in:
thePR0M3TH3AN
2025-07-10 10:37:29 -04:00
parent 03511c2e43
commit 69f1c0783b
6 changed files with 3580 additions and 1 deletions

3475
assets/lunr.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -20,6 +20,35 @@ body {
align-items: center;
padding: 0.5rem 1rem;
background: var(--sidebar-bg);
position: relative;
}
.search-input {
margin-left: auto;
padding: 0.25rem;
}
.search-results {
display: none;
position: absolute;
right: 1rem;
top: 100%;
background: var(--bg-color);
border: 1px solid #ccc;
width: 250px;
max-height: 200px;
overflow-y: auto;
z-index: 100;
}
.search-results a {
display: block;
padding: 0.25rem;
color: var(--text-color);
text-decoration: none;
}
.search-results a:hover {
background: var(--sidebar-bg);
}
.search-results .no-results {
padding: 0.25rem;
}
.logo { text-decoration: none; color: var(--text-color); font-weight: bold; }
.sidebar-toggle,

View File

@@ -1,6 +1,8 @@
document.addEventListener('DOMContentLoaded', () => {
const sidebarToggle = document.getElementById('sidebar-toggle');
const themeToggle = document.getElementById('theme-toggle');
const searchInput = document.getElementById('search-input');
const searchResults = document.getElementById('search-results');
const root = document.documentElement;
function setTheme(theme) {
@@ -19,6 +21,58 @@ document.addEventListener('DOMContentLoaded', () => {
setTheme(next);
});
// search
let lunrIndex;
let docs = [];
async function loadIndex() {
if (lunrIndex) return;
try {
const res = await fetch('/search-index.json');
const data = await res.json();
lunrIndex = lunr.Index.load(data.index);
docs = data.docs;
} catch (e) {
console.error('Search index failed to load', e);
}
}
function highlight(text, q) {
const re = new RegExp('(' + q.replace(/[.*+?^${}()|[\\]\\]/g, '\\$&') + ')', 'gi');
return text.replace(re, '<mark>$1</mark>');
}
searchInput?.addEventListener('input', async e => {
const q = e.target.value.trim();
await loadIndex();
if (!lunrIndex || !q) {
searchResults.style.display = 'none';
searchResults.innerHTML = '';
return;
}
const matches = lunrIndex.search(q);
searchResults.innerHTML = '';
if (!matches.length) {
searchResults.innerHTML = '<div class="no-results">No matches found</div>';
searchResults.style.display = 'block';
return;
}
matches.forEach(m => {
const doc = docs.find(d => d.id === m.ref);
if (!doc) return;
const a = document.createElement('a');
a.href = doc.url;
a.innerHTML = '<strong>' + highlight(doc.title, q) + '</strong><br><small>' + highlight(doc.headings, q) + '</small>';
searchResults.appendChild(a);
});
searchResults.style.display = 'block';
});
document.addEventListener('click', e => {
if (!searchResults.contains(e.target) && e.target !== searchInput) {
searchResults.style.display = 'none';
}
});
// breadcrumbs
const bc = document.getElementById('breadcrumbs');
if (bc) {