提交 852f2494 编写于 作者: EvanOne(文一)'s avatar EvanOne(文一)

feat: Add local search whitout dependencies

上级 250db90c
......@@ -9,13 +9,13 @@ header#header(
div.header-nav-inner
div.fa.fa-bars.header-nav-menu-icon
if theme.algolia_search && theme.algolia_search.enable
if theme.algolia_search.enable || theme.local_search.enable
div.header-nav-search
if !theme.menu_settings.text_only
i.fa.fa-search
if !theme.menu_settings.icon_only
span!= _p("nav.search")
div.header-nav-menu
each value, name in (theme.menu || [])
if name && value
......@@ -29,7 +29,7 @@ header#header(
menuItemIcon : "question-circle-o"}`)
if !theme.menu_settings.icon_only
!= _p(`nav.${name}`)
div.header-info
div.header-info-inner
div.header-info-title= config.title
......
......@@ -11,8 +11,6 @@ html(lang=config.language)
if theme.back2top.enable
include ../_components/back2top.pug
if theme.algolia_search.enable
include ../_components/search/algolia.pug
include ../_components/search/index.pug
include ../_scripts/cdn.pug
include ../_scripts/common.pug
if theme.algolia_search.enable
include ./algolia.pug
else if theme.local_search.enable
include ./localsearch.pug
div.localsearch-mask
div.localsearch-popup
div.localsearch-input-wrapper
input(placeholder= _p("local_search.input_placeholder"))
span.localsearch-close
i.fa.fa-times
div.localsearch-result
if theme.algolia_search.enable
include ./algolia.pug
else if theme.local_search.enable
include ./localsearch.pug
if theme.local_search.enable
script.
window.onload = function () {
$('.header-nav-search').on('click', function (e) {
e.stopPropagation();
$('body').css('overflow', 'hidden');
$('.localsearch-popup')
.velocity('stop')
.velocity('transition.expandIn', {
duration: 300,
complete: function () {
$('.localsearch-popup input').focus();
}
});
$('.localsearch-mask')
.velocity('stop')
.velocity('transition.fadeIn', {
duration: 300
});
initSearch();
});
$('.localsearch-mask, .localsearch-close').on('click', function () {
closeSearch();
});
$(document).on('keydown', function (e) {
// Escape <=> 27
if (e.keyCode === Stun.utils.codeToKeyCode('Escape')) {
closeSearch();
}
});
var isXML = true;
var search_path = '!{ config.search.path }';
if (!search_path) {
search_path = 'search.xml';
} else if (/json$/i.test(search_path)) {
isXML = false;
}
var path = '!{ config.root }' + search_path;
function initSearch() {
$.ajax({
url: path,
dataType: isXML ? 'xml' : 'json',
async: true,
success: function (res) {
var datas = isXML ? $('entry', res).map(function () {
// 将 XML 转为 JSON
return {
title: $('title', this).text(),
content: $('content', this).text(),
url: $('url', this).text(),
categories: $('category', this).text().trim().split(/[\s]+/),
tags: $('tag', this).text().trim().split(/[\s]+/)
};
}).get() : res;
var $input = $('.localsearch-input-wrapper input');
var $result = $('.localsearch-result');
// 搜索对象(标题,标签,分类,内容)的权值,影响显示顺序
var WEIGHT = { title: 100, tag: 10, category: 10, content: 1 };
var searchPost = function () {
var searchText = $input.val().toLowerCase().trim();
// 根据空白字符分隔关键字
var keywords = searchText.split(/[\s]+/);
// 将关键字进行排列组合,使得搜索到结果的可能性更大
var groupKeywords = sortGroup(keywords);
// 搜索结果
var matchPosts = [];
// 防止未输入字符时搜索
if (searchText.length > 0) {
datas.forEach(function (data) {
var isMatch = false;
// 没有标题的文章使用预设的 i18n 变量代替
var title = (data.title && data.title.trim()) || '!{_p("post.untitled")}';;
var titleLower = title && title.toLowerCase();
// 删除 HTML 标签 和 所有空白字符
var content = data.content && data.content.replace(/<[^>]+>|\s+/, '');
var contentLower = content && content.toLowerCase();
// 删除重复的 /
var postURL = data.url && decodeURI(data.url).replace(/\/{2,}/, '/');
var tags = data.tags;
var categories = data.categories;
// 标题中匹配到的关键词
var titleHitSlice = [];
// 内容中匹配到的关键词
var contentHitSlice = [];
groupKeywords.forEach(function (keyword) {
/**
* 获取匹配文字的索引
* @param {String} keyword 要匹配的关键字
* @param {String} text 原文字
* @param {Boolean} caseSensitive 是否区分大小写
* @param {Number} weight 权重
* @return {Array}
*/
function getIndexByword (word, text, caseSensitive, weight) {
if (!word || !text) return [];
var startIndex = 0; // 开始匹配的索引
var index = -1; // 匹配到的索引值
var result = []; // 匹配结果
if (!caseSensitive) {
word = word.toLowerCase();
text = text.toLowerCase();
}
while((index = text.indexOf(word, startIndex)) !== -1) {
result.push({ index: index, word: word, weight: weight });
startIndex = index + word.length;
}
return result;
}
titleHitSlice = titleHitSlice.concat(getIndexByword(keyword, titleLower, false, WEIGHT.title));
contentHitSlice = contentHitSlice.concat(getIndexByword(keyword, contentLower, false, WEIGHT.content));
});
var hitTitle = titleHitSlice.length;
var hitContent = contentHitSlice.length;
if (hitTitle > 0 || hitContent > 0) {
isMatch = true;
}
if (isMatch) {
/**
* 给匹配的文本添加标记标签
* @param {String} text 原文本
* @param {Array} hitSlice 匹配项的索引信息
* @return {String}
*/
function highlightKeyword (text, hitSlice) {
if (!text || !hitSlice || !hitSlice.length) return;
var startIndex = 0;
var result = '';
hitSlice.forEach(function (hit) {
// 匹配到较长的字符串后,防止再匹配较短的字符串
if (hit.index < startIndex) return;
var hitWordEnd = hit.index + hit.word.length;
result += text.slice(startIndex, hit.index);
result += '<b>' + text.slice(hit.index, hitWordEnd) + '</b>';
startIndex = hitWordEnd;
});
result += text.slice(startIndex);
return result;
}
var postData = {};
// 计算文章总的搜索权重
var postWeight = titleHitSlice.length * 100 + contentHitSlice.length;
var postTitle = highlightKeyword(title, titleHitSlice) || title;
var postContent = highlightKeyword(content, contentHitSlice) || content;
// 截取匹配的第一个字符,前后共 200 个字符来显示
if (contentHitSlice.length > 0) {
var firstIndex = contentHitSlice[0].index;
var start = firstIndex > 20 ? firstIndex - 20 : 0;
var end = firstIndex + 180;
postContent = postContent.slice(start, end);
} else { // 未匹配到内容,直接截取前 200 个字符来显示
postContent = postContent.slice(0, 200);
}
postData.title = postTitle;
postData.content = postContent;
postData.url = postURL;
postData.weight = postWeight;
matchPosts.push(postData);
}
});
}
var resultInnerHtml = '<ul>';
// 按权重递增的顺序排序,使权重大的优先显示
matchPosts.sort(function (left, right) {
return right.weight - left.weight;
});
matchPosts.forEach(function (post) {
resultInnerHtml += '<li><a class="localsearch-result-title" href="' + post.url + '">';
resultInnerHtml += post.title;
resultInnerHtml += '</a><div class="localsearch-result-content">';
resultInnerHtml += post.content;
resultInnerHtml += '</div></li>';
});
resultInnerHtml += '</ul>';
$result.html(resultInnerHtml);
};
$input.on('input', searchPost);
$input.on('keypress', function (e) {
if (e.keyCode === Stun.utils.codeToKeyCode('Enter')) {
searchPost();
}
});
/**
* 将关键字进行排列组合
* @param {Array} keywords 关键词数组
* @return {Array}
*/
function sortGroup (keywords) {
if (!Array.isArray(keywords)) return;
if (keywords.length <= 1) return keywords;
// 对数组中的元素进行组合
function groupSplit (arr, size) {
var r = []; // result
function group(t, a, n) { // tempArr, arr, num
if (n === 0) {
r[r.length] = t;
return;
}
for (var i = 0, l = a.length - n; i <= l; i++) {
var b = t.slice();
b.push(a[i]);
group(b, a.slice(i + 1), n - 1);
}
}
group([], arr, size);
return r;
}
// 组合结果
var result = [];
for (var i = keywords.length; i > 0; i--) {
var groupKeyword = groupSplit(keywords, i);
if (groupKeyword.length !== 0) {
groupKeyword.forEach(function (item) {
// 将排列组合后的项,连接成新的关键词
if (item.length > 1) {
result.push(item.join(''));
result.push(item.join(' '));
result.push(item.slice(0).reverse().join(''));
result.push(item.slice(0).reverse().join(' '));
} else {
result.push(item.join(''));
}
});
}
}
return result;
}
}
});
}
function closeSearch () {
$('body').css('overflow', 'auto');
$('.localsearch-popup')
.velocity('stop')
.velocity('transition.expandOut', {
duration: 300
});
$('.localsearch-mask')
.velocity('stop')
.velocity('transition.fadeOut', {
duration: 300
});
}
};
......@@ -78,7 +78,8 @@
.header-info-subtitle
font-size: 1rem
div.algolia-popup
div.algolia-popup,
div.localsearch-popup
left: 0
margin: 0 1rem
width: calc(100% - 2rem)
......@@ -153,3 +154,11 @@
.gallery-image
width: 100%
div.algolia-popup,
div.localsearch-popup
padding: .8rem .6rem 1rem
div.localsearch-result
& > ul
padding-left: 1rem
.algolia-mask
display: none
position: fixed
top: 0
left: 0
z-index: $z-index2
width: 100%
height: 100%
background-color: alpha($black-dark, .7)
.algolia-popup
display: none
position: fixed
top: 10%
left: 50%
z-index: $z-index2
margin-left: -350px
border-radius: 5px
padding: 1rem
width: 700px
max-height: 80%
font-size: $font-size-large
color: $font-color
background-color: $white-light
.algolia-input-wrapper
margin: 1rem 0
.ais-search-box
max-width: 100%
input
border: 2px solid alpha($blue-light, .5)
border-radius: 2rem
padding: .4rem .8rem
outline: 0
transition: border-color .3s
&:hover,
&:focus
border-color: $blue-light
.algolia-close
position: absolute
top: 0
right: .6rem
font-size: 1.25em
color: $black-light
cursor: pointer
.algolia-results
overflow-x: hidden
overflow-y: scroll
max-height: 16rem
em
font-style: normal
background-color: $yellow-light
.algolia-hit-item
&::before
content: ''
......
.algolia-mask,
.localsearch-mask
display: none
position: fixed
top: 0
left: 0
z-index: $z-index2
width: 100%
height: 100%
background-color: alpha($black-dark, .7)
.algolia-popup,
.localsearch-popup
display: none
position: fixed
top: 10%
left: 50%
z-index: $z-index2
margin-left: -350px
border-radius: 5px
padding: 1rem
width: 700px
max-height: 80%
font-size: $font-size-large
color: $font-color
background-color: $white-light
.algolia-input-wrapper,
.localsearch-input-wrapper
margin: 1.2rem 0 1rem
input
border: 2px solid alpha($blue-light, .5)
border-radius: 2rem
padding: .2rem .8rem
line-height: 1.3rem
outline: 0
transition: border-color .3s
&:hover,
&:focus
border-color: $blue-light
.algolia-close,
.localsearch-close
position: absolute
top: 0
right: .6rem
font-size: 1.25em
color: $black-light
cursor: pointer
.algolia-results,
.localsearch-results
overflow-x: hidden
overflow-y: scroll
max-height: 16rem
em
font-style: normal
background-color: $yellow-light
\ No newline at end of file
@import './common.styl'
@import './algolia.styl' if (hexo-config('algolia_search.enable'))
@import './localsearch.styl' if (hexo-config('local_search.enable'))
.localsearch-input-wrapper
input
width: 100%
.localsearch-result
overflow: auto
max-height: calc(80vh - 6rem)
b
border-bottom: 1px dashed red
color: red
& > ul
margin: 0
padding-left: 1.4rem
li
&:not(:last-child)
margin-bottom: 1rem
&::after
content: ''
display: block
border-bottom: 1px dashed #ccc
padding-bottom: .5rem
width: 100%
&-title
clearAStyle()
font-weight: $font-weight-bold
color: #222
&-content
overflow: hidden
width: 100%
max-height: 5rem
color: #666
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册