${this.escapeHtml(item.title)}
- -${this.escapeHtml(item.contents)}
-From 3add1fd106fa09f0c248326b7d17b9eb813ba9e2 Mon Sep 17 00:00:00 2001 From: geekifan Date: Sun, 27 Apr 2025 21:11:17 +0800 Subject: [PATCH] support multilingual search --- assets/js/search.js | 207 ----------------------- layouts/partials/head.html | 2 - layouts/partials/search-results.html | 240 ++++++++++++++++++++++++++- 3 files changed, 239 insertions(+), 210 deletions(-) delete mode 100644 assets/js/search.js diff --git a/assets/js/search.js b/assets/js/search.js deleted file mode 100644 index d340a61..0000000 --- a/assets/js/search.js +++ /dev/null @@ -1,207 +0,0 @@ -// Configuration -const DEFAULT_CONFIG = { - search: { - minChars: 2, // Minimum characters before searching - maxResults: 5, // Maximum number of results to show - fields: { // Fields to search through - title: true, // Allow searching in title - description: true, // Allow searching in description - section: true // Allow searching in section - } - } -}; - -class FastSearch { - constructor({ - searchInput, resultsContainer, json, - searchResultTemplate = null, - noResultsText = null, - }) { - this.searchInput = searchInput; - this.resultsContainer = resultsContainer; - this.json = json; - this.searchResultTemplate = searchResultTemplate; - this.noResultsText = noResultsText; - - this.init(); - } - - init() { - this.loadSearchIndex(); - // this.initShortcutListener(); - this.searchInput.addEventListener('input', (event) => { - if (!this.searchIndex) { - this.resultsContainer.innerHTML = '
'; - return; - } - this.performSearch(this.searchInput.value); - }); - } - - // Load the search index - async loadSearchIndex() { - try { - const response = await fetch(this.json); - if (!response.ok) throw new Error('Failed to load search index'); - const data = await response.json(); - - this.searchIndex = data.map(item => ({ - ...item, - searchableTitle: item.title?.toLowerCase() || '', - searchableDesc: item.desc?.toLowerCase() || '', - searchableSection: item.section?.toLowerCase() || '' - })); - } catch (error) { - console.error('Error loading search index:', error); - this.resultsContainer.innerHTML = ' '; - } - } - - escapeHtml(unsafe) { - if (!unsafe) return ''; - return unsafe - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """) - .replace(/'/g, "'"); - } - - - // Simple fuzzy match for single words - simpleFuzzyMatch(text, term) { - if (text.includes(term)) return true; - if (term.length < 3) return false; - - let matches = 0; - let lastMatchIndex = -1; - - for (let i = 0; i < term.length; i++) { - const found = text.indexOf(term[i], lastMatchIndex + 1); - if (found > -1) { - matches++; - lastMatchIndex = found; - } - } - - return matches === term.length; - } - - // Check if keyboard event matches shortcut config - matchesShortcut(event, shortcutConfig) { - return event.key === shortcutConfig.key && - event.metaKey === shortcutConfig.metaKey && - event.altKey === shortcutConfig.altKey && - event.ctrlKey === shortcutConfig.ctrlKey && - event.shiftKey === shortcutConfig.shiftKey; - } - - initShortcutListener() { - // Keyboard shortcuts - document.addEventListener('keydown', (event) => { - // ESC to close search - if (event.key === 'Escape' && this.searchInput.focus) { - this.searchInput.blur(); - this.searchInput.value = ''; - this.resultsContainer.innerHTML = ''; - } - }); - } - - - performSearch(term) { - term = term.toLowerCase().trim(); - - if (!term || !this.searchIndex) { - this.resultsContainer.innerHTML = ''; - let resultsAvailable = false; - return; - } - - // Split search into terms - const searchTerms = term.split(/\s+/).filter(t => t.length > 0); - - // Search with scoring - const results = this.searchIndex - .map(item => { - let score = 0; - const matchesAllTerms = searchTerms.every(term => { - let matched = false; - - // Title matches (weighted higher) - if (DEFAULT_CONFIG.search.fields.title) { - if (item.searchableTitle.startsWith(term)) { - score += 3; // Highest score for prefix matches in title - matched = true; - } else if (this.simpleFuzzyMatch(item.searchableTitle, term)) { - score += 2; // Good score for fuzzy matches in title - matched = true; - } - } - - // Other field matches - if (!matched) { - if (DEFAULT_CONFIG.search.fields.description && item.searchableDesc.includes(term)) { - score += 0.5; // Lower score for description matches - matched = true; - } - if (DEFAULT_CONFIG.search.fields.section && item.searchableSection.includes(term)) { - score += 0.5; // Lower score for section matches - matched = true; - } - } - - return matched; - }); - - return { - item, - score: matchesAllTerms ? score : 0 - }; - }) - .filter(result => result.score > 0) - .sort((a, b) => b.score - a.score) - .slice(0, DEFAULT_CONFIG.search.maxResults) - .map(result => result.item); - - let resultsAvailable = results.length > 0; - - if (!resultsAvailable) { - this.resultsContainer.innerHTML = 'Oops! No results found.
'; - return; - } - - const searchItems = results.map( (item) => { - let categories = ''; - let tags = ''; - if (item.categories) { - categories = item.categories.join(', '); - categories = `${this.escapeHtml(item.contents)}
-