diff --git a/layout/_common/header.pug b/layout/_common/header.pug index 76384a970f54bd12e09e8946a2f0565b564c0598..c9f51e9425c0fdd1f0835f610b649a6b340a74b3 100644 --- a/layout/_common/header.pug +++ b/layout/_common/header.pug @@ -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 diff --git a/layout/_common/layout.pug b/layout/_common/layout.pug index 72afc79939db4b3e92fadfb52a25e9dd3943bb88..ad937da5e82deed817c211a6a6363af1cce91019 100644 --- a/layout/_common/layout.pug +++ b/layout/_common/layout.pug @@ -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 diff --git a/layout/_components/search/index.pug b/layout/_components/search/index.pug new file mode 100644 index 0000000000000000000000000000000000000000..9bb74e5fdf3ff1f89a18cbcdb229a55131f40bf6 --- /dev/null +++ b/layout/_components/search/index.pug @@ -0,0 +1,4 @@ +if theme.algolia_search.enable + include ./algolia.pug +else if theme.local_search.enable + include ./localsearch.pug diff --git a/layout/_components/search/localsearch.pug b/layout/_components/search/localsearch.pug new file mode 100644 index 0000000000000000000000000000000000000000..b5d897024a2802e6c11b01a3604b7d3a559a8cdf --- /dev/null +++ b/layout/_components/search/localsearch.pug @@ -0,0 +1,9 @@ +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 diff --git a/layout/_third-party/search/index.pug b/layout/_third-party/search/index.pug index 7c768ddefa291ea2d430ab23f1eecaeb533acfaf..9bb74e5fdf3ff1f89a18cbcdb229a55131f40bf6 100644 --- a/layout/_third-party/search/index.pug +++ b/layout/_third-party/search/index.pug @@ -1,2 +1,4 @@ if theme.algolia_search.enable include ./algolia.pug +else if theme.local_search.enable + include ./localsearch.pug diff --git a/layout/_third-party/search/localsearch.pug b/layout/_third-party/search/localsearch.pug new file mode 100644 index 0000000000000000000000000000000000000000..0adb30317364badc0304a0e49107f73d90d5c3f6 --- /dev/null +++ b/layout/_third-party/search/localsearch.pug @@ -0,0 +1,288 @@ +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 += '' + text.slice(hit.index, hitWordEnd) + ''; + 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 = '