diff --git a/source/js/local-search.js b/source/js/local-search.js
index 9a1d230..31f0c8f 100644
--- a/source/js/local-search.js
+++ b/source/js/local-search.js
@@ -1,297 +1,298 @@
/* global CONFIG */
window.addEventListener('DOMContentLoaded', () => {
- // Popup Window
- let isfetched = false;
- let datas;
- let isXml = true;
- // Search DB path
- let searchPath = CONFIG.path;
- if (searchPath.length === 0) {
- searchPath = 'search.xml';
- } else if (searchPath.endsWith('json')) {
- isXml = false;
+ // Popup Window
+ let isfetched = false;
+ let datas;
+ let isXml = true;
+ // Search DB path
+ let searchPath = CONFIG.path;
+ if (!searchPath) return false;
+ 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 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();
}
- const input = document.querySelector('.search-input');
- const resultContent = document.getElementById('search-result');
+ while ((position = text.indexOf(word, startPosition)) > -1) {
+ index.push({position, word});
+ startPosition = position + wordLen;
+ }
+ return index;
+ };
- // Ref: https://github.com/ForbesLindesay/unescape-html
- const unescapeHtml = html => {
- return String(html)
- .replace(/"/g, '"')
- .replace(/'/g, '\'')
- .replace(/:/g, ':')
- // Replace all the other chars
- .replace(/(\d+);/g, (m, p) => {
- return String.fromCharCode(p);
- })
- .replace(/</g, '<')
- .replace(/>/g, '>')
- .replace(/&/g, '&');
- };
+ // 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;
- 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 += `${text.substring(hit.position, end)}`;
- 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(CONFIG.localsearch.top_n_per_article ? CONFIG.localsearch.top_n_per_article : 1, 10);
- if (upperBound >= 0) {
- slicesOfContent = slicesOfContent.slice(0, upperBound);
- }
-
- let resultItem = '';
-
- if (slicesOfTitle.length !== 0) {
- resultItem += `
${highlightKeyword(title, slicesOfTitle[0])}`;
- } else {
- resultItem += `${title}`;
- }
-
- slicesOfContent.forEach(slice => {
- resultItem += `${highlightKeyword(content, slice)}...
`;
- });
-
- resultItem += '';
- resultItems.push({
- item: resultItem,
- id: resultItems.length,
- hitCount,
- searchTextCount
- });
- }
- });
- }
- if (keywords.length === 1 && keywords[0] === '') {
- resultContent.innerHTML = '
';
- } else if (resultItems.length === 0) {
- resultContent.innerHTML = '
';
+ // 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 {
- 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 = '';
- resultItems.forEach(result => {
- searchResultList += result.item;
- });
- searchResultList += '
';
- resultContent.innerHTML = searchResultList;
- window.pjax && window.pjax.refresh(resultContent);
+ break;
}
- };
-
- const fetchData = () => {
- fetch(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 (CONFIG.localsearch.unescape) {
- data.content = unescapeHtml(data.content);
- }
- data.url = decodeURIComponent(data.url).replace(/\/{2,}/g, '/');
- return data;
- });
- // Remove loading animation
- document.getElementById('no-result').innerHTML = '';
- });
- };
-
- if (CONFIG.localsearch.preload) {
- fetchData();
+ }
}
+ return {
+ hits,
+ start,
+ end,
+ searchTextCount: searchTextCountInSlice
+ };
+ };
- if (CONFIG.localsearch.trigger === 'auto') {
- if (input) {
- input.addEventListener('input', inputEventFunction);
- }
+ // 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 += `${text.substring(hit.position, end)}`;
+ prevEnd = end;
+ });
+ result += text.substring(prevEnd, slice.end);
+ return result;
+ };
- } else {
- document.querySelector('.search-icon').addEventListener('click', inputEventFunction);
- input.addEventListener('keypress', event => {
- if (event.key === 'Enter') {
- inputEventFunction();
+ 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(CONFIG.localsearch.top_n_per_article ? CONFIG.localsearch.top_n_per_article : 1, 10);
+ if (upperBound >= 0) {
+ slicesOfContent = slicesOfContent.slice(0, upperBound);
+ }
+
+ let resultItem = '';
+
+ if (slicesOfTitle.length !== 0) {
+ resultItem += `${highlightKeyword(title, slicesOfTitle[0])}`;
+ } else {
+ resultItem += `${title}`;
+ }
+
+ slicesOfContent.forEach(slice => {
+ resultItem += `${highlightKeyword(content, slice)}...
`;
+ });
+
+ resultItem += '';
+ resultItems.push({
+ item: resultItem,
+ id: resultItems.length,
+ hitCount,
+ searchTextCount
+ });
+ }
+ });
+ }
+ if (keywords.length === 1 && keywords[0] === '') {
+ resultContent.innerHTML = '
';
+ } else if (resultItems.length === 0) {
+ resultContent.innerHTML = '
';
+ } 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 = '';
+ resultItems.forEach(result => {
+ searchResultList += result.item;
+ });
+ searchResultList += '
';
+ resultContent.innerHTML = searchResultList;
+ window.pjax && window.pjax.refresh(resultContent);
+ }
+ };
+
+ const fetchData = () => {
+ fetch(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 (CONFIG.localsearch.unescape) {
+ data.content = unescapeHtml(data.content);
+ }
+ data.url = decodeURIComponent(data.url).replace(/\/{2,}/g, '/');
+ return data;
});
+ // Remove loading animation
+ document.getElementById('no-result').innerHTML = '';
+ });
+ };
+
+ if (CONFIG.localsearch.preload) {
+ fetchData();
+ }
+
+ if (CONFIG.localsearch.trigger === 'auto') {
+ if (input) {
+ input.addEventListener('input', 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();
- });
+ } else {
+ document.querySelector('.search-icon').addEventListener('click', inputEventFunction);
+ input.addEventListener('keypress', event => {
+ if (event.key === 'Enter') {
+ inputEventFunction();
+ }
});
+ }
- // Monitor main search box
- const onPopupClose = () => {
- document.body.style.overflow = '';
- document.querySelector('.search-pop-overlay').style.display = '';
- };
+ // 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();
+ });
+ });
- 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();
- }
- });
+ // 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();
+ }
+ });
});