1const searchBtn = document.querySelector("#search-btn") 2const searchInput = document.querySelector("#search-input") 3const searchResult = document.querySelector("#search-result") 4const searchClearBtn = document.querySelector("#search-clear") 5 6const searchMask = document.querySelector("#search-mask") 7 8let searchStatus = 0 9 10function showSearchDialog() { 11 if (searchStatus) { 12 return 13 } 14 searchMask.style.display = 'block' 15 document.body.style.overflow = 'hidden' 16 searchInput.focus() 17 searchStatus = 1 18} 19 20function closeSearchDialog() { 21 if (!searchStatus) { 22 return 23 } 24 searchMask.style.display = 'None' 25 document.body.style.overflow = 'inherit' 26 searchInput.value = "" 27 searchStatus = 0 28} 29 30function searchInitialize(url) { 31 fetch(url).then(res => res.json()).then(res => { 32 const inputHandler = debounce(doSearch) 33 34 searchBtn.style.display = "flex" 35 36 document.addEventListener('keydown', e => { 37 if ((e.ctrlKey || e.metaKey) && e.key == "k") { 38 if (searchStatus) { 39 closeSearchDialog() 40 } else { 41 showSearchDialog() 42 } 43 } 44 if (e.key == 'Escape') { 45 closeSearchDialog() 46 } 47 }) 48 49 searchClearBtn.addEventListener('click', () => { 50 searchInput.value = "" 51 clearResult() 52 }) 53 54 searchBtn.addEventListener('click', () => { 55 showSearchDialog() 56 }) 57 58 searchMask.addEventListener('click', e => { 59 if (e.target !== searchMask) { 60 return 61 } 62 closeSearchDialog() 63 }, true) 64 65 searchInput.addEventListener('input', inputHandler.bind(searchInput, res)) 66 }) 67} 68 69function clearResult() { 70 searchResult.innerHTML = '' 71} 72 73function doSearch(data) { 74 if (this.value.trim().length <= 0) { 75 return clearResult() 76 } 77 78 var keywords = this.value.trim().toLowerCase().split(/[\s\-]+/); 79 80 const result = search(data, keywords) 81 82 renderSearchResult(result, searchResult) 83} 84 85function search(data, keywords) { 86 const matchedPost = [] 87 88 data.filter(post => post.content).forEach(post => {
89 let postTitle = post.title && post.title.trim() 90 postTitle = (postTitle && postTitle.length > 0) ? postTitle : "Untitled" 91 92 const postContent = post.content.trim().replace(/<[^>]+>/g, "") 93 94 const matchedContentIdices = [] 95 96 keywords.forEach((keyword) => { 97 const index_title = postTitle.toLowerCase().indexOf(keyword); 98 const index_content = postContent.toLowerCase().indexOf(keyword); 99 100 if (index_title < 0 && index_content < 0) { 101 return 102 } 103 104 matchedContentIdices.push(Math.max(index_content, 0)) 105 }); 106 107 if (matchedContentIdices.length) { 108 matchedPost.push({ 109 url: post.url, 110 content: trimeContent(matchedContentIdices, postContent, keywords), 111 title: postTitle 112 }) 113 } 114 }); 115 116 return matchedPost 117} 118 119function renderSearchResult(result, el) { 120 el.innerHTML = '' 121 122 if (!result || result.length <= 0) { 123 el.innerHTML = '<div class="search-result-empty">æ ç»æ</div>' 124 } 125 126 const list = document.createDocumentFragment() 127 result.forEach(res => { 128 const item = document.createElement('a') 129 item.className = 'search-result-item' 130 item.href = res.url 131 132 const title = document.createElement('DIV') 133 title.className = 'search-result__head' 134 title.innerText = res.title 135 136 const content = document.createElement('DIV') 137 content.className = 'search-result__body' 138 res.content.forEach(contentText => { 139 const contentItem = document.createElement('DIV') 140 contentItem.innerHTML = contentText 141 content.appendChild(contentItem) 142 }) 143 144 item.append(title, content) 145 list.append(item) 146 }) 147 148 el.appendChild(list) 149} 150 151function trimeContent(keyIndexs, content, keywords, wordLen = 20) { 152 const reg = /[\u4e00-\u9fa5]|\w+/gd 153 const splitIndex = [] 154 let arr 155 while ((arr = reg.exec(content)) !== null) { 156 splitIndex.push(arr.indices[0]) 157 } 158 159 return keyIndexs.map(key => { 160 let pos = binaryFind(splitIndex, key) 161 const wordStart = Math.max(0, pos - 10) 162 const wordEnd = Math.min(wordLen + wordStart, splitIndex.length - 1) 163 const start = splitIndex[wordStart][0] 164 const end = splitIndex[wordEnd][1] 165 166 keywords.forEach(keyword => { 167 content = content 168 .slice(start, end) 169 .replace(new RegExp(keyword, 'ig'), `<span class="search-keyword">${keyword}</span>`) 170 }) 171 172 return content 173 }) 174} 175 176function binaryFind(arr, value) { 177 let i = 0, j = arr.length - 1, mid = j 178 while (i <= j) { 179 if (value < arr[mid][0]) j = mid - 1; 180 else if (value > arr[mid][1]) i = mid + 1 181 else return mid 182 mid = Math.floor((i + j) / 2) 183 } 184 return mid 185} 186 187function debounce(fn, t = 400) { 188 let timer = null 189 return function (...args) { 190 clearTimeout(timer) 191 timer = setTimeout(() => { 192 fn.call(this, ...args) 193 }, t) 194 } 195}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.