diff --git a/packages/saber-plugin-search/LICENSE b/packages/saber-plugin-search/LICENSE new file mode 100644 index 000000000..0fa9bb511 --- /dev/null +++ b/packages/saber-plugin-search/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) EGOIST <0x142857@gmail.com> (https://github.com/egoist) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/saber-plugin-search/README.md b/packages/saber-plugin-search/README.md new file mode 100644 index 000000000..c53d1fa19 --- /dev/null +++ b/packages/saber-plugin-search/README.md @@ -0,0 +1,83 @@ +# saber-plugin-search + +Adds a hyper-fast, easy to integrate and highly customizable search to your app. + +## Install + +```bash +yarn add saber-plugin-search +``` + +## Usage + +In your `saber-config.yml`: + +```yml +plugins: + - resolve: saber-plugin-search +``` + +Then in your Vue components, you can call `this.$fetchSearchDatabase()` to get the database that you can query from, this method returns a Promise which resolves an array of `Page` objects: + +```js +;[ + { + type: 'page', + title: 'About this site', + excerpt: '...', + permalink: '/about.html' + }, + { + type: 'post', + title: 'Hello World', + excerpt: '...', + permalink: '/posts/hello-world.html' + } +] +``` + +Now you can query a keyword like this: + +```js +const database = await this.$fetchSearchDatabase() +// Typically you need to get the keyword from an `input` element +// We hardcoded it for convenience +const keyword = 'hello' +const matchedResults = database.filter(page => { + return page.title.includes(keyword) || page.excerpt.includes(keyword) +}) +``` + +The above example simply uses `String.prototype.includes` to check if the page matches the keyword, however you can use a more powerful library like [Fuse.js](https://fusejs.io/) if you want more accurate result: + +```js +import Fuse from 'fuse.js' + +const options = { + keys: [ + { + name: 'title', + weight: 0.6 + }, + { + name: 'excerpt', + weight: 0.4 + } + ] +} +const fuse = new Fuse(database, options) +const matchedResults = fuse.search(keyword) +``` + +## Plugin Options + +### index + +- Type: `string[]` +- Default: `['type', 'title', 'excerpt', 'permalink']` + +Only specified page properties will be included in the generated database. + +## License + +MIT. diff --git a/packages/saber-plugin-search/lib/index.js b/packages/saber-plugin-search/lib/index.js new file mode 100644 index 000000000..5a04cf711 --- /dev/null +++ b/packages/saber-plugin-search/lib/index.js @@ -0,0 +1,115 @@ +const { join } = require('path') + +const ID = 'search' + +exports.name = ID + +let db = {} + +function getLocale(locale) { + return db[locale] +} + +function stripHTML(input) { + return input.replace(/<(?:.|\n)*?>/gm, '') +} + +exports.apply = (api, options) => { + const { index } = Object.assign( + { + index: ['type', 'title', 'excerpt', 'permalink'] + }, + options + ) + + const { fs } = api.utils + + async function generateLocale(localePath) { + const pages = [] + + await Promise.all( + [...api.pages.values()].map(async page => { + if (page.draft || !page.type) { + return + } + + const matchedLocalePath = api.pages.getMatchedLocalePath(page.permalink) + if (localePath !== matchedLocalePath) { + return + } + + const item = {} + + for (const element of index) { + const value = page[element] + if (value !== undefined) { + if (element === 'content') { + item.content = stripHTML( + await api.renderer.renderPageContent(page.permalink) + ) + } else { + item[element] = stripHTML(page[element]) + } + } + } + + pages.push(item) + }) + ) + + return pages + } + + async function generateDatabase() { + const allLocalePaths = ['/'].concat(Object.keys(api.config.locales || {})) + + const results = await Promise.all( + allLocalePaths.map(localePath => generateLocale(localePath)) + ) + + const localDb = {} + results.forEach((result, i) => { + const locale = allLocalePaths[i].slice(1) || 'default' + localDb[locale] = result + }) + + return localDb + } + + api.browserApi.add(join(__dirname, 'saber-browser.js')) + + if (api.dev) { + api.hooks.onCreatePages.tapPromise(ID, async () => { + db = await generateDatabase() + }) + + api.hooks.onCreateServer.tap(ID, server => { + server.get('/_saber/plugin-search/:locale.json', (req, res) => { + const db = getLocale(req.params.locale) + if (db) { + res.writeHead(200, { + 'Content-Type': 'application/json' + }) + return res.end(JSON.stringify(db)) + } + + res.statusCode = 404 + res.end() + }) + }) + } else { + api.hooks.afterGenerate.tapPromise(ID, async () => { + const db = await generateDatabase() + for (const locale of Object.keys(db)) { + const items = db[locale] + const path = api.resolveOutDir( + '_saber', + 'plugin-search', + `${locale}.json` + ) + await fs.ensureDir(api.resolveOutDir('_saber', 'plugin-search')) + await fs.writeJson(path, items) + } + }) + } +} diff --git a/packages/saber-plugin-search/lib/saber-browser.js b/packages/saber-plugin-search/lib/saber-browser.js new file mode 100644 index 000000000..bcbb33887 --- /dev/null +++ b/packages/saber-plugin-search/lib/saber-browser.js @@ -0,0 +1,11 @@ +/* eslint-env browser */ +/* globals __PUBLIC_URL__ */ + +export default ({ Vue }) => { + Vue.prototype.$fetchSearchDatabase = function() { + const locale = this.$localePath.slice(1) || 'default' + return window + .fetch(`${__PUBLIC_URL__}_saber/plugin-search/${locale}.json`) + .then(res => res.json()) + } +} diff --git a/packages/saber-plugin-search/package.json b/packages/saber-plugin-search/package.json new file mode 100644 index 000000000..7122b4058 --- /dev/null +++ b/packages/saber-plugin-search/package.json @@ -0,0 +1,13 @@ +{ + "name": "saber-plugin-search", + "version": "0.0.1", + "description": "Add a fast search to your app.", + "license": "MIT", + "main": "lib/index.js", + "files": [ + "lib" + ], + "peerDependencies": { + "saber": ">=0.7.0" + } +} diff --git a/website/saber-config.js b/website/saber-config.js index 7bc9a3afd..62c68dbd6 100644 --- a/website/saber-config.js +++ b/website/saber-config.js @@ -93,6 +93,9 @@ module.exports = { ] } } + }, + { + resolve: '../packages/saber-plugin-search' } ] } diff --git a/yarn.lock b/yarn.lock index d2560adcc..ea3b837ea 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8095,7 +8095,7 @@ lodash.clonedeep@^4.5.0: lodash.debounce@^4.0.8: version "4.0.8" - resolved "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" + resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= lodash.get@^4.4.2: