1/* global CONFIG */ 2 3document.addEventListener('DOMContentLoaded', () => { 4 // Popup Window 5 let isfetched = false; 6 let datas; 7 let isXml = true; 8 // Search DB path 9 let searchPath = CONFIG.path; 10 if (searchPath.length === 0) { 11 searchPath = 'search.xml'; 12 } else if (searchPath.endsWith('json')) { 13 isXml = false; 14 } 15 const input = document.querySelector('.search-input'); 16 const resultContent = document.getElementById('search-result'); 17 18 const getIndexByWord = (word, text, caseSensitive) => { 19 if (CONFIG.localsearch.unescape) { 20 let div = document.createElement('div'); 21 div.innerText = word; 22 word = div.innerHTML; 23 } 24 let wordLen = word.length; 25 if (wordLen === 0) return []; 26 let startPosition = 0; 27 let position = []; 28 let index = []; 29 if (!caseSensitive) { 30 text = text.toLowerCase(); 31 word = word.toLowerCase(); 32 } 33 while ((position = text.indexOf(word, startPosition)) > -1) { 34 index.push({ position, word }); 35 startPosition = position + wordLen; 36 } 37 return index; 38 }; 39 40 // Merge hits into slices 41 const mergeIntoSlice = (start, end, index, searchText) => { 42 let item = index[index.length - 1]; 43 let { position, word } = item; 44 let hits = []; 45 let searchTextCountInSlice = 0; 46 while (position + word.length <= end && index.length !== 0) { 47 if (word === searchText) { 48 searchTextCountInSlice++; 49 } 50 hits.push({ 51 position, 52 length: word.length 53 }); 54 let wordEnd = position + word.length; 55 56 // Move to next position of hit 57 index.pop(); 58 while (index.length !== 0) { 59 item = index[index.length - 1]; 60 position = item.position; 61 word = item.word; 62 if (wordEnd > position) { 63 index.pop(); 64 } else { 65 break; 66 } 67 } 68 } 69 return { 70 hits, 71 start, 72 end, 73 searchTextCount: searchTextCountInSlice 74 }; 75 }; 76 77 // Highlight title and content 78 const highlightKeyword = (text, slice) => { 79 let result = ''; 80 let prevEnd = slice.start; 81 slice.hits.forEach(hit => { 82 result += text.substring(prevEnd, hit.position); 83 let end = hit.position + hit.length; 84 result += `<b class="search-keyword">${text.substring(hit.position, end)}</b>`; 85 prevEnd = end; 86 }); 87 result += text.substring(prevEnd, slice.end); 88 return result; 89 }; 90 91 const inputEventFunction = () => { 92 if (!isfetched) return; 93 let searchText = input.value.trim().toLowerCase(); 94 let keywords = searchText.split(/[-\s]+/); 95 if (keywords.length > 1) { 96 keywords.push(searchText); 97 } 98 let resultItems = []; 99 if (searchText.length > 0) { 100 // Perform local searching 101 datas.forEach(({ title, content, url }) => { 102 let titleInLowerCase = title.toLowerCase(); 103 let contentInLowerCase = content.toLowerCase(); 104 let indexOfTitle = []; 105 let indexOfContent = []; 106 let searchTextCount = 0; 107 keywords.forEach(keyword => { 108 indexOfTitle = indexOfTitle.concat(getIndexByWord(keyword, titleInLowerCase, false)); 109 indexOfContent = indexOfContent.concat(getIndexByWord(keyword, contentInLowerCase, false)); 110 }); 111 112 // Show search results 113 if (indexOfTitle.length > 0 || indexOfContent.length > 0) { 114 let hitCount = indexOfTitle.length + indexOfContent.length; 115 // Sort index by position of keyword 116 [indexOfTitle, indexOfContent].forEach(index => { 117 index.sort((itemLeft, itemRight) => { 118 if (itemRight.position !== itemLeft.position) { 119 return itemRight.position - itemLeft.position; 120 } 121 return itemLeft.word.length - itemRight.word.length; 122 }); 123 }); 124 125 let slicesOfTitle = []; 126 if (indexOfTitle.length !== 0) { 127 let tmp = mergeIntoSlice(0, title.length, indexOfTitle, searchText); 128 searchTextCount += tmp.searchTextCountInSlice; 129 slicesOfTitle.push(tmp); 130 } 131
132 let slicesOfContent = []; 133 while (indexOfContent.length !== 0) { 134 let item = indexOfContent[indexOfContent.length - 1]; 135 let { position, word } = item; 136 // Cut out 100 characters 137 let start = position - 20; 138 let end = position + 80; 139 if (start < 0) { 140 start = 0; 141 } 142 if (end < position + word.length) { 143 end = position + word.length; 144 } 145 if (end > content.length) { 146 end = content.length; 147 } 148 let tmp = mergeIntoSlice(start, end, indexOfContent, searchText); 149 searchTextCount += tmp.searchTextCountInSlice; 150 slicesOfContent.push(tmp); 151 } 152 153 // Sort slices in content by search text's count and hits' count 154 slicesOfContent.sort((sliceLeft, sliceRight) => { 155 if (sliceLeft.searchTextCount !== sliceRight.searchTextCount) { 156 return sliceRight.searchTextCount - sliceLeft.searchTextCount; 157 } else if (sliceLeft.hits.length !== sliceRight.hits.length) { 158 return sliceRight.hits.length - sliceLeft.hits.length; 159 } 160 return sliceLeft.start - sliceRight.start; 161 }); 162 163 // Select top N slices in content 164 let upperBound = parseInt(CONFIG.localsearch.top_n_per_article, 10); 165 if (upperBound >= 0) { 166 slicesOfContent = slicesOfContent.slice(0, upperBound); 167 } 168 169 let resultItem = ''; 170 171 if (slicesOfTitle.length !== 0) { 172 resultItem += `<li><a href="${url}" class="search-result-title">${highlightKeyword(title, slicesOfTitle[0])}</a>`; 173 } else { 174 resultItem += `<li><a href="${url}" class="search-result-title">${title}</a>`; 175 } 176 177 slicesOfContent.forEach(slice => { 178 resultItem += `<a href="${url}"><p class="search-result">${highlightKeyword(content, slice)}...</p></a>`; 179 }); 180 181 resultItem += '</li>'; 182 resultItems.push({ 183 item: resultItem, 184 id : resultItems.length, 185 hitCount, 186 searchTextCount 187 }); 188 } 189 }); 190 } 191 if (keywords.length === 1 && keywords[0] === '') { 192 resultContent.innerHTML = '<div id="no-result"><i class="fa fa-search fa-5x"></i></div>'; 193 } else if (resultItems.length === 0) { 194 resultContent.innerHTML = '<div id="no-result"><i class="far fa-frown fa-5x"></i></div>'; 195 } else { 196 resultItems.sort((resultLeft, resultRight) => { 197 if (resultLeft.searchTextCount !== resultRight.searchTextCount) { 198 return resultRight.searchTextCount - resultLeft.searchTextCount; 199 } else if (resultLeft.hitCount !== resultRight.hitCount) { 200 return resultRight.hitCount - resultLeft.hitCount; 201 } 202 return resultRight.id - resultLeft.id; 203 }); 204 resultContent.innerHTML = `<ul class="search-result-list">${resultItems.map(result => result.item).join('')}</ul>`; 205 window.pjax && window.pjax.refresh(resultContent); 206 } 207 }; 208 209 const fetchData = () => { 210 fetch(CONFIG.root + searchPath) 211 .then(response => response.text()) 212 .then(res => { 213 // Get the contents from search data 214 isfetched = true; 215 datas = isXml ? [...new DOMParser().parseFromString(res, 'text/xml').querySelectorAll('entry')].map(element => { 216 return { 217 title : element.querySelector('title').textContent, 218 content: element.querySelector('content').textContent, 219 url : element.querySelector('url').textContent 220 }; 221 }) : JSON.parse(res); 222 // Only match articles with not empty titles 223 datas = datas.filter(data => data.title).map(data => { 224 data.title = data.title.trim(); 225 data.content = data.content ? data.content.trim().replace(/<[^>]+>/g, '') : ''; 226 data.url = decodeURIComponent(data.url).replace(/\/{2,}/g, '/'); 227 return data; 228 }); 229 // Remove loading animation 230 document.getElementById('no-result').innerHTML = '<i class="fa fa-search fa-5x"></i>'; 231 inputEventFunction(); 232 }); 233 }; 234 235 if (CONFIG.localsearch.preload) { 236 fetchData(); 237 } 238 239 if (CONFIG.localsearch.trigger === 'auto') { 240 input.addEventListener('input', inputEventFunction); 241 } else { 242 document.querySelector('.search-icon').addEventListener('click', inputEventFunction); 243 input.addEventListener('keypress', event => { 244 if (event.key === 'Enter') { 245 inputEventFunction(); 246 } 247 }); 248 } 249 250 // Handle and trigger popup window 251 document.querySelectorAll('.popup-trigger').forEach(element => { 252 element.addEventListener('click', () => { 253 document.body.style.overflow = 'hidden'; 254 document.querySelector('.search-pop-overlay').classList.add('search-active'); 255 input.focus(); 256 if (!isfetched) fetchData(); 257 }); 258 }); 259 260 // Monitor main search box 261 const onPopupClose = () => { 262 document.body.style.overflow = ''; 263 document.querySelector('.search-pop-overlay').classList.remove('search-active'); 264 }; 265 266 document.querySelector('.search-pop-overlay').addEventListener('click', event => { 267 if (event.target === document.querySelector('.search-pop-overlay')) { 268 onPopupClose(); 269 } 270 }); 271 document.querySelector('.popup-btn-close').addEventListener('click', onPopupClose); 272 window.addEventListener('pjax:success', onPopupClose); 273 window.addEventListener('keyup', event => { 274 if (event.key === 'Escape') { 275 onPopupClose(); 276 } 277 }); 278});
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.