This commit is contained in:
NotAShelf 2026-04-22 21:19:14 +00:00
commit d39bd7cecd
64 changed files with 8748 additions and 249256 deletions

View file

@ -1,13 +1,25 @@
if (!window.searchNamespace) window.searchNamespace = {};
class SearchEngine {
// Characters to strip from search term ends for better matching
static STRIP_TRAILING_CHARS_RE = /[.,!?;:'"…—–-]+$/g;
constructor() {
this.documents = [];
this.tokenMap = new Map();
this.lowercaseCache = [];
this.isLoaded = false;
this.loadError = false;
this.fullDocuments = null; // for lazy loading
this.rootPath = window.searchNamespace?.rootPath || "";
// Search configuration (loaded from search data)
this.config = {
minWordLength: 2,
stopwords: [],
boostTitle: 100.0,
boostContent: 30.0,
boostAnchor: 10.0,
};
}
// Check if we can use Web Worker
@ -53,19 +65,30 @@ class SearchEngine {
throw new Error("Search data file not found at any expected location");
}
console.log(`Loading search data from: ${usedPath}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const documents = await response.json();
if (!Array.isArray(documents)) {
throw new Error("Invalid search data format");
// New format with config
if (documents.documents && Array.isArray(documents.documents)) {
this.config = {
minWordLength: documents.min_word_length || 2,
stopwords: documents.stopwords || [],
boostTitle: documents.boost_title || 100.0,
boostContent: documents.boost_content || 30.0,
boostAnchor: documents.boost_anchor || 10.0,
};
this.initializeFromDocuments(documents.documents);
} else {
throw new Error("Invalid search data format");
}
} else {
// Legacy format - just an array of documents
this.initializeFromDocuments(documents);
}
this.initializeFromDocuments(documents);
this.isLoaded = true;
console.log(`Loaded ${documents.length} documents for search`);
} catch (error) {
console.error("Error loading search data:", error);
this.documents = [];
@ -81,7 +104,6 @@ class SearchEngine {
this.documents = [];
} else {
this.documents = documents;
console.log(`Initialized with ${documents.length} documents`);
}
try {
await this.buildTokenMap();
@ -94,10 +116,13 @@ class SearchEngine {
initializeIndex(indexData) {
this.documents = indexData.documents || [];
this.tokenMap = new Map(Object.entries(indexData.tokenMap || {}));
this.lowercaseCache = this.documents.map((doc) => ({
title: (doc.title || "").toLowerCase(),
content: (doc.content || "").toLowerCase(),
}));
}
// Build token map
// This is helpful for faster searching with progressive loading
// Build token map for faster searching
buildTokenMap() {
return new Promise((resolve, reject) => {
this.tokenMap.clear();
@ -111,6 +136,8 @@ class SearchEngine {
const totalDocs = this.documents.length;
let processedDocs = 0;
this.lowercaseCache = [];
try {
// Process in chunks to avoid blocking UI
const processChunk = (startIndex, chunkSize) => {
@ -128,7 +155,14 @@ class SearchEngine {
continue;
}
const tokens = this.tokenize(doc.title + " " + doc.content);
const lowerTitle = doc.title.toLowerCase();
const lowerContent = doc.content.toLowerCase();
this.lowercaseCache[i] = {
title: lowerTitle,
content: lowerContent,
};
const tokens = this.tokenize(lowerTitle + " " + lowerContent);
tokens.forEach((token) => {
if (!this.tokenMap.has(token)) {
this.tokenMap.set(token, []);
@ -143,9 +177,6 @@ class SearchEngine {
if (endIndex < totalDocs) {
setTimeout(() => processChunk(endIndex, chunkSize), 0);
} else {
console.log(
`Built token map with ${this.tokenMap.size} unique tokens from ${processedDocs} documents`,
);
resolve();
}
} catch (error) {
@ -308,8 +339,8 @@ class SearchEngine {
}
score = Math.min(1.0, score + boundaryBonus);
const lengthPenalty = Math.abs(query.length - n) /
Math.max(query.length, m);
const lengthPenalty =
Math.abs(query.length - n) / Math.max(query.length, m);
score -= lengthPenalty * 0.2;
return Math.max(0, Math.min(1.0, score));
@ -319,7 +350,13 @@ class SearchEngine {
if (!text || typeof text !== "string") return [];
const words = text.toLowerCase().match(/\b[a-zA-Z0-9_-]+\b/g) || [];
const tokens = words.filter((word) => word.length > 2);
const stopwordsSet = new Set(
this.config.stopwords.map((w) => w.toLowerCase()),
);
const tokens = words.filter(
(word) =>
word.length >= this.config.minWordLength && !stopwordsSet.has(word),
);
return Array.from(new Set(tokens));
}
@ -343,7 +380,6 @@ class SearchEngine {
}
if (!this.isLoaded || this.documents.length === 0) {
console.log("Search data not available");
return [];
}
@ -357,12 +393,23 @@ class SearchEngine {
const useFuzzySearch = rawQuery.length >= 3;
const candidateDocIds = new Set();
searchTerms.forEach((term) => {
if (this.tokenMap.has(term)) {
const docIds = this.tokenMap.get(term);
docIds.forEach((docId) => candidateDocIds.add(docId));
}
});
if (candidateDocIds.size === 0) {
return [];
}
const pageMatches = new Map();
const totalDocs = this.documents.length;
let lastCheckTime = Date.now();
const CHECK_INTERVAL = 16; // Check every ~16ms (one frame)
for (let docIdx = 0; docIdx < totalDocs; docIdx++) {
for (const docIdx of candidateDocIds) {
// Check for abort periodically
if (Date.now() - lastCheckTime > CHECK_INTERVAL) {
if (options.signal?.aborted) {
@ -384,33 +431,37 @@ class SearchEngine {
pageMatches.set(docIdx, match);
}
const lowerTitle = (
typeof doc.title === "string" ? doc.title : ""
).toLowerCase();
const lowerContent = (
typeof doc.content === "string" ? doc.content : ""
).toLowerCase();
const cached = this.lowercaseCache?.[docIdx];
const lowerTitle =
cached?.title ??
(typeof doc.title === "string" ? doc.title : "").toLowerCase();
const lowerContent =
cached?.content ??
(typeof doc.content === "string" ? doc.content : "").toLowerCase();
if (useFuzzySearch) {
const fuzzyTitleScore = this.fuzzyMatch(rawQuery, lowerTitle);
if (fuzzyTitleScore !== null) {
match.pageScore += fuzzyTitleScore * 100;
match.pageScore += fuzzyTitleScore * this.config.boostTitle;
}
const fuzzyContentScore = this.fuzzyMatch(rawQuery, lowerContent);
if (fuzzyContentScore !== null) {
match.pageScore += fuzzyContentScore * 30;
match.pageScore += fuzzyContentScore * this.config.boostContent;
}
}
searchTerms.forEach((term) => {
if (lowerTitle.includes(term)) {
match.pageScore += lowerTitle === term ? 20 : 10;
match.pageScore +=
lowerTitle === term
? this.config.boostTitle / 5
: this.config.boostTitle / 10;
}
if (lowerContent.includes(term)) {
match.pageScore += 2;
match.pageScore += this.config.boostContent / 15;
}
});
}
@ -629,9 +680,9 @@ class SearchEngine {
}
return new Promise((resolve, reject) => {
const messageId = `search_${Date.now()}_${
Math.random().toString(36).substring(2, 11)
}`;
const messageId = `search_${Date.now()}_${Math.random()
.toString(36)
.substring(2, 11)}`;
const timeout = setTimeout(() => {
cleanup();
reject(new Error("Web Worker search timeout"));
@ -664,14 +715,12 @@ class SearchEngine {
worker.addEventListener("message", handleMessage);
worker.addEventListener("error", handleError);
worker.postMessage(
{
messageId,
type: "search",
data: { query, limit },
documents: this.documents,
},
);
worker.postMessage({
messageId,
type: "search",
data: { query, limit },
documents: this.documents,
});
});
}
@ -681,7 +730,7 @@ class SearchEngine {
return text
.toLowerCase()
.replace(/\s+/g, " ")
.replace(/[.,!?;:'"…—–-]+$/g, "")
.replace(SearchEngine.STRIP_TRAILING_CHARS_RE, "")
.trim();
}
@ -829,13 +878,13 @@ class SearchEngine {
let pageScore = 0;
if (titleMatch !== -1) {
pageScore += 10;
pageScore += this.config.boostTitle / 10;
if (doc.title.toLowerCase() === lowerQuery) {
pageScore += 20;
pageScore += this.config.boostTitle / 5;
}
}
if (contentMatch !== -1) {
pageScore += 2;
pageScore += this.config.boostContent / 15;
}
// Find matching anchors
@ -872,6 +921,105 @@ class SearchEngine {
// Create Web Worker if supported - initialized lazily to use rootPath
let searchWorker = null;
// Keyboard navigation helper class
class SearchKeyboardNav {
constructor(container, selector) {
this.container = container;
this.selector = selector;
this.activeIndex = -1;
this.items = [];
this.navigationPending = false;
}
updateItems() {
this.items = Array.from(this.container.querySelectorAll(this.selector));
if (this.activeIndex >= this.items.length) {
this.activeIndex = -1;
}
}
clear() {
this.setActive(-1);
this.items = [];
}
setActive(index) {
// Remove active class from previous item
if (this.activeIndex >= 0 && this.activeIndex < this.items.length) {
this.items[this.activeIndex].classList.remove("search-result-active");
}
this.activeIndex = index;
// Add active class to new item
if (this.activeIndex >= 0 && this.activeIndex < this.items.length) {
this.items[this.activeIndex].classList.add("search-result-active");
this.items[this.activeIndex].scrollIntoView({
block: "nearest",
behavior: "smooth",
});
}
}
moveDown() {
if (this.items.length === 0) return;
const newIndex = Math.min(this.activeIndex + 1, this.items.length - 1);
this.setActive(newIndex);
}
moveUp() {
if (this.items.length === 0) return;
const newIndex = Math.max(this.activeIndex - 1, -1);
this.setActive(newIndex);
}
moveToFirst() {
if (this.items.length === 0) return;
this.setActive(0);
}
moveToLast() {
if (this.items.length === 0) return;
this.setActive(this.items.length - 1);
}
select() {
// Guard against double-navigation
if (this.navigationPending) return false;
if (this.activeIndex >= 0 && this.activeIndex < this.items.length) {
const link = this.items[this.activeIndex].querySelector("a");
if (link) {
this.navigationPending = true;
// Add search query to URL if it's a result link
const currentQuery =
this.container.closest(".search-container")?.querySelector("input")
?.value || document.getElementById("search-page-input")?.value;
if (currentQuery) {
const url = new URL(link.href, window.location.origin);
url.searchParams.set("highlight", currentQuery);
// Clear flag after navigation starts
setTimeout(() => {
this.navigationPending = false;
}, 100);
window.location.href = url.toString();
} else {
// Clear flag before click to allow navigation
setTimeout(() => {
this.navigationPending = false;
}, 100);
link.click();
}
return true;
}
}
return false;
}
}
function debounce(func, wait) {
let timeout = null;
return function (...args) {
@ -891,7 +1039,6 @@ function initializeSearchWorker() {
? `${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);
@ -913,9 +1060,7 @@ document.addEventListener("DOMContentLoaded", function () {
// Initialize search engine immediately
window.searchNamespace.engine
.loadData()
.then(() => {
console.log("Search data loaded successfully");
})
.then(() => {})
.catch((error) => {
console.error("Failed to initialize search:", error);
});
@ -923,13 +1068,53 @@ document.addEventListener("DOMContentLoaded", function () {
// Search page specific functionality
const searchPageInput = document.getElementById("search-page-input");
if (searchPageInput) {
// Initialize keyboard navigation for search page
const searchPageResults = document.getElementById("search-page-results");
const searchPageKeyboardNav = new SearchKeyboardNav(
searchPageResults,
".search-result-item",
);
// Keyboard navigation for search page
searchPageInput.addEventListener("keydown", function (event) {
const hasResults =
searchPageResults &&
searchPageResults.querySelector(".search-result-item");
if (!hasResults) return;
if (event.key === "ArrowDown") {
event.preventDefault();
searchPageKeyboardNav.moveDown();
} else if (event.key === "ArrowUp") {
event.preventDefault();
searchPageKeyboardNav.moveUp();
} else if (event.key === "Home") {
event.preventDefault();
searchPageKeyboardNav.moveToFirst();
} else if (event.key === "End") {
event.preventDefault();
searchPageKeyboardNav.moveToLast();
} else if (
event.key === "Enter" &&
searchPageKeyboardNav.activeIndex >= 0
) {
event.preventDefault();
searchPageKeyboardNav.select();
} else if (event.key === "Escape") {
event.preventDefault();
searchPageKeyboardNav.clear();
searchPageInput.blur();
}
});
// Set up event listener with debouncing
searchPageInput.addEventListener(
"input",
debounce(function () {
const query = this.value.trim();
if (query.length >= 2) {
performSearch(query);
performSearch(query, searchPageKeyboardNav);
} else {
const resultsContainer = document.getElementById(
"search-page-results",
@ -938,6 +1123,7 @@ document.addEventListener("DOMContentLoaded", function () {
resultsContainer.innerHTML =
"<p>Please enter at least 2 characters to search</p>";
}
searchPageKeyboardNav.clear();
}
}, 200),
);
@ -947,7 +1133,7 @@ document.addEventListener("DOMContentLoaded", function () {
const query = params.get("q");
if (query) {
searchPageInput.value = query;
performSearch(query);
performSearch(query, searchPageKeyboardNav);
}
}
@ -956,6 +1142,11 @@ document.addEventListener("DOMContentLoaded", function () {
if (searchInput) {
const searchResults = document.getElementById("search-results");
const searchContainer = searchInput.closest(".search-container");
// Initialize keyboard navigation for desktop search
const desktopKeyboardNav = new SearchKeyboardNav(
searchResults,
".search-result-item",
);
searchInput.addEventListener(
"input",
@ -967,6 +1158,7 @@ document.addEventListener("DOMContentLoaded", function () {
searchResults.innerHTML = "";
searchResults.style.display = "none";
if (searchContainer) searchContainer.classList.remove("has-results");
desktopKeyboardNav.clear();
return;
}
@ -987,11 +1179,10 @@ document.addEventListener("DOMContentLoaded", function () {
searchResults.innerHTML = results
.map((result) => {
const { doc, matchingAnchors } = result;
const queryTerms = window.searchNamespace.engine.tokenize(
searchTerm,
);
const highlightedTitle = window.searchNamespace.engine
.highlightTerms(
const queryTerms =
window.searchNamespace.engine.tokenize(searchTerm);
const highlightedTitle =
window.searchNamespace.engine.highlightTerms(
doc.title,
queryTerms,
);
@ -1008,20 +1199,20 @@ document.addEventListener("DOMContentLoaded", function () {
if (matchingAnchors && matchingAnchors.length > 0) {
matchingAnchors.forEach((anchor) => {
// Skip anchors that duplicate the page title
const normalizedAnchor = window.searchNamespace.engine
.normalizeForComparison(
const normalizedAnchor =
window.searchNamespace.engine.normalizeForComparison(
anchor.text,
);
const normalizedTitle = window.searchNamespace.engine
.normalizeForComparison(
const normalizedTitle =
window.searchNamespace.engine.normalizeForComparison(
doc.title,
);
if (normalizedAnchor === normalizedTitle) {
return;
}
const highlightedAnchor = window.searchNamespace.engine
.highlightTerms(
const highlightedAnchor =
window.searchNamespace.engine.highlightTerms(
anchor.text,
queryTerms,
);
@ -1039,6 +1230,7 @@ document.addEventListener("DOMContentLoaded", function () {
.join("");
searchResults.style.display = "block";
if (searchContainer) searchContainer.classList.add("has-results");
desktopKeyboardNav.updateItems();
} else {
searchResults.innerHTML =
'<div class="search-result-item">No results found</div>';
@ -1048,7 +1240,7 @@ document.addEventListener("DOMContentLoaded", function () {
} catch (error) {
console.error("Search error:", error);
searchResults.innerHTML =
'<div class="search-result-item">Search unavailable</div>';
'<div class="search-result-item search-error" role="alert">Search unavailable. <a href="#" onclick="event.preventDefault(); window.searchNamespace.engine.loadData();">Retry</a></div>';
searchResults.style.display = "block";
if (searchContainer) searchContainer.classList.add("has-results");
}
@ -1063,6 +1255,35 @@ document.addEventListener("DOMContentLoaded", function () {
) {
searchResults.style.display = "none";
if (searchContainer) searchContainer.classList.remove("has-results");
desktopKeyboardNav.clear();
}
});
// Keyboard navigation for desktop search
searchInput.addEventListener("keydown", function (event) {
if (searchResults.style.display !== "block") return;
if (event.key === "ArrowDown") {
event.preventDefault();
desktopKeyboardNav.moveDown();
} else if (event.key === "ArrowUp") {
event.preventDefault();
desktopKeyboardNav.moveUp();
} else if (event.key === "Home") {
event.preventDefault();
desktopKeyboardNav.moveToFirst();
} else if (event.key === "End") {
event.preventDefault();
desktopKeyboardNav.moveToLast();
} else if (event.key === "Enter" && desktopKeyboardNav.activeIndex >= 0) {
event.preventDefault();
desktopKeyboardNav.select();
} else if (event.key === "Escape") {
event.preventDefault();
searchResults.style.display = "none";
if (searchContainer) searchContainer.classList.remove("has-results");
desktopKeyboardNav.clear();
searchInput.blur();
}
});
@ -1072,17 +1293,6 @@ document.addEventListener("DOMContentLoaded", function () {
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);
@ -1094,8 +1304,8 @@ document.addEventListener("DOMContentLoaded", function () {
searchContainer,
) {
document.addEventListener("click", function (event) {
const isMobileSearchActive = mobileSearchPopup &&
mobileSearchPopup.classList.contains("active");
const isMobileSearchActive =
mobileSearchPopup && mobileSearchPopup.classList.contains("active");
const isDesktopResultsVisible = searchResults.style.display === "block";
if (
@ -1174,6 +1384,76 @@ document.addEventListener("DOMContentLoaded", function () {
const mobileSearchResults = document.getElementById("mobile-search-results");
const closeMobileSearchBtn = document.getElementById("close-mobile-search");
// Store cleanup function to prevent memory leaks
let mobileFocusTrapCleanup = null;
function setupMobileFocusTrap() {
if (!mobileSearchPopup || !mobileSearchPopup.classList.contains("active")) {
return;
}
const focusableElements = [
mobileSearchInput,
closeMobileSearchBtn,
...Array.from(mobileSearchResults.querySelectorAll("a[href]")),
].filter((el) => el !== null);
if (focusableElements.length === 0) return;
const firstFocusable = focusableElements[0];
const lastFocusable = focusableElements[focusableElements.length - 1];
const handleKeyDown = (e) => {
if (e.key === "Tab") {
if (e.shiftKey && document.activeElement === firstFocusable) {
e.preventDefault();
lastFocusable.focus();
} else if (!e.shiftKey && document.activeElement === lastFocusable) {
e.preventDefault();
firstFocusable.focus();
}
}
if (e.key === "Escape") {
closeMobileSearch();
}
// Arrow key navigation in results
if (["ArrowDown", "ArrowUp"].includes(e.key)) {
const links = Array.from(
mobileSearchResults.querySelectorAll("a[href]"),
);
if (links.length === 0) return;
const currentIndex = links.indexOf(document.activeElement);
if (e.key === "ArrowDown") {
e.preventDefault();
if (currentIndex === -1) {
links[0].focus();
} else {
const nextIndex = Math.min(currentIndex + 1, links.length - 1);
links[nextIndex].focus();
}
} else if (e.key === "ArrowUp") {
e.preventDefault();
if (currentIndex > 0) {
links[currentIndex - 1].focus();
} else if (currentIndex === 0) {
mobileSearchInput.focus();
}
}
}
};
mobileSearchPopup.addEventListener("keydown", handleKeyDown);
// Return cleanup function
return () => {
mobileSearchPopup.removeEventListener("keydown", handleKeyDown);
};
}
function openMobileSearch() {
if (mobileSearchPopup) {
mobileSearchPopup.classList.add("active");
@ -1182,12 +1462,23 @@ document.addEventListener("DOMContentLoaded", function () {
if (mobileSearchInput) {
mobileSearchInput.focus();
}
// Clean up previous session's listeners before setting up new ones
if (mobileFocusTrapCleanup) {
mobileFocusTrapCleanup();
mobileFocusTrapCleanup = null;
}
mobileFocusTrapCleanup = setupMobileFocusTrap();
}, 100);
}
}
function closeMobileSearch() {
if (mobileSearchPopup) {
// Clean up event listeners before closing
if (mobileFocusTrapCleanup) {
mobileFocusTrapCleanup();
mobileFocusTrapCleanup = null;
}
mobileSearchPopup.classList.remove("active");
if (mobileSearchInput) {
mobileSearchInput.value = "";
@ -1235,11 +1526,10 @@ document.addEventListener("DOMContentLoaded", function () {
mobileSearchResults.innerHTML = results
.map((result) => {
const { doc, matchingAnchors } = result;
const queryTerms = window.searchNamespace.engine.tokenize(
searchTerm,
);
const highlightedTitle = window.searchNamespace.engine
.highlightTerms(
const queryTerms =
window.searchNamespace.engine.tokenize(searchTerm);
const highlightedTitle =
window.searchNamespace.engine.highlightTerms(
doc.title,
queryTerms,
);
@ -1258,25 +1548,25 @@ document.addEventListener("DOMContentLoaded", function () {
if (matchingAnchors && matchingAnchors.length > 0) {
matchingAnchors.forEach((anchor) => {
// Skip anchors that duplicate the page title
const normalizedAnchor = window.searchNamespace.engine
.normalizeForComparison(
const normalizedAnchor =
window.searchNamespace.engine.normalizeForComparison(
anchor.text,
);
const normalizedTitle = window.searchNamespace.engine
.normalizeForComparison(
const normalizedTitle =
window.searchNamespace.engine.normalizeForComparison(
doc.title,
);
if (normalizedAnchor === normalizedTitle) {
return;
}
const highlightedAnchor = window.searchNamespace.engine
.highlightTerms(
const highlightedAnchor =
window.searchNamespace.engine.highlightTerms(
anchor.text,
queryTerms,
);
const sectionPreview = window.searchNamespace.engine
.generateSectionPreview(
const sectionPreview =
window.searchNamespace.engine.generateSectionPreview(
doc,
anchor,
searchTerm,
@ -1298,6 +1588,12 @@ document.addEventListener("DOMContentLoaded", function () {
})
.join("");
mobileSearchResults.style.display = "block";
// Clean up previous listeners before setting up new ones
if (mobileFocusTrapCleanup) {
mobileFocusTrapCleanup();
mobileFocusTrapCleanup = null;
}
mobileFocusTrapCleanup = setupMobileFocusTrap();
} else {
mobileSearchResults.innerHTML =
'<div class="search-result-item">No results found</div>';
@ -1308,7 +1604,7 @@ document.addEventListener("DOMContentLoaded", function () {
// Verify once more
if (mobileSearchInput.value.trim() !== searchTerm) return;
mobileSearchResults.innerHTML =
'<div class="search-result-item">Search unavailable</div>';
'<div class="search-result-item search-error" role="alert">Search unavailable. <a href="#" onclick="event.preventDefault(); window.searchNamespace.engine.loadData();">Retry</a></div>';
mobileSearchResults.style.display = "block";
}
}, 300);
@ -1330,13 +1626,14 @@ document.addEventListener("DOMContentLoaded", function () {
});
});
async function performSearch(query) {
async function performSearch(query, keyboardNav = null) {
query = query.trim();
const resultsContainer = document.getElementById("search-page-results");
if (query.length < 2) {
resultsContainer.innerHTML =
"<p>Please enter at least 2 characters to search</p>";
if (keyboardNav) keyboardNav.clear();
return;
}
@ -1348,6 +1645,7 @@ async function performSearch(query) {
// Show loading state
resultsContainer.innerHTML = "<p>Searching...</p>";
if (keyboardNav) keyboardNav.clear();
try {
const results = await window.searchNamespace.engine.search(query, 50, {
@ -1390,21 +1688,21 @@ async function performSearch(query) {
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);
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(
const highlightedAnchor =
window.searchNamespace.engine.highlightTerms(
anchor.text,
queryTerms,
);
const sectionPreview = window.searchNamespace.engine
.generateSectionPreview(
const sectionPreview =
window.searchNamespace.engine.generateSectionPreview(
doc,
anchor,
query,
@ -1421,8 +1719,10 @@ async function performSearch(query) {
}
html += "</ul>";
resultsContainer.innerHTML = html;
if (keyboardNav) keyboardNav.updateItems();
} else {
resultsContainer.innerHTML = "<p>No results found</p>";
if (keyboardNav) keyboardNav.clear();
}
// Update URL with query
@ -1434,6 +1734,14 @@ async function performSearch(query) {
return;
}
console.error("Search error:", error);
resultsContainer.innerHTML = "<p>Search temporarily unavailable</p>";
resultsContainer.innerHTML = `
<div class="search-error" role="alert">
<p>Search is temporarily unavailable. Please try again.</p>
<button type="button" onclick="window.searchNamespace.engine.loadData().then(() => { this.closest('.search-error').innerHTML = '<p>Search reloaded. Please try your search again.</p>'; })">
Retry
</button>
</div>
`;
if (keyboardNav) keyboardNav.clear();
}
}