├── cypress.json ├── example ├── Gemfile ├── _layouts │ ├── page.html │ ├── post.html │ └── default.html ├── search.json ├── _plugins │ ├── simple_search_filter_cn.rb │ └── simple_search_filter.rb ├── about.md ├── index.html ├── _includes │ ├── head.html │ ├── header.html │ └── footer.html ├── css │ └── main.scss ├── _posts │ ├── 2014-11-01-welcome-to-jekyll.markdown │ └── 2014-11-02-test.markdown ├── _sass │ ├── _syntax-highlighting.scss │ ├── _base.scss │ └── _layout.scss └── js │ ├── simple-jekyll-search.min.js │ └── simple-jekyll-search.js ├── xscode.png ├── cypress ├── fixtures │ ├── profile.json │ ├── example.json │ └── users.json ├── plugins │ └── index.js ├── support │ ├── index.js │ └── commands.js └── integration │ └── simple-jekyll-search.js ├── .gitignore ├── index.html ├── CYPRESS.md ├── src ├── SearchStrategies │ ├── FuzzySearchStrategy.js │ └── LiteralSearchStrategy.js ├── utils.js ├── JSONLoader.js ├── Templater.js ├── OptionsValidator.js ├── Repository.js └── index.js ├── scripts └── stamp.js ├── tests ├── utils.test.js ├── OptionsValidator.test.js ├── SearchStrategies │ ├── FuzzySearchStrategy.test.js │ └── LiteralSearchStrategy.test.js ├── Repository.test.js └── Templater.test.js ├── .github └── workflows │ └── main.yml ├── LICENSE.md ├── package.json ├── dest ├── simple-jekyll-search.min.js └── simple-jekyll-search.js ├── WIKI.md └── README.md /cypress.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /example/Gemfile: -------------------------------------------------------------------------------- 1 | gem "jekyll" 2 | -------------------------------------------------------------------------------- /xscode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christian-fei/Simple-Jekyll-Search/HEAD/xscode.png -------------------------------------------------------------------------------- /cypress/fixtures/profile.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 8739, 3 | "name": "Jane", 4 | "email": "jane@example.com" 5 | } -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.sublime-workspace 2 | npm-debug.log 3 | 4 | .sass-cache/ 5 | _site/ 6 | node_modules/ 7 | cypress/videos/ 8 | cypress/screenshots/ 9 | .DS_Store 10 | .idea 11 | package-lock.json 12 | .jekyll-cache -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /CYPRESS.md: -------------------------------------------------------------------------------- 1 | # Cypress 2 | 3 | ```bash 4 | npm i 5 | ``` 6 | 7 | run example blog with jekyll search build 8 | 9 | ```bash 10 | cd example 11 | jekyll serve 12 | ``` 13 | 14 | run cypress tests 15 | 16 | ```bash 17 | npm run cypress run 18 | # or 19 | npm run cypress open 20 | ``` 21 | -------------------------------------------------------------------------------- /example/_layouts/page.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | --- 4 |
5 | 6 |
7 |

{{ page.title }}

8 |
9 | 10 |
11 | {{ content }} 12 |
13 | 14 |
15 | -------------------------------------------------------------------------------- /src/SearchStrategies/FuzzySearchStrategy.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fuzzysearch = require('fuzzysearch') 4 | 5 | module.exports = new FuzzySearchStrategy() 6 | 7 | function FuzzySearchStrategy () { 8 | this.matches = function (string, crit) { 9 | if (string === null) { 10 | return false 11 | } 12 | return fuzzysearch(crit.toLowerCase(), string.toLowerCase()) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /example/search.json: -------------------------------------------------------------------------------- 1 | --- 2 | layout: none 3 | --- 4 | [ 5 | {% for post in site.posts %} 6 | { 7 | "title" : "{{ post.title | escape }}", 8 | "category" : "{{ post.category }}", 9 | "tags" : "{{ post.tags | join: ', ' }}", 10 | "url" : "{{ site.baseurl }}{{ post.url }}", 11 | "date" : "{{ post.date }}" 12 | } {% unless forloop.last %},{% endunless %} 13 | {% endfor %} 14 | ] 15 | -------------------------------------------------------------------------------- /scripts/stamp.js: -------------------------------------------------------------------------------- 1 | // This file is taken from Bootstrap and adapted. 2 | 3 | /* eslint-env node, es6 */ 4 | 5 | 'use strict' 6 | 7 | const year = new Date().getFullYear() 8 | 9 | const stampTop = 10 | `/*! 11 | * Simple-Jekyll-Search 12 | * Copyright 2015-${year}, Christian Fei 13 | * Licensed under the MIT License. 14 | */ 15 | 16 | ` 17 | 18 | process.stdout.write(stampTop) 19 | process.stdin.pipe(process.stdout) 20 | -------------------------------------------------------------------------------- /example/_layouts/post.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | --- 4 |
5 | 6 |
7 |

{{ page.title }}

8 | 9 |
10 | 11 |
12 | {{ content }} 13 |
14 | 15 |
16 | -------------------------------------------------------------------------------- /src/SearchStrategies/LiteralSearchStrategy.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = new LiteralSearchStrategy() 4 | 5 | function LiteralSearchStrategy () { 6 | this.matches = function (str, crit) { 7 | if (!str) return false 8 | str = str.trim().toLowerCase() 9 | crit = crit.endsWith(' ') ? [crit.toLowerCase()] : crit.trim().toLowerCase().split(' ') 10 | 11 | return crit.filter(word => str.indexOf(word) >= 0).length === crit.length 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/_plugins/simple_search_filter_cn.rb: -------------------------------------------------------------------------------- 1 | module Jekyll 2 | module CharFilter 3 | def remove_chars_cn(input) 4 | input.gsub! '\\','\' 5 | input.gsub! /\t/, ' ' 6 | input.gsub! '@','' 7 | input.gsub! '$','' 8 | input.gsub! '%','' 9 | input.gsub! '&','' 10 | input.gsub! '"','' 11 | input.gsub! '{','' 12 | input.gsub! '}','' 13 | input 14 | end 15 | end 16 | end 17 | 18 | Liquid::Template.register_filter(Jekyll::CharFilter) 19 | -------------------------------------------------------------------------------- /tests/utils.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | const utils = require('../src/utils') 3 | 4 | test('merges objects', t => { 5 | const defaultOptions = { foo: '', bar: '' } 6 | const options = { bar: 'overwritten' } 7 | const mergedOptions = utils.merge(defaultOptions, options) 8 | 9 | t.deepEqual(mergedOptions.foo, defaultOptions.foo) 10 | t.deepEqual(mergedOptions.bar, options.bar) 11 | }) 12 | 13 | test('returns true if is JSON object', t => { 14 | t.true(utils.isJSON({ foo: 'bar' })) 15 | }) 16 | -------------------------------------------------------------------------------- /example/about.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: About 4 | permalink: /about/ 5 | --- 6 | 7 | This is the base Jekyll theme. You can find out more info about customizing your Jekyll theme, as well as basic Jekyll usage documentation at [jekyllrb.com](http://jekyllrb.com/) 8 | 9 | You can find the source code for the Jekyll new theme at: [github.com/jglovier/jekyll-new](https://github.com/jglovier/jekyll-new) 10 | 11 | You can find the source code for Jekyll at [github.com/jekyll/jekyll](https://github.com/jekyll/jekyll) 12 | -------------------------------------------------------------------------------- /example/_plugins/simple_search_filter.rb: -------------------------------------------------------------------------------- 1 | module Jekyll 2 | module CharFilter 3 | def remove_chars(input) 4 | input.gsub! '\\','\' 5 | input.gsub! /\t/, ' ' 6 | input.strip_control_and_extended_characters 7 | end 8 | end 9 | end 10 | 11 | Liquid::Template.register_filter(Jekyll::CharFilter) 12 | 13 | class String 14 | def strip_control_and_extended_characters() 15 | chars.each_with_object("") do |char, str| 16 | str << char if char.ascii_only? and char.ord.between?(32,126) 17 | end 18 | end 19 | end -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | --- 4 | 5 |
6 | 7 |

Posts

8 | 9 | 20 | 21 |

subscribe via RSS

22 | 23 |
24 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | merge: merge, 5 | isJSON: isJSON 6 | } 7 | 8 | function merge (defaultParams, mergeParams) { 9 | const mergedOptions = {} 10 | for (const option in defaultParams) { 11 | mergedOptions[option] = defaultParams[option] 12 | if (typeof mergeParams[option] !== 'undefined') { 13 | mergedOptions[option] = mergeParams[option] 14 | } 15 | } 16 | return mergedOptions 17 | } 18 | 19 | function isJSON (json) { 20 | try { 21 | if (json instanceof Object && JSON.parse(JSON.stringify(json))) { 22 | return true 23 | } 24 | return false 25 | } catch (err) { 26 | return false 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example plugins/index.js can be used to load plugins 3 | // 4 | // You can change the location of this file or turn off loading 5 | // the plugins file with the 'pluginsFile' configuration option. 6 | // 7 | // You can read more here: 8 | // https://on.cypress.io/plugins-guide 9 | // *********************************************************** 10 | 11 | // This function is called when a project is opened or re-opened (e.g. due to 12 | // the project's config changing) 13 | 14 | module.exports = (on, config) => { 15 | // `on` is used to hook into various events Cypress emits 16 | // `config` is the resolved Cypress config 17 | } 18 | -------------------------------------------------------------------------------- /cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /src/JSONLoader.js: -------------------------------------------------------------------------------- 1 | /* globals ActiveXObject:false */ 2 | 3 | 'use strict' 4 | 5 | module.exports = { 6 | load: load 7 | } 8 | 9 | function load (location, callback) { 10 | const xhr = getXHR() 11 | xhr.open('GET', location, true) 12 | xhr.onreadystatechange = createStateChangeListener(xhr, callback) 13 | xhr.send() 14 | } 15 | 16 | function createStateChangeListener (xhr, callback) { 17 | return function () { 18 | if (xhr.readyState === 4 && xhr.status === 200) { 19 | try { 20 | callback(null, JSON.parse(xhr.responseText)) 21 | } catch (err) { 22 | callback(err, null) 23 | } 24 | } 25 | } 26 | } 27 | 28 | function getXHR () { 29 | return window.XMLHttpRequest ? new window.XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP') 30 | } 31 | -------------------------------------------------------------------------------- /src/Templater.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | compile: compile, 5 | setOptions: setOptions 6 | } 7 | 8 | const options = {} 9 | options.pattern = /\{(.*?)\}/g 10 | options.template = '' 11 | options.middleware = function () {} 12 | 13 | function setOptions (_options) { 14 | options.pattern = _options.pattern || options.pattern 15 | options.template = _options.template || options.template 16 | if (typeof _options.middleware === 'function') { 17 | options.middleware = _options.middleware 18 | } 19 | } 20 | 21 | function compile (data) { 22 | return options.template.replace(options.pattern, function (match, prop) { 23 | const value = options.middleware(prop, data[prop], options.template) 24 | if (typeof value !== 'undefined') { 25 | return value 26 | } 27 | return data[prop] || match 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /cypress/integration/simple-jekyll-search.js: -------------------------------------------------------------------------------- 1 | describe('Simple Jekyll Search', function () { 2 | it('Searching a Post', function () { 3 | cy.visit('http://localhost:4000') 4 | 5 | cy.get('#search-input') 6 | .type('This') 7 | 8 | cy.get('#results-container') 9 | .contains('This is just a test') 10 | }) 11 | 12 | it('Searching a Post follows link with query', function () { 13 | cy.visit('http://localhost:4000') 14 | 15 | cy.get('#search-input') 16 | .type('This') 17 | 18 | cy.get('#results-container') 19 | .contains('This is just a test') 20 | .click() 21 | 22 | cy.url().should('include', '?query=This') 23 | }) 24 | 25 | it('No results found', function () { 26 | cy.visit('http://localhost:4000') 27 | 28 | cy.get('#search-input') 29 | .type('404') 30 | 31 | cy.contains('No results found') 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add("login", (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This is will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Simple-Jekyll-Search 2 | on: [push] 3 | jobs: 4 | build_deploy: 5 | runs-on: ubuntu-18.04 6 | steps: 7 | - uses: actions/checkout@master 8 | with: 9 | ref: refs/heads/master 10 | - name: install 11 | run: | 12 | npm install 13 | - name: test 14 | run: | 15 | npm t 16 | # - name: start server for uat 17 | # run: | 18 | # npx http-server example/_site -p 4000 19 | # - name: uat 20 | # uses: cypress-io/github-action@v2 21 | # with: 22 | # record: false 23 | # # cache-key: node-v${{ matrix.node }}-on-${{ runner.os }}-hash-${{ hashFiles('package-lock.json') }} 24 | - name: publish 25 | env: 26 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 27 | run: | 28 | npm config set '//registry.npmjs.org/:_authToken' "${{ secrets.NPM_TOKEN }}" 29 | npm publish || true 30 | -------------------------------------------------------------------------------- /src/OptionsValidator.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = function OptionsValidator (params) { 4 | if (!validateParams(params)) { 5 | throw new Error('-- OptionsValidator: required options missing') 6 | } 7 | 8 | if (!(this instanceof OptionsValidator)) { 9 | return new OptionsValidator(params) 10 | } 11 | 12 | const requiredOptions = params.required 13 | 14 | this.getRequiredOptions = function () { 15 | return requiredOptions 16 | } 17 | 18 | this.validate = function (parameters) { 19 | const errors = [] 20 | requiredOptions.forEach(function (requiredOptionName) { 21 | if (typeof parameters[requiredOptionName] === 'undefined') { 22 | errors.push(requiredOptionName) 23 | } 24 | }) 25 | return errors 26 | } 27 | 28 | function validateParams (params) { 29 | if (!params) { 30 | return false 31 | } 32 | return typeof params.required !== 'undefined' && params.required instanceof Array 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /example/_includes/head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {% if page.title %}{{ page.title }}{% else %}{{ site.title }}{% endif %} 7 | 8 | 9 | 10 | 11 | 12 | 29 | 30 | -------------------------------------------------------------------------------- /tests/OptionsValidator.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | 3 | const OptionsValidator = require('../src/OptionsValidator.js') 4 | 5 | test('can be instanciated with options', t => { 6 | const requiredOptions = ['foo', 'bar'] 7 | const optionsValidator = new OptionsValidator({ 8 | required: requiredOptions 9 | }) 10 | 11 | t.deepEqual(optionsValidator.getRequiredOptions(), requiredOptions) 12 | }) 13 | 14 | test('returns empty errors array for valid options', t => { 15 | const requiredOptions = ['foo', 'bar'] 16 | const optionsValidator = new OptionsValidator({ 17 | required: requiredOptions 18 | }) 19 | 20 | const errors = optionsValidator.validate({ 21 | foo: '', 22 | bar: '' 23 | }) 24 | 25 | t.is(errors.length, 0) 26 | }) 27 | test('returns array with errors for invalid options', t => { 28 | const requiredOptions = ['foo', 'bar'] 29 | const optionsValidator = new OptionsValidator({ 30 | required: requiredOptions 31 | }) 32 | 33 | const errors = optionsValidator.validate({ 34 | foo: '' 35 | }) 36 | 37 | t.is(errors.length, 1) 38 | }) 39 | -------------------------------------------------------------------------------- /tests/SearchStrategies/FuzzySearchStrategy.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | const FuzzySearchStrategy = require('../../src/SearchStrategies/FuzzySearchStrategy') 3 | 4 | test('does not match words that don\'t contain the search criteria', t => { 5 | t.deepEqual(FuzzySearchStrategy.matches('fuzzy', 'fzyyy'), false) 6 | t.deepEqual(FuzzySearchStrategy.matches('react', 'angular'), false) 7 | 8 | t.deepEqual(FuzzySearchStrategy.matches('what the heck', 'wth?'), false) 9 | }) 10 | 11 | test('matches words containing the search criteria', t => { 12 | t.deepEqual(FuzzySearchStrategy.matches('fuzzy', 'fzy'), true) 13 | t.deepEqual(FuzzySearchStrategy.matches('react', 'rct'), true) 14 | 15 | t.deepEqual(FuzzySearchStrategy.matches('what the heck', 'wth'), true) 16 | }) 17 | 18 | test('is case insensitive', t => { 19 | t.deepEqual(FuzzySearchStrategy.matches('Different Cases', 'dc'), true) 20 | t.deepEqual(FuzzySearchStrategy.matches('UPPERCASE', 'upprcs'), true) 21 | t.deepEqual(FuzzySearchStrategy.matches('lowercase', 'lc'), true) 22 | t.deepEqual(FuzzySearchStrategy.matches('DiFfErENt cASeS', 'dc'), true) 23 | }) 24 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Christian Fei 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /example/_layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% include head.html %} 5 | 6 | 7 | 8 | {% include header.html %} 9 | 10 |
11 | 12 | 13 |
14 | 15 |
16 |
17 | {{ content }} 18 |
19 |
20 | 21 | {% include footer.html %} 22 | 23 | 24 | 25 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /example/css/main.scss: -------------------------------------------------------------------------------- 1 | --- 2 | # Only the main Sass file needs front matter (the dashes are enough) 3 | --- 4 | @charset "utf-8"; 5 | 6 | 7 | 8 | // Our variables 9 | $base-font-family: Helvetica, Arial, sans-serif; 10 | $base-font-size: 16px; 11 | $small-font-size: $base-font-size * 0.875; 12 | $base-line-height: 1.5; 13 | 14 | $spacing-unit: 30px; 15 | 16 | $text-color: #111; 17 | $background-color: #fdfdfd; 18 | $brand-color: #2a7ae2; 19 | 20 | $grey-color: #828282; 21 | $grey-color-light: lighten($grey-color, 40%); 22 | $grey-color-dark: darken($grey-color, 25%); 23 | 24 | $on-palm: 600px; 25 | $on-laptop: 800px; 26 | 27 | 28 | 29 | // Using media queries with like this: 30 | // @include media-query($palm) { 31 | // .wrapper { 32 | // padding-right: $spacing-unit / 2; 33 | // padding-left: $spacing-unit / 2; 34 | // } 35 | // } 36 | @mixin media-query($device) { 37 | @media screen and (max-width: $device) { 38 | @content; 39 | } 40 | } 41 | 42 | 43 | 44 | // Import partials from `sass_dir` (defaults to `_sass`) 45 | @import 46 | "base", 47 | "layout", 48 | "syntax-highlighting" 49 | ; 50 | -------------------------------------------------------------------------------- /example/_includes/header.html: -------------------------------------------------------------------------------- 1 | 28 | -------------------------------------------------------------------------------- /example/_posts/2014-11-01-welcome-to-jekyll.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: "Welcome to Jekyll!" 4 | date: 2014-11-01 20:07:22 5 | categories: jekyll update 6 | --- 7 | You’ll find this post in your `_posts` directory. Go ahead and edit it and re-build the site to see your changes. You can rebuild the site in many different ways, but the most common way is to run `jekyll serve --watch`, which launches a web server and auto-regenerates your site when a file is updated. 8 | 9 | To add new posts, simply add a file in the `_posts` directory that follows the convention `YYYY-MM-DD-name-of-post.ext` and includes the necessary front matter. Take a look at the source for this post to get an idea about how it works. 10 | 11 | Jekyll also offers powerful support for code snippets: 12 | 13 | {% highlight ruby %} 14 | def print_hi(name) 15 | puts "Hi, #{name}" 16 | end 17 | print_hi('Tom') 18 | # => prints 'Hi, Tom' to STDOUT. 19 | {% endhighlight %} 20 | 21 | Check out the [Jekyll docs][jekyll] for more info on how to get the most out of Jekyll. File all bugs/feature requests at [Jekyll’s GitHub repo][jekyll-gh]. If you have questions, you can ask them on [Jekyll’s dedicated Help repository][jekyll-help]. 22 | 23 | [jekyll]: http://jekyllrb.com 24 | [jekyll-gh]: https://github.com/jekyll/jekyll 25 | [jekyll-help]: https://github.com/jekyll/jekyll-help 26 | -------------------------------------------------------------------------------- /example/_posts/2014-11-02-test.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: "This is just a test" 4 | date: 2014-11-01 20:07:22 5 | categories: jekyll update 6 | --- 7 | 8 | Lorem ispums just some test. 9 | 10 | 11 | You’ll find this post in your `_posts` directory. Go ahead and edit it and re-build the site to see your changes. You can rebuild the site in many different ways, but the most common way is to run `jekyll serve --watch`, which launches a web server and auto-regenerates your site when a file is updated. 12 | 13 | To add new posts, simply add a file in the `_posts` directory that follows the convention `YYYY-MM-DD-name-of-post.ext` and includes the necessary front matter. Take a look at the source for this post to get an idea about how it works. 14 | 15 | Jekyll also offers powerful support for code snippets: 16 | 17 | {% highlight ruby %} 18 | def print_hi(name) 19 | puts "Hi, #{name}" 20 | end 21 | print_hi('Tom') 22 | # => prints 'Hi, Tom' to STDOUT. 23 | {% endhighlight %} 24 | 25 | Check out the [Jekyll docs][jekyll] for more info on how to get the most out of Jekyll. File all bugs/feature requests at [Jekyll’s GitHub repo][jekyll-gh]. If you have questions, you can ask them on [Jekyll’s dedicated Help repository][jekyll-help]. 26 | 27 | [jekyll]: http://jekyllrb.com 28 | [jekyll-gh]: https://github.com/jekyll/jekyll 29 | [jekyll-help]: https://github.com/jekyll/jekyll-help 30 | -------------------------------------------------------------------------------- /tests/SearchStrategies/LiteralSearchStrategy.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | 3 | const LiteralSearchStrategy = require('../../src/SearchStrategies/LiteralSearchStrategy') 4 | 5 | test('matches a word that is contained in the search criteria (single words)', t => { 6 | t.deepEqual(LiteralSearchStrategy.matches('hello world test search text', 'world'), true) 7 | }) 8 | 9 | test('does not match if a word is not contained in the search criteria', t => { 10 | t.deepEqual(LiteralSearchStrategy.matches('hello world test search text', 'hello my world'), false) 11 | }) 12 | 13 | test('matches a word that is contained in the search criteria (multiple words)', t => { 14 | t.deepEqual(LiteralSearchStrategy.matches('hello world test search text', 'hello text world'), true) 15 | }) 16 | 17 | test('matches exact words when exacts words with space in the search criteria', t => { 18 | t.deepEqual(LiteralSearchStrategy.matches('hello world test search text', 'hello world '), true) 19 | }) 20 | 21 | test('does not matches multiple words if not exact words with space in the search criteria', t => { 22 | t.deepEqual(LiteralSearchStrategy.matches('hello world test search text', 'hello text world '), false) 23 | }) 24 | 25 | test('matches a word that is partially contained in the search criteria', t => { 26 | t.deepEqual(LiteralSearchStrategy.matches('this tasty tester text', 'test'), true) 27 | }) 28 | 29 | test('does not matches a word that is partially contained in the search criteria when followed by a space', t => { 30 | t.deepEqual(LiteralSearchStrategy.matches('this tasty tester text', 'test '), false) 31 | }) 32 | -------------------------------------------------------------------------------- /tests/Repository.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | 3 | const barElement = { title: 'bar', content: 'bar' } 4 | const almostBarElement = { title: 'almostbar', content: 'almostbar' } 5 | const loremElement = { title: 'lorem', content: 'lorem ipsum' } 6 | 7 | const data = [barElement, almostBarElement, loremElement] 8 | 9 | let repository 10 | 11 | test.beforeEach(() => { 12 | repository = require('../src/Repository.js') 13 | repository.put(data) 14 | }) 15 | 16 | test.afterEach(() => { 17 | repository.clear() 18 | }) 19 | 20 | test('finds a simple string', t => { 21 | t.deepEqual(repository.search('bar'), [barElement, almostBarElement]) 22 | }) 23 | 24 | test('limits the search results to one even if found more', t => { 25 | repository.setOptions({ limit: 1 }) 26 | t.deepEqual(repository.search('bar'), [barElement]) 27 | }) 28 | 29 | test('finds a long string', t => { 30 | t.deepEqual(repository.search('lorem ipsum'), [loremElement]) 31 | }) 32 | 33 | test('finds a fuzzy string', t => { 34 | repository.setOptions({ fuzzy: true }) 35 | t.deepEqual(repository.search('lrm ism'), [loremElement]) 36 | }) 37 | 38 | test('returns empty search results when an empty criteria is provided', t => { 39 | t.deepEqual(repository.search(''), []) 40 | }) 41 | 42 | test('excludes items from search #1', t => { 43 | repository.setOptions({ 44 | exclude: ['almostbar'] 45 | }) 46 | t.deepEqual(repository.search('almostbar'), []) 47 | }) 48 | 49 | test('excludes items from search #2', t => { 50 | repository.setOptions({ 51 | sort: (a, b) => { 52 | return a.title.localeCompare(b.title) 53 | } 54 | }) 55 | t.deepEqual(repository.search('r'), [almostBarElement, barElement, loremElement]) 56 | }) 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-jekyll-search", 3 | "version": "1.10.0", 4 | "description": "Simple Jekyll site search using javascript and json", 5 | "main": "dest/simple-jekyll-search.js", 6 | "scripts": { 7 | "cypress": "cypress", 8 | "lint": "standard", 9 | "pretest": "npm run lint", 10 | "prebuild": "npm run test", 11 | "postbuild": "npm run copy-example-code", 12 | "browserify": "browserify -p browser-pack-flat/plugin src/index.js | node scripts/stamp.js > dest/simple-jekyll-search.js", 13 | "build": "npm run browserify && npm run uglify", 14 | "dist": "npm run build && npm run copy-example-code", 15 | "test": "ava", 16 | "start": "cd example; jekyll serve", 17 | "copy-example-code": "cp dest/* example/js/", 18 | "uglify": "uglifyjs --compress --mangle --ie8 --comments \"/^/*!/\" --output dest/simple-jekyll-search.min.js dest/simple-jekyll-search.js" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/christian-fei/Simple-Jekyll-Search.git" 23 | }, 24 | "author": "Christian Fei", 25 | "license": "MIT", 26 | "files": [ 27 | "dest", 28 | "src" 29 | ], 30 | "bugs": { 31 | "url": "https://github.com/christian-fei/Simple-Jekyll-Search/issues" 32 | }, 33 | "homepage": "https://github.com/christian-fei/Simple-Jekyll-Search", 34 | "dependencies": { 35 | "fuzzysearch": "^1.0.3" 36 | }, 37 | "devDependencies": { 38 | "ava": "^3.14.0", 39 | "browser-pack-flat": "^3.4.2", 40 | "browserify": "^17.0.0", 41 | "cypress": "^6.2.0", 42 | "standard": "^16.0.3", 43 | "uglify-js": "^3.12.3" 44 | }, 45 | "standard": { 46 | "ignore": [ 47 | "cypress/**", 48 | "example/**", 49 | "dest/**" 50 | ] 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/Templater.test.js: -------------------------------------------------------------------------------- 1 | const { serial: test, beforeEach } = require('ava') 2 | 3 | let templater 4 | 5 | beforeEach(() => { 6 | templater = require('../src/Templater.js') 7 | templater.setOptions({ 8 | template: '{foo}', 9 | pattern: /\{(.*?)\}/g 10 | }) 11 | }) 12 | 13 | test('renders the template with the provided data', t => { 14 | t.deepEqual(templater.compile({ foo: 'bar' }), 'bar') 15 | 16 | templater.setOptions({ 17 | template: 'url' 18 | }) 19 | 20 | t.deepEqual(templater.compile({ url: 'http://google.com' }), 'url') 21 | }) 22 | 23 | test('renders the template with the provided data and query', t => { 24 | t.deepEqual(templater.compile({ foo: 'bar' }), 'bar') 25 | 26 | templater.setOptions({ 27 | template: 'url' 28 | }) 29 | 30 | t.deepEqual(templater.compile({ url: 'http://google.com', query: 'bar' }), 'url') 31 | }) 32 | 33 | test('replaces not found properties with the original pattern', t => { 34 | const template = '{foo}' 35 | templater.setOptions({ 36 | template 37 | }) 38 | t.deepEqual(templater.compile({ x: 'bar' }), template) 39 | }) 40 | 41 | test('allows custom patterns to be set', t => { 42 | templater.setOptions({ 43 | template: '{{foo}}', 44 | pattern: /\{\{(.*?)\}\}/g 45 | }) 46 | t.deepEqual(templater.compile({ foo: 'bar' }), 'bar') 47 | }) 48 | 49 | test('middleware gets parameter to return new replacement', t => { 50 | templater.setOptions({ 51 | template: '{foo} - {bar}', 52 | middleware (prop, value) { 53 | if (prop === 'bar') { 54 | return value.replace(/^\//, '') 55 | } 56 | } 57 | }) 58 | 59 | const compiled = templater.compile({ foo: 'foo', bar: '/leading/slash' }) 60 | 61 | t.deepEqual(compiled, 'foo - leading/slash') 62 | }) 63 | -------------------------------------------------------------------------------- /src/Repository.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | put: put, 5 | clear: clear, 6 | search: search, 7 | setOptions: setOptions 8 | } 9 | 10 | const FuzzySearchStrategy = require('./SearchStrategies/FuzzySearchStrategy') 11 | const LiteralSearchStrategy = require('./SearchStrategies/LiteralSearchStrategy') 12 | 13 | function NoSort () { 14 | return 0 15 | } 16 | 17 | const data = [] 18 | let opt = {} 19 | 20 | opt.fuzzy = false 21 | opt.limit = 10 22 | opt.searchStrategy = opt.fuzzy ? FuzzySearchStrategy : LiteralSearchStrategy 23 | opt.sort = NoSort 24 | opt.exclude = [] 25 | 26 | function put (data) { 27 | if (isObject(data)) { 28 | return addObject(data) 29 | } 30 | if (isArray(data)) { 31 | return addArray(data) 32 | } 33 | return undefined 34 | } 35 | function clear () { 36 | data.length = 0 37 | return data 38 | } 39 | 40 | function isObject (obj) { 41 | return Boolean(obj) && Object.prototype.toString.call(obj) === '[object Object]' 42 | } 43 | 44 | function isArray (obj) { 45 | return Boolean(obj) && Object.prototype.toString.call(obj) === '[object Array]' 46 | } 47 | 48 | function addObject (_data) { 49 | data.push(_data) 50 | return data 51 | } 52 | 53 | function addArray (_data) { 54 | const added = [] 55 | clear() 56 | for (let i = 0, len = _data.length; i < len; i++) { 57 | if (isObject(_data[i])) { 58 | added.push(addObject(_data[i])) 59 | } 60 | } 61 | return added 62 | } 63 | 64 | function search (crit) { 65 | if (!crit) { 66 | return [] 67 | } 68 | return findMatches(data, crit, opt.searchStrategy, opt).sort(opt.sort) 69 | } 70 | 71 | function setOptions (_opt) { 72 | opt = _opt || {} 73 | 74 | opt.fuzzy = _opt.fuzzy || false 75 | opt.limit = _opt.limit || 10 76 | opt.searchStrategy = _opt.fuzzy ? FuzzySearchStrategy : LiteralSearchStrategy 77 | opt.sort = _opt.sort || NoSort 78 | opt.exclude = _opt.exclude || [] 79 | } 80 | 81 | function findMatches (data, crit, strategy, opt) { 82 | const matches = [] 83 | for (let i = 0; i < data.length && matches.length < opt.limit; i++) { 84 | const match = findMatchesInObject(data[i], crit, strategy, opt) 85 | if (match) { 86 | matches.push(match) 87 | } 88 | } 89 | return matches 90 | } 91 | 92 | function findMatchesInObject (obj, crit, strategy, opt) { 93 | for (const key in obj) { 94 | if (!isExcluded(obj[key], opt.exclude) && strategy.matches(obj[key], crit)) { 95 | return obj 96 | } 97 | } 98 | } 99 | 100 | function isExcluded (term, excludedTerms) { 101 | for (let i = 0, len = excludedTerms.length; i < len; i++) { 102 | const excludedTerm = excludedTerms[i] 103 | if (new RegExp(excludedTerm).test(term)) { 104 | return true 105 | } 106 | } 107 | return false 108 | } 109 | -------------------------------------------------------------------------------- /example/_includes/footer.html: -------------------------------------------------------------------------------- 1 | 56 | -------------------------------------------------------------------------------- /example/_sass/_syntax-highlighting.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Syntax highlighting styles 3 | */ 4 | .highlight { 5 | background: #fff; 6 | @extend %vertical-rhythm; 7 | 8 | .c { color: #998; font-style: italic } // Comment 9 | .err { color: #a61717; background-color: #e3d2d2 } // Error 10 | .k { font-weight: bold } // Keyword 11 | .o { font-weight: bold } // Operator 12 | .cm { color: #998; font-style: italic } // Comment.Multiline 13 | .cp { color: #999; font-weight: bold } // Comment.Preproc 14 | .c1 { color: #998; font-style: italic } // Comment.Single 15 | .cs { color: #999; font-weight: bold; font-style: italic } // Comment.Special 16 | .gd { color: #000; background-color: #fdd } // Generic.Deleted 17 | .gd .x { color: #000; background-color: #faa } // Generic.Deleted.Specific 18 | .ge { font-style: italic } // Generic.Emph 19 | .gr { color: #a00 } // Generic.Error 20 | .gh { color: #999 } // Generic.Heading 21 | .gi { color: #000; background-color: #dfd } // Generic.Inserted 22 | .gi .x { color: #000; background-color: #afa } // Generic.Inserted.Specific 23 | .go { color: #888 } // Generic.Output 24 | .gp { color: #555 } // Generic.Prompt 25 | .gs { font-weight: bold } // Generic.Strong 26 | .gu { color: #aaa } // Generic.Subheading 27 | .gt { color: #a00 } // Generic.Traceback 28 | .kc { font-weight: bold } // Keyword.Constant 29 | .kd { font-weight: bold } // Keyword.Declaration 30 | .kp { font-weight: bold } // Keyword.Pseudo 31 | .kr { font-weight: bold } // Keyword.Reserved 32 | .kt { color: #458; font-weight: bold } // Keyword.Type 33 | .m { color: #099 } // Literal.Number 34 | .s { color: #d14 } // Literal.String 35 | .na { color: #008080 } // Name.Attribute 36 | .nb { color: #0086B3 } // Name.Builtin 37 | .nc { color: #458; font-weight: bold } // Name.Class 38 | .no { color: #008080 } // Name.Constant 39 | .ni { color: #800080 } // Name.Entity 40 | .ne { color: #900; font-weight: bold } // Name.Exception 41 | .nf { color: #900; font-weight: bold } // Name.Function 42 | .nn { color: #555 } // Name.Namespace 43 | .nt { color: #000080 } // Name.Tag 44 | .nv { color: #008080 } // Name.Variable 45 | .ow { font-weight: bold } // Operator.Word 46 | .w { color: #bbb } // Text.Whitespace 47 | .mf { color: #099 } // Literal.Number.Float 48 | .mh { color: #099 } // Literal.Number.Hex 49 | .mi { color: #099 } // Literal.Number.Integer 50 | .mo { color: #099 } // Literal.Number.Oct 51 | .sb { color: #d14 } // Literal.String.Backtick 52 | .sc { color: #d14 } // Literal.String.Char 53 | .sd { color: #d14 } // Literal.String.Doc 54 | .s2 { color: #d14 } // Literal.String.Double 55 | .se { color: #d14 } // Literal.String.Escape 56 | .sh { color: #d14 } // Literal.String.Heredoc 57 | .si { color: #d14 } // Literal.String.Interpol 58 | .sx { color: #d14 } // Literal.String.Other 59 | .sr { color: #009926 } // Literal.String.Regex 60 | .s1 { color: #d14 } // Literal.String.Single 61 | .ss { color: #990073 } // Literal.String.Symbol 62 | .bp { color: #999 } // Name.Builtin.Pseudo 63 | .vc { color: #008080 } // Name.Variable.Class 64 | .vg { color: #008080 } // Name.Variable.Global 65 | .vi { color: #008080 } // Name.Variable.Instance 66 | .il { color: #099 } // Literal.Number.Integer.Long 67 | } 68 | -------------------------------------------------------------------------------- /example/_sass/_base.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Reset some basic elements 3 | */ 4 | body, h1, h2, h3, h4, h5, h6, 5 | p, blockquote, pre, hr, 6 | dl, dd, ol, ul, figure { 7 | margin: 0; 8 | padding: 0; 9 | } 10 | 11 | 12 | 13 | /** 14 | * Basic styling 15 | */ 16 | body { 17 | font-family: $base-font-family; 18 | font-size: $base-font-size; 19 | line-height: $base-line-height; 20 | font-weight: 300; 21 | color: $text-color; 22 | background-color: $background-color; 23 | -webkit-text-size-adjust: 100%; 24 | } 25 | 26 | 27 | 28 | /** 29 | * Set `margin-bottom` to maintain vertical rhythm 30 | */ 31 | h1, h2, h3, h4, h5, h6, 32 | p, blockquote, pre, 33 | ul, ol, dl, figure, 34 | %vertical-rhythm { 35 | margin-bottom: $spacing-unit / 2; 36 | } 37 | 38 | 39 | 40 | /** 41 | * Images 42 | */ 43 | img { 44 | max-width: 100%; 45 | vertical-align: middle; 46 | } 47 | 48 | 49 | 50 | /** 51 | * Figures 52 | */ 53 | figure > img { 54 | display: block; 55 | } 56 | 57 | figcaption { 58 | font-size: $small-font-size; 59 | } 60 | 61 | 62 | 63 | /** 64 | * Lists 65 | */ 66 | ul, ol { 67 | margin-left: $spacing-unit; 68 | } 69 | 70 | li { 71 | > ul, 72 | > ol { 73 | margin-bottom: 0; 74 | } 75 | } 76 | 77 | 78 | 79 | /** 80 | * Headings 81 | */ 82 | h1, h2, h3, h4, h5, h6 { 83 | font-weight: 300; 84 | } 85 | 86 | 87 | 88 | /** 89 | * Links 90 | */ 91 | a { 92 | color: $brand-color; 93 | text-decoration: none; 94 | 95 | &:visited { 96 | color: darken($brand-color, 15%); 97 | } 98 | 99 | &:hover { 100 | color: $text-color; 101 | text-decoration: underline; 102 | } 103 | } 104 | 105 | 106 | 107 | /** 108 | * Blockquotes 109 | */ 110 | blockquote { 111 | color: $grey-color; 112 | border-left: 4px solid $grey-color-light; 113 | padding-left: $spacing-unit / 2; 114 | font-size: 18px; 115 | letter-spacing: -1px; 116 | font-style: italic; 117 | 118 | > :last-child { 119 | margin-bottom: 0; 120 | } 121 | } 122 | 123 | 124 | 125 | /** 126 | * Code formatting 127 | */ 128 | pre, 129 | code { 130 | font-size: 15px; 131 | border: 1px solid $grey-color-light; 132 | border-radius: 3px; 133 | background-color: #eef; 134 | } 135 | 136 | code { 137 | padding: 1px 5px; 138 | } 139 | 140 | pre { 141 | padding: 8px 12px; 142 | overflow-x: scroll; 143 | 144 | > code { 145 | border: 0; 146 | padding-right: 0; 147 | padding-left: 0; 148 | } 149 | } 150 | 151 | 152 | 153 | /** 154 | * Wrapper 155 | */ 156 | .wrapper { 157 | max-width: -webkit-calc(800px - (#{$spacing-unit} * 2)); 158 | max-width: calc(800px - (#{$spacing-unit} * 2)); 159 | margin-right: auto; 160 | margin-left: auto; 161 | padding-right: $spacing-unit; 162 | padding-left: $spacing-unit; 163 | @extend %clearfix; 164 | 165 | @include media-query($on-laptop) { 166 | max-width: -webkit-calc(800px - (#{$spacing-unit})); 167 | max-width: calc(800px - (#{$spacing-unit})); 168 | padding-right: $spacing-unit / 2; 169 | padding-left: $spacing-unit / 2; 170 | } 171 | } 172 | 173 | 174 | 175 | /** 176 | * Clearfix 177 | */ 178 | %clearfix { 179 | 180 | &:after { 181 | content: ""; 182 | display: table; 183 | clear: both; 184 | } 185 | } 186 | 187 | 188 | 189 | /** 190 | * Icons 191 | */ 192 | .icon { 193 | 194 | > svg { 195 | display: inline-block; 196 | width: 16px; 197 | height: 16px; 198 | vertical-align: middle; 199 | 200 | path { 201 | fill: $grey-color; 202 | } 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | (function (window) { 2 | 'use strict' 3 | 4 | let options = { 5 | searchInput: null, 6 | resultsContainer: null, 7 | json: [], 8 | success: Function.prototype, 9 | searchResultTemplate: '
  • {title}
  • ', 10 | templateMiddleware: Function.prototype, 11 | sortMiddleware: function () { 12 | return 0 13 | }, 14 | noResultsText: 'No results found', 15 | limit: 10, 16 | fuzzy: false, 17 | debounceTime: null, 18 | exclude: [], 19 | onSearch: Function.prototype 20 | } 21 | 22 | let debounceTimerHandle 23 | const debounce = function (func, delayMillis) { 24 | if (delayMillis) { 25 | clearTimeout(debounceTimerHandle) 26 | debounceTimerHandle = setTimeout(func, delayMillis) 27 | } else { 28 | func.call() 29 | } 30 | } 31 | 32 | const requiredOptions = ['searchInput', 'resultsContainer', 'json'] 33 | 34 | const templater = require('./Templater') 35 | const repository = require('./Repository') 36 | const jsonLoader = require('./JSONLoader') 37 | const optionsValidator = require('./OptionsValidator')({ 38 | required: requiredOptions 39 | }) 40 | const utils = require('./utils') 41 | 42 | window.SimpleJekyllSearch = function (_options) { 43 | const errors = optionsValidator.validate(_options) 44 | if (errors.length > 0) { 45 | throwError('You must specify the following required options: ' + requiredOptions) 46 | } 47 | 48 | options = utils.merge(options, _options) 49 | 50 | templater.setOptions({ 51 | template: options.searchResultTemplate, 52 | middleware: options.templateMiddleware 53 | }) 54 | 55 | repository.setOptions({ 56 | fuzzy: options.fuzzy, 57 | limit: options.limit, 58 | sort: options.sortMiddleware, 59 | exclude: options.exclude 60 | }) 61 | 62 | if (utils.isJSON(options.json)) { 63 | initWithJSON(options.json) 64 | } else { 65 | initWithURL(options.json) 66 | } 67 | 68 | const rv = { 69 | search: search 70 | } 71 | 72 | typeof options.success === 'function' && options.success.call(rv) 73 | return rv 74 | } 75 | 76 | function initWithJSON (json) { 77 | repository.put(json) 78 | registerInput() 79 | } 80 | 81 | function initWithURL (url) { 82 | jsonLoader.load(url, function (err, json) { 83 | if (err) { 84 | throwError('failed to get JSON (' + url + ')') 85 | } 86 | initWithJSON(json) 87 | }) 88 | } 89 | 90 | function emptyResultsContainer () { 91 | options.resultsContainer.innerHTML = '' 92 | } 93 | 94 | function appendToResultsContainer (text) { 95 | options.resultsContainer.innerHTML += text 96 | } 97 | 98 | function registerInput () { 99 | options.searchInput.addEventListener('input', function (e) { 100 | if (isWhitelistedKey(e.which)) { 101 | emptyResultsContainer() 102 | debounce(function () { search(e.target.value) }, options.debounceTime) 103 | } 104 | }) 105 | } 106 | 107 | function search (query) { 108 | if (isValidQuery(query)) { 109 | emptyResultsContainer() 110 | render(repository.search(query), query) 111 | 112 | typeof options.onSearch === 'function' && options.onSearch.call() 113 | } 114 | } 115 | 116 | function render (results, query) { 117 | const len = results.length 118 | if (len === 0) { 119 | return appendToResultsContainer(options.noResultsText) 120 | } 121 | for (let i = 0; i < len; i++) { 122 | results[i].query = query 123 | appendToResultsContainer(templater.compile(results[i])) 124 | } 125 | } 126 | 127 | function isValidQuery (query) { 128 | return query && query.length > 0 129 | } 130 | 131 | function isWhitelistedKey (key) { 132 | return [13, 16, 20, 37, 38, 39, 40, 91].indexOf(key) === -1 133 | } 134 | 135 | function throwError (message) { 136 | throw new Error('SimpleJekyllSearch --- ' + message) 137 | } 138 | })(window) 139 | -------------------------------------------------------------------------------- /dest/simple-jekyll-search.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Simple-Jekyll-Search 3 | * Copyright 2015-2020, Christian Fei 4 | * Licensed under the MIT License. 5 | */ 6 | !function(){"use strict";var f={compile:function(r){return i.template.replace(i.pattern,function(t,e){var n=i.middleware(e,r[e],i.template);return void 0!==n?n:r[e]||t})},setOptions:function(t){i.pattern=t.pattern||i.pattern,i.template=t.template||i.template,"function"==typeof t.middleware&&(i.middleware=t.middleware)}};const i={pattern:/\{(.*?)\}/g,template:"",middleware:function(){}};var n=function(t,e){var n=e.length,r=t.length;if(n{title}',templateMiddleware:Function.prototype,sortMiddleware:function(){return 0},noResultsText:"No results found",limit:10,fuzzy:!1,debounceTime:null,exclude:[]},n;const e=function(t,e){e?(clearTimeout(n),n=setTimeout(t,e)):t.call()};var r=["searchInput","resultsContainer","json"];const o=m({required:r});function u(t){d.put(t),i.searchInput.addEventListener("input",function(t){-1===[13,16,20,37,38,39,40,91].indexOf(t.which)&&(c(),e(function(){l(t.target.value)},i.debounceTime))})}function c(){i.resultsContainer.innerHTML=""}function s(t){i.resultsContainer.innerHTML+=t}function l(t){var e;(e=t)&&0{title}',templateMiddleware:Function.prototype,sortMiddleware:function(){return 0},noResultsText:"No results found",limit:10,fuzzy:!1,debounceTime:null,exclude:[]},n;const e=function(t,e){e?(clearTimeout(n),n=setTimeout(t,e)):t.call()};var r=["searchInput","resultsContainer","json"];const o=m({required:r});function u(t){d.put(t),i.searchInput.addEventListener("input",function(t){-1===[13,16,20,37,38,39,40,91].indexOf(t.which)&&(c(),e(function(){l(t.target.value)},i.debounceTime))})}function c(){i.resultsContainer.innerHTML=""}function s(t){i.resultsContainer.innerHTML+=t}function l(t){var e;(e=t)&&0 svg { 63 | width: 18px; 64 | height: 15px; 65 | 66 | path { 67 | fill: $grey-color-dark; 68 | } 69 | } 70 | } 71 | 72 | .trigger { 73 | clear: both; 74 | display: none; 75 | } 76 | 77 | &:hover .trigger { 78 | display: block; 79 | padding-bottom: 5px; 80 | } 81 | 82 | .page-link { 83 | display: block; 84 | padding: 5px 10px; 85 | } 86 | } 87 | } 88 | 89 | 90 | 91 | /** 92 | * Site footer 93 | */ 94 | .site-footer { 95 | border-top: 1px solid $grey-color-light; 96 | padding: $spacing-unit 0; 97 | } 98 | 99 | .footer-heading { 100 | font-size: 18px; 101 | margin-bottom: $spacing-unit / 2; 102 | } 103 | 104 | .contact-list, 105 | .social-media-list { 106 | list-style: none; 107 | margin-left: 0; 108 | } 109 | 110 | .footer-col-wrapper { 111 | font-size: 15px; 112 | color: $grey-color; 113 | margin-left: -$spacing-unit / 2; 114 | @extend %clearfix; 115 | } 116 | 117 | .footer-col { 118 | float: left; 119 | margin-bottom: $spacing-unit / 2; 120 | padding-left: $spacing-unit / 2; 121 | } 122 | 123 | .footer-col-1 { 124 | width: -webkit-calc(35% - (#{$spacing-unit} / 2)); 125 | width: calc(35% - (#{$spacing-unit} / 2)); 126 | } 127 | 128 | .footer-col-2 { 129 | width: -webkit-calc(20% - (#{$spacing-unit} / 2)); 130 | width: calc(20% - (#{$spacing-unit} / 2)); 131 | } 132 | 133 | .footer-col-3 { 134 | width: -webkit-calc(45% - (#{$spacing-unit} / 2)); 135 | width: calc(45% - (#{$spacing-unit} / 2)); 136 | } 137 | 138 | @include media-query($on-laptop) { 139 | .footer-col-1, 140 | .footer-col-2 { 141 | width: -webkit-calc(50% - (#{$spacing-unit} / 2)); 142 | width: calc(50% - (#{$spacing-unit} / 2)); 143 | } 144 | 145 | .footer-col-3 { 146 | width: -webkit-calc(100% - (#{$spacing-unit} / 2)); 147 | width: calc(100% - (#{$spacing-unit} / 2)); 148 | } 149 | } 150 | 151 | @include media-query($on-palm) { 152 | .footer-col { 153 | float: none; 154 | width: -webkit-calc(100% - (#{$spacing-unit} / 2)); 155 | width: calc(100% - (#{$spacing-unit} / 2)); 156 | } 157 | } 158 | 159 | 160 | 161 | /** 162 | * Page content 163 | */ 164 | .page-content { 165 | padding: $spacing-unit 0; 166 | } 167 | 168 | .page-heading { 169 | font-size: 20px; 170 | } 171 | 172 | .post-list { 173 | margin-left: 0; 174 | list-style: none; 175 | 176 | > li { 177 | margin-bottom: $spacing-unit; 178 | } 179 | } 180 | 181 | .post-meta { 182 | font-size: $small-font-size; 183 | color: $grey-color; 184 | } 185 | 186 | .post-link { 187 | display: block; 188 | font-size: 24px; 189 | } 190 | 191 | 192 | 193 | /** 194 | * Posts 195 | */ 196 | .post-header { 197 | margin-bottom: $spacing-unit; 198 | } 199 | 200 | .post-title { 201 | font-size: 42px; 202 | letter-spacing: -1px; 203 | line-height: 1; 204 | 205 | @include media-query($on-laptop) { 206 | font-size: 36px; 207 | } 208 | } 209 | 210 | .post-content { 211 | margin-bottom: $spacing-unit; 212 | 213 | h2 { 214 | font-size: 32px; 215 | 216 | @include media-query($on-laptop) { 217 | font-size: 28px; 218 | } 219 | } 220 | 221 | h3 { 222 | font-size: 26px; 223 | 224 | @include media-query($on-laptop) { 225 | font-size: 22px; 226 | } 227 | } 228 | 229 | h4 { 230 | font-size: 20px; 231 | 232 | @include media-query($on-laptop) { 233 | font-size: 18px; 234 | } 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /WIKI.md: -------------------------------------------------------------------------------- 1 | Welcome to the Simple-Jekyll-Search wiki! 2 | 3 | Here is a list of the available options, usage questions, troubleshootings, guides. 4 | 5 | ## Options 6 | ### searchInput (Element) [required] 7 | 8 | The input element on which the plugin should listen for keyboard event and trigger the searching and rendering for articles. 9 | 10 | 11 | ### resultsContainer (Element) [required] 12 | 13 | The container element in which the search results should be rendered in. Typically an `
      `. 14 | 15 | 16 | ### json (String|JSON) [required] 17 | 18 | You can either pass in an URL to the `search.json` file, or the results in form of JSON directly, to save one round trip to get the data. 19 | 20 | 21 | ### searchResultTemplate (String) [optional] 22 | 23 | The template of a single rendered search result. 24 | 25 | The templating syntax is very simple: You just enclose the properties you want to replace with curly braces. 26 | 27 | E.g. 28 | 29 | The template 30 | 31 | ```html 32 |
    • {title}
    • 33 | ``` 34 | 35 | will render to the following 36 | 37 | ```html 38 |
    • Welcome to Jekyll!
    • 39 | ``` 40 | 41 | If the `search.json` contains this data 42 | 43 | ```json 44 | [ 45 | { 46 | "title" : "Welcome to Jekyll!", 47 | "category" : "", 48 | "tags" : "", 49 | "url" : "/jekyll/update/2014/11/01/welcome-to-jekyll.html", 50 | "date" : "2014-11-01 21:07:22 +0100" 51 | } 52 | ] 53 | ``` 54 | 55 | 56 | ### templateMiddleware (Function) [optional] 57 | 58 | A function that will be called whenever a match in the template is found. 59 | 60 | It gets passed the current property name, property value, and the template. 61 | 62 | If the function returns a non-undefined value, it gets replaced in the template. 63 | 64 | This can be potentially useful for manipulating URLs etc. 65 | 66 | Example: 67 | 68 | ```js 69 | SimpleJekyllSearch({ 70 | ... 71 | templateMiddleware: function(prop, value, template) { 72 | if (prop === 'bar') { 73 | return value.replace(/^\//, '') 74 | } 75 | } 76 | ... 77 | }) 78 | ``` 79 | 80 | See the [tests](tests/Templater.test.js) for an in-depth code example 81 | 82 | ### sortMiddleware (Function) [optional] 83 | 84 | A function that will be used to sort the filtered results. 85 | 86 | It can be used for example to group the sections together. 87 | 88 | Example: 89 | 90 | ```js 91 | SimpleJekyllSearch({ 92 | ... 93 | sortMiddleware: function(a, b) { 94 | var astr = String(a.section) + "-" + String(a.caption); 95 | var bstr = String(b.section) + "-" + String(b.caption); 96 | return astr.localeCompare(bstr) 97 | } 98 | ... 99 | }) 100 | ``` 101 | 102 | ### noResultsText (String) [optional] 103 | 104 | The HTML that will be shown if the query didn't match anything. 105 | 106 | 107 | ### limit (Number) [optional] 108 | 109 | You can limit the number of posts rendered on the page. 110 | 111 | 112 | ### fuzzy (Boolean) [optional] 113 | 114 | Enable fuzzy search to allow less restrictive matching. 115 | 116 | ### exclude (Array) [optional] 117 | 118 | Pass in a list of terms you want to exclude (terms will be matched against a regex, so urls, words are allowed). 119 | 120 | ### debounceTime (Number) [optional] 121 | 122 | Limit how many times the search function can be executed over the given time window. This is especially useful to improve the user experience when searching 123 | over a large dataset (either with rare terms ou because the number of posts to display is large). If no `debounceTime` is provided a search will be triggered 124 | on each keystroke. 125 | 126 | 127 | --- 128 | 129 | ## If search isn't working due to invalid JSON 130 | 131 | - There is a filter plugin in the _plugins folder which should remove most characters that cause invalid JSON. To use it, add the simple_search_filter.rb file to your _plugins folder, and use `remove_chars` as a filter. 132 | 133 | For example: in search.json, replace 134 | ```json 135 | "content" : "{{ page.content | strip_html | strip_newlines }}" 136 | ``` 137 | with 138 | ```json 139 | "content" : "{{ page.content | strip_html | strip_newlines | remove_chars | escape }}" 140 | ``` 141 | 142 | If this doesn't work when using Github pages you can try ```jsonify``` to make sure the content is json compatible: 143 | ```js 144 | "content" : {{ page.content | jsonify }} 145 | ``` 146 | **Note: you don't need to use quotes ' " ' in this since ```jsonify``` automatically inserts them.** 147 | 148 | 149 | ## Enabling full-text search 150 | 151 | Replace 'search.json' with the following code: 152 | 153 | ```yaml 154 | --- 155 | layout: null 156 | --- 157 | [ 158 | {% for post in site.posts %} 159 | { 160 | "title" : "{{ post.title | escape }}", 161 | "category" : "{{ post.category }}", 162 | "tags" : "{{ post.tags | join: ', ' }}", 163 | "url" : "{{ site.baseurl }}{{ post.url }}", 164 | "date" : "{{ post.date }}", 165 | "content" : "{{ post.content | strip_html | strip_newlines }}" 166 | } {% unless forloop.last %},{% endunless %} 167 | {% endfor %} 168 | , 169 | {% for page in site.pages %} 170 | { 171 | {% if page.title != nil %} 172 | "title" : "{{ page.title | escape }}", 173 | "category" : "{{ page.category }}", 174 | "tags" : "{{ page.tags | join: ', ' }}", 175 | "url" : "{{ site.baseurl }}{{ page.url }}", 176 | "date" : "{{ page.date }}", 177 | "content" : "{{ page.content | strip_html | strip_newlines }}" 178 | {% endif %} 179 | } {% unless forloop.last %},{% endunless %} 180 | {% endfor %} 181 | ] 182 | ``` 183 | -------------------------------------------------------------------------------- /cypress/fixtures/users.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 1, 4 | "name": "Leanne Graham", 5 | "username": "Bret", 6 | "email": "Sincere@april.biz", 7 | "address": { 8 | "street": "Kulas Light", 9 | "suite": "Apt. 556", 10 | "city": "Gwenborough", 11 | "zipcode": "92998-3874", 12 | "geo": { 13 | "lat": "-37.3159", 14 | "lng": "81.1496" 15 | } 16 | }, 17 | "phone": "1-770-736-8031 x56442", 18 | "website": "hildegard.org", 19 | "company": { 20 | "name": "Romaguera-Crona", 21 | "catchPhrase": "Multi-layered client-server neural-net", 22 | "bs": "harness real-time e-markets" 23 | } 24 | }, 25 | { 26 | "id": 2, 27 | "name": "Ervin Howell", 28 | "username": "Antonette", 29 | "email": "Shanna@melissa.tv", 30 | "address": { 31 | "street": "Victor Plains", 32 | "suite": "Suite 879", 33 | "city": "Wisokyburgh", 34 | "zipcode": "90566-7771", 35 | "geo": { 36 | "lat": "-43.9509", 37 | "lng": "-34.4618" 38 | } 39 | }, 40 | "phone": "010-692-6593 x09125", 41 | "website": "anastasia.net", 42 | "company": { 43 | "name": "Deckow-Crist", 44 | "catchPhrase": "Proactive didactic contingency", 45 | "bs": "synergize scalable supply-chains" 46 | } 47 | }, 48 | { 49 | "id": 3, 50 | "name": "Clementine Bauch", 51 | "username": "Samantha", 52 | "email": "Nathan@yesenia.net", 53 | "address": { 54 | "street": "Douglas Extension", 55 | "suite": "Suite 847", 56 | "city": "McKenziehaven", 57 | "zipcode": "59590-4157", 58 | "geo": { 59 | "lat": "-68.6102", 60 | "lng": "-47.0653" 61 | } 62 | }, 63 | "phone": "1-463-123-4447", 64 | "website": "ramiro.info", 65 | "company": { 66 | "name": "Romaguera-Jacobson", 67 | "catchPhrase": "Face to face bifurcated interface", 68 | "bs": "e-enable strategic applications" 69 | } 70 | }, 71 | { 72 | "id": 4, 73 | "name": "Patricia Lebsack", 74 | "username": "Karianne", 75 | "email": "Julianne.OConner@kory.org", 76 | "address": { 77 | "street": "Hoeger Mall", 78 | "suite": "Apt. 692", 79 | "city": "South Elvis", 80 | "zipcode": "53919-4257", 81 | "geo": { 82 | "lat": "29.4572", 83 | "lng": "-164.2990" 84 | } 85 | }, 86 | "phone": "493-170-9623 x156", 87 | "website": "kale.biz", 88 | "company": { 89 | "name": "Robel-Corkery", 90 | "catchPhrase": "Multi-tiered zero tolerance productivity", 91 | "bs": "transition cutting-edge web services" 92 | } 93 | }, 94 | { 95 | "id": 5, 96 | "name": "Chelsey Dietrich", 97 | "username": "Kamren", 98 | "email": "Lucio_Hettinger@annie.ca", 99 | "address": { 100 | "street": "Skiles Walks", 101 | "suite": "Suite 351", 102 | "city": "Roscoeview", 103 | "zipcode": "33263", 104 | "geo": { 105 | "lat": "-31.8129", 106 | "lng": "62.5342" 107 | } 108 | }, 109 | "phone": "(254)954-1289", 110 | "website": "demarco.info", 111 | "company": { 112 | "name": "Keebler LLC", 113 | "catchPhrase": "User-centric fault-tolerant solution", 114 | "bs": "revolutionize end-to-end systems" 115 | } 116 | }, 117 | { 118 | "id": 6, 119 | "name": "Mrs. Dennis Schulist", 120 | "username": "Leopoldo_Corkery", 121 | "email": "Karley_Dach@jasper.info", 122 | "address": { 123 | "street": "Norberto Crossing", 124 | "suite": "Apt. 950", 125 | "city": "South Christy", 126 | "zipcode": "23505-1337", 127 | "geo": { 128 | "lat": "-71.4197", 129 | "lng": "71.7478" 130 | } 131 | }, 132 | "phone": "1-477-935-8478 x6430", 133 | "website": "ola.org", 134 | "company": { 135 | "name": "Considine-Lockman", 136 | "catchPhrase": "Synchronised bottom-line interface", 137 | "bs": "e-enable innovative applications" 138 | } 139 | }, 140 | { 141 | "id": 7, 142 | "name": "Kurtis Weissnat", 143 | "username": "Elwyn.Skiles", 144 | "email": "Telly.Hoeger@billy.biz", 145 | "address": { 146 | "street": "Rex Trail", 147 | "suite": "Suite 280", 148 | "city": "Howemouth", 149 | "zipcode": "58804-1099", 150 | "geo": { 151 | "lat": "24.8918", 152 | "lng": "21.8984" 153 | } 154 | }, 155 | "phone": "210.067.6132", 156 | "website": "elvis.io", 157 | "company": { 158 | "name": "Johns Group", 159 | "catchPhrase": "Configurable multimedia task-force", 160 | "bs": "generate enterprise e-tailers" 161 | } 162 | }, 163 | { 164 | "id": 8, 165 | "name": "Nicholas Runolfsdottir V", 166 | "username": "Maxime_Nienow", 167 | "email": "Sherwood@rosamond.me", 168 | "address": { 169 | "street": "Ellsworth Summit", 170 | "suite": "Suite 729", 171 | "city": "Aliyaview", 172 | "zipcode": "45169", 173 | "geo": { 174 | "lat": "-14.3990", 175 | "lng": "-120.7677" 176 | } 177 | }, 178 | "phone": "586.493.6943 x140", 179 | "website": "jacynthe.com", 180 | "company": { 181 | "name": "Abernathy Group", 182 | "catchPhrase": "Implemented secondary concept", 183 | "bs": "e-enable extensible e-tailers" 184 | } 185 | }, 186 | { 187 | "id": 9, 188 | "name": "Glenna Reichert", 189 | "username": "Delphine", 190 | "email": "Chaim_McDermott@dana.io", 191 | "address": { 192 | "street": "Dayna Park", 193 | "suite": "Suite 449", 194 | "city": "Bartholomebury", 195 | "zipcode": "76495-3109", 196 | "geo": { 197 | "lat": "24.6463", 198 | "lng": "-168.8889" 199 | } 200 | }, 201 | "phone": "(775)976-6794 x41206", 202 | "website": "conrad.com", 203 | "company": { 204 | "name": "Yost and Sons", 205 | "catchPhrase": "Switchable contextually-based project", 206 | "bs": "aggregate real-time technologies" 207 | } 208 | }, 209 | { 210 | "id": 10, 211 | "name": "Clementina DuBuque", 212 | "username": "Moriah.Stanton", 213 | "email": "Rey.Padberg@karina.biz", 214 | "address": { 215 | "street": "Kattie Turnpike", 216 | "suite": "Suite 198", 217 | "city": "Lebsackbury", 218 | "zipcode": "31428-2261", 219 | "geo": { 220 | "lat": "-38.2386", 221 | "lng": "57.2232" 222 | } 223 | }, 224 | "phone": "024-648-3804", 225 | "website": "ambrose.net", 226 | "company": { 227 | "name": "Hoeger LLC", 228 | "catchPhrase": "Centralized empowering task-force", 229 | "bs": "target end-to-end models" 230 | } 231 | } 232 | ] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Simple-Jekyll-Search](https://www.npmjs.com/package/simple-jekyll-search) 2 | 3 | [![Build Status](https://img.shields.io/travis/christian-fei/Simple-Jekyll-Search/master.svg?)](https://travis-ci.org/christian-fei/Simple-Jekyll-Search) 4 | [![dependencies Status](https://img.shields.io/david/christian-fei/Simple-Jekyll-Search.svg)](https://david-dm.org/christian-fei/Simple-Jekyll-Search) 5 | [![devDependencies Status](https://img.shields.io/david/dev/christian-fei/Simple-Jekyll-Search.svg)](https://david-dm.org/christian-fei/Simple-Jekyll-Search?type=dev) 6 | 7 | A JavaScript library to add search functionality to any Jekyll blog. 8 | 9 | ## Use case 10 | 11 | You have a blog, built with Jekyll, and want a **lightweight search functionality** on your blog, purely client-side? 12 | 13 | *No server configurations or databases to maintain*. 14 | 15 | Just **5 minutes** to have a **fully working searchable blog**. 16 | 17 | --- 18 | 19 | ## Installation 20 | 21 | ### npm 22 | 23 | ```sh 24 | npm install simple-jekyll-search 25 | ``` 26 | 27 | ## Getting started 28 | 29 | ### Create `search.json` 30 | 31 | Place the following code in a file called `search.json` in the **root** of your Jekyll blog. (You can also get a copy [from here](/example/search.json)) 32 | 33 | This file will be used as a small data source to perform the searches on the client side: 34 | 35 | ```yaml 36 | --- 37 | layout: none 38 | --- 39 | [ 40 | {% for post in site.posts %} 41 | { 42 | "title" : "{{ post.title | escape }}", 43 | "category" : "{{ post.category }}", 44 | "tags" : "{{ post.tags | join: ', ' }}", 45 | "url" : "{{ site.baseurl }}{{ post.url }}", 46 | "date" : "{{ post.date }}" 47 | } {% unless forloop.last %},{% endunless %} 48 | {% endfor %} 49 | ] 50 | ``` 51 | 52 | 53 | ## Preparing the plugin 54 | 55 | ### Add DOM elements 56 | 57 | SimpleJekyllSearch needs two `DOM` elements to work: 58 | 59 | - a search input field 60 | - a result container to display the results 61 | 62 | #### Give me the code 63 | 64 | Here is the code you can use with the default configuration: 65 | 66 | You need to place the following code within the layout where you want the search to appear. (See the configuration section below to customize it) 67 | 68 | For example in **_layouts/default.html**: 69 | 70 | ```html 71 | 72 | 73 |
        74 | 75 | 76 | 77 | ``` 78 | 79 | 80 | ## Usage 81 | 82 | Customize SimpleJekyllSearch by passing in your configuration options: 83 | 84 | ```js 85 | var sjs = SimpleJekyllSearch({ 86 | searchInput: document.getElementById('search-input'), 87 | resultsContainer: document.getElementById('results-container'), 88 | json: '/search.json' 89 | }) 90 | ``` 91 | 92 | ### returns { search } 93 | 94 | A new instance of SimpleJekyllSearch returns an object, with the only property `search`. 95 | 96 | `search` is a function used to simulate a user input and display the matching results.  97 | 98 | E.g.: 99 | 100 | ```js 101 | var sjs = SimpleJekyllSearch({ ...options }) 102 | sjs.search('Hello') 103 | ``` 104 | 105 | 💡 it can be used to filter posts by tags or categories! 106 | 107 | ## Options 108 | 109 | Here is a list of the available options, usage questions, troubleshooting & guides. 110 | 111 | ### searchInput (Element) [required] 112 | 113 | The input element on which the plugin should listen for keyboard event and trigger the searching and rendering for articles. 114 | 115 | 116 | ### resultsContainer (Element) [required] 117 | 118 | The container element in which the search results should be rendered in. Typically a `
          `. 119 | 120 | 121 | ### json (String|JSON) [required] 122 | 123 | You can either pass in an URL to the `search.json` file, or the results in form of JSON directly, to save one round trip to get the data. 124 | 125 | 126 | ### searchResultTemplate (String) [optional] 127 | 128 | The template of a single rendered search result. 129 | 130 | The templating syntax is very simple: You just enclose the properties you want to replace with curly braces. 131 | 132 | E.g. 133 | 134 | The template 135 | 136 | ```js 137 | var sjs = SimpleJekyllSearch({ 138 | searchInput: document.getElementById('search-input'), 139 | resultsContainer: document.getElementById('results-container'), 140 | json: '/search.json', 141 | searchResultTemplate: '
        • {title}
        • ' 142 | }) 143 | ``` 144 | 145 | will render to the following 146 | 147 | ```html 148 |
        • Welcome to Jekyll!
        • 149 | ``` 150 | 151 | If the `search.json` contains this data 152 | 153 | ```json 154 | [ 155 | { 156 | "title" : "Welcome to Jekyll!", 157 | "category" : "", 158 | "tags" : "", 159 | "url" : "/jekyll/update/2014/11/01/welcome-to-jekyll.html", 160 | "date" : "2014-11-01 21:07:22 +0100" 161 | } 162 | ] 163 | ``` 164 | 165 | 166 | ### templateMiddleware (Function) [optional] 167 | 168 | A function that will be called whenever a match in the template is found. 169 | 170 | It gets passed the current property name, property value, and the template. 171 | 172 | If the function returns a non-undefined value, it gets replaced in the template. 173 | 174 | This can be potentially useful for manipulating URLs etc. 175 | 176 | Example: 177 | 178 | ```js 179 | SimpleJekyllSearch({ 180 | ... 181 | templateMiddleware: function(prop, value, template) { 182 | if (prop === 'bar') { 183 | return value.replace(/^\//, '') 184 | } 185 | } 186 | ... 187 | }) 188 | ``` 189 | 190 | See the [tests](https://github.com/christian-fei/Simple-Jekyll-Search/blob/master/tests/Templater.test.js) for an in-depth code example 191 | 192 | ### sortMiddleware (Function) [optional] 193 | 194 | A function that will be used to sort the filtered results. 195 | 196 | It can be used for example to group the sections together. 197 | 198 | Example: 199 | 200 | ```js 201 | SimpleJekyllSearch({ 202 | ... 203 | sortMiddleware: function(a, b) { 204 | var astr = String(a.section) + "-" + String(a.caption); 205 | var bstr = String(b.section) + "-" + String(b.caption); 206 | return astr.localeCompare(bstr) 207 | } 208 | ... 209 | }) 210 | ``` 211 | 212 | ### noResultsText (String) [optional] 213 | 214 | The HTML that will be shown if the query didn't match anything. 215 | 216 | 217 | ### limit (Number) [optional] 218 | 219 | You can limit the number of posts rendered on the page. 220 | 221 | 222 | ### fuzzy (Boolean) [optional] 223 | 224 | Enable fuzzy search to allow less restrictive matching. 225 | 226 | ### exclude (Array) [optional] 227 | 228 | Pass in a list of terms you want to exclude (terms will be matched against a regex, so URLs, words are allowed). 229 | 230 | ### success (Function) [optional] 231 | 232 | A function called once the data has been loaded. 233 | 234 | ### debounceTime (Number) [optional] 235 | 236 | Limit how many times the search function can be executed over the given time window. This is especially useful to improve the user experience when searching over a large dataset (either with rare terms or because the number of posts to display is large). If no `debounceTime` (milliseconds) is provided a search will be triggered on each keystroke. 237 | 238 | --- 239 | 240 | ## If search isn't working due to invalid JSON 241 | 242 | - There is a filter plugin in the _plugins folder which should remove most characters that cause invalid JSON. To use it, add the simple_search_filter.rb file to your _plugins folder, and use `remove_chars` as a filter. 243 | 244 | For example: in search.json, replace 245 | 246 | ```json 247 | "content": "{{ page.content | strip_html | strip_newlines }}" 248 | ``` 249 | 250 | with 251 | 252 | ```json 253 | "content": "{{ page.content | strip_html | strip_newlines | remove_chars | escape }}" 254 | ``` 255 | 256 | If this doesn't work when using Github pages you can try `jsonify` to make sure the content is json compatible: 257 | 258 | ```js 259 | "content": {{ page.content | jsonify }} 260 | ``` 261 | 262 | **Note: you don't need to use quotes `"` in this since `jsonify` automatically inserts them.** 263 | 264 | 265 | ## Enabling full-text search 266 | 267 | Replace `search.json` with the following code: 268 | 269 | ```yaml 270 | --- 271 | layout: none 272 | --- 273 | [ 274 | {% for post in site.posts %} 275 | { 276 | "title" : "{{ post.title | escape }}", 277 | "category" : "{{ post.category }}", 278 | "tags" : "{{ post.tags | join: ', ' }}", 279 | "url" : "{{ site.baseurl }}{{ post.url }}", 280 | "date" : "{{ post.date }}", 281 | "content" : "{{ post.content | strip_html | strip_newlines }}" 282 | } {% unless forloop.last %},{% endunless %} 283 | {% endfor %} 284 | , 285 | {% for page in site.pages %} 286 | { 287 | {% if page.title != nil %} 288 | "title" : "{{ page.title | escape }}", 289 | "category" : "{{ page.category }}", 290 | "tags" : "{{ page.tags | join: ', ' }}", 291 | "url" : "{{ site.baseurl }}{{ page.url }}", 292 | "date" : "{{ page.date }}", 293 | "content" : "{{ page.content | strip_html | strip_newlines }}" 294 | {% endif %} 295 | } {% unless forloop.last %},{% endunless %} 296 | {% endfor %} 297 | ] 298 | ``` 299 | 300 | 301 | 302 | ## Development 303 | 304 | - `npm install` 305 | - `npm test` 306 | 307 | #### Acceptance tests 308 | 309 | ```bash 310 | cd example; jekyll serve 311 | 312 | # in another tab 313 | 314 | npm run cypress -- run 315 | ``` 316 | 317 | ## Contributors 318 | 319 | Thanks to all [contributors](https://github.com/christian-fei/Simple-Jekyll-Search/graphs/contributors) over the years! You are the best :) 320 | 321 | > [@daviddarnes](https://github.com/daviddarnes) 322 | [@XhmikosR](https://github.com/XhmikosR) 323 | [@PeterDaveHello](https://github.com/PeterDaveHello) 324 | [@mikeybeck](https://github.com/mikeybeck) 325 | [@egladman](https://github.com/egladman) 326 | [@midzer](https://github.com/midzer) 327 | [@eduardoboucas](https://github.com/eduardoboucas) 328 | [@kremalicious](https://github.com/kremalicious) 329 | [@tibotiber](https://github.com/tibotiber) 330 | and many others! 331 | 332 | ## Stargazers over time 333 | 334 | [![Stargazers over time](https://starchart.cc/christian-fei/Simple-Jekyll-Search.svg)](https://starchart.cc/christian-fei/Simple-Jekyll-Search) 335 | -------------------------------------------------------------------------------- /dest/simple-jekyll-search.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Simple-Jekyll-Search 3 | * Copyright 2015-2020, Christian Fei 4 | * Licensed under the MIT License. 5 | */ 6 | 7 | (function(){ 8 | 'use strict' 9 | 10 | var _$Templater_7 = { 11 | compile: compile, 12 | setOptions: setOptions 13 | } 14 | 15 | const options = {} 16 | options.pattern = /\{(.*?)\}/g 17 | options.template = '' 18 | options.middleware = function () {} 19 | 20 | function setOptions (_options) { 21 | options.pattern = _options.pattern || options.pattern 22 | options.template = _options.template || options.template 23 | if (typeof _options.middleware === 'function') { 24 | options.middleware = _options.middleware 25 | } 26 | } 27 | 28 | function compile (data) { 29 | return options.template.replace(options.pattern, function (match, prop) { 30 | const value = options.middleware(prop, data[prop], options.template) 31 | if (typeof value !== 'undefined') { 32 | return value 33 | } 34 | return data[prop] || match 35 | }) 36 | } 37 | 38 | 'use strict'; 39 | 40 | function fuzzysearch (needle, haystack) { 41 | var tlen = haystack.length; 42 | var qlen = needle.length; 43 | if (qlen > tlen) { 44 | return false; 45 | } 46 | if (qlen === tlen) { 47 | return needle === haystack; 48 | } 49 | outer: for (var i = 0, j = 0; i < qlen; i++) { 50 | var nch = needle.charCodeAt(i); 51 | while (j < tlen) { 52 | if (haystack.charCodeAt(j++) === nch) { 53 | continue outer; 54 | } 55 | } 56 | return false; 57 | } 58 | return true; 59 | } 60 | 61 | var _$fuzzysearch_1 = fuzzysearch; 62 | 63 | 'use strict' 64 | 65 | /* removed: const _$fuzzysearch_1 = require('fuzzysearch') */; 66 | 67 | var _$FuzzySearchStrategy_5 = new FuzzySearchStrategy() 68 | 69 | function FuzzySearchStrategy () { 70 | this.matches = function (string, crit) { 71 | return _$fuzzysearch_1(crit.toLowerCase(), string.toLowerCase()) 72 | } 73 | } 74 | 75 | 'use strict' 76 | 77 | var _$LiteralSearchStrategy_6 = new LiteralSearchStrategy() 78 | 79 | function LiteralSearchStrategy () { 80 | this.matches = function (str, crit) { 81 | if (!str) return false 82 | 83 | str = str.trim().toLowerCase() 84 | crit = crit.trim().toLowerCase() 85 | 86 | return crit.split(' ').filter(function (word) { 87 | return str.indexOf(word) >= 0 88 | }).length === crit.split(' ').length 89 | } 90 | } 91 | 92 | 'use strict' 93 | 94 | var _$Repository_4 = { 95 | put: put, 96 | clear: clear, 97 | search: search, 98 | setOptions: __setOptions_4 99 | } 100 | 101 | /* removed: const _$FuzzySearchStrategy_5 = require('./SearchStrategies/FuzzySearchStrategy') */; 102 | /* removed: const _$LiteralSearchStrategy_6 = require('./SearchStrategies/LiteralSearchStrategy') */; 103 | 104 | function NoSort () { 105 | return 0 106 | } 107 | 108 | const data = [] 109 | let opt = {} 110 | 111 | opt.fuzzy = false 112 | opt.limit = 10 113 | opt.searchStrategy = opt.fuzzy ? _$FuzzySearchStrategy_5 : _$LiteralSearchStrategy_6 114 | opt.sort = NoSort 115 | opt.exclude = [] 116 | 117 | function put (data) { 118 | if (isObject(data)) { 119 | return addObject(data) 120 | } 121 | if (isArray(data)) { 122 | return addArray(data) 123 | } 124 | return undefined 125 | } 126 | function clear () { 127 | data.length = 0 128 | return data 129 | } 130 | 131 | function isObject (obj) { 132 | return Boolean(obj) && Object.prototype.toString.call(obj) === '[object Object]' 133 | } 134 | 135 | function isArray (obj) { 136 | return Boolean(obj) && Object.prototype.toString.call(obj) === '[object Array]' 137 | } 138 | 139 | function addObject (_data) { 140 | data.push(_data) 141 | return data 142 | } 143 | 144 | function addArray (_data) { 145 | const added = [] 146 | clear() 147 | for (let i = 0, len = _data.length; i < len; i++) { 148 | if (isObject(_data[i])) { 149 | added.push(addObject(_data[i])) 150 | } 151 | } 152 | return added 153 | } 154 | 155 | function search (crit) { 156 | if (!crit) { 157 | return [] 158 | } 159 | return findMatches(data, crit, opt.searchStrategy, opt).sort(opt.sort) 160 | } 161 | 162 | function __setOptions_4 (_opt) { 163 | opt = _opt || {} 164 | 165 | opt.fuzzy = _opt.fuzzy || false 166 | opt.limit = _opt.limit || 10 167 | opt.searchStrategy = _opt.fuzzy ? _$FuzzySearchStrategy_5 : _$LiteralSearchStrategy_6 168 | opt.sort = _opt.sort || NoSort 169 | opt.exclude = _opt.exclude || [] 170 | } 171 | 172 | function findMatches (data, crit, strategy, opt) { 173 | const matches = [] 174 | for (let i = 0; i < data.length && matches.length < opt.limit; i++) { 175 | const match = findMatchesInObject(data[i], crit, strategy, opt) 176 | if (match) { 177 | matches.push(match) 178 | } 179 | } 180 | return matches 181 | } 182 | 183 | function findMatchesInObject (obj, crit, strategy, opt) { 184 | for (const key in obj) { 185 | if (!isExcluded(obj[key], opt.exclude) && strategy.matches(obj[key], crit)) { 186 | return obj 187 | } 188 | } 189 | } 190 | 191 | function isExcluded (term, excludedTerms) { 192 | for (let i = 0, len = excludedTerms.length; i < len; i++) { 193 | const excludedTerm = excludedTerms[i] 194 | if (new RegExp(excludedTerm).test(term)) { 195 | return true 196 | } 197 | } 198 | return false 199 | } 200 | 201 | /* globals ActiveXObject:false */ 202 | 203 | 'use strict' 204 | 205 | var _$JSONLoader_2 = { 206 | load: load 207 | } 208 | 209 | function load (location, callback) { 210 | const xhr = getXHR() 211 | xhr.open('GET', location, true) 212 | xhr.onreadystatechange = createStateChangeListener(xhr, callback) 213 | xhr.send() 214 | } 215 | 216 | function createStateChangeListener (xhr, callback) { 217 | return function () { 218 | if (xhr.readyState === 4 && xhr.status === 200) { 219 | try { 220 | callback(null, JSON.parse(xhr.responseText)) 221 | } catch (err) { 222 | callback(err, null) 223 | } 224 | } 225 | } 226 | } 227 | 228 | function getXHR () { 229 | return window.XMLHttpRequest ? new window.XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP') 230 | } 231 | 232 | 'use strict' 233 | 234 | var _$OptionsValidator_3 = function OptionsValidator (params) { 235 | if (!validateParams(params)) { 236 | throw new Error('-- OptionsValidator: required options missing') 237 | } 238 | 239 | if (!(this instanceof OptionsValidator)) { 240 | return new OptionsValidator(params) 241 | } 242 | 243 | const requiredOptions = params.required 244 | 245 | this.getRequiredOptions = function () { 246 | return requiredOptions 247 | } 248 | 249 | this.validate = function (parameters) { 250 | const errors = [] 251 | requiredOptions.forEach(function (requiredOptionName) { 252 | if (typeof parameters[requiredOptionName] === 'undefined') { 253 | errors.push(requiredOptionName) 254 | } 255 | }) 256 | return errors 257 | } 258 | 259 | function validateParams (params) { 260 | if (!params) { 261 | return false 262 | } 263 | return typeof params.required !== 'undefined' && params.required instanceof Array 264 | } 265 | } 266 | 267 | 'use strict' 268 | 269 | var _$utils_9 = { 270 | merge: merge, 271 | isJSON: isJSON 272 | } 273 | 274 | function merge (defaultParams, mergeParams) { 275 | const mergedOptions = {} 276 | for (const option in defaultParams) { 277 | mergedOptions[option] = defaultParams[option] 278 | if (typeof mergeParams[option] !== 'undefined') { 279 | mergedOptions[option] = mergeParams[option] 280 | } 281 | } 282 | return mergedOptions 283 | } 284 | 285 | function isJSON (json) { 286 | try { 287 | if (json instanceof Object && JSON.parse(JSON.stringify(json))) { 288 | return true 289 | } 290 | return false 291 | } catch (err) { 292 | return false 293 | } 294 | } 295 | 296 | var _$src_8 = {}; 297 | (function (window) { 298 | 'use strict' 299 | 300 | let options = { 301 | searchInput: null, 302 | resultsContainer: null, 303 | json: [], 304 | success: Function.prototype, 305 | searchResultTemplate: '
        • {title}
        • ', 306 | templateMiddleware: Function.prototype, 307 | sortMiddleware: function () { 308 | return 0 309 | }, 310 | noResultsText: 'No results found', 311 | limit: 10, 312 | fuzzy: false, 313 | debounceTime: null, 314 | exclude: [] 315 | } 316 | 317 | let debounceTimerHandle 318 | const debounce = function (func, delayMillis) { 319 | if (delayMillis) { 320 | clearTimeout(debounceTimerHandle) 321 | debounceTimerHandle = setTimeout(func, delayMillis) 322 | } else { 323 | func.call() 324 | } 325 | } 326 | 327 | const requiredOptions = ['searchInput', 'resultsContainer', 'json'] 328 | 329 | /* removed: const _$Templater_7 = require('./Templater') */; 330 | /* removed: const _$Repository_4 = require('./Repository') */; 331 | /* removed: const _$JSONLoader_2 = require('./JSONLoader') */; 332 | const optionsValidator = _$OptionsValidator_3({ 333 | required: requiredOptions 334 | }) 335 | /* removed: const _$utils_9 = require('./utils') */; 336 | 337 | window.SimpleJekyllSearch = function (_options) { 338 | const errors = optionsValidator.validate(_options) 339 | if (errors.length > 0) { 340 | throwError('You must specify the following required options: ' + requiredOptions) 341 | } 342 | 343 | options = _$utils_9.merge(options, _options) 344 | 345 | _$Templater_7.setOptions({ 346 | template: options.searchResultTemplate, 347 | middleware: options.templateMiddleware 348 | }) 349 | 350 | _$Repository_4.setOptions({ 351 | fuzzy: options.fuzzy, 352 | limit: options.limit, 353 | sort: options.sortMiddleware, 354 | exclude: options.exclude 355 | }) 356 | 357 | if (_$utils_9.isJSON(options.json)) { 358 | initWithJSON(options.json) 359 | } else { 360 | initWithURL(options.json) 361 | } 362 | 363 | const rv = { 364 | search: search 365 | } 366 | 367 | typeof options.success === 'function' && options.success.call(rv) 368 | return rv 369 | } 370 | 371 | function initWithJSON (json) { 372 | _$Repository_4.put(json) 373 | registerInput() 374 | } 375 | 376 | function initWithURL (url) { 377 | _$JSONLoader_2.load(url, function (err, json) { 378 | if (err) { 379 | throwError('failed to get JSON (' + url + ')') 380 | } 381 | initWithJSON(json) 382 | }) 383 | } 384 | 385 | function emptyResultsContainer () { 386 | options.resultsContainer.innerHTML = '' 387 | } 388 | 389 | function appendToResultsContainer (text) { 390 | options.resultsContainer.innerHTML += text 391 | } 392 | 393 | function registerInput () { 394 | options.searchInput.addEventListener('input', function (e) { 395 | if (isWhitelistedKey(e.which)) { 396 | emptyResultsContainer() 397 | debounce(function () { search(e.target.value) }, options.debounceTime) 398 | } 399 | }) 400 | } 401 | 402 | function search (query) { 403 | if (isValidQuery(query)) { 404 | emptyResultsContainer() 405 | render(_$Repository_4.search(query), query) 406 | } 407 | } 408 | 409 | function render (results, query) { 410 | const len = results.length 411 | if (len === 0) { 412 | return appendToResultsContainer(options.noResultsText) 413 | } 414 | for (let i = 0; i < len; i++) { 415 | results[i].query = query 416 | appendToResultsContainer(_$Templater_7.compile(results[i])) 417 | } 418 | } 419 | 420 | function isValidQuery (query) { 421 | return query && query.length > 0 422 | } 423 | 424 | function isWhitelistedKey (key) { 425 | return [13, 16, 20, 37, 38, 39, 40, 91].indexOf(key) === -1 426 | } 427 | 428 | function throwError (message) { 429 | throw new Error('SimpleJekyllSearch --- ' + message) 430 | } 431 | })(window) 432 | 433 | }()); 434 | -------------------------------------------------------------------------------- /example/js/simple-jekyll-search.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Simple-Jekyll-Search 3 | * Copyright 2015-2020, Christian Fei 4 | * Licensed under the MIT License. 5 | */ 6 | 7 | (function(){ 8 | 'use strict' 9 | 10 | var _$Templater_7 = { 11 | compile: compile, 12 | setOptions: setOptions 13 | } 14 | 15 | const options = {} 16 | options.pattern = /\{(.*?)\}/g 17 | options.template = '' 18 | options.middleware = function () {} 19 | 20 | function setOptions (_options) { 21 | options.pattern = _options.pattern || options.pattern 22 | options.template = _options.template || options.template 23 | if (typeof _options.middleware === 'function') { 24 | options.middleware = _options.middleware 25 | } 26 | } 27 | 28 | function compile (data) { 29 | return options.template.replace(options.pattern, function (match, prop) { 30 | const value = options.middleware(prop, data[prop], options.template) 31 | if (typeof value !== 'undefined') { 32 | return value 33 | } 34 | return data[prop] || match 35 | }) 36 | } 37 | 38 | 'use strict'; 39 | 40 | function fuzzysearch (needle, haystack) { 41 | var tlen = haystack.length; 42 | var qlen = needle.length; 43 | if (qlen > tlen) { 44 | return false; 45 | } 46 | if (qlen === tlen) { 47 | return needle === haystack; 48 | } 49 | outer: for (var i = 0, j = 0; i < qlen; i++) { 50 | var nch = needle.charCodeAt(i); 51 | while (j < tlen) { 52 | if (haystack.charCodeAt(j++) === nch) { 53 | continue outer; 54 | } 55 | } 56 | return false; 57 | } 58 | return true; 59 | } 60 | 61 | var _$fuzzysearch_1 = fuzzysearch; 62 | 63 | 'use strict' 64 | 65 | /* removed: const _$fuzzysearch_1 = require('fuzzysearch') */; 66 | 67 | var _$FuzzySearchStrategy_5 = new FuzzySearchStrategy() 68 | 69 | function FuzzySearchStrategy () { 70 | this.matches = function (string, crit) { 71 | return _$fuzzysearch_1(crit.toLowerCase(), string.toLowerCase()) 72 | } 73 | } 74 | 75 | 'use strict' 76 | 77 | var _$LiteralSearchStrategy_6 = new LiteralSearchStrategy() 78 | 79 | function LiteralSearchStrategy () { 80 | this.matches = function (str, crit) { 81 | if (!str) return false 82 | 83 | str = str.trim().toLowerCase() 84 | crit = crit.trim().toLowerCase() 85 | 86 | return crit.split(' ').filter(function (word) { 87 | return str.indexOf(word) >= 0 88 | }).length === crit.split(' ').length 89 | } 90 | } 91 | 92 | 'use strict' 93 | 94 | var _$Repository_4 = { 95 | put: put, 96 | clear: clear, 97 | search: search, 98 | setOptions: __setOptions_4 99 | } 100 | 101 | /* removed: const _$FuzzySearchStrategy_5 = require('./SearchStrategies/FuzzySearchStrategy') */; 102 | /* removed: const _$LiteralSearchStrategy_6 = require('./SearchStrategies/LiteralSearchStrategy') */; 103 | 104 | function NoSort () { 105 | return 0 106 | } 107 | 108 | const data = [] 109 | let opt = {} 110 | 111 | opt.fuzzy = false 112 | opt.limit = 10 113 | opt.searchStrategy = opt.fuzzy ? _$FuzzySearchStrategy_5 : _$LiteralSearchStrategy_6 114 | opt.sort = NoSort 115 | opt.exclude = [] 116 | 117 | function put (data) { 118 | if (isObject(data)) { 119 | return addObject(data) 120 | } 121 | if (isArray(data)) { 122 | return addArray(data) 123 | } 124 | return undefined 125 | } 126 | function clear () { 127 | data.length = 0 128 | return data 129 | } 130 | 131 | function isObject (obj) { 132 | return Boolean(obj) && Object.prototype.toString.call(obj) === '[object Object]' 133 | } 134 | 135 | function isArray (obj) { 136 | return Boolean(obj) && Object.prototype.toString.call(obj) === '[object Array]' 137 | } 138 | 139 | function addObject (_data) { 140 | data.push(_data) 141 | return data 142 | } 143 | 144 | function addArray (_data) { 145 | const added = [] 146 | clear() 147 | for (let i = 0, len = _data.length; i < len; i++) { 148 | if (isObject(_data[i])) { 149 | added.push(addObject(_data[i])) 150 | } 151 | } 152 | return added 153 | } 154 | 155 | function search (crit) { 156 | if (!crit) { 157 | return [] 158 | } 159 | return findMatches(data, crit, opt.searchStrategy, opt).sort(opt.sort) 160 | } 161 | 162 | function __setOptions_4 (_opt) { 163 | opt = _opt || {} 164 | 165 | opt.fuzzy = _opt.fuzzy || false 166 | opt.limit = _opt.limit || 10 167 | opt.searchStrategy = _opt.fuzzy ? _$FuzzySearchStrategy_5 : _$LiteralSearchStrategy_6 168 | opt.sort = _opt.sort || NoSort 169 | opt.exclude = _opt.exclude || [] 170 | } 171 | 172 | function findMatches (data, crit, strategy, opt) { 173 | const matches = [] 174 | for (let i = 0; i < data.length && matches.length < opt.limit; i++) { 175 | const match = findMatchesInObject(data[i], crit, strategy, opt) 176 | if (match) { 177 | matches.push(match) 178 | } 179 | } 180 | return matches 181 | } 182 | 183 | function findMatchesInObject (obj, crit, strategy, opt) { 184 | for (const key in obj) { 185 | if (!isExcluded(obj[key], opt.exclude) && strategy.matches(obj[key], crit)) { 186 | return obj 187 | } 188 | } 189 | } 190 | 191 | function isExcluded (term, excludedTerms) { 192 | for (let i = 0, len = excludedTerms.length; i < len; i++) { 193 | const excludedTerm = excludedTerms[i] 194 | if (new RegExp(excludedTerm).test(term)) { 195 | return true 196 | } 197 | } 198 | return false 199 | } 200 | 201 | /* globals ActiveXObject:false */ 202 | 203 | 'use strict' 204 | 205 | var _$JSONLoader_2 = { 206 | load: load 207 | } 208 | 209 | function load (location, callback) { 210 | const xhr = getXHR() 211 | xhr.open('GET', location, true) 212 | xhr.onreadystatechange = createStateChangeListener(xhr, callback) 213 | xhr.send() 214 | } 215 | 216 | function createStateChangeListener (xhr, callback) { 217 | return function () { 218 | if (xhr.readyState === 4 && xhr.status === 200) { 219 | try { 220 | callback(null, JSON.parse(xhr.responseText)) 221 | } catch (err) { 222 | callback(err, null) 223 | } 224 | } 225 | } 226 | } 227 | 228 | function getXHR () { 229 | return window.XMLHttpRequest ? new window.XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP') 230 | } 231 | 232 | 'use strict' 233 | 234 | var _$OptionsValidator_3 = function OptionsValidator (params) { 235 | if (!validateParams(params)) { 236 | throw new Error('-- OptionsValidator: required options missing') 237 | } 238 | 239 | if (!(this instanceof OptionsValidator)) { 240 | return new OptionsValidator(params) 241 | } 242 | 243 | const requiredOptions = params.required 244 | 245 | this.getRequiredOptions = function () { 246 | return requiredOptions 247 | } 248 | 249 | this.validate = function (parameters) { 250 | const errors = [] 251 | requiredOptions.forEach(function (requiredOptionName) { 252 | if (typeof parameters[requiredOptionName] === 'undefined') { 253 | errors.push(requiredOptionName) 254 | } 255 | }) 256 | return errors 257 | } 258 | 259 | function validateParams (params) { 260 | if (!params) { 261 | return false 262 | } 263 | return typeof params.required !== 'undefined' && params.required instanceof Array 264 | } 265 | } 266 | 267 | 'use strict' 268 | 269 | var _$utils_9 = { 270 | merge: merge, 271 | isJSON: isJSON 272 | } 273 | 274 | function merge (defaultParams, mergeParams) { 275 | const mergedOptions = {} 276 | for (const option in defaultParams) { 277 | mergedOptions[option] = defaultParams[option] 278 | if (typeof mergeParams[option] !== 'undefined') { 279 | mergedOptions[option] = mergeParams[option] 280 | } 281 | } 282 | return mergedOptions 283 | } 284 | 285 | function isJSON (json) { 286 | try { 287 | if (json instanceof Object && JSON.parse(JSON.stringify(json))) { 288 | return true 289 | } 290 | return false 291 | } catch (err) { 292 | return false 293 | } 294 | } 295 | 296 | var _$src_8 = {}; 297 | (function (window) { 298 | 'use strict' 299 | 300 | let options = { 301 | searchInput: null, 302 | resultsContainer: null, 303 | json: [], 304 | success: Function.prototype, 305 | searchResultTemplate: '
        • {title}
        • ', 306 | templateMiddleware: Function.prototype, 307 | sortMiddleware: function () { 308 | return 0 309 | }, 310 | noResultsText: 'No results found', 311 | limit: 10, 312 | fuzzy: false, 313 | debounceTime: null, 314 | exclude: [] 315 | } 316 | 317 | let debounceTimerHandle 318 | const debounce = function (func, delayMillis) { 319 | if (delayMillis) { 320 | clearTimeout(debounceTimerHandle) 321 | debounceTimerHandle = setTimeout(func, delayMillis) 322 | } else { 323 | func.call() 324 | } 325 | } 326 | 327 | const requiredOptions = ['searchInput', 'resultsContainer', 'json'] 328 | 329 | /* removed: const _$Templater_7 = require('./Templater') */; 330 | /* removed: const _$Repository_4 = require('./Repository') */; 331 | /* removed: const _$JSONLoader_2 = require('./JSONLoader') */; 332 | const optionsValidator = _$OptionsValidator_3({ 333 | required: requiredOptions 334 | }) 335 | /* removed: const _$utils_9 = require('./utils') */; 336 | 337 | window.SimpleJekyllSearch = function (_options) { 338 | const errors = optionsValidator.validate(_options) 339 | if (errors.length > 0) { 340 | throwError('You must specify the following required options: ' + requiredOptions) 341 | } 342 | 343 | options = _$utils_9.merge(options, _options) 344 | 345 | _$Templater_7.setOptions({ 346 | template: options.searchResultTemplate, 347 | middleware: options.templateMiddleware 348 | }) 349 | 350 | _$Repository_4.setOptions({ 351 | fuzzy: options.fuzzy, 352 | limit: options.limit, 353 | sort: options.sortMiddleware, 354 | exclude: options.exclude 355 | }) 356 | 357 | if (_$utils_9.isJSON(options.json)) { 358 | initWithJSON(options.json) 359 | } else { 360 | initWithURL(options.json) 361 | } 362 | 363 | const rv = { 364 | search: search 365 | } 366 | 367 | typeof options.success === 'function' && options.success.call(rv) 368 | return rv 369 | } 370 | 371 | function initWithJSON (json) { 372 | _$Repository_4.put(json) 373 | registerInput() 374 | } 375 | 376 | function initWithURL (url) { 377 | _$JSONLoader_2.load(url, function (err, json) { 378 | if (err) { 379 | throwError('failed to get JSON (' + url + ')') 380 | } 381 | initWithJSON(json) 382 | }) 383 | } 384 | 385 | function emptyResultsContainer () { 386 | options.resultsContainer.innerHTML = '' 387 | } 388 | 389 | function appendToResultsContainer (text) { 390 | options.resultsContainer.innerHTML += text 391 | } 392 | 393 | function registerInput () { 394 | options.searchInput.addEventListener('input', function (e) { 395 | if (isWhitelistedKey(e.which)) { 396 | emptyResultsContainer() 397 | debounce(function () { search(e.target.value) }, options.debounceTime) 398 | } 399 | }) 400 | } 401 | 402 | function search (query) { 403 | if (isValidQuery(query)) { 404 | emptyResultsContainer() 405 | render(_$Repository_4.search(query), query) 406 | } 407 | } 408 | 409 | function render (results, query) { 410 | const len = results.length 411 | if (len === 0) { 412 | return appendToResultsContainer(options.noResultsText) 413 | } 414 | for (let i = 0; i < len; i++) { 415 | results[i].query = query 416 | appendToResultsContainer(_$Templater_7.compile(results[i])) 417 | } 418 | } 419 | 420 | function isValidQuery (query) { 421 | return query && query.length > 0 422 | } 423 | 424 | function isWhitelistedKey (key) { 425 | return [13, 16, 20, 37, 38, 39, 40, 91].indexOf(key) === -1 426 | } 427 | 428 | function throwError (message) { 429 | throw new Error('SimpleJekyllSearch --- ' + message) 430 | } 431 | })(window) 432 | 433 | }()); 434 | --------------------------------------------------------------------------------