// search.js — minimal client-side search. // Loads /search.json?lang=, builds an in-memory index, renders results // as the user types in the header search box. // // Deliberately tiny — no FlexSearch dependency yet (the page set is small, // linear scan with case-insensitive substring is plenty). Swap in FlexSearch // or Lunr if the corpus grows past a few hundred pages. (function () { const input = document.getElementById('search-input'); const results = document.getElementById('search-results'); if (!input || !results) return; const lang = document.documentElement.lang || 'en'; let pages = []; let loaded = false; function loadIndex() { if (loaded) return Promise.resolve(); return fetch('/search.json?lang=' + encodeURIComponent(lang)) .then(r => r.json()) .then(data => { pages = data || []; loaded = true; }) .catch(() => { /* silent — search just stays empty */ }); } function escapeHtml(s) { return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); } function search(q) { if (!q || q.length < 2) return []; const needle = q.toLowerCase(); const out = []; for (const p of pages) { const titleHit = p.title && p.title.toLowerCase().includes(needle); const summaryHit = p.summary && p.summary.toLowerCase().includes(needle); const bodyHit = p.body && p.body.toLowerCase().includes(needle); if (titleHit || summaryHit || bodyHit) { // crude scoring: title > summary > body const score = (titleHit ? 100 : 0) + (summaryHit ? 10 : 0) + (bodyHit ? 1 : 0); out.push({ ...p, score }); } if (out.length >= 50) break; } out.sort((a, b) => b.score - a.score); return out.slice(0, 8); } function render(matches) { if (!matches.length) { results.hidden = true; results.innerHTML = ''; return; } results.innerHTML = matches.map(m => `
  • ${escapeHtml(m.title)} ${m.summary ? `${escapeHtml(m.summary)}` : ''}
  • ` ).join(''); results.hidden = false; } let timer = null; input.addEventListener('focus', loadIndex); input.addEventListener('input', () => { clearTimeout(timer); timer = setTimeout(() => { loadIndex().then(() => render(search(input.value.trim()))); }, 80); }); input.addEventListener('blur', () => { // Delay so click on a result registers before hiding. setTimeout(() => { results.hidden = true; }, 150); }); })();