summaryInclude = 60;
const fuseOptions = {
shouldSort: true,
includeMatches: true,
threshold: 0.0,
tokenize: true,
location: 0,
distance: 100,
maxPatternLength: 32,
minMatchCharLength: 1,
keys: [
{ name: "title", weight: 0.9 },
{ name: "contents", weight: 0.5 },
{ name: "tags", weight: 0.3 },
{ name: "categories", weight: 0.3 },
],
};
// Display error message in the search results container
function displayError(message) {
const searchResultsElement = document.getElementById("search-results");
if (searchResultsElement) {
const sanitizedMessage = DOMPurify.sanitize(message);
searchResultsElement.innerHTML = `
${sanitizedMessage}
`;
} else {
console.error("Search results container not found");
}
}
// Safely get DOM element with error handling
function getElement(id) {
const element = document.getElementById(id);
if (!element) {
console.error(`Element with ID '${id}' not found`);
}
return element;
}
// Debounce function to prevent excessive search calls
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// Function to update URL with search query
function updateURL(query) {
try {
// Create a URL object from the current URL
const url = new URL(window.location.href);
if (query && query.length >= 2) {
// Set or update the 's' parameter
url.searchParams.set("s", query);
} else {
// Remove the 's' parameter if query is empty or too short
url.searchParams.delete("s");
}
// Update the URL without reloading the page
window.history.replaceState({}, "", url.toString());
} catch (error) {
console.error("Error updating URL:", error);
// Continue without URL update - non-critical error
}
}
// Safe parameter extraction with error handling
function param(name) {
try {
const paramValue = (location.search.split(`${name}=`)[1] || "").split("&")[0];
return paramValue ? decodeURIComponent(paramValue).replace(/\+/g, " ") : "";
} catch (error) {
console.error(`Error parsing URL parameter '${name}':`, error);
return "";
}
}
// Get search query from URL parameter
const searchQuery = param("s");
try {
const searchInput = getElement("search-query");
const searchResults = getElement("search-results");
if (searchInput && searchQuery) {
searchInput.value = searchQuery;
executeSearch(searchQuery);
} else if (searchResults) {
searchResults.innerHTML =
"Please enter at least 2 characters to search
";
}
} catch (error) {
console.error("Error initializing search:", error);
displayError(
"There was a problem initializing the search. Please try again later.",
);
}
// Add event listener for real-time searching
document.addEventListener("DOMContentLoaded", () => {
try {
const searchInput = getElement("search-query");
if (!searchInput) {
throw new Error("Search input not found");
}
// Create debounced search function - 300ms is a good balance
const debouncedSearch = debounce((query) => {
// Update URL with current search query
updateURL(query);
const searchResults = getElement("search-results");
if (!searchResults) {
throw new Error("Search results container not found");
}
if (query.length >= 2) {
executeSearch(query);
} else if (query.length === 0 || query.length === 1) {
searchResults.innerHTML =
"Please enter at least 2 characters to search
";
}
}, 300);
// Set up input event for real-time searching
searchInput.addEventListener("input", function () {
const query = this.value.trim();
debouncedSearch(query);
});
// Handle form submission to prevent page reload
const searchForm = searchInput.closest("form");
if (searchForm) {
searchForm.addEventListener("submit", (e) => {
e.preventDefault();
const query = searchInput.value.trim();
if (query.length >= 2) {
updateURL(query);
executeSearch(query);
}
});
}
} catch (error) {
console.error("Error setting up search event listeners:", error);
displayError(
"There was a problem setting up the search functionality. Please try reloading the page.",
);
}
});
function executeSearch(searchQuery) {
try {
if (!searchQuery || typeof searchQuery !== "string") {
throw new Error("Invalid search query");
}
const searchResults = getElement("search-results");
if (!searchResults) {
throw new Error("Search results container not found");
}
// Show loading indicator
searchResults.innerHTML =
'Loading...
';
fetch("/index.json")
.then((response) => {
if (!response.ok) {
throw new Error(
`Network response was not ok: ${response.status} ${response.statusText}`,
);
}
return response.json();
})
.then((data) => {
if (!Array.isArray(data)) {
throw new Error("Received invalid data format from server");
}
const pages = data;
const fuse = new Fuse(pages, fuseOptions);
const result = fuse.search(searchQuery);
// Clear previous results only when we have new results to show
searchResults.innerHTML = "";
if (result.length > 0) {
populateResults(result);
} else {
searchResults.insertAdjacentHTML(
"beforeend",
"No matches found
",
);
}
})
.catch((error) => {
console.error("Error executing search:", error);
displayError(`Failed to search: ${error.message}`);
});
} catch (error) {
console.error("Error in search execution:", error);
displayError("There was a problem with the search. Please try again later.");
}
}
function populateResults(result) {
try {
if (!Array.isArray(result)) {
throw new Error("Invalid search results");
}
const searchResults = getElement("search-results");
if (!searchResults) {
throw new Error("Search results container not found");
}
const templateElement = document.getElementById("search-result-template");
if (!templateElement) {
throw new Error("Search result template not found");
}
const templateDefinition = templateElement.innerHTML;
for (const [key, value] of result.entries()) {
if (!value || !value.item) {
console.warn("Skipping invalid search result item", value);
continue;
}
const contents = value.item.contents || "";
let snippet = "";
const snippetHighlights = [];
let start;
let end;
if (fuseOptions.tokenize) {
snippetHighlights.push(searchQuery);
} else {
if (value.matches) {
for (const mvalue of value.matches) {
if (!mvalue || typeof mvalue.key !== "string") {
continue;
}
if (
mvalue.key === "tags" ||
mvalue.key === "categories"
) {
snippetHighlights.push(mvalue.value);
} else if (
mvalue.key === "contents" &&
Array.isArray(mvalue.indices) &&
mvalue.indices.length > 0
) {
try {
start =
mvalue.indices[0][0] - summaryInclude > 0
? mvalue.indices[0][0] - summaryInclude
: 0;
end =
mvalue.indices[0][1] + summaryInclude < contents.length
? mvalue.indices[0][1] + summaryInclude
: contents.length;
snippet += contents.substring(start, end);
if (typeof mvalue.value === "string") {
const highlightValue =
mvalue.indices[0][1] - mvalue.indices[0][0] + 1;
if (
highlightValue > 0 &&
mvalue.indices[0][0] < mvalue.value.length
) {
snippetHighlights.push(
mvalue.value.substring(
mvalue.indices[0][0],
mvalue.indices[0][0] + highlightValue,
),
);
}
}
} catch (e) {
console.warn("Error processing match indices", e);
}
}
}
}
}
if (snippet.length < 1 && contents) {
snippet += contents.substring(0, summaryInclude * 2);
}
try {
// Insert the templated result
const output = render(templateDefinition, {
key: key,
title: value.item.title || "Untitled",
link: value.item.permalink || "#",
tags: value.item.tags || "",
categories: value.item.categories || "",
snippet: snippet || "No preview available",
});
searchResults.insertAdjacentHTML("beforeend", output);
// Add highlighting after insertion
for (const snipvalue of snippetHighlights) {
if (!snipvalue) continue;
const summaryElem = document.getElementById(`summary-${key}`);
if (summaryElem && typeof Mark !== "undefined") {
try {
const markInstance = new Mark(summaryElem);
markInstance.mark(snipvalue);
} catch (e) {
console.warn("Error highlighting text:", e);
}
}
}
} catch (error) {
console.error("Error rendering search result:", error);
}
}
} catch (error) {
console.error("Error populating results:", error);
displayError("There was a problem displaying search results.");
}
}
function render(templateString, data) {
try {
if (!templateString || !data) {
throw new Error("Invalid template or data");
}
let conditionalMatches, conditionalPattern, copy;
conditionalPattern = /\$\{\s*isset ([a-zA-Z]*) \s*\}(.*)\$\{\s*end\s*}/g;
// Since loop below depends on re.lastIndex, we use a copy to capture any manipulations
copy = templateString;
while ((conditionalMatches = conditionalPattern.exec(templateString)) !== null) {
if (conditionalMatches.length < 3) continue;
if (data[conditionalMatches[1]]) {
// Valid key, remove conditionals, leave contents
copy = copy.replace(conditionalMatches[0], conditionalMatches[2]);
} else {
// Not valid, remove entire section
copy = copy.replace(conditionalMatches[0], "");
}
}
let result = copy;
// Now any conditionals removed we can do simple substitution
for (const key in data) {
if (Object.prototype.hasOwnProperty.call(data, key)) {
const value = data[key];
if (value !== undefined && value !== null) {
const find = `\\$\\{\\s*${key}\\s*\\}`;
const re = new RegExp(find, "g");
result = result.replace(re, value);
}
}
}
return result;
} catch (error) {
console.error("Error rendering template:", error);
return 'Error rendering result
';
}
}