├── .coveralls.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── .travis.yml ├── Appraisals ├── CONTRIBUTING.md ├── Gemfile ├── Guardfile_jekyllv2 ├── Guardfile_jekyllv3 ├── LICENSE.txt ├── README.md ├── Rakefile ├── algoliasearch-jekyll.gemspec ├── docs └── travis-settings.png ├── gemfiles ├── jekyll_v2.gemfile └── jekyll_v3.gemfile ├── lib ├── algoliasearch-jekyll.rb ├── credential_checker.rb ├── error_handler.rb ├── push.rb ├── record_extractor.rb └── version.rb ├── scripts ├── bump_version ├── check_flay ├── check_flog ├── coverage ├── git_hooks │ ├── pre-commit │ └── pre-push ├── lint ├── release ├── test ├── test_ci ├── test_v2 ├── test_v3 ├── watch ├── watch_v2 └── watch_v3 ├── spec ├── credential_checker_spec.rb ├── error_handler_spec.rb ├── fixtures │ ├── jekyll_version_2 │ │ ├── _config.yml │ │ ├── _layouts │ │ │ └── default.html │ │ ├── _my-collection │ │ │ ├── collection-item.html │ │ │ └── collection-item.md │ │ ├── _posts │ │ │ ├── 2015-07-02-test-post.md │ │ │ └── 2015-07-03-test-post-again.md │ │ ├── about.md │ │ ├── api_key_dir │ │ │ └── _algolia_api_key │ │ ├── assets │ │ │ └── ring.png │ │ ├── authors.html │ │ ├── excluded.html │ │ ├── hierarchy.md │ │ ├── index.html │ │ ├── page2 │ │ │ └── index.html │ │ └── weight.md │ └── jekyll_version_3 │ │ ├── _config.yml │ │ ├── _layouts │ │ └── default.html │ │ ├── _my-collection │ │ ├── collection-item.html │ │ └── collection-item.md │ │ ├── _posts │ │ ├── 2015-07-02-test-post.md │ │ └── 2015-07-03-test-post-again.md │ │ ├── about.md │ │ ├── api_key_dir │ │ └── _algolia_api_key │ │ ├── assets │ │ └── ring.png │ │ ├── authors.html │ │ ├── excluded.html │ │ ├── hierarchy.md │ │ ├── index.html │ │ ├── page2 │ │ └── index.html │ │ └── weight.md ├── push_spec.rb ├── record_extractor_spec.rb ├── spec_helper.rb └── spec_helper_simplecov.rb └── txt ├── api_key_missing ├── application_id_missing ├── check_key_acl_to_tmp_index ├── index_name_missing └── sample /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-ci 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Gemfile.lock 2 | pkg/ 3 | coverage/ 4 | gemfiles/*.lock 5 | spec/fixtures/*/_site 6 | docs/ 7 | 8 | 9 | .DS_Store 10 | .*.sw[a-z] 11 | Thumbs.db 12 | nohup.out 13 | tmp 14 | *~ 15 | *.crcdownload 16 | *.orig 17 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format progress 3 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | # Defaults: 2 | # https://github.com/bbatsov/rubocop/blob/master/config/default.yml 3 | Metrics/AbcSize: 4 | Max: 100 5 | 6 | Metrics/ClassLength: 7 | Max: 200 8 | 9 | Metrics/ModuleLength: 10 | Max: 200 11 | 12 | Metrics/MethodLength: 13 | Max: 50 14 | 15 | Metrics/CyclomaticComplexity: 16 | Max: 10 17 | 18 | Metrics/PerceivedComplexity: 19 | Max: 10 20 | 21 | Style/FileName: 22 | Enabled: false 23 | 24 | Style/MultilineOperationIndentation: 25 | Enabled: false 26 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | cache: bundler 3 | gemfile: 4 | - gemfiles/jekyll_v2.gemfile 5 | - gemfiles/jekyll_v3.gemfile 6 | before_script: bundle update 7 | script: ./scripts/test_ci 8 | rvm: 9 | - 2.4.2 10 | - 2.3.5 11 | - 2.2.5 12 | - 2.1.10 13 | notifications: 14 | email: 15 | on_success: never 16 | on_failure: never 17 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | appraise 'jekyll-v2' do 2 | gem 'jekyll', '~> 2.5' 3 | end 4 | 5 | appraise 'jekyll-v3' do 6 | gem 'jekyll', '~> 3.0' 7 | gem 'jekyll-paginate', '~> 1.1.0' 8 | end 9 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Hi collaborator! 2 | 3 | If you have a fix or a new feature, please start by checking in the 4 | [issues](https://github.com/algolia/algoliasearch-jekyll/issues) if it is 5 | already referenced. If not, feel free to open one. 6 | 7 | We use [pull requests](https://github.com/algolia/algoliasearch-jekyll/pulls) 8 | for collaboration. The workflow is as follow: 9 | 10 | - Create a local branch, starting from `develop` 11 | - Submit the PR on `develop` 12 | - Wait for review 13 | - Do the changes requested (if any) 14 | - We may ask you to rebase the branch to latest `develop` if it gets out of sync 15 | - Receive the thanks of the Algolia team :) 16 | 17 | # Development workflow 18 | 19 | After the necessary `bundle install`, you'll also need to run `appraisal 20 | install`. This will configure the repository so that tests can be run both from 21 | Jekyll 2.5 and Jekyll 3. 22 | 23 | You can then launch: 24 | - `./scripts/test_v2` to launch tests on Jekyll v2 25 | - `./scripts/test_v3` to launch tests on Jekyll v3 26 | - `./scripts/test` to launch tests on both 27 | - `./scripts/watch` to start a test watcher (for TDD) for Jekyll v2 28 | - `./scripts/watch_v3` to start a test watcher (for TDD) for Jekyll v3 29 | 30 | The watched test will both launch Guard (with `guard-rspec`), but each will use 31 | its own `Guardfile` version, launching the correct `appraisal` before the 32 | `rspec` command. 33 | 34 | If you want to test the plugin on an existing Jekyll website while developping, 35 | I suggest updating the website `Gemfile` to point to the correct local directory 36 | 37 | ```ruby 38 | gem "algoliasearch-jekyll", :path => "/path/to/local/gem/folder" 39 | ``` 40 | You should also run `rake gemspec` from the `algoliasearch-jekyll` repository if 41 | you added/deleted any file or dependency. 42 | 43 | If you plan on submitting a PR, I suggest you install the git hooks. This will 44 | run pre-commit and pre-push checks. Those checks will also be run by TravisCI, 45 | but running them locally gives faster feedback. 46 | 47 | # Tagging and releasing 48 | 49 | This part is for main contributors: 50 | 51 | ``` 52 | # Bump the version (in develop) 53 | ./scripts/bump_version minor 54 | 55 | # Update master and release 56 | ./scripts/release 57 | 58 | # Install the gem locally (optional) 59 | rake install 60 | ``` 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | 3 | gem 'algoliasearch', '~> 1.12' 4 | gem 'appraisal', '~> 2.1.0' 5 | gem 'awesome_print', '~> 1.6' 6 | gem 'json', '>= 1.8.6' 7 | gem 'nokogiri', '~> 1.7', '>= 1.7.2' 8 | gem 'verbal_expressions', '~> 0.1.5' 9 | 10 | group :development do 11 | gem 'rake', '< 11.0' 12 | gem 'coveralls', '~> 0.8' 13 | gem 'flay', '~> 2.6' 14 | gem 'flog', '~> 4.3' 15 | gem 'guard-rspec', '~> 4.6' 16 | gem 'jeweler', '~> 2.0' 17 | gem 'rspec', '~> 3.0' 18 | gem 'rubocop', '~> 0.31' 19 | gem 'simplecov', '~> 0.10' 20 | gem "rack", "< 2" 21 | end 22 | -------------------------------------------------------------------------------- /Guardfile_jekyllv2: -------------------------------------------------------------------------------- 1 | guard :rspec, cmd: 'appraisal jekyll-v2 bundle exec rspec --color --format documentation' do 2 | watch(%r{^spec/.+_spec\.rb$}) 3 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } 4 | watch('spec/spec_helper.rb') { 'spec' } 5 | end 6 | 7 | notification :off 8 | -------------------------------------------------------------------------------- /Guardfile_jekyllv3: -------------------------------------------------------------------------------- 1 | guard :rspec, cmd: 'appraisal jekyll-v3 bundle exec rspec --color --format documentation' do 2 | watch(%r{^spec/.+_spec\.rb$}) 3 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } 4 | watch('spec/spec_helper.rb') { 'spec' } 5 | end 6 | 7 | notification :off 8 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Algolia 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Algolia Jekyll Plugin 2 | 3 | ## :warning: DEPRECATION NOTICE 4 | 5 | This gem has been deprecated in favor of [jekyll-algolia][1]. No further development will be happening on that gem. 6 | 7 | We strongly encourage you to try the new gem. It is officially supported by 8 | Algolia and you can find its [complete documentation][2] online. A [migration guide][3] will help you transition. 9 | 10 | [1]: https://github.com/algolia/jekyll-algolia 11 | [2]: https://community.algolia.com/jekyll-algolia/getting-started.html 12 | [3]: https://community.algolia.com/jekyll-algolia/migration-guide.html 13 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler' 3 | begin 4 | Bundler.setup(:default, :development) 5 | rescue Bundler::BundlerError => e 6 | warn e.message 7 | warn 'Run `bundle install` to install missing gems' 8 | exit e.status_code 9 | end 10 | require 'rake' 11 | 12 | require 'jeweler' 13 | require_relative 'lib/version' 14 | Jeweler::Tasks.new do |gem| 15 | # gem is a Gem::Specification... see 16 | # http://guides.rubygems.org/specification-reference/ for more options 17 | gem.name = 'algoliasearch-jekyll' 18 | gem.version = AlgoliaSearchJekyllVersion.to_s 19 | gem.homepage = 'https://github.com/algolia/algoliasearch-jekyll' 20 | gem.license = 'MIT' 21 | gem.summary = '[⚠ DEPRECATED ⚠]: Use jekyll-algolia instead'.freeze 22 | gem.description = '[⚠ DEPRECATED ⚠]: Use jekyll-algolia instead'.freeze 23 | gem.email = 'tim@pixelastic.com' 24 | gem.authors = ['Tim Carry'] 25 | gem.post_install_message = <<-MESSAGE 26 | ! The 'algolisearch-jekyll' gem has been deprecated and has been replaced with 'jekyll-algolia'. 27 | ! See: https://rubygems.org/gems/jekyll-algolia 28 | ! And: https://github.com/algolia/jekyll-algolia 29 | ! 30 | ! You can get quickly started on the new plugin using this documentation: 31 | ! https://community.algolia.com/jekyll-algolia/getting-started.html 32 | MESSAGE 33 | 34 | # dependencies defined in Gemfile 35 | end 36 | Jeweler::RubygemsDotOrgTasks.new 37 | 38 | require 'rspec/core' 39 | require 'rspec/core/rake_task' 40 | RSpec::Core::RakeTask.new(:spec) do |spec| 41 | spec.rspec_opts = '--color --format documentation' 42 | spec.pattern = FileList['spec/**/*_spec.rb'] 43 | end 44 | task test: :spec 45 | 46 | desc 'Code coverage detail' 47 | task :coverage do 48 | ENV['COVERAGE'] = 'true' 49 | Rake::Task['spec'].execute 50 | end 51 | 52 | task default: :test 53 | -------------------------------------------------------------------------------- /algoliasearch-jekyll.gemspec: -------------------------------------------------------------------------------- 1 | # Generated by jeweler 2 | # DO NOT EDIT THIS FILE DIRECTLY 3 | # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec' 4 | # -*- encoding: utf-8 -*- 5 | # stub: algoliasearch-jekyll 0.9.1 ruby lib 6 | 7 | Gem::Specification.new do |s| 8 | s.name = "algoliasearch-jekyll".freeze 9 | s.version = "0.9.1" 10 | 11 | s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version= 12 | s.require_paths = ["lib".freeze] 13 | s.authors = ["Tim Carry".freeze] 14 | s.date = "2017-12-21" 15 | s.description = "[\u26A0 DEPRECATED \u26A0]: Use jekyll-algolia instead".freeze 16 | s.email = "tim@pixelastic.com".freeze 17 | s.extra_rdoc_files = [ 18 | "LICENSE.txt", 19 | "README.md" 20 | ] 21 | s.files = [ 22 | ".coveralls.yml", 23 | ".rspec", 24 | ".rubocop.yml", 25 | ".travis.yml", 26 | "Appraisals", 27 | "CONTRIBUTING.md", 28 | "Gemfile", 29 | "Guardfile_jekyllv2", 30 | "Guardfile_jekyllv3", 31 | "LICENSE.txt", 32 | "README.md", 33 | "Rakefile", 34 | "algoliasearch-jekyll.gemspec", 35 | "docs/travis-settings.png", 36 | "gemfiles/jekyll_v2.gemfile", 37 | "gemfiles/jekyll_v3.gemfile", 38 | "lib/algoliasearch-jekyll.rb", 39 | "lib/credential_checker.rb", 40 | "lib/error_handler.rb", 41 | "lib/push.rb", 42 | "lib/record_extractor.rb", 43 | "lib/version.rb", 44 | "scripts/bump_version", 45 | "scripts/check_flay", 46 | "scripts/check_flog", 47 | "scripts/coverage", 48 | "scripts/git_hooks/pre-commit", 49 | "scripts/git_hooks/pre-push", 50 | "scripts/lint", 51 | "scripts/release", 52 | "scripts/test", 53 | "scripts/test_ci", 54 | "scripts/test_v2", 55 | "scripts/test_v3", 56 | "scripts/watch", 57 | "scripts/watch_v2", 58 | "scripts/watch_v3", 59 | "spec/credential_checker_spec.rb", 60 | "spec/error_handler_spec.rb", 61 | "spec/fixtures/jekyll_version_2/_config.yml", 62 | "spec/fixtures/jekyll_version_2/_layouts/default.html", 63 | "spec/fixtures/jekyll_version_2/_my-collection/collection-item.html", 64 | "spec/fixtures/jekyll_version_2/_my-collection/collection-item.md", 65 | "spec/fixtures/jekyll_version_2/_posts/2015-07-02-test-post.md", 66 | "spec/fixtures/jekyll_version_2/_posts/2015-07-03-test-post-again.md", 67 | "spec/fixtures/jekyll_version_2/about.md", 68 | "spec/fixtures/jekyll_version_2/api_key_dir/_algolia_api_key", 69 | "spec/fixtures/jekyll_version_2/assets/ring.png", 70 | "spec/fixtures/jekyll_version_2/authors.html", 71 | "spec/fixtures/jekyll_version_2/excluded.html", 72 | "spec/fixtures/jekyll_version_2/hierarchy.md", 73 | "spec/fixtures/jekyll_version_2/index.html", 74 | "spec/fixtures/jekyll_version_2/page2/index.html", 75 | "spec/fixtures/jekyll_version_2/weight.md", 76 | "spec/fixtures/jekyll_version_3/_config.yml", 77 | "spec/fixtures/jekyll_version_3/_layouts/default.html", 78 | "spec/fixtures/jekyll_version_3/_my-collection/collection-item.html", 79 | "spec/fixtures/jekyll_version_3/_my-collection/collection-item.md", 80 | "spec/fixtures/jekyll_version_3/_posts/2015-07-02-test-post.md", 81 | "spec/fixtures/jekyll_version_3/_posts/2015-07-03-test-post-again.md", 82 | "spec/fixtures/jekyll_version_3/about.md", 83 | "spec/fixtures/jekyll_version_3/api_key_dir/_algolia_api_key", 84 | "spec/fixtures/jekyll_version_3/assets/ring.png", 85 | "spec/fixtures/jekyll_version_3/authors.html", 86 | "spec/fixtures/jekyll_version_3/excluded.html", 87 | "spec/fixtures/jekyll_version_3/hierarchy.md", 88 | "spec/fixtures/jekyll_version_3/index.html", 89 | "spec/fixtures/jekyll_version_3/page2/index.html", 90 | "spec/fixtures/jekyll_version_3/weight.md", 91 | "spec/push_spec.rb", 92 | "spec/record_extractor_spec.rb", 93 | "spec/spec_helper.rb", 94 | "spec/spec_helper_simplecov.rb", 95 | "txt/api_key_missing", 96 | "txt/application_id_missing", 97 | "txt/check_key_acl_to_tmp_index", 98 | "txt/index_name_missing", 99 | "txt/sample" 100 | ] 101 | s.homepage = "https://github.com/algolia/algoliasearch-jekyll".freeze 102 | s.licenses = ["MIT".freeze] 103 | s.post_install_message = " ! The 'algolisearch-jekyll' gem has been deprecated and has been replaced with 'jekyll-algolia'.\n ! See: https://rubygems.org/gems/jekyll-algolia\n ! And: https://github.com/algolia/jekyll-algolia\n !\n ! You can get quickly started on the new plugin using this documentation:\n ! https://community.algolia.com/jekyll-algolia/getting-started.html\n".freeze 104 | s.rubygems_version = "2.6.13".freeze 105 | s.summary = "[\u26A0 DEPRECATED \u26A0]: Use jekyll-algolia instead".freeze 106 | 107 | if s.respond_to? :specification_version then 108 | s.specification_version = 4 109 | 110 | if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then 111 | s.add_runtime_dependency(%q.freeze, ["~> 1.12"]) 112 | s.add_runtime_dependency(%q.freeze, ["~> 2.1.0"]) 113 | s.add_runtime_dependency(%q.freeze, ["~> 1.6"]) 114 | s.add_runtime_dependency(%q.freeze, [">= 1.8.6"]) 115 | s.add_runtime_dependency(%q.freeze, [">= 1.7.2", "~> 1.7"]) 116 | s.add_runtime_dependency(%q.freeze, ["~> 0.1.5"]) 117 | s.add_development_dependency(%q.freeze, ["< 11.0"]) 118 | s.add_development_dependency(%q.freeze, ["~> 0.8"]) 119 | s.add_development_dependency(%q.freeze, ["~> 2.6"]) 120 | s.add_development_dependency(%q.freeze, ["~> 4.3"]) 121 | s.add_development_dependency(%q.freeze, ["~> 4.6"]) 122 | s.add_development_dependency(%q.freeze, ["~> 2.0"]) 123 | s.add_development_dependency(%q.freeze, ["~> 3.0"]) 124 | s.add_development_dependency(%q.freeze, ["~> 0.31"]) 125 | s.add_development_dependency(%q.freeze, ["~> 0.10"]) 126 | s.add_development_dependency(%q.freeze, ["< 2"]) 127 | else 128 | s.add_dependency(%q.freeze, ["~> 1.12"]) 129 | s.add_dependency(%q.freeze, ["~> 2.1.0"]) 130 | s.add_dependency(%q.freeze, ["~> 1.6"]) 131 | s.add_dependency(%q.freeze, [">= 1.8.6"]) 132 | s.add_dependency(%q.freeze, [">= 1.7.2", "~> 1.7"]) 133 | s.add_dependency(%q.freeze, ["~> 0.1.5"]) 134 | s.add_dependency(%q.freeze, ["< 11.0"]) 135 | s.add_dependency(%q.freeze, ["~> 0.8"]) 136 | s.add_dependency(%q.freeze, ["~> 2.6"]) 137 | s.add_dependency(%q.freeze, ["~> 4.3"]) 138 | s.add_dependency(%q.freeze, ["~> 4.6"]) 139 | s.add_dependency(%q.freeze, ["~> 2.0"]) 140 | s.add_dependency(%q.freeze, ["~> 3.0"]) 141 | s.add_dependency(%q.freeze, ["~> 0.31"]) 142 | s.add_dependency(%q.freeze, ["~> 0.10"]) 143 | s.add_dependency(%q.freeze, ["< 2"]) 144 | end 145 | else 146 | s.add_dependency(%q.freeze, ["~> 1.12"]) 147 | s.add_dependency(%q.freeze, ["~> 2.1.0"]) 148 | s.add_dependency(%q.freeze, ["~> 1.6"]) 149 | s.add_dependency(%q.freeze, [">= 1.8.6"]) 150 | s.add_dependency(%q.freeze, [">= 1.7.2", "~> 1.7"]) 151 | s.add_dependency(%q.freeze, ["~> 0.1.5"]) 152 | s.add_dependency(%q.freeze, ["< 11.0"]) 153 | s.add_dependency(%q.freeze, ["~> 0.8"]) 154 | s.add_dependency(%q.freeze, ["~> 2.6"]) 155 | s.add_dependency(%q.freeze, ["~> 4.3"]) 156 | s.add_dependency(%q.freeze, ["~> 4.6"]) 157 | s.add_dependency(%q.freeze, ["~> 2.0"]) 158 | s.add_dependency(%q.freeze, ["~> 3.0"]) 159 | s.add_dependency(%q.freeze, ["~> 0.31"]) 160 | s.add_dependency(%q.freeze, ["~> 0.10"]) 161 | s.add_dependency(%q.freeze, ["< 2"]) 162 | end 163 | end 164 | 165 | -------------------------------------------------------------------------------- /docs/travis-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algolia/algoliasearch-jekyll/cb21e83402de3cea46201fe12bd1b9a2eaacebfb/docs/travis-settings.png -------------------------------------------------------------------------------- /gemfiles/jekyll_v2.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "http://rubygems.org" 4 | 5 | gem "algoliasearch", "~> 1.4" 6 | gem "appraisal", "~> 2.1.0" 7 | gem "awesome_print", "~> 1.6" 8 | gem "json", ">= 1.8.6" 9 | gem "nokogiri", '~> 1.7', '>= 1.7.2' 10 | gem "verbal_expressions", "~> 0.1.5" 11 | gem "jekyll", "~> 2.5" 12 | 13 | group :development do 14 | gem "coveralls", "~> 0.8" 15 | gem "flay", "~> 2.6" 16 | gem "flog", "~> 4.3" 17 | gem "guard-rspec", "~> 4.6" 18 | gem "jeweler", "~> 2.0" 19 | gem "rspec", "~> 3.0" 20 | gem "rubocop", "~> 0.31" 21 | gem "simplecov", "~> 0.10" 22 | gem "rack", "< 2" 23 | end 24 | -------------------------------------------------------------------------------- /gemfiles/jekyll_v3.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "http://rubygems.org" 4 | 5 | gem "algoliasearch", "~> 1.4" 6 | gem "appraisal", "~> 2.1.0" 7 | gem "awesome_print", "~> 1.6" 8 | gem "json", ">= 1.8.6" 9 | gem "nokogiri", '~> 1.7', '>= 1.7.2' 10 | gem "verbal_expressions", "~> 0.1.5" 11 | gem "jekyll", "~> 3.0" 12 | gem "jekyll-paginate", "~> 1.1.0" 13 | 14 | group :development do 15 | gem "coveralls", "~> 0.8" 16 | gem "flay", "~> 2.6" 17 | gem "flog", "~> 4.3" 18 | gem "guard-rspec", "~> 4.6" 19 | gem "jeweler", "~> 2.0" 20 | gem "rspec", "~> 3.0" 21 | gem "rubocop", "~> 0.31" 22 | gem "simplecov", "~> 0.10" 23 | gem "rack", "< 2" 24 | end 25 | -------------------------------------------------------------------------------- /lib/algoliasearch-jekyll.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/setup' 3 | 4 | require 'awesome_print' 5 | 6 | require_relative './version' 7 | require_relative './push' 8 | 9 | # `jekyll algolia` main entry 10 | class AlgoliaSearchJekyll < Jekyll::Command 11 | class << self 12 | def init_with_program(prog) 13 | prog.command(:algolia) do |command| 14 | command.syntax 'algolia [options]' 15 | command.description 'Keep your content in sync with your Algolia index' 16 | 17 | command.command(:push) do |subcommand| 18 | subcommand.syntax 'push [options]' 19 | subcommand.description 'Push your content to your index' 20 | 21 | add_build_options(subcommand) 22 | 23 | subcommand.action do |args, options| 24 | default_options = { 25 | 'dry_run' => false, 26 | 'verbose' => false 27 | } 28 | options = default_options.merge(options) 29 | @config = configuration_from_options(options) 30 | 31 | AlgoliaSearchJekyllPush.init_options(args, options, @config) 32 | .jekyll_new(@config) 33 | .process 34 | end 35 | end 36 | end 37 | end 38 | 39 | # Allow a subset of the default `jekyll build` options 40 | def add_build_options(command) 41 | command.option 'config', '--config CONFIG_FILE[,CONFIG_FILE2,...]', 42 | Array, 'Custom configuration file' 43 | command.option 'future', '--future', 'Index posts with a future date' 44 | command.option 'limit_posts', '--limit_posts MAX_POSTS', Integer, 45 | 'Limits the number of posts to parse and index' 46 | command.option 'show_drafts', '-D', '--drafts', 47 | 'Index posts in the _drafts folder' 48 | command.option 'unpublished', '--unpublished', 49 | 'Index posts that were marked as unpublished' 50 | command.option 'dry_run', '--dry-run', '-n', 51 | 'Do a dry run, do not push records' 52 | command.option 'verbose', '--verbose', 53 | 'Display more information on what is indexed' 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/credential_checker.rb: -------------------------------------------------------------------------------- 1 | require 'algoliasearch' 2 | require 'nokogiri' 3 | require 'json' 4 | require_relative './error_handler.rb' 5 | 6 | # Given an HTML file as input, will return an array of records to index 7 | class AlgoliaSearchCredentialChecker 8 | attr_accessor :config, :logger 9 | 10 | def initialize(config) 11 | @config = config 12 | @logger = AlgoliaSearchErrorHandler.new 13 | end 14 | 15 | # Read the API key either from ENV or from an _algolia_api_key file in 16 | # source folder 17 | def api_key 18 | # First read in ENV 19 | return ENV['ALGOLIA_API_KEY'] if ENV['ALGOLIA_API_KEY'] 20 | 21 | # Otherwise from file in source directory 22 | key_file = File.join(@config['source'], '_algolia_api_key') 23 | if File.exist?(key_file) && File.size(key_file) > 0 24 | return File.open(key_file).read.strip 25 | end 26 | nil 27 | end 28 | 29 | # Read key either from ENV or _config.yml 30 | def get_key(env_name, config_key) 31 | # First read in ENV 32 | return ENV[env_name] if ENV[env_name] 33 | 34 | # Otherwise read from _config.yml 35 | if @config['algolia'] && @config['algolia'][config_key] 36 | return @config['algolia'][config_key] 37 | end 38 | 39 | nil 40 | end 41 | 42 | # Read the application id either from the config file or from ENV 43 | def application_id 44 | get_key('ALGOLIA_APPLICATION_ID', 'application_id') 45 | end 46 | 47 | # Read the index name either from the config file or from ENV 48 | def index_name 49 | get_key('ALGOLIA_INDEX_NAME', 'index_name') 50 | end 51 | 52 | # Check that the API key is available 53 | def check_api_key 54 | return if api_key 55 | @logger.display('api_key_missing') 56 | exit 1 57 | end 58 | 59 | # Check that the application id is defined 60 | def check_application_id 61 | return if application_id 62 | @logger.display('application_id_missing') 63 | exit 1 64 | end 65 | 66 | # Check that the index name is defined 67 | def check_index_name 68 | return if index_name 69 | @logger.display('index_name_missing') 70 | exit 1 71 | end 72 | 73 | # Check that all credentials are present 74 | # Stop with a helpful message if not 75 | def assert_valid 76 | check_api_key 77 | check_application_id 78 | check_index_name 79 | 80 | Algolia.init( 81 | application_id: application_id, 82 | api_key: api_key 83 | ) 84 | 85 | nil 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /lib/error_handler.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | require 'verbal_expressions' 3 | 4 | # Helps in displaying useful error messages to users, to help them debug their 5 | # issues 6 | class AlgoliaSearchErrorHandler 7 | # Will output the specified error file. 8 | # First line is displayed as error, next ones as warning 9 | def display(file) 10 | file = File.expand_path(File.join(File.dirname(__FILE__), '../txt', file)) 11 | content = File.open(file).readlines.map(&:chomp) 12 | content.each_with_index do |line, index| 13 | if index == 0 14 | Jekyll.logger.error line 15 | next 16 | end 17 | Jekyll.logger.warn line 18 | end 19 | end 20 | 21 | def error_tester 22 | # Ex: Cannot PUT to https://appid.algolia.net/1/indexes/index_name/settings: 23 | # {"message":"Invalid Application-ID or API key","status":403} (403) 24 | VerEx.new do 25 | find 'Cannot ' 26 | capture('verb') { word } 27 | find ' to ' 28 | capture('scheme') { word } 29 | find '://' 30 | capture('app_id') { word } 31 | anything_but '/' 32 | find '/' 33 | capture('api_version') { digit } 34 | find '/' 35 | capture('api_section') { word } 36 | find '/' 37 | capture('index_name') { word } 38 | find '/' 39 | capture('api_action') { word } 40 | find ': ' 41 | capture('json') do 42 | find '{' 43 | anything_but('}') 44 | find '}' 45 | end 46 | find ' (' 47 | capture('http_error') { word } 48 | find ')' 49 | end 50 | end 51 | 52 | def parse_algolia_error(error) 53 | error.delete!("\n") 54 | 55 | tester = error_tester 56 | matches = tester.match(error) 57 | 58 | return false unless matches 59 | 60 | hash = {} 61 | matches.names.each do |match| 62 | hash[match] = matches[match] 63 | end 64 | 65 | # Cast integers 66 | hash['api_version'] = hash['api_version'].to_i 67 | hash['http_error'] = hash['http_error'].to_i 68 | 69 | # Parse JSON 70 | hash['json'] = JSON.parse(hash['json']) 71 | 72 | hash 73 | end 74 | 75 | # Given an Algolia API error message, will return the best error message 76 | def readable_algolia_error(error) 77 | error = parse_algolia_error(error) 78 | return false unless error 79 | 80 | # Given API key does not have rights on the _tmp index 81 | if error['http_error'] == 403 && error['index_name'] =~ /_tmp$/ 82 | return 'check_key_acl_to_tmp_index' 83 | end 84 | 85 | false 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /lib/push.rb: -------------------------------------------------------------------------------- 1 | require 'algoliasearch' 2 | require 'nokogiri' 3 | require 'json' 4 | require_relative './version' 5 | require_relative './record_extractor' 6 | require_relative './credential_checker' 7 | require_relative './error_handler' 8 | 9 | # `jekyll algolia push` command 10 | class AlgoliaSearchJekyllPush < Jekyll::Command 11 | class << self 12 | attr_accessor :options, :config 13 | 14 | def init_with_program(_prog) 15 | end 16 | 17 | # Init the command with options passed on the command line 18 | # `jekyll algolia push ARG1 ARG2 --OPTION_NAME1 OPTION_VALUE1` 19 | # config comes from _config.yml 20 | def init_options(args = [], options = {}, config = {}) 21 | args = [] unless args 22 | @args = args 23 | @options = options 24 | @config = config 25 | @is_verbose = @config['verbose'] 26 | @is_dry_run = @config['dry_run'] 27 | 28 | self 29 | end 30 | 31 | # Check if the specified file should be indexed (we exclude static files, 32 | # robots.txt and custom defined exclusions). 33 | def indexable?(file) 34 | return false if file.is_a?(Jekyll::StaticFile) 35 | 36 | basename = File.basename(file.path) 37 | extname = File.extname(basename)[1..-1] 38 | 39 | # Keep only markdown and html files 40 | allowed_extensions = %w(html) 41 | if @config['markdown_ext'] 42 | allowed_extensions += @config['markdown_ext'].split(',') 43 | end 44 | if @config['algolia'] 45 | allowed_extensions += (@config['algolia']['allowed_extensions'] || []) 46 | end 47 | return false unless allowed_extensions.include?(extname) 48 | 49 | return false if excluded_file?(file) 50 | 51 | true 52 | end 53 | 54 | # Check if the file is in the list of excluded files 55 | def excluded_file?(file) 56 | excluded = [ 57 | %r{^page([0-9]*)/index\.html} 58 | ] 59 | if @config['algolia'] 60 | excluded += (@config['algolia']['excluded_files'] || []) 61 | end 62 | 63 | # Exclude files explicitly excluded in _config 64 | excluded.each do |pattern| 65 | pattern = /#{Regexp.quote(pattern)}/ if pattern.is_a? String 66 | return true if file.path =~ pattern 67 | end 68 | 69 | # Call user custom exclude hook on remaining files 70 | return true if custom_hook_excluded_file?(file) 71 | 72 | false 73 | end 74 | 75 | # User custom method to exclude some files when algolia.excluded_files is 76 | # not enough 77 | def custom_hook_excluded_file?(_file) 78 | false 79 | end 80 | 81 | # Return a patched version of a Jekyll instance 82 | def jekyll_new(config) 83 | site = Jekyll::Site.new(config) 84 | 85 | # Patched version of `write` that will push to Algolia instead of writing 86 | # on disk 87 | def site.write 88 | items = [] 89 | is_verbose = config['verbose'] 90 | each_site_file do |file| 91 | next unless AlgoliaSearchJekyllPush.indexable?(file) 92 | Jekyll.logger.info "Extracting data from #{file.path}" if is_verbose 93 | new_items = AlgoliaSearchRecordExtractor.new(file).extract 94 | next if new_items.nil? 95 | ap new_items if is_verbose 96 | 97 | items += new_items 98 | end 99 | AlgoliaSearchJekyllPush.push(items) 100 | end 101 | 102 | site 103 | end 104 | 105 | # Get index settings 106 | def configure_index(index) 107 | settings = { 108 | distinct: true, 109 | attributeForDistinct: 'url', 110 | attributesForFaceting: %w(tags type title), 111 | attributesToIndex: %w( 112 | title h1 h2 h3 h4 h5 h6 113 | unordered(text) 114 | unordered(tags) 115 | ), 116 | attributesToRetrieve: nil, 117 | customRanking: [ 118 | 'desc(posted_at)', 119 | 'desc(weight.tag_name)', 120 | 'asc(weight.position)' 121 | ], 122 | highlightPreTag: '', 123 | highlightPostTag: '' 124 | } 125 | 126 | # Merge default settings with user custom ones 127 | if @config['algolia'] 128 | (@config['algolia']['settings'] || []).each do |key, value| 129 | settings[key.to_sym] = value 130 | end 131 | end 132 | 133 | begin 134 | index.set_settings(settings) 135 | rescue StandardError => error 136 | display_error(error) 137 | exit 1 138 | end 139 | end 140 | 141 | # Display the error in a human-friendly way if possible 142 | def display_error(error) 143 | error_handler = AlgoliaSearchErrorHandler.new 144 | readable_error = error_handler.readable_algolia_error(error.message) 145 | 146 | if readable_error 147 | error_handler.display(readable_error) 148 | else 149 | Jekyll.logger.error 'Algolia Error: HTTP Error' 150 | Jekyll.logger.warn error.message 151 | end 152 | end 153 | 154 | # Change the User-Agent header to isolate calls from this plugin 155 | def set_user_agent_header 156 | version = AlgoliaSearchJekyllVersion.to_s 157 | Algolia.set_extra_header('User-Agent', "Algolia for Jekyll #{version}") 158 | end 159 | 160 | # Create an index to push our data 161 | def create_index(index_name) 162 | set_user_agent_header 163 | index = Algolia::Index.new(index_name) 164 | configure_index(index) unless @is_dry_run 165 | index 166 | end 167 | 168 | # Push records to the index 169 | def batch_add_items(items, index) 170 | items.each_slice(1000) do |batch| 171 | Jekyll.logger.info "Indexing #{batch.size} items" 172 | begin 173 | index.add_objects!(batch) unless @is_dry_run 174 | rescue StandardError => error 175 | display_error(error) 176 | exit 1 177 | end 178 | end 179 | end 180 | 181 | def push(items) 182 | checker = AlgoliaSearchCredentialChecker.new(@config) 183 | checker.assert_valid 184 | 185 | Jekyll.logger.info '=== DRY RUN ===' if @is_dry_run 186 | 187 | # Add items to a temp index, then rename it 188 | index_name = checker.index_name 189 | index_name_tmp = "#{index_name}_tmp" 190 | batch_add_items(items, create_index(index_name_tmp)) 191 | Algolia.move_index(index_name_tmp, index_name) unless @is_dry_run 192 | 193 | Jekyll.logger.info "Indexing of #{items.size} items " \ 194 | "in #{index_name} done." 195 | end 196 | end 197 | end 198 | -------------------------------------------------------------------------------- /lib/record_extractor.rb: -------------------------------------------------------------------------------- 1 | require 'algoliasearch' 2 | require 'nokogiri' 3 | require 'json' 4 | 5 | # Given an HTML file as input, will return an array of records to index 6 | class AlgoliaSearchRecordExtractor 7 | attr_reader :file 8 | 9 | def initialize(file) 10 | @file = file 11 | @config = file.site.config 12 | default_config = { 13 | 'record_css_selector' => 'p' 14 | } 15 | @config = default_config.merge(file.site.config['algolia']) 16 | end 17 | 18 | # Hook to modify a record after extracting 19 | def custom_hook_each(item, _node) 20 | item 21 | end 22 | 23 | # Hook to modify all records after extracting 24 | def custom_hook_all(items) 25 | items 26 | end 27 | 28 | # Returns metadata from the current file 29 | def metadata 30 | metadata = {} 31 | @file.data.each { |key, value| metadata[key.to_sym] = value } 32 | 33 | metadata[:type] = @file.class.name.split('::')[1].downcase 34 | metadata[:url] = @file.url 35 | 36 | metadata[:slug] = slug 37 | 38 | metadata[:posted_at] = @file.date.to_time.to_i if @file.respond_to? :date 39 | metadata[:tags] = tags 40 | 41 | metadata 42 | end 43 | 44 | # Returns the slug of the document 45 | def slug 46 | # Jekyll v3 has it in data 47 | return @file.data['slug'] if @file.data.key?('slug') 48 | # Old Jekyll v2 has it at the root 49 | return @file.slug if @file.respond_to? :slug 50 | # Otherwise, we guess it from the filename 51 | basename = File.basename(@file.path) 52 | extname = File.extname(basename) 53 | File.basename(basename, extname) 54 | end 55 | 56 | # Extract a list of tags 57 | def tags 58 | tags = nil 59 | 60 | # Jekyll v3 has it in data, while v2 have it at the root 61 | if @file.data.key?('tags') 62 | tags = @file.data['tags'] 63 | elsif @file.respond_to? :tags 64 | tags = @file.tags 65 | end 66 | 67 | return tags if tags.nil? 68 | 69 | # Anyway, we force cast it to string as some plugins will extend the tags to 70 | # full featured objects 71 | tags.map(&:to_s) 72 | end 73 | 74 | # Get the list of all HTML nodes to index 75 | def html_nodes 76 | document = Nokogiri::HTML(@file.content) 77 | document.css(@config['record_css_selector']) 78 | end 79 | 80 | # Check if node is a heading 81 | def node_heading?(node) 82 | %w(h1 h2 h3 h4 h5 h6).include?(node.name) 83 | end 84 | 85 | # Get the closest heading parent 86 | def node_heading_parent(node, level = 'h7') 87 | # If initially called on a heading, we only accept stronger headings 88 | level = node.name if level == 'h7' && node_heading?(node) 89 | 90 | previous = node.previous_element 91 | 92 | # No previous element, we go up to the parent 93 | unless previous 94 | parent = node.parent 95 | # No more parent, then no heading found 96 | return nil if parent.name == 'body' 97 | return node_heading_parent(parent, level) 98 | end 99 | 100 | # This is a heading, we return it 101 | return previous if node_heading?(previous) && previous.name < level 102 | 103 | node_heading_parent(previous, level) 104 | end 105 | 106 | # Get all the parent headings of the specified node 107 | # If the node itself is a heading, we include it 108 | def node_hierarchy(node, state = { level: 7 }) 109 | tag_name = node.name 110 | level = tag_name.delete('h').to_i 111 | 112 | if node_heading?(node) && level < state[:level] 113 | state[tag_name.to_sym] = node_text(node) 114 | state[:level] = level 115 | end 116 | 117 | heading = node_heading_parent(node) 118 | 119 | # No previous heading, we can stop the recursion 120 | unless heading 121 | state.delete(:level) 122 | return state 123 | end 124 | 125 | node_hierarchy(heading, state) 126 | end 127 | 128 | # Return the raw HTML of the element to index 129 | def node_raw_html(node) 130 | node.to_s 131 | end 132 | 133 | # Return the text of the element, sanitized to be displayed 134 | def node_text(node) 135 | node.content.gsub('<', '<').gsub('>', '>') 136 | end 137 | 138 | # Returns a unique string of hierarchy from title to h6, used for distinct 139 | def unique_hierarchy(data) 140 | headings = %w(title h1 h2 h3 h4 h5 h6) 141 | headings.map { |heading| data[heading.to_sym] }.compact.join(' > ') 142 | end 143 | 144 | # Returns a hash of two CSS selectors. One for the node itself, and one its 145 | # closest heading parent 146 | def node_css_selector(node) 147 | return nil if node.nil? 148 | 149 | # Use the CSS id if one is set 150 | return "##{node['id']}" if node['id'] 151 | 152 | # Default Nokogiri selector 153 | node.css_path.gsub('html > body > ', '') 154 | end 155 | 156 | # The more words are in common between this node and its parent heading, the 157 | # higher the score 158 | def weight_heading_relevance(data) 159 | # Get list of unique words in headings 160 | title_words = %i(title h1 h2 h3 h4 h5 h6) 161 | .select { |title| data.key?(title) } 162 | .map { |title| data[title].to_s.split(/\W+/) } 163 | .flatten 164 | .compact 165 | .map(&:downcase) 166 | .uniq 167 | # Intersect words in headings with words in test 168 | text_words = data[:text].downcase.split(/\W+/) 169 | (title_words & text_words).size 170 | end 171 | 172 | # Returns a weight based on the tag_name 173 | def weight_tag_name(item) 174 | tag_name = item[:tag_name] 175 | # No a heading, no weight 176 | return 0 unless %w(h1 h2 h3 h4 h5 h6).include?(tag_name) 177 | # h1: 100, h2: 90, ..., h6: 50 178 | 100 - (tag_name.delete('h').to_i - 1) * 10 179 | end 180 | 181 | # Returns an object of all weights 182 | def weight(item, index) 183 | { 184 | tag_name: weight_tag_name(item), 185 | heading_relevance: weight_heading_relevance(item), 186 | position: index 187 | } 188 | end 189 | 190 | def extract 191 | items = [] 192 | html_nodes.each_with_index do |node, index| 193 | next if node.text.empty? 194 | 195 | item = metadata.clone 196 | item.merge!(node_hierarchy(node)) 197 | item[:tag_name] = node.name 198 | item[:raw_html] = node_raw_html(node) 199 | item[:text] = node_text(node) 200 | item[:unique_hierarchy] = unique_hierarchy(item) 201 | item[:css_selector] = node_css_selector(node) 202 | item[:css_selector_parent] = node_css_selector(node_heading_parent(node)) 203 | item[:weight] = weight(item, index) 204 | 205 | # We pass item through the user defined custom hook 206 | item = custom_hook_each(item, node) 207 | next if item.nil? 208 | 209 | items << item 210 | end 211 | custom_hook_all(items) 212 | end 213 | end 214 | -------------------------------------------------------------------------------- /lib/version.rb: -------------------------------------------------------------------------------- 1 | # Expose gem version 2 | class AlgoliaSearchJekyllVersion 3 | def self.to_s 4 | '0.9.1' 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /scripts/bump_version: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../lib/version.rb' 3 | 4 | class BumpVersion 5 | def initialize(*args) 6 | @type = args[0] 7 | if !valid_type?(@type) 8 | puts "Invalid bump type: #{@type}" 9 | exit 1 10 | end 11 | end 12 | 13 | def valid_type?(type) 14 | %w(major minor patch).include?(type) 15 | end 16 | 17 | def bump(current_version, type) 18 | major, minor, patch = current_version.split('.').map(&:to_i) 19 | if type == 'major' 20 | major += 1 21 | minor = 0 22 | patch = 0 23 | end 24 | if type == 'minor' 25 | minor += 1 26 | patch = 0 27 | end 28 | patch += 1 if type == 'patch' 29 | "#{major}.#{minor}.#{patch}" 30 | end 31 | 32 | def run 33 | old_version = AlgoliaSearchJekyllVersion.to_s 34 | new_version = bump(old_version, @type) 35 | 36 | script_dir = File.expand_path(File.dirname(__FILE__)) 37 | file = File.join(script_dir, '../lib/version.rb') 38 | old_content = File.read(file) 39 | new_content = old_content.gsub(old_version, new_version) 40 | File.write(file, new_content) 41 | 42 | `git add #{file}` 43 | `git commit -m "chore(bump): Version bump to #{new_version}"` 44 | end 45 | 46 | end 47 | BumpVersion.new(*ARGV).run 48 | -------------------------------------------------------------------------------- /scripts/check_flay: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | MAX_SCORE = 45 4 | 5 | flay_lines = `flay -s ./lib/`.split("\n") 6 | 7 | errors = [] 8 | flay_lines.each_with_index do |line, index| 9 | # Skip header 10 | next if index < 2 11 | 12 | pattern = /^ *(.*): (.*)/ 13 | matches = line.match(pattern) 14 | next if matches.nil? 15 | score = matches[1].to_f 16 | 17 | next if score < MAX_SCORE 18 | errors << { 19 | score: score, 20 | file: matches[2] 21 | } 22 | end 23 | 24 | exit 0 if errors.size == 0 25 | 26 | puts 'Flay test failed:' 27 | errors.sort_by { |a| a[:score] }.each do |error| 28 | puts "#{error[:score]} / #{MAX_SCORE} in #{error[:file]}" 29 | end 30 | exit 1 31 | -------------------------------------------------------------------------------- /scripts/check_flog: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | MAX_SCORE = 45 4 | 5 | flog_lines = `flog ./lib/`.split("\n") 6 | 7 | errors = [] 8 | flog_lines.each_with_index do |line, index| 9 | # Skip header 10 | next if index < 3 11 | 12 | pattern = /^ *(.*): (.*) (.*):[0-9]*/ 13 | matches = line.match(pattern) 14 | next if matches.nil? 15 | score = matches[1].to_f 16 | 17 | next if score < MAX_SCORE 18 | errors << { 19 | score: score, 20 | method: matches[2], 21 | file: matches[3] 22 | } 23 | end 24 | 25 | exit 0 if errors.size == 0 26 | 27 | puts 'Flog test failed:' 28 | errors.sort_by { |a| a[:score] }.each do |error| 29 | puts "#{error[:score]} / #{MAX_SCORE}: #{error[:method]} in #{error[:file]}" 30 | end 31 | exit 1 32 | -------------------------------------------------------------------------------- /scripts/coverage: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | COVERAGE=1 appraisal jekyll-v2 bundle exec rspec 4 | -------------------------------------------------------------------------------- /scripts/git_hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Succeed fast if we did not change any ruby file 4 | if ! git status --short | grep -q '\.rb$'; then 5 | exit 0 6 | fi 7 | 8 | # Do not commit any focused or excluded tests 9 | if grep --color -r 'spec' -E -e '^( |\t)*(fit|fdescribe|xit|xdescribe)'; then 10 | echo '✘ You have focused and/or skipped tests' 11 | exit 1 12 | fi 13 | 14 | # Match style guide 15 | ./scripts/lint || exit 1 16 | 17 | -------------------------------------------------------------------------------- /scripts/git_hooks/pre-push: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ./scripts/test || exit 1 4 | 5 | # No over-complex methods 6 | ./scripts/check_flog || exit 1 7 | 8 | # No duplication 9 | ./scripts/check_flay 10 | -------------------------------------------------------------------------------- /scripts/lint: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | rubocop -F './lib/' './spec' 3 | -------------------------------------------------------------------------------- /scripts/release: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | git checkout master || exit 1 4 | git pull || exit 1 5 | bundle install && appraisal install 6 | 7 | git rebase develop || exit 1 8 | bundle install && appraisal install 9 | rake release || exit 1 10 | 11 | git checkout develop || exit 1 12 | bundle install && appraisal install 13 | git rebase master || exit 1 14 | bundle install && appraisal install 15 | -------------------------------------------------------------------------------- /scripts/test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | cd "$(dirname "$BASH_SOURCE")" 3 | 4 | ./test_v2 && ./test_v3 5 | -------------------------------------------------------------------------------- /scripts/test_ci: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # This script will be started by Travis, in the correct context (matrix of Ruby 3 | # version + Gemfile version), so it only needs to load the tests, without 4 | # worrying about appraisal 5 | cd "$(dirname "$BASH_SOURCE")"/.. 6 | 7 | COVERAGE=1 bundle exec rspec 8 | -------------------------------------------------------------------------------- /scripts/test_v2: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | cd "$(dirname "$BASH_SOURCE")"/.. 3 | 4 | echo "Testing under Jekyll 2.5" 5 | COVERAGE=1 appraisal jekyll-v2 bundle exec rspec 6 | -------------------------------------------------------------------------------- /scripts/test_v3: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | cd "$(dirname "$BASH_SOURCE")"/.. 3 | 4 | echo "Testing under Jekyll 3.0" 5 | COVERAGE=1 appraisal jekyll-v3 bundle exec rspec 6 | 7 | -------------------------------------------------------------------------------- /scripts/watch: -------------------------------------------------------------------------------- 1 | ./watch_v2 -------------------------------------------------------------------------------- /scripts/watch_v2: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | cd "$(dirname "$BASH_SOURCE")"/.. 3 | 4 | guard --guardfile ./Guardfile_jekyllv2 5 | -------------------------------------------------------------------------------- /scripts/watch_v3: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | cd "$(dirname "$BASH_SOURCE")"/.. 3 | guard --guardfile ./Guardfile_jekyllv3 4 | 5 | -------------------------------------------------------------------------------- /spec/credential_checker_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe(AlgoliaSearchCredentialChecker) do 4 | let(:config) do 5 | { 6 | 'source' => fixture_path, 7 | 'markdown_ext' => 'md,mkd', 8 | 'algolia' => { 9 | 'application_id' => 'APPID', 10 | 'index_name' => 'INDEXNAME' 11 | } 12 | } 13 | end 14 | let(:checker) { AlgoliaSearchCredentialChecker.new(config) } 15 | 16 | describe 'api_key' do 17 | it 'returns nil if no key found' do 18 | # Given 19 | 20 | # When 21 | actual = checker.api_key 22 | 23 | # Then 24 | expect(actual).to be_nil 25 | end 26 | 27 | it 'reads from ENV var if set' do 28 | # Given 29 | stub_const('ENV', 'ALGOLIA_API_KEY' => 'APIKEY_FROM_ENV') 30 | 31 | # When 32 | actual = checker.api_key 33 | 34 | # Then 35 | expect(actual).to eq 'APIKEY_FROM_ENV' 36 | end 37 | 38 | it 'reads from _algolia_api_key in source if set' do 39 | # Given 40 | checker.config['source'] = File.join(config['source'], 'api_key_dir') 41 | 42 | # When 43 | actual = checker.api_key 44 | 45 | # Then 46 | expect(actual).to eq 'APIKEY_FROM_FILE' 47 | end 48 | 49 | it 'reads from ENV before from file' do 50 | # Given 51 | checker.config['source'] = File.join(config['source'], 'api_key_dir') 52 | stub_const('ENV', 'ALGOLIA_API_KEY' => 'APIKEY_FROM_ENV') 53 | 54 | # When 55 | actual = checker.api_key 56 | 57 | # Then 58 | expect(actual).to eq 'APIKEY_FROM_ENV' 59 | end 60 | end 61 | 62 | describe 'check_api_key' do 63 | it 'should exit with error if no API key' do 64 | # Given 65 | allow(checker).to receive(:api_key).and_return(nil) 66 | allow(checker.logger).to receive(:display) 67 | 68 | # When / Then 69 | expect(-> { checker.check_api_key }).to raise_error SystemExit 70 | end 71 | 72 | it 'should do nothing when an API key is found' do 73 | # Given 74 | allow(checker).to receive(:api_key).and_return('APIKEY') 75 | 76 | # When / Then 77 | expect(-> { checker.check_api_key }).not_to raise_error 78 | end 79 | end 80 | 81 | describe 'application_id' do 82 | it 'reads value from the _config.yml file' do 83 | # Given 84 | 85 | # When 86 | actual = checker.application_id 87 | 88 | # Then 89 | expect(actual).to eq 'APPID' 90 | end 91 | 92 | it 'reads from ENV var if set' do 93 | # Given 94 | stub_const('ENV', 'ALGOLIA_APPLICATION_ID' => 'APPLICATION_ID_FROM_ENV') 95 | 96 | # When 97 | actual = checker.application_id 98 | 99 | # Then 100 | expect(actual).to eq 'APPLICATION_ID_FROM_ENV' 101 | end 102 | 103 | it 'returns nil if no key found' do 104 | # Given 105 | config['algolia']['application_id'] = nil 106 | 107 | # When 108 | actual = checker.application_id 109 | 110 | # Then 111 | expect(actual).to be_nil 112 | end 113 | end 114 | 115 | describe 'check_application_id' do 116 | it 'should exit with error if no application ID' do 117 | # Given 118 | allow(checker).to receive(:application_id).and_return(nil) 119 | allow(checker.logger).to receive(:display) 120 | 121 | # When / Then 122 | expect(-> { checker.check_application_id }).to raise_error SystemExit 123 | end 124 | 125 | it 'should do nothing when an application ID is found' do 126 | # Given 127 | allow(checker).to receive(:application_id).and_return('APPLICATIONID') 128 | 129 | # When / Then 130 | expect(-> { checker.check_application_id }).not_to raise_error 131 | end 132 | end 133 | 134 | describe 'index_name' do 135 | it 'reads value from the _config.yml file' do 136 | # Given 137 | 138 | # When 139 | actual = checker.index_name 140 | 141 | # Then 142 | expect(actual).to eq 'INDEXNAME' 143 | end 144 | 145 | it 'reads from ENV var if set' do 146 | # Given 147 | stub_const('ENV', 'ALGOLIA_INDEX_NAME' => 'INDEX_NAME_FROM_ENV') 148 | 149 | # When 150 | actual = checker.index_name 151 | 152 | # Then 153 | expect(actual).to eq 'INDEX_NAME_FROM_ENV' 154 | end 155 | 156 | it 'returns nil if no key found' do 157 | # Given 158 | config['algolia']['index_name'] = nil 159 | 160 | # When 161 | actual = checker.index_name 162 | 163 | # Then 164 | expect(actual).to be_nil 165 | end 166 | end 167 | describe 'check_index_name' do 168 | it 'should exit with error if no index name' do 169 | # Given 170 | allow(checker).to receive(:index_name).and_return(nil) 171 | allow(checker.logger).to receive(:display) 172 | 173 | # When / Then 174 | expect(-> { checker.check_index_name }).to raise_error SystemExit 175 | end 176 | 177 | it 'should do nothing when an index name is found' do 178 | # Given 179 | allow(checker).to receive(:index_name).and_return('INDEXNAME') 180 | 181 | # When / Then 182 | expect(-> { checker.check_index_name }).not_to raise_error 183 | end 184 | end 185 | 186 | describe 'assert_valid' do 187 | before(:each) do 188 | allow(checker.logger).to receive(:display) 189 | end 190 | it 'should display error if no api key' do 191 | # Given 192 | allow(checker).to receive(:api_key).and_return nil 193 | 194 | # Then 195 | expect(-> { checker.assert_valid }).to raise_error SystemExit 196 | expect(checker.logger).to have_received(:display).with('api_key_missing') 197 | end 198 | 199 | it 'should display error if no application id' do 200 | # Given 201 | checker.config['algolia'] = { 202 | 'application_id' => nil, 203 | 'index_name' => 'INDEX_NAME' 204 | } 205 | stub_const('ENV', 'ALGOLIA_API_KEY' => 'APIKEY_FROM_ENV') 206 | 207 | # Then 208 | expect(-> { checker.assert_valid }).to raise_error SystemExit 209 | expect(checker.logger) 210 | .to have_received(:display) 211 | .with('application_id_missing') 212 | end 213 | 214 | it 'should display error if no index name' do 215 | # Given 216 | checker.config['algolia'] = { 217 | 'application_id' => 'APPLICATION_ID', 218 | 'index_name' => nil 219 | } 220 | stub_const('ENV', 'ALGOLIA_API_KEY' => 'APIKEY_FROM_ENV') 221 | 222 | # Then 223 | expect(-> { checker.assert_valid }).to raise_error SystemExit 224 | expect(checker.logger) 225 | .to have_received(:display) 226 | .with('index_name_missing') 227 | end 228 | 229 | it 'should init the Algolia client' do 230 | # Given 231 | allow(checker).to receive(:application_id).and_return('FOO') 232 | allow(checker).to receive(:api_key).and_return('BAR') 233 | allow(Algolia).to receive(:init) 234 | 235 | # When 236 | checker.assert_valid 237 | 238 | # Then 239 | expect(Algolia).to have_received(:init).with( 240 | application_id: 'FOO', 241 | api_key: 'BAR' 242 | ) 243 | end 244 | end 245 | end 246 | -------------------------------------------------------------------------------- /spec/error_handler_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe(AlgoliaSearchErrorHandler) do 4 | before(:each) do 5 | @error_handler = AlgoliaSearchErrorHandler.new 6 | end 7 | 8 | describe 'display' do 9 | before(:each) do 10 | allow(Jekyll.logger).to receive(:error) 11 | allow(Jekyll.logger).to receive(:warn) 12 | end 13 | 14 | it 'should display first line as error' do 15 | # Given 16 | input = 'sample' 17 | 18 | # When 19 | @error_handler.display(input) 20 | 21 | # Then 22 | expect(Jekyll.logger).to have_received(:error).exactly(1).times 23 | end 24 | 25 | it 'should display all other lines as warnings' do 26 | # Given 27 | input = 'sample' 28 | 29 | # When 30 | @error_handler.display(input) 31 | 32 | # Then 33 | expect(Jekyll.logger).to have_received(:warn).exactly(3).times 34 | end 35 | end 36 | 37 | describe 'parse_algolia_error' do 38 | before(:each) do 39 | @algolia_error = 'Cannot PUT to ' \ 40 | 'https://appid.algolia.net/1/indexes/index_name/settings: ' \ 41 | '{"message":"Invalid Application-ID or API key","status":403} (403)' 42 | end 43 | 44 | it 'should extract all the url parts' do 45 | # Given 46 | input = @algolia_error 47 | 48 | # When 49 | actual = @error_handler.parse_algolia_error(input) 50 | 51 | # Then 52 | expect(actual['verb']).to eq 'PUT' 53 | expect(actual['scheme']).to eq 'https' 54 | expect(actual['app_id']).to eq 'appid' 55 | expect(actual['api_section']).to eq 'indexes' 56 | expect(actual['index_name']).to eq 'index_name' 57 | expect(actual['api_action']).to eq 'settings' 58 | end 59 | 60 | it 'should cast integers to integers' do 61 | # Given 62 | input = @algolia_error 63 | 64 | # When 65 | actual = @error_handler.parse_algolia_error(input) 66 | 67 | # Then 68 | expect(actual['api_version']).to eq 1 69 | expect(actual['http_error']).to eq 403 70 | end 71 | 72 | it 'should parse the JSON part' do 73 | # Given 74 | input = @algolia_error 75 | 76 | # When 77 | actual = @error_handler.parse_algolia_error(input) 78 | 79 | # Then 80 | expect(actual['json']).to be_a(Hash) 81 | expect(actual['json']['status']).to eq 403 82 | end 83 | 84 | it 'should return false if this is not parsable' do 85 | # Given 86 | input = 'foo bar baz' 87 | 88 | # When 89 | actual = @error_handler.parse_algolia_error(input) 90 | 91 | # Then 92 | expect(actual).to eq(false) 93 | end 94 | 95 | it 'should work on multiline errors' do 96 | # Given 97 | input = @algolia_error.gsub('}', "}\n") 98 | 99 | # When 100 | actual = @error_handler.parse_algolia_error(input) 101 | 102 | # Then 103 | expect(actual).to be_a(Hash) 104 | expect(actual['http_error']).to eq 403 105 | end 106 | end 107 | 108 | describe 'readable_algolia_error' do 109 | it 'should warn about key ACL' do 110 | # Given 111 | parsed = { 112 | 'http_error' => 403, 113 | 'index_name' => 'something_tmp' 114 | } 115 | allow(@error_handler).to receive(:parse_algolia_error).and_return(parsed) 116 | 117 | # When 118 | actual = @error_handler.readable_algolia_error('error') 119 | 120 | # Then 121 | expect(actual).to eq('check_key_acl_to_tmp_index') 122 | end 123 | 124 | it 'should return false if no nice message found' do 125 | # Given 126 | parsed = false 127 | allow(@error_handler).to receive(:parse_algolia_error).and_return(parsed) 128 | 129 | # When 130 | actual = @error_handler.readable_algolia_error('error') 131 | 132 | # Then 133 | expect(actual).to eq(false) 134 | end 135 | 136 | it 'should return false if message does not match any known case' do 137 | # Given 138 | parsed = { 139 | 'http_error' => 42 140 | } 141 | allow(@error_handler).to receive(:parse_algolia_error).and_return(parsed) 142 | 143 | # When 144 | actual = @error_handler.readable_algolia_error('error') 145 | 146 | # Then 147 | expect(actual).to eq(false) 148 | end 149 | end 150 | end 151 | -------------------------------------------------------------------------------- /spec/fixtures/jekyll_version_2/_config.yml: -------------------------------------------------------------------------------- 1 | collections: 2 | my-collection: 3 | output: true 4 | markdown_ext: 'md,mkd' 5 | paginate: 1 6 | timezone: Europe/Paris 7 | 8 | gems: 9 | - jekyll-paginate 10 | 11 | algolia: 12 | application_id: APPID 13 | index_name: INDEXNAME 14 | excluded_files: 15 | - excluded.html 16 | 17 | -------------------------------------------------------------------------------- /spec/fixtures/jekyll_version_2/_layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ content }} 5 | 6 | 7 | -------------------------------------------------------------------------------- /spec/fixtures/jekyll_version_2/_my-collection/collection-item.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: Collection Item 3 | --- 4 | 5 |

The grandest of omelettes. Those that feast on dragon eggs often find that there 6 | is very little they would not dare to do.

7 | -------------------------------------------------------------------------------- /spec/fixtures/jekyll_version_2/_my-collection/collection-item.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Collection Item 3 | custom: Foo 4 | --- 5 | 6 | The grandest of omelettes. Those that feast on dragon eggs often find that there 7 | is very little they would not dare to do. 8 | -------------------------------------------------------------------------------- /spec/fixtures/jekyll_version_2/_posts/2015-07-02-test-post.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Test post" 3 | tags: 4 | - tag 5 | - another tag 6 | custom: Foo 7 | --- 8 | 9 | Introduction text that also includes [some link](https://www.algolia.com). To 10 | add a bit of fancy, we will also __bold__ and _italicize_ some text. 11 | 12 | # Main title 13 | 14 | We like writing stuff and then indexing it in a very fast engine. Here is why 15 | a fast engine is good for you: 16 | 17 | * fast 18 | * fast 19 | * fast 20 | * and fast 21 | 22 | ## Built with hands 23 | 24 | All this text was typed with my own hands, on my own keyboard. I also did use 25 | a Chair© and a Table™. 26 | 27 | ## Features 28 | 29 | The whole plugin is composed of parts of `code`, and sometime even 30 | <code>. 31 | 32 | Code is __✔ checked__ and errors are __✘ deleted__. 33 | -------------------------------------------------------------------------------- /spec/fixtures/jekyll_version_2/_posts/2015-07-03-test-post-again.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Test post again" 3 | --- 4 | 5 | The goal of this post is simply to trigger pagination, and see that we do not 6 | index the pagination results. 7 | 8 | -------------------------------------------------------------------------------- /spec/fixtures/jekyll_version_2/about.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: About page 3 | custom: Foo 4 | --- 5 | 6 | # Heading 1 7 | 8 | Text 1 9 | 10 | ## Heading 2 11 | 12 | Text 2 13 | 14 | ### Heading 3 15 | 16 | Text 3 17 | 18 | - item 1 19 | - item 2 20 | - item 3 21 | 22 | ### Another Heading 3 23 | 24 |

Another text 4

25 | 26 |

Another Heading 2

27 | 28 | Another `` 5 29 | 30 | ### Last Heading 3 31 | 32 |
Just a div
33 | 34 |

Last text 6

35 | -------------------------------------------------------------------------------- /spec/fixtures/jekyll_version_2/api_key_dir/_algolia_api_key: -------------------------------------------------------------------------------- 1 | APIKEY_FROM_FILE 2 | -------------------------------------------------------------------------------- /spec/fixtures/jekyll_version_2/assets/ring.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algolia/algoliasearch-jekyll/cb21e83402de3cea46201fe12bd1b9a2eaacebfb/spec/fixtures/jekyll_version_2/assets/ring.png -------------------------------------------------------------------------------- /spec/fixtures/jekyll_version_2/authors.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: Authors 3 | --- 4 | 5 |

This is an HTML page

6 | -------------------------------------------------------------------------------- /spec/fixtures/jekyll_version_2/excluded.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: Excluded file 3 | --- 4 | 5 |

This should not be indexed

6 | -------------------------------------------------------------------------------- /spec/fixtures/jekyll_version_2/hierarchy.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Hierarchy test 3 | --- 4 | 5 | # H1 6 | 7 | TEXT1-H1 8 | 9 | ## H2A 10 | 11 | TEXT2-H2A-H1 12 | 13 | TEXT3-H2A-H1 14 | 15 | ## H2B 16 | 17 | TEXT4-H2B-H1 18 | 19 | ### H3A 20 | 21 | TEXT5-H3-H2B-H1 22 | 23 |
24 |

H4

25 |

TEXT7-H4-H3-H2B-H1

26 |
27 | 28 | ## H2C 29 | 30 | TEXT8-H2C-H1 31 | 32 | ### H3B `` 33 | 34 | TEXT9-H3B-H2C-H1 35 | 36 | -------------------------------------------------------------------------------- /spec/fixtures/jekyll_version_2/index.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Home 4 | --- 5 | 6 | This default index page is used to display the paginated posts 7 | 8 | {% for post in paginator.posts %} 9 | 10 | {{ post.title }} 11 | 12 | {{ post.content }} 13 | {% endfor %} 14 | -------------------------------------------------------------------------------- /spec/fixtures/jekyll_version_2/page2/index.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | --- 4 | 5 | This pagination page should not be indexed. 6 | 7 | -------------------------------------------------------------------------------- /spec/fixtures/jekyll_version_2/weight.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Weight test 3 | --- 4 | 5 | # AAA BBB CCC DDD 6 | 7 | aaa xxx aaa xxx aaa 8 | 9 | ## AAA BBB 10 | 11 | aaa bbb 12 | 13 | ## CCC DDD 14 | 15 | ccc ddd 16 | 17 | ### DDD 18 | 19 | aaa bbb ccc dddd 20 | -------------------------------------------------------------------------------- /spec/fixtures/jekyll_version_3/_config.yml: -------------------------------------------------------------------------------- 1 | collections: 2 | my-collection: 3 | output: true 4 | markdown_ext: 'md,mkd' 5 | paginate: 1 6 | timezone: Europe/Paris 7 | 8 | algolia: 9 | application_id: APPID 10 | index_name: INDEXNAME 11 | excluded_files: 12 | - excluded.html 13 | 14 | # Jekyll 3.0 extracted the secondary features into their own plugins 15 | plugins: 16 | - jekyll-paginate 17 | 18 | -------------------------------------------------------------------------------- /spec/fixtures/jekyll_version_3/_layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ content }} 5 | 6 | 7 | -------------------------------------------------------------------------------- /spec/fixtures/jekyll_version_3/_my-collection/collection-item.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: Collection Item 3 | --- 4 | 5 |

The grandest of omelettes. Those that feast on dragon eggs often find that there 6 | is very little they would not dare to do.

7 | -------------------------------------------------------------------------------- /spec/fixtures/jekyll_version_3/_my-collection/collection-item.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Collection Item 3 | custom: Foo 4 | --- 5 | 6 | The grandest of omelettes. Those that feast on dragon eggs often find that there 7 | is very little they would not dare to do. 8 | -------------------------------------------------------------------------------- /spec/fixtures/jekyll_version_3/_posts/2015-07-02-test-post.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Test post" 3 | tags: 4 | - tag 5 | - another tag 6 | custom: Foo 7 | --- 8 | 9 | Introduction text that also includes [some link](https://www.algolia.com). To 10 | add a bit of fancy, we will also __bold__ and _italicize_ some text. 11 | 12 | # Main title 13 | 14 | We like writing stuff and then indexing it in a very fast engine. Here is why 15 | a fast engine is good for you: 16 | 17 | * fast 18 | * fast 19 | * fast 20 | * and fast 21 | 22 | ## Built with hands 23 | 24 | All this text was typed with my own hands, on my own keyboard. I also did use 25 | a Chair© and a Table™. 26 | 27 | ## Features 28 | 29 | The whole plugin is composed of parts of `code`, and sometime even 30 | <code>. 31 | 32 | Code is __✔ checked__ and errors are __✘ deleted__. 33 | -------------------------------------------------------------------------------- /spec/fixtures/jekyll_version_3/_posts/2015-07-03-test-post-again.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Test post again" 3 | --- 4 | 5 | The goal of this post is simply to trigger pagination, and see that we do not 6 | index the pagination results. 7 | 8 | -------------------------------------------------------------------------------- /spec/fixtures/jekyll_version_3/about.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: About page 3 | custom: Foo 4 | --- 5 | 6 | # Heading 1 7 | 8 | Text 1 9 | 10 | ## Heading 2 11 | 12 | Text 2 13 | 14 | ### Heading 3 15 | 16 | Text 3 17 | 18 | - item 1 19 | - item 2 20 | - item 3 21 | 22 | ### Another Heading 3 23 | 24 |

Another text 4

25 | 26 |

Another Heading 2

27 | 28 | Another `` 5 29 | 30 | ### Last Heading 3 31 | 32 |
Just a div
33 | 34 |

Last text 6

35 | -------------------------------------------------------------------------------- /spec/fixtures/jekyll_version_3/api_key_dir/_algolia_api_key: -------------------------------------------------------------------------------- 1 | APIKEY_FROM_FILE 2 | -------------------------------------------------------------------------------- /spec/fixtures/jekyll_version_3/assets/ring.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algolia/algoliasearch-jekyll/cb21e83402de3cea46201fe12bd1b9a2eaacebfb/spec/fixtures/jekyll_version_3/assets/ring.png -------------------------------------------------------------------------------- /spec/fixtures/jekyll_version_3/authors.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: Authors 3 | --- 4 | 5 |

This is an HTML page

6 | -------------------------------------------------------------------------------- /spec/fixtures/jekyll_version_3/excluded.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: Excluded file 3 | --- 4 | 5 |

This should not be indexed

6 | -------------------------------------------------------------------------------- /spec/fixtures/jekyll_version_3/hierarchy.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Hierarchy test 3 | --- 4 | 5 | # H1 6 | 7 | TEXT1-H1 8 | 9 | ## H2A 10 | 11 | TEXT2-H2A-H1 12 | 13 | TEXT3-H2A-H1 14 | 15 | ## H2B 16 | 17 | TEXT4-H2B-H1 18 | 19 | ### H3A 20 | 21 | TEXT5-H3-H2B-H1 22 | 23 |
24 |

H4

25 |

TEXT7-H4-H3-H2B-H1

26 |
27 | 28 | ## H2C 29 | 30 | TEXT8-H2C-H1 31 | 32 | ### H3B `` 33 | 34 | TEXT9-H3B-H2C-H1 35 | 36 | -------------------------------------------------------------------------------- /spec/fixtures/jekyll_version_3/index.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Home 4 | --- 5 | 6 | This default index page is used to display the paginated posts 7 | 8 | {% for post in paginator.posts %} 9 | 10 | {{ post.title }} 11 | 12 | {{ post.content }} 13 | {% endfor %} 14 | -------------------------------------------------------------------------------- /spec/fixtures/jekyll_version_3/page2/index.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | --- 4 | 5 | This pagination page should not be indexed. 6 | 7 | -------------------------------------------------------------------------------- /spec/fixtures/jekyll_version_3/weight.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Weight test 3 | --- 4 | 5 | # AAA BBB CCC DDD 6 | 7 | aaa xxx aaa xxx aaa 8 | 9 | ## AAA BBB 10 | 11 | aaa bbb 12 | 13 | ## CCC DDD 14 | 15 | ccc ddd 16 | 17 | ### DDD 18 | 19 | aaa bbb ccc dddd 20 | -------------------------------------------------------------------------------- /spec/push_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe(AlgoliaSearchJekyllPush) do 4 | let(:push) { AlgoliaSearchJekyllPush } 5 | let(:site) { get_site } 6 | let(:page_file) { site.file_by_name('about.md') } 7 | let(:html_page_file) { site.file_by_name('authors.html') } 8 | let(:excluded_page_file) { site.file_by_name('excluded.html') } 9 | let(:post_file) { site.file_by_name('2015-07-02-test-post.md') } 10 | let(:static_file) { site.file_by_name('ring.png') } 11 | let(:document_file) { site.file_by_name('collection-item.md') } 12 | let(:html_document_file) { site.file_by_name('collection-item.html') } 13 | let(:pagination_page) { site.file_by_name('page2/index.html') } 14 | let(:items) do 15 | [{ 16 | name: 'foo', 17 | url: '/foo' 18 | }, { 19 | name: 'bar', 20 | url: '/bar' 21 | }] 22 | end 23 | 24 | describe 'init_options' do 25 | it 'sets options and config' do 26 | # Given 27 | args = nil 28 | options = { 'foo' => 'bar' } 29 | config = { 'bar' => 'foo' } 30 | 31 | # When 32 | push.init_options(args, options, config) 33 | 34 | # Then 35 | expect(push.options).to include(options) 36 | expect(push.config).to include(config) 37 | end 38 | end 39 | 40 | describe 'indexable?' do 41 | it 'exclude StaticFiles' do 42 | expect(push.indexable?(static_file)).to eq false 43 | end 44 | 45 | it 'keeps markdown files' do 46 | expect(push.indexable?(page_file)).to eq true 47 | end 48 | 49 | it 'keeps html files' do 50 | expect(push.indexable?(html_page_file)).to eq true 51 | end 52 | 53 | it 'keeps markdown documents' do 54 | expect(push.indexable?(document_file)).to eq true 55 | end 56 | 57 | it 'keeps html documents' do 58 | expect(push.indexable?(html_document_file)).to eq true 59 | end 60 | 61 | it 'exclude file specified in config' do 62 | expect(push.indexable?(excluded_page_file)).to eq false 63 | end 64 | 65 | it 'does not index pagination pages' do 66 | expect(push.indexable?(pagination_page)).to eq false 67 | end 68 | end 69 | 70 | describe 'excluded_files?' do 71 | before(:each) do 72 | push.init_options(nil, {}, site.config) 73 | end 74 | 75 | it 'should not exclude normal pages' do 76 | expect(push.excluded_file?(html_page_file)).to eq false 77 | end 78 | 79 | it 'should alway exclude pagination pages' do 80 | expect(push.excluded_file?(pagination_page)).to eq true 81 | end 82 | 83 | it 'should exclude user specified strings' do 84 | expect(push.excluded_file?(excluded_page_file)).to eq true 85 | end 86 | end 87 | 88 | describe 'custom_hook_excluded_file?' do 89 | it 'let the user call a custom hook to exclude some files' do 90 | # Given 91 | def push.custom_hook_excluded_file?(_file) 92 | true 93 | end 94 | 95 | # Then 96 | expect(push.excluded_file?(html_page_file)).to eq true 97 | end 98 | end 99 | 100 | describe 'configure_index' do 101 | it 'sets some sane defaults' do 102 | # Given 103 | push.init_options(nil, {}, {}) 104 | index = double 105 | 106 | # Then 107 | expected = { 108 | attributeForDistinct: 'url', 109 | distinct: true, 110 | customRanking: [ 111 | 'desc(posted_at)', 112 | 'desc(weight.tag_name)', 113 | 'asc(weight.position)' 114 | ] 115 | } 116 | expect(index).to receive(:set_settings).with(hash_including(expected)) 117 | 118 | # When 119 | push.configure_index(index) 120 | end 121 | 122 | it 'allow user to override all settings' do 123 | # Given 124 | settings = { 125 | distinct: false, 126 | customSetting: 'foo', 127 | customRanking: ['asc(foo)', 'desc(bar)'] 128 | } 129 | config = { 130 | 'algolia' => { 131 | 'settings' => settings 132 | } 133 | } 134 | push.init_options(nil, {}, config) 135 | index = double 136 | 137 | # Then 138 | expect(index).to receive(:set_settings).with(hash_including(settings)) 139 | 140 | # When 141 | push.configure_index(index) 142 | end 143 | 144 | describe 'throw an error' do 145 | before(:each) do 146 | @index_double = double('Algolia Index').as_null_object 147 | @error_handler_double = double('Error Handler double').as_null_object 148 | push.init_options(nil, {}, {}) 149 | allow(@index_double).to receive(:set_settings).and_raise 150 | allow(Jekyll.logger).to receive(:error) 151 | end 152 | 153 | it 'stops if API throw an error' do 154 | # Given 155 | 156 | # When 157 | 158 | # Then 159 | expect(-> { push.configure_index(@index_double) }) 160 | .to raise_error SystemExit 161 | end 162 | 163 | it 'displays the error directly if unknown' do 164 | # Given 165 | allow(@error_handler_double) 166 | .to receive(:readable_algolia_error).and_return false 167 | allow(@error_handler_double) 168 | .to receive(:display) 169 | allow(AlgoliaSearchErrorHandler) 170 | .to receive(:new).and_return(@error_handler_double) 171 | 172 | # When 173 | 174 | # Then 175 | expect(-> { push.configure_index(@index_double) }) 176 | .to raise_error SystemExit 177 | expect(@error_handler_double) 178 | .to have_received(:display).exactly(0).times 179 | expect(Jekyll.logger) 180 | .to have_received(:error).with('Algolia Error: HTTP Error') 181 | end 182 | 183 | it 'display a human readable version of the error if one is found' do 184 | # Given 185 | allow(@error_handler_double) 186 | .to receive(:readable_algolia_error).and_return 'known_errors' 187 | allow(@error_handler_double) 188 | .to receive(:display) 189 | allow(AlgoliaSearchErrorHandler) 190 | .to receive(:new).and_return(@error_handler_double) 191 | 192 | # When 193 | 194 | # Then 195 | expect(-> { push.configure_index(@index_double) }) 196 | .to raise_error SystemExit 197 | expect(@error_handler_double) 198 | .to have_received(:display) 199 | .exactly(1).times 200 | .with('known_errors') 201 | end 202 | end 203 | end 204 | 205 | describe 'jekyll_new' do 206 | it 'should return a patched version of site with a custom write' do 207 | # Given 208 | normal_site = Jekyll::Site.new(Jekyll.configuration) 209 | normal_method = normal_site.method(:write).source_location 210 | 211 | patched_site = get_site({}, mock_write_method: false, process: false) 212 | patched_method = patched_site.method(:write).source_location 213 | 214 | # When 215 | # Then 216 | expect(patched_method).not_to eq normal_method 217 | end 218 | end 219 | 220 | describe 'process' do 221 | it 'should call the site write method' do 222 | # Given 223 | site = get_site({}, process: false) 224 | 225 | # When 226 | site.process 227 | 228 | # Then 229 | expect(site).to have_received(:write) 230 | end 231 | 232 | it 'should push items to Algolia' do 233 | # Given 234 | site = get_site({}, mock_write_method: false, process: false) 235 | # Keep only page_file 236 | allow(AlgoliaSearchJekyllPush).to receive(:indexable?) do |file| 237 | file.path == page_file.path 238 | end 239 | allow(AlgoliaSearchJekyllPush).to receive(:push) 240 | 241 | # When 242 | site.process 243 | 244 | # Then 245 | expect(AlgoliaSearchJekyllPush).to have_received(:push) do |arg| 246 | expect(arg.size).to eq 6 247 | end 248 | end 249 | end 250 | 251 | describe 'set_user_agent_header' do 252 | before(:each) do 253 | allow(Algolia).to receive(:set_extra_header) 254 | end 255 | 256 | it 'should set a User-Agent with the plugin name and version' do 257 | # Given 258 | allow(AlgoliaSearchJekyllVersion).to receive(:to_s).and_return '99.42' 259 | 260 | # When 261 | push.set_user_agent_header 262 | 263 | # Then 264 | expect(Algolia).to have_received(:set_extra_header).with( 265 | 'User-Agent', 266 | /Jekyll(.*)99.42/ 267 | ) 268 | end 269 | end 270 | 271 | describe 'push' do 272 | let(:index_double) { double('Algolia Index').as_null_object } 273 | let(:config) do 274 | { 275 | 'algolia' => { 276 | 'index_name' => 'INDEXNAME' 277 | } 278 | } 279 | end 280 | 281 | before(:each) do 282 | push.init_options(nil, {}, config) 283 | # Mock all calls to not send anything 284 | allow_any_instance_of(AlgoliaSearchCredentialChecker) 285 | .to receive(:assert_valid) 286 | allow(Algolia).to receive(:set_extra_header) 287 | allow(Algolia).to receive(:init) 288 | allow(Algolia).to receive(:move_index) 289 | allow(Algolia::Index).to receive(:new).and_return(index_double) 290 | allow(Jekyll.logger).to receive(:info) 291 | end 292 | 293 | it 'should create a temporary index' do 294 | # Given 295 | 296 | # When 297 | push.push(items) 298 | 299 | # Then 300 | expect(Algolia::Index).to have_received(:new).with('INDEXNAME_tmp') 301 | end 302 | 303 | it 'should add elements to the temporary index' do 304 | # Given 305 | 306 | # When 307 | push.push(items) 308 | 309 | # Then 310 | expect(index_double).to have_received(:add_objects!) 311 | end 312 | 313 | it 'should move the temporary index as the main one' do 314 | # Given 315 | 316 | # When 317 | push.push(items) 318 | 319 | # Then 320 | expect(Algolia).to have_received(:move_index) 321 | .with('INDEXNAME_tmp', 'INDEXNAME') 322 | end 323 | 324 | it 'should display the number of elements indexed' do 325 | # Given 326 | 327 | # When 328 | push.push(items) 329 | 330 | # Then 331 | expect(Jekyll.logger).to have_received(:info).with(/of 2 items/i) 332 | end 333 | 334 | it 'should display an error if `add_objects!` failed' do 335 | allow(index_double).to receive(:add_objects!).and_raise 336 | 337 | expect(Jekyll.logger).to receive(:error) 338 | expect(-> { push.push(items) }).to raise_error SystemExit 339 | end 340 | end 341 | end 342 | -------------------------------------------------------------------------------- /spec/record_extractor_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe(AlgoliaSearchRecordExtractor) do 4 | let(:extractor) { AlgoliaSearchRecordExtractor } 5 | let(:site) { get_site } 6 | let(:page_file) { extractor.new(site.file_by_name('about.md')) } 7 | let(:html_page_file) { extractor.new(site.file_by_name('authors.html')) } 8 | let(:post_file) { extractor.new(site.file_by_name('test-post.md')) } 9 | let(:hierarchy_page_file) { extractor.new(site.file_by_name('hierarchy.md')) } 10 | let(:weight_page_file) { extractor.new(site.file_by_name('weight.md')) } 11 | let(:document_file) { extractor.new(site.file_by_name('collection-item.md')) } 12 | 13 | before(:each) do 14 | # Disabling the logs, while still allowing to spy them 15 | Jekyll.logger = double('Specific Mock Logger').as_null_object 16 | @logger = Jekyll.logger.writer 17 | end 18 | 19 | describe 'metadata' do 20 | it 'gets metadata from page' do 21 | # Given 22 | actual = page_file.metadata 23 | 24 | # Then 25 | expect(actual[:type]).to eq 'page' 26 | expect(actual[:slug]).to eq 'about' 27 | expect(actual[:title]).to eq 'About page' 28 | expect(actual[:url]).to eq '/about.html' 29 | expect(actual[:custom]).to eq 'Foo' 30 | end 31 | 32 | it 'gets metadata from post' do 33 | # Given 34 | actual = post_file.metadata 35 | 36 | # Then 37 | expect(actual[:slug]).to eq 'test-post' 38 | expect(actual[:title]).to eq 'Test post' 39 | expect(actual[:url]).to eq '/2015/07/02/test-post.html' 40 | expect(actual[:posted_at]).to eq 1_435_788_000 41 | expect(actual[:custom]).to eq 'Foo' 42 | end 43 | 44 | it 'gets posted_at timestamp based on the configured timezone' do 45 | # Given 46 | site = get_site(timezone: 'America/New_York') 47 | post_file = extractor.new(site.file_by_name('test-post.md')) 48 | actual = post_file.metadata 49 | 50 | # Then 51 | expect(actual[:posted_at]).to eq 1_435_809_600 52 | end 53 | 54 | it 'gets metadata from document' do 55 | # Given 56 | actual = document_file.metadata 57 | 58 | # Then 59 | expect(actual[:type]).to eq 'document' 60 | expect(actual[:slug]).to eq 'collection-item' 61 | expect(actual[:title]).to eq 'Collection Item' 62 | expect(actual[:url]).to eq '/my-collection/collection-item.html' 63 | expect(actual[:custom]).to eq 'Foo' 64 | end 65 | 66 | if restrict_jekyll_version(more_than: '3.0') 67 | describe 'Jekyll > 3.0' do 68 | it 'should not throw any deprecation warnings' do 69 | # Given 70 | 71 | # When 72 | post_file.metadata 73 | 74 | # Expect 75 | expect(@logger).to_not have_received(:warn) 76 | end 77 | end 78 | 79 | end 80 | end 81 | 82 | describe 'slug' do 83 | it 'gets it from data if available' do 84 | # Given 85 | post_file.file.data['slug'] = 'foo' 86 | allow(post_file.file).to receive(:respond_to?).with(:slug) do 87 | false 88 | end 89 | 90 | # When 91 | actual = post_file.slug 92 | 93 | # Then 94 | expect(actual).to eql('foo') 95 | end 96 | 97 | it 'gets it from the root if not in data' do 98 | # Given 99 | post_file.file.data.delete 'slug' 100 | allow(post_file.file).to receive(:slug).and_return('foo') 101 | 102 | # When 103 | actual = post_file.slug 104 | 105 | # Then 106 | expect(actual).to eql('foo') 107 | end 108 | 109 | it 'gets it from the data even if in the root' do 110 | # Given 111 | post_file.file.data['slug'] = 'foo' 112 | allow(post_file.file).to receive(:slug).and_return('bar') 113 | 114 | # When 115 | actual = post_file.slug 116 | 117 | # Then 118 | expect(actual).to eql('foo') 119 | end 120 | 121 | it 'guesses it from the path if not found' do 122 | # Given 123 | post_file.file.data.delete 'slug' 124 | allow(post_file.file).to receive(:respond_to?).with(:slug) do 125 | false 126 | end 127 | allow(post_file.file).to receive(:path) do 128 | '/path/to/file/foo.html' 129 | end 130 | 131 | # When 132 | actual = post_file.slug 133 | 134 | # # Then 135 | expect(actual).to eql('foo') 136 | end 137 | end 138 | 139 | describe 'tags' do 140 | it 'returns tags in data if available' do 141 | # Given 142 | post_file.file.data['tags'] = %w(foo bar) 143 | allow(post_file.file).to receive(:respond_to?).with(:tags) do 144 | false 145 | end 146 | 147 | # When 148 | actual = post_file.tags 149 | 150 | # Then 151 | expect(actual).to include('foo', 'bar') 152 | end 153 | 154 | it 'returns tags at the root if not in data' do 155 | # Given 156 | post_file.file.data.delete 'tags' 157 | allow(post_file.file).to receive(:tags).and_return(%w(foo bar)) 158 | 159 | # When 160 | actual = post_file.tags 161 | 162 | # Then 163 | expect(actual).to include('foo', 'bar') 164 | end 165 | 166 | it 'returns tags in data even if in root' do 167 | # Given 168 | post_file.file.data['tags'] = %w(foo bar) 169 | allow(post_file.file).to receive(:tags).and_return(%w(js css)) 170 | 171 | # When 172 | actual = post_file.tags 173 | 174 | # Then 175 | expect(actual).to include('foo', 'bar') 176 | end 177 | 178 | it 'parses tags as string if they are another type' do 179 | # Given 180 | tag_foo = double('Extended Tag', to_s: 'foo') 181 | tag_bar = double('Extended Tag', to_s: 'bar') 182 | post_file.file.data['tags'] = [tag_foo, tag_bar] 183 | allow(post_file.file).to receive(:respond_to?).with(:tags) do 184 | false 185 | end 186 | 187 | # When 188 | actual = post_file.tags 189 | 190 | # Then 191 | expect(actual).to include('foo', 'bar') 192 | end 193 | 194 | it 'extract tags from front matter' do 195 | # Given 196 | actual = post_file.tags 197 | 198 | # Then 199 | expect(actual).to include('tag', 'another tag') 200 | end 201 | end 202 | 203 | describe 'html_nodes' do 204 | it 'returns the list of all

by default' do 205 | expect(page_file.html_nodes.size).to eq 6 206 | end 207 | 208 | it 'allow _config.yml to override the selector' do 209 | # Given 210 | site = get_site(algolia: { 'record_css_selector' => 'p,ul' }) 211 | page_file = extractor.new(site.file_by_name('about.md')) 212 | 213 | expect(page_file.html_nodes.size).to eq 7 214 | end 215 | end 216 | 217 | describe 'node_heading_parent' do 218 | it 'returns the direct heading right above' do 219 | # Given 220 | nodes = hierarchy_page_file.html_nodes 221 | p = nodes[0] 222 | 223 | # When 224 | actual = hierarchy_page_file.node_heading_parent(p) 225 | 226 | # Then 227 | expect(actual.name).to eq 'h1' 228 | expect(actual.text).to eq 'H1' 229 | end 230 | 231 | it 'returns the closest heading even if in a sub tag' do 232 | # Given 233 | nodes = hierarchy_page_file.html_nodes 234 | p = nodes[2] 235 | 236 | # When 237 | actual = hierarchy_page_file.node_heading_parent(p) 238 | 239 | # Then 240 | expect(actual.name).to eq 'h2' 241 | expect(actual.text).to eq 'H2A' 242 | end 243 | 244 | it 'should automatically go up one level when indexing headings' do 245 | # Given 246 | site = get_site(algolia: { 'record_css_selector' => 'p,h2' }) 247 | hierarchy_page_file = extractor.new(site.file_by_name('hierarchy.md')) 248 | nodes = hierarchy_page_file.html_nodes 249 | h2 = nodes[4] 250 | 251 | # When 252 | actual = hierarchy_page_file.node_heading_parent(h2) 253 | 254 | # Then 255 | expect(actual.name).to eq 'h1' 256 | expect(actual.text).to eq 'H1' 257 | end 258 | 259 | it 'should find the correct parent when indexing deep headings' do 260 | # Given 261 | site = get_site(algolia: { 'record_css_selector' => 'h2' }) 262 | hierarchy_page_file = extractor.new(site.file_by_name('hierarchy.md')) 263 | nodes = hierarchy_page_file.html_nodes 264 | h2 = nodes[2] 265 | 266 | # When 267 | actual = hierarchy_page_file.node_heading_parent(h2) 268 | 269 | # Then 270 | expect(actual.name).to eq 'h1' 271 | expect(actual.text).to eq 'H1' 272 | end 273 | end 274 | 275 | describe 'node_hierarchy' do 276 | it 'returns the unique parent of a simple element' do 277 | # Note: First

should only have a h1 as hierarchy 278 | # Given 279 | nodes = hierarchy_page_file.html_nodes 280 | p = nodes[0] 281 | 282 | # When 283 | actual = hierarchy_page_file.node_hierarchy(p) 284 | 285 | # Then 286 | expect(actual).to include(h1: 'H1') 287 | end 288 | 289 | it 'returns the heading hierarchy of multiple headings' do 290 | # Note: 5th

is inside h3, second h2 and main h1 291 | # Given 292 | nodes = hierarchy_page_file.html_nodes 293 | p = nodes[4] 294 | 295 | # When 296 | actual = hierarchy_page_file.node_hierarchy(p) 297 | 298 | # Then 299 | expect(actual).to include(h1: 'H1', h2: 'H2B', h3: 'H3A') 300 | end 301 | 302 | it 'works even if heading not on the same level' do 303 | # Note: The 6th

is inside a div 304 | # Given 305 | nodes = hierarchy_page_file.html_nodes 306 | p = nodes[5] 307 | 308 | # When 309 | actual = hierarchy_page_file.node_hierarchy(p) 310 | 311 | # Then 312 | expect(actual).to include(h1: 'H1', h2: 'H2B', h3: 'H3A', h4: 'H4') 313 | end 314 | 315 | it 'includes node in the output if headings are indexed' do 316 | # Given 317 | site = get_site(algolia: { 'record_css_selector' => 'h1' }) 318 | hierarchy_page_file = extractor.new(site.file_by_name('hierarchy.md')) 319 | nodes = hierarchy_page_file.html_nodes 320 | h1 = nodes[0] 321 | 322 | # When 323 | actual = hierarchy_page_file.node_hierarchy(h1) 324 | 325 | # Then 326 | expect(actual).to include(h1: 'H1') 327 | end 328 | 329 | it 'escape html in headings' do 330 | # Given 331 | nodes = hierarchy_page_file.html_nodes 332 | p = nodes[7] 333 | 334 | # When 335 | actual = hierarchy_page_file.node_hierarchy(p) 336 | 337 | # Then 338 | expect(actual).to include(h3: 'H3B <code>') 339 | end 340 | end 341 | 342 | describe 'node_raw_html' do 343 | it 'returns html including surrounding tags' do 344 | # Note: 3rd

is a real HTML with a custom class 345 | # Given 346 | nodes = page_file.html_nodes 347 | p = nodes[3] 348 | 349 | # When 350 | actual = page_file.node_raw_html(p) 351 | 352 | # Then 353 | expect(actual).to eq '

Another text 4

' 354 | end 355 | end 356 | 357 | describe 'node_text' do 358 | it 'returns inner text with <> escaped' do 359 | # Note: 4th

contains a tag with <> 360 | # Given 361 | nodes = page_file.html_nodes 362 | p = nodes[4] 363 | 364 | # When 365 | actual = page_file.node_text(p) 366 | 367 | # Then 368 | expect(actual).to eq 'Another <text> 5' 369 | end 370 | end 371 | 372 | describe 'unique_hierarchy' do 373 | it 'combines title and headings' do 374 | # Given 375 | hierarchy = { 376 | title: 'title', 377 | h1: 'h1', 378 | h2: 'h2', 379 | h3: 'h3', 380 | h4: 'h4', 381 | h5: 'h5', 382 | h6: 'h6' 383 | } 384 | 385 | # When 386 | actual = page_file.unique_hierarchy(hierarchy) 387 | 388 | # Then 389 | expect(actual).to eq 'title > h1 > h2 > h3 > h4 > h5 > h6' 390 | end 391 | 392 | it 'combines title and headings even with missing elements' do 393 | # Given 394 | hierarchy = { 395 | title: 'title', 396 | h2: 'h2', 397 | h4: 'h4', 398 | h6: 'h6' 399 | } 400 | 401 | # When 402 | actual = page_file.unique_hierarchy(hierarchy) 403 | 404 | # Then 405 | expect(actual).to eq 'title > h2 > h4 > h6' 406 | end 407 | end 408 | 409 | describe 'node_css_selector' do 410 | it 'uses the #id to make the selector more precise if one is found' do 411 | # Given 412 | nodes = page_file.html_nodes 413 | p = nodes[3] 414 | 415 | # When 416 | actual = page_file.node_css_selector(p) 417 | 418 | # Then 419 | expect(actual).to eq '#text4' 420 | end 421 | 422 | it 'uses p:nth-of-type if no #id found' do 423 | # Given 424 | nodes = page_file.html_nodes 425 | p = nodes[2] 426 | 427 | # When 428 | actual = page_file.node_css_selector(p) 429 | 430 | # Then 431 | expect(actual).to eq 'p:nth-of-type(3)' 432 | end 433 | 434 | it 'handles custom

markup' do 435 | # Given 436 | nodes = page_file.html_nodes 437 | p = nodes[5] 438 | 439 | # When 440 | actual = page_file.node_css_selector(p) 441 | 442 | # Then 443 | expect(actual).to eq 'div:nth-of-type(2) > p' 444 | end 445 | end 446 | 447 | describe 'weight_heading_relevance' do 448 | it 'gets the number of words in text also in the title' do 449 | # Given 450 | data = { 451 | title: 'foo bar', 452 | text: 'Lorem ipsum dolor foo bar, consectetur adipiscing elit' 453 | } 454 | 455 | # When 456 | actual = page_file.weight_heading_relevance(data) 457 | 458 | # Then 459 | expect(actual).to eq 2 460 | end 461 | 462 | it 'gets the number of words in text also in the headings' do 463 | # Given 464 | data = { 465 | title: 'foo', 466 | h1: 'bar', 467 | h2: 'baz', 468 | text: 'Lorem baz dolor foo bar, consectetur adipiscing elit' 469 | } 470 | 471 | # When 472 | actual = page_file.weight_heading_relevance(data) 473 | 474 | # Then 475 | expect(actual).to eq 3 476 | end 477 | 478 | it 'count each word only once' do 479 | # Given 480 | data = { 481 | title: 'foo', 482 | h1: 'foo foo foo', 483 | h2: 'bar bar foo bar', 484 | text: 'foo bar bar bar bar baz foo bar baz' 485 | } 486 | 487 | # When 488 | actual = page_file.weight_heading_relevance(data) 489 | 490 | # Then 491 | expect(actual).to eq 2 492 | end 493 | 494 | it 'is case-insensitive' do 495 | # Given 496 | data = { 497 | title: 'FOO', 498 | h1: 'bar Bar BAR', 499 | text: 'foo BAR' 500 | } 501 | 502 | # When 503 | actual = page_file.weight_heading_relevance(data) 504 | 505 | # Then 506 | expect(actual).to eq 2 507 | end 508 | 509 | it 'should only use words, no partial matches' do 510 | # Given 511 | data = { 512 | title: 'foo bar', 513 | text: 'xxxfooxxx bar' 514 | } 515 | 516 | # When 517 | actual = page_file.weight_heading_relevance(data) 518 | 519 | # Then 520 | expect(actual).to eq 1 521 | end 522 | 523 | it 'should still work with non-string keys' do 524 | # Given 525 | data = { 526 | title: nil, 527 | h1: [], 528 | h2: {}, 529 | h3: true, 530 | h4: false, 531 | h5: 'foo bar', 532 | text: 'foo bar' 533 | } 534 | 535 | # When 536 | actual = page_file.weight_heading_relevance(data) 537 | 538 | # Then 539 | expect(actual).to eq 2 540 | end 541 | end 542 | 543 | describe 'weight_tag_name' do 544 | it 'gives a score of 0 to non-headings' do 545 | # Given 546 | data = { 547 | tag_name: 'p' 548 | } 549 | 550 | # When 551 | actual = page_file.weight_tag_name(data) 552 | 553 | # Then 554 | expect(actual).to eq 0 555 | end 556 | it 'gives a score of 100 to h1' do 557 | # Given 558 | data = { 559 | tag_name: 'h1' 560 | } 561 | 562 | # When 563 | actual = page_file.weight_tag_name(data) 564 | 565 | # Then 566 | expect(actual).to eq 100 567 | end 568 | it 'gives a score of 40 to h6' do 569 | # Given 570 | data = { 571 | tag_name: 'h6' 572 | } 573 | 574 | # When 575 | actual = page_file.weight_tag_name(data) 576 | 577 | # Then 578 | expect(actual).to eq 50 579 | end 580 | end 581 | 582 | describe 'weight' do 583 | it 'returns an object with all weights' do 584 | # Given 585 | item = { 586 | tag_name: 'p' 587 | } 588 | allow(page_file).to receive(:weight_tag_name) { 10 } 589 | allow(page_file).to receive(:weight_heading_relevance) { 20 } 590 | 591 | # When 592 | actual = page_file.weight(item, 42) 593 | 594 | # Then 595 | expect(actual).to include(tag_name: 10) 596 | expect(actual).to include(heading_relevance: 20) 597 | expect(actual).to include(position: 42) 598 | end 599 | end 600 | 601 | describe 'custom_hook_each' do 602 | it 'let the user call a custom hook to modify a record' do 603 | # Given 604 | def page_file.custom_hook_each(item, _) 605 | item[:custom_attribute] = 'foo' 606 | item 607 | end 608 | 609 | # When 610 | actual = page_file.extract 611 | 612 | # Then 613 | expect(actual[0]).to include(custom_attribute: 'foo') 614 | end 615 | 616 | it 'let the user discard a record by returning nil' do 617 | # Given 618 | def page_file.custom_hook_each(_, _) 619 | nil 620 | end 621 | 622 | # When 623 | actual = page_file.extract 624 | 625 | # Then 626 | expect(actual.size).to eq 0 627 | end 628 | end 629 | 630 | describe 'custom_hook_all' do 631 | it 'let the user call a custom hook to modify the list of records' do 632 | # Given 633 | def page_file.custom_hook_all(items) 634 | [items[0], { foo: 'bar' }] 635 | end 636 | 637 | # When 638 | actual = page_file.extract 639 | 640 | # Then 641 | expect(actual.size).to eq 2 642 | expect(actual[1]).to include(foo: 'bar') 643 | end 644 | end 645 | end 646 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | if ENV['TRAVIS'] 2 | require 'coveralls' 3 | Coveralls.wear! 4 | end 5 | 6 | require 'awesome_print' 7 | require 'jekyll' 8 | require_relative './spec_helper_simplecov.rb' 9 | require './lib/push.rb' 10 | 11 | RSpec.configure do |config| 12 | config.filter_run(focus: true) 13 | config.fail_fast = true 14 | config.run_all_when_everything_filtered = true 15 | end 16 | 17 | # Disabling the logs 18 | Jekyll.logger.log_level = :error 19 | 20 | # Create a Jekyll::Site instance, patched with a `file_by_name` method 21 | def get_site(config = {}, options = {}) 22 | default_options = { 23 | mock_write_method: true, 24 | process: true 25 | } 26 | options = default_options.merge(options) 27 | 28 | config = config.merge( 29 | 'source' => fixture_path 30 | ) 31 | config = Jekyll.configuration(config) 32 | 33 | site = AlgoliaSearchJekyllPush.init_options({}, options, config) 34 | .jekyll_new(config) 35 | 36 | def site.file_by_name(file_name) 37 | each_site_file do |file| 38 | return file if file.path =~ /#{file_name}$/ 39 | end 40 | nil 41 | end 42 | 43 | allow(site).to receive(:write) if options[:mock_write_method] 44 | 45 | site.process if options[:process] 46 | site 47 | end 48 | 49 | # Return the fixture path, according to the current Jekyll version being tested 50 | def fixture_path 51 | jekyll_version = Jekyll::VERSION[0] 52 | fixture_path = "./spec/fixtures/jekyll_version_#{jekyll_version}" 53 | File.expand_path(fixture_path) 54 | end 55 | 56 | # Check the current Jekyll version 57 | def restrict_jekyll_version(more_than: nil, less_than: nil) 58 | jekyll_version = Gem::Version.new(Jekyll::VERSION) 59 | minimum_version = Gem::Version.new(more_than) 60 | maximum_version = Gem::Version.new(less_than) 61 | 62 | return false if !more_than.nil? && jekyll_version < minimum_version 63 | return false if !less_than.nil? && jekyll_version > maximum_version 64 | true 65 | end 66 | -------------------------------------------------------------------------------- /spec/spec_helper_simplecov.rb: -------------------------------------------------------------------------------- 1 | require 'simplecov' 2 | 3 | SimpleCov.configure do 4 | load_profile 'test_frameworks' 5 | end 6 | 7 | ENV['COVERAGE'] && SimpleCov.start do 8 | add_filter '/.rvm/' 9 | end 10 | -------------------------------------------------------------------------------- /txt/api_key_missing: -------------------------------------------------------------------------------- 1 | Algolia Error: No API key defined 2 | You have two ways to configure your API key: 3 | - The ALGOLIA_API_KEY environment variable 4 | - A file named _algolia_api_key in your source folder 5 | -------------------------------------------------------------------------------- /txt/application_id_missing: -------------------------------------------------------------------------------- 1 | Algolia Error: No application ID defined 2 | Please set your application id in the _config.yml file, like so: 3 | 4 | algolia: 5 | application_id: {your_application_id} 6 | 7 | You can also define the ALGOLIA_APPLICATION_ID environment variable. 8 | 9 | Your application ID can be found in your algolia dashboard: 10 | https://www.algolia.com/licensing 11 | -------------------------------------------------------------------------------- /txt/check_key_acl_to_tmp_index: -------------------------------------------------------------------------------- 1 | Algolia Error: API key cannot write to `{index_name}_tmp` index 2 | In order to do atomic pushes to your Algolia index, the plugin first pushes to 3 | a temporary index (suffixed with `_tmp`), then renames it. 4 | 5 | You see this error because the plugin wasn't able to push to that 6 | `{index_name}_tmp` index, with the API key you provided. 7 | 8 | Make sure the API key you're using has rights to write on both your index and 9 | its `{index_name}_tmp` suffixed version. 10 | -------------------------------------------------------------------------------- /txt/index_name_missing: -------------------------------------------------------------------------------- 1 | Algolia Error: No index name defined 2 | Please set your index name in the _config.yml file, like so: 3 | 4 | algolia: 5 | index_name: {your_index_name} 6 | 7 | You can also define the ALGOLIA_INDEX_NAME environment variable. 8 | 9 | You can edit your indices in your dashboard: 10 | https://www.algolia.com/explorer 11 | 12 | -------------------------------------------------------------------------------- /txt/sample: -------------------------------------------------------------------------------- 1 | Algolia Error: Sample Error 2 | This is where you can explain the error and: 3 | - one way to fix it 4 | - or another way to fix it 5 | --------------------------------------------------------------------------------