mirror of
https://github.com/PR0M3TH3AN/Archivox.git
synced 2025-09-08 06:58:43 +00:00
Add client-side search using Lunr
This commit is contained in:
3475
assets/lunr.js
Normal file
3475
assets/lunr.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -20,6 +20,35 @@ body {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
background: var(--sidebar-bg);
|
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; }
|
.logo { text-decoration: none; color: var(--text-color); font-weight: bold; }
|
||||||
.sidebar-toggle,
|
.sidebar-toggle,
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const sidebarToggle = document.getElementById('sidebar-toggle');
|
const sidebarToggle = document.getElementById('sidebar-toggle');
|
||||||
const themeToggle = document.getElementById('theme-toggle');
|
const themeToggle = document.getElementById('theme-toggle');
|
||||||
|
const searchInput = document.getElementById('search-input');
|
||||||
|
const searchResults = document.getElementById('search-results');
|
||||||
const root = document.documentElement;
|
const root = document.documentElement;
|
||||||
|
|
||||||
function setTheme(theme) {
|
function setTheme(theme) {
|
||||||
@@ -19,6 +21,58 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
setTheme(next);
|
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
|
// breadcrumbs
|
||||||
const bc = document.getElementById('breadcrumbs');
|
const bc = document.getElementById('breadcrumbs');
|
||||||
if (bc) {
|
if (bc) {
|
||||||
|
@@ -3,6 +3,8 @@ const fs = require('fs');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const matter = require('gray-matter');
|
const matter = require('gray-matter');
|
||||||
const Eleventy = require('@11ty/eleventy');
|
const Eleventy = require('@11ty/eleventy');
|
||||||
|
const lunr = require('lunr');
|
||||||
|
const { lexer } = require('marked');
|
||||||
const loadConfig = require('../config/loadConfig');
|
const loadConfig = require('../config/loadConfig');
|
||||||
|
|
||||||
async function readDirRecursive(dir) {
|
async function readDirRecursive(dir) {
|
||||||
@@ -61,6 +63,7 @@ async function generate({ contentDir = 'content', outputDir = '_site', configPat
|
|||||||
const files = await readDirRecursive(contentDir);
|
const files = await readDirRecursive(contentDir);
|
||||||
const pages = [];
|
const pages = [];
|
||||||
const assets = [];
|
const assets = [];
|
||||||
|
const searchDocs = [];
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const rel = path.relative(contentDir, file);
|
const rel = path.relative(contentDir, file);
|
||||||
@@ -75,7 +78,11 @@ async function generate({ contentDir = 'content', outputDir = '_site', configPat
|
|||||||
}
|
}
|
||||||
const raw = await fs.promises.readFile(file, 'utf8');
|
const raw = await fs.promises.readFile(file, 'utf8');
|
||||||
const parsed = matter(raw);
|
const parsed = matter(raw);
|
||||||
pages.push({ file: rel, data: { ...parsed.data, title: parsed.data.title || path.basename(rel, '.md') } });
|
const title = parsed.data.title || path.basename(rel, '.md');
|
||||||
|
const tokens = lexer(parsed.content || '');
|
||||||
|
const headings = tokens.filter(t => t.type === 'heading').map(t => t.text).join(' ');
|
||||||
|
pages.push({ file: rel, data: { ...parsed.data, title } });
|
||||||
|
searchDocs.push({ id: rel.replace(/\.md$/, '.html'), url: '/' + rel.replace(/\.md$/, '.html'), title, headings });
|
||||||
} else {
|
} else {
|
||||||
assets.push(rel);
|
assets.push(rel);
|
||||||
}
|
}
|
||||||
@@ -86,6 +93,17 @@ async function generate({ contentDir = 'content', outputDir = '_site', configPat
|
|||||||
await fs.promises.writeFile(path.join(outputDir, 'navigation.json'), JSON.stringify(nav, null, 2));
|
await fs.promises.writeFile(path.join(outputDir, 'navigation.json'), JSON.stringify(nav, null, 2));
|
||||||
await fs.promises.writeFile(path.join(outputDir, 'config.json'), JSON.stringify(config, null, 2));
|
await fs.promises.writeFile(path.join(outputDir, 'config.json'), JSON.stringify(config, null, 2));
|
||||||
|
|
||||||
|
const searchIndex = lunr(function() {
|
||||||
|
this.ref('id');
|
||||||
|
this.field('title');
|
||||||
|
this.field('headings');
|
||||||
|
searchDocs.forEach(d => this.add(d));
|
||||||
|
});
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(outputDir, 'search-index.json'),
|
||||||
|
JSON.stringify({ index: searchIndex.toJSON(), docs: searchDocs }, null, 2)
|
||||||
|
);
|
||||||
|
|
||||||
const elev = new Eleventy(contentDir, outputDir);
|
const elev = new Eleventy(contentDir, outputDir);
|
||||||
elev.setConfig({
|
elev.setConfig({
|
||||||
dir: {
|
dir: {
|
||||||
|
@@ -16,6 +16,7 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
{% include "partials/footer.njk" %}
|
{% include "partials/footer.njk" %}
|
||||||
|
<script src="/assets/lunr.js"></script>
|
||||||
<script src="/assets/theme.js"></script>
|
<script src="/assets/theme.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
<header class="header">
|
<header class="header">
|
||||||
<button id="sidebar-toggle" class="sidebar-toggle" aria-label="Toggle navigation">☰</button>
|
<button id="sidebar-toggle" class="sidebar-toggle" aria-label="Toggle navigation">☰</button>
|
||||||
<a href="/" class="logo">{{ config.site.title }}</a>
|
<a href="/" class="logo">{{ config.site.title }}</a>
|
||||||
|
<input id="search-input" class="search-input" type="search" placeholder="Search..." aria-label="Search" />
|
||||||
<button id="theme-toggle" class="theme-toggle" aria-label="Toggle dark mode">🌓</button>
|
<button id="theme-toggle" class="theme-toggle" aria-label="Toggle dark mode">🌓</button>
|
||||||
|
<div id="search-results" class="search-results"></div>
|
||||||
</header>
|
</header>
|
||||||
|
Reference in New Issue
Block a user