diff --git a/include/register.js b/include/register.js index 6a5a4b77d..4c656b70c 100644 --- a/include/register.js +++ b/include/register.js @@ -5,7 +5,8 @@ module.exports = hexo => { require('hexo-component-inferno/lib/hexo/filter/locals')(hexo); require('./hexo/filter/stylus')(hexo); require('./hexo/generator/category')(hexo); - require('./hexo/generator/insight')(hexo); + require('hexo-component-inferno/lib/hexo/generator/assets')(hexo); + require('hexo-component-inferno/lib/hexo/generator/insight')(hexo); require('hexo-component-inferno/lib/hexo/generator/categories')(hexo); require('hexo-component-inferno/lib/hexo/generator/tags')(hexo); require('hexo-component-inferno/lib/hexo/helper/cdn')(hexo); diff --git a/layout/search/.gitkeep b/layout/search/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/layout/search/algolia.jsx b/layout/search/algolia.jsx deleted file mode 100644 index 2df359925..000000000 --- a/layout/search/algolia.jsx +++ /dev/null @@ -1,70 +0,0 @@ -const { Component, Fragment } = require('inferno'); -const { cacheComponent } = require('hexo-component-inferno/lib/util/cache'); - -class Algolia extends Component { - render() { - const { - translation, - applicationId, - apiKey, - indexName, - jsUrl, - algoliaSearchUrl, - instantSearchUrl - } = this.props; - - if (!applicationId || !apiKey || !indexName) { - return
- It seems that you forget to set the applicationId, apiKey, - or indexName for the Aloglia. - Please set it in _config.yml. -
; - } - - const js = `document.addEventListener('DOMContentLoaded', function () { - loadAlgolia(${JSON.stringify({ applicationId, apiKey, indexName })}, ${JSON.stringify(translation)}); - });`; - - return - - - - - - ; - } -} - -Algolia.Cacheable = cacheComponent(Algolia, 'search.algolia', props => { - const { config, helper } = props; - const { algolia } = config; - - return { - translation: { - hint: helper.__('search.hint'), - no_result: helper.__('search.no_result'), - untitled: helper.__('search.untitled'), - empty_preview: helper.__('search.empty_preview') - }, - applicationId: algolia ? algolia.applicationID : null, - apiKey: algolia ? algolia.apiKey : null, - indexName: algolia ? algolia.indexName : null, - algoliaSearchUrl: helper.cdn('algoliasearch', '4.0.3', 'dist/algoliasearch-lite.umd.js'), - instantSearchUrl: helper.cdn('instantsearch.js', '4.3.1', 'dist/instantsearch.production.min.js'), - jsUrl: helper.url_for('/js/algolia.js') - }; -}); - -module.exports = Algolia; diff --git a/layout/search/insight.jsx b/layout/search/insight.jsx deleted file mode 100644 index 78540367e..000000000 --- a/layout/search/insight.jsx +++ /dev/null @@ -1,47 +0,0 @@ -const { Component, Fragment } = require('inferno'); -const { cacheComponent } = require('hexo-component-inferno/lib/util/cache'); - -class Insight extends Component { - render() { - const { translation, contentUrl, jsUrl } = this.props; - - const js = `document.addEventListener('DOMContentLoaded', function () { - loadInsight(${JSON.stringify({ contentUrl })}, ${JSON.stringify(translation)}); - });`; - - return - - - - ; - } -} - -Insight.Cacheable = cacheComponent(Insight, 'search.insight', props => { - const { helper } = props; - - return { - translation: { - hint: helper.__('search.hint'), - untitled: helper.__('search.untitled'), - posts: helper._p('common.post', Infinity), - pages: helper._p('common.page', Infinity), - categories: helper._p('common.category', Infinity), - tags: helper._p('common.tag', Infinity) - }, - contentUrl: helper.url_for('/content.json'), - jsUrl: helper.url_for('/js/insight.js') - }; -}); - -module.exports = Insight; diff --git a/package.json b/package.json index def20d5d8..d34c2af70 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "bulma-stylus": "0.8.0", "deepmerge": "^4.2.2", "hexo": "^4.2.0", - "hexo-component-inferno": "^0.1.3", + "hexo-component-inferno": "^0.2.0", "hexo-log": "^1.0.0", "hexo-pagination": "^1.0.0", "hexo-renderer-inferno": "^0.1.3", diff --git a/source/js/algolia.js b/source/js/algolia.js deleted file mode 100644 index f4719651b..000000000 --- a/source/js/algolia.js +++ /dev/null @@ -1,87 +0,0 @@ -/* global instantsearch, algoliasearch */ -function loadAlgolia(config, translation) { // eslint-disable-line no-unused-vars - const search = instantsearch({ - indexName: config.indexName, - searchClient: algoliasearch(config.applicationId, config.apiKey) - }); - - search.addWidgets([ - instantsearch.widgets.configure({ - attributesToSnippet: ['excerpt'] - }) - ]); - - search.addWidget(instantsearch.widgets.searchBox({ - container: '#algolia-input', - placeholder: translation.hint, - showReset: false, - showSubmit: false, - showLoadingIndicator: false, - cssClasses: { - root: 'searchbox-input-container', - form: 'searchbox-input-container', - input: 'searchbox-input' - } - })); - - search.addWidget(instantsearch.widgets.poweredBy({ - container: '#algolia-poweredby' - })); - - search.addWidget(instantsearch.widgets.hits({ - container: '.searchbox-body', - escapeHTML: false, - cssClasses: { - root: 'searchbox-result-container', - emptyRoot: ['searchbox-result-item', 'disabled'] - }, - templates: { - empty: function(results) { - return translation.no_result + ': ' + results.query; - }, - item: function(hit) { - const title = instantsearch.highlight({ attribute: 'title', hit }); - let excerpt = instantsearch.highlight({ attribute: 'excerpt', hit }); - excerpt = excerpt.replace(new RegExp('', 'ig'), '[algolia-highlight]') - .replace(new RegExp('', 'ig'), '[/algolia-highlight]') - .replace(/(<([^>]+)>)/ig, '') - .replace(/(\[algolia-highlight\])/ig, '') - .replace(/(\[\/algolia-highlight\])/ig, ''); - return `
- - - ${title ? title : translation.untitled} - ${excerpt ? excerpt : translation.empty_preview} - - -
`; - } - } - })); - - search.addWidget(instantsearch.widgets.pagination({ - container: '.searchbox-footer', - cssClasses: { - list: 'searchbox-pagination', - item: 'searchbox-pagination-item', - link: 'searchbox-pagination-link', - selectedItem: 'active', - disabledItem: 'disabled' - } - })); - - search.start(); - - if (location.hash.trim() === '#algolia-search') { - $('.searchbox').addClass('show'); - } - - $(document).on('click', '.navbar-main .search', () => { - $('.searchbox').toggleClass('show'); - $('.searchbox-input').focus(); - }).on('click', '.searchbox .searchbox-mask', () => { - $('.searchbox').removeClass('show'); - }).on('click', '.searchbox-close', () => { - $('.searchbox').removeClass('show'); - }); -} diff --git a/source/js/insight.js b/source/js/insight.js deleted file mode 100644 index 33150b327..000000000 --- a/source/js/insight.js +++ /dev/null @@ -1,313 +0,0 @@ -/** - * Insight search plugin - * @author PPOffice { @link https://github.com/ppoffice } - */ -function loadInsight(config, translation) { // eslint-disable-line no-unused-vars - const $main = $('.searchbox'); - const $input = $main.find('.searchbox-input'); - const $container = $main.find('.searchbox-body'); - - function section(title) { - return $('
').addClass('searchbox-result-section').append($('
').text(title)); - } - - function merge(ranges) { - let last; - const result = []; - - ranges.forEach(r => { - if (!last || r[0] > last[1]) { - result.push(last = r); - } else if (r[1] > last[1]) { - last[1] = r[1]; - } - }); - - return result; - } - - function findAndHighlight(text, matches, maxlen) { - if (!Array.isArray(matches) || !matches.length || !text) { - return maxlen ? text.slice(0, maxlen) : text; - } - const testText = text.toLowerCase(); - const indices = matches.map(match => { - const index = testText.indexOf(match.toLowerCase()); - if (!match || index === -1) { - return null; - } - return [index, index + match.length]; - }).filter(match => { - return match !== null; - }).sort((a, b) => { - return a[0] - b[0] || a[1] - b[1]; - }); - - if (!indices.length) { - return text; - } - - let result = ''; let last = 0; - const ranges = merge(indices); - const sumRange = [ranges[0][0], ranges[ranges.length - 1][1]]; - if (maxlen && maxlen < sumRange[1]) { - last = sumRange[0]; - } - - for (let i = 0; i < ranges.length; i++) { - const range = ranges[i]; - result += text.slice(last, Math.min(range[0], sumRange[0] + maxlen)); - if (maxlen && range[0] >= sumRange[0] + maxlen) { - break; - } - result += '' + text.slice(range[0], range[1]) + ''; - last = range[1]; - if (i === ranges.length - 1) { - if (maxlen) { - result += text.slice(range[1], Math.min(text.length, sumRange[0] + maxlen + 1)); - } else { - result += text.slice(range[1]); - } - } - } - - return result; - } - - function searchItem(icon, title, slug, preview, url) { - title = title != null && title !== '' ? title : translation.untitled; - - return ` - - - - - - ${title} - ${slug ? '(' + slug + ')' : ''} - - ${preview ? '' + preview + '' : ''} - - `; - } - - function sectionFactory(keywords, type, array) { - let $searchItems; - if (array.length === 0) return null; - const sectionTitle = translation[type.toLowerCase()]; - switch (type) { - case 'POSTS': - case 'PAGES': - $searchItems = array.map(item => { - const title = findAndHighlight(item.title, keywords); - const text = findAndHighlight(item.text, keywords, 100); - return searchItem('file', title, null, text, item.link); - }); - break; - case 'CATEGORIES': - case 'TAGS': - $searchItems = array.map(item => { - const name = findAndHighlight(item.name, keywords); - const slug = findAndHighlight(item.slug, keywords); - return searchItem(type === 'CATEGORIES' ? 'folder' : 'tag', name, slug, null, item.link); - }); - break; - default: - return null; - } - return section(sectionTitle).append($searchItems); - } - - function parseKeywords(keywords) { - return keywords.split(' ').filter(keyword => { - return !!keyword; - }).map(keyword => { - return keyword.toLowerCase(); - }); - } - - /** - * Judge if a given post/page/category/tag contains all of the keywords. - * @param Object obj Object to be weighted - * @param Array fields Object's fields to find matches - */ - function filter(keywords, obj, fields) { - const keywordArray = parseKeywords(keywords); - const containKeywords = keywordArray.filter(keyword => { - const containFields = fields.filter(field => { - if (!Object.prototype.hasOwnProperty.call(obj, field)) { - return false; - } - if (obj[field].toLowerCase().indexOf(keyword) > -1) { - return true; - } - return false; - }); - if (containFields.length > 0) { - return true; - } - return false; - }); - return containKeywords.length === keywordArray.length; - } - - function filterFactory(keywords) { - return { - post: function(obj) { - return filter(keywords, obj, ['title', 'text']); - }, - page: function(obj) { - return filter(keywords, obj, ['title', 'text']); - }, - category: function(obj) { - return filter(keywords, obj, ['name', 'slug']); - }, - tag: function(obj) { - return filter(keywords, obj, ['name', 'slug']); - } - }; - } - - /** - * Calculate the weight of a matched post/page/category/tag. - * @param Object obj Object to be weighted - * @param Array fields Object's fields to find matches - * @param Array weights Weight of every field - */ - function weight(keywords, obj, fields, weights) { - let value = 0; - parseKeywords(keywords).forEach(keyword => { - const pattern = new RegExp(keyword, 'img'); // Global, Multi-line, Case-insensitive - fields.forEach((field, index) => { - if (Object.prototype.hasOwnProperty.call(obj, field)) { - const matches = obj[field].match(pattern); - value += matches ? matches.length * weights[index] : 0; - } - }); - }); - return value; - } - - function weightFactory(keywords) { - return { - post: function(obj) { - return weight(keywords, obj, ['title', 'text'], [3, 1]); - }, - page: function(obj) { - return weight(keywords, obj, ['title', 'text'], [3, 1]); - }, - category: function(obj) { - return weight(keywords, obj, ['name', 'slug'], [1, 1]); - }, - tag: function(obj) { - return weight(keywords, obj, ['name', 'slug'], [1, 1]); - } - }; - } - - function search(json, keywords) { - const weights = weightFactory(keywords); - const filters = filterFactory(keywords); - const posts = json.posts; - const pages = json.pages; - const tags = json.tags; - const categories = json.categories; - return { - posts: posts.filter(filters.post).sort((a, b) => { return weights.post(b) - weights.post(a); }).slice(0, 5), - pages: pages.filter(filters.page).sort((a, b) => { return weights.page(b) - weights.page(a); }).slice(0, 5), - categories: categories.filter(filters.category).sort((a, b) => { return weights.category(b) - weights.category(a); }).slice(0, 5), - tags: tags.filter(filters.tag).sort((a, b) => { return weights.tag(b) - weights.tag(a); }).slice(0, 5) - }; - } - - function searchResultToDOM(keywords, searchResult) { - $container.empty(); - for (const key in searchResult) { - $container.append(sectionFactory(parseKeywords(keywords), - key.toUpperCase(), searchResult[key])); - } - } - - function scrollTo($item) { - if ($item.length === 0) return; - const wrapperHeight = $container[0].clientHeight; - const itemTop = $item.position().top - $container.scrollTop(); - const itemBottom = $item[0].clientHeight + $item.position().top; - if (itemBottom > wrapperHeight + $container.scrollTop()) { - $container.scrollTop(itemBottom - $container[0].clientHeight); - } - if (itemTop < 0) { - $container.scrollTop($item.position().top); - } - } - - function selectItemByDiff(value) { - const $items = $.makeArray($container.find('.searchbox-result-item')); - let prevPosition = -1; - $items.forEach((item, index) => { - if ($(item).hasClass('active')) { - prevPosition = index; - - } - }); - const nextPosition = ($items.length + prevPosition + value) % $items.length; - $($items[prevPosition]).removeClass('active'); - $($items[nextPosition]).addClass('active'); - scrollTo($($items[nextPosition])); - } - - function gotoLink($item) { - if ($item && $item.length) { - location.href = $item.attr('href'); - } - } - - $.getJSON(config.contentUrl, json => { - if (location.hash.trim() === '#insight-search') { - $main.addClass('show'); - } - $input.on('input', function() { - const keywords = $(this).val(); - searchResultToDOM(keywords, search(json, keywords)); - }); - $input.trigger('input'); - }); - - let touch = false; - $(document).on('click focus', '.navbar-main .search', () => { - $main.addClass('show'); - $main.find('.searchbox-input').focus(); - }).on('click touchend', '.searchbox-result-item', function(e) { - if (e.type !== 'click' && !touch) { - return; - } - gotoLink($(this)); - touch = false; - }).on('click touchend', '.searchbox-close', e => { - if (e.type !== 'click' && !touch) { - return; - } - $('.navbar-main').css('pointer-events', 'none'); - setTimeout(() => { - $('.navbar-main').css('pointer-events', 'auto'); - }, 400); - $main.removeClass('show'); - touch = false; - }).on('keydown', e => { - if (!$main.hasClass('show')) return; - switch (e.keyCode) { - case 27: // ESC - $main.removeClass('show'); break; - case 38: // UP - selectItemByDiff(-1); break; - case 40: // DOWN - selectItemByDiff(1); break; - case 13: // ENTER - gotoLink($container.find('.searchbox-result-item.active').eq(0)); break; - } - }).on('touchstart', e => { - touch = true; - }).on('touchmove', e => { - touch = false; - }); -}