} Search results
*/
searchWithWorker(query, limit) {
const worker = initializeSearchWorker();
if (!worker) {
return this.fallbackSearch(query, limit);
}
return new Promise((resolve, reject) => {
const messageId = `search_${Date.now()}_${
Math.random().toString(36).substring(2, 11)
}`;
const timeout = setTimeout(() => {
cleanup();
reject(new Error("Web Worker search timeout"));
}, 5000);
const handleMessage = (e) => {
if (e.data.messageId !== messageId) return;
clearTimeout(timeout);
cleanup();
if (e.data.type === "results") {
resolve(e.data.data);
} else if (e.data.type === "error") {
reject(new Error(e.data.error || "Unknown worker error"));
}
};
const handleError = (error) => {
clearTimeout(timeout);
cleanup();
reject(error);
};
const cleanup = () => {
worker.removeEventListener("message", handleMessage);
worker.removeEventListener("error", handleError);
};
worker.addEventListener("message", handleMessage);
worker.addEventListener("error", handleError);
worker.postMessage(
{
messageId,
type: "search",
data: { query, limit },
documents: this.documents,
},
);
});
}
// Normalize text for comparison
normalizeForComparison(text) {
if (!text || typeof text !== "string") return "";
return text
.toLowerCase()
.replace(/\s+/g, " ")
.replace(/[.,!?;:'"…—–-]+$/g, "")
.trim();
}
// Find which section/heading a content match belongs to
findContainingSection(doc, matchIndex) {
if (!doc.content || !doc.anchors || doc.anchors.length === 0) {
return null;
}
const paragraphs = doc.content.split("\n").filter((p) => p.trim());
// Find which paragraph contains the match
let currentPos = 0;
let matchParagraphIndex = -1;
for (let i = 0; i < paragraphs.length; i++) {
const paragraphEnd = currentPos + paragraphs[i].length;
if (matchIndex >= currentPos && matchIndex < paragraphEnd) {
matchParagraphIndex = i;
break;
}
currentPos = paragraphEnd + 1;
}
if (matchParagraphIndex === -1) {
return null;
}
// Find the last heading that appears before this paragraph
let containingAnchor = null;
for (let i = 0; i <= matchParagraphIndex; i++) {
const para = paragraphs[i].trim();
const matchingAnchor = doc.anchors.find((a) => {
const normalizedAnchor = this.normalizeForComparison(a.text);
const normalizedPara = this.normalizeForComparison(para);
return normalizedAnchor === normalizedPara;
});
if (matchingAnchor) {
containingAnchor = matchingAnchor;
}
}
return containingAnchor;
}
// Generate preview for a specific section
generateSectionPreview(doc, anchor, query, maxLength = 200) {
if (!doc.content || !anchor) {
return "";
}
const paragraphs = doc.content.split("\n").filter((p) => p.trim());
// Find where this section starts and ends
let sectionStart = -1;
let sectionEnd = paragraphs.length;
for (let i = 0; i < paragraphs.length; i++) {
const para = paragraphs[i].trim();
const normalizedPara = this.normalizeForComparison(para);
const normalizedAnchor = this.normalizeForComparison(anchor.text);
if (normalizedPara === normalizedAnchor) {
sectionStart = i;
} else if (sectionStart !== -1 && doc.anchors) {
// Check if this is another heading
const isHeading = doc.anchors.some((a) => {
const norm = this.normalizeForComparison(a.text);
return norm === normalizedPara;
});
if (isHeading) {
sectionEnd = i;
break;
}
}
}
if (sectionStart === -1) {
return "";
}
// Get content of this section (excluding the heading itself)
const sectionParagraphs = paragraphs.slice(sectionStart + 1, sectionEnd);
const sectionContent = sectionParagraphs.join("\n");
// Use existing generatePreview on just this section's content
return this.generatePreview(sectionContent, query, maxLength);
}
// Escape regex special characters
escapeRegex(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
// Resolve path relative to current page location
resolvePath(path) {
// If path already starts with '/', it's absolute from domain root
if (path.startsWith("/")) {
return path;
}
// If path starts with '#', it's a fragment on current page
if (path.startsWith("#")) {
return path;
}
// Prepend root path for relative navigation
return this.rootPath + path;
}
// Lazy loading for search results
lazyLoadDocuments(docIds, limit = 10) {
if (!this.fullDocuments) {
// Store full documents separately for memory efficiency
this.fullDocuments = this.documents;
// Create lightweight index documents
this.documents = this.documents.map((doc) => ({
id: doc.id,
title: doc.title,
path: doc.path,
}));
}
return docIds.slice(0, limit).map((id) => this.fullDocuments[id]);
}
// Fallback search method via simple string matching
fallbackSearch(query, limit = 10) {
if (!query || typeof query !== "string") return [];
const lowerQuery = query.toLowerCase();
if (lowerQuery.length < 2) return [];
const results = this.documents
.map((doc) => {
if (!doc || !doc.title || !doc.content) {
return null;
}
const titleMatch = doc.title.toLowerCase().indexOf(lowerQuery);
const contentMatch = doc.content.toLowerCase().indexOf(lowerQuery);
let pageScore = 0;
if (titleMatch !== -1) {
pageScore += 10;
if (doc.title.toLowerCase() === lowerQuery) {
pageScore += 20;
}
}
if (contentMatch !== -1) {
pageScore += 2;
}
// Find matching anchors
const matchingAnchors = [];
if (
doc.anchors &&
Array.isArray(doc.anchors) &&
doc.anchors.length > 0
) {
doc.anchors.forEach((anchor) => {
if (!anchor || !anchor.text) return;
const anchorText = anchor.text.toLowerCase();
if (anchorText.includes(lowerQuery)) {
matchingAnchors.push(anchor);
}
});
}
return { doc, pageScore, matchingAnchors, titleMatch, contentMatch };
})
.filter((item) => item !== null && item.pageScore > 0)
.sort((a, b) => {
if (a.pageScore !== b.pageScore) return b.pageScore - a.pageScore;
if (a.titleMatch !== b.titleMatch) return a.titleMatch - b.titleMatch;
return a.contentMatch - b.contentMatch;
})
.slice(0, limit);
return results;
}
}
// Web Worker for background search processing
// Create Web Worker if supported - initialized lazily to use rootPath
let searchWorker = null;
function debounce(func, wait) {
let timeout = null;
return function (...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
function initializeSearchWorker() {
if (searchWorker !== null || typeof Worker === "undefined") {
return searchWorker;
}
try {
const rootPath = window.searchNamespace?.rootPath || "";
const workerPath = rootPath
? `${rootPath}assets/search-worker.js`
: "/assets/search-worker.js";
searchWorker = new Worker(workerPath);
console.log("Web Worker initialized for background search");
return searchWorker;
} catch (error) {
console.warn("Web Worker creation failed, using main thread:", error);
searchWorker = false; // mark as failed so we don't retry
return null;
}
}
// Global search engine instance
window.searchNamespace.engine = new SearchEngine();
// Mobile search timeout for debouncing
let mobileSearchTimeout = null;
// AbortController for cancelling pending search requests
let searchPageController = null;
document.addEventListener("DOMContentLoaded", function () {
// Initialize search engine immediately
window.searchNamespace.engine
.loadData()
.then(() => {
console.log("Search data loaded successfully");
})
.catch((error) => {
console.error("Failed to initialize search:", error);
});
// Search page specific functionality
const searchPageInput = document.getElementById("search-page-input");
if (searchPageInput) {
// Set up event listener with debouncing
searchPageInput.addEventListener(
"input",
debounce(function () {
const query = this.value.trim();
if (query.length >= 2) {
performSearch(query);
} else {
const resultsContainer = document.getElementById(
"search-page-results",
);
if (resultsContainer) {
resultsContainer.innerHTML =
"Please enter at least 2 characters to search
";
}
}
}, 200),
);
// Perform search if URL has query
const params = new URLSearchParams(window.location.search);
const query = params.get("q");
if (query) {
searchPageInput.value = query;
performSearch(query);
}
}
// Desktop Sidebar Toggle
const searchInput = document.getElementById("search-input");
if (searchInput) {
const searchResults = document.getElementById("search-results");
const searchContainer = searchInput.closest(".search-container");
searchInput.addEventListener(
"input",
debounce(async function () {
const searchTerm = this.value.trim();
const currentSearchTerm = searchTerm;
if (searchTerm.length < 2) {
searchResults.innerHTML = "";
searchResults.style.display = "none";
if (searchContainer) searchContainer.classList.remove("has-results");
return;
}
searchResults.innerHTML =
'Loading...
';
searchResults.style.display = "block";
if (searchContainer) searchContainer.classList.add("has-results");
try {
const results = await window.searchNamespace.engine.search(
searchTerm,
8,
);
if (currentSearchTerm !== searchTerm) return;
if (results.length > 0) {
searchResults.innerHTML = results
.map((result) => {
const { doc, matchingAnchors } = result;
const queryTerms = window.searchNamespace.engine.tokenize(
searchTerm,
);
const highlightedTitle = window.searchNamespace.engine
.highlightTerms(
doc.title,
queryTerms,
);
const resolvedPath = window.searchNamespace.engine.resolvePath(
doc.path,
);
let html = `
`;
if (matchingAnchors && matchingAnchors.length > 0) {
matchingAnchors.forEach((anchor) => {
// Skip anchors that duplicate the page title
const normalizedAnchor = window.searchNamespace.engine
.normalizeForComparison(
anchor.text,
);
const normalizedTitle = window.searchNamespace.engine
.normalizeForComparison(
doc.title,
);
if (normalizedAnchor === normalizedTitle) {
return;
}
const highlightedAnchor = window.searchNamespace.engine
.highlightTerms(
anchor.text,
queryTerms,
);
const anchorPath = `${resolvedPath}#${anchor.id}`;
html += `
`;
});
}
return html;
})
.join("");
searchResults.style.display = "block";
if (searchContainer) searchContainer.classList.add("has-results");
} else {
searchResults.innerHTML =
'No results found
';
searchResults.style.display = "block";
if (searchContainer) searchContainer.classList.add("has-results");
}
} catch (error) {
console.error("Search error:", error);
searchResults.innerHTML =
'Search unavailable
';
searchResults.style.display = "block";
if (searchContainer) searchContainer.classList.add("has-results");
}
}, 150),
);
// Hide results when clicking outside
document.addEventListener("click", function (event) {
if (
!searchInput.contains(event.target) &&
!searchResults.contains(event.target)
) {
searchResults.style.display = "none";
if (searchContainer) searchContainer.classList.remove("has-results");
}
});
// Focus search when pressing slash key
document.addEventListener("keydown", function (event) {
if (event.key === "/" && document.activeElement !== searchInput) {
event.preventDefault();
searchInput.focus();
}
// Close search results on Escape key
if (
event.key === "Escape" &&
(document.activeElement === searchInput ||
searchResults.style.display === "block")
) {
searchResults.style.display = "none";
if (searchContainer) searchContainer.classList.remove("has-results");
searchInput.blur();
}
});
setupDocumentEventHandlers(searchInput, searchResults, searchContainer);
}
function setupDocumentEventHandlers(
searchInput,
searchResults,
searchContainer,
) {
document.addEventListener("click", function (event) {
const isMobileSearchActive = mobileSearchPopup &&
mobileSearchPopup.classList.contains("active");
const isDesktopResultsVisible = searchResults.style.display === "block";
if (
isMobileSearchActive &&
!mobileSearchPopup.contains(event.target) &&
!searchInput.contains(event.target)
) {
closeMobileSearch();
}
if (
isDesktopResultsVisible &&
!searchInput.contains(event.target) &&
!searchResults.contains(event.target)
) {
searchResults.style.display = "none";
if (searchContainer) searchContainer.classList.remove("has-results");
}
});
document.addEventListener("keydown", function (event) {
if (event.key === "/" && document.activeElement !== searchInput) {
event.preventDefault();
searchInput.focus();
}
if (
event.key === "Escape" &&
(document.activeElement === searchInput ||
searchResults.style.display === "block")
) {
searchResults.style.display = "none";
if (searchContainer) searchContainer.classList.remove("has-results");
searchInput.blur();
}
if (
event.key === "Escape" &&
mobileSearchPopup &&
mobileSearchPopup.classList.contains("active")
) {
closeMobileSearch();
}
});
}
// Mobile search functionality
// This detects mobile viewport and adds click behavior
function isMobile() {
return window.innerWidth <= 800;
}
if (searchInput) {
// Add mobile search behavior
searchInput.addEventListener("click", function (e) {
if (isMobile()) {
e.preventDefault();
e.stopPropagation();
openMobileSearch();
}
// On desktop, we let the normal click behavior work (focus the input)
});
// Prevent typing on mobile (input should only open popup)
searchInput.addEventListener("keydown", function (e) {
if (isMobile()) {
e.preventDefault();
openMobileSearch();
}
});
}
// Mobile search popup functionality
const mobileSearchPopup = document.getElementById("mobile-search-popup");
const mobileSearchInput = document.getElementById("mobile-search-input");
const mobileSearchResults = document.getElementById("mobile-search-results");
const closeMobileSearchBtn = document.getElementById("close-mobile-search");
function openMobileSearch() {
if (mobileSearchPopup) {
mobileSearchPopup.classList.add("active");
// Focus the input after a small delay to ensure the popup is visible
setTimeout(() => {
if (mobileSearchInput) {
mobileSearchInput.focus();
}
}, 100);
}
}
function closeMobileSearch() {
if (mobileSearchPopup) {
mobileSearchPopup.classList.remove("active");
if (mobileSearchInput) {
mobileSearchInput.value = "";
}
if (mobileSearchResults) {
mobileSearchResults.innerHTML = "";
mobileSearchResults.style.display = "none";
}
}
}
if (closeMobileSearchBtn) {
closeMobileSearchBtn.addEventListener("click", closeMobileSearch);
}
// Mobile search input
if (mobileSearchInput && mobileSearchResults) {
function handleMobileSearchInput() {
clearTimeout(mobileSearchTimeout);
const searchTerm = mobileSearchInput.value.trim();
if (searchTerm.length < 2) {
mobileSearchResults.innerHTML = "";
mobileSearchResults.style.display = "none";
return;
}
mobileSearchTimeout = setTimeout(async () => {
// Verify the input still matches before proceeding
if (mobileSearchInput.value.trim() !== searchTerm) return;
// Show loading state
mobileSearchResults.innerHTML =
'Loading...
';
mobileSearchResults.style.display = "block";
try {
const results = await window.searchNamespace.engine.search(
searchTerm,
8,
);
// Verify again after async operation
if (mobileSearchInput.value.trim() !== searchTerm) return;
if (results.length > 0) {
mobileSearchResults.innerHTML = results
.map((result) => {
const { doc, matchingAnchors } = result;
const queryTerms = window.searchNamespace.engine.tokenize(
searchTerm,
);
const highlightedTitle = window.searchNamespace.engine
.highlightTerms(
doc.title,
queryTerms,
);
const resolvedPath = window.searchNamespace.engine.resolvePath(
doc.path,
);
// Build page result
let html = `
`;
// Add anchor results if any
if (matchingAnchors && matchingAnchors.length > 0) {
matchingAnchors.forEach((anchor) => {
// Skip anchors that duplicate the page title
const normalizedAnchor = window.searchNamespace.engine
.normalizeForComparison(
anchor.text,
);
const normalizedTitle = window.searchNamespace.engine
.normalizeForComparison(
doc.title,
);
if (normalizedAnchor === normalizedTitle) {
return;
}
const highlightedAnchor = window.searchNamespace.engine
.highlightTerms(
anchor.text,
queryTerms,
);
const sectionPreview = window.searchNamespace.engine
.generateSectionPreview(
doc,
anchor,
searchTerm,
100,
);
const anchorPath = `${resolvedPath}#${anchor.id}`;
html += `
`;
});
}
return html;
})
.join("");
mobileSearchResults.style.display = "block";
} else {
mobileSearchResults.innerHTML =
'No results found
';
mobileSearchResults.style.display = "block";
}
} catch (error) {
console.error("Mobile search error:", error);
// Verify once more
if (mobileSearchInput.value.trim() !== searchTerm) return;
mobileSearchResults.innerHTML =
'Search unavailable
';
mobileSearchResults.style.display = "block";
}
}, 300);
}
mobileSearchInput.addEventListener("input", handleMobileSearchInput);
}
// Handle window resize to update mobile behavior
window.addEventListener("resize", function () {
// Close mobile search if window is resized to desktop size
if (
!isMobile() &&
mobileSearchPopup &&
mobileSearchPopup.classList.contains("active")
) {
closeMobileSearch();
}
});
});
async function performSearch(query) {
query = query.trim();
const resultsContainer = document.getElementById("search-page-results");
if (query.length < 2) {
resultsContainer.innerHTML =
"Please enter at least 2 characters to search
";
return;
}
// Cancel any pending search
if (searchPageController) {
searchPageController.abort();
}
searchPageController = new AbortController();
// Show loading state
resultsContainer.innerHTML = "Searching...
";
try {
const results = await window.searchNamespace.engine.search(query, 50, {
signal: searchPageController.signal,
});
// Check if aborted before rendering
if (searchPageController.signal.aborted) {
return;
}
// Display results
if (results.length > 0) {
let html = '';
const queryTerms = window.searchNamespace.engine.tokenize(query);
for (const result of results) {
const { doc, matchingAnchors } = result;
const highlightedTitle = window.searchNamespace.engine.highlightTerms(
doc.title,
queryTerms,
);
const preview = window.searchNamespace.engine.generatePreview(
doc.content,
query,
);
const resolvedPath = window.searchNamespace.engine.resolvePath(
doc.path,
);
// Page result
html += `-
${highlightedTitle}
${preview}
`;
// Anchor results
if (matchingAnchors && matchingAnchors.length > 0) {
matchingAnchors.forEach((anchor) => {
// Skip anchors that have the same text as the page title to avoid duplication
const normalizedAnchor = window.searchNamespace.engine
.normalizeForComparison(anchor.text);
const normalizedTitle = window.searchNamespace.engine
.normalizeForComparison(doc.title);
if (normalizedAnchor === normalizedTitle) {
return;
}
const highlightedAnchor = window.searchNamespace.engine
.highlightTerms(
anchor.text,
queryTerms,
);
const sectionPreview = window.searchNamespace.engine
.generateSectionPreview(
doc,
anchor,
query,
);
const anchorPath = `${resolvedPath}#${anchor.id}`;
html += `-
${highlightedAnchor}
${sectionPreview}
`;
});
}
}
html += "
";
resultsContainer.innerHTML = html;
} else {
resultsContainer.innerHTML = "No results found
";
}
// Update URL with query
const url = new URL(window.location.href);
url.searchParams.set("q", query);
window.history.replaceState({}, "", url.toString());
} catch (error) {
if (error.name === "AbortError") {
return;
}
console.error("Search error:", error);
resultsContainer.innerHTML = "Search temporarily unavailable
";
}
}