/* global CONFIG */ window.addEventListener('DOMContentLoaded', () => { // Search DB path let searchPath = KEEP.hexo_config.path; if (!searchPath) { // Search DB path console.warn('`hexo-generator-searchdb` plugin is not installed!'); return; } // Popup Window let isfetched = false; let datas; let isXml = true; if (searchPath.length === 0) { searchPath = 'search.xml'; } else if (searchPath.endsWith('json')) { isXml = false; } const input = document.querySelector('.search-input'); const resultContent = document.getElementById('search-result'); // Ref: https://github.com/ForbesLindesay/unescape-html const unescapeHtml = html => { return String(html) .replace(/"/g, '"') .replace(/'/g, '\'') .replace(/:/g, ':') // Replace all the other &#x; chars .replace(/&#(\d+);/g, (m, p) => { return String.fromCharCode(p); }) .replace(/</g, '<') .replace(/>/g, '>') .replace(/&/g, '&'); }; const getIndexByWord = (word, text, caseSensitive) => { let wordLen = word.length; if (wordLen === 0) return []; let startPosition = 0; let position = []; let index = []; if (!caseSensitive) { text = text.toLowerCase(); word = word.toLowerCase(); } while ((position = text.indexOf(word, startPosition)) > -1) { index.push({position, word}); startPosition = position + wordLen; } return index; }; // Merge hits into slices const mergeIntoSlice = (start, end, index, searchText) => { let item = index[index.length - 1]; let {position, word} = item; let hits = []; let searchTextCountInSlice = 0; while (position + word.length <= end && index.length !== 0) { if (word === searchText) { searchTextCountInSlice++; } hits.push({ position, length: word.length }); let wordEnd = position + word.length; // Move to next position of hit index.pop(); while (index.length !== 0) { item = index[index.length - 1]; position = item.position; word = item.word; if (wordEnd > position) { index.pop(); } else { break; } } } return { hits, start, end, searchTextCount: searchTextCountInSlice }; }; // Highlight title and content const highlightKeyword = (text, slice) => { let result = ''; let prevEnd = slice.start; slice.hits.forEach(hit => { result += text.substring(prevEnd, hit.position); let end = hit.position + hit.length; result += `<b class="search-keyword">${text.substring(hit.position, end)}</b>`; prevEnd = end; }); result += text.substring(prevEnd, slice.end); return result; }; const inputEventFunction = () => { if (!isfetched) return; let searchText = input.value.trim().toLowerCase(); let keywords = searchText.split(/[-\s]+/); if (keywords.length > 1) { keywords.push(searchText); } let resultItems = []; if (searchText.length > 0) { // Perform local searching datas.forEach(({title, content, url}) => { let titleInLowerCase = title.toLowerCase(); let contentInLowerCase = content.toLowerCase(); let indexOfTitle = []; let indexOfContent = []; let searchTextCount = 0; keywords.forEach(keyword => { indexOfTitle = indexOfTitle.concat(getIndexByWord(keyword, titleInLowerCase, false)); indexOfContent = indexOfContent.concat(getIndexByWord(keyword, contentInLowerCase, false)); }); // Show search results if (indexOfTitle.length > 0 || indexOfContent.length > 0) { let hitCount = indexOfTitle.length + indexOfContent.length; // Sort index by position of keyword [indexOfTitle, indexOfContent].forEach(index => { index.sort((itemLeft, itemRight) => { if (itemRight.position !== itemLeft.position) { return itemRight.position - itemLeft.position; } return itemLeft.word.length - itemRight.word.length; }); }); let slicesOfTitle = []; if (indexOfTitle.length !== 0) { let tmp = mergeIntoSlice(0, title.length, indexOfTitle, searchText); searchTextCount += tmp.searchTextCountInSlice; slicesOfTitle.push(tmp); } let slicesOfContent = []; while (indexOfContent.length !== 0) { let item = indexOfContent[indexOfContent.length - 1]; let {position, word} = item; // Cut out 100 characters let start = position - 20; let end = position + 80; if (start < 0) { start = 0; } if (end < position + word.length) { end = position + word.length; } if (end > content.length) { end = content.length; } let tmp = mergeIntoSlice(start, end, indexOfContent, searchText); searchTextCount += tmp.searchTextCountInSlice; slicesOfContent.push(tmp); } // Sort slices in content by search text's count and hits' count slicesOfContent.sort((sliceLeft, sliceRight) => { if (sliceLeft.searchTextCount !== sliceRight.searchTextCount) { return sliceRight.searchTextCount - sliceLeft.searchTextCount; } else if (sliceLeft.hits.length !== sliceRight.hits.length) { return sliceRight.hits.length - sliceLeft.hits.length; } return sliceLeft.start - sliceRight.start; }); // Select top N slices in content let upperBound = parseInt(KEEP.theme_config.local_search.top_n_per_article ? KEEP.theme_config.local_search.top_n_per_article : 1, 10); if (upperBound >= 0) { slicesOfContent = slicesOfContent.slice(0, upperBound); } let resultItem = ''; if (slicesOfTitle.length !== 0) { resultItem += `<li><a href="${url}" class="search-result-title">${highlightKeyword(title, slicesOfTitle[0])}</a>`; } else { resultItem += `<li><a href="${url}" class="search-result-title">${title}</a>`; } slicesOfContent.forEach(slice => { resultItem += `<a href="${url}"><p class="search-result">${highlightKeyword(content, slice)}...</p></a>`; }); resultItem += '</li>'; resultItems.push({ item: resultItem, id: resultItems.length, hitCount, searchTextCount }); } }); } if (keywords.length === 1 && keywords[0] === '') { resultContent.innerHTML = '<div id="no-result"><i class="fas fa-search fa-5x"></i></div>'; } else if (resultItems.length === 0) { resultContent.innerHTML = '<div id="no-result"><i class="fas fa-box-open fa-5x"></i></div>'; } else { resultItems.sort((resultLeft, resultRight) => { if (resultLeft.searchTextCount !== resultRight.searchTextCount) { return resultRight.searchTextCount - resultLeft.searchTextCount; } else if (resultLeft.hitCount !== resultRight.hitCount) { return resultRight.hitCount - resultLeft.hitCount; } return resultRight.id - resultLeft.id; }); let searchResultList = '<ul class="search-result-list">'; resultItems.forEach(result => { searchResultList += result.item; }); searchResultList += '</ul>'; resultContent.innerHTML = searchResultList; window.pjax && window.pjax.refresh(resultContent); } }; const fetchData = () => { fetch(KEEP.hexo_config.root + searchPath) .then(response => response.text()) .then(res => { // Get the contents from search data isfetched = true; datas = isXml ? [...new DOMParser().parseFromString(res, 'text/xml').querySelectorAll('entry')].map(element => { return { title: element.querySelector('title').textContent, content: element.querySelector('content').textContent, url: element.querySelector('url').textContent }; }) : JSON.parse(res); // Only match articles with not empty titles datas = datas.filter(data => data.title).map(data => { data.title = data.title.trim(); data.content = data.content ? data.content.trim().replace(/<[^>]+>/g, '') : ''; if (KEEP.theme_config.local_search.unescape) { data.content = unescapeHtml(data.content); } data.url = decodeURIComponent(data.url).replace(/\/{2,}/g, '/'); return data; }); // Remove loading animation document.getElementById('no-result').innerHTML = '<i class="fas fa-search fa-5x"></i>'; }); }; if (KEEP.theme_config.local_search.preload) { fetchData(); } if (KEEP.theme_config.local_search.trigger === 'auto') { if (input) { input.addEventListener('input', inputEventFunction); } } else { document.querySelector('.search-icon').addEventListener('click', inputEventFunction); input.addEventListener('keypress', event => { if (event.key === 'Enter') { inputEventFunction(); } }); } // Handle and trigger popup window document.querySelectorAll('.popup-trigger').forEach(element => { element.addEventListener('click', () => { document.body.style.overflow = 'hidden'; document.querySelector('.search-pop-overlay').style.display = 'block'; input.focus(); if (!isfetched) fetchData(); }); }); // Monitor main search box const onPopupClose = () => { document.body.style.overflow = ''; document.querySelector('.search-pop-overlay').style.display = ''; }; document.querySelector('.search-pop-overlay').addEventListener('click', event => { if (event.target === document.querySelector('.search-pop-overlay')) { onPopupClose(); } }); document.querySelector('.popup-btn-close').addEventListener('click', onPopupClose); window.addEventListener('pjax:success', onPopupClose); window.addEventListener('keyup', event => { if (event.key === 'Escape') { onPopupClose(); } }); });