├── 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 |
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 |
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 |
10 | {% for post in site.posts %}
11 |
12 | {{ post.date | date: "%b %-d, %Y" }}
13 |
14 |
17 |
18 | {% endfor %}
19 |
20 |
21 |
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 |
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 | [](https://travis-ci.org/christian-fei/Simple-Jekyll-Search)
4 | [](https://david-dm.org/christian-fei/Simple-Jekyll-Search)
5 | [](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 | [](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 |
--------------------------------------------------------------------------------