// Shared client-side news search over /search.json (no dependencies, no modules). // Used by the homepage hero (live suggestions) and /search (full results). (function () { window.newsSearch = function (opts) { const input = document.getElementById(opts.input); const list = document.getElementById(opts.list); const status = document.getElementById(opts.status); let index = null; let timer; // First title match wrapped in (Starlight search pattern) — built // from text nodes, never innerHTML. function highlight(text, q) { const i = text.toLowerCase().indexOf(q); if (i < 0) return [text]; const mark = document.createElement('mark'); mark.textContent = text.slice(i, i + q.length); return [text.slice(0, i), mark, text.slice(i + q.length)]; } async function run() { const q = input.value.trim().toLowerCase(); list.innerHTML = ''; if (q.length < 2) { status.textContent = opts.hint || ''; return; } if (!index) index = await (await fetch('/search.json')).json(); const hits = index .filter((p) => (p.title + ' ' + p.description + ' ' + p.category).toLowerCase().includes(q)) .slice(0, opts.limit); const unit = opts.unit || 'result'; status.textContent = hits.length ? hits.length + ' ' + unit + (hits.length === 1 ? '' : 's') + ' for “' + input.value.trim() + '”' : 'No results for “' + input.value.trim() + '” — try a shorter or different term.'; for (const p of hits) { const li = document.createElement('li'); const a = document.createElement('a'); a.href = '/news/' + encodeURIComponent(p.slug); a.append(...highlight(p.title, q)); const small = document.createElement('small'); small.textContent = p.date; li.append(a, small); list.append(li); } } input.addEventListener('input', () => { clearTimeout(timer); timer = setTimeout(run, 150); }); return run; // callers may run() immediately (e.g. /search?q=…) }; })();