├── .document ├── .editorconfig ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── ci.yml ├── .gitignore ├── .rspec ├── .travis.yml ├── Appraisals ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── assets └── loaf_logo.png ├── bin ├── console └── setup ├── config └── locales │ └── loaf.en.yml ├── gemfiles ├── rails3.2.gemfile ├── rails4.0.gemfile ├── rails4.1.gemfile ├── rails4.2.gemfile ├── rails5.0.gemfile ├── rails5.1.gemfile ├── rails5.2.gemfile ├── rails6.0.gemfile └── rails6.1.gemfile ├── lib ├── generators │ └── loaf │ │ └── install_generator.rb ├── loaf.rb └── loaf │ ├── breadcrumb.rb │ ├── configuration.rb │ ├── controller_extensions.rb │ ├── crumb.rb │ ├── errors.rb │ ├── options_validator.rb │ ├── railtie.rb │ ├── translation.rb │ ├── version.rb │ └── view_extensions.rb ├── loaf.gemspec ├── spec ├── integration │ ├── breadcrumb_trail_spec.rb │ └── configuration_spec.rb ├── rails_app │ ├── Rakefile │ ├── app │ │ ├── assets │ │ │ └── config │ │ │ │ └── manifest.js │ │ ├── controllers │ │ │ ├── application_controller.rb │ │ │ ├── comments_controller.rb │ │ │ ├── home_controller.rb │ │ │ └── posts_controller.rb │ │ └── views │ │ │ ├── comments │ │ │ └── index.html.erb │ │ │ ├── home │ │ │ └── index.html.erb │ │ │ ├── layouts │ │ │ ├── _breadcrumbs.html.erb │ │ │ └── application.html.erb │ │ │ └── posts │ │ │ ├── index.html.erb │ │ │ ├── new.html.erb │ │ │ └── show.html.erb │ ├── config.ru │ ├── config │ │ ├── application.rb │ │ ├── boot.rb │ │ ├── database.yml │ │ ├── environment.rb │ │ ├── environments │ │ │ ├── development.rb │ │ │ ├── production.rb │ │ │ └── test.rb │ │ ├── initializers │ │ │ ├── backtrace_silencers.rb │ │ │ ├── inflections.rb │ │ │ ├── mime_types.rb │ │ │ ├── secret_token.rb │ │ │ ├── session_store.rb │ │ │ └── wrap_parameters.rb │ │ ├── locales │ │ │ ├── en.yml │ │ │ └── loaf.en.yml │ │ ├── routes.rb │ │ └── secrets.yml │ ├── db │ │ └── seeds.rb │ ├── log │ │ └── .gitkeep │ └── public │ │ ├── 404.html │ │ ├── 422.html │ │ ├── 500.html │ │ ├── favicon.ico │ │ └── robots.txt ├── spec_helper.rb ├── support │ ├── capybara.rb │ ├── dummy_controller.rb │ ├── dummy_view.rb │ └── load_routes.rb └── unit │ ├── configuration_spec.rb │ ├── controller_extensions_spec.rb │ ├── crumb_spec.rb │ ├── generators │ └── install_generator_spec.rb │ ├── options_validator_spec.rb │ ├── translation_spec.rb │ └── view_extensions │ ├── breadcrumb_spec.rb │ ├── breadcrumb_trail_spec.rb │ └── has_breadcrumbs_spec.rb └── tasks ├── console.rake ├── coverage.rake └── spec.rake /.document: -------------------------------------------------------------------------------- 1 | lib/**/*.rb 2 | bin/* 3 | - 4 | features/**/*.feature 5 | LICENSE.txt 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.rb] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 2 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: piotrmurach 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Are you in the right place? 2 | * For issues or feature requests file a GitHub issue in this repository 3 | * For general questions or discussion post on StackOverflow 4 | 5 | ### Describe the problem 6 | A brief description of the issue/feature. 7 | 8 | ### Steps to reproduce the problem 9 | ``` 10 | Your code here to reproduce the issue 11 | ``` 12 | 13 | ### Actual behaviour 14 | What happened? This could be a description, log output, error raised etc... 15 | 16 | ### Expected behaviour 17 | What did you expect to happen? 18 | 19 | ### Describe your environment 20 | 21 | * OS version: 22 | * Ruby version: 23 | * Loaf version: 24 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Describe the change 2 | What does this Pull Request do? 3 | 4 | ### Why are we doing this? 5 | Any related context as to why is this is a desirable change. 6 | 7 | ### Benefits 8 | How will the library improve? 9 | 10 | ### Drawbacks 11 | Possible drawbacks applying this change. 12 | 13 | ### Requirements 14 | 15 | - [ ] Tests written & passing locally? 16 | - [ ] Code style checked? 17 | - [ ] Rebased with `master` branch? 18 | - [ ] Documentation updated? 19 | - [ ] Changelog updated? 20 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CI 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths-ignore: 8 | - "bin/**" 9 | - "*.md" 10 | pull_request: 11 | branches: 12 | - master 13 | paths-ignore: 14 | - "bin/**" 15 | - "*.md" 16 | jobs: 17 | tests: 18 | name: Ruby ${{ matrix.ruby }}, ${{ matrix.gemfile }} 19 | runs-on: ${{ matrix.os }} 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | ruby: 24 | - 2.1 25 | - 2.2 26 | - 2.3 27 | - 2.4 28 | - 2.5 29 | - 2.6 30 | - 2.7 31 | - 3.0 32 | gemfile: 33 | - gemfiles/rails3.2.gemfile 34 | - gemfiles/rails4.0.gemfile 35 | - gemfiles/rails4.1.gemfile 36 | - gemfiles/rails4.2.gemfile 37 | - gemfiles/rails5.0.gemfile 38 | - gemfiles/rails5.1.gemfile 39 | - gemfiles/rails5.2.gemfile 40 | - gemfiles/rails6.0.gemfile 41 | - gemfiles/rails6.1.gemfile 42 | exclude: 43 | - ruby: 2.1 44 | gemfile: gemfiles/rails5.0.gemfile 45 | - ruby: 2.1 46 | gemfile: gemfiles/rails5.1.gemfile 47 | - ruby: 2.1 48 | gemfile: gemfiles/rails5.2.gemfile 49 | - ruby: 2.1 50 | gemfile: gemfiles/rails6.0.gemfile 51 | - ruby: 2.1 52 | gemfile: gemfiles/rails6.1.gemfile 53 | - ruby: 2.2 54 | gemfile: gemfiles/rails5.2.gemfile 55 | - ruby: 2.2 56 | gemfile: gemfiles/rails6.0.gemfile 57 | - ruby: 2.2 58 | gemfile: gemfiles/rails6.1.gemfile 59 | - ruby: 2.3 60 | gemfile: gemfiles/rails6.0.gemfile 61 | - ruby: 2.3 62 | gemfile: gemfiles/rails6.1.gemfile 63 | - ruby: 2.4 64 | gemfile: gemfiles/rails3.2.gemfile 65 | - ruby: 2.4 66 | gemfile: gemfiles/rails4.0.gemfile 67 | - ruby: 2.4 68 | gemfile: gemfiles/rails4.1.gemfile 69 | - ruby: 2.4 70 | gemfile: gemfiles/rails6.0.gemfile 71 | - ruby: 2.4 72 | gemfile: gemfiles/rails6.1.gemfile 73 | - ruby: 2.5 74 | gemfile: gemfiles/rails3.2.gemfile 75 | - ruby: 2.5 76 | gemfile: gemfiles/rails4.0.gemfile 77 | - ruby: 2.5 78 | gemfile: gemfiles/rails4.1.gemfile 79 | - ruby: 2.6 80 | gemfile: gemfiles/rails3.2.gemfile 81 | - ruby: 2.6 82 | gemfile: gemfiles/rails4.0.gemfile 83 | - ruby: 2.6 84 | gemfile: gemfiles/rails4.1.gemfile 85 | - ruby: 2.7 86 | gemfile: gemfiles/rails3.2.gemfile 87 | - ruby: 2.7 88 | gemfile: gemfiles/rails4.0.gemfile 89 | - ruby: 2.7 90 | gemfile: gemfiles/rails4.1.gemfile 91 | - ruby: 2.7 92 | gemfile: gemfiles/rails4.2.gemfile 93 | - ruby: 3.0 94 | gemfile: gemfiles/rails3.2.gemfile 95 | - ruby: 3.0 96 | gemfile: gemfiles/rails4.0.gemfile 97 | - ruby: 3.0 98 | gemfile: gemfiles/rails4.1.gemfile 99 | - ruby: 3.0 100 | gemfile: gemfiles/rails4.2.gemfile 101 | - ruby: 3.0 102 | gemfile: gemfiles/rails5.0.gemfile 103 | - ruby: 3.0 104 | gemfile: gemfiles/rails5.1.gemfile 105 | - ruby: 3.0 106 | gemfile: gemfiles/rails5.2.gemfile 107 | include: 108 | - ruby: 2.7 109 | os: ubuntu-latest 110 | coverage: true 111 | gemfile: gemfiles/rails6.1.gemfile 112 | os: 113 | - ubuntu-latest 114 | continue-on-error: ${{ endsWith(matrix.ruby, 'head') }} 115 | env: 116 | BUNDLE_GEMFILE: ${{ matrix.gemfile }} 117 | COVERAGE: ${{ matrix.coverage }} 118 | COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} 119 | steps: 120 | - uses: actions/checkout@v2 121 | - name: Set up Ruby 122 | uses: ruby/setup-ruby@v1 123 | with: 124 | ruby-version: ${{ matrix.ruby }} 125 | bundler-cache: false 126 | - name: Install bundler 127 | run: gem install bundler -v '< 2.0' 128 | - name: Install dependencies 129 | run: bundle _1.17.3_ install --jobs 4 --retry 3 130 | - name: Run tests 131 | run: bundle exec rake ci 132 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | gemfiles/*.lock 8 | InstalledFiles 9 | _yardoc 10 | coverage 11 | doc/ 12 | lib/bundler/man 13 | pkg 14 | rdoc 15 | spec/reports 16 | spec/rails_app/tmp 17 | spec/rails_app/db 18 | spec/rails_app/log 19 | tmp 20 | *.bundle 21 | *.so 22 | *.o 23 | *.a 24 | mkmf.log 25 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: ruby 3 | cache: bundler 4 | sudo: false 5 | before_install: 6 | - gem uninstall -v '>= 2' -i $(rvm gemdir)@global -ax bundler || true 7 | - gem install bundler -v '< 2' 8 | - bundle -v 9 | - echo $TRAVIS_RUBY_VERSION 10 | script: "bundle exec rake ci" 11 | rvm: 12 | - 2.1.10 13 | - 2.2.10 14 | - 2.3.8 15 | - 2.4.5 16 | - 2.5.3 17 | - 2.6.5 18 | - 2.7.0 19 | gemfile: 20 | - gemfiles/rails3.2.gemfile 21 | - gemfiles/rails4.0.gemfile 22 | - gemfiles/rails4.1.gemfile 23 | - gemfiles/rails4.2.gemfile 24 | - gemfiles/rails5.0.gemfile 25 | - gemfiles/rails5.1.gemfile 26 | - gemfiles/rails5.2.gemfile 27 | - gemfiles/rails6.0.gemfile 28 | matrix: 29 | exclude: 30 | - rvm: 2.1.10 31 | gemfile: gemfiles/rails5.0.gemfile 32 | - rvm: 2.1.10 33 | gemfile: gemfiles/rails5.1.gemfile 34 | - rvm: 2.1.10 35 | gemfile: gemfiles/rails5.2.gemfile 36 | - rvm: 2.1.10 37 | gemfile: gemfiles/rails6.0.gemfile 38 | - rvm: 2.2.10 39 | gemfile: gemfiles/rails5.2.gemfile 40 | - rvm: 2.2.10 41 | gemfile: gemfiles/rails6.0.gemfile 42 | - rvm: 2.3.8 43 | gemfile: gemfiles/rails6.0.gemfile 44 | - rvm: 2.4.5 45 | gemfile: gemfiles/rails3.2.gemfile 46 | - rvm: 2.4.5 47 | gemfile: gemfiles/rails4.0.gemfile 48 | - rvm: 2.4.5 49 | gemfile: gemfiles/rails4.1.gemfile 50 | - rvm: 2.4.5 51 | gemfile: gemfiles/rails6.0.gemfile 52 | - rvm: 2.5.3 53 | gemfile: gemfiles/rails3.2.gemfile 54 | - rvm: 2.5.3 55 | gemfile: gemfiles/rails4.0.gemfile 56 | - rvm: 2.5.3 57 | gemfile: gemfiles/rails4.1.gemfile 58 | - rvm: 2.6.5 59 | gemfile: gemfiles/rails3.2.gemfile 60 | - rvm: 2.6.5 61 | gemfile: gemfiles/rails4.0.gemfile 62 | - rvm: 2.6.5 63 | gemfile: gemfiles/rails4.1.gemfile 64 | - rvm: 2.7.0 65 | gemfile: gemfiles/rails3.2.gemfile 66 | - rvm: 2.7.0 67 | gemfile: gemfiles/rails4.0.gemfile 68 | - rvm: 2.7.0 69 | gemfile: gemfiles/rails4.1.gemfile 70 | - rvm: 2.7.0 71 | gemfile: gemfiles/rails4.2.gemfile 72 | fast_finish: true 73 | branches: 74 | only: master 75 | notifications: 76 | email: false 77 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | rails_versions = [ 2 | %w[3.2 3.2.22.5], 3 | %w[4.0 4.0.13], 4 | %w[4.1 4.1.16], 5 | %w[4.2 4.2.11], 6 | %w[5.0 5.0.7], 7 | %w[5.1 5.1.7], 8 | %w[5.2 5.2.4], 9 | %w[6.0 6.0.3], 10 | %w[6.1 6.1.0] 11 | ] 12 | 13 | rails_versions.each do |(version, rails)| 14 | gem_version = Gem::Version.new(version) 15 | 16 | appraise "rails#{version}" do 17 | gem "railties", "~> #{rails}" 18 | gem "capybara", "~> 2.18.0" 19 | 20 | if gem_version == Gem::Version.new("3.2") 21 | gem "tzinfo", "~> 0.3" 22 | end 23 | 24 | if gem_version <= Gem::Version.new("4.0") 25 | gem "test-unit", "~> 3.0" 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change log 2 | 3 | ## [v0.10.0] - 2020-11-21 4 | 5 | ### Changed 6 | * Reduce gem dependencies to `railties` by Christian Sutter (@csutter) 7 | * Use `URI::DEFAULT_PARSER` instead of deprecated `URI.parser` by (@dsazup) 8 | * Support Rails 6.1 in tests 9 | 10 | ### Fixed 11 | * Fix #breadcrumb_trail to allow overriding the match option 12 | * Fix #breadcrumb_trail to return enumerator that includes passed in match option 13 | 14 | ## [v0.9.0] - 2020-01-19 15 | 16 | ### Changed 17 | * Change gemspec to include metadata, license info and remove test artifacts 18 | * Change to update testing to include Ruby 2.7 19 | * Change to limit Ruby to 1.9.3 or greater 20 | 21 | ### Fixed 22 | * Fix Ruby 2.7 warnings 23 | 24 | ## [v0.8.1] - 2019-02-04 25 | 26 | ### Added 27 | * Add console binary 28 | 29 | ### Changed 30 | * Remove rake & appraisal binaries 31 | * Change setup binary to load correct env 32 | * Change gemspec to load files directly in without using git 33 | 34 | ## [v0.8.0] - 2018-08-07 35 | 36 | ### Changed 37 | * Change Translation to skip translating nil and empty string 38 | * Change view extension to only lookup breadcrumb name translation 39 | * Remove Configuration #crumb_length and #capitalize options 40 | * Remove CrumbFormatter to skip truncating and formatting crumb names 41 | 42 | ## Fix 43 | * Fix issue with breadcrumb names being modified 44 | 45 | ## [v0.7.0] - 2018-06-20 46 | 47 | ### Added 48 | * Add test setup for Rails 5.2 by Brendon Muir(@brendon) 49 | 50 | ### Changed 51 | * Change controller level #breadcrumb helper to accept Proc as name without controller parameter by Brendon Muir(@brendon) 52 | 53 | ## [v0.6.2] - 2018-03-30 54 | 55 | ### Added 56 | * Add :match to Configuration by Johan Kim(@hiphapis) 57 | 58 | ## [v0.6.1] - 2018-03-26 59 | 60 | ### Added 61 | * Add nil guard and clear error messages to Loaf::Crumb initialization by Dan Matthews(@dmvt) 62 | 63 | ### Fixed 64 | * Fix Loaf::Crumb to stop modifying options hash by Marcel Müller(@TheNeikos) 65 | 66 | ## [v0.6.0] - 2017-10-19 67 | 68 | ### Added 69 | * Add new :match option to allow customisation of breadcrumb matching behaviour 70 | * Add #current_crumb? for checking if breadcrumb is current in view 71 | * Add tests setup for Rails 5.0, 5.1 72 | 73 | ### Changed 74 | * Change view helper name from #breadcrumbs to #breadcrumb_trail 75 | * Change Configuration to accept attributes at initilization 76 | * Change Loaf::Railtie to load for both old and new rails versions 77 | * Remove Builder class 78 | * Remove configuration options :root, :last_crumb_linked, :style_classes 79 | * Remove #add_breadcrumbs from controller api 80 | 81 | ### Fixed 82 | * Fix current page matching logic to allow for inclusive paths 83 | * Fix controller filter to work with new Rails action semantics 84 | 85 | ## [v0.5.0] - 2015-01-31 86 | 87 | ### Added 88 | * Add generator for locales file 89 | * Add breadcrumbs scope for translations 90 | * Add ability to pass proc as title and/or url for breadcrumb helper inside controller 91 | 92 | ### Changed 93 | * Change breadcrumb formatter to use translations for title formatting 94 | 95 | ## [v0.4.0] - 2015-01-10 96 | 97 | ### Added 98 | * Add ability to force current path through :force option 99 | 100 | ### Changed 101 | * Change breadcrumbs view method to return enumerator without block 102 | * Change Crumb to ruby class and add force option 103 | * Change Configuration to ruby class and scope config options 104 | * Change format_name to only take name argument 105 | * Change to expose config settings on configuration 106 | * Update test suite to work against different rubies 1.9.x, 2.x 107 | * Test Rails 3.2.x, 4.0, 4.1, 4.2 108 | 109 | ### Fixed 110 | * Fix bug with url parameter to allow for regular rails path variables 111 | 112 | ## [v0.3.0] - 2012-02-25 113 | 114 | ### Added 115 | * Add loaf gem errors 116 | * Add custom options validator for filtering invalid breadcrumbs params 117 | * Add specs for isolated view testing. 118 | 119 | ### Changed 120 | * Renamed main gem helpers for adding breadcrumbs from `add_breadcrumb` to 121 | `breadcrumb`, both inside controllers and views. 122 | 123 | ## [v0.2.1] - 2012-02-22 124 | 125 | ### Added 126 | * Add more integration tests and fixed bug with adding breadcrumbs inside view 127 | * Add specs for controller extensions 128 | 129 | ## [v0.2.0] - 2012-02-18 130 | 131 | ### Added 132 | * Add integration tests for breadcrumbs view rendering 133 | * Add translation module for breadcrumbs title lookup 134 | * Add breadcrumb formatting module with tests 135 | 136 | ### Changed 137 | * Change gemspec with new gem dependencies, use git 138 | * Setup testing environment with dummy rails app 139 | * Refactor names for controller and view extensions 140 | 141 | ## [v0.1.0] - 2011-10-22 142 | 143 | * Initial implementation and release 144 | 145 | [v0.10.0]: https://github.com/piotrmurach/loaf/compare/v0.9.0...v0.10.0 146 | [v0.9.0]: https://github.com/piotrmurach/loaf/compare/v0.8.1...v0.9.0 147 | [v0.8.1]: https://github.com/piotrmurach/loaf/compare/v0.8.0...v0.8.1 148 | [v0.8.0]: https://github.com/piotrmurach/loaf/compare/v0.7.0...v0.8.0 149 | [v0.7.0]: https://github.com/piotrmurach/loaf/compare/v0.6.2...v0.7.0 150 | [v0.6.2]: https://github.com/piotrmurach/loaf/compare/v0.6.1...v0.6.2 151 | [v0.6.1]: https://github.com/piotrmurach/loaf/compare/v0.6.0...v0.6.1 152 | [v0.6.0]: https://github.com/piotrmurach/loaf/compare/v0.5.0...v0.6.0 153 | [v0.5.0]: https://github.com/piotrmurach/loaf/compare/v0.4.0...v0.5.0 154 | [v0.4.0]: https://github.com/piotrmurach/loaf/compare/v0.3.0...v0.4.0 155 | [v0.3.0]: https://github.com/piotrmurach/loaf/compare/v0.2.1...v0.3.0 156 | [v0.2.1]: https://github.com/piotrmurach/loaf/compare/v0.2.0...v0.2.1 157 | [v0.2.0]: https://github.com/piotrmurach/loaf/compare/v0.1.0...v0.2.0 158 | [v0.1.0]: https://github.com/piotrmurach/loaf/compare/v0.1.0...HEAD 159 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at piotr@piotrmurach.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | gem 'ammeter', '~> 1.1.4' 6 | gem 'appraisal', '~> 2.2.0' 7 | gem 'yard', '~> 0.9.24' 8 | gem 'capybara', '~> 3.30.0' 9 | gem 'rspec-rails', '~> 3.9.0' 10 | gem 'public_suffix', '~> 2.0.5' 11 | 12 | group :metrics do 13 | gem 'coveralls', '0.8.23' 14 | gem 'simplecov', '~> 0.16.1' 15 | gem 'yardstick', '~> 0.9.9' 16 | end 17 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Piotr Murach (https://piotrmurach.com) 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 |
2 | Loaf gem logo 3 |
4 | 5 | # Loaf 6 | 7 | [![Gem Version](https://badge.fury.io/rb/loaf.svg)][gem] 8 | [![Actions CI](https://github.com/piotrmurach/loaf/workflows/CI/badge.svg?branch=master)][gh_actions_ci] 9 | [![Maintainability](https://api.codeclimate.com/v1/badges/966193dafa3895766977/maintainability)][codeclimate] 10 | [![Coverage Status](https://coveralls.io/repos/github/piotrmurach/loaf/badge.svg?branch=master)][coveralls] 11 | [![Inline docs](http://inch-ci.org/github/piotrmurach/loaf.svg?branch=master)][inchpages] 12 | 13 | [gem]: http://badge.fury.io/rb/loaf 14 | [gh_actions_ci]: https://github.com/piotrmurach/loaf/actions?query=workflow%3ACI 15 | [codeclimate]: https://codeclimate.com/github/piotrmurach/loaf/maintainability 16 | [coveralls]: https://coveralls.io/github/piotrmurach/loaf 17 | [inchpages]: http://inch-ci.org/github/piotrmurach/loaf 18 | 19 | > **Loaf** manages and displays breadcrumb trails in your Rails application. 20 | 21 | ## Features 22 | 23 | * Use controllers and/or views to specify breadcrumb trails 24 | * Specify urls using Rails conventions 25 | * No markup assumptions for breadcrumbs trails rendering 26 | * Use locales file for breadcrumb names 27 | * Tested with Rails `>= 3.2` and Ruby `>= 2.0.0` 28 | 29 | ## Installation 30 | 31 | Add this line to your application's Gemfile: 32 | 33 | ```ruby 34 | gem "loaf" 35 | ``` 36 | 37 | And then execute: 38 | 39 | ```ruby 40 | $ bundle 41 | ``` 42 | 43 | Or install it yourself as: 44 | 45 | ```ruby 46 | gem install loaf 47 | ``` 48 | 49 | Then run the generator: 50 | 51 | ```ruby 52 | rails generate loaf:install 53 | ``` 54 | 55 | ## Contents 56 | 57 | * [1. Usage](#1-usage) 58 | * [2. API](#2-api) 59 | * [2.1 breadcrumb](#21-breadcrumb) 60 | * [2.1.1 controller](#211-controller) 61 | * [2.1.2 view](#212-view) 62 | * [2.1.3 :match](#213-match) 63 | * [2.2 breadcrumb_trail](#22-breadcrumb_trail) 64 | * [3. Configuration](#3-configuration) 65 | * [4. Translation](#4-translation) 66 | 67 | ## 1. Usage 68 | 69 | **Loaf** allows you to add breadcrumbs in controllers and views. 70 | 71 | In order to add breadcrumbs in controller use `breadcrumb` method ([see 2.1](#21-breadcrumb)). 72 | 73 | ```ruby 74 | class Blog::CategoriesController < ApplicationController 75 | 76 | breadcrumb "Article Categories", :blog_categories_path, only: [:show] 77 | 78 | def show 79 | breadcrumb @category.title, blog_category_path(@category) 80 | end 81 | end 82 | ``` 83 | 84 | Then in your view render the breadcrumbs trail using [breadcrumb_trail](#22-breadcrumb_trail) 85 | 86 | ## 2. API 87 | 88 | ### 2.1 breadcrumb 89 | 90 | Creation of breadcrumb in Rails is achieved by the `breadcrumb` helper. 91 | 92 | The `breadcrumb` method takes at minimum two arguments: the first is a name for the crumb that will be displayed and the second is a url that the name points to. The url parameter uses the familiar Rails conventions. 93 | 94 | When using path variable `blog_categories_path`: 95 | 96 | ```ruby 97 | breadcrumb "Categories", blog_categories_path 98 | ``` 99 | 100 | When using an instance `@category`: 101 | 102 | ```ruby 103 | breadcrumb @category.title, blog_category_path(@category) 104 | ``` 105 | 106 | You can also use set of objects: 107 | 108 | ```ruby 109 | breadcrumb @category.title, [:blog, @category] 110 | ``` 111 | 112 | You can specify segments of the url: 113 | 114 | ```ruby 115 | breadcrumb @category.title, {controller: "categories", action: "show", id: @category.id} 116 | ``` 117 | 118 | #### 2.1.1 controller 119 | 120 | Breadcrumbs are inherited, so if you set a breadcrumb in `ApplicationController`, it will be inserted as a first element inside every breadcrumb trail. It is customary to set root breadcrumb like so: 121 | 122 | ```ruby 123 | class ApplicationController < ActionController::Base 124 | breadcrumb "Home", :root_path 125 | end 126 | ``` 127 | 128 | Outside of controller actions the `breadcrumb` helper behaviour is similar to filters/actions and as such you can limit breadcrumb scope with familiar options `:only`, `:except`. Any breadcrumb specified inside actions creates another level in breadcrumbs trail. 129 | 130 | ```ruby 131 | class ArticlesController < ApplicationController 132 | breadcrumb "All Articles", :articles_path, only: [:new, :create] 133 | end 134 | ``` 135 | 136 | Each time you call the `breadcrumb` helper, a new element is added to a breadcrumb trial stack: 137 | 138 | ```ruby 139 | class ArticlesController < ApplicationController 140 | breadcrumb "Home", :root_path 141 | breadcrumb "All Articles", :articles_path 142 | 143 | def show 144 | breadcrumb "Article One", article_path(:one) 145 | breadcrumb "Article Two", article_path(:two) 146 | end 147 | end 148 | ``` 149 | 150 | **Loaf** allows you to call controller instance methods inside the `breadcrumb` helper outside of any action. This is useful if your breadcrumb has parameterized behaviour. For example, to dynamically evaluate parameters for breadcrumb title do: 151 | 152 | ```ruby 153 | class CommentsController < ApplicationController 154 | breadcrumb -> { find_article(params[:post_id]).title }, :articles_path 155 | end 156 | ``` 157 | 158 | Also, to dynamically evaluate parameters inside the url argument do: 159 | 160 | ```ruby 161 | class CommentsController < ApplicationController 162 | breadcrumb "All Comments", -> { post_comments_path(params[:post_id]) } 163 | end 164 | ``` 165 | 166 | You may wish to define breadcrumbs over a collection. This is easy within views, and controller actions (just loop your collection), but if you want to do this in the controller class you can use the `before_action` approach: 167 | 168 | ```ruby 169 | before_action do 170 | ancestors.each do |ancestor| 171 | breadcrumb ancestor.name, [:admin, ancestor] 172 | end 173 | end 174 | ``` 175 | 176 | Assume `ancestors` method is defined inside the controller. 177 | 178 | #### 2.1.2 view 179 | 180 | **Loaf** adds `breadcrumb` helper also to the views. Together with controller breadcrumbs, the view breadcrumbs are appended as the last in breadcrumb trail. For instance, to specify view breadcrumb do: 181 | 182 | ```ruby 183 | <% breadcrumb @category.title, blog_category_path(@category) %> 184 | ``` 185 | 186 | #### 2.1.3 :match 187 | 188 | **Loaf** allows you to define matching conditions in order to make a breadcrumb current with the `:match` option. 189 | 190 | The `:match` key accepts the following values: 191 | 192 | * `:inclusive` - the default value, which matches nested paths 193 | * `:exact` - matches only the exact same path 194 | * `:exclusive` - matches only direct path and its query parameters if present 195 | * `/regex/` - matches based on regular expression 196 | * `{foo: bar}` - match based on query parameters 197 | 198 | For example, to force a breadcrumb to be the current regardless do: 199 | 200 | ```ruby 201 | breadcrumb "New Post", new_post_path, match: :exact 202 | ``` 203 | 204 | To make a breadcrumb current based on the query parameters do: 205 | 206 | ```ruby 207 | breadcrumb "Posts", posts_path(order: :desc), match: {order: :desc} 208 | ``` 209 | 210 | ### 2.2 breadcrumb_trail 211 | 212 | In order to display breadcrumbs use the `breadcrumb_trail` view helper. It accepts optional argument of configuration options and can be used in two ways. 213 | 214 | One way, given a block it will yield all the breadcrumbs in order of definition: 215 | 216 | ```ruby 217 | breadcrumb_trail do |crumb| 218 | ... 219 | end 220 | ``` 221 | 222 | The yielded parameter is an instance of `Loaf::Crumb` object with the following methods: 223 | 224 | ```ruby 225 | crumb.name # => the name as string 226 | crumb.path # => the path as string 227 | crumb.url # => alias for path 228 | crumb.current? # => true or false 229 | ``` 230 | 231 | For example, you can add the following semantic markup to show breadcrumbs using the `breadcrumb_trail` helper like so: 232 | 233 | ```erb 234 | 244 | ``` 245 | 246 | For Bootstrap 4: 247 | 248 | ```erb 249 | <% #erb %> 250 | 259 | ``` 260 | 261 | And, if you are using `HAML` do: 262 | 263 | ```haml 264 | - # haml 265 | %ol.breadcrumb 266 | - breadcrumb_trail do |crumb| 267 | %li.breadcrumb-item{class: crumb.current? ? "active" : "" } 268 | = link_to_unless crumb.current?, crumb.name, crumb.url, (crumb.current? ? {"aria-current" => "page"} : {}) 269 | ``` 270 | 271 | Usually best practice is to put such snippet inside its own `_breadcrumbs.html.erb` partial. 272 | 273 | The second way is to use the `breadcrumb_trail` without passing a block. In this case, the helper will return an enumerator that you can use to, for example, access an array of names pushed into the breadcrumb trail in order of addition. This can be handy for generating page titles from breadcrumb data. 274 | 275 | For example, you can define a `breadcrumbs_to_title` method in `ApplicationHelper` like so: 276 | 277 | ```ruby 278 | module ApplicationHelper 279 | def breadcrumbs_to_title 280 | breadcrumb_trail.map(&:name).join(">") 281 | end 282 | end 283 | ``` 284 | 285 | Use whichever of the two ways is more convenient given your application structure and needs. 286 | 287 | ## 3. Configuration 288 | 289 | There is a small set of custom opinionated defaults. The following options are valid parameters: 290 | 291 | ```ruby 292 | :match # set match type, default :inclusive (see [:match](#213-match) for more details) 293 | ``` 294 | 295 | You can override them in your views by passing them to the view `breadcrumb` helper 296 | 297 | ```erb 298 | <% breadcrumb_trail(match: :exclusive) do |name, url, styles| %> 299 | .. 300 | <% end %> 301 | ``` 302 | 303 | or by configuring an option in `config/initializers/loaf.rb`: 304 | 305 | ```ruby 306 | Loaf.configure do |config| 307 | config.match = :exclusive 308 | end 309 | ``` 310 | 311 | ## 4. Translation 312 | 313 | You can use locales files for breadcrumbs' titles. **Loaf** assumes that all breadcrumb names are scoped inside `breadcrumbs` namespace inside `loaf` scope. However, this can be easily changed by passing `scope: 'new_scope_name'` configuration option. 314 | 315 | ```ruby 316 | en: 317 | loaf: 318 | breadcrumbs: 319 | name: 'my-breadcrumb-name' 320 | ``` 321 | 322 | Therefore, in your controller/view you would do: 323 | 324 | ```ruby 325 | class Blog::CategoriesController < ApplicationController 326 | breadcrumb 'blog.categories', :blog_categories_path 327 | end 328 | ``` 329 | 330 | And corresponding entry in locale would be: 331 | 332 | ```ruby 333 | en: 334 | loaf: 335 | breadcrumbs: 336 | blog: 337 | categories: 'Article Categories' 338 | ``` 339 | 340 | ## Contributing 341 | 342 | Questions or problems? Please post them on the [issue tracker](https://github.com/piotrmurach/loaf/issues). You can contribute changes by forking the project and submitting a pull request. You can ensure the tests are passing by running `bundle` and `rake`. 343 | 344 | ## License 345 | 346 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 347 | 348 | ## Code of Conduct 349 | 350 | Everyone interacting in the Loaf project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/piotrmurach/loaf/blob/master/CODE_OF_CONDUCT.md). 351 | 352 | ## Copyright 353 | 354 | Copyright (c) 2011 Piotr Murach. See LICENSE.txt for further details. 355 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'rubygems' 4 | require 'bundler/setup' 5 | require 'bundler/gem_tasks' 6 | 7 | desc "Default: run loaf unit & integration tests." 8 | task default: :spec 9 | 10 | FileList['tasks/**/*.rake'].each(&method(:import)) 11 | 12 | desc 'Run all specs' 13 | task ci: %w[ spec ] 14 | -------------------------------------------------------------------------------- /assets/loaf_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piotrmurach/loaf/27b508c813f0dd32ce15c8b01f5a94550ee1ebc0/assets/loaf_logo.png -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "loaf" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | IFS=$'\n\t' 5 | set -vx 6 | 7 | # Install gems required by Appraisal 8 | bundle install 9 | bundle exec appraisal install 10 | -------------------------------------------------------------------------------- /config/locales/loaf.en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | loaf: 3 | errors: 4 | invalid_options: "Invalid option :%{invalid}. Valid options are: %{valid}, make sure these are the ones you are using." 5 | breadcrumbs: 6 | home: 'Home' 7 | -------------------------------------------------------------------------------- /gemfiles/rails3.2.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "ammeter", "~> 1.1.4" 6 | gem "appraisal", "~> 2.2.0" 7 | gem "yard", "~> 0.9.24" 8 | gem "capybara", "~> 2.18.0" 9 | gem "rspec-rails", "~> 3.9.0" 10 | gem "public_suffix", "~> 2.0.5" 11 | gem "railties", "~> 3.2.22.5" 12 | gem "tzinfo", "~> 0.3" 13 | gem "test-unit", "~> 3.0" 14 | 15 | group :metrics do 16 | gem "coveralls", "0.8.23" 17 | gem "simplecov", "~> 0.16.1" 18 | gem "yardstick", "~> 0.9.9" 19 | end 20 | 21 | gemspec path: "../" 22 | -------------------------------------------------------------------------------- /gemfiles/rails4.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "ammeter", "~> 1.1.4" 6 | gem "appraisal", "~> 2.2.0" 7 | gem "yard", "~> 0.9.24" 8 | gem "capybara", "~> 2.18.0" 9 | gem "rspec-rails", "~> 3.9.0" 10 | gem "public_suffix", "~> 2.0.5" 11 | gem "railties", "~> 4.0.13" 12 | gem "test-unit", "~> 3.0" 13 | 14 | group :metrics do 15 | gem "coveralls", "0.8.23" 16 | gem "simplecov", "~> 0.16.1" 17 | gem "yardstick", "~> 0.9.9" 18 | end 19 | 20 | gemspec path: "../" 21 | -------------------------------------------------------------------------------- /gemfiles/rails4.1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "ammeter", "~> 1.1.4" 6 | gem "appraisal", "~> 2.2.0" 7 | gem "yard", "~> 0.9.24" 8 | gem "capybara", "~> 2.18.0" 9 | gem "rspec-rails", "~> 3.9.0" 10 | gem "public_suffix", "~> 2.0.5" 11 | gem "railties", "~> 4.1.16" 12 | 13 | group :metrics do 14 | gem "coveralls", "0.8.23" 15 | gem "simplecov", "~> 0.16.1" 16 | gem "yardstick", "~> 0.9.9" 17 | end 18 | 19 | gemspec path: "../" 20 | -------------------------------------------------------------------------------- /gemfiles/rails4.2.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "ammeter", "~> 1.1.4" 6 | gem "appraisal", "~> 2.2.0" 7 | gem "yard", "~> 0.9.24" 8 | gem "capybara", "~> 2.18.0" 9 | gem "rspec-rails", "~> 3.9.0" 10 | gem "public_suffix", "~> 2.0.5" 11 | gem "railties", "~> 4.2.11" 12 | 13 | group :metrics do 14 | gem "coveralls", "0.8.23" 15 | gem "simplecov", "~> 0.16.1" 16 | gem "yardstick", "~> 0.9.9" 17 | end 18 | 19 | gemspec path: "../" 20 | -------------------------------------------------------------------------------- /gemfiles/rails5.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "ammeter", "~> 1.1.4" 6 | gem "appraisal", "~> 2.2.0" 7 | gem "yard", "~> 0.9.24" 8 | gem "capybara", "~> 2.18.0" 9 | gem "rspec-rails", "~> 3.9.0" 10 | gem "public_suffix", "~> 2.0.5" 11 | gem "railties", "~> 5.0.7" 12 | 13 | group :metrics do 14 | gem "coveralls", "0.8.23" 15 | gem "simplecov", "~> 0.16.1" 16 | gem "yardstick", "~> 0.9.9" 17 | end 18 | 19 | gemspec path: "../" 20 | -------------------------------------------------------------------------------- /gemfiles/rails5.1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "ammeter", "~> 1.1.4" 6 | gem "appraisal", "~> 2.2.0" 7 | gem "yard", "~> 0.9.24" 8 | gem "capybara", "~> 2.18.0" 9 | gem "rspec-rails", "~> 3.9.0" 10 | gem "public_suffix", "~> 2.0.5" 11 | gem "railties", "~> 5.1.7" 12 | 13 | group :metrics do 14 | gem "coveralls", "0.8.23" 15 | gem "simplecov", "~> 0.16.1" 16 | gem "yardstick", "~> 0.9.9" 17 | end 18 | 19 | gemspec path: "../" 20 | -------------------------------------------------------------------------------- /gemfiles/rails5.2.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "ammeter", "~> 1.1.4" 6 | gem "appraisal", "~> 2.2.0" 7 | gem "yard", "~> 0.9.24" 8 | gem "capybara", "~> 2.18.0" 9 | gem "rspec-rails", "~> 3.9.0" 10 | gem "public_suffix", "~> 2.0.5" 11 | gem "railties", "~> 5.2.4" 12 | 13 | group :metrics do 14 | gem "coveralls", "0.8.23" 15 | gem "simplecov", "~> 0.16.1" 16 | gem "yardstick", "~> 0.9.9" 17 | end 18 | 19 | gemspec path: "../" 20 | -------------------------------------------------------------------------------- /gemfiles/rails6.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "ammeter", "~> 1.1.4" 6 | gem "appraisal", "~> 2.2.0" 7 | gem "yard", "~> 0.9.24" 8 | gem "capybara", "~> 2.18.0" 9 | gem "rspec-rails", "~> 3.9.0" 10 | gem "public_suffix", "~> 2.0.5" 11 | gem "railties", "~> 6.0.3" 12 | 13 | group :metrics do 14 | gem "coveralls", "0.8.23" 15 | gem "simplecov", "~> 0.16.1" 16 | gem "yardstick", "~> 0.9.9" 17 | end 18 | 19 | gemspec path: "../" 20 | -------------------------------------------------------------------------------- /gemfiles/rails6.1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "ammeter", "~> 1.1.4" 6 | gem "appraisal", "~> 2.2.0" 7 | gem "yard", "~> 0.9.24" 8 | gem "capybara", "~> 2.18.0" 9 | gem "rspec-rails", "~> 3.9.0" 10 | gem "public_suffix", "~> 2.0.5" 11 | gem "railties", "~> 6.1.0" 12 | 13 | group :metrics do 14 | gem "coveralls", "0.8.23" 15 | gem "simplecov", "~> 0.16.1" 16 | gem "yardstick", "~> 0.9.9" 17 | end 18 | 19 | gemspec path: "../" 20 | -------------------------------------------------------------------------------- /lib/generators/loaf/install_generator.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'rails/generators' 4 | 5 | module Loaf 6 | module Generators 7 | class InstallGenerator < Rails::Generators::Base 8 | source_root File.expand_path("../../../..", __FILE__) 9 | 10 | desc 'Copy locale file to your application' 11 | 12 | def copy_locale 13 | copy_file "#{self.class.source_root}/config/locales/loaf.en.yml", "config/locales/loaf.en.yml" 14 | end 15 | end # InstallGenerator 16 | end # Generators 17 | end # Loaf 18 | -------------------------------------------------------------------------------- /lib/loaf.rb: -------------------------------------------------------------------------------- 1 | require_relative 'loaf/configuration' 2 | require_relative 'loaf/railtie' 3 | require_relative 'loaf/version' 4 | 5 | module Loaf 6 | # Set global configuration 7 | # 8 | # @api public 9 | def self.configuration=(config) 10 | @configuration = config 11 | end 12 | 13 | # Get global configuration 14 | # 15 | # @api public 16 | def self.configuration 17 | @configuration ||= Configuration.new 18 | end 19 | 20 | # Sets the Loaf configuration options. Best used by passing a block. 21 | # 22 | # Loaf.configure do |config| 23 | # config.capitalize = true 24 | # end 25 | def self.configure 26 | yield configuration 27 | end 28 | end # Loaf 29 | -------------------------------------------------------------------------------- /lib/loaf/breadcrumb.rb: -------------------------------------------------------------------------------- 1 | module Loaf 2 | # A container for breadcrumb values 3 | # @api public 4 | class Breadcrumb 5 | attr_reader :name 6 | 7 | attr_reader :path 8 | alias url path 9 | 10 | def self.[](*args) 11 | new(*args) 12 | end 13 | 14 | def initialize(name, path, current) 15 | @name = name 16 | @path = path 17 | @current = current 18 | freeze 19 | end 20 | 21 | def current? 22 | @current 23 | end 24 | 25 | def to_ary 26 | [@name, @path, @current] 27 | end 28 | alias to_a to_ary 29 | end # Breadcrumb 30 | end # Loaf 31 | -------------------------------------------------------------------------------- /lib/loaf/configuration.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Loaf 4 | class Configuration 5 | VALID_ATTRIBUTES = [ 6 | :locales_path, 7 | :match 8 | ].freeze 9 | 10 | attr_accessor(*VALID_ATTRIBUTES) 11 | 12 | DEFAULT_LOCALES_PATH = '/' 13 | 14 | DEFAULT_MATCH = :inclusive 15 | 16 | # Setup this configuration 17 | # 18 | # @api public 19 | def initialize(attributes = {}) 20 | VALID_ATTRIBUTES.each do |attr| 21 | default = self.class.const_get("DEFAULT_#{attr.to_s.upcase}") 22 | attr_value = attributes.fetch(attr) { default } 23 | send("#{attr}=", attr_value) 24 | end 25 | end 26 | 27 | # Convert all properties into hash 28 | # 29 | # @return [Hash] 30 | # 31 | # @api public 32 | def to_hash 33 | VALID_ATTRIBUTES.reduce({}) { |acc, k| acc[k] = send(k); acc } 34 | end 35 | end # Configuration 36 | end # Loaf 37 | -------------------------------------------------------------------------------- /lib/loaf/controller_extensions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'crumb' 4 | 5 | module Loaf 6 | module ControllerExtensions 7 | # Module injection 8 | # 9 | # @api private 10 | def self.included(base) 11 | base.extend ClassMethods 12 | base.send :include, InstanceMethods 13 | base.send :helper_method, :_breadcrumbs 14 | end 15 | 16 | module ClassMethods 17 | # Add breacrumb to the trail in controller as class method 18 | # 19 | # @param [String] 20 | # 21 | # @api public 22 | def breadcrumb(name, url, options = {}) 23 | normalizer = method(:_normalize_name) 24 | send(_filter_name, options) do |instance| 25 | normalized_name = normalizer.call(name, instance) 26 | normalized_url = normalizer.call(url, instance) 27 | instance.send(:breadcrumb, normalized_name, normalized_url, options) 28 | end 29 | end 30 | alias add_breadcrumb breadcrumb 31 | 32 | private 33 | 34 | # Choose available filter name 35 | # 36 | # @api private 37 | def _filter_name 38 | respond_to?(:before_action) ? :before_action : :before_filter 39 | end 40 | 41 | # Convert breadcrumb name to string 42 | # 43 | # @return [String] 44 | # 45 | # @api private 46 | def _normalize_name(name, instance) 47 | case name 48 | when NilClass 49 | when Proc 50 | if name.arity == 1 51 | instance.instance_exec(instance, &name) 52 | else 53 | instance.instance_exec(&name) 54 | end 55 | else 56 | name 57 | end 58 | end 59 | end # ClassMethods 60 | 61 | module InstanceMethods 62 | # Add breadcrumb in controller as instance method 63 | # 64 | # @param [String] name 65 | # 66 | # @param [Object] url 67 | # 68 | # @api public 69 | def breadcrumb(name, url, options = {}) 70 | _breadcrumbs << Loaf::Crumb.new(name, url, options) 71 | end 72 | alias add_breadcrumb breadcrumb 73 | 74 | # Collection of breadcrumbs 75 | # 76 | # @api private 77 | def _breadcrumbs 78 | @_breadcrumbs ||= [] 79 | end 80 | 81 | # Remove all current breadcrumbs 82 | # 83 | # @api public 84 | def clear_breadcrumbs 85 | _breadcrumbs.clear 86 | end 87 | end # InstanceMethods 88 | end # ControllerExtensions 89 | end # Loaf 90 | -------------------------------------------------------------------------------- /lib/loaf/crumb.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Loaf 4 | # Basic crumb container for internal use 5 | # @api private 6 | class Crumb 7 | attr_reader :name 8 | 9 | attr_reader :url 10 | 11 | attr_reader :match 12 | 13 | def initialize(name, url, options = {}) 14 | @name = name || raise_name_error 15 | @url = url || raise_url_error 16 | @match = options.fetch(:match, Loaf.configuration.match) 17 | freeze 18 | end 19 | 20 | def raise_name_error 21 | raise ArgumentError, 'breadcrumb first argument, `name`, cannot be nil' 22 | end 23 | 24 | def raise_url_error 25 | raise ArgumentError, 'breadcrumb second argument, `url`, cannot be nil' 26 | end 27 | end # Crumb 28 | end # Loaf 29 | -------------------------------------------------------------------------------- /lib/loaf/errors.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Loaf #:nodoc: 4 | # Default Loaf error for all custom errors. 5 | # 6 | class LoafError < StandardError 7 | BASE_KEY = "loaf.errors" 8 | 9 | def error_message(key, attributes) 10 | translate(key, attributes) 11 | end 12 | 13 | def translate(key, options) 14 | ::I18n.translate("#{BASE_KEY}.#{key}", **{ :locale => :en }.merge(options)) 15 | end 16 | end 17 | 18 | # Raised when invalid options are passed to breadcrumbs view renderer. 19 | # InvalidOptions.new :name, :crumber, [:crumb] 20 | # 21 | class InvalidOptions < LoafError 22 | def initialize(invalid, valid) 23 | super( 24 | error_message("invalid_options", 25 | { :invalid => invalid, :valid => valid.join(', ') } 26 | ) 27 | ) 28 | end 29 | end 30 | end # Loaf 31 | -------------------------------------------------------------------------------- /lib/loaf/options_validator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'errors' 4 | 5 | module Loaf 6 | # A mixin to validate configuration options 7 | module OptionsValidator 8 | # Check if options are valid or not 9 | # 10 | # @param [Hash] options 11 | # 12 | # @return [Boolean] 13 | # 14 | # @api public 15 | def valid?(options) 16 | valid_options = Loaf::Configuration::VALID_ATTRIBUTES 17 | options.each_key do |key| 18 | unless valid_options.include?(key) 19 | fail Loaf::InvalidOptions.new(key, valid_options) 20 | end 21 | end 22 | true 23 | end 24 | end # OptionsValidator 25 | end # Loaf 26 | -------------------------------------------------------------------------------- /lib/loaf/railtie.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'action_controller' 4 | require 'action_view' 5 | 6 | require_relative 'controller_extensions' 7 | require_relative 'view_extensions' 8 | 9 | module Loaf 10 | class RailtieHelpers 11 | class << self 12 | def insert_view 13 | ActionController::Base.helper Loaf::ViewExtensions 14 | end 15 | 16 | def insert_controller 17 | ActionController::Base.send :include, Loaf::ControllerExtensions 18 | end 19 | end 20 | end # RailtieHelpers 21 | 22 | if defined?(Rails::Railtie) 23 | class Railtie < Rails::Railtie 24 | initializer "loaf.extend_action_controller_base" do |app| 25 | ActiveSupport.on_load :action_controller do 26 | Loaf::RailtieHelpers.insert_controller 27 | Loaf::RailtieHelpers.insert_view 28 | end 29 | end 30 | end 31 | else 32 | Loaf::RailtieHelpers.insert_controller 33 | Loaf::RailtieHelpers.insert_view 34 | end 35 | end # Loaf 36 | -------------------------------------------------------------------------------- /lib/loaf/translation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Loaf 4 | module Translation 5 | # Returns translation lookup 6 | # 7 | # @return [String] 8 | # 9 | # @api private 10 | def translation_scope 11 | 'loaf.breadcrumbs' 12 | end 13 | module_function :translation_scope 14 | 15 | # Translate breadcrumb title 16 | # 17 | # @param [String] :title 18 | # @param [Hash] options 19 | # @option options [String] :scope 20 | # The translation scope 21 | # @option options [String] :default 22 | # The default translation 23 | # 24 | # @return [String] 25 | # 26 | # @api public 27 | def find_title(title, options = {}) 28 | return title if title.nil? || title.empty? 29 | 30 | options[:scope] ||= translation_scope 31 | options[:default] = Array(options[:default]) 32 | options[:default] << title if options[:default].empty? 33 | I18n.t(title.to_s, **options) 34 | end 35 | module_function :find_title 36 | end # Translation 37 | end # Loaf 38 | -------------------------------------------------------------------------------- /lib/loaf/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Loaf 4 | VERSION = "0.10.0" 5 | end # Loaf 6 | -------------------------------------------------------------------------------- /lib/loaf/view_extensions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'breadcrumb' 4 | require_relative 'crumb' 5 | require_relative 'options_validator' 6 | require_relative 'translation' 7 | 8 | module Loaf 9 | # A mixin to define view extensions 10 | module ViewExtensions 11 | include Loaf::OptionsValidator 12 | 13 | def initialize(*) 14 | @_breadcrumbs ||= [] 15 | super 16 | end 17 | 18 | # Checks to see if any breadcrumbs have been added 19 | # 20 | # @return [Boolean] 21 | # 22 | # @api public 23 | def breadcrumbs? 24 | _breadcrumbs.present? 25 | end 26 | 27 | # Adds breadcrumbs inside view. 28 | # 29 | # @param [String] name 30 | # the breadcrumb name 31 | # @param [Object] url 32 | # the breadcrumb url 33 | # @param [Hash] options 34 | # the breadcrumb options 35 | # 36 | # @api public 37 | def breadcrumb(name, url, options = {}) 38 | _breadcrumbs << Loaf::Crumb.new(name, url, options) 39 | end 40 | alias add_breadcrumb breadcrumb 41 | 42 | # Renders breadcrumbs inside view. 43 | # 44 | # @param [Hash] options 45 | # 46 | # @api public 47 | def breadcrumb_trail(options = {}) 48 | return enum_for(:breadcrumb_trail, options) unless block_given? 49 | 50 | valid?(options) 51 | 52 | _breadcrumbs.each do |crumb| 53 | name = title_for(crumb.name) 54 | path = url_for(_expand_url(crumb.url)) 55 | current = current_crumb?(path, options.fetch(:match) { crumb.match }) 56 | 57 | yield(Loaf::Breadcrumb[name, path, current]) 58 | end 59 | end 60 | 61 | # Check if breadcrumb is current based on the pattern 62 | # 63 | # @param [String] path 64 | # @param [Object] pattern 65 | # the pattern to match on 66 | # 67 | # @api public 68 | def current_crumb?(path, pattern = :inclusive) 69 | return false unless request.get? || request.head? 70 | 71 | origin_path = URI::DEFAULT_PARSER.unescape(path).force_encoding(Encoding::BINARY) 72 | 73 | request_uri = request.fullpath 74 | request_uri = URI::DEFAULT_PARSER.unescape(request_uri) 75 | request_uri.force_encoding(Encoding::BINARY) 76 | 77 | # strip away trailing slash 78 | if origin_path.start_with?('/') && origin_path != '/' 79 | origin_path.chomp!('/') 80 | request_uri.chomp!('/') 81 | end 82 | 83 | if %r{^\w+://} =~ origin_path 84 | origin_path.chomp!('/') 85 | request_uri.insert(0, "#{request.protocol}#{request.host_with_port}") 86 | end 87 | 88 | case pattern 89 | when :inclusive 90 | !request_uri.match(/^#{Regexp.escape(origin_path)}(\/.*|\?.*)?$/).nil? 91 | when :exclusive 92 | !request_uri.match(/^#{Regexp.escape(origin_path)}\/?(\?.*)?$/).nil? 93 | when :exact 94 | request_uri == origin_path 95 | when :force 96 | true 97 | when Regexp 98 | !request_uri.match(pattern).nil? 99 | when Hash 100 | query_params = URI.encode_www_form(pattern) 101 | !request_uri.match(/^#{Regexp.escape(origin_path)}\/?(\?.*)?.*?#{query_params}.*$/).nil? 102 | else 103 | raise ArgumentError, "Unknown `:#{pattern}` match option!" 104 | end 105 | end 106 | 107 | private 108 | 109 | # Find title translation for a crumb name 110 | # 111 | # @return [String] 112 | # 113 | # @api private 114 | def title_for(name) 115 | Translation.find_title(name) 116 | end 117 | 118 | # Expand url in the current context of the view 119 | # 120 | # @api private 121 | def _expand_url(url) 122 | case url 123 | when String, Symbol 124 | respond_to?(url) ? send(url) : url 125 | when Proc 126 | url.call(self) 127 | else 128 | url 129 | end 130 | end 131 | end # ViewExtensions 132 | end # Loaf 133 | -------------------------------------------------------------------------------- /loaf.gemspec: -------------------------------------------------------------------------------- 1 | require_relative "lib/loaf/version" 2 | 3 | Gem::Specification.new do |spec| 4 | spec.name = "loaf" 5 | spec.version = Loaf::VERSION.dup 6 | spec.authors = ["Piotr Murach"] 7 | spec.email = ["piotr@piotrmurach.com"] 8 | spec.summary = %q{Loaf manages and displays breadcrumb trails in your Rails application.} 9 | spec.description = %q{Loaf manages and displays breadcrumb trails in your Rails app. It aims to handle breadcrumb data through easy dsl and expose it through view helpers without any assumptions about markup.} 10 | spec.homepage = "https://github.com/piotrmurach/loaf" 11 | spec.license = "MIT" 12 | if spec.respond_to?(:metadata=) 13 | spec.metadata = { 14 | "allowed_push_host" => "https://rubygems.org", 15 | "bug_tracker_uri" => "https://github.com/piotrmurach/loaf/issues", 16 | "changelog_uri" => "https://github.com/piotrmurach/loaf/blob/master/CHANGELOG.md", 17 | "documentation_uri" => "https://www.rubydoc.info/gems/loaf", 18 | "homepage_uri" => spec.homepage, 19 | "source_code_uri" => "https://github.com/piotrmurach/loaf", 20 | } 21 | end 22 | spec.files = Dir["{lib,config}/**/*"] 23 | spec.extra_rdoc_files = Dir["README.md", "CHANGELOG.md", "LICENSE.txt"] 24 | spec.bindir = "exe" 25 | spec.require_paths = ["lib"] 26 | spec.required_ruby_version = ">= 1.9.3" 27 | 28 | spec.add_dependency "railties", ">= 3.2" 29 | 30 | spec.add_development_dependency "rake" 31 | end 32 | -------------------------------------------------------------------------------- /spec/integration/breadcrumb_trail_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | RSpec.describe "breadcrumbs trail" do 4 | before do 5 | include ActionView::TestCase::Behavior 6 | end 7 | 8 | it "shows root breadcrumb" do 9 | visit root_path 10 | 11 | page.within '#breadcrumbs .selected' do 12 | expect(page.html).to include('Home') 13 | end 14 | end 15 | 16 | it "inherits controller breadcrumb and adds index action breadcrumb" do 17 | visit posts_path 18 | 19 | page.within '#breadcrumbs' do 20 | expect(page.html).to include('Home') 21 | end 22 | page.within '#breadcrumbs .selected' do 23 | expect(page.html).to include('All Posts') 24 | end 25 | end 26 | 27 | it 'filters out controller breadcrumb and adds new action breadcrumb' do 28 | visit new_post_path 29 | 30 | page.within '#breadcrumbs' do 31 | expect(page).to_not have_content('Home') 32 | expect(page).to have_content('New Post') 33 | end 34 | end 35 | 36 | it "adds breadcrumb in view with path variable" do 37 | visit post_path(1) 38 | 39 | page.within '#breadcrumbs .selected' do 40 | expect(page.html).to include('Show Post in view') 41 | end 42 | end 43 | 44 | it 'is current when forced' do 45 | visit new_post_path 46 | 47 | expect(page.current_path).to eq(new_post_path) 48 | page.within '#breadcrumbs' do 49 | expect(page).to have_selector('li.selected', count: 2) 50 | expect(page.html).to include('All') 51 | expect(page.html).to include('New Post') 52 | end 53 | end 54 | 55 | it "allows for procs in name and url" do 56 | visit post_comments_path(1) 57 | 58 | page.within '#breadcrumbs .selected' do 59 | expect(page.html).to include('Post comments') 60 | end 61 | end 62 | 63 | it "allows for procs in name and url without supplying the controller" do 64 | visit post_comments_path(1) 65 | 66 | page.within "#breadcrumbs .selected" do 67 | expect(page.html).to include( 68 | ''\ 69 | "Post comments No Controller" 70 | ) 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /spec/integration/configuration_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | RSpec.describe 'setting configuration options' do 4 | it "contains 'selected' inside the breadcrumb markup" do 5 | visit posts_path 6 | page.within '#breadcrumbs' do 7 | expect(page).to have_selector('.selected') 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/rails_app/Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | # Add your own tasks in files placed in lib/tasks ending in .rake, 3 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 4 | 5 | require File.expand_path('../config/application', __FILE__) 6 | require 'rake' 7 | 8 | RailsApp::Application.load_tasks 9 | -------------------------------------------------------------------------------- /spec/rails_app/app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | /* manifest */ 2 | -------------------------------------------------------------------------------- /spec/rails_app/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | protect_from_forgery 3 | 4 | breadcrumb 'Home', :root_path, only: :index 5 | end 6 | -------------------------------------------------------------------------------- /spec/rails_app/app/controllers/comments_controller.rb: -------------------------------------------------------------------------------- 1 | class Article < Struct.new(:id, :title); end 2 | 3 | class CommentsController < ApplicationController 4 | 5 | breadcrumb lambda { |c| c.find_article(c.params[:post_id]).title }, 6 | lambda { |c| c.post_comments_path(c.params[:post_id]) } 7 | 8 | breadcrumb -> { find_article(params[:post_id]).title + " No Controller" }, 9 | -> { post_comments_path(params[:post_id], no_controller: true) } 10 | 11 | def index 12 | end 13 | 14 | def show 15 | end 16 | 17 | def find_article(id) 18 | ::Article.new(id, 'Post comments') 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/rails_app/app/controllers/home_controller.rb: -------------------------------------------------------------------------------- 1 | class HomeController < ApplicationController 2 | def index 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /spec/rails_app/app/controllers/posts_controller.rb: -------------------------------------------------------------------------------- 1 | class Post < Struct.new(:id); end 2 | 3 | class PostsController < ApplicationController 4 | def index 5 | breadcrumb 'all_posts', posts_path 6 | end 7 | 8 | def show 9 | @post = ::Post.new(1) 10 | end 11 | 12 | def new 13 | breadcrumb 'All', :posts_path, match: :force 14 | breadcrumb 'New Post', new_post_path 15 | end 16 | 17 | def create 18 | render action: :new 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/rails_app/app/views/comments/index.html.erb: -------------------------------------------------------------------------------- 1 |

Post comments index

2 | -------------------------------------------------------------------------------- /spec/rails_app/app/views/home/index.html.erb: -------------------------------------------------------------------------------- 1 |

Home page

2 | -------------------------------------------------------------------------------- /spec/rails_app/app/views/layouts/_breadcrumbs.html.erb: -------------------------------------------------------------------------------- 1 | <% if breadcrumb_trail.any? %> 2 | 12 | <% end %> 13 | -------------------------------------------------------------------------------- /spec/rails_app/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | RailsApp 5 | <%= csrf_meta_tags %> 6 | 7 | 8 | 9 | <%= render 'layouts/breadcrumbs' %> 10 | 11 | <%= yield %> 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /spec/rails_app/app/views/posts/index.html.erb: -------------------------------------------------------------------------------- 1 |

Posts:index

2 | -------------------------------------------------------------------------------- /spec/rails_app/app/views/posts/new.html.erb: -------------------------------------------------------------------------------- 1 |

Posts:new

2 | <%= form_tag posts_path, :method => "POST" do %> 3 | <% submit_tag "Create"%> 4 | <% end %> 5 | -------------------------------------------------------------------------------- /spec/rails_app/app/views/posts/show.html.erb: -------------------------------------------------------------------------------- 1 | <% breadcrumb 'Show Post in view', post_path(@post.id) %> 2 | -------------------------------------------------------------------------------- /spec/rails_app/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run RailsApp::Application 5 | -------------------------------------------------------------------------------- /spec/rails_app/config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | require "action_controller/railtie" 4 | require "action_view/railtie" 5 | require "rails/test_unit/railtie" 6 | 7 | if defined?(Bundler) 8 | # If you precompile assets before deploying to production, use this line 9 | Bundler.require *Rails.groups(:assets => %w(development test)) 10 | # If you want your assets lazily compiled in production, use this line 11 | # Bundler.require(:default, :assets, Rails.env) 12 | end 13 | 14 | module RailsApp 15 | class Application < Rails::Application 16 | # Settings in config/environments/* take precedence over those specified here. 17 | # Application configuration should go into files in config/initializers 18 | # -- all .rb files in that directory are automatically loaded. 19 | 20 | # Custom directories with classes and modules you want to be autoloadable. 21 | # config.autoload_paths += %W(#{config.root}/extras) 22 | 23 | # Only load the plugins named here, in the order given (default is alphabetical). 24 | # :all can be used as a placeholder for all plugins not explicitly named. 25 | # config.plugins = [ :exception_notification, :ssl_requirement, :all ] 26 | 27 | # Activate observers that should always be running. 28 | # config.active_record.observers = :cacher, :garbage_collector, :forum_observer 29 | 30 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 31 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 32 | # config.time_zone = 'Central Time (US & Canada)' 33 | 34 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 35 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 36 | # config.i18n.default_locale = :de 37 | 38 | # Configure the default encoding used in templates for Ruby 1.9. 39 | config.encoding = "utf-8" 40 | 41 | # Configure sensitive parameters which will be filtered from the log file. 42 | config.filter_parameters += [:password] 43 | 44 | # Enable the asset pipeline 45 | # config.assets.enabled = true 46 | 47 | # Version of your assets, change this if you want to expire all your assets 48 | # config.assets.version = '1.0' 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /spec/rails_app/config/boot.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | gemfile = File.expand_path('../../../../Gemfile', __FILE__) 3 | 4 | if File.exists?(gemfile) 5 | ENV['BUNDLE_GEMFILE'] ||= gemfile 6 | require 'bundler' 7 | Bundler.setup 8 | end 9 | 10 | $:.unshift File.expand_path('../../../../lib', __FILE__) 11 | -------------------------------------------------------------------------------- /spec/rails_app/config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite version 3.x 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem 'sqlite3' 6 | development: 7 | adapter: sqlite3 8 | database: db/development.sqlite3 9 | pool: 5 10 | timeout: 5000 11 | 12 | # Warning: The database defined as "test" will be erased and 13 | # re-generated from your development database when you run "rake". 14 | # Do not set this db to the same as development or production. 15 | test: 16 | adapter: sqlite3 17 | database: db/test.sqlite3 18 | pool: 5 19 | timeout: 5000 20 | 21 | production: 22 | adapter: sqlite3 23 | database: db/production.sqlite3 24 | pool: 5 25 | timeout: 5000 26 | -------------------------------------------------------------------------------- /spec/rails_app/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the rails application 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the rails application 5 | RailsApp::Application.initialize! 6 | -------------------------------------------------------------------------------- /spec/rails_app/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | RailsApp::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Log error messages when you accidentally call methods on nil. 10 | config.whiny_nils = true 11 | 12 | # Show full error reports and disable caching 13 | config.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Don't care if the mailer can't send 17 | # config.action_mailer.raise_delivery_errors = false 18 | 19 | # Print deprecation notices to the Rails logger 20 | config.active_support.deprecation = :log 21 | 22 | # Only use best-standards-support built into browsers 23 | config.action_dispatch.best_standards_support = :builtin 24 | 25 | # Do not compress assets 26 | # config.assets.compress = false 27 | 28 | # Expands the lines which load the assets 29 | # config.assets.debug = true 30 | 31 | config.eager_load = false 32 | end 33 | -------------------------------------------------------------------------------- /spec/rails_app/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | RailsApp::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # Code is not reloaded between requests 5 | config.cache_classes = true 6 | 7 | # Full error reports are disabled and caching is turned on 8 | config.consider_all_requests_local = false 9 | config.action_controller.perform_caching = true 10 | 11 | # Disable Rails's static asset server (Apache or nginx will already do this) 12 | config.serve_static_assets = false 13 | 14 | # Compress JavaScripts and CSS 15 | config.assets.compress = true 16 | 17 | # Don't fallback to assets pipeline if a precompiled asset is missed 18 | config.assets.compile = false 19 | 20 | # Generate digests for assets URLs 21 | config.assets.digest = true 22 | 23 | # Defaults to Rails.root.join("public/assets") 24 | # config.assets.manifest = YOUR_PATH 25 | 26 | # Specifies the header that your server uses for sending files 27 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache 28 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx 29 | 30 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 31 | # config.force_ssl = true 32 | 33 | # See everything in the log (default is :info) 34 | # config.log_level = :debug 35 | 36 | # Use a different logger for distributed setups 37 | # config.logger = SyslogLogger.new 38 | 39 | # Use a different cache store in production 40 | # config.cache_store = :mem_cache_store 41 | 42 | # Enable serving of images, stylesheets, and JavaScripts from an asset server 43 | # config.action_controller.asset_host = "http://assets.example.com" 44 | 45 | # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added) 46 | # config.assets.precompile += %w( search.js ) 47 | 48 | # Disable delivery errors, bad email addresses will be ignored 49 | # config.action_mailer.raise_delivery_errors = false 50 | 51 | # Enable threaded mode 52 | # config.threadsafe! 53 | 54 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 55 | # the I18n.default_locale when a translation can not be found) 56 | config.i18n.fallbacks = true 57 | 58 | # Send deprecation notices to registered listeners 59 | config.active_support.deprecation = :notify 60 | 61 | config.eager_load = true 62 | end 63 | -------------------------------------------------------------------------------- /spec/rails_app/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | RailsApp::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Configure static asset server for tests with Cache-Control for performance 11 | config.serve_static_files = true 12 | config.static_cache_control = "public, max-age=3600" 13 | 14 | # Show full error reports and disable caching 15 | config.consider_all_requests_local = true 16 | config.action_controller.perform_caching = false 17 | 18 | # Raise exceptions instead of rendering exception templates 19 | config.action_dispatch.show_exceptions = false 20 | 21 | # Disable request forgery protection in test environment 22 | config.action_controller.allow_forgery_protection = false 23 | 24 | # Tell Action Mailer not to deliver emails to the real world. 25 | # The :test delivery method accumulates sent emails in the 26 | # ActionMailer::Base.deliveries array. 27 | # config.action_mailer.delivery_method = :test 28 | 29 | # Use SQL instead of Active Record's schema dumper when creating the test database. 30 | # This is necessary if your schema can't be completely dumped by the schema dumper, 31 | # like if you have constraints or database-specific column types 32 | # config.active_record.schema_format = :sql 33 | 34 | # Print deprecation notices to the stderr 35 | config.active_support.deprecation = :stderr 36 | 37 | # Allow pass debug_assets=true as a query parameter to load pages with unpackaged assets 38 | # config.assets.allow_debugging = true 39 | 40 | config.eager_load = false 41 | end 42 | -------------------------------------------------------------------------------- /spec/rails_app/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /spec/rails_app/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format 4 | # (all these examples are active by default): 5 | # ActiveSupport::Inflector.inflections do |inflect| 6 | # inflect.plural /^(ox)$/i, '\1en' 7 | # inflect.singular /^(ox)en/i, '\1' 8 | # inflect.irregular 'person', 'people' 9 | # inflect.uncountable %w( fish sheep ) 10 | # end 11 | -------------------------------------------------------------------------------- /spec/rails_app/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | # Mime::Type.register_alias "text/html", :iphone 6 | -------------------------------------------------------------------------------- /spec/rails_app/config/initializers/secret_token.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | # Make sure the secret is at least 30 characters and all random, 6 | # no regular words or you'll be exposed to dictionary attacks. 7 | RailsApp::Application.config.secret_token = '2bc2743124978fc50d054e09cc457b9a6de3cb8c1b7000d78a9af674e93245bf292f2f57f56ad782dc6f7da1695206251d7c9541ec3870467275e4c65447db48' 8 | -------------------------------------------------------------------------------- /spec/rails_app/config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | RailsApp::Application.config.session_store :cookie_store, key: '_rails_app_session' 4 | 5 | # Use the database for sessions instead of the cookie-based default, 6 | # which shouldn't be used to store highly confidential information 7 | # (create the session table with "rails generate session_migration") 8 | # RailsApp::Application.config.session_store :active_record_store 9 | -------------------------------------------------------------------------------- /spec/rails_app/config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | # 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # Disable root element in JSON by default. 12 | ActiveSupport.on_load(:active_record) do 13 | self.include_root_in_json = false 14 | end 15 | -------------------------------------------------------------------------------- /spec/rails_app/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Sample localization file for English. Add more files in this directory for other locales. 2 | # See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. 3 | 4 | en: 5 | hello: "Hello world" 6 | -------------------------------------------------------------------------------- /spec/rails_app/config/locales/loaf.en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | loaf: 3 | errors: 4 | invalid_options: "Invalid option :%{invalid}. Valid options are: %{valid}, make sure these are the ones you are using." 5 | breadcrumbs: 6 | all_posts: 'All Posts' 7 | -------------------------------------------------------------------------------- /spec/rails_app/config/routes.rb: -------------------------------------------------------------------------------- 1 | RailsApp::Application.routes.draw do 2 | root :to => 'home#index' 3 | 4 | resources :posts do 5 | resources :comments 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/rails_app/config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rake secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | development: 14 | secret_key_base: c392df63ec28b42b36b92a5b9e8603fa62d7807024d07801ab162ec7a2a165a87d27a4a8535ce09cd2bf4a82afe45e3080fb8cc56613d608e5b18335594782b3 15 | 16 | test: 17 | secret_key_base: 4b1fba0d12c53b8b21880fda47b603b3dcca117ccf41fcc70091351b88ede5d4c5c91371f407cd056efd66fa5f306c23add65a89d4304e06a4243289ee97be1d 18 | 19 | # Do not keep production secrets in the repository, 20 | # instead read values from the environment. 21 | production: 22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 23 | -------------------------------------------------------------------------------- /spec/rails_app/db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }]) 7 | # Mayor.create(name: 'Emanuel', city: cities.first) 8 | -------------------------------------------------------------------------------- /spec/rails_app/log/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piotrmurach/loaf/27b508c813f0dd32ce15c8b01f5a94550ee1ebc0/spec/rails_app/log/.gitkeep -------------------------------------------------------------------------------- /spec/rails_app/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

The page you were looking for doesn't exist.

23 |

You may have mistyped the address or the page may have moved.

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /spec/rails_app/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

The change you wanted was rejected.

23 |

Maybe you tried to change something you didn't have access to.

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /spec/rails_app/public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

We're sorry, but something went wrong.

23 |

We've been notified about this issue and we'll take a look at it shortly.

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /spec/rails_app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piotrmurach/loaf/27b508c813f0dd32ce15c8b01f5a94550ee1ebc0/spec/rails_app/public/favicon.ico -------------------------------------------------------------------------------- /spec/rails_app/public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-Agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | if ENV["COVERAGE"] == "true" 4 | require "simplecov" 5 | require "coveralls" 6 | 7 | SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([ 8 | SimpleCov::Formatter::HTMLFormatter, 9 | Coveralls::SimpleCov::Formatter 10 | ]) 11 | 12 | SimpleCov.start do 13 | command_name "spec" 14 | add_filter "spec" 15 | end 16 | end 17 | 18 | # Configure Rails Environment 19 | ENV["RAILS_ENV"] = "test" 20 | 21 | require "rails_app/config/environment.rb" 22 | require "rspec/rails" 23 | require "loaf" 24 | 25 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f} 26 | 27 | RSpec.configure do |config| 28 | config.expect_with :rspec do |expectations| 29 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 30 | end 31 | 32 | config.mock_with :rspec do |mocks| 33 | mocks.verify_partial_doubles = true 34 | end 35 | 36 | # Limits the available syntax to the non-monkey patched syntax that is recommended. 37 | config.disable_monkey_patching! 38 | 39 | # This setting enables warnings. It's recommended, but in some cases may 40 | # be too noisy due to issues in dependencies. 41 | config.warnings = true 42 | 43 | if config.files_to_run.one? 44 | config.default_formatter = "doc" 45 | end 46 | 47 | config.profile_examples = 2 48 | 49 | config.order = :random 50 | 51 | Kernel.srand config.seed 52 | end 53 | -------------------------------------------------------------------------------- /spec/support/capybara.rb: -------------------------------------------------------------------------------- 1 | require 'capybara/rails' 2 | require 'capybara/dsl' 3 | 4 | RSpec.configure do |c| 5 | c.include Capybara::DSL, :file_path => /\bspec\/integration\// 6 | end 7 | Capybara.default_driver = :rack_test 8 | Capybara.default_selector = :css 9 | -------------------------------------------------------------------------------- /spec/support/dummy_controller.rb: -------------------------------------------------------------------------------- 1 | class DummyController < ActionController::Base 2 | def self.before_filter(options, &block) 3 | yield self.new 4 | end 5 | class << self 6 | alias before_action before_filter 7 | end 8 | include Loaf::ControllerExtensions 9 | end 10 | -------------------------------------------------------------------------------- /spec/support/dummy_view.rb: -------------------------------------------------------------------------------- 1 | require "action_view" 2 | 3 | class DummyView < ActionView::Base 4 | module FakeRequest 5 | class Request 6 | attr_accessor :path, :fullpath, :protocol, :host_with_port 7 | def get? 8 | true 9 | end 10 | end 11 | def request 12 | @request ||= Request.new 13 | end 14 | def params 15 | @params ||= {} 16 | end 17 | end 18 | 19 | include FakeRequest 20 | include ActionView::Helpers::UrlHelper 21 | include Loaf::ViewExtensions 22 | 23 | def initialize 24 | context = ActionView::LookupContext.new([]) 25 | assigns = {} 26 | controller = nil 27 | super(context, assigns, controller) 28 | end 29 | 30 | attr_reader :_breadcrumbs 31 | 32 | routes = ActionDispatch::Routing::RouteSet.new 33 | routes.draw do 34 | get "/" => "foo#bar", :as => :home 35 | get "/posts" => "foo#posts" 36 | get "/posts/:title" => "foo#posts" 37 | get "/post/:id" => "foo#post", :as => :post 38 | get "/post/:title" => "foo#title" 39 | get "/post/:id/comments" => "foo#comments" 40 | 41 | namespace :blog do 42 | get "/" => "foo#bar" 43 | end 44 | end 45 | 46 | include routes.url_helpers 47 | 48 | def set_path(path) 49 | request.path = path 50 | request.fullpath = path 51 | request.protocol = "http://" 52 | request.host_with_port = "www.example.com" 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /spec/support/load_routes.rb: -------------------------------------------------------------------------------- 1 | RSpec.configure do |c| 2 | c.include Rails.application.routes.url_helpers, 3 | :file_path => /\bspec\/integration\// 4 | end 5 | -------------------------------------------------------------------------------- /spec/unit/configuration_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Loaf::Configuration do 4 | it "allows to set and read attributes" do 5 | config = Loaf::Configuration.new 6 | config.match = :exact 7 | expect(config.match).to eq(:exact) 8 | end 9 | 10 | it "accepts attributes at initialization" do 11 | options = { locales_path: "/lib", match: :exact } 12 | config = Loaf::Configuration.new(options) 13 | 14 | expect(config.locales_path).to eq("/lib") 15 | expect(config.match).to eq(:exact) 16 | end 17 | 18 | it "exports configuration as hash" do 19 | config = Loaf::Configuration.new 20 | expect(config.to_hash).to eq({ 21 | locales_path: "/", 22 | match: :inclusive 23 | }) 24 | end 25 | 26 | it "yields configuration" do 27 | conf = double(:conf) 28 | allow(Loaf).to receive(:configuration).and_return(conf) 29 | expect { |b| 30 | Loaf.configure(&b) 31 | }.to yield_with_args(conf) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/unit/controller_extensions_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | RSpec.describe Loaf::ControllerExtensions do 4 | 5 | context "when classes extend controller_extensions" do 6 | it { expect(DummyController).to respond_to(:add_breadcrumb) } 7 | it { expect(DummyController).to respond_to(:breadcrumb) } 8 | it { expect(DummyController.new).to respond_to(:add_breadcrumb) } 9 | it { expect(DummyController.new).to respond_to(:breadcrumb) } 10 | it { expect(DummyController.new).to respond_to(:clear_breadcrumbs) } 11 | end 12 | 13 | context "class methods" do 14 | it "invokes before_action" do 15 | allow(DummyController).to receive(:before_action) 16 | allow(DummyController).to receive(:respond_to?).and_return(true) 17 | DummyController.breadcrumb("name", "url_path") 18 | expect(DummyController).to have_received(:before_action) 19 | end 20 | 21 | it "delegates breadcrumb registration to controller instance" do 22 | name = "List objects" 23 | url = :object_path 24 | options = {force: true} 25 | instance = double(:controller_instance).as_null_object 26 | 27 | allow(DummyController).to receive(:new).and_return(instance) 28 | DummyController.breadcrumb(name, url, options) 29 | expect(instance).to have_received(:breadcrumb).with(name, url, options) 30 | end 31 | end 32 | 33 | context "instance methods" do 34 | it "instantiates breadcrumbs container" do 35 | name = "List objects" 36 | url = :object_path 37 | instance = DummyController.new 38 | 39 | allow(Loaf::Crumb).to receive(:new) 40 | instance.breadcrumb(name, url) 41 | expect(Loaf::Crumb).to have_received(:new).with(name, url, {}) 42 | end 43 | 44 | it "adds breadcrumb to collection" do 45 | name = "List objects" 46 | url = :object_path 47 | instance = DummyController.new 48 | 49 | expect { 50 | instance.breadcrumb(name, url) 51 | }.to change { instance._breadcrumbs.size }.by(1) 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /spec/unit/crumb_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe Loaf::Crumb do 2 | it "fails when name is nil" do 3 | expect { 4 | Loaf::Crumb.new(nil, "path") 5 | }.to raise_error(ArgumentError, 6 | /breadcrumb first argument, `name`, cannot be nil/) 7 | end 8 | 9 | it "fails when url is nil" do 10 | expect { 11 | Loaf::Crumb.new("name", nil) 12 | }.to raise_error(ArgumentError, 13 | /breadcrumb second argument, `url`, cannot be nil/) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/unit/generators/install_generator_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'fileutils' 4 | require 'generators/loaf/install_generator' 5 | 6 | RSpec.describe Loaf::Generators::InstallGenerator, type: :generator do 7 | destination File.expand_path("../../../tmp", __FILE__) 8 | 9 | before { prepare_destination } 10 | 11 | it "copies loaf locales to the host application" do 12 | run_generator 13 | locale = file("config/locales/loaf.en.yml") 14 | expect(locale).to exist 15 | FileUtils.rm_rf(File.expand_path("../../../tmp", __FILE__)) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/unit/options_validator_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | RSpec.describe Loaf::OptionsValidator, ".valid?" do 4 | let(:klass) { Class.extend Loaf::OptionsValidator } 5 | 6 | it "validates succesfully known option" do 7 | expect(klass.valid?(match: :exact)).to eq(true) 8 | end 9 | 10 | it "validates unknown option with an error" do 11 | expect { 12 | klass.valid?(invalid_param: true) 13 | }.to raise_error(Loaf::InvalidOptions) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/unit/translation_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | RSpec.describe Loaf::Translation do 4 | 5 | before { I18n.backend = I18n::Backend::Simple.new } 6 | 7 | after { I18n.backend.reload! } 8 | 9 | it "doesn't translate empty title" do 10 | expect(described_class.find_title("")).to eql("") 11 | end 12 | 13 | it "skips translation if doesn't find a matching scope" do 14 | expect(described_class.find_title("unknown")).to eql("unknown") 15 | end 16 | 17 | it "translates breadcrumb title" do 18 | I18n.backend.store_translations "en", loaf: { breadcrumbs: { home: "Home"}} 19 | expect(described_class.find_title("home")).to eql("Home") 20 | end 21 | 22 | it "does not translates breadcrumb name with missing scope" do 23 | I18n.backend.store_translations "en", breadcrumbs: {home: "Home"} 24 | expect(described_class.find_title("home")).to eql("home") 25 | end 26 | 27 | it "translates breadcrumb name using default option" do 28 | expect(described_class.find_title("home", default: "breadcrumb default name")).to eql("breadcrumb default name") 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/unit/view_extensions/breadcrumb_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | RSpec.describe Loaf::ViewExtensions, "#breadcrumb" do 4 | it "exposes add_breadcrumb alias" do 5 | expect(DummyView.new).to respond_to(:add_breadcrumb) 6 | end 7 | 8 | it "creates crumb instance" do 9 | instance = DummyView.new 10 | name = "Home" 11 | url = :home_path 12 | allow(Loaf::Crumb).to receive(:new).with(name, url, {}) 13 | instance.breadcrumb name, url 14 | expect(Loaf::Crumb).to have_received(:new).with(name, url, {}) 15 | end 16 | 17 | it "adds crumb to breadcrumbs storage" do 18 | instance = DummyView.new 19 | expect { 20 | instance.breadcrumb "Home", :home_path 21 | }.to change { instance._breadcrumbs.size }.by(1) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/unit/view_extensions/breadcrumb_trail_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | RSpec.describe Loaf::ViewExtensions, "#breadcrumb_trail" do 4 | it "doesn't configure any breadcrumbs by default" do 5 | view = DummyView.new 6 | 7 | expect(view.breadcrumb_trail.to_a).to be_empty 8 | end 9 | 10 | it "resolves breadcrumb paths" do 11 | view = DummyView.new 12 | view.breadcrumb("home", :home_path) 13 | view.breadcrumb("posts", :posts_path) 14 | view.set_path("/posts") 15 | 16 | yielded = [] 17 | block = -> (crumb) { yielded << crumb.to_a } 18 | view.breadcrumb_trail(&block) 19 | 20 | expect(yielded).to eq([ 21 | ["home", "/", false], 22 | ["posts", "/posts", true] 23 | ]) 24 | end 25 | 26 | it "matches current path with :inclusive option as default" do 27 | view = DummyView.new 28 | view.breadcrumb("home", :home_path) 29 | view.breadcrumb("posts", :posts_path) 30 | view.set_path("/posts?id=73-title") 31 | 32 | yielded = [] 33 | block = -> (crumb) { yielded << crumb.to_a } 34 | view.breadcrumb_trail(&block) 35 | 36 | expect(yielded).to eq([ 37 | ["home", "/", false], 38 | ["posts", "/posts", true] 39 | ]) 40 | end 41 | 42 | it "matches current path with :inclusive when query params" do 43 | view = DummyView.new 44 | view.breadcrumb("posts", "/posts", match: :inclusive) 45 | view.set_path("/posts?foo=bar") 46 | 47 | expect(view.breadcrumb_trail.map(&:to_a)).to eq([["posts", "/posts", true]]) 48 | end 49 | 50 | it "matches current path with :inclusive when nested" do 51 | view = DummyView.new 52 | view.breadcrumb("posts", "/posts", match: :inclusive) 53 | view.set_path("/posts/1/comment") 54 | 55 | expect(view.breadcrumb_trail.map(&:to_a)).to eq([["posts", "/posts", true]]) 56 | end 57 | 58 | it "doesn't match with :inclusive when unrelated path" do 59 | view = DummyView.new 60 | view.breadcrumb("posts", "/posts", match: :inclusive) 61 | view.set_path("/post/1") 62 | 63 | expect(view.breadcrumb_trail.map(&:to_a)).to eq([["posts", "/posts", false]]) 64 | end 65 | 66 | it "matches current path with :inclusive when extra trailing slash" do 67 | view = DummyView.new 68 | view.breadcrumb("posts", "/posts/", match: :inclusive) 69 | view.set_path("/posts") 70 | 71 | expect(view.breadcrumb_trail.map(&:to_a)).to eq([["posts", "/posts/", true]]) 72 | end 73 | 74 | it "matches current path with :inclusive when absolute path" do 75 | view = DummyView.new 76 | view.breadcrumb("posts", "http://www.example.com/posts/", match: :inclusive) 77 | view.set_path("/posts") 78 | 79 | expect(view.breadcrumb_trail.map(&:to_a)).to eq([ 80 | ["posts", "http://www.example.com/posts/", true] 81 | ]) 82 | end 83 | 84 | it "matches current path with :exact when trailing slash" do 85 | view = DummyView.new 86 | view.breadcrumb("posts", "/posts/", match: :exact) 87 | view.set_path("/posts") 88 | 89 | expect(view.breadcrumb_trail.map(&:to_a)).to eq([["posts", "/posts/", true]]) 90 | end 91 | 92 | it "fails to match current path with :exact when nested" do 93 | view = DummyView.new 94 | view.breadcrumb("posts", "/posts", match: :exact) 95 | view.set_path("/posts/1/comment") 96 | 97 | expect(view.breadcrumb_trail.map(&:to_a)).to eq([["posts", "/posts", false]]) 98 | end 99 | 100 | it "fails to match current path with :exact when query params" do 101 | view = DummyView.new 102 | view.breadcrumb("posts", "/posts", match: :exact) 103 | view.set_path("/posts?foo=bar") 104 | 105 | expect(view.breadcrumb_trail.map(&:to_a)).to eq([["posts", "/posts", false]]) 106 | end 107 | 108 | it "matches current path with :exclusive option when query params" do 109 | view = DummyView.new 110 | view.breadcrumb("posts", "/posts", match: :exclusive) 111 | view.set_path("/posts?foo=bar") 112 | 113 | expect(view.breadcrumb_trail.map(&:to_a)).to eq([["posts", "/posts", true]]) 114 | end 115 | 116 | it "fails to match current path with :exclusive option when nested" do 117 | view = DummyView.new 118 | view.breadcrumb("posts", "/posts", match: :exclusive) 119 | view.set_path("/posts/1/comment") 120 | 121 | expect(view.breadcrumb_trail.map(&:to_a)).to eq([["posts", "/posts", false]]) 122 | end 123 | 124 | it "matches current path with regex option when query params" do 125 | view = DummyView.new 126 | view.breadcrumb("posts", "/posts", match: %r{/po}) 127 | view.set_path("/posts?foo=bar") 128 | 129 | expect(view.breadcrumb_trail.map(&:to_a)).to eq([["posts", "/posts", true]]) 130 | end 131 | 132 | it "matches current path with query params option" do 133 | view = DummyView.new 134 | view.breadcrumb("posts", "/posts", match: {foo: :bar}) 135 | view.set_path("/posts?foo=bar&baz=boo") 136 | 137 | expect(view.breadcrumb_trail.map(&:to_a)).to eq([["posts", "/posts", true]]) 138 | end 139 | 140 | it "fails to match current path with query params option" do 141 | view = DummyView.new 142 | view.breadcrumb("posts", "/posts", match: {foo: :bar}) 143 | view.set_path("/posts?foo=2&baz=boo") 144 | 145 | expect(view.breadcrumb_trail.map(&:to_a)).to eq([["posts", "/posts", false]]) 146 | end 147 | 148 | it "overrides breadcrumb :inclusive match option with :exclusive" do 149 | view = DummyView.new 150 | view.breadcrumb("posts", "/posts", match: :inclusive) 151 | view.set_path("/posts/1/comment") 152 | 153 | yielded = [] 154 | block = ->(crumb) { yielded << crumb.to_a } 155 | view.breadcrumb_trail(match: :exclusive, &block) 156 | 157 | expect(yielded).to eq([["posts", "/posts", false]]) 158 | end 159 | 160 | it "overrides default :inclusive match option with :exact" do 161 | view = DummyView.new 162 | view.breadcrumb("posts", :posts_path) 163 | view.set_path("/posts/1/comment") 164 | 165 | yielded = [] 166 | block = ->(crumb) { yielded << crumb.to_a } 167 | view.breadcrumb_trail(match: :exact, &block) 168 | 169 | expect(yielded).to eq([["posts", "/posts", false]]) 170 | end 171 | 172 | it "fails to recognize the match option" do 173 | view = DummyView.new 174 | view.breadcrumb("posts", "http://www.example.com/posts/", match: :boom) 175 | view.set_path("/posts") 176 | block = -> (*args) { } 177 | expect { 178 | view.breadcrumb_trail(&block) 179 | }.to raise_error(ArgumentError, "Unknown `:boom` match option!") 180 | end 181 | 182 | it "forces current path" do 183 | view = DummyView.new 184 | view.breadcrumb("home", :home_path) 185 | view.breadcrumb("posts", :posts_path, match: :force) 186 | view.set_path("/") 187 | 188 | yielded = [] 189 | block = -> (crumb) { yielded << crumb.to_a } 190 | view.breadcrumb_trail(&block) 191 | 192 | expect(yielded).to eq([ 193 | ["home", "/", true], 194 | ["posts", "/posts", true] 195 | ]) 196 | end 197 | 198 | it "returns enumerator without block" do 199 | view = DummyView.new 200 | view.breadcrumb("home", :home_path) 201 | view.breadcrumb("posts", :posts_path) 202 | view.set_path("/posts") 203 | 204 | result = view.breadcrumb_trail 205 | 206 | expect(result).to be_a(Enumerable) 207 | expect(result.take(2).map(&:to_a)).to eq([ 208 | ["home", "/", false], 209 | ["posts", "/posts", true] 210 | ]) 211 | end 212 | 213 | it "validates passed options" do 214 | view = DummyView.new 215 | block = -> (name, url, styles) { } 216 | expect { 217 | view.breadcrumb_trail(unknown: true, &block) 218 | }.to raise_error(Loaf::InvalidOptions) 219 | end 220 | 221 | it "permits arbitrary length crumb names" do 222 | view = DummyView.new 223 | view.breadcrumb("", :home_path) 224 | view.set_path("/posts") 225 | 226 | yielded = [] 227 | block = -> (crumb) { yielded << crumb.to_a } 228 | view.breadcrumb_trail(&block) 229 | 230 | expect(yielded).to eq([ 231 | ["", "/", false] 232 | ]) 233 | end 234 | 235 | it "uses global configuration for crumb formatting" do 236 | view = DummyView.new 237 | view.breadcrumb("home-sweet-home", :home_path) 238 | view.breadcrumb("posts-for-everybody", :posts_path) 239 | view.set_path("/posts") 240 | 241 | yielded = [] 242 | block = -> (crumb) { yielded << crumb.to_a } 243 | view.breadcrumb_trail(&block) 244 | 245 | expect(yielded).to eq([ 246 | ["home-sweet-home", "/", false], 247 | ["posts-for-everybody", "/posts", true] 248 | ]) 249 | end 250 | 251 | it "overwrites global configuration" do 252 | view = DummyView.new 253 | view.breadcrumb("home-sweet-home", :home_path) 254 | view.breadcrumb("posts-for-everybody", :posts_path) 255 | view.set_path("/posts/1") 256 | 257 | yielded = [] 258 | block = -> (crumb) { yielded << crumb.to_a } 259 | view.breadcrumb_trail(match: :exact, &block) 260 | 261 | expect(yielded).to eq([ 262 | ["home-sweet-home", "/", false], 263 | ["posts-for-everybody", "/posts", false] 264 | ]) 265 | end 266 | 267 | it "allows to enumerate all breadcrumb properties individually" do 268 | view = DummyView.new 269 | links = { 270 | "Home" => :home_path, 271 | "Posts" => :posts_path, 272 | "Edit post" => "/posts/1" 273 | } 274 | links.each do |name, url| 275 | view.breadcrumb(name, url) 276 | end 277 | view.set_path("/posts/1") 278 | 279 | expect(view.breadcrumb_trail.map(&:name)).to eq(links.keys) 280 | expect(view.breadcrumb_trail.map(&:url)).to eq(%w[/ /posts /posts/1]) 281 | expect(view.breadcrumb_trail(match: :exact).map(&:current?)).to eq([false, false, true]) 282 | end 283 | end 284 | -------------------------------------------------------------------------------- /spec/unit/view_extensions/has_breadcrumbs_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | RSpec.describe Loaf::ViewExtensions, "#breadcrumbs?" do 4 | it "checks for breadcrumbs existance" do 5 | instance = DummyView.new 6 | expect(instance.breadcrumbs?).to eq(false) 7 | instance.breadcrumb "Home", :home_path 8 | expect(instance.breadcrumbs?).to eq(true) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /tasks/console.rake: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | desc 'Load gem inside irb console' 4 | task :console do 5 | require 'irb' 6 | require 'irb/completion' 7 | require File.join(__FILE__, '../../lib/loaf') 8 | ARGV.clear 9 | IRB.start 10 | end 11 | -------------------------------------------------------------------------------- /tasks/coverage.rake: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | desc 'Measure code coverage' 4 | task :coverage do 5 | begin 6 | original, ENV['COVERAGE'] = ENV['COVERAGE'], 'true' 7 | Rake::Task['spec'].invoke 8 | ensure 9 | ENV['COVERAGE'] = original 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /tasks/spec.rake: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | begin 4 | require 'rspec/core/rake_task' 5 | 6 | desc 'Run all specs' 7 | RSpec::Core::RakeTask.new(:spec) do |task| 8 | task.pattern = 'spec/{unit,integration}{,/*/**}/*_spec.rb' 9 | end 10 | 11 | namespace :spec do 12 | desc 'Run unit specs' 13 | RSpec::Core::RakeTask.new(:unit) do |task| 14 | task.pattern = 'spec/unit{,/*/**}/*_spec.rb' 15 | end 16 | 17 | desc 'Run integration specs' 18 | RSpec::Core::RakeTask.new(:integration) do |task| 19 | task.pattern = 'spec/integration{,/*/**}/*_spec.rb' 20 | end 21 | end 22 | 23 | rescue LoadError 24 | %w[spec spec:unit spec:integration].each do |name| 25 | task name do 26 | $stderr.puts "In order to run #{name}, do `gem install rspec`" 27 | end 28 | end 29 | end 30 | --------------------------------------------------------------------------------