search.html (3308B)
1 <script src="https://unpkg.com/lunr/lunr.js"></script> 2 <input type="text" id="searchBox" placeholder="Search..."> 3 <script> 4 let idx = null; 5 let store = null; 6 let originalContent = ""; 7 8 // Escape regex special chars in search terms 9 function escapeRegExp(string) { 10 return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); 11 } 12 13 // Highlight matched terms in text 14 function highlightTerms(text, terms) { 15 if (!terms.length) return text; 16 const regex = new RegExp(`\\b(${terms.map(t => escapeRegExp(t)).join("|")})\\b`, "gi"); 17 return text.replace(regex, "<mark>$1</mark>"); 18 } 19 20 // Get snippet around first matched term and highlight terms 21 function getSnippetWithHighlight(text, terms, contextWords = 40) { 22 if (!terms.length) return text; 23 24 const div = document.createElement("div"); 25 div.innerHTML = text; 26 const plainText = div.textContent || div.innerText || ""; 27 28 const words = plainText.split(/\s+/); 29 let matchIndex = -1; 30 31 for (let i = 0; i < words.length; i++) { 32 if (terms.some(term => words[i].toLowerCase().includes(term.toLowerCase()))) { 33 matchIndex = i; 34 break; 35 } 36 } 37 38 if (matchIndex === -1) { 39 const snippet = words.slice(0, contextWords).join(" "); 40 return snippet + "…"; 41 } 42 43 const start = Math.max(0, Math.floor(matchIndex - contextWords / 2)); 44 const end = Math.min(words.length, Math.floor(matchIndex + contextWords / 2)); 45 const snippet = words.slice(start, end).join(" "); 46 47 const highlighted = highlightTerms(snippet, terms); 48 return highlighted + "…"; 49 } 50 51 // Load Lunr index and data 52 fetch("/index.json") 53 .then(response => response.json()) 54 .then(data => { 55 store = data; 56 57 idx = lunr(function () { 58 this.ref("permalink"); 59 this.field("title"); 60 this.field("content"); 61 62 data.forEach(doc => this.add(doc), this); 63 }); 64 }); 65 66 // Save original content for restoring 67 document.addEventListener("DOMContentLoaded", () => { 68 const content = document.getElementById("content"); 69 if (content) { 70 originalContent = content.innerHTML; 71 } 72 }); 73 74 // Search input event handler 75 document.getElementById("searchBox").addEventListener("input", function () { 76 const query = this.value.trim(); 77 const content = document.getElementById("content"); 78 79 if (!idx || !store || !content) return; 80 81 if (!query) { 82 content.innerHTML = originalContent; 83 return; 84 } 85 86 const results = idx.search(query); 87 const terms = query.split(/\s+/).filter(term => term.length > 1); 88 89 if (results.length === 0) { 90 content.innerHTML = `<p>No results found for "<strong>${query}</strong>".</p>`; 91 return; 92 } 93 94 let html = `<h2>Search Results for "<em>${query}</em>":</h2><ul>`; 95 results.forEach(result => { 96 const item = store.find(page => page.permalink === result.ref); 97 if (!item) return; // safety check 98 99 const highlightedTitle = highlightTerms(item.title, terms); 100 const highlightedSummary = getSnippetWithHighlight(item.summary, terms, 40); 101 102 html += ` 103 <li> 104 <a href="${item.permalink}">${highlightedTitle}</a> 105 <p>${highlightedSummary}</p> 106 </li>`; 107 }); 108 html += "</ul>"; 109 content.innerHTML = html; 110 }); 111 </script>