mirror of
https://github.com/next-theme/hexo-theme-next.git
synced 2026-01-18 18:33:42 +00:00
Move local-search.js to hexo-generator-searchdb (#369)
This commit is contained in:
parent
292b8a258e
commit
fbf0ea788f
@ -138,6 +138,13 @@ instant_search:
|
|||||||
version: 4.28.0
|
version: 4.28.0
|
||||||
file: dist/instantsearch.production.min.js
|
file: dist/instantsearch.production.min.js
|
||||||
integrity: sha256-x+1kTZNP+yy5U5ncci4pes6ALSaVss1UY5hnGy+mfqI=
|
integrity: sha256-x+1kTZNP+yy5U5ncci4pes6ALSaVss1UY5hnGy+mfqI=
|
||||||
|
local_search:
|
||||||
|
name: hexo-generator-searchdb
|
||||||
|
version: 1.4.0
|
||||||
|
file: dist/search.js
|
||||||
|
unavailable:
|
||||||
|
- cdnjs
|
||||||
|
integrity: sha256-vXZMYLEqsROAXkEw93GGIvaB2ab+QW6w3+1ahD9nXXA=
|
||||||
pdfobject:
|
pdfobject:
|
||||||
name: pdfobject
|
name: pdfobject
|
||||||
version: 2.2.6
|
version: 2.2.6
|
||||||
|
|||||||
3
layout/_third-party/search/localsearch.njk
vendored
3
layout/_third-party/search/localsearch.njk
vendored
@ -1 +1,2 @@
|
|||||||
{{- next_js('third-party/search/local-search.js') }}
|
{{ next_vendors('local_search') }}
|
||||||
|
{{ next_js('third-party/search/local-search.js') }}
|
||||||
|
|||||||
236
source/js/third-party/search/local-search.js
vendored
236
source/js/third-party/search/local-search.js
vendored
@ -1,4 +1,4 @@
|
|||||||
/* global CONFIG, pjax */
|
/* global CONFIG, pjax, LocalSearch */
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
if (!CONFIG.path) {
|
if (!CONFIG.path) {
|
||||||
@ -6,170 +6,23 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
console.warn('`hexo-generator-searchdb` plugin is not installed!');
|
console.warn('`hexo-generator-searchdb` plugin is not installed!');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Popup Window
|
const localSearch = new LocalSearch({
|
||||||
let isfetched = false;
|
path : CONFIG.path,
|
||||||
let datas;
|
top_n_per_article: CONFIG.localsearch.top_n_per_article,
|
||||||
|
unescape : CONFIG.localsearch.unescape
|
||||||
|
});
|
||||||
|
|
||||||
const input = document.querySelector('.search-input');
|
const input = document.querySelector('.search-input');
|
||||||
|
|
||||||
const getIndexByWord = (words, text, caseSensitive = false) => {
|
|
||||||
const index = [];
|
|
||||||
const included = new Set();
|
|
||||||
words.forEach(word => {
|
|
||||||
if (CONFIG.localsearch.unescape) {
|
|
||||||
const div = document.createElement('div');
|
|
||||||
div.innerText = word;
|
|
||||||
word = div.innerHTML;
|
|
||||||
}
|
|
||||||
const wordLen = word.length;
|
|
||||||
if (wordLen === 0) return;
|
|
||||||
let startPosition = 0;
|
|
||||||
let position = -1;
|
|
||||||
if (!caseSensitive) {
|
|
||||||
text = text.toLowerCase();
|
|
||||||
word = word.toLowerCase();
|
|
||||||
}
|
|
||||||
while ((position = text.indexOf(word, startPosition)) > -1) {
|
|
||||||
index.push({ position, word });
|
|
||||||
included.add(word);
|
|
||||||
startPosition = position + wordLen;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Sort index by position of keyword
|
|
||||||
index.sort((left, right) => {
|
|
||||||
if (left.position !== right.position) {
|
|
||||||
return left.position - right.position;
|
|
||||||
}
|
|
||||||
return right.word.length - left.word.length;
|
|
||||||
});
|
|
||||||
return [index, included];
|
|
||||||
};
|
|
||||||
|
|
||||||
// Merge hits into slices
|
|
||||||
const mergeIntoSlice = (start, end, index) => {
|
|
||||||
let item = index[0];
|
|
||||||
let { position, word } = item;
|
|
||||||
const hits = [];
|
|
||||||
const count = new Set();
|
|
||||||
while (position + word.length <= end && index.length !== 0) {
|
|
||||||
count.add(word);
|
|
||||||
hits.push({
|
|
||||||
position,
|
|
||||||
length: word.length
|
|
||||||
});
|
|
||||||
const wordEnd = position + word.length;
|
|
||||||
|
|
||||||
// Move to next position of hit
|
|
||||||
index.shift();
|
|
||||||
while (index.length !== 0) {
|
|
||||||
item = index[0];
|
|
||||||
position = item.position;
|
|
||||||
word = item.word;
|
|
||||||
if (wordEnd > position) {
|
|
||||||
index.shift();
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
hits,
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
count: count.size
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// Highlight title and content
|
|
||||||
const highlightKeyword = (val, slice) => {
|
|
||||||
let result = '';
|
|
||||||
let index = slice.start;
|
|
||||||
for (const { position, length } of slice.hits) {
|
|
||||||
result += val.substring(index, position);
|
|
||||||
index = position + length;
|
|
||||||
result += `<mark class="search-keyword">${val.substr(position, length)}</mark>`;
|
|
||||||
}
|
|
||||||
result += val.substring(index, slice.end);
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getResultItems = keywords => {
|
|
||||||
const resultItems = [];
|
|
||||||
datas.forEach(({ title, content, url }) => {
|
|
||||||
// The number of different keywords included in the article.
|
|
||||||
const [indexOfTitle, keysOfTitle] = getIndexByWord(keywords, title);
|
|
||||||
const [indexOfContent, keysOfContent] = getIndexByWord(keywords, content);
|
|
||||||
const includedCount = new Set([...keysOfTitle, ...keysOfContent]).size;
|
|
||||||
|
|
||||||
// Show search results
|
|
||||||
const hitCount = indexOfTitle.length + indexOfContent.length;
|
|
||||||
if (hitCount === 0) return;
|
|
||||||
|
|
||||||
const slicesOfTitle = [];
|
|
||||||
if (indexOfTitle.length !== 0) {
|
|
||||||
slicesOfTitle.push(mergeIntoSlice(0, title.length, indexOfTitle));
|
|
||||||
}
|
|
||||||
|
|
||||||
let slicesOfContent = [];
|
|
||||||
while (indexOfContent.length !== 0) {
|
|
||||||
const item = indexOfContent[0];
|
|
||||||
const { position } = item;
|
|
||||||
// Cut out 100 characters. The maxlength of .search-input is 80.
|
|
||||||
const start = Math.max(0, position - 20);
|
|
||||||
const end = Math.min(content.length, position + 80);
|
|
||||||
slicesOfContent.push(mergeIntoSlice(start, end, indexOfContent));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort slices in content by included keywords' count and hits' count
|
|
||||||
slicesOfContent.sort((left, right) => {
|
|
||||||
if (left.count !== right.count) {
|
|
||||||
return right.count - left.count;
|
|
||||||
} else if (left.hits.length !== right.hits.length) {
|
|
||||||
return right.hits.length - left.hits.length;
|
|
||||||
}
|
|
||||||
return left.start - right.start;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Select top N slices in content
|
|
||||||
const upperBound = parseInt(CONFIG.localsearch.top_n_per_article, 10);
|
|
||||||
if (upperBound >= 0) {
|
|
||||||
slicesOfContent = slicesOfContent.slice(0, upperBound);
|
|
||||||
}
|
|
||||||
|
|
||||||
let resultItem = '';
|
|
||||||
|
|
||||||
url = new URL(url, location.origin);
|
|
||||||
url.searchParams.append('highlight', keywords.join(' '));
|
|
||||||
|
|
||||||
if (slicesOfTitle.length !== 0) {
|
|
||||||
resultItem += `<li><a href="${url.href}" class="search-result-title">${highlightKeyword(title, slicesOfTitle[0])}</a>`;
|
|
||||||
} else {
|
|
||||||
resultItem += `<li><a href="${url.href}" class="search-result-title">${title}</a>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
slicesOfContent.forEach(slice => {
|
|
||||||
resultItem += `<a href="${url.href}"><p class="search-result">${highlightKeyword(content, slice)}...</p></a>`;
|
|
||||||
});
|
|
||||||
|
|
||||||
resultItem += '</li>';
|
|
||||||
resultItems.push({
|
|
||||||
item: resultItem,
|
|
||||||
id : resultItems.length,
|
|
||||||
hitCount,
|
|
||||||
includedCount
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return resultItems;
|
|
||||||
};
|
|
||||||
|
|
||||||
const inputEventFunction = () => {
|
const inputEventFunction = () => {
|
||||||
if (!isfetched) return;
|
if (!localSearch.isfetched) return;
|
||||||
const searchText = input.value.trim().toLowerCase();
|
const searchText = input.value.trim().toLowerCase();
|
||||||
const keywords = searchText.split(/[-\s]+/);
|
const keywords = searchText.split(/[-\s]+/);
|
||||||
const container = document.querySelector('.search-result-container');
|
const container = document.querySelector('.search-result-container');
|
||||||
let resultItems = [];
|
let resultItems = [];
|
||||||
if (searchText.length > 0) {
|
if (searchText.length > 0) {
|
||||||
// Perform local searching
|
// Perform local searching
|
||||||
resultItems = getResultItems(keywords);
|
resultItems = localSearch.getResultItems(keywords);
|
||||||
}
|
}
|
||||||
if (keywords.length === 1 && keywords[0] === '') {
|
if (keywords.length === 1 && keywords[0] === '') {
|
||||||
container.classList.add('no-result');
|
container.classList.add('no-result');
|
||||||
@ -196,71 +49,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchData = () => {
|
localSearch.highlightSearchWords(document.querySelector('.post-body'));
|
||||||
const isXml = !CONFIG.path.endsWith('json');
|
|
||||||
fetch(CONFIG.path)
|
|
||||||
.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 => ({
|
|
||||||
title : element.querySelector('title').textContent,
|
|
||||||
content: element.querySelector('content').textContent,
|
|
||||||
url : element.querySelector('url').textContent
|
|
||||||
})) : JSON.parse(res);
|
|
||||||
// Only match articles with non-empty titles
|
|
||||||
datas = datas.filter(data => data.title).map(data => {
|
|
||||||
data.title = data.title.trim();
|
|
||||||
data.content = data.content ? data.content.trim().replace(/<[^>]+>/g, '') : '';
|
|
||||||
data.url = decodeURIComponent(data.url).replace(/\/{2,}/g, '/');
|
|
||||||
return data;
|
|
||||||
});
|
|
||||||
// Remove loading animation
|
|
||||||
inputEventFunction();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Highlight by wrapping node in mark elements with the given class name
|
|
||||||
const highlightText = (node, slice, className) => {
|
|
||||||
const val = node.nodeValue;
|
|
||||||
let index = slice.start;
|
|
||||||
const children = [];
|
|
||||||
for (const { position, length } of slice.hits) {
|
|
||||||
const text = document.createTextNode(val.substring(index, position));
|
|
||||||
index = position + length;
|
|
||||||
const mark = document.createElement('mark');
|
|
||||||
mark.className = className;
|
|
||||||
mark.appendChild(document.createTextNode(val.substr(position, length)));
|
|
||||||
children.push(text, mark);
|
|
||||||
}
|
|
||||||
node.nodeValue = val.substring(index, slice.end);
|
|
||||||
children.forEach(element => {
|
|
||||||
node.parentNode.insertBefore(element, node);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Highlight the search words provided in the url in the text
|
|
||||||
const highlightSearchWords = () => {
|
|
||||||
const params = new URL(location.href).searchParams.get('highlight');
|
|
||||||
const keywords = params ? params.split(' ') : [];
|
|
||||||
const body = document.querySelector('.post-body');
|
|
||||||
if (!keywords.length || !body) return;
|
|
||||||
const walk = document.createTreeWalker(body, NodeFilter.SHOW_TEXT, null);
|
|
||||||
const allNodes = [];
|
|
||||||
while (walk.nextNode()) {
|
|
||||||
if (!walk.currentNode.parentNode.matches('button, select, textarea')) allNodes.push(walk.currentNode);
|
|
||||||
}
|
|
||||||
allNodes.forEach(node => {
|
|
||||||
const [indexOfNode] = getIndexByWord(keywords, node.nodeValue);
|
|
||||||
if (!indexOfNode.length) return;
|
|
||||||
const slice = mergeIntoSlice(0, node.nodeValue.length, indexOfNode);
|
|
||||||
highlightText(node, slice, 'search-keyword');
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
highlightSearchWords();
|
|
||||||
if (CONFIG.localsearch.preload) {
|
if (CONFIG.localsearch.preload) {
|
||||||
fetchData();
|
localSearch.fetchData();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (CONFIG.localsearch.trigger === 'auto') {
|
if (CONFIG.localsearch.trigger === 'auto') {
|
||||||
@ -273,6 +64,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
window.addEventListener('search:loaded', inputEventFunction);
|
||||||
|
|
||||||
// Handle and trigger popup window
|
// Handle and trigger popup window
|
||||||
document.querySelectorAll('.popup-trigger').forEach(element => {
|
document.querySelectorAll('.popup-trigger').forEach(element => {
|
||||||
@ -280,7 +72,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
document.body.classList.add('search-active');
|
document.body.classList.add('search-active');
|
||||||
// Wait for search-popup animation to complete
|
// Wait for search-popup animation to complete
|
||||||
setTimeout(() => input.focus(), 500);
|
setTimeout(() => input.focus(), 500);
|
||||||
if (!isfetched) fetchData();
|
if (!localSearch.isfetched) localSearch.fetchData();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -296,7 +88,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
});
|
});
|
||||||
document.querySelector('.popup-btn-close').addEventListener('click', onPopupClose);
|
document.querySelector('.popup-btn-close').addEventListener('click', onPopupClose);
|
||||||
document.addEventListener('pjax:success', () => {
|
document.addEventListener('pjax:success', () => {
|
||||||
highlightSearchWords();
|
localSearch.highlightSearchWords(document.querySelector('.post-body'));
|
||||||
onPopupClose();
|
onPopupClose();
|
||||||
});
|
});
|
||||||
window.addEventListener('keyup', event => {
|
window.addEventListener('keyup', event => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user