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