let lunrIndex; let documents = []; let activeIndex = -1; let currentResults = []; let searchReady = false; const searchInput = document.getElementById("search-input"); const searchBtn = document.getElementById("search-btn"); const searchForm = searchInput?.closest("form"); const searchContainer = searchInput?.closest(".site-actions"); const searchPanel = document.createElement("section"); searchPanel.id = "search-inline-panel"; searchPanel.className = "search-inline-panel"; searchPanel.setAttribute("aria-hidden", "true"); searchPanel.setAttribute("aria-labelledby", "search-pane-title"); searchPanel.innerHTML = `
`; if (searchContainer) { searchContainer.appendChild(searchPanel); } const inlineResults = searchPanel.querySelector("#search-inline-results"); const paneCount = searchPanel.querySelector("#search-pane-count"); function extractDocuments(node, currentPath = "") { if (node.type === "folder" && node.children) { const nextPath = currentPath ? `${currentPath}/${node.name}` : node.name; node.children.forEach((child) => { extractDocuments(child, nextPath); }); } if (node.type === "file") { documents.push({ id: documents.length.toString(), title: node.name, content: node.content || "", path: node.url, }); } } function buildIndex() { lunrIndex = lunr(function () { this.ref("id"); this.field("title", { boost: 10 }); this.field("url"); this.field("content"); documents.forEach((doc) => this.add(doc)); }); } async function initSearch() { try { const response = await fetch("/test.json"); const json = await response.json(); extractDocuments(json); buildIndex(); searchReady = true; if (isSearchOpen()) renderResults(searchInput.value); } catch (error) { console.warn("Search index could not be loaded", error); if (paneCount) paneCount.textContent = "Search is unavailable right now."; } } initSearch(); function buildExactQuery(query) { return query .toLowerCase() .split(/\s+/) .filter((term) => term.length > 1) .map((term) => `${term}*`) .join(" "); } function buildSnippet(content, terms, radius = 90) { if (!content || terms.length === 0) return ""; const lower = content.toLowerCase(); let index = -1; let matchedTerm = ""; for (const term of terms) { const i = lower.indexOf(term); if (i !== -1 && (index === -1 || i < index)) { index = i; matchedTerm = term; } } if (index === -1) { return content.slice(0, radius * 2) + "..."; } const start = Math.max(0, index - radius); const end = Math.min(content.length, index + matchedTerm.length + radius); const prefix = start > 0 ? "..." : ""; const suffix = end < content.length ? "..." : ""; return prefix + content.slice(start, end) + suffix; } function getSearchTerms(query) { return query .toLowerCase() .split(/\s+/) .filter((t) => t.length > 2); } function escapeHtml(text) { return String(text || "") .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } function highlight(text, terms) { const escapedText = escapeHtml(text); if (!escapedText || terms.length === 0) return escapedText; const escapedTerms = terms.map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")); const regex = new RegExp(`(${escapedTerms.join("|")})`, "gi"); return escapedText.replace(regex, "$1"); } function search(query) { if (!lunrIndex) return []; const q = buildExactQuery(query); if (!q) return []; const terms = getSearchTerms(query); const results = lunrIndex.search(q); return results .map((r) => { const doc = documents.find((d) => d.id === r.ref); if (!doc) return null; const snippet = buildSnippet(doc.content, terms); return { title: highlight(doc.title, terms), path: doc.path, score: r.score, preview: highlight(snippet, terms), }; }) .filter(Boolean); } function isSearchOpen() { return searchContainer?.classList.contains("search-inline-open"); } function openSearchPane(initialValue = null, focusInput = true) { if (!searchContainer || !searchInput) return; if (initialValue !== null) { searchInput.value = initialValue; } searchContainer.classList.add("search-inline-open"); searchPanel.setAttribute("aria-hidden", "false"); searchBtn?.setAttribute("aria-expanded", "true"); renderResults(searchInput.value); if (focusInput) { searchInput.focus({ preventScroll: true }); try { searchInput.setSelectionRange(searchInput.value.length, searchInput.value.length); } catch (_) {} } } function closeSearchPane() { if (!searchContainer) return; searchContainer.classList.remove("search-inline-open"); searchPanel.setAttribute("aria-hidden", "true"); searchBtn?.setAttribute("aria-expanded", "false"); activeIndex = -1; } function getResultItems() { return Array.from(inlineResults.querySelectorAll(".search-result")); } function updateActiveResult(focusActive = false) { const items = getResultItems(); items.forEach((item, index) => { const isActive = index === activeIndex; item.classList.toggle("active", isActive); item.setAttribute("aria-selected", String(isActive)); }); if (activeIndex >= 0 && activeIndex < items.length) { items[activeIndex].scrollIntoView({ block: "nearest", behavior: "smooth", }); if (focusActive) searchInput?.focus({ preventScroll: true }); } } function setActiveResult(index, focusActive = false) { const items = getResultItems(); if (!items.length) { activeIndex = -1; return; } activeIndex = Math.max(0, Math.min(index, items.length - 1)); updateActiveResult(focusActive); } function openActiveResult() { const items = getResultItems(); if (!items.length) return; const index = activeIndex >= 0 ? activeIndex : 0; items[index]?.click(); } function renderResults(query) { const trimmed = query.trim(); currentResults = trimmed.length >= 2 ? search(trimmed) : []; activeIndex = currentResults.length ? 0 : -1; inlineResults.innerHTML = ""; if (!trimmed) { paneCount.textContent = "Start typing to search your notes."; inlineResults.innerHTML = `