[DRE-commits] [nanoc] 01/21: New upstream version 4.4.6
Cédric Boutillier
boutil at moszumanska.debian.org
Thu Jan 5 14:04:55 UTC 2017
This is an automated email from the git hooks/post-receive script.
boutil pushed a commit to branch master
in repository nanoc.
commit 8f88dff73e538b4c3190deea91443073e51bf03b
Author: Cédric Boutillier <boutil at debian.org>
Date: Wed Dec 28 22:41:20 2016 +0100
New upstream version 4.4.6
---
Appraisals | 11 +
ChangeLog | 3 -
Gemfile | 18 +-
Gemfile.lock | 168 ++++---
NEWS.md | 123 ++++-
README.md | 2 +-
Rakefile | 21 +-
doc/yardoc_handlers/identifier.rb | 30 --
.../default/layout/html/footer.erb | 19 -
lib/nanoc.rb | 4 +
lib/nanoc/base.rb | 23 +-
lib/nanoc/base/compilation/compiler.rb | 300 -------------
lib/nanoc/base/compilation/dependency_tracker.rb | 43 --
lib/nanoc/base/compilation/outdatedness_checker.rb | 210 ---------
lib/nanoc/base/contracts_support.rb | 60 ++-
lib/nanoc/base/core_ext/array.rb | 2 -
lib/nanoc/base/core_ext/hash.rb | 2 -
lib/nanoc/base/entities.rb | 12 +-
lib/nanoc/base/entities/configuration.rb | 32 +-
lib/nanoc/base/{ => entities}/context.rb | 12 +-
lib/nanoc/base/entities/dependency.rb | 28 ++
lib/nanoc/base/{ => entities}/directed_graph.rb | 30 +-
lib/nanoc/base/entities/document.rb | 27 +-
lib/nanoc/base/entities/identifiable_collection.rb | 3 +-
lib/nanoc/base/entities/identifier.rb | 14 +-
lib/nanoc/base/entities/item_rep.rb | 61 +--
.../outdatedness_reasons.rb | 29 +-
lib/nanoc/base/entities/outdatedness_status.rb | 23 +
...{rule_memory_action.rb => processing_action.rb} | 6 +-
lib/nanoc/base/entities/processing_actions.rb | 3 +
.../filter.rb | 4 +-
.../layout.rb | 4 +-
.../base/entities/processing_actions/snapshot.rb | 28 ++
lib/nanoc/base/entities/props.rb | 76 ++++
lib/nanoc/base/entities/rule_memory.rb | 43 +-
lib/nanoc/base/entities/rule_memory_actions.rb | 3 -
.../base/entities/rule_memory_actions/snapshot.rb | 26 --
lib/nanoc/base/entities/site.rb | 22 +-
lib/nanoc/base/entities/snapshot_def.rb | 10 +-
lib/nanoc/base/errors.rb | 24 +
lib/nanoc/base/feature.rb | 79 +++-
lib/nanoc/base/memoization.rb | 2 -
lib/nanoc/base/plugin_registry.rb | 2 +-
lib/nanoc/base/repos.rb | 1 +
lib/nanoc/base/repos/checksum_store.rb | 57 ++-
lib/nanoc/base/repos/compiled_content_cache.rb | 41 +-
lib/nanoc/base/repos/config_loader.rb | 14 +-
lib/nanoc/base/repos/data_source.rb | 26 +-
lib/nanoc/base/repos/dependency_store.rb | 81 ++--
.../base/{compilation => repos}/item_rep_repo.rb | 0
lib/nanoc/base/repos/rule_memory_store.rb | 4 +-
lib/nanoc/base/repos/site_loader.rb | 2 +-
lib/nanoc/base/repos/store.rb | 41 +-
lib/nanoc/base/services.rb | 9 +
lib/nanoc/base/services/action_provider.rb | 4 +
lib/nanoc/base/{ => services}/checksummer.rb | 68 ++-
lib/nanoc/base/services/compiler.rb | 377 ++++++++++++++++
lib/nanoc/base/services/compiler_loader.rb | 20 +-
lib/nanoc/base/services/dependency_tracker.rb | 55 +++
lib/nanoc/base/services/executor.rb | 77 +---
lib/nanoc/base/{compilation => services}/filter.rb | 11 +-
lib/nanoc/base/services/item_rep_builder.rb | 4 +
lib/nanoc/base/services/item_rep_router.rb | 23 +-
lib/nanoc/base/services/item_rep_selector.rb | 19 +-
lib/nanoc/base/services/item_rep_writer.rb | 7 +-
lib/nanoc/base/services/outdatedness_checker.rb | 181 ++++++++
lib/nanoc/base/services/outdatedness_rule.rb | 21 +
lib/nanoc/base/services/outdatedness_rules.rb | 121 +++++
lib/nanoc/base/services/pruner.rb | 110 +++++
lib/nanoc/base/views.rb | 2 +
lib/nanoc/base/views/item_rep_collection_view.rb | 15 +-
lib/nanoc/base/views/item_rep_view.rb | 6 +-
lib/nanoc/base/views/mixins/document_view_mixin.rb | 9 +-
.../base/views/mixins/with_reps_view_mixin.rb | 2 +-
.../views/post_compile_item_rep_collection_view.rb | 8 +
lib/nanoc/base/views/post_compile_item_rep_view.rb | 18 +
lib/nanoc/base/views/post_compile_item_view.rb | 4 +
lib/nanoc/base/views/view_context.rb | 6 +-
lib/nanoc/checking.rb | 11 +
lib/nanoc/{extra => }/checking/check.rb | 8 +-
lib/nanoc/checking/checks.rb | 20 +
lib/nanoc/{extra => }/checking/checks/css.rb | 4 +-
.../{extra => }/checking/checks/external_links.rb | 48 +-
lib/nanoc/{extra => }/checking/checks/html.rb | 4 +-
.../{extra => }/checking/checks/internal_links.rb | 4 +-
.../{extra => }/checking/checks/mixed_content.rb | 4 +-
lib/nanoc/{extra => }/checking/checks/stale.rb | 7 +-
.../{extra => }/checking/checks/w3c_validator.rb | 4 +-
lib/nanoc/{extra => }/checking/dsl.rb | 7 +-
lib/nanoc/{extra => }/checking/issue.rb | 2 +-
lib/nanoc/{extra => }/checking/runner.rb | 8 +-
lib/nanoc/cli.rb | 4 -
lib/nanoc/cli/cleaning_stream.rb | 9 +-
lib/nanoc/cli/command_runner.rb | 13 +-
lib/nanoc/cli/commands/check.rb | 2 +-
lib/nanoc/cli/commands/compile.rb | 146 +++---
lib/nanoc/cli/commands/create-site.rb | 3 +-
lib/nanoc/cli/commands/deploy.rb | 8 +-
lib/nanoc/cli/commands/nanoc.rb | 4 +
lib/nanoc/cli/commands/prune.rb | 4 +-
lib/nanoc/cli/commands/shell.rb | 25 +-
lib/nanoc/cli/commands/show-data.rb | 29 +-
lib/nanoc/cli/commands/show-plugins.rb | 8 +-
lib/nanoc/cli/commands/show-rules.rb | 5 +-
lib/nanoc/cli/commands/view.rb | 4 +-
lib/nanoc/cli/error_handler.rb | 44 +-
lib/nanoc/data_sources/filesystem.rb | 61 ++-
lib/nanoc/data_sources/filesystem/errors.rb | 55 +++
.../filesystem/tools.rb} | 35 +-
lib/nanoc/deploying.rb | 8 +
lib/nanoc/{extra => deploying}/deployer.rb | 4 +-
lib/nanoc/deploying/deployers.rb | 10 +
lib/nanoc/{extra => deploying}/deployers/fog.rb | 6 +-
lib/nanoc/{extra => deploying}/deployers/rsync.rb | 6 +-
lib/nanoc/extra.rb | 18 +-
lib/nanoc/extra/checking.rb | 11 -
lib/nanoc/extra/checking/checks.rb | 20 -
lib/nanoc/extra/core_ext/time.rb | 2 +-
lib/nanoc/extra/deployers.rb | 10 -
lib/nanoc/extra/parallel_collection.rb | 57 +++
lib/nanoc/extra/pruner.rb | 87 ----
lib/nanoc/filters/asciidoc.rb | 2 -
lib/nanoc/filters/coffeescript.rb | 2 -
lib/nanoc/filters/colorize_syntax.rb | 21 +-
lib/nanoc/filters/handlebars.rb | 2 -
lib/nanoc/filters/kramdown.rb | 16 +-
lib/nanoc/filters/less.rb | 68 ++-
lib/nanoc/filters/mustache.rb | 4 +-
lib/nanoc/filters/rdoc.rb | 5 -
lib/nanoc/filters/redcarpet.rb | 4 -
lib/nanoc/filters/relativize_paths.rb | 2 +-
lib/nanoc/filters/slim.rb | 2 -
lib/nanoc/filters/typogruby.rb | 2 -
lib/nanoc/filters/xsl.rb | 2 -
lib/nanoc/filters/yui_compressor.rb | 2 -
lib/nanoc/helpers/blogging.rb | 8 +-
lib/nanoc/helpers/breadcrumbs.rb | 36 +-
lib/nanoc/helpers/capturing.rb | 46 +-
lib/nanoc/helpers/link_to.rb | 10 +-
lib/nanoc/helpers/rendering.rb | 38 +-
lib/nanoc/rule_dsl/action_provider.rb | 4 +-
lib/nanoc/rule_dsl/compiler_dsl.rb | 12 +-
lib/nanoc/rule_dsl/recording_executor.rb | 44 +-
lib/nanoc/rule_dsl/rule.rb | 2 -
lib/nanoc/rule_dsl/rule_context.rb | 6 +-
lib/nanoc/rule_dsl/rule_memory_calculator.rb | 66 ++-
lib/nanoc/spec.rb | 48 +-
lib/nanoc/version.rb | 2 +-
nanoc.gemspec | 5 +-
spec/contributors_spec.rb | 18 +
spec/nanoc/base/checksummer_spec.rb | 381 ++++++++++++++++
spec/nanoc/base/compiler_spec.rb | 181 ++++++++
spec/nanoc/base/entities/configuration_spec.rb | 49 ++
spec/nanoc/base/entities/content_spec.rb | 193 ++++++++
spec/nanoc/base/entities/document_spec.rb | 206 +++++++++
spec/nanoc/base/entities/identifier_spec.rb | 460 +++++++++++++++++++
spec/nanoc/base/entities/item_rep_spec.rb | 226 ++++++++++
spec/nanoc/base/entities/item_spec.rb | 3 +
spec/nanoc/base/entities/layout_spec.rb | 3 +
spec/nanoc/base/entities/lazy_value_spec.rb | 106 +++++
.../base/entities/outdatedness_status_spec.rb | 113 +++++
spec/nanoc/base/entities/pattern_spec.rb | 125 ++++++
spec/nanoc/base/entities/processing_action_spec.rb | 9 +
.../entities/processing_actions/filter_spec.rb | 18 +
.../entities/processing_actions/layout_spec.rb | 18 +
.../entities/processing_actions/snapshot_spec.rb | 32 ++
spec/nanoc/base/entities/props_spec.rb | 195 ++++++++
spec/nanoc/base/entities/rule_memory_spec.rb | 131 ++++++
spec/nanoc/base/entities/site_spec.rb | 73 +++
spec/nanoc/base/feature_spec.rb | 107 +++++
spec/nanoc/base/filter_spec.rb | 99 +++++
spec/nanoc/base/item_rep_writer_spec.rb | 131 ++++++
spec/nanoc/base/plugin_registry_spec.rb | 29 ++
spec/nanoc/base/repos/checksum_store_spec.rb | 133 ++++++
.../base/repos/compiled_content_cache_spec.rb | 55 +++
spec/nanoc/base/repos/config_loader_spec.rb | 243 ++++++++++
spec/nanoc/base/repos/dependency_store_spec.rb | 195 ++++++++
spec/nanoc/base/repos/site_loader_spec.rb | 214 +++++++++
.../nanoc/base/services/dependency_tracker_spec.rb | 238 ++++++++++
spec/nanoc/base/services/executor_spec.rb | 495 +++++++++++++++++++++
spec/nanoc/base/services/item_rep_router_spec.rb | 134 ++++++
spec/nanoc/base/services/item_rep_selector_spec.rb | 169 +++++++
.../base/services/outdatedness_checker_spec.rb | 370 +++++++++++++++
.../nanoc/base/services/outdatedness_rules_spec.rb | 432 ++++++++++++++++++
spec/nanoc/base/services/pruner_spec.rb | 105 +++++
.../base/services/temp_filename_factory_spec.rb | 87 ++++
spec/nanoc/base/views/config_view_spec.rb | 96 ++++
spec/nanoc/base/views/document_view_spec.rb | 332 ++++++++++++++
.../views/identifiable_collection_view_spec.rb | 190 ++++++++
.../views/item_collection_with_reps_view_spec.rb | 18 +
.../item_collection_without_reps_view_spec.rb | 18 +
.../base/views/item_rep_collection_view_spec.rb | 143 ++++++
spec/nanoc/base/views/item_rep_view_spec.rb | 265 +++++++++++
spec/nanoc/base/views/item_view_spec.rb | 341 ++++++++++++++
.../base/views/layout_collection_view_spec.rb | 18 +
spec/nanoc/base/views/layout_view_spec.rb | 14 +
spec/nanoc/base/views/mutable_config_view_spec.rb | 16 +
.../nanoc/base/views/mutable_document_view_spec.rb | 92 ++++
.../mutable_identifiable_collection_view_spec.rb | 36 ++
.../views/mutable_item_collection_view_spec.rb | 49 ++
spec/nanoc/base/views/mutable_item_view_spec.rb | 22 +
.../views/mutable_layout_collection_view_spec.rb | 49 ++
spec/nanoc/base/views/mutable_layout_view_spec.rb | 13 +
.../post_compile_item_rep_collection_view_spec.rb | 4 +
.../base/views/post_compile_item_rep_view_spec.rb | 137 ++++++
.../base/views/post_compile_item_view_spec.rb | 56 +++
.../commands/compile/file_action_printer_spec.rb | 76 ++++
.../cli/commands/compile/timing_recorder_spec.rb | 66 +++
spec/nanoc/cli/commands/compile_spec.rb | 64 +++
spec/nanoc/cli/commands/deploy_spec.rb | 327 ++++++++++++++
spec/nanoc/cli/commands/shell_spec.rb | 54 +++
spec/nanoc/cli/commands/show_data_spec.rb | 126 ++++++
spec/nanoc/cli/commands/show_rules_spec.rb | 112 +++++
spec/nanoc/cli/commands/view_spec.rb | 58 +++
spec/nanoc/data_sources/filesystem_spec.rb | 56 +++
spec/nanoc/deploying/fog_spec.rb | 193 ++++++++
spec/nanoc/extra/parallel_collection_spec.rb | 108 +++++
spec/nanoc/filters/colorize_syntax/rouge_spec.rb | 195 ++++++++
spec/nanoc/filters/less_spec.rb | 120 +++++
spec/nanoc/helpers/blogging_spec.rb | 216 +++++++++
spec/nanoc/helpers/breadcrumbs_spec.rb | 133 ++++++
spec/nanoc/helpers/capturing_spec.rb | 181 ++++++++
spec/nanoc/helpers/child_parent_spec.rb | 105 +++++
spec/nanoc/helpers/filtering_spec.rb | 72 +++
spec/nanoc/helpers/html_escape_spec.rb | 35 ++
spec/nanoc/helpers/link_to_spec.rb | 275 ++++++++++++
spec/nanoc/helpers/rendering_spec.rb | 141 ++++++
spec/nanoc/helpers/tagging_spec.rb | 104 +++++
spec/nanoc/helpers/text_spec.rb | 58 +++
.../integration/outdatedness_integration_spec.rb | 208 +++++++++
spec/nanoc/regressions/gh_1015_spec.rb | 17 +
spec/nanoc/regressions/gh_1031_spec.rb | 54 +++
spec/nanoc/regressions/gh_1035_spec.rb | 33 ++
spec/nanoc/regressions/gh_1040_spec.rb | 22 +
spec/nanoc/regressions/gh_761_spec.rb | 23 +
spec/nanoc/regressions/gh_767_spec.rb | 19 +
spec/nanoc/regressions/gh_769_spec.rb | 30 ++
spec/nanoc/regressions/gh_776_spec.rb | 43 ++
spec/nanoc/regressions/gh_787_spec.rb | 19 +
spec/nanoc/regressions/gh_795_spec.rb | 19 +
spec/nanoc/regressions/gh_804_spec.rb | 26 ++
spec/nanoc/regressions/gh_807_spec.rb | 17 +
spec/nanoc/regressions/gh_809_spec.rb | 17 +
spec/nanoc/regressions/gh_813_spec.rb | 22 +
spec/nanoc/regressions/gh_815_spec.rb | 18 +
spec/nanoc/regressions/gh_828_spec.rb | 23 +
spec/nanoc/regressions/gh_833_spec.rb | 14 +
spec/nanoc/regressions/gh_841_spec.rb | 15 +
spec/nanoc/regressions/gh_867_spec.rb | 15 +
spec/nanoc/regressions/gh_882_spec.rb | 29 ++
spec/nanoc/regressions/gh_885_spec.rb | 30 ++
spec/nanoc/regressions/gh_891_spec.rb | 26 ++
spec/nanoc/regressions/gh_913_spec.rb | 24 +
spec/nanoc/regressions/gh_928_spec.rb | 5 +
spec/nanoc/regressions/gh_937_spec.rb | 25 ++
spec/nanoc/regressions/gh_942_spec.rb | 21 +
spec/nanoc/regressions/gh_947_spec.rb | 21 +
spec/nanoc/regressions/gh_948_spec.rb | 16 +
spec/nanoc/regressions/gh_951_spec.rb | 19 +
spec/nanoc/regressions/gh_954_spec.rb | 33 ++
spec/nanoc/regressions/gh_970a_spec.rb | 17 +
spec/nanoc/regressions/gh_970b_spec.rb | 50 +++
spec/nanoc/regressions/gh_974_spec.rb | 17 +
spec/nanoc/regressions/gh_981_spec.rb | 21 +
spec/nanoc/rule_dsl/recording_executor_spec.rb | 142 ++++++
spec/nanoc/rule_dsl/rule_context_spec.rb | 177 ++++++++
spec/nanoc/rule_dsl/rule_memory_calculator_spec.rb | 233 ++++++++++
spec/nanoc/rule_dsl/rules_collection_spec.rb | 299 +++++++++++++
spec/regression_filenames_spec.rb | 16 +
spec/spec_helper.rb | 173 +++++++
tasks/doc.rake | 16 -
tasks/rubocop.rake | 6 -
tasks/test.rake | 25 --
test/base/core_ext/array_spec.rb | 2 +
test/base/core_ext/hash_spec.rb | 2 +
test/base/core_ext/string_spec.rb | 2 +
test/base/temp_filename_factory_spec.rb | 66 ---
test/base/test_checksum_store.rb | 28 --
test/base/test_code_snippet.rb | 2 +
test/base/test_compiler.rb | 23 +-
test/base/test_context.rb | 14 +-
test/base/test_data_source.rb | 24 +
test/base/test_dependency_tracker.rb | 109 ++---
test/base/test_directed_graph.rb | 51 ++-
test/base/test_filter.rb | 12 +-
test/base/test_item.rb | 2 +
test/base/test_item_array.rb | 4 +-
test/base/test_item_rep.rb | 153 -------
test/base/test_layout.rb | 2 +
test/base/test_memoization.rb | 2 +
test/base/test_notification_center.rb | 2 +
test/base/test_outdatedness_checker.rb | 37 +-
test/base/test_plugin.rb | 2 +
test/base/test_site.rb | 2 +
test/base/test_store.rb | 24 +
test/{extra => }/checking/checks/test_css.rb | 10 +-
.../checking/checks/test_external_links.rb | 22 +-
test/{extra => }/checking/checks/test_html.rb | 14 +-
.../checking/checks/test_internal_links.rb | 26 +-
.../checking/checks/test_mixed_content.rb | 20 +-
test/{extra => }/checking/checks/test_stale.rb | 8 +-
test/{extra => }/checking/test_check.rb | 10 +-
test/{extra => }/checking/test_dsl.rb | 18 +-
test/{extra => }/checking/test_runner.rb | 10 +-
test/cli/commands/test_check.rb | 2 +
test/cli/commands/test_compile.rb | 2 +
test/cli/commands/test_create_site.rb | 4 +-
test/cli/commands/test_help.rb | 2 +
test/cli/commands/test_info.rb | 2 +
test/cli/commands/test_prune.rb | 2 +
test/cli/test_cleaning_stream.rb | 17 +-
test/cli/test_cli.rb | 20 +-
test/cli/test_error_handler.rb | 39 +-
test/cli/test_logger.rb | 5 +-
test/data_sources/test_filesystem.rb | 42 +-
.../test_filesystem_tools.rb | 28 +-
test/{extra/deployers => deploying}/test_fog.rb | 18 +-
test/{extra/deployers => deploying}/test_rsync.rb | 20 +-
test/extra/core_ext/test_pathname.rb | 2 +
test/extra/core_ext/test_time.rb | 8 +-
test/extra/test_link_collector.rb | 2 +
test/extra/test_piper.rb | 2 +
.../test_coderay.rb} | 180 +-------
test/filters/colorize_syntax/test_common.rb | 83 ++++
test/filters/colorize_syntax/test_pygmentize.rb | 37 ++
test/filters/colorize_syntax/test_pygments.rb | 19 +
test/filters/colorize_syntax/test_simon.rb | 22 +
test/filters/test_asciidoc.rb | 2 +
test/filters/test_bluecloth.rb | 2 +
test/filters/test_coffeescript.rb | 4 +
test/filters/test_erb.rb | 12 +-
test/filters/test_erubis.rb | 10 +-
test/filters/test_haml.rb | 12 +-
test/filters/test_handlebars.rb | 6 +
test/filters/test_kramdown.rb | 32 +-
test/filters/test_less.rb | 126 ------
test/filters/test_markaby.rb | 2 +
test/filters/test_maruku.rb | 2 +
test/filters/test_mustache.rb | 6 +-
test/filters/test_pandoc.rb | 2 +
test/filters/test_rainpress.rb | 2 +
test/filters/test_rdiscount.rb | 2 +
test/filters/test_rdoc.rb | 2 +
test/filters/test_redcarpet.rb | 2 +
test/filters/test_redcloth.rb | 2 +
test/filters/test_relativize_paths.rb | 28 ++
test/filters/test_rubypants.rb | 2 +
test/filters/test_sass.rb | 6 +-
test/filters/test_slim.rb | 6 +-
test/filters/test_typogruby.rb | 2 +
test/filters/test_uglify_js.rb | 6 +
test/filters/test_xsl.rb | 46 +-
test/filters/test_yui_compressor.rb | 8 +-
test/fixtures/vcr_cassettes/css_run_error.yml | 23 +-
test/fixtures/vcr_cassettes/css_run_ok.yml | 25 +-
.../fixtures/vcr_cassettes/css_run_parse_error.yml | 25 +-
test/fixtures/vcr_cassettes/html_run_error.yml | 52 ++-
test/fixtures/vcr_cassettes/html_run_ok.yml | 116 ++++-
test/helper.rb | 6 +
test/helpers/test_blogging.rb | 64 +++
test/helpers/test_capturing.rb | 9 +-
test/helpers/test_link_to.rb | 2 +
test/helpers/test_xml_sitemap.rb | 10 +-
test/rule_dsl/test_action_provider.rb | 2 +
test/rule_dsl/test_compiler_dsl.rb | 32 +-
test/rule_dsl/test_rule.rb | 2 +
test/rule_dsl/test_rules_collection.rb | 2 +
test/test_gem.rb | 2 +
368 files changed, 16341 insertions(+), 2655 deletions(-)
diff --git a/Appraisals b/Appraisals
new file mode 100644
index 0000000..94a8ece
--- /dev/null
+++ b/Appraisals
@@ -0,0 +1,11 @@
+appraise 'rouge-1' do
+ group :plugins do
+ gem 'rouge', '~> 1.0'
+ end
+end
+
+appraise 'rouge-2' do
+ group :plugins do
+ gem 'rouge', '~> 2.0'
+ end
+end
\ No newline at end of file
diff --git a/ChangeLog b/ChangeLog
deleted file mode 100644
index 706bed1..0000000
--- a/ChangeLog
+++ /dev/null
@@ -1,3 +0,0 @@
-For a list of all changes, please see the changelog on the project repository
-instead (https://github.com/nanoc/nanoc). For release notes, please see the
-NEWS file.
diff --git a/Gemfile b/Gemfile
index 3a93341..2c9a378 100644
--- a/Gemfile
+++ b/Gemfile
@@ -2,25 +2,29 @@ source 'https://rubygems.org'
gemspec
-gem 'json', '~> 2.0'
-
group :devel do
gem 'contracts', '~> 0.14'
gem 'coveralls', require: false
- gem 'guard-rake'
gem 'fuubar'
+ gem 'guard-rake'
+ gem 'json', '~> 2.0'
+ gem 'm', '~> 1.5'
gem 'minitest', '~> 5.0'
gem 'mocha'
gem 'pry'
+ gem 'rainbow', '~> 2.1'
gem 'rake'
- gem 'rdoc'
+ gem 'rdoc', '~> 5.0'
gem 'rspec'
+ gem 'rspec-its', '~> 1.2'
gem 'rspec-mocks'
- gem 'rubocop'
+ gem 'rubocop', github: 'bbatsov/rubocop'
gem 'simplecov', require: false
+ gem 'timecop'
gem 'vcr'
gem 'webmock'
gem 'yard'
+ gem 'yard-contracts'
end
group :plugins do
@@ -43,7 +47,8 @@ group :plugins do
gem 'mustache', '~> 1.0'
gem 'nokogiri', '~> 1.6'
gem 'pandoc-ruby'
- gem 'pygments.rb', platforms: [:ruby, :mswin]
+ gem 'parallel'
+ gem 'pygments.rb', github: 'tmm1/pygments.rb', platforms: [:ruby, :mswin]
gem 'rack'
gem 'rainpress'
gem 'rdiscount', '~> 2.2', platforms: [:ruby, :mswin]
@@ -53,6 +58,7 @@ group :plugins do
gem 'rubypants'
gem 'sass'
gem 'slim'
+ gem 'therubyracer', github: 'cowboyd/therubyracer'
gem 'typogruby'
gem 'uglifier'
gem 'w3c_validators'
diff --git a/Gemfile.lock b/Gemfile.lock
index 4f55ad7..f6fcbe7 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,7 +1,33 @@
+GIT
+ remote: git://github.com/bbatsov/rubocop.git
+ revision: 186b16e10514eaf304990d614184b980abc0ad2b
+ specs:
+ rubocop (0.46.0)
+ parser (>= 2.3.3.1, < 3.0)
+ powerpack (~> 0.1)
+ rainbow (>= 1.99.1, < 3.0)
+ ruby-progressbar (~> 1.7)
+ unicode-display_width (~> 1.0, >= 1.0.1)
+
+GIT
+ remote: git://github.com/cowboyd/therubyracer.git
+ revision: 2f1127bed45f03b91f3c92469be26399d3c1bcd8
+ specs:
+ therubyracer (0.12.2)
+ libv8 (~> 3.16.14.15)
+ ref
+
+GIT
+ remote: git://github.com/tmm1/pygments.rb.git
+ revision: a988d127d3270d21685d2ac2e586060bf43709f3
+ specs:
+ pygments.rb (1.1.0)
+ multi_json (>= 1.0.0)
+
PATH
remote: .
specs:
- nanoc (4.3.4)
+ nanoc (4.4.6)
cri (~> 2.3)
hamster (~> 3.0)
ref (~> 2.0)
@@ -9,20 +35,25 @@ PATH
GEM
remote: https://rubygems.org/
specs:
- CFPropertyList (2.3.3)
+ CFPropertyList (2.3.4)
RedCloth (4.3.2)
- addressable (2.4.0)
+ addressable (2.5.0)
+ public_suffix (~> 2.0, >= 2.0.2)
adsf (1.2.1)
rack (>= 1.0.0)
+ appraisal (2.1.0)
+ bundler
+ rake
+ thor (>= 0.14.0)
ast (2.3.0)
bluecloth (2.2.0)
builder (3.2.2)
- chunky_png (1.3.7)
+ chunky_png (1.3.8)
coderay (1.1.1)
coffee-script (2.4.1)
coffee-script-source
execjs
- coffee-script-source (1.10.0)
+ coffee-script-source (1.12.2)
colored (1.2)
commonjs (0.2.7)
compass (1.0.3)
@@ -37,22 +68,22 @@ GEM
sass (>= 3.3.0, < 3.5)
compass-import-once (1.0.5)
sass (>= 3.2, < 3.5)
- concurrent-ruby (1.0.2)
+ concurrent-ruby (1.0.4)
contracts (0.14.0)
- coveralls (0.8.15)
+ coveralls (0.8.17)
json (>= 1.8, < 3)
simplecov (~> 0.12.0)
term-ansicolor (~> 1.3)
thor (~> 0.19.1)
- tins (>= 1.6.0, < 2)
+ tins (~> 1.6)
crack (0.4.3)
safe_yaml (~> 1.0.0)
- cri (2.7.0)
+ cri (2.7.1)
colored (~> 1.2)
diff-lcs (1.2.5)
docile (1.1.5)
erubis (2.7.0)
- excon (0.53.0)
+ excon (0.54.0)
execjs (2.7.0)
ffi (1.9.14)
fission (0.5.0)
@@ -94,7 +125,7 @@ GEM
fog-atmos (0.1.0)
fog-core
fog-xml
- fog-aws (0.12.0)
+ fog-aws (1.1.0)
fog-core (~> 1.38)
fog-json (~> 1.0)
fog-xml (~> 0.1)
@@ -126,9 +157,9 @@ GEM
fog-json (1.0.2)
fog-core (~> 1.0)
multi_json (~> 1.10)
- fog-local (0.3.0)
+ fog-local (0.3.1)
fog-core (~> 1.27)
- fog-openstack (0.1.12)
+ fog-openstack (0.1.18)
fog-core (>= 1.40)
fog-json (>= 1.0)
ipaddress (>= 0.8)
@@ -136,10 +167,10 @@ GEM
fog-core (~> 1.27)
fog-json (~> 1.0)
fog-xml (~> 0.1)
- fog-profitbricks (2.0.1)
+ fog-profitbricks (3.0.0)
fog-core (~> 1.42)
fog-json (~> 1.0)
- fog-rackspace (0.1.1)
+ fog-rackspace (0.1.2)
fog-core (>= 1.35)
fog-json (>= 1.0)
fog-xml (>= 0.1)
@@ -173,9 +204,9 @@ GEM
fog-voxel (0.1.0)
fog-core
fog-xml
- fog-vsphere (1.2.0)
+ fog-vsphere (1.5.2)
fog-core
- rbvmomi (~> 1.8.0)
+ rbvmomi (~> 1.9)
fog-xenserver (0.2.3)
fog-core
fog-xml
@@ -205,20 +236,23 @@ GEM
handlebars (0.8.0)
handlebars-source (~> 4.0.5)
therubyracer (~> 0.12.1)
- handlebars-source (4.0.5)
- hashdiff (0.3.0)
+ handlebars-source (4.0.6)
+ hashdiff (0.3.2)
inflecto (0.0.2)
ipaddress (0.8.3)
json (2.0.2)
- kramdown (1.12.0)
+ kramdown (1.13.1)
less (2.6.0)
commonjs (~> 0.2.7)
- libv8 (3.16.14.15)
+ libv8 (3.16.14.17)
listen (3.1.5)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
ruby_dep (~> 1.2)
lumberjack (1.0.10)
+ m (1.5.0)
+ method_source (>= 0.6.7)
+ rake (>= 0.9.2.2)
markaby (0.8.0)
builder
maruku (0.7.2)
@@ -228,47 +262,44 @@ GEM
mime-types-data (~> 3.2015)
mime-types-data (3.2016.0521)
mini_portile2 (2.1.0)
- minitest (5.9.1)
- mocha (1.1.0)
+ minitest (5.10.1)
+ mocha (1.2.1)
metaclass (~> 0.0.1)
multi_json (1.12.1)
mustache (1.0.3)
nenv (0.3.0)
- nokogiri (1.6.8)
+ nokogiri (1.7.0)
mini_portile2 (~> 2.1.0)
- pkg-config (~> 1.1.7)
notiffany (0.1.1)
nenv (~> 0.1)
shellany (~> 0.0)
pandoc-ruby (2.0.1)
- parser (2.3.1.4)
+ parallel (1.10.0)
+ parser (2.3.3.1)
ast (~> 2.2)
- pkg-config (1.1.7)
- posix-spawn (0.3.11)
powerpack (0.1.1)
pry (0.10.4)
coderay (~> 1.1.0)
method_source (~> 0.8.1)
slop (~> 3.4)
- pygments.rb (0.6.3)
- posix-spawn (~> 0.3.6)
- yajl-ruby (~> 1.2.0)
+ public_suffix (2.0.4)
rack (2.0.1)
- rainbow (2.1.0)
+ rainbow (2.2.0)
rainpress (1.0)
- rake (11.3.0)
- rb-fsevent (0.9.7)
+ rake (12.0.0)
+ rb-fsevent (0.9.8)
rb-inotify (0.9.7)
ffi (>= 0.5.0)
- rbvmomi (1.8.2)
- builder
- nokogiri (>= 1.4.1)
- trollop
+ rbvmomi (1.9.4)
+ builder (~> 3.2)
+ json (>= 1.8)
+ nokogiri (~> 1.5)
+ trollop (~> 2.1)
rdiscount (2.2.0.1)
- rdoc (4.2.1)
- redcarpet (3.3.4)
+ rdoc (5.0.0)
+ redcarpet (3.4.0)
ref (2.0.0)
- rouge (2.0.6)
+ rouge (2.0.7)
rspec (3.5.0)
rspec-core (~> 3.5.0)
rspec-expectations (~> 3.5.0)
@@ -278,21 +309,18 @@ GEM
rspec-expectations (3.5.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.5.0)
+ rspec-its (1.2.0)
+ rspec-core (>= 3.0.0)
+ rspec-expectations (>= 3.0.0)
rspec-mocks (3.5.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.5.0)
rspec-support (3.5.0)
- rubocop (0.43.0)
- parser (>= 2.3.1.1, < 3.0)
- powerpack (~> 0.1)
- rainbow (>= 1.99.1, < 3.0)
- ruby-progressbar (~> 1.7)
- unicode-display_width (~> 1.0, >= 1.0.1)
ruby-progressbar (1.8.1)
- ruby_dep (1.4.0)
- rubypants (0.5.1)
+ ruby_dep (1.5.0)
+ rubypants (0.6.0)
safe_yaml (1.0.4)
- sass (3.4.22)
+ sass (3.4.23)
shellany (0.0.1)
simplecov (0.12.0)
docile (~> 1.1.0)
@@ -306,29 +334,29 @@ GEM
temple (0.7.7)
term-ansicolor (1.4.0)
tins (~> 1.0)
- therubyracer (0.12.2)
- libv8 (~> 3.16.14.0)
- ref
- thor (0.19.1)
+ thor (0.19.4)
tilt (2.0.5)
- tins (1.12.0)
+ timecop (0.8.1)
+ tins (1.13.0)
trollop (2.1.2)
typogruby (1.0.18)
rubypants
- uglifier (3.0.2)
+ uglifier (3.0.4)
execjs (>= 0.3.0, < 3)
- unicode-display_width (1.1.1)
+ unicode-display_width (1.1.2)
vcr (3.0.3)
- w3c_validators (1.2)
- json
- nokogiri
- webmock (2.1.0)
+ w3c_validators (1.3.1)
+ json (~> 2.0)
+ nokogiri (~> 1.6)
+ webmock (2.3.1)
addressable (>= 2.3.6)
crack (>= 0.3.2)
hashdiff
xml-simple (1.1.5)
- yajl-ruby (1.2.1)
yard (0.9.5)
+ yard-contracts (0.1.5)
+ contracts (~> 0.7)
+ yard (~> 0.8)
yuicompressor (1.3.3)
PLATFORMS
@@ -337,6 +365,7 @@ PLATFORMS
DEPENDENCIES
RedCloth
adsf
+ appraisal (~> 2.1)
bluecloth
builder
bundler (>= 1.7.10, < 2.0)
@@ -355,6 +384,7 @@ DEPENDENCIES
kramdown
less (~> 2.0)
listen
+ m (~> 1.5)
markaby
maruku
mime-types
@@ -364,29 +394,35 @@ DEPENDENCIES
nanoc!
nokogiri (~> 1.6)
pandoc-ruby
+ parallel
pry
- pygments.rb
+ pygments.rb!
rack
+ rainbow (~> 2.1)
rainpress
rake
rdiscount (~> 2.2)
- rdoc
+ rdoc (~> 5.0)
redcarpet
rouge
rspec
+ rspec-its (~> 1.2)
rspec-mocks
- rubocop
+ rubocop!
rubypants
sass
simplecov
slim
+ therubyracer!
+ timecop
typogruby
uglifier
vcr
w3c_validators
webmock
yard
+ yard-contracts
yuicompressor
BUNDLED WITH
- 1.13.1
+ 1.13.7
diff --git a/NEWS.md b/NEWS.md
index 129b9ad..a608770 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -1,5 +1,120 @@
# Nanoc news
+## 4.4.6 (2016-12-28)
+
+Fixes:
+
+* Fixed issue where `#compiled_content` would not return the correct content (#1040, #1041)
+
+## 4.4.5 (2016-12-24)
+
+Fixes:
+
+* Prevented stale data from making it into the checksum store and thereby blowing up in memory (#1004, #1027)
+* Fixed slow recompile after adding many items to a site (#1028)
+* Fixed wrong capturing helper output when the output field separator (`$,`) is set
+* Fixed issue that could cause items with multiple reps not to be recompiled when needed (#1031, #1032)
+* Fixed error when fetching textual content of item whose `:last` snapshot is binary (#1035, #1036)
+
+## 4.4.4 (2016-12-19)
+
+Enhancements:
+
+* Improved speed of incremental compilations (#1017, #1019, #1024)
+
+## 4.4.3 (2016-12-17)
+
+Fixes:
+
+* Prevented stale data from making it into the compiled content cache and thereby blowing up in memory (#1004, #1013)
+* Fixed “about” and “IRC channel” links in default site
+* Fixed accuracy of `<updated>` in Atom feed (use most recent `updated_at` or `created_at`) (#1007, #1014)
+
+Enhancements:
+
+* Added support for non-legacy identifiers in `#breadcrumbs_trail` (#1010, #1011)
+* Defined checksum for `Nanoc::Int::Context` to make outdatedness checker more precise (#1008, #1012)
+* Made Nanoc raise an error when item reps are routed to a path that does not start with a slash (#1015, #1016)
+
+## 4.4.2 (2016-11-27)
+
+Fixes:
+
+* Fixed “Maximum call stack size exceeded” issue in the `less` filter (#1001)
+* Fixed issue that could cause the `less` filter to not generate all necessary dependencies (#1003)
+
+Enhancements:
+
+* Improved the way that the crash log displays the item rep that is being compiled (#1000)
+
+## 4.4.1 (2016-11-21)
+
+Fixes:
+
+* Fixed an issue where the `xsl` filter would not generate a correct dependency on the layout (#996)
+
+Enhancements:
+
+* Made `view` command use index filenames specified in the `index_filenames` site configuration attribute (#998)
+
+## 4.4.0 (2016-11-19)
+
+Features:
+
+* Added support for Nanoc environments (#859)
+
+## 4.3.8 (2016-11-18)
+
+Enhancements:
+
+* Improved support for Rouge 1.x and 2.x (#880) [Rémi Barraquand]
+* Added `#include` to the `nanoc shell` command (#973)
+* Improved speed of full and incremental compilations (#977, #985)
+
+Fixes:
+
+* Made routing rules and `#write` calls accept an identifier, and not just a string (#976)
+* Removed GC speed-up hacks, which became counterproductive in Ruby 2.2 (#975)
+* Fixed issue which caused items to be always recompiled if `rep`/`item_rep` or `self` are used in those items’ rules (#982)
+
+## 4.3.7 (2016-10-29)
+
+Fixes:
+
+* Fixed issue with `show-data` and `show-rules` commands not showing all data (#970) [Chris Chapman]
+
+Enhancements:
+
+* Improved speed of `compile` command (#968)
+* Improved speed of `prune` command (#969)
+* Made kramdown warnings include affected item rep (#967) [Gregory Pakosz]
+* Made kramdown warnings configurable (#967) [Gregory Pakosz]
+
+## 4.3.6 (2016-10-23)
+
+Fixes:
+
+* Made legacy patterns properly support full identifiers (#957)
+* Fixed timezone issues in `#to_iso8601_date` (#961)
+* Fixed error when accessing item (rep) paths in shell command (#963)
+* Fixed issue that caused `#path` to be nil inside compilation rules (#964)
+* Made `__FILE__` in Checks file be a absolute path (#966)
+
+Enhancements:
+
+* Made the command line write status information to stderr, not stdout (#958)
+
+## 4.3.5 (2016-10-14)
+
+Fixes:
+
+* Handle `form/@action` in `relativize_paths` filter (#950) [Lorin Werthen]
+
+Experimental features:
+
+* `profiler`: adds `--profile` option to the `compile` command to profile compilation (#903)
+* `environments`: adds support for Nanoc environments (#859)
+
## 4.3.4 (2016-10-02)
Fixes:
@@ -635,7 +750,7 @@ Fixes:
* Updated references to old web site and old repository
* Made `require` errors mention Bundler if appropriate
-* Fixed bug which caused pruner not to delete directories in some cases [@reima]
+* Fixed bug which caused pruner not to delete directories in some cases [Matthias Reitinger]
* Made `check` command exit with the proper exit status
* Added support for the `HTML_TOC` Redcarpet renderer
* Made `stale` check honor files excluded by the pruner
@@ -689,7 +804,7 @@ Fixes:
* Made passthrough rules be inserted in the right place [Gregory Pakosz]
* Fixed crashes in the progress indicator when compiling
-* Made auto-pruning honor excluded files [Greg Karékinian]
+* Made auto-pruning honor excluded files [Grégory Karékinian]
* Made lack of which/where not crash watch command
Improvements:
@@ -933,7 +1048,7 @@ Changed:
* The `filesystem` data source is now known as `filesystem_verbose`
* Meta files and content files are now optional
* The `filesystem_compact` and `filesystem_combined` data sources have been merged into a new `filesystem_unified` data source
-* The metadata section in `filesystem_unified` is now optional [Christopher Eppstein]
+* The metadata section in `filesystem_unified` is now optional [Chris Eppstein]
* The `--server` autocompile option is now known as `--handler`
* Assigns in filters are now available as instance variables and methods
* The `#breadcrumbs_trail` function now allows missing parents
@@ -1210,7 +1325,7 @@ Removed:
## 1.2 (2007-06-05)
-* Sites now have an `assets` directory, whose contents are copied to the `output` directory when compiling [Soryu]
+* Sites now have an `assets` directory, whose contents are copied to the `output` directory when compiling [Stanley Rost]
* Added support for non-eRuby layouts (Markaby, Haml, Liquid, …)
* Added more filters (Markaby, Haml, Liquid, RDoc [Dmitry Bilunov])
* Improved error reporting
diff --git a/README.md b/README.md
index 52b9eb0..4440185 100644
--- a/README.md
+++ b/README.md
@@ -19,4 +19,4 @@ Contributions are greatly appreciated! Consult the [Development guidelines](http
Many thanks to everyone who has contributed to Nanoc in one way or another:
-Ale Muñoz, Alexander Mankuta, Arnau Siches, Ben Armston, Bil Bas, Brian Candler, Bruno Dufour, Chris Eppstein, Christian Plessl, Colin Barrett, Colin Seymour, Croath Liu, Damien Pollet, Dan Callahan, Daniel Hofstetter, Daniel Mendler, Daniel Wollschlaeger, David Alexander, David Everitt, Denis Defreyne, Dennis Sutch, Devon Luke Buchanan, Dmitry Bilunov, Eric Sunshine, Erik Hollensbe, Fabian Buch, Felix Hanley, Garen Torikian, Go Maeda, Gregory Pakosz, Grégory Karékinian, Guilherme Garnie [...]
+Ale Muñoz, Alexander Mankuta, Andy Drop, Arnau Siches, Ben Armston, Bil Bas, Brian Candler, Bruno Dufour, Chris Chapman, Chris Eppstein, Christian Plessl, Colin Barrett, Colin Seymour, Croath Liu, Damien Pollet, Dan Callahan, Daniel Hofstetter, Daniel Mendler, Daniel Wollschlaeger, David Alexander, David Everitt, Denis Defreyne, Dennis Sutch, Devon Luke Buchanan, Dmitry Bilunov, Eric Sunshine, Erik Hollensbe, Fabian Buch, Felix Hanley, Garen Torikian, Go Maeda, Grégory Karékinian, Gregor [...]
diff --git a/Rakefile b/Rakefile
index fb3f6e4..7c1a237 100644
--- a/Rakefile
+++ b/Rakefile
@@ -1,3 +1,20 @@
-Rake.add_rakelib 'tasks'
+require 'rubocop/rake_task'
+require 'rspec/core/rake_task'
+require 'rake/testtask'
+require 'coveralls/rake/task'
-task default: [:test, :rubocop]
+RuboCop::RakeTask.new(:rubocop)
+
+Coveralls::RakeTask.new
+
+Rake::TestTask.new(:test_all) do |t|
+ t.test_files = Dir['test/**/*_spec.rb'] + Dir['test/**/test_*.rb']
+ t.libs << 'test'
+end
+
+RSpec::Core::RakeTask.new(:spec)
+
+task test: [:spec, :test_all, :rubocop]
+task test_ci: [:test, :'coveralls:push']
+
+task default: :test
diff --git a/doc/yardoc_handlers/identifier.rb b/doc/yardoc_handlers/identifier.rb
deleted file mode 100644
index 37a6ca3..0000000
--- a/doc/yardoc_handlers/identifier.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-class NanocIdentifierHandler < ::YARD::Handlers::Ruby::AttributeHandler
- # e.g. identifier :foo, :bar
-
- handles method_call(:identifier), method_call(:identifiers)
- namespace_only
-
- def process
- identifiers = statement.parameters(false).map { |param| param.jump(:ident)[0] }
- namespace['nanoc_identifiers'] = identifiers
- end
-end
-
-class NanocRegisterFilterHandler < ::YARD::Handlers::Ruby::AttributeHandler
- # e.g. Nanoc::Filter.register '::Nanoc::Filters::AsciiDoc', :asciidoc
-
- handles method_call(:register)
- namespace_only
-
- def process
- target = statement.jump(:const_path_ref)
- return if target != s(:const_path_ref, s(:var_ref, s(:const, 'Nanoc')), s(:const, 'Filter'))
-
- class_name = statement.jump(:string_literal).jump(:tstring_content)[0]
- identifier = statement.jump(:symbol_literal).jump(:ident)[0]
-
- obj = YARD::Registry.at(class_name.sub(/^::/, ''))
- obj['nanoc_identifiers'] ||= []
- obj['nanoc_identifiers'] << identifier
- end
-end
diff --git a/doc/yardoc_templates/default/layout/html/footer.erb b/doc/yardoc_templates/default/layout/html/footer.erb
deleted file mode 100644
index 17cac0c..0000000
--- a/doc/yardoc_templates/default/layout/html/footer.erb
+++ /dev/null
@@ -1,19 +0,0 @@
-<%= superb %>
-<script type="text/javascript">
- var _gaq = _gaq || [];
- var pluginUrl = '//www.google-analytics.com/plugins/ga/inpage_linkid.js';
- _gaq.push(['_require', 'inpage_linkid', pluginUrl]);
- _gaq.push(['_setAccount', 'UA-15639968-1']);
- _gaq.push(['_setDomainName', 'nanoc.ws']);
- _gaq.push(['_setAllowLinker', true]);
- _gaq.push(['_trackPageview']);
-
- (function() {
- var ga = document.createElement('script');
- ga.type = 'text/javascript';
- ga.async = true;
- ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
- var s = document.getElementsByTagName('script')[0];
- s.parentNode.insertBefore(ga, s);
- })();
-</script>
diff --git a/lib/nanoc.rb b/lib/nanoc.rb
index 66fdb89..accd1cb 100644
--- a/lib/nanoc.rb
+++ b/lib/nanoc.rb
@@ -27,11 +27,13 @@ require 'ref'
# Load general requirements
require 'digest'
require 'enumerator'
+require 'fiber'
require 'fileutils'
require 'forwardable'
require 'pathname'
require 'pstore'
require 'set'
+require 'singleton'
require 'tempfile'
require 'thread'
require 'time'
@@ -41,6 +43,8 @@ require 'English'
# Load Nanoc
require 'nanoc/version'
require 'nanoc/base'
+require 'nanoc/checking'
+require 'nanoc/deploying'
require 'nanoc/extra'
require 'nanoc/data_sources'
require 'nanoc/filters'
diff --git a/lib/nanoc/base.rb b/lib/nanoc/base.rb
index 23ec368..1b78b26 100644
--- a/lib/nanoc/base.rb
+++ b/lib/nanoc/base.rb
@@ -1,28 +1,13 @@
-module Nanoc
- autoload 'Error', 'nanoc/base/error'
- autoload 'Filter', 'nanoc/base/compilation/filter'
-end
-
# @api private
module Nanoc::Int
- # Load helper classes
- autoload 'Context', 'nanoc/base/context'
- autoload 'Checksummer', 'nanoc/base/checksummer'
- autoload 'DirectedGraph', 'nanoc/base/directed_graph'
- autoload 'Errors', 'nanoc/base/errors'
- autoload 'Memoization', 'nanoc/base/memoization'
- autoload 'PluginRegistry', 'nanoc/base/plugin_registry'
-
- # Load compilation classes
- autoload 'Compiler', 'nanoc/base/compilation/compiler'
- autoload 'DependencyTracker', 'nanoc/base/compilation/dependency_tracker'
- autoload 'ItemRepRepo', 'nanoc/base/compilation/item_rep_repo'
- autoload 'OutdatednessChecker', 'nanoc/base/compilation/outdatedness_checker'
- autoload 'OutdatednessReasons', 'nanoc/base/compilation/outdatedness_reasons'
end
require_relative 'base/core_ext'
require_relative 'base/contracts_support'
+require_relative 'base/memoization'
+require_relative 'base/plugin_registry'
+require_relative 'base/error'
+require_relative 'base/errors'
require_relative 'base/entities'
require_relative 'base/feature'
diff --git a/lib/nanoc/base/compilation/compiler.rb b/lib/nanoc/base/compilation/compiler.rb
deleted file mode 100644
index 38de206..0000000
--- a/lib/nanoc/base/compilation/compiler.rb
+++ /dev/null
@@ -1,300 +0,0 @@
-module Nanoc::Int
- # Responsible for compiling a site’s item representations.
- #
- # The compilation process makes use of notifications (see
- # {Nanoc::Int::NotificationCenter}) to track dependencies between items,
- # layouts, etc. The following notifications are used:
- #
- # * `compilation_started` — indicates that the compiler has started
- # compiling this item representation. Has one argument: the item
- # representation itself. Only one item can be compiled at a given moment;
- # therefore, it is not possible to get two consecutive
- # `compilation_started` notifications without also getting a
- # `compilation_ended` notification in between them.
- #
- # * `compilation_ended` — indicates that the compiler has finished compiling
- # this item representation (either successfully or with failure). Has one
- # argument: the item representation itself.
- #
- # * `processing_started` — indicates that the compiler has started
- # processing the specified object, which can be an item representation
- # (when it is compiled) or a layout (when it is used to lay out an item
- # representation or when it is used as a partial)
- #
- # * `processing_ended` — indicates that the compiler has finished processing
- # the specified object.
- #
- # @api private
- class Compiler
- # @api private
- attr_reader :site
-
- # The compilation stack. When the compiler begins compiling a rep or a
- # layout, it will be placed on the stack; when it is done compiling the
- # rep or layout, it will be removed from the stack.
- #
- # @return [Array] The compilation stack
- attr_reader :stack
-
- # @api private
- attr_reader :compiled_content_cache
-
- # @api private
- attr_reader :checksum_store
-
- # @api private
- attr_reader :rule_memory_store
-
- # @api private
- attr_reader :action_provider
-
- # @api private
- attr_reader :dependency_store
-
- # @api private
- attr_reader :outdatedness_checker
-
- # @api private
- attr_reader :reps
-
- def initialize(site, compiled_content_cache:, checksum_store:, rule_memory_store:, action_provider:, dependency_store:, outdatedness_checker:, reps:)
- @site = site
-
- @compiled_content_cache = compiled_content_cache
- @checksum_store = checksum_store
- @rule_memory_store = rule_memory_store
- @dependency_store = dependency_store
- @outdatedness_checker = outdatedness_checker
- @reps = reps
- @action_provider = action_provider
-
- @stack = []
- end
-
- def run_all
- @action_provider.preprocess(@site)
- build_reps
- prune
- run
- @action_provider.postprocess(@site, @reps)
- end
-
- def run
- load_stores
- @site.freeze
-
- # Determine which reps need to be recompiled
- forget_dependencies_if_outdated
-
- @stack = []
- compile_reps
- store
- ensure
- Nanoc::Int::TempFilenameFactory.instance.cleanup(
- Nanoc::Filter::TMP_BINARY_ITEMS_DIR,
- )
- Nanoc::Int::TempFilenameFactory.instance.cleanup(
- Nanoc::Int::ItemRepWriter::TMP_TEXT_ITEMS_DIR,
- )
- end
-
- def load_stores
- # FIXME: icky hack to update the dependency store’s list of objects
- # (does not include preprocessed objects otherwise)
- dependency_store.objects = site.items.to_a + site.layouts.to_a
-
- stores.each(&:load)
- end
-
- # Store the modified helper data used for compiling the site.
- #
- # @return [void]
- def store
- # Calculate rule memory
- (@reps.to_a + @site.layouts.to_a).each do |obj|
- rule_memory_store[obj] = action_provider.memory_for(obj).serialize
- end
-
- # Calculate checksums
- objects_to_checksum =
- site.items.to_a + site.layouts.to_a + site.code_snippets + [site.config]
- objects_to_checksum.each do |obj|
- checksum_store[obj] = Nanoc::Int::Checksummer.calc(obj)
- end
-
- # Store
- stores.each(&:store)
- end
-
- def build_reps
- builder = Nanoc::Int::ItemRepBuilder.new(
- site, action_provider, @reps
- )
- builder.run
- end
-
- # @param [Nanoc::Int::ItemRep] rep The item representation for which the
- # assigns should be fetched
- #
- # @return [Hash] The assigns that should be used in the next filter/layout
- # operation
- #
- # @api private
- def assigns_for(rep, dependency_tracker)
- content_or_filename_assigns =
- if rep.binary?
- { filename: rep.snapshot_contents[:last].filename }
- else
- { content: rep.snapshot_contents[:last].string }
- end
-
- view_context = create_view_context(dependency_tracker)
-
- content_or_filename_assigns.merge(
- item: Nanoc::ItemWithRepsView.new(rep.item, view_context),
- rep: Nanoc::ItemRepView.new(rep, view_context),
- item_rep: Nanoc::ItemRepView.new(rep, view_context),
- items: Nanoc::ItemCollectionWithRepsView.new(site.items, view_context),
- layouts: Nanoc::LayoutCollectionView.new(site.layouts, view_context),
- config: Nanoc::ConfigView.new(site.config, view_context),
- )
- end
-
- def create_view_context(dependency_tracker)
- Nanoc::ViewContext.new(
- reps: @reps,
- items: @site.items,
- dependency_tracker: dependency_tracker,
- compiler: self,
- )
- end
-
- # @api private
- def filter_name_and_args_for_layout(layout)
- mem = action_provider.memory_for(layout)
- if mem.nil? || mem.size != 1 || !mem[0].is_a?(Nanoc::Int::RuleMemoryActions::Filter)
- # FIXME: Provide a nicer error message
- raise Nanoc::Int::Errors::Generic, "No rule memory found for #{layout.identifier}"
- end
- [mem[0].filter_name, mem[0].params]
- end
-
- private
-
- def prune
- if site.config[:prune][:auto_prune]
- Nanoc::Extra::Pruner.new(site, exclude: prune_config_exclude).run
- end
- end
-
- def prune_config
- site.config[:prune] || {}
- end
-
- def prune_config_exclude
- prune_config[:exclude] || {}
- end
-
- def compile_reps
- # Listen to processing start/stop
- Nanoc::Int::NotificationCenter.on(:processing_started, self) { |obj| @stack.push(obj) }
- Nanoc::Int::NotificationCenter.on(:processing_ended, self) { |_obj| @stack.pop }
-
- # Assign snapshots
- @reps.each do |rep|
- rep.snapshot_defs = action_provider.snapshots_defs_for(rep)
- end
-
- # Find item reps to compile and compile them
- selector = Nanoc::Int::ItemRepSelector.new(@reps)
- selector.each do |rep|
- @stack = []
- compile_rep(rep)
- end
- ensure
- Nanoc::Int::NotificationCenter.remove(:processing_started, self)
- Nanoc::Int::NotificationCenter.remove(:processing_ended, self)
- end
-
- # Compiles the given item representation.
- #
- # This method should not be called directly; please use
- # {Nanoc::Int::Compiler#run} instead, and pass this item representation's item
- # as its first argument.
- #
- # @param [Nanoc::Int::ItemRep] rep The rep that is to be compiled
- #
- # @return [void]
- def compile_rep(rep)
- dependency_tracker = Nanoc::Int::DependencyTracker.new(@dependency_store)
-
- Nanoc::Int::NotificationCenter.post(:compilation_started, rep)
- Nanoc::Int::NotificationCenter.post(:processing_started, rep)
- dependency_tracker.enter(rep.item)
-
- if can_reuse_content_for_rep?(rep)
- Nanoc::Int::NotificationCenter.post(:cached_content_used, rep)
- rep.snapshot_contents = compiled_content_cache[rep]
- else
- recalculate_content_for_rep(rep, dependency_tracker)
- end
-
- rep.compiled = true
- compiled_content_cache[rep] = rep.snapshot_contents
-
- Nanoc::Int::NotificationCenter.post(:processing_ended, rep)
- Nanoc::Int::NotificationCenter.post(:compilation_ended, rep)
- rescue => e
- rep.forget_progress
- Nanoc::Int::NotificationCenter.post(:compilation_failed, rep, e)
- raise e
- ensure
- dependency_tracker.exit(rep.item)
- end
-
- # @return [Boolean]
- def can_reuse_content_for_rep?(rep)
- !outdatedness_checker.outdated?(rep) && compiled_content_cache[rep]
- end
-
- # @return [void]
- def recalculate_content_for_rep(rep, dependency_tracker)
- executor = Nanoc::Int::Executor.new(self, dependency_tracker)
-
- action_provider.memory_for(rep).each do |action|
- case action
- when Nanoc::Int::RuleMemoryActions::Filter
- executor.filter(rep, action.filter_name, action.params)
- when Nanoc::Int::RuleMemoryActions::Layout
- executor.layout(rep, action.layout_identifier, action.params)
- when Nanoc::Int::RuleMemoryActions::Snapshot
- executor.snapshot(rep, action.snapshot_name, final: action.final?, path: action.path)
- else
- raise "Internal inconsistency: unknown action #{action.inspect}"
- end
- end
- end
-
- # Clears the list of dependencies for items that will be recompiled.
- #
- # @return [void]
- def forget_dependencies_if_outdated
- @site.items.each do |i|
- if @reps[i].any? { |r| outdatedness_checker.outdated?(r) }
- @dependency_store.forget_dependencies_for(i)
- end
- end
- end
-
- # Returns all stores that can load/store data that can be used for
- # compilation.
- def stores
- [
- checksum_store,
- compiled_content_cache,
- @dependency_store,
- rule_memory_store,
- ]
- end
- end
-end
diff --git a/lib/nanoc/base/compilation/dependency_tracker.rb b/lib/nanoc/base/compilation/dependency_tracker.rb
deleted file mode 100644
index 005a148..0000000
--- a/lib/nanoc/base/compilation/dependency_tracker.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-module Nanoc::Int
- # @api private
- class DependencyTracker
- class Null
- def enter(_obj)
- end
-
- def exit(_obj)
- end
-
- def bounce(_obj)
- end
- end
-
- include Nanoc::Int::ContractsSupport
-
- def initialize(dependency_store)
- @dependency_store = dependency_store
- @stack = []
- end
-
- contract C::Or[Nanoc::Int::Item, Nanoc::Int::Layout] => C::Any
- def enter(obj)
- unless @stack.empty?
- Nanoc::Int::NotificationCenter.post(:dependency_created, @stack.last, obj)
- @dependency_store.record_dependency(@stack.last, obj)
- end
-
- @stack.push(obj)
- end
-
- contract C::Or[Nanoc::Int::Item, Nanoc::Int::Layout] => C::Any
- def exit(_obj)
- @stack.pop
- end
-
- contract C::Or[Nanoc::Int::Item, Nanoc::Int::Layout] => C::Any
- def bounce(obj)
- enter(obj)
- exit(obj)
- end
- end
-end
diff --git a/lib/nanoc/base/compilation/outdatedness_checker.rb b/lib/nanoc/base/compilation/outdatedness_checker.rb
deleted file mode 100644
index eabab56..0000000
--- a/lib/nanoc/base/compilation/outdatedness_checker.rb
+++ /dev/null
@@ -1,210 +0,0 @@
-module Nanoc::Int
- # Responsible for determining whether an item or a layout is outdated.
- #
- # @api private
- class OutdatednessChecker
- extend Nanoc::Int::Memoization
-
- attr_reader :checksum_store
- attr_reader :dependency_store
- attr_reader :rule_memory_store
- attr_reader :site
-
- Reasons = Nanoc::Int::OutdatednessReasons
-
- # @param [Nanoc::Int::Site] site
- # @param [Nanoc::Int::ChecksumStore] checksum_store
- # @param [Nanoc::Int::DependencyStore] dependency_store
- # @param [Nanoc::Int::RuleMemoryStore] rule_memory_store
- # @param [Nanoc::Int::ActionProvider] action_provider
- # @param [Nanoc::Int::ItemRepRepo] reps
- def initialize(site:, checksum_store:, dependency_store:, rule_memory_store:, action_provider:, reps:)
- @site = site
- @checksum_store = checksum_store
- @dependency_store = dependency_store
- @rule_memory_store = rule_memory_store
- @action_provider = action_provider
- @reps = reps
-
- @basic_outdatedness_reasons = {}
- @outdatedness_reasons = {}
- @objects_outdated_due_to_dependencies = {}
- end
-
- # Checks whether the given object is outdated and therefore needs to be
- # recompiled.
- #
- # @param [Nanoc::Int::Item, Nanoc::Int::ItemRep, Nanoc::Int::Layout] obj The object
- # whose outdatedness should be checked.
- #
- # @return [Boolean] true if the object is outdated, false otherwise
- def outdated?(obj)
- !outdatedness_reason_for(obj).nil?
- end
-
- # Calculates the reason why the given object is outdated.
- #
- # @param [Nanoc::Int::Item, Nanoc::Int::ItemRep, Nanoc::Int::Layout] obj The object
- # whose outdatedness reason should be calculated.
- #
- # @return [Reasons::Generic, nil] The reason why the
- # given object is outdated, or nil if the object is not outdated.
- def outdatedness_reason_for(obj)
- reason = basic_outdatedness_reason_for(obj)
- if reason.nil? && outdated_due_to_dependencies?(obj)
- reason = Reasons::DependenciesOutdated
- end
- reason
- end
- memoize :outdatedness_reason_for
-
- private
-
- # Checks whether the given object is outdated and therefore needs to be
- # recompiled. This method does not take dependencies into account; use
- # {#outdated?} if you want to include dependencies in the outdatedness
- # check.
- #
- # @param [Nanoc::Int::Item, Nanoc::Int::ItemRep, Nanoc::Int::Layout] obj The object
- # whose outdatedness should be checked.
- #
- # @return [Boolean] true if the object is outdated, false otherwise
- def basic_outdated?(obj)
- !basic_outdatedness_reason_for(obj).nil?
- end
-
- # Calculates the reason why the given object is outdated. This method does
- # not take dependencies into account; use {#outdatedness_reason_for?} if
- # you want to include dependencies in the outdatedness check.
- #
- # @param [Nanoc::Int::Item, Nanoc::Int::ItemRep, Nanoc::Int::Layout] obj The object
- # whose outdatedness reason should be calculated.
- #
- # @return [Reasons::Generic, nil] The reason why the
- # given object is outdated, or nil if the object is not outdated.
- def basic_outdatedness_reason_for(obj)
- case obj
- when Nanoc::Int::ItemRep
- # Outdated if rules outdated
- return Reasons::RulesModified if
- rule_memory_differs_for(obj)
-
- # Outdated if checksums are missing or different
- return Reasons::NotEnoughData unless checksums_available?(obj.item)
- return Reasons::SourceModified unless checksums_identical?(obj.item)
-
- # Outdated if compiled file doesn't exist (yet)
- return Reasons::NotWritten if obj.raw_path && !File.file?(obj.raw_path)
-
- # Outdated if code snippets outdated
- return Reasons::CodeSnippetsModified if site.code_snippets.any? do |cs|
- object_modified?(cs)
- end
-
- # Outdated if configuration outdated
- return Reasons::ConfigurationModified if object_modified?(site.config)
-
- # Not outdated
- return nil
- when Nanoc::Int::Item
- @reps[obj].find { |rep| basic_outdatedness_reason_for(rep) }
- when Nanoc::Int::Layout
- # Outdated if rules outdated
- return Reasons::RulesModified if
- rule_memory_differs_for(obj)
-
- # Outdated if checksums are missing or different
- return Reasons::NotEnoughData unless checksums_available?(obj)
- return Reasons::SourceModified unless checksums_identical?(obj)
-
- # Not outdated
- return nil
- else
- raise "do not know how to check outdatedness of #{obj.inspect}"
- end
- end
- memoize :basic_outdatedness_reason_for
-
- # Checks whether the given object is outdated due to dependencies.
- #
- # @param [Nanoc::Int::Item, Nanoc::Int::ItemRep, Nanoc::Int::Layout] obj The object
- # whose outdatedness should be checked.
- #
- # @param [Set] processed The collection of items that has been visited
- # during this outdatedness check. This is used to prevent checks for
- # items that (indirectly) depend on their own from looping
- # indefinitely. It should not be necessary to pass this a custom value.
- #
- # @return [Boolean] true if the object is outdated, false otherwise
- def outdated_due_to_dependencies?(obj, processed = Hamster::Set.new)
- # Convert from rep to item if necessary
- obj = obj.item if obj.is_a?(Nanoc::Int::ItemRep)
-
- # Get from cache
- if @objects_outdated_due_to_dependencies.key?(obj)
- return @objects_outdated_due_to_dependencies[obj]
- end
-
- # Check processed
- # Don’t return true; the false will be or’ed into a true if there
- # really is a dependency that is causing outdatedness.
- return false if processed.include?(obj)
-
- # Calculate
- is_outdated = dependency_store.objects_causing_outdatedness_of(obj).any? do |other|
- other.nil? || basic_outdated?(other) || outdated_due_to_dependencies?(other, processed.merge([obj]))
- end
-
- # Cache
- @objects_outdated_due_to_dependencies[obj] = is_outdated
-
- # Done
- is_outdated
- end
-
- # @param [Nanoc::Int::ItemRep, Nanoc::Int::Layout] obj The layout or item
- # representation to check the rule memory for
- #
- # @return [Boolean] true if the rule memory for the given item
- # represenation has changed, false otherwise
- def rule_memory_differs_for(obj)
- !rule_memory_store[obj].eql?(@action_provider.memory_for(obj).serialize)
- end
- memoize :rule_memory_differs_for
-
- # @param obj The object to create a checksum for
- #
- # @return [String] The digest
- def calc_checksum(obj)
- Nanoc::Int::Checksummer.calc(obj)
- end
- memoize :calc_checksum
-
- # @param obj
- #
- # @return [Boolean] false if either the new or the old checksum for the
- # given object is not available, true if both checksums are available
- def checksums_available?(obj)
- checksum_store[obj] && calc_checksum(obj)
- end
- memoize :checksums_available?
-
- # @param obj
- #
- # @return [Boolean] false if the old and new checksums for the given
- # object differ, true if they are identical
- def checksums_identical?(obj)
- checksum_store[obj] == calc_checksum(obj)
- end
- memoize :checksums_identical?
-
- # @param obj
- #
- # @return [Boolean] true if the old and new checksums for the given object
- # are available and identical, false otherwise
- def object_modified?(obj)
- !checksums_available?(obj) || !checksums_identical?(obj)
- end
- memoize :object_modified?
- end
-end
diff --git a/lib/nanoc/base/contracts_support.rb b/lib/nanoc/base/contracts_support.rb
index e8079cc..4cc7071 100644
--- a/lib/nanoc/base/contracts_support.rb
+++ b/lib/nanoc/base/contracts_support.rb
@@ -29,18 +29,58 @@ module Nanoc::Int
Or = Ignorer.instance
Func = Ignorer.instance
RespondTo = Ignorer.instance
+ Named = Ignorer.instance
+ IterOf = Ignorer.instance
+ HashOf = Ignorer.instance
- def contract(*args)
- end
+ def contract(*args); end
end
module EnabledContracts
+ class AbstractContract
+ def self.[](*vals)
+ new(*vals)
+ end
+ end
+
+ class Named < AbstractContract
+ def initialize(name)
+ @name = name
+ end
+
+ def valid?(val)
+ val.is_a?(Kernel.const_get(@name))
+ end
+
+ def inspect
+ "#{self.class}(#{@name})"
+ end
+ end
+
+ class IterOf < AbstractContract
+ def initialize(contract)
+ @contract = contract
+ end
+
+ def valid?(val)
+ val.respond_to?(:each) && val.all? { |v| Contract.valid?(v, @contract) }
+ end
+
+ def inspect
+ "#{self.class}(#{@contract})"
+ end
+ end
+
def contract(*args)
Contract(*args)
end
end
- def self.included(base)
+ def self.setup_once
+ @_contracts_support__setup ||= false
+ return @_contracts_support__should_enable if @_contracts_support__setup
+ @_contracts_support__setup = true
+
contracts_loadable =
begin
require 'contracts'
@@ -49,7 +89,19 @@ module Nanoc::Int
false
end
- should_enable = contracts_loadable && !ENV.key?('DISABLE_CONTRACTS')
+ @_contracts_support__should_enable = contracts_loadable && !ENV.key?('DISABLE_CONTRACTS')
+
+ if @_contracts_support__should_enable
+ # FIXME: ugly
+ ::Contracts.const_set('Named', EnabledContracts::Named)
+ ::Contracts.const_set('IterOf', EnabledContracts::IterOf)
+ end
+
+ @_contracts_support__should_enable
+ end
+
+ def self.included(base)
+ should_enable = setup_once
if should_enable
unless base.include?(::Contracts::Core)
diff --git a/lib/nanoc/base/core_ext/array.rb b/lib/nanoc/base/core_ext/array.rb
index 98fafa3..d8e077d 100644
--- a/lib/nanoc/base/core_ext/array.rb
+++ b/lib/nanoc/base/core_ext/array.rb
@@ -20,8 +20,6 @@ module Nanoc::ArrayExtensions
# @see Hash#__nanoc_freeze_recursively
#
# @return [void]
- #
- # @since 3.2.0
def __nanoc_freeze_recursively
return if frozen?
freeze
diff --git a/lib/nanoc/base/core_ext/hash.rb b/lib/nanoc/base/core_ext/hash.rb
index c0890c2..84bb22a 100644
--- a/lib/nanoc/base/core_ext/hash.rb
+++ b/lib/nanoc/base/core_ext/hash.rb
@@ -22,8 +22,6 @@ module Nanoc::HashExtensions
# @see Array#__nanoc_freeze_recursively
#
# @return [void]
- #
- # @since 3.2.0
def __nanoc_freeze_recursively
return if frozen?
freeze
diff --git a/lib/nanoc/base/entities.rb b/lib/nanoc/base/entities.rb
index 5794d7d..f55b90b 100644
--- a/lib/nanoc/base/entities.rb
+++ b/lib/nanoc/base/entities.rb
@@ -1,7 +1,10 @@
+require_relative 'entities/context'
+require_relative 'entities/directed_graph'
+
require_relative 'entities/identifier'
require_relative 'entities/content'
-require_relative 'entities/rule_memory_action'
-require_relative 'entities/rule_memory_actions'
+require_relative 'entities/processing_action'
+require_relative 'entities/processing_actions'
require_relative 'entities/code_snippet'
require_relative 'entities/configuration'
@@ -12,6 +15,11 @@ require_relative 'entities/item'
require_relative 'entities/item_rep'
require_relative 'entities/layout'
require_relative 'entities/pattern'
+require_relative 'entities/props'
require_relative 'entities/rule_memory'
require_relative 'entities/site'
require_relative 'entities/snapshot_def'
+
+require_relative 'entities/outdatedness_status'
+require_relative 'entities/outdatedness_reasons'
+require_relative 'entities/dependency'
diff --git a/lib/nanoc/base/entities/configuration.rb b/lib/nanoc/base/entities/configuration.rb
index a99c562..f1cefee 100644
--- a/lib/nanoc/base/entities/configuration.rb
+++ b/lib/nanoc/base/entities/configuration.rb
@@ -33,11 +33,21 @@ module Nanoc::Int
string_pattern_type: 'glob',
}.freeze
- contract Hash => C::Any
+ # @return [String, nil] The active environment for the configuration
+ attr_reader :env_name
+
+ # Configuration environments property key
+ ENVIRONMENTS_CONFIG_KEY = :environments
+ NANOC_ENV = 'NANOC_ENV'.freeze
+ NANOC_ENV_DEFAULT = 'default'.freeze
+
+ contract C::KeywordArgs[hash: C::Optional[Hash], env_name: C::Maybe[String]] => C::Any
# Creates a new configuration with the given hash.
#
# @param [Hash] hash The actual configuration hash
- def initialize(hash = {})
+ # @param [String, nil] env_name The active environment for this configuration
+ def initialize(hash: {}, env_name: nil)
+ @env_name = env_name
@wrapped = hash.__nanoc_symbolize_keys_recursively
end
@@ -48,7 +58,19 @@ module Nanoc::Int
DEFAULT_DATA_SOURCE_CONFIG.merge(ds)
end
- self.class.new(new_wrapped)
+ self.class.new(hash: new_wrapped)
+ end
+
+ def with_environment
+ return self unless @wrapped.key?(ENVIRONMENTS_CONFIG_KEY)
+
+ # Set active environment
+ env_name = @env_name || ENV.fetch(NANOC_ENV, NANOC_ENV_DEFAULT)
+
+ # Load given environment configuration
+ env_config = @wrapped[ENVIRONMENTS_CONFIG_KEY].fetch(env_name.to_sym, {})
+
+ self.class.new(hash: @wrapped, env_name: env_name).merge(env_config)
end
contract C::None => Hash
@@ -86,12 +108,12 @@ module Nanoc::Int
contract C::Or[Hash, self] => self
def merge(hash)
- self.class.new(@wrapped.merge(hash.to_h))
+ self.class.new(hash: @wrapped.merge(hash.to_h), env_name: @env_name)
end
contract C::Any => self
def without(key)
- self.class.new(@wrapped.reject { |k, _v| k == key })
+ self.class.new(hash: @wrapped.reject { |k, _v| k == key }, env_name: @env_name)
end
contract C::Any => self
diff --git a/lib/nanoc/base/context.rb b/lib/nanoc/base/entities/context.rb
similarity index 83%
rename from lib/nanoc/base/context.rb
rename to lib/nanoc/base/entities/context.rb
index 93357aa..95cd15e 100644
--- a/lib/nanoc/base/context.rb
+++ b/lib/nanoc/base/entities/context.rb
@@ -24,12 +24,9 @@ module Nanoc::Int
# end
# # => "I am Max Payne and I am hiding in a cheap motel."
def initialize(hash)
+ metaclass = class << self; self; end
hash.each_pair do |key, value|
- # Build instance variable
instance_variable_set('@' + key.to_s, value)
-
- # Define method
- metaclass = (class << self; self; end)
metaclass.send(:define_method, key) { value }
end
end
@@ -37,8 +34,15 @@ module Nanoc::Int
# Returns a binding for this instance.
#
# @return [Binding] A binding for this instance
+ # rubocop:disable Style/AccessorMethodName
def get_binding
binding
end
+ # rubocop:enable Style/AccessorMethodName
+
+ def include(mod)
+ metaclass = class << self; self; end
+ metaclass.instance_eval { include(mod) }
+ end
end
end
diff --git a/lib/nanoc/base/entities/dependency.rb b/lib/nanoc/base/entities/dependency.rb
new file mode 100644
index 0000000..bf4c1e9
--- /dev/null
+++ b/lib/nanoc/base/entities/dependency.rb
@@ -0,0 +1,28 @@
+module Nanoc::Int
+ # @api private
+ # A dependency between two items/layouts.
+ class Dependency
+ include Nanoc::Int::ContractsSupport
+
+ contract C::None => C::Maybe[C::Or[Nanoc::Int::Item, Nanoc::Int::Layout]]
+ attr_reader :from
+
+ contract C::None => C::Maybe[C::Or[Nanoc::Int::Item, Nanoc::Int::Layout]]
+ attr_reader :to
+
+ contract C::None => Nanoc::Int::Props
+ attr_reader :props
+
+ contract C::Maybe[C::Or[Nanoc::Int::Item, Nanoc::Int::Layout]], C::Maybe[C::Or[Nanoc::Int::Item, Nanoc::Int::Layout]], Nanoc::Int::Props => C::Any
+ def initialize(from, to, props)
+ @from = from
+ @to = to
+ @props = props
+ end
+
+ contract C::None => String
+ def inspect
+ "Dependency(#{@from.inspect} -> #{@to.inspect}, #{@props.inspect})"
+ end
+ end
+end
diff --git a/lib/nanoc/base/directed_graph.rb b/lib/nanoc/base/entities/directed_graph.rb
similarity index 93%
rename from lib/nanoc/base/directed_graph.rb
rename to lib/nanoc/base/entities/directed_graph.rb
index 7460983..acf9b98 100644
--- a/lib/nanoc/base/directed_graph.rb
+++ b/lib/nanoc/base/entities/directed_graph.rb
@@ -41,6 +41,8 @@ module Nanoc::Int
@from_graph = {}
@to_graph = {}
+ @edge_props = {}
+
@roots = Set.new(@vertices.keys)
invalidate_caches
@@ -55,7 +57,7 @@ module Nanoc::Int
# @param to Vertex where the edge should end
#
# @return [void]
- def add_edge(from, to)
+ def add_edge(from, to, props: nil)
add_vertex(from)
add_vertex(to)
@@ -65,6 +67,10 @@ module Nanoc::Int
@to_graph[to] ||= Set.new
@to_graph[to] << from
+ if props
+ @edge_props[[from, to]] = props
+ end
+
@roots.delete(to)
invalidate_caches
@@ -78,8 +84,6 @@ module Nanoc::Int
# @param to End vertex of the edge
#
# @return [void]
- #
- # @since 3.2.0
def delete_edge(from, to)
@from_graph[from] ||= Set.new
@from_graph[from].delete(to)
@@ -87,6 +91,8 @@ module Nanoc::Int
@to_graph[to] ||= Set.new
@to_graph[to].delete(from)
+ @edge_props.delete([from, to])
+
@roots.add(to) if @to_graph[to].empty?
invalidate_caches
@@ -97,8 +103,6 @@ module Nanoc::Int
# @param v The vertex to add to the graph
#
# @return [void]
- #
- # @since 3.2.0
def add_vertex(v)
return if @vertices.key?(v)
@@ -112,13 +116,12 @@ module Nanoc::Int
# @param from Vertex from which all edges should be removed
#
# @return [void]
- #
- # @since 3.2.0
def delete_edges_from(from)
return if @from_graph[from].nil?
@from_graph[from].each do |to|
@to_graph[to].delete(from)
+ @edge_props.delete([from, to])
@roots.add(to) if @to_graph[to].empty?
end
@from_graph.delete(from)
@@ -134,6 +137,7 @@ module Nanoc::Int
@to_graph[to].each do |from|
@from_graph[from].delete(to)
+ @edge_props.delete([from, to])
end
@to_graph.delete(to)
@roots.add(to)
@@ -144,8 +148,6 @@ module Nanoc::Int
# @param v Vertex to remove from the graph
#
# @return [void]
- #
- # @since 3.2.0
def delete_vertex(v)
delete_edges_to(v)
delete_edges_from(v)
@@ -196,6 +198,10 @@ module Nanoc::Int
@successors[from] ||= recursively_find_vertices(from, :direct_successors_of)
end
+ def props_for(from, to)
+ @edge_props[[from, to]]
+ end
+
# @return [Array] The list of all vertices in this graph.
def vertices
@vertices.keys.sort_by { |v| @vertices[v] }
@@ -208,8 +214,8 @@ module Nanoc::Int
def edges
result = []
@vertices.each_pair do |v1, i1|
- direct_successors_of(v1).map { |v2| @vertices[v2] }.each do |i2|
- result << [i1, i2]
+ direct_successors_of(v1).map { |v2| [@vertices[v2], v2] }.each do |i2, v2|
+ result << [i1, i2, @edge_props[[v1, v2]]]
end
end
result
@@ -218,8 +224,6 @@ module Nanoc::Int
# Returns all root vertices, i.e. vertices where no edge points to.
#
# @return [Set] The set of all root vertices in this graph.
- #
- # @since 3.2.0
def roots
@roots
end
diff --git a/lib/nanoc/base/entities/document.rb b/lib/nanoc/base/entities/document.rb
index f130fc1..86f64f6 100644
--- a/lib/nanoc/base/entities/document.rb
+++ b/lib/nanoc/base/entities/document.rb
@@ -5,7 +5,7 @@ module Nanoc
include Nanoc::Int::ContractsSupport
# @return [Nanoc::Int::Content]
- attr_reader :content
+ attr_accessor :content
# @return [Hash]
def attributes
@@ -18,24 +18,41 @@ module Nanoc
# @return [String, nil]
attr_accessor :checksum_data
+ # @return [String, nil]
+ attr_accessor :content_checksum_data
+
+ # @return [String, nil]
+ attr_accessor :attributes_checksum_data
+
c_content = C::Or[String, Nanoc::Int::Content]
c_attributes = C::Or[Hash, Proc]
c_identifier = C::Or[String, Nanoc::Identifier]
- c_checksum_data = C::Optional[C::Maybe[String]]
+ c_checksum_data = C::KeywordArgs[
+ checksum_data: C::Optional[C::Maybe[String]],
+ content_checksum_data: C::Optional[C::Maybe[String]],
+ attributes_checksum_data: C::Optional[C::Maybe[String]],
+ ]
- contract c_content, c_attributes, c_identifier, C::KeywordArgs[checksum_data: c_checksum_data] => C::Any
+ contract c_content, c_attributes, c_identifier, c_checksum_data => C::Any
# @param [String, Nanoc::Int::Content] content
#
# @param [Hash, Proc] attributes
#
# @param [String, Nanoc::Identifier] identifier
#
- # @param [String, nil] checksum_data Used to determine whether the document has changed
- def initialize(content, attributes, identifier, checksum_data: nil)
+ # @param [String, nil] checksum_data
+ #
+ # @param [String, nil] content_checksum_data
+ #
+ # @param [String, nil] attributes_checksum_data
+ def initialize(content, attributes, identifier, checksum_data: nil, content_checksum_data: nil, attributes_checksum_data: nil)
@content = Nanoc::Int::Content.create(content)
@attributes = Nanoc::Int::LazyValue.new(attributes).map(&:__nanoc_symbolize_keys_recursively)
@identifier = Nanoc::Identifier.from(identifier)
+
@checksum_data = checksum_data
+ @content_checksum_data = content_checksum_data
+ @attributes_checksum_data = attributes_checksum_data
end
contract C::None => self
diff --git a/lib/nanoc/base/entities/identifiable_collection.rb b/lib/nanoc/base/entities/identifiable_collection.rb
index eaa1995..fe421a1 100644
--- a/lib/nanoc/base/entities/identifiable_collection.rb
+++ b/lib/nanoc/base/entities/identifiable_collection.rb
@@ -11,8 +11,7 @@ module Nanoc::Int
def_delegator :@objects, :<<
def_delegator :@objects, :concat
- # FIXME: use Nanoc::Int::Configuration
- contract C::Any => C::Any
+ contract C::Or[Hash, C::Named['Nanoc::Int::Configuration']] => C::Any
def initialize(config)
@config = config
diff --git a/lib/nanoc/base/entities/identifier.rb b/lib/nanoc/base/entities/identifier.rb
index 8ed8e82..a62a960 100644
--- a/lib/nanoc/base/entities/identifier.rb
+++ b/lib/nanoc/base/entities/identifier.rb
@@ -98,15 +98,13 @@ module Nanoc
end
contract C::None => C::Bool
- # @return [Boolean] True if this is a full-type identifier (i.e. includes
- # the extension), false otherwise
+ # Whether or not this is a full identifier (i.e.includes the extension).
def full?
@type == :full
end
contract C::None => C::Bool
- # @return [Boolean] True if this is a legacy identifier (i.e. does not
- # include the extension), false otherwise
+ # Whether or not this is a legacy identifier (i.e. does not include the extension).
def legacy?
@type == :legacy
end
@@ -133,7 +131,7 @@ module Nanoc
end
contract C::None => String
- # @return [String]
+ # The identifier, as string, with the last extension removed
def without_ext
unless full?
raise UnsupportedLegacyOperationError
@@ -149,7 +147,7 @@ module Nanoc
end
contract C::None => C::Maybe[String]
- # @return [String, nil] The extension, without a leading dot.
+ # The extension, without a leading dot
def ext
unless full?
raise UnsupportedLegacyOperationError
@@ -160,7 +158,7 @@ module Nanoc
end
contract C::None => String
- # @return [String]
+ # The identifier, as string, with all extensions removed
def without_exts
extname = exts.join('.')
if !extname.empty?
@@ -171,7 +169,7 @@ module Nanoc
end
contract C::None => C::ArrayOf[String]
- # @return [Array] List of extensions, without a leading dot.
+ # The list of extensions, without a leading dot
def exts
unless full?
raise UnsupportedLegacyOperationError
diff --git a/lib/nanoc/base/entities/item_rep.rb b/lib/nanoc/base/entities/item_rep.rb
index 166d985..5cf0cca 100644
--- a/lib/nanoc/base/entities/item_rep.rb
+++ b/lib/nanoc/base/entities/item_rep.rb
@@ -42,7 +42,7 @@ module Nanoc::Int
@raw_paths = {}
@paths = {}
@snapshot_defs = []
- initialize_content
+ @snapshot_contents = { last: @item.content }
# Reset flags
@compiled = false
@@ -64,48 +64,31 @@ module Nanoc::Int
# @return [String] The compiled content at the given snapshot (or the
# default snapshot if no snapshot is specified)
def compiled_content(snapshot: nil)
- # Make sure we're not binary
- if binary?
- raise Nanoc::Int::Errors::CannotGetCompiledContentOfBinaryItem.new(self)
- end
-
# Get name of last pre-layout snapshot
snapshot_name = snapshot || (@snapshot_contents[:pre] ? :pre : :last)
- is_moving = [:pre, :post, :last].include?(snapshot_name)
# Check existance of snapshot
snapshot_def = snapshot_defs.reverse.find { |sd| sd.name == snapshot_name }
- if !is_moving && (snapshot_def.nil? || !snapshot_def.final?)
+ unless snapshot_def
raise Nanoc::Int::Errors::NoSuchSnapshot.new(self, snapshot_name)
end
# Verify snapshot is usable
- is_still_moving =
- case snapshot_name
- when :post, :last
- true
- when :pre
- snapshot_def.nil? || !snapshot_def.final?
- end
- is_usable_snapshot = @snapshot_contents[snapshot_name] && (compiled? || !is_still_moving)
+ stopped_moving = snapshot_name != :last || compiled?
+ is_usable_snapshot = @snapshot_contents[snapshot_name] && stopped_moving
unless is_usable_snapshot
- raise Nanoc::Int::Errors::UnmetDependency.new(self)
+ Fiber.yield(Nanoc::Int::Errors::UnmetDependency.new(self))
+ return compiled_content(snapshot: snapshot)
end
- @snapshot_contents[snapshot_name].string
- end
+ # Verify snapshot is not binary
+ snapshot_content = @snapshot_contents[snapshot_name]
+ if snapshot_content.binary?
+ raise Nanoc::Int::Errors::CannotGetCompiledContentOfBinaryItem.new(self)
+ end
- contract Symbol => C::Bool
- # Checks whether content exists at a given snapshot.
- #
- # @return [Boolean] True if content exists for the snapshot with the
- # given name, false otherwise
- #
- # @since 3.2.0
- def snapshot?(snapshot_name)
- !@snapshot_contents[snapshot_name].nil?
+ snapshot_content.string
end
- alias has_snapshot? snapshot?
contract C::KeywordArgs[snapshot: C::Optional[Symbol]] => C::Maybe[String]
# Returns the item rep’s raw path. It includes the path to the output
@@ -133,18 +116,6 @@ module Nanoc::Int
@paths[snapshot]
end
- contract C::None => nil
- # Resets the compilation progress for this item representation. This is
- # necessary when an unmet dependency is detected during compilation.
- #
- # @api private
- #
- # @return [void]
- def forget_progress
- initialize_content
- nil
- end
-
# Returns an object that can be used for uniquely identifying objects.
#
# @api private
@@ -155,13 +126,7 @@ module Nanoc::Int
end
def inspect
- "<#{self.class} name=\"#{name}\" binary=#{binary?} raw_path=\"#{raw_path}\" item.identifier=\"#{item.identifier}\">"
- end
-
- private
-
- def initialize_content
- @snapshot_contents = { last: @item.content }
+ "<#{self.class} name=\"#{name}\" raw_path=\"#{raw_path}\" item.identifier=\"#{item.identifier}\">"
end
end
end
diff --git a/lib/nanoc/base/compilation/outdatedness_reasons.rb b/lib/nanoc/base/entities/outdatedness_reasons.rb
similarity index 56%
rename from lib/nanoc/base/compilation/outdatedness_reasons.rb
rename to lib/nanoc/base/entities/outdatedness_reasons.rb
index 09b76b9..f23e543 100644
--- a/lib/nanoc/base/compilation/outdatedness_reasons.rb
+++ b/lib/nanoc/base/entities/outdatedness_reasons.rb
@@ -9,39 +9,54 @@ module Nanoc::Int
# @return [String] A descriptive message for this outdatedness reason
attr_reader :message
+ # @return [Nanoc::Int::Props]
+ attr_reader :props
+
# @param [String] message The descriptive message for this outdatedness
# reason
- def initialize(message)
+ def initialize(message, props = Nanoc::Int::Props.new)
@message = message
+ @props = props
end
end
CodeSnippetsModified = Generic.new(
'The code snippets have been modified since the last time the site was compiled.',
+ Props.new(raw_content: true, attributes: true, compiled_content: true, path: true),
)
ConfigurationModified = Generic.new(
'The site configuration has been modified since the last time the site was compiled.',
+ Props.new(raw_content: true, attributes: true, compiled_content: true, path: true),
)
DependenciesOutdated = Generic.new(
'This item uses content or attributes that have changed since the last time the site was compiled.',
)
- NotEnoughData = Generic.new(
- 'Not enough data is present to correctly determine whether the item is outdated.',
- )
-
NotWritten = Generic.new(
'This item representation has not yet been written to the output directory (but it does have a path).',
+ Props.new(raw_content: true, attributes: true, compiled_content: true, path: true),
)
RulesModified = Generic.new(
'The rules file has been modified since the last time the site was compiled.',
+ Props.new(compiled_content: true, path: true),
+ )
+
+ ContentModified = Generic.new(
+ 'The content of this item has been modified since the last time the site was compiled.',
+ Props.new(raw_content: true, compiled_content: true),
+ )
+
+ AttributesModified = Generic.new(
+ 'The attributes of this item have been modified since the last time the site was compiled.',
+ Props.new(attributes: true, compiled_content: true),
)
- SourceModified = Generic.new(
- 'The source file of this item has been modified since the last time the site was compiled.',
+ PathsModified = Generic.new(
+ 'One or more output paths of this item have been modified since the last time the site was compiled.',
+ Props.new(path: true),
)
end
end
diff --git a/lib/nanoc/base/entities/outdatedness_status.rb b/lib/nanoc/base/entities/outdatedness_status.rb
new file mode 100644
index 0000000..98049bd
--- /dev/null
+++ b/lib/nanoc/base/entities/outdatedness_status.rb
@@ -0,0 +1,23 @@
+module Nanoc::Int
+ # @api private
+ class OutdatednessStatus
+ attr_reader :reasons
+ attr_reader :props
+
+ def initialize(reasons: [], props: Props.new)
+ @reasons = reasons
+ @props = props
+ end
+
+ def useful_to_apply?(rule)
+ (rule.instance.reason.props.active - @props.active).any?
+ end
+
+ def update(reason)
+ self.class.new(
+ reasons: @reasons + [reason],
+ props: @props.merge(reason.props),
+ )
+ end
+ end
+end
diff --git a/lib/nanoc/base/entities/rule_memory_action.rb b/lib/nanoc/base/entities/processing_action.rb
similarity index 67%
rename from lib/nanoc/base/entities/rule_memory_action.rb
rename to lib/nanoc/base/entities/processing_action.rb
index 6de3756..bdcf5e4 100644
--- a/lib/nanoc/base/entities/rule_memory_action.rb
+++ b/lib/nanoc/base/entities/processing_action.rb
@@ -1,11 +1,11 @@
module Nanoc::Int
- class RuleMemoryAction
+ class ProcessingAction
def serialize
- raise NotImplementedError.new('Nanoc::RuleMemoryAction subclasses must implement #serialize and #to_s')
+ raise NotImplementedError.new('Nanoc::ProcessingAction subclasses must implement #serialize and #to_s')
end
def to_s
- raise NotImplementedError.new('Nanoc::RuleMemoryAction subclasses must implement #serialize and #to_s')
+ raise NotImplementedError.new('Nanoc::ProcessingAction subclasses must implement #serialize and #to_s')
end
def inspect
diff --git a/lib/nanoc/base/entities/processing_actions.rb b/lib/nanoc/base/entities/processing_actions.rb
new file mode 100644
index 0000000..d33260b
--- /dev/null
+++ b/lib/nanoc/base/entities/processing_actions.rb
@@ -0,0 +1,3 @@
+require_relative 'processing_actions/filter'
+require_relative 'processing_actions/layout'
+require_relative 'processing_actions/snapshot'
diff --git a/lib/nanoc/base/entities/rule_memory_actions/filter.rb b/lib/nanoc/base/entities/processing_actions/filter.rb
similarity index 82%
rename from lib/nanoc/base/entities/rule_memory_actions/filter.rb
rename to lib/nanoc/base/entities/processing_actions/filter.rb
index 0878282..54cc3b7 100644
--- a/lib/nanoc/base/entities/rule_memory_actions/filter.rb
+++ b/lib/nanoc/base/entities/processing_actions/filter.rb
@@ -1,5 +1,5 @@
-module Nanoc::Int::RuleMemoryActions
- class Filter < Nanoc::Int::RuleMemoryAction
+module Nanoc::Int::ProcessingActions
+ class Filter < Nanoc::Int::ProcessingAction
# filter :foo
# filter :foo, params
diff --git a/lib/nanoc/base/entities/rule_memory_actions/layout.rb b/lib/nanoc/base/entities/processing_actions/layout.rb
similarity index 84%
rename from lib/nanoc/base/entities/rule_memory_actions/layout.rb
rename to lib/nanoc/base/entities/processing_actions/layout.rb
index 004c350..2b5c359 100644
--- a/lib/nanoc/base/entities/rule_memory_actions/layout.rb
+++ b/lib/nanoc/base/entities/processing_actions/layout.rb
@@ -1,5 +1,5 @@
-module Nanoc::Int::RuleMemoryActions
- class Layout < Nanoc::Int::RuleMemoryAction
+module Nanoc::Int::ProcessingActions
+ class Layout < Nanoc::Int::ProcessingAction
# layout '/foo.erb'
# layout '/foo.erb', params
diff --git a/lib/nanoc/base/entities/processing_actions/snapshot.rb b/lib/nanoc/base/entities/processing_actions/snapshot.rb
new file mode 100644
index 0000000..dd171fd
--- /dev/null
+++ b/lib/nanoc/base/entities/processing_actions/snapshot.rb
@@ -0,0 +1,28 @@
+module Nanoc::Int::ProcessingActions
+ class Snapshot < Nanoc::Int::ProcessingAction
+ # snapshot :before_layout
+ # snapshot :before_layout, path: '/about.md'
+
+ attr_reader :snapshot_name
+ attr_reader :path
+
+ def initialize(snapshot_name, path)
+ @snapshot_name = snapshot_name
+ @path = path
+ end
+
+ def serialize
+ [:snapshot, @snapshot_name, true, @path]
+ end
+
+ NONE = Object.new
+
+ def copy(path: NONE)
+ self.class.new(@snapshot_name, path.equal?(NONE) ? @path : path)
+ end
+
+ def to_s
+ "snapshot #{@snapshot_name.inspect}, path: #{@path.inspect}"
+ end
+ end
+end
diff --git a/lib/nanoc/base/entities/props.rb b/lib/nanoc/base/entities/props.rb
new file mode 100644
index 0000000..e4e0845
--- /dev/null
+++ b/lib/nanoc/base/entities/props.rb
@@ -0,0 +1,76 @@
+module Nanoc::Int
+ # @api private
+ class Props
+ include Nanoc::Int::ContractsSupport
+
+ contract C::KeywordArgs[raw_content: C::Optional[C::Bool], attributes: C::Optional[C::Bool], compiled_content: C::Optional[C::Bool], path: C::Optional[C::Bool]] => C::Any
+ def initialize(raw_content: false, attributes: false, compiled_content: false, path: false)
+ @raw_content = raw_content
+ @attributes = attributes
+ @compiled_content = compiled_content
+ @path = path
+ end
+
+ contract C::None => String
+ def inspect
+ ''.tap do |s|
+ s << 'Props('
+ s << (raw_content? ? 'r' : '_')
+ s << (attributes? ? 'a' : '_')
+ s << (compiled_content? ? 'c' : '_')
+ s << (path? ? 'p' : '_')
+ s << ')'
+ end
+ end
+
+ contract C::None => C::Bool
+ def raw_content?
+ @raw_content
+ end
+
+ contract C::None => C::Bool
+ def attributes?
+ @attributes
+ end
+
+ contract C::None => C::Bool
+ def compiled_content?
+ @compiled_content
+ end
+
+ contract C::None => C::Bool
+ def path?
+ @path
+ end
+
+ contract Nanoc::Int::Props => Nanoc::Int::Props
+ def merge(other)
+ Props.new(
+ raw_content: raw_content? || other.raw_content?,
+ attributes: attributes? || other.attributes?,
+ compiled_content: compiled_content? || other.compiled_content?,
+ path: path? || other.path?,
+ )
+ end
+
+ contract C::None => Set
+ def active
+ Set.new.tap do |pr|
+ pr << :raw_content if raw_content?
+ pr << :attributes if attributes?
+ pr << :compiled_content if compiled_content?
+ pr << :path if path?
+ end
+ end
+
+ contract C::None => Hash
+ def to_h
+ {
+ raw_content: raw_content?,
+ attributes: attributes?,
+ compiled_content: compiled_content?,
+ path: path?,
+ }
+ end
+ end
+end
diff --git a/lib/nanoc/base/entities/rule_memory.rb b/lib/nanoc/base/entities/rule_memory.rb
index 74aa973..6618b06 100644
--- a/lib/nanoc/base/entities/rule_memory.rb
+++ b/lib/nanoc/base/entities/rule_memory.rb
@@ -3,9 +3,9 @@ module Nanoc::Int
include Nanoc::Int::ContractsSupport
include Enumerable
- def initialize(item_rep)
+ def initialize(item_rep, actions: [])
@item_rep = item_rep
- @actions = []
+ @actions = actions
end
contract C::None => Numeric
@@ -13,51 +13,66 @@ module Nanoc::Int
@actions.size
end
- contract Numeric => C::Maybe[Nanoc::Int::RuleMemoryAction]
+ contract Numeric => C::Maybe[Nanoc::Int::ProcessingAction]
def [](idx)
@actions[idx]
end
contract Symbol, Hash => self
def add_filter(filter_name, params)
- @actions << Nanoc::Int::RuleMemoryActions::Filter.new(filter_name, params)
+ @actions << Nanoc::Int::ProcessingActions::Filter.new(filter_name, params)
self
end
contract String, C::Maybe[Hash] => self
def add_layout(layout_identifier, params)
- @actions << Nanoc::Int::RuleMemoryActions::Layout.new(layout_identifier, params)
+ @actions << Nanoc::Int::ProcessingActions::Layout.new(layout_identifier, params)
self
end
- contract Symbol, C::Bool, C::Maybe[String] => self
- def add_snapshot(snapshot_name, final, path)
- will_add_snapshot(snapshot_name) if final
- @actions << Nanoc::Int::RuleMemoryActions::Snapshot.new(snapshot_name, final, path)
+ contract Symbol, C::Maybe[String] => self
+ def add_snapshot(snapshot_name, path)
+ will_add_snapshot(snapshot_name)
+ @actions << Nanoc::Int::ProcessingActions::Snapshot.new(snapshot_name, path)
self
end
- contract C::None => C::ArrayOf[Nanoc::Int::RuleMemoryAction]
+ contract C::None => C::ArrayOf[Nanoc::Int::ProcessingAction]
def snapshot_actions
- @actions.select { |a| a.is_a?(Nanoc::Int::RuleMemoryActions::Snapshot) }
+ @actions.select { |a| a.is_a?(Nanoc::Int::ProcessingActions::Snapshot) }
end
contract C::None => C::Bool
def any_layouts?
- @actions.any? { |a| a.is_a?(Nanoc::Int::RuleMemoryActions::Layout) }
+ @actions.any? { |a| a.is_a?(Nanoc::Int::ProcessingActions::Layout) }
+ end
+
+ contract C::None => Hash
+ def paths
+ snapshot_actions.each_with_object({}) do |action, paths|
+ paths[action.snapshot_name] = action.path
+ end
end
# TODO: Add contract
def serialize
- map(&:serialize)
+ to_a.map(&:serialize)
end
- contract C::Func[Nanoc::Int::RuleMemoryAction => C::Any] => self
+ contract C::Func[Nanoc::Int::ProcessingAction => C::Any] => self
def each
@actions.each { |a| yield(a) }
self
end
+ contract C::Func[Nanoc::Int::ProcessingAction => C::Any] => self
+ def map
+ self.class.new(
+ @item_rep,
+ actions: @actions.map { |a| yield(a) },
+ )
+ end
+
private
def will_add_snapshot(name)
diff --git a/lib/nanoc/base/entities/rule_memory_actions.rb b/lib/nanoc/base/entities/rule_memory_actions.rb
deleted file mode 100644
index 483ea36..0000000
--- a/lib/nanoc/base/entities/rule_memory_actions.rb
+++ /dev/null
@@ -1,3 +0,0 @@
-require_relative 'rule_memory_actions/filter'
-require_relative 'rule_memory_actions/layout'
-require_relative 'rule_memory_actions/snapshot'
diff --git a/lib/nanoc/base/entities/rule_memory_actions/snapshot.rb b/lib/nanoc/base/entities/rule_memory_actions/snapshot.rb
deleted file mode 100644
index 230707e..0000000
--- a/lib/nanoc/base/entities/rule_memory_actions/snapshot.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-module Nanoc::Int::RuleMemoryActions
- class Snapshot < Nanoc::Int::RuleMemoryAction
- # snapshot :before_layout
- # snapshot :before_layout, final: true
- # snapshot :before_layout, path: '/about.md'
-
- attr_reader :snapshot_name
- attr_reader :final
- attr_reader :path
- alias final? final
-
- def initialize(snapshot_name, final, path)
- @snapshot_name = snapshot_name
- @final = final
- @path = path
- end
-
- def serialize
- [:snapshot, @snapshot_name, @final, @path]
- end
-
- def to_s
- "snapshot #{@snapshot_name.inspect}, final: #{@final.inspect}, path: #{@path.inspect}"
- end
- end
-end
diff --git a/lib/nanoc/base/entities/site.rb b/lib/nanoc/base/entities/site.rb
index e93806c..1358aeb 100644
--- a/lib/nanoc/base/entities/site.rb
+++ b/lib/nanoc/base/entities/site.rb
@@ -5,11 +5,7 @@ module Nanoc::Int
attr_accessor :compiler
- contract C::KeywordArgs[config: Nanoc::Int::Configuration, code_snippets: C::RespondTo[:each], items: C::RespondTo[:each], layouts: C::RespondTo[:each]] => C::Any
- # @param [Nanoc::Int::Configuration] config
- # @param [Enumerable<Nanoc::Int::CodeSnippet>] code_snippets
- # @param [Enumerable<Nanoc::Int::Item>] items
- # @param [Enumerable<Nanoc::Int::Layout>] layouts
+ contract C::KeywordArgs[config: Nanoc::Int::Configuration, code_snippets: C::IterOf[Nanoc::Int::CodeSnippet], items: C::IterOf[Nanoc::Int::Item], layouts: C::IterOf[Nanoc::Int::Layout]] => C::Any
def initialize(config:, code_snippets:, items:, layouts:)
@config = config
@code_snippets = code_snippets
@@ -21,21 +17,12 @@ module Nanoc::Int
end
contract C::None => self
- # Compiles the site.
- #
- # @return [void]
- #
- # @since 3.2.0
def compile
compiler.run_all
self
end
- contract C::None => Nanoc::Int::Compiler
- # Returns the compiler for this site. Will create a new compiler if none
- # exists yet.
- #
- # @return [Nanoc::Int::Compiler] The compiler for this site
+ contract C::None => C::Named['Nanoc::Int::Compiler']
def compiler
@compiler ||= Nanoc::Int::CompilerLoader.new.load(self)
end
@@ -46,9 +33,6 @@ module Nanoc::Int
attr_reader :layouts
contract C::None => self
- # Prevents all further modifications to itself, its items, its layouts etc.
- #
- # @return [void]
def freeze
config.freeze
items.freeze
@@ -57,7 +41,7 @@ module Nanoc::Int
self
end
- contract C::RespondTo[:each], String => self
+ contract C::IterOf[C::Or[Nanoc::Int::Item, Nanoc::Int::Layout]], String => self
def ensure_identifier_uniqueness(objects, type)
seen = Set.new
objects.each do |obj|
diff --git a/lib/nanoc/base/entities/snapshot_def.rb b/lib/nanoc/base/entities/snapshot_def.rb
index 2af7177..9f05d71 100644
--- a/lib/nanoc/base/entities/snapshot_def.rb
+++ b/lib/nanoc/base/entities/snapshot_def.rb
@@ -5,15 +5,9 @@ module Nanoc
attr_reader :name
- contract Symbol, C::Bool => C::Any
- def initialize(name, is_final)
+ contract Symbol => C::Any
+ def initialize(name)
@name = name
- @is_final = is_final
- end
-
- contract C::None => C::Bool
- def final?
- @is_final
end
end
end
diff --git a/lib/nanoc/base/errors.rb b/lib/nanoc/base/errors.rb
index 88ceee2..d1002fa 100644
--- a/lib/nanoc/base/errors.rb
+++ b/lib/nanoc/base/errors.rb
@@ -10,6 +10,21 @@ module Nanoc::Int
class GenericTrivial < Generic
end
+ # Error that is raised when compilation of an item rep fails. The
+ # underlying error is available by calling `#unwrap`.
+ class CompilationError < Generic
+ attr_reader :item_rep
+
+ def initialize(wrapped, item_rep)
+ @wrapped = wrapped
+ @item_rep = item_rep
+ end
+
+ def unwrap
+ @wrapped
+ end
+ end
+
# Error that is raised when a site is loaded that uses a data source with
# an unknown identifier.
class UnknownDataSource < Generic
@@ -192,5 +207,14 @@ module Nanoc::Int
super("You cannot get the parent or children of an item that has a “full” identifier (#{identifier}). Getting the parent or children of an item is only possible for items that have a legacy identifier.")
end
end
+
+ class UndefinedFilterForLayout < Generic
+ def initialize(layout)
+ super("There is no filter defined for the layout #{layout.identifier}")
+ end
+ end
+
+ class InternalInconsistency < Generic
+ end
end
end
diff --git a/lib/nanoc/base/feature.rb b/lib/nanoc/base/feature.rb
index 6d63be7..b3bc0ed 100644
--- a/lib/nanoc/base/feature.rb
+++ b/lib/nanoc/base/feature.rb
@@ -1,19 +1,92 @@
module Nanoc
# @api private
+ #
+ # @example Defining a feature and checking its enabledness
+ #
+ # Nanoc::Feature.define('environments', version: '4.3')
+ # Nanoc::Feaure.enabled?(Nanoc::Feature::ENVIRONMENTS)
+ #
module Feature
- PROFILER = 'profiler'.freeze
+ # Defines a new feature with the given name, experimental in the given
+ # version. The feature will be made available as a constant with the same
+ # name, in uppercase, on the Nanoc::Feature module.
+ #
+ # @example Defining Nanoc::Feature::ENVIRONMENTS
+ #
+ # Nanoc::Feature.define('environments', version: '4.3')
+ #
+ # @param name The name of the feature
+ #
+ # @param version The minor version in which the feature is considered
+ # experimental.
+ #
+ # @return [void]
+ def self.define(name, version:)
+ repo[name] = version
+ const_set(name.upcase, name)
+ end
- def self.enabled_features
- @enabled_features ||= Set.new(ENV.fetch('NANOC_FEATURES', '').split(','))
+ # Undefines the feature with the given name. For testing purposes only.
+ #
+ # @param name The name of the feature
+ #
+ # @return [void]
+ #
+ # @private
+ def self.undefine(name)
+ repo.delete(name)
+ remove_const(name.upcase)
end
+ # @param [String] feature_name
+ #
+ # @return [Boolean] Whether or not the feature with the given name is enabled
def self.enabled?(feature_name)
enabled_features.include?(feature_name) ||
enabled_features.include?('all')
end
+ # @api private
+ def self.enable(feature_name)
+ raise ArgumentError, 'no block given' unless block_given?
+
+ if enabled?(feature_name)
+ yield
+ else
+ begin
+ enabled_features << feature_name
+ yield
+ ensure
+ enabled_features.delete(feature_name)
+ end
+ end
+ end
+
+ # @api private
def self.reset_caches
@enabled_features = nil
end
+
+ # @api private
+ def self.enabled_features
+ @enabled_features ||= Set.new(ENV.fetch('NANOC_FEATURES', '').split(','))
+ end
+
+ # @api private
+ def self.repo
+ @repo ||= {}
+ end
+
+ # @return [Enumerable<String>] Names of features that still exist, but
+ # should not be considered as experimental in the current version of
+ # Nanoc.
+ def self.all_outdated
+ repo.keys.reject do |name|
+ version = repo[name]
+ Nanoc::VERSION.start_with?(version)
+ end
+ end
end
end
+
+Nanoc::Feature.define('profiler', version: '4.4')
diff --git a/lib/nanoc/base/memoization.rb b/lib/nanoc/base/memoization.rb
index 156d616..d21edc5 100644
--- a/lib/nanoc/base/memoization.rb
+++ b/lib/nanoc/base/memoization.rb
@@ -4,8 +4,6 @@ module Nanoc::Int
# Adds support for memoizing functions.
#
# @api private
- #
- # @since 3.2.0
module Memoization
class Wrapper
attr_reader :ref
diff --git a/lib/nanoc/base/plugin_registry.rb b/lib/nanoc/base/plugin_registry.rb
index 84f18e3..2463bd4 100644
--- a/lib/nanoc/base/plugin_registry.rb
+++ b/lib/nanoc/base/plugin_registry.rb
@@ -157,7 +157,7 @@ module Nanoc::Int
res
end
- # @param [Class] klass
+ # @param [Class] subclass
#
# @return [Class]
#
diff --git a/lib/nanoc/base/repos.rb b/lib/nanoc/base/repos.rb
index 331579e..8d1b299 100644
--- a/lib/nanoc/base/repos.rb
+++ b/lib/nanoc/base/repos.rb
@@ -5,5 +5,6 @@ require_relative 'repos/compiled_content_cache'
require_relative 'repos/config_loader'
require_relative 'repos/data_source'
require_relative 'repos/dependency_store'
+require_relative 'repos/item_rep_repo'
require_relative 'repos/rule_memory_store'
require_relative 'repos/site_loader'
diff --git a/lib/nanoc/base/repos/checksum_store.rb b/lib/nanoc/base/repos/checksum_store.rb
index 5c590d2..123ffdc 100644
--- a/lib/nanoc/base/repos/checksum_store.rb
+++ b/lib/nanoc/base/repos/checksum_store.rb
@@ -4,32 +4,46 @@ module Nanoc::Int
#
# @api private
class ChecksumStore < ::Nanoc::Int::Store
- # @param [Nanoc::Int::Site] site
- def initialize(site: nil)
- super('tmp/checksums', 1)
+ include Nanoc::Int::ContractsSupport
- @site = site
+ attr_accessor :objects
+
+ c_obj = C::Or[Nanoc::Int::Item, Nanoc::Int::Layout, Nanoc::Int::Configuration, Nanoc::Int::CodeSnippet]
+
+ contract C::KeywordArgs[site: C::Maybe[Nanoc::Int::Site], objects: C::IterOf[c_obj]] => C::Any
+ def initialize(site: nil, objects:)
+ super(Nanoc::Int::Store.tmp_path_for(env_name: (site.config.env_name if site), store_name: 'checksums'), 1)
+
+ @objects = objects
@checksums = {}
end
- # Returns the old checksum for the given object. This makes sense for
- # items, layouts and code snippets.
- #
- # @param [#reference] obj The object for which to fetch the checksum
- #
- # @return [String] The checksum for the given object
+ contract c_obj => C::Maybe[String]
def [](obj)
@checksums[obj.reference]
end
- # Sets the checksum for the given object.
- #
- # @param [#reference] obj The object for which to set the checksum
- #
- # @param [String] checksum The checksum
- def []=(obj, checksum)
- @checksums[obj.reference] = checksum
+ contract c_obj => self
+ def add(obj)
+ if obj.is_a?(Nanoc::Int::Document)
+ @checksums[[obj.reference, :content]] = Nanoc::Int::Checksummer.calc_for_content_of(obj)
+ @checksums[[obj.reference, :attributes]] = Nanoc::Int::Checksummer.calc_for_attributes_of(obj)
+ end
+
+ @checksums[obj.reference] = Nanoc::Int::Checksummer.calc(obj)
+
+ self
+ end
+
+ contract c_obj => C::Maybe[String]
+ def content_checksum_for(obj)
+ @checksums[[obj.reference, :content]]
+ end
+
+ contract c_obj => C::Maybe[String]
+ def attributes_checksum_for(obj)
+ @checksums[[obj.reference, :attributes]]
end
protected
@@ -39,7 +53,14 @@ module Nanoc::Int
end
def data=(new_data)
- @checksums = new_data
+ references = Set.new(@objects.map(&:reference))
+
+ @checksums = {}
+ new_data.each_pair do |key, checksum|
+ if references.include?(key) || references.include?(key.first)
+ @checksums[key] = checksum
+ end
+ end
end
end
end
diff --git a/lib/nanoc/base/repos/compiled_content_cache.rb b/lib/nanoc/base/repos/compiled_content_cache.rb
index e753d09..1a35918 100644
--- a/lib/nanoc/base/repos/compiled_content_cache.rb
+++ b/lib/nanoc/base/repos/compiled_content_cache.rb
@@ -4,38 +4,35 @@ module Nanoc::Int
#
# @api private
class CompiledContentCache < ::Nanoc::Int::Store
- def initialize
- super('tmp/compiled_content', 2)
+ include Nanoc::Int::ContractsSupport
+ contract C::KeywordArgs[env_name: C::Maybe[String], items: C::IterOf[Nanoc::Int::Item]] => C::Any
+ def initialize(env_name: nil, items:)
+ super(Nanoc::Int::Store.tmp_path_for(env_name: env_name, store_name: 'compiled_content'), 2)
+
+ @items = items
@cache = {}
end
- # Returns the cached compiled content for the given item
- # representation. This cached compiled content is a hash where the keys
- # are the snapshot names and the values the compiled content at the
- # given snapshot.
- #
- # @param [Nanoc::Int::ItemRep] rep The item rep to fetch the content for
+ contract Nanoc::Int::ItemRep => C::Maybe[C::HashOf[Symbol => Nanoc::Int::Content]]
+ # Returns the cached compiled content for the given item representation.
#
- # @return [Hash<Symbol,String>] A hash containing the cached compiled
- # content for the given item representation
+ # This cached compiled content is a hash where the keys are the snapshot
+ # names. and the values the compiled content at the given snapshot.
def [](rep)
item_cache = @cache[rep.item.identifier] || {}
item_cache[rep.name]
end
+ contract Nanoc::Int::ItemRep, C::HashOf[Symbol => Nanoc::Int::Content] => self
# Sets the compiled content for the given representation.
#
- # @param [Nanoc::Int::ItemRep] rep The item representation for which to set
- # the compiled content
- #
- # @param [Hash<Symbol,String>] content A hash containing the compiled
- # content of the given representation
- #
- # @return [void]
+ # This cached compiled content is a hash where the keys are the snapshot
+ # names. and the values the compiled content at the given snapshot.
def []=(rep, content)
@cache[rep.item.identifier] ||= {}
@cache[rep.item.identifier][rep.name] = content
+ self
end
protected
@@ -45,7 +42,15 @@ module Nanoc::Int
end
def data=(new_data)
- @cache = new_data
+ @cache = {}
+
+ item_identifiers = Set.new(@items.map(&:identifier))
+
+ new_data.each_pair do |item_identifier, content_per_rep|
+ if item_identifiers.include?(item_identifier)
+ @cache[item_identifier] ||= content_per_rep
+ end
+ end
end
end
end
diff --git a/lib/nanoc/base/repos/config_loader.rb b/lib/nanoc/base/repos/config_loader.rb
index 8fa18de..5a68ef9 100644
--- a/lib/nanoc/base/repos/config_loader.rb
+++ b/lib/nanoc/base/repos/config_loader.rb
@@ -37,10 +37,14 @@ module Nanoc::Int
raise NoConfigFileFoundError if filename.nil?
# Read
- apply_parent_config(
- Nanoc::Int::Configuration.new(YAML.load_file(filename)),
- [filename],
- ).with_defaults
+ config =
+ apply_parent_config(
+ Nanoc::Int::Configuration.new(hash: YAML.load_file(filename)),
+ [filename],
+ ).with_defaults
+
+ # Load environment
+ config.with_environment
end
# @api private
@@ -60,7 +64,7 @@ module Nanoc::Int
end
# Load
- parent_config = Nanoc::Int::Configuration.new(YAML.load_file(parent_path))
+ parent_config = Nanoc::Int::Configuration.new(hash: YAML.load_file(parent_path))
full_parent_config = apply_parent_config(parent_config, processed_paths + [parent_path])
full_parent_config.merge(config.without(:parent_config_file))
end
diff --git a/lib/nanoc/base/repos/data_source.rb b/lib/nanoc/base/repos/data_source.rb
index b959f01..08c87d9 100644
--- a/lib/nanoc/base/repos/data_source.rb
+++ b/lib/nanoc/base/repos/data_source.rb
@@ -85,8 +85,7 @@ module Nanoc
# default implementation simply does nothing.
#
# @return [void]
- def up
- end
+ def up; end
# Brings down the connection to the data. This method should undo the
# effects of the {#up} method. For example, a database connection
@@ -96,8 +95,7 @@ module Nanoc
# default implementation simply does nothing.
#
# @return [void]
- def down
- end
+ def down; end
# Returns the collection of items (represented by {Nanoc::Int::Item}) in
# this site. The default implementation simply returns an empty array.
@@ -140,10 +138,14 @@ module Nanoc
#
# @param [Boolean] binary Whether or not this item is binary
#
- # @param [String, nil] checksum_data Used to determine whether the item has changed
- def new_item(content, attributes, identifier, binary: false, checksum_data: nil)
+ # @param [String, nil] checksum_data
+ #
+ # @param [String, nil] content_checksum_data
+ #
+ # @param [String, nil] attributes_checksum_data
+ def new_item(content, attributes, identifier, binary: false, checksum_data: nil, content_checksum_data: nil, attributes_checksum_data: nil)
content = Nanoc::Int::Content.create(content, binary: binary)
- Nanoc::Int::Item.new(content, attributes, identifier, checksum_data: checksum_data)
+ Nanoc::Int::Item.new(content, attributes, identifier, checksum_data: checksum_data, content_checksum_data: content_checksum_data, attributes_checksum_data: attributes_checksum_data)
end
# Creates a new in-memory layout instance. This is intended for use within
@@ -155,9 +157,13 @@ module Nanoc
#
# @param [String] identifier This layout's identifier.
#
- # @param [String, nil] checksum_data Used to determine whether the layout has changed
- def new_layout(raw_content, attributes, identifier, checksum_data: nil)
- Nanoc::Int::Layout.new(raw_content, attributes, identifier, checksum_data: checksum_data)
+ # @param [String, nil] checksum_data
+ #
+ # @param [String, nil] content_checksum_data
+ #
+ # @param [String, nil] attributes_checksum_data
+ def new_layout(raw_content, attributes, identifier, checksum_data: nil, content_checksum_data: nil, attributes_checksum_data: nil)
+ Nanoc::Int::Layout.new(raw_content, attributes, identifier, checksum_data: checksum_data, content_checksum_data: content_checksum_data, attributes_checksum_data: attributes_checksum_data)
end
end
end
diff --git a/lib/nanoc/base/repos/dependency_store.rb b/lib/nanoc/base/repos/dependency_store.rb
index 7114e79..62318e4 100644
--- a/lib/nanoc/base/repos/dependency_store.rb
+++ b/lib/nanoc/base/repos/dependency_store.rb
@@ -1,15 +1,36 @@
module Nanoc::Int
# @api private
class DependencyStore < ::Nanoc::Int::Store
+ include Nanoc::Int::ContractsSupport
+
# @return [Array<Nanoc::Int::Item, Nanoc::Int::Layout>]
attr_accessor :objects
# @param [Array<Nanoc::Int::Item, Nanoc::Int::Layout>] objects
- def initialize(objects)
- super('tmp/dependencies', 4)
+ def initialize(objects, env_name: nil)
+ super(Nanoc::Int::Store.tmp_path_for(env_name: env_name, store_name: 'dependencies'), 4)
@objects = objects
- @graph = Nanoc::Int::DirectedGraph.new([nil] + @objects)
+ @new_objects = []
+ @graph = Nanoc::Int::DirectedGraph.new([nil] + @objects)
+ end
+
+ contract C::Or[Nanoc::Int::Item, Nanoc::Int::ItemRep, Nanoc::Int::Layout] => C::ArrayOf[Nanoc::Int::Dependency]
+ def dependencies_causing_outdatedness_of(object)
+ objects_causing_outdatedness_of(object).map do |other_object|
+ props = props_for(other_object, object)
+
+ Nanoc::Int::Dependency.new(
+ other_object,
+ object,
+ Nanoc::Int::Props.new(
+ raw_content: props.fetch(:raw_content, false),
+ attributes: props.fetch(:attributes, false),
+ compiled_content: props.fetch(:compiled_content, false),
+ path: props.fetch(:path, false),
+ ),
+ )
+ end
end
# Returns the direct dependencies for the given object.
@@ -30,26 +51,14 @@ module Nanoc::Int
# predecessors of
# the given object
def objects_causing_outdatedness_of(object)
- @graph.direct_predecessors_of(object)
- end
-
- # Returns the direct inverse dependencies for the given object.
- #
- # The direct inverse dependencies of the given object include the objects
- # that will be marked as outdated when the given object is outdated.
- # Indirect dependencies will not be returned (e.g. if A depends on B which
- # depends on C, then the direct inverse dependencies of C do not include
- # A).
- #
- # @param [Nanoc::Int::Item, Nanoc::Int::Layout] object The object for which to
- # fetch the direct successors
- #
- # @return [Array<Nanoc::Int::Item, Nanoc::Int::Layout>] The direct successors of
- # the given object
- def objects_outdated_due_to(object)
- @graph.direct_successors_of(object).compact
+ if @new_objects.any?
+ [@new_objects.first]
+ else
+ @graph.direct_predecessors_of(object)
+ end
end
+ contract C::Maybe[C::Or[Nanoc::Int::Item, Nanoc::Int::Layout]], C::Maybe[C::Or[Nanoc::Int::Item, Nanoc::Int::Layout]], C::KeywordArgs[raw_content: C::Optional[C::Bool], attributes: C::Optional[C::Bool], compiled_content: C::Optional[C::Bool], path: C::Optional[C::Bool]] => C::Any
# Records a dependency from `src` to `dst` in the dependency graph. When
# `dst` is oudated, `src` will also become outdated.
#
@@ -61,9 +70,13 @@ module Nanoc::Int
# outdated if the destination is outdated
#
# @return [void]
- def record_dependency(src, dst)
+ def record_dependency(src, dst, raw_content: false, attributes: false, compiled_content: false, path: false)
+ existing_props = Nanoc::Int::Props.new(@graph.props_for(dst, src) || {})
+ new_props = Nanoc::Int::Props.new(raw_content: raw_content, attributes: attributes, compiled_content: compiled_content, path: path)
+ props = existing_props.merge(new_props)
+
# Warning! dst and src are *reversed* here!
- @graph.add_edge(dst, src) unless src == dst
+ @graph.add_edge(dst, src, props: props.to_h) unless src == dst
end
# Empties the list of dependencies for the given object. This is necessary
@@ -81,6 +94,16 @@ module Nanoc::Int
protected
+ def props_for(a, b)
+ props = @graph.props_for(a, b) || {}
+
+ if props.values.any? { |v| v }
+ props
+ else
+ { raw_content: true, attributes: true, compiled_content: true, path: true }
+ end
+ end
+
def data
{
edges: @graph.edges,
@@ -99,20 +122,14 @@ module Nanoc::Int
# Load edges
new_data[:edges].each do |edge|
- from_index, to_index = *edge
+ from_index, to_index, props = *edge
from = from_index && previous_objects[from_index]
to = to_index && previous_objects[to_index]
- @graph.add_edge(from, to)
+ @graph.add_edge(from, to, props: props)
end
# Record dependency from all items on new items
- new_objects = (@objects - previous_objects)
- new_objects.each do |new_obj|
- @objects.each do |obj|
- next unless obj.is_a?(Nanoc::Int::Item)
- @graph.add_edge(new_obj, obj)
- end
- end
+ @new_objects = @objects - previous_objects
end
end
end
diff --git a/lib/nanoc/base/compilation/item_rep_repo.rb b/lib/nanoc/base/repos/item_rep_repo.rb
similarity index 100%
rename from lib/nanoc/base/compilation/item_rep_repo.rb
rename to lib/nanoc/base/repos/item_rep_repo.rb
diff --git a/lib/nanoc/base/repos/rule_memory_store.rb b/lib/nanoc/base/repos/rule_memory_store.rb
index e44fc40..53f3025 100644
--- a/lib/nanoc/base/repos/rule_memory_store.rb
+++ b/lib/nanoc/base/repos/rule_memory_store.rb
@@ -4,8 +4,8 @@ module Nanoc::Int
#
# @api private
class RuleMemoryStore < ::Nanoc::Int::Store
- def initialize
- super('tmp/rule_memory', 1)
+ def initialize(env_name: nil)
+ super(Nanoc::Int::Store.tmp_path_for(env_name: env_name, store_name: 'rule_memory'), 1)
@rule_memories = {}
end
diff --git a/lib/nanoc/base/repos/site_loader.rb b/lib/nanoc/base/repos/site_loader.rb
index 5c0cb61..6772e96 100644
--- a/lib/nanoc/base/repos/site_loader.rb
+++ b/lib/nanoc/base/repos/site_loader.rb
@@ -5,7 +5,7 @@ module Nanoc::Int
end
def new_with_config(hash)
- site_from_config(Nanoc::Int::Configuration.new(hash).with_defaults)
+ site_from_config(Nanoc::Int::Configuration.new(hash: hash).with_defaults)
end
def new_from_cwd
diff --git a/lib/nanoc/base/repos/store.rb b/lib/nanoc/base/repos/store.rb
index 17f8c0f..c8035ba 100644
--- a/lib/nanoc/base/repos/store.rb
+++ b/lib/nanoc/base/repos/store.rb
@@ -12,6 +12,8 @@ module Nanoc::Int
#
# @api private
class Store
+ include Nanoc::Int::ContractsSupport
+
# @return [String] The name of the file where data will be loaded from and
# stored to.
attr_reader :filename
@@ -34,6 +36,13 @@ module Nanoc::Int
@version = version
end
+ # Logic for building tmp path from active environment and store name
+ # @api private
+ contract C::KeywordArgs[env_name: C::Maybe[String], store_name: String] => String
+ def self.tmp_path_for(env_name:, store_name:)
+ File.join('tmp', env_name.to_s, store_name)
+ end
+
# @group Loading and storing data
# @return The data that should be written to the disk
@@ -57,21 +66,11 @@ module Nanoc::Int
#
# @return [void]
def load
- # Check file existance
- unless File.file?(filename)
- no_data_found
- return
- end
+ return unless File.file?(filename)
begin
pstore.transaction do
- # Check version
- if pstore[:version] != version
- version_mismatch_detected
- return
- end
-
- # Load
+ return if pstore[:version] != version
self.data = pstore[:data]
end
rescue
@@ -93,24 +92,6 @@ module Nanoc::Int
end
end
- # @group Callback methods
-
- # Callback method that is called when no data file was found. By default,
- # this implementation does nothing, but it should probably be overridden
- # by the subclass.
- #
- # @return [void]
- def no_data_found
- end
-
- # Callback method that is called when a version mismatch is detected. By
- # default, this implementation does nothing, but it should probably be
- # overridden by the subclass.
- #
- # @return [void]
- def version_mismatch_detected
- end
-
private
def pstore
diff --git a/lib/nanoc/base/services.rb b/lib/nanoc/base/services.rb
index 6c3bb8a..05f865b 100644
--- a/lib/nanoc/base/services.rb
+++ b/lib/nanoc/base/services.rb
@@ -1,9 +1,18 @@
require_relative 'services/action_provider'
+require_relative 'services/checksummer'
+require_relative 'services/compiler'
require_relative 'services/compiler_loader'
+require_relative 'services/dependency_tracker'
require_relative 'services/executor'
+require_relative 'services/filter'
require_relative 'services/item_rep_builder'
require_relative 'services/item_rep_router'
require_relative 'services/item_rep_selector'
require_relative 'services/item_rep_writer'
require_relative 'services/notification_center'
+require_relative 'services/pruner'
require_relative 'services/temp_filename_factory'
+require_relative 'services/outdatedness_rule'
+require_relative 'services/outdatedness_rules'
+
+require_relative 'services/outdatedness_checker'
diff --git a/lib/nanoc/base/services/action_provider.rb b/lib/nanoc/base/services/action_provider.rb
index 6a127d2..92e1483 100644
--- a/lib/nanoc/base/services/action_provider.rb
+++ b/lib/nanoc/base/services/action_provider.rb
@@ -18,5 +18,9 @@ module Nanoc::Int
def snapshots_defs_for(_rep)
raise NotImplementedError
end
+
+ def paths_for(rep)
+ memory_for(rep).paths
+ end
end
end
diff --git a/lib/nanoc/base/checksummer.rb b/lib/nanoc/base/services/checksummer.rb
similarity index 71%
rename from lib/nanoc/base/checksummer.rb
rename to lib/nanoc/base/services/checksummer.rb
index adf7b09..ebc572a 100644
--- a/lib/nanoc/base/checksummer.rb
+++ b/lib/nanoc/base/services/checksummer.rb
@@ -44,6 +44,14 @@ module Nanoc::Int
digest.to_s
end
+ def calc_for_content_of(obj)
+ obj.content_checksum_data || obj.checksum_data || Nanoc::Int::Checksummer.calc(obj.content)
+ end
+
+ def calc_for_attributes_of(obj)
+ obj.attributes_checksum_data || obj.checksum_data || Nanoc::Int::Checksummer.calc(obj.attributes)
+ end
+
private
def update(obj, digest, visited = Hamster::Set.new)
@@ -72,6 +80,8 @@ module Nanoc::Int
HashUpdateBehavior
when Nanoc::Int::Item, Nanoc::Int::Layout
DocumentUpdateBehavior
+ when Nanoc::Int::ItemRep
+ ItemRepUpdateBehavior
when NilClass, TrueClass, FalseClass
NoUpdateBehavior
when Time
@@ -84,6 +94,10 @@ module Nanoc::Int
StringUpdateBehavior
when Nanoc::View
UnwrapUpdateBehavior
+ when Nanoc::RuleDSL::RuleContext
+ RuleContextUpdateBehavior
+ when Nanoc::Int::Context
+ ContextUpdateBehavior
else
RescueUpdateBehavior
end
@@ -96,6 +110,32 @@ module Nanoc::Int
end
end
+ class RuleContextUpdateBehavior < UpdateBehavior
+ def self.update(obj, digest)
+ digest.update('item=')
+ yield(obj.item)
+ digest.update(',rep=')
+ yield(obj.rep)
+ digest.update(',items=')
+ yield(obj.items)
+ digest.update(',layouts=')
+ yield(obj.layouts)
+ digest.update(',config=')
+ yield(obj.config)
+ end
+ end
+
+ class ContextUpdateBehavior < UpdateBehavior
+ def self.update(obj, digest)
+ obj.instance_variables.each do |var|
+ digest.update(var.to_s)
+ digest.update('=')
+ yield(obj.instance_variable_get(var))
+ digest.update(',')
+ end
+ end
+ end
+
class RawUpdateBehavior < UpdateBehavior
def self.update(obj, digest)
digest.update(obj.to_s)
@@ -127,8 +167,7 @@ module Nanoc::Int
end
class NoUpdateBehavior < UpdateBehavior
- def self.update(_obj, _digest)
- end
+ def self.update(_obj, _digest); end
end
class UnwrapUpdateBehavior < UpdateBehavior
@@ -162,11 +201,19 @@ module Nanoc::Int
if obj.checksum_data
digest.update('checksum_data=' + obj.checksum_data)
else
- digest.update('content=')
- yield(obj.content)
+ if obj.content_checksum_data
+ digest.update('content_checksum_data=' + obj.content_checksum_data)
+ else
+ digest.update('content=')
+ yield(obj.content)
+ end
- digest.update(',attributes=')
- yield(obj.attributes)
+ if obj.attributes_checksum_data
+ digest.update(',attributes_checksum_data=' + obj.attributes_checksum_data)
+ else
+ digest.update(',attributes=')
+ yield(obj.attributes)
+ end
digest.update(',identifier=')
yield(obj.identifier)
@@ -174,6 +221,15 @@ module Nanoc::Int
end
end
+ class ItemRepUpdateBehavior < UpdateBehavior
+ def self.update(obj, digest)
+ digest.update('item=')
+ yield(obj.item)
+ digest.update(',name=')
+ yield(obj.name)
+ end
+ end
+
class PathnameUpdateBehavior < UpdateBehavior
def self.update(obj, digest)
filename = obj.to_s
diff --git a/lib/nanoc/base/services/compiler.rb b/lib/nanoc/base/services/compiler.rb
new file mode 100644
index 0000000..c5df878
--- /dev/null
+++ b/lib/nanoc/base/services/compiler.rb
@@ -0,0 +1,377 @@
+module Nanoc::Int
+ # Responsible for compiling a site’s item representations.
+ #
+ # The compilation process makes use of notifications (see
+ # {Nanoc::Int::NotificationCenter}) to track dependencies between items,
+ # layouts, etc. The following notifications are used:
+ #
+ # * `compilation_started` — indicates that the compiler has started
+ # compiling this item representation. Has one argument: the item
+ # representation itself. Only one item can be compiled at a given moment;
+ # therefore, it is not possible to get two consecutive
+ # `compilation_started` notifications without also getting a
+ # `compilation_ended` notification in between them.
+ #
+ # * `compilation_ended` — indicates that the compiler has finished compiling
+ # this item representation (either successfully or with failure). Has one
+ # argument: the item representation itself.
+ #
+ # @api private
+ class Compiler
+ # Provides common functionality for accesing “context” of an item that is being compiled.
+ class CompilationContext
+ def initialize(action_provider:, reps:, site:, compiled_content_cache:)
+ @action_provider = action_provider
+ @reps = reps
+ @site = site
+ @compiled_content_cache = compiled_content_cache
+ end
+
+ def filter_name_and_args_for_layout(layout)
+ mem = @action_provider.memory_for(layout)
+ if mem.nil? || mem.size != 1 || !mem[0].is_a?(Nanoc::Int::ProcessingActions::Filter)
+ raise Nanoc::Int::Errors::UndefinedFilterForLayout.new(layout)
+ end
+ [mem[0].filter_name, mem[0].params]
+ end
+
+ def create_view_context(dependency_tracker)
+ Nanoc::ViewContext.new(
+ reps: @reps,
+ items: @site.items,
+ dependency_tracker: dependency_tracker,
+ compilation_context: self,
+ )
+ end
+
+ def assigns_for(rep, dependency_tracker)
+ content_or_filename_assigns =
+ if rep.binary?
+ { filename: rep.snapshot_contents[:last].filename }
+ else
+ { content: rep.snapshot_contents[:last].string }
+ end
+
+ view_context = create_view_context(dependency_tracker)
+
+ content_or_filename_assigns.merge(
+ item: Nanoc::ItemWithRepsView.new(rep.item, view_context),
+ rep: Nanoc::ItemRepView.new(rep, view_context),
+ item_rep: Nanoc::ItemRepView.new(rep, view_context),
+ items: Nanoc::ItemCollectionWithRepsView.new(@site.items, view_context),
+ layouts: Nanoc::LayoutCollectionView.new(@site.layouts, view_context),
+ config: Nanoc::ConfigView.new(@site.config, view_context),
+ )
+ end
+
+ def site
+ @site
+ end
+
+ def compiled_content_cache
+ @compiled_content_cache
+ end
+ end
+
+ # Provides functionality for (re)calculating the content of an item rep, without caching or
+ # outdatedness checking.
+ class RecalculatePhase
+ include Nanoc::Int::ContractsSupport
+
+ def initialize(action_provider:, dependency_store:, compilation_context:)
+ @action_provider = action_provider
+ @dependency_store = dependency_store
+ @compilation_context = compilation_context
+ end
+
+ contract Nanoc::Int::ItemRep, C::KeywordArgs[is_outdated: C::Bool] => C::Any
+ def run(rep, is_outdated:) # rubocop:disable Lint/UnusedMethodArgument
+ dependency_tracker = Nanoc::Int::DependencyTracker.new(@dependency_store)
+ dependency_tracker.enter(rep.item)
+
+ executor = Nanoc::Int::Executor.new(rep, @compilation_context, dependency_tracker)
+
+ @action_provider.memory_for(rep).each do |action|
+ case action
+ when Nanoc::Int::ProcessingActions::Filter
+ executor.filter(action.filter_name, action.params)
+ when Nanoc::Int::ProcessingActions::Layout
+ executor.layout(action.layout_identifier, action.params)
+ when Nanoc::Int::ProcessingActions::Snapshot
+ executor.snapshot(action.snapshot_name)
+ else
+ raise Nanoc::Int::Errors::InternalInconsistency, "unknown action #{action.inspect}"
+ end
+ end
+ ensure
+ dependency_tracker.exit
+ end
+ end
+
+ # Provides functionality for (re)calculating the content of an item rep, with caching or
+ # outdatedness checking. Delegates to RecalculatePhase if outdated or no cache available.
+ class CachePhase
+ include Nanoc::Int::ContractsSupport
+
+ def initialize(compiled_content_cache:, wrapped:)
+ @compiled_content_cache = compiled_content_cache
+ @wrapped = wrapped
+ end
+
+ contract Nanoc::Int::ItemRep, C::KeywordArgs[is_outdated: C::Bool] => C::Any
+ def run(rep, is_outdated:)
+ if can_reuse_content_for_rep?(rep, is_outdated: is_outdated)
+ Nanoc::Int::NotificationCenter.post(:cached_content_used, rep)
+ rep.snapshot_contents = @compiled_content_cache[rep]
+ else
+ @wrapped.run(rep, is_outdated: is_outdated)
+ end
+
+ rep.compiled = true
+ @compiled_content_cache[rep] = rep.snapshot_contents
+ end
+
+ contract Nanoc::Int::ItemRep, C::KeywordArgs[is_outdated: C::Bool] => C::Bool
+ def can_reuse_content_for_rep?(rep, is_outdated:)
+ !is_outdated && !@compiled_content_cache[rep].nil?
+ end
+ end
+
+ # Provides functionality for suspending and resuming item rep compilation (using fibers).
+ class ResumePhase
+ include Nanoc::Int::ContractsSupport
+
+ def initialize(wrapped:)
+ @wrapped = wrapped
+ end
+
+ contract Nanoc::Int::ItemRep, C::KeywordArgs[is_outdated: C::Bool] => C::Any
+ def run(rep, is_outdated:)
+ fiber = fiber_for(rep, is_outdated: is_outdated)
+ while fiber.alive?
+ Nanoc::Int::NotificationCenter.post(:compilation_started, rep)
+ res = fiber.resume
+
+ case res
+ when Nanoc::Int::Errors::UnmetDependency
+ Nanoc::Int::NotificationCenter.post(:compilation_suspended, rep, res)
+ raise(res)
+ when Proc
+ fiber.resume(res.call)
+ else
+ # TODO: raise
+ end
+ end
+
+ Nanoc::Int::NotificationCenter.post(:compilation_ended, rep)
+ end
+
+ private
+
+ contract Nanoc::Int::ItemRep, C::KeywordArgs[is_outdated: C::Bool] => Fiber
+ def fiber_for(rep, is_outdated:)
+ @fibers ||= {}
+
+ @fibers[rep] ||=
+ Fiber.new do
+ @wrapped.run(rep, is_outdated: is_outdated)
+ @fibers.delete(rep)
+ end
+
+ @fibers[rep]
+ end
+ end
+
+ class WritePhase
+ include Nanoc::Int::ContractsSupport
+
+ def initialize(wrapped:)
+ @wrapped = wrapped
+ end
+
+ contract Nanoc::Int::ItemRep, C::KeywordArgs[is_outdated: C::Bool] => C::Any
+ def run(rep, is_outdated:)
+ @wrapped.run(rep, is_outdated: is_outdated)
+
+ rep.snapshot_defs.each do |sdef|
+ ItemRepWriter.new.write(rep, sdef.name)
+ end
+ end
+ end
+
+ include Nanoc::Int::ContractsSupport
+
+ # @api private
+ attr_reader :site
+
+ # @api private
+ attr_reader :compiled_content_cache
+
+ # @api private
+ attr_reader :checksum_store
+
+ # @api private
+ attr_reader :rule_memory_store
+
+ # @api private
+ attr_reader :action_provider
+
+ # @api private
+ attr_reader :dependency_store
+
+ # @api private
+ attr_reader :outdatedness_checker
+
+ # @api private
+ attr_reader :reps
+
+ def initialize(site, compiled_content_cache:, checksum_store:, rule_memory_store:, action_provider:, dependency_store:, outdatedness_checker:, reps:)
+ @site = site
+
+ @compiled_content_cache = compiled_content_cache
+ @checksum_store = checksum_store
+ @rule_memory_store = rule_memory_store
+ @dependency_store = dependency_store
+ @outdatedness_checker = outdatedness_checker
+ @reps = reps
+ @action_provider = action_provider
+ end
+
+ def run_all
+ @action_provider.preprocess(@site)
+ build_reps
+ prune
+ run
+ @action_provider.postprocess(@site, @reps)
+ end
+
+ def run
+ load_stores
+ @site.freeze
+
+ compile_reps
+ store
+ ensure
+ Nanoc::Int::TempFilenameFactory.instance.cleanup(
+ Nanoc::Filter::TMP_BINARY_ITEMS_DIR,
+ )
+ Nanoc::Int::TempFilenameFactory.instance.cleanup(
+ Nanoc::Int::ItemRepWriter::TMP_TEXT_ITEMS_DIR,
+ )
+ end
+
+ def load_stores
+ # FIXME: icky hack to update the dependency/checksum store’s list of objects
+ # (does not include preprocessed objects otherwise)
+ dependency_store.objects = site.items.to_a + site.layouts.to_a
+ checksum_store.objects = site.items.to_a + site.layouts.to_a + site.code_snippets + [site.config]
+
+ stores.each(&:load)
+ end
+
+ # Store the modified helper data used for compiling the site.
+ #
+ # @return [void]
+ def store
+ # Calculate rule memory
+ (@reps.to_a + @site.layouts.to_a).each do |obj|
+ rule_memory_store[obj] = action_provider.memory_for(obj).serialize
+ end
+
+ # Calculate checksums
+ objects_to_checksum =
+ site.items.to_a + site.layouts.to_a + site.code_snippets + [site.config]
+ objects_to_checksum.each { |obj| checksum_store.add(obj) }
+
+ # Store
+ stores.each(&:store)
+ end
+
+ def build_reps
+ builder = Nanoc::Int::ItemRepBuilder.new(
+ site, action_provider, @reps
+ )
+ builder.run
+ end
+
+ def compilation_context
+ @_compilation_context ||= CompilationContext.new(
+ action_provider: action_provider,
+ reps: @reps,
+ site: @site,
+ compiled_content_cache: compiled_content_cache,
+ )
+ end
+
+ private
+
+ def prune
+ if site.config[:prune][:auto_prune]
+ Nanoc::Pruner.new(site.config, reps, exclude: prune_config_exclude).run
+ end
+ end
+
+ def prune_config
+ site.config[:prune] || {}
+ end
+
+ def prune_config_exclude
+ prune_config[:exclude] || {}
+ end
+
+ def compile_reps
+ outdated_items = @reps.select { |r| outdatedness_checker.outdated?(r) }.map(&:item).uniq
+ outdated_items.each { |i| @dependency_store.forget_dependencies_for(i) }
+
+ reps_to_recompile = Set.new(outdated_items.flat_map { |i| @reps[i] })
+ selector = Nanoc::Int::ItemRepSelector.new(reps_to_recompile)
+ selector.each do |rep|
+ handle_errors_while(rep) { compile_rep(rep, is_outdated: reps_to_recompile.include?(rep)) }
+ end
+ end
+
+ def handle_errors_while(item_rep)
+ yield
+ rescue => e
+ raise Nanoc::Int::Errors::CompilationError.new(e, item_rep)
+ end
+
+ def compile_rep(rep, is_outdated:)
+ item_rep_compiler.run(rep, is_outdated: is_outdated)
+ end
+
+ def item_rep_compiler
+ @_item_rep_compiler ||= begin
+ recalculate_phase = RecalculatePhase.new(
+ action_provider: action_provider,
+ dependency_store: @dependency_store,
+ compilation_context: compilation_context,
+ )
+
+ cache_phase = CachePhase.new(
+ compiled_content_cache: compiled_content_cache,
+ wrapped: recalculate_phase,
+ )
+
+ resume_phase = ResumePhase.new(
+ wrapped: cache_phase,
+ )
+
+ WritePhase.new(
+ wrapped: resume_phase,
+ )
+ end
+ end
+
+ # Returns all stores that can load/store data that can be used for
+ # compilation.
+ def stores
+ [
+ checksum_store,
+ compiled_content_cache,
+ @dependency_store,
+ rule_memory_store,
+ ]
+ end
+ end
+end
diff --git a/lib/nanoc/base/services/compiler_loader.rb b/lib/nanoc/base/services/compiler_loader.rb
index 9fa809b..77fde3a 100644
--- a/lib/nanoc/base/services/compiler_loader.rb
+++ b/lib/nanoc/base/services/compiler_loader.rb
@@ -1,18 +1,20 @@
module Nanoc::Int
# @api private
class CompilerLoader
- def load(site)
- rule_memory_store = Nanoc::Int::RuleMemoryStore.new
+ def load(site, action_provider: nil)
+ rule_memory_store = Nanoc::Int::RuleMemoryStore.new(env_name: site.config.env_name)
dependency_store =
- Nanoc::Int::DependencyStore.new(site.items.to_a + site.layouts.to_a)
+ Nanoc::Int::DependencyStore.new(site.items.to_a + site.layouts.to_a, env_name: site.config.env_name)
+
+ objects = site.items.to_a + site.layouts.to_a + site.code_snippets + [site.config]
checksum_store =
- Nanoc::Int::ChecksumStore.new(site: site)
+ Nanoc::Int::ChecksumStore.new(site: site, objects: objects)
item_rep_repo = Nanoc::Int::ItemRepRepo.new
- action_provider = Nanoc::Int::ActionProvider.named(:rule_dsl).for(site)
+ action_provider ||= Nanoc::Int::ActionProvider.named(:rule_dsl).for(site)
outdatedness_checker =
Nanoc::Int::OutdatednessChecker.new(
@@ -24,8 +26,14 @@ module Nanoc::Int
reps: item_rep_repo,
)
+ compiled_content_cache =
+ Nanoc::Int::CompiledContentCache.new(
+ env_name: site.config.env_name,
+ items: site.items,
+ )
+
params = {
- compiled_content_cache: Nanoc::Int::CompiledContentCache.new,
+ compiled_content_cache: compiled_content_cache,
checksum_store: checksum_store,
rule_memory_store: rule_memory_store,
dependency_store: dependency_store,
diff --git a/lib/nanoc/base/services/dependency_tracker.rb b/lib/nanoc/base/services/dependency_tracker.rb
new file mode 100644
index 0000000..750c042
--- /dev/null
+++ b/lib/nanoc/base/services/dependency_tracker.rb
@@ -0,0 +1,55 @@
+module Nanoc::Int
+ # @api private
+ class DependencyTracker
+ include Nanoc::Int::ContractsSupport
+
+ C_OBJ = C::Or[Nanoc::Int::Item, Nanoc::Int::Layout]
+ C_ARGS = C::KeywordArgs[raw_content: C::Optional[C::Bool], attributes: C::Optional[C::Bool], compiled_content: C::Optional[C::Bool], path: C::Optional[C::Bool]]
+
+ class Null
+ include Nanoc::Int::ContractsSupport
+
+ contract C_OBJ, C_ARGS => C::Any
+ def enter(_obj, raw_content: false, attributes: false, compiled_content: false, path: false); end
+
+ contract C_OBJ => C::Any
+ def exit; end
+
+ contract C_OBJ, C_ARGS => C::Any
+ def bounce(_obj, raw_content: false, attributes: false, compiled_content: false, path: false); end
+ end
+
+ def initialize(dependency_store)
+ @dependency_store = dependency_store
+ @stack = []
+ end
+
+ contract C_OBJ, C_ARGS => C::Any
+ def enter(obj, raw_content: false, attributes: false, compiled_content: false, path: false)
+ unless @stack.empty?
+ Nanoc::Int::NotificationCenter.post(:dependency_created, @stack.last, obj)
+ @dependency_store.record_dependency(
+ @stack.last,
+ obj,
+ raw_content: raw_content,
+ attributes: attributes,
+ compiled_content: compiled_content,
+ path: path,
+ )
+ end
+
+ @stack.push(obj)
+ end
+
+ contract C_OBJ => C::Any
+ def exit
+ @stack.pop
+ end
+
+ contract C_OBJ, C_ARGS => C::Any
+ def bounce(obj, raw_content: false, attributes: false, compiled_content: false, path: false)
+ enter(obj, raw_content: raw_content, attributes: attributes, compiled_content: compiled_content, path: path)
+ exit
+ end
+ end
+end
diff --git a/lib/nanoc/base/services/executor.rb b/lib/nanoc/base/services/executor.rb
index 98740d1..bdb00ab 100644
--- a/lib/nanoc/base/services/executor.rb
+++ b/lib/nanoc/base/services/executor.rb
@@ -7,23 +7,24 @@ module Nanoc
end
end
- def initialize(compiler, dependency_tracker)
- @compiler = compiler
+ def initialize(rep, compilation_context, dependency_tracker)
+ @rep = rep
+ @compilation_context = compilation_context
@dependency_tracker = dependency_tracker
end
- def filter(rep, filter_name, filter_args = {})
- filter = filter_for_filtering(rep, filter_name)
+ def filter(filter_name, filter_args = {})
+ filter = filter_for_filtering(@rep, filter_name)
begin
- Nanoc::Int::NotificationCenter.post(:filtering_started, rep, filter_name)
+ Nanoc::Int::NotificationCenter.post(:filtering_started, @rep, filter_name)
# Run filter
- last = rep.snapshot_contents[:last]
- source = rep.binary? ? last.filename : last.string
+ last = @rep.snapshot_contents[:last]
+ source = @rep.binary? ? last.filename : last.string
filter_args.freeze
result = filter.setup_and_run(source, filter_args)
- rep.snapshot_contents[:last] =
+ @rep.snapshot_contents[:last] =
if filter.class.to_binary?
Nanoc::Int::BinaryContent.new(filter.output_filename).tap(&:freeze)
else
@@ -34,17 +35,14 @@ module Nanoc
if filter.class.to_binary? && !File.file?(filter.output_filename)
raise OutputNotWrittenError.new(filter_name, filter.output_filename)
end
-
- # Create snapshot
- snapshot(rep, rep.snapshot_contents[:post] ? :post : :pre, final: false) unless rep.binary?
ensure
- Nanoc::Int::NotificationCenter.post(:filtering_ended, rep, filter_name)
+ Nanoc::Int::NotificationCenter.post(:filtering_ended, @rep, filter_name)
end
end
- def layout(rep, layout_identifier, extra_filter_args = nil)
+ def layout(layout_identifier, extra_filter_args = nil)
layout = find_layout(layout_identifier)
- filter_name, filter_args = *@compiler.filter_name_and_args_for_layout(layout)
+ filter_name, filter_args = *@compilation_context.filter_name_and_args_for_layout(layout)
if filter_name.nil?
raise Nanoc::Int::Errors::Generic, "Cannot find rule for layout matching #{layout_identifier}"
end
@@ -52,68 +50,41 @@ module Nanoc
filter_args.freeze
# Check whether item can be laid out
- raise Nanoc::Int::Errors::CannotLayoutBinaryItem.new(rep) if rep.binary?
-
- # Create "pre" snapshot
- if rep.snapshot_contents[:post].nil?
- snapshot(rep, :pre, final: true)
- end
+ raise Nanoc::Int::Errors::CannotLayoutBinaryItem.new(@rep) if @rep.binary?
# Create filter
klass = Nanoc::Filter.named(filter_name)
raise Nanoc::Int::Errors::UnknownFilter.new(filter_name) if klass.nil?
- view_context = @compiler.create_view_context(@dependency_tracker)
+ view_context = @compilation_context.create_view_context(@dependency_tracker)
layout_view = Nanoc::LayoutView.new(layout, view_context)
- filter = klass.new(assigns_for(rep).merge({ layout: layout_view }))
+ filter = klass.new(assigns_for(@rep).merge(layout: layout_view))
# Visit
- @dependency_tracker.bounce(layout)
+ @dependency_tracker.bounce(layout, raw_content: true)
begin
- # Notify start
- Nanoc::Int::NotificationCenter.post(:processing_started, layout)
- Nanoc::Int::NotificationCenter.post(:filtering_started, rep, filter_name)
+ Nanoc::Int::NotificationCenter.post(:filtering_started, @rep, filter_name)
# Layout
content = layout.content
arg = content.binary? ? content.filename : content.string
res = filter.setup_and_run(arg, filter_args)
- rep.snapshot_contents[:last] = Nanoc::Int::TextualContent.new(res).tap(&:freeze)
-
- # Create "post" snapshot
- snapshot(rep, :post, final: false)
+ @rep.snapshot_contents[:last] = Nanoc::Int::TextualContent.new(res).tap(&:freeze)
ensure
- # Notify end
- Nanoc::Int::NotificationCenter.post(:filtering_ended, rep, filter_name)
- Nanoc::Int::NotificationCenter.post(:processing_ended, layout)
+ Nanoc::Int::NotificationCenter.post(:filtering_ended, @rep, filter_name)
end
end
- def snapshot(rep, snapshot_name, final: true, path: nil) # rubocop:disable Lint/UnusedMethodArgument
- # NOTE: :path is irrelevant
-
- unless rep.binary?
- rep.snapshot_contents[snapshot_name] = rep.snapshot_contents[:last]
- end
-
- if snapshot_name == :pre && final
- rep.snapshot_defs << Nanoc::Int::SnapshotDef.new(:pre, true)
- end
-
- if final
- raw_path = rep.raw_path(snapshot: snapshot_name)
- if raw_path
- ItemRepWriter.new.write(rep, raw_path)
- end
- end
+ def snapshot(snapshot_name)
+ @rep.snapshot_contents[snapshot_name] = @rep.snapshot_contents[:last]
end
def assigns_for(rep)
- @compiler.assigns_for(rep, @dependency_tracker)
+ @compilation_context.assigns_for(rep, @dependency_tracker)
end
def layouts
- @compiler.site.layouts
+ @compilation_context.site.layouts
end
def find_layout(arg)
@@ -144,7 +115,7 @@ module Nanoc
end
def use_globs?
- @compiler.site.config[:string_pattern_type] == 'glob'
+ @compilation_context.site.config[:string_pattern_type] == 'glob'
end
end
end
diff --git a/lib/nanoc/base/compilation/filter.rb b/lib/nanoc/base/services/filter.rb
similarity index 96%
rename from lib/nanoc/base/compilation/filter.rb
rename to lib/nanoc/base/services/filter.rb
index 11d5aac..633b011 100644
--- a/lib/nanoc/base/compilation/filter.rb
+++ b/lib/nanoc/base/services/filter.rb
@@ -129,7 +129,7 @@ module Nanoc
# Sets up the filter and runs the filter. This method passes its arguments
# to {#run} unchanged and returns the return value from {#run}.
#
- # @see {#run}
+ # @see #run
#
# @api private
def setup_and_run(*args)
@@ -184,6 +184,11 @@ module Nanoc
end
end
+ # @api private
+ def on_main_fiber(&block)
+ Fiber.yield(block)
+ end
+
# Creates a dependency from the item that is currently being filtered onto
# the given collection of items. In other words, require the given items
# to be compiled first before this items is processed.
@@ -195,12 +200,12 @@ module Nanoc
# Notify
dependency_tracker = @assigns[:item]._context.dependency_tracker
- items.each { |item| dependency_tracker.bounce(item) }
+ items.each { |item| dependency_tracker.bounce(item, compiled_content: true) }
# Raise unmet dependency error if necessary
items.each do |item|
rep = orig_items.sample._context.reps[item].find { |r| !r.compiled? }
- raise Nanoc::Int::Errors::UnmetDependency.new(rep) if rep
+ Fiber.yield(Nanoc::Int::Errors::UnmetDependency.new(rep)) if rep
end
end
end
diff --git a/lib/nanoc/base/services/item_rep_builder.rb b/lib/nanoc/base/services/item_rep_builder.rb
index 6a2816d..2b8ecdd 100644
--- a/lib/nanoc/base/services/item_rep_builder.rb
+++ b/lib/nanoc/base/services/item_rep_builder.rb
@@ -17,6 +17,10 @@ module Nanoc::Int
end
Nanoc::Int::ItemRepRouter.new(@reps, @action_provider, @site).run
+
+ @reps.each do |rep|
+ rep.snapshot_defs = @action_provider.snapshots_defs_for(rep)
+ end
end
end
end
diff --git a/lib/nanoc/base/services/item_rep_router.rb b/lib/nanoc/base/services/item_rep_router.rb
index 853850f..e5413c2 100644
--- a/lib/nanoc/base/services/item_rep_router.rb
+++ b/lib/nanoc/base/services/item_rep_router.rb
@@ -9,6 +9,12 @@ module Nanoc::Int
end
end
+ class RouteWithoutSlashError < ::Nanoc::Error
+ def initialize(output_path, rep)
+ super("The item representation #{rep.inspect} is routed to #{output_path}, which does not start with a slash, as required.")
+ end
+ end
+
def initialize(reps, action_provider, site)
@reps = reps
@action_provider = action_provider
@@ -18,18 +24,21 @@ module Nanoc::Int
def run
paths_to_reps = {}
@reps.each do |rep|
- mem = @action_provider.memory_for(rep)
- mem.snapshot_actions.each do |snapshot_action|
- route_rep(rep, snapshot_action, paths_to_reps)
+ @action_provider.paths_for(rep).each do |snapshot_name, path|
+ route_rep(rep, path, snapshot_name, paths_to_reps)
end
end
end
- def route_rep(rep, snapshot_action, paths_to_reps)
- basic_path = snapshot_action.path
+ def route_rep(rep, path, snapshot_name, paths_to_reps)
+ basic_path = path
return if basic_path.nil?
basic_path = basic_path.encode('UTF-8')
+ unless basic_path.start_with?('/')
+ raise RouteWithoutSlashError.new(basic_path, rep)
+ end
+
# Check for duplicate paths
if paths_to_reps.key?(basic_path)
raise IdenticalRoutesError.new(basic_path, paths_to_reps[basic_path], rep)
@@ -37,8 +46,8 @@ module Nanoc::Int
paths_to_reps[basic_path] = rep
end
- rep.raw_paths[snapshot_action.snapshot_name] = @site.config[:output_dir] + basic_path
- rep.paths[snapshot_action.snapshot_name] = strip_index_filename(basic_path)
+ rep.raw_paths[snapshot_name] = @site.config[:output_dir] + basic_path
+ rep.paths[snapshot_name] = strip_index_filename(basic_path)
end
def strip_index_filename(basic_path)
diff --git a/lib/nanoc/base/services/item_rep_selector.rb b/lib/nanoc/base/services/item_rep_selector.rb
index ee6f56c..2778f76 100644
--- a/lib/nanoc/base/services/item_rep_selector.rb
+++ b/lib/nanoc/base/services/item_rep_selector.rb
@@ -17,8 +17,8 @@ module Nanoc::Int
begin
yield(rep)
graph.delete_vertex(rep)
- rescue Nanoc::Int::Errors::UnmetDependency => e
- handle_dependency_error(e, rep, graph)
+ rescue => e
+ handle_error(e, rep, graph)
end
end
@@ -28,6 +28,21 @@ module Nanoc::Int
end
end
+ def handle_error(e, rep, graph)
+ actual_error =
+ if e.is_a?(Nanoc::Int::Errors::CompilationError)
+ e.unwrap
+ else
+ e
+ end
+
+ if actual_error.is_a?(Nanoc::Int::Errors::UnmetDependency)
+ handle_dependency_error(actual_error, rep, graph)
+ else
+ raise(e)
+ end
+ end
+
def handle_dependency_error(e, rep, graph)
other_rep = e.rep
graph.add_edge(other_rep, rep)
diff --git a/lib/nanoc/base/services/item_rep_writer.rb b/lib/nanoc/base/services/item_rep_writer.rb
index f6b6de6..fe60655 100644
--- a/lib/nanoc/base/services/item_rep_writer.rb
+++ b/lib/nanoc/base/services/item_rep_writer.rb
@@ -3,7 +3,10 @@ module Nanoc::Int
class ItemRepWriter
TMP_TEXT_ITEMS_DIR = 'text_items'.freeze
- def write(item_rep, raw_path)
+ def write(item_rep, snapshot_name)
+ raw_path = item_rep.raw_path(snapshot: snapshot_name)
+ return unless raw_path
+
# Create parent directory
FileUtils.mkdir_p(File.dirname(raw_path))
@@ -15,7 +18,7 @@ module Nanoc::Int
:will_write_rep, item_rep, raw_path
)
- content = item_rep.snapshot_contents[:last]
+ content = item_rep.snapshot_contents[snapshot_name]
if content.binary?
temp_path = content.filename
else
diff --git a/lib/nanoc/base/services/outdatedness_checker.rb b/lib/nanoc/base/services/outdatedness_checker.rb
new file mode 100644
index 0000000..eb8ad19
--- /dev/null
+++ b/lib/nanoc/base/services/outdatedness_checker.rb
@@ -0,0 +1,181 @@
+module Nanoc::Int
+ # Responsible for determining whether an item or a layout is outdated.
+ #
+ # @api private
+ class OutdatednessChecker
+ class Basic
+ extend Nanoc::Int::Memoization
+
+ include Nanoc::Int::ContractsSupport
+
+ Rules = Nanoc::Int::OutdatednessRules
+
+ RULES_FOR_ITEM_REP =
+ [
+ Rules::RulesModified,
+ Rules::PathsModified,
+ Rules::ContentModified,
+ Rules::AttributesModified,
+ Rules::NotWritten,
+ Rules::CodeSnippetsModified,
+ Rules::ConfigurationModified,
+ ].freeze
+
+ RULES_FOR_LAYOUT =
+ [
+ Rules::RulesModified,
+ Rules::ContentModified,
+ Rules::AttributesModified,
+ ].freeze
+
+ contract C::KeywordArgs[outdatedness_checker: OutdatednessChecker, reps: Nanoc::Int::ItemRepRepo] => C::Any
+ def initialize(outdatedness_checker:, reps:)
+ @outdatedness_checker = outdatedness_checker
+ @reps = reps
+ end
+
+ contract C::Or[Nanoc::Int::Item, Nanoc::Int::ItemRep, Nanoc::Int::Layout] => C::Maybe[OutdatednessStatus]
+ def outdatedness_status_for(obj)
+ case obj
+ when Nanoc::Int::ItemRep
+ apply_rules(RULES_FOR_ITEM_REP, obj)
+ when Nanoc::Int::Item
+ apply_rules_multi(RULES_FOR_ITEM_REP, @reps[obj])
+ when Nanoc::Int::Layout
+ apply_rules(RULES_FOR_LAYOUT, obj)
+ else
+ raise Nanoc::Int::Errors::InternalInconsistency, "do not know how to check outdatedness of #{obj.inspect}"
+ end
+ end
+ memoize :outdatedness_status_for
+
+ private
+
+ contract C::ArrayOf[Class], C::Or[Nanoc::Int::Item, Nanoc::Int::ItemRep, Nanoc::Int::Layout], OutdatednessStatus => C::Maybe[OutdatednessStatus]
+ def apply_rules(rules, obj, status = OutdatednessStatus.new)
+ rules.inject(status) do |acc, rule|
+ if !acc.useful_to_apply?(rule)
+ acc
+ elsif rule.instance.apply(obj, @outdatedness_checker)
+ acc.update(rule.instance.reason)
+ else
+ acc
+ end
+ end
+ end
+
+ contract C::ArrayOf[Class], C::ArrayOf[C::Or[Nanoc::Int::Item, Nanoc::Int::ItemRep, Nanoc::Int::Layout]] => C::Maybe[OutdatednessStatus]
+ def apply_rules_multi(rules, objs)
+ objs.inject(OutdatednessStatus.new) { |acc, elem| apply_rules(rules, elem, acc) }
+ end
+ end
+
+ extend Nanoc::Int::Memoization
+
+ include Nanoc::Int::ContractsSupport
+
+ attr_reader :checksum_store
+ attr_reader :dependency_store
+ attr_reader :rule_memory_store
+ attr_reader :action_provider
+ attr_reader :site
+
+ Reasons = Nanoc::Int::OutdatednessReasons
+
+ # @param [Nanoc::Int::Site] site
+ # @param [Nanoc::Int::ChecksumStore] checksum_store
+ # @param [Nanoc::Int::DependencyStore] dependency_store
+ # @param [Nanoc::Int::RuleMemoryStore] rule_memory_store
+ # @param [Nanoc::Int::ActionProvider] action_provider
+ # @param [Nanoc::Int::ItemRepRepo] reps
+ def initialize(site:, checksum_store:, dependency_store:, rule_memory_store:, action_provider:, reps:)
+ @site = site
+ @checksum_store = checksum_store
+ @dependency_store = dependency_store
+ @rule_memory_store = rule_memory_store
+ @action_provider = action_provider
+ @reps = reps
+
+ @objects_outdated_due_to_dependencies = {}
+ end
+
+ contract C::Or[Nanoc::Int::Item, Nanoc::Int::ItemRep, Nanoc::Int::Layout] => C::Bool
+ # Checks whether the given object is outdated and therefore needs to be
+ # recompiled.
+ #
+ # @param [Nanoc::Int::Item, Nanoc::Int::ItemRep, Nanoc::Int::Layout] obj The object
+ # whose outdatedness should be checked.
+ #
+ # @return [Boolean] true if the object is outdated, false otherwise
+ def outdated?(obj)
+ !outdatedness_reason_for(obj).nil?
+ end
+
+ contract C::Or[Nanoc::Int::Item, Nanoc::Int::ItemRep, Nanoc::Int::Layout] => C::Maybe[Reasons::Generic]
+ # Calculates the reason why the given object is outdated.
+ #
+ # @param [Nanoc::Int::Item, Nanoc::Int::ItemRep, Nanoc::Int::Layout] obj The object
+ # whose outdatedness reason should be calculated.
+ #
+ # @return [Reasons::Generic, nil] The reason why the
+ # given object is outdated, or nil if the object is not outdated.
+ def outdatedness_reason_for(obj)
+ reason = basic_outdatedness_reason_for(obj)
+ if reason.nil? && outdated_due_to_dependencies?(obj)
+ reason = Reasons::DependenciesOutdated
+ end
+ reason
+ end
+ memoize :outdatedness_reason_for
+
+ private
+
+ contract C::None => Basic
+ def basic
+ @_basic ||= Basic.new(outdatedness_checker: self, reps: @reps)
+ end
+
+ contract C::Or[Nanoc::Int::Item, Nanoc::Int::ItemRep, Nanoc::Int::Layout] => C::Maybe[Reasons::Generic]
+ def basic_outdatedness_reason_for(obj)
+ # FIXME: Stop using this; it is no longer accurate, as there can be >1 reasons
+ basic.outdatedness_status_for(obj).reasons.first
+ end
+
+ contract C::Or[Nanoc::Int::Item, Nanoc::Int::ItemRep, Nanoc::Int::Layout], Hamster::Set => C::Bool
+ def outdated_due_to_dependencies?(obj, processed = Hamster::Set.new)
+ # Convert from rep to item if necessary
+ obj = obj.item if obj.is_a?(Nanoc::Int::ItemRep)
+
+ # Get from cache
+ if @objects_outdated_due_to_dependencies.key?(obj)
+ return @objects_outdated_due_to_dependencies[obj]
+ end
+
+ # Check processed
+ # Don’t return true; the false will be or’ed into a true if there
+ # really is a dependency that is causing outdatedness.
+ return false if processed.include?(obj)
+
+ # Calculate
+ is_outdated = dependency_store.dependencies_causing_outdatedness_of(obj).any? do |dep|
+ dependency_causes_outdatedness?(dep) ||
+ (dep.props.compiled_content? &&
+ outdated_due_to_dependencies?(dep.from, processed.merge([obj])))
+ end
+
+ # Cache
+ @objects_outdated_due_to_dependencies[obj] = is_outdated
+
+ # Done
+ is_outdated
+ end
+
+ contract Nanoc::Int::Dependency => C::Bool
+ def dependency_causes_outdatedness?(dependency)
+ return true if dependency.from.nil?
+
+ status = basic.outdatedness_status_for(dependency.from)
+ (status.props.active & dependency.props.active).any?
+ end
+ end
+end
diff --git a/lib/nanoc/base/services/outdatedness_rule.rb b/lib/nanoc/base/services/outdatedness_rule.rb
new file mode 100644
index 0000000..333f734
--- /dev/null
+++ b/lib/nanoc/base/services/outdatedness_rule.rb
@@ -0,0 +1,21 @@
+module Nanoc::Int
+ # @api private
+ class OutdatednessRule
+ include Nanoc::Int::ContractsSupport
+ include Singleton
+
+ def apply(_obj, _outdatedness_checker)
+ raise NotImplementedError.new('Nanoc::Int::OutdatednessRule subclasses must implement ##reason, and #apply')
+ end
+
+ contract C::None => String
+ def inspect
+ "#{self.class.name}(#{reason})"
+ end
+
+ # TODO: remove
+ def reason
+ raise NotImplementedError.new('Nanoc::Int::OutdatednessRule subclasses must implement ##reason, and #apply')
+ end
+ end
+end
diff --git a/lib/nanoc/base/services/outdatedness_rules.rb b/lib/nanoc/base/services/outdatedness_rules.rb
new file mode 100644
index 0000000..c59bc7c
--- /dev/null
+++ b/lib/nanoc/base/services/outdatedness_rules.rb
@@ -0,0 +1,121 @@
+module Nanoc::Int
+ # @api private
+ module OutdatednessRules
+ class CodeSnippetsModified < OutdatednessRule
+ extend Nanoc::Int::Memoization
+
+ include Nanoc::Int::ContractsSupport
+
+ def reason
+ Nanoc::Int::OutdatednessReasons::CodeSnippetsModified
+ end
+
+ def apply(_obj, outdatedness_checker)
+ any_snippets_modified?(outdatedness_checker)
+ end
+
+ private
+
+ def any_snippets_modified?(outdatedness_checker)
+ outdatedness_checker.site.code_snippets.any? do |cs|
+ ch_old = outdatedness_checker.checksum_store[cs]
+ ch_new = Nanoc::Int::Checksummer.calc(cs)
+ ch_old != ch_new
+ end
+ end
+ memoize :any_snippets_modified?
+ end
+
+ class ConfigurationModified < OutdatednessRule
+ extend Nanoc::Int::Memoization
+
+ def reason
+ Nanoc::Int::OutdatednessReasons::ConfigurationModified
+ end
+
+ def apply(_obj, outdatedness_checker)
+ config_modified?(outdatedness_checker)
+ end
+
+ private
+
+ def config_modified?(outdatedness_checker)
+ obj = outdatedness_checker.site.config
+ ch_old = outdatedness_checker.checksum_store[obj]
+ ch_new = Nanoc::Int::Checksummer.calc(obj)
+ ch_old != ch_new
+ end
+ memoize :config_modified?
+ end
+
+ class NotWritten < OutdatednessRule
+ def reason
+ Nanoc::Int::OutdatednessReasons::NotWritten
+ end
+
+ def apply(obj, _outdatedness_checker)
+ # FIXME: check all paths (for all snapshots)
+ obj.raw_path && !File.file?(obj.raw_path)
+ end
+ end
+
+ class ContentModified < OutdatednessRule
+ def reason
+ Nanoc::Int::OutdatednessReasons::ContentModified
+ end
+
+ def apply(obj, outdatedness_checker)
+ obj = obj.item if obj.is_a?(Nanoc::Int::ItemRep)
+
+ ch_old = outdatedness_checker.checksum_store.content_checksum_for(obj)
+ ch_new = Nanoc::Int::Checksummer.calc_for_content_of(obj)
+ ch_old != ch_new
+ end
+ end
+
+ class AttributesModified < OutdatednessRule
+ def reason
+ Nanoc::Int::OutdatednessReasons::AttributesModified
+ end
+
+ def apply(obj, outdatedness_checker)
+ obj = obj.item if obj.is_a?(Nanoc::Int::ItemRep)
+
+ ch_old = outdatedness_checker.checksum_store.attributes_checksum_for(obj)
+ ch_new = Nanoc::Int::Checksummer.calc_for_attributes_of(obj)
+ ch_old != ch_new
+ end
+ end
+
+ class RulesModified < OutdatednessRule
+ def reason
+ Nanoc::Int::OutdatednessReasons::RulesModified
+ end
+
+ def apply(obj, outdatedness_checker)
+ mem_old = outdatedness_checker.rule_memory_store[obj]
+ mem_new = outdatedness_checker.action_provider.memory_for(obj).serialize
+ !mem_old.eql?(mem_new)
+ end
+ end
+
+ class PathsModified < OutdatednessRule
+ def reason
+ Nanoc::Int::OutdatednessReasons::PathsModified
+ end
+
+ def apply(obj, outdatedness_checker)
+ # FIXME: Prefer to not work on serialised version
+
+ mem_old = outdatedness_checker.rule_memory_store[obj]
+ mem_new = outdatedness_checker.action_provider.memory_for(obj).serialize
+ return true if mem_old.nil?
+
+ paths_old = mem_old.select { |pa| pa[0] == :snapshot }
+ paths_new = mem_new.select { |pa| pa[0] == :snapshot }
+
+ paths_old != paths_new
+ end
+ end
+ end
+end
diff --git a/lib/nanoc/base/services/pruner.rb b/lib/nanoc/base/services/pruner.rb
new file mode 100644
index 0000000..65e9863
--- /dev/null
+++ b/lib/nanoc/base/services/pruner.rb
@@ -0,0 +1,110 @@
+require 'find'
+
+module Nanoc
+ # Responsible for finding and deleting files in the site’s output directory
+ # that are not managed by Nanoc.
+ #
+ # @api private
+ class Pruner
+ # @param [Nanoc::Int::Configuration] config
+ #
+ # @param [Nanoc::Int::ItemRepRepo] reps
+ #
+ # @param [Boolean] dry_run true if the files to be deleted
+ # should only be printed instead of actually deleted, false if the files
+ # should actually be deleted.
+ #
+ # @param [Enumerable<String>] exclude
+ def initialize(config, reps, dry_run: false, exclude: [])
+ @config = config
+ @reps = reps
+ @dry_run = dry_run
+ @exclude = Set.new(exclude)
+ end
+
+ # Prunes all output files not managed by Nanoc.
+ #
+ # @return [void]
+ def run
+ return unless File.directory?(@config[:output_dir])
+
+ compiled_files = @reps.flat_map { |r| r.raw_paths.values }.compact
+ present_files, present_dirs = files_and_dirs_in(@config[:output_dir] + '/')
+
+ remove_stray_files(present_files, compiled_files)
+ remove_empty_directories(present_dirs)
+ end
+
+ def exclude?(component)
+ @exclude.include?(component)
+ end
+
+ # @param [String] filename The filename to check
+ #
+ # @return [Boolean] true if the given file is excluded, false otherwise
+ def filename_excluded?(filename)
+ pathname = Pathname.new(filename)
+ @exclude.any? { |e| pathname.__nanoc_include_component?(e) }
+ end
+
+ # @api private
+ def remove_stray_files(present_files, compiled_files)
+ (present_files - compiled_files).each do |f|
+ delete_file(f) unless exclude?(f)
+ end
+ end
+
+ # @api private
+ def remove_empty_directories(present_dirs)
+ present_dirs.reverse_each do |dir|
+ next if Dir.foreach(dir) { |n| break true if n !~ /\A\.\.?\z/ }
+ next if exclude?(dir)
+ delete_dir(dir)
+ end
+ end
+
+ # @api private
+ def files_and_dirs_in(dir)
+ present_files = []
+ present_dirs = []
+
+ Find.find(dir) do |f|
+ basename = File.basename(f)
+
+ case File.ftype(f)
+ when 'file'.freeze
+ unless exclude?(basename)
+ present_files << f
+ end
+ when 'directory'.freeze
+ if exclude?(basename)
+ Find.prune
+ else
+ present_dirs << f
+ end
+ end
+ end
+
+ [present_files, present_dirs]
+ end
+
+ protected
+
+ def delete_file(file)
+ log_delete_and_run(file) { FileUtils.rm(file) }
+ end
+
+ def delete_dir(dir)
+ log_delete_and_run(dir) { Dir.rmdir(dir) }
+ end
+
+ def log_delete_and_run(thing)
+ if @dry_run
+ puts thing
+ else
+ Nanoc::CLI::Logger.instance.file(:high, :delete, thing)
+ yield
+ end
+ end
+ end
+end
diff --git a/lib/nanoc/base/views.rb b/lib/nanoc/base/views.rb
index 473e0b2..b0a38c6 100644
--- a/lib/nanoc/base/views.rb
+++ b/lib/nanoc/base/views.rb
@@ -24,3 +24,5 @@ require_relative 'views/mutable_layout_collection_view'
require_relative 'views/post_compile_item_view'
require_relative 'views/post_compile_item_collection_view'
+require_relative 'views/post_compile_item_rep_view'
+require_relative 'views/post_compile_item_rep_collection_view'
diff --git a/lib/nanoc/base/views/item_rep_collection_view.rb b/lib/nanoc/base/views/item_rep_collection_view.rb
index e40dc38..2018678 100644
--- a/lib/nanoc/base/views/item_rep_collection_view.rb
+++ b/lib/nanoc/base/views/item_rep_collection_view.rb
@@ -19,19 +19,24 @@ module Nanoc
@item_reps
end
+ # @api private
+ def view_class
+ Nanoc::ItemRepView
+ end
+
def to_ary
- @item_reps.map { |ir| Nanoc::ItemRepView.new(ir, @context) }
+ @item_reps.map { |ir| view_class.new(ir, @context) }
end
# Calls the given block once for each item rep, passing that item rep as a parameter.
#
- # @yieldparam [Nanoc::ItemRepView] item rep
+ # @yieldparam [Object] item rep view
#
# @yieldreturn [void]
#
# @return [self]
def each
- @item_reps.each { |ir| yield Nanoc::ItemRepView.new(ir, @context) }
+ @item_reps.each { |ir| yield view_class.new(ir, @context) }
self
end
@@ -51,7 +56,7 @@ module Nanoc
case rep_name
when Symbol
res = @item_reps.find { |ir| ir.name == rep_name }
- res && Nanoc::ItemRepView.new(res, @context)
+ res && view_class.new(res, @context)
when Fixnum
raise ArgumentError, "expected ItemRepCollectionView#[] to be called with a symbol (you likely want `.reps[:default]` rather than `.reps[#{rep_name}]`)"
else
@@ -70,7 +75,7 @@ module Nanoc
def fetch(rep_name)
res = @item_reps.find { |ir| ir.name == rep_name }
if res
- Nanoc::ItemRepView.new(res, @context)
+ view_class.new(res, @context)
else
raise NoSuchItemRepError.new(rep_name)
end
diff --git a/lib/nanoc/base/views/item_rep_view.rb b/lib/nanoc/base/views/item_rep_view.rb
index 5befd8c..be22745 100644
--- a/lib/nanoc/base/views/item_rep_view.rb
+++ b/lib/nanoc/base/views/item_rep_view.rb
@@ -42,7 +42,7 @@ module Nanoc
#
# @return [String] The content at the given snapshot.
def compiled_content(snapshot: nil)
- @context.dependency_tracker.bounce(unwrap.item)
+ @context.dependency_tracker.bounce(unwrap.item, compiled_content: true)
@item_rep.compiled_content(snapshot: snapshot)
end
@@ -56,7 +56,7 @@ module Nanoc
#
# @return [String] The item rep’s path.
def path(snapshot: :last)
- @context.dependency_tracker.bounce(unwrap.item)
+ @context.dependency_tracker.bounce(unwrap.item, path: true)
@item_rep.path(snapshot: snapshot)
end
@@ -69,7 +69,7 @@ module Nanoc
# @api private
def raw_path(snapshot: :last)
- @context.dependency_tracker.bounce(unwrap.item)
+ @context.dependency_tracker.bounce(unwrap.item, path: true)
@item_rep.raw_path(snapshot: snapshot)
end
diff --git a/lib/nanoc/base/views/mixins/document_view_mixin.rb b/lib/nanoc/base/views/mixins/document_view_mixin.rb
index 5e88bba..fb16136 100644
--- a/lib/nanoc/base/views/mixins/document_view_mixin.rb
+++ b/lib/nanoc/base/views/mixins/document_view_mixin.rb
@@ -36,19 +36,19 @@ module Nanoc
# @see Hash#[]
def [](key)
- @context.dependency_tracker.bounce(unwrap)
+ @context.dependency_tracker.bounce(unwrap, attributes: true)
unwrap.attributes[key]
end
# @return [Hash]
def attributes
- @context.dependency_tracker.bounce(unwrap)
+ @context.dependency_tracker.bounce(unwrap, attributes: true)
unwrap.attributes
end
# @see Hash#fetch
def fetch(key, fallback = NONE, &_block)
- @context.dependency_tracker.bounce(unwrap)
+ @context.dependency_tracker.bounce(unwrap, attributes: true)
if unwrap.attributes.key?(key)
unwrap.attributes[key]
@@ -63,7 +63,7 @@ module Nanoc
# @see Hash#key?
def key?(key)
- @context.dependency_tracker.bounce(unwrap)
+ @context.dependency_tracker.bounce(unwrap, attributes: true)
unwrap.attributes.key?(key)
end
@@ -74,6 +74,7 @@ module Nanoc
# @api private
def raw_content
+ @context.dependency_tracker.bounce(unwrap, raw_content: true)
unwrap.content.string
end
diff --git a/lib/nanoc/base/views/mixins/with_reps_view_mixin.rb b/lib/nanoc/base/views/mixins/with_reps_view_mixin.rb
index 68bcebd..71cc65d 100644
--- a/lib/nanoc/base/views/mixins/with_reps_view_mixin.rb
+++ b/lib/nanoc/base/views/mixins/with_reps_view_mixin.rb
@@ -12,7 +12,7 @@ module Nanoc
# any).
#
# @return [String] The content of the given rep at the given snapshot.
- def compiled_content(rep: :default, snapshot: :pre)
+ def compiled_content(rep: :default, snapshot: nil)
reps.fetch(rep).compiled_content(snapshot: snapshot)
end
diff --git a/lib/nanoc/base/views/post_compile_item_rep_collection_view.rb b/lib/nanoc/base/views/post_compile_item_rep_collection_view.rb
new file mode 100644
index 0000000..75e4a2c
--- /dev/null
+++ b/lib/nanoc/base/views/post_compile_item_rep_collection_view.rb
@@ -0,0 +1,8 @@
+module Nanoc
+ class PostCompileItemRepCollectionView < Nanoc::ItemRepCollectionView
+ # @api private
+ def view_class
+ Nanoc::PostCompileItemRepView
+ end
+ end
+end
diff --git a/lib/nanoc/base/views/post_compile_item_rep_view.rb b/lib/nanoc/base/views/post_compile_item_rep_view.rb
new file mode 100644
index 0000000..4bc0f70
--- /dev/null
+++ b/lib/nanoc/base/views/post_compile_item_rep_view.rb
@@ -0,0 +1,18 @@
+module Nanoc
+ class PostCompileItemRepView < ::Nanoc::ItemRepView
+ def compiled_content(snapshot: nil)
+ if unwrap.binary?
+ raise Nanoc::Int::Errors::CannotGetCompiledContentOfBinaryItem.new(unwrap)
+ end
+
+ snapshot_contents = @context.compilation_context.compiled_content_cache[unwrap]
+ snapshot_name = snapshot || (snapshot_contents[:pre] ? :pre : :last)
+
+ if snapshot_contents[snapshot_name]
+ snapshot_contents[snapshot_name].string
+ else
+ raise Nanoc::Int::Errors::NoSuchSnapshot.new(unwrap, snapshot_name)
+ end
+ end
+ end
+end
diff --git a/lib/nanoc/base/views/post_compile_item_view.rb b/lib/nanoc/base/views/post_compile_item_view.rb
index 88ffb48..890ba46 100644
--- a/lib/nanoc/base/views/post_compile_item_view.rb
+++ b/lib/nanoc/base/views/post_compile_item_view.rb
@@ -1,5 +1,9 @@
module Nanoc
class PostCompileItemView < Nanoc::ItemWithRepsView
+ def reps
+ Nanoc::PostCompileItemRepCollectionView.new(@context.reps[unwrap], @context)
+ end
+
# @deprecated Use {#modified_reps} instead
def modified
modified_reps
diff --git a/lib/nanoc/base/views/view_context.rb b/lib/nanoc/base/views/view_context.rb
index bd54821..f8bc794 100644
--- a/lib/nanoc/base/views/view_context.rb
+++ b/lib/nanoc/base/views/view_context.rb
@@ -4,13 +4,13 @@ module Nanoc
attr_reader :reps
attr_reader :items
attr_reader :dependency_tracker
- attr_reader :compiler
+ attr_reader :compilation_context
- def initialize(reps:, items:, dependency_tracker:, compiler:)
+ def initialize(reps:, items:, dependency_tracker:, compilation_context:)
@reps = reps
@items = items
@dependency_tracker = dependency_tracker
- @compiler = compiler
+ @compilation_context = compilation_context
end
end
end
diff --git a/lib/nanoc/checking.rb b/lib/nanoc/checking.rb
new file mode 100644
index 0000000..8b31d18
--- /dev/null
+++ b/lib/nanoc/checking.rb
@@ -0,0 +1,11 @@
+module Nanoc
+ # @api private
+ module Checking
+ autoload 'Check', 'nanoc/checking/check'
+ autoload 'DSL', 'nanoc/checking/dsl'
+ autoload 'Runner', 'nanoc/checking/runner.rb'
+ autoload 'Issue', 'nanoc/checking/issue'
+ end
+end
+
+require 'nanoc/checking/checks'
diff --git a/lib/nanoc/extra/checking/check.rb b/lib/nanoc/checking/check.rb
similarity index 78%
rename from lib/nanoc/extra/checking/check.rb
rename to lib/nanoc/checking/check.rb
index 186bfb1..8abac87 100644
--- a/lib/nanoc/extra/checking/check.rb
+++ b/lib/nanoc/checking/check.rb
@@ -1,4 +1,4 @@
-module Nanoc::Extra::Checking
+module Nanoc::Checking
# @api private
class OutputDirNotFoundError < Nanoc::Int::Errors::Generic
def initialize(directory_path)
@@ -15,12 +15,12 @@ module Nanoc::Extra::Checking
def self.create(site)
output_dir = site.config[:output_dir]
unless File.exist?(output_dir)
- raise Nanoc::Extra::Checking::OutputDirNotFoundError.new(output_dir)
+ raise Nanoc::Checking::OutputDirNotFoundError.new(output_dir)
end
output_filenames = Dir[output_dir + '/**/*'].select { |f| File.file?(f) }
# FIXME: ugly
- view_context = site.compiler.create_view_context(Nanoc::Int::DependencyTracker::Null.new)
+ view_context = site.compiler.compilation_context.create_view_context(Nanoc::Int::DependencyTracker::Null.new)
context = {
items: Nanoc::ItemCollectionWithRepsView.new(site.items, view_context),
@@ -39,7 +39,7 @@ module Nanoc::Extra::Checking
end
def run
- raise NotImplementedError.new('Nanoc::Extra::Checking::Check subclasses must implement #run')
+ raise NotImplementedError.new('Nanoc::Checking::Check subclasses must implement #run')
end
def add_issue(desc, subject: nil)
diff --git a/lib/nanoc/checking/checks.rb b/lib/nanoc/checking/checks.rb
new file mode 100644
index 0000000..fd6a924
--- /dev/null
+++ b/lib/nanoc/checking/checks.rb
@@ -0,0 +1,20 @@
+require_relative 'checks/w3c_validator'
+
+# @api private
+module Nanoc::Checking::Checks
+ autoload 'CSS', 'nanoc/checking/checks/css'
+ autoload 'ExternalLinks', 'nanoc/checking/checks/external_links'
+ autoload 'HTML', 'nanoc/checking/checks/html'
+ autoload 'InternalLinks', 'nanoc/checking/checks/internal_links'
+ autoload 'Stale', 'nanoc/checking/checks/stale'
+ autoload 'MixedContent', 'nanoc/checking/checks/mixed_content'
+
+ Nanoc::Checking::Check.register '::Nanoc::Checking::Checks::CSS', :css
+ Nanoc::Checking::Check.register '::Nanoc::Checking::Checks::ExternalLinks', :external_links
+ Nanoc::Checking::Check.register '::Nanoc::Checking::Checks::ExternalLinks', :elinks
+ Nanoc::Checking::Check.register '::Nanoc::Checking::Checks::HTML', :html
+ Nanoc::Checking::Check.register '::Nanoc::Checking::Checks::InternalLinks', :internal_links
+ Nanoc::Checking::Check.register '::Nanoc::Checking::Checks::InternalLinks', :ilinks
+ Nanoc::Checking::Check.register '::Nanoc::Checking::Checks::Stale', :stale
+ Nanoc::Checking::Check.register '::Nanoc::Checking::Checks::MixedContent', :mixed_content
+end
diff --git a/lib/nanoc/extra/checking/checks/css.rb b/lib/nanoc/checking/checks/css.rb
similarity index 60%
rename from lib/nanoc/extra/checking/checks/css.rb
rename to lib/nanoc/checking/checks/css.rb
index ae3f901..8accf94 100644
--- a/lib/nanoc/extra/checking/checks/css.rb
+++ b/lib/nanoc/checking/checks/css.rb
@@ -1,6 +1,6 @@
-module ::Nanoc::Extra::Checking::Checks
+module ::Nanoc::Checking::Checks
# @api private
- class CSS < ::Nanoc::Extra::Checking::Checks::W3CValidator
+ class CSS < ::Nanoc::Checking::Checks::W3CValidator
identifier :css
def extension
diff --git a/lib/nanoc/extra/checking/checks/external_links.rb b/lib/nanoc/checking/checks/external_links.rb
similarity index 80%
rename from lib/nanoc/extra/checking/checks/external_links.rb
rename to lib/nanoc/checking/checks/external_links.rb
index 2bb7f5f..e8d92e7 100644
--- a/lib/nanoc/extra/checking/checks/external_links.rb
+++ b/lib/nanoc/checking/checks/external_links.rb
@@ -4,14 +4,16 @@ require 'nokogiri'
require 'timeout'
require 'uri'
-module ::Nanoc::Extra::Checking::Checks
+module ::Nanoc::Checking::Checks
# A validator that verifies that all external links point to a location that exists.
#
# @api private
- class ExternalLinks < ::Nanoc::Extra::Checking::Check
+ class ExternalLinks < ::Nanoc::Checking::Check
identifiers :external_links, :elinks
def run
+ require 'parallel'
+
# Find all broken external hrefs
# TODO: de-duplicate this (duplicated in internal links check)
filenames = output_filenames.select { |f| File.extname(f) == '.html' && !excluded_file?(f) }
@@ -40,45 +42,9 @@ module ::Nanoc::Extra::Checking::Checks
end
end
- class ArrayEnumerator
- def initialize(array)
- @array = array
- @index = 0
- @mutex = Mutex.new
- end
-
- def next
- @mutex.synchronize do
- @index += 1
- return @array[@index - 1]
- end
- end
- end
-
def select_invalid(hrefs)
- enum = ArrayEnumerator.new(hrefs.sort)
- mutex = Mutex.new
- invalid = Set.new
-
- threads = []
- 10.times do
- threads << Thread.new do
- loop do
- href = enum.next
- break if href.nil?
-
- res = validate(href)
- next unless res
-
- mutex.synchronize do
- invalid << res
- end
- end
- end
- end
- threads.each(&:join)
-
- invalid
+ col = Nanoc::Extra::ParallelCollection.new(hrefs, parallelism: 10)
+ col.map { |href| validate(href) }.compact
end
def validate(href)
@@ -138,7 +104,7 @@ module ::Nanoc::Extra::Checking::Checks
if last_err
return Result.new(href, last_err.message)
else
- raise 'should not have gotten here'
+ raise Nanoc::Int::Errors::InternalInconsistency, 'last_err cannot be nil'
end
end
diff --git a/lib/nanoc/extra/checking/checks/html.rb b/lib/nanoc/checking/checks/html.rb
similarity index 61%
rename from lib/nanoc/extra/checking/checks/html.rb
rename to lib/nanoc/checking/checks/html.rb
index 6c5cfa7..8665226 100644
--- a/lib/nanoc/extra/checking/checks/html.rb
+++ b/lib/nanoc/checking/checks/html.rb
@@ -1,6 +1,6 @@
-module ::Nanoc::Extra::Checking::Checks
+module ::Nanoc::Checking::Checks
# @api private
- class HTML < ::Nanoc::Extra::Checking::Checks::W3CValidator
+ class HTML < ::Nanoc::Checking::Checks::W3CValidator
identifier :html
def extension
diff --git a/lib/nanoc/extra/checking/checks/internal_links.rb b/lib/nanoc/checking/checks/internal_links.rb
similarity index 96%
rename from lib/nanoc/extra/checking/checks/internal_links.rb
rename to lib/nanoc/checking/checks/internal_links.rb
index e8ad11d..c7f6592 100644
--- a/lib/nanoc/extra/checking/checks/internal_links.rb
+++ b/lib/nanoc/checking/checks/internal_links.rb
@@ -1,10 +1,10 @@
require 'uri'
-module Nanoc::Extra::Checking::Checks
+module Nanoc::Checking::Checks
# A check that verifies that all internal links point to a location that exists.
#
# @api private
- class InternalLinks < ::Nanoc::Extra::Checking::Check
+ class InternalLinks < ::Nanoc::Checking::Check
# Starts the validator. The results will be printed to stdout.
#
# Internal links that match a regexp pattern in `@config[:checks][:internal_links][:exclude]` will
diff --git a/lib/nanoc/extra/checking/checks/mixed_content.rb b/lib/nanoc/checking/checks/mixed_content.rb
similarity index 89%
rename from lib/nanoc/extra/checking/checks/mixed_content.rb
rename to lib/nanoc/checking/checks/mixed_content.rb
index 226ffd8..e30899e 100644
--- a/lib/nanoc/extra/checking/checks/mixed_content.rb
+++ b/lib/nanoc/checking/checks/mixed_content.rb
@@ -1,9 +1,9 @@
-module Nanoc::Extra::Checking::Checks
+module Nanoc::Checking::Checks
# A check that verifies HTML files do not reference external resources with
# URLs that would trigger "mixed content" warnings.
#
# @api private
- class MixedContent < ::Nanoc::Extra::Checking::Check
+ class MixedContent < ::Nanoc::Checking::Check
PROTOCOL_PATTERN = /^(\w+):\/\//
def run
diff --git a/lib/nanoc/extra/checking/checks/stale.rb b/lib/nanoc/checking/checks/stale.rb
similarity index 79%
rename from lib/nanoc/extra/checking/checks/stale.rb
rename to lib/nanoc/checking/checks/stale.rb
index 7f65f13..fa10071 100644
--- a/lib/nanoc/extra/checking/checks/stale.rb
+++ b/lib/nanoc/checking/checks/stale.rb
@@ -1,6 +1,6 @@
-module Nanoc::Extra::Checking::Checks
+module Nanoc::Checking::Checks
# @api private
- class Stale < ::Nanoc::Extra::Checking::Check
+ class Stale < ::Nanoc::Checking::Check
def run
require 'set'
@@ -30,7 +30,8 @@ module Nanoc::Extra::Checking::Checks
def pruner
exclude_config = @config.fetch(:prune, {}).fetch(:exclude, [])
- @pruner ||= Nanoc::Extra::Pruner.new(@site, exclude: exclude_config)
+ # FIXME: reps=nil is icky
+ @pruner ||= Nanoc::Pruner.new(@config, nil, exclude: exclude_config)
end
end
end
diff --git a/lib/nanoc/extra/checking/checks/w3c_validator.rb b/lib/nanoc/checking/checks/w3c_validator.rb
similarity index 87%
rename from lib/nanoc/extra/checking/checks/w3c_validator.rb
rename to lib/nanoc/checking/checks/w3c_validator.rb
index 2b6c57e..1ebc3be 100644
--- a/lib/nanoc/extra/checking/checks/w3c_validator.rb
+++ b/lib/nanoc/checking/checks/w3c_validator.rb
@@ -1,6 +1,6 @@
-module ::Nanoc::Extra::Checking::Checks
+module ::Nanoc::Checking::Checks
# @api private
- class W3CValidator < ::Nanoc::Extra::Checking::Check
+ class W3CValidator < ::Nanoc::Checking::Check
def run
require 'w3c_validators'
diff --git a/lib/nanoc/extra/checking/dsl.rb b/lib/nanoc/checking/dsl.rb
similarity index 68%
rename from lib/nanoc/extra/checking/dsl.rb
rename to lib/nanoc/checking/dsl.rb
index 0df8c7d..f6218e6 100644
--- a/lib/nanoc/extra/checking/dsl.rb
+++ b/lib/nanoc/checking/dsl.rb
@@ -1,11 +1,12 @@
-module Nanoc::Extra::Checking
+module Nanoc::Checking
# @api private
class DSL
attr_reader :deploy_checks
def self.from_file(filename)
dsl = new
- dsl.instance_eval(File.read(filename), filename)
+ absolute_filename = File.expand_path(filename)
+ dsl.instance_eval(File.read(filename), absolute_filename)
dsl
end
@@ -14,7 +15,7 @@ module Nanoc::Extra::Checking
end
def check(identifier, &block)
- klass = Class.new(::Nanoc::Extra::Checking::Check)
+ klass = Class.new(::Nanoc::Checking::Check)
klass.send(:define_method, :run, &block)
klass.send(:identifier, identifier)
end
diff --git a/lib/nanoc/extra/checking/issue.rb b/lib/nanoc/checking/issue.rb
similarity index 90%
rename from lib/nanoc/extra/checking/issue.rb
rename to lib/nanoc/checking/issue.rb
index b5dfdb4..a53a57a 100644
--- a/lib/nanoc/extra/checking/issue.rb
+++ b/lib/nanoc/checking/issue.rb
@@ -1,4 +1,4 @@
-module Nanoc::Extra::Checking
+module Nanoc::Checking
# @api private
class Issue
attr_reader :description
diff --git a/lib/nanoc/extra/checking/runner.rb b/lib/nanoc/checking/runner.rb
similarity index 94%
rename from lib/nanoc/extra/checking/runner.rb
rename to lib/nanoc/checking/runner.rb
index 75ed526..bde9dba 100644
--- a/lib/nanoc/extra/checking/runner.rb
+++ b/lib/nanoc/checking/runner.rb
@@ -1,4 +1,4 @@
-module Nanoc::Extra::Checking
+module Nanoc::Checking
# Runner is reponsible for running issue checks.
#
# @api private
@@ -67,7 +67,7 @@ module Nanoc::Extra::Checking
unless @dsl_loaded
@dsl =
if dsl_present?
- Nanoc::Extra::Checking::DSL.from_file(checks_filename)
+ Nanoc::Checking::DSL.from_file(checks_filename)
else
nil
end
@@ -93,12 +93,12 @@ module Nanoc::Extra::Checking
end
def all_check_classes
- Nanoc::Extra::Checking::Check.all.map(&:last).uniq
+ Nanoc::Checking::Check.all.map(&:last).uniq
end
def check_classes_named(n)
n.map do |a|
- klass = Nanoc::Extra::Checking::Check.named(a)
+ klass = Nanoc::Checking::Check.named(a)
raise Nanoc::Int::Errors::GenericTrivial, "Unknown check: #{a}" if klass.nil?
klass
end
diff --git a/lib/nanoc/cli.rb b/lib/nanoc/cli.rb
index 420ce62..fd78a79 100644
--- a/lib/nanoc/cli.rb
+++ b/lib/nanoc/cli.rb
@@ -19,8 +19,6 @@ module Nanoc::CLI
autoload 'ErrorHandler', 'nanoc/cli/error_handler'
# @return [Boolean] true if debug output is enabled, false if not
- #
- # @since 3.2.0
def self.debug?
@debug || false
end
@@ -29,8 +27,6 @@ module Nanoc::CLI
# false if it should not
#
# @return [void]
- #
- # @since 3.2.0
def self.debug=(boolean)
@debug = boolean
end
diff --git a/lib/nanoc/cli/cleaning_stream.rb b/lib/nanoc/cli/cleaning_stream.rb
index dd50a9c..8d13a59 100644
--- a/lib/nanoc/cli/cleaning_stream.rb
+++ b/lib/nanoc/cli/cleaning_stream.rb
@@ -56,6 +56,11 @@ module Nanoc::CLI
@cached_is_tty ||= @stream.tty?
end
+ # @see IO#isatty
+ def isatty
+ tty?
+ end
+
# @see IO#flush
def flush
_nanoc_swallow_broken_pipe_errors_while do
@@ -133,14 +138,16 @@ module Nanoc::CLI
end
# @see ARGF.set_encoding
+ # rubocop:disable Style/AccessorMethodName
def set_encoding(*args)
@stream.set_encoding(*args)
end
+ # rubocop:enable Style/AccessorMethodName
protected
def _nanoc_clean(s)
- @stream_cleaners.reduce(s.to_s) { |a, e| e.clean(a) }
+ @stream_cleaners.reduce(s.to_s) { |acc, elem| elem.clean(acc) }
end
def _nanoc_swallow_broken_pipe_errors_while
diff --git a/lib/nanoc/cli/command_runner.rb b/lib/nanoc/cli/command_runner.rb
index d7b92f4..0b65b63 100644
--- a/lib/nanoc/cli/command_runner.rb
+++ b/lib/nanoc/cli/command_runner.rb
@@ -45,8 +45,8 @@ module Nanoc::CLI
#
# @return [void]
def load_site(preprocess: false)
- print 'Loading site… '
- $stdout.flush
+ $stderr.print 'Loading site… '
+ $stderr.flush
if site.nil?
raise ::Nanoc::Int::Errors::GenericTrivial, 'The current working directory does not seem to be a Nanoc site.'
@@ -56,7 +56,7 @@ module Nanoc::CLI
site.compiler.action_provider.preprocess(site)
end
- puts 'done'
+ $stderr.puts 'done'
end
# @return [Boolean] true if debug output is enabled, false if not
@@ -65,12 +65,5 @@ module Nanoc::CLI
def debug?
Nanoc::CLI.debug?
end
-
- protected
-
- # @return [Array] The compilation stack.
- def stack
- (site && site.compiler.stack) || []
- end
end
end
diff --git a/lib/nanoc/cli/commands/check.rb b/lib/nanoc/cli/commands/check.rb
index 67f6b87..0c37255 100644
--- a/lib/nanoc/cli/commands/check.rb
+++ b/lib/nanoc/cli/commands/check.rb
@@ -14,7 +14,7 @@ module Nanoc::CLI::Commands
validate_options_and_arguments
load_site(preprocess: true)
- runner = Nanoc::Extra::Checking::Runner.new(site)
+ runner = Nanoc::Checking::Runner.new(site)
if options[:list]
runner.list_checks
diff --git a/lib/nanoc/cli/commands/compile.rb b/lib/nanoc/cli/commands/compile.rb
index 414e14b..3cc68d4 100644
--- a/lib/nanoc/cli/commands/compile.rb
+++ b/lib/nanoc/cli/commands/compile.rb
@@ -2,17 +2,6 @@ usage 'compile [options]'
summary 'compile items of this site'
description <<-EOS
Compile all items of the current site.
-
-The compile command will show all items of the site as they are processed. The time spent compiling the item will be printed, as well as a status message, which can be one of the following:
-
-CREATED - The compiled item did not yet exist and has been created
-
-UPDATED - The compiled item did already exist and has been modified
-
-IDENTICAL - The item was deemed outdated and has been recompiled, but the compiled version turned out to be identical to the already existing version
-
-SKIP - The item was deemed not outdated and was therefore not recompiled
-
EOS
flag nil, :profile, 'profile compilation' if Nanoc::Feature.enabled?(Nanoc::Feature::PROFILER)
@@ -24,8 +13,7 @@ module Nanoc::CLI::Commands
#
# @abstract Subclasses must override {#start} and may override {#stop}.
class Listener
- def initialize(*)
- end
+ def initialize(*); end
# @param [Nanoc::CLI::CommandRunner] command_runner The command runner for this listener
#
@@ -48,8 +36,7 @@ module Nanoc::CLI::Commands
# Stops the listener. The default implementation removes self from all notification center observers.
#
# @return [void]
- def stop
- end
+ def stop; end
# @api private
def start_safely
@@ -164,19 +151,55 @@ module Nanoc::CLI::Commands
# @param [Enumerable<Nanoc::Int::ItemRep>] reps
def initialize(reps:)
- @times = {}
+ # rep ->
+ # filter_name ->
+ # accum -> 0.0
+ # last_start -> nil
+ @times_per_rep = {}
@reps = reps
end
# @see Listener#start
def start
- Nanoc::Int::NotificationCenter.on(:filtering_started) do |_rep, filter_name|
- @times[filter_name] ||= []
- @times[filter_name] << { start: Time.now }
+ Nanoc::Int::NotificationCenter.on(:filtering_started) do |rep, filter_name|
+ @times_per_rep[rep] ||= {}
+ @times_per_rep[rep][filter_name] ||= {}
+
+ @times_per_rep[rep][filter_name][:last_start] = Time.now
+ @times_per_rep[rep][filter_name][:accum] ||= []
+ @times_per_rep[rep][filter_name][:suspended] = false
end
- Nanoc::Int::NotificationCenter.on(:filtering_ended) do |_rep, filter_name|
- @times[filter_name].last[:stop] = Time.now
+
+ Nanoc::Int::NotificationCenter.on(:filtering_ended) do |rep, filter_name|
+ times = @times_per_rep[rep][filter_name]
+ last_start = @times_per_rep[rep][filter_name][:last_start]
+
+ times[:accum] << (Time.now - last_start)
+ @times_per_rep[rep][filter_name].delete(:last_start)
+ end
+
+ Nanoc::Int::NotificationCenter.on(:compilation_suspended) do |rep, _exception|
+ @times_per_rep.fetch(rep, {}).each do |_filter_name, times|
+ if times[:last_start]
+ times[:accum] << (Time.now - times[:last_start])
+ times.delete(:last_start)
+ times[:suspended] = true
+
+ break
+ end
+ end
+ end
+
+ Nanoc::Int::NotificationCenter.on(:compilation_started) do |rep|
+ @times_per_rep.fetch(rep, {}).each do |filter_name, times|
+ if times[:suspended]
+ @times_per_rep[rep][filter_name][:last_start] = Time.now
+ times[:suspended] = false
+
+ break
+ end
+ end
end
end
@@ -217,7 +240,7 @@ module Nanoc::CLI::Commands
# Calculate stats
count = samples.size
min = samples.min
- tot = samples.reduce(0) { |a, e| a + e }
+ tot = samples.reduce(0) { |acc, elem| acc + elem }
avg = tot / count
max = samples.max
@@ -236,55 +259,17 @@ module Nanoc::CLI::Commands
def durations_per_filter
@_durations_per_filter ||= begin
result = {}
- @times.keys.each do |filter_name|
- durations = durations_for_filter(filter_name)
- if durations
- result[filter_name] = durations
- end
- end
- result
- end
- end
- def durations_for_filter(filter_name)
- result = []
- @times[filter_name].each do |sample|
- if sample[:start] && sample[:stop]
- result << sample[:stop] - sample[:start]
+ @times_per_rep.each do |_rep, times_per_filter|
+ times_per_filter.each do |filter_name, data|
+ result[filter_name] ||= []
+ result[filter_name].concat(data[:accum])
+ end
end
- end
- result
- end
- end
-
- # Controls garbage collection so that it only occurs once every 20 items
- class GCController < Listener
- # @see Listener#enable_for?
- def self.enable_for?(_command_runner)
- !ENV.key?('TRAVIS')
- end
- def initialize(*)
- @gc_count = 0
- end
-
- # @see Listener#start
- def start
- Nanoc::Int::NotificationCenter.on(:compilation_started) do |_rep|
- if (@gc_count % 20).zero?
- GC.enable
- GC.start
- GC.disable
- end
- @gc_count += 1
+ result
end
end
-
- # @see Listener#stop
- def stop
- super
- GC.enable
- end
end
# Prints debug information (compilation started/ended, filtering started/ended, …)
@@ -303,7 +288,7 @@ module Nanoc::CLI::Commands
puts "*** Ended compilation of #{rep.inspect}"
puts
end
- Nanoc::Int::NotificationCenter.on(:compilation_failed) do |rep, e|
+ Nanoc::Int::NotificationCenter.on(:compilation_suspended) do |rep, e|
puts "*** Suspended compilation of #{rep.inspect}: #{e.message}"
end
Nanoc::Int::NotificationCenter.on(:cached_content_used) do |rep|
@@ -325,17 +310,26 @@ module Nanoc::CLI::Commands
class FileActionPrinter < Listener
def initialize(reps:)
@start_times = {}
+ @acc_durations = {}
@reps = reps
end
# @see Listener#start
def start
- Nanoc::Int::NotificationCenter.on(:compilation_started) do |rep|
- @start_times[rep.raw_path] = Time.now
+ Nanoc::Int::NotificationCenter.on(:compilation_started, self) do |rep|
+ @start_times[rep] = Time.now
+ @acc_durations[rep] ||= 0.0
+ end
+
+ Nanoc::Int::NotificationCenter.on(:compilation_suspended, self) do |rep|
+ @acc_durations[rep] += Time.now - @start_times[rep]
end
- Nanoc::Int::NotificationCenter.on(:rep_written) do |_rep, path, is_created, is_modified|
- duration = path && @start_times[path] ? Time.now - @start_times[path] : nil
+
+ Nanoc::Int::NotificationCenter.on(:rep_written, self) do |rep, path, is_created, is_modified|
+ @acc_durations[rep] += Time.now - @start_times[rep]
+ duration = @acc_durations[rep]
+
action =
if is_created then :create
elsif is_modified then :update
@@ -353,6 +347,11 @@ module Nanoc::CLI::Commands
# @see Listener#stop
def stop
super
+
+ Nanoc::Int::NotificationCenter.remove(:compilation_started, self)
+ Nanoc::Int::NotificationCenter.remove(:compilation_suspended, self)
+ Nanoc::Int::NotificationCenter.remove(:rep_written, self)
+
@reps.select { |r| !r.compiled? }.each do |rep|
rep.raw_paths.each do |_snapshot_name, raw_path|
log(:low, :skip, raw_path, nil)
@@ -415,12 +414,11 @@ module Nanoc::CLI::Commands
def default_listener_classes
[
+ Nanoc::CLI::Commands::Compile::StackProfProfiler,
Nanoc::CLI::Commands::Compile::DiffGenerator,
Nanoc::CLI::Commands::Compile::DebugPrinter,
Nanoc::CLI::Commands::Compile::TimingRecorder,
- Nanoc::CLI::Commands::Compile::GCController,
Nanoc::CLI::Commands::Compile::FileActionPrinter,
- Nanoc::CLI::Commands::Compile::StackProfProfiler,
]
end
@@ -445,7 +443,7 @@ module Nanoc::CLI::Commands
end
def teardown_listeners
- @listeners.each(&:stop_safely)
+ @listeners.reverse_each(&:stop_safely)
end
def reps
diff --git a/lib/nanoc/cli/commands/create-site.rb b/lib/nanoc/cli/commands/create-site.rb
index 0c0eae5..8891319 100644
--- a/lib/nanoc/cli/commands/create-site.rb
+++ b/lib/nanoc/cli/commands/create-site.rb
@@ -278,14 +278,13 @@ EOS
<div id="sidebar">
<h2>Documentation</h2>
<ul>
- <li><a href="http://nanoc.ws/about/">About</a></li>
<li><a href="http://nanoc.ws/doc/">Documentation</a></li>
<li><a href="http://nanoc.ws/doc/tutorial/">Tutorial</a></li>
</ul>
<h2>Community</h2>
<ul>
<li><a href="http://groups.google.com/group/nanoc/">Discussion group</a></li>
- <li><a href="irc://chat.freenode.net/#nanoc">IRC channel</a></li>
+ <li><a href="https://gitter.im/nanoc/nanoc">Gitter channel</a></li>
<li><a href="http://nanoc.ws/contributing/">Contributing</a></li>
</ul>
</div>
diff --git a/lib/nanoc/cli/commands/deploy.rb b/lib/nanoc/cli/commands/deploy.rb
index 6547a64..02c4110 100644
--- a/lib/nanoc/cli/commands/deploy.rb
+++ b/lib/nanoc/cli/commands/deploy.rb
@@ -27,7 +27,7 @@ module Nanoc::CLI::Commands
private
def list_deployers
- deployers = Nanoc::Int::PluginRegistry.instance.find_all(Nanoc::Extra::Deployer)
+ deployers = Nanoc::Int::PluginRegistry.instance.find_all(Nanoc::Deploying::Deployer)
deployer_names = deployers.keys.sort_by(&:to_s)
puts 'Available deployers:'
deployer_names.each do |name|
@@ -85,7 +85,7 @@ module Nanoc::CLI::Commands
end
def check
- runner = Nanoc::Extra::Checking::Runner.new(site)
+ runner = Nanoc::Checking::Runner.new(site)
if runner.dsl_present?
puts 'Running issue checks…'
is_success = runner.run_for_deploy
@@ -105,13 +105,13 @@ module Nanoc::CLI::Commands
end
def deployer_class_for_config(config)
- names = Nanoc::Extra::Deployer.all.keys
+ names = Nanoc::Deploying::Deployer.all.keys
name = config.fetch(:kind) do
$stderr.puts 'Warning: The specified deploy target does not have a kind attribute. Assuming rsync.'
'rsync'
end
- deployer_class = Nanoc::Extra::Deployer.named(name)
+ deployer_class = Nanoc::Deploying::Deployer.named(name)
if deployer_class.nil?
raise Nanoc::Int::Errors::GenericTrivial, "The specified deploy target has an unrecognised kind “#{name}” (expected one of #{names.join(', ')})."
end
diff --git a/lib/nanoc/cli/commands/nanoc.rb b/lib/nanoc/cli/commands/nanoc.rb
index 0372018..ad70f55 100644
--- a/lib/nanoc/cli/commands/nanoc.rb
+++ b/lib/nanoc/cli/commands/nanoc.rb
@@ -10,6 +10,10 @@ opt :d, :debug, 'enable debugging' do
Nanoc::CLI.debug = true
end
+opt :e, :env, 'set environment', argument: :required do |value|
+ ENV.store('NANOC_ENV', value)
+end
+
opt :h, :help, 'show the help message and quit' do |_value, cmd|
puts cmd.help
exit 0
diff --git a/lib/nanoc/cli/commands/prune.rb b/lib/nanoc/cli/commands/prune.rb
index 4de401f..e8d2e71 100644
--- a/lib/nanoc/cli/commands/prune.rb
+++ b/lib/nanoc/cli/commands/prune.rb
@@ -19,9 +19,9 @@ module Nanoc::CLI::Commands
site.compiler.build_reps
if options.key?(:yes)
- Nanoc::Extra::Pruner.new(site, exclude: prune_config_exclude).run
+ Nanoc::Pruner.new(site.config, site.compiler.reps, exclude: prune_config_exclude).run
elsif options.key?(:'dry-run')
- Nanoc::Extra::Pruner.new(site, exclude: prune_config_exclude, dry_run: true).run
+ Nanoc::Pruner.new(site.config, site.compiler.reps, exclude: prune_config_exclude, dry_run: true).run
else
$stderr.puts 'WARNING: Since the prune command is a destructive command, it requires an additional --yes flag in order to work.'
$stderr.puts
diff --git a/lib/nanoc/cli/commands/shell.rb b/lib/nanoc/cli/commands/shell.rb
index 6a3127b..77521dc 100644
--- a/lib/nanoc/cli/commands/shell.rb
+++ b/lib/nanoc/cli/commands/shell.rb
@@ -21,11 +21,30 @@ module Nanoc::CLI::Commands
self.class.env_for_site(site)
end
+ def self.reps_for(site)
+ Nanoc::Int::ItemRepRepo.new.tap do |reps|
+ action_provider = Nanoc::Int::ActionProvider.named(:rule_dsl).for(site)
+ builder = Nanoc::Int::ItemRepBuilder.new(site, action_provider, reps)
+ builder.run
+ end
+ end
+
+ def self.view_context_for(site)
+ Nanoc::ViewContext.new(
+ reps: reps_for(site),
+ items: site.items,
+ dependency_tracker: Nanoc::Int::DependencyTracker::Null.new,
+ compilation_context: nil,
+ )
+ end
+
def self.env_for_site(site)
+ view_context = view_context_for(site)
+
{
- items: Nanoc::ItemCollectionWithRepsView.new(site.items, nil),
- layouts: Nanoc::LayoutCollectionView.new(site.layouts, nil),
- config: Nanoc::ConfigView.new(site.config, nil),
+ items: Nanoc::ItemCollectionWithRepsView.new(site.items, view_context),
+ layouts: Nanoc::LayoutCollectionView.new(site.layouts, view_context),
+ config: Nanoc::ConfigView.new(site.config, view_context),
}
end
end
diff --git a/lib/nanoc/cli/commands/show-data.rb b/lib/nanoc/cli/commands/show-data.rb
index 6a4cb32..929c8f1 100644
--- a/lib/nanoc/cli/commands/show-data.rb
+++ b/lib/nanoc/cli/commands/show-data.rb
@@ -20,6 +20,9 @@ module Nanoc::CLI::Commands
compiler.load_stores
dependency_store = compiler.dependency_store
+ # Build reps
+ compiler.build_reps
+
# Print data
print_item_dependencies(items, dependency_store)
print_item_rep_paths(items)
@@ -59,11 +62,23 @@ module Nanoc::CLI::Commands
def print_item_dependencies(items, dependency_store)
print_header('Item dependencies')
+ puts 'Legend:'
+ puts ' r = dependency on raw content'
+ puts ' a = dependency on attributes'
+ puts ' c = dependency on compiled content'
+ puts ' p = dependency on the path'
+ puts
+
sorted_with_prev(items) do |item, prev|
puts if prev
puts "item #{item.identifier} depends on:"
- predecessors = dependency_store.objects_causing_outdatedness_of(item).sort_by { |i| i ? i.identifier : '' }
- predecessors.each do |pred|
+ dependencies =
+ dependency_store
+ .dependencies_causing_outdatedness_of(item)
+ .sort_by { |dep| dep.from ? dep.from.identifier : '' }
+ dependencies.each do |dep|
+ pred = dep.from
+
type =
case pred
when Nanoc::Int::Layout
@@ -74,13 +89,19 @@ module Nanoc::CLI::Commands
'item'
end
+ props = ''
+ props << (dep.props.raw_content? ? 'r' : '_')
+ props << (dep.props.attributes? ? 'a' : '_')
+ props << (dep.props.compiled_content? ? 'c' : '_')
+ props << (dep.props.path? ? 'p' : '_')
+
if pred
- puts " [ #{format '%6s', type} ] #{pred.identifier}"
+ puts " [ #{format '%6s', type} ] (#{props}) #{pred.identifier}"
else
puts ' ( removed item )'
end
end
- puts ' (nothing)' if predecessors.empty?
+ puts ' (nothing)' if dependencies.empty?
end
end
diff --git a/lib/nanoc/cli/commands/show-plugins.rb b/lib/nanoc/cli/commands/show-plugins.rb
index 7b4911c..c9d15e7 100644
--- a/lib/nanoc/cli/commands/show-plugins.rb
+++ b/lib/nanoc/cli/commands/show-plugins.rb
@@ -72,14 +72,14 @@ module Nanoc::CLI::Commands
PLUGIN_CLASS_ORDER = [
Nanoc::Filter,
Nanoc::DataSource,
- Nanoc::Extra::Deployer,
- ].freeze unless defined? PLUGIN_CLASS_ORDER
+ Nanoc::Deploying::Deployer,
+ ].freeze
PLUGIN_CLASSES = {
Nanoc::Filter => 'Filters',
Nanoc::DataSource => 'Data Sources',
- Nanoc::Extra::Deployer => 'Deployers',
- }.freeze unless defined? PLUGIN_CLASSES
+ Nanoc::Deploying::Deployer => 'Deployers',
+ }.freeze
def name_for_plugin_class(klass)
PLUGIN_CLASSES[klass]
diff --git a/lib/nanoc/cli/commands/show-rules.rb b/lib/nanoc/cli/commands/show-rules.rb
index 58d95ea..8cb8e43 100644
--- a/lib/nanoc/cli/commands/show-rules.rb
+++ b/lib/nanoc/cli/commands/show-rules.rb
@@ -11,7 +11,10 @@ module Nanoc::CLI::Commands
load_site
@c = Nanoc::CLI::ANSIStringColorizer
- @reps = site.compiler.reps
+
+ compiler = site.compiler
+ compiler.build_reps
+ @reps = compiler.reps
action_provider = site.compiler.action_provider
unless action_provider.respond_to?(:rules_collection)
diff --git a/lib/nanoc/cli/commands/view.rb b/lib/nanoc/cli/commands/view.rb
index f625755..2072bab 100644
--- a/lib/nanoc/cli/commands/view.rb
+++ b/lib/nanoc/cli/commands/view.rb
@@ -44,7 +44,9 @@ module Nanoc::CLI::Commands
use Rack::ShowExceptions
use Rack::Lint
use Rack::Head
- use Adsf::Rack::IndexFileFinder, root: site.config[:output_dir]
+ use Adsf::Rack::IndexFileFinder,
+ root: site.config[:output_dir],
+ index_filenames: site.config[:index_filenames]
run Rack::File.new(site.config[:output_dir])
end.to_app
diff --git a/lib/nanoc/cli/error_handler.rb b/lib/nanoc/cli/error_handler.rb
index 79c036c..0f075ad 100644
--- a/lib/nanoc/cli/error_handler.rb
+++ b/lib/nanoc/cli/error_handler.rb
@@ -113,7 +113,7 @@ module Nanoc::CLI
# Sections
write_error_message(stream, error)
- write_compilation_stack(stream, error)
+ write_item_rep(stream, error)
write_stack_trace(stream, error)
# Issue link
@@ -133,7 +133,7 @@ module Nanoc::CLI
# Sections
write_error_message(stream, error, verbose: true)
- write_compilation_stack(stream, error, verbose: true)
+ write_item_rep(stream, error, verbose: true)
write_stack_trace(stream, error, verbose: true)
write_version_information(stream, verbose: true)
write_system_information(stream, verbose: true)
@@ -161,11 +161,6 @@ module Nanoc::CLI
site && site.compiler
end
- # @return [Array] The current compilation stack
- def stack
- (compiler && compiler.stack) || []
- end
-
# @return [Hash<String, Array>] A hash containing the gem names as keys and gem versions as value
def gems_and_versions
gems = {}
@@ -216,6 +211,8 @@ module Nanoc::CLI
#
# @return [String] The resolution for the given error
def resolution_for(error)
+ error = unwrap_error(error)
+
case error
when LoadError
# Get gem name
@@ -257,30 +254,28 @@ module Nanoc::CLI
def write_error_message(stream, error, verbose: false)
write_section_header(stream, 'Message', verbose: verbose)
+ error = unwrap_error(error)
+
stream.puts "#{error.class}: #{error.message}"
resolution = resolution_for(error)
stream.puts resolution.to_s if resolution
end
- def write_compilation_stack(stream, _error, verbose: false)
- write_section_header(stream, 'Compilation stack', verbose: verbose)
+ def write_item_rep(stream, error, verbose: false)
+ return unless error.is_a?(Nanoc::Int::Errors::CompilationError)
- if stack.empty?
- stream.puts ' (empty)'
- else
- stack.reverse_each do |obj|
- if obj.is_a?(Nanoc::Int::ItemRep)
- stream.puts " - [item] #{obj.item.identifier} (rep #{obj.name})"
- else # layout
- stream.puts " - [layout] #{obj.identifier}"
- end
- end
- end
+ write_section_header(stream, 'Item being compiled', verbose: verbose)
+
+ item_rep = error.item_rep
+ stream.puts "Item identifier: #{item_rep.item.identifier}"
+ stream.puts "Item rep name: #{item_rep.name.inspect}"
end
def write_stack_trace(stream, error, verbose: false)
write_section_header(stream, 'Stack trace', verbose: verbose)
+ error = unwrap_error(error)
+
count = verbose ? -1 : 10
error.backtrace[0...count].each_with_index do |item, index|
stream.puts " #{index}. #{item}"
@@ -330,5 +325,14 @@ module Nanoc::CLI
stream.puts " #{index}. #{i}"
end
end
+
+ def unwrap_error(e)
+ case e
+ when Nanoc::Int::Errors::CompilationError
+ e.unwrap
+ else
+ e
+ end
+ end
end
end
diff --git a/lib/nanoc/data_sources/filesystem.rb b/lib/nanoc/data_sources/filesystem.rb
index 5fa6bff..5fdce86 100644
--- a/lib/nanoc/data_sources/filesystem.rb
+++ b/lib/nanoc/data_sources/filesystem.rb
@@ -47,12 +47,10 @@ module Nanoc::DataSources
# @api private
class Filesystem < Nanoc::DataSource
# See {Nanoc::DataSource#up}.
- def up
- end
+ def up; end
# See {Nanoc::DataSource#down}.
- def down
- end
+ def down; end
def content_dir_name
config.fetch(:content_dir, 'content')
@@ -76,11 +74,12 @@ module Nanoc::DataSources
class ProtoDocument
attr_reader :attributes
- attr_reader :checksum_data
+ attr_reader :content_checksum_data
+ attr_reader :attributes_checksum_data
attr_reader :is_binary
alias binary? is_binary
- def initialize(is_binary:, content: nil, filename: nil, attributes:, checksum_data: nil)
+ def initialize(is_binary:, content: nil, filename: nil, attributes:, content_checksum_data: nil, attributes_checksum_data: nil)
if content.nil? && filename.nil?
raise ArgumentError, '#initialize needs at least content or filename'
end
@@ -89,7 +88,8 @@ module Nanoc::DataSources
@content = content
@filename = filename
@attributes = attributes
- @checksum_data = checksum_data
+ @content_checksum_data = content_checksum_data
+ @attributes_checksum_data = attributes_checksum_data
end
def content
@@ -117,7 +117,7 @@ module Nanoc::DataSources
ProtoDocument.new(is_binary: true, filename: content_filename, attributes: meta)
elsif is_binary && klass == Nanoc::Int::Layout
- raise "The layout file '#{content_filename}' is a binary file, but layouts can only be textual"
+ raise Errors::BinaryLayout.new(content_filename)
else
parse_result = parse(content_filename, meta_filename)
@@ -125,7 +125,8 @@ module Nanoc::DataSources
is_binary: false,
content: parse_result.content,
attributes: parse_result.attributes,
- checksum_data: "content=#{parse_result.content},meta=#{parse_result.attributes_data}",
+ content_checksum_data: parse_result.content,
+ attributes_checksum_data: parse_result.attributes_data,
)
end
end
@@ -157,7 +158,13 @@ module Nanoc::DataSources
attributes = attributes_for(proto_doc, content_filename, meta_filename)
identifier = identifier_for(content_filename, meta_filename, dir_name)
- res << klass.new(content, attributes, identifier, checksum_data: proto_doc.checksum_data)
+ res << klass.new(
+ content,
+ attributes,
+ identifier,
+ content_checksum_data: proto_doc.content_checksum_data,
+ attributes_checksum_data: proto_doc.attributes_checksum_data,
+ )
end
end
@@ -232,11 +239,11 @@ module Nanoc::DataSources
# Check number of files per type
unless [0, 1].include?(meta_filenames.size)
- raise "Found #{meta_filenames.size} meta files for #{basename}; expected 0 or 1"
+ raise Errors::MultipleMetaFiles.new(meta_filenames, basename)
end
unless config[:identifier_type] == 'full'
unless [0, 1].include?(content_filenames.size)
- raise "Found #{content_filenames.size} content files for #{basename}; expected 0 or 1"
+ raise Errors::MultipleContentFiles.new(meta_filenames, basename)
end
end
@@ -252,7 +259,7 @@ module Nanoc::DataSources
# Returns all files in the given directory and directories below it.
def all_files_in(dir_name)
- Nanoc::Extra::FilesystemTools.all_files_in(dir_name, config[:extra_files])
+ Nanoc::DataSources::Filesystem::Tools.all_files_in(dir_name, config[:extra_files])
end
# Returns the filename for the given base filename and the extension.
@@ -345,7 +352,7 @@ module Nanoc::DataSources
pieces = data.split(/^(-{5}|-{3})[ \t]*\r?\n?/, 3)
if pieces.size < 4
- raise "The file '#{content_filename}' appears to start with a metadata section (three or five dashes at the top) but it does not seem to be in the correct format."
+ raise Errors::InvalidFormat.new(content_filename)
end
meta = parse_metadata(pieces[2], content_filename)
@@ -358,8 +365,8 @@ module Nanoc::DataSources
def parse_metadata(data, filename)
begin
meta = YAML.load(data) || {}
- rescue Exception => e
- raise "Could not parse YAML for #{filename}: #{e.message}"
+ rescue => e
+ raise Errors::UnparseableMetadata.new(filename, e)
end
verify_meta(meta, filename)
@@ -379,16 +386,10 @@ module Nanoc::DataSources
end
end
- class InvalidMetadataError < Nanoc::Error
- def initialize(filename, klass)
- super("The file #{filename} has invalid metadata (expected key-value pairs, found #{klass} instead)")
- end
- end
-
def verify_meta(meta, filename)
return if meta.is_a?(Hash)
- raise InvalidMetadataError.new(filename, meta.class)
+ raise Errors::InvalidMetadata.new(filename, meta.class)
end
# Reads the content of the file with the given name and returns a string
@@ -400,7 +401,7 @@ module Nanoc::DataSources
begin
data = File.read(filename)
rescue => e
- raise "Could not read #{filename}: #{e.inspect}"
+ raise Errors::FileUnreadable.new(filename, e)
end
# Fix
@@ -415,11 +416,11 @@ module Nanoc::DataSources
begin
data.encode!('UTF-8')
rescue
- raise_encoding_error(filename, original_encoding)
+ raise Errors::InvalidEncoding.new(filename, original_encoding)
end
unless data.valid_encoding?
- raise_encoding_error(filename, original_encoding)
+ raise Errors::InvalidEncoding.new(filename, original_encoding)
end
end
@@ -428,10 +429,8 @@ module Nanoc::DataSources
data
end
-
- # Raises an invalid encoding error for the given filename and encoding.
- def raise_encoding_error(filename, encoding)
- raise "Could not read #{filename} because the file is not valid #{encoding}."
- end
end
end
+
+require_relative 'filesystem/tools'
+require_relative 'filesystem/errors'
diff --git a/lib/nanoc/data_sources/filesystem/errors.rb b/lib/nanoc/data_sources/filesystem/errors.rb
new file mode 100644
index 0000000..99e3ed6
--- /dev/null
+++ b/lib/nanoc/data_sources/filesystem/errors.rb
@@ -0,0 +1,55 @@
+class Nanoc::DataSources::Filesystem < Nanoc::DataSource
+ # @api private
+ module Errors
+ class Generic < ::Nanoc::Error
+ end
+
+ class BinaryLayout < Generic
+ def initialize(content_filename)
+ super("The layout file '#{content_filename}' is a binary file, but layouts can only be textual")
+ end
+ end
+
+ class MultipleMetaFiles < Generic
+ def initialize(meta_filenames, basename)
+ super("Found #{meta_filenames.size} meta files for #{basename}; expected 0 or 1")
+ end
+ end
+
+ class MultipleContentFiles < Generic
+ def initialize(content_filenames, basename)
+ super("Found #{content_filenames.size} content files for #{basename}; expected 0 or 1")
+ end
+ end
+
+ class InvalidFormat < Generic
+ def initialize(content_filename)
+ super("The file '#{content_filename}' appears to start with a metadata section (three or five dashes at the top) but it does not seem to be in the correct format.")
+ end
+ end
+
+ class UnparseableMetadata < Generic
+ def initialize(filename, error)
+ super("Could not parse metadata for #{filename}: #{error.message}")
+ end
+ end
+
+ class InvalidMetadata < Generic
+ def initialize(filename, klass)
+ super("The file #{filename} has invalid metadata (expected key-value pairs, found #{klass} instead)")
+ end
+ end
+
+ class InvalidEncoding < Generic
+ def initialize(filename, encoding)
+ super("Could not read #{filename} because the file is not valid #{encoding}.")
+ end
+ end
+
+ class FileUnreadable < Generic
+ def initialize(filename, error)
+ super("Could not read #{filename}: #{error.inspect}")
+ end
+ end
+ end
+end
diff --git a/lib/nanoc/extra/filesystem_tools.rb b/lib/nanoc/data_sources/filesystem/tools.rb
similarity index 89%
rename from lib/nanoc/extra/filesystem_tools.rb
rename to lib/nanoc/data_sources/filesystem/tools.rb
index 66221cc..d9adf37 100644
--- a/lib/nanoc/extra/filesystem_tools.rb
+++ b/lib/nanoc/data_sources/filesystem/tools.rb
@@ -1,8 +1,8 @@
-module Nanoc::Extra
+class Nanoc::DataSources::Filesystem < Nanoc::DataSource
# Contains useful functions for managing the filesystem.
#
# @api private
- module FilesystemTools
+ module Tools
# Error that is raised when too many symlink indirections are encountered.
class MaxSymlinkDepthExceededError < ::Nanoc::Int::Errors::GenericTrivial
# @return [String] The last filename that was attempted to be
@@ -92,19 +92,24 @@ module Nanoc::Extra
#
# @raise [GenericTrivial] when pattern can not be handled
def all_files_and_dirs_in(dir_name, extra_files)
- patterns = ["#{dir_name}/**/*"]
- case extra_files
- when nil
- when String
- patterns << "#{dir_name}/#{extra_files}"
- when Array
- patterns.concat(extra_files.map { |extra_file| "#{dir_name}/#{extra_file}" })
- else
- raise(
- Nanoc::Int::Errors::GenericTrivial,
- "Do not know how to handle extra_files: #{extra_files.inspect}",
- )
- end
+ base_patterns = ["#{dir_name}/**/*"]
+
+ extra_patterns =
+ case extra_files
+ when nil
+ []
+ when String
+ ["#{dir_name}/#{extra_files}"]
+ when Array
+ extra_files.map { |extra_file| "#{dir_name}/#{extra_file}" }
+ else
+ raise(
+ Nanoc::Int::Errors::GenericTrivial,
+ "Do not know how to handle extra_files: #{extra_files.inspect}",
+ )
+ end
+
+ patterns = base_patterns + extra_patterns
Dir.glob(patterns)
end
module_function :all_files_and_dirs_in
diff --git a/lib/nanoc/deploying.rb b/lib/nanoc/deploying.rb
new file mode 100644
index 0000000..d2b756b
--- /dev/null
+++ b/lib/nanoc/deploying.rb
@@ -0,0 +1,8 @@
+module Nanoc
+ # @api private
+ module Deploying
+ end
+end
+
+require 'nanoc/deploying/deployer'
+require 'nanoc/deploying/deployers'
diff --git a/lib/nanoc/extra/deployer.rb b/lib/nanoc/deploying/deployer.rb
similarity index 91%
rename from lib/nanoc/extra/deployer.rb
rename to lib/nanoc/deploying/deployer.rb
index 7dd3d85..ff3a9e1 100644
--- a/lib/nanoc/extra/deployer.rb
+++ b/lib/nanoc/deploying/deployer.rb
@@ -1,4 +1,4 @@
-module Nanoc::Extra
+module Nanoc::Deploying
# Represents a deployer, an object that allows uploading the compiled site
# to a specific (remote) location.
#
@@ -37,7 +37,7 @@ module Nanoc::Extra
#
# @abstract
def run
- raise NotImplementedError.new('Nanoc::Extra::Deployer subclasses must implement #run')
+ raise NotImplementedError.new('Nanoc::Deploying::Deployer subclasses must implement #run')
end
end
end
diff --git a/lib/nanoc/deploying/deployers.rb b/lib/nanoc/deploying/deployers.rb
new file mode 100644
index 0000000..3965363
--- /dev/null
+++ b/lib/nanoc/deploying/deployers.rb
@@ -0,0 +1,10 @@
+module Nanoc::Deploying
+ # @api private
+ module Deployers
+ autoload 'Fog', 'nanoc/deploying/deployers/fog'
+ autoload 'Rsync', 'nanoc/deploying/deployers/rsync'
+
+ Nanoc::Deploying::Deployer.register '::Nanoc::Deploying::Deployers::Fog', :fog
+ Nanoc::Deploying::Deployer.register '::Nanoc::Deploying::Deployers::Rsync', :rsync
+ end
+end
diff --git a/lib/nanoc/extra/deployers/fog.rb b/lib/nanoc/deploying/deployers/fog.rb
similarity index 97%
rename from lib/nanoc/extra/deployers/fog.rb
rename to lib/nanoc/deploying/deployers/fog.rb
index aef6253..8b97874 100644
--- a/lib/nanoc/extra/deployers/fog.rb
+++ b/lib/nanoc/deploying/deployers/fog.rb
@@ -1,4 +1,4 @@
-module Nanoc::Extra::Deployers
+module Nanoc::Deploying::Deployers
# A deployer that deploys a site using [fog](https://github.com/geemus/fog).
#
# @example A deployment configuration with public and staging configurations
@@ -20,7 +20,7 @@ module Nanoc::Extra::Deployers
# bucket: nanoc-site-staging
#
# @api private
- class Fog < ::Nanoc::Extra::Deployer
+ class Fog < ::Nanoc::Deploying::Deployer
class FogWrapper
def initialize(directory, is_dry_run)
@directory = directory
@@ -75,7 +75,7 @@ module Nanoc::Extra::Deployers
end
end
- # @see Nanoc::Extra::Deployer#run
+ # @see Nanoc::Deploying::Deployer#run
def run
require 'fog'
diff --git a/lib/nanoc/extra/deployers/rsync.rb b/lib/nanoc/deploying/deployers/rsync.rb
similarity index 93%
rename from lib/nanoc/extra/deployers/rsync.rb
rename to lib/nanoc/deploying/deployers/rsync.rb
index 3016807..b7c037a 100644
--- a/lib/nanoc/extra/deployers/rsync.rb
+++ b/lib/nanoc/deploying/deployers/rsync.rb
@@ -1,4 +1,4 @@
-module Nanoc::Extra::Deployers
+module Nanoc::Deploying::Deployers
# A deployer that deploys a site using rsync.
#
# The configuration has should include a `:dst` value, a string containing
@@ -18,7 +18,7 @@ module Nanoc::Extra::Deployers
# options: [ "-glpPrtvz" ]
#
# @api private
- class Rsync < ::Nanoc::Extra::Deployer
+ class Rsync < ::Nanoc::Deploying::Deployer
# Default rsync options
DEFAULT_OPTIONS = [
'--group',
@@ -35,7 +35,7 @@ module Nanoc::Extra::Deployers
'--exclude=".git"',
].freeze
- # @see Nanoc::Extra::Deployer#run
+ # @see Nanoc::Deploying::Deployer#run
def run
# Get params
src = source_path + '/'
diff --git a/lib/nanoc/extra.rb b/lib/nanoc/extra.rb
index b2cd153..1b98718 100644
--- a/lib/nanoc/extra.rb
+++ b/lib/nanoc/extra.rb
@@ -1,13 +1,21 @@
+require 'nanoc/checking'
+require 'nanoc/deploying'
+
# @api private
module Nanoc::Extra
- autoload 'Checking', 'nanoc/extra/checking'
- autoload 'FilesystemTools', 'nanoc/extra/filesystem_tools'
autoload 'LinkCollector', 'nanoc/extra/link_collector.rb'
- autoload 'Pruner', 'nanoc/extra/pruner'
autoload 'Piper', 'nanoc/extra/piper'
autoload 'JRubyNokogiriWarner', 'nanoc/extra/jruby_nokogiri_warner'
+
+ # @deprecated
+ Checking = Nanoc::Checking
+
+ # @deprecated
+ Deployer = Nanoc::Deploying::Deployer
+
+ # @deprecated
+ Pruner = Nanoc::Pruner
end
require 'nanoc/extra/core_ext'
-require 'nanoc/extra/deployer'
-require 'nanoc/extra/deployers'
+require 'nanoc/extra/parallel_collection'
diff --git a/lib/nanoc/extra/checking.rb b/lib/nanoc/extra/checking.rb
deleted file mode 100644
index 1928031..0000000
--- a/lib/nanoc/extra/checking.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-module Nanoc::Extra
- # @api private
- module Checking
- autoload 'Check', 'nanoc/extra/checking/check'
- autoload 'DSL', 'nanoc/extra/checking/dsl'
- autoload 'Runner', 'nanoc/extra/checking/runner.rb'
- autoload 'Issue', 'nanoc/extra/checking/issue'
- end
-end
-
-require 'nanoc/extra/checking/checks'
diff --git a/lib/nanoc/extra/checking/checks.rb b/lib/nanoc/extra/checking/checks.rb
deleted file mode 100644
index 7821b72..0000000
--- a/lib/nanoc/extra/checking/checks.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-require_relative 'checks/w3c_validator'
-
-# @api private
-module Nanoc::Extra::Checking::Checks
- autoload 'CSS', 'nanoc/extra/checking/checks/css'
- autoload 'ExternalLinks', 'nanoc/extra/checking/checks/external_links'
- autoload 'HTML', 'nanoc/extra/checking/checks/html'
- autoload 'InternalLinks', 'nanoc/extra/checking/checks/internal_links'
- autoload 'Stale', 'nanoc/extra/checking/checks/stale'
- autoload 'MixedContent', 'nanoc/extra/checking/checks/mixed_content'
-
- Nanoc::Extra::Checking::Check.register '::Nanoc::Extra::Checking::Checks::CSS', :css
- Nanoc::Extra::Checking::Check.register '::Nanoc::Extra::Checking::Checks::ExternalLinks', :external_links
- Nanoc::Extra::Checking::Check.register '::Nanoc::Extra::Checking::Checks::ExternalLinks', :elinks
- Nanoc::Extra::Checking::Check.register '::Nanoc::Extra::Checking::Checks::HTML', :html
- Nanoc::Extra::Checking::Check.register '::Nanoc::Extra::Checking::Checks::InternalLinks', :internal_links
- Nanoc::Extra::Checking::Check.register '::Nanoc::Extra::Checking::Checks::InternalLinks', :ilinks
- Nanoc::Extra::Checking::Check.register '::Nanoc::Extra::Checking::Checks::Stale', :stale
- Nanoc::Extra::Checking::Check.register '::Nanoc::Extra::Checking::Checks::MixedContent', :mixed_content
-end
diff --git a/lib/nanoc/extra/core_ext/time.rb b/lib/nanoc/extra/core_ext/time.rb
index 6973c7a..b37e2cf 100644
--- a/lib/nanoc/extra/core_ext/time.rb
+++ b/lib/nanoc/extra/core_ext/time.rb
@@ -2,7 +2,7 @@
module Nanoc::Extra::TimeExtensions
# @return [String] The time in an ISO-8601 date format.
def __nanoc_to_iso8601_date
- strftime('%Y-%m-%d')
+ getutc.strftime('%Y-%m-%d')
end
# @return [String] The time in an ISO-8601 time format.
diff --git a/lib/nanoc/extra/deployers.rb b/lib/nanoc/extra/deployers.rb
deleted file mode 100644
index 56c2309..0000000
--- a/lib/nanoc/extra/deployers.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-module Nanoc::Extra
- # @api private
- module Deployers
- autoload 'Fog', 'nanoc/extra/deployers/fog'
- autoload 'Rsync', 'nanoc/extra/deployers/rsync'
-
- Nanoc::Extra::Deployer.register '::Nanoc::Extra::Deployers::Fog', :fog
- Nanoc::Extra::Deployer.register '::Nanoc::Extra::Deployers::Rsync', :rsync
- end
-end
diff --git a/lib/nanoc/extra/parallel_collection.rb b/lib/nanoc/extra/parallel_collection.rb
new file mode 100644
index 0000000..43c856d
--- /dev/null
+++ b/lib/nanoc/extra/parallel_collection.rb
@@ -0,0 +1,57 @@
+require 'thread'
+
+module Nanoc::Extra
+ # @api private
+ class ParallelCollection
+ STOP = Object.new
+
+ include Nanoc::Int::ContractsSupport
+
+ contract C::RespondTo[:each], C::KeywordArgs[parallelism: Fixnum] => C::Any
+ def initialize(enum, parallelism: 2)
+ @enum = enum
+ @parallelism = parallelism
+ end
+
+ contract C::Func[C::Any => C::Any] => self
+ def each
+ queue = SizedQueue.new(2 * @parallelism)
+ error = nil
+
+ threads = (1.. at parallelism).map do
+ Thread.new do
+ loop do
+ begin
+ elem = queue.pop
+ break if error
+ break if STOP.equal?(elem)
+ yield elem
+ rescue => err
+ error = err
+ break
+ end
+ end
+ end
+ end
+
+ @enum.each { |e| queue << e }
+ @parallelism.times { queue << STOP }
+
+ threads.each(&:join)
+
+ raise error if error
+ self
+ end
+
+ contract C::Func[C::Any => C::Any] => C::RespondTo[:each]
+ def map
+ [].tap do |all|
+ mutex = Mutex.new
+ each do |e|
+ res = yield(e)
+ mutex.synchronize { all << res }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/nanoc/extra/pruner.rb b/lib/nanoc/extra/pruner.rb
deleted file mode 100644
index a9059a5..0000000
--- a/lib/nanoc/extra/pruner.rb
+++ /dev/null
@@ -1,87 +0,0 @@
-module Nanoc::Extra
- # Responsible for finding and deleting files in the site’s output directory
- # that are not managed by Nanoc.
- #
- # @api private
- class Pruner
- # @return [Nanoc::Int::Site] The site this pruner belongs to
- attr_reader :site
-
- # @param [Nanoc::Int::Site] site The site for which a pruner is created
- #
- # @param [Boolean] dry_run true if the files to be deleted
- # should only be printed instead of actually deleted, false if the files
- # should actually be deleted.
- #
- # @param [Enumerable<String>] exclude
- def initialize(site, dry_run: false, exclude: [])
- @site = site
- @dry_run = dry_run
- @exclude = exclude
- end
-
- # Prunes all output files not managed by Nanoc.
- #
- # @return [void]
- def run
- require 'find'
-
- return unless File.directory?(site.config[:output_dir])
-
- # Get compiled files
- # FIXME: requires #build_reps to have been called
- all_raw_paths = site.compiler.reps.flat_map { |r| r.raw_paths.values }
- compiled_files = all_raw_paths.flatten.compact.select { |f| File.file?(f) }
-
- # Get present files and dirs
- present_files = []
- present_dirs = []
- Find.find(site.config[:output_dir] + '/') do |f|
- present_files << f if File.file?(f)
- present_dirs << f if File.directory?(f)
- end
-
- # Remove stray files
- stray_files = (present_files - compiled_files)
- stray_files.each do |f|
- next if filename_excluded?(f)
- delete_file(f)
- end
-
- # Remove empty directories
- present_dirs.reverse_each do |dir|
- next if Dir.foreach(dir) { |n| break true if n !~ /\A\.\.?\z/ }
- next if filename_excluded?(dir)
- delete_dir(dir)
- end
- end
-
- # @param [String] filename The filename to check
- #
- # @return [Boolean] true if the given file is excluded, false otherwise
- def filename_excluded?(filename)
- pathname = Pathname.new(filename)
- @exclude.any? { |e| pathname.__nanoc_include_component?(e) }
- end
-
- protected
-
- def delete_file(file)
- if @dry_run
- puts file
- else
- Nanoc::CLI::Logger.instance.file(:high, :delete, file)
- FileUtils.rm(file)
- end
- end
-
- def delete_dir(dir)
- if @dry_run
- puts dir
- else
- Nanoc::CLI::Logger.instance.file(:high, :delete, dir)
- Dir.rmdir(dir)
- end
- end
- end
-end
diff --git a/lib/nanoc/filters/asciidoc.rb b/lib/nanoc/filters/asciidoc.rb
index ba5908d..de983fc 100644
--- a/lib/nanoc/filters/asciidoc.rb
+++ b/lib/nanoc/filters/asciidoc.rb
@@ -1,6 +1,4 @@
module Nanoc::Filters
- # @since 3.2.0
- #
# @api private
class AsciiDoc < Nanoc::Filter
# Runs the content through [AsciiDoc](http://www.methods.co.nz/asciidoc/).
diff --git a/lib/nanoc/filters/coffeescript.rb b/lib/nanoc/filters/coffeescript.rb
index 687bdcc..cb0356a 100644
--- a/lib/nanoc/filters/coffeescript.rb
+++ b/lib/nanoc/filters/coffeescript.rb
@@ -1,6 +1,4 @@
module Nanoc::Filters
- # @since 3.3.0
- #
# @api private
class CoffeeScript < Nanoc::Filter
requires 'coffee-script'
diff --git a/lib/nanoc/filters/colorize_syntax.rb b/lib/nanoc/filters/colorize_syntax.rb
index 566b4d2..d5ca8fa 100644
--- a/lib/nanoc/filters/colorize_syntax.rb
+++ b/lib/nanoc/filters/colorize_syntax.rb
@@ -250,8 +250,6 @@ module Nanoc::Filters
# Runs the content through [Highlight](http://www.andre-simon.de/doku/highlight/en/highlight.html).
#
- # @since 3.2.0
- #
# @param [String] code The code to colorize
#
# @param [String] language The language the code is written in
@@ -312,10 +310,21 @@ module Nanoc::Filters
def rouge(code, language, params = {})
require 'rouge'
- formatter_options = {
- css_class: params.fetch(:css_class, 'highlight'),
- }
- formatter = Rouge::Formatters::HTML.new(formatter_options)
+ if Rouge.version < '2' || params.fetch(:legacy, false)
+ # Rouge 1.x or Rouge 2.x legacy options
+ formatter_options = {
+ css_class: params.fetch(:css_class, 'highlight'),
+ inline_theme: params.fetch(:inline_theme, nil),
+ line_numbers: params.fetch(:line_numbers, false),
+ start_line: params.fetch(:start_line, 1),
+ wrap: params.fetch(:wrap, false),
+ }
+ formatter_cls = Rouge::Formatters.const_get(Rouge.version < '2' ? 'HTML' : 'HTMLLegacy')
+ formatter = formatter_cls.new(formatter_options)
+ else
+ formatter = params.fetch(:formatter, Rouge::Formatters::HTML.new)
+ end
+
lexer = Rouge::Lexer.find_fancy(language, code) || Rouge::Lexers::PlainText
formatter.format(lexer.lex(code))
end
diff --git a/lib/nanoc/filters/handlebars.rb b/lib/nanoc/filters/handlebars.rb
index 5768491..f6be6ab 100644
--- a/lib/nanoc/filters/handlebars.rb
+++ b/lib/nanoc/filters/handlebars.rb
@@ -1,6 +1,4 @@
module Nanoc::Filters
- # @since 3.4.0
- #
# @api private
class Handlebars < Nanoc::Filter
requires 'handlebars'
diff --git a/lib/nanoc/filters/kramdown.rb b/lib/nanoc/filters/kramdown.rb
index caa63c7..eb8c49a 100644
--- a/lib/nanoc/filters/kramdown.rb
+++ b/lib/nanoc/filters/kramdown.rb
@@ -10,10 +10,22 @@ module Nanoc::Filters
#
# @return [String] The filtered content
def run(content, params = {})
+ params = params.dup
+ warning_filters = params.delete(:warning_filters)
document = ::Kramdown::Document.new(content, params)
- document.warnings.each do |warning|
- $stderr.puts "kramdown warning: #{warning}"
+ if warning_filters
+ r = Regexp.union(warning_filters)
+ warnings = document.warnings.reject { |warning| r =~ warning }
+ else
+ warnings = document.warnings
+ end
+
+ if warnings.any?
+ $stderr.puts "kramdown warning(s) for #{@item_rep.inspect}"
+ warnings.each do |warning|
+ $stderr.puts " #{warning}"
+ end
end
document.to_html
diff --git a/lib/nanoc/filters/less.rb b/lib/nanoc/filters/less.rb
index b7bd874..5479c21 100644
--- a/lib/nanoc/filters/less.rb
+++ b/lib/nanoc/filters/less.rb
@@ -10,41 +10,65 @@ module Nanoc::Filters
#
# @return [String] The filtered content
def run(content, params = {})
- # Find imports (hacky)
+ # Create dependencies
+ imported_filenames = imported_filenames_from(content)
+ imported_items = imported_filenames_to_items(imported_filenames)
+ depend_on(imported_items)
+
+ # Add filename to load path
+ paths = [File.dirname(@item[:content_filename])]
+ on_main_fiber do
+ parser = ::Less::Parser.new(paths: paths)
+ parser.parse(content).to_css(params)
+ end
+ end
+
+ def imported_filenames_from(content)
imports = []
imports.concat(content.scan(/^@import\s+(["'])([^\1]+?)\1;/))
imports.concat(content.scan(/^@import\s+url\((["']?)([^)]+?)\1\);/))
- imported_filenames = imports.map do |i|
- i[1] =~ /\.(less|css)$/ ? i[1] : i[1] + '.less'
- end
- # Convert to items
- imported_items = imported_filenames.map do |filename|
- # Find directory for this item
- current_dir_pathname = Pathname.new(@item[:content_filename]).dirname.realpath
+ imports.map { |i| i[1] =~ /\.(less|css)$/ ? i[1] : i[1] + '.less' }
+ end
- # Find absolute pathname for imported item
- imported_pathname = Pathname.new(filename)
- if imported_pathname.relative?
- imported_pathname = current_dir_pathname + imported_pathname
- end
- next unless imported_pathname.exist?
- imported_filename = imported_pathname.realpath
+ def imported_filenames_to_items(imported_filenames)
+ item_dir_path = Pathname.new(@item[:content_filename]).dirname.realpath
+ cwd = Pathname.pwd # FIXME: ugly (get site dir instead)
+
+ imported_filenames.map do |filename|
+ full_paths = Set.new
+
+ imported_pathname = Pathname.new(filename)
+ full_paths << find_file(imported_pathname, item_dir_path)
+ full_paths << find_file(imported_pathname, cwd)
# Find matching item
@items.find do |i|
next if i[:content_filename].nil?
- Pathname.new(i[:content_filename]).realpath == imported_filename
+ item_path = Pathname.new(i[:content_filename]).realpath
+ full_paths.any? { |fp| fp == item_path }
end
end.compact
+ end
- # Create dependencies
- depend_on(imported_items)
+ # @param [Pathname] pathname Pathname of the file to find. Can be relative or absolute.
+ #
+ # @param [Pathname] root_pathname Directory pathname from which the search will start.
+ #
+ # @return [String, nil] A string containing the full path if a file is found, otherwise nil.
+ def find_file(pathname, root_pathname)
+ absolute_pathname =
+ if pathname.relative?
+ root_pathname + pathname
+ else
+ pathname
+ end
- # Add filename to load path
- paths = [File.dirname(@item[:content_filename])]
- parser = ::Less::Parser.new(paths: paths)
- parser.parse(content).to_css params
+ if absolute_pathname.exist?
+ absolute_pathname.realpath
+ else
+ nil
+ end
end
end
end
diff --git a/lib/nanoc/filters/mustache.rb b/lib/nanoc/filters/mustache.rb
index 597929c..e6cf28d 100644
--- a/lib/nanoc/filters/mustache.rb
+++ b/lib/nanoc/filters/mustache.rb
@@ -1,6 +1,4 @@
module Nanoc::Filters
- # @since 3.2.0
- #
# @api private
class Mustache < Nanoc::Filter
requires 'mustache'
@@ -13,7 +11,7 @@ module Nanoc::Filters
#
# @return [String] The filtered content
def run(content, _params = {})
- context = item.attributes.merge({ yield: assigns[:content] })
+ context = item.attributes.merge(yield: assigns[:content])
::Mustache.render(content, context)
end
end
diff --git a/lib/nanoc/filters/rdoc.rb b/lib/nanoc/filters/rdoc.rb
index bb65b3e..47012b4 100644
--- a/lib/nanoc/filters/rdoc.rb
+++ b/lib/nanoc/filters/rdoc.rb
@@ -3,11 +3,6 @@ module Nanoc::Filters
class RDoc < Nanoc::Filter
requires 'rdoc'
- def self.setup
- gem 'rdoc', '~> 4.0'
- super
- end
-
# Runs the content through [RDoc::Markup](http://docs.seattlerb.org/rdoc/RDoc/Markup.html).
# This method takes no options.
#
diff --git a/lib/nanoc/filters/redcarpet.rb b/lib/nanoc/filters/redcarpet.rb
index 0295c7c..6cacd75 100644
--- a/lib/nanoc/filters/redcarpet.rb
+++ b/lib/nanoc/filters/redcarpet.rb
@@ -1,6 +1,4 @@
module Nanoc::Filters
- # @since 3.2.0
- #
# @api private
class Redcarpet < Nanoc::Filter
requires 'redcarpet'
@@ -23,8 +21,6 @@ module Nanoc::Filters
#
# For Redcarpet 2.x
#
- # @since 3.2.4
- #
# @param [String] content The content to filter
#
# @option params [Hash] :options ({}) A list of options to pass on to
diff --git a/lib/nanoc/filters/relativize_paths.rb b/lib/nanoc/filters/relativize_paths.rb
index 8e296f3..d5bdd12 100644
--- a/lib/nanoc/filters/relativize_paths.rb
+++ b/lib/nanoc/filters/relativize_paths.rb
@@ -4,7 +4,7 @@ module Nanoc::Filters
require 'nanoc/helpers/link_to'
include Nanoc::Helpers::LinkTo
- SELECTORS = ['*/@href', '*/@src', 'object/@data', 'param[@name="movie"]/@content', 'comment()'].freeze
+ SELECTORS = ['*/@href', '*/@src', 'object/@data', 'param[@name="movie"]/@content', 'form/@action', 'comment()'].freeze
# Relativizes all paths in the given content, which can be HTML, XHTML, XML
# or CSS. This filter is quite useful if a site needs to be hosted in a
diff --git a/lib/nanoc/filters/slim.rb b/lib/nanoc/filters/slim.rb
index 7040fef..654350e 100644
--- a/lib/nanoc/filters/slim.rb
+++ b/lib/nanoc/filters/slim.rb
@@ -1,6 +1,4 @@
module Nanoc::Filters
- # @since 3.2.0
- #
# @api private
class Slim < Nanoc::Filter
requires 'slim'
diff --git a/lib/nanoc/filters/typogruby.rb b/lib/nanoc/filters/typogruby.rb
index 30e6b05..0e5026f 100644
--- a/lib/nanoc/filters/typogruby.rb
+++ b/lib/nanoc/filters/typogruby.rb
@@ -1,6 +1,4 @@
module Nanoc::Filters
- # @since 3.2.0
- #
# @api private
class Typogruby < Nanoc::Filter
requires 'typogruby'
diff --git a/lib/nanoc/filters/xsl.rb b/lib/nanoc/filters/xsl.rb
index a3dbc25..af863be 100644
--- a/lib/nanoc/filters/xsl.rb
+++ b/lib/nanoc/filters/xsl.rb
@@ -1,6 +1,4 @@
module Nanoc::Filters
- # @since 3.3.0
- #
# @api private
class XSL < Nanoc::Filter
requires 'nokogiri'
diff --git a/lib/nanoc/filters/yui_compressor.rb b/lib/nanoc/filters/yui_compressor.rb
index dc572aa..b589087 100644
--- a/lib/nanoc/filters/yui_compressor.rb
+++ b/lib/nanoc/filters/yui_compressor.rb
@@ -1,6 +1,4 @@
module Nanoc::Filters
- # @since 3.3.0
- #
# @api private
class YUICompressor < Nanoc::Filter
requires 'yuicompressor'
diff --git a/lib/nanoc/helpers/blogging.rb b/lib/nanoc/helpers/blogging.rb
index 867d65c..a19aa79 100644
--- a/lib/nanoc/helpers/blogging.rb
+++ b/lib/nanoc/helpers/blogging.rb
@@ -72,6 +72,10 @@ module Nanoc::Helpers
sorted_relevant_articles.first
end
+ def updated
+ relevant_articles.map { |a| attribute_to_time(a[:updated_at] || a[:created_at]) }.max
+ end
+
def validate_config
if @config[:base_url].nil?
raise Nanoc::Int::Errors::GenericTrivial.new('Cannot build Atom feed: site configuration has no base_url')
@@ -109,7 +113,7 @@ module Nanoc::Helpers
xml.title title
# Add date
- xml.updated(attribute_to_time(last_article[:created_at]).__nanoc_to_iso8601_time)
+ xml.updated(updated.__nanoc_to_iso8601_time)
# Add links
xml.link(rel: 'alternate', href: root_url)
@@ -244,7 +248,7 @@ module Nanoc::Helpers
when DateTime
arg.to_time
when Date
- Time.local(arg.year, arg.month, arg.day)
+ Time.utc(arg.year, arg.month, arg.day)
when String
Time.parse(arg)
else
diff --git a/lib/nanoc/helpers/breadcrumbs.rb b/lib/nanoc/helpers/breadcrumbs.rb
index c30eb0a..26908e3 100644
--- a/lib/nanoc/helpers/breadcrumbs.rb
+++ b/lib/nanoc/helpers/breadcrumbs.rb
@@ -1,31 +1,25 @@
module Nanoc::Helpers
# @see http://nanoc.ws/doc/reference/helpers/#breadcrumbs
module Breadcrumbs
- class CannotGetBreadcrumbsForNonLegacyItem < Nanoc::Int::Errors::Generic
- def initialize(identifier)
- super("You cannot build a breadcrumbs trail for an item that has a “full” identifier (#{identifier}). Doing so is only possible for items that have a legacy identifier.")
- end
- end
-
# @return [Array]
def breadcrumbs_trail
- unless @item.identifier.legacy?
- raise CannotGetBreadcrumbsForNonLegacyItem.new(@item.identifier)
- end
+ # e.g. ['', '/foo', '/foo/bar']
+ components = item.identifier.components
+ prefixes = components.inject(['']) { |acc, elem| acc + [acc.last + '/' + elem] }
- trail = []
- idx_start = 0
-
- loop do
- idx = @item.identifier.to_s.index('/', idx_start)
- break if idx.nil?
-
- idx_start = idx + 1
- identifier = @item.identifier.to_s[0..idx]
- trail << @items[identifier]
+ if @item.identifier.legacy?
+ prefixes.map { |pr| @items[Nanoc::Identifier.new('/' + pr, type: :legacy)] }
+ else
+ prefixes
+ .reject { |pr| pr =~ /^\/index\./ }
+ .map do |pr|
+ if pr == ''
+ @items['/index.*']
+ else
+ @items[Nanoc::Identifier.new(pr).without_ext + '.*']
+ end
+ end
end
-
- trail
end
end
end
diff --git a/lib/nanoc/helpers/capturing.rb b/lib/nanoc/helpers/capturing.rb
index 3a1df2b..cc0ded7 100644
--- a/lib/nanoc/helpers/capturing.rb
+++ b/lib/nanoc/helpers/capturing.rb
@@ -27,30 +27,32 @@ module Nanoc::Helpers
existing_behavior = params.fetch(:existing, :error)
# Capture
- content = capture(&block)
+ content_string = capture(&block)
- # Prepare for store
+ # Get existing contents and prep for store
snapshot_contents = @item.reps[:default].unwrap.snapshot_contents
capture_name = "__capture_#{name}".to_sym
- case existing_behavior
- when :overwrite
- snapshot_contents[capture_name] = ''
- when :append
- snapshot_contents[capture_name] ||= ''
- when :error
- if snapshot_contents[capture_name] && snapshot_contents[capture_name] != content
- # FIXME: get proper exception
- raise "a capture named #{name.inspect} for #{@item.identifier} already exists"
+ old_content_string =
+ case existing_behavior
+ when :overwrite
+ ''
+ when :append
+ c = snapshot_contents[capture_name]
+ c ? c.string : ''
+ when :error
+ if snapshot_contents[capture_name] && snapshot_contents[capture_name].string != content_string
+ # FIXME: get proper exception
+ raise "a capture named #{name.inspect} for #{@item.identifier} already exists"
+ else
+ ''
+ end
else
- snapshot_contents[capture_name] = ''
+ raise ArgumentError, 'expected :existing_behavior param to #content_for to be one of ' \
+ ":overwrite, :append, or :error, but #{existing_behavior.inspect} was given"
end
- else
- raise ArgumentError, 'expected :existing_behavior param to #content_for to be one of ' \
- ":overwrite, :append, or :error, but #{existing_behavior.inspect} was given"
- end
# Store
- snapshot_contents[capture_name] << content
+ snapshot_contents[capture_name] = Nanoc::Int::TextualContent.new(old_content_string + content_string)
else # Get content
if args.size != 2
raise ArgumentError, 'expected 2 arguments (the item ' \
@@ -64,14 +66,16 @@ module Nanoc::Helpers
# Create dependency
if @item.nil? || item != @item.unwrap
dependency_tracker = @config._context.dependency_tracker
- dependency_tracker.bounce(item.unwrap)
+ dependency_tracker.bounce(item.unwrap, compiled_content: true)
unless rep.compiled?
- raise Nanoc::Int::Errors::UnmetDependency.new(rep)
+ Fiber.yield(Nanoc::Int::Errors::UnmetDependency.new(rep))
+ return content_for(*args, &block)
end
end
- rep.snapshot_contents["__capture_#{name}".to_sym]
+ content = rep.snapshot_contents["__capture_#{name}".to_sym]
+ content ? content.string : nil
end
end
@@ -92,7 +96,7 @@ module Nanoc::Helpers
# Depending on how the filter outputs, the result might be a
# single string or an array of strings (slim outputs the latter).
- erbout_addition = erbout_addition.join if erbout_addition.is_a? Array
+ erbout_addition = erbout_addition.join('') if erbout_addition.is_a? Array
# Done.
erbout_addition
diff --git a/lib/nanoc/helpers/link_to.rb b/lib/nanoc/helpers/link_to.rb
index cfe2027..c7aad56 100644
--- a/lib/nanoc/helpers/link_to.rb
+++ b/lib/nanoc/helpers/link_to.rb
@@ -78,15 +78,11 @@ module Nanoc::Helpers
# Calculate the relative path (method depends on whether destination is
# a directory or not).
- relative_path =
- if src_path.to_s[-1, 1] != '/'
- dst_path.relative_path_from(src_path.dirname).to_s
- else
- dst_path.relative_path_from(src_path).to_s
- end
+ from = src_path.to_s.end_with?('/') ? src_path : src_path.dirname
+ relative_path = dst_path.relative_path_from(from).to_s
# Add trailing slash if necessary
- if dst_path.to_s[-1, 1] == '/'
+ if dst_path.to_s.end_with?('/')
relative_path << '/'
end
diff --git a/lib/nanoc/helpers/rendering.rb b/lib/nanoc/helpers/rendering.rb
index 49822f7..87b385c 100644
--- a/lib/nanoc/helpers/rendering.rb
+++ b/lib/nanoc/helpers/rendering.rb
@@ -20,7 +20,7 @@ module Nanoc::Helpers
# Visit
dependency_tracker = @config._context.dependency_tracker
- dependency_tracker.bounce(layout)
+ dependency_tracker.bounce(layout, raw_content: true)
# Capture content, if any
captured_content = block_given? ? capture(&block) : nil
@@ -37,7 +37,7 @@ module Nanoc::Helpers
}.merge(other_assigns)
# Get filter name
- filter_name, filter_args = *@config._context.compiler.filter_name_and_args_for_layout(layout)
+ filter_name, filter_args = *@config._context.compilation_context.filter_name_and_args_for_layout(layout)
raise Nanoc::Int::Errors::CannotDetermineFilter.new(layout.identifier) if filter_name.nil?
# Get filter class
@@ -47,28 +47,20 @@ module Nanoc::Helpers
# Create filter
filter = filter_class.new(assigns)
- begin
- # Notify start
- Nanoc::Int::NotificationCenter.post(:processing_started, layout)
+ # Layout
+ content = layout.content
+ arg = content.binary? ? content.filename : content.string
+ result = filter.setup_and_run(arg, filter_args)
- # Layout
- content = layout.content
- arg = content.binary? ? content.filename : content.string
- result = filter.setup_and_run(arg, filter_args)
-
- # Append to erbout if we have a block
- if block_given?
- # Append result and return nothing
- erbout = eval('_erbout', block.binding)
- erbout << result
- ''
- else
- # Return result
- result
- end
- ensure
- # Notify end
- Nanoc::Int::NotificationCenter.post(:processing_ended, layout)
+ # Append to erbout if we have a block
+ if block_given?
+ # Append result and return nothing
+ erbout = eval('_erbout', block.binding)
+ erbout << result
+ ''
+ else
+ # Return result
+ result
end
end
end
diff --git a/lib/nanoc/rule_dsl/action_provider.rb b/lib/nanoc/rule_dsl/action_provider.rb
index f8a063a..e14294d 100644
--- a/lib/nanoc/rule_dsl/action_provider.rb
+++ b/lib/nanoc/rule_dsl/action_provider.rb
@@ -55,7 +55,7 @@ module Nanoc::RuleDSL
reps: reps,
items: site.items,
dependency_tracker: dependency_tracker,
- compiler: site.compiler,
+ compilation_context: site.compiler.compilation_context,
)
ctx = new_postprocessor_context(site, view_context)
@@ -72,7 +72,7 @@ module Nanoc::RuleDSL
reps: nil,
items: nil,
dependency_tracker: dependency_tracker,
- compiler: nil,
+ compilation_context: nil,
)
Nanoc::Int::Context.new(
diff --git a/lib/nanoc/rule_dsl/compiler_dsl.rb b/lib/nanoc/rule_dsl/compiler_dsl.rb
index 1efa6cd..0831184 100644
--- a/lib/nanoc/rule_dsl/compiler_dsl.rb
+++ b/lib/nanoc/rule_dsl/compiler_dsl.rb
@@ -159,8 +159,6 @@ module Nanoc::RuleDSL
#
# @return [void]
#
- # @since 3.2.0
- #
# @example Copying the `/foo/` item as-is
#
# passthrough '/foo/'
@@ -278,9 +276,15 @@ module Nanoc::RuleDSL
# Add leading/trailing slashes if necessary
new_identifier = identifier.dup
new_identifier[/^/] = '/' if identifier[0, 1] != '/'
- new_identifier[/$/] = '/' unless ['*', '/'].include?(identifier[-1, 1])
+ new_identifier[/$/] = '/?' unless ['*', '/'].include?(identifier[-1, 1])
+
+ regex_string =
+ new_identifier
+ .gsub('.', '\.')
+ .gsub('*', '(.*?)')
+ .gsub('+', '(.+?)')
- /^#{new_identifier.gsub('*', '(.*?)').gsub('+', '(.+?)')}$/
+ /^#{regex_string}$/
else
identifier
end
diff --git a/lib/nanoc/rule_dsl/recording_executor.rb b/lib/nanoc/rule_dsl/recording_executor.rb
index 3e56670..d3b2d1d 100644
--- a/lib/nanoc/rule_dsl/recording_executor.rb
+++ b/lib/nanoc/rule_dsl/recording_executor.rb
@@ -1,55 +1,33 @@
module Nanoc
module RuleDSL
class RecordingExecutor
- class PathWithoutInitialSlashError < ::Nanoc::Error
- def initialize(rep, basic_path)
- super("The path returned for the #{rep.inspect} item representation, “#{basic_path}”, does not start with a slash. Please ensure that all routing rules return a path that starts with a slash.")
- end
- end
-
- attr_reader :rule_memory
-
- def initialize(item_rep, rules_collection, site)
- @item_rep = item_rep
- @rules_collection = rules_collection
- @site = site
+ include Nanoc::Int::ContractsSupport
- @rule_memory = Nanoc::Int::RuleMemory.new(item_rep)
+ def initialize(rule_memory)
+ @rule_memory = rule_memory
end
- def filter(_rep, filter_name, filter_args = {})
+ def filter(filter_name, filter_args = {})
@rule_memory.add_filter(filter_name, filter_args)
end
- def layout(_rep, layout_identifier, extra_filter_args = {})
+ def layout(layout_identifier, extra_filter_args = {})
unless layout_identifier.is_a?(String)
raise ArgumentError.new('The layout passed to #layout must be a string')
end
unless @rule_memory.any_layouts?
- @rule_memory.add_snapshot(:pre, true, nil)
+ @rule_memory.add_snapshot(:pre, nil)
end
@rule_memory.add_layout(layout_identifier, extra_filter_args)
end
- def snapshot(rep, snapshot_name, final: true, path: nil)
- actual_path = final ? (path || basic_path_from_rules_for(rep, snapshot_name)) : nil
- @rule_memory.add_snapshot(snapshot_name, final, actual_path)
- end
-
- def basic_path_from_rules_for(rep, snapshot_name)
- routing_rules = @rules_collection.routing_rules_for(rep)
- routing_rule = routing_rules[snapshot_name]
- return nil if routing_rule.nil?
-
- dependency_tracker = Nanoc::Int::DependencyTracker::Null.new
- view_context = Nanoc::ViewContext.new(reps: nil, items: nil, dependency_tracker: dependency_tracker, compiler: nil)
- basic_path = routing_rule.apply_to(rep, executor: nil, site: @site, view_context: view_context)
- if basic_path && !basic_path.start_with?('/')
- raise PathWithoutInitialSlashError.new(rep, basic_path)
- end
- basic_path
+ Pathlike = C::Maybe[C::Or[String, Nanoc::Identifier]]
+ contract Symbol, C::KeywordArgs[path: C::Optional[Pathlike]] => nil
+ def snapshot(snapshot_name, path: nil)
+ @rule_memory.add_snapshot(snapshot_name, path && path.to_s)
+ nil
end
end
end
diff --git a/lib/nanoc/rule_dsl/rule.rb b/lib/nanoc/rule_dsl/rule.rb
index 301c50b..ee11bf4 100644
--- a/lib/nanoc/rule_dsl/rule.rb
+++ b/lib/nanoc/rule_dsl/rule.rb
@@ -9,8 +9,6 @@ module Nanoc::RuleDSL
# @return [Symbol] The name of the snapshot this rule will apply to.
# Ignored for compilation rules, but used for routing rules.
- #
- # @since 3.2.0
attr_reader :snapshot_name
attr_reader :pattern
diff --git a/lib/nanoc/rule_dsl/rule_context.rb b/lib/nanoc/rule_dsl/rule_context.rb
index 6753f95..58dbf5b 100644
--- a/lib/nanoc/rule_dsl/rule_context.rb
+++ b/lib/nanoc/rule_dsl/rule_context.rb
@@ -35,7 +35,7 @@ module Nanoc::RuleDSL
#
# @return [void]
def filter(filter_name, filter_args = {})
- @_executor.filter(rep.unwrap, filter_name, filter_args)
+ @_executor.filter(filter_name, filter_args)
end
# Layouts the current representation (calls {Nanoc::Int::ItemRep#layout} with
@@ -48,7 +48,7 @@ module Nanoc::RuleDSL
#
# @return [void]
def layout(layout_identifier, extra_filter_args = nil)
- @_executor.layout(rep.unwrap, layout_identifier, extra_filter_args)
+ @_executor.layout(layout_identifier, extra_filter_args)
end
# Creates a snapshot of the current compiled item content. Calls
@@ -62,7 +62,7 @@ module Nanoc::RuleDSL
#
# @return [void]
def snapshot(snapshot_name, path: nil)
- @_executor.snapshot(rep.unwrap, snapshot_name, path: path)
+ @_executor.snapshot(snapshot_name, path: path)
end
# Creates a snapshot named :last the current compiled item content, with
diff --git a/lib/nanoc/rule_dsl/rule_memory_calculator.rb b/lib/nanoc/rule_dsl/rule_memory_calculator.rb
index 0c85b9c..5db8787 100644
--- a/lib/nanoc/rule_dsl/rule_memory_calculator.rb
+++ b/lib/nanoc/rule_dsl/rule_memory_calculator.rb
@@ -24,6 +24,12 @@ module Nanoc::RuleDSL
end
end
+ class PathWithoutInitialSlashError < ::Nanoc::Error
+ def initialize(rep, basic_path)
+ super("The path returned for the #{rep.inspect} item representation, “#{basic_path}”, does not start with a slash. Please ensure that all routing rules return a path that starts with a slash.")
+ end
+ end
+
# @api private
attr_accessor :rules_collection
@@ -38,9 +44,6 @@ module Nanoc::RuleDSL
#
# @return [Nanoc::Int::RuleMemory]
def [](obj)
- # FIXME: Remove this
- obj = obj.unwrap if obj.respond_to?(:unwrap)
-
case obj
when Nanoc::Int::ItemRep
new_rule_memory_for_rep(obj)
@@ -50,17 +53,10 @@ module Nanoc::RuleDSL
raise UnsupportedObjectTypeException.new(obj)
end
end
- memoize :[]
- # @param [Nanoc::Int::ItemRep] rep The item representation for which to fetch
- # the list of snapshots
- #
- # @return [Array] A list of snapshots, represented as arrays where the
- # first element is the snapshot name (a Symbol) and the last element is
- # a Boolean indicating whether the snapshot is final or not
def snapshots_defs_for(rep)
self[rep].snapshot_actions.map do |a|
- Nanoc::Int::SnapshotDef.new(a.snapshot_name, a.final?)
+ Nanoc::Int::SnapshotDef.new(a.snapshot_name)
end
end
@@ -70,26 +66,26 @@ module Nanoc::RuleDSL
# @return [Nanoc::Int::RuleMemory]
def new_rule_memory_for_rep(rep)
dependency_tracker = Nanoc::Int::DependencyTracker::Null.new
- view_context = @site.compiler.create_view_context(dependency_tracker)
+ view_context = @site.compiler.compilation_context.create_view_context(dependency_tracker)
- executor = Nanoc::RuleDSL::RecordingExecutor.new(rep, @rules_collection, @site)
+ rule_memory = Nanoc::Int::RuleMemory.new(rep)
+ executor = Nanoc::RuleDSL::RecordingExecutor.new(rule_memory)
rule = @rules_collection.compilation_rule_for(rep)
unless rule
raise NoRuleMemoryForItemRepException.new(rep)
end
- executor.snapshot(rep, :raw)
- executor.snapshot(rep, :pre, final: false)
+ executor.snapshot(:raw)
rule.apply_to(rep, executor: executor, site: @site, view_context: view_context)
- if executor.rule_memory.any_layouts?
- executor.snapshot(rep, :post)
+ if rule_memory.any_layouts?
+ executor.snapshot(:post)
end
- unless executor.rule_memory.snapshot_actions.any? { |sa| sa.snapshot_name == :last }
- executor.snapshot(rep, :last)
+ unless rule_memory.snapshot_actions.any? { |sa| sa.snapshot_name == :last }
+ executor.snapshot(:last)
end
- executor.rule_memory
+ assign_paths_to_mem(rule_memory, rep: rep)
end
# @param [Nanoc::Int::Layout] layout
@@ -106,5 +102,35 @@ module Nanoc::RuleDSL
rm.add_filter(res[0], res[1])
end
end
+
+ def assign_paths_to_mem(mem, rep:)
+ mem.map do |action|
+ if action.is_a?(Nanoc::Int::ProcessingActions::Snapshot) && action.path.nil?
+ path_from_rules = basic_path_from_rules_for(rep, action.snapshot_name)
+ if path_from_rules
+ action.copy(path: path_from_rules.to_s)
+ else
+ action
+ end
+ else
+ action
+ end
+ end
+ end
+
+ # FIXME: ugly
+ def basic_path_from_rules_for(rep, snapshot_name)
+ routing_rules = @rules_collection.routing_rules_for(rep)
+ routing_rule = routing_rules[snapshot_name]
+ return nil if routing_rule.nil?
+
+ dependency_tracker = Nanoc::Int::DependencyTracker::Null.new
+ view_context = Nanoc::ViewContext.new(reps: nil, items: nil, dependency_tracker: dependency_tracker, compilation_context: nil)
+ basic_path = routing_rule.apply_to(rep, executor: nil, site: @site, view_context: view_context)
+ if basic_path && !basic_path.start_with?('/')
+ raise PathWithoutInitialSlashError.new(rep, basic_path)
+ end
+ basic_path
+ end
end
end
diff --git a/lib/nanoc/spec.rb b/lib/nanoc/spec.rb
index 47b80eb..0576d55 100644
--- a/lib/nanoc/spec.rb
+++ b/lib/nanoc/spec.rb
@@ -35,7 +35,15 @@ module Nanoc
Nanoc::ItemWithRepsView.new(item, view_context)
end
- # TODO: document
+ # Creates a new layout and adds it to the site’s collection of layouts.
+ #
+ # @param [String] content The raw layout content
+ #
+ # @param [Hash] attributes A hash containing this layout's attributes
+ #
+ # @param [Nanoc::Identifier, String] identifier This layout's identifier
+ #
+ # @return [Nanoc::ItemWithRepsView] A view for the newly created layout
def create_layout(content, attributes, identifier)
layout = Nanoc::Int::Layout.new(content, attributes, identifier)
@layouts << layout
@@ -110,7 +118,7 @@ module Nanoc
reps: @reps,
items: @items,
dependency_tracker: @dependency_tracker,
- compiler: new_site.compiler,
+ compilation_context: new_site.compiler.compilation_context,
)
end
@@ -133,45 +141,13 @@ module Nanoc
end
def snapshots_defs_for(_rep)
- [Nanoc::Int::SnapshotDef.new(:last, false)]
+ [Nanoc::Int::SnapshotDef.new(:last)]
end
end.new(self)
end
def new_compiler_for(site)
- rule_memory_store = Nanoc::Int::RuleMemoryStore.new
-
- dependency_store =
- Nanoc::Int::DependencyStore.new(site.items.to_a + site.layouts.to_a)
-
- checksum_store =
- Nanoc::Int::ChecksumStore.new(site: site)
-
- item_rep_repo = Nanoc::Int::ItemRepRepo.new
-
- action_provider = new_action_provider
-
- outdatedness_checker =
- Nanoc::Int::OutdatednessChecker.new(
- site: site,
- checksum_store: checksum_store,
- dependency_store: dependency_store,
- rule_memory_store: rule_memory_store,
- action_provider: action_provider,
- reps: item_rep_repo,
- )
-
- params = {
- compiled_content_cache: Nanoc::Int::CompiledContentCache.new,
- checksum_store: checksum_store,
- rule_memory_store: rule_memory_store,
- dependency_store: dependency_store,
- outdatedness_checker: outdatedness_checker,
- reps: item_rep_repo,
- action_provider: action_provider,
- }
-
- Nanoc::Int::Compiler.new(site, params)
+ Nanoc::Int::CompilerLoader.new.load(site, action_provider: new_action_provider)
end
def new_site
diff --git a/lib/nanoc/version.rb b/lib/nanoc/version.rb
index 5a09e1d..f850667 100644
--- a/lib/nanoc/version.rb
+++ b/lib/nanoc/version.rb
@@ -1,4 +1,4 @@
module Nanoc
# The current Nanoc version.
- VERSION = '4.3.4'.freeze
+ VERSION = '4.4.6'.freeze
end
diff --git a/nanoc.gemspec b/nanoc.gemspec
index 9105dfd..354e795 100644
--- a/nanoc.gemspec
+++ b/nanoc.gemspec
@@ -14,13 +14,13 @@ Gem::Specification.new do |s|
s.files =
Dir['[A-Z]*'] +
Dir['doc/yardoc_{templates,handlers}/**/*'] +
- Dir['{bin,lib,tasks,test}/**/*'] +
+ Dir['{bin,lib,tasks,test,spec}/**/*'] +
['nanoc.gemspec']
s.executables = ['nanoc']
s.require_paths = ['lib']
s.rdoc_options = ['--main', 'README.md']
- s.extra_rdoc_files = ['ChangeLog', 'LICENSE', 'README.md', 'NEWS.md']
+ s.extra_rdoc_files = ['LICENSE', 'README.md', 'NEWS.md']
s.required_ruby_version = '>= 2.1.0'
@@ -29,4 +29,5 @@ Gem::Specification.new do |s|
s.add_runtime_dependency('ref', '~> 2.0')
s.add_development_dependency('bundler', '>= 1.7.10', '< 2.0')
+ s.add_development_dependency('appraisal', '~> 2.1')
end
diff --git a/spec/contributors_spec.rb b/spec/contributors_spec.rb
new file mode 100644
index 0000000..c440761
--- /dev/null
+++ b/spec/contributors_spec.rb
@@ -0,0 +1,18 @@
+describe 'list of contributors in README', chdir: false do
+ let(:contributors_in_readme) do
+ File.readlines('README.md').last.chomp("\n").split(', ')
+ end
+
+ let(:contributors_in_release_notes) do
+ File.read('NEWS.md').scan(/\[[^\]]+\]$/).map { |s| s[1..-2].split(', ') }.flatten
+ end
+
+ it 'should include everyone mentioned in NEWS.md' do
+ diff = (contributors_in_release_notes - contributors_in_readme).uniq.sort
+ expect(diff).to be_empty, "some contributors are missing from the README: #{diff.join(', ')}"
+ end
+
+ it 'should be sorted' do
+ expect(contributors_in_readme).to be_humanly_sorted
+ end
+end
diff --git a/spec/nanoc/base/checksummer_spec.rb b/spec/nanoc/base/checksummer_spec.rb
new file mode 100644
index 0000000..aa4aec5
--- /dev/null
+++ b/spec/nanoc/base/checksummer_spec.rb
@@ -0,0 +1,381 @@
+require 'tempfile'
+
+describe Nanoc::Int::Checksummer::VerboseDigest do
+ let(:digest) { described_class.new }
+
+ it 'concatenates' do
+ digest.update('foo')
+ digest.update('bar')
+ expect(digest.to_s).to eql('foobar')
+ end
+end
+
+describe Nanoc::Int::Checksummer::CompactDigest do
+ let(:digest) { described_class.new }
+
+ it 'uses SHA1 and Base64' do
+ digest.update('foo')
+ digest.update('bar')
+ expect(digest.to_s).to eql(Digest::SHA1.base64digest('foobar'))
+ end
+end
+
+describe Nanoc::Int::Checksummer do
+ subject { described_class.calc(obj, Nanoc::Int::Checksummer::VerboseDigest) }
+
+ context 'String' do
+ let(:obj) { 'hello' }
+ it { is_expected.to eql('String<hello>') }
+ end
+
+ context 'Symbol' do
+ let(:obj) { :hello }
+ it { is_expected.to eql('Symbol<hello>') }
+ end
+
+ context 'nil' do
+ let(:obj) { nil }
+ it { is_expected.to eql('NilClass<>') }
+ end
+
+ context 'true' do
+ let(:obj) { true }
+ it { is_expected.to eql('TrueClass<>') }
+ end
+
+ context 'false' do
+ let(:obj) { false }
+ it { is_expected.to eql('FalseClass<>') }
+ end
+
+ context 'Array' do
+ let(:obj) { %w(hello goodbye) }
+ it { is_expected.to eql('Array<String<hello>,String<goodbye>,>') }
+
+ context 'different order' do
+ let(:obj) { %w(goodbye hello) }
+ it { is_expected.to eql('Array<String<goodbye>,String<hello>,>') }
+ end
+
+ context 'recursive' do
+ let(:obj) { [].tap { |arr| arr << ['hello', arr] } }
+ it { is_expected.to eql('Array<Array<String<hello>,Array<recur>,>,>') }
+ end
+
+ context 'non-serializable' do
+ let(:obj) { [-> {}] }
+ it { is_expected.to match(/\AArray<Proc<#<Proc:0x.*@.*:\d+.*>>,>\z/) }
+ end
+ end
+
+ context 'Hash' do
+ let(:obj) { { 'a' => 'foo', 'b' => 'bar' } }
+ it { is_expected.to eql('Hash<String<a>=String<foo>,String<b>=String<bar>,>') }
+
+ context 'different order' do
+ let(:obj) { { 'b' => 'bar', 'a' => 'foo' } }
+ it { is_expected.to eql('Hash<String<b>=String<bar>,String<a>=String<foo>,>') }
+ end
+
+ context 'non-serializable' do
+ let(:obj) { { 'a' => -> {} } }
+ it { is_expected.to match(/\AHash<String<a>=Proc<#<Proc:0x.*@.*:\d+.*>>,>\z/) }
+ end
+
+ context 'recursive values' do
+ let(:obj) { {}.tap { |hash| hash['a'] = hash } }
+ it { is_expected.to eql('Hash<String<a>=Hash<recur>,>') }
+ end
+
+ context 'recursive keys' do
+ let(:obj) { {}.tap { |hash| hash[hash] = 'hello' } }
+ it { is_expected.to eql('Hash<Hash<recur>=String<hello>,>') }
+ end
+ end
+
+ context 'Pathname' do
+ let(:obj) { ::Pathname.new(filename) }
+
+ let(:filename) { '/tmp/whatever' }
+ let(:mtime) { 200 }
+ let(:data) { 'stuffs' }
+
+ before do
+ FileUtils.mkdir_p(File.dirname(filename))
+ File.write(filename, data)
+ File.utime(mtime, mtime, filename)
+ end
+
+ it { is_expected.to eql('Pathname<6-200>') }
+
+ context 'does not exist' do
+ before do
+ FileUtils.rm_rf(filename)
+ end
+
+ it { is_expected.to eql('Pathname<???>') }
+ end
+
+ context 'different data' do
+ let(:data) { 'other stuffs :o' }
+ it { is_expected.to eql('Pathname<15-200>') }
+ end
+ end
+
+ context 'Time' do
+ let(:obj) { Time.at(111_223) }
+ it { is_expected.to eql('Time<111223>') }
+ end
+
+ context 'Float' do
+ let(:obj) { 3.14 }
+ it { is_expected.to eql('Float<3.14>') }
+ end
+
+ context 'Fixnum/Integer' do
+ let(:obj) { 3 }
+ it { is_expected.to match(/\A(Integer|Fixnum)<3>\z/) }
+ end
+
+ context 'Nanoc::Identifier' do
+ let(:obj) { Nanoc::Identifier.new('/foo.md') }
+ it { is_expected.to eql('Nanoc::Identifier<String</foo.md>>') }
+ end
+
+ context 'Nanoc::RuleDSL::RulesCollection' do
+ let(:obj) do
+ Nanoc::RuleDSL::RulesCollection.new.tap { |rc| rc.data = data }
+ end
+
+ let(:data) { 'STUFF!' }
+
+ it { is_expected.to eql('Nanoc::RuleDSL::RulesCollection<String<STUFF!>>') }
+ end
+
+ context 'Nanoc::Int::CodeSnippet' do
+ let(:obj) { Nanoc::Int::CodeSnippet.new('asdf', '/bob.rb') }
+ it { is_expected.to eql('Nanoc::Int::CodeSnippet<String<asdf>>') }
+ end
+
+ context 'Nanoc::Int::Configuration' do
+ let(:obj) { Nanoc::Int::Configuration.new(hash: { 'foo' => 'bar' }) }
+ it { is_expected.to eql('Nanoc::Int::Configuration<Symbol<foo>=String<bar>,>') }
+ end
+
+ context 'Nanoc::Int::Item' do
+ let(:obj) { Nanoc::Int::Item.new('asdf', { 'foo' => 'bar' }, '/foo.md') }
+
+ it { is_expected.to eql('Nanoc::Int::Item<content=Nanoc::Int::TextualContent<String<asdf>>,attributes=Hash<Symbol<foo>=String<bar>,>,identifier=Nanoc::Identifier<String</foo.md>>>') }
+
+ context 'binary' do
+ let(:filename) { File.expand_path('foo.dat') }
+ let(:content) { Nanoc::Int::BinaryContent.new(filename) }
+ let(:obj) { Nanoc::Int::Item.new(content, { 'foo' => 'bar' }, '/foo.md') }
+
+ let(:mtime) { 200 }
+ let(:data) { 'stuffs' }
+
+ before do
+ File.write(content.filename, data)
+ File.utime(mtime, mtime, content.filename)
+ end
+
+ it { is_expected.to eql('Nanoc::Int::Item<content=Nanoc::Int::BinaryContent<Pathname<6-200>>,attributes=Hash<Symbol<foo>=String<bar>,>,identifier=Nanoc::Identifier<String</foo.md>>>') }
+ end
+
+ context 'recursive attributes' do
+ before do
+ obj.attributes[:foo] = obj
+ end
+
+ it { is_expected.to eql('Nanoc::Int::Item<content=Nanoc::Int::TextualContent<String<asdf>>,attributes=Hash<Symbol<foo>=Nanoc::Int::Item<recur>,>,identifier=Nanoc::Identifier<String</foo.md>>>') }
+ end
+
+ context 'with checksum' do
+ let(:obj) { Nanoc::Int::Item.new('asdf', { 'foo' => 'bar' }, '/foo.md', checksum_data: 'abcdef') }
+
+ it { is_expected.to eql('Nanoc::Int::Item<checksum_data=abcdef>') }
+ end
+
+ context 'with content checksum' do
+ let(:obj) { Nanoc::Int::Item.new('asdf', { 'foo' => 'bar' }, '/foo.md', content_checksum_data: 'con-cs') }
+
+ it { is_expected.to eql('Nanoc::Int::Item<content_checksum_data=con-cs,attributes=Hash<Symbol<foo>=String<bar>,>,identifier=Nanoc::Identifier<String</foo.md>>>') }
+ end
+
+ context 'with attributes checksum' do
+ let(:obj) { Nanoc::Int::Item.new('asdf', { 'foo' => 'bar' }, '/foo.md', attributes_checksum_data: 'attr-cs') }
+
+ it { is_expected.to eql('Nanoc::Int::Item<content=Nanoc::Int::TextualContent<String<asdf>>,attributes_checksum_data=attr-cs,identifier=Nanoc::Identifier<String</foo.md>>>') }
+ end
+ end
+
+ context 'Nanoc::Int::Layout' do
+ let(:obj) { Nanoc::Int::Layout.new('asdf', { 'foo' => 'bar' }, '/foo.md') }
+
+ it { is_expected.to eql('Nanoc::Int::Layout<content=Nanoc::Int::TextualContent<String<asdf>>,attributes=Hash<Symbol<foo>=String<bar>,>,identifier=Nanoc::Identifier<String</foo.md>>>') }
+
+ context 'recursive attributes' do
+ before do
+ obj.attributes[:foo] = obj
+ end
+
+ it { is_expected.to eql('Nanoc::Int::Layout<content=Nanoc::Int::TextualContent<String<asdf>>,attributes=Hash<Symbol<foo>=Nanoc::Int::Layout<recur>,>,identifier=Nanoc::Identifier<String</foo.md>>>') }
+ end
+
+ context 'with checksum' do
+ let(:obj) { Nanoc::Int::Layout.new('asdf', { 'foo' => 'bar' }, '/foo.md', checksum_data: 'abcdef') }
+
+ it { is_expected.to eql('Nanoc::Int::Layout<checksum_data=abcdef>') }
+ end
+ end
+
+ context 'Nanoc::ItemWithRepsView' do
+ let(:obj) { Nanoc::ItemWithRepsView.new(item, nil) }
+ let(:item) { Nanoc::Int::Item.new('asdf', {}, '/foo.md') }
+
+ it { is_expected.to eql('Nanoc::ItemWithRepsView<Nanoc::Int::Item<content=Nanoc::Int::TextualContent<String<asdf>>,attributes=Hash<>,identifier=Nanoc::Identifier<String</foo.md>>>>') }
+ end
+
+ context 'Nanoc::Int::ItemRep' do
+ let(:obj) { Nanoc::Int::ItemRep.new(item, :pdf) }
+ let(:item) { Nanoc::Int::Item.new('asdf', {}, '/foo.md') }
+
+ it { is_expected.to eql('Nanoc::Int::ItemRep<item=Nanoc::Int::Item<content=Nanoc::Int::TextualContent<String<asdf>>,attributes=Hash<>,identifier=Nanoc::Identifier<String</foo.md>>>,name=Symbol<pdf>>') }
+ end
+
+ context 'Nanoc::ItemRepView' do
+ let(:obj) { Nanoc::ItemRepView.new(rep, :_unused_context) }
+ let(:rep) { Nanoc::Int::ItemRep.new(item, :pdf) }
+ let(:item) { Nanoc::Int::Item.new('asdf', {}, '/foo.md') }
+
+ it { is_expected.to eql('Nanoc::ItemRepView<Nanoc::Int::ItemRep<item=Nanoc::Int::Item<content=Nanoc::Int::TextualContent<String<asdf>>,attributes=Hash<>,identifier=Nanoc::Identifier<String</foo.md>>>,name=Symbol<pdf>>>') }
+ end
+
+ context 'Nanoc::ItemWithoutRepsView' do
+ let(:obj) { Nanoc::ItemWithoutRepsView.new(item, nil) }
+ let(:item) { Nanoc::Int::Item.new('asdf', {}, '/foo.md') }
+
+ it { is_expected.to eql('Nanoc::ItemWithoutRepsView<Nanoc::Int::Item<content=Nanoc::Int::TextualContent<String<asdf>>,attributes=Hash<>,identifier=Nanoc::Identifier<String</foo.md>>>>') }
+ end
+
+ context 'Nanoc::LayoutView' do
+ let(:obj) { Nanoc::LayoutView.new(layout, nil) }
+ let(:layout) { Nanoc::Int::Layout.new('asdf', {}, '/foo.md') }
+
+ it { is_expected.to eql('Nanoc::LayoutView<Nanoc::Int::Layout<content=Nanoc::Int::TextualContent<String<asdf>>,attributes=Hash<>,identifier=Nanoc::Identifier<String</foo.md>>>>') }
+ end
+
+ context 'Nanoc::ConfigView' do
+ let(:obj) { Nanoc::ConfigView.new(config, nil) }
+ let(:config) { Nanoc::Int::Configuration.new(hash: { 'foo' => 'bar' }) }
+
+ it { is_expected.to eql('Nanoc::ConfigView<Nanoc::Int::Configuration<Symbol<foo>=String<bar>,>>') }
+ end
+
+ context 'Nanoc::ItemCollectionWithRepsView' do
+ let(:obj) { Nanoc::ItemCollectionWithRepsView.new(wrapped, nil) }
+
+ let(:config) { Nanoc::Int::Configuration.new(hash: { 'foo' => 'bar' }) }
+
+ let(:wrapped) do
+ Nanoc::Int::IdentifiableCollection.new(config).tap do |arr|
+ arr << Nanoc::Int::Item.new('foo', {}, '/foo.md')
+ arr << Nanoc::Int::Item.new('bar', {}, '/foo.md')
+ end
+ end
+
+ it { is_expected.to eql('Nanoc::ItemCollectionWithRepsView<Nanoc::Int::IdentifiableCollection<Nanoc::Int::Item<content=Nanoc::Int::TextualContent<String<foo>>,attributes=Hash<>,identifier=Nanoc::Identifier<String</foo.md>>>,Nanoc::Int::Item<content=Nanoc::Int::TextualContent<String<bar>>,attributes=Hash<>,identifier=Nanoc::Identifier<String</foo.md>>>,>>') }
+ end
+
+ context 'Nanoc::ItemCollectionWithoutRepsView' do
+ let(:obj) { Nanoc::ItemCollectionWithoutRepsView.new(wrapped, nil) }
+
+ let(:config) { Nanoc::Int::Configuration.new(hash: { 'foo' => 'bar' }) }
+
+ let(:wrapped) do
+ Nanoc::Int::IdentifiableCollection.new(config).tap do |arr|
+ arr << Nanoc::Int::Item.new('foo', {}, '/foo.md')
+ arr << Nanoc::Int::Item.new('bar', {}, '/foo.md')
+ end
+ end
+
+ it { is_expected.to eql('Nanoc::ItemCollectionWithoutRepsView<Nanoc::Int::IdentifiableCollection<Nanoc::Int::Item<content=Nanoc::Int::TextualContent<String<foo>>,attributes=Hash<>,identifier=Nanoc::Identifier<String</foo.md>>>,Nanoc::Int::Item<content=Nanoc::Int::TextualContent<String<bar>>,attributes=Hash<>,identifier=Nanoc::Identifier<String</foo.md>>>,>>') }
+ end
+
+ context 'Nanoc::RuleDSL::RuleContext' do
+ let(:obj) { Nanoc::RuleDSL::RuleContext.new(rep: rep, site: site, executor: executor, view_context: view_context) }
+
+ let(:rep) { Nanoc::Int::ItemRep.new(item, :pdf) }
+ let(:item) { Nanoc::Int::Item.new('stuff', {}, '/stuff.md') }
+
+ let(:site) do
+ Nanoc::Int::Site.new(config: config, code_snippets: code_snippets, items: items, layouts: layouts)
+ end
+
+ let(:config) { Nanoc::Int::Configuration.new(hash: { 'foo' => 'bar' }) }
+ let(:code_snippets) { [Nanoc::Int::CodeSnippet.new('asdf', '/bob.rb')] }
+ let(:items) { [item] }
+ let(:layouts) { [Nanoc::Int::Layout.new('asdf', {}, '/foo.md')] }
+
+ let(:executor) { :_unused_ }
+ let(:view_context) { :_unused_ }
+
+ let(:expected_item_checksum) { 'Nanoc::Int::Item<content=Nanoc::Int::TextualContent<String<stuff>>,attributes=Hash<>,identifier=Nanoc::Identifier<String</stuff.md>>>' }
+ let(:expected_item_rep_checksum) { 'Nanoc::Int::ItemRep<item=' + expected_item_checksum + ',name=Symbol<pdf>>' }
+ let(:expected_layout_checksum) { 'Nanoc::Int::Layout<content=Nanoc::Int::TextualContent<String<asdf>>,attributes=Hash<>,identifier=Nanoc::Identifier<String</foo.md>>>' }
+ let(:expected_config_checksum) { 'Nanoc::Int::Configuration<Symbol<foo>=String<bar>,>' }
+
+ let(:expected_checksum) do
+ [
+ 'Nanoc::RuleDSL::RuleContext<',
+ 'item=',
+ 'Nanoc::ItemWithoutRepsView<' + expected_item_checksum + '>',
+ ',rep=',
+ 'Nanoc::ItemRepView<' + expected_item_rep_checksum + '>',
+ ',items=',
+ 'Nanoc::ItemCollectionWithoutRepsView<Array<' + expected_item_checksum + ',>>',
+ ',layouts=',
+ 'Nanoc::LayoutCollectionView<Array<' + expected_layout_checksum + ',>>',
+ ',config=',
+ 'Nanoc::ConfigView<' + expected_config_checksum + '>',
+ '>',
+ ].join('')
+ end
+
+ it { is_expected.to eql(expected_checksum) }
+ end
+
+ context 'Nanoc::Int::Context' do
+ let(:obj) { Nanoc::Int::Context.new(foo: 123) }
+
+ it { is_expected.to match(/\ANanoc::Int::Context<@foo=(Fixnum|Integer)<123>,>\z/) }
+ end
+
+ context 'Sass::Importers::Filesystem' do
+ let(:obj) { Sass::Importers::Filesystem.new('/foo') }
+
+ before { require 'sass' }
+
+ it { is_expected.to eql('Sass::Importers::Filesystem<root=/foo>') }
+ end
+
+ context 'other marshal-able classes' do
+ let(:obj) { klass.new('hello') }
+
+ let(:klass) do
+ Class.new do
+ def initialize(a)
+ @a = a
+ end
+ end
+ end
+
+ it { is_expected.to match(/\A#<Class:0x[0-9a-f]+><.*>\z/) }
+ end
+
+ context 'other non-marshal-able classes' do
+ let(:obj) { proc {} }
+ it { is_expected.to match(/\AProc<#<Proc:0x.*@.*:\d+.*>>\z/) }
+ end
+end
diff --git a/spec/nanoc/base/compiler_spec.rb b/spec/nanoc/base/compiler_spec.rb
new file mode 100644
index 0000000..abfff16
--- /dev/null
+++ b/spec/nanoc/base/compiler_spec.rb
@@ -0,0 +1,181 @@
+describe Nanoc::Int::Compiler do
+ let(:compiler) do
+ described_class.new(
+ site,
+ compiled_content_cache: compiled_content_cache,
+ checksum_store: checksum_store,
+ rule_memory_store: rule_memory_store,
+ action_provider: action_provider,
+ dependency_store: dependency_store,
+ outdatedness_checker: outdatedness_checker,
+ reps: reps,
+ )
+ end
+
+ let(:checksum_store) { :__irrelevant_checksum_store }
+ let(:rule_memory_store) { :__irrelevant_rule_memory_store }
+
+ let(:dependency_store) { Nanoc::Int::DependencyStore.new(items.to_a) }
+ let(:reps) { Nanoc::Int::ItemRepRepo.new }
+
+ let(:outdatedness_checker) { double(:outdatedness_checker) }
+ let(:action_provider) { double(:action_provider) }
+
+ let(:compiled_content_cache) { Nanoc::Int::CompiledContentCache.new(items: items) }
+
+ let(:rep) { Nanoc::Int::ItemRep.new(item, :default) }
+ let(:item) { Nanoc::Int::Item.new('<%= 1 + 2 %>', {}, '/hi.md') }
+
+ let(:other_rep) { Nanoc::Int::ItemRep.new(other_item, :default) }
+ let(:other_item) { Nanoc::Int::Item.new('other content', {}, '/other.md') }
+
+ let(:site) do
+ Nanoc::Int::Site.new(
+ config: config,
+ code_snippets: code_snippets,
+ items: items,
+ layouts: layouts,
+ )
+ end
+
+ let(:config) { Nanoc::Int::Configuration.new.with_defaults }
+ let(:layouts) { [] }
+ let(:code_snippets) { [] }
+
+ let(:items) do
+ Nanoc::Int::IdentifiableCollection.new(config).tap do |col|
+ col << item
+ col << other_item
+ end
+ end
+
+ let(:memory) do
+ [
+ Nanoc::Int::ProcessingActions::Filter.new(:erb, {}),
+ Nanoc::Int::ProcessingActions::Snapshot.new(:last, nil),
+ ]
+ end
+
+ before do
+ reps << rep
+ reps << other_rep
+
+ reps.each do |rep|
+ rep.snapshot_defs << Nanoc::Int::SnapshotDef.new(:last)
+ end
+
+ allow(outdatedness_checker).to receive(:outdated?).with(rep).and_return(true)
+ allow(outdatedness_checker).to receive(:outdated?).with(other_rep).and_return(true)
+
+ allow(action_provider).to receive(:memory_for).with(rep).and_return(memory)
+ allow(action_provider).to receive(:memory_for).with(other_rep).and_return(memory)
+ end
+
+ describe '#compile_reps' do
+ subject { compiler.send(:compile_reps) }
+
+ before do
+ allow(action_provider).to receive(:snapshots_defs_for).with(rep).and_return(snapshot_defs_for_rep)
+ allow(action_provider).to receive(:snapshots_defs_for).with(other_rep).and_return(snapshot_defs_for_rep)
+ end
+
+ let(:snapshot_defs_for_rep) do
+ [Nanoc::Int::SnapshotDef.new(:last)]
+ end
+
+ let(:snapshot_defs_for_other_rep) do
+ [Nanoc::Int::SnapshotDef.new(:last)]
+ end
+
+ it 'compiles individual reps' do
+ expect { subject }.to change { rep.snapshot_contents[:last].string }
+ .from('<%= 1 + 2 %>')
+ .to('3')
+ end
+
+ context 'exception' do
+ let(:item) { Nanoc::Int::Item.new('<%= raise "lol" %>', {}, '/hi.md') }
+
+ it 'wraps exception' do
+ expect { subject }.to raise_error(Nanoc::Int::Errors::CompilationError)
+ end
+
+ it 'contains the right item rep in the wrapped exception' do
+ expect { subject }.to raise_error do |err|
+ expect(err.item_rep).to eql(rep)
+ end
+ end
+
+ it 'contains the right wrapped exception' do
+ expect { subject }.to raise_error do |err|
+ expect(err.unwrap).to be_a(RuntimeError)
+ expect(err.unwrap.message).to eq('lol')
+ end
+ end
+ end
+ end
+
+ describe '#compile_rep' do
+ subject { compiler.send(:compile_rep, rep, is_outdated: is_outdated) }
+
+ let(:is_outdated) { true }
+
+ it 'generates expected output' do
+ expect(rep.snapshot_contents[:last].string).to eql(item.content.string)
+ subject
+ expect(rep.snapshot_contents[:last].string).to eql('3')
+ end
+
+ it 'generates notifications in the proper order' do
+ expect(Nanoc::Int::NotificationCenter).to receive(:post).with(:compilation_started, rep).ordered
+ expect(Nanoc::Int::NotificationCenter).to receive(:post).with(:filtering_started, rep, :erb).ordered
+ expect(Nanoc::Int::NotificationCenter).to receive(:post).with(:filtering_ended, rep, :erb).ordered
+ expect(Nanoc::Int::NotificationCenter).to receive(:post).with(:compilation_ended, rep).ordered
+
+ subject
+ end
+
+ context 'interrupted compilation' do
+ let(:item) { Nanoc::Int::Item.new('other=<%= @items["/other.*"].compiled_content %>', {}, '/hi.md') }
+
+ before do
+ expect(action_provider).to receive(:memory_for).with(other_rep).and_return(memory)
+ end
+
+ it 'generates expected output' do
+ expect(rep.snapshot_contents[:last].string).to eql(item.content.string)
+
+ expect { compiler.send(:compile_rep, rep, is_outdated: true) }
+ .to raise_error(Nanoc::Int::Errors::UnmetDependency)
+ compiler.send(:compile_rep, other_rep, is_outdated: true)
+ compiler.send(:compile_rep, rep, is_outdated: true)
+
+ expect(rep.snapshot_contents[:last].string).to eql('other=other content')
+ end
+
+ it 'generates notifications in the proper order' do
+ # rep 1
+ expect(Nanoc::Int::NotificationCenter).to receive(:post).with(:compilation_started, rep).ordered
+ expect(Nanoc::Int::NotificationCenter).to receive(:post).with(:filtering_started, rep, :erb).ordered
+ expect(Nanoc::Int::NotificationCenter).to receive(:post).with(:dependency_created, item, other_item).ordered
+ expect(Nanoc::Int::NotificationCenter).to receive(:post).with(:compilation_suspended, rep, anything).ordered
+
+ # rep 2
+ expect(Nanoc::Int::NotificationCenter).to receive(:post).with(:compilation_started, other_rep).ordered
+ expect(Nanoc::Int::NotificationCenter).to receive(:post).with(:filtering_started, other_rep, :erb).ordered
+ expect(Nanoc::Int::NotificationCenter).to receive(:post).with(:filtering_ended, other_rep, :erb).ordered
+ expect(Nanoc::Int::NotificationCenter).to receive(:post).with(:compilation_ended, other_rep).ordered
+
+ # rep 1 (again)
+ expect(Nanoc::Int::NotificationCenter).to receive(:post).with(:compilation_started, rep).ordered
+ expect(Nanoc::Int::NotificationCenter).to receive(:post).with(:filtering_ended, rep, :erb).ordered
+ expect(Nanoc::Int::NotificationCenter).to receive(:post).with(:compilation_ended, rep).ordered
+
+ expect { compiler.send(:compile_rep, rep, is_outdated: true) }
+ .to raise_error(Nanoc::Int::Errors::UnmetDependency)
+ compiler.send(:compile_rep, other_rep, is_outdated: true)
+ compiler.send(:compile_rep, rep, is_outdated: true)
+ end
+ end
+ end
+end
diff --git a/spec/nanoc/base/entities/configuration_spec.rb b/spec/nanoc/base/entities/configuration_spec.rb
new file mode 100644
index 0000000..8bcf364
--- /dev/null
+++ b/spec/nanoc/base/entities/configuration_spec.rb
@@ -0,0 +1,49 @@
+describe Nanoc::Int::Configuration do
+ let(:hash) { { foo: 'bar' } }
+ let(:config) { described_class.new(hash: hash) }
+
+ describe '#key?' do
+ subject { config.key?(key) }
+
+ context 'non-existent key' do
+ let(:key) { :donkey }
+ it { is_expected.not_to be }
+ end
+
+ context 'existent key' do
+ let(:key) { :foo }
+ it { is_expected.to be }
+ end
+ end
+
+ context 'with environments defined' do
+ let(:hash) { { foo: 'bar', environments: { test: { foo: 'test-bar' }, default: { foo: 'default-bar' } } } }
+ let(:config) { described_class.new(hash: hash, env_name: env_name).with_environment }
+
+ subject { config }
+
+ context 'with existing environment' do
+ let(:env_name) { 'test' }
+
+ it 'inherits options from given environment' do
+ expect(subject[:foo]).to eq('test-bar')
+ end
+ end
+
+ context 'with unknown environment' do
+ let(:env_name) { 'wtf' }
+
+ it 'does not inherits options from any environment' do
+ expect(subject[:foo]).to eq('bar')
+ end
+ end
+
+ context 'without given environment' do
+ let(:env_name) { nil }
+
+ it 'inherits options from default environment' do
+ expect(subject[:foo]).to eq('default-bar')
+ end
+ end
+ end
+end
diff --git a/spec/nanoc/base/entities/content_spec.rb b/spec/nanoc/base/entities/content_spec.rb
new file mode 100644
index 0000000..0a66969
--- /dev/null
+++ b/spec/nanoc/base/entities/content_spec.rb
@@ -0,0 +1,193 @@
+describe Nanoc::Int::Content do
+ describe '.create' do
+ subject { described_class.create(arg, params) }
+
+ let(:params) { {} }
+
+ context 'nil arg' do
+ let(:arg) { nil }
+
+ it 'raises' do
+ expect { subject }.to raise_error(ArgumentError)
+ end
+ end
+
+ context 'content arg' do
+ let(:arg) { Nanoc::Int::TextualContent.new('foo') }
+
+ it { is_expected.to eql(arg) }
+ end
+
+ context 'with binary: true param' do
+ let(:arg) { '/foo.dat' }
+ let(:params) { { binary: true } }
+
+ it 'returns binary content' do
+ expect(subject).to be_a(Nanoc::Int::BinaryContent)
+ expect(subject.filename).to eql('/foo.dat')
+ end
+ end
+
+ context 'with binary: false param' do
+ context 'with filename param' do
+ let(:arg) { 'foo' }
+ let(:params) { { binary: false, filename: '/foo.md' } }
+
+ it 'returns textual content' do
+ expect(subject).to be_a(Nanoc::Int::TextualContent)
+ expect(subject.string).to eql('foo')
+ expect(subject.filename).to eql('/foo.md')
+ end
+ end
+
+ context 'without filename param' do
+ let(:arg) { 'foo' }
+ let(:params) { { binary: false } }
+
+ it 'returns textual content' do
+ expect(subject).to be_a(Nanoc::Int::TextualContent)
+ expect(subject.string).to eql('foo')
+ expect(subject.filename).to be_nil
+ end
+ end
+ end
+ end
+end
+
+describe Nanoc::Int::TextualContent do
+ describe '#initialize' do
+ context 'without filename' do
+ let(:content) { described_class.new('foo') }
+
+ it 'sets string and filename' do
+ expect(content.string).to eq('foo')
+ expect(content.filename).to be_nil
+ end
+ end
+
+ context 'with absolute filename' do
+ let(:content) { described_class.new('foo', filename: '/foo.md') }
+
+ it 'sets string and filename' do
+ expect(content.string).to eq('foo')
+ expect(content.filename).to eq('/foo.md')
+ end
+ end
+
+ context 'with relative filename' do
+ let(:content) { described_class.new('foo', filename: 'foo.md') }
+
+ it 'errors' do
+ expect { content }.to raise_error(ArgumentError)
+ end
+ end
+
+ context 'with proc' do
+ let(:content_proc) { -> { 'foo' } }
+ let(:content) { described_class.new(content_proc) }
+
+ it 'does not call the proc immediately' do
+ expect(content_proc).not_to receive(:call)
+
+ content
+ end
+
+ it 'sets string' do
+ expect(content_proc).to receive(:call).once.and_return('dataz')
+
+ expect(content.string).to eq('dataz')
+ end
+
+ it 'only calls the proc once' do
+ expect(content_proc).to receive(:call).once.and_return('dataz')
+
+ expect(content.string).to eq('dataz')
+ expect(content.string).to eq('dataz')
+ end
+ end
+ end
+
+ describe '#binary?' do
+ subject { content.binary? }
+ let(:content) { described_class.new('foo') }
+ it { is_expected.to eql(false) }
+ end
+
+ describe '#freeze' do
+ let(:content) { described_class.new('foo', filename: '/asdf.md') }
+
+ before do
+ content.freeze
+ end
+
+ it 'prevents changes to string' do
+ expect(content.string).to be_frozen
+ expect { content.string << 'asdf' }.to raise_frozen_error
+ end
+
+ it 'prevents changes to filename' do
+ expect(content.filename).to be_frozen
+ expect { content.filename << 'asdf' }.to raise_frozen_error
+ end
+
+ context 'with proc' do
+ let(:content) { described_class.new(proc { 'foo' }) }
+
+ it 'prevents changes to string' do
+ expect(content.string).to be_frozen
+ expect { content.string << 'asdf' }.to raise_frozen_error
+ end
+ end
+ end
+
+ describe 'marshalling' do
+ let(:content) { described_class.new('foo', filename: '/foo.md') }
+
+ it 'dumps as an array' do
+ expect(content.marshal_dump).to eq(['/foo.md', 'foo'])
+ end
+
+ it 'restores a dumped object' do
+ restored = Marshal.load(Marshal.dump(content))
+ expect(restored.string).to eq('foo')
+ expect(restored.filename).to eq('/foo.md')
+ end
+ end
+end
+
+describe Nanoc::Int::BinaryContent do
+ describe '#initialize' do
+ let(:content) { described_class.new('/foo.dat') }
+
+ it 'sets filename' do
+ expect(content.filename).to eql('/foo.dat')
+ end
+
+ context 'with relative filename' do
+ let(:content) { described_class.new('foo.dat') }
+
+ it 'errors' do
+ expect { content }.to raise_error(ArgumentError)
+ end
+ end
+ end
+
+ describe '#binary?' do
+ subject { content.binary? }
+ let(:content) { described_class.new('/foo.dat') }
+ it { is_expected.to eql(true) }
+ end
+
+ describe '#freeze' do
+ let(:content) { described_class.new('/foo.dat') }
+
+ before do
+ content.freeze
+ end
+
+ it 'prevents changes' do
+ expect(content.filename).to be_frozen
+ expect { content.filename << 'asdf' }.to raise_frozen_error
+ end
+ end
+end
diff --git a/spec/nanoc/base/entities/document_spec.rb b/spec/nanoc/base/entities/document_spec.rb
new file mode 100644
index 0000000..a8e28f0
--- /dev/null
+++ b/spec/nanoc/base/entities/document_spec.rb
@@ -0,0 +1,206 @@
+shared_examples 'a document' do
+ describe '#initialize' do
+ let(:content_arg) { 'Hello world' }
+ let(:attributes_arg) { { 'title' => 'Home' } }
+ let(:identifier_arg) { '/home.md' }
+ let(:checksum_data_arg) { 'abcdef' }
+ let(:content_checksum_data_arg) { 'con-cs' }
+ let(:attributes_checksum_data_arg) { 'attr-cs' }
+
+ subject do
+ described_class.new(
+ content_arg,
+ attributes_arg,
+ identifier_arg,
+ checksum_data: checksum_data_arg,
+ content_checksum_data: content_checksum_data_arg,
+ attributes_checksum_data: attributes_checksum_data_arg,
+ )
+ end
+
+ describe 'content arg' do
+ context 'string' do
+ it 'converts content' do
+ expect(subject.content).to be_a(Nanoc::Int::TextualContent)
+ expect(subject.content.string).to eql('Hello world')
+ end
+ end
+
+ context 'content' do
+ let(:content_arg) { Nanoc::Int::TextualContent.new('foo') }
+
+ it 'reuses content' do
+ expect(subject.content).to equal(content_arg)
+ end
+ end
+ end
+
+ describe 'attributes arg' do
+ context 'hash' do
+ it 'symbolizes attributes' do
+ expect(subject.attributes).to eq(title: 'Home')
+ end
+ end
+
+ context 'proc' do
+ call_count = 0
+ let(:attributes_arg) do
+ proc do
+ call_count += 1
+ { 'title' => 'Home' }
+ end
+ end
+
+ before do
+ call_count = 0
+ end
+
+ it 'does not call the proc immediately' do
+ expect(call_count).to eql(0)
+ end
+
+ it 'symbolizes attributes' do
+ expect(subject.attributes).to eq(title: 'Home')
+ end
+
+ it 'only calls the proc once' do
+ subject.attributes
+ subject.attributes
+ expect(call_count).to eql(1)
+ end
+ end
+ end
+
+ describe 'identifier arg' do
+ context 'string' do
+ it 'converts identifier' do
+ expect(subject.identifier).to be_a(Nanoc::Identifier)
+ expect(subject.identifier.to_s).to eql('/home.md')
+ end
+ end
+
+ context 'identifier' do
+ let(:identifier_arg) { Nanoc::Identifier.new('/foo.md') }
+
+ it 'retains identifier' do
+ expect(subject.identifier).to equal(identifier_arg)
+ end
+ end
+ end
+
+ describe 'checksum_data arg' do
+ it 'reuses checksum_data' do
+ expect(subject.checksum_data).to eql(checksum_data_arg)
+ end
+ end
+
+ describe 'content_checksum_data arg' do
+ it 'reuses content_checksum_data' do
+ expect(subject.content_checksum_data).to eql(content_checksum_data_arg)
+ end
+ end
+
+ describe 'attributes_checksum_data arg' do
+ it 'reuses attributes_checksum_data' do
+ expect(subject.attributes_checksum_data).to eql(attributes_checksum_data_arg)
+ end
+ end
+ end
+
+ describe '#freeze' do
+ let(:content_arg) { 'Hallo' }
+ let(:attributes_arg) { { foo: { bar: 'asdf' } } }
+ let(:document) { described_class.new(content_arg, attributes_arg, '/foo.md') }
+
+ before do
+ document.freeze
+ end
+
+ it 'refuses changes to content' do
+ expect { document.instance_variable_set(:@content, 'hah') }.to raise_frozen_error
+ expect { document.content.string << 'hah' }.to raise_frozen_error
+ end
+
+ it 'refuses to change attributes' do
+ expect { document.instance_variable_set(:@attributes, a: 'Hi') }.to raise_frozen_error
+ expect { document.attributes[:title] = 'Bye' }.to raise_frozen_error
+ expect { document.attributes[:foo][:bar] = 'fdsa' }.to raise_frozen_error
+ end
+
+ it 'refuses to change identifier' do
+ expect { document.identifier = '/asdf' }.to raise_frozen_error
+ expect { document.identifier.to_s << '/omg' }.to raise_frozen_error
+ end
+
+ context 'binary content' do
+ let(:content_arg) { Nanoc::Int::BinaryContent.new(File.expand_path('foo.dat')) }
+
+ it 'refuses changes to content' do
+ expect { document.instance_variable_set(:@content, 'hah') }.to raise_frozen_error
+ expect { document.content.filename << 'hah' }.to raise_frozen_error
+ end
+ end
+
+ context 'attributes block' do
+ let(:attributes_arg) { proc { { foo: { bar: 'asdf' } } } }
+
+ it 'gives access to the attributes' do
+ expect(document.attributes).to eql(foo: { bar: 'asdf' })
+ end
+
+ it 'refuses to change attributes' do
+ expect { document.instance_variable_set(:@attributes, a: 'Hi') }.to raise_frozen_error
+ expect { document.attributes[:title] = 'Bye' }.to raise_frozen_error
+ expect { document.attributes[:foo][:bar] = 'fdsa' }.to raise_frozen_error
+ end
+ end
+ end
+
+ describe 'equality' do
+ let(:content_arg_a) { 'Hello world' }
+ let(:content_arg_b) { 'Bye world' }
+
+ let(:attributes_arg_a) { { 'title' => 'Home' } }
+ let(:attributes_arg_b) { { 'title' => 'About' } }
+
+ let(:identifier_arg_a) { '/home.md' }
+ let(:identifier_arg_b) { '/home.md' }
+
+ let(:document_a) { described_class.new(content_arg_a, attributes_arg_a, identifier_arg_a) }
+ let(:document_b) { described_class.new(content_arg_b, attributes_arg_b, identifier_arg_b) }
+
+ subject { document_a == document_b }
+
+ context 'same identifier' do
+ let(:identifier_arg_a) { '/home.md' }
+ let(:identifier_arg_b) { '/home.md' }
+
+ it { is_expected.to eql(true) }
+
+ it 'has same hashes' do
+ expect(document_a.hash).to eql(document_b.hash)
+ end
+ end
+
+ context 'different identifier' do
+ let(:identifier_arg_a) { '/home.md' }
+ let(:identifier_arg_b) { '/about.md' }
+
+ it { is_expected.to eql(false) }
+
+ it 'has different hashes' do
+ expect(document_a.hash).not_to eql(document_b.hash)
+ end
+ end
+
+ context 'comparing with non-document' do
+ let(:document_b) { nil }
+
+ it { is_expected.to eql(false) }
+
+ it 'has different hashes' do
+ expect(document_a.hash).not_to eql(document_b.hash)
+ end
+ end
+ end
+end
diff --git a/spec/nanoc/base/entities/identifier_spec.rb b/spec/nanoc/base/entities/identifier_spec.rb
new file mode 100644
index 0000000..21738d6
--- /dev/null
+++ b/spec/nanoc/base/entities/identifier_spec.rb
@@ -0,0 +1,460 @@
+describe Nanoc::Identifier do
+ describe '.from' do
+ subject { described_class.from(arg) }
+
+ context 'given an identifier' do
+ let(:arg) { Nanoc::Identifier.new('/foo.md') }
+
+ it 'returns an identifier' do
+ expect(subject).to be_a(Nanoc::Identifier)
+ expect(subject.to_s).to eq('/foo.md')
+ expect(subject).to be_full
+ end
+ end
+
+ context 'given a string' do
+ let(:arg) { '/foo.md' }
+
+ it 'returns an identifier' do
+ expect(subject).to be_a(Nanoc::Identifier)
+ expect(subject.to_s).to eq('/foo.md')
+ expect(subject).to be_full
+ end
+ end
+
+ context 'given something else' do
+ let(:arg) { 12_345 }
+
+ it 'raises' do
+ expect { subject }.to raise_error(Nanoc::Identifier::NonCoercibleObjectError)
+ end
+ end
+ end
+
+ describe '#initialize' do
+ context 'legacy type' do
+ it 'does not convert already clean paths' do
+ expect(described_class.new('/foo/bar/', type: :legacy).to_s).to eql('/foo/bar/')
+ end
+
+ it 'prepends slash if necessary' do
+ expect(described_class.new('foo/bar/', type: :legacy).to_s).to eql('/foo/bar/')
+ end
+
+ it 'appends slash if necessary' do
+ expect(described_class.new('/foo/bar', type: :legacy).to_s).to eql('/foo/bar/')
+ end
+
+ it 'removes double slashes at start' do
+ expect(described_class.new('//foo/bar/', type: :legacy).to_s).to eql('/foo/bar/')
+ end
+
+ it 'removes double slashes at end' do
+ expect(described_class.new('/foo/bar//', type: :legacy).to_s).to eql('/foo/bar/')
+ end
+
+ it 'freezes' do
+ identifier = described_class.new('/foo/bar/', type: :legacy)
+ expect { identifier.to_s << 'bbq' }.to raise_frozen_error
+ end
+ end
+
+ context 'full type' do
+ it 'refuses string not starting with a slash' do
+ expect { described_class.new('foo') }
+ .to raise_error(Nanoc::Identifier::InvalidIdentifierError)
+ end
+
+ it 'has proper string representation' do
+ expect(described_class.new('/foo').to_s).to eql('/foo')
+ end
+
+ it 'freezes' do
+ identifier = described_class.new('/foo/bar')
+ expect { identifier.to_s << 'bbq' }.to raise_frozen_error
+ end
+ end
+
+ context 'other type' do
+ it 'errors' do
+ expect { described_class.new('foo', type: :donkey) }
+ .to raise_error(Nanoc::Identifier::InvalidTypeError)
+ end
+ end
+
+ context 'default type' do
+ it 'is full' do
+ expect(described_class.new('/foo')).to be_full
+ end
+ end
+
+ context 'other args specified' do
+ it 'errors' do
+ expect { described_class.new('?', animal: :donkey) }
+ .to raise_error(ArgumentError)
+ end
+ end
+ end
+
+ describe '#to_s' do
+ it 'returns immutable string' do
+ expect { described_class.new('foo/', type: :legacy).to_s << 'lols' }.to raise_frozen_error
+ expect { described_class.new('/foo').to_s << 'lols' }.to raise_frozen_error
+ end
+ end
+
+ describe '#to_str' do
+ it 'returns immutable string' do
+ expect { described_class.new('/foo/bar').to_str << 'lols' }.to raise_frozen_error
+ end
+ end
+
+ describe 'Comparable' do
+ it 'can be compared' do
+ expect(described_class.new('/foo/bar') <= '/qux').to eql(true)
+ end
+ end
+
+ describe '#inspect' do
+ let(:identifier) { described_class.new('/foo/bar') }
+
+ subject { identifier.inspect }
+
+ it { should == '<Nanoc::Identifier type=full "/foo/bar">' }
+ end
+
+ describe '#== and #eql?' do
+ context 'comparing with equal identifier' do
+ let(:identifier_a) { described_class.new('//foo/bar/', type: :legacy) }
+ let(:identifier_b) { described_class.new('/foo/bar//', type: :legacy) }
+
+ it 'is ==' do
+ expect(identifier_a).to eq(identifier_b)
+ end
+
+ it 'is eql?' do
+ expect(identifier_a).to eql(identifier_b)
+ end
+ end
+
+ context 'comparing with equal string' do
+ let(:identifier_a) { described_class.new('//foo/bar/', type: :legacy) }
+ let(:identifier_b) { '/foo/bar/' }
+
+ it 'is ==' do
+ expect(identifier_a).to eq(identifier_b.to_s)
+ end
+
+ it 'is not eql?' do
+ expect(identifier_a).not_to eql(identifier_b.to_s)
+ end
+ end
+
+ context 'comparing with different identifier' do
+ let(:identifier_a) { described_class.new('//foo/bar/', type: :legacy) }
+ let(:identifier_b) { described_class.new('/baz/qux//', type: :legacy) }
+
+ it 'is not ==' do
+ expect(identifier_a).not_to eq(identifier_b)
+ end
+
+ it 'is not eql?' do
+ expect(identifier_a).not_to eql(identifier_b)
+ end
+ end
+
+ context 'comparing with different string' do
+ let(:identifier_a) { described_class.new('//foo/bar/', type: :legacy) }
+ let(:identifier_b) { '/baz/qux/' }
+
+ it 'is not equal' do
+ expect(identifier_a).not_to eq(identifier_b)
+ end
+
+ it 'is not eql?' do
+ expect(identifier_a).not_to eql(identifier_b)
+ end
+ end
+
+ context 'comparing with something that is not an identifier' do
+ let(:identifier_a) { described_class.new('//foo/bar/', type: :legacy) }
+ let(:identifier_b) { :donkey }
+
+ it 'is not equal' do
+ expect(identifier_a).not_to eq(identifier_b)
+ expect(identifier_a).not_to eql(identifier_b)
+ end
+ end
+ end
+
+ describe '#hash' do
+ context 'equal identifiers' do
+ let(:identifier_a) { described_class.new('//foo/bar/', type: :legacy) }
+ let(:identifier_b) { described_class.new('/foo/bar//', type: :legacy) }
+
+ it 'is the same' do
+ expect(identifier_a.hash == identifier_b.hash).to eql(true)
+ end
+ end
+
+ context 'different identifiers' do
+ let(:identifier_a) { described_class.new('//foo/bar/', type: :legacy) }
+ let(:identifier_b) { described_class.new('/monkey/', type: :legacy) }
+
+ it 'is different' do
+ expect(identifier_a.hash == identifier_b.hash).to eql(false)
+ end
+ end
+ end
+
+ describe '#=~' do
+ let(:identifier) { described_class.new('/foo/bar') }
+
+ subject { identifier =~ pat }
+
+ context 'given a regex' do
+ context 'matching regex' do
+ let(:pat) { %r{\A/foo/bar} }
+ it { is_expected.to eql(0) }
+ end
+
+ context 'non-matching regex' do
+ let(:pat) { %r{\A/qux/monkey} }
+ it { is_expected.to eql(nil) }
+ end
+ end
+
+ context 'given a string' do
+ context 'matching string' do
+ let(:pat) { '/foo/*' }
+ it { is_expected.to eql(0) }
+ end
+
+ context 'non-matching string' do
+ let(:pat) { '/qux/*' }
+ it { is_expected.to eql(nil) }
+ end
+ end
+ end
+
+ describe '#<=>' do
+ let(:identifier) { described_class.new('/foo/bar') }
+
+ it 'compares by string' do
+ expect(identifier <=> '/foo/aarghh').to eql(1)
+ expect(identifier <=> '/foo/bar').to eql(0)
+ expect(identifier <=> '/foo/qux').to eql(-1)
+ end
+ end
+
+ describe '#prefix' do
+ let(:identifier) { described_class.new('/foo') }
+
+ subject { identifier.prefix(prefix) }
+
+ context 'prefix not ending nor starting with a slash' do
+ let(:prefix) { 'asdf' }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(
+ Nanoc::Identifier::InvalidPrefixError,
+ 'Invalid prefix (does not start with a slash): "asdf"',
+ )
+ end
+ end
+
+ context 'prefix ending with a slash' do
+ let(:prefix) { 'asdf/' }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(
+ Nanoc::Identifier::InvalidPrefixError,
+ 'Invalid prefix (does not start with a slash): "asdf/"',
+ )
+ end
+ end
+
+ context 'prefix ending and starting with a slash' do
+ let(:prefix) { '/asdf/' }
+
+ it 'returns a proper new identifier' do
+ expect(subject).to be_a(Nanoc::Identifier)
+ expect(subject.to_s).to eql('/asdf/foo')
+ end
+ end
+
+ context 'prefix nstarting with a slash' do
+ let(:prefix) { '/asdf' }
+
+ it 'returns a proper new identifier' do
+ expect(subject).to be_a(Nanoc::Identifier)
+ expect(subject.to_s).to eql('/asdf/foo')
+ end
+ end
+ end
+
+ describe '#without_ext' do
+ subject { identifier.without_ext }
+
+ context 'legacy type' do
+ let(:identifier) { described_class.new('/foo/', type: :legacy) }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(Nanoc::Identifier::UnsupportedLegacyOperationError)
+ end
+ end
+
+ context 'identifier with no extension' do
+ let(:identifier) { described_class.new('/foo') }
+
+ it 'does nothing' do
+ expect(subject).to eql('/foo')
+ end
+ end
+
+ context 'identifier with extension' do
+ let(:identifier) { described_class.new('/foo.md') }
+
+ it 'removes the extension' do
+ expect(subject).to eql('/foo')
+ end
+ end
+ end
+
+ describe '#ext' do
+ subject { identifier.ext }
+
+ context 'legacy type' do
+ let(:identifier) { described_class.new('/foo/', type: :legacy) }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(Nanoc::Identifier::UnsupportedLegacyOperationError)
+ end
+ end
+
+ context 'identifier with no extension' do
+ let(:identifier) { described_class.new('/foo') }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'identifier with extension' do
+ let(:identifier) { described_class.new('/foo.md') }
+
+ it { is_expected.to eql('md') }
+ end
+ end
+
+ describe '#without_exts' do
+ subject { identifier.without_exts }
+
+ context 'legacy type' do
+ let(:identifier) { described_class.new('/foo/', type: :legacy) }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(Nanoc::Identifier::UnsupportedLegacyOperationError)
+ end
+ end
+
+ context 'identifier with no extension' do
+ let(:identifier) { described_class.new('/foo') }
+
+ it 'does nothing' do
+ expect(subject).to eql('/foo')
+ end
+ end
+
+ context 'identifier with one extension' do
+ let(:identifier) { described_class.new('/foo.md') }
+
+ it 'removes the extension' do
+ expect(subject).to eql('/foo')
+ end
+ end
+
+ context 'identifier with multiple extensions' do
+ let(:identifier) { described_class.new('/foo.html.md') }
+
+ it 'removes the extension' do
+ expect(subject).to eql('/foo')
+ end
+ end
+ end
+
+ describe '#exts' do
+ subject { identifier.exts }
+
+ context 'legacy type' do
+ let(:identifier) { described_class.new('/foo/', type: :legacy) }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(Nanoc::Identifier::UnsupportedLegacyOperationError)
+ end
+ end
+
+ context 'identifier with no extension' do
+ let(:identifier) { described_class.new('/foo') }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'identifier with one extension' do
+ let(:identifier) { described_class.new('/foo.md') }
+
+ it { is_expected.to eql(['md']) }
+ end
+
+ context 'identifier with multiple extensions' do
+ let(:identifier) { described_class.new('/foo.html.md') }
+
+ it { is_expected.to eql(%w(html md)) }
+ end
+ end
+
+ describe '#legacy?' do
+ subject { identifier.legacy? }
+
+ context 'legacy type' do
+ let(:identifier) { described_class.new('/foo/', type: :legacy) }
+ it { is_expected.to eql(true) }
+ end
+
+ context 'full type' do
+ let(:identifier) { described_class.new('/foo/', type: :full) }
+ it { is_expected.to eql(false) }
+ end
+ end
+
+ describe '#full?' do
+ subject { identifier.full? }
+
+ context 'legacy type' do
+ let(:identifier) { described_class.new('/foo/', type: :legacy) }
+ it { is_expected.to eql(false) }
+ end
+
+ context 'full type' do
+ let(:identifier) { described_class.new('/foo/', type: :full) }
+ it { is_expected.to eql(true) }
+ end
+ end
+
+ describe '#components' do
+ subject { identifier.components }
+
+ context 'no components' do
+ let(:identifier) { described_class.new('/') }
+ it { is_expected.to eql([]) }
+ end
+
+ context 'one component' do
+ let(:identifier) { described_class.new('/foo.md') }
+ it { is_expected.to eql(['foo.md']) }
+ end
+
+ context 'two components' do
+ let(:identifier) { described_class.new('/foo/bar.md') }
+ it { is_expected.to eql(['foo', 'bar.md']) }
+ end
+ end
+end
diff --git a/spec/nanoc/base/entities/item_rep_spec.rb b/spec/nanoc/base/entities/item_rep_spec.rb
new file mode 100644
index 0000000..ec17ae2
--- /dev/null
+++ b/spec/nanoc/base/entities/item_rep_spec.rb
@@ -0,0 +1,226 @@
+describe Nanoc::Int::ItemRep do
+ let(:item) { Nanoc::Int::Item.new('asdf', {}, '/foo.md') }
+ let(:rep) { Nanoc::Int::ItemRep.new(item, :giraffe) }
+
+ describe '#compiled_content' do
+ let(:snapshot_name) { raise 'override me' }
+ subject { rep.compiled_content(snapshot: snapshot_name) }
+
+ shared_examples 'a non-moving snapshot with content' do
+ context 'no snapshot def' do
+ it 'raises' do
+ expect { subject }.to raise_error(Nanoc::Int::Errors::NoSuchSnapshot)
+ end
+ end
+
+ context 'snapshot def exists' do
+ before do
+ rep.snapshot_defs = [Nanoc::Int::SnapshotDef.new(snapshot_name)]
+ rep.snapshot_contents = { snapshot_name => content }
+ end
+
+ context 'content is textual' do
+ let(:content) { Nanoc::Int::TextualContent.new('hellos') }
+ it { is_expected.to eql('hellos') }
+ end
+
+ context 'content is binary' do
+ before { File.write('donkey.dat', 'binary data') }
+ let(:content) { Nanoc::Int::BinaryContent.new(File.expand_path('donkey.dat')) }
+
+ it 'raises' do
+ expect { subject }.to raise_error(Nanoc::Int::Errors::CannotGetCompiledContentOfBinaryItem)
+ end
+ end
+ end
+ end
+
+ shared_examples 'a non-moving snapshot' do
+ include_examples 'a non-moving snapshot with content'
+
+ context 'snapshot def exists, but not content' do
+ before do
+ rep.snapshot_defs = [Nanoc::Int::SnapshotDef.new(snapshot_name)]
+ rep.snapshot_contents = {}
+ end
+
+ it 'errors' do
+ expect { subject }.to yield_from_fiber(an_instance_of(Nanoc::Int::Errors::UnmetDependency))
+ end
+ end
+ end
+
+ shared_examples 'snapshot :last' do
+ context 'no snapshot def' do
+ it 'errors' do
+ expect { subject }.to raise_error(Nanoc::Int::Errors::NoSuchSnapshot)
+ end
+ end
+
+ context 'snapshot exists' do
+ context 'snapshot is not final' do
+ before do
+ rep.snapshot_defs = [Nanoc::Int::SnapshotDef.new(snapshot_name)]
+ end
+
+ context 'snapshot content does not exist' do
+ before do
+ rep.snapshot_contents = {}
+ end
+
+ it 'errors' do
+ expect { subject }.to yield_from_fiber(an_instance_of(Nanoc::Int::Errors::UnmetDependency))
+ end
+ end
+
+ context 'snapshot content exists' do
+ context 'content is textual' do
+ before do
+ rep.snapshot_contents[snapshot_name] = Nanoc::Int::TextualContent.new('hellos')
+ end
+
+ context 'not compiled' do
+ before { rep.compiled = false }
+
+ it 'raises' do
+ expect { subject }.to yield_from_fiber(an_instance_of(Nanoc::Int::Errors::UnmetDependency))
+ end
+ end
+
+ context 'compiled' do
+ before { rep.compiled = true }
+
+ it { is_expected.to eql('hellos') }
+ end
+ end
+
+ context 'content is binary' do
+ before do
+ File.write('donkey.dat', 'binary data')
+ rep.snapshot_contents[snapshot_name] = Nanoc::Int::BinaryContent.new(File.expand_path('donkey.dat'))
+ end
+
+ context 'not compiled' do
+ before { rep.compiled = false }
+
+ it 'raises' do
+ expect { subject }.to yield_from_fiber(an_instance_of(Nanoc::Int::Errors::UnmetDependency))
+ end
+ end
+
+ context 'compiled' do
+ before { rep.compiled = true }
+
+ it 'raises' do
+ expect { subject }.to raise_error(Nanoc::Int::Errors::CannotGetCompiledContentOfBinaryItem)
+ end
+ end
+ end
+ end
+ end
+
+ context 'snapshot is final' do
+ before do
+ rep.snapshot_defs = [Nanoc::Int::SnapshotDef.new(snapshot_name)]
+ end
+
+ context 'snapshot content does not exist' do
+ before do
+ rep.snapshot_contents = {}
+ end
+
+ it 'errors' do
+ expect { subject }.to yield_from_fiber(an_instance_of(Nanoc::Int::Errors::UnmetDependency))
+ end
+ end
+
+ context 'snapshot content exists' do
+ context 'content is textual' do
+ before do
+ rep.snapshot_contents[snapshot_name] = Nanoc::Int::TextualContent.new('hellos')
+ end
+
+ context 'not compiled' do
+ before { rep.compiled = false }
+
+ it 'errors' do
+ expect { subject }.to yield_from_fiber(an_instance_of(Nanoc::Int::Errors::UnmetDependency))
+ end
+ end
+
+ context 'compiled' do
+ before { rep.compiled = true }
+
+ it { is_expected.to eql('hellos') }
+ end
+ end
+
+ context 'content is binary' do
+ before do
+ File.write('donkey.dat', 'binary data')
+ rep.snapshot_contents[snapshot_name] = Nanoc::Int::BinaryContent.new(File.expand_path('donkey.dat'))
+ end
+
+ context 'not compiled' do
+ before { rep.compiled = false }
+
+ it 'raises' do
+ expect { subject }.to yield_from_fiber(an_instance_of(Nanoc::Int::Errors::UnmetDependency))
+ end
+ end
+
+ context 'compiled' do
+ before { rep.compiled = true }
+
+ it 'raises' do
+ expect { subject }.to raise_error(Nanoc::Int::Errors::CannotGetCompiledContentOfBinaryItem)
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+
+ context 'snapshot nil' do
+ let(:snapshot_name) { :last }
+ subject { rep.compiled_content(snapshot: nil) }
+ include_examples 'snapshot :last'
+ end
+
+ context 'snapshot not specified' do
+ subject { rep.compiled_content }
+
+ context 'pre exists' do
+ before { rep.snapshot_contents[:pre] = Nanoc::Int::TextualContent.new('omg') }
+ let(:snapshot_name) { :pre }
+ include_examples 'a non-moving snapshot with content'
+ end
+
+ context 'pre does not exist' do
+ let(:snapshot_name) { :last }
+ include_examples 'snapshot :last'
+ end
+ end
+
+ context 'snapshot :pre specified' do
+ let(:snapshot_name) { :pre }
+ include_examples 'a non-moving snapshot'
+ end
+
+ context 'snapshot :post specified' do
+ let(:snapshot_name) { :post }
+ include_examples 'a non-moving snapshot'
+ end
+
+ context 'snapshot :last specified' do
+ let(:snapshot_name) { :last }
+ include_examples 'snapshot :last'
+ end
+
+ context 'snapshot :donkey specified' do
+ let(:snapshot_name) { :donkey }
+ include_examples 'a non-moving snapshot'
+ end
+ end
+end
diff --git a/spec/nanoc/base/entities/item_spec.rb b/spec/nanoc/base/entities/item_spec.rb
new file mode 100644
index 0000000..fceaf2e
--- /dev/null
+++ b/spec/nanoc/base/entities/item_spec.rb
@@ -0,0 +1,3 @@
+describe Nanoc::Int::Item do
+ it_behaves_like 'a document'
+end
diff --git a/spec/nanoc/base/entities/layout_spec.rb b/spec/nanoc/base/entities/layout_spec.rb
new file mode 100644
index 0000000..a15211d
--- /dev/null
+++ b/spec/nanoc/base/entities/layout_spec.rb
@@ -0,0 +1,3 @@
+describe Nanoc::Int::Layout do
+ it_behaves_like 'a document'
+end
diff --git a/spec/nanoc/base/entities/lazy_value_spec.rb b/spec/nanoc/base/entities/lazy_value_spec.rb
new file mode 100644
index 0000000..ca5f932
--- /dev/null
+++ b/spec/nanoc/base/entities/lazy_value_spec.rb
@@ -0,0 +1,106 @@
+describe Nanoc::Int::LazyValue do
+ describe '#value' do
+ let(:value_arg) { 'Hello world' }
+ let(:lazy_value) { described_class.new(value_arg) }
+
+ subject { lazy_value.value }
+
+ context 'object' do
+ it { is_expected.to equal(value_arg) }
+ end
+
+ context 'proc' do
+ it 'does not call the proc immediately' do
+ expect(value_arg).not_to receive(:call)
+
+ lazy_value
+ end
+
+ it 'returns proc return value' do
+ expect(value_arg).to receive(:call).once.and_return('Hello proc')
+
+ expect(subject).to eql('Hello proc')
+ end
+
+ it 'only calls the proc once' do
+ expect(value_arg).to receive(:call).once.and_return('Hello proc')
+
+ expect(subject).to eql('Hello proc')
+ expect(subject).to eql('Hello proc')
+ end
+ end
+ end
+
+ describe '#map' do
+ let(:value_arg) { -> { 'Hello world' } }
+ let(:lazy_value) { described_class.new(value_arg) }
+
+ subject { lazy_value.map(&:upcase) }
+
+ it 'does not call the proc immediately' do
+ expect(value_arg).not_to receive(:call)
+
+ subject
+ end
+
+ it 'returns proc return value' do
+ expect(value_arg).to receive(:call).once.and_return('Hello proc')
+
+ expect(subject.value).to eql('HELLO PROC')
+ end
+
+ it 'only calls the proc once' do
+ expect(value_arg).to receive(:call).once.and_return('Hello proc')
+
+ expect(subject.value).to eql('HELLO PROC')
+ expect(subject.value).to eql('HELLO PROC')
+ end
+ end
+
+ describe '#freeze' do
+ let(:value_arg) { 'Hello world' }
+
+ subject { described_class.new(value_arg) }
+
+ before do
+ subject.freeze
+ end
+
+ context 'object' do
+ it 'returns value' do
+ expect(subject.value).to equal(value_arg)
+ end
+
+ it 'freezes value' do
+ expect(subject.value).to be_frozen
+ end
+ end
+
+ context 'proc' do
+ call_count = 0
+ let(:value_arg) do
+ proc do
+ call_count += 1
+ 'Hello proc'
+ end
+ end
+
+ before do
+ call_count = 0
+ subject.freeze
+ end
+
+ it 'does not call the proc immediately' do
+ expect(call_count).to eql(0)
+ end
+
+ it 'returns proc return value' do
+ expect(subject.value).to eq('Hello proc')
+ end
+
+ it 'freezes upon access' do
+ expect(subject.value).to be_frozen
+ end
+ end
+ end
+end
diff --git a/spec/nanoc/base/entities/outdatedness_status_spec.rb b/spec/nanoc/base/entities/outdatedness_status_spec.rb
new file mode 100644
index 0000000..319219f
--- /dev/null
+++ b/spec/nanoc/base/entities/outdatedness_status_spec.rb
@@ -0,0 +1,113 @@
+describe Nanoc::Int::OutdatednessStatus do
+ let(:status) { described_class.new }
+
+ describe '#reasons' do
+ subject { status.reasons }
+
+ context 'default' do
+ it { is_expected.to eql([]) }
+ end
+
+ context 'one passed in' do
+ let(:reasons) do
+ [
+ Nanoc::Int::OutdatednessReasons::CodeSnippetsModified,
+ ]
+ end
+
+ let(:status) { described_class.new(reasons: reasons) }
+
+ it { is_expected.to eql(reasons) }
+ end
+
+ context 'two passed in' do
+ let(:reasons) do
+ [
+ Nanoc::Int::OutdatednessReasons::CodeSnippetsModified,
+ Nanoc::Int::OutdatednessReasons::ContentModified,
+ ]
+ end
+
+ let(:status) { described_class.new(reasons: reasons) }
+
+ it { is_expected.to eql(reasons) }
+ end
+ end
+
+ describe '#props' do
+ subject { status.props.active }
+
+ context 'default' do
+ it { is_expected.to eql(Set.new) }
+ end
+
+ context 'specific one passed in' do
+ let(:props) do
+ Nanoc::Int::Props.new(attributes: true)
+ end
+
+ let(:status) { described_class.new(props: props) }
+
+ it { is_expected.to eql(Set.new([:attributes])) }
+ end
+ end
+
+ describe '#useful_to_apply' do
+ subject { status.useful_to_apply?(rule) }
+
+ let(:status) { described_class.new(props: props) }
+ let(:props) { Nanoc::Int::Props.new }
+ let(:rule) { Nanoc::Int::OutdatednessRules::RulesModified }
+
+ context 'no props' do
+ it { is_expected.to be }
+ end
+
+ context 'some props' do
+ context 'same props' do
+ let(:props) { Nanoc::Int::Props.new(compiled_content: true, path: true) }
+ it { is_expected.not_to be }
+ end
+
+ context 'different props' do
+ let(:props) { Nanoc::Int::Props.new(attributes: true) }
+ it { is_expected.to be }
+ end
+ end
+
+ context 'all props' do
+ let(:props) { Nanoc::Int::Props.new(raw_content: true, attributes: true, compiled_content: true, path: true) }
+ it { is_expected.not_to be }
+ end
+ end
+
+ describe '#update' do
+ subject { status.update(reason) }
+
+ let(:reason) { Nanoc::Int::OutdatednessReasons::ContentModified }
+
+ context 'no existing reason or props' do
+ it 'adds a reason' do
+ expect(subject.reasons).to eql([reason])
+ end
+ end
+
+ context 'existing reason' do
+ let(:status) { described_class.new(reasons: [old_reason]) }
+
+ let(:old_reason) { Nanoc::Int::OutdatednessReasons::NotWritten }
+
+ it 'adds a reason' do
+ expect(subject.reasons).to eql([old_reason, reason])
+ end
+ end
+
+ context 'existing props' do
+ let(:status) { described_class.new(props: Nanoc::Int::Props.new(attributes: true)) }
+
+ it 'updates props' do
+ expect(subject.props.active).to eql(Set.new([:raw_content, :attributes, :compiled_content]))
+ end
+ end
+ end
+end
diff --git a/spec/nanoc/base/entities/pattern_spec.rb b/spec/nanoc/base/entities/pattern_spec.rb
new file mode 100644
index 0000000..c0629e5
--- /dev/null
+++ b/spec/nanoc/base/entities/pattern_spec.rb
@@ -0,0 +1,125 @@
+describe Nanoc::Int::Pattern do
+ describe '.from' do
+ it 'converts from string' do
+ pattern = described_class.from('/foo/x[ab]z/bar.*')
+ expect(pattern.match?('/foo/xaz/bar.html')).to eql(true)
+ expect(pattern.match?('/foo/xyz/bar.html')).to eql(false)
+ end
+
+ it 'converts from regex' do
+ pattern = described_class.from(%r{\A/foo/x[ab]z/bar\..*\z})
+ expect(pattern.match?('/foo/xaz/bar.html')).to eql(true)
+ expect(pattern.match?('/foo/xyz/bar.html')).to eql(false)
+ end
+
+ it 'converts from pattern' do
+ pattern = described_class.from('/foo/x[ab]z/bar.*')
+ pattern = described_class.from(pattern)
+ expect(pattern.match?('/foo/xaz/bar.html')).to eql(true)
+ expect(pattern.match?('/foo/xyz/bar.html')).to eql(false)
+ end
+
+ it 'errors on other inputs' do
+ expect { described_class.from(123) }.to raise_error(ArgumentError)
+ end
+
+ it 'errors with a proper error message on other inputs' do
+ expect { described_class.from(nil) }
+ .to raise_error(ArgumentError, 'Do not know how to convert `nil` into a Nanoc::Pattern')
+ end
+ end
+
+ describe '#initialize' do
+ it 'errors' do
+ expect { described_class.new('/stuff') }
+ .to raise_error(NotImplementedError)
+ end
+ end
+
+ describe '#match?' do
+ it 'errors' do
+ expect { described_class.allocate.match?('/foo.md') }
+ .to raise_error(NotImplementedError)
+ end
+ end
+
+ describe '#captures' do
+ it 'errors' do
+ expect { described_class.allocate.captures('/foo.md') }
+ .to raise_error(NotImplementedError)
+ end
+ end
+end
+
+describe Nanoc::Int::RegexpPattern do
+ let(:pattern) { described_class.new(/the answer is (\d+)/) }
+
+ describe '#match?' do
+ it 'matches' do
+ expect(pattern.match?('the answer is 42')).to eql(true)
+ expect(pattern.match?('the answer is donkey')).to eql(false)
+ end
+ end
+
+ describe '#captures' do
+ it 'returns nil if it does not match' do
+ expect(pattern.captures('the answer is donkey')).to be_nil
+ end
+
+ it 'returns array if it matches' do
+ expect(pattern.captures('the answer is 42')).to eql(['42'])
+ end
+ end
+
+ describe '#to_s' do
+ subject { pattern.to_s }
+
+ it 'returns the regex' do
+ expect(subject).to eq('(?-mix:the answer is (\d+))')
+ end
+ end
+end
+
+describe Nanoc::Int::StringPattern do
+ describe '#match?' do
+ it 'matches simple strings' do
+ pattern = described_class.new('d*key')
+
+ expect(pattern.match?('donkey')).to eql(true)
+ expect(pattern.match?('giraffe')).to eql(false)
+ end
+
+ it 'matches with pathname option' do
+ pattern = described_class.new('/foo/*/bar/**/*.animal')
+
+ expect(pattern.match?('/foo/x/bar/a/b/donkey.animal')).to eql(true)
+ expect(pattern.match?('/foo/x/bar/donkey.animal')).to eql(true)
+ expect(pattern.match?('/foo/x/railroad/donkey.animal')).to eql(false)
+ end
+
+ it 'matches with extglob option' do
+ pattern = described_class.new('{b,gl}oat')
+
+ expect(pattern.match?('boat')).to eql(true)
+ expect(pattern.match?('gloat')).to eql(true)
+ expect(pattern.match?('stoat')).to eql(false)
+ end
+ end
+
+ describe '#captures' do
+ it 'returns nil' do
+ pattern = described_class.new('d*key')
+ expect(pattern.captures('donkey')).to be_nil
+ end
+ end
+
+ describe '#to_s' do
+ let(:pattern) { described_class.new('/foo/*/bar/**/*.animal') }
+
+ subject { pattern.to_s }
+
+ it 'returns the regex' do
+ expect(subject).to eq('/foo/*/bar/**/*.animal')
+ end
+ end
+end
diff --git a/spec/nanoc/base/entities/processing_action_spec.rb b/spec/nanoc/base/entities/processing_action_spec.rb
new file mode 100644
index 0000000..95793e0
--- /dev/null
+++ b/spec/nanoc/base/entities/processing_action_spec.rb
@@ -0,0 +1,9 @@
+describe Nanoc::Int::ProcessingAction do
+ let(:action) { described_class.new }
+
+ it 'is abstract' do
+ expect { action.serialize }.to raise_error(NotImplementedError)
+ expect { action.to_s }.to raise_error(NotImplementedError)
+ expect { action.inspect }.to raise_error(NotImplementedError)
+ end
+end
diff --git a/spec/nanoc/base/entities/processing_actions/filter_spec.rb b/spec/nanoc/base/entities/processing_actions/filter_spec.rb
new file mode 100644
index 0000000..4455906
--- /dev/null
+++ b/spec/nanoc/base/entities/processing_actions/filter_spec.rb
@@ -0,0 +1,18 @@
+describe Nanoc::Int::ProcessingActions::Filter do
+ let(:action) { described_class.new(:foo, awesome: true) }
+
+ describe '#serialize' do
+ subject { action.serialize }
+ it { is_expected.to eql([:filter, :foo, 'sJYzLjHGo1e4ytuDfnOLkqrt9QE=']) }
+ end
+
+ describe '#to_s' do
+ subject { action.to_s }
+ it { is_expected.to eql('filter :foo, {:awesome=>true}') }
+ end
+
+ describe '#inspect' do
+ subject { action.inspect }
+ it { is_expected.to eql('<Nanoc::Int::ProcessingActions::Filter :foo, "sJYzLjHGo1e4ytuDfnOLkqrt9QE=">') }
+ end
+end
diff --git a/spec/nanoc/base/entities/processing_actions/layout_spec.rb b/spec/nanoc/base/entities/processing_actions/layout_spec.rb
new file mode 100644
index 0000000..78d0f21
--- /dev/null
+++ b/spec/nanoc/base/entities/processing_actions/layout_spec.rb
@@ -0,0 +1,18 @@
+describe Nanoc::Int::ProcessingActions::Layout do
+ let(:action) { described_class.new('/foo.erb', awesome: true) }
+
+ describe '#serialize' do
+ subject { action.serialize }
+ it { is_expected.to eql([:layout, '/foo.erb', 'sJYzLjHGo1e4ytuDfnOLkqrt9QE=']) }
+ end
+
+ describe '#to_s' do
+ subject { action.to_s }
+ it { is_expected.to eql('layout "/foo.erb", {:awesome=>true}') }
+ end
+
+ describe '#inspect' do
+ subject { action.inspect }
+ it { is_expected.to eql('<Nanoc::Int::ProcessingActions::Layout "/foo.erb", "sJYzLjHGo1e4ytuDfnOLkqrt9QE=">') }
+ end
+end
diff --git a/spec/nanoc/base/entities/processing_actions/snapshot_spec.rb b/spec/nanoc/base/entities/processing_actions/snapshot_spec.rb
new file mode 100644
index 0000000..a06776a
--- /dev/null
+++ b/spec/nanoc/base/entities/processing_actions/snapshot_spec.rb
@@ -0,0 +1,32 @@
+describe Nanoc::Int::ProcessingActions::Snapshot do
+ let(:action) { described_class.new(:before_layout, '/foo.md') }
+
+ describe '#serialize' do
+ subject { action.serialize }
+ it { is_expected.to eql([:snapshot, :before_layout, true, '/foo.md']) }
+ end
+
+ describe '#to_s' do
+ subject { action.to_s }
+ it { is_expected.to eql('snapshot :before_layout, path: "/foo.md"') }
+ end
+
+ describe '#inspect' do
+ subject { action.inspect }
+ it { is_expected.to eql('<Nanoc::Int::ProcessingActions::Snapshot :before_layout, true, "/foo.md">') }
+ end
+
+ describe '#copy' do
+ context 'without path' do
+ subject { action.copy }
+ its(:snapshot_name) { is_expected.to eql(:before_layout) }
+ its(:path) { is_expected.to eql('/foo.md') }
+ end
+
+ context 'with path' do
+ subject { action.copy(path: '/donkey.md') }
+ its(:snapshot_name) { is_expected.to eql(:before_layout) }
+ its(:path) { is_expected.to eql('/donkey.md') }
+ end
+ end
+end
diff --git a/spec/nanoc/base/entities/props_spec.rb b/spec/nanoc/base/entities/props_spec.rb
new file mode 100644
index 0000000..b7f0ac2
--- /dev/null
+++ b/spec/nanoc/base/entities/props_spec.rb
@@ -0,0 +1,195 @@
+describe Nanoc::Int::Props do
+ let(:props) { described_class.new }
+
+ let(:props_all) do
+ described_class.new(raw_content: true, attributes: true, compiled_content: true, path: true)
+ end
+
+ describe '#inspect' do
+ subject { props.inspect }
+
+ context 'nothing active' do
+ it { is_expected.to eql('Props(____)') }
+ end
+
+ context 'attributes active' do
+ let(:props) { described_class.new(attributes: true) }
+ it { is_expected.to eql('Props(_a__)') }
+ end
+
+ context 'attributes and compiled_content active' do
+ let(:props) { described_class.new(attributes: true, compiled_content: true) }
+ it { is_expected.to eql('Props(_ac_)') }
+ end
+
+ context 'compiled_content active' do
+ let(:props) { described_class.new(compiled_content: true) }
+ it { is_expected.to eql('Props(__c_)') }
+ end
+ end
+
+ describe '#raw_content?' do
+ # …
+ end
+
+ describe '#attributes?' do
+ subject { props.attributes? }
+
+ context 'nothing active' do
+ it { is_expected.not_to be }
+ end
+
+ context 'attributes active' do
+ let(:props) { described_class.new(attributes: true) }
+ it { is_expected.to be }
+ end
+
+ context 'attributes and compiled_content active' do
+ let(:props) { described_class.new(attributes: true, compiled_content: true) }
+ it { is_expected.to be }
+ end
+
+ context 'compiled_content active' do
+ let(:props) { described_class.new(compiled_content: true) }
+ it { is_expected.not_to be }
+ end
+
+ context 'all active' do
+ let(:props) { described_class.new(raw_content: true, attributes: true, compiled_content: true, path: true) }
+ it { is_expected.to be }
+ end
+ end
+
+ describe '#compiled_content?' do
+ # …
+ end
+
+ describe '#path?' do
+ # …
+ end
+
+ describe '#merge' do
+ subject { props.merge(other_props).active }
+
+ context 'nothing + nothing' do
+ let(:props) { described_class.new }
+ let(:other_props) { described_class.new }
+
+ it { is_expected.to eql(Set.new) }
+ end
+
+ context 'nothing + some' do
+ let(:props) { described_class.new }
+ let(:other_props) { described_class.new(raw_content: true) }
+
+ it { is_expected.to eql(Set.new([:raw_content])) }
+ end
+
+ context 'nothing + all' do
+ let(:props) { described_class.new }
+ let(:other_props) { props_all }
+
+ it { is_expected.to eql(Set.new([:raw_content, :attributes, :compiled_content, :path])) }
+ end
+
+ context 'some + nothing' do
+ let(:props) { described_class.new(compiled_content: true) }
+ let(:other_props) { described_class.new }
+
+ it { is_expected.to eql(Set.new([:compiled_content])) }
+ end
+
+ context 'some + others' do
+ let(:props) { described_class.new(compiled_content: true) }
+ let(:other_props) { described_class.new(raw_content: true) }
+
+ it { is_expected.to eql(Set.new([:raw_content, :compiled_content])) }
+ end
+
+ context 'some + all' do
+ let(:props) { described_class.new(compiled_content: true) }
+ let(:other_props) { props_all }
+
+ it { is_expected.to eql(Set.new([:raw_content, :attributes, :compiled_content, :path])) }
+ end
+
+ context 'all + nothing' do
+ let(:props) { props_all }
+ let(:other_props) { described_class.new }
+
+ it { is_expected.to eql(Set.new([:raw_content, :attributes, :compiled_content, :path])) }
+ end
+
+ context 'some + all' do
+ let(:props) { props_all }
+ let(:other_props) { described_class.new(compiled_content: true) }
+
+ it { is_expected.to eql(Set.new([:raw_content, :attributes, :compiled_content, :path])) }
+ end
+
+ context 'all + all' do
+ let(:props) { props_all }
+ let(:other_props) { props_all }
+
+ it { is_expected.to eql(Set.new([:raw_content, :attributes, :compiled_content, :path])) }
+ end
+ end
+
+ describe '#active' do
+ subject { props.active }
+
+ context 'nothing active' do
+ let(:props) { described_class.new }
+ it { is_expected.to eql(Set.new) }
+ end
+
+ context 'raw_content active' do
+ let(:props) { described_class.new(raw_content: true) }
+ it { is_expected.to eql(Set.new([:raw_content])) }
+ end
+
+ context 'attributes active' do
+ let(:props) { described_class.new(attributes: true) }
+ it { is_expected.to eql(Set.new([:attributes])) }
+ end
+
+ context 'compiled_content active' do
+ let(:props) { described_class.new(compiled_content: true) }
+ it { is_expected.to eql(Set.new([:compiled_content])) }
+ end
+
+ context 'path active' do
+ let(:props) { described_class.new(path: true) }
+ it { is_expected.to eql(Set.new([:path])) }
+ end
+
+ context 'attributes and compiled_content active' do
+ let(:props) { described_class.new(attributes: true, compiled_content: true) }
+ it { is_expected.to eql(Set.new([:attributes, :compiled_content])) }
+ end
+
+ context 'all active' do
+ let(:props) { described_class.new(raw_content: true, attributes: true, compiled_content: true, path: true) }
+ it { is_expected.to eql(Set.new([:raw_content, :attributes, :compiled_content, :path])) }
+ end
+ end
+
+ describe '#to_h' do
+ subject { props.to_h }
+
+ context 'nothing' do
+ let(:props) { described_class.new }
+ it { is_expected.to eql(raw_content: false, attributes: false, compiled_content: false, path: false) }
+ end
+
+ context 'some' do
+ let(:props) { described_class.new(attributes: true, compiled_content: true) }
+ it { is_expected.to eql(raw_content: false, attributes: true, compiled_content: true, path: false) }
+ end
+
+ context 'all' do
+ let(:props) { props_all }
+ it { is_expected.to eql(raw_content: true, attributes: true, compiled_content: true, path: true) }
+ end
+ end
+end
diff --git a/spec/nanoc/base/entities/rule_memory_spec.rb b/spec/nanoc/base/entities/rule_memory_spec.rb
new file mode 100644
index 0000000..5d1a16e
--- /dev/null
+++ b/spec/nanoc/base/entities/rule_memory_spec.rb
@@ -0,0 +1,131 @@
+describe Nanoc::Int::RuleMemory do
+ let(:rule_memory) { described_class.new(rep) }
+ let(:rep) { double(:rep) }
+
+ describe '#size' do
+ subject { rule_memory.size }
+
+ context 'no actions' do
+ it { is_expected.to eql(0) }
+ end
+
+ context 'some actions' do
+ before do
+ rule_memory.add_filter(:foo, {})
+ end
+
+ it { is_expected.to eql(1) }
+ end
+ end
+
+ describe '#[]' do
+ subject { rule_memory[index] }
+ let(:index) { 0 }
+
+ context 'no actions' do
+ it { is_expected.to be_nil }
+ end
+
+ context 'some actions' do
+ before do
+ rule_memory.add_filter(:foo, {})
+ end
+
+ it { is_expected.to be_a(Nanoc::Int::ProcessingActions::Filter) }
+ end
+ end
+
+ describe '#add_filter' do
+ example do
+ rule_memory.add_filter(:foo, donkey: 123)
+
+ expect(rule_memory.size).to eql(1)
+ expect(rule_memory[0]).to be_a(Nanoc::Int::ProcessingActions::Filter)
+ expect(rule_memory[0].filter_name).to eql(:foo)
+ expect(rule_memory[0].params).to eql(donkey: 123)
+ end
+ end
+
+ describe '#add_layout' do
+ example do
+ rule_memory.add_layout('/foo.*', donkey: 123)
+
+ expect(rule_memory.size).to eql(1)
+ expect(rule_memory[0]).to be_a(Nanoc::Int::ProcessingActions::Layout)
+ expect(rule_memory[0].layout_identifier).to eql('/foo.*')
+ expect(rule_memory[0].params).to eql(donkey: 123)
+ end
+ end
+
+ describe '#add_snapshot' do
+ context 'snapshot does not yet exist' do
+ example do
+ rule_memory.add_snapshot(:before_layout, '/foo.md')
+
+ expect(rule_memory.size).to eql(1)
+ expect(rule_memory[0]).to be_a(Nanoc::Int::ProcessingActions::Snapshot)
+ expect(rule_memory[0].snapshot_name).to eql(:before_layout)
+ expect(rule_memory[0].path).to eql('/foo.md')
+ end
+ end
+
+ context 'snapshot already exist' do
+ before do
+ rule_memory.add_snapshot(:before_layout, '/bar.md')
+ end
+
+ it 'raises' do
+ expect { rule_memory.add_snapshot(:before_layout, '/foo.md') }
+ .to raise_error(Nanoc::Int::Errors::CannotCreateMultipleSnapshotsWithSameName)
+ end
+ end
+ end
+
+ describe '#each' do
+ before do
+ rule_memory.add_filter(:erb, awesomeness: 'high')
+ rule_memory.add_snapshot(:bar, '/foo.md')
+ rule_memory.add_layout('/default.erb', somelayoutparam: 'yes')
+ end
+
+ example do
+ actions = []
+ rule_memory.each { |a| actions << a }
+ expect(actions.size).to eq(3)
+ end
+ end
+
+ describe '#map' do
+ before do
+ rule_memory.add_filter(:erb, awesomeness: 'high')
+ rule_memory.add_snapshot(:bar, '/foo.md')
+ rule_memory.add_layout('/default.erb', somelayoutparam: 'yes')
+ end
+
+ example do
+ res = rule_memory.map { Nanoc::Int::ProcessingActions::Filter.new(:donkey, {}) }
+ expect(res.to_a.size).to eq(3)
+ expect(res.to_a).to all(be_a(Nanoc::Int::ProcessingActions::Filter))
+ end
+ end
+
+ describe '#serialize' do
+ subject { rule_memory.serialize }
+
+ before do
+ rule_memory.add_filter(:erb, awesomeness: 'high')
+ rule_memory.add_snapshot(:bar, '/foo.md')
+ rule_memory.add_layout('/default.erb', somelayoutparam: 'yes')
+ end
+
+ example do
+ expect(subject).to eql(
+ [
+ [:filter, :erb, 'PeWUm2PtXYtqeHJdTqnY7kkwAow='],
+ [:snapshot, :bar, true, '/foo.md'],
+ [:layout, '/default.erb', '97LAe1pYTLKczxBsu+x4MmvqdkU='],
+ ],
+ )
+ end
+ end
+end
diff --git a/spec/nanoc/base/entities/site_spec.rb b/spec/nanoc/base/entities/site_spec.rb
new file mode 100644
index 0000000..a730fdd
--- /dev/null
+++ b/spec/nanoc/base/entities/site_spec.rb
@@ -0,0 +1,73 @@
+describe Nanoc::Int::Site do
+ describe '#freeze' do
+ let(:site) do
+ described_class.new(
+ config: config,
+ code_snippets: code_snippets,
+ items: items,
+ layouts: layouts,
+ )
+ end
+
+ let(:config) do
+ Nanoc::Int::Configuration.new.with_defaults
+ end
+
+ let(:code_snippets) do
+ [
+ Nanoc::Int::CodeSnippet.new('FOO = 123', 'hello.rb'),
+ Nanoc::Int::CodeSnippet.new('BAR = 123', 'hi.rb'),
+ ]
+ end
+
+ let(:items) do
+ Nanoc::Int::IdentifiableCollection.new(config).tap do |coll|
+ coll << Nanoc::Int::Item.new('foo', {}, '/foo.md')
+ coll << Nanoc::Int::Item.new('bar', {}, '/bar.md')
+ end
+ end
+
+ let(:layouts) do
+ Nanoc::Int::IdentifiableCollection.new(config).tap do |coll|
+ coll << Nanoc::Int::Layout.new('foo', {}, '/foo.md')
+ coll << Nanoc::Int::Layout.new('bar', {}, '/bar.md')
+ end
+ end
+
+ before do
+ site.freeze
+ end
+
+ it 'freezes the configuration' do
+ expect(site.config).to be_frozen
+ end
+
+ it 'freezes the configuration contents' do
+ expect(site.config[:output_dir]).to be_frozen
+ end
+
+ it 'freezes items collection' do
+ expect(site.items).to be_frozen
+ end
+
+ it 'freezes individual items' do
+ expect(site.items).to all(be_frozen)
+ end
+
+ it 'freezes layouts collection' do
+ expect(site.layouts).to be_frozen
+ end
+
+ it 'freezes individual layouts' do
+ expect(site.layouts).to all(be_frozen)
+ end
+
+ it 'freezes code snippets collection' do
+ expect(site.code_snippets).to be_frozen
+ end
+
+ it 'freezes individual code snippets' do
+ expect(site.code_snippets).to all(be_frozen)
+ end
+ end
+end
diff --git a/spec/nanoc/base/feature_spec.rb b/spec/nanoc/base/feature_spec.rb
new file mode 100644
index 0000000..93149c8
--- /dev/null
+++ b/spec/nanoc/base/feature_spec.rb
@@ -0,0 +1,107 @@
+describe Nanoc::Feature do
+ describe '.enabled?' do
+ subject { described_class.enabled?(feature_name) }
+
+ let(:feature_name) { 'magic' }
+
+ before do
+ Nanoc::Feature.reset_caches
+ ENV['NANOC_FEATURES'] = ''
+ end
+
+ context 'not set' do
+ it { is_expected.not_to be }
+ end
+
+ context 'set to list not including feature' do
+ before { ENV['NANOC_FEATURES'] = 'foo,bar' }
+ it { is_expected.not_to be }
+ end
+
+ context 'set to all' do
+ before { ENV['NANOC_FEATURES'] = 'all' }
+ it { is_expected.to be }
+ end
+
+ context 'set to list including feature' do
+ before { ENV['NANOC_FEATURES'] = 'foo,magic,bar' }
+ it { is_expected.to be }
+ end
+ end
+
+ describe '.enable' do
+ subject do
+ described_class.enable(feature_name) do
+ Nanoc::Feature.enabled?(feature_name)
+ end
+ end
+
+ let(:feature_name) { 'magic' }
+
+ before do
+ Nanoc::Feature.reset_caches
+ ENV['NANOC_FEATURES'] = ''
+ end
+
+ context 'not set' do
+ it { is_expected.to be }
+
+ it 'unsets afterwards' do
+ expect(Nanoc::Feature.enabled?(feature_name)).not_to be
+ end
+ end
+
+ context 'set to list not including feature' do
+ before { ENV['NANOC_FEATURES'] = 'foo,bar' }
+ it { is_expected.to be }
+
+ it 'unsets afterwards' do
+ expect(Nanoc::Feature.enabled?(feature_name)).not_to be
+ end
+ end
+
+ context 'set to all' do
+ before { ENV['NANOC_FEATURES'] = 'all' }
+ it { is_expected.to be }
+ end
+
+ context 'set to list including feature' do
+ before { ENV['NANOC_FEATURES'] = 'foo,magic,bar' }
+ it { is_expected.to be }
+ end
+ end
+
+ describe '.all_outdated' do
+ it 'refuses outdated features' do
+ # If this spec fails, there are features marked as experimental in the previous minor or major
+ # release, but not in the current one. Either remove the feature, or mark it as experimental
+ # in the current release.
+ expect(Nanoc::Feature.all_outdated).to be_empty
+ end
+
+ describe 'fake outdated features' do
+ before { Nanoc::Feature.define('abc', version: '4.2.x') }
+ after { Nanoc::Feature.undefine('abc') }
+
+ it 'detects outdated features' do
+ expect(Nanoc::Feature.all_outdated).to eq(['abc'])
+ end
+ end
+ end
+
+ describe '.define and .undefine' do
+ let(:feature_name) { 'testing123' }
+ after { Nanoc::Feature.undefine(feature_name) if defined?(Nanoc::Feature::TESTING123) }
+
+ it 'can define' do
+ Nanoc::Feature.define(feature_name, version: '4.3.x')
+ expect(Nanoc::Feature::TESTING123).not_to be_nil
+ end
+
+ it 'can undefine' do
+ Nanoc::Feature.define(feature_name, version: '4.3.x')
+ Nanoc::Feature.undefine(feature_name)
+ expect { Nanoc::Feature::TESTING123 }.to raise_error(NameError)
+ end
+ end
+end
diff --git a/spec/nanoc/base/filter_spec.rb b/spec/nanoc/base/filter_spec.rb
new file mode 100644
index 0000000..f032a92
--- /dev/null
+++ b/spec/nanoc/base/filter_spec.rb
@@ -0,0 +1,99 @@
+describe Nanoc::Filter do
+ describe '.define' do
+ context 'simple filter' do
+ let(:filter_name) { 'b5355bbb4d772b9853d21be57da614dba521dbbb' }
+ let(:filter_class) { Nanoc::Filter.named(filter_name) }
+
+ before do
+ Nanoc::Filter.define(filter_name) do |content, _params|
+ content.upcase
+ end
+ end
+
+ it 'defines a filter' do
+ expect(filter_class).not_to be_nil
+ end
+
+ it 'defines a callable filter' do
+ expect(filter_class.new.run('foo', {})).to eql('FOO')
+ end
+ end
+
+ context 'filter that accesses assigns' do
+ let(:filter_name) { 'd7ed105d460e99a3d38f46af023d9490c140fdd9' }
+ let(:filter_class) { Nanoc::Filter.named(filter_name) }
+ let(:filter) { filter_class.new(assigns) }
+ let(:assigns) { { animal: 'Giraffe' } }
+
+ before do
+ Nanoc::Filter.define(filter_name) do |_content, _params|
+ @animal
+ end
+ end
+
+ it 'can access assigns' do
+ expect(filter.setup_and_run(:__irrelevant__, {})).to eq('Giraffe')
+ end
+ end
+ end
+
+ describe '#depend_on' do
+ subject { filter.depend_on(item_views) }
+
+ let(:filter) { Nanoc::Filters::ERB.new(assigns) }
+ let(:item_views) { [item_view] }
+
+ let(:item) { Nanoc::Int::Item.new('foo', {}, '/stuff.md') }
+ let(:item_view) { Nanoc::ItemWithRepsView.new(item, view_context) }
+ let(:rep) { Nanoc::Int::ItemRep.new(item, :default) }
+
+ let(:view_context) do
+ Nanoc::ViewContext.new(
+ reps: reps,
+ items: double(:items),
+ dependency_tracker: dependency_tracker,
+ compilation_context: double(:compilation_context),
+ )
+ end
+
+ let(:dependency_tracker) { double(:dependency_tracker) }
+
+ let(:reps) { Nanoc::Int::ItemRepRepo.new }
+
+ let(:assigns) do
+ {
+ item: item_view,
+ }
+ end
+
+ before do
+ reps << rep
+
+ expect(dependency_tracker).to receive(:bounce).with(item, compiled_content: true)
+ end
+
+ context 'rep is compiled' do
+ before do
+ rep.compiled = true
+ end
+
+ example do
+ expect { subject }.not_to yield_from_fiber(an_instance_of(Nanoc::Int::Errors::UnmetDependency))
+ end
+ end
+
+ context 'rep is not compiled' do
+ example do
+ fiber = Fiber.new { subject }
+
+ # resume 1
+ res = fiber.resume
+ expect(res).to be_a(Nanoc::Int::Errors::UnmetDependency)
+ expect(res.rep).to eql(rep)
+
+ # resume 2
+ expect(fiber.resume).not_to be_a(Nanoc::Int::Errors::UnmetDependency)
+ end
+ end
+ end
+end
diff --git a/spec/nanoc/base/item_rep_writer_spec.rb b/spec/nanoc/base/item_rep_writer_spec.rb
new file mode 100644
index 0000000..5bfc1a1
--- /dev/null
+++ b/spec/nanoc/base/item_rep_writer_spec.rb
@@ -0,0 +1,131 @@
+describe Nanoc::Int::ItemRepWriter do
+ describe '#write' do
+ let(:raw_path) { 'output/blah.dat' }
+
+ let(:item) { Nanoc::Int::Item.new(orig_content, {}, '/') }
+
+ let(:item_rep) do
+ Nanoc::Int::ItemRep.new(item, :default).tap do |ir|
+ ir.snapshot_contents = snapshot_contents
+ ir.raw_paths = raw_paths
+ end
+ end
+
+ let(:snapshot_contents) do
+ {
+ last: Nanoc::Int::TextualContent.new('last content'),
+ donkey: Nanoc::Int::TextualContent.new('donkey content'),
+ }
+ end
+
+ let(:snapshot_name) { :donkey }
+
+ let(:raw_paths) do
+ { snapshot_name => raw_path }
+ end
+
+ subject { described_class.new.write(item_rep, snapshot_name) }
+
+ before do
+ expect(File.directory?('output')).to be_falsy
+ end
+
+ context 'binary item rep' do
+ let(:orig_content) { Nanoc::Int::BinaryContent.new(File.expand_path('foo.dat')) }
+
+ let(:snapshot_contents) do
+ {
+ last: Nanoc::Int::BinaryContent.new(File.expand_path('input-last.dat')),
+ donkey: Nanoc::Int::BinaryContent.new(File.expand_path('input-donkey.dat')),
+ }
+ end
+
+ before do
+ File.write(snapshot_contents[:last].filename, 'binary last stuff')
+ File.write(snapshot_contents[:donkey].filename, 'binary donkey stuff')
+ end
+
+ it 'copies' do
+ expect(Nanoc::Int::NotificationCenter).to receive(:post)
+ .with(:will_write_rep, item_rep, 'output/blah.dat')
+ expect(Nanoc::Int::NotificationCenter).to receive(:post)
+ .with(:rep_written, item_rep, 'output/blah.dat', true, true)
+
+ subject
+
+ expect(File.read('output/blah.dat')).to eql('binary donkey stuff')
+ end
+
+ context 'output file already exists' do
+ let(:old_mtime) { Time.at((Time.now - 600).to_i) }
+
+ before do
+ FileUtils.mkdir_p('output')
+ File.write('output/blah.dat', old_content)
+ FileUtils.touch('output/blah.dat', mtime: old_mtime)
+ end
+
+ context 'file is identical' do
+ let(:old_content) { 'binary donkey stuff' }
+
+ it 'keeps mtime' do
+ subject
+ expect(File.mtime('output/blah.dat')).to eql(old_mtime)
+ end
+ end
+
+ context 'file is not identical' do
+ let(:old_content) { 'other binary donkey stuff' }
+
+ it 'updates mtime' do
+ subject
+ expect(File.mtime('output/blah.dat')).to be > (Time.now - 1)
+ end
+ end
+ end
+ end
+
+ context 'textual item rep' do
+ let(:orig_content) { Nanoc::Int::TextualContent.new('Hallo Welt') }
+
+ it 'writes' do
+ expect(Nanoc::Int::NotificationCenter).to receive(:post)
+ .with(:will_write_rep, item_rep, 'output/blah.dat')
+ expect(Nanoc::Int::NotificationCenter).to receive(:post)
+ .with(:rep_written, item_rep, 'output/blah.dat', true, true)
+
+ subject
+
+ expect(File.read('output/blah.dat')).to eql('donkey content')
+ end
+
+ context 'output file already exists' do
+ let(:old_mtime) { Time.at((Time.now - 600).to_i) }
+
+ before do
+ FileUtils.mkdir_p('output')
+ File.write('output/blah.dat', old_content)
+ FileUtils.touch('output/blah.dat', mtime: old_mtime)
+ end
+
+ context 'file is identical' do
+ let(:old_content) { 'donkey content' }
+
+ it 'keeps mtime' do
+ subject
+ expect(File.mtime('output/blah.dat')).to eql(old_mtime)
+ end
+ end
+
+ context 'file is not identical' do
+ let(:old_content) { 'other donkey content' }
+
+ it 'updates mtime' do
+ subject
+ expect(File.mtime('output/blah.dat')).to be > (Time.now - 1)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/nanoc/base/plugin_registry_spec.rb b/spec/nanoc/base/plugin_registry_spec.rb
new file mode 100644
index 0000000..f752fde
--- /dev/null
+++ b/spec/nanoc/base/plugin_registry_spec.rb
@@ -0,0 +1,29 @@
+describe Nanoc::Int::PluginRegistry do
+ describe '.identifier(s)' do
+ let(:identifier) { :ce79f6b8ddb22233e9aaf7d8f011689492acf02f }
+
+ context 'direct subclass' do
+ example do
+ klass =
+ Class.new(Nanoc::Filter) do
+ identifier :plugin_registry_spec
+ end
+
+ expect(klass.identifier).to eql(:plugin_registry_spec)
+ end
+ end
+
+ context 'indirect subclass' do
+ example do
+ superclass = Class.new(Nanoc::Filter)
+
+ klass =
+ Class.new(superclass) do
+ identifier :plugin_registry_spec
+ end
+
+ expect(klass.identifier).to eql(:plugin_registry_spec)
+ end
+ end
+ end
+end
diff --git a/spec/nanoc/base/repos/checksum_store_spec.rb b/spec/nanoc/base/repos/checksum_store_spec.rb
new file mode 100644
index 0000000..24f8f5a
--- /dev/null
+++ b/spec/nanoc/base/repos/checksum_store_spec.rb
@@ -0,0 +1,133 @@
+describe Nanoc::Int::ChecksumStore do
+ let(:store) { described_class.new(objects: objects) }
+
+ let(:objects) { [item, code_snippet] }
+
+ let(:item) { Nanoc::Int::Item.new('asdf', {}, '/foo.md') }
+ let(:other_item) { Nanoc::Int::Item.new('asdf', {}, '/sneaky.md') }
+
+ let(:code_snippet) { Nanoc::Int::CodeSnippet.new('def hi ; end', 'lib/foo.rb') }
+ let(:other_code_snippet) { Nanoc::Int::CodeSnippet.new('def ho ; end', 'lib/bar.rb') }
+
+ context 'nothing added' do
+ it 'has no checksum' do
+ expect(store[item]).to be_nil
+ end
+
+ it 'has no content checksum' do
+ expect(store.content_checksum_for(item)).to be_nil
+ end
+
+ it 'has no attributes checksum' do
+ expect(store.attributes_checksum_for(item)).to be_nil
+ end
+ end
+
+ context 'setting content on known non-document' do
+ before { store.add(code_snippet) }
+
+ it 'has checksum' do
+ expect(store[code_snippet]).not_to be_nil
+ end
+
+ it 'has no content checksum' do
+ expect(store.content_checksum_for(code_snippet)).to be_nil
+ end
+
+ it 'has no attributes checksum' do
+ expect(store.attributes_checksum_for(code_snippet)).to be_nil
+ end
+
+ context 'after storing and loading' do
+ before do
+ store.store
+ store.load
+ end
+
+ it 'has checksum' do
+ expect(store[code_snippet]).not_to be_nil
+ end
+ end
+ end
+
+ context 'setting content on unknown non-document' do
+ before { store.add(other_code_snippet) }
+
+ it 'has checksum' do
+ expect(store[other_code_snippet]).not_to be_nil
+ end
+
+ it 'has no content checksum' do
+ expect(store.content_checksum_for(other_code_snippet)).to be_nil
+ end
+
+ it 'has no attributes checksum' do
+ expect(store.attributes_checksum_for(other_code_snippet)).to be_nil
+ end
+
+ context 'after storing and loading' do
+ before do
+ store.store
+ store.load
+ end
+
+ it 'has no checksum' do
+ expect(store[other_code_snippet]).to be_nil
+ end
+ end
+ end
+
+ context 'setting content on known item' do
+ before { store.add(item) }
+
+ it 'has checksum' do
+ expect(store[item]).not_to be_nil
+ end
+
+ it 'has content checksum' do
+ expect(store.content_checksum_for(item)).not_to be_nil
+ end
+
+ it 'has attributes checksum' do
+ expect(store.attributes_checksum_for(item)).not_to be_nil
+ end
+
+ context 'after storing and loading' do
+ before do
+ store.store
+ store.load
+ end
+
+ it 'has checksum' do
+ expect(store[item]).not_to be_nil
+ end
+ end
+ end
+
+ context 'setting content on unknown item' do
+ before { store.add(other_item) }
+
+ it 'has checksum' do
+ expect(store[other_item]).not_to be_nil
+ end
+
+ it 'has content checksum' do
+ expect(store.content_checksum_for(other_item)).not_to be_nil
+ end
+
+ it 'has attributes checksum' do
+ expect(store.attributes_checksum_for(other_item)).not_to be_nil
+ end
+
+ context 'after storing and loading' do
+ before do
+ store.store
+ store.load
+ end
+
+ it 'has no checksum' do
+ expect(store[other_item]).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/nanoc/base/repos/compiled_content_cache_spec.rb b/spec/nanoc/base/repos/compiled_content_cache_spec.rb
new file mode 100644
index 0000000..3ae0d4c
--- /dev/null
+++ b/spec/nanoc/base/repos/compiled_content_cache_spec.rb
@@ -0,0 +1,55 @@
+describe Nanoc::Int::CompiledContentCache do
+ let(:cache) { described_class.new(items: items) }
+
+ let(:items) { [item] }
+
+ let(:item) { Nanoc::Int::Item.new('asdf', {}, '/foo.md') }
+ let(:item_rep) { Nanoc::Int::ItemRep.new(item, :default) }
+
+ let(:other_item) { Nanoc::Int::Item.new('asdf', {}, '/sneaky.md') }
+ let(:other_item_rep) { Nanoc::Int::ItemRep.new(other_item, :default) }
+
+ let(:content) { Nanoc::Int::Content.create('omg') }
+
+ it 'has no content by default' do
+ expect(cache[item_rep]).to be_nil
+ end
+
+ context 'setting content on known item' do
+ before { cache[item_rep] = { last: content } }
+
+ it 'has content' do
+ expect(cache[item_rep][:last].string).to eql('omg')
+ end
+
+ context 'after storing and loading' do
+ before do
+ cache.store
+ cache.load
+ end
+
+ it 'has content' do
+ expect(cache[item_rep][:last].string).to eql('omg')
+ end
+ end
+ end
+
+ context 'setting content on unknown item' do
+ before { cache[other_item_rep] = { last: content } }
+
+ it 'has content' do
+ expect(cache[other_item_rep][:last].string).to eql('omg')
+ end
+
+ context 'after storing and loading' do
+ before do
+ cache.store
+ cache.load
+ end
+
+ it 'has no content' do
+ expect(cache[other_item_rep]).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/nanoc/base/repos/config_loader_spec.rb b/spec/nanoc/base/repos/config_loader_spec.rb
new file mode 100644
index 0000000..5435f45
--- /dev/null
+++ b/spec/nanoc/base/repos/config_loader_spec.rb
@@ -0,0 +1,243 @@
+describe Nanoc::Int::ConfigLoader do
+ let(:loader) { described_class.new }
+
+ describe '#new_from_cwd' do
+ subject { loader.new_from_cwd }
+
+ context 'no config file present' do
+ it 'errors' do
+ expect { subject }.to raise_error(
+ Nanoc::Int::ConfigLoader::NoConfigFileFoundError,
+ )
+ end
+ end
+
+ context 'config file present' do
+ before do
+ File.write('nanoc.yaml', YAML.dump(foo: 'bar'))
+ end
+
+ it 'returns a configuration' do
+ expect(subject).to be_a(Nanoc::Int::Configuration)
+ end
+
+ it 'has the defaults' do
+ expect(subject[:output_dir]).to eq('output')
+ end
+
+ it 'has the custom option' do
+ expect(subject[:foo]).to eq('bar')
+ end
+ end
+
+ context 'config file and parent present' do
+ before do
+ File.write('nanoc.yaml', YAML.dump(parent_config_file: 'parent.yaml'))
+ File.write('parent.yaml', YAML.dump(foo: 'bar'))
+ end
+
+ it 'returns the configuration' do
+ expect(subject).to be_a(Nanoc::Int::Configuration)
+ end
+
+ it 'has the defaults' do
+ expect(subject[:output_dir]).to eq('output')
+ end
+
+ it 'has the custom option' do
+ expect(subject[:foo]).to eq('bar')
+ end
+
+ it 'does not include parent config option' do
+ expect(subject[:parent_config_file]).to be_nil
+ end
+ end
+
+ context 'config file present, environment defined' do
+ let(:active_env_name) { 'default' }
+
+ let(:config) do
+ {
+ foo: 'bar',
+ tofoo: 'bar',
+ environments: {
+ test: { foo: 'test-bar' },
+ default: { foo: 'default-bar' },
+ },
+ }
+ end
+
+ before do
+ File.write('nanoc.yaml', YAML.dump(config))
+ end
+
+ before do
+ expect(ENV).to receive(:fetch).with('NANOC_ENV', 'default').and_return(active_env_name)
+ end
+
+ it 'returns the configuration' do
+ expect(subject).to be_a(Nanoc::Int::Configuration)
+ end
+
+ it 'has option defined not within environments' do
+ expect(subject[:tofoo]).to eq('bar')
+ end
+
+ context 'current env is test' do
+ let(:active_env_name) { 'test' }
+
+ it 'has the test environment custom option' do
+ expect(subject[:foo]).to eq('test-bar')
+ end
+ end
+
+ it 'has the default environment custom option' do
+ expect(subject[:foo]).to eq('default-bar')
+ end
+ end
+ end
+
+ describe '.cwd_is_nanoc_site? + .config_filename_for_cwd' do
+ context 'no config files' do
+ it 'is not considered a nanoc site dir' do
+ expect(described_class.cwd_is_nanoc_site?).to eq(false)
+ expect(described_class.config_filename_for_cwd).to be_nil
+ end
+ end
+
+ context 'nanoc.yaml config file' do
+ before do
+ File.write('nanoc.yaml', 'stuff')
+ end
+
+ it 'is considered a nanoc site dir' do
+ expect(described_class.cwd_is_nanoc_site?).to eq(true)
+ expect(described_class.config_filename_for_cwd).to eq(File.expand_path('nanoc.yaml'))
+ end
+ end
+
+ context 'config.yaml config file' do
+ before do
+ File.write('config.yaml', 'stuff')
+ end
+
+ it 'is considered a nanoc site dir' do
+ expect(described_class.cwd_is_nanoc_site?).to eq(true)
+ expect(described_class.config_filename_for_cwd).to eq(File.expand_path('config.yaml'))
+ end
+ end
+ end
+
+ describe '#apply_parent_config' do
+ subject { loader.apply_parent_config(config, processed_paths) }
+
+ let(:config) { Nanoc::Int::Configuration.new(hash: { foo: 'bar' }) }
+
+ let(:processed_paths) { ['nanoc.yaml'] }
+
+ context 'no parent_config_file' do
+ it 'returns self' do
+ expect(subject).to eq(config)
+ end
+ end
+
+ context 'parent config file is set' do
+ let(:config) do
+ Nanoc::Int::Configuration.new(hash: { parent_config_file: 'foo.yaml', foo: 'bar' })
+ end
+
+ context 'parent config file is not present' do
+ it 'errors' do
+ expect { subject }.to raise_error(
+ Nanoc::Int::ConfigLoader::NoParentConfigFileFoundError,
+ )
+ end
+ end
+
+ context 'parent config file is present' do
+ context 'parent-child cycle' do
+ before do
+ File.write('foo.yaml', 'parent_config_file: bar.yaml')
+ File.write('bar.yaml', 'parent_config_file: foo.yaml')
+ end
+
+ it 'errors' do
+ expect { subject }.to raise_error(
+ Nanoc::Int::ConfigLoader::CyclicalConfigFileError,
+ )
+ end
+ end
+
+ context 'self parent-child cycle' do
+ before do
+ File.write('foo.yaml', 'parent_config_file: foo.yaml')
+ end
+
+ it 'errors' do
+ expect { subject }.to raise_error(
+ Nanoc::Int::ConfigLoader::CyclicalConfigFileError,
+ )
+ end
+ end
+
+ context 'no parent-child cycle' do
+ before do
+ File.write('foo.yaml', 'animal: giraffe')
+ end
+
+ it 'returns a configuration' do
+ expect(subject).to be_a(Nanoc::Int::Configuration)
+ end
+
+ it 'has no defaults (added in #new_from_cwd only)' do
+ expect(subject[:output_dir]).to be_nil
+ end
+
+ it 'inherits options from parent' do
+ expect(subject[:animal]).to eq('giraffe')
+ end
+
+ it 'takes options from child' do
+ expect(subject[:foo]).to eq('bar')
+ end
+
+ it 'does not include parent config option' do
+ expect(subject[:parent_config_file]).to be_nil
+ end
+ end
+
+ context 'long parent chain' do
+ before do
+ File.write('foo.yaml', "parrots: 43\nparent_config_file: bar.yaml\n")
+ File.write('bar.yaml', "day_one: lasers\nslugs: false\n")
+ end
+
+ it 'returns a configuration' do
+ expect(subject).to be_a(Nanoc::Int::Configuration)
+ end
+
+ it 'has no defaults (added in #new_from_cwd only)' do
+ expect(subject[:output_dir]).to be_nil
+ end
+
+ it 'inherits options from grandparent' do
+ expect(subject[:day_one]).to eq('lasers')
+ expect(subject[:slugs]).to eq(false)
+ end
+
+ it 'inherits options from parent' do
+ expect(subject[:parrots]).to eq(43)
+ end
+
+ it 'takes options from child' do
+ expect(subject[:foo]).to eq('bar')
+ end
+
+ it 'does not include parent config option' do
+ expect(subject[:parent_config_file]).to be_nil
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/nanoc/base/repos/dependency_store_spec.rb b/spec/nanoc/base/repos/dependency_store_spec.rb
new file mode 100644
index 0000000..455f758
--- /dev/null
+++ b/spec/nanoc/base/repos/dependency_store_spec.rb
@@ -0,0 +1,195 @@
+describe Nanoc::Int::DependencyStore do
+ let(:store) { described_class.new(objects) }
+
+ let(:objects) do
+ [obj_a, obj_b, obj_c]
+ end
+
+ let(:obj_a) { Nanoc::Int::Item.new('a', {}, '/a.md') }
+ let(:obj_b) { Nanoc::Int::Item.new('b', {}, '/b.md') }
+ let(:obj_c) { Nanoc::Int::Item.new('c', {}, '/c.md') }
+
+ describe '#dependencies_causing_outdatedness_of' do
+ context 'no dependencies' do
+ it 'returns nothing for each' do
+ expect(store.dependencies_causing_outdatedness_of(obj_a)).to be_empty
+ expect(store.dependencies_causing_outdatedness_of(obj_b)).to be_empty
+ expect(store.dependencies_causing_outdatedness_of(obj_c)).to be_empty
+ end
+ end
+
+ context 'one dependency' do
+ context 'no props' do
+ before do
+ # FIXME: weird argument order (obj_b depends on obj_a, not th other way around)
+ store.record_dependency(obj_a, obj_b)
+ end
+
+ it 'returns one dependency' do
+ deps = store.dependencies_causing_outdatedness_of(obj_a)
+ expect(deps.size).to eql(1)
+ end
+
+ it 'returns dependency from b to a' do
+ deps = store.dependencies_causing_outdatedness_of(obj_a)
+ expect(deps[0].from).to eql(obj_b)
+ expect(deps[0].to).to eql(obj_a)
+ end
+
+ it 'returns true for all props by default' do
+ deps = store.dependencies_causing_outdatedness_of(obj_a)
+ expect(deps[0].props.raw_content?).to eq(true)
+ expect(deps[0].props.attributes?).to eq(true)
+ expect(deps[0].props.compiled_content?).to eq(true)
+ expect(deps[0].props.path?).to eq(true)
+ end
+
+ it 'returns nothing for the others' do
+ expect(store.dependencies_causing_outdatedness_of(obj_b)).to be_empty
+ expect(store.dependencies_causing_outdatedness_of(obj_c)).to be_empty
+ end
+ end
+
+ context 'one prop' do
+ before do
+ # FIXME: weird argument order (obj_b depends on obj_a, not th other way around)
+ store.record_dependency(obj_a, obj_b, compiled_content: true)
+ end
+
+ it 'returns false for all unspecified props' do
+ deps = store.dependencies_causing_outdatedness_of(obj_a)
+ expect(deps[0].props.raw_content?).to eq(false)
+ expect(deps[0].props.attributes?).to eq(false)
+ expect(deps[0].props.path?).to eq(false)
+ end
+
+ it 'returns the specified props' do
+ deps = store.dependencies_causing_outdatedness_of(obj_a)
+ expect(deps[0].props.compiled_content?).to eq(true)
+ end
+ end
+
+ context 'two props' do
+ before do
+ # FIXME: weird argument order (obj_b depends on obj_a, not th other way around)
+ store.record_dependency(obj_a, obj_b, compiled_content: true)
+ store.record_dependency(obj_a, obj_b, attributes: true)
+ end
+
+ it 'returns false for all unspecified props' do
+ deps = store.dependencies_causing_outdatedness_of(obj_a)
+ expect(deps[0].props.raw_content?).to eq(false)
+ expect(deps[0].props.path?).to eq(false)
+ end
+
+ it 'returns the specified props' do
+ deps = store.dependencies_causing_outdatedness_of(obj_a)
+ expect(deps[0].props.attributes?).to eq(true)
+ expect(deps[0].props.compiled_content?).to eq(true)
+ end
+ end
+ end
+
+ context 'two dependency in a chain' do
+ before do
+ # FIXME: weird argument order (obj_b depends on obj_a, not th other way around)
+ store.record_dependency(obj_a, obj_b)
+ store.record_dependency(obj_b, obj_c)
+ end
+
+ it 'returns one dependency for object A' do
+ deps = store.dependencies_causing_outdatedness_of(obj_a)
+ expect(deps.size).to eql(1)
+ expect(deps[0].from).to eql(obj_b)
+ end
+
+ it 'returns one dependency for object B' do
+ deps = store.dependencies_causing_outdatedness_of(obj_b)
+ expect(deps.size).to eql(1)
+ expect(deps[0].from).to eql(obj_c)
+ end
+
+ it 'returns nothing for the others' do
+ expect(store.dependencies_causing_outdatedness_of(obj_c)).to be_empty
+ end
+ end
+ end
+
+ describe 'reloading' do
+ before do
+ store.record_dependency(obj_a, obj_b, compiled_content: true)
+ store.record_dependency(obj_a, obj_b, attributes: true)
+
+ store.store
+ store.objects = objects_after
+ store.load
+ end
+
+ context 'no new objects' do
+ let(:objects_after) { objects }
+
+ it 'has the right dependencies for item A' do
+ deps = store.dependencies_causing_outdatedness_of(obj_a)
+ expect(deps.size).to eql(1)
+
+ expect(deps[0].from).to eql(obj_b)
+ expect(deps[0].to).to eql(obj_a)
+
+ expect(deps[0].props.raw_content?).to eq(false)
+ expect(deps[0].props.attributes?).to eq(true)
+ expect(deps[0].props.compiled_content?).to eq(true)
+ expect(deps[0].props.path?).to eq(false)
+ end
+
+ it 'has the right dependencies for item B' do
+ deps = store.dependencies_causing_outdatedness_of(obj_b)
+ expect(deps).to be_empty
+ end
+
+ it 'has the right dependencies for item C' do
+ deps = store.dependencies_causing_outdatedness_of(obj_c)
+ expect(deps).to be_empty
+ end
+ end
+
+ context 'one new object' do
+ let(:objects_after) do
+ [obj_a, obj_b, obj_c, obj_d]
+ end
+
+ let(:obj_d) { Nanoc::Int::Item.new('d', {}, '/d.md') }
+
+ it 'marks existing items as outdated' do
+ expect(store.objects_causing_outdatedness_of(obj_a)).to eq([obj_d])
+ expect(store.objects_causing_outdatedness_of(obj_b)).to eq([obj_d])
+ expect(store.objects_causing_outdatedness_of(obj_c)).to eq([obj_d])
+ end
+
+ it 'marks new items as outdated' do
+ expect(store.objects_causing_outdatedness_of(obj_d)).to eq([obj_d])
+ end
+ end
+
+ context 'two new objects' do
+ let(:objects_after) do
+ [obj_a, obj_b, obj_c, obj_d, obj_e]
+ end
+
+ let(:obj_d) { Nanoc::Int::Item.new('d', {}, '/d.md') }
+ let(:obj_e) { Nanoc::Int::Item.new('e', {}, '/e.md') }
+
+ it 'marks existing items as outdated' do
+ # Only one of obj D or E needed!
+ expect(store.objects_causing_outdatedness_of(obj_a)).to eq([obj_d]).or eq([obj_e])
+ expect(store.objects_causing_outdatedness_of(obj_b)).to eq([obj_d]).or eq([obj_e])
+ expect(store.objects_causing_outdatedness_of(obj_c)).to eq([obj_d]).or eq([obj_e])
+ end
+
+ it 'marks new items as outdated' do
+ # Only one of obj D or E needed!
+ expect(store.objects_causing_outdatedness_of(obj_d)).to eq([obj_d]).or eq([obj_e])
+ expect(store.objects_causing_outdatedness_of(obj_e)).to eq([obj_d]).or eq([obj_e])
+ end
+ end
+ end
+end
diff --git a/spec/nanoc/base/repos/site_loader_spec.rb b/spec/nanoc/base/repos/site_loader_spec.rb
new file mode 100644
index 0000000..99aab07
--- /dev/null
+++ b/spec/nanoc/base/repos/site_loader_spec.rb
@@ -0,0 +1,214 @@
+describe Nanoc::Int::SiteLoader do
+ let(:loader) { described_class.new }
+
+ describe '#new_empty' do
+ subject { loader.new_empty }
+
+ it 'has the default configuration' do
+ expect(subject.config).to be_a(Nanoc::Int::Configuration)
+ expect(subject.config[:index_filenames]).to eq(['index.html'])
+ end
+
+ it 'has no code snippets' do
+ expect(subject.code_snippets).to be_empty
+ end
+
+ it 'has no items' do
+ expect(subject.items).to be_empty
+ end
+
+ it 'has no layouts' do
+ expect(subject.layouts).to be_empty
+ end
+ end
+
+ describe '#new_with_config' do
+ subject { loader.new_with_config(arg) }
+
+ let(:arg) { { foo: 'bar' } }
+
+ it 'has a slightly modified configuration' do
+ expect(subject.config).to be_a(Nanoc::Int::Configuration)
+ expect(subject.config[:index_filenames]).to eq(['index.html'])
+ expect(subject.config[:foo]).to eq('bar')
+ end
+
+ it 'has no code snippets' do
+ expect(subject.code_snippets).to be_empty
+ end
+
+ it 'has no items' do
+ expect(subject.items).to be_empty
+ end
+
+ it 'has no layouts' do
+ expect(subject.layouts).to be_empty
+ end
+ end
+
+ describe '#new_from_cwd' do
+ subject { loader.new_from_cwd }
+
+ context 'no config file' do
+ it 'errors' do
+ expect { subject }.to raise_error(
+ Nanoc::Int::ConfigLoader::NoConfigFileFoundError,
+ )
+ end
+ end
+
+ shared_examples 'a directory with a config file' do
+ it 'has the default configuration' do
+ expect(subject.config).to be_a(Nanoc::Int::Configuration)
+ expect(subject.config[:index_filenames]).to eq(['index.html'])
+ expect(subject.config[:foo]).to eq('bar')
+ end
+
+ it 'has no code snippets' do
+ expect(subject.code_snippets).to be_empty
+ end
+
+ it 'has no items' do
+ expect(subject.items).to be_empty
+ end
+
+ it 'has no layouts' do
+ expect(subject.layouts).to be_empty
+ end
+
+ context 'some items, layouts, and code snippets' do
+ before do
+ FileUtils.mkdir_p('lib')
+ File.write('lib/foo.rb', '$spirit_animal = :donkey')
+
+ FileUtils.mkdir_p('content')
+ File.write('content/about.md', 'I am Denis!')
+
+ FileUtils.mkdir_p('layouts')
+ File.write('layouts/page.erb', '<html><%= yield %></html>')
+ end
+
+ it 'has a code snippet' do
+ expect(subject.code_snippets.size).to eq(1)
+ expect(subject.code_snippets[0].data).to eq('$spirit_animal = :donkey')
+ end
+
+ it 'has an item' do
+ expect(subject.items.size).to eq(1)
+ expect(subject.items['/about.md'].content).to be_a(Nanoc::Int::TextualContent)
+ expect(subject.items['/about.md'].content.string).to eq('I am Denis!')
+ expect(subject.items['/about.md'].attributes[:content_filename])
+ .to eq('content/about.md')
+ expect(subject.items['/about.md'].attributes[:extension])
+ .to eq('md')
+ expect(subject.items['/about.md'].attributes[:filename])
+ .to eq('content/about.md')
+ expect(subject.items['/about.md'].attributes[:meta_filename])
+ .to be_nil
+ expect(subject.items['/about.md'].attributes[:mtime])
+ .to be > Time.now - 5
+ expect(subject.items['/about.md'].identifier.to_s).to eq('/about.md')
+ end
+
+ it 'has a layout' do
+ expect(subject.layouts.size).to eq(1)
+ expect(subject.layouts['/page.erb'].content).to be_a(Nanoc::Int::TextualContent)
+ expect(subject.layouts['/page.erb'].content.string).to eq('<html><%= yield %></html>')
+ expect(subject.layouts['/page.erb'].attributes[:content_filename])
+ .to eq('layouts/page.erb')
+ expect(subject.layouts['/page.erb'].attributes[:extension])
+ .to eq('erb')
+ expect(subject.layouts['/page.erb'].attributes[:filename])
+ .to eq('layouts/page.erb')
+ expect(subject.layouts['/page.erb'].attributes[:meta_filename])
+ .to be_nil
+ expect(subject.layouts['/page.erb'].attributes[:mtime])
+ .to be > Time.now - 5
+ expect(subject.layouts['/page.erb'].identifier.to_s).to eq('/page.erb')
+ end
+ end
+ end
+
+ context 'nanoc.yaml config file' do
+ before do
+ File.write('nanoc.yaml', "---\nfoo: bar\n")
+ end
+
+ it_behaves_like 'a directory with a config file'
+ end
+
+ context 'config.yaml config file' do
+ before do
+ File.write('config.yaml', "---\nfoo: bar\n")
+ end
+
+ it_behaves_like 'a directory with a config file'
+ end
+
+ context 'configuration has non-existant data source' do
+ before do
+ File.write('nanoc.yaml', <<-EOS.gsub(/^ {10}/, ''))
+ data_sources:
+ - type: eenvaleed
+ EOS
+ end
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(Nanoc::Int::Errors::UnknownDataSource)
+ end
+ end
+
+ context 'environments defined' do
+ before do
+ File.write('nanoc.yaml', <<-EOS.gsub(/^ {10}/, ''))
+ animal: donkey
+ environments:
+ staging:
+ animal: giraffe
+ EOS
+ end
+
+ before do
+ expect(ENV).to receive(:fetch).with('NANOC_ENV', 'default').and_return('staging')
+ end
+
+ it 'does not load environment' do
+ expect(subject.config[:animal]).to eq('giraffe')
+ end
+ end
+
+ context 'code snippet with data source implementation' do
+ before do
+ FileUtils.mkdir_p('lib')
+ File.write('lib/foo_data_source.rb', <<-EOS.gsub(/^ {10}/, ''))
+ class FooDataSource < Nanoc::DataSource
+ identifier :site_loader_spec_sample
+
+ def items
+ [
+ Nanoc::Int::Item.new(
+ 'Generated content!',
+ { generated: true },
+ '/generated.txt',
+ )
+ ]
+ end
+ end
+ EOS
+
+ File.write('nanoc.yaml', <<-EOS.gsub(/^ {10}/, ''))
+ data_sources:
+ - type: site_loader_spec_sample
+ EOS
+ end
+
+ it 'loads code snippets before items/layouts' do
+ expect(subject.items.size).to eq(1)
+ expect(subject.items['/generated.txt'].content).to be_a(Nanoc::Int::TextualContent)
+ expect(subject.items['/generated.txt'].content.string).to eq('Generated content!')
+ expect(subject.items['/generated.txt'].attributes).to eq(generated: true)
+ expect(subject.items['/generated.txt'].identifier.to_s).to eq('/generated.txt')
+ end
+ end
+ end
+end
diff --git a/spec/nanoc/base/services/dependency_tracker_spec.rb b/spec/nanoc/base/services/dependency_tracker_spec.rb
new file mode 100644
index 0000000..04d370e
--- /dev/null
+++ b/spec/nanoc/base/services/dependency_tracker_spec.rb
@@ -0,0 +1,238 @@
+describe Nanoc::Int::DependencyTracker do
+ let(:tracker) { described_class.new(store) }
+
+ let(:store) { Nanoc::Int::DependencyStore.new([]) }
+
+ let(:item_a) { Nanoc::Int::Item.new('a', {}, '/a.md') }
+ let(:item_b) { Nanoc::Int::Item.new('b', {}, '/b.md') }
+ let(:item_c) { Nanoc::Int::Item.new('c', {}, '/c.md') }
+
+ shared_examples 'a null dependency tracker' do
+ let(:tracker) { Nanoc::Int::DependencyTracker::Null.new }
+
+ example do
+ expect { subject }.not_to change { store.objects_causing_outdatedness_of(item_a) }
+ end
+
+ example do
+ expect { subject }.not_to change { store.objects_causing_outdatedness_of(item_b) }
+ end
+
+ example do
+ expect { subject }.not_to change { store.objects_causing_outdatedness_of(item_c) }
+ end
+
+ example do
+ expect { subject }.not_to change { store.dependencies_causing_outdatedness_of(item_a) }
+ end
+
+ example do
+ expect { subject }.not_to change { store.dependencies_causing_outdatedness_of(item_b) }
+ end
+
+ example do
+ expect { subject }.not_to change { store.dependencies_causing_outdatedness_of(item_c) }
+ end
+ end
+
+ describe '#enter and #exit' do
+ context 'enter' do
+ subject do
+ tracker.enter(item_a)
+ end
+
+ it_behaves_like 'a null dependency tracker'
+
+ example do
+ expect { subject }.not_to change { store.objects_causing_outdatedness_of(item_a) }
+ end
+
+ example do
+ expect { subject }.not_to change { store.objects_causing_outdatedness_of(item_b) }
+ end
+
+ example do
+ expect { subject }.not_to change { store.objects_causing_outdatedness_of(item_c) }
+ end
+ end
+
+ context 'enter + enter' do
+ subject do
+ tracker.enter(item_a)
+ tracker.enter(item_b)
+ end
+
+ it_behaves_like 'a null dependency tracker'
+
+ it 'changes predecessors of item A' do
+ expect { subject }.to change { store.objects_causing_outdatedness_of(item_a) }
+ .from([]).to([item_b])
+ end
+
+ example do
+ expect { subject }.not_to change { store.objects_causing_outdatedness_of(item_b) }
+ end
+
+ example do
+ expect { subject }.not_to change { store.objects_causing_outdatedness_of(item_c) }
+ end
+ end
+
+ context 'enter + enter with props' do
+ subject do
+ tracker.enter(item_a)
+ tracker.enter(item_b, compiled_content: true)
+ end
+
+ it_behaves_like 'a null dependency tracker'
+
+ it 'changes predecessors of item A' do
+ expect { subject }.to change { store.objects_causing_outdatedness_of(item_a) }
+ .from([]).to([item_b])
+ end
+
+ it 'changes dependencies causing outdatedness of item A' do
+ expect { subject }.to change { store.dependencies_causing_outdatedness_of(item_a).size }
+ .from(0).to(1)
+ end
+
+ it 'creates correct new dependency causing outdatedness of item A' do
+ subject
+ dep = store.dependencies_causing_outdatedness_of(item_a)[0]
+
+ expect(dep.from).to eql(item_b)
+ expect(dep.to).to eql(item_a)
+ end
+
+ it 'creates dependency with correct props causing outdatedness of item A' do
+ subject
+ dep = store.dependencies_causing_outdatedness_of(item_a)[0]
+
+ expect(dep.props.compiled_content?).to eq(true)
+
+ expect(dep.props.raw_content?).to eq(false)
+ expect(dep.props.attributes?).to eq(false)
+ expect(dep.props.path?).to eq(false)
+ end
+
+ example do
+ expect { subject }.not_to change { store.objects_causing_outdatedness_of(item_b) }
+ end
+
+ example do
+ expect { subject }.not_to change { store.objects_causing_outdatedness_of(item_c) }
+ end
+
+ example do
+ expect { subject }.not_to change { store.dependencies_causing_outdatedness_of(item_b) }
+ end
+
+ example do
+ expect { subject }.not_to change { store.dependencies_causing_outdatedness_of(item_c) }
+ end
+ end
+
+ context 'enter + enter with prop + exit + enter with prop' do
+ subject do
+ tracker.enter(item_a)
+ tracker.enter(item_b, compiled_content: true)
+ tracker.exit
+ tracker.enter(item_b, attributes: true)
+ end
+
+ it_behaves_like 'a null dependency tracker'
+
+ it 'changes predecessors of item A' do
+ expect { subject }.to change { store.objects_causing_outdatedness_of(item_a) }
+ .from([]).to([item_b])
+ end
+
+ it 'changes dependencies causing outdatedness of item A' do
+ expect { subject }.to change { store.dependencies_causing_outdatedness_of(item_a).size }
+ .from(0).to(1)
+ end
+
+ it 'creates correct new dependency causing outdatedness of item A' do
+ subject
+ dep = store.dependencies_causing_outdatedness_of(item_a)[0]
+
+ expect(dep.from).to eql(item_b)
+ expect(dep.to).to eql(item_a)
+ end
+
+ it 'creates dependency with correct props causing outdatedness of item A' do
+ subject
+ dep = store.dependencies_causing_outdatedness_of(item_a)[0]
+
+ expect(dep.props.compiled_content?).to eq(true)
+ expect(dep.props.attributes?).to eq(true)
+
+ expect(dep.props.raw_content?).to eq(false)
+ expect(dep.props.path?).to eq(false)
+ end
+
+ example do
+ expect { subject }.not_to change { store.objects_causing_outdatedness_of(item_b) }
+ end
+
+ example do
+ expect { subject }.not_to change { store.objects_causing_outdatedness_of(item_c) }
+ end
+
+ example do
+ expect { subject }.not_to change { store.dependencies_causing_outdatedness_of(item_b) }
+ end
+
+ example do
+ expect { subject }.not_to change { store.dependencies_causing_outdatedness_of(item_c) }
+ end
+ end
+
+ context 'enter + enter + exit + enter' do
+ subject do
+ tracker.enter(item_a)
+ tracker.enter(item_b)
+ tracker.exit
+ tracker.enter(item_c)
+ end
+
+ it_behaves_like 'a null dependency tracker'
+
+ it 'changes predecessors of item A' do
+ expect { subject }.to change { store.objects_causing_outdatedness_of(item_a) }
+ .from([]).to([item_b, item_c])
+ end
+
+ example do
+ expect { subject }.not_to change { store.objects_causing_outdatedness_of(item_b) }
+ end
+
+ example do
+ expect { subject }.not_to change { store.objects_causing_outdatedness_of(item_c) }
+ end
+ end
+
+ context 'enter + bounce + enter' do
+ subject do
+ tracker.enter(item_a)
+ tracker.bounce(item_b)
+ tracker.enter(item_c)
+ end
+
+ it_behaves_like 'a null dependency tracker'
+
+ it 'changes predecessors of item A' do
+ expect { subject }.to change { store.objects_causing_outdatedness_of(item_a) }
+ .from([]).to([item_b, item_c])
+ end
+
+ example do
+ expect { subject }.not_to change { store.objects_causing_outdatedness_of(item_b) }
+ end
+
+ example do
+ expect { subject }.not_to change { store.objects_causing_outdatedness_of(item_c) }
+ end
+ end
+ end
+end
diff --git a/spec/nanoc/base/services/executor_spec.rb b/spec/nanoc/base/services/executor_spec.rb
new file mode 100644
index 0000000..2e66751
--- /dev/null
+++ b/spec/nanoc/base/services/executor_spec.rb
@@ -0,0 +1,495 @@
+describe Nanoc::Int::Executor do
+ let(:executor) { described_class.new(rep, compilation_context, dependency_tracker) }
+
+ let(:compilation_context) do
+ Nanoc::Int::Compiler::CompilationContext.new(
+ action_provider: action_provider,
+ reps: reps,
+ site: site,
+ compiled_content_cache: compiled_content_cache,
+ )
+ end
+
+ let(:item) { Nanoc::Int::Item.new(content, {}, '/index.md') }
+ let(:rep) { Nanoc::Int::ItemRep.new(item, :donkey) }
+ let(:content) { Nanoc::Int::TextualContent.new('Donkey Power').tap(&:freeze) }
+
+ let(:action_provider) { double(:action_provider) }
+ let(:reps) { double(:reps) }
+ let(:site) { double(:site) }
+ let(:compiled_content_cache) { double(:compiled_content_cache) }
+
+ let(:dependency_tracker) { Nanoc::Int::DependencyTracker.new(double(:dependency_store)) }
+
+ describe '#filter' do
+ let(:assigns) { {} }
+
+ let(:content) { Nanoc::Int::TextualContent.new('<%= "Donkey" %> Power') }
+
+ before do
+ allow(compilation_context).to receive(:assigns_for) { assigns }
+ end
+
+ context 'normal flow with textual rep' do
+ before do
+ expect(Nanoc::Int::NotificationCenter)
+ .to receive(:post).with(:filtering_started, rep, :erb)
+ expect(Nanoc::Int::NotificationCenter)
+ .to receive(:post).with(:filtering_ended, rep, :erb)
+ end
+
+ example do
+ executor.filter(:erb)
+
+ expect(rep.snapshot_contents[:last].string).to eq('Donkey Power')
+ expect(rep.snapshot_contents[:pre]).to be_nil
+ expect(rep.snapshot_contents[:post]).to be_nil
+ end
+
+ it 'returns frozen data' do
+ executor.filter(:erb)
+
+ expect(rep.snapshot_contents[:last]).to be_frozen
+ end
+ end
+
+ context 'normal flow with binary rep' do
+ let(:content) { Nanoc::Int::BinaryContent.new(File.expand_path('foo.dat')) }
+
+ before do
+ expect(Nanoc::Int::NotificationCenter)
+ .to receive(:post).with(:filtering_started, rep, :whatever)
+ expect(Nanoc::Int::NotificationCenter)
+ .to receive(:post).with(:filtering_ended, rep, :whatever)
+
+ File.write(content.filename, 'Foo Data')
+
+ filter_class = Class.new(::Nanoc::Filter) do
+ type :binary
+
+ def run(filename, _params = {})
+ File.write(output_filename, "Compiled data for #{filename}")
+ end
+ end
+
+ expect(Nanoc::Filter).to receive(:named).with(:whatever) { filter_class }
+ end
+
+ example do
+ executor.filter(:whatever)
+
+ expect(File.read(rep.snapshot_contents[:last].filename))
+ .to match(/\ACompiled data for \/.*\/foo.dat\z/)
+ expect(rep.snapshot_contents[:pre]).to be_nil
+ expect(rep.snapshot_contents[:post]).to be_nil
+ end
+
+ it 'returns frozen data' do
+ executor.filter(:whatever)
+
+ expect(rep.snapshot_contents[:last]).to be_frozen
+ end
+ end
+
+ context 'normal flow with binary rep and binary-to-text filter' do
+ let(:content) { Nanoc::Int::BinaryContent.new(File.expand_path('foo.dat')) }
+
+ before do
+ expect(Nanoc::Int::NotificationCenter)
+ .to receive(:post).with(:filtering_started, rep, :whatever)
+ expect(Nanoc::Int::NotificationCenter)
+ .to receive(:post).with(:filtering_ended, rep, :whatever)
+
+ File.write(content.filename, 'Foo Data')
+
+ filter_class = Class.new(::Nanoc::Filter) do
+ type binary: :text
+
+ def run(filename, _params = {})
+ "Compiled data for #{filename}"
+ end
+ end
+
+ expect(Nanoc::Filter).to receive(:named).with(:whatever) { filter_class }
+ end
+
+ example do
+ executor.filter(:whatever)
+
+ expect(rep.snapshot_contents[:last].string).to match(/\ACompiled data for \/.*\/foo.dat\z/)
+ expect(rep.snapshot_contents[:pre]).to be_nil
+ expect(rep.snapshot_contents[:post]).to be_nil
+ end
+ end
+
+ context 'normal flow with textual rep and text-to-binary filter' do
+ before do
+ expect(Nanoc::Int::NotificationCenter)
+ .to receive(:post).with(:filtering_started, rep, :whatever)
+ expect(Nanoc::Int::NotificationCenter)
+ .to receive(:post).with(:filtering_ended, rep, :whatever)
+
+ filter_class = Class.new(::Nanoc::Filter) do
+ type text: :binary
+
+ def run(content, _params = {})
+ File.write(output_filename, "Binary #{content}")
+ end
+ end
+
+ expect(Nanoc::Filter).to receive(:named).with(:whatever) { filter_class }
+ end
+
+ example do
+ executor.filter(:whatever)
+
+ expect(File.read(rep.snapshot_contents[:last].filename))
+ .to eq('Binary <%= "Donkey" %> Power')
+ expect(rep.snapshot_contents[:pre]).to be_nil
+ expect(rep.snapshot_contents[:post]).to be_nil
+ end
+ end
+
+ context 'non-existant filter' do
+ it 'raises' do
+ expect { executor.filter(:ajlsdfjklaskldfj) }
+ .to raise_error(Nanoc::Int::Errors::UnknownFilter)
+ end
+ end
+
+ context 'non-binary rep, binary-to-something filter' do
+ before do
+ filter_class = Class.new(::Nanoc::Filter) do
+ type :binary
+
+ def run(_content, _params = {}); end
+ end
+
+ expect(Nanoc::Filter).to receive(:named).with(:whatever) { filter_class }
+ end
+
+ it 'raises' do
+ expect { executor.filter(:whatever) }
+ .to raise_error(Nanoc::Int::Errors::CannotUseBinaryFilter)
+ end
+ end
+
+ context 'binary rep, text-to-something filter' do
+ let(:content) { Nanoc::Int::BinaryContent.new(File.expand_path('foo.md')) }
+
+ it 'raises' do
+ expect { executor.filter(:erb) }
+ .to raise_error(Nanoc::Int::Errors::CannotUseTextualFilter)
+ end
+ end
+
+ context 'binary filter that does not write anything' do
+ let(:content) { Nanoc::Int::BinaryContent.new(File.expand_path('foo.dat')) }
+
+ before do
+ expect(Nanoc::Int::NotificationCenter)
+ .to receive(:post).with(:filtering_started, rep, :whatever)
+ expect(Nanoc::Int::NotificationCenter)
+ .to receive(:post).with(:filtering_ended, rep, :whatever)
+
+ File.write(content.filename, 'Foo Data')
+
+ filter_class = Class.new(::Nanoc::Filter) do
+ type :binary
+
+ def run(_filename, _params = {}); end
+ end
+
+ expect(Nanoc::Filter).to receive(:named).with(:whatever) { filter_class }
+ end
+
+ example do
+ expect { executor.filter(:whatever) }
+ .to raise_error(Nanoc::Int::Executor::OutputNotWrittenError)
+ end
+ end
+
+ context 'content is frozen' do
+ let(:item) do
+ Nanoc::Int::Item.new('foo bar', {}, '/foo.md').tap(&:freeze)
+ end
+
+ let(:filter_that_modifies_content) do
+ Class.new(::Nanoc::Filter) do
+ def run(content, _params = {})
+ content.gsub!('foo', 'moo')
+ content
+ end
+ end
+ end
+
+ let(:filter_that_modifies_params) do
+ Class.new(::Nanoc::Filter) do
+ def run(_content, params = {})
+ params[:foo] = 'bar'
+ 'asdf'
+ end
+ end
+ end
+
+ it 'errors when attempting to modify content' do
+ expect(Nanoc::Filter).to receive(:named).with(:whatever).and_return(filter_that_modifies_content)
+ expect { executor.filter(:whatever) }.to raise_frozen_error
+ end
+
+ it 'receives frozen filter args' do
+ expect(Nanoc::Filter).to receive(:named).with(:whatever).and_return(filter_that_modifies_params)
+ expect { executor.filter(:whatever) }.to raise_frozen_error
+ end
+ end
+ end
+
+ describe '#layout' do
+ let(:site) { double(:site, config: config, layouts: layouts) }
+
+ let(:config) do
+ {
+ string_pattern_type: 'glob',
+ }
+ end
+
+ let(:layout) do
+ Nanoc::Int::Layout.new(layout_content, { bug: 'Gum Emperor' }, '/default.erb')
+ end
+
+ let(:layouts) { [layout] }
+
+ let(:layout_content) { 'head <%= @foo %> foot' }
+
+ let(:assigns) do
+ { foo: 'hallo' }
+ end
+
+ let(:view_context) do
+ Nanoc::ViewContext.new(
+ reps: double(:reps),
+ items: double(:items),
+ dependency_tracker: dependency_tracker,
+ compilation_context: double(:compilation_context),
+ )
+ end
+
+ let(:rule_memory) do
+ Nanoc::Int::RuleMemory.new(rep).tap do |mem|
+ mem.add_filter(:erb, {})
+ end
+ end
+
+ before do
+ rep.snapshot_defs = [Nanoc::Int::SnapshotDef.new(:pre)]
+
+ allow(compilation_context).to receive(:site) { site }
+ allow(compilation_context).to receive(:assigns_for).with(rep, dependency_tracker) { assigns }
+ allow(compilation_context).to receive(:create_view_context).with(dependency_tracker).and_return(view_context)
+
+ allow(action_provider).to receive(:memory_for).with(layout).and_return(rule_memory)
+ end
+
+ subject { executor.layout('/default.*') }
+
+ context 'accessing layout attributes' do
+ let(:layout_content) { 'head <%= @layout[:bug] %> foot' }
+
+ it 'exposes @layout as view' do
+ allow(dependency_tracker).to receive(:enter)
+ .with(layout, raw_content: true, attributes: false, compiled_content: false, path: false)
+ allow(dependency_tracker).to receive(:enter)
+ .with(layout, raw_content: false, attributes: true, compiled_content: false, path: false)
+ allow(dependency_tracker).to receive(:exit)
+ subject
+ expect(rep.snapshot_contents[:last].string).to eq('head Gum Emperor foot')
+ end
+ end
+
+ context 'normal flow' do
+ it 'updates last content' do
+ subject
+ expect(rep.snapshot_contents[:last].string).to eq('head hallo foot')
+ end
+
+ it 'sets frozen content' do
+ subject
+ expect(rep.snapshot_contents[:last]).to be_frozen
+ expect(rep.snapshot_contents[:pre]).to be_frozen
+ end
+
+ it 'does not create pre snapshot' do
+ # a #layout is followed by a #snapshot(:pre, …)
+ expect(rep.snapshot_contents[:pre]).to be_nil
+ subject
+ expect(rep.snapshot_contents[:pre]).to be_nil
+ end
+
+ it 'sends notifications' do
+ expect(Nanoc::Int::NotificationCenter).to receive(:post).with(:filtering_started, rep, :erb).ordered
+ expect(Nanoc::Int::NotificationCenter).to receive(:post).with(:filtering_ended, rep, :erb).ordered
+
+ subject
+ end
+
+ context 'compiled_content reference in layout' do
+ let(:layout_content) { 'head <%= @item_rep.compiled_content(snapshot: :pre) %> foot' }
+
+ let(:assigns) do
+ { item_rep: Nanoc::ItemRepView.new(rep, view_context) }
+ end
+
+ before do
+ executor.snapshot(:pre)
+ end
+
+ it 'can contain compiled_content reference' do
+ subject
+ expect(rep.snapshot_contents[:last].string).to eq('head Donkey Power foot')
+ end
+ end
+
+ context 'content with layout reference' do
+ let(:layout_content) { 'head <%= @layout.identifier %> foot' }
+
+ it 'includes layout in assigns' do
+ subject
+ expect(rep.snapshot_contents[:last].string).to eq('head /default.erb foot')
+ end
+ end
+ end
+
+ context 'no layout found' do
+ let(:layouts) do
+ [Nanoc::Int::Layout.new('head <%= @foo %> foot', {}, '/other.erb')]
+ end
+
+ it 'raises' do
+ expect { subject }.to raise_error(Nanoc::Int::Errors::UnknownLayout)
+ end
+ end
+
+ context 'no filter specified' do
+ let(:rule_memory) do
+ Nanoc::Int::RuleMemory.new(rep)
+ end
+
+ it 'raises' do
+ expect { subject }.to raise_error(Nanoc::Int::Errors::UndefinedFilterForLayout)
+ end
+ end
+
+ context 'binary item' do
+ let(:content) { Nanoc::Int::BinaryContent.new(File.expand_path('donkey.md')) }
+
+ it 'raises' do
+ expect { subject }.to raise_error(Nanoc::Int::Errors::CannotLayoutBinaryItem)
+ end
+ end
+
+ it 'receives frozen filter args' do
+ filter_class = Class.new(::Nanoc::Filter) do
+ def run(_content, params = {})
+ params[:foo] = 'bar'
+ 'asdf'
+ end
+ end
+
+ expect(Nanoc::Filter).to receive(:named).with(:erb) { filter_class }
+
+ expect { subject }.to raise_frozen_error
+ end
+ end
+
+ describe '#snapshot' do
+ context 'binary content' do
+ let(:content) { Nanoc::Int::BinaryContent.new(File.expand_path('donkey.dat')) }
+
+ it 'creates snapshots' do
+ executor.snapshot(:something)
+
+ expect(rep.snapshot_contents[:something]).not_to be_nil
+ end
+ end
+
+ context 'textual content' do
+ let(:content) { Nanoc::Int::TextualContent.new('Donkey Power') }
+
+ it 'creates a snapshot' do
+ executor.snapshot(:something)
+
+ expect(rep.snapshot_contents[:something].string).to eq('Donkey Power')
+ end
+ end
+
+ context 'final snapshot' do
+ let(:content) { Nanoc::Int::TextualContent.new('Donkey Power') }
+
+ context 'raw path' do
+ before do
+ rep.raw_paths = { something: 'output/donkey.md' }
+ end
+
+ it 'does not write' do
+ executor.snapshot(:something)
+
+ expect(File.file?('output/donkey.md')).not_to be
+ end
+ end
+
+ context 'no raw path' do
+ it 'does not write' do
+ executor.snapshot(:something)
+
+ expect(File.file?('output/donkey.md')).to eq(false)
+ end
+ end
+ end
+ end
+
+ describe '#find_layout' do
+ let(:site) { double(:site, config: config, layouts: layouts) }
+
+ let(:config) { {} }
+
+ before do
+ allow(compilation_context).to receive(:site) { site }
+ end
+
+ subject { executor.find_layout(arg) }
+
+ context 'layout with cleaned identifier exists' do
+ let(:arg) { '/default' }
+
+ let(:layouts) do
+ [Nanoc::Int::Layout.new('head <%= @foo %> foot', {}, '/default/')]
+ end
+
+ it { is_expected.to eq(layouts[0]) }
+ end
+
+ context 'no layout with cleaned identifier exists' do
+ let(:layouts) do
+ [Nanoc::Int::Layout.new('head <%= @foo %> foot', {}, '/default.erb')]
+ end
+
+ context 'globs' do
+ let(:config) { { string_pattern_type: 'glob' } }
+
+ let(:arg) { '/default.*' }
+
+ it { is_expected.to eq(layouts[0]) }
+ end
+
+ context 'no globs' do
+ let(:config) { { string_pattern_type: 'legacy' } }
+
+ let(:arg) { '/default.*' }
+
+ it 'raises' do
+ expect { subject }.to raise_error(Nanoc::Int::Errors::UnknownLayout)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/nanoc/base/services/item_rep_router_spec.rb b/spec/nanoc/base/services/item_rep_router_spec.rb
new file mode 100644
index 0000000..4eaac58
--- /dev/null
+++ b/spec/nanoc/base/services/item_rep_router_spec.rb
@@ -0,0 +1,134 @@
+describe(Nanoc::Int::ItemRepRouter) do
+ subject(:item_rep_router) { described_class.new(reps, action_provider, site) }
+
+ let(:reps) { double(:reps) }
+ let(:action_provider) { double(:action_provider) }
+ let(:site) { double(:site, config: config) }
+ let(:config) { Nanoc::Int::Configuration.new.with_defaults }
+
+ describe '#run' do
+ subject { item_rep_router.run }
+
+ let(:item) { Nanoc::Int::Item.new('content', {}, '/foo.md') }
+
+ let(:reps) do
+ [
+ Nanoc::Int::ItemRep.new(item, :default),
+ Nanoc::Int::ItemRep.new(item, :csv),
+ ]
+ end
+
+ let(:paths_0) do
+ { last: '/foo/index.html' }
+ end
+
+ let(:paths_1) do
+ { last: '/bar.html' }
+ end
+
+ example do
+ expect(action_provider).to receive(:paths_for).with(reps[0]).and_return(paths_0)
+ expect(action_provider).to receive(:paths_for).with(reps[1]).and_return(paths_1)
+
+ subject
+
+ expect(reps[0].raw_paths).to eql(last: 'output/foo/index.html')
+ expect(reps[0].paths).to eql(last: '/foo/')
+
+ expect(reps[1].raw_paths).to eql(last: 'output/bar.html')
+ expect(reps[1].paths).to eql(last: '/bar.html')
+ end
+ end
+
+ describe '#route_rep' do
+ subject { item_rep_router.route_rep(rep, path, snapshot_name, paths_to_reps) }
+
+ let(:path) { basic_path }
+ let(:snapshot_name) { :foo }
+ let(:rep) { Nanoc::Int::ItemRep.new(item, :default) }
+ let(:item) { Nanoc::Int::Item.new('content', {}, '/foo.md') }
+ let(:paths_to_reps) { {} }
+
+ context 'basic path is nil' do
+ let(:basic_path) { nil }
+ it { is_expected.to be_nil }
+ end
+
+ context 'basic path is not nil' do
+ let(:basic_path) { '/foo/index.html' }
+
+ context 'other snapshot with this path already exists' do
+ let(:paths_to_reps) { { '/foo/index.html' => double(:other_rep) } }
+
+ it 'errors' do
+ expect { subject }.to raise_error(Nanoc::Int::ItemRepRouter::IdenticalRoutesError)
+ end
+ end
+
+ context 'path is unique' do
+ it 'sets the raw path' do
+ subject
+ expect(rep.raw_paths).to eql(foo: 'output/foo/index.html')
+ end
+
+ it 'sets the path' do
+ subject
+ expect(rep.paths).to eql(foo: '/foo/')
+ end
+
+ it 'adds to paths_to_reps' do
+ subject
+ expect(paths_to_reps).to have_key('/foo/index.html')
+ end
+
+ context 'path does not start with a slash' do
+ let(:basic_path) { 'foo/index.html' }
+
+ it 'errors' do
+ expect { subject }.to raise_error(Nanoc::Int::ItemRepRouter::RouteWithoutSlashError)
+ end
+ end
+
+ context 'path is not UTF-8' do
+ let(:basic_path) { '/foo/index.html'.encode('ISO-8859-1') }
+
+ it 'sets the path as UTF-8' do
+ subject
+ expect(rep.paths).to eql(foo: '/foo/')
+ expect(rep.paths[:foo].encoding.to_s).to eql('UTF-8')
+ end
+
+ it 'sets the raw path as UTF-8' do
+ subject
+ expect(rep.raw_paths).to eql(foo: 'output/foo/index.html')
+ expect(rep.raw_paths[:foo].encoding.to_s).to eql('UTF-8')
+ end
+ end
+ end
+ end
+ end
+
+ describe '#strip_index_filename' do
+ subject { item_rep_router.strip_index_filename(basic_path) }
+
+ context 'basic path ends with /index.html' do
+ let(:basic_path) { '/bar/index.html' }
+ it { is_expected.to eql('/bar/') }
+ end
+
+ context 'basic path contains /index.html' do
+ let(:basic_path) { '/bar/index.html/foo' }
+ it { is_expected.to eql('/bar/index.html/foo') }
+ end
+
+ context 'basic path ends with xindex.html' do
+ let(:basic_path) { '/bar/xindex.html' }
+ it { is_expected.to eql('/bar/xindex.html') }
+ end
+
+ context 'basic path does not contain /index.html' do
+ let(:basic_path) { '/bar/foo.html' }
+ it { is_expected.to eql('/bar/foo.html') }
+ end
+ end
+end
diff --git a/spec/nanoc/base/services/item_rep_selector_spec.rb b/spec/nanoc/base/services/item_rep_selector_spec.rb
new file mode 100644
index 0000000..46c73fe
--- /dev/null
+++ b/spec/nanoc/base/services/item_rep_selector_spec.rb
@@ -0,0 +1,169 @@
+describe Nanoc::Int::ItemRepSelector do
+ let(:selector) { described_class.new(reps_for_selector) }
+
+ let(:item) do
+ Nanoc::Int::Item.new('stuff', {}, '/foo.md')
+ end
+
+ let(:reps_array) do
+ [
+ Nanoc::Int::ItemRep.new(item, :a),
+ Nanoc::Int::ItemRep.new(item, :b),
+ Nanoc::Int::ItemRep.new(item, :c),
+ Nanoc::Int::ItemRep.new(item, :d),
+ Nanoc::Int::ItemRep.new(item, :e),
+ ]
+ end
+
+ let(:reps_for_selector) { reps_array }
+
+ let(:names_to_reps) do
+ reps_array.each_with_object({}) do |rep, acc|
+ acc[rep.name] = rep
+ end
+ end
+
+ let(:dependencies) { {} }
+
+ let(:result) do
+ tentatively_yielded = []
+ successfully_yielded = []
+ selector.each do |rep|
+ tentatively_yielded << rep.name
+
+ dependencies.fetch(rep.name, []).each do |name|
+ unless successfully_yielded.include?(name)
+ raise Nanoc::Int::Errors::UnmetDependency.new(names_to_reps[name])
+ end
+ end
+
+ successfully_yielded << rep.name
+ end
+
+ [tentatively_yielded, successfully_yielded]
+ end
+
+ let(:tentatively_yielded) { result[0] }
+ let(:successfully_yielded) { result[1] }
+
+ describe 'error' do
+ context 'plain error' do
+ subject { selector.each { |_rep| raise 'heh' } }
+
+ it 'raises' do
+ expect { subject }.to raise_error(RuntimeError, 'heh')
+ end
+ end
+
+ context 'plain dependency error' do
+ subject do
+ idx = 0
+ selector.each do |_rep|
+ idx += 1
+
+ raise Nanoc::Int::Errors::UnmetDependency.new(reps_array[2]) if idx == 1
+ end
+ end
+
+ it 'does not raise' do
+ expect { subject }.not_to raise_error
+ end
+ end
+
+ context 'wrapped error' do
+ subject do
+ selector.each do |rep|
+ begin
+ raise 'heh'
+ rescue => e
+ raise Nanoc::Int::Errors::CompilationError.new(e, rep)
+ end
+ end
+ end
+
+ it 'raises original error' do
+ expect { subject }.to raise_error(Nanoc::Int::Errors::CompilationError) do |err|
+ expect(err.unwrap).to be_a(RuntimeError)
+ expect(err.unwrap.message).to eq('heh')
+ end
+ end
+ end
+
+ context 'wrapped dependency error' do
+ subject do
+ idx = 0
+ selector.each do |rep|
+ idx += 1
+
+ begin
+ raise Nanoc::Int::Errors::UnmetDependency.new(reps_array[2]) if idx == 1
+ rescue => e
+ raise Nanoc::Int::Errors::CompilationError.new(e, rep)
+ end
+ end
+ end
+
+ it 'does not raise' do
+ expect { subject }.not_to raise_error
+ end
+ end
+ end
+
+ describe 'yield order' do
+ context 'linear dependencies' do
+ let(:dependencies) do
+ {
+ a: [:b],
+ b: [:c],
+ c: [:d],
+ d: [:e],
+ e: [],
+ }
+ end
+
+ example do
+ expect(successfully_yielded).to eq [:e, :d, :c, :b, :a]
+ expect(tentatively_yielded).to eq [:a, :b, :c, :d, :e, :d, :c, :b, :a]
+ end
+ end
+
+ context 'no dependencies' do
+ let(:dependencies) do
+ {}
+ end
+
+ example do
+ expect(successfully_yielded).to eq [:a, :b, :c, :d, :e]
+ expect(tentatively_yielded).to eq [:a, :b, :c, :d, :e]
+ end
+ end
+
+ context 'star dependencies' do
+ let(:dependencies) do
+ {
+ a: [:b, :c, :d, :e],
+ }
+ end
+
+ example do
+ expect(successfully_yielded).to eq [:b, :c, :d, :e, :a]
+ expect(tentatively_yielded).to eq [:a, :b, :c, :d, :e, :a]
+ end
+ end
+
+ context 'star dependencies; selectively recompiling' do
+ let(:reps_for_selector) { reps_array.first(1) }
+
+ let(:dependencies) do
+ {
+ a: [:b, :c, :d, :e],
+ }
+ end
+
+ example do
+ expect(successfully_yielded).to eq [:b, :c, :d, :e, :a]
+ expect(tentatively_yielded).to eq [:a, :b, :a, :c, :a, :d, :a, :e, :a]
+ end
+ end
+ end
+end
diff --git a/spec/nanoc/base/services/outdatedness_checker_spec.rb b/spec/nanoc/base/services/outdatedness_checker_spec.rb
new file mode 100644
index 0000000..6eeb7ad
--- /dev/null
+++ b/spec/nanoc/base/services/outdatedness_checker_spec.rb
@@ -0,0 +1,370 @@
+describe Nanoc::Int::OutdatednessChecker do
+ let(:outdatedness_checker) do
+ described_class.new(
+ site: site,
+ checksum_store: checksum_store,
+ dependency_store: dependency_store,
+ rule_memory_store: rule_memory_store,
+ action_provider: action_provider,
+ reps: reps,
+ )
+ end
+
+ let(:site) { double(:site) }
+ let(:checksum_store) { double(:checksum_store) }
+ let(:dependency_store) { double(:dependency_store) }
+
+ let(:rule_memory_store) do
+ Nanoc::Int::RuleMemoryStore.new
+ end
+
+ let(:old_memory_for_item_rep) do
+ Nanoc::Int::RuleMemory.new(item_rep).tap do |mem|
+ mem.add_filter(:erb, {})
+ end
+ end
+
+ let(:new_memory_for_item_rep) { old_memory_for_item_rep }
+
+ let(:action_provider) { double(:action_provider) }
+
+ let(:reps) do
+ Nanoc::Int::ItemRepRepo.new
+ end
+
+ let(:item_rep) { Nanoc::Int::ItemRep.new(item, :default) }
+ let(:item) { Nanoc::Int::Item.new('stuff', {}, '/foo.md') }
+
+ let(:objects) { [item] }
+
+ before do
+ reps << item_rep
+ rule_memory_store[item_rep] = old_memory_for_item_rep.serialize
+
+ allow(action_provider).to receive(:memory_for).with(item_rep).and_return(new_memory_for_item_rep)
+ end
+
+ describe '#basic_outdatedness_reason_for' do
+ subject { outdatedness_checker.send(:basic_outdatedness_reason_for, obj) }
+
+ let(:checksum_store) { Nanoc::Int::ChecksumStore.new(objects: objects) }
+
+ let(:config) { Nanoc::Int::Configuration.new }
+
+ before do
+ checksum_store.add(item)
+
+ allow(site).to receive(:code_snippets).and_return([])
+ allow(site).to receive(:config).and_return(config)
+ end
+
+ context 'with item' do
+ let(:obj) { item }
+
+ context 'rule memory differs' do
+ let(:new_memory_for_item_rep) do
+ Nanoc::Int::RuleMemory.new(item_rep).tap do |mem|
+ mem.add_filter(:super_erb, {})
+ end
+ end
+
+ it 'is outdated due to rule differences' do
+ expect(subject).to eql(Nanoc::Int::OutdatednessReasons::RulesModified)
+ end
+ end
+
+ # …
+ end
+
+ context 'with item rep' do
+ let(:obj) { item_rep }
+
+ context 'rule memory differs' do
+ let(:new_memory_for_item_rep) do
+ Nanoc::Int::RuleMemory.new(item_rep).tap do |mem|
+ mem.add_filter(:super_erb, {})
+ end
+ end
+
+ it 'is outdated due to rule differences' do
+ expect(subject).to eql(Nanoc::Int::OutdatednessReasons::RulesModified)
+ end
+ end
+
+ # …
+ end
+
+ context 'with layout' do
+ # …
+ end
+ end
+
+ describe '#outdated_due_to_dependencies?' do
+ subject { outdatedness_checker.send(:outdated_due_to_dependencies?, item) }
+
+ let(:dependency_store) do
+ Nanoc::Int::DependencyStore.new(objects)
+ end
+
+ let(:checksum_store) { Nanoc::Int::ChecksumStore.new(objects: objects) }
+
+ let(:other_item) { Nanoc::Int::Item.new('other stuff', {}, '/other.md') }
+ let(:other_item_rep) { Nanoc::Int::ItemRep.new(other_item, :default) }
+
+ let(:config) { Nanoc::Int::Configuration.new }
+
+ let(:objects) { [item, other_item] }
+
+ let(:old_memory_for_other_item_rep) do
+ Nanoc::Int::RuleMemory.new(other_item_rep).tap do |mem|
+ mem.add_filter(:erb, {})
+ end
+ end
+
+ let(:new_memory_for_other_item_rep) { old_memory_for_other_item_rep }
+
+ before do
+ reps << other_item_rep
+ rule_memory_store[other_item_rep] = old_memory_for_other_item_rep.serialize
+ checksum_store.add(item)
+ checksum_store.add(other_item)
+ checksum_store.add(config)
+
+ allow(action_provider).to receive(:memory_for).with(other_item_rep).and_return(new_memory_for_other_item_rep)
+ allow(site).to receive(:code_snippets).and_return([])
+ allow(site).to receive(:config).and_return(config)
+ end
+
+ context 'transitive dependency' do
+ let(:distant_item) { Nanoc::Int::Item.new('distant stuff', {}, '/distant.md') }
+ let(:distant_item_rep) { Nanoc::Int::ItemRep.new(distant_item, :default) }
+
+ before do
+ reps << distant_item_rep
+ checksum_store.add(distant_item)
+ rule_memory_store[distant_item_rep] = old_memory_for_other_item_rep.serialize
+ allow(action_provider).to receive(:memory_for).with(distant_item_rep).and_return(new_memory_for_other_item_rep)
+ end
+
+ context 'on attribute + attribute' do
+ before do
+ dependency_store.record_dependency(item, other_item, attributes: true)
+ dependency_store.record_dependency(other_item, distant_item, attributes: true)
+ end
+
+ context 'distant attribute changed' do
+ before { distant_item.attributes[:title] = 'omg new title' }
+
+ it 'has correct outdatedness of item' do
+ expect(outdatedness_checker.send(:outdated_due_to_dependencies?, item)).not_to be
+ end
+
+ it 'has correct outdatedness of other item' do
+ expect(outdatedness_checker.send(:outdated_due_to_dependencies?, other_item)).to be
+ end
+ end
+
+ context 'distant raw content changed' do
+ before { distant_item.content = Nanoc::Int::TextualContent.new('omg new content') }
+
+ it 'has correct outdatedness of item' do
+ expect(outdatedness_checker.send(:outdated_due_to_dependencies?, item)).not_to be
+ end
+
+ it 'has correct outdatedness of other item' do
+ expect(outdatedness_checker.send(:outdated_due_to_dependencies?, other_item)).not_to be
+ end
+ end
+ end
+
+ context 'on compiled content + attribute' do
+ before do
+ dependency_store.record_dependency(item, other_item, compiled_content: true)
+ dependency_store.record_dependency(other_item, distant_item, attributes: true)
+ end
+
+ context 'distant attribute changed' do
+ before { distant_item.attributes[:title] = 'omg new title' }
+
+ it 'has correct outdatedness of item' do
+ expect(outdatedness_checker.send(:outdated_due_to_dependencies?, item)).to be
+ end
+
+ it 'has correct outdatedness of other item' do
+ expect(outdatedness_checker.send(:outdated_due_to_dependencies?, other_item)).to be
+ end
+ end
+
+ context 'distant raw content changed' do
+ before { distant_item.content = Nanoc::Int::TextualContent.new('omg new content') }
+
+ it 'has correct outdatedness of item' do
+ expect(outdatedness_checker.send(:outdated_due_to_dependencies?, item)).not_to be
+ end
+
+ it 'has correct outdatedness of other item' do
+ expect(outdatedness_checker.send(:outdated_due_to_dependencies?, other_item)).not_to be
+ end
+ end
+ end
+ end
+
+ context 'only attribute dependency' do
+ before do
+ dependency_store.record_dependency(item, other_item, attributes: true)
+ end
+
+ context 'attribute changed' do
+ before { other_item.attributes[:title] = 'omg new title' }
+ it { is_expected.to be }
+ end
+
+ context 'raw content changed' do
+ before { other_item.content = Nanoc::Int::TextualContent.new('omg new content') }
+ it { is_expected.not_to be }
+ end
+
+ context 'attribute + raw content changed' do
+ before { other_item.attributes[:title] = 'omg new title' }
+ before { other_item.content = Nanoc::Int::TextualContent.new('omg new content') }
+ it { is_expected.to be }
+ end
+
+ context 'path changed' do
+ let(:new_memory_for_other_item_rep) do
+ Nanoc::Int::RuleMemory.new(other_item_rep).tap do |mem|
+ mem.add_filter(:erb, {})
+ mem.add_snapshot(:donkey, '/giraffe.txt')
+ end
+ end
+
+ it { is_expected.not_to be }
+ end
+ end
+
+ context 'only raw content dependency' do
+ before do
+ dependency_store.record_dependency(item, other_item, raw_content: true)
+ end
+
+ context 'attribute changed' do
+ before { other_item.attributes[:title] = 'omg new title' }
+ it { is_expected.not_to be }
+ end
+
+ context 'raw content changed' do
+ before { other_item.content = Nanoc::Int::TextualContent.new('omg new content') }
+ it { is_expected.to be }
+ end
+
+ context 'attribute + raw content changed' do
+ before { other_item.attributes[:title] = 'omg new title' }
+ before { other_item.content = Nanoc::Int::TextualContent.new('omg new content') }
+ it { is_expected.to be }
+ end
+
+ context 'path changed' do
+ let(:new_memory_for_other_item_rep) do
+ Nanoc::Int::RuleMemory.new(other_item_rep).tap do |mem|
+ mem.add_filter(:erb, {})
+ mem.add_snapshot(:donkey, '/giraffe.txt')
+ end
+ end
+
+ it { is_expected.not_to be }
+ end
+ end
+
+ context 'only path dependency' do
+ before do
+ dependency_store.record_dependency(item, other_item, raw_content: true)
+ end
+
+ context 'attribute changed' do
+ before { other_item.attributes[:title] = 'omg new title' }
+ it { is_expected.not_to be }
+ end
+
+ context 'raw content changed' do
+ before { other_item.content = Nanoc::Int::TextualContent.new('omg new content') }
+ it { is_expected.to be }
+ end
+
+ context 'path changed' do
+ let(:new_memory_for_other_item_rep) do
+ Nanoc::Int::RuleMemory.new(other_item_rep).tap do |mem|
+ mem.add_filter(:erb, {})
+ mem.add_snapshot(:donkey, '/giraffe.txt')
+ end
+ end
+
+ it { is_expected.not_to be }
+ end
+ end
+
+ context 'attribute + raw content dependency' do
+ before do
+ dependency_store.record_dependency(item, other_item, attributes: true, raw_content: true)
+ end
+
+ context 'attribute changed' do
+ before { other_item.attributes[:title] = 'omg new title' }
+ it { is_expected.to be }
+ end
+
+ context 'raw content changed' do
+ before { other_item.content = Nanoc::Int::TextualContent.new('omg new content') }
+ it { is_expected.to be }
+ end
+
+ context 'attribute + raw content changed' do
+ before { other_item.attributes[:title] = 'omg new title' }
+ before { other_item.content = Nanoc::Int::TextualContent.new('omg new content') }
+ it { is_expected.to be }
+ end
+
+ context 'rules changed' do
+ let(:new_memory_for_other_item_rep) do
+ Nanoc::Int::RuleMemory.new(other_item_rep).tap do |mem|
+ mem.add_filter(:erb, {})
+ mem.add_filter(:donkey, {})
+ end
+ end
+
+ it { is_expected.not_to be }
+ end
+ end
+
+ context 'attribute + other dependency' do
+ before do
+ dependency_store.record_dependency(item, other_item, attributes: true, path: true)
+ end
+
+ context 'attribute changed' do
+ before { other_item.attributes[:title] = 'omg new title' }
+ it { is_expected.to be }
+ end
+
+ context 'raw content changed' do
+ before { other_item.content = Nanoc::Int::TextualContent.new('omg new content') }
+ it { is_expected.not_to be }
+ end
+ end
+
+ context 'other dependency' do
+ before do
+ dependency_store.record_dependency(item, other_item, path: true)
+ end
+
+ context 'attribute changed' do
+ before { other_item.attributes[:title] = 'omg new title' }
+ it { is_expected.not_to be }
+ end
+
+ context 'raw content changed' do
+ before { other_item.content = Nanoc::Int::TextualContent.new('omg new content') }
+ it { is_expected.not_to be }
+ end
+ end
+ end
+end
diff --git a/spec/nanoc/base/services/outdatedness_rules_spec.rb b/spec/nanoc/base/services/outdatedness_rules_spec.rb
new file mode 100644
index 0000000..1888cfd
--- /dev/null
+++ b/spec/nanoc/base/services/outdatedness_rules_spec.rb
@@ -0,0 +1,432 @@
+describe Nanoc::Int::OutdatednessRules do
+ describe '#apply' do
+ subject { rule_class.instance.apply(obj, outdatedness_checker) }
+
+ let(:obj) { item_rep }
+
+ let(:outdatedness_checker) do
+ Nanoc::Int::OutdatednessChecker.new(
+ site: site,
+ checksum_store: checksum_store,
+ dependency_store: dependency_store,
+ rule_memory_store: rule_memory_store,
+ action_provider: action_provider,
+ reps: reps,
+ )
+ end
+
+ let(:item_rep) { Nanoc::Int::ItemRep.new(item, :default) }
+ let(:item) { Nanoc::Int::Item.new('stuff', {}, '/foo.md') }
+
+ let(:site) { double(:site) }
+ let(:config) { Nanoc::Int::Configuration.new }
+ let(:code_snippets) { [] }
+ let(:objects) { [config] + code_snippets + [item] }
+
+ let(:action_provider) { double(:action_provider) }
+ let(:reps) { Nanoc::Int::ItemRepRepo.new }
+ let(:dependency_store) { Nanoc::Int::DependencyStore.new(dependency_store_objects) }
+ let(:rule_memory_store) { Nanoc::Int::RuleMemoryStore.new }
+ let(:checksum_store) { Nanoc::Int::ChecksumStore.new(objects: objects) }
+
+ let(:dependency_store_objects) { [item] }
+
+ before do
+ allow(site).to receive(:code_snippets).and_return(code_snippets)
+ allow(site).to receive(:config).and_return(config)
+ end
+
+ context 'CodeSnippetsModified' do
+ let(:rule_class) { Nanoc::Int::OutdatednessRules::CodeSnippetsModified }
+
+ context 'no snippets' do
+ let(:code_snippets) { [] }
+ it { is_expected.not_to be }
+ end
+
+ context 'only non-outdated snippets' do
+ let(:code_snippet) { Nanoc::Int::CodeSnippet.new('asdf', 'lib/foo.md') }
+ let(:code_snippets) { [code_snippet] }
+
+ before { checksum_store.add(code_snippet) }
+
+ it { is_expected.not_to be }
+ end
+
+ context 'only non-outdated snippets' do
+ let(:code_snippet) { Nanoc::Int::CodeSnippet.new('asdf', 'lib/foo.md') }
+ let(:code_snippet_old) { Nanoc::Int::CodeSnippet.new('aaaaaaaa', 'lib/foo.md') }
+ let(:code_snippets) { [code_snippet] }
+
+ before { checksum_store.add(code_snippet_old) }
+
+ it { is_expected.to be }
+ end
+ end
+
+ context 'ConfigurationModified' do
+ let(:rule_class) { Nanoc::Int::OutdatednessRules::ConfigurationModified }
+
+ context 'only non-outdated snippets' do
+ let(:config) { Nanoc::Int::CodeSnippet.new('asdf', 'lib/foo.md') }
+
+ before { checksum_store.add(config) }
+
+ it { is_expected.not_to be }
+ end
+
+ context 'only non-outdated snippets' do
+ let(:config) { Nanoc::Int::Configuration.new }
+ let(:config_old) { Nanoc::Int::Configuration.new(hash: { foo: 125 }) }
+
+ before { checksum_store.add(config_old) }
+
+ it { is_expected.to be }
+ end
+ end
+
+ context 'NotWritten' do
+ let(:rule_class) { Nanoc::Int::OutdatednessRules::NotWritten }
+
+ context 'no path' do
+ before { item_rep.paths = {} }
+
+ it { is_expected.not_to be }
+ end
+
+ context 'path' do
+ let(:path) { 'foo.txt' }
+
+ before { item_rep.raw_paths = { last: path } }
+
+ context 'not written' do
+ it { is_expected.to be }
+ end
+
+ context 'written' do
+ before { File.write(path, 'hello') }
+ it { is_expected.not_to be }
+ end
+ end
+ end
+
+ context 'ContentModified' do
+ let(:rule_class) { Nanoc::Int::OutdatednessRules::ContentModified }
+
+ context 'item' do
+ let(:obj) { item }
+
+ before { reps << item_rep }
+
+ context 'no checksum available' do
+ it { is_expected.to be }
+ end
+
+ context 'checksum available and same' do
+ before { checksum_store.add(item) }
+ it { is_expected.not_to be }
+ end
+
+ context 'checksum available, but content different' do
+ let(:old_item) { Nanoc::Int::Item.new('other stuff!!!!', {}, '/foo.md') }
+ before { checksum_store.add(old_item) }
+ it { is_expected.to be }
+ end
+
+ context 'checksum available, but attributes different' do
+ let(:old_item) { Nanoc::Int::Item.new('stuff', { greeting: 'hi' }, '/foo.md') }
+ before { checksum_store.add(old_item) }
+ it { is_expected.not_to be }
+ end
+ end
+
+ context 'item rep' do
+ let(:obj) { item_rep }
+
+ context 'no checksum available' do
+ it { is_expected.to be }
+ end
+
+ context 'checksum available and same' do
+ before { checksum_store.add(item) }
+ it { is_expected.not_to be }
+ end
+
+ context 'checksum available, but content different' do
+ let(:old_item) { Nanoc::Int::Item.new('other stuff!!!!', {}, '/foo.md') }
+ before { checksum_store.add(old_item) }
+ it { is_expected.to be }
+ end
+
+ context 'checksum available, but attributes different' do
+ let(:old_item) { Nanoc::Int::Item.new('stuff', { greeting: 'hi' }, '/foo.md') }
+ before { checksum_store.add(old_item) }
+ it { is_expected.not_to be }
+ end
+ end
+ end
+
+ context 'AttributesModified' do
+ let(:rule_class) { Nanoc::Int::OutdatednessRules::AttributesModified }
+
+ context 'item' do
+ let(:obj) { item }
+
+ before { reps << item_rep }
+
+ context 'no checksum available' do
+ it { is_expected.to be }
+ end
+
+ context 'checksum available and same' do
+ before { checksum_store.add(item) }
+ it { is_expected.not_to be }
+ end
+
+ context 'checksum available, but content different' do
+ let(:old_item) { Nanoc::Int::Item.new('other stuff!!!!', {}, '/foo.md') }
+ before { checksum_store.add(old_item) }
+ it { is_expected.not_to be }
+ end
+
+ context 'checksum available, but attributes different' do
+ let(:old_item) { Nanoc::Int::Item.new('stuff', { greeting: 'hi' }, '/foo.md') }
+ before { checksum_store.add(old_item) }
+ it { is_expected.to be }
+ end
+ end
+
+ context 'item rep' do
+ let(:obj) { item_rep }
+
+ context 'no checksum available' do
+ it { is_expected.to be }
+ end
+
+ context 'checksum available and same' do
+ before { checksum_store.add(item) }
+ it { is_expected.not_to be }
+ end
+
+ context 'checksum available, but content different' do
+ let(:old_item) { Nanoc::Int::Item.new('other stuff!!!!', {}, '/foo.md') }
+ before { checksum_store.add(old_item) }
+ it { is_expected.not_to be }
+ end
+
+ context 'checksum available, but attributes different' do
+ let(:old_item) { Nanoc::Int::Item.new('stuff', { greeting: 'hi' }, '/foo.md') }
+ before { checksum_store.add(old_item) }
+ it { is_expected.to be }
+ end
+ end
+ end
+
+ context 'RulesModified' do
+ let(:rule_class) { Nanoc::Int::OutdatednessRules::RulesModified }
+
+ let(:old_mem) do
+ Nanoc::Int::RuleMemory.new(item_rep).tap do |mem|
+ mem.add_filter(:erb, {})
+ end
+ end
+
+ before do
+ rule_memory_store[item_rep] = old_mem.serialize
+ allow(action_provider).to receive(:memory_for).with(item_rep).and_return(new_mem)
+ end
+
+ context 'memory is the same' do
+ let(:new_mem) { old_mem }
+ it { is_expected.not_to be }
+ end
+
+ context 'memory is different' do
+ let(:new_mem) do
+ Nanoc::Int::RuleMemory.new(item_rep).tap do |mem|
+ mem.add_filter(:erb, {})
+ mem.add_filter(:donkey, {})
+ end
+ end
+
+ it { is_expected.to be }
+ end
+ end
+
+ context 'PathsModified' do
+ let(:rule_class) { Nanoc::Int::OutdatednessRules::PathsModified }
+
+ before do
+ allow(action_provider).to receive(:memory_for).with(item_rep).and_return(new_mem)
+ end
+
+ context 'old mem does not exist' do
+ let(:new_mem) do
+ Nanoc::Int::RuleMemory.new(item_rep).tap do |mem|
+ mem.add_snapshot(:donkey, '/foo.md')
+ mem.add_filter(:asdf, {})
+ end
+ end
+
+ it { is_expected.to be }
+ end
+
+ context 'old mem exists' do
+ let(:old_mem) do
+ Nanoc::Int::RuleMemory.new(item_rep).tap do |mem|
+ mem.add_filter(:erb, {})
+ mem.add_snapshot(:donkey, '/foo.md')
+ end
+ end
+
+ before do
+ rule_memory_store[item_rep] = old_mem.serialize
+ end
+
+ context 'paths in memory are the same' do
+ let(:new_mem) do
+ Nanoc::Int::RuleMemory.new(item_rep).tap do |mem|
+ mem.add_snapshot(:donkey, '/foo.md')
+ mem.add_filter(:asdf, {})
+ end
+ end
+
+ it { is_expected.not_to be }
+ end
+
+ context 'paths in memory are different' do
+ let(:new_mem) do
+ Nanoc::Int::RuleMemory.new(item_rep).tap do |mem|
+ mem.add_filter(:erb, {})
+ mem.add_snapshot(:donkey, '/foo.md')
+ mem.add_filter(:donkey, {})
+ mem.add_snapshot(:giraffe, '/bar.md')
+ end
+ end
+
+ it { is_expected.to be }
+ end
+ end
+ end
+
+ describe '#{Content,Attributes}Modified' do
+ subject do
+ # TODO: remove negation
+ [
+ Nanoc::Int::OutdatednessRules::ContentModified,
+ Nanoc::Int::OutdatednessRules::AttributesModified,
+ ].map { |c| !c.instance.apply(new_obj, outdatedness_checker) }
+ end
+
+ let(:stored_obj) { raise 'override me' }
+ let(:new_obj) { raise 'override me' }
+
+ shared_examples 'a document' do
+ let(:stored_obj) { klass.new('a', {}, '/foo.md') }
+ let(:new_obj) { stored_obj }
+
+ context 'no checksum data' do
+ context 'not stored' do
+ it { is_expected.to eql([false, false]) }
+ end
+
+ context 'stored' do
+ before { checksum_store.add(stored_obj) }
+
+ context 'but content changed afterwards' do
+ let(:new_obj) { klass.new('aaaaaaaa', {}, '/foo.md') }
+ it { is_expected.to eql([false, true]) }
+ end
+
+ context 'but attributes changed afterwards' do
+ let(:new_obj) { klass.new('a', { animal: 'donkey' }, '/foo.md') }
+ it { is_expected.to eql([true, false]) }
+ end
+
+ context 'and unchanged' do
+ it { is_expected.to eql([true, true]) }
+ end
+ end
+ end
+
+ context 'checksum_data' do
+ let(:stored_obj) { klass.new('a', {}, '/foo.md', checksum_data: 'cs-data') }
+ let(:new_obj) { stored_obj }
+
+ context 'not stored' do
+ it { is_expected.to eql([false, false]) }
+ end
+
+ context 'stored' do
+ before { checksum_store.add(stored_obj) }
+
+ context 'but checksum data afterwards' do
+ let(:new_obj) { klass.new('a', {}, '/foo.md', checksum_data: 'cs-data-new') }
+ it { is_expected.to eql([false, false]) }
+ end
+
+ context 'and unchanged' do
+ it { is_expected.to eql([true, true]) }
+ end
+ end
+ end
+
+ context 'content_checksum_data' do
+ let(:stored_obj) { klass.new('a', {}, '/foo.md', content_checksum_data: 'cs-data') }
+ let(:new_obj) { stored_obj }
+
+ context 'not stored' do
+ it { is_expected.to eql([false, false]) }
+ end
+
+ context 'stored' do
+ before { checksum_store.add(stored_obj) }
+
+ context 'but checksum data afterwards' do
+ let(:new_obj) { klass.new('a', {}, '/foo.md', content_checksum_data: 'cs-data-new') }
+ it { is_expected.to eql([false, true]) }
+ end
+
+ context 'and unchanged' do
+ it { is_expected.to eql([true, true]) }
+ end
+ end
+ end
+
+ context 'attributes_checksum_data' do
+ let(:stored_obj) { klass.new('a', {}, '/foo.md', attributes_checksum_data: 'cs-data') }
+ let(:new_obj) { stored_obj }
+
+ context 'not stored' do
+ it { is_expected.to eql([false, false]) }
+ end
+
+ context 'stored' do
+ before { checksum_store.add(stored_obj) }
+
+ context 'but checksum data afterwards' do
+ let(:new_obj) { klass.new('a', {}, '/foo.md', attributes_checksum_data: 'cs-data-new') }
+ it { is_expected.to eql([true, false]) }
+ end
+
+ context 'and unchanged' do
+ it { is_expected.to eql([true, true]) }
+ end
+ end
+ end
+ end
+
+ context 'item' do
+ let(:klass) { Nanoc::Int::Item }
+ it_behaves_like 'a document'
+ end
+
+ context 'layout' do
+ let(:klass) { Nanoc::Int::Layout }
+ it_behaves_like 'a document'
+ end
+
+ # …
+ end
+ end
+end
diff --git a/spec/nanoc/base/services/pruner_spec.rb b/spec/nanoc/base/services/pruner_spec.rb
new file mode 100644
index 0000000..c86d38e
--- /dev/null
+++ b/spec/nanoc/base/services/pruner_spec.rb
@@ -0,0 +1,105 @@
+describe Nanoc::Pruner do
+ subject(:pruner) { described_class.new(config, reps, dry_run: dry_run, exclude: exclude) }
+
+ let(:config) { Nanoc::Int::Configuration.new({}) }
+ let(:dry_run) { false }
+ let(:exclude) { [] }
+
+ let(:reps) do
+ Nanoc::Int::ItemRepRepo.new.tap do |reps|
+ reps << Nanoc::Int::ItemRep.new(item, :default).tap do |rep|
+ rep.raw_paths = { last: 'output/asdf.html' }
+ end
+
+ reps << Nanoc::Int::ItemRep.new(item, :text).tap do |rep|
+ rep.raw_paths = { last: 'output/asdf.txt' }
+ end
+ end
+ end
+
+ let(:item) { Nanoc::Int::Item.new('asdf', {}, '/a.md') }
+
+ it 'is accessible through Nanoc::Extra::Pruner' do
+ expect(Nanoc::Extra::Pruner).to equal(Nanoc::Pruner)
+ end
+
+ describe '#files_and_dirs_in' do
+ subject { pruner.files_and_dirs_in('output/') }
+
+ before do
+ FileUtils.mkdir_p('output/projects')
+ FileUtils.mkdir_p('output/.git')
+
+ File.write('output/asdf.html', '<p>text</p>')
+ File.write('output/.htaccess', 'secret stuff here')
+ File.write('output/projects/nanoc.html', '<p>Nanoc is v cool!!</p>')
+ File.write('output/.git/HEAD', 'some content here')
+ end
+
+ context 'nothing excluded' do
+ let(:exclude) { [] }
+
+ it 'returns all files' do
+ files = [
+ 'output/asdf.html',
+ 'output/.htaccess',
+ 'output/projects/nanoc.html',
+ 'output/.git/HEAD',
+ ]
+ expect(subject[0]).to match_array(files)
+ end
+
+ it 'returns all directories' do
+ dirs = [
+ 'output/',
+ 'output/projects',
+ 'output/.git',
+ ]
+ expect(subject[1]).to match_array(dirs)
+ end
+ end
+
+ context 'directory (.git) excluded' do
+ let(:exclude) { ['.git'] }
+
+ it 'returns all files' do
+ files = [
+ 'output/asdf.html',
+ 'output/.htaccess',
+ 'output/projects/nanoc.html',
+ ]
+ expect(subject[0]).to match_array(files)
+ end
+
+ it 'returns all directories' do
+ dirs = [
+ 'output/',
+ 'output/projects',
+ ]
+ expect(subject[1]).to match_array(dirs)
+ end
+ end
+
+ context 'file (.htaccess) excluded' do
+ let(:exclude) { ['.htaccess'] }
+
+ it 'returns all files' do
+ files = [
+ 'output/asdf.html',
+ 'output/projects/nanoc.html',
+ 'output/.git/HEAD',
+ ]
+ expect(subject[0]).to match_array(files)
+ end
+
+ it 'returns all directories' do
+ dirs = [
+ 'output/',
+ 'output/projects',
+ 'output/.git',
+ ]
+ expect(subject[1]).to match_array(dirs)
+ end
+ end
+ end
+end
diff --git a/spec/nanoc/base/services/temp_filename_factory_spec.rb b/spec/nanoc/base/services/temp_filename_factory_spec.rb
new file mode 100644
index 0000000..6284584
--- /dev/null
+++ b/spec/nanoc/base/services/temp_filename_factory_spec.rb
@@ -0,0 +1,87 @@
+describe Nanoc::Int::TempFilenameFactory do
+ subject(:factory) { described_class.new }
+
+ let(:prefix) { 'foo' }
+
+ describe '#create' do
+ it 'creates unique paths' do
+ path_a = subject.create(prefix)
+ path_b = subject.create(prefix)
+
+ expect(path_a).not_to eq(path_b)
+ end
+
+ it 'returns absolute paths' do
+ path = subject.create(prefix)
+
+ expect(path).to match(/\A\//)
+ end
+
+ it 'creates the containing directory' do
+ expect(Dir[subject.root_dir + '/**/*']).to be_empty
+
+ path = subject.create(prefix)
+
+ expect(File.directory?(File.dirname(path))).to be(true)
+ end
+
+ it 'reuses the same path after cleanup' do
+ path_a = subject.create(prefix)
+
+ subject.cleanup(prefix)
+
+ path_b = subject.create(prefix)
+ expect(path_a).to eq(path_b)
+ end
+
+ it 'does not create the file' do
+ path = subject.create(prefix)
+ expect(File.file?(path)).not_to be(true)
+ end
+ end
+
+ describe '#cleanup' do
+ subject { factory.cleanup(prefix) }
+
+ let!(:path) { factory.create(prefix) }
+
+ before { File.write(path, 'hello') }
+
+ def files
+ Dir[factory.root_dir + '/**/*'].select { |fn| File.file?(fn) }
+ end
+
+ it 'removes generated files' do
+ expect { subject }.to change { files }.from([path]).to([])
+ end
+
+ context 'files with other prefixes exist' do
+ before do
+ factory.create('donkey')
+ end
+
+ it 'does not delete root dir' do
+ expect(File.directory?(factory.root_dir)).to be(true)
+ expect { subject }.not_to change { File.directory?(factory.root_dir) }
+ end
+ end
+
+ context 'no files with other prefixes exist' do
+ it 'deletes root dir' do
+ expect { subject }.to change { File.directory?(factory.root_dir) }.from(true).to(false)
+ end
+ end
+ end
+
+ describe 'other instance' do
+ let(:other_instance) do
+ Nanoc::Int::TempFilenameFactory.new
+ end
+
+ it 'creates unique paths across instances' do
+ path_a = subject.create(prefix)
+ path_b = other_instance.create(prefix)
+ expect(path_a).not_to eq(path_b)
+ end
+ end
+end
diff --git a/spec/nanoc/base/views/config_view_spec.rb b/spec/nanoc/base/views/config_view_spec.rb
new file mode 100644
index 0000000..bf2beea
--- /dev/null
+++ b/spec/nanoc/base/views/config_view_spec.rb
@@ -0,0 +1,96 @@
+describe Nanoc::ConfigView do
+ let(:config) do
+ Nanoc::Int::Configuration.new(hash: hash)
+ end
+
+ let(:hash) { { amount: 9000, animal: 'donkey' } }
+
+ let(:view) { described_class.new(config, nil) }
+
+ describe '#frozen?' do
+ subject { view.frozen? }
+
+ context 'non-frozen config' do
+ it { is_expected.to be(false) }
+ end
+
+ context 'frozen config' do
+ before { config.freeze }
+ it { is_expected.to be(true) }
+ end
+ end
+
+ describe '#[]' do
+ subject { view[key] }
+
+ context 'with existant key' do
+ let(:key) { :animal }
+ it { is_expected.to eql('donkey') }
+ end
+
+ context 'with non-existant key' do
+ let(:key) { :weapon }
+ it { is_expected.to eql(nil) }
+ end
+ end
+
+ describe '#fetch' do
+ context 'with existant key' do
+ let(:key) { :animal }
+
+ subject { view.fetch(key) }
+
+ it { is_expected.to eql('donkey') }
+ end
+
+ context 'with non-existant key' do
+ let(:key) { :weapon }
+
+ context 'with fallback' do
+ subject { view.fetch(key, 'nothing sorry') }
+ it { is_expected.to eql('nothing sorry') }
+ end
+
+ context 'with block' do
+ subject { view.fetch(key) { 'nothing sorry' } }
+ it { is_expected.to eql('nothing sorry') }
+ end
+
+ context 'with no fallback and no block' do
+ subject { view.fetch(key) }
+
+ it 'raises' do
+ expect { subject }.to raise_error(KeyError)
+ end
+ end
+ end
+ end
+
+ describe '#key?' do
+ subject { view.key?(key) }
+
+ context 'with existant key' do
+ let(:key) { :animal }
+ it { is_expected.to eql(true) }
+ end
+
+ context 'with non-existant key' do
+ let(:key) { :weapon }
+ it { is_expected.to eql(false) }
+ end
+ end
+
+ describe '#each' do
+ example do
+ res = []
+ view.each { |k, v| res << [k, v] }
+
+ expect(res).to eql([[:amount, 9000], [:animal, 'donkey']])
+ end
+ end
+
+ describe '#inspect' do
+ subject { view.inspect }
+ it { is_expected.to eql('<Nanoc::ConfigView>') }
+ end
+end
diff --git a/spec/nanoc/base/views/document_view_spec.rb b/spec/nanoc/base/views/document_view_spec.rb
new file mode 100644
index 0000000..22c024f
--- /dev/null
+++ b/spec/nanoc/base/views/document_view_spec.rb
@@ -0,0 +1,332 @@
+shared_examples 'a document view' do
+ let(:view) { described_class.new(document, view_context) }
+
+ let(:view_context) do
+ Nanoc::ViewContext.new(
+ reps: double(:reps),
+ items: double(:items),
+ dependency_tracker: dependency_tracker,
+ compilation_context: double(:compilation_context),
+ )
+ end
+
+ let(:dependency_tracker) { Nanoc::Int::DependencyTracker.new(dependency_store) }
+ let(:dependency_store) { Nanoc::Int::DependencyStore.new([]) }
+ let(:base_item) { Nanoc::Int::Item.new('base', {}, '/base.md') }
+
+ before do
+ dependency_tracker.enter(base_item)
+ end
+
+ describe '#frozen?' do
+ let(:document) { entity_class.new('content', {}, '/asdf/') }
+
+ subject { view.frozen? }
+
+ context 'non-frozen document' do
+ it { is_expected.to be(false) }
+ end
+
+ context 'frozen document' do
+ before { document.freeze }
+ it { is_expected.to be(true) }
+ end
+ end
+
+ describe '#== and #eql?' do
+ let(:document) { entity_class.new('content', {}, '/asdf/') }
+
+ context 'comparing with document with same identifier' do
+ let(:other) { entity_class.new('content', {}, '/asdf/') }
+
+ it 'is ==' do
+ expect(view).to eq(other)
+ end
+
+ it 'is not eql?' do
+ expect(view).not_to eql(other)
+ end
+ end
+
+ context 'comparing with document with different identifier' do
+ let(:other) { entity_class.new('content', {}, '/fdsa/') }
+
+ it 'is not ==' do
+ expect(view).not_to eq(other)
+ end
+
+ it 'is not eql?' do
+ expect(view).not_to eql(other)
+ end
+ end
+
+ context 'comparing with document view with same identifier' do
+ let(:other) { other_view_class.new(entity_class.new('content', {}, '/asdf/'), nil) }
+
+ it 'is ==' do
+ expect(view).to eq(other)
+ end
+
+ it 'is not eql?' do
+ expect(view).not_to eql(other)
+ end
+ end
+
+ context 'comparing with document view with different identifier' do
+ let(:other) { other_view_class.new(entity_class.new('content', {}, '/fdsa/'), nil) }
+
+ it 'is not ==' do
+ expect(view).not_to eq(other)
+ end
+
+ it 'is not eql?' do
+ expect(view).not_to eql(other)
+ end
+ end
+
+ context 'comparing with other object' do
+ let(:other) { nil }
+
+ it 'is not ==' do
+ expect(view).not_to eq(other)
+ end
+
+ it 'is not eql?' do
+ expect(view).not_to eql(other)
+ end
+ end
+ end
+
+ describe '#[]' do
+ let(:document) { entity_class.new('stuff', { animal: 'donkey' }, '/foo/') }
+
+ subject { view[key] }
+
+ context 'with existant key' do
+ let(:key) { :animal }
+
+ it { is_expected.to eql('donkey') }
+
+ it 'creates a dependency' do
+ expect { subject }.to change { dependency_store.objects_causing_outdatedness_of(base_item) }.from([]).to([document])
+ end
+
+ it 'creates a dependency with the right props' do
+ subject
+ dep = dependency_store.dependencies_causing_outdatedness_of(base_item)[0]
+
+ expect(dep.props.attributes?).to eq(true)
+
+ expect(dep.props.raw_content?).to eq(false)
+ expect(dep.props.compiled_content?).to eq(false)
+ expect(dep.props.path?).to eq(false)
+ end
+ end
+
+ context 'with non-existant key' do
+ let(:key) { :weapon }
+
+ it { is_expected.to eql(nil) }
+
+ it 'creates a dependency' do
+ expect { subject }.to change { dependency_store.objects_causing_outdatedness_of(base_item) }.from([]).to([document])
+ end
+
+ it 'creates a dependency with the right props' do
+ subject
+ dep = dependency_store.dependencies_causing_outdatedness_of(base_item)[0]
+
+ expect(dep.props.attributes?).to eq(true)
+
+ expect(dep.props.raw_content?).to eq(false)
+ expect(dep.props.compiled_content?).to eq(false)
+ expect(dep.props.path?).to eq(false)
+ end
+ end
+ end
+
+ describe '#attributes' do
+ let(:document) { entity_class.new('stuff', { animal: 'donkey' }, '/foo/') }
+
+ subject { view.attributes }
+
+ it 'creates a dependency' do
+ expect { subject }.to change { dependency_store.objects_causing_outdatedness_of(base_item) }.from([]).to([document])
+ end
+
+ it 'creates a dependency with the right props' do
+ subject
+ dep = dependency_store.dependencies_causing_outdatedness_of(base_item)[0]
+
+ expect(dep.props.attributes?).to eq(true)
+
+ expect(dep.props.raw_content?).to eq(false)
+ expect(dep.props.compiled_content?).to eq(false)
+ expect(dep.props.path?).to eq(false)
+ end
+
+ it 'returns attributes' do
+ expect(subject).to eql(animal: 'donkey')
+ end
+ end
+
+ describe '#fetch' do
+ let(:document) { entity_class.new('stuff', { animal: 'donkey' }, '/foo/') }
+
+ context 'with existant key' do
+ let(:key) { :animal }
+
+ subject { view.fetch(key) }
+
+ it { is_expected.to eql('donkey') }
+
+ it 'creates a dependency' do
+ expect { subject }.to change { dependency_store.objects_causing_outdatedness_of(base_item) }.from([]).to([document])
+ end
+
+ it 'creates a dependency with the right props' do
+ subject
+ dep = dependency_store.dependencies_causing_outdatedness_of(base_item)[0]
+
+ expect(dep.props.attributes?).to eq(true)
+
+ expect(dep.props.raw_content?).to eq(false)
+ expect(dep.props.compiled_content?).to eq(false)
+ expect(dep.props.path?).to eq(false)
+ end
+ end
+
+ context 'with non-existant key' do
+ let(:key) { :weapon }
+
+ context 'with fallback' do
+ subject { view.fetch(key, 'nothing sorry') }
+
+ it { is_expected.to eql('nothing sorry') }
+
+ it 'creates a dependency' do
+ expect { subject }.to change { dependency_store.objects_causing_outdatedness_of(base_item) }.from([]).to([document])
+ end
+
+ it 'creates a dependency with the right props' do
+ subject
+ dep = dependency_store.dependencies_causing_outdatedness_of(base_item)[0]
+
+ expect(dep.props.attributes?).to eq(true)
+
+ expect(dep.props.raw_content?).to eq(false)
+ expect(dep.props.compiled_content?).to eq(false)
+ expect(dep.props.path?).to eq(false)
+ end
+ end
+
+ context 'with block' do
+ subject { view.fetch(key) { 'nothing sorry' } }
+
+ it { is_expected.to eql('nothing sorry') }
+
+ it 'creates a dependency' do
+ expect { subject }.to change { dependency_store.objects_causing_outdatedness_of(base_item) }.from([]).to([document])
+ end
+
+ it 'creates a dependency with the right props' do
+ subject
+ dep = dependency_store.dependencies_causing_outdatedness_of(base_item)[0]
+
+ expect(dep.props.attributes?).to eq(true)
+
+ expect(dep.props.raw_content?).to eq(false)
+ expect(dep.props.compiled_content?).to eq(false)
+ expect(dep.props.path?).to eq(false)
+ end
+ end
+
+ context 'with no fallback and no block' do
+ subject { view.fetch(key) }
+
+ it 'raises' do
+ expect { subject }.to raise_error(KeyError)
+ end
+ end
+ end
+ end
+
+ describe '#key?' do
+ let(:document) { entity_class.new('stuff', { animal: 'donkey' }, '/foo/') }
+
+ subject { view.key?(key) }
+
+ context 'with existant key' do
+ let(:key) { :animal }
+
+ it { is_expected.to eql(true) }
+
+ it 'creates a dependency' do
+ expect { subject }.to change { dependency_store.objects_causing_outdatedness_of(base_item) }.from([]).to([document])
+ end
+
+ it 'creates a dependency with the right props' do
+ subject
+ dep = dependency_store.dependencies_causing_outdatedness_of(base_item)[0]
+
+ expect(dep.props.attributes?).to eq(true)
+
+ expect(dep.props.raw_content?).to eq(false)
+ expect(dep.props.compiled_content?).to eq(false)
+ expect(dep.props.path?).to eq(false)
+ end
+ end
+
+ context 'with non-existant key' do
+ let(:key) { :weapon }
+
+ it { is_expected.to eql(false) }
+
+ it 'creates a dependency' do
+ expect { subject }.to change { dependency_store.objects_causing_outdatedness_of(base_item) }.from([]).to([document])
+ end
+
+ it 'creates a dependency with the right props' do
+ subject
+ dep = dependency_store.dependencies_causing_outdatedness_of(base_item)[0]
+
+ expect(dep.props.attributes?).to eq(true)
+
+ expect(dep.props.raw_content?).to eq(false)
+ expect(dep.props.compiled_content?).to eq(false)
+ expect(dep.props.path?).to eq(false)
+ end
+ end
+ end
+
+ describe '#hash' do
+ let(:document) { double(:document, identifier: '/foo/') }
+
+ subject { view.hash }
+
+ it { should == described_class.hash ^ '/foo/'.hash }
+ end
+
+ describe '#raw_content' do
+ let(:document) { entity_class.new('stuff', { animal: 'donkey' }, '/foo/') }
+
+ subject { view.raw_content }
+
+ it { is_expected.to eql('stuff') }
+
+ it 'creates a dependency' do
+ expect { subject }.to change { dependency_store.objects_causing_outdatedness_of(base_item) }.from([]).to([document])
+ end
+
+ it 'creates a dependency with the right props' do
+ subject
+ dep = dependency_store.dependencies_causing_outdatedness_of(base_item)[0]
+
+ expect(dep.props.raw_content?).to eq(true)
+
+ expect(dep.props.attributes?).to eq(false)
+ expect(dep.props.compiled_content?).to eq(false)
+ expect(dep.props.path?).to eq(false)
+ end
+ end
+end
diff --git a/spec/nanoc/base/views/identifiable_collection_view_spec.rb b/spec/nanoc/base/views/identifiable_collection_view_spec.rb
new file mode 100644
index 0000000..cb7cd5a
--- /dev/null
+++ b/spec/nanoc/base/views/identifiable_collection_view_spec.rb
@@ -0,0 +1,190 @@
+# Needs :view_class
+shared_examples 'an identifiable collection' do
+ let(:view) { described_class.new(wrapped, view_context) }
+
+ let(:view_context) { double(:view_context) }
+
+ let(:config) do
+ { string_pattern_type: 'glob' }
+ end
+
+ describe '#frozen?' do
+ let(:wrapped) do
+ Nanoc::Int::IdentifiableCollection.new(config).tap do |arr|
+ arr << double(:identifiable, identifier: Nanoc::Identifier.new('/foo'))
+ arr << double(:identifiable, identifier: Nanoc::Identifier.new('/bar'))
+ end
+ end
+
+ subject { view.frozen? }
+
+ context 'non-frozen collection' do
+ it { is_expected.to be(false) }
+ end
+
+ context 'frozen collection' do
+ before do
+ wrapped.each { |o| expect(o).to receive(:freeze) }
+ wrapped.freeze
+ end
+
+ it { is_expected.to be(true) }
+ end
+ end
+
+ describe '#unwrap' do
+ let(:wrapped) do
+ Nanoc::Int::IdentifiableCollection.new(config).tap do |arr|
+ arr << double(:identifiable, identifier: Nanoc::Identifier.new('/foo'))
+ arr << double(:identifiable, identifier: Nanoc::Identifier.new('/bar'))
+ arr << double(:identifiable, identifier: Nanoc::Identifier.new('/baz'))
+ end
+ end
+
+ subject { view.unwrap }
+
+ it { should equal(wrapped) }
+ end
+
+ describe '#each' do
+ let(:wrapped) do
+ Nanoc::Int::IdentifiableCollection.new(config).tap do |arr|
+ arr << double(:identifiable, identifier: Nanoc::Identifier.new('/foo'))
+ arr << double(:identifiable, identifier: Nanoc::Identifier.new('/bar'))
+ arr << double(:identifiable, identifier: Nanoc::Identifier.new('/baz'))
+ end
+ end
+
+ it 'returns self' do
+ expect(view.each { |_i| }).to equal(view)
+ end
+
+ it 'yields elements with the right context' do
+ view.each { |v| expect(v._context).to equal(view_context) }
+ end
+ end
+
+ describe '#size' do
+ let(:wrapped) do
+ Nanoc::Int::IdentifiableCollection.new(config).tap do |arr|
+ arr << double(:identifiable, identifier: Nanoc::Identifier.new('/foo'))
+ arr << double(:identifiable, identifier: Nanoc::Identifier.new('/bar'))
+ arr << double(:identifiable, identifier: Nanoc::Identifier.new('/baz'))
+ end
+ end
+
+ subject { view.size }
+
+ it { should == 3 }
+ end
+
+ describe '#[]' do
+ let(:page_object) do
+ double(:identifiable, identifier: Nanoc::Identifier.new('/page.erb'))
+ end
+
+ let(:home_object) do
+ double(:identifiable, identifier: Nanoc::Identifier.new('/home.erb'))
+ end
+
+ let(:wrapped) do
+ Nanoc::Int::IdentifiableCollection.new(config).tap do |arr|
+ arr << page_object
+ arr << home_object
+ end
+ end
+
+ subject { view[arg] }
+
+ context 'no objects found' do
+ let(:arg) { '/donkey.*' }
+ it { is_expected.to equal(nil) }
+ end
+
+ context 'string' do
+ let(:arg) { '/home.erb' }
+
+ it 'returns wrapped object' do
+ expect(subject.class).to equal(view_class)
+ expect(subject.unwrap).to equal(home_object)
+ end
+
+ it 'returns objects with right context' do
+ expect(subject._context).to equal(view_context)
+ end
+ end
+
+ context 'identifier' do
+ let(:arg) { Nanoc::Identifier.new('/home.erb') }
+
+ it 'returns wrapped object' do
+ expect(subject.class).to equal(view_class)
+ expect(subject.unwrap).to equal(home_object)
+ end
+ end
+
+ context 'glob' do
+ let(:arg) { '/home.*' }
+
+ context 'globs not enabled' do
+ let(:config) { { string_pattern_type: 'legacy' } }
+
+ it 'returns nil' do
+ expect(subject).to be_nil
+ end
+ end
+
+ context 'globs enabled' do
+ it 'returns wrapped object' do
+ expect(subject.class).to equal(view_class)
+ expect(subject.unwrap).to equal(home_object)
+ end
+ end
+ end
+
+ context 'regex' do
+ let(:arg) { %r{\A/home} }
+
+ it 'returns wrapped object' do
+ expect(subject.class).to equal(view_class)
+ expect(subject.unwrap).to equal(home_object)
+ end
+ end
+ end
+
+ describe '#find_all' do
+ let(:wrapped) do
+ Nanoc::Int::IdentifiableCollection.new(config).tap do |arr|
+ arr << double(:identifiable, identifier: Nanoc::Identifier.new('/about.css'))
+ arr << double(:identifiable, identifier: Nanoc::Identifier.new('/about.md'))
+ arr << double(:identifiable, identifier: Nanoc::Identifier.new('/style.css'))
+ end
+ end
+
+ subject { view.find_all(arg) }
+
+ context 'with string' do
+ let(:arg) { '/*.css' }
+
+ it 'contains views' do
+ expect(subject.size).to eql(2)
+ about_css = subject.find { |iv| iv.identifier == '/about.css' }
+ style_css = subject.find { |iv| iv.identifier == '/style.css' }
+ expect(about_css.class).to equal(view_class)
+ expect(style_css.class).to equal(view_class)
+ end
+ end
+
+ context 'with regex' do
+ let(:arg) { %r{\.css\z} }
+
+ it 'contains views' do
+ expect(subject.size).to eql(2)
+ about_css = subject.find { |iv| iv.identifier == '/about.css' }
+ style_css = subject.find { |iv| iv.identifier == '/style.css' }
+ expect(about_css.class).to equal(view_class)
+ expect(style_css.class).to equal(view_class)
+ end
+ end
+ end
+end
diff --git a/spec/nanoc/base/views/item_collection_with_reps_view_spec.rb b/spec/nanoc/base/views/item_collection_with_reps_view_spec.rb
new file mode 100644
index 0000000..0121fb4
--- /dev/null
+++ b/spec/nanoc/base/views/item_collection_with_reps_view_spec.rb
@@ -0,0 +1,18 @@
+describe Nanoc::ItemCollectionWithRepsView do
+ let(:view_class) { Nanoc::ItemWithRepsView }
+ it_behaves_like 'an identifiable collection'
+
+ describe '#inspect' do
+ let(:wrapped) do
+ Nanoc::Int::IdentifiableCollection.new(config)
+ end
+
+ let(:view) { described_class.new(wrapped, view_context) }
+ let(:view_context) { double(:view_context) }
+ let(:config) { { string_pattern_type: 'glob' } }
+
+ subject { view.inspect }
+
+ it { is_expected.to eql('<Nanoc::ItemCollectionWithRepsView>') }
+ end
+end
diff --git a/spec/nanoc/base/views/item_collection_without_reps_view_spec.rb b/spec/nanoc/base/views/item_collection_without_reps_view_spec.rb
new file mode 100644
index 0000000..0a54a09
--- /dev/null
+++ b/spec/nanoc/base/views/item_collection_without_reps_view_spec.rb
@@ -0,0 +1,18 @@
+describe Nanoc::ItemCollectionWithoutRepsView do
+ let(:view_class) { Nanoc::ItemWithoutRepsView }
+ it_behaves_like 'an identifiable collection'
+
+ describe '#inspect' do
+ let(:wrapped) do
+ Nanoc::Int::IdentifiableCollection.new(config)
+ end
+
+ let(:view) { described_class.new(wrapped, view_context) }
+ let(:view_context) { double(:view_context) }
+ let(:config) { { string_pattern_type: 'glob' } }
+
+ subject { view.inspect }
+
+ it { is_expected.to eql('<Nanoc::ItemCollectionWithoutRepsView>') }
+ end
+end
diff --git a/spec/nanoc/base/views/item_rep_collection_view_spec.rb b/spec/nanoc/base/views/item_rep_collection_view_spec.rb
new file mode 100644
index 0000000..6c6df32
--- /dev/null
+++ b/spec/nanoc/base/views/item_rep_collection_view_spec.rb
@@ -0,0 +1,143 @@
+shared_examples 'an item rep collection view' do
+ let(:view) { described_class.new(wrapped, view_context) }
+
+ let(:view_context) { double(:view_context) }
+
+ let(:wrapped) do
+ [
+ double(:item_rep, name: :foo),
+ double(:item_rep, name: :bar),
+ double(:item_rep, name: :baz),
+ ]
+ end
+
+ describe '#unwrap' do
+ subject { view.unwrap }
+
+ it { should equal(wrapped) }
+ end
+
+ describe '#frozen?' do
+ subject { view.frozen? }
+
+ context 'non-frozen collection' do
+ it { is_expected.to be(false) }
+ end
+
+ context 'frozen collection' do
+ before { wrapped.freeze }
+ it { is_expected.to be(true) }
+ end
+ end
+
+ describe '#each' do
+ it 'yields' do
+ actual = [].tap { |res| view.each { |v| res << v } }
+ expect(actual.size).to eq(3)
+ end
+
+ it 'returns self' do
+ expect(view.each { |_i| }).to equal(view)
+ end
+
+ it 'yields elements with the right context' do
+ view.each { |v| expect(v._context).to equal(view_context) }
+ end
+ end
+
+ describe '#size' do
+ subject { view.size }
+
+ it { should == 3 }
+ end
+
+ describe '#to_ary' do
+ subject { view.to_ary }
+
+ it 'returns an array of item rep views' do
+ expect(subject.class).to eq(Array)
+ expect(subject.size).to eq(3)
+ expect(subject[0].class).to eql(expected_view_class)
+ expect(subject[0].name).to eql(:foo)
+ end
+
+ it 'returns an array with correct contexts' do
+ expect(subject[0]._context).to equal(view_context)
+ end
+ end
+
+ describe '#[]' do
+ subject { view[name] }
+
+ context 'when not found' do
+ let(:name) { :donkey }
+
+ it { should be_nil }
+ end
+
+ context 'when found' do
+ let(:name) { :foo }
+
+ it 'returns a view' do
+ expect(subject.class).to eq(expected_view_class)
+ expect(subject.name).to eq(:foo)
+ end
+
+ it 'returns a view with the correct context' do
+ expect(subject._context).to equal(view_context)
+ end
+ end
+
+ context 'when given a string' do
+ let(:name) { 'foo' }
+
+ it 'raises' do
+ expect { subject }.to raise_error(ArgumentError, 'expected ItemRepCollectionView#[] to be called with a symbol')
+ end
+ end
+
+ context 'when given a number' do
+ let(:name) { 0 }
+
+ it 'raises' do
+ expect { subject }.to raise_error(ArgumentError, 'expected ItemRepCollectionView#[] to be called with a symbol (you likely want `.reps[:default]` rather than `.reps[0]`)')
+ end
+ end
+ end
+
+ describe '#fetch' do
+ subject { view.fetch(name) }
+
+ context 'when not found' do
+ let(:name) { :donkey }
+
+ it 'raises' do
+ expect { subject }.to raise_error(Nanoc::ItemRepCollectionView::NoSuchItemRepError)
+ end
+ end
+
+ context 'when found' do
+ let(:name) { :foo }
+
+ it 'returns a view' do
+ expect(subject.class).to eq(expected_view_class)
+ expect(subject.name).to eq(:foo)
+ end
+
+ it 'returns a view with the correct context' do
+ expect(subject._context).to equal(view_context)
+ end
+ end
+ end
+
+ describe '#inspect' do
+ subject { view.inspect }
+
+ it { is_expected.to eql('<' + described_class.name + '>') }
+ end
+end
+
+describe Nanoc::ItemRepCollectionView do
+ it_behaves_like 'an item rep collection view'
+ let(:expected_view_class) { Nanoc::ItemRepView }
+end
diff --git a/spec/nanoc/base/views/item_rep_view_spec.rb b/spec/nanoc/base/views/item_rep_view_spec.rb
new file mode 100644
index 0000000..5e897e0
--- /dev/null
+++ b/spec/nanoc/base/views/item_rep_view_spec.rb
@@ -0,0 +1,265 @@
+describe Nanoc::ItemRepView do
+ let(:view_context) { Nanoc::ViewContext.new(reps: reps, items: items, dependency_tracker: dependency_tracker, compilation_context: compilation_context) }
+
+ let(:reps) { double(:reps) }
+ let(:items) { double(:items) }
+ let(:compilation_context) { double(:compilation_context) }
+
+ let(:dependency_tracker) { Nanoc::Int::DependencyTracker.new(dependency_store) }
+ let(:dependency_store) { Nanoc::Int::DependencyStore.new([]) }
+ let(:base_item) { Nanoc::Int::Item.new('base', {}, '/base.md') }
+
+ before do
+ dependency_tracker.enter(base_item)
+ end
+
+ describe '#frozen?' do
+ let(:item_rep) { Nanoc::Int::ItemRep.new(item, :jacques) }
+ let(:item) { Nanoc::Int::Item.new('asdf', {}, '/foo/') }
+ let(:view) { described_class.new(item_rep, view_context) }
+
+ subject { view.frozen? }
+
+ context 'non-frozen item rep' do
+ it { is_expected.to be(false) }
+ end
+
+ context 'frozen item rep' do
+ before { item_rep.freeze }
+ it { is_expected.to be(true) }
+ end
+ end
+
+ describe '#== and #eql?' do
+ let(:item_rep) { Nanoc::Int::ItemRep.new(item, :jacques) }
+ let(:item) { Nanoc::Int::Item.new('asdf', {}, '/foo/') }
+ let(:view) { described_class.new(item_rep, view_context) }
+
+ context 'comparing with item rep with same identifier' do
+ let(:other_item) { double(:other_item, identifier: '/foo/') }
+ let(:other) { double(:other_item_rep, item: other_item, name: :jacques) }
+
+ it 'is ==' do
+ expect(view).to eq(other)
+ end
+
+ it 'is eql?' do
+ expect(view).not_to eql(other)
+ end
+ end
+
+ context 'comparing with item rep with different identifier' do
+ let(:other_item) { double(:other_item, identifier: '/bar/') }
+ let(:other) { double(:other_item_rep, item: other_item, name: :jacques) }
+
+ it 'is not ==' do
+ expect(view).not_to eq(other)
+ end
+
+ it 'is not eql?' do
+ expect(view).not_to eql(other)
+ end
+ end
+
+ context 'comparing with item rep with different name' do
+ let(:other_item) { double(:other_item, identifier: '/foo/') }
+ let(:other) { double(:other_item_rep, item: other_item, name: :marvin) }
+
+ it 'is not ==' do
+ expect(view).not_to eq(other)
+ end
+
+ it 'is not eql?' do
+ expect(view).not_to eql(other)
+ end
+ end
+
+ context 'comparing with item rep with same identifier' do
+ let(:other_item) { double(:other_item, identifier: '/foo/') }
+ let(:other) { described_class.new(double(:other_item_rep, item: other_item, name: :jacques), view_context) }
+
+ it 'is ==' do
+ expect(view).to eq(other)
+ end
+
+ it 'is eql?' do
+ expect(view).not_to eql(other)
+ end
+ end
+
+ context 'comparing with item rep with different identifier' do
+ let(:other_item) { double(:other_item, identifier: '/bar/') }
+ let(:other) { described_class.new(double(:other_item_rep, item: other_item, name: :jacques), view_context) }
+
+ it 'is not equal' do
+ expect(view).not_to eq(other)
+ expect(view).not_to eql(other)
+ end
+ end
+
+ context 'comparing with item rep with different name' do
+ let(:other_item) { double(:other_item, identifier: '/foo/') }
+ let(:other) { described_class.new(double(:other_item_rep, item: other_item, name: :marvin), view_context) }
+
+ it 'is not equal' do
+ expect(view).not_to eq(other)
+ expect(view).not_to eql(other)
+ end
+ end
+
+ context 'comparing with something that is not an item rep' do
+ let(:other_item) { double(:other_item, identifier: '/foo/') }
+ let(:other) { :donkey }
+
+ it 'is not equal' do
+ expect(view).not_to eq(other)
+ expect(view).not_to eql(other)
+ end
+ end
+ end
+
+ describe '#hash' do
+ let(:item_rep) { Nanoc::Int::ItemRep.new(item, :jacques) }
+ let(:item) { Nanoc::Int::Item.new('asdf', {}, '/foo/') }
+ let(:view) { described_class.new(item_rep, view_context) }
+
+ subject { view.hash }
+
+ it { should == described_class.hash ^ Nanoc::Identifier.new('/foo/').hash ^ :jacques.hash }
+ end
+
+ describe '#compiled_content' do
+ subject { view.compiled_content }
+
+ let(:view) { described_class.new(rep, view_context) }
+
+ let(:rep) do
+ Nanoc::Int::ItemRep.new(item, :default).tap do |ir|
+ ir.compiled = true
+ ir.snapshot_defs = [
+ Nanoc::Int::SnapshotDef.new(:last),
+ ]
+ ir.snapshot_contents = {
+ last: Nanoc::Int::TextualContent.new('Hallo'),
+ }
+ end
+ end
+
+ let(:item) do
+ Nanoc::Int::Item.new('content', {}, '/asdf.md')
+ end
+
+ it 'creates a dependency' do
+ expect { subject }.to change { dependency_store.objects_causing_outdatedness_of(base_item) }.from([]).to([item])
+ end
+
+ it 'creates a dependency with the right props' do
+ subject
+ dep = dependency_store.dependencies_causing_outdatedness_of(base_item)[0]
+
+ expect(dep.props.compiled_content?).to eq(true)
+
+ expect(dep.props.raw_content?).to eq(false)
+ expect(dep.props.attributes?).to eq(false)
+ expect(dep.props.path?).to eq(false)
+ end
+
+ it { should eq('Hallo') }
+ end
+
+ describe '#path' do
+ subject { view.path }
+
+ let(:view) { described_class.new(rep, view_context) }
+
+ let(:rep) do
+ Nanoc::Int::ItemRep.new(item, :default).tap do |ir|
+ ir.paths = {
+ last: '/about/',
+ }
+ end
+ end
+
+ let(:item) do
+ Nanoc::Int::Item.new('content', {}, '/asdf.md')
+ end
+
+ it 'creates a dependency' do
+ expect { subject }.to change { dependency_store.objects_causing_outdatedness_of(base_item) }.from([]).to([item])
+ end
+
+ it 'creates a dependency with the right props' do
+ subject
+ dep = dependency_store.dependencies_causing_outdatedness_of(base_item)[0]
+
+ expect(dep.props.path?).to eq(true)
+
+ expect(dep.props.raw_content?).to eq(false)
+ expect(dep.props.attributes?).to eq(false)
+ expect(dep.props.compiled_content?).to eq(false)
+ end
+
+ it { should eq('/about/') }
+ end
+
+ describe '#raw_path' do
+ subject { view.raw_path }
+
+ let(:view) { described_class.new(rep, view_context) }
+
+ let(:rep) do
+ Nanoc::Int::ItemRep.new(item, :default).tap do |ir|
+ ir.raw_paths = {
+ last: 'output/about/index.html',
+ }
+ end
+ end
+
+ let(:item) do
+ Nanoc::Int::Item.new('content', {}, '/asdf.md')
+ end
+
+ it 'creates a dependency' do
+ expect { subject }.to change { dependency_store.objects_causing_outdatedness_of(base_item) }.from([]).to([item])
+ end
+
+ it 'creates a dependency with the right props' do
+ subject
+ dep = dependency_store.dependencies_causing_outdatedness_of(base_item)[0]
+
+ expect(dep.props.path?).to eq(true)
+
+ expect(dep.props.raw_content?).to eq(false)
+ expect(dep.props.attributes?).to eq(false)
+ expect(dep.props.compiled_content?).to eq(false)
+ end
+
+ it { should eq('output/about/index.html') }
+ end
+
+ describe '#item' do
+ let(:item_rep) { Nanoc::Int::ItemRep.new(item, :jacques) }
+ let(:item) { Nanoc::Int::Item.new('asdf', {}, '/foo/') }
+ let(:view) { described_class.new(item_rep, view_context) }
+
+ subject { view.item }
+
+ it 'returns an item view' do
+ expect(subject).to be_a(Nanoc::ItemWithRepsView)
+ end
+
+ it 'returns an item view with the right context' do
+ expect(subject._context).to equal(view_context)
+ end
+ end
+
+ describe '#inspect' do
+ let(:item_rep) { Nanoc::Int::ItemRep.new(item, :jacques) }
+ let(:item) { Nanoc::Int::Item.new('asdf', {}, '/foo/') }
+ let(:view) { described_class.new(item_rep, view_context) }
+
+ subject { view.inspect }
+
+ it { is_expected.to eql('<Nanoc::ItemRepView item.identifier=/foo/ name=jacques>') }
+ end
+end
diff --git a/spec/nanoc/base/views/item_view_spec.rb b/spec/nanoc/base/views/item_view_spec.rb
new file mode 100644
index 0000000..52502f2
--- /dev/null
+++ b/spec/nanoc/base/views/item_view_spec.rb
@@ -0,0 +1,341 @@
+describe Nanoc::ItemWithRepsView do
+ let(:entity_class) { Nanoc::Int::Item }
+ let(:other_view_class) { Nanoc::LayoutView }
+ it_behaves_like 'a document view'
+
+ let(:view_context) { Nanoc::ViewContext.new(reps: reps, items: items, dependency_tracker: dependency_tracker, compilation_context: compilation_context) }
+ let(:reps) { [] }
+ let(:items) { [] }
+ let(:dependency_tracker) { Nanoc::Int::DependencyTracker.new(dependency_store) }
+ let(:dependency_store) { Nanoc::Int::DependencyStore.new([]) }
+ let(:compilation_context) { double(:compilation_context) }
+
+ let(:base_item) { Nanoc::Int::Item.new('base', {}, '/base.md') }
+
+ before do
+ dependency_tracker.enter(base_item)
+ end
+
+ describe '#parent' do
+ let(:item) do
+ Nanoc::Int::Item.new('me', {}, identifier)
+ end
+
+ let(:view) { described_class.new(item, view_context) }
+
+ let(:items) do
+ Nanoc::Int::IdentifiableCollection.new({}).tap do |arr|
+ arr << item
+ arr << parent_item if parent_item
+ end
+ end
+
+ subject { view.parent }
+
+ context 'with parent' do
+ let(:parent_item) do
+ Nanoc::Int::Item.new('parent', {}, '/parent/')
+ end
+
+ context 'full identifier' do
+ let(:identifier) do
+ Nanoc::Identifier.new('/parent/me.md')
+ end
+
+ it 'raises' do
+ expect { subject }.to raise_error(Nanoc::Int::Errors::CannotGetParentOrChildrenOfNonLegacyItem)
+ end
+ end
+
+ context 'legacy identifier' do
+ let(:identifier) do
+ Nanoc::Identifier.new('/parent/me/', type: :legacy)
+ end
+
+ it 'returns a view for the parent' do
+ expect(subject.class).to eql(Nanoc::ItemWithRepsView)
+ expect(subject.unwrap).to eql(parent_item)
+ end
+
+ it 'returns a view with the right context' do
+ expect(subject._context).to equal(view_context)
+ end
+
+ context 'frozen parent' do
+ before { parent_item.freeze }
+ it { is_expected.to be_frozen }
+ end
+
+ context 'non-frozen parent' do
+ it { is_expected.not_to be_frozen }
+ end
+
+ context 'with root parent' do
+ let(:parent_item) { Nanoc::Int::Item.new('parent', {}, '/') }
+ let(:identifier) { Nanoc::Identifier.new('/me/', type: :legacy) }
+
+ it 'returns a view for the parent' do
+ expect(subject.class).to eql(Nanoc::ItemWithRepsView)
+ expect(subject.unwrap).to eql(parent_item)
+ end
+ end
+ end
+ end
+
+ context 'without parent' do
+ let(:parent_item) do
+ nil
+ end
+
+ context 'full identifier' do
+ let(:identifier) do
+ Nanoc::Identifier.new('/me.md')
+ end
+
+ it 'raises' do
+ expect { subject }.to raise_error(Nanoc::Int::Errors::CannotGetParentOrChildrenOfNonLegacyItem)
+ end
+ end
+
+ context 'legacy identifier' do
+ let(:identifier) do
+ Nanoc::Identifier.new('/me/', type: :legacy)
+ end
+
+ it { is_expected.to be_nil }
+ it { is_expected.to be_frozen }
+ end
+ end
+ end
+
+ describe '#children' do
+ let(:item) do
+ Nanoc::Int::Item.new('me', {}, identifier)
+ end
+
+ let(:children) do
+ [Nanoc::Int::Item.new('child', {}, '/me/child/')]
+ end
+
+ let(:view) { described_class.new(item, view_context) }
+
+ let(:items) do
+ Nanoc::Int::IdentifiableCollection.new({}).tap do |arr|
+ arr << item
+ children.each { |child| arr << child }
+ end
+ end
+
+ subject { view.children }
+
+ context 'full identifier' do
+ let(:identifier) do
+ Nanoc::Identifier.new('/me.md')
+ end
+
+ it 'raises' do
+ expect { subject }.to raise_error(Nanoc::Int::Errors::CannotGetParentOrChildrenOfNonLegacyItem)
+ end
+ end
+
+ context 'legacy identifier' do
+ let(:identifier) do
+ Nanoc::Identifier.new('/me/', type: :legacy)
+ end
+
+ it 'returns views for the children' do
+ expect(subject.size).to eql(1)
+ expect(subject[0].class).to eql(Nanoc::ItemWithRepsView)
+ expect(subject[0].unwrap).to eql(children[0])
+ end
+
+ it { is_expected.to be_frozen }
+ end
+ end
+
+ describe '#reps' do
+ let(:item) { Nanoc::Int::Item.new('blah', {}, '/foo.md') }
+ let(:rep_a) { Nanoc::Int::ItemRep.new(item, :a) }
+ let(:rep_b) { Nanoc::Int::ItemRep.new(item, :b) }
+
+ let(:reps) do
+ Nanoc::Int::ItemRepRepo.new.tap do |reps|
+ reps << rep_a
+ reps << rep_b
+ end
+ end
+
+ let(:view) { described_class.new(item, view_context) }
+
+ subject { view.reps }
+
+ it 'returns a proper item rep collection' do
+ expect(subject.size).to eq(2)
+ expect(subject.class).to eql(Nanoc::ItemRepCollectionView)
+ end
+
+ it 'returns a view with the right context' do
+ expect(subject._context).to eq(view_context)
+ end
+ end
+
+ describe '#compiled_content' do
+ subject { view.compiled_content(params) }
+
+ let(:view) { described_class.new(item, view_context) }
+
+ let(:item) do
+ Nanoc::Int::Item.new('content', {}, '/asdf/')
+ end
+
+ let(:reps) do
+ Nanoc::Int::ItemRepRepo.new.tap do |reps|
+ reps << rep
+ end
+ end
+
+ let(:rep) do
+ Nanoc::Int::ItemRep.new(item, :default).tap do |ir|
+ ir.compiled = true
+ ir.snapshot_defs = [
+ Nanoc::Int::SnapshotDef.new(:last),
+ Nanoc::Int::SnapshotDef.new(:pre),
+ Nanoc::Int::SnapshotDef.new(:post),
+ Nanoc::Int::SnapshotDef.new(:specific),
+ ]
+ ir.snapshot_contents = {
+ last: Nanoc::Int::TextualContent.new('Last Hallo'),
+ pre: Nanoc::Int::TextualContent.new('Pre Hallo'),
+ post: Nanoc::Int::TextualContent.new('Post Hallo'),
+ specific: Nanoc::Int::TextualContent.new('Specific Hallo'),
+ }
+ end
+ end
+
+ context 'requesting implicit default rep' do
+ let(:params) { {} }
+
+ it { is_expected.to eq('Pre Hallo') }
+
+ it 'creates a dependency' do
+ expect { subject }.to change { dependency_store.objects_causing_outdatedness_of(base_item) }.from([]).to([item])
+ end
+
+ context 'requesting explicit snapshot' do
+ let(:params) { { snapshot: :specific } }
+
+ it { is_expected.to eq('Specific Hallo') }
+
+ it 'creates a dependency' do
+ expect { subject }.to change { dependency_store.objects_causing_outdatedness_of(base_item) }.from([]).to([item])
+ end
+ end
+ end
+
+ context 'requesting explicit default rep' do
+ let(:params) { { rep: :default } }
+
+ it 'creates a dependency' do
+ expect { subject }.to change { dependency_store.objects_causing_outdatedness_of(base_item) }.from([]).to([item])
+ end
+
+ it { is_expected.to eq('Pre Hallo') }
+
+ context 'requesting explicit snapshot' do
+ let(:params) { { snapshot: :specific } }
+
+ it { is_expected.to eq('Specific Hallo') }
+ end
+ end
+
+ context 'requesting other rep' do
+ let(:params) { { rep: :other } }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(Nanoc::ItemRepCollectionView::NoSuchItemRepError)
+ end
+ end
+ end
+
+ describe '#path' do
+ subject { view.path(params) }
+
+ let(:view) { described_class.new(item, view_context) }
+
+ let(:item) do
+ Nanoc::Int::Item.new('content', {}, '/asdf.md')
+ end
+
+ let(:reps) do
+ Nanoc::Int::ItemRepRepo.new.tap do |reps|
+ reps << rep
+ end
+ end
+
+ let(:rep) do
+ Nanoc::Int::ItemRep.new(item, :default).tap do |ir|
+ ir.paths = {
+ last: '/about/',
+ specific: '/about.txt',
+ }
+ end
+ end
+
+ context 'requesting implicit default rep' do
+ let(:params) { {} }
+
+ it 'creates a dependency' do
+ expect { subject }.to change { dependency_store.objects_causing_outdatedness_of(base_item) }.from([]).to([item])
+ end
+
+ it { is_expected.to eq('/about/') }
+
+ context 'requesting explicit snapshot' do
+ let(:params) { { snapshot: :specific } }
+
+ it { is_expected.to eq('/about.txt') }
+ end
+ end
+
+ context 'requesting explicit default rep' do
+ let(:params) { { rep: :default } }
+
+ it 'creates a dependency' do
+ expect { subject }.to change { dependency_store.objects_causing_outdatedness_of(base_item) }.from([]).to([item])
+ end
+
+ it { is_expected.to eq('/about/') }
+
+ context 'requesting explicit snapshot' do
+ let(:params) { { snapshot: :specific } }
+
+ it { is_expected.to eq('/about.txt') }
+ end
+ end
+
+ context 'requesting other rep' do
+ let(:params) { { rep: :other } }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(Nanoc::ItemRepCollectionView::NoSuchItemRepError)
+ end
+ end
+ end
+
+ describe '#binary?' do
+ # TODO: implement
+ end
+
+ describe '#raw_filename' do
+ # TODO: implement
+ end
+
+ describe '#inspect' do
+ let(:item) { Nanoc::Int::Item.new('content', {}, '/asdf/') }
+ let(:view) { described_class.new(item, nil) }
+
+ subject { view.inspect }
+
+ it { is_expected.to eql('<Nanoc::ItemWithRepsView identifier=/asdf/>') }
+ end
+end
diff --git a/spec/nanoc/base/views/layout_collection_view_spec.rb b/spec/nanoc/base/views/layout_collection_view_spec.rb
new file mode 100644
index 0000000..054f61d
--- /dev/null
+++ b/spec/nanoc/base/views/layout_collection_view_spec.rb
@@ -0,0 +1,18 @@
+describe Nanoc::LayoutCollectionView do
+ let(:view_class) { Nanoc::LayoutView }
+ it_behaves_like 'an identifiable collection'
+
+ describe '#inspect' do
+ let(:wrapped) do
+ Nanoc::Int::IdentifiableCollection.new(config)
+ end
+
+ let(:view) { described_class.new(wrapped, view_context) }
+ let(:view_context) { double(:view_context) }
+ let(:config) { { string_pattern_type: 'glob' } }
+
+ subject { view.inspect }
+
+ it { is_expected.to eql('<Nanoc::LayoutCollectionView>') }
+ end
+end
diff --git a/spec/nanoc/base/views/layout_view_spec.rb b/spec/nanoc/base/views/layout_view_spec.rb
new file mode 100644
index 0000000..d682d8f
--- /dev/null
+++ b/spec/nanoc/base/views/layout_view_spec.rb
@@ -0,0 +1,14 @@
+describe Nanoc::LayoutView do
+ let(:entity_class) { Nanoc::Int::Layout }
+ let(:other_view_class) { Nanoc::ItemWithRepsView }
+ it_behaves_like 'a document view'
+
+ describe '#inspect' do
+ let(:item) { Nanoc::Int::Layout.new('content', {}, '/asdf/') }
+ let(:view) { described_class.new(item, nil) }
+
+ subject { view.inspect }
+
+ it { is_expected.to eql('<Nanoc::LayoutView identifier=/asdf/>') }
+ end
+end
diff --git a/spec/nanoc/base/views/mutable_config_view_spec.rb b/spec/nanoc/base/views/mutable_config_view_spec.rb
new file mode 100644
index 0000000..d97adb0
--- /dev/null
+++ b/spec/nanoc/base/views/mutable_config_view_spec.rb
@@ -0,0 +1,16 @@
+describe Nanoc::MutableConfigView do
+ let(:config) { {} }
+ let(:view) { described_class.new(config, nil) }
+
+ describe '#[]=' do
+ it 'sets attributes' do
+ view[:awesomeness] = 'rather high'
+ expect(config[:awesomeness]).to eq('rather high')
+ end
+ end
+
+ describe '#inspect' do
+ subject { view.inspect }
+ it { is_expected.to eql('<Nanoc::MutableConfigView>') }
+ end
+end
diff --git a/spec/nanoc/base/views/mutable_document_view_spec.rb b/spec/nanoc/base/views/mutable_document_view_spec.rb
new file mode 100644
index 0000000..cacbd18
--- /dev/null
+++ b/spec/nanoc/base/views/mutable_document_view_spec.rb
@@ -0,0 +1,92 @@
+shared_examples 'a mutable document view' do
+ let(:view) { described_class.new(item, view_context) }
+
+ let(:view_context) do
+ Nanoc::ViewContext.new(
+ reps: double(:reps),
+ items: double(:items),
+ dependency_tracker: dependency_tracker,
+ compilation_context: double(:compilation_context),
+ )
+ end
+
+ let(:dependency_tracker) { Nanoc::Int::DependencyTracker.new(double(:dependency_store)) }
+
+ describe '#[]=' do
+ # FIXME: rename :item to :document
+ let(:item) { entity_class.new('content', {}, '/asdf/') }
+
+ it 'sets attributes' do
+ view[:title] = 'Donkey'
+ expect(view[:title]).to eq('Donkey')
+ end
+
+ it 'disallows items' do
+ item = Nanoc::Int::Item.new('content', {}, '/foo.md')
+ expect { view[:item] = item }.to raise_error(Nanoc::MutableDocumentViewMixin::DisallowedAttributeValueError)
+ end
+
+ it 'disallows layouts' do
+ layout = Nanoc::Int::Layout.new('content', {}, '/foo.md')
+ expect { view[:layout] = layout }.to raise_error(Nanoc::MutableDocumentViewMixin::DisallowedAttributeValueError)
+ end
+
+ it 'disallows item views' do
+ item = Nanoc::ItemWithRepsView.new(Nanoc::Int::Item.new('content', {}, '/foo.md'), nil)
+ expect { view[:item] = item }.to raise_error(Nanoc::MutableDocumentViewMixin::DisallowedAttributeValueError)
+ end
+
+ it 'disallows layout views' do
+ layout = Nanoc::LayoutView.new(Nanoc::Int::Layout.new('content', {}, '/foo.md'), nil)
+ expect { view[:layout] = layout }.to raise_error(Nanoc::MutableDocumentViewMixin::DisallowedAttributeValueError)
+ end
+ end
+
+ describe '#identifier=' do
+ let(:item) { entity_class.new('content', {}, '/about.md') }
+
+ subject { view.identifier = arg }
+
+ context 'given a string' do
+ let(:arg) { '/about.adoc' }
+
+ it 'changes the identifier' do
+ subject
+ expect(view.identifier).to eq('/about.adoc')
+ end
+ end
+
+ context 'given an identifier' do
+ let(:arg) { Nanoc::Identifier.new('/about.adoc') }
+
+ it 'changes the identifier' do
+ subject
+ expect(view.identifier).to eq('/about.adoc')
+ end
+ end
+
+ context 'given anything else' do
+ let(:arg) { :donkey }
+
+ it 'raises' do
+ expect { subject }.to raise_error(Nanoc::Identifier::NonCoercibleObjectError)
+ end
+ end
+ end
+
+ describe '#update_attributes' do
+ let(:item) { entity_class.new('content', {}, '/asdf/') }
+
+ let(:update) { { friend: 'Giraffe' } }
+
+ subject { view.update_attributes(update) }
+
+ it 'sets attributes' do
+ expect { subject }.to change { view[:friend] }.from(nil).to('Giraffe')
+ end
+
+ it 'returns self' do
+ expect(subject).to equal(view)
+ end
+ end
+end
diff --git a/spec/nanoc/base/views/mutable_identifiable_collection_view_spec.rb b/spec/nanoc/base/views/mutable_identifiable_collection_view_spec.rb
new file mode 100644
index 0000000..1fff44c
--- /dev/null
+++ b/spec/nanoc/base/views/mutable_identifiable_collection_view_spec.rb
@@ -0,0 +1,36 @@
+shared_examples 'a mutable identifiable collection' do
+ let(:view) { described_class.new(wrapped, view_context) }
+
+ let(:view_context) { double(:view_context) }
+
+ let(:config) do
+ {}
+ end
+
+ describe '#delete_if' do
+ let(:wrapped) do
+ Nanoc::Int::IdentifiableCollection.new(config).tap do |coll|
+ coll << double(:identifiable, identifier: Nanoc::Identifier.new('/asdf/'))
+ end
+ end
+
+ it 'deletes matching' do
+ view.delete_if { |i| i.identifier == '/asdf/' }
+ expect(wrapped).to be_empty
+ end
+
+ it 'deletes no non-matching' do
+ view.delete_if { |i| i.identifier == '/blah/' }
+ expect(wrapped).not_to be_empty
+ end
+
+ it 'returns self' do
+ ret = view.delete_if { |_i| false }
+ expect(ret).to equal(view)
+ end
+
+ it 'yields items with the proper context' do
+ view.delete_if { |i| expect(i._context).to equal(view_context) }
+ end
+ end
+end
diff --git a/spec/nanoc/base/views/mutable_item_collection_view_spec.rb b/spec/nanoc/base/views/mutable_item_collection_view_spec.rb
new file mode 100644
index 0000000..ef8d2c8
--- /dev/null
+++ b/spec/nanoc/base/views/mutable_item_collection_view_spec.rb
@@ -0,0 +1,49 @@
+describe Nanoc::MutableItemCollectionView do
+ let(:view_class) { Nanoc::MutableItemView }
+ it_behaves_like 'an identifiable collection'
+ it_behaves_like 'a mutable identifiable collection'
+
+ let(:config) do
+ { string_pattern_type: 'glob' }
+ end
+
+ describe '#create' do
+ let(:item) do
+ Nanoc::Int::Layout.new('content', {}, '/asdf/')
+ end
+
+ let(:wrapped) do
+ Nanoc::Int::IdentifiableCollection.new(config).tap do |coll|
+ coll << item
+ end
+ end
+
+ let(:view) { described_class.new(wrapped, nil) }
+
+ it 'creates an object' do
+ view.create('new content', { title: 'New Page' }, '/new/')
+
+ expect(wrapped.size).to eq(2)
+ expect(wrapped['/new/'].content.string).to eq('new content')
+ end
+
+ it 'returns self' do
+ ret = view.create('new content', { title: 'New Page' }, '/new/')
+ expect(ret).to equal(view)
+ end
+ end
+
+ describe '#inspect' do
+ let(:wrapped) do
+ Nanoc::Int::IdentifiableCollection.new(config)
+ end
+
+ let(:view) { described_class.new(wrapped, view_context) }
+ let(:view_context) { double(:view_context) }
+ let(:config) { { string_pattern_type: 'glob' } }
+
+ subject { view.inspect }
+
+ it { is_expected.to eql('<Nanoc::MutableItemCollectionView>') }
+ end
+end
diff --git a/spec/nanoc/base/views/mutable_item_view_spec.rb b/spec/nanoc/base/views/mutable_item_view_spec.rb
new file mode 100644
index 0000000..c42b241
--- /dev/null
+++ b/spec/nanoc/base/views/mutable_item_view_spec.rb
@@ -0,0 +1,22 @@
+describe Nanoc::MutableItemView do
+ let(:entity_class) { Nanoc::Int::Item }
+ it_behaves_like 'a mutable document view'
+
+ let(:item) { entity_class.new('content', {}, '/asdf/') }
+ let(:view) { described_class.new(item, nil) }
+
+ it 'does have rep access' do
+ expect(view).not_to respond_to(:compiled_content)
+ expect(view).not_to respond_to(:path)
+ expect(view).not_to respond_to(:reps)
+ end
+
+ describe '#inspect' do
+ let(:item) { Nanoc::Int::Item.new('content', {}, '/asdf/') }
+ let(:view) { described_class.new(item, nil) }
+
+ subject { view.inspect }
+
+ it { is_expected.to eql('<Nanoc::MutableItemView identifier=/asdf/>') }
+ end
+end
diff --git a/spec/nanoc/base/views/mutable_layout_collection_view_spec.rb b/spec/nanoc/base/views/mutable_layout_collection_view_spec.rb
new file mode 100644
index 0000000..b57c108
--- /dev/null
+++ b/spec/nanoc/base/views/mutable_layout_collection_view_spec.rb
@@ -0,0 +1,49 @@
+describe Nanoc::MutableLayoutCollectionView do
+ let(:view_class) { Nanoc::MutableLayoutView }
+ it_behaves_like 'an identifiable collection'
+ it_behaves_like 'a mutable identifiable collection'
+
+ let(:config) do
+ { string_pattern_type: 'glob' }
+ end
+
+ describe '#create' do
+ let(:layout) do
+ Nanoc::Int::Layout.new('content', {}, '/asdf/')
+ end
+
+ let(:wrapped) do
+ Nanoc::Int::IdentifiableCollection.new(config).tap do |coll|
+ coll << layout
+ end
+ end
+
+ let(:view) { described_class.new(wrapped, nil) }
+
+ it 'creates an object' do
+ view.create('new content', { title: 'New Page' }, '/new/')
+
+ expect(wrapped.size).to eq(2)
+ expect(wrapped['/new/'].content.string).to eq('new content')
+ end
+
+ it 'returns self' do
+ ret = view.create('new content', { title: 'New Page' }, '/new/')
+ expect(ret).to equal(view)
+ end
+ end
+
+ describe '#inspect' do
+ let(:wrapped) do
+ Nanoc::Int::IdentifiableCollection.new(config)
+ end
+
+ let(:view) { described_class.new(wrapped, view_context) }
+ let(:view_context) { double(:view_context) }
+ let(:config) { { string_pattern_type: 'glob' } }
+
+ subject { view.inspect }
+
+ it { is_expected.to eql('<Nanoc::MutableLayoutCollectionView>') }
+ end
+end
diff --git a/spec/nanoc/base/views/mutable_layout_view_spec.rb b/spec/nanoc/base/views/mutable_layout_view_spec.rb
new file mode 100644
index 0000000..91d93e8
--- /dev/null
+++ b/spec/nanoc/base/views/mutable_layout_view_spec.rb
@@ -0,0 +1,13 @@
+describe Nanoc::MutableLayoutView do
+ let(:entity_class) { Nanoc::Int::Layout }
+ it_behaves_like 'a mutable document view'
+
+ describe '#inspect' do
+ let(:item) { Nanoc::Int::Item.new('content', {}, '/asdf/') }
+ let(:view) { described_class.new(item, nil) }
+
+ subject { view.inspect }
+
+ it { is_expected.to eql('<Nanoc::MutableLayoutView identifier=/asdf/>') }
+ end
+end
diff --git a/spec/nanoc/base/views/post_compile_item_rep_collection_view_spec.rb b/spec/nanoc/base/views/post_compile_item_rep_collection_view_spec.rb
new file mode 100644
index 0000000..6dd9bcf
--- /dev/null
+++ b/spec/nanoc/base/views/post_compile_item_rep_collection_view_spec.rb
@@ -0,0 +1,4 @@
+describe Nanoc::PostCompileItemRepCollectionView do
+ it_behaves_like 'an item rep collection view'
+ let(:expected_view_class) { Nanoc::PostCompileItemRepView }
+end
diff --git a/spec/nanoc/base/views/post_compile_item_rep_view_spec.rb b/spec/nanoc/base/views/post_compile_item_rep_view_spec.rb
new file mode 100644
index 0000000..f167732
--- /dev/null
+++ b/spec/nanoc/base/views/post_compile_item_rep_view_spec.rb
@@ -0,0 +1,137 @@
+describe Nanoc::PostCompileItemRepView do
+ let(:item_rep) { Nanoc::Int::ItemRep.new(item, :jacques) }
+ let(:item) { Nanoc::Int::Item.new('asdf', {}, '/foo/') }
+ let(:view) { described_class.new(item_rep, view_context) }
+
+ let(:view_context) do
+ Nanoc::ViewContext.new(
+ reps: reps,
+ items: items,
+ dependency_tracker: dependency_tracker,
+ compilation_context: compilation_context,
+ )
+ end
+
+ let(:reps) { double(:reps) }
+ let(:items) { Nanoc::Int::IdentifiableCollection.new(config) }
+ let(:config) { Nanoc::Int::Configuration.new }
+ let(:dependency_tracker) { Nanoc::Int::DependencyTracker.new(double(:dependency_store)) }
+ let(:compilation_context) { double(:compilation_context, compiled_content_cache: compiled_content_cache) }
+
+ let(:snapshot_contents) do
+ {
+ last: Nanoc::Int::TextualContent.new('content-last'),
+ pre: Nanoc::Int::TextualContent.new('content-pre'),
+ donkey: Nanoc::Int::TextualContent.new('content-donkey'),
+ }
+ end
+
+ let(:compiled_content_cache) do
+ Nanoc::Int::CompiledContentCache.new(items: items).tap do |ccc|
+ ccc[item_rep] = snapshot_contents
+ end
+ end
+
+ describe '#compiled_content' do
+ subject { view.compiled_content }
+
+ context 'binary' do
+ let(:item) do
+ content = Nanoc::Int::Content.create('/foo.dat', binary: true)
+ Nanoc::Int::Item.new(content, {}, '/foo.dat')
+ end
+
+ it 'raises error' do
+ err = Nanoc::Int::Errors::CannotGetCompiledContentOfBinaryItem
+ expect { subject }.to raise_error(err)
+ end
+ end
+
+ shared_examples 'returns pre content' do
+ example { expect(subject).to eq('content-pre') }
+ end
+
+ shared_examples 'returns last content' do
+ example { expect(subject).to eq('content-last') }
+ end
+
+ shared_examples 'returns donkey content' do
+ example { expect(subject).to eq('content-donkey') }
+ end
+
+ shared_examples 'raises no-such-snapshot error' do
+ it 'raises error' do
+ err = Nanoc::Int::Errors::NoSuchSnapshot
+ expect { subject }.to raise_error(err)
+ end
+ end
+
+ context 'textual' do
+ context 'snapshot provided' do
+ subject { view.compiled_content(snapshot: :donkey) }
+ let(:expected_snapshot) { :donkey }
+
+ context 'snapshot exists' do
+ include_examples 'returns donkey content'
+ end
+
+ context 'snapshot does not exist' do
+ let(:snapshot_contents) do
+ {
+ last: Nanoc::Int::TextualContent.new('content-last'),
+ pre: Nanoc::Int::TextualContent.new('content-pre'),
+ }
+ end
+
+ include_examples 'raises no-such-snapshot error'
+ end
+ end
+
+ context 'no snapshot provided' do
+ context 'pre and last snapshots exist' do
+ let(:snapshot_contents) do
+ {
+ last: Nanoc::Int::TextualContent.new('content-last'),
+ pre: Nanoc::Int::TextualContent.new('content-pre'),
+ donkey: Nanoc::Int::TextualContent.new('content-donkey'),
+ }
+ end
+
+ include_examples 'returns pre content'
+ end
+
+ context 'pre snapshot exists' do
+ let(:snapshot_contents) do
+ {
+ pre: Nanoc::Int::TextualContent.new('content-pre'),
+ donkey: Nanoc::Int::TextualContent.new('content-donkey'),
+ }
+ end
+
+ include_examples 'returns pre content'
+ end
+
+ context 'last snapshot exists' do
+ let(:snapshot_contents) do
+ {
+ last: Nanoc::Int::TextualContent.new('content-last'),
+ donkey: Nanoc::Int::TextualContent.new('content-donkey'),
+ }
+ end
+
+ include_examples 'returns last content'
+ end
+
+ context 'neither pre nor last snapshot exists' do
+ let(:snapshot_contents) do
+ {
+ donkey: Nanoc::Int::TextualContent.new('content-donkey'),
+ }
+ end
+
+ include_examples 'raises no-such-snapshot error'
+ end
+ end
+ end
+ end
+end
diff --git a/spec/nanoc/base/views/post_compile_item_view_spec.rb b/spec/nanoc/base/views/post_compile_item_view_spec.rb
new file mode 100644
index 0000000..80b451e
--- /dev/null
+++ b/spec/nanoc/base/views/post_compile_item_view_spec.rb
@@ -0,0 +1,56 @@
+describe Nanoc::PostCompileItemView do
+ let(:item) { Nanoc::Int::Item.new('blah', {}, '/foo.md') }
+ let(:rep_a) { Nanoc::Int::ItemRep.new(item, :no_mod) }
+ let(:rep_b) { Nanoc::Int::ItemRep.new(item, :modded).tap { |r| r.modified = true } }
+
+ let(:reps) do
+ Nanoc::Int::ItemRepRepo.new.tap do |reps|
+ reps << rep_a
+ reps << rep_b
+ end
+ end
+
+ let(:view_context) { double(:view_context, reps: reps) }
+ let(:view) { described_class.new(item, view_context) }
+
+ shared_examples 'a method that returns modified reps only' do
+ it 'returns only modified items' do
+ expect(subject.size).to eq(1)
+ expect(subject.map(&:name)).to eq(%i(modded))
+ end
+
+ it 'returns an array' do
+ expect(subject.class).to eql(Array)
+ end
+ end
+
+ shared_examples 'a method that returns PostCompileItemRepViews' do
+ it 'returns PostCompileItemRepViews' do
+ expect(subject).to all(be_a(Nanoc::PostCompileItemRepView))
+ end
+ end
+
+ describe '#modified_reps' do
+ subject { view.modified_reps }
+
+ it_behaves_like 'a method that returns modified reps only'
+ it_behaves_like 'a method that returns PostCompileItemRepViews'
+ end
+
+ describe '#modified' do
+ subject { view.modified }
+
+ it_behaves_like 'a method that returns modified reps only'
+ it_behaves_like 'a method that returns PostCompileItemRepViews'
+ end
+
+ describe '#reps' do
+ subject { view.reps }
+
+ it_behaves_like 'a method that returns PostCompileItemRepViews'
+
+ it 'returns a PostCompileItemRepCollectionView' do
+ expect(subject).to be_a(Nanoc::PostCompileItemRepCollectionView)
+ end
+ end
+end
diff --git a/spec/nanoc/cli/commands/compile/file_action_printer_spec.rb b/spec/nanoc/cli/commands/compile/file_action_printer_spec.rb
new file mode 100644
index 0000000..5977af1
--- /dev/null
+++ b/spec/nanoc/cli/commands/compile/file_action_printer_spec.rb
@@ -0,0 +1,76 @@
+describe Nanoc::CLI::Commands::Compile::FileActionPrinter, stdio: true do
+ let(:listener) { described_class.new(reps: reps) }
+
+ before { Timecop.freeze(Time.local(2008, 1, 2, 14, 5, 0)) }
+ after { Timecop.return }
+
+ let(:reps) do
+ Nanoc::Int::ItemRepRepo.new.tap do |reps|
+ reps << rep
+ end
+ end
+
+ let(:item) { Nanoc::Int::Item.new('<%= 1 + 2 %>', {}, '/hi.md') }
+
+ let(:rep) do
+ Nanoc::Int::ItemRep.new(item, :default).tap do |rep|
+ rep.raw_paths = { default: '/hi.html' }
+ end
+ end
+
+ it 'records from compilation_started to rep_written' do
+ listener.start
+
+ Timecop.freeze(Time.local(2008, 9, 1, 10, 5, 0))
+ Nanoc::Int::NotificationCenter.post(:compilation_started, rep)
+ Timecop.freeze(Time.local(2008, 9, 1, 10, 5, 1))
+
+ expect { Nanoc::Int::NotificationCenter.post(:rep_written, rep, '/foo.html', true, true) }
+ .to output(/create.*\[1\.00s\]/).to_stdout
+ end
+
+ it 'stops listening after #stop' do
+ listener.start
+ listener.stop
+
+ Nanoc::Int::NotificationCenter.post(:compilation_started, rep)
+
+ expect { Nanoc::Int::NotificationCenter.post(:rep_written, rep, '/foo.html', true, true) }
+ .not_to output(/create/).to_stdout
+ end
+
+ it 'records from compilation_started over compilation_suspended to rep_written' do
+ listener.start
+
+ Timecop.freeze(Time.local(2008, 9, 1, 10, 5, 0))
+ Nanoc::Int::NotificationCenter.post(:compilation_started, rep)
+ Timecop.freeze(Time.local(2008, 9, 1, 10, 5, 1))
+ Nanoc::Int::NotificationCenter.post(:compilation_suspended, rep, :__irrelevant__)
+ Timecop.freeze(Time.local(2008, 9, 1, 10, 5, 3))
+ Nanoc::Int::NotificationCenter.post(:compilation_started, rep)
+ Timecop.freeze(Time.local(2008, 9, 1, 10, 5, 6))
+
+ expect { Nanoc::Int::NotificationCenter.post(:rep_written, rep, '/foo.html', true, true) }
+ .to output(/create.*\[4\.00s\]/).to_stdout
+ end
+
+ context 'log level = high' do
+ before { listener.start }
+ before { Nanoc::CLI::Logger.instance.level = :high }
+
+ it 'prints skipped (uncompiled) reps' do
+ expect { listener.stop }
+ .not_to output(/skip/).to_stdout
+ end
+ end
+
+ context 'log level = low' do
+ before { listener.start }
+ before { Nanoc::CLI::Logger.instance.level = :low }
+
+ it 'prints nothing' do
+ expect { listener.stop }
+ .to output(/skip.*\/hi\.html/).to_stdout
+ end
+ end
+end
diff --git a/spec/nanoc/cli/commands/compile/timing_recorder_spec.rb b/spec/nanoc/cli/commands/compile/timing_recorder_spec.rb
new file mode 100644
index 0000000..edb900e
--- /dev/null
+++ b/spec/nanoc/cli/commands/compile/timing_recorder_spec.rb
@@ -0,0 +1,66 @@
+describe Nanoc::CLI::Commands::Compile::TimingRecorder, stdio: true do
+ let(:listener) { described_class.new(reps: reps) }
+
+ before { Timecop.freeze(Time.local(2008, 1, 2, 14, 5, 0)) }
+ after { Timecop.return }
+
+ let(:reps) do
+ Nanoc::Int::ItemRepRepo.new.tap do |reps|
+ reps << rep
+ end
+ end
+
+ let(:item) { Nanoc::Int::Item.new('<%= 1 + 2 %>', {}, '/hi.md') }
+
+ let(:rep) do
+ Nanoc::Int::ItemRep.new(item, :default).tap do |rep|
+ rep.raw_paths = { default: '/hi.html' }
+ end
+ end
+
+ it 'records single from filtering_started to filtering_ended' do
+ listener.start
+
+ Timecop.freeze(Time.local(2008, 9, 1, 10, 5, 0))
+ Nanoc::Int::NotificationCenter.post(:filtering_started, rep, :erb)
+ Timecop.freeze(Time.local(2008, 9, 1, 10, 5, 1))
+ Nanoc::Int::NotificationCenter.post(:filtering_ended, rep, :erb)
+
+ expect { listener.stop }
+ .to output(/^erb \| 1 1\.00s 1\.00s 1\.00s 1\.00s$/).to_stdout
+ end
+
+ it 'records multiple from filtering_started to filtering_ended' do
+ listener.start
+
+ Timecop.freeze(Time.local(2008, 9, 1, 10, 5, 0))
+ Nanoc::Int::NotificationCenter.post(:filtering_started, rep, :erb)
+ Timecop.freeze(Time.local(2008, 9, 1, 10, 5, 1))
+ Nanoc::Int::NotificationCenter.post(:filtering_ended, rep, :erb)
+ Timecop.freeze(Time.local(2008, 9, 1, 10, 14, 1))
+ Nanoc::Int::NotificationCenter.post(:filtering_started, rep, :erb)
+ Timecop.freeze(Time.local(2008, 9, 1, 10, 14, 3))
+ Nanoc::Int::NotificationCenter.post(:filtering_ended, rep, :erb)
+
+ expect { listener.stop }
+ .to output(/^erb \| 2 1\.00s 1\.50s 2\.00s 3\.00s$/).to_stdout
+ end
+
+ it 'records single from filtering_started over compilation_{suspended,started} to filtering_ended' do
+ listener.start
+
+ Nanoc::Int::NotificationCenter.post(:compilation_started, rep)
+ Timecop.freeze(Time.local(2008, 9, 1, 10, 5, 0))
+ Nanoc::Int::NotificationCenter.post(:filtering_started, rep, :erb)
+ Timecop.freeze(Time.local(2008, 9, 1, 10, 5, 1))
+ Nanoc::Int::NotificationCenter.post(:compilation_suspended, rep, :__anything__)
+ Timecop.freeze(Time.local(2008, 9, 1, 10, 5, 3))
+ Nanoc::Int::NotificationCenter.post(:compilation_started, rep)
+ Timecop.freeze(Time.local(2008, 9, 1, 10, 5, 7))
+ Nanoc::Int::NotificationCenter.post(:filtering_ended, rep, :erb)
+
+ # FIXME: wrong count (should be 1, not 2)
+ expect { listener.stop }
+ .to output(/^erb \| 2 1\.00s 2\.50s 4\.00s 5\.00s$/).to_stdout
+ end
+end
diff --git a/spec/nanoc/cli/commands/compile_spec.rb b/spec/nanoc/cli/commands/compile_spec.rb
new file mode 100644
index 0000000..2e6686c
--- /dev/null
+++ b/spec/nanoc/cli/commands/compile_spec.rb
@@ -0,0 +1,64 @@
+describe Nanoc::CLI::Commands::Compile::Listener do
+ let(:klass) do
+ Class.new(described_class) do
+ attr_reader :started
+ attr_reader :stopped
+
+ def initialize
+ @started = false
+ @stopped = false
+ end
+
+ def start
+ @started = true
+ end
+
+ def stop
+ @stopped = true
+ end
+ end
+ end
+
+ subject { klass.new }
+
+ it 'starts' do
+ subject.start
+ expect(subject.started).to be
+ end
+
+ it 'stops' do
+ subject.start
+ subject.stop
+ expect(subject.stopped).to be
+ end
+
+ it 'starts safely' do
+ subject.start_safely
+ expect(subject.started).to be
+ end
+
+ it 'stops safely' do
+ subject.start_safely
+ subject.stop_safely
+ expect(subject.stopped).to be
+ end
+
+ context 'listener that does not start or stop properly' do
+ let(:klass) do
+ Class.new(described_class) do
+ def start
+ raise 'boom'
+ end
+
+ def stop
+ raise 'boom'
+ end
+ end
+ end
+
+ it 'raises on start, but not stop' do
+ expect { subject.start_safely }.to raise_error(RuntimeError)
+ expect { subject.stop_safely }.not_to raise_error
+ end
+ end
+end
diff --git a/spec/nanoc/cli/commands/deploy_spec.rb b/spec/nanoc/cli/commands/deploy_spec.rb
new file mode 100644
index 0000000..4c25168
--- /dev/null
+++ b/spec/nanoc/cli/commands/deploy_spec.rb
@@ -0,0 +1,327 @@
+describe Nanoc::CLI::Commands::Shell, site: true, stdio: true do
+ describe '#run' do
+ let(:config) { {} }
+
+ before do
+ # Prevent double-loading
+ expect(Nanoc::CLI).to receive(:setup)
+
+ File.write('nanoc.yaml', YAML.dump(config))
+ end
+
+ shared_examples 'no effective deploy' do
+ it 'does not write any files' do
+ expect { run rescue nil }.not_to change { Dir['remote/*'] }
+ expect(Dir['remote/*']).to be_empty
+ end
+ end
+
+ shared_examples 'effective deploy' do
+ it 'writes files' do
+ expect { run }.to change { Dir['remote/*'] }.from([]).to(['remote/success.txt'])
+ expect(File.read('remote/success.txt')).to eql('hurrah')
+ end
+ end
+
+ shared_examples 'attempted/effective deploy' do
+ context 'no checks' do
+ include_examples 'effective deploy'
+ end
+
+ context 'checks fail' do
+ before do
+ File.write(
+ 'Checks',
+ "check :donkey do\n" \
+ " add_issue('things are broken', subject: 'success.txt')\n" \
+ "end\n" \
+ "\n" \
+ "deploy_check :donkey\n",
+ )
+ end
+
+ include_examples 'no effective deploy'
+
+ context 'checks disabled' do
+ context '--no-check' do
+ let(:command) { super() + ['--no-check'] }
+ include_examples 'effective deploy'
+ end
+
+ context '--Ck' do
+ let(:command) { super() + ['-C'] }
+ include_examples 'effective deploy'
+ end
+ end
+ end
+
+ context 'checks pass' do
+ before do
+ File.write(
+ 'Checks',
+ "check :donkey do\n" \
+ "end\n" \
+ "\n" \
+ "deploy_check :donkey\n",
+ )
+ end
+
+ include_examples 'effective deploy'
+ end
+ end
+
+ describe 'listing deployers' do
+ shared_examples 'lists all deployers' do
+ let(:run) { Nanoc::CLI.run(command) }
+
+ it 'lists all deployers' do
+ expect { run }.to output(/Available deployers:\n fog\n rsync/).to_stdout
+ end
+
+ include_examples 'no effective deploy'
+ end
+
+ context '--list-deployers' do
+ let(:command) { %w(deploy --list-deployers) }
+ include_examples 'lists all deployers'
+ end
+
+ context '-D' do
+ let(:command) { %w(deploy -D) }
+ include_examples 'lists all deployers'
+ end
+ end
+
+ describe 'listing deployment configurations' do
+ shared_examples 'lists all deployment configurations' do
+ let(:run) { Nanoc::CLI.run(command) }
+
+ context 'no deployment configurations' do
+ let(:config) { { donkeys: 'lots' } }
+
+ it 'says nothing is found' do
+ expect { run }.to output(/No deployment configurations./).to_stdout
+ end
+
+ include_examples 'no effective deploy'
+ end
+
+ context 'some deployment configurations' do
+ let(:config) do
+ {
+ deploy: {
+ production: {
+ kind: 'rsync',
+ dst: 'remote',
+ },
+ staging: {
+ kind: 'rsync',
+ dst: 'remote',
+ },
+ },
+ }
+ end
+
+ it 'says some targets are found' do
+ expect { run }.to output(/Available deployment configurations:\n production\n staging/).to_stdout
+ end
+
+ include_examples 'no effective deploy'
+ end
+ end
+
+ context '--list' do
+ let(:command) { %w(deploy --list) }
+ include_examples 'lists all deployment configurations'
+ end
+
+ context '-L' do
+ let(:command) { %w(deploy -L) }
+ include_examples 'lists all deployment configurations'
+ end
+ end
+
+ describe 'deploying' do
+ let(:run) { Nanoc::CLI.run(command) }
+ let(:command) { %w(deploy) }
+
+ before do
+ FileUtils.mkdir_p('output')
+ FileUtils.mkdir_p('remote')
+ File.write('output/success.txt', 'hurrah')
+ end
+
+ shared_examples 'missing kind warning' do
+ it 'warns about missing kind' do
+ expect { run }.to output(/Warning: The specified deploy target does not have a kind attribute. Assuming rsync./).to_stderr
+ end
+ end
+
+ context 'no deploy configs' do
+ it 'errors' do
+ expect { run }.to raise_error(
+ Nanoc::Int::Errors::GenericTrivial,
+ 'The site has no deployment configurations.',
+ )
+ end
+
+ include_examples 'no effective deploy'
+
+ context 'configuration created in preprocessor' do
+ before do
+ File.write(
+ 'Rules',
+ "preprocess do\n" \
+ " @config[:deploy] = {\n" \
+ " default: { dst: 'remote' },\n" \
+ " }\n" \
+ "end\n\n" + File.read('Rules'),
+ )
+ end
+
+ include_examples 'attempted/effective deploy'
+ end
+ end
+
+ context 'some deploy configs' do
+ let(:config) do
+ {
+ deploy: {
+ irrelevant: {
+ kind: 'rsync',
+ dst: 'remote',
+ },
+ },
+ }
+ end
+
+ context 'default target' do
+ context 'requested deploy config does not exist' do
+ it 'errors' do
+ expect { run }.to raise_error(
+ Nanoc::Int::Errors::GenericTrivial,
+ 'The site has no deployment configuration named `default`.',
+ )
+ end
+
+ include_examples 'no effective deploy'
+ end
+
+ context 'requested deploy config exists' do
+ let(:config) do
+ {
+ deploy: {
+ default: {
+ kind: 'rsync',
+ dst: 'remote',
+ },
+ },
+ }
+ end
+
+ include_examples 'attempted/effective deploy'
+
+ context 'dry run' do
+ let(:command) { super() + ['--dry-run'] }
+ include_examples 'no effective deploy'
+ end
+ end
+
+ context 'requested deploy config exists, but has no kind' do
+ let(:config) do
+ {
+ deploy: {
+ default: {
+ dst: 'remote',
+ },
+ },
+ }
+ end
+
+ include_examples 'attempted/effective deploy'
+ include_examples 'missing kind warning'
+
+ context 'dry run' do
+ let(:command) { super() + ['--dry-run'] }
+ include_examples 'no effective deploy'
+ end
+ end
+ end
+
+ shared_examples 'deploy with non-default target' do
+ context 'requested deploy config does not exist' do
+ it 'errors' do
+ expect { run }.to raise_error(
+ Nanoc::Int::Errors::GenericTrivial,
+ 'The site has no deployment configuration named `production`.',
+ )
+ end
+
+ include_examples 'no effective deploy'
+ end
+
+ context 'requested deploy config exists' do
+ let(:config) do
+ {
+ deploy: {
+ production: {
+ kind: 'rsync',
+ dst: 'remote',
+ },
+ },
+ }
+ end
+
+ include_examples 'attempted/effective deploy'
+
+ context 'dry run' do
+ let(:command) { (super() + ['--dry-run']) }
+ include_examples 'no effective deploy'
+ end
+ end
+
+ context 'requested deploy config exists, but has no kind' do
+ let(:config) do
+ {
+ deploy: {
+ production: {
+ dst: 'remote',
+ },
+ },
+ }
+ end
+
+ include_examples 'attempted/effective deploy'
+ include_examples 'missing kind warning'
+
+ context 'dry run' do
+ let(:command) { (super() + ['--dry-run']) }
+ include_examples 'no effective deploy'
+ end
+ end
+ end
+
+ context 'non-default target, specified as argument' do
+ let(:command) { %w(deploy production) }
+ include_examples 'deploy with non-default target'
+ end
+
+ context 'non-default target, specified as option (--target)' do
+ let(:command) { %w(deploy --target production) }
+ include_examples 'deploy with non-default target'
+ end
+
+ context 'multiple targets specified' do
+ let(:command) { %w(deploy --target staging production) }
+
+ it 'errors' do
+ expect { run }.to raise_error(
+ Nanoc::Int::Errors::GenericTrivial,
+ 'Only one deployment target can be specified on the command line.',
+ )
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/nanoc/cli/commands/shell_spec.rb b/spec/nanoc/cli/commands/shell_spec.rb
new file mode 100644
index 0000000..a38aa55
--- /dev/null
+++ b/spec/nanoc/cli/commands/shell_spec.rb
@@ -0,0 +1,54 @@
+describe Nanoc::CLI::Commands::Shell, site: true, stdio: true do
+ describe '#run' do
+ before do
+ # Prevent double-loading
+ expect(Nanoc::CLI).to receive(:setup)
+ end
+
+ it 'can be invoked' do
+ context = Object.new
+ allow(Nanoc::Int::Context).to receive(:new).with(anything).and_return(context)
+ expect(context).to receive(:pry)
+
+ Nanoc::CLI.run(['shell'])
+ end
+ end
+
+ describe '#env_for_site' do
+ subject { described_class.env_for_site(site) }
+
+ before do
+ File.write('content/hello.md', 'Hello!')
+ File.write('layouts/default.erb', '<title>MY SITE!</title><%= yield %>')
+ end
+
+ let(:site) do
+ Nanoc::Int::SiteLoader.new.new_from_cwd
+ end
+
+ it 'returns views' do
+ expect(subject[:items]).to be_a(Nanoc::ItemCollectionWithRepsView)
+ expect(subject[:layouts]).to be_a(Nanoc::LayoutCollectionView)
+ expect(subject[:config]).to be_a(Nanoc::ConfigView)
+ end
+
+ it 'returns correct items' do
+ expect(subject[:items].size).to eq(1)
+ expect(subject[:items].first.identifier.to_s).to eq('/hello.md')
+ end
+
+ it 'returns correct layouts' do
+ expect(subject[:layouts].size).to eq(1)
+ expect(subject[:layouts].first.identifier.to_s).to eq('/default.erb')
+ end
+
+ it 'returns items with reps' do
+ expect(subject[:items].first.reps).not_to be_nil
+ expect(subject[:items].first.reps.first.name).to eq(:default)
+ end
+
+ it 'returns items with rep paths' do
+ expect(subject[:items].first.reps.first.path).to eq('/hello.md')
+ end
+ end
+end
diff --git a/spec/nanoc/cli/commands/show_data_spec.rb b/spec/nanoc/cli/commands/show_data_spec.rb
new file mode 100644
index 0000000..0cf10d9
--- /dev/null
+++ b/spec/nanoc/cli/commands/show_data_spec.rb
@@ -0,0 +1,126 @@
+describe Nanoc::CLI::Commands::ShowData, stdio: true do
+ describe '#print_item_dependencies' do
+ subject { runner.send(:print_item_dependencies, items, dependency_store) }
+
+ let(:runner) do
+ described_class.new(options, arguments, command)
+ end
+
+ let(:options) { {} }
+ let(:arguments) { [] }
+ let(:command) { double(:command) }
+
+ let(:items) do
+ Nanoc::Int::IdentifiableCollection.new(config).tap do |ic|
+ ic << item_about
+ ic << item_dog
+ ic << item_other
+ end
+ end
+
+ let(:item_about) { Nanoc::Int::Item.new('About Me', {}, '/about.md') }
+ let(:item_dog) { Nanoc::Int::Item.new('About My Dog', {}, '/dog.md') }
+ let(:item_other) { Nanoc::Int::Item.new('Raw Data', {}, '/other.dat') }
+
+ let(:config) { Nanoc::Int::Configuration.new }
+
+ let(:dependency_store) do
+ Nanoc::Int::DependencyStore.new(objects)
+ end
+
+ let(:objects) do
+ items.to_a + layouts.to_a
+ end
+
+ let(:layouts) do
+ Nanoc::Int::IdentifiableCollection.new(config).tap do |ic|
+ end
+ end
+
+ it 'prints a legend' do
+ expect { subject }.to output(%r{Item dependencies =+\n\nLegend:}).to_stdout
+ end
+
+ context 'no dependencies' do
+ it 'outputs no dependencies for /about.md' do
+ expect { subject }.to output(%r{^item /about.md depends on:\n \(nothing\)$}m).to_stdout
+ end
+
+ it 'outputs no dependencies for /dog.md' do
+ expect { subject }.to output(%r{^item /dog.md depends on:\n \(nothing\)$}m).to_stdout
+ end
+
+ it 'outputs no dependencies for /other.dat' do
+ expect { subject }.to output(%r{^item /other.dat depends on:\n \(nothing\)$}m).to_stdout
+ end
+ end
+
+ context 'dependency (without props) from about to dog' do
+ before do
+ dependency_store.record_dependency(item_dog, item_about)
+ end
+
+ it 'outputs no dependencies for /about.md' do
+ expect { subject }.to output(%r{^item /about.md depends on:\n \(nothing\)$}m).to_stdout
+ end
+
+ it 'outputs dependencies for /dog.md' do
+ expect { subject }.to output(%r{^item /dog.md depends on:\n \[ item \] \(racp\) /about.md$}m).to_stdout
+ end
+
+ it 'outputs no dependencies for /other.dat' do
+ expect { subject }.to output(%r{^item /other.dat depends on:\n \(nothing\)$}m).to_stdout
+ end
+ end
+
+ context 'dependency (with raw_content prop) from about to dog' do
+ before do
+ dependency_store.record_dependency(item_dog, item_about, raw_content: true)
+ end
+
+ it 'outputs dependencies for /dog.md' do
+ expect { subject }.to output(%r{^item /dog.md depends on:\n \[ item \] \(r___\) /about.md$}m).to_stdout
+ end
+ end
+
+ context 'dependency (with attributes prop) from about to dog' do
+ before do
+ dependency_store.record_dependency(item_dog, item_about, attributes: true)
+ end
+
+ it 'outputs dependencies for /dog.md' do
+ expect { subject }.to output(%r{^item /dog.md depends on:\n \[ item \] \(_a__\) /about.md$}m).to_stdout
+ end
+ end
+
+ context 'dependency (with compiled_content prop) from about to dog' do
+ before do
+ dependency_store.record_dependency(item_dog, item_about, compiled_content: true)
+ end
+
+ it 'outputs dependencies for /dog.md' do
+ expect { subject }.to output(%r{^item /dog.md depends on:\n \[ item \] \(__c_\) /about.md$}m).to_stdout
+ end
+ end
+
+ context 'dependency (with path prop) from about to dog' do
+ before do
+ dependency_store.record_dependency(item_dog, item_about, path: true)
+ end
+
+ it 'outputs dependencies for /dog.md' do
+ expect { subject }.to output(%r{^item /dog.md depends on:\n \[ item \] \(___p\) /about.md$}m).to_stdout
+ end
+ end
+
+ context 'dependency (with multiple props) from about to dog' do
+ before do
+ dependency_store.record_dependency(item_dog, item_about, attributes: true, raw_content: true)
+ end
+
+ it 'outputs dependencies for /dog.md' do
+ expect { subject }.to output(%r{^item /dog.md depends on:\n \[ item \] \(ra__\) /about.md$}m).to_stdout
+ end
+ end
+ end
+end
diff --git a/spec/nanoc/cli/commands/show_rules_spec.rb b/spec/nanoc/cli/commands/show_rules_spec.rb
new file mode 100644
index 0000000..a4f8121
--- /dev/null
+++ b/spec/nanoc/cli/commands/show_rules_spec.rb
@@ -0,0 +1,112 @@
+describe Nanoc::CLI::Commands::ShowRules, stdio: true do
+ describe '#run' do
+ subject { runner.run }
+
+ let(:runner) do
+ described_class.new(options, arguments, command).tap do |runner|
+ runner.site = site
+ end
+ end
+
+ let(:options) { {} }
+
+ let(:arguments) { [] }
+
+ let(:command) { double(:command) }
+
+ let(:site) do
+ double(
+ :site,
+ items: items,
+ layouts: layouts,
+ compiler: compiler,
+ )
+ end
+
+ let(:items) do
+ Nanoc::Int::IdentifiableCollection.new(config).tap do |ic|
+ ic << Nanoc::Int::Item.new('About Me', {}, '/about.md')
+ ic << Nanoc::Int::Item.new('About My Dog', {}, '/dog.md')
+ ic << Nanoc::Int::Item.new('Raw Data', {}, '/other.dat')
+ end
+ end
+
+ let(:reps) do
+ Nanoc::Int::ItemRepRepo.new.tap do |reps|
+ reps << Nanoc::Int::ItemRep.new(items['/about.md'], :default)
+ reps << Nanoc::Int::ItemRep.new(items['/about.md'], :text)
+ reps << Nanoc::Int::ItemRep.new(items['/dog.md'], :default)
+ reps << Nanoc::Int::ItemRep.new(items['/dog.md'], :text)
+ reps << Nanoc::Int::ItemRep.new(items['/other.dat'], :default)
+ end
+ end
+
+ let(:layouts) do
+ Nanoc::Int::IdentifiableCollection.new(config).tap do |ic|
+ ic << Nanoc::Int::Layout.new('Default', {}, '/default.erb')
+ ic << Nanoc::Int::Layout.new('Article', {}, '/article.haml')
+ ic << Nanoc::Int::Layout.new('Other', {}, '/other.xyzzy')
+ end
+ end
+
+ let(:config) { Nanoc::Int::Configuration.new }
+
+ let(:action_provider) { double(:action_provider, rules_collection: rules_collection) }
+ let(:compiler) { double(:compiler, action_provider: action_provider, reps: reps) }
+
+ let(:rules_collection) do
+ Nanoc::RuleDSL::RulesCollection.new.tap do |rc|
+ rc.add_item_compilation_rule(
+ Nanoc::RuleDSL::Rule.new(Nanoc::Int::Pattern.from('/dog.*'), :default, proc {}),
+ )
+ rc.add_item_compilation_rule(
+ Nanoc::RuleDSL::Rule.new(Nanoc::Int::Pattern.from('/*.md'), :default, proc {}),
+ )
+ rc.add_item_compilation_rule(
+ Nanoc::RuleDSL::Rule.new(Nanoc::Int::Pattern.from('/**/*'), :text, proc {}),
+ )
+
+ rc.layout_filter_mapping[Nanoc::Int::Pattern.from('/*.haml')] = [:haml, {}]
+ rc.layout_filter_mapping[Nanoc::Int::Pattern.from('/*.erb')] = [:erb, {}]
+ end
+ end
+
+ let(:expected_out) do
+ <<-EOS
+ \e[1m\e[33mItem /about.md\e[0m:
+ Rep default: /*.md
+ Rep text: /**/*
+
+ \e[1m\e[33mItem /dog.md\e[0m:
+ Rep default: /dog.*
+ Rep text: /**/*
+
+ \e[1m\e[33mItem /other.dat\e[0m:
+ Rep default: (none)
+
+ \e[1m\e[33mLayout /article.haml\e[0m:
+ /*.haml
+
+ \e[1m\e[33mLayout /default.erb\e[0m:
+ /*.erb
+
+ \e[1m\e[33mLayout /other.xyzzy\e[0m:
+ (none)
+
+ EOS
+ .gsub(/^ {8}/, '')
+ end
+
+ before do
+ expect(compiler).to receive(:build_reps).once
+ end
+
+ it 'writes item and layout rules to stdout' do
+ expect { subject }.to output(expected_out).to_stdout
+ end
+
+ it 'writes status informaion to stderr' do
+ expect { subject }.to output("Loading site… done\n").to_stderr
+ end
+ end
+end
diff --git a/spec/nanoc/cli/commands/view_spec.rb b/spec/nanoc/cli/commands/view_spec.rb
new file mode 100644
index 0000000..2b64b19
--- /dev/null
+++ b/spec/nanoc/cli/commands/view_spec.rb
@@ -0,0 +1,58 @@
+describe Nanoc::CLI::Commands::View, site: true, stdio: true do
+ describe '#run' do
+ def run_nanoc_cmd(cmd)
+ pid = fork { Nanoc::CLI.run(cmd) }
+
+ # Wait for server to start up
+ 20.times do |i|
+ begin
+ Net::HTTP.get('127.0.0.1', '/', 50_385)
+ rescue Errno::ECONNREFUSED, Errno::ECONNRESET
+ sleep(0.1 * 1.2**i)
+ retry
+ end
+ break
+ end
+
+ yield
+ ensure
+ Process.kill('TERM', pid)
+ end
+
+ context 'default configuration' do
+ it 'serves /index.html as /' do
+ File.write('output/index.html', 'Hello there! Nanoc loves you! <3')
+ run_nanoc_cmd(['view', '--port', '50385']) do
+ expect(Net::HTTP.get('127.0.0.1', '/', 50_385)).to eql('Hello there! Nanoc loves you! <3')
+ end
+ end
+
+ it 'does not serve /index.xhtml as /' do
+ File.write('output/index.xhtml', 'Hello there! Nanoc loves you! <3')
+ run_nanoc_cmd(['view', '--port', '50385']) do
+ expect(Net::HTTP.get('127.0.0.1', '/', 50_385)).to eql("File not found: /\n")
+ end
+ end
+ end
+
+ context 'index_filenames including index.xhtml' do
+ before do
+ File.write('nanoc.yaml', 'index_filenames: [index.xhtml]')
+ end
+
+ it 'serves /index.xhtml as /' do
+ File.write('output/index.xhtml', 'Hello there! Nanoc loves you! <3')
+ run_nanoc_cmd(['view', '--port', '50385']) do
+ expect(Net::HTTP.get('127.0.0.1', '/', 50_385)).to eql('Hello there! Nanoc loves you! <3')
+ end
+ end
+ end
+
+ it 'does not serve other files as /' do
+ File.write('output/index.html666', 'Hello there! Nanoc loves you! <3')
+ run_nanoc_cmd(['view', '--port', '50385']) do
+ expect(Net::HTTP.get('127.0.0.1', '/', 50_385)).to eql("File not found: /\n")
+ end
+ end
+ end
+end
diff --git a/spec/nanoc/data_sources/filesystem_spec.rb b/spec/nanoc/data_sources/filesystem_spec.rb
new file mode 100644
index 0000000..90c02e6
--- /dev/null
+++ b/spec/nanoc/data_sources/filesystem_spec.rb
@@ -0,0 +1,56 @@
+describe Nanoc::DataSources::Filesystem do
+ let(:data_source) { Nanoc::DataSources::Filesystem.new(site.config, nil, nil, params) }
+ let(:params) { {} }
+ let(:site) { Nanoc::Int::SiteLoader.new.new_empty }
+
+ before { Timecop.freeze(now) }
+ after { Timecop.return }
+
+ let(:now) { Time.local(2008, 1, 2, 14, 5, 0) }
+
+ describe '#load_objects' do
+ subject { data_source.send(:load_objects, 'foo', klass) }
+
+ let(:klass) { raise 'override me' }
+
+ context 'items' do
+ let(:klass) { Nanoc::Int::Item }
+
+ context 'no files' do
+ it 'loads nothing' do
+ expect(subject).to be_empty
+ end
+ end
+
+ context 'one regular file' do
+ before do
+ FileUtils.mkdir_p('foo')
+ File.write('foo/bar.html', "---\nnum: 1\n---\ntest 1")
+ FileUtils.touch('foo/bar.html', mtime: now)
+ end
+
+ let(:expected_attributes) do
+ {
+ content_filename: 'foo/bar.html',
+ extension: 'html',
+ filename: 'foo/bar.html',
+ meta_filename: nil,
+ mtime: now,
+ num: 1,
+ }
+ end
+
+ it 'loads that file' do
+ expect(subject.size).to eq(1)
+
+ expect(subject[0].content.string).to eq('test 1')
+ expect(subject[0].attributes).to eq(expected_attributes)
+ expect(subject[0].identifier).to eq(Nanoc::Identifier.new('/bar/'))
+ expect(subject[0].checksum_data).to be_nil
+ expect(subject[0].content_checksum_data).to eq('test 1')
+ expect(subject[0].attributes_checksum_data).to eq("num: 1\n")
+ end
+ end
+ end
+ end
+end
diff --git a/spec/nanoc/deploying/fog_spec.rb b/spec/nanoc/deploying/fog_spec.rb
new file mode 100644
index 0000000..e6550c0
--- /dev/null
+++ b/spec/nanoc/deploying/fog_spec.rb
@@ -0,0 +1,193 @@
+describe Nanoc::Deploying::Deployers::Fog, stdio: true do
+ let(:deployer) do
+ Nanoc::Deploying::Deployers::Fog.new(
+ 'output/',
+ config,
+ dry_run: is_dry_run,
+ )
+ end
+
+ let(:is_dry_run) { false }
+
+ let(:config) do
+ {
+ bucket: 'bucky',
+ provider: 'local',
+ local_root: 'remote',
+ }
+ end
+
+ before do
+ # create output
+ FileUtils.mkdir_p('output')
+ FileUtils.mkdir_p('output/etc')
+ File.open('output/woof', 'w') { |io| io.write 'I am a dog!' }
+ File.open('output/etc/meow', 'w') { |io| io.write 'I am a cat!' }
+
+ # create local cloud
+ FileUtils.mkdir_p('remote')
+ end
+
+ subject { deployer.run }
+
+ shared_examples 'no effective deploy' do
+ it 'does not modify remote' do
+ expect { subject }.not_to change { Dir['remote/**/*'].sort }
+ end
+ end
+
+ shared_examples 'effective deploy' do
+ it 'modifies remote' do
+ expect { subject }.to change { Dir['remote/**/*'].sort }
+ .to([
+ 'remote/bucky',
+ 'remote/bucky/etc',
+ 'remote/bucky/etc/meow',
+ 'remote/bucky/woof',
+ ])
+ end
+ end
+
+ context 'dry run' do
+ let(:is_dry_run) { true }
+
+ before do
+ FileUtils.mkdir_p('remote/bucky')
+ FileUtils.mkdir_p('remote/bucky/tiny')
+ File.write('remote/bucky/pig', 'oink?')
+ File.write('remote/bucky/tiny/piglet', 'little oink?')
+ end
+
+ include_examples 'no effective deploy'
+
+ context 'with CDN ID' do
+ let(:config) { super().merge(cdn_id: 'donkey-cdn') }
+
+ let(:cdn) { Object.new }
+ let(:distribution) { Object.new }
+
+ it 'does not actually invalidate' do
+ expect(::Fog::CDN).to receive(:new).with(provider: 'local', local_root: 'remote').and_return(cdn)
+ expect(cdn).to receive(:get_distribution).with('donkey-cdn').and_return(distribution)
+
+ subject
+ end
+ end
+ end
+
+ context 'effective run' do
+ include_examples 'effective deploy'
+
+ context 'custom path' do
+ context 'custom path ends with /' do
+ let(:config) do
+ super().merge(path: 'foo/')
+ end
+
+ it 'raises' do
+ expect { subject }.to raise_error('The path `foo/` is not supposed to have a trailing slash')
+ end
+ end
+
+ context 'custom path does not end with /' do
+ let(:config) do
+ super().merge(path: 'foo')
+ end
+
+ it 'modifies remote' do
+ expect { subject }.to change { Dir['remote/**/*'].sort }
+ .to([
+ 'remote/bucky',
+ 'remote/bucky/foo',
+ 'remote/bucky/foo/etc',
+ 'remote/bucky/foo/etc/meow',
+ 'remote/bucky/foo/woof',
+ ])
+ end
+ end
+ end
+
+ context 'bucket already exists' do
+ before do
+ FileUtils.mkdir_p('remote/bucky')
+ end
+
+ include_examples 'effective deploy'
+ end
+
+ context 'remote contains stale file at root' do
+ before do
+ FileUtils.mkdir_p('remote/bucky')
+ File.write('remote/bucky/pig', 'oink?')
+ end
+
+ include_examples 'effective deploy'
+
+ it 'does not contain stale files' do
+ subject
+ expect(Dir['remote/**/*'].sort).not_to include('remote/bucky/pig')
+ end
+ end
+
+ context 'remote contains stale file in subdirectory' do
+ before do
+ FileUtils.mkdir_p('remote/bucky/secret')
+ File.write('remote/bucky/secret/pig', 'oink?')
+ end
+
+ include_examples 'effective deploy'
+
+ it 'does not contain stale files' do
+ subject
+ expect(Dir['remote/**/*'].sort).not_to include('remote/bucky/secret/pig')
+ end
+ end
+
+ context 'with CDN ID' do
+ let(:config) { super().merge(cdn_id: 'donkey-cdn') }
+
+ let(:cdn) { Object.new }
+ let(:distribution) { Object.new }
+
+ it 'invalidates' do
+ expect(::Fog::CDN).to receive(:new).with(provider: 'local', local_root: 'remote').and_return(cdn)
+ expect(cdn).to receive(:get_distribution).with('donkey-cdn').and_return(distribution)
+ expect(cdn).to receive(:post_invalidation).with(distribution, contain_exactly('etc/meow', 'woof'))
+
+ subject
+ end
+ end
+
+ context 'remote list consists of truncated sets' do
+ before do
+ expect(::Fog::Storage).to receive(:new).and_return(fog_storage)
+ expect(fog_storage).to receive(:directories).and_return(directories)
+ expect(directories).to receive(:get).and_return(directory)
+ allow(directory).to receive(:files).and_return(files)
+ expect(files).to receive(:get).with('stray').and_return(file_stray).ordered
+ expect(files).to receive(:each)
+ .and_yield(double(:woof, key: 'woof'))
+ .and_yield(double(:meow, key: 'etc/meow'))
+ .and_yield(double(:stray, key: 'stray'))
+ expect(file_stray).to receive(:destroy)
+
+ expect(files).to receive(:create).with(key: 'woof', body: anything, public: true) do
+ FileUtils.mkdir_p('remote/bucky')
+ File.write('remote/bucky/woof', 'hi')
+ end
+ expect(files).to receive(:create).with(key: 'etc/meow', body: anything, public: true) do
+ FileUtils.mkdir_p('remote/bucky/etc')
+ File.write('remote/bucky/etc/meow', 'hi')
+ end
+ end
+
+ let(:fog_storage) { double(:fog_storage) }
+ let(:directories) { double(:directories) }
+ let(:directory) { double(:directory) }
+ let(:files) { double(:files) }
+ let(:file_stray) { double(:file_stray) }
+
+ include_examples 'effective deploy'
+ end
+ end
+end
diff --git a/spec/nanoc/extra/parallel_collection_spec.rb b/spec/nanoc/extra/parallel_collection_spec.rb
new file mode 100644
index 0000000..4674093
--- /dev/null
+++ b/spec/nanoc/extra/parallel_collection_spec.rb
@@ -0,0 +1,108 @@
+describe Nanoc::Extra::ParallelCollection do
+ subject(:col) { described_class.new(wrapped, parallelism: parallelism) }
+ let(:wrapped) { [1, 2, 3, 4, 5] }
+ let(:parallelism) { 5 }
+
+ describe '#each' do
+ subject do
+ col.each do |e|
+ sleep 0.1
+ out << e
+ end
+ end
+ let!(:out) { [] }
+
+ it 'is fast' do
+ expect { subject }.to finish_in_under(0.2).seconds
+ end
+
+ it 'is correct' do
+ expect { subject }.to change { out.sort }.from([]).to([1, 2, 3, 4, 5])
+ end
+
+ it 'does not leave threads lingering' do
+ expect { subject }.not_to change { Thread.list.size }
+ end
+
+ context 'errors' do
+ subject do
+ col.each do |e|
+ if e == 1
+ sleep 0.02
+ raise 'ugh'
+ else
+ sleep 0.1
+ out << e
+ end
+ end
+ end
+
+ let(:parallelism) { 3 }
+
+ it 'raises' do
+ expect { subject }.to raise_error(RuntimeError, 'ugh')
+ end
+
+ it 'aborts early' do
+ expect { subject rescue nil }.to change { out.sort }.from([]).to([2, 3])
+ end
+ end
+
+ context 'low parallelism' do
+ let(:parallelism) { 1 }
+
+ it 'is not fast' do
+ expect { subject }.not_to finish_in_under(0.5).seconds
+ end
+ end
+ end
+
+ describe '#map' do
+ subject do
+ col.map do |e|
+ sleep 0.1
+ e * 10
+ end
+ end
+
+ it 'is fast' do
+ expect { subject }.to finish_in_under(0.2).seconds
+ end
+
+ it 'does not leave threads lingering' do
+ expect { subject }.not_to change { Thread.list.size }
+ end
+
+ it 'is correct' do
+ expect(subject.sort).to eq([10, 20, 30, 40, 50])
+ end
+
+ context 'errors' do
+ subject do
+ col.each do |e|
+ if e == 1
+ sleep 0.02
+ raise 'ugh'
+ else
+ sleep 0.1
+ e * 10
+ end
+ end
+ end
+
+ let(:parallelism) { 3 }
+
+ it 'raises' do
+ expect { subject }.to raise_error(RuntimeError, 'ugh')
+ end
+ end
+
+ context 'low parallelism' do
+ let(:parallelism) { 1 }
+
+ it 'is not fast' do
+ expect { subject }.not_to finish_in_under(0.5).seconds
+ end
+ end
+ end
+end
diff --git a/spec/nanoc/filters/colorize_syntax/rouge_spec.rb b/spec/nanoc/filters/colorize_syntax/rouge_spec.rb
new file mode 100644
index 0000000..1026fcf
--- /dev/null
+++ b/spec/nanoc/filters/colorize_syntax/rouge_spec.rb
@@ -0,0 +1,195 @@
+require 'rouge'
+
+describe Nanoc::Filters::ColorizeSyntax, filter: true, rouge: true do
+ subject { filter.setup_and_run(input, default_colorizer: :rouge, rouge: params) }
+ let(:filter) { ::Nanoc::Filters::ColorizeSyntax.new }
+ let(:params) { {} }
+ let(:wrap) { false }
+ let(:css_class) { 'highlight' }
+ let(:input) do
+ <<-EOS.freeze
+before
+<pre><code class="language-ruby">
+ def foo
+ end
+</code></pre>
+after
+ EOS
+ end
+ let(:output) do
+ <<-EOS
+before
+<pre><code class=\"language-ruby#{wrap ? " #{css_class}" : ''}\"> <span class=\"k\">def</span> <span class=\"nf\">foo</span>
+ <span class=\"k\">end</span></code></pre>
+after
+ EOS
+ end
+
+ context 'with Rouge' do
+ context 'with 1.x', if: Rouge.version < '2' do
+ context 'with default options' do
+ it { is_expected.to eql output }
+ end
+
+ context 'with pygments wrapper' do
+ let(:wrap) { true }
+ let(:params) { super().merge(wrap: wrap) }
+
+ it { is_expected.to eql output }
+
+ context 'with css_class' do
+ let(:css_class) { 'nanoc' }
+ let(:params) { super().merge(css_class: css_class) }
+
+ it { is_expected.to eql output }
+ end
+ end
+
+ context 'with line number' do
+ let(:line_numbers) { true }
+ let(:params) { super().merge(line_numbers: line_numbers) }
+ let(:output) do
+ <<-EOS
+before
+<pre><code class="language-ruby"><table style="border-spacing: 0"><tbody><tr>
+<td class="gutter gl" style="text-align: right"><pre class="lineno">1
+2</pre></td>
+<td class="code"><pre> <span class="k">def</span> <span class="nf">foo</span>
+ <span class="k">end</span><span class="w">
+</span></pre></td>
+</tr></tbody></table></code></pre>
+after
+ EOS
+ end
+
+ it { is_expected.to eql output }
+ end
+ end
+
+ context 'with 2.x', if: Rouge.version >= '2' do
+ context 'with default options' do
+ it { is_expected.to eql output }
+ end
+
+ context 'with legacy' do
+ let(:legacy) { true }
+ let(:params) { super().merge(legacy: legacy) }
+
+ it { is_expected.to eql output }
+
+ context 'with pygments wrapper' do
+ let(:wrap) { true }
+ let(:params) { super().merge(wrap: wrap) }
+
+ it { is_expected.to eql output }
+
+ context 'with css_class' do
+ let(:css_class) { 'nanoc' }
+ let(:params) { super().merge(css_class: css_class) }
+
+ it { is_expected.to eql output }
+ end
+ end
+
+ context 'with line number' do
+ let(:line_numbers) { true }
+ let(:params) { super().merge(line_numbers: line_numbers) }
+ let(:output) do
+ <<-EOS
+before
+<pre><code class="language-ruby"><table class="rouge-table"><tbody><tr>
+<td class="rouge-gutter gl"><pre class="lineno">1
+2
+</pre></td>
+<td class="rouge-code"><pre> <span class="k">def</span> <span class="nf">foo</span>
+ <span class="k">end</span></pre></td>
+</tr></tbody></table></code></pre>
+after
+ EOS
+ end
+
+ it { is_expected.to eql output }
+ end
+ end
+
+ context 'with formater' do
+ let(:params) { super().merge(formatter: formatter) }
+
+ context 'with inline' do
+ let(:formatter) { Rouge::Formatters::HTMLInline.new(theme) }
+
+ context 'with github theme' do
+ let(:theme) { Rouge::Themes::Github.new }
+ let(:output) do
+ <<-EOS
+before
+<pre><code class="language-ruby"> <span style="color: #000000;font-weight: bold">def</span> <span style="color: #990000;font-weight: bold">foo</span>
+ <span style="color: #000000;font-weight: bold">end</span></code></pre>
+after
+ EOS
+ end
+
+ it { is_expected.to eql output }
+ end
+
+ context 'with colorful theme' do
+ let(:theme) { Rouge::Themes::Colorful.new }
+ let(:output) do
+ <<-EOS
+before
+<pre><code class="language-ruby"> <span style="color: #080;font-weight: bold">def</span> <span style="color: #06B;font-weight: bold">foo</span>
+ <span style="color: #080;font-weight: bold">end</span></code></pre>
+after
+ EOS
+ end
+
+ it { is_expected.to eql output }
+ end
+ end
+
+ context 'with linewise' do
+ let(:formatter) { Rouge::Formatters::HTMLLinewise.new(Rouge::Formatters::HTML.new) }
+ let(:output) do
+ <<-EOS
+before
+<pre><code class="language-ruby"><div class="line-1"> <span class="k">def</span> <span class="nf">foo</span>
+</div>
+<div class="line-2"> <span class="k">end</span>
+</div></code></pre>
+after
+ EOS
+ end
+
+ it { is_expected.to eql output }
+ end
+
+ context 'with pygments' do
+ let(:wrap) { true }
+ let(:css_class) { 'codehilite' }
+ let(:formatter) { Rouge::Formatters::HTMLPygments.new(Rouge::Formatters::HTML.new) }
+
+ it { is_expected.to eql output }
+ end
+
+ context 'with table' do
+ let(:formatter) { Rouge::Formatters::HTMLTable.new(Rouge::Formatters::HTML.new) }
+ let(:output) do
+ <<-EOS
+before
+<pre><code class="language-ruby"><table class="rouge-table"><tbody><tr>
+<td class="rouge-gutter gl"><pre class="lineno">1
+2
+</pre></td>
+<td class="rouge-code"><pre> <span class="k">def</span> <span class="nf">foo</span>
+ <span class="k">end</span></pre></td>
+</tr></tbody></table></code></pre>
+after
+ EOS
+ end
+
+ it { is_expected.to eql output }
+ end
+ end
+ end
+ end
+end
diff --git a/spec/nanoc/filters/less_spec.rb b/spec/nanoc/filters/less_spec.rb
new file mode 100644
index 0000000..9c28b81
--- /dev/null
+++ b/spec/nanoc/filters/less_spec.rb
@@ -0,0 +1,120 @@
+describe Nanoc::Filters::Less, site: true, stdio: true, v8: true do
+ # These tests are high-level in order to interact well with the compiler. This is important for
+ # this :less filter, because of the way it handles fibers.
+
+ before do
+ File.open('Rules', 'w') do |io|
+ io.write "compile '/**/*.less' do\n"
+ io.write " filter :less\n"
+ io.write " write item.identifier.without_ext + '.css'\n"
+ io.write "end\n"
+ end
+ end
+
+ context 'one file' do
+ let(:content_a) { 'p { color: red; }' }
+
+ before do
+ File.write('content/a.less', content_a)
+ end
+
+ it 'compiles a.less' do
+ Nanoc::CLI.run(%w(compile))
+ expect(File.read('output/a.css')).to match(/^p\s*\{\s*color:\s*red;?\s*\}/)
+ end
+
+ context 'with compression' do
+ let(:content_a) { '.foo { bar: a; } .bar { foo: b; }' }
+
+ before do
+ File.open('Rules', 'w') do |io|
+ io.write "compile '/*.less' do\n"
+ io.write " filter :less, compress: true\n"
+ io.write " write item.identifier.without_ext + '.css'\n"
+ io.write "end\n"
+ end
+ end
+
+ it 'compiles and compresses a.less' do
+ Nanoc::CLI.run(%w(compile))
+ expect(File.read('output/a.css')).to match(/^\.foo\{bar:a\}\n?\.bar\{foo:b\}/)
+ end
+ end
+ end
+
+ context 'two files' do
+ let(:content_a) { '@import "b.less";' }
+ let(:content_b) { 'p { color: red; }' }
+
+ before do
+ File.write('content/a.less', content_a)
+ File.write('content/b.less', content_b)
+ end
+
+ it 'compiles a.less' do
+ Nanoc::CLI.run(%w(compile))
+ expect(File.read('output/a.css')).to match(/^p\s*\{\s*color:\s*red;?\s*\}/)
+ end
+
+ it 'recompiles a.less if b.less has changed' do
+ Nanoc::CLI.run(%w(compile))
+
+ File.write('content/b.less', 'p { color: blue; }')
+
+ Nanoc::CLI.run(%w(compile))
+ expect(File.read('output/a.css')).to match(/^p\s*\{\s*color:\s*blue;?\s*\}/)
+ end
+ end
+
+ context 'paths relative to site directory' do
+ let(:content_a) { '@import "content/foo/bar/imported_file.less";' }
+ let(:content_b) { 'p { color: red; }' }
+
+ before do
+ FileUtils.mkdir_p('content/foo/bar')
+
+ File.write('content/a.less', content_a)
+ File.write('content/foo/bar/imported_file.less', content_b)
+ end
+
+ it 'compiles a.less' do
+ Nanoc::CLI.run(%w(compile))
+ expect(File.read('output/a.css')).to match(/^p\s*\{\s*color:\s*red;?\s*\}/)
+ end
+
+ it 'recompiles a.less if b.less has changed' do
+ Nanoc::CLI.run(%w(compile))
+
+ File.write('content/foo/bar/imported_file.less', 'p { color: blue; }')
+
+ Nanoc::CLI.run(%w(compile))
+ expect(File.read('output/a.css')).to match(/^p\s*\{\s*color:\s*blue;?\s*\}/)
+ end
+ end
+
+ context 'paths relative to current file' do
+ let(:content_a) { '@import "bar/imported_file.less";' }
+ let(:content_b) { 'p { color: red; }' }
+
+ before do
+ FileUtils.mkdir_p('content/foo/bar')
+
+ File.write('content/foo/a.less', content_a)
+ File.write('content/foo/bar/imported_file.less', content_b)
+ end
+
+ it 'compiles a.less' do
+ Nanoc::CLI.run(%w(compile))
+ expect(File.read('output/foo/a.css')).to match(/^p\s*\{\s*color:\s*red;?\s*\}/)
+ end
+
+ it 'recompiles a.less if b.less has changed' do
+ Nanoc::CLI.run(%w(compile))
+
+ File.write('content/foo/bar/imported_file.less', 'p { color: blue; }')
+
+ Nanoc::CLI.run(%w(compile))
+ expect(File.read('output/foo/a.css')).to match(/^p\s*\{\s*color:\s*blue;?\s*\}/)
+ end
+ end
+end
diff --git a/spec/nanoc/helpers/blogging_spec.rb b/spec/nanoc/helpers/blogging_spec.rb
new file mode 100644
index 0000000..ca24616
--- /dev/null
+++ b/spec/nanoc/helpers/blogging_spec.rb
@@ -0,0 +1,216 @@
+describe Nanoc::Helpers::Blogging, helper: true do
+ before do
+ allow(ctx.dependency_tracker).to receive(:enter)
+ allow(ctx.dependency_tracker).to receive(:exit)
+ end
+
+ describe '#articles' do
+ subject { helper.articles }
+
+ before do
+ ctx.create_item('blah', { kind: 'item' }, '/0/')
+ ctx.create_item('blah blah', { kind: 'article' }, '/1/')
+ ctx.create_item('blah blah blah', { kind: 'article' }, '/2/')
+ end
+
+ it 'returns the two articles' do
+ expect(subject.map(&:identifier)).to match_array(['/1/', '/2/'])
+ end
+ end
+
+ describe '#sorted_articles' do
+ subject { helper.sorted_articles }
+
+ before do
+ attrs = { kind: 'item' }
+ ctx.create_item('blah', attrs, '/0/')
+
+ attrs = { kind: 'article', created_at: (Date.today - 1).to_s }
+ ctx.create_item('blah blah', attrs, '/1/')
+
+ attrs = { kind: 'article', created_at: (Time.now - 500).to_s }
+ ctx.create_item('blah blah blah', attrs, '/2/')
+ end
+
+ it 'returns the two articles in descending order' do
+ expect(subject.map(&:identifier)).to eq(['/2/', '/1/'])
+ end
+ end
+
+ describe '#url_for' do
+ subject { helper.url_for(ctx.items['/stuff/']) }
+
+ let(:item_attributes) { {} }
+
+ before do
+ item = ctx.create_item('Stuff', item_attributes, '/stuff/')
+ ctx.create_rep(item, '/rep/path/stuff.html')
+
+ ctx.config[:base_url] = base_url
+ end
+
+ context 'without base_url' do
+ let(:base_url) { nil }
+
+ it 'raises' do
+ expect { subject }.to raise_error(Nanoc::Error)
+ end
+ end
+
+ context 'with base_url' do
+ let(:base_url) { 'http://url.base' }
+
+ context 'with custom_url_in_feed' do
+ let(:item_attributes) do
+ { custom_url_in_feed: 'http://example.com/stuff.html' }
+ end
+
+ it 'returns custom URL' do
+ expect(subject).to eql('http://example.com/stuff.html')
+ end
+ end
+
+ context 'without custom_url_in_feed' do
+ context 'with custom_path_in_feed' do
+ let(:item_attributes) do
+ { custom_path_in_feed: '/stuff.html' }
+ end
+
+ it 'returns base URL + custom path' do
+ expect(subject).to eql('http://url.base/stuff.html')
+ end
+ end
+
+ context 'without custom_path_in_feed' do
+ it 'returns base URL + path' do
+ expect(subject).to eql('http://url.base/rep/path/stuff.html')
+ end
+ end
+ end
+ end
+ end
+
+ describe '#feed_url' do
+ subject { helper.feed_url }
+
+ let(:item_attributes) { {} }
+
+ before do
+ ctx.item = ctx.create_item('Feed', item_attributes, '/feed/')
+ ctx.create_rep(ctx.item, '/feed.xml')
+
+ ctx.config[:base_url] = base_url
+ end
+
+ context 'without base_url' do
+ let(:base_url) { nil }
+
+ it 'raises' do
+ expect { subject }.to raise_error(Nanoc::Error)
+ end
+ end
+
+ context 'with base_url' do
+ let(:base_url) { 'http://url.base' }
+
+ context 'with feed_url' do
+ let(:item_attributes) do
+ { feed_url: 'http://custom.feed.url/feed.rss' }
+ end
+
+ it 'returns custom URL' do
+ expect(subject).to eql('http://custom.feed.url/feed.rss')
+ end
+ end
+
+ context 'without feed_url' do
+ it 'returns base URL + path' do
+ expect(subject).to eql('http://url.base/feed.xml')
+ end
+ end
+ end
+ end
+
+ describe '#attribute_to_time' do
+ subject { helper.attribute_to_time(arg) }
+
+ let(:noon_s) { 1_446_903_076 }
+ let(:beginning_of_day_s) { 1_446_854_400 }
+
+ let(:around_noon_local) { Time.at(noon_s - Time.at(noon_s).utc_offset) }
+ let(:around_noon_utc) { Time.at(noon_s) }
+ let(:beginning_of_day_utc) { Time.at(beginning_of_day_s) }
+
+ context 'with Time instance' do
+ let(:arg) { around_noon_utc }
+ it { is_expected.to eql(around_noon_utc) }
+ end
+
+ context 'with Date instance' do
+ let(:arg) { Date.new(2015, 11, 7) }
+ it { is_expected.to eql(beginning_of_day_utc) }
+ end
+
+ context 'with DateTime instance' do
+ let(:arg) { DateTime.new(2015, 11, 7, 13, 31, 16) }
+ it { is_expected.to eql(around_noon_utc) }
+ end
+
+ context 'with string' do
+ let(:arg) { '2015-11-7 13:31:16' }
+ it { is_expected.to eql(around_noon_local) }
+ end
+ end
+
+ describe '#atom_tag_for' do
+ subject { helper.atom_tag_for(ctx.items['/stuff/']) }
+
+ let(:item_attributes) { { created_at: '2015-05-19 12:34:56' } }
+ let(:item_rep_path) { '/stuff.xml' }
+ let(:base_url) { 'http://url.base' }
+
+ before do
+ item = ctx.create_item('Stuff', item_attributes, '/stuff/')
+ ctx.create_rep(item, item_rep_path)
+
+ ctx.config[:base_url] = base_url
+ end
+
+ context 'item with path' do
+ let(:item_rep_path) { '/stuff.xml' }
+ it { is_expected.to eql('tag:url.base,2015-05-19:/stuff.xml') }
+ end
+
+ context 'item without path' do
+ let(:item_rep_path) { nil }
+ it { is_expected.to eql('tag:url.base,2015-05-19:/stuff/') }
+ end
+
+ context 'bare URL without subdir' do
+ let(:base_url) { 'http://url.base' }
+ it { is_expected.to eql('tag:url.base,2015-05-19:/stuff.xml') }
+ end
+
+ context 'bare URL with subdir' do
+ let(:base_url) { 'http://url.base/sub' }
+ it { is_expected.to eql('tag:url.base,2015-05-19:/sub/stuff.xml') }
+ end
+
+ context 'created_at is date' do
+ let(:item_attributes) do
+ { created_at: Date.parse('2015-05-19 12:34:56') }
+ end
+ it { is_expected.to eql('tag:url.base,2015-05-19:/stuff.xml') }
+ end
+
+ context 'created_at is time' do
+ let(:item_attributes) do
+ { created_at: Time.parse('2015-05-19 12:34:56') }
+ end
+ it { is_expected.to eql('tag:url.base,2015-05-19:/stuff.xml') }
+ end
+
+ # TODO: handle missing base_dir
+ # TODO: handle missing created_at
+ end
+end
diff --git a/spec/nanoc/helpers/breadcrumbs_spec.rb b/spec/nanoc/helpers/breadcrumbs_spec.rb
new file mode 100644
index 0000000..d9d439c
--- /dev/null
+++ b/spec/nanoc/helpers/breadcrumbs_spec.rb
@@ -0,0 +1,133 @@
+describe Nanoc::Helpers::Breadcrumbs, helper: true do
+ before do
+ allow(ctx.dependency_tracker).to receive(:enter)
+ allow(ctx.dependency_tracker).to receive(:exit)
+ end
+
+ describe '#breadcrumbs_trail' do
+ subject { helper.breadcrumbs_trail }
+
+ context 'legacy identifiers' do
+ context 'root' do
+ before do
+ ctx.create_item('root', {}, Nanoc::Identifier.new('/', type: :legacy))
+
+ ctx.item = ctx.items['/']
+ end
+
+ it 'returns an array with the item' do
+ expect(subject).to eql([ctx.items['/']])
+ end
+ end
+
+ context 'root and direct child' do
+ before do
+ ctx.create_item('child', {}, Nanoc::Identifier.new('/foo/', type: :legacy))
+ ctx.create_item('root', {}, Nanoc::Identifier.new('/', type: :legacy))
+
+ ctx.item = ctx.items['/foo/']
+ end
+
+ it 'returns an array with the items' do
+ expect(subject).to eql([ctx.items['/'], ctx.items['/foo/']])
+ end
+ end
+
+ context 'root, child and grandchild' do
+ before do
+ ctx.create_item('grandchild', {}, Nanoc::Identifier.new('/foo/bar/', type: :legacy))
+ ctx.create_item('child', {}, Nanoc::Identifier.new('/foo/', type: :legacy))
+ ctx.create_item('root', {}, Nanoc::Identifier.new('/', type: :legacy))
+
+ ctx.item = ctx.items['/foo/bar/']
+ end
+
+ it 'returns an array with the items' do
+ expect(subject).to eql([ctx.items['/'], ctx.items['/foo/'], ctx.items['/foo/bar/']])
+ end
+ end
+
+ context 'root, missing child and grandchild' do
+ before do
+ ctx.create_item('grandchild', {}, Nanoc::Identifier.new('/foo/bar/', type: :legacy))
+ ctx.create_item('root', {}, Nanoc::Identifier.new('/', type: :legacy))
+
+ ctx.item = ctx.items['/foo/bar/']
+ end
+
+ it 'returns an array with the items' do
+ expect(subject).to eql([ctx.items['/'], nil, ctx.items['/foo/bar/']])
+ end
+ end
+ end
+
+ context 'non-legacy identifiers' do
+ context 'root' do
+ before do
+ ctx.create_item('root', {}, Nanoc::Identifier.new('/index.md'))
+
+ ctx.item = ctx.items['/index.md']
+ end
+
+ it 'returns an array with the item' do
+ expect(subject).to eql([ctx.items['/index.md']])
+ end
+ end
+
+ context 'root and direct child' do
+ before do
+ ctx.create_item('child', {}, Nanoc::Identifier.new('/foo.md'))
+ ctx.create_item('root', {}, Nanoc::Identifier.new('/index.md'))
+
+ ctx.item = ctx.items['/foo.md']
+ end
+
+ it 'returns an array with the items' do
+ expect(subject).to eql([ctx.items['/index.md'], ctx.items['/foo.md']])
+ end
+ end
+
+ context 'root, child and grandchild' do
+ before do
+ ctx.create_item('grandchild', {}, Nanoc::Identifier.new('/foo/bar.md'))
+ ctx.create_item('child', {}, Nanoc::Identifier.new('/foo.md'))
+ ctx.create_item('root', {}, Nanoc::Identifier.new('/index.md'))
+
+ ctx.item = ctx.items['/foo/bar.md']
+ end
+
+ it 'returns an array with the items' do
+ expect(subject).to eql([ctx.items['/index.md'], ctx.items['/foo.md'], ctx.items['/foo/bar.md']])
+ end
+ end
+
+ context 'root, missing child and grandchild' do
+ before do
+ ctx.create_item('grandchild', {}, Nanoc::Identifier.new('/foo/bar.md'))
+ ctx.create_item('root', {}, Nanoc::Identifier.new('/index.md'))
+
+ ctx.item = ctx.items['/foo/bar.md']
+ end
+
+ it 'returns an array with the items' do
+ expect(subject).to eql([ctx.items['/index.md'], nil, ctx.items['/foo/bar.md']])
+ end
+ end
+
+ context 'index.md child' do
+ # No special handling of non-root index.* files.
+
+ before do
+ ctx.create_item('grandchild', {}, Nanoc::Identifier.new('/foo/index.md'))
+ ctx.create_item('root', {}, Nanoc::Identifier.new('/index.md'))
+
+ ctx.item = ctx.items['/foo/index.md']
+ end
+
+ it 'returns an array with the items' do
+ expect(subject).to eql([ctx.items['/index.md'], nil, ctx.items['/foo/index.md']])
+ end
+ end
+ end
+ end
+end
diff --git a/spec/nanoc/helpers/capturing_spec.rb b/spec/nanoc/helpers/capturing_spec.rb
new file mode 100644
index 0000000..4f5d20e
--- /dev/null
+++ b/spec/nanoc/helpers/capturing_spec.rb
@@ -0,0 +1,181 @@
+describe Nanoc::Helpers::Capturing, helper: true do
+ describe '#content_for' do
+ before do
+ ctx.item = ctx.create_item('some content', {}, '/about.md')
+ ctx.create_rep(ctx.item, '/about.html')
+ end
+
+ describe 'with name + block' do
+ let(:_erbout) { 'existing content' }
+
+ context 'only name given' do
+ subject { helper.content_for(:foo) { _erbout << 'foo' } }
+
+ it 'stores snapshot content' do
+ subject
+ expect(ctx.item.reps[:default].unwrap.snapshot_contents[:__capture_foo].string).to eql('foo')
+ end
+ end
+
+ context 'name and params given' do
+ subject { helper.content_for(:foo, params) { _erbout << 'foo' } }
+ let(:params) { raise 'overwrite me' }
+
+ context 'no existing behavior specified' do
+ let(:params) { {} }
+
+ it 'errors after two times' do
+ helper.content_for(:foo, params) { _erbout << 'foo' }
+ expect { helper.content_for(:foo, params) { _erbout << 'bar' } }.to raise_error(RuntimeError)
+ end
+ end
+
+ context 'existing behavior is :overwrite' do
+ let(:params) { { existing: :overwrite } }
+
+ it 'overwrites' do
+ helper.content_for(:foo, params) { _erbout << 'foo' }
+ helper.content_for(:foo, params) { _erbout << 'bar' }
+ expect(ctx.item.reps[:default].unwrap.snapshot_contents[:__capture_foo].string).to eql('bar')
+ end
+ end
+
+ context 'existing behavior is :append' do
+ let(:params) { { existing: :append } }
+
+ it 'appends' do
+ helper.content_for(:foo, params) { _erbout << 'foo' }
+ helper.content_for(:foo, params) { _erbout << 'bar' }
+ expect(ctx.item.reps[:default].unwrap.snapshot_contents[:__capture_foo].string).to eql('foobar')
+ end
+ end
+
+ context 'existing behavior is :error' do
+ let(:params) { { existing: :error } }
+
+ it 'errors after two times' do
+ helper.content_for(:foo, params) { _erbout << 'foo' }
+ expect { helper.content_for(:foo, params) { _erbout << 'bar' } }.to raise_error(RuntimeError)
+ end
+ end
+
+ context 'existing behavior is :something else' do
+ let(:params) { { existing: :donkey } }
+
+ it 'errors' do
+ expect { subject }.to raise_error(ArgumentError)
+ end
+ end
+ end
+ end
+
+ describe 'with item + name' do
+ subject { helper.content_for(item, :foo) }
+
+ let(:_erbout) { 'existing content' }
+
+ context 'requesting for same item' do
+ let(:item) { ctx.item }
+
+ context 'nothing captured' do
+ it { is_expected.to be_nil }
+ end
+
+ context 'something captured' do
+ before do
+ helper.content_for(:foo) { _erbout << 'I have been captured!' }
+ end
+
+ it { is_expected.to eql('I have been captured!') }
+ end
+ end
+
+ context 'requesting for other item' do
+ let(:item) { ctx.items['/other.md'] }
+
+ before do
+ item = ctx.create_item('other content', {}, '/other.md')
+ ctx.create_rep(item, '/other.html')
+ end
+
+ context 'other item is not yet compiled' do
+ it 'raises an unmet dependency error' do
+ expect(ctx.dependency_tracker).to receive(:bounce).with(item.unwrap, compiled_content: true)
+ expect { subject }.to raise_error(FiberError)
+ end
+ end
+
+ context 'other item is compiled' do
+ before do
+ item.reps[:default].unwrap.compiled = true
+ item.reps[:default].unwrap.snapshot_contents[:__capture_foo] =
+ Nanoc::Int::TextualContent.new('other captured foo')
+ end
+
+ it 'returns the captured content' do
+ expect(ctx.dependency_tracker).to receive(:bounce).with(item.unwrap, compiled_content: true)
+ expect(subject).to eql('other captured foo')
+ end
+ end
+ end
+ end
+ end
+
+ describe '#capture' do
+ context 'with string' do
+ let(:_erbout) { 'existing content' }
+
+ subject { helper.capture { _erbout << 'new content' } }
+
+ it 'returns the appended content' do
+ expect(subject).to eql('new content')
+ end
+
+ it 'does not modify _erbout' do
+ expect { subject }.not_to change { _erbout }
+ end
+ end
+
+ context 'with array' do
+ let(:_erbout) { ['existing content'] }
+
+ shared_examples 'returns properly joined output' do
+ subject { helper.capture { _erbout << %w(new _ content) } }
+
+ it 'returns the appended content, joined' do
+ expect(subject).to eql('new_content')
+ end
+
+ it 'does not modify _erbout' do
+ expect { subject }.not_to change { _erbout.join('') }
+ end
+ end
+
+ context 'default output field separator' do
+ include_examples 'returns properly joined output'
+ end
+
+ context 'output field separator set to ,' do
+ around do |ex|
+ orig_output_field_separator = $OUTPUT_FIELD_SEPARATOR
+ $OUTPUT_FIELD_SEPARATOR = ','
+ ex.run
+ $OUTPUT_FIELD_SEPARATOR = orig_output_field_separator
+ end
+
+ include_examples 'returns properly joined output'
+ end
+
+ context 'output field separator set to nothing' do
+ around do |ex|
+ orig_output_field_separator = $OUTPUT_FIELD_SEPARATOR
+ $OUTPUT_FIELD_SEPARATOR = ''
+ ex.run
+ $OUTPUT_FIELD_SEPARATOR = orig_output_field_separator
+ end
+
+ include_examples 'returns properly joined output'
+ end
+ end
+ end
+end
diff --git a/spec/nanoc/helpers/child_parent_spec.rb b/spec/nanoc/helpers/child_parent_spec.rb
new file mode 100644
index 0000000..b7fdf9b
--- /dev/null
+++ b/spec/nanoc/helpers/child_parent_spec.rb
@@ -0,0 +1,105 @@
+describe Nanoc::Helpers::ChildParent, helper: true do
+ describe '#children_of' do
+ subject { helper.children_of(item) }
+
+ let(:item) { ctx.create_item('some content', {}, identifier) }
+
+ context 'legacy identifier' do
+ let(:identifier) { Nanoc::Identifier.new('/foo/', type: :legacy) }
+
+ let!(:child_item) do
+ ctx.create_item('abc', {}, Nanoc::Identifier.new('/foo/a/', type: :legacy))
+ end
+
+ let!(:grandchild_item) do
+ ctx.create_item('def', {}, Nanoc::Identifier.new('/foo/a/b/', type: :legacy))
+ end
+
+ let!(:sibling_item) do
+ ctx.create_item('xyz', {}, Nanoc::Identifier.new('/bar/', type: :legacy))
+ end
+
+ it 'returns only direct children' do
+ expect(subject).to eql([child_item])
+ end
+ end
+
+ context 'full identifier' do
+ let(:identifier) { Nanoc::Identifier.new('/foo.md', type: :full) }
+
+ let!(:child_item) do
+ ctx.create_item('abc', {}, Nanoc::Identifier.new('/foo/a.md', type: :full))
+ end
+
+ let!(:grandchild_item) do
+ ctx.create_item('def', {}, Nanoc::Identifier.new('/foo/a/b.md', type: :full))
+ end
+
+ let!(:sibling_item) do
+ ctx.create_item('xyz', {}, Nanoc::Identifier.new('/bar.md', type: :full))
+ end
+
+ let!(:index_child_item) do
+ ctx.create_item('xyz', {}, Nanoc::Identifier.new('/foo/a/index.md', type: :full))
+ end
+
+ it 'returns only direct children' do
+ expect(subject).to eql([child_item])
+ end
+ end
+ end
+
+ describe '#parent_of' do
+ subject { helper.parent_of(item) }
+
+ let(:item) { ctx.create_item('some content', {}, identifier) }
+
+ context 'legacy identifier' do
+ let(:identifier) { Nanoc::Identifier.new('/foo/bar/', type: :legacy) }
+
+ let!(:parent_item) do
+ ctx.create_item('abc', {}, Nanoc::Identifier.new('/foo/', type: :legacy))
+ end
+
+ let!(:sibling_item) do
+ ctx.create_item('def', {}, Nanoc::Identifier.new('/foo/qux/', type: :legacy))
+ end
+
+ let!(:child_item) do
+ ctx.create_item('xyz', {}, Nanoc::Identifier.new('/foo/bar/asdf/', type: :legacy))
+ end
+
+ let!(:grandparent_item) do
+ ctx.create_item('opq', {}, Nanoc::Identifier.new('/', type: :legacy))
+ end
+
+ it 'returns parent' do
+ expect(subject).to eql(parent_item)
+ end
+ end
+
+ context 'full identifier' do
+ let(:identifier) { Nanoc::Identifier.new('/foo/bar.md', type: :full) }
+
+ let!(:parent_item) do
+ ctx.create_item('abc', {}, Nanoc::Identifier.new('/foo.md', type: :full))
+ end
+
+ let!(:sibling_item) do
+ ctx.create_item('def', {}, Nanoc::Identifier.new('/foo/qux.md', type: :full))
+ end
+
+ let!(:child_item) do
+ ctx.create_item('xyz', {}, Nanoc::Identifier.new('/foo/bar/asdf.md', type: :full))
+ end
+
+ let!(:grandparent_item) do
+ ctx.create_item('opq', {}, Nanoc::Identifier.new('/index.md', type: :full))
+ end
+
+ it 'returns parent' do
+ expect(subject).to eql(parent_item)
+ end
+ end
+ end
+end
diff --git a/spec/nanoc/helpers/filtering_spec.rb b/spec/nanoc/helpers/filtering_spec.rb
new file mode 100644
index 0000000..4dc49c3
--- /dev/null
+++ b/spec/nanoc/helpers/filtering_spec.rb
@@ -0,0 +1,72 @@
+describe Nanoc::Helpers::Filtering, helper: true do
+ describe '#filter' do
+ before do
+ ctx.item = ctx.create_item('some content', { title: 'Hello!' }, '/about.md')
+ ctx.item_rep = ctx.create_rep(ctx.item, '/about.html')
+ end
+
+ let(:content) do
+ "A<% filter :erb do %><%%= 'X' %><% end %>B"
+ end
+
+ subject { ::ERB.new(content).result(helper.get_binding) }
+
+ context 'basic case' do
+ it { is_expected.to eql('AXB') }
+
+ it 'notifies' do
+ ns = Set.new
+ Nanoc::Int::NotificationCenter.on(:filtering_started) { ns << :filtering_started }
+ Nanoc::Int::NotificationCenter.on(:filtering_ended) { ns << :filtering_ended }
+
+ subject
+
+ expect(ns).to include(:filtering_started)
+ expect(ns).to include(:filtering_ended)
+ end
+ end
+
+ context 'with assigns' do
+ let(:content) do
+ 'A<% filter :erb do %><%%= @item[:title] %><% end %>B'
+ end
+
+ it { is_expected.to eql('AHello!B') }
+ end
+
+ context 'unknonwn filter name' do
+ let(:content) do
+ 'A<% filter :donkey do %>X<% end %>B'
+ end
+
+ it 'errors' do
+ expect { subject }.to raise_error(Nanoc::Int::Errors::UnknownFilter)
+ end
+ end
+
+ context 'with locals' do
+ let(:content) do
+ "A<% filter :erb, locals: { sheep: 'baah' } do %><%%= @sheep %><% end %>B"
+ end
+
+ it { is_expected.to eql('AbaahB') }
+ end
+
+ context 'with Haml' do
+ let(:content) do
+ "%p Foo.\n" \
+ "- filter(:erb) do\n" \
+ " <%= 'abc' + 'xyz' %>\n" \
+ "%p Bar.\n"
+ end
+
+ before do
+ require 'haml'
+ end
+
+ subject { ::Haml::Engine.new(content).render(helper.get_binding) }
+
+ it { is_expected.to match(%r{^<p>Foo.</p>\s*abcxyz\s*<p>Bar.</p>$}) }
+ end
+ end
+end
diff --git a/spec/nanoc/helpers/html_escape_spec.rb b/spec/nanoc/helpers/html_escape_spec.rb
new file mode 100644
index 0000000..7599f4d
--- /dev/null
+++ b/spec/nanoc/helpers/html_escape_spec.rb
@@ -0,0 +1,35 @@
+describe Nanoc::Helpers::HTMLEscape, helper: true do
+ describe '#html_escape' do
+ subject { helper.html_escape(string) }
+
+ context 'given strings to escape' do
+ let(:string) { '< > & "' }
+ it { is_expected.to eql('< > & "') }
+ end
+
+ context 'given a block' do
+ let!(:_erbout) { 'moo' }
+
+ it 'adds escaped content to _erbout' do
+ helper.html_escape { _erbout << '<h1>Stuff!</h1>' }
+ expect(_erbout).to eql('moo<h1>Stuff!</h1>')
+ end
+ end
+
+ context 'given no argument nor block' do
+ subject { helper.html_escape }
+
+ it 'raises' do
+ expect { subject }.to raise_error(RuntimeError)
+ end
+ end
+
+ context 'given argument that is not a string' do
+ let(:string) { 1 }
+
+ it 'raises an ArgumentError' do
+ expect { subject }.to raise_error(ArgumentError)
+ end
+ end
+ end
+end
diff --git a/spec/nanoc/helpers/link_to_spec.rb b/spec/nanoc/helpers/link_to_spec.rb
new file mode 100644
index 0000000..3328dd7
--- /dev/null
+++ b/spec/nanoc/helpers/link_to_spec.rb
@@ -0,0 +1,275 @@
+describe Nanoc::Helpers::LinkTo, helper: true do
+ describe '#link_to' do
+ subject { helper.link_to(text, target, attributes) }
+
+ let(:text) { 'Text' }
+ let(:target) { raise 'override me' }
+ let(:attributes) { {} }
+
+ context 'with string path' do
+ let(:target) { '/foo/' }
+ it { is_expected.to eql('<a href="/foo/">Text</a>') }
+
+ context 'with attributes' do
+ let(:attributes) { { title: 'Donkey' } }
+ it { is_expected.to eql('<a title="Donkey" href="/foo/">Text</a>') }
+ end
+
+ context 'special HTML characters in text' do
+ let(:text) { 'Foo & Bar' }
+ it { is_expected.to eql('<a href="/foo/">Foo & Bar</a>') }
+ # Not escaped!
+ end
+
+ context 'special HTML characters in URL' do
+ let(:target) { '/r&d/' }
+ it { is_expected.to eql('<a href="/r&d/">Text</a>') }
+ end
+
+ context 'special HTML characters in attribute' do
+ let(:attributes) { { title: 'Research & Development' } }
+ it { is_expected.to eql('<a title="Research & Development" href="/foo/">Text</a>') }
+ end
+ end
+
+ context 'with rep' do
+ let(:item) { ctx.create_item('content', {}, '/target/') }
+ let(:target) { ctx.create_rep(item, '/target.html') }
+
+ it { is_expected.to eql('<a href="/target.html">Text</a>') }
+ end
+
+ context 'with item' do
+ let(:target) { ctx.create_item('content', {}, '/target/') }
+
+ before do
+ ctx.create_rep(target, '/target.html')
+ end
+
+ it { is_expected.to eql('<a href="/target.html">Text</a>') }
+ end
+
+ context 'with nil' do
+ let(:target) { nil }
+
+ it 'raises' do
+ expect { subject }.to raise_error(ArgumentError)
+ end
+ end
+
+ context 'with something else' do
+ let(:target) { :donkey }
+
+ it 'raises' do
+ expect { subject }.to raise_error(ArgumentError)
+ end
+ end
+
+ context 'with nil path' do
+ let(:item) { ctx.create_item('content', {}, '/target/') }
+ let(:target) { ctx.create_rep(item, nil) }
+
+ it 'raises' do
+ expect { subject }.to raise_error(RuntimeError)
+ end
+ end
+ end
+
+ describe '#link_to_unless_current' do
+ subject { helper.link_to_unless_current(text, target, attributes) }
+
+ let(:text) { 'Text' }
+ let(:target) { raise 'override me' }
+ let(:attributes) { {} }
+
+ context 'with string path' do
+ let(:target) { '/target.html' }
+
+ context 'current' do
+ before do
+ ctx.item = ctx.create_item('content', {}, '/target.md')
+ ctx.item_rep = ctx.create_rep(ctx.item, '/target.html')
+ end
+
+ it { is_expected.to eql('<span class="active">Text</span>') }
+ end
+
+ context 'no item rep present' do
+ it { is_expected.to eql('<a href="/target.html">Text</a>') }
+ end
+
+ context 'item rep present, but not current' do
+ before do
+ ctx.item = ctx.create_item('content', {}, '/other.md')
+ ctx.item_rep = ctx.create_rep(ctx.item, '/other.html')
+ end
+
+ it { is_expected.to eql('<a href="/target.html">Text</a>') }
+ end
+ end
+
+ context 'with rep' do
+ before do
+ ctx.item = ctx.create_item('content', {}, '/target.md')
+ ctx.item_rep = ctx.create_rep(ctx.item, '/target.html')
+ end
+
+ let(:some_item) { ctx.create_item('content', {}, '/other.md') }
+ let(:some_item_rep) { ctx.create_rep(some_item, '/other.html') }
+
+ context 'current' do
+ let(:target) { ctx.item_rep }
+ it { is_expected.to eql('<span class="active">Text</span>') }
+ end
+
+ context 'no item rep present' do
+ let(:target) { some_item_rep }
+
+ before do
+ ctx.item = nil
+ ctx.item_rep = nil
+ end
+
+ it { is_expected.to eql('<a href="/other.html">Text</a>') }
+ end
+
+ context 'item rep present, but not current' do
+ let(:target) { some_item_rep }
+ it { is_expected.to eql('<a href="/other.html">Text</a>') }
+ end
+ end
+
+ context 'with item' do
+ before do
+ ctx.item = ctx.create_item('content', {}, '/target.md')
+ ctx.item_rep = ctx.create_rep(ctx.item, '/target.html')
+ end
+
+ let!(:some_item) { ctx.create_item('content', {}, '/other.md') }
+ let!(:some_item_rep) { ctx.create_rep(some_item, '/other.html') }
+
+ context 'current' do
+ let(:target) { ctx.item }
+ it { is_expected.to eql('<span class="active">Text</span>') }
+ end
+
+ context 'no item rep present' do
+ let(:target) { some_item }
+
+ before do
+ ctx.item = nil
+ ctx.item_rep = nil
+ end
+
+ it { is_expected.to eql('<a href="/other.html">Text</a>') }
+ end
+
+ context 'item rep present, but not current' do
+ let(:target) { some_item }
+ it { is_expected.to eql('<a href="/other.html">Text</a>') }
+ end
+ end
+ end
+
+ describe '#relative_path_to' do
+ subject { helper.relative_path_to(target) }
+
+ before do
+ ctx.item = ctx.create_item('content', {}, '/foo/self.md')
+ ctx.item_rep = ctx.create_rep(ctx.item, self_path)
+ end
+
+ context 'current item rep has non-nil path' do
+ let(:self_path) { '/foo/self.html' }
+
+ context 'to string path' do
+ context 'to relative path' do
+ let(:target) { 'bar/target.html' }
+
+ it 'errors' do
+ # TODO: Might make sense to allow this case (and return the path itself)
+ expect { subject }.to raise_error(ArgumentError)
+ end
+ end
+
+ context 'to path without trailing slash' do
+ let(:target) { '/bar/target.html' }
+ it { is_expected.to eql('../bar/target.html') }
+ end
+
+ context 'to path with trailing slash' do
+ let(:target) { '/bar/target/' }
+ it { is_expected.to eql('../bar/target/') }
+ end
+
+ context 'to Windows/UNC path (forward slashes)' do
+ let(:target) { '//foo' }
+ it { is_expected.to eql('//foo') }
+ end
+
+ context 'to Windows/UNC path (backslashes)' do
+ let(:target) { '\\\\foo' }
+ it { is_expected.to eql('\\\\foo') }
+ end
+ end
+
+ context 'to rep' do
+ let(:target) { ctx.create_rep(ctx.item, '/bar/target.html') }
+ it { is_expected.to eql('../bar/target.html') }
+
+ context 'to self' do
+ let(:target) { ctx.item_rep }
+
+ context 'self is a filename' do
+ it { is_expected.to eql('self.html') }
+ end
+
+ context 'self is a directory' do
+ let(:self_path) { '/foo/self/' }
+ it { is_expected.to eql('./') }
+ end
+ end
+ end
+
+ context 'to item' do
+ let(:target) { ctx.create_item('content', {}, '/bar/target.md') }
+
+ before do
+ ctx.create_rep(target, '/bar/target.html')
+ end
+
+ it { is_expected.to eql('../bar/target.html') }
+
+ context 'to self' do
+ let(:target) { ctx.item }
+
+ context 'self is a filename' do
+ it { is_expected.to eql('self.html') }
+ end
+
+ context 'self is a directory' do
+ let(:self_path) { '/foo/self/' }
+ it { is_expected.to eql('./') }
+ end
+ end
+ end
+
+ context 'to nil path' do
+ let(:target) { ctx.create_rep(ctx.item, nil) }
+
+ it 'raises' do
+ expect { subject }.to raise_error(RuntimeError)
+ end
+ end
+ end
+
+ context 'current item rep has nil path' do
+ let(:self_path) { nil }
+ let(:target) { '/bar/target.html' }
+
+ it 'errors' do
+ expect { subject }.to raise_error(RuntimeError)
+ end
+ end
+ end
+end
diff --git a/spec/nanoc/helpers/rendering_spec.rb b/spec/nanoc/helpers/rendering_spec.rb
new file mode 100644
index 0000000..5d4a939
--- /dev/null
+++ b/spec/nanoc/helpers/rendering_spec.rb
@@ -0,0 +1,141 @@
+describe Nanoc::Helpers::Rendering, helper: true do
+ describe '#render' do
+ subject { helper.instance_eval { render('/partial.erb') } }
+
+ let(:rule_memory_for_layout) do
+ [Nanoc::Int::ProcessingActions::Filter.new(:erb, {})]
+ end
+
+ let(:layout_view) do
+ ctx.create_layout(layout_content, {}, layout_identifier)
+ end
+
+ let(:layout) do
+ layout_view.unwrap
+ end
+
+ before do
+ ctx.update_rule_memory(layout, rule_memory_for_layout)
+ end
+
+ context 'legacy identifier' do
+ let(:layout_identifier) { Nanoc::Identifier.new('/partial/', type: :legacy) }
+
+ context 'cleaned identifier' do
+ subject { helper.instance_eval { render('/partial/') } }
+
+ context 'layout without instructions' do
+ let(:layout_content) { 'blah' }
+
+ it { is_expected.to eql('blah') }
+
+ it 'tracks proper dependencies' do
+ expect(ctx.dependency_tracker).to receive(:enter)
+ .with(layout, raw_content: true, attributes: false, compiled_content: false, path: false)
+ subject
+ end
+ end
+
+ context 'layout with instructions' do
+ let(:layout_content) { 'blah <%= @layout.identifier %>' }
+ it { is_expected.to eql('blah /partial/') }
+ end
+ end
+
+ context 'non-cleaned identifier' do
+ subject { helper.instance_eval { render('/partial') } }
+
+ context 'layout without instructions' do
+ let(:layout_content) { 'blah' }
+ it { is_expected.to eql('blah') }
+ end
+
+ context 'layout with instructions' do
+ let(:layout_content) { 'blah <%= @layout.identifier %>' }
+ it { is_expected.to eql('blah /partial/') }
+ end
+ end
+ end
+
+ context 'full-style identifier' do
+ let(:layout_identifier) { Nanoc::Identifier.new('/partial.erb') }
+
+ context 'layout without instructions' do
+ let(:layout_content) { 'blah' }
+ it { is_expected.to eql('blah') }
+ end
+
+ context 'layout with instructions' do
+ let(:layout_content) { 'blah <%= @layout.identifier %>' }
+ it { is_expected.to eql('blah /partial.erb') }
+ end
+
+ context 'printing wrapped layout class' do
+ let(:layout_content) { 'blah <%= @layout.class %>' }
+ it { is_expected.to eql('blah Nanoc::LayoutView') }
+ end
+
+ context 'printing unwrapped layout class' do
+ let(:layout_content) { 'blah <%= @layout.unwrap.class %>' }
+ it { is_expected.to eql('blah Nanoc::Int::Layout') }
+ end
+
+ context 'unknown layout' do
+ subject { helper.instance_eval { render('/unknown.erb') } }
+
+ let(:layout_content) { 'blah' }
+
+ it 'raises' do
+ expect { subject }.to raise_error(Nanoc::Int::Errors::UnknownLayout)
+ end
+ end
+
+ context 'layout with unknown filter' do
+ let(:rule_memory_for_layout) do
+ [Nanoc::Int::ProcessingActions::Filter.new(:donkey, {})]
+ end
+
+ let(:layout_content) { 'blah' }
+
+ it 'raises' do
+ expect { subject }.to raise_error(Nanoc::Int::Errors::UnknownFilter)
+ end
+ end
+
+ context 'layout without filter' do
+ let(:rule_memory_for_layout) do
+ [Nanoc::Int::ProcessingActions::Filter.new(nil, {})]
+ end
+
+ let(:layout_content) { 'blah' }
+
+ it 'raises' do
+ expect { subject }.to raise_error(Nanoc::Int::Errors::CannotDetermineFilter)
+ end
+ end
+
+ context 'with block' do
+ subject do
+ helper.instance_eval do
+ render('/partial.erb') { _erbout << 'extra content' }
+ end
+ end
+
+ before do
+ ctx.erbout << '[erbout-before]'
+ end
+
+ let(:layout_content) { '[partial-before]<%= yield %>[partial-after]' }
+
+ it 'returns an empty string' do
+ expect(subject).to eql('')
+ end
+
+ it 'modifies erbout' do
+ subject
+ expect(ctx.erbout).to eql('[erbout-before][partial-before]extra content[partial-after]')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/nanoc/helpers/tagging_spec.rb b/spec/nanoc/helpers/tagging_spec.rb
new file mode 100644
index 0000000..b31ec11
--- /dev/null
+++ b/spec/nanoc/helpers/tagging_spec.rb
@@ -0,0 +1,104 @@
+describe Nanoc::Helpers::Tagging, helper: true do
+ describe '#tags_for' do
+ subject { helper.tags_for(item, params) }
+
+ let(:item) { ctx.items['/me.*'] }
+ let(:params) { {} }
+ let(:item_attributes) { {} }
+
+ before do
+ ctx.create_item('content', item_attributes, '/me.md')
+ end
+
+ context 'no tags' do
+ let(:item_attributes) { {} }
+ it { is_expected.to eql('(none)') }
+ end
+
+ context 'nil tag list' do
+ let(:item_attributes) { { tags: nil } }
+ it { is_expected.to eql('(none)') }
+ end
+
+ context 'empty tag list' do
+ let(:item_attributes) { { tags: [] } }
+ it { is_expected.to eql('(none)') }
+ end
+
+ context 'no tags, and custom none text' do
+ let(:item_attributes) { {} }
+ let(:params) { { none_text: 'no tags for you, fool' } }
+ it { is_expected.to eql('no tags for you, fool') }
+ end
+
+ context 'one tag' do
+ let(:item_attributes) { { tags: %w(donkey) } }
+
+ context 'implicit base_url' do
+ it { is_expected.to eql('donkey') }
+ end
+
+ context 'explicit nil base_url' do
+ let(:params) { { base_url: nil } }
+ it { is_expected.to eql('donkey') }
+ end
+
+ context 'explicit other base_url' do
+ let(:params) { { base_url: 'http://nanoc.ws/tag/' } }
+ it { is_expected.to eql('<a href="http://nanoc.ws/tag/donkey" rel="tag">donkey</a>') }
+ end
+ end
+
+ context 'two tags' do
+ let(:item_attributes) { { tags: %w(donkey giraffe) } }
+ it { is_expected.to eql('donkey, giraffe') }
+ end
+
+ context 'three tags' do
+ let(:item_attributes) { { tags: %w(donkey giraffe zebra) } }
+ it { is_expected.to eql('donkey, giraffe, zebra') }
+
+ context 'custom separator' do
+ let(:item_attributes) { { tags: %w(donkey giraffe zebra) } }
+ let(:params) { { separator: ' / ' } }
+ it { is_expected.to eql('donkey / giraffe / zebra') }
+ end
+ end
+ end
+
+ describe '#items_with_tag' do
+ subject { helper.items_with_tag(tag) }
+
+ before do
+ ctx.create_item('item 1', { tags: [:foo] }, '/item1.md')
+ ctx.create_item('item 2', { tags: [:bar] }, '/item2.md')
+ ctx.create_item('item 3', { tags: [:foo, :bar] }, '/item3.md')
+ ctx.create_item('item 4', { tags: nil }, '/item4.md')
+ ctx.create_item('item 5', {}, '/item5.md')
+ end
+
+ context 'tag that exists' do
+ let(:tag) { :foo }
+ it { is_expected.to contain_exactly(ctx.items['/item1.md'], ctx.items['/item3.md']) }
+ end
+
+ context 'tag that does not exists' do
+ let(:tag) { :other }
+ it { is_expected.to be_empty }
+ end
+ end
+
+ describe '#link_for_tag' do
+ subject { helper.link_for_tag(tag, base_url) }
+
+ let(:tag) { 'foo' }
+ let(:base_url) { 'http://nanoc.ws/tag/' }
+
+ it { is_expected.to eql('<a href="http://nanoc.ws/tag/foo" rel="tag">foo</a>') }
+
+ context 'tag with special HTML characters' do
+ let(:tag) { 'R&D' }
+ it { is_expected.to eql('<a href="http://nanoc.ws/tag/R&D" rel="tag">R&D</a>') }
+ end
+ end
+end
diff --git a/spec/nanoc/helpers/text_spec.rb b/spec/nanoc/helpers/text_spec.rb
new file mode 100644
index 0000000..c77a585
--- /dev/null
+++ b/spec/nanoc/helpers/text_spec.rb
@@ -0,0 +1,58 @@
+describe Nanoc::Helpers::Text, helper: true do
+ describe '#excerptize' do
+ subject { helper.excerptize(string, params) }
+
+ let(:string) { 'Foo bar baz quux meow woof' }
+ let(:params) { {} }
+
+ context 'no params' do
+ it 'takes 25 characters' do
+ expect(subject).to eql('Foo bar baz quux meow ...')
+ end
+ end
+
+ context 'perfect fit' do
+ let(:params) { { length: 26 } }
+
+ it 'does not truncate' do
+ expect(subject).to eql('Foo bar baz quux meow woof')
+ end
+ end
+
+ context 'long length' do
+ let(:params) { { length: 27 } }
+
+ it 'does not truncate' do
+ expect(subject).to eql('Foo bar baz quux meow woof')
+ end
+ end
+
+ context 'short length' do
+ let(:params) { { length: 3 } }
+
+ it 'truncates' do
+ expect(subject).to eql('...')
+ end
+ end
+
+ context 'length shorter than omission' do
+ let(:params) { { length: 2 } }
+
+ it 'truncates, disregarding length' do
+ expect(subject).to eql('...')
+ end
+ end
+
+ context 'custom omission' do
+ let(:params) { { omission: '[continued]' } }
+
+ it 'uses custom omission string' do
+ expect(subject).to eql('Foo bar baz qu[continued]')
+ end
+ end
+ end
+
+ describe '#strip_html' do
+ # TODO: test this… or get rid of it (it’s bad!)
+ end
+end
diff --git a/spec/nanoc/integration/outdatedness_integration_spec.rb b/spec/nanoc/integration/outdatedness_integration_spec.rb
new file mode 100644
index 0000000..df1aa50
--- /dev/null
+++ b/spec/nanoc/integration/outdatedness_integration_spec.rb
@@ -0,0 +1,208 @@
+describe 'Outdatedness integration', site: true, stdio: true do
+ context 'only attribute dependency' do
+ before do
+ File.write('content/foo.md', "---\ntitle: hello\n---\n\nfoo")
+ File.write('content/bar.md', '<%= @items["/foo.*"][:title] %>')
+
+ File.write('Rules', <<EOS)
+compile '/foo.*' do
+ write '/foo.html'
+end
+
+compile '/bar.*' do
+ filter :erb
+ write '/bar.html'
+end
+EOS
+ end
+
+ before { Nanoc::CLI.run(%w(compile)) }
+
+ it 'shows default rep outdatedness' do
+ expect { Nanoc::CLI.run(%w(show-data --no-color)) }.to(
+ output(/^item \/foo\.md, rep default:\n is not outdated/).to_stdout,
+ )
+ expect { Nanoc::CLI.run(%w(show-data --no-color)) }.to(
+ output(/^item \/bar\.md, rep default:\n is not outdated/).to_stdout,
+ )
+ end
+
+ it 'shows file as outdated after modification' do
+ File.write('content/bar.md', 'JUST BAR!')
+
+ expect { Nanoc::CLI.run(%w(show-data --no-color)) }.to(
+ output(/^item \/foo\.md, rep default:\n is not outdated/).to_stdout,
+ )
+ expect { Nanoc::CLI.run(%w(show-data --no-color)) }.to(
+ output(/^item \/bar\.md, rep default:\n is outdated: /).to_stdout,
+ )
+ end
+
+ it 'shows file and dependencies as not outdated after content modification' do
+ File.write('content/foo.md', "---\ntitle: hello\n---\n\nfoooOoooOOoooOooo")
+
+ expect { Nanoc::CLI.run(%w(show-data --no-color)) }.to(
+ output(/^item \/foo\.md, rep default:\n is outdated: /).to_stdout,
+ )
+ expect { Nanoc::CLI.run(%w(show-data --no-color)) }.to(
+ output(/^item \/bar\.md, rep default:\n is not outdated/).to_stdout,
+ )
+ end
+
+ it 'shows file and dependencies as outdated after title modification' do
+ File.write('content/foo.md', "---\ntitle: bye\n---\n\nfoo")
+
+ expect { Nanoc::CLI.run(%w(show-data --no-color)) }.to(
+ output(/^item \/foo\.md, rep default:\n is outdated: /).to_stdout,
+ )
+ expect { Nanoc::CLI.run(%w(show-data --no-color)) }.to(
+ output(/^item \/bar\.md, rep default:\n is outdated: /).to_stdout,
+ )
+ end
+ end
+
+ context 'only raw content dependency' do
+ before do
+ File.write('content/foo.md', "---\ntitle: hello\n---\n\nfoo")
+ File.write('content/bar.md', '<%= @items["/foo.*"].raw_content %>')
+
+ File.write('Rules', <<EOS)
+compile '/foo.*' do
+ write '/foo.html'
+end
+
+compile '/bar.*' do
+ filter :erb
+ write '/bar.html'
+end
+EOS
+ end
+
+ before { Nanoc::CLI.run(%w(compile)) }
+
+ it 'shows default rep outdatedness' do
+ expect { Nanoc::CLI.run(%w(show-data --no-color)) }.to(
+ output(/^item \/foo\.md, rep default:\n is not outdated/).to_stdout,
+ )
+ expect { Nanoc::CLI.run(%w(show-data --no-color)) }.to(
+ output(/^item \/bar\.md, rep default:\n is not outdated/).to_stdout,
+ )
+ end
+
+ it 'shows file as outdated after modification' do
+ File.write('content/bar.md', 'JUST BAR!')
+
+ expect { Nanoc::CLI.run(%w(show-data --no-color)) }.to(
+ output(/^item \/foo\.md, rep default:\n is not outdated/).to_stdout,
+ )
+ expect { Nanoc::CLI.run(%w(show-data --no-color)) }.to(
+ output(/^item \/bar\.md, rep default:\n is outdated: /).to_stdout,
+ )
+ end
+
+ it 'shows file and dependencies as outdated after content modification' do
+ File.write('content/foo.md', "---\ntitle: hello\n---\n\nfoooOoooOOoooOooo")
+
+ expect { Nanoc::CLI.run(%w(show-data --no-color)) }.to(
+ output(/^item \/foo\.md, rep default:\n is outdated: /).to_stdout,
+ )
+ expect { Nanoc::CLI.run(%w(show-data --no-color)) }.to(
+ output(/^item \/bar\.md, rep default:\n is outdated: /).to_stdout,
+ )
+ end
+
+ it 'shows file and dependencies as not outdated after title modification' do
+ File.write('content/foo.md', "---\ntitle: bye\n---\n\nfoo")
+
+ expect { Nanoc::CLI.run(%w(show-data --no-color)) }.to(
+ output(/^item \/foo\.md, rep default:\n is outdated: /).to_stdout,
+ )
+ expect { Nanoc::CLI.run(%w(show-data --no-color)) }.to(
+ output(/^item \/bar\.md, rep default:\n is not outdated/).to_stdout,
+ )
+ end
+ end
+
+ context 'attribute and raw content dependency' do
+ before do
+ File.write('content/foo.md', "---\ntitle: hello\n---\n\nfoo")
+ File.write('content/bar.md', '<%= @items["/foo.*"].raw_content %> / <%= @items["/foo.*"][:title] %>')
+
+ File.write('Rules', <<EOS)
+compile '/foo.*' do
+ write '/foo.html'
+end
+
+compile '/bar.*' do
+ filter :erb
+ write '/bar.html'
+end
+EOS
+ end
+
+ before { Nanoc::CLI.run(%w(compile)) }
+
+ it 'shows default rep outdatedness' do
+ expect { Nanoc::CLI.run(%w(show-data --no-color)) }.to(
+ output(/^item \/foo\.md, rep default:\n is not outdated/).to_stdout,
+ )
+ expect { Nanoc::CLI.run(%w(show-data --no-color)) }.to(
+ output(/^item \/bar\.md, rep default:\n is not outdated/).to_stdout,
+ )
+ end
+
+ it 'shows file as outdated after modification' do
+ File.write('content/bar.md', 'JUST BAR!')
+
+ expect { Nanoc::CLI.run(%w(show-data --no-color)) }.to(
+ output(/^item \/foo\.md, rep default:\n is not outdated/).to_stdout,
+ )
+ expect { Nanoc::CLI.run(%w(show-data --no-color)) }.to(
+ output(/^item \/bar\.md, rep default:\n is outdated: /).to_stdout,
+ )
+ end
+
+ it 'shows file and dependencies as outdated after content modification' do
+ File.write('content/foo.md', "---\ntitle: hello\n---\n\nfoooOoooOOoooOooo")
+
+ expect { Nanoc::CLI.run(%w(show-data --no-color)) }.to(
+ output(/^item \/foo\.md, rep default:\n is outdated: /).to_stdout,
+ )
+ expect { Nanoc::CLI.run(%w(show-data --no-color)) }.to(
+ output(/^item \/bar\.md, rep default:\n is outdated: /).to_stdout,
+ )
+ end
+
+ it 'shows file and dependencies as outdated after title modification' do
+ File.write('content/foo.md', "---\ntitle: bye\n---\n\nfoo")
+
+ expect { Nanoc::CLI.run(%w(show-data --no-color)) }.to(
+ output(/^item \/foo\.md, rep default:\n is outdated: /).to_stdout,
+ )
+ expect { Nanoc::CLI.run(%w(show-data --no-color)) }.to(
+ output(/^item \/bar\.md, rep default:\n is outdated: /).to_stdout,
+ )
+ end
+
+ it 'shows file and dependencies as not outdated after rule modification' do
+ File.write('Rules', <<EOS)
+compile '/foo.*' do
+ filter :erb
+ write '/foo.html'
+end
+
+compile '/bar.*' do
+ filter :erb
+ write '/bar.html'
+end
+EOS
+
+ expect { Nanoc::CLI.run(%w(show-data --no-color)) }.to(
+ output(/^item \/foo\.md, rep default:\n is outdated: /).to_stdout,
+ )
+ expect { Nanoc::CLI.run(%w(show-data --no-color)) }.to(
+ output(/^item \/bar\.md, rep default:\n is not outdated/).to_stdout,
+ )
+ end
+ end
+end
diff --git a/spec/nanoc/regressions/gh_1015_spec.rb b/spec/nanoc/regressions/gh_1015_spec.rb
new file mode 100644
index 0000000..af647da
--- /dev/null
+++ b/spec/nanoc/regressions/gh_1015_spec.rb
@@ -0,0 +1,17 @@
+describe 'GH-1015', site: true, stdio: true do
+ before do
+ File.write('content/foo.md', 'I am foo!')
+
+ File.write('Rules', <<EOS)
+ compile '/foo.*' do
+ filter :erb, stuff: self
+ write 'foo.html'
+ end
+EOS
+ end
+
+ it 'errors' do
+ expect { Nanoc::CLI.run(%w(compile --verbose)) }.to raise_exception(Nanoc::Int::ItemRepRouter::RouteWithoutSlashError)
+ expect(File.file?('outputfoo.html')).not_to be
+ end
+end
diff --git a/spec/nanoc/regressions/gh_1031_spec.rb b/spec/nanoc/regressions/gh_1031_spec.rb
new file mode 100644
index 0000000..223fc41
--- /dev/null
+++ b/spec/nanoc/regressions/gh_1031_spec.rb
@@ -0,0 +1,54 @@
+describe 'GH-1031', site: true, stdio: true do
+ before do
+ File.write('content/foo.md', '[<%= @items["/bar.*"].compiled_content %>]')
+ File.write('content/bar.md', 'I am bar!')
+
+ File.write('Rules', <<EOS)
+ compile '/bar.*' do
+ write '/bar.txt'
+ end
+
+ compile '/foo.*', rep: :default do
+ write '/foo.txt'
+ end
+
+ compile '/foo.*', rep: :depz do
+ filter :erb
+ write '/foo_deps.txt'
+ end
+EOS
+ end
+
+ it 'recompiles all reps of a changed item' do
+ Nanoc::CLI.run(%w(compile))
+ expect(File.file?('output/bar.txt')).to be
+ expect(File.file?('output/foo.txt')).to be
+ expect(File.file?('output/foo_deps.txt')).to be
+
+ File.write('Rules', <<EOS)
+ compile '/bar.*' do
+ write '/bar.txt'
+ end
+
+ compile '/foo.*', rep: :default do
+ write '/foo-new.txt'
+ end
+
+ compile '/foo.*', rep: :depz do
+ filter :erb
+ write '/foo_deps.txt'
+ end
+EOS
+
+ Nanoc::CLI.run(%w(compile))
+ expect(File.file?('output/bar.txt')).to be
+ expect(File.file?('output/foo.txt')).to be
+ expect(File.file?('output/foo_deps.txt')).to be
+ expect(File.read('output/foo_deps.txt')).to eq('[I am bar!]')
+
+ File.write('content/bar.md', 'I am a newer bar!')
+
+ Nanoc::CLI.run(%w(compile))
+ expect(File.read('output/foo_deps.txt')).to eq('[I am a newer bar!]')
+ end
+end
diff --git a/spec/nanoc/regressions/gh_1035_spec.rb b/spec/nanoc/regressions/gh_1035_spec.rb
new file mode 100644
index 0000000..4a13ea9
--- /dev/null
+++ b/spec/nanoc/regressions/gh_1035_spec.rb
@@ -0,0 +1,33 @@
+describe 'GH-1035', site: true, stdio: true do
+ before do
+ File.write('content/foo.md', '[<%= @items["/bar.*"].compiled_content(snapshot: :raw) %>]')
+ File.write('content/bar.md', 'I am bar!')
+
+ File.write('lib/stuff.rb', <<EOS)
+Class.new(Nanoc::Filter) do
+ identifier :gh_1031_text2bin
+ type :text => :binary
+
+ def run(content, params = {})
+ File.write(output_filename, content)
+ end
+end
+EOS
+
+ File.write('Rules', <<EOS)
+ compile '/bar.*' do
+ filter :gh_1031_text2bin
+ end
+
+ compile '/foo.*' do
+ filter :erb
+ write '/foo.txt'
+ end
+EOS
+ end
+
+ it 'can access textual content of now-binary item' do
+ Nanoc::CLI.run(%w(compile))
+ expect(File.read('output/foo.txt')).to eql('[I am bar!]')
+ end
+end
diff --git a/spec/nanoc/regressions/gh_1040_spec.rb b/spec/nanoc/regressions/gh_1040_spec.rb
new file mode 100644
index 0000000..fd08aee
--- /dev/null
+++ b/spec/nanoc/regressions/gh_1040_spec.rb
@@ -0,0 +1,22 @@
+describe 'GH-1040', site: true, stdio: true do
+ before do
+ File.write('content/foo.txt', 'bar=<%= @items["/bar.*"].compiled_content %>')
+ File.write('content/bar.txt', 'foo=<%= @items["/foo.*"].compiled_content %>')
+
+ File.write('layouts/default.erb', '*<%= yield %>*')
+
+ File.write('Rules', <<EOS)
+ compile '/*' do
+ filter :erb
+ layout '/default.*'
+ write item.identifier
+ end
+
+ layout '/*.erb', :erb
+EOS
+ end
+
+ it 'errors' do
+ expect { Nanoc::CLI.run(%w(compile)) }.to raise_error(Nanoc::Int::Errors::RecursiveCompilation)
+ end
+end
diff --git a/spec/nanoc/regressions/gh_761_spec.rb b/spec/nanoc/regressions/gh_761_spec.rb
new file mode 100644
index 0000000..afbacff
--- /dev/null
+++ b/spec/nanoc/regressions/gh_761_spec.rb
@@ -0,0 +1,23 @@
+describe 'GH-761', site: true do
+ before do
+ File.write('content/donkey.md', 'Compiled content donkey!')
+
+ File.write('layouts/foo.erb', '[<%= @item.compiled_content %>]')
+
+ File.write('Rules', <<EOS)
+ compile '/**/*' do
+ layout '/foo.*'
+ write '/donkey.html'
+ end
+
+ layout '/foo.*', :erb
+EOS
+ end
+
+ it 'supports #compiled_content instead of yield' do
+ site = Nanoc::Int::SiteLoader.new.new_from_cwd
+ site.compile
+
+ expect(File.read('output/donkey.html')).to eql('[Compiled content donkey!]')
+ end
+end
diff --git a/spec/nanoc/regressions/gh_767_spec.rb b/spec/nanoc/regressions/gh_767_spec.rb
new file mode 100644
index 0000000..fb73a2f
--- /dev/null
+++ b/spec/nanoc/regressions/gh_767_spec.rb
@@ -0,0 +1,19 @@
+describe 'GH-767', site: true do
+ before do
+ File.write('content/donkey.md', 'Compiled content donkey!')
+
+ File.write('Rules', <<EOS)
+ compile '/**/*' do
+ filter :erb, stuff: item.path
+ write '/donkey.html'
+ end
+
+ layout '/foo.*', :erb
+EOS
+ end
+
+ it 'does not expose #path on @item' do
+ site = Nanoc::Int::SiteLoader.new.new_from_cwd
+ expect { site.compile }.to raise_error(NoMethodError, /undefined method .*path.* for .*Nanoc::ItemWithoutRepsView/)
+ end
+end
diff --git a/spec/nanoc/regressions/gh_769_spec.rb b/spec/nanoc/regressions/gh_769_spec.rb
new file mode 100644
index 0000000..d3c3aad
--- /dev/null
+++ b/spec/nanoc/regressions/gh_769_spec.rb
@@ -0,0 +1,30 @@
+describe 'GH-769', site: true do
+ before do
+ File.write('content/index.md', 'Index!')
+ File.write('content/donkey.md', 'Donkey! [<%= @item.parent.identifier %>]')
+
+ File.open('nanoc.yaml', 'w') do |io|
+ io << 'string_pattern_type: legacy' << "\n"
+ io << 'data_sources:' << "\n"
+ io << ' -' << "\n"
+ io << ' type: filesystem' << "\n"
+ io << ' identifier_type: legacy' << "\n"
+ end
+
+ File.write('Rules', <<EOS)
+ compile '*' do
+ filter :erb
+ write item.identifier + 'index.html'
+ end
+
+ layout '/foo.*', :erb
+EOS
+ end
+
+ it 'finds the parent if the parent is root' do
+ site = Nanoc::Int::SiteLoader.new.new_from_cwd
+ site.compile
+
+ expect(File.read('output/donkey/index.html')).to eql('Donkey! [/]')
+ end
+end
diff --git a/spec/nanoc/regressions/gh_776_spec.rb b/spec/nanoc/regressions/gh_776_spec.rb
new file mode 100644
index 0000000..e847215
--- /dev/null
+++ b/spec/nanoc/regressions/gh_776_spec.rb
@@ -0,0 +1,43 @@
+describe 'GH-776', site: true do
+ before do
+ File.write('content/donkey.md', 'Donkey!')
+
+ File.write('Rules', <<EOS)
+ route '/donkey.*', snapshot: :secret do
+ '/donkey-secret.html'
+ end
+
+ compile '/donkey.*' do
+ filter :erb
+ snapshot :secret
+ write '/donkey.html'
+ end
+
+ layout '/foo.*', :erb
+EOS
+ end
+
+ let(:site) { Nanoc::Int::SiteLoader.new.new_from_cwd }
+
+ before do
+ site.compile
+ end
+
+ context 'without pruning' do
+ it 'writes two files' do
+ expect(File.read('output/donkey.html')).to eql('Donkey!')
+ expect(File.read('output/donkey-secret.html')).to eql('Donkey!')
+ end
+ end
+
+ context 'with pruning' do
+ before do
+ Nanoc::Pruner.new(site.config, site.compiler.reps).run
+ end
+
+ it 'does not prune written snapshots' do
+ expect(File.read('output/donkey.html')).to eql('Donkey!')
+ expect(File.read('output/donkey-secret.html')).to eql('Donkey!')
+ end
+ end
+end
diff --git a/spec/nanoc/regressions/gh_787_spec.rb b/spec/nanoc/regressions/gh_787_spec.rb
new file mode 100644
index 0000000..395427f
--- /dev/null
+++ b/spec/nanoc/regressions/gh_787_spec.rb
@@ -0,0 +1,19 @@
+describe 'GH-787', site: true, stdio: true do
+ before do
+ File.write('Rules', <<EOS)
+ preprocess do
+ @items.create('foo', {}, '/pig.md')
+ end
+
+ compile '/**/*' do
+ write '/oink.html'
+ end
+
+ layout '/foo.*', :erb
+EOS
+ end
+
+ it 'runs the preprocessor only once' do
+ expect { Nanoc::CLI.run(['compile']) }.not_to raise_error
+ end
+end
diff --git a/spec/nanoc/regressions/gh_795_spec.rb b/spec/nanoc/regressions/gh_795_spec.rb
new file mode 100644
index 0000000..0e03117
--- /dev/null
+++ b/spec/nanoc/regressions/gh_795_spec.rb
@@ -0,0 +1,19 @@
+describe 'GH-795', site: true, stdio: true do
+ before do
+ File.write('content/items.md', 'Frozen? <%= @items.unwrap.frozen? %>!')
+ File.write('content/items-view.md', 'Frozen? <%= @items.frozen? %>!')
+ File.write('Rules', <<EOS)
+ compile '/**/*' do
+ filter :erb
+ write item.identifier.without_ext + '.html'
+ end
+EOS
+ end
+
+ it 'freezes @items' do
+ Nanoc::CLI.run(['compile'])
+
+ expect(File.read('output/items.html')).to eql('Frozen? true!')
+ expect(File.read('output/items-view.html')).to eql('Frozen? true!')
+ end
+end
diff --git a/spec/nanoc/regressions/gh_804_spec.rb b/spec/nanoc/regressions/gh_804_spec.rb
new file mode 100644
index 0000000..f39d93b
--- /dev/null
+++ b/spec/nanoc/regressions/gh_804_spec.rb
@@ -0,0 +1,26 @@
+describe 'GH-804', site: true, stdio: true do
+ before do
+ File.write('content/item.md', 'Stuff!')
+ File.write('Rules', <<EOS)
+ compile '/**/*' do
+ filter :erb if item[:dynamic]
+ write item.identifier.without_ext + '.html'
+ end
+EOS
+
+ File.write('Checks', <<EOS)
+check :donkey do
+ self.add_issue('Not enough donkeys')
+ self.add_issue('Too many cats', subject: '/catlady.md')
+end
+EOS
+ end
+
+ it 'does not crash' do
+ expect { Nanoc::CLI.run(%w(check donkey)) }.to(
+ raise_error(Nanoc::Int::Errors::GenericTrivial, 'One or more checks failed').and(
+ output(/Issues found!\n \(global\):\n \[ (\e\[31m)?ERROR(\e\[0m)? \] donkey - Not enough donkeys\n \/catlady.md:\n \[ (\e\[31m)?ERROR(\e\[0m)? \] donkey - Too many cats\n/).to_stdout,
+ ),
+ )
+ end
+end
diff --git a/spec/nanoc/regressions/gh_807_spec.rb b/spec/nanoc/regressions/gh_807_spec.rb
new file mode 100644
index 0000000..7231243
--- /dev/null
+++ b/spec/nanoc/regressions/gh_807_spec.rb
@@ -0,0 +1,17 @@
+describe 'GH-807', site: true, stdio: true do
+ before do
+ File.write('content/item.md', 'Stuff!')
+ File.write('Rules', <<EOS)
+ compile '/**/*' do
+ filter :erb if item[:dynamic]
+ write item.identifier.without_ext + '.html'
+ end
+EOS
+ end
+
+ it 'does not crash' do
+ Nanoc::CLI.run(['compile'])
+
+ expect(File.read('output/item.html')).to eql('Stuff!')
+ end
+end
diff --git a/spec/nanoc/regressions/gh_809_spec.rb b/spec/nanoc/regressions/gh_809_spec.rb
new file mode 100644
index 0000000..fc67add
--- /dev/null
+++ b/spec/nanoc/regressions/gh_809_spec.rb
@@ -0,0 +1,17 @@
+describe 'GH-809', site: true, stdio: true do
+ before do
+ File.write('content/greeting.md', 'Hallöchen!')
+ File.write('Rules', <<EOS)
+ compile '/**/*' do
+ snapshot :something, path: '/greeting.tmp'
+ end
+EOS
+ end
+
+ specify 'stale check does not consider output/greeting.tmp as stale' do
+ Nanoc::CLI.run(['compile'])
+
+ regex = /Running check stale… (\e\[32m)?ok(\e\[0m)?/
+ expect { Nanoc::CLI.run(%w(check stale)) }.to output(regex).to_stdout
+ end
+end
diff --git a/spec/nanoc/regressions/gh_813_spec.rb b/spec/nanoc/regressions/gh_813_spec.rb
new file mode 100644
index 0000000..8bf8625
--- /dev/null
+++ b/spec/nanoc/regressions/gh_813_spec.rb
@@ -0,0 +1,22 @@
+describe 'GH-813', site: true, stdio: true do
+ before do
+ File.write('nanoc.yaml', "enable_output_diff: true\n")
+ File.write('content/greeting.md', 'Hallöchen!')
+ File.write('Rules', <<EOS)
+ compile '/**/*' do
+ snapshot :donkey, path: '/donkey.html'
+ filter :kramdown
+ end
+EOS
+
+ Nanoc::CLI.run(['compile'])
+ end
+
+ specify 'Nanoc generates diff for proper path' do
+ File.write('content/greeting.md', 'Hellosies!')
+ Nanoc::CLI.run(['compile'])
+
+ diff = File.read('output.diff')
+ expect(diff).to start_with("--- output/donkey.html\n+++ output/donkey.html\n")
+ end
+end
diff --git a/spec/nanoc/regressions/gh_815_spec.rb b/spec/nanoc/regressions/gh_815_spec.rb
new file mode 100644
index 0000000..35eb11d
--- /dev/null
+++ b/spec/nanoc/regressions/gh_815_spec.rb
@@ -0,0 +1,18 @@
+describe 'GH-815', site: true, stdio: true do
+ before do
+ File.write('nanoc.yaml', "animal: \"donkey\"\n")
+ File.write('content/foo.md', '<%= @config.key?(:animal) %>')
+ File.write('Rules', <<EOS)
+ compile '/**/*' do
+ filter :erb
+ write item.identifier.without_ext + '.txt'
+ end
+EOS
+ end
+
+ it 'handles #key? properly' do
+ Nanoc::CLI.run(['compile'])
+
+ expect(File.read('output/foo.txt')).to eql('true')
+ end
+end
diff --git a/spec/nanoc/regressions/gh_828_spec.rb b/spec/nanoc/regressions/gh_828_spec.rb
new file mode 100644
index 0000000..759787c
--- /dev/null
+++ b/spec/nanoc/regressions/gh_828_spec.rb
@@ -0,0 +1,23 @@
+describe 'GH-828', site: true, stdio: true do
+ before do
+ File.write('content/bad.md', "---\nbad: true\n---\n\nI am bad!")
+ File.write('content/good.md', "---\nbad: false\n---\n\nI am good!")
+ File.write('Rules', <<EOS)
+ preprocess do
+ @items.delete_if { |i| i[:bad] }
+ end
+
+ compile '/**/*' do
+ filter :erb
+ write item.identifier.without_ext + '.txt'
+ end
+EOS
+ end
+
+ it 'only writes good page' do
+ Nanoc::CLI.run(['compile'])
+
+ expect(File.file?('output/good.txt')).to be
+ expect(File.file?('output/bad.txt')).not_to be
+ end
+end
diff --git a/spec/nanoc/regressions/gh_833_spec.rb b/spec/nanoc/regressions/gh_833_spec.rb
new file mode 100644
index 0000000..4c7eb68
--- /dev/null
+++ b/spec/nanoc/regressions/gh_833_spec.rb
@@ -0,0 +1,14 @@
+describe 'GH-833', site: true, stdio: true do
+ before do
+ File.write('content/foo.md', 'stuff')
+ File.write('Rules', <<EOS)
+ compile '/**/*' do
+ write item.identifier.without_ext + '.txt'
+ end
+EOS
+ end
+
+ it 'runs show-data without crashing' do
+ Nanoc::CLI.run(['show-data'])
+ end
+end
diff --git a/spec/nanoc/regressions/gh_841_spec.rb b/spec/nanoc/regressions/gh_841_spec.rb
new file mode 100644
index 0000000..2302c7d
--- /dev/null
+++ b/spec/nanoc/regressions/gh_841_spec.rb
@@ -0,0 +1,15 @@
+describe 'GH-841', site: true, stdio: true do
+ before do
+ File.write('content/foo.md', 'stuff')
+
+ File.write('Rules', <<EOS)
+ preprocess do
+ items.delete_if { |_| true }
+ end
+EOS
+ end
+
+ it 'preprocesses before running the check' do
+ Nanoc::CLI.run(%w(check stale))
+ end
+end
diff --git a/spec/nanoc/regressions/gh_867_spec.rb b/spec/nanoc/regressions/gh_867_spec.rb
new file mode 100644
index 0000000..9e9c7e3
--- /dev/null
+++ b/spec/nanoc/regressions/gh_867_spec.rb
@@ -0,0 +1,15 @@
+describe 'GH-867', site: true, stdio: true do
+ before do
+ File.write('content/foo.md', 'stuff')
+
+ File.write('Rules', <<EOS)
+ preprocess do
+ items.delete_if { |_| true }
+ end
+EOS
+ end
+
+ it 'preprocesses before running show-data' do
+ expect { Nanoc::CLI.run(%w(show-data)) }.not_to output(/foo/).to_stdout
+ end
+end
diff --git a/spec/nanoc/regressions/gh_882_spec.rb b/spec/nanoc/regressions/gh_882_spec.rb
new file mode 100644
index 0000000..86baa5d
--- /dev/null
+++ b/spec/nanoc/regressions/gh_882_spec.rb
@@ -0,0 +1,29 @@
+describe 'GH-882', site: true, stdio: true do
+ before do
+ File.write('content/foo.md', 'I am foo!')
+ File.write('content/bar.md', 'I am bar!')
+
+ File.write('Rules', <<EOS)
+ compile '/**/*' do
+ write item.identifier.without_ext + '.html'
+ end
+
+ postprocess do
+ modified_reps = items.flat_map(&:modified)
+ modified_reps.each do |rep|
+ puts "Modified: \#{rep.item.identifier} - \#{rep.name}"
+ end
+ end
+EOS
+ end
+
+ example do
+ Nanoc::CLI.run(%w(compile))
+
+ File.write('content/bar.md', 'I am bar! Modified!')
+ expect { Nanoc::CLI.run(%w(compile)) }.to output(%r{^Modified: /bar.md - default$}).to_stdout
+
+ File.write('content/bar.md', 'I am bar! Modified again!')
+ expect { Nanoc::CLI.run(%w(compile)) }.not_to output(%r{^Modified: /foo.md - default$}).to_stdout
+ end
+end
diff --git a/spec/nanoc/regressions/gh_885_spec.rb b/spec/nanoc/regressions/gh_885_spec.rb
new file mode 100644
index 0000000..095b888
--- /dev/null
+++ b/spec/nanoc/regressions/gh_885_spec.rb
@@ -0,0 +1,30 @@
+describe 'GH-885', site: true, stdio: true do
+ before do
+ File.write(
+ 'content/index.html',
+ "<%= @items['/hello.*'].compiled_content %> - <%= Time.now.to_f %>",
+ )
+
+ File.write('Rules', <<EOS)
+ preprocess do
+ items.create('hi!', {}, '/hello.html')
+ end
+
+ compile '/**/*' do
+ filter :erb
+ write item.identifier.without_ext + '.html'
+ end
+EOS
+ end
+
+ example do
+ Nanoc::CLI.run(%w(compile))
+ before = File.read('output/index.html')
+
+ sleep(0.1)
+ Nanoc::CLI.run(%w(compile))
+ after = File.read('output/index.html')
+ expect(after).to eql(before)
+ expect(after).to match(/\Ahi! - \d+/)
+ end
+end
diff --git a/spec/nanoc/regressions/gh_891_spec.rb b/spec/nanoc/regressions/gh_891_spec.rb
new file mode 100644
index 0000000..476cfe6
--- /dev/null
+++ b/spec/nanoc/regressions/gh_891_spec.rb
@@ -0,0 +1,26 @@
+describe 'GH-891', site: true, stdio: true do
+ before do
+ File.write('layouts/foo.erb', 'giraffes? <%= yield %>')
+ File.write('Rules', <<EOS)
+ preprocess do
+ items.create('yes!', {}, '/hello.html')
+ end
+
+ compile '/**/*' do
+ layout '/foo.*'
+ write item.identifier.without_ext + '.html'
+ end
+
+ layout '/foo.*', :erb
+EOS
+ end
+
+ example do
+ Nanoc::CLI.run(%w(compile))
+ expect(File.read('output/hello.html')).to include('giraffes?')
+
+ File.write('layouts/foo.erb', 'donkeys? <%= yield %>')
+ Nanoc::CLI.run(%w(compile))
+ expect(File.read('output/hello.html')).to include('donkeys?')
+ end
+end
diff --git a/spec/nanoc/regressions/gh_913_spec.rb b/spec/nanoc/regressions/gh_913_spec.rb
new file mode 100644
index 0000000..7118ad9
--- /dev/null
+++ b/spec/nanoc/regressions/gh_913_spec.rb
@@ -0,0 +1,24 @@
+describe 'GH-913', site: true, stdio: true do
+ before do
+ File.write('content/hello.html', 'hi!')
+
+ File.write('Rules', <<EOS)
+ postprocess do
+ items.map(&:compiled_content)
+ end
+
+ compile '/**/*' do
+ write item.identifier.without_ext + '.html'
+ end
+
+ layout '/foo.*', :erb
+EOS
+ end
+
+ example do
+ 2.times do
+ Nanoc::CLI.run(%w(compile))
+ expect(File.read('output/hello.html')).to eq('hi!')
+ end
+ end
+end
diff --git a/spec/nanoc/regressions/gh_928_spec.rb b/spec/nanoc/regressions/gh_928_spec.rb
new file mode 100644
index 0000000..f4b95c6
--- /dev/null
+++ b/spec/nanoc/regressions/gh_928_spec.rb
@@ -0,0 +1,5 @@
+describe 'GH-928', site: true, stdio: true do
+ example do
+ expect { Nanoc::CLI.run(%w(check --list)) }.to output(%r{^ css$}).to_stdout
+ end
+end
diff --git a/spec/nanoc/regressions/gh_937_spec.rb b/spec/nanoc/regressions/gh_937_spec.rb
new file mode 100644
index 0000000..ea2fadb
--- /dev/null
+++ b/spec/nanoc/regressions/gh_937_spec.rb
@@ -0,0 +1,25 @@
+describe 'GH-937', site: true, stdio: true do
+ before do
+ File.write('content/style.sass', ".test\n color: red")
+
+ File.write(
+ 'nanoc.yaml',
+ "sass_style: compact\nenvironments:\n staging:\n sass_style: expanded",
+ )
+
+ File.write('Rules', <<EOS)
+compile '/*.sass' do
+ filter :sass, style: @config[:sass_style].to_sym
+ write item.identifier.without_ext + '.css'
+end
+EOS
+ end
+
+ it 'does not use cache when switching environments' do
+ Nanoc::CLI.run(%w(compile))
+ expect(File.read('output/style.css')).to eq(".test { color: red; }\n")
+
+ Nanoc::CLI.run(%w(compile --env=staging))
+ expect(File.read('output/style.css')).to eq(".test {\n color: red;\n}\n")
+ end
+end
diff --git a/spec/nanoc/regressions/gh_942_spec.rb b/spec/nanoc/regressions/gh_942_spec.rb
new file mode 100644
index 0000000..a0bb383
--- /dev/null
+++ b/spec/nanoc/regressions/gh_942_spec.rb
@@ -0,0 +1,21 @@
+describe 'GH-942', site: true, stdio: true do
+ before do
+ File.write('content/foo.md', 'Foo!')
+ File.write('Rules', <<EOS)
+ compile '/foo.*' do
+ write '/parent/foo'
+ end
+EOS
+
+ File.open('nanoc.yaml', 'w') do |io|
+ io << 'prune:' << "\n"
+ io << ' auto_prune: true' << "\n"
+ end
+ end
+
+ example do
+ File.write('output/parent', 'Hahaaa! I am a file and not a directory!')
+ Nanoc::CLI.run(%w(compile))
+ expect(File.read('output/parent/foo')).to eq('Foo!')
+ end
+end
diff --git a/spec/nanoc/regressions/gh_947_spec.rb b/spec/nanoc/regressions/gh_947_spec.rb
new file mode 100644
index 0000000..54c704d
--- /dev/null
+++ b/spec/nanoc/regressions/gh_947_spec.rb
@@ -0,0 +1,21 @@
+describe 'GH-947', site: true, stdio: true do
+ before do
+ File.write('content/foo.md', 'Foo!')
+ File.write('Rules', <<EOS)
+ compile '/foo.*' do
+ write '/foo'
+ end
+EOS
+
+ File.open('nanoc.yaml', 'w') do |io|
+ io << 'prune:' << "\n"
+ io << ' auto_prune: true' << "\n"
+ end
+ end
+
+ example do
+ File.write('output/foo', 'I am an older foo!')
+ expect { Nanoc::CLI.run(%w(compile)) }.to output(%r{\s+update.* output/foo$}).to_stdout
+ expect(File.read('output/foo')).to eq('Foo!')
+ end
+end
diff --git a/spec/nanoc/regressions/gh_948_spec.rb b/spec/nanoc/regressions/gh_948_spec.rb
new file mode 100644
index 0000000..e7b9d84
--- /dev/null
+++ b/spec/nanoc/regressions/gh_948_spec.rb
@@ -0,0 +1,16 @@
+describe 'GH-948', site: true, stdio: true do
+ before do
+ File.write('content/foo.md', 'Foo!')
+
+ File.open('nanoc.yaml', 'w') do |io|
+ io << 'prune:' << "\n"
+ io << ' auto_prune: true' << "\n"
+ end
+
+ FileUtils.rm_rf('output')
+ end
+
+ it 'does not crash when output dir is not present' do
+ Nanoc::CLI.run(%w(compile))
+ end
+end
diff --git a/spec/nanoc/regressions/gh_951_spec.rb b/spec/nanoc/regressions/gh_951_spec.rb
new file mode 100644
index 0000000..aa284fb
--- /dev/null
+++ b/spec/nanoc/regressions/gh_951_spec.rb
@@ -0,0 +1,19 @@
+describe 'GH-951', site: true, stdio: true do
+ before do
+ File.write('content/foo.md', 'Foo!')
+
+ File.open('nanoc.yaml', 'w') do |io|
+ io << 'string_pattern_type: legacy' << "\n"
+ end
+
+ File.write('Rules', <<EOS)
+ passthrough '/foo.md'
+EOS
+ end
+
+ it 'copies foo.md' do
+ Nanoc::CLI.run(%w(compile))
+
+ expect(File.file?('output/foo.md')).to eq(true)
+ end
+end
diff --git a/spec/nanoc/regressions/gh_954_spec.rb b/spec/nanoc/regressions/gh_954_spec.rb
new file mode 100644
index 0000000..c44709e
--- /dev/null
+++ b/spec/nanoc/regressions/gh_954_spec.rb
@@ -0,0 +1,33 @@
+describe 'GH-954', site: true, stdio: true do
+ before do
+ File.write('content/foo.md', 'foo <a href="/">root</a>')
+ File.write('content/bar.md', 'bar <a href="/">root</a>')
+ File.write('content/bar-copy.md', '<%= @items["/bar.*"].compiled_content(snapshot: :last) %>')
+
+ File.write('Rules', <<EOS)
+compile '/foo.*' do
+ filter :relativize_paths, type: :html unless rep.path.nil?
+ write item.identifier.without_ext + '.html'
+end
+
+compile '/bar.*' do
+ filter :relativize_paths, type: :html unless rep.path.nil?
+end
+
+compile '/bar-copy.*' do
+ filter :erb
+ write item.identifier.without_ext + '.html'
+end
+EOS
+ end
+
+ it 'properly filters foo.md' do
+ Nanoc::CLI.run(%w(compile))
+
+ # Path is relativized
+ expect(File.read('output/foo.html')).to eq('foo <a href="./">root</a>')
+
+ # Path is not relativized
+ expect(File.read('output/bar-copy.html')).to eq('bar <a href="/">root</a>')
+ end
+end
diff --git a/spec/nanoc/regressions/gh_970a_spec.rb b/spec/nanoc/regressions/gh_970a_spec.rb
new file mode 100644
index 0000000..b36176d
--- /dev/null
+++ b/spec/nanoc/regressions/gh_970a_spec.rb
@@ -0,0 +1,17 @@
+describe 'GH-970 (show-rules)', site: true, stdio: true do
+ before do
+ File.write('content/foo.md', 'foo')
+
+ File.write('Rules', <<EOS)
+compile '/foo.*' do
+ write '/donkey.html'
+end
+EOS
+ end
+
+ it 'shows reps' do
+ expect { Nanoc::CLI.run(%w(show-rules --no-color)) }.to(
+ output(/^Item \/foo\.md:\n Rep default: \/foo\.\*$/).to_stdout,
+ )
+ end
+end
diff --git a/spec/nanoc/regressions/gh_970b_spec.rb b/spec/nanoc/regressions/gh_970b_spec.rb
new file mode 100644
index 0000000..0b03932
--- /dev/null
+++ b/spec/nanoc/regressions/gh_970b_spec.rb
@@ -0,0 +1,50 @@
+describe 'GH-970 (show-data)', site: true, stdio: true do
+ before do
+ File.write('content/foo.md', 'foo')
+ File.write('content/bar.md', '<%= @items["/foo.*"].compiled_content %>')
+
+ File.write('Rules', <<EOS)
+compile '/foo.*' do
+ write '/foo.html'
+end
+
+compile '/bar.*' do
+ filter :erb
+ write '/bar.html'
+end
+EOS
+ end
+
+ before { Nanoc::CLI.run(%w(compile)) }
+
+ it 'shows default rep outdatedness' do
+ expect { Nanoc::CLI.run(%w(show-data --no-color)) }.to(
+ output(/^item \/foo\.md, rep default:\n is not outdated/).to_stdout,
+ )
+ expect { Nanoc::CLI.run(%w(show-data --no-color)) }.to(
+ output(/^item \/bar\.md, rep default:\n is not outdated/).to_stdout,
+ )
+ end
+
+ it 'shows file as outdated after modification' do
+ File.write('content/bar.md', 'JUST BAR!')
+
+ expect { Nanoc::CLI.run(%w(show-data --no-color)) }.to(
+ output(/^item \/foo\.md, rep default:\n is not outdated/).to_stdout,
+ )
+ expect { Nanoc::CLI.run(%w(show-data --no-color)) }.to(
+ output(/^item \/bar\.md, rep default:\n is outdated: /).to_stdout,
+ )
+ end
+
+ it 'shows file and dependencies as outdated after modification' do
+ File.write('content/foo.md', 'FOO!')
+
+ expect { Nanoc::CLI.run(%w(show-data --no-color)) }.to(
+ output(/^item \/foo\.md, rep default:\n is outdated: /).to_stdout,
+ )
+ expect { Nanoc::CLI.run(%w(show-data --no-color)) }.to(
+ output(/^item \/bar\.md, rep default:\n is outdated: /).to_stdout,
+ )
+ end
+end
diff --git a/spec/nanoc/regressions/gh_974_spec.rb b/spec/nanoc/regressions/gh_974_spec.rb
new file mode 100644
index 0000000..0b5767c
--- /dev/null
+++ b/spec/nanoc/regressions/gh_974_spec.rb
@@ -0,0 +1,17 @@
+describe 'GH-974', site: true, stdio: true do
+ before do
+ File.write('content/foo.md', 'foo')
+
+ File.write('Rules', <<EOS)
+compile '/foo.*' do
+ write item.identifier
+end
+EOS
+ end
+
+ it 'writes to path corresponding to identifier' do
+ Nanoc::CLI.run(%w(compile))
+
+ expect(File.file?('output/foo.md')).to eq(true)
+ end
+end
diff --git a/spec/nanoc/regressions/gh_981_spec.rb b/spec/nanoc/regressions/gh_981_spec.rb
new file mode 100644
index 0000000..6e1867a
--- /dev/null
+++ b/spec/nanoc/regressions/gh_981_spec.rb
@@ -0,0 +1,21 @@
+describe 'GH-981', site: true, stdio: true do
+ before do
+ File.write('content/foo.md', 'I am foo!')
+
+ File.write('Rules', <<EOS)
+ compile '/foo.*' do
+ filter :erb, stuff: self
+ write '/foo.html'
+ end
+EOS
+ end
+
+ it 'creates at first' do
+ expect { Nanoc::CLI.run(%w(compile --verbose)) }.to output(%r{create.*output/foo\.html$}).to_stdout
+ end
+
+ it 'skips the item on second try' do
+ Nanoc::CLI.run(%w(compile))
+ expect { Nanoc::CLI.run(%w(compile --verbose)) }.to output(%r{skip.*output/foo\.html$}).to_stdout
+ end
+end
diff --git a/spec/nanoc/rule_dsl/recording_executor_spec.rb b/spec/nanoc/rule_dsl/recording_executor_spec.rb
new file mode 100644
index 0000000..f6d2e97
--- /dev/null
+++ b/spec/nanoc/rule_dsl/recording_executor_spec.rb
@@ -0,0 +1,142 @@
+describe Nanoc::RuleDSL::RecordingExecutor do
+ let(:executor) { described_class.new(rule_memory) }
+
+ let(:rule_memory) { Nanoc::Int::RuleMemory.new(rep) }
+ let(:rep) { double(:rep) }
+
+ describe '#filter' do
+ it 'records filter call without arguments' do
+ executor.filter(:erb)
+
+ expect(rule_memory.size).to eql(1)
+ expect(rule_memory[0]).to be_a(Nanoc::Int::ProcessingActions::Filter)
+ expect(rule_memory[0].filter_name).to eql(:erb)
+ expect(rule_memory[0].params).to eql({})
+ end
+
+ it 'records filter call with arguments' do
+ executor.filter(:erb, x: 123)
+
+ expect(rule_memory.size).to eql(1)
+ expect(rule_memory[0]).to be_a(Nanoc::Int::ProcessingActions::Filter)
+ expect(rule_memory[0].filter_name).to eql(:erb)
+ expect(rule_memory[0].params).to eql(x: 123)
+ end
+ end
+
+ describe '#layout' do
+ it 'records layout call without arguments' do
+ executor.layout('/default.*')
+
+ expect(rule_memory.size).to eql(2)
+
+ expect(rule_memory[0]).to be_a(Nanoc::Int::ProcessingActions::Snapshot)
+ expect(rule_memory[0].snapshot_name).to eql(:pre)
+ expect(rule_memory[0].path).to be_nil
+
+ expect(rule_memory[1]).to be_a(Nanoc::Int::ProcessingActions::Layout)
+ expect(rule_memory[1].layout_identifier).to eql('/default.*')
+ expect(rule_memory[1].params).to eql({})
+ end
+
+ it 'records layout call with arguments' do
+ executor.layout('/default.*', donkey: 123)
+
+ expect(rule_memory.size).to eql(2)
+
+ expect(rule_memory[0]).to be_a(Nanoc::Int::ProcessingActions::Snapshot)
+ expect(rule_memory[0].snapshot_name).to eql(:pre)
+ expect(rule_memory[0].path).to be_nil
+
+ expect(rule_memory[1]).to be_a(Nanoc::Int::ProcessingActions::Layout)
+ expect(rule_memory[1].layout_identifier).to eql('/default.*')
+ expect(rule_memory[1].params).to eql(donkey: 123)
+ end
+
+ it 'fails when passed a symbol' do
+ expect { executor.layout(:default, donkey: 123) }.to raise_error(ArgumentError)
+ end
+ end
+
+ describe '#snapshot' do
+ context 'snapshot already exists' do
+ before do
+ executor.snapshot(:foo)
+ end
+
+ it 'raises when creating same snapshot' do
+ expect { executor.snapshot(:foo) }
+ .to raise_error(Nanoc::Int::Errors::CannotCreateMultipleSnapshotsWithSameName)
+ end
+ end
+
+ context 'no arguments' do
+ subject { executor.snapshot(:foo) }
+
+ it 'records' do
+ subject
+ expect(rule_memory.size).to eql(1)
+ expect(rule_memory[0]).to be_a(Nanoc::Int::ProcessingActions::Snapshot)
+ expect(rule_memory[0].snapshot_name).to eql(:foo)
+ expect(rule_memory[0].path).to be_nil
+ end
+ end
+
+ context 'final argument' do
+ subject { executor.snapshot(:foo, path: path) }
+ let(:path) { nil }
+
+ context 'routing rule does not exist' do
+ context 'no explicit path given' do
+ it 'records' do
+ subject
+ expect(rule_memory.size).to eql(1)
+ expect(rule_memory[0]).to be_a(Nanoc::Int::ProcessingActions::Snapshot)
+ expect(rule_memory[0].snapshot_name).to eql(:foo)
+ expect(rule_memory[0].path).to be_nil
+ end
+ end
+
+ context 'explicit path given as string' do
+ let(:path) { '/routed-foo.html' }
+
+ it 'records' do
+ subject
+ expect(rule_memory.size).to eql(1)
+ expect(rule_memory[0]).to be_a(Nanoc::Int::ProcessingActions::Snapshot)
+ expect(rule_memory[0].snapshot_name).to eql(:foo)
+ expect(rule_memory[0].path).to eql('/routed-foo.html')
+ end
+ end
+
+ context 'explicit path given as identifier' do
+ let(:path) { Nanoc::Identifier.from('/routed-foo.html') }
+
+ it 'records' do
+ subject
+ expect(rule_memory.size).to eql(1)
+ expect(rule_memory[0]).to be_a(Nanoc::Int::ProcessingActions::Snapshot)
+ expect(rule_memory[0].snapshot_name).to eql(:foo)
+ expect(rule_memory[0].path).to eql('/routed-foo.html')
+ end
+ end
+ end
+ end
+
+ it 'raises when given unknown arguments' do
+ expect { executor.snapshot(:foo, animal: 'giraffe') }
+ .to raise_error(ArgumentError)
+ end
+
+ it 'can create multiple snapshots with different names' do
+ executor.snapshot(:foo)
+ executor.snapshot(:bar)
+
+ expect(rule_memory.size).to eql(2)
+ expect(rule_memory[0]).to be_a(Nanoc::Int::ProcessingActions::Snapshot)
+ expect(rule_memory[0].snapshot_name).to eql(:foo)
+ expect(rule_memory[1]).to be_a(Nanoc::Int::ProcessingActions::Snapshot)
+ expect(rule_memory[1].snapshot_name).to eql(:bar)
+ end
+ end
+end
diff --git a/spec/nanoc/rule_dsl/rule_context_spec.rb b/spec/nanoc/rule_dsl/rule_context_spec.rb
new file mode 100644
index 0000000..79e1248
--- /dev/null
+++ b/spec/nanoc/rule_dsl/rule_context_spec.rb
@@ -0,0 +1,177 @@
+describe(Nanoc::RuleDSL::RuleContext) do
+ subject(:rule_context) do
+ described_class.new(rep: rep, site: site, executor: executor, view_context: view_context)
+ end
+
+ let(:item_identifier) { Nanoc::Identifier.new('/foo.md') }
+ let(:item) { Nanoc::Int::Item.new('content', {}, item_identifier) }
+ let(:config) { Nanoc::Int::Configuration.new }
+ let(:items) { Nanoc::Int::IdentifiableCollection.new(config) }
+ let(:layouts) { Nanoc::Int::IdentifiableCollection.new(config) }
+
+ let(:rep) { double(:rep, item: item) }
+ let(:site) { double(:site, items: items, layouts: layouts, config: config) }
+ let(:executor) { double(:executor) }
+ let(:reps) { double(:reps) }
+ let(:compilation_context) { double(:compilation_context) }
+ let(:view_context) { Nanoc::ViewContext.new(reps: reps, items: items, dependency_tracker: dependency_tracker, compilation_context: compilation_context) }
+ let(:dependency_tracker) { double(:dependency_tracker) }
+
+ describe '#initialize' do
+ it 'wraps objects in view classes' do
+ expect(subject.rep.class).to eql(Nanoc::ItemRepView)
+ expect(subject.item.class).to eql(Nanoc::ItemWithoutRepsView)
+ expect(subject.config.class).to eql(Nanoc::ConfigView)
+ expect(subject.layouts.class).to eql(Nanoc::LayoutCollectionView)
+ expect(subject.items.class).to eql(Nanoc::ItemCollectionWithoutRepsView)
+ end
+
+ it 'contains the right objects' do
+ expect(rule_context.rep.unwrap).to eql(rep)
+ expect(rule_context.item.unwrap).to eql(item)
+ expect(rule_context.config.unwrap).to eql(config)
+ expect(rule_context.layouts.unwrap).to eql(layouts)
+ expect(rule_context.items.unwrap).to eql(items)
+ end
+ end
+
+ describe '#item' do
+ subject { rule_context.item }
+
+ it 'is a view without reps access' do
+ expect(subject.class).to eql(Nanoc::ItemWithoutRepsView)
+ end
+
+ it 'contains the right item' do
+ expect(subject.unwrap).to eql(item)
+ end
+
+ context 'with legacy identifier and children/parent' do
+ let(:item_identifier) { Nanoc::Identifier.new('/foo/', type: :legacy) }
+
+ let(:parent_identifier) { Nanoc::Identifier.new('/', type: :legacy) }
+ let(:parent) { Nanoc::Int::Item.new('parent', {}, parent_identifier) }
+
+ let(:child_identifier) { Nanoc::Identifier.new('/foo/bar/', type: :legacy) }
+ let(:child) { Nanoc::Int::Item.new('child', {}, child_identifier) }
+
+ before do
+ items << item
+ items << parent
+ items << child
+ end
+
+ it 'has a parent' do
+ expect(subject.parent.unwrap).to eql(parent)
+ end
+
+ it 'wraps the parent in a view without reps access' do
+ expect(subject.parent.class).to eql(Nanoc::ItemWithoutRepsView)
+ expect(subject.parent).not_to respond_to(:compiled_content)
+ expect(subject.parent).not_to respond_to(:path)
+ expect(subject.parent).not_to respond_to(:reps)
+ end
+
+ it 'has children' do
+ expect(subject.children.map(&:unwrap)).to eql([child])
+ end
+
+ it 'wraps the children in a view without reps access' do
+ expect(subject.children.map(&:class)).to eql([Nanoc::ItemWithoutRepsView])
+ expect(subject.children[0]).not_to respond_to(:compiled_content)
+ expect(subject.children[0]).not_to respond_to(:path)
+ expect(subject.children[0]).not_to respond_to(:reps)
+ end
+ end
+ end
+
+ describe '#items' do
+ subject { rule_context.items }
+
+ let(:item_identifier) { Nanoc::Identifier.new('/foo/', type: :legacy) }
+
+ let(:parent_identifier) { Nanoc::Identifier.new('/', type: :legacy) }
+ let(:parent) { Nanoc::Int::Item.new('parent', {}, parent_identifier) }
+
+ let(:child_identifier) { Nanoc::Identifier.new('/foo/bar/', type: :legacy) }
+ let(:child) { Nanoc::Int::Item.new('child', {}, child_identifier) }
+
+ before do
+ items << item
+ items << parent
+ items << child
+ end
+
+ it 'is a view without reps access' do
+ expect(subject.class).to eql(Nanoc::ItemCollectionWithoutRepsView)
+ end
+
+ it 'contains all items' do
+ expect(subject.unwrap).to match_array([item, parent, child])
+ end
+
+ it 'provides no rep access' do
+ expect(subject['/']).not_to be_nil
+ expect(subject['/']).not_to respond_to(:compiled_content)
+ expect(subject['/']).not_to respond_to(:path)
+ expect(subject['/']).not_to respond_to(:reps)
+
+ expect(subject['/foo/']).not_to be_nil
+ expect(subject['/foo/']).not_to respond_to(:compiled_content)
+ expect(subject['/foo/']).not_to respond_to(:path)
+ expect(subject['/foo/']).not_to respond_to(:reps)
+
+ expect(subject['/foo/bar/']).not_to be_nil
+ expect(subject['/foo/bar/']).not_to respond_to(:compiled_content)
+ expect(subject['/foo/bar/']).not_to respond_to(:path)
+ expect(subject['/foo/bar/']).not_to respond_to(:reps)
+ end
+ end
+
+ describe '#filter' do
+ subject { rule_context.filter(filter_name, filter_args) }
+
+ let(:filter_name) { :donkey }
+ let(:filter_args) { { color: 'grey' } }
+
+ it 'makes a request to the executor' do
+ expect(executor).to receive(:filter).with(filter_name, filter_args)
+ subject
+ end
+ end
+
+ describe '#layout' do
+ subject { rule_context.layout(layout_identifier, extra_filter_args) }
+
+ let(:layout_identifier) { '/default.*' }
+ let(:extra_filter_args) { { color: 'grey' } }
+
+ it 'makes a request to the executor' do
+ expect(executor).to receive(:layout).with(layout_identifier, extra_filter_args)
+ subject
+ end
+ end
+
+ describe '#snapshot' do
+ subject { rule_context.snapshot(snapshot_name, path: path) }
+
+ let(:snapshot_name) { :for_snippet }
+ let(:path) { '/foo.html' }
+
+ it 'makes a request to the executor' do
+ expect(executor).to receive(:snapshot).with(:for_snippet, path: '/foo.html')
+ subject
+ end
+ end
+
+ describe '#write' do
+ subject { rule_context.write(path) }
+
+ let(:path) { '/foo.html' }
+
+ it 'makes a request to the executor' do
+ expect(executor).to receive(:snapshot).with(:last, path: '/foo.html')
+ subject
+ end
+ end
+end
diff --git a/spec/nanoc/rule_dsl/rule_memory_calculator_spec.rb b/spec/nanoc/rule_dsl/rule_memory_calculator_spec.rb
new file mode 100644
index 0000000..b506ecf
--- /dev/null
+++ b/spec/nanoc/rule_dsl/rule_memory_calculator_spec.rb
@@ -0,0 +1,233 @@
+describe(Nanoc::RuleDSL::RuleMemoryCalculator) do
+ subject(:rule_memory_calculator) do
+ described_class.new(site: site, rules_collection: rules_collection)
+ end
+
+ let(:rules_collection) { Nanoc::RuleDSL::RulesCollection.new }
+ let(:site) { double(:site) }
+
+ describe '#[]' do
+ subject { rule_memory_calculator[obj] }
+
+ context 'with item rep' do
+ let(:obj) { Nanoc::Int::ItemRep.new(item, :csv) }
+
+ let(:item) { Nanoc::Int::Item.new('content', {}, Nanoc::Identifier.from('/list.md')) }
+ let(:config) { Nanoc::Int::Configuration.new.with_defaults }
+ let(:items) { Nanoc::Int::IdentifiableCollection.new(config) }
+ let(:layouts) { Nanoc::Int::IdentifiableCollection.new(config) }
+ let(:site) { double(:site, items: items, layouts: layouts, config: config, compiler: compiler) }
+ let(:compiler) { double(:compiler, compilation_context: compilation_context) }
+ let(:compilation_context) { double(:compilation_context) }
+ let(:view_context) { double(:view_context) }
+
+ before do
+ expect(compilation_context).to receive(:create_view_context).and_return(view_context)
+ end
+
+ context 'no rules exist' do
+ it 'raises error' do
+ error = Nanoc::RuleDSL::RuleMemoryCalculator::NoRuleMemoryForItemRepException
+ expect { subject }.to raise_error(error)
+ end
+ end
+
+ context 'rules exist' do
+ before do
+ rules_proc = proc do
+ filter :erb, speed: :over_9000
+ layout '/default.*'
+ filter :typohero
+ end
+ rule = Nanoc::RuleDSL::Rule.new(Nanoc::Int::Pattern.from('/list.*'), :csv, rules_proc)
+ rules_collection.add_item_compilation_rule(rule)
+ end
+
+ example do
+ subject
+
+ expect(subject[0]).to be_a(Nanoc::Int::ProcessingActions::Snapshot)
+ expect(subject[0].snapshot_name).to eql(:raw)
+ expect(subject[0].path).to be_nil
+
+ expect(subject[1]).to be_a(Nanoc::Int::ProcessingActions::Filter)
+ expect(subject[1].filter_name).to eql(:erb)
+ expect(subject[1].params).to eql(speed: :over_9000)
+
+ expect(subject[2]).to be_a(Nanoc::Int::ProcessingActions::Snapshot)
+ expect(subject[2].snapshot_name).to eql(:pre)
+ expect(subject[2].path).to be_nil
+
+ expect(subject[3]).to be_a(Nanoc::Int::ProcessingActions::Layout)
+ expect(subject[3].layout_identifier).to eql('/default.*')
+ expect(subject[3].params).to be_nil
+
+ expect(subject[4]).to be_a(Nanoc::Int::ProcessingActions::Filter)
+ expect(subject[4].filter_name).to eql(:typohero)
+ expect(subject[4].params).to eql({})
+
+ expect(subject[5]).to be_a(Nanoc::Int::ProcessingActions::Snapshot)
+ expect(subject[5].snapshot_name).to eql(:post)
+ expect(subject[5].path).to be_nil
+
+ expect(subject[6]).to be_a(Nanoc::Int::ProcessingActions::Snapshot)
+ expect(subject[6].snapshot_name).to eql(:last)
+ expect(subject[6].path).to be_nil
+
+ expect(subject.size).to eql(7)
+ end
+ end
+
+ context 'no routing rule exists' do
+ before do
+ # Add compilation rule
+ compilation_rule = Nanoc::RuleDSL::Rule.new(Nanoc::Int::Pattern.from('/list.*'), :csv, proc {})
+ rules_collection.add_item_compilation_rule(compilation_rule)
+ end
+
+ example do
+ subject
+
+ expect(subject[0]).to be_a(Nanoc::Int::ProcessingActions::Snapshot)
+ expect(subject[0].snapshot_name).to eql(:raw)
+ expect(subject[0].path).to be_nil
+
+ expect(subject[1]).to be_a(Nanoc::Int::ProcessingActions::Snapshot)
+ expect(subject[1].snapshot_name).to eql(:last)
+ expect(subject[1].path).to be_nil
+
+ expect(subject.size).to eql(2)
+ end
+ end
+
+ context 'routing rule exists' do
+ before do
+ # Add compilation rule
+ compilation_rule = Nanoc::RuleDSL::Rule.new(Nanoc::Int::Pattern.from('/list.*'), :csv, proc {})
+ rules_collection.add_item_compilation_rule(compilation_rule)
+
+ # Add routing rule
+ routing_rule = Nanoc::RuleDSL::Rule.new(Nanoc::Int::Pattern.from('/list.*'), :csv, proc { '/foo.md' }, snapshot_name: :last)
+ rules_collection.add_item_routing_rule(routing_rule)
+ end
+
+ example do
+ subject
+
+ expect(subject[0]).to be_a(Nanoc::Int::ProcessingActions::Snapshot)
+ expect(subject[0].snapshot_name).to eql(:raw)
+ expect(subject[0].path).to be_nil
+
+ expect(subject[1]).to be_a(Nanoc::Int::ProcessingActions::Snapshot)
+ expect(subject[1].snapshot_name).to eql(:last)
+ expect(subject[1].path).to eq('/foo.md')
+
+ expect(subject.size).to eql(2)
+ end
+ end
+
+ context 'routing rule for other rep exists' do
+ before do
+ # Add compilation rule
+ compilation_rule = Nanoc::RuleDSL::Rule.new(Nanoc::Int::Pattern.from('/list.*'), :csv, proc {})
+ rules_collection.add_item_compilation_rule(compilation_rule)
+
+ # Add routing rule
+ routing_rule = Nanoc::RuleDSL::Rule.new(Nanoc::Int::Pattern.from('/list.*'), :abc, proc { '/foo.md' }, snapshot_name: :last)
+ rules_collection.add_item_routing_rule(routing_rule)
+ end
+
+ example do
+ subject
+
+ expect(subject[0]).to be_a(Nanoc::Int::ProcessingActions::Snapshot)
+ expect(subject[0].snapshot_name).to eql(:raw)
+ expect(subject[0].path).to be_nil
+
+ expect(subject[1]).to be_a(Nanoc::Int::ProcessingActions::Snapshot)
+ expect(subject[1].snapshot_name).to eql(:last)
+ expect(subject[1].path).to be_nil
+
+ expect(subject.size).to eql(2)
+ end
+ end
+ end
+
+ context 'with layout' do
+ let(:obj) { Nanoc::Int::Layout.new('content', {}, '/default.erb') }
+
+ context 'no rules exist' do
+ it 'raises error' do
+ error = Nanoc::RuleDSL::RuleMemoryCalculator::NoRuleMemoryForLayoutException
+ expect { subject }.to raise_error(error)
+ end
+ end
+
+ context 'rule exists' do
+ before do
+ pat = Nanoc::Int::Pattern.from('/*.erb')
+ rules_collection.layout_filter_mapping[pat] = [:erb, { x: 123 }]
+ end
+
+ it 'contains memory for the rule' do
+ expect(subject.size).to eql(1)
+ expect(subject[0]).to be_a(Nanoc::Int::ProcessingActions::Filter)
+ expect(subject[0].filter_name).to eql(:erb)
+ expect(subject[0].params).to eql(x: 123)
+ end
+ end
+ end
+
+ context 'with something else' do
+ let(:obj) { :donkey }
+
+ it 'errors' do
+ error = Nanoc::RuleDSL::RuleMemoryCalculator::UnsupportedObjectTypeException
+ expect { subject }.to raise_error(error)
+ end
+ end
+ end
+
+ describe '#snapshots_defs_for' do
+ subject { rule_memory_calculator.snapshots_defs_for(rep) }
+
+ let(:rep) { Nanoc::Int::ItemRep.new(item, :csv) }
+
+ let(:item) { Nanoc::Int::Item.new('content', {}, Nanoc::Identifier.from('/list.md')) }
+ let(:config) { Nanoc::Int::Configuration.new.with_defaults }
+ let(:items) { Nanoc::Int::IdentifiableCollection.new(config) }
+ let(:layouts) { Nanoc::Int::IdentifiableCollection.new(config) }
+ let(:site) { double(:site, items: items, layouts: layouts, config: config, compiler: compiler) }
+ let(:compiler) { double(:compiler, compilation_context: compilation_context) }
+ let(:compilation_context) { double(:compilation_context) }
+ let(:view_context) { double(:view_context) }
+
+ before do
+ rules_proc = proc do
+ filter :erb, speed: :over_9000
+ layout '/default.*'
+ filter :typohero
+ end
+ rule = Nanoc::RuleDSL::Rule.new(Nanoc::Int::Pattern.from('/list.*'), :csv, rules_proc)
+ rules_collection.add_item_compilation_rule(rule)
+
+ expect(compilation_context).to receive(:create_view_context).and_return(view_context)
+ end
+
+ example do
+ expect(subject[0]).to be_a(Nanoc::Int::SnapshotDef)
+ expect(subject[0].name).to eql(:raw)
+
+ expect(subject[1]).to be_a(Nanoc::Int::SnapshotDef)
+ expect(subject[1].name).to eql(:pre)
+
+ expect(subject[2]).to be_a(Nanoc::Int::SnapshotDef)
+ expect(subject[2].name).to eql(:post)
+
+ expect(subject[3]).to be_a(Nanoc::Int::SnapshotDef)
+ expect(subject[3].name).to eql(:last)
+
+ expect(subject.size).to eql(4)
+ end
+ end
+end
diff --git a/spec/nanoc/rule_dsl/rules_collection_spec.rb b/spec/nanoc/rule_dsl/rules_collection_spec.rb
new file mode 100644
index 0000000..85c721c
--- /dev/null
+++ b/spec/nanoc/rule_dsl/rules_collection_spec.rb
@@ -0,0 +1,299 @@
+describe Nanoc::RuleDSL::RulesCollection do
+ let(:rules_collection) { described_class.new }
+
+ describe '#data' do
+ subject { rules_collection.data }
+
+ it 'is nil by default' do
+ expect(subject).to be_nil
+ end
+
+ it 'can be set' do
+ rules_collection.data = 'asdf'
+ expect(subject).to eq('asdf')
+ end
+ end
+
+ describe '#compilation_rule_for' do
+ let(:item) { Nanoc::Int::Item.new('content', {}, '/foo.md') }
+
+ let(:rep) { Nanoc::Int::ItemRep.new(item, rep_name) }
+
+ let(:rep_name) { :default }
+
+ subject { rules_collection.compilation_rule_for(rep) }
+
+ context 'no rules' do
+ it 'is nil' do
+ expect(subject).to be_nil
+ end
+ end
+
+ context 'some rules, none matching' do
+ before do
+ rules_collection.add_item_compilation_rule(rule)
+ end
+
+ let(:rule) do
+ Nanoc::RuleDSL::Rule.new(Nanoc::Int::Pattern.from('/bar.*'), :default, proc {})
+ end
+
+ it 'is nil' do
+ expect(subject).to be_nil
+ end
+ end
+
+ context 'some rules, one matching' do
+ before do
+ rules_collection.add_item_compilation_rule(rule_a)
+ rules_collection.add_item_compilation_rule(rule_b)
+ end
+
+ let(:rule_a) do
+ Nanoc::RuleDSL::Rule.new(Nanoc::Int::Pattern.from('/foo.*'), :default, proc {})
+ end
+
+ let(:rule_b) do
+ Nanoc::RuleDSL::Rule.new(Nanoc::Int::Pattern.from('/bar.*'), :default, proc {})
+ end
+
+ context 'rep name does not match' do
+ let(:rep_name) { :platypus }
+
+ it 'is nil' do
+ expect(subject).to be_nil
+ end
+ end
+
+ context 'rep name matches' do
+ it 'is the rule' do
+ expect(subject).to equal(rule_a)
+ end
+ end
+ end
+
+ context 'some rules, multiple matching' do
+ before do
+ rules_collection.add_item_compilation_rule(rule_a)
+ rules_collection.add_item_compilation_rule(rule_b)
+ rules_collection.add_item_compilation_rule(rule_c)
+ end
+
+ let(:rule_a) do
+ Nanoc::RuleDSL::Rule.new(Nanoc::Int::Pattern.from('/foo.*'), :default, proc {})
+ end
+
+ let(:rule_b) do
+ Nanoc::RuleDSL::Rule.new(Nanoc::Int::Pattern.from('/*.*'), :default, proc {})
+ end
+
+ let(:rule_c) do
+ Nanoc::RuleDSL::Rule.new(Nanoc::Int::Pattern.from('/*.*'), :foo, proc {})
+ end
+
+ context 'no rep name matches' do
+ let(:rep_name) { :platypus }
+
+ it 'is the first matching rule' do
+ expect(subject).to be_nil
+ end
+ end
+
+ context 'one rep name matches' do
+ let(:rep_name) { :foo }
+
+ it 'is the first matching rule' do
+ expect(subject).to equal(rule_c)
+ end
+ end
+
+ context 'multiple rep names match' do
+ it 'is the first matching rule' do
+ expect(subject).to equal(rule_a)
+ end
+ end
+ end
+ end
+
+ describe '#item_compilation_rules_for' do
+ let(:item) { Nanoc::Int::Item.new('content', {}, '/foo.md') }
+
+ subject { rules_collection.item_compilation_rules_for(item) }
+
+ context 'no rules' do
+ it 'is none' do
+ expect(subject).to be_empty
+ end
+ end
+
+ context 'some rules, none matching' do
+ before do
+ rules_collection.add_item_compilation_rule(rule)
+ end
+
+ let(:rule) do
+ Nanoc::RuleDSL::Rule.new(Nanoc::Int::Pattern.from('/bar.*'), :default, proc {})
+ end
+
+ it 'is none' do
+ expect(subject).to be_empty
+ end
+ end
+
+ context 'some rules, one matching' do
+ before do
+ rules_collection.add_item_compilation_rule(rule_a)
+ rules_collection.add_item_compilation_rule(rule_b)
+ end
+
+ let(:rule_a) do
+ Nanoc::RuleDSL::Rule.new(Nanoc::Int::Pattern.from('/foo.*'), :default, proc {})
+ end
+
+ let(:rule_b) do
+ Nanoc::RuleDSL::Rule.new(Nanoc::Int::Pattern.from('/bar.*'), :default, proc {})
+ end
+
+ it 'is the single rule' do
+ expect(subject).to contain_exactly(rule_a)
+ end
+ end
+
+ context 'some rules, multiple matching' do
+ before do
+ rules_collection.add_item_compilation_rule(rule_a)
+ rules_collection.add_item_compilation_rule(rule_b)
+ end
+
+ let(:rule_a) do
+ Nanoc::RuleDSL::Rule.new(Nanoc::Int::Pattern.from('/foo.*'), :default, proc {})
+ end
+
+ let(:rule_b) do
+ Nanoc::RuleDSL::Rule.new(Nanoc::Int::Pattern.from('/*.*'), :default, proc {})
+ end
+
+ it 'is all matching rule' do
+ expect(subject).to contain_exactly(rule_a, rule_b)
+ end
+ end
+ end
+
+ describe '#routing_rules_for' do
+ let(:item) { Nanoc::Int::Item.new('content', {}, '/foo.md') }
+
+ let(:rep) { Nanoc::Int::ItemRep.new(item, :default) }
+
+ subject { rules_collection.routing_rules_for(rep) }
+
+ let(:rules) do
+ [
+ # Matching item, matching rep
+ Nanoc::RuleDSL::Rule.new(
+ Nanoc::Int::Pattern.from('/foo.*'), :default, proc {}, snapshot_name: :a
+ ),
+ Nanoc::RuleDSL::Rule.new(
+ Nanoc::Int::Pattern.from('/foo.*'), :default, proc {}, snapshot_name: :b
+ ),
+
+ # Matching item, non-matching rep
+ Nanoc::RuleDSL::Rule.new(
+ Nanoc::Int::Pattern.from('/foo.*'), :raw, proc {}, snapshot_name: :a
+ ),
+ Nanoc::RuleDSL::Rule.new(
+ Nanoc::Int::Pattern.from('/foo.*'), :raw, proc {}, snapshot_name: :b
+ ),
+
+ # Non-matching item, matching rep
+ Nanoc::RuleDSL::Rule.new(
+ Nanoc::Int::Pattern.from('/bar.*'), :default, proc {}, snapshot_name: :a
+ ),
+ Nanoc::RuleDSL::Rule.new(
+ Nanoc::Int::Pattern.from('/bar.*'), :default, proc {}, snapshot_name: :b
+ ),
+
+ # Non-matching item, non-matching rep
+ Nanoc::RuleDSL::Rule.new(
+ Nanoc::Int::Pattern.from('/bar.*'), :raw, proc {}, snapshot_name: :a
+ ),
+ Nanoc::RuleDSL::Rule.new(
+ Nanoc::Int::Pattern.from('/bar.*'), :raw, proc {}, snapshot_name: :b
+ ),
+
+ # Matching item, matching rep, but not the first
+ Nanoc::RuleDSL::Rule.new(
+ Nanoc::Int::Pattern.from('/*.*'), :default, proc {}, snapshot_name: :a
+ ),
+ Nanoc::RuleDSL::Rule.new(
+ Nanoc::Int::Pattern.from('/*.*'), :default, proc {}, snapshot_name: :b
+ ),
+ ]
+ end
+
+ before do
+ rules.each do |rule|
+ rules_collection.add_item_routing_rule(rule)
+ end
+ end
+
+ it 'returns the first matching rule for every snapshot' do
+ expect(subject).to eq(
+ a: rules[0],
+ b: rules[1],
+ )
+ end
+ end
+
+ describe '#filter_for_layout' do
+ let(:layout) { Nanoc::Int::Layout.new('Some content', {}, '/foo.md') }
+
+ subject { rules_collection.filter_for_layout(layout) }
+
+ let(:mapping) { {} }
+
+ before do
+ mapping.each_pair do |key, value|
+ rules_collection.layout_filter_mapping[Nanoc::Int::Pattern.from(key)] = value
+ end
+ end
+
+ context 'no rules' do
+ it { is_expected.to be_nil }
+ end
+
+ context 'one non-matching rule' do
+ let(:mapping) do
+ {
+ '/default.*' => [:erb, {}],
+ }
+ end
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'one matching rule' do
+ let(:mapping) do
+ {
+ '/foo.*' => [:erb, {}],
+ }
+ end
+
+ it 'is the single one' do
+ expect(subject).to eq([:erb, {}])
+ end
+ end
+
+ context 'multiple matching rules' do
+ let(:mapping) do
+ {
+ '/foo.*' => [:erb, {}],
+ '/*' => [:haml, {}],
+ }
+ end
+
+ it 'is the first one' do
+ expect(subject).to eq([:erb, {}])
+ end
+ end
+ end
+end
diff --git a/spec/regression_filenames_spec.rb b/spec/regression_filenames_spec.rb
new file mode 100644
index 0000000..a82e3ae
--- /dev/null
+++ b/spec/regression_filenames_spec.rb
@@ -0,0 +1,16 @@
+describe 'regression tests', chdir: false do
+ let(:regression_test_filenames) do
+ Dir['spec/nanoc/regressions/*']
+ end
+
+ let(:regression_test_numbers) do
+ regression_test_filenames
+ .map { |fn| File.readlines(fn).first.match(/GH-(\d+)/)[1] }
+ end
+
+ it 'should have the proper filenames' do
+ regression_test_filenames.zip(regression_test_numbers) do |fn, num|
+ expect(fn).to match(/gh_#{num}[a-z]*_spec/), "#{fn} has the wrong name in its #define block"
+ end
+ end
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
new file mode 100644
index 0000000..85bd899
--- /dev/null
+++ b/spec/spec_helper.rb
@@ -0,0 +1,173 @@
+require 'simplecov'
+SimpleCov.start
+
+require 'nanoc'
+require 'nanoc/cli'
+require 'nanoc/spec'
+
+require 'timecop'
+require 'rspec/its'
+
+Nanoc::CLI.setup
+
+RSpec.configure do |c|
+ c.around(:each) do |example|
+ Nanoc::CLI::ErrorHandler.disable
+ example.run
+ Nanoc::CLI::ErrorHandler.enable
+ end
+
+ c.around(:each) do |example|
+ Dir.mktmpdir('nanoc-test') do |dir|
+ FileUtils.cd(dir) do
+ example.run
+ end
+ end
+ end
+
+ c.around(:each, chdir: false) do |example|
+ FileUtils.cd(File.dirname(__FILE__) + '/..') do
+ example.run
+ end
+ end
+
+ c.before(:each) do
+ Nanoc::Int::NotificationCenter.reset
+ end
+
+ c.before(:each, v8: true) do
+ if ENV.key?('DISABLE_V8')
+ skip 'V8 specs are disabled (broken on Ruby 2.4)'
+ end
+ end
+
+ c.around(:each, stdio: true) do |example|
+ orig_stdout = $stdout
+ orig_stderr = $stderr
+
+ unless ENV['QUIET'] == 'false'
+ $stdout = StringIO.new
+ $stderr = StringIO.new
+ end
+
+ example.run
+
+ $stdout = orig_stdout
+ $stderr = orig_stderr
+ end
+
+ c.before(:each, site: true) do
+ FileUtils.mkdir_p('content')
+ FileUtils.mkdir_p('layouts')
+ FileUtils.mkdir_p('lib')
+ FileUtils.mkdir_p('output')
+
+ File.write('nanoc.yaml', '{}')
+
+ File.write('Rules', 'passthrough "/**/*"')
+ end
+
+ c.include(Nanoc::Spec::HelperHelper, helper: true)
+
+ # Set focus if any
+ if ENV.fetch('FOCUS', false)
+ $stdout.puts "Focusing spec on '#{ENV['FOCUS']}'"
+ c.filter_run_including ENV['FOCUS'].to_sym => true
+ end
+end
+
+RSpec::Matchers.define :raise_frozen_error do |_expected|
+ match do |actual|
+ begin
+ actual.call
+ false
+ rescue => e
+ if e.is_a?(RuntimeError) || e.is_a?(TypeError)
+ e.message =~ /(^can't modify frozen |^unable to modify frozen object$)/
+ else
+ false
+ end
+ end
+ end
+
+ supports_block_expectations
+
+ failure_message do |_actual|
+ 'expected that proc would raise a frozen error'
+ end
+
+ failure_message_when_negated do |_actual|
+ 'expected that proc would not raise a frozen error'
+ end
+end
+
+RSpec::Matchers.define :be_humanly_sorted do
+ match do |actual|
+ actual == sort(actual)
+ end
+
+ description do
+ 'be humanly sorted'
+ end
+
+ failure_message do |actual|
+ expected_order = []
+ actual.zip(sort(actual)).each do |a, b|
+ if a != b
+ expected_order << b
+ end
+ end
+
+ "expected collection to be sorted (incorrect order: #{expected_order.join(' < ')})"
+ end
+
+ def sort(x)
+ x.sort_by { |n| n.dup.unicode_normalize(:nfd).encode('ASCII', fallback: ->(_) { '' }).downcase }
+ end
+end
+
+RSpec::Matchers.define :finish_in_under do |expected|
+ supports_block_expectations
+
+ match do |actual|
+ before = Time.now
+ actual.call
+ after = Time.now
+ @actual_duration = after - before
+ @actual_duration < expected
+ end
+
+ chain :seconds do
+ end
+
+ failure_message do |_actual|
+ "expected that proc would finish in under #{expected}s, but took #{format '%0.1fs', @actual_duration}"
+ end
+
+ failure_message_when_negated do |_actual|
+ "expected that proc would not finish in under #{expected}s, but took #{format '%0.1fs', @actual_duration}"
+ end
+end
+
+RSpec::Matchers.define :yield_from_fiber do |expected|
+ supports_block_expectations
+
+ include RSpec::Matchers::Composable
+
+ match do |actual|
+ @res = Fiber.new { actual.call }.resume
+ values_match?(expected, @res)
+ end
+
+ description do
+ "yield #{expected.inspect} from fiber"
+ end
+
+ failure_message do |_actual|
+ "expected that proc would yield #{expected.inspect} from fiber, but was #{@res.inspect}"
+ end
+
+ failure_message_when_negated do |_actual|
+ "expected that proc would not yield #{expected.inspect} from fiber, but was #{@res.inspect}"
+ end
+end
diff --git a/tasks/doc.rake b/tasks/doc.rake
deleted file mode 100644
index bf69f74..0000000
--- a/tasks/doc.rake
+++ /dev/null
@@ -1,16 +0,0 @@
-require 'yard'
-
-YARD::Rake::YardocTask.new(:doc) do |yard|
- yard.files = Dir['lib/**/*.rb']
- yard.options = [
- '--markup', 'markdown',
- '--markup-provider', 'kramdown',
- '--charset', 'utf-8',
- '--readme', 'README.md',
- '--files', 'NEWS.md,LICENSE',
- '--output-dir', 'doc/yardoc',
- '--template-path', 'doc/yardoc_templates',
- '--load', 'doc/yardoc_handlers/identifier.rb',
- '--query', '@api.text != "private"'
- ]
-end
diff --git a/tasks/rubocop.rake b/tasks/rubocop.rake
deleted file mode 100644
index 2b576b6..0000000
--- a/tasks/rubocop.rake
+++ /dev/null
@@ -1,6 +0,0 @@
-require 'rubocop/rake_task'
-
-RuboCop::RakeTask.new(:rubocop) do |task|
- task.options = %w(--display-cop-names --format simple)
- task.patterns = ['bin/nanoc', 'lib/**/*.rb', 'spec/**/*.rb', 'test/**/*.rb']
-end
diff --git a/tasks/test.rake b/tasks/test.rake
deleted file mode 100644
index f8180ce..0000000
--- a/tasks/test.rake
+++ /dev/null
@@ -1,25 +0,0 @@
-require 'rspec/core/rake_task'
-require 'rake/testtask'
-require 'coveralls/rake/task'
-
-Coveralls::RakeTask.new
-
-SUBDIRS = %w(* base cli data_sources extra filters helpers).freeze
-
-namespace :test do
- SUBDIRS.each do |dir|
- Rake::TestTask.new(dir == '*' ? 'all' : dir) do |t|
- t.test_files = Dir["test/#{dir}/**/*_spec.rb"] + Dir["test/#{dir}/**/test_*.rb"]
- t.libs = ['./lib', '.']
- t.ruby_opts = ['-r./test/helper']
- end
- end
-end
-
-RSpec::Core::RakeTask.new(:spec) do |t|
- t.rspec_opts = '-r ./spec/spec_helper.rb --format Fuubar --color'
- t.verbose = false
-end
-
-desc 'Run all tests and specs'
-task test: [:spec, :'test:all', :'coveralls:push']
diff --git a/test/base/core_ext/array_spec.rb b/test/base/core_ext/array_spec.rb
index 9dbaade..ea39478 100644
--- a/test/base/core_ext/array_spec.rb
+++ b/test/base/core_ext/array_spec.rb
@@ -1,3 +1,5 @@
+require 'helper'
+
describe 'Array#__nanoc_symbolize_keys_recursively' do
it 'should convert keys to symbols' do
array_old = [:abc, 'xyz', { 'foo' => 'bar', :baz => :qux }]
diff --git a/test/base/core_ext/hash_spec.rb b/test/base/core_ext/hash_spec.rb
index 1dfca0e..6925eb6 100644
--- a/test/base/core_ext/hash_spec.rb
+++ b/test/base/core_ext/hash_spec.rb
@@ -1,3 +1,5 @@
+require 'helper'
+
describe 'Hash#__nanoc_symbolize_keys_recursively' do
it 'should convert keys to symbols' do
hash_old = { 'foo' => 'bar' }
diff --git a/test/base/core_ext/string_spec.rb b/test/base/core_ext/string_spec.rb
index b281fb7..028b620 100644
--- a/test/base/core_ext/string_spec.rb
+++ b/test/base/core_ext/string_spec.rb
@@ -1,3 +1,5 @@
+require 'helper'
+
describe 'String#__nanoc_cleaned_identifier' do
it 'should not convert already clean paths' do
'/foo/bar/'.__nanoc_cleaned_identifier.must_equal '/foo/bar/'
diff --git a/test/base/temp_filename_factory_spec.rb b/test/base/temp_filename_factory_spec.rb
deleted file mode 100644
index 9adc431..0000000
--- a/test/base/temp_filename_factory_spec.rb
+++ /dev/null
@@ -1,66 +0,0 @@
-describe Nanoc::Int::TempFilenameFactory do
- subject do
- Nanoc::Int::TempFilenameFactory.new
- end
-
- let(:prefix) { 'foo' }
-
- describe '#create' do
- it 'should create unique paths' do
- path_a = subject.create(prefix)
- path_b = subject.create(prefix)
- path_a.wont_equal(path_b)
- end
-
- it 'should return absolute paths' do
- path = subject.create(prefix)
- path.must_match(/\A\//)
- end
-
- it 'should create the containing directory' do
- Dir[subject.root_dir + '/**/*'].must_equal([])
- path = subject.create(prefix)
- File.directory?(File.dirname(path)).must_equal(true)
- end
-
- it 'should reuse the same path after cleanup' do
- path_a = subject.create(prefix)
- subject.cleanup(prefix)
- path_b = subject.create(prefix)
- path_a.must_equal(path_b)
- end
- end
-
- describe '#cleanup' do
- it 'should remove generated files' do
- path_a = subject.create(prefix)
- File.file?(path_a).wont_equal(true) # not yet used
-
- File.open(path_a, 'w') { |io| io << 'hi!' }
- File.file?(path_a).must_equal(true)
-
- subject.cleanup(prefix)
- File.file?(path_a).wont_equal(true)
- end
-
- it 'should eventually delete the root directory' do
- subject.create(prefix)
- File.directory?(subject.root_dir).must_equal(true)
-
- subject.cleanup(prefix)
- File.directory?(subject.root_dir).wont_equal(true)
- end
- end
-
- describe 'other instance' do
- let(:other_instance) do
- Nanoc::Int::TempFilenameFactory.new
- end
-
- it 'should create unique paths across instances' do
- path_a = subject.create(prefix)
- path_b = other_instance.create(prefix)
- path_a.wont_equal(path_b)
- end
- end
-end
diff --git a/test/base/test_checksum_store.rb b/test/base/test_checksum_store.rb
deleted file mode 100644
index a3785b0..0000000
--- a/test/base/test_checksum_store.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-class Nanoc::Int::ChecksumStoreTest < Nanoc::TestCase
- def test_get_with_existing_object
- require 'pstore'
-
- # Create store
- FileUtils.mkdir_p('tmp')
- pstore = PStore.new('tmp/checksums')
- pstore.transaction do
- pstore[:data] = { [:item, '/moo/'] => 'zomg' }
- pstore[:version] = 1
- end
-
- # Check
- store = Nanoc::Int::ChecksumStore.new
- store.load
- obj = Nanoc::Int::Item.new('Moo?', {}, '/moo/')
- assert_equal 'zomg', store[obj]
- end
-
- def test_get_with_nonexistant_object
- store = Nanoc::Int::ChecksumStore.new
- store.load
-
- # Check
- obj = Nanoc::Int::Item.new('Moo?', {}, '/animals/cow/')
- assert_equal nil, store[obj]
- end
-end
diff --git a/test/base/test_code_snippet.rb b/test/base/test_code_snippet.rb
index 0edf3ad..49a2f12 100644
--- a/test/base/test_code_snippet.rb
+++ b/test/base/test_code_snippet.rb
@@ -1,3 +1,5 @@
+require 'helper'
+
class Nanoc::Int::CodeSnippetTest < Nanoc::TestCase
def test_load
# Initialize
diff --git a/test/base/test_compiler.rb b/test/base/test_compiler.rb
index f8cfdff..17613bb 100644
--- a/test/base/test_compiler.rb
+++ b/test/base/test_compiler.rb
@@ -1,3 +1,5 @@
+require 'helper'
+
class Nanoc::Int::CompilerTest < Nanoc::TestCase
def new_compiler(site = nil)
site ||= Nanoc::Int::Site.new(
@@ -11,9 +13,11 @@ class Nanoc::Int::CompilerTest < Nanoc::TestCase
action_provider = Nanoc::Int::ActionProvider.named(:rule_dsl).for(site)
+ objects = site.items.to_a + site.layouts.to_a
+
params = {
- compiled_content_cache: Nanoc::Int::CompiledContentCache.new,
- checksum_store: Nanoc::Int::ChecksumStore.new(site: site),
+ compiled_content_cache: Nanoc::Int::CompiledContentCache.new(items: site.items),
+ checksum_store: Nanoc::Int::ChecksumStore.new(site: site, objects: objects),
rule_memory_store: Nanoc::Int::RuleMemoryStore.new,
dependency_store: Nanoc::Int::DependencyStore.new(
site.items.to_a + site.layouts.to_a,
@@ -208,21 +212,6 @@ class Nanoc::Int::CompilerTest < Nanoc::TestCase
end
end
- def test_compile_should_recompile_all_reps
- Nanoc::CLI.run %w(create_site bar)
-
- FileUtils.cd('bar') do
- Nanoc::CLI.run %w(compile)
-
- site = Nanoc::Int::SiteLoader.new.new_from_cwd
- site.compile
-
- # At this point, even the already compiled items in the previous pass
- # should have their compiled content assigned, so this should work:
- site.compiler.reps[site.items['/index.*']][0].compiled_content
- end
- end
-
def test_disallow_multiple_snapshots_with_the_same_name
# Create site
Nanoc::CLI.run %w(create_site bar)
diff --git a/test/base/test_context.rb b/test/base/test_context.rb
index a74b4d5..63b36f0 100644
--- a/test/base/test_context.rb
+++ b/test/base/test_context.rb
@@ -1,7 +1,9 @@
+require 'helper'
+
class Nanoc::Int::ContextTest < Nanoc::TestCase
def test_context_with_instance_variable
# Create context
- context = Nanoc::Int::Context.new({ foo: 'bar', baz: 'quux' })
+ context = Nanoc::Int::Context.new(foo: 'bar', baz: 'quux')
# Ensure correct evaluation
assert_equal('bar', eval('@foo', context.get_binding))
@@ -9,7 +11,7 @@ class Nanoc::Int::ContextTest < Nanoc::TestCase
def test_context_with_instance_method
# Create context
- context = Nanoc::Int::Context.new({ foo: 'bar', baz: 'quux' })
+ context = Nanoc::Int::Context.new(foo: 'bar', baz: 'quux')
# Ensure correct evaluation
assert_equal('bar', eval('foo', context.get_binding))
@@ -17,9 +19,15 @@ class Nanoc::Int::ContextTest < Nanoc::TestCase
def test_example
# Parse
- YARD.parse(LIB_DIR + '/nanoc/base/context.rb')
+ YARD.parse(LIB_DIR + '/nanoc/base/entities/context.rb')
# Run
assert_examples_correct 'Nanoc::Int::Context#initialize'
end
+
+ def test_include
+ context = Nanoc::Int::Context.new({})
+ eval('include Nanoc::Helpers::HTMLEscape', context.get_binding)
+ assert_equal('<>', eval('h("<>")', context.get_binding))
+ end
end
diff --git a/test/base/test_data_source.rb b/test/base/test_data_source.rb
index f527ebd..d2c95f9 100644
--- a/test/base/test_data_source.rb
+++ b/test/base/test_data_source.rb
@@ -1,3 +1,5 @@
+require 'helper'
+
class Nanoc::DataSourceTest < Nanoc::TestCase
def test_loading
# Create data source
@@ -40,6 +42,17 @@ class Nanoc::DataSourceTest < Nanoc::TestCase
assert_equal 'abcdef', item.checksum_data
end
+ def test_new_item_with_checksums
+ data_source = Nanoc::DataSource.new(nil, nil, nil, nil)
+
+ item = data_source.new_item('stuff', { title: 'Stuff!' }, '/asdf/', content_checksum_data: 'con-cs', attributes_checksum_data: 'attr-cs')
+ assert_equal 'stuff', item.content.string
+ assert_equal 'Stuff!', item.attributes[:title]
+ assert_equal Nanoc::Identifier.new('/asdf/'), item.identifier
+ assert_equal 'con-cs', item.content_checksum_data
+ assert_equal 'attr-cs', item.attributes_checksum_data
+ end
+
def test_new_layout
data_source = Nanoc::DataSource.new(nil, nil, nil, nil)
@@ -49,4 +62,15 @@ class Nanoc::DataSourceTest < Nanoc::TestCase
assert_equal Nanoc::Identifier.new('/asdf/'), layout.identifier
assert_equal 'abcdef', layout.checksum_data
end
+
+ def test_new_layout_with_checksums
+ data_source = Nanoc::DataSource.new(nil, nil, nil, nil)
+
+ layout = data_source.new_layout('stuff', { title: 'Stuff!' }, '/asdf/', content_checksum_data: 'con-cs', attributes_checksum_data: 'attr-cs')
+ assert_equal 'stuff', layout.content.string
+ assert_equal 'Stuff!', layout.attributes[:title]
+ assert_equal Nanoc::Identifier.new('/asdf/'), layout.identifier
+ assert_equal 'con-cs', layout.content_checksum_data
+ assert_equal 'attr-cs', layout.attributes_checksum_data
+ end
end
diff --git a/test/base/test_dependency_tracker.rb b/test/base/test_dependency_tracker.rb
index fd520fa..7eca473 100644
--- a/test/base/test_dependency_tracker.rb
+++ b/test/base/test_dependency_tracker.rb
@@ -1,7 +1,12 @@
+require 'helper'
+
class Nanoc::Int::DependencyTrackerTest < Nanoc::TestCase
def test_initialize
# Mock items
- items = [mock, mock]
+ items = [
+ Nanoc::Int::Item.new('a', {}, '/a.md'),
+ Nanoc::Int::Item.new('b', {}, '/b.md'),
+ ]
# Create
store = Nanoc::Int::DependencyStore.new(items)
@@ -13,7 +18,10 @@ class Nanoc::Int::DependencyTrackerTest < Nanoc::TestCase
def test_record_dependency
# Mock items
- items = [mock, mock]
+ items = [
+ Nanoc::Int::Item.new('a', {}, '/a.md'),
+ Nanoc::Int::Item.new('b', {}, '/b.md'),
+ ]
# Create
store = Nanoc::Int::DependencyStore.new(items)
@@ -27,7 +35,10 @@ class Nanoc::Int::DependencyTrackerTest < Nanoc::TestCase
def test_record_dependency_no_self
# Mock items
- items = [mock, mock]
+ items = [
+ Nanoc::Int::Item.new('a', {}, '/a.md'),
+ Nanoc::Int::Item.new('b', {}, '/b.md'),
+ ]
# Create
store = Nanoc::Int::DependencyStore.new(items)
@@ -42,7 +53,10 @@ class Nanoc::Int::DependencyTrackerTest < Nanoc::TestCase
def test_record_dependency_no_doubles
# Mock items
- items = [mock, mock]
+ items = [
+ Nanoc::Int::Item.new('a', {}, '/a.md'),
+ Nanoc::Int::Item.new('b', {}, '/b.md'),
+ ]
# Create
store = Nanoc::Int::DependencyStore.new(items)
@@ -58,7 +72,11 @@ class Nanoc::Int::DependencyTrackerTest < Nanoc::TestCase
def test_objects_causing_outdatedness_of
# Mock items
- items = [mock, mock, mock]
+ items = [
+ Nanoc::Int::Item.new('a', {}, '/a.md'),
+ Nanoc::Int::Item.new('b', {}, '/b.md'),
+ Nanoc::Int::Item.new('c', {}, '/c.md'),
+ ]
# Create
store = Nanoc::Int::DependencyStore.new(items)
@@ -71,48 +89,15 @@ class Nanoc::Int::DependencyTrackerTest < Nanoc::TestCase
assert_contains_exactly [items[1]], store.objects_causing_outdatedness_of(items[0])
end
- def test_objects_outdated_due_to
+ def test_store_graph_and_load_graph_simple
# Mock items
- items = [mock, mock, mock]
-
- # Create
- store = Nanoc::Int::DependencyStore.new(items)
-
- # Record some dependencies
- store.record_dependency(items[0], items[1])
- store.record_dependency(items[1], items[2])
-
- # Verify dependencies
- assert_contains_exactly [items[0]], store.objects_outdated_due_to(items[1])
- end
-
- def test_enter_and_exit
items = [
- Nanoc::Int::Item.new('Foo', {}, '/foo.md'),
- Nanoc::Int::Item.new('Bar', {}, '/bar.md'),
+ Nanoc::Int::Item.new('a', {}, '/a.md'),
+ Nanoc::Int::Item.new('b', {}, '/b.md'),
+ Nanoc::Int::Item.new('c', {}, '/c.md'),
+ Nanoc::Int::Item.new('d', {}, '/d.md'),
]
- store = Nanoc::Int::DependencyStore.new(items)
- tracker = Nanoc::Int::DependencyTracker.new(store)
-
- tracker.enter(items[0])
- tracker.enter(items[1])
- tracker.exit(items[1])
- tracker.exit(items[0])
-
- assert_contains_exactly [items[1]], store.objects_causing_outdatedness_of(items[0])
- assert_empty store.objects_causing_outdatedness_of(items[1])
- end
-
- def test_store_graph_and_load_graph_simple
- # Mock items
- items = [mock('0'), mock('1'), mock('2'), mock('3')]
- items.each { |i| i.stubs(:type).returns(:item) }
- items[0].stubs(:reference).returns([:item, '/aaa/'])
- items[1].stubs(:reference).returns([:item, '/bbb/'])
- items[2].stubs(:reference).returns([:item, '/ccc/'])
- items[3].stubs(:reference).returns([:item, '/ddd/'])
-
# Create
store = Nanoc::Int::DependencyStore.new(items)
@@ -140,12 +125,12 @@ class Nanoc::Int::DependencyTrackerTest < Nanoc::TestCase
def test_store_graph_and_load_graph_with_removed_items
# Mock items
- items = [mock('0'), mock('1'), mock('2'), mock('3')]
- items.each { |i| i.stubs(:type).returns(:item) }
- items[0].stubs(:reference).returns([:item, '/aaa/'])
- items[1].stubs(:reference).returns([:item, '/bbb/'])
- items[2].stubs(:reference).returns([:item, '/ccc/'])
- items[3].stubs(:reference).returns([:item, '/ddd/'])
+ items = [
+ Nanoc::Int::Item.new('a', {}, '/a.md'),
+ Nanoc::Int::Item.new('b', {}, '/b.md'),
+ Nanoc::Int::Item.new('c', {}, '/c.md'),
+ Nanoc::Int::Item.new('d', {}, '/d.md'),
+ ]
# Create new and old lists
old_items = [items[0], items[1], items[2], items[3]]
@@ -177,11 +162,11 @@ class Nanoc::Int::DependencyTrackerTest < Nanoc::TestCase
def test_store_graph_with_nils_in_dst
# Mock items
- items = [mock('0'), mock('1'), mock('2')]
- items.each { |i| i.stubs(:type).returns(:item) }
- items[0].stubs(:reference).returns([:item, '/aaa/'])
- items[1].stubs(:reference).returns([:item, '/bbb/'])
- items[2].stubs(:reference).returns([:item, '/ccc/'])
+ items = [
+ Nanoc::Int::Item.new('a', {}, '/a.md'),
+ Nanoc::Int::Item.new('b', {}, '/b.md'),
+ Nanoc::Int::Item.new('c', {}, '/c.md'),
+ ]
# Create
store = Nanoc::Int::DependencyStore.new(items)
@@ -207,11 +192,11 @@ class Nanoc::Int::DependencyTrackerTest < Nanoc::TestCase
def test_store_graph_with_nils_in_src
# Mock items
- items = [mock('0'), mock('1'), mock('2')]
- items.each { |i| i.stubs(:type).returns(:item) }
- items[0].stubs(:reference).returns([:item, '/aaa/'])
- items[1].stubs(:reference).returns([:item, '/bbb/'])
- items[2].stubs(:reference).returns([:item, '/ccc/'])
+ items = [
+ Nanoc::Int::Item.new('a', {}, '/a.md'),
+ Nanoc::Int::Item.new('b', {}, '/b.md'),
+ Nanoc::Int::Item.new('c', {}, '/c.md'),
+ ]
# Create
store = Nanoc::Int::DependencyStore.new(items)
@@ -237,7 +222,11 @@ class Nanoc::Int::DependencyTrackerTest < Nanoc::TestCase
def test_forget_dependencies_for
# Mock items
- items = [mock, mock, mock]
+ items = [
+ Nanoc::Int::Item.new('a', {}, '/a.md'),
+ Nanoc::Int::Item.new('b', {}, '/b.md'),
+ Nanoc::Int::Item.new('c', {}, '/c.md'),
+ ]
# Create
store = Nanoc::Int::DependencyStore.new(items)
diff --git a/test/base/test_directed_graph.rb b/test/base/test_directed_graph.rb
index 26070a6..5b81753 100644
--- a/test/base/test_directed_graph.rb
+++ b/test/base/test_directed_graph.rb
@@ -1,3 +1,5 @@
+require 'helper'
+
class Nanoc::Int::DirectedGraphTest < Nanoc::TestCase
def test_direct_predecessors
graph = Nanoc::Int::DirectedGraph.new([1, 2, 3])
@@ -44,7 +46,7 @@ class Nanoc::Int::DirectedGraphTest < Nanoc::TestCase
graph.add_edge(1, 2)
graph.add_edge(2, 3)
- assert_equal [[0, 1], [1, 2]], graph.edges.sort
+ assert_equal [[0, 1, nil], [1, 2, nil]], graph.edges.sort
end
def test_edges_with_new_vertices
@@ -55,7 +57,50 @@ class Nanoc::Int::DirectedGraphTest < Nanoc::TestCase
graph.add_edge(3, 2)
assert_equal [1, 2, 3], graph.vertices
- assert_equal [[0, 1], [2, 1]], graph.edges.sort
+ assert_equal [[0, 1, nil], [2, 1, nil]], graph.edges.sort
+ end
+
+ def test_edge_with_props
+ graph = Nanoc::Int::DirectedGraph.new([1, 2, 3])
+ graph.add_edge(1, 2, props: { donkey: 14 })
+ graph.add_edge(2, 3, props: { giraffe: 3 })
+
+ assert_equal [[0, 1, { donkey: 14 }], [1, 2, { giraffe: 3 }]], graph.edges.sort
+ end
+
+ def test_props_for
+ graph = Nanoc::Int::DirectedGraph.new([1, 2, 3, 4])
+ graph.add_edge(1, 2, props: { donkey: 14 })
+ graph.add_edge(2, 3, props: { giraffe: 3 })
+ graph.add_edge(3, 4)
+
+ assert_equal({ donkey: 14 }, graph.props_for(1, 2))
+ assert_equal({ giraffe: 3 }, graph.props_for(2, 3))
+ assert_equal(nil, graph.props_for(3, 4))
+ end
+
+ def test_props_for_with_deleted_edge
+ graph = Nanoc::Int::DirectedGraph.new([1, 2])
+ graph.add_edge(1, 2, props: { donkey: 14 })
+ graph.delete_edge(1, 2)
+
+ assert_equal(nil, graph.props_for(1, 2))
+ end
+
+ def test_props_for_with_deleted_edges_from
+ graph = Nanoc::Int::DirectedGraph.new([1, 2])
+ graph.add_edge(1, 2, props: { donkey: 14 })
+ graph.delete_edges_from(1)
+
+ assert_equal(nil, graph.props_for(1, 2))
+ end
+
+ def test_props_for_with_deleted_edges_to
+ graph = Nanoc::Int::DirectedGraph.new([1, 2])
+ graph.add_edge(1, 2, props: { donkey: 14 })
+ graph.delete_edges_to(2)
+
+ assert_equal(nil, graph.props_for(1, 2))
end
def test_add_edge
@@ -279,7 +324,7 @@ class Nanoc::Int::DirectedGraphTest < Nanoc::TestCase
end
def test_example
- YARD.parse(LIB_DIR + '/nanoc/base/directed_graph.rb')
+ YARD.parse(LIB_DIR + '/nanoc/base/entities/directed_graph.rb')
assert_examples_correct 'Nanoc::Int::DirectedGraph'
end
end
diff --git a/test/base/test_filter.rb b/test/base/test_filter.rb
index abaa89a..f89af43 100644
--- a/test/base/test_filter.rb
+++ b/test/base/test_filter.rb
@@ -1,3 +1,5 @@
+require 'helper'
+
class Nanoc::FilterTest < Nanoc::TestCase
def test_initialize
# Create filter
@@ -9,7 +11,7 @@ class Nanoc::FilterTest < Nanoc::TestCase
def test_assigns
# Create filter
- filter = Nanoc::Filter.new({ foo: 'bar' })
+ filter = Nanoc::Filter.new(foo: 'bar')
# Check assigns
assert_equal('bar', filter.assigns[:foo])
@@ -17,7 +19,7 @@ class Nanoc::FilterTest < Nanoc::TestCase
def test_assigns_with_instance_variables
# Create filter
- filter = Nanoc::Filter.new({ foo: 'bar' })
+ filter = Nanoc::Filter.new(foo: 'bar')
# Check assigns
assert_equal('bar', filter.instance_eval { @foo })
@@ -25,7 +27,7 @@ class Nanoc::FilterTest < Nanoc::TestCase
def test_assigns_with_instance_methods
# Create filter
- filter = Nanoc::Filter.new({ foo: 'bar' })
+ filter = Nanoc::Filter.new(foo: 'bar')
# Check assigns
assert_equal('bar', filter.instance_eval { foo })
@@ -49,7 +51,7 @@ class Nanoc::FilterTest < Nanoc::TestCase
item_rep.expects(:name).returns(:quux)
# Create filter
- filter = Nanoc::Filter.new({ item: item, item_rep: item_rep })
+ filter = Nanoc::Filter.new(item: item, item_rep: item_rep)
# Check filename
assert_equal('item /foo/bar/baz/ (rep quux)', filter.filename)
@@ -61,7 +63,7 @@ class Nanoc::FilterTest < Nanoc::TestCase
layout.expects(:identifier).returns('/wohba/')
# Create filter
- filter = Nanoc::Filter.new({ item: mock, item_rep: mock, layout: layout })
+ filter = Nanoc::Filter.new(item: mock, item_rep: mock, layout: layout)
# Check filename
assert_equal('layout /wohba/', filter.filename)
diff --git a/test/base/test_item.rb b/test/base/test_item.rb
index 2a9d355..ed7e88d 100644
--- a/test/base/test_item.rb
+++ b/test/base/test_item.rb
@@ -1,3 +1,5 @@
+require 'helper'
+
class Nanoc::Int::ItemTest < Nanoc::TestCase
def test_initialize_with_attributes_with_string_keys
item = Nanoc::Int::Item.new('foo', { 'abc' => 'xyz' }, '/foo/')
diff --git a/test/base/test_item_array.rb b/test/base/test_item_array.rb
index 17932eb..b45a058 100644
--- a/test/base/test_item_array.rb
+++ b/test/base/test_item_array.rb
@@ -1,3 +1,5 @@
+require 'helper'
+
class Nanoc::Int::IdentifiableCollectionTest < Nanoc::TestCase
def setup
super
@@ -25,7 +27,7 @@ class Nanoc::Int::IdentifiableCollectionTest < Nanoc::TestCase
end
def test_brackets_with_glob
- @items = Nanoc::Int::IdentifiableCollection.new({ string_pattern_type: 'glob' })
+ @items = Nanoc::Int::IdentifiableCollection.new(string_pattern_type: 'glob')
@items << @one
@items << @two
diff --git a/test/base/test_item_rep.rb b/test/base/test_item_rep.rb
deleted file mode 100644
index 19bd191..0000000
--- a/test/base/test_item_rep.rb
+++ /dev/null
@@ -1,153 +0,0 @@
-class Nanoc::Int::ItemRepTest < Nanoc::TestCase
- def test_compiled_content_with_only_last_available
- # Create rep
- item = Nanoc::Int::Item.new(
- 'blah blah blah', {}, '/'
- )
- rep = Nanoc::Int::ItemRep.new(item, :donkeys)
- rep.snapshot_contents = {
- last: Nanoc::Int::TextualContent.new('last content'),
- }
- rep.expects(:compiled?).returns(true)
-
- # Check
- assert_equal 'last content', rep.compiled_content
- end
-
- def test_compiled_content_with_pre_and_last_available
- # Create rep
- item = Nanoc::Int::Item.new(
- 'blah blah blah', {}, '/'
- )
- rep = Nanoc::Int::ItemRep.new(item, :donkeys)
- rep.snapshot_contents = {
- pre: Nanoc::Int::TextualContent.new('pre content'),
- last: Nanoc::Int::TextualContent.new('last content'),
- }
- rep.expects(:compiled?).returns(true)
-
- # Check
- assert_equal 'pre content', rep.compiled_content
- end
-
- def test_compiled_content_with_custom_snapshot
- # Create rep
- item = Nanoc::Int::Item.new(
- 'blah blah blah', {}, '/'
- )
- rep = Nanoc::Int::ItemRep.new(item, :donkeys)
- rep.snapshot_contents = {
- pre: Nanoc::Int::TextualContent.new('pre content'),
- last: Nanoc::Int::TextualContent.new('last content'),
- }
- rep.expects(:compiled?).returns(true)
-
- # Check
- assert_equal 'last content', rep.compiled_content(snapshot: :last)
- end
-
- def test_compiled_content_with_invalid_snapshot
- # Create rep
- item = Nanoc::Int::Item.new(
- 'blah blah blah', {}, '/'
- )
- rep = Nanoc::Int::ItemRep.new(item, :donkeys)
- rep.snapshot_contents = {
- pre: Nanoc::Int::TextualContent.new('pre content'),
- last: Nanoc::Int::TextualContent.new('last content'),
- }
-
- # Check
- assert_raises Nanoc::Int::Errors::NoSuchSnapshot do
- rep.compiled_content(snapshot: :klsjflkasdfl)
- end
- end
-
- def test_compiled_content_with_uncompiled_content
- # Create rep
- item = Nanoc::Int::Item.new(
- 'blah blah', {}, '/'
- )
- rep = Nanoc::Int::ItemRep.new(item, :donkeys)
- rep.expects(:compiled?).returns(false)
-
- # Check
- assert_raises(Nanoc::Int::Errors::UnmetDependency) do
- rep.compiled_content
- end
- end
-
- def test_compiled_content_with_moving_pre_snapshot
- # Create rep
- item = Nanoc::Int::Item.new(
- 'blah blah', {}, '/'
- )
- rep = Nanoc::Int::ItemRep.new(item, :donkeys)
- rep.expects(:compiled?).returns(false)
- rep.snapshot_contents = {
- pre: Nanoc::Int::TextualContent.new('pre!'),
- last: Nanoc::Int::TextualContent.new('last!'),
- }
-
- # Check
- assert_raises(Nanoc::Int::Errors::UnmetDependency) do
- rep.compiled_content(snapshot: :pre)
- end
- end
-
- def test_compiled_content_with_non_moving_pre_snapshot
- # Create rep
- item = Nanoc::Int::Item.new(
- 'blah blah', {}, '/'
- )
- rep = Nanoc::Int::ItemRep.new(item, :donkeys)
- rep.expects(:compiled?).returns(false)
- rep.snapshot_defs = [
- Nanoc::Int::SnapshotDef.new(:pre, true),
- ]
- rep.snapshot_contents = {
- pre: Nanoc::Int::TextualContent.new('pre!'),
- post: Nanoc::Int::TextualContent.new('post!'),
- last: Nanoc::Int::TextualContent.new('last!'),
- }
-
- # Check
- assert_equal 'pre!', rep.compiled_content(snapshot: :pre)
- end
-
- def test_compiled_content_with_multiple_pre_snapshots
- # Create rep
- item = Nanoc::Int::Item.new(
- 'blah blah', {}, '/'
- )
- rep = Nanoc::Int::ItemRep.new(item, :donkeys)
- rep.expects(:compiled?).returns(false)
- rep.snapshot_defs = [
- Nanoc::Int::SnapshotDef.new(:pre, false),
- Nanoc::Int::SnapshotDef.new(:pre, true),
- ]
- rep.snapshot_contents = {
- pre: Nanoc::Int::TextualContent.new('pre!'),
- post: Nanoc::Int::TextualContent.new('post!'),
- last: Nanoc::Int::TextualContent.new('last!'),
- }
-
- # Check
- assert_equal 'pre!', rep.compiled_content(snapshot: :pre)
- end
-
- def test_access_compiled_content_of_binary_item
- content = Nanoc::Int::BinaryContent.new(File.expand_path('content/somefile.dat'))
- item = Nanoc::Int::Item.new(content, {}, '/somefile/')
- item_rep = Nanoc::Int::ItemRep.new(item, :foo)
- assert_raises(Nanoc::Int::Errors::CannotGetCompiledContentOfBinaryItem) do
- item_rep.compiled_content
- end
- end
-
- private
-
- def create_rep_for(item, name)
- Nanoc::Int::ItemRep.new(item, name)
- end
-end
diff --git a/test/base/test_layout.rb b/test/base/test_layout.rb
index 3c2dd19..42166f2 100644
--- a/test/base/test_layout.rb
+++ b/test/base/test_layout.rb
@@ -1,3 +1,5 @@
+require 'helper'
+
class Nanoc::Int::LayoutTest < Nanoc::TestCase
def test_initialize
# Make sure attributes are cleaned
diff --git a/test/base/test_memoization.rb b/test/base/test_memoization.rb
index 187d451..87526d9 100644
--- a/test/base/test_memoization.rb
+++ b/test/base/test_memoization.rb
@@ -1,3 +1,5 @@
+require 'helper'
+
class Nanoc::Int::MemoizationTest < Nanoc::TestCase
class Sample1
extend Nanoc::Int::Memoization
diff --git a/test/base/test_notification_center.rb b/test/base/test_notification_center.rb
index 6858ad7..7c080ae 100644
--- a/test/base/test_notification_center.rb
+++ b/test/base/test_notification_center.rb
@@ -1,3 +1,5 @@
+require 'helper'
+
class Nanoc::Int::NotificationCenterTest < Nanoc::TestCase
def test_post
# Set up notification
diff --git a/test/base/test_outdatedness_checker.rb b/test/base/test_outdatedness_checker.rb
index ae0055c..6db356c 100644
--- a/test/base/test_outdatedness_checker.rb
+++ b/test/base/test_outdatedness_checker.rb
@@ -1,3 +1,5 @@
+require 'helper'
+
class Nanoc::Int::OutdatednessCheckerTest < Nanoc::TestCase
def test_not_outdated
# Compile once
@@ -42,7 +44,7 @@ class Nanoc::Int::OutdatednessCheckerTest < Nanoc::TestCase
site.compiler.load_stores
outdatedness_checker = site.compiler.send :outdatedness_checker
rep = site.compiler.reps[site.items.find { |i| i.identifier == '/' }][0]
- assert_equal ::Nanoc::Int::OutdatednessReasons::NotEnoughData, outdatedness_checker.outdatedness_reason_for(rep)
+ assert_equal ::Nanoc::Int::OutdatednessReasons::ContentModified, outdatedness_checker.outdatedness_reason_for(rep)
end
end
@@ -72,7 +74,7 @@ class Nanoc::Int::OutdatednessCheckerTest < Nanoc::TestCase
end
end
- def test_outdated_if_item_checksum_is_different
+ def test_outdated_if_item_content_checksum_is_different
# Compile once
with_site(name: 'foo') do |site|
File.open('content/index.html', 'w') { |io| io.write('o hello') }
@@ -83,7 +85,7 @@ class Nanoc::Int::OutdatednessCheckerTest < Nanoc::TestCase
site.compile
end
- # Create new item
+ # Update item
FileUtils.cd('foo') do
File.open('content/new.html', 'w') { |io| io.write('o hello DIFFERENT!!!') }
end
@@ -95,7 +97,34 @@ class Nanoc::Int::OutdatednessCheckerTest < Nanoc::TestCase
site.compiler.load_stores
outdatedness_checker = site.compiler.send :outdatedness_checker
rep = site.compiler.reps[site.items.find { |i| i.identifier == '/new/' }][0]
- assert_equal ::Nanoc::Int::OutdatednessReasons::SourceModified, outdatedness_checker.outdatedness_reason_for(rep)
+ assert_equal ::Nanoc::Int::OutdatednessReasons::ContentModified, outdatedness_checker.outdatedness_reason_for(rep)
+ end
+ end
+
+ def test_outdated_if_item_attributes_checksum_is_different
+ # Compile once
+ with_site(name: 'foo') do |site|
+ File.open('content/index.html', 'w') { |io| io.write('o hello') }
+ File.open('content/new.html', 'w') { |io| io.write('o hello too') }
+ File.open('lib/stuff.rb', 'w') { |io| io.write('$foo = 123') }
+
+ site = Nanoc::Int::SiteLoader.new.new_from_cwd
+ site.compile
+ end
+
+ # Update item
+ FileUtils.cd('foo') do
+ File.open('content/new.html', 'w') { |io| io.write("---\ntitle: donkey\n---\no hello too") }
+ end
+
+ # Check
+ with_site(name: 'foo') do |site|
+ site = Nanoc::Int::SiteLoader.new.new_from_cwd
+ site.compiler.build_reps
+ site.compiler.load_stores
+ outdatedness_checker = site.compiler.send :outdatedness_checker
+ rep = site.compiler.reps[site.items.find { |i| i.identifier == '/new/' }][0]
+ assert_equal ::Nanoc::Int::OutdatednessReasons::AttributesModified, outdatedness_checker.outdatedness_reason_for(rep)
end
end
diff --git a/test/base/test_plugin.rb b/test/base/test_plugin.rb
index 3df2fdf..4165056 100644
--- a/test/base/test_plugin.rb
+++ b/test/base/test_plugin.rb
@@ -1,3 +1,5 @@
+require 'helper'
+
class Nanoc::PluginTest < Nanoc::TestCase
class SampleFilter < Nanoc::Filter
identifier :_plugin_test_sample_filter
diff --git a/test/base/test_site.rb b/test/base/test_site.rb
index 4bb05bf..0fcc3d5 100644
--- a/test/base/test_site.rb
+++ b/test/base/test_site.rb
@@ -1,3 +1,5 @@
+require 'helper'
+
class Nanoc::Int::SiteTest < Nanoc::TestCase
def test_initialize_with_dir_without_config_yaml
assert_raises(Nanoc::Int::ConfigLoader::NoConfigFileFoundError) do
diff --git a/test/base/test_store.rb b/test/base/test_store.rb
index 049a6ca..3505a1d 100644
--- a/test/base/test_store.rb
+++ b/test/base/test_store.rb
@@ -1,3 +1,5 @@
+require 'helper'
+
class Nanoc::Int::StoreTest < Nanoc::TestCase
class TestStore < Nanoc::Int::Store
def data
@@ -32,4 +34,26 @@ class Nanoc::Int::StoreTest < Nanoc::TestCase
store.load
assert_equal(nil, store.data)
end
+
+ def test_tmp_path_with_nil_env
+ tmp_path_for_checksum = Nanoc::Int::Store.tmp_path_for(env_name: nil, store_name: 'checksum')
+ tmp_path_for_rule_memory = Nanoc::Int::Store.tmp_path_for(env_name: nil, store_name: 'rule_memory')
+ tmp_path_for_dependencies = Nanoc::Int::Store.tmp_path_for(env_name: nil, store_name: 'dependencies')
+ tmp_path_for_compiled_content = Nanoc::Int::Store.tmp_path_for(env_name: nil, store_name: 'compiled_content')
+ assert_equal('tmp/checksum', tmp_path_for_checksum)
+ assert_equal('tmp/rule_memory', tmp_path_for_rule_memory)
+ assert_equal('tmp/dependencies', tmp_path_for_dependencies)
+ assert_equal('tmp/compiled_content', tmp_path_for_compiled_content)
+ end
+
+ def test_tmp_path_with_test_env
+ tmp_path_for_checksum = Nanoc::Int::Store.tmp_path_for(env_name: 'test', store_name: 'checksum')
+ tmp_path_for_rule_memory = Nanoc::Int::Store.tmp_path_for(env_name: 'test', store_name: 'rule_memory')
+ tmp_path_for_dependencies = Nanoc::Int::Store.tmp_path_for(env_name: 'test', store_name: 'dependencies')
+ tmp_path_for_compiled_content = Nanoc::Int::Store.tmp_path_for(env_name: 'test', store_name: 'compiled_content')
+ assert_equal('tmp/test/checksum', tmp_path_for_checksum)
+ assert_equal('tmp/test/rule_memory', tmp_path_for_rule_memory)
+ assert_equal('tmp/test/dependencies', tmp_path_for_dependencies)
+ assert_equal('tmp/test/compiled_content', tmp_path_for_compiled_content)
+ end
end
diff --git a/test/extra/checking/checks/test_css.rb b/test/checking/checks/test_css.rb
similarity index 86%
rename from test/extra/checking/checks/test_css.rb
rename to test/checking/checks/test_css.rb
index ac6d6cf..a65176c 100644
--- a/test/extra/checking/checks/test_css.rb
+++ b/test/checking/checks/test_css.rb
@@ -1,4 +1,6 @@
-class Nanoc::Extra::Checking::Checks::CSSTest < Nanoc::TestCase
+require 'helper'
+
+class Nanoc::Checking::Checks::CSSTest < Nanoc::TestCase
def test_run_ok
VCR.use_cassette('css_run_ok') do
with_site do |site|
@@ -8,7 +10,7 @@ class Nanoc::Extra::Checking::Checks::CSSTest < Nanoc::TestCase
File.open('output/style.css', 'w') { |io| io.write('h1 { color: red; }') }
# Run check
- check = Nanoc::Extra::Checking::Checks::CSS.create(site)
+ check = Nanoc::Checking::Checks::CSS.create(site)
check.run
# Check
@@ -26,7 +28,7 @@ class Nanoc::Extra::Checking::Checks::CSSTest < Nanoc::TestCase
File.open('output/style.css', 'w') { |io| io.write('h1 { coxlor: rxed; }') }
# Run check
- check = Nanoc::Extra::Checking::Checks::CSS.create(site)
+ check = Nanoc::Checking::Checks::CSS.create(site)
check.run
# Check
@@ -49,7 +51,7 @@ class Nanoc::Extra::Checking::Checks::CSSTest < Nanoc::TestCase
File.open('output/style.css', 'w') { |io| io.write('h1 { ; {') }
# Run check
- check = Nanoc::Extra::Checking::Checks::CSS.create(site)
+ check = Nanoc::Checking::Checks::CSS.create(site)
check.run
# Check
diff --git a/test/extra/checking/checks/test_external_links.rb b/test/checking/checks/test_external_links.rb
similarity index 78%
rename from test/extra/checking/checks/test_external_links.rb
rename to test/checking/checks/test_external_links.rb
index 994df7d..bda8230 100644
--- a/test/extra/checking/checks/test_external_links.rb
+++ b/test/checking/checks/test_external_links.rb
@@ -1,4 +1,6 @@
-class Nanoc::Extra::Checking::Checks::ExternalLinksTest < Nanoc::TestCase
+require 'helper'
+
+class Nanoc::Checking::Checks::ExternalLinksTest < Nanoc::TestCase
def test_run
with_site do |site|
# Create files
@@ -7,7 +9,7 @@ class Nanoc::Extra::Checking::Checks::ExternalLinksTest < Nanoc::TestCase
File.open('output/bar.html', 'w') { |io| io.write('<a href="http://example.com/">not broken</a>') }
# Create check
- check = Nanoc::Extra::Checking::Checks::ExternalLinks.create(site)
+ check = Nanoc::Checking::Checks::ExternalLinks.create(site)
def check.request_url_once(url)
Net::HTTPResponse.new('1.1', url.path == '/' ? '200' : '404', 'okay')
end
@@ -21,7 +23,7 @@ class Nanoc::Extra::Checking::Checks::ExternalLinksTest < Nanoc::TestCase
def test_valid_by_path
with_site do |site|
# Create check
- check = Nanoc::Extra::Checking::Checks::ExternalLinks.create(site)
+ check = Nanoc::Checking::Checks::ExternalLinks.create(site)
def check.request_url_once(url)
Net::HTTPResponse.new('1.1', url.path == '/200' ? '200' : '404', 'okay')
end
@@ -36,7 +38,7 @@ class Nanoc::Extra::Checking::Checks::ExternalLinksTest < Nanoc::TestCase
def test_valid_by_query
with_site do |site|
# Create check
- check = Nanoc::Extra::Checking::Checks::ExternalLinks.create(site)
+ check = Nanoc::Checking::Checks::ExternalLinks.create(site)
def check.request_url_once(url)
Net::HTTPResponse.new('1.1', url.query == 'status=200' ? '200' : '404', 'okay')
end
@@ -50,7 +52,7 @@ class Nanoc::Extra::Checking::Checks::ExternalLinksTest < Nanoc::TestCase
def test_fallback_to_get_when_head_is_not_allowed
with_site do |site|
# Create check
- check = Nanoc::Extra::Checking::Checks::ExternalLinks.create(site)
+ check = Nanoc::Checking::Checks::ExternalLinks.create(site)
def check.request_url_once(url, req_method = Net::HTTP::Head)
Net::HTTPResponse.new('1.1', req_method == Net::HTTP::Head || url.path == '/405' ? '405' : '200', 'okay')
end
@@ -63,7 +65,7 @@ class Nanoc::Extra::Checking::Checks::ExternalLinksTest < Nanoc::TestCase
def test_path_for_url
with_site do |site|
- check = Nanoc::Extra::Checking::Checks::ExternalLinks.create(site)
+ check = Nanoc::Checking::Checks::ExternalLinks.create(site)
assert_equal '/', check.send(:path_for_url, URI.parse('http://example.com'))
assert_equal '/', check.send(:path_for_url, URI.parse('http://example.com/'))
@@ -76,8 +78,8 @@ class Nanoc::Extra::Checking::Checks::ExternalLinksTest < Nanoc::TestCase
def test_excluded
with_site do |site|
# Create check
- check = Nanoc::Extra::Checking::Checks::ExternalLinks.create(site)
- site.config.update({ checks: { external_links: { exclude: ['^http://excluded.com$'] } } })
+ check = Nanoc::Checking::Checks::ExternalLinks.create(site)
+ site.config.update(checks: { external_links: { exclude: ['^http://excluded.com$'] } })
# Test
assert check.send(:excluded?, 'http://excluded.com')
@@ -89,8 +91,8 @@ class Nanoc::Extra::Checking::Checks::ExternalLinksTest < Nanoc::TestCase
def test_excluded_file
with_site do |site|
# Create check
- check = Nanoc::Extra::Checking::Checks::ExternalLinks.create(site)
- site.config.update({ checks: { external_links: { exclude_files: ['blog/page'] } } })
+ check = Nanoc::Checking::Checks::ExternalLinks.create(site)
+ site.config.update(checks: { external_links: { exclude_files: ['blog/page'] } })
# Test
assert check.send(:excluded_file?, 'output/blog/page1/index.html')
diff --git a/test/extra/checking/checks/test_html.rb b/test/checking/checks/test_html.rb
similarity index 76%
rename from test/extra/checking/checks/test_html.rb
rename to test/checking/checks/test_html.rb
index e42badb..250482e 100644
--- a/test/extra/checking/checks/test_html.rb
+++ b/test/checking/checks/test_html.rb
@@ -1,5 +1,13 @@
-class Nanoc::Extra::Checking::Checks::HTMLTest < Nanoc::TestCase
+require 'helper'
+
+class Nanoc::Checking::Checks::HTMLTest < Nanoc::TestCase
def test_run_ok
+ require 'w3c_validators'
+
+ if ::W3CValidators::VERSION =~ /\A1\.3|1\.3\.1\z/
+ skip 'broken (see https://github.com/w3c-validators/w3c_validators/issues/25)'
+ end
+
VCR.use_cassette('html_run_ok') do
with_site do |site|
# Create files
@@ -8,7 +16,7 @@ class Nanoc::Extra::Checking::Checks::HTMLTest < Nanoc::TestCase
File.open('output/style.css', 'w') { |io| io.write('h1 { coxlor: rxed; }') }
# Run check
- check = Nanoc::Extra::Checking::Checks::HTML.create(site)
+ check = Nanoc::Checking::Checks::HTML.create(site)
check.run
# Check
@@ -26,7 +34,7 @@ class Nanoc::Extra::Checking::Checks::HTMLTest < Nanoc::TestCase
File.open('output/style.css', 'w') { |io| io.write('h1 { coxlor: rxed; }') }
# Run check
- check = Nanoc::Extra::Checking::Checks::HTML.create(site)
+ check = Nanoc::Checking::Checks::HTML.create(site)
check.run
# Check
diff --git a/test/extra/checking/checks/test_internal_links.rb b/test/checking/checks/test_internal_links.rb
similarity index 75%
rename from test/extra/checking/checks/test_internal_links.rb
rename to test/checking/checks/test_internal_links.rb
index 45f2b9a..1106620 100644
--- a/test/extra/checking/checks/test_internal_links.rb
+++ b/test/checking/checks/test_internal_links.rb
@@ -1,4 +1,6 @@
-class Nanoc::Extra::Checking::Checks::InternalLinksTest < Nanoc::TestCase
+require 'helper'
+
+class Nanoc::Checking::Checks::InternalLinksTest < Nanoc::TestCase
def test_run
with_site do |site|
# Create files
@@ -8,7 +10,7 @@ class Nanoc::Extra::Checking::Checks::InternalLinksTest < Nanoc::TestCase
File.open('output/bar.html', 'w') { |io| io.write('<a href="/foo.txt">not broken</a>') }
# Create check
- check = Nanoc::Extra::Checking::Checks::InternalLinks.create(site)
+ check = Nanoc::Checking::Checks::InternalLinks.create(site)
check.run
# Test
@@ -23,7 +25,7 @@ class Nanoc::Extra::Checking::Checks::InternalLinksTest < Nanoc::TestCase
File.open('output/bar.html', 'w') { |io| io.write('<link rel="stylesheet" href="/styledinges.css">') }
# Create check
- check = Nanoc::Extra::Checking::Checks::InternalLinks.create(site)
+ check = Nanoc::Checking::Checks::InternalLinks.create(site)
check.run
# Test
@@ -41,7 +43,7 @@ class Nanoc::Extra::Checking::Checks::InternalLinksTest < Nanoc::TestCase
File.open('output/stuff/blah', 'w') { |io| io.write('hi') }
# Create check
- check = Nanoc::Extra::Checking::Checks::InternalLinks.create(site)
+ check = Nanoc::Checking::Checks::InternalLinks.create(site)
# Test
assert check.send(:valid?, 'foo', 'output/origin')
@@ -58,7 +60,7 @@ class Nanoc::Extra::Checking::Checks::InternalLinksTest < Nanoc::TestCase
FileUtils.mkdir_p('output/stuff')
File.open('output/stuff/right', 'w') { |io| io.write('hi') }
- check = Nanoc::Extra::Checking::Checks::InternalLinks.create(site)
+ check = Nanoc::Checking::Checks::InternalLinks.create(site)
assert check.send(:valid?, 'stuff/right?foo=123', 'output/origin')
refute check.send(:valid?, 'stuff/wrong?foo=123', 'output/origin')
@@ -68,8 +70,8 @@ class Nanoc::Extra::Checking::Checks::InternalLinksTest < Nanoc::TestCase
def test_exclude
with_site do |site|
# Create check
- check = Nanoc::Extra::Checking::Checks::InternalLinks.create(site)
- site.config.update({ checks: { internal_links: { exclude: ['^/excluded\d+'] } } })
+ check = Nanoc::Checking::Checks::InternalLinks.create(site)
+ site.config.update(checks: { internal_links: { exclude: ['^/excluded\d+'] } })
# Test
assert check.send(:valid?, '/excluded1', 'output/origin')
@@ -81,8 +83,8 @@ class Nanoc::Extra::Checking::Checks::InternalLinksTest < Nanoc::TestCase
def test_exclude_targets
with_site do |site|
# Create check
- check = Nanoc::Extra::Checking::Checks::InternalLinks.create(site)
- site.config.update({ checks: { internal_links: { exclude_targets: ['^/excluded\d+'] } } })
+ check = Nanoc::Checking::Checks::InternalLinks.create(site)
+ site.config.update(checks: { internal_links: { exclude_targets: ['^/excluded\d+'] } })
# Test
assert check.send(:valid?, '/excluded1', 'output/origin')
@@ -94,8 +96,8 @@ class Nanoc::Extra::Checking::Checks::InternalLinksTest < Nanoc::TestCase
def test_exclude_origins
with_site do |site|
# Create check
- check = Nanoc::Extra::Checking::Checks::InternalLinks.create(site)
- site.config.update({ checks: { internal_links: { exclude_origins: ['^/excluded'] } } })
+ check = Nanoc::Checking::Checks::InternalLinks.create(site)
+ site.config.update(checks: { internal_links: { exclude_origins: ['^/excluded'] } })
# Test
assert check.send(:valid?, '/foo', 'output/excluded')
@@ -108,7 +110,7 @@ class Nanoc::Extra::Checking::Checks::InternalLinksTest < Nanoc::TestCase
FileUtils.mkdir_p('output/stuff')
File.open('output/stuff/right foo', 'w') { |io| io.write('hi') }
- check = Nanoc::Extra::Checking::Checks::InternalLinks.create(site)
+ check = Nanoc::Checking::Checks::InternalLinks.create(site)
assert check.send(:valid?, 'stuff/right%20foo', 'output/origin')
refute check.send(:valid?, 'stuff/wrong%20foo', 'output/origin')
diff --git a/test/extra/checking/checks/test_mixed_content.rb b/test/checking/checks/test_mixed_content.rb
similarity index 89%
rename from test/extra/checking/checks/test_mixed_content.rb
rename to test/checking/checks/test_mixed_content.rb
index 4574c9c..79ea031 100644
--- a/test/extra/checking/checks/test_mixed_content.rb
+++ b/test/checking/checks/test_mixed_content.rb
@@ -1,4 +1,6 @@
-class Nanoc::Extra::Checking::Checks::MixedContentTest < Nanoc::TestCase
+require 'helper'
+
+class Nanoc::Checking::Checks::MixedContentTest < Nanoc::TestCase
def create_output_file(name, lines)
FileUtils.mkdir_p('output')
File.open('output/' + name, 'w') do |io|
@@ -22,7 +24,7 @@ class Nanoc::Extra::Checking::Checks::MixedContentTest < Nanoc::TestCase
'<audio src="https://nanoc.ws/theme-song.flac"></audio>',
'<video src="https://nanoc.ws/screen-cast.mkv"></video>',
])
- check = Nanoc::Extra::Checking::Checks::MixedContent.create(site)
+ check = Nanoc::Checking::Checks::MixedContent.create(site)
check.run
assert_empty check.issues
@@ -40,7 +42,7 @@ class Nanoc::Extra::Checking::Checks::MixedContentTest < Nanoc::TestCase
'<audio src="/theme-song.flac"></audio>',
'<video src="/screen-cast.mkv"></video>',
])
- check = Nanoc::Extra::Checking::Checks::MixedContent.create(site)
+ check = Nanoc::Checking::Checks::MixedContent.create(site)
check.run
assert_empty check.issues
@@ -58,7 +60,7 @@ class Nanoc::Extra::Checking::Checks::MixedContentTest < Nanoc::TestCase
'<audio src="//nanoc.ws/theme-song.flac"></audio>',
'<video src="//nanoc.ws/screen-cast.mkv"></video>',
])
- check = Nanoc::Extra::Checking::Checks::MixedContent.create(site)
+ check = Nanoc::Checking::Checks::MixedContent.create(site)
check.run
assert_empty check.issues
@@ -76,7 +78,7 @@ class Nanoc::Extra::Checking::Checks::MixedContentTest < Nanoc::TestCase
'<audio src="theme-song.flac"></audio>',
'<video src="screen-cast.mkv"></video>',
])
- check = Nanoc::Extra::Checking::Checks::MixedContent.create(site)
+ check = Nanoc::Checking::Checks::MixedContent.create(site)
check.run
assert_empty check.issues
@@ -94,7 +96,7 @@ class Nanoc::Extra::Checking::Checks::MixedContentTest < Nanoc::TestCase
'<audio src="?query-string"></audio>',
'<video src="?query-string"></video>',
])
- check = Nanoc::Extra::Checking::Checks::MixedContent.create(site)
+ check = Nanoc::Checking::Checks::MixedContent.create(site)
check.run
assert_empty check.issues
@@ -112,7 +114,7 @@ class Nanoc::Extra::Checking::Checks::MixedContentTest < Nanoc::TestCase
'<audio src="#fragment"></audio>',
'<video src="#fragment"></video>',
])
- check = Nanoc::Extra::Checking::Checks::MixedContent.create(site)
+ check = Nanoc::Checking::Checks::MixedContent.create(site)
check.run
assert_empty check.issues
@@ -131,7 +133,7 @@ class Nanoc::Extra::Checking::Checks::MixedContentTest < Nanoc::TestCase
'<audio src="http://nanoc.ws/theme-song.flac"></audio>',
'<video src="http://nanoc.ws/screencast.mkv"></video>',
])
- check = Nanoc::Extra::Checking::Checks::MixedContent.create(site)
+ check = Nanoc::Checking::Checks::MixedContent.create(site)
check.run
issues = check.issues.to_a
@@ -177,7 +179,7 @@ class Nanoc::Extra::Checking::Checks::MixedContentTest < Nanoc::TestCase
'<video target="http://nanoc.ws/screen-cast.mkv"></video>',
'<p>http://nanoc.ws/harmless-text</p>',
])
- check = Nanoc::Extra::Checking::Checks::MixedContent.create(site)
+ check = Nanoc::Checking::Checks::MixedContent.create(site)
check.run
assert_empty check.issues
diff --git a/test/extra/checking/checks/test_stale.rb b/test/checking/checks/test_stale.rb
similarity index 92%
rename from test/extra/checking/checks/test_stale.rb
rename to test/checking/checks/test_stale.rb
index 0fde96f..16bf988 100644
--- a/test/extra/checking/checks/test_stale.rb
+++ b/test/checking/checks/test_stale.rb
@@ -1,11 +1,13 @@
-class Nanoc::Extra::Checking::Checks::StaleTest < Nanoc::TestCase
+require 'helper'
+
+class Nanoc::Checking::Checks::StaleTest < Nanoc::TestCase
def check_class
- Nanoc::Extra::Checking::Checks::Stale
+ Nanoc::Checking::Checks::Stale
end
def calc_issues
site = Nanoc::Int::SiteLoader.new.new_from_cwd
- runner = Nanoc::Extra::Checking::Runner.new(site)
+ runner = Nanoc::Checking::Runner.new(site)
runner.run_checks([check_class])
end
diff --git a/test/extra/checking/test_check.rb b/test/checking/test_check.rb
similarity index 57%
rename from test/extra/checking/test_check.rb
rename to test/checking/test_check.rb
index d9c3a9f..1b614aa 100644
--- a/test/extra/checking/test_check.rb
+++ b/test/checking/test_check.rb
@@ -1,8 +1,10 @@
-class Nanoc::Extra::Checking::CheckTest < Nanoc::TestCase
+require 'helper'
+
+class Nanoc::Checking::CheckTest < Nanoc::TestCase
def test_output_filenames
with_site do |site|
File.open('output/foo.html', 'w') { |io| io.write 'hello' }
- check = Nanoc::Extra::Checking::Check.create(site)
+ check = Nanoc::Checking::Check.create(site)
assert_equal ['output/foo.html'], check.output_filenames
end
end
@@ -10,8 +12,8 @@ class Nanoc::Extra::Checking::CheckTest < Nanoc::TestCase
def test_no_output_dir
with_site do |site|
site.config[:output_dir] = 'non-existent'
- assert_raises Nanoc::Extra::Checking::OutputDirNotFoundError do
- Nanoc::Extra::Checking::Check.create(site)
+ assert_raises Nanoc::Checking::OutputDirNotFoundError do
+ Nanoc::Checking::Check.create(site)
end
end
end
diff --git a/test/extra/checking/test_dsl.rb b/test/checking/test_dsl.rb
similarity index 52%
rename from test/extra/checking/test_dsl.rb
rename to test/checking/test_dsl.rb
index 7ee0302..e4916cd 100644
--- a/test/extra/checking/test_dsl.rb
+++ b/test/checking/test_dsl.rb
@@ -1,11 +1,13 @@
-class Nanoc::Extra::Checking::DSLTest < Nanoc::TestCase
+require 'helper'
+
+class Nanoc::Checking::DSLTest < Nanoc::TestCase
def test_from_file
with_site do |_site|
File.open('Checks', 'w') { |io| io.write("check :foo do\n\nend\ndeploy_check :bar\n") }
- dsl = Nanoc::Extra::Checking::DSL.from_file('Checks')
+ dsl = Nanoc::Checking::DSL.from_file('Checks')
# One new check
- refute Nanoc::Extra::Checking::Check.named(:foo).nil?
+ refute Nanoc::Checking::Check.named(:foo).nil?
# One check marked for deployment
assert_equal [:bar], dsl.deploy_checks
@@ -16,8 +18,16 @@ class Nanoc::Extra::Checking::DSLTest < Nanoc::TestCase
with_site do |_site|
File.write('stuff.rb', '$greeting = "hello"')
File.write('Checks', 'require "./stuff"')
- Nanoc::Extra::Checking::DSL.from_file('Checks')
+ Nanoc::Checking::DSL.from_file('Checks')
assert_equal 'hello', $greeting
end
end
+
+ def test_has_absolute_path
+ with_site do |_site|
+ File.write('Checks', '$stuff = __FILE__')
+ Nanoc::Checking::DSL.from_file('Checks')
+ assert($stuff.start_with?('/'))
+ end
+ end
end
diff --git a/test/extra/checking/test_runner.rb b/test/checking/test_runner.rb
similarity index 79%
rename from test/extra/checking/test_runner.rb
rename to test/checking/test_runner.rb
index d19f814..3a90c7a 100644
--- a/test/extra/checking/test_runner.rb
+++ b/test/checking/test_runner.rb
@@ -1,8 +1,10 @@
-class Nanoc::Extra::Checking::RunnerTest < Nanoc::TestCase
+require 'helper'
+
+class Nanoc::Checking::RunnerTest < Nanoc::TestCase
def test_run_specific
with_site do |site|
File.open('output/blah', 'w') { |io| io.write('I am stale! Haha!') }
- runner = Nanoc::Extra::Checking::Runner.new(site)
+ runner = Nanoc::Checking::Runner.new(site)
runner.run_specific(%w(stale))
end
end
@@ -13,7 +15,7 @@ class Nanoc::Extra::Checking::RunnerTest < Nanoc::TestCase
io.write('check :my_foo_check do ; puts "I AM FOO!" ; end')
end
- runner = Nanoc::Extra::Checking::Runner.new(site)
+ runner = Nanoc::Checking::Runner.new(site)
ios = capturing_stdio do
runner.run_specific(%w(my_foo_check))
end
@@ -28,7 +30,7 @@ class Nanoc::Extra::Checking::RunnerTest < Nanoc::TestCase
io.write('check :my_foo_check do ; end')
end
- runner = Nanoc::Extra::Checking::Runner.new(site)
+ runner = Nanoc::Checking::Runner.new(site)
ios = capturing_stdio do
runner.list_checks
end
diff --git a/test/cli/commands/test_check.rb b/test/cli/commands/test_check.rb
index d75f756..faaf675 100644
--- a/test/cli/commands/test_check.rb
+++ b/test/cli/commands/test_check.rb
@@ -1,3 +1,5 @@
+require 'helper'
+
class Nanoc::CLI::Commands::CheckTest < Nanoc::TestCase
def test_check_stale
with_site do |_site|
diff --git a/test/cli/commands/test_compile.rb b/test/cli/commands/test_compile.rb
index d0f2695..cd0d4d5 100644
--- a/test/cli/commands/test_compile.rb
+++ b/test/cli/commands/test_compile.rb
@@ -1,3 +1,5 @@
+require 'helper'
+
class Nanoc::CLI::Commands::CompileTest < Nanoc::TestCase
def test_profiling_information
with_site do |_site|
diff --git a/test/cli/commands/test_create_site.rb b/test/cli/commands/test_create_site.rb
index 255f4ad..7616db8 100644
--- a/test/cli/commands/test_create_site.rb
+++ b/test/cli/commands/test_create_site.rb
@@ -1,3 +1,5 @@
+require 'helper'
+
class Nanoc::CLI::Commands::CreateSiteTest < Nanoc::TestCase
def test_create_site_with_existing_name
Nanoc::CLI.run %w(create_site foo)
@@ -75,7 +77,7 @@ class Nanoc::CLI::Commands::CreateSiteTest < Nanoc::TestCase
FileUtils.cd('foo') do
# Try with encoding = default encoding = utf-8
File.open('content/index.html', 'w') { |io| io.write('Hello ' + 0xD6.chr + "!\n") }
- exception = assert_raises(RuntimeError) do
+ exception = assert_raises(Nanoc::DataSources::Filesystem::Errors::InvalidEncoding) do
Nanoc::Int::SiteLoader.new.new_from_cwd
end
assert_equal 'Could not read content/index.html because the file is not valid UTF-8.', exception.message
diff --git a/test/cli/commands/test_help.rb b/test/cli/commands/test_help.rb
index 69548b0..8757bc2 100644
--- a/test/cli/commands/test_help.rb
+++ b/test/cli/commands/test_help.rb
@@ -1,3 +1,5 @@
+require 'helper'
+
class Nanoc::CLI::Commands::HelpTest < Nanoc::TestCase
def test_run
Nanoc::CLI.run %w(help)
diff --git a/test/cli/commands/test_info.rb b/test/cli/commands/test_info.rb
index 9d293b7..e639db1 100644
--- a/test/cli/commands/test_info.rb
+++ b/test/cli/commands/test_info.rb
@@ -1,3 +1,5 @@
+require 'helper'
+
class Nanoc::CLI::Commands::InfoTest < Nanoc::TestCase
def test_run
Nanoc::CLI.run %w(info)
diff --git a/test/cli/commands/test_prune.rb b/test/cli/commands/test_prune.rb
index e2ed950..e598d96 100644
--- a/test/cli/commands/test_prune.rb
+++ b/test/cli/commands/test_prune.rb
@@ -1,3 +1,5 @@
+require 'helper'
+
class Nanoc::CLI::Commands::PruneTest < Nanoc::TestCase
def test_run_without_yes
with_site do |_site|
diff --git a/test/cli/test_cleaning_stream.rb b/test/cli/test_cleaning_stream.rb
index 4a1faea..7cd1587 100644
--- a/test/cli/test_cleaning_stream.rb
+++ b/test/cli/test_cleaning_stream.rb
@@ -1,9 +1,11 @@
+require 'helper'
+
class Nanoc::CLI::CleaningStreamTest < Nanoc::TestCase
class Stream
attr_accessor :called_methods
def initialize
- @called_methods = Set.new
+ @called_methods = []
end
# rubocop:disable Style/MethodMissing
@@ -18,7 +20,7 @@ class Nanoc::CLI::CleaningStreamTest < Nanoc::TestCase
end
def test_forward
- methods = [:write, :<<, :tty?, :flush, :tell, :print, :puts, :string, :reopen, :exist?, :exists?, :close]
+ methods = [:write, :<<, :tty?, :tty?, :flush, :tell, :print, :puts, :string, :reopen, :exist?, :exists?, :close]
s = Stream.new
cs = Nanoc::CLI::CleaningStream.new(s)
@@ -26,6 +28,7 @@ class Nanoc::CLI::CleaningStreamTest < Nanoc::TestCase
cs.write('aaa')
cs << 'bb'
cs.tty?
+ cs.isatty
cs.flush
cs.tell
cs.print('cc')
@@ -41,6 +44,16 @@ class Nanoc::CLI::CleaningStreamTest < Nanoc::TestCase
end
end
+ def test_forward_tty_cached
+ s = Stream.new
+ cs = Nanoc::CLI::CleaningStream.new(s)
+
+ cs.tty?
+ cs.isatty
+
+ assert_equal [:tty?], s.called_methods
+ end
+
def test_works_with_logger
require 'logger'
stream = StringIO.new
diff --git a/test/cli/test_cli.rb b/test/cli/test_cli.rb
index 32d53d6..3c7340c 100644
--- a/test/cli/test_cli.rb
+++ b/test/cli/test_cli.rb
@@ -1,3 +1,5 @@
+require 'helper'
+
class Nanoc::CLITest < Nanoc::TestCase
COMMAND_CODE = <<EOS.freeze
usage '_test [options]'
@@ -176,17 +178,17 @@ EOS
with_env_vars(new_env_diff) do
refute Nanoc::CLI.enable_utf8?(io)
- with_env_vars({ 'LC_ALL' => 'en_US.UTF-8' }) { assert Nanoc::CLI.enable_utf8?(io) }
- with_env_vars({ 'LC_CTYPE' => 'en_US.UTF-8' }) { assert Nanoc::CLI.enable_utf8?(io) }
- with_env_vars({ 'LANG' => 'en_US.UTF-8' }) { assert Nanoc::CLI.enable_utf8?(io) }
+ with_env_vars('LC_ALL' => 'en_US.UTF-8') { assert Nanoc::CLI.enable_utf8?(io) }
+ with_env_vars('LC_CTYPE' => 'en_US.UTF-8') { assert Nanoc::CLI.enable_utf8?(io) }
+ with_env_vars('LANG' => 'en_US.UTF-8') { assert Nanoc::CLI.enable_utf8?(io) }
- with_env_vars({ 'LC_ALL' => 'en_US.utf-8' }) { assert Nanoc::CLI.enable_utf8?(io) }
- with_env_vars({ 'LC_CTYPE' => 'en_US.utf-8' }) { assert Nanoc::CLI.enable_utf8?(io) }
- with_env_vars({ 'LANG' => 'en_US.utf-8' }) { assert Nanoc::CLI.enable_utf8?(io) }
+ with_env_vars('LC_ALL' => 'en_US.utf-8') { assert Nanoc::CLI.enable_utf8?(io) }
+ with_env_vars('LC_CTYPE' => 'en_US.utf-8') { assert Nanoc::CLI.enable_utf8?(io) }
+ with_env_vars('LANG' => 'en_US.utf-8') { assert Nanoc::CLI.enable_utf8?(io) }
- with_env_vars({ 'LC_ALL' => 'en_US.utf8' }) { assert Nanoc::CLI.enable_utf8?(io) }
- with_env_vars({ 'LC_CTYPE' => 'en_US.utf8' }) { assert Nanoc::CLI.enable_utf8?(io) }
- with_env_vars({ 'LANG' => 'en_US.utf8' }) { assert Nanoc::CLI.enable_utf8?(io) }
+ with_env_vars('LC_ALL' => 'en_US.utf8') { assert Nanoc::CLI.enable_utf8?(io) }
+ with_env_vars('LC_CTYPE' => 'en_US.utf8') { assert Nanoc::CLI.enable_utf8?(io) }
+ with_env_vars('LANG' => 'en_US.utf8') { assert Nanoc::CLI.enable_utf8?(io) }
end
end
end
diff --git a/test/cli/test_error_handler.rb b/test/cli/test_error_handler.rb
index 304e4c9..5dec48a 100644
--- a/test/cli/test_error_handler.rb
+++ b/test/cli/test_error_handler.rb
@@ -1,3 +1,5 @@
+require 'helper'
+
class Nanoc::CLI::ErrorHandlerTest < Nanoc::TestCase
def setup
super
@@ -46,7 +48,42 @@ class Nanoc::CLI::ErrorHandlerTest < Nanoc::TestCase
refute_match(/See full crash log for details./, stream.string)
end
- def new_error(amount_factor)
+ def test_write_error_message_wrapped
+ stream = StringIO.new
+ @handler.send(:write_error_message, stream, new_wrapped_error(new_error), verbose: true)
+ refute_match(/CompilationError/, stream.string)
+ end
+
+ def test_write_stack_trace_wrapped
+ stream = StringIO.new
+ @handler.send(:write_stack_trace, stream, new_wrapped_error(new_error), verbose: false)
+ assert_match(/new_error/, stream.string)
+ end
+
+ def test_write_item_rep
+ stream = StringIO.new
+ @handler.send(:write_item_rep, stream, new_wrapped_error(new_error), verbose: false)
+ assert_match(/^Item identifier: \/about\.md$/, stream.string)
+ assert_match(/^Item rep name: :latex$/, stream.string)
+ end
+
+ def test_resolution_for_wrapped
+ def @handler.using_bundler?
+ true
+ end
+ error = new_wrapped_error(LoadError.new('no such file to load -- kramdown'))
+ assert_match(/^Make sure the gem is added to Gemfile/, @handler.send(:resolution_for, error))
+ end
+
+ def new_wrapped_error(wrapped)
+ item = Nanoc::Int::Item.new('asdf', {}, '/about.md')
+ item_rep = Nanoc::Int::ItemRep.new(item, :latex)
+ raise Nanoc::Int::Errors::CompilationError.new(wrapped, item_rep)
+ rescue => e
+ return e
+ end
+
+ def new_error(amount_factor = 1)
backtrace_generator = lambda do |af|
if af.zero?
raise 'finally!'
diff --git a/test/cli/test_logger.rb b/test/cli/test_logger.rb
index 4a9e5a6..94a029c 100644
--- a/test/cli/test_logger.rb
+++ b/test/cli/test_logger.rb
@@ -1,4 +1,5 @@
+require 'helper'
+
class Nanoc::CLI::LoggerTest < Nanoc::TestCase
- def test_stub
- end
+ def test_stub; end
end
diff --git a/test/data_sources/test_filesystem.rb b/test/data_sources/test_filesystem.rb
index 72b92f3..595e888 100644
--- a/test/data_sources/test_filesystem.rb
+++ b/test/data_sources/test_filesystem.rb
@@ -1,3 +1,5 @@
+require 'helper'
+
class Nanoc::DataSources::FilesystemTest < Nanoc::TestCase
def new_data_source(params = nil)
# Mock site
@@ -69,7 +71,7 @@ class Nanoc::DataSources::FilesystemTest < Nanoc::TestCase
def test_load_objects_with_same_extensions
# Create data source
- data_source = new_data_source({ identifier_type: 'full' })
+ data_source = new_data_source(identifier_type: 'full')
# Create a fake class
klass = Class.new do
@@ -135,14 +137,14 @@ class Nanoc::DataSources::FilesystemTest < Nanoc::TestCase
File.open('foo/stuff.dat', 'w') { |io| io.write('random binary data') }
# Load
- assert_raises(RuntimeError) do
+ assert_raises(Nanoc::DataSources::Filesystem::Errors::BinaryLayout) do
data_source.send(:load_objects, 'foo', Nanoc::Int::Layout)
end
end
def test_identifier_for_filename_with_full_style_identifier
# Create data source
- data_source = new_data_source({ identifier_type: 'full' })
+ data_source = new_data_source(identifier_type: 'full')
# Get input and expected output
expected = {
@@ -463,7 +465,7 @@ class Nanoc::DataSources::FilesystemTest < Nanoc::TestCase
end
def test_load_objects_correct_identifier_with_separate_yaml_file
- data_source = new_data_source({ identifier_type: 'full' })
+ data_source = new_data_source(identifier_type: 'full')
FileUtils.mkdir_p('foo')
File.write('foo/donkey.jpeg', 'data')
@@ -483,24 +485,6 @@ class Nanoc::DataSources::FilesystemTest < Nanoc::TestCase
assert_equal nil, data_source.send(:filename_for, '/foo', nil)
end
- def test_compile_huge_site
- if_implemented do
- # Create data source
- data_source = new_data_source
-
- # Create a lot of items
- count = Process.getrlimit(Process::RLIMIT_NOFILE)[0] + 5
- count.times do |i|
- FileUtils.mkdir_p("content/#{i}")
- File.open("content/#{i}/#{i}.html", 'w') { |io| io << "This is item #{i}." }
- File.open("content/#{i}/#{i}.yaml", 'w') { |io| io << "title: Item #{i}" }
- end
-
- # Read all items
- data_source.items
- end
- end
-
def test_compile_iso_8859_1_site
# Check encoding
unless ''.respond_to?(:encode)
@@ -559,7 +543,7 @@ class Nanoc::DataSources::FilesystemTest < Nanoc::TestCase
def test_all_split_files_in_allowing_periods_in_identifiers
# Create data source
- data_source = Nanoc::DataSources::Filesystem.new(nil, nil, nil, { allow_periods_in_identifiers: true })
+ data_source = Nanoc::DataSources::Filesystem.new(nil, nil, nil, allow_periods_in_identifiers: true)
# Write sample files
FileUtils.mkdir_p('foo')
@@ -660,14 +644,14 @@ class Nanoc::DataSources::FilesystemTest < Nanoc::TestCase
end
# Check
- assert_raises RuntimeError do
+ assert_raises(Nanoc::DataSources::Filesystem::Errors::MultipleContentFiles) do
data_source.send(:all_split_files_in, '.')
end
end
def test_basename_of_allowing_periods_in_identifiers
# Create data source
- data_source = Nanoc::DataSources::Filesystem.new(nil, nil, nil, { allow_periods_in_identifiers: true })
+ data_source = Nanoc::DataSources::Filesystem.new(nil, nil, nil, allow_periods_in_identifiers: true)
# Get input and expected output
expected = {
@@ -727,7 +711,7 @@ class Nanoc::DataSources::FilesystemTest < Nanoc::TestCase
def test_ext_of_allowing_periods_in_identifiers
# Create data source
- data_source = Nanoc::DataSources::Filesystem.new(nil, nil, nil, { allow_periods_in_identifiers: true })
+ data_source = Nanoc::DataSources::Filesystem.new(nil, nil, nil, allow_periods_in_identifiers: true)
# Get input and expected output
expected = {
@@ -847,7 +831,7 @@ class Nanoc::DataSources::FilesystemTest < Nanoc::TestCase
data_source = Nanoc::DataSources::Filesystem.new(nil, nil, nil, nil)
# Parse it
- assert_raises(RuntimeError) do
+ assert_raises(Nanoc::DataSources::Filesystem::Errors::InvalidFormat) do
data_source.instance_eval { parse('test.html', nil) }
end
end
@@ -999,7 +983,7 @@ class Nanoc::DataSources::FilesystemTest < Nanoc::TestCase
data_source = Nanoc::DataSources::Filesystem.new(nil, nil, nil, nil)
- assert_raises(Nanoc::DataSources::Filesystem::InvalidMetadataError) do
+ assert_raises(Nanoc::DataSources::Filesystem::Errors::InvalidMetadata) do
data_source.instance_eval { parse('test.html', nil) }
end
end
@@ -1010,7 +994,7 @@ class Nanoc::DataSources::FilesystemTest < Nanoc::TestCase
data_source = Nanoc::DataSources::Filesystem.new(nil, nil, nil, nil)
- assert_raises(Nanoc::DataSources::Filesystem::InvalidMetadataError) do
+ assert_raises(Nanoc::DataSources::Filesystem::Errors::InvalidMetadata) do
data_source.instance_eval { parse('test.html', 'test.yaml') }
end
end
diff --git a/test/extra/test_filesystem_tools.rb b/test/data_sources/test_filesystem_tools.rb
similarity index 76%
rename from test/extra/test_filesystem_tools.rb
rename to test/data_sources/test_filesystem_tools.rb
index bcef74f..138ab53 100644
--- a/test/extra/test_filesystem_tools.rb
+++ b/test/data_sources/test_filesystem_tools.rb
@@ -1,4 +1,6 @@
-class Nanoc::Extra::FilesystemToolsTest < Nanoc::TestCase
+require 'helper'
+
+class Nanoc::DataSources::FilesystemToolsTest < Nanoc::TestCase
def setup
super
skip_unless_symlinks_supported
@@ -30,7 +32,7 @@ class Nanoc::Extra::FilesystemToolsTest < Nanoc::TestCase
'dir0/sub/sub/sub/sub/sub/sub/sub/sub/sub/foo.md',
'dir0/sub/sub/sub/sub/sub/sub/sub/sub/sub/sub/foo.md',
]
- actual_files = Nanoc::Extra::FilesystemTools.all_files_in('dir0', nil).sort
+ actual_files = Nanoc::DataSources::Filesystem::Tools.all_files_in('dir0', nil).sort
assert_equal expected_files, actual_files
end
@@ -44,8 +46,8 @@ class Nanoc::Extra::FilesystemToolsTest < Nanoc::TestCase
File.symlink("../dir#{i}", "dir#{i - 1}/sub")
end
- assert_raises Nanoc::Extra::FilesystemTools::MaxSymlinkDepthExceededError do
- Nanoc::Extra::FilesystemTools.all_files_in('dir0', nil)
+ assert_raises Nanoc::DataSources::Filesystem::Tools::MaxSymlinkDepthExceededError do
+ Nanoc::DataSources::Filesystem::Tools.all_files_in('dir0', nil)
end
end
@@ -59,7 +61,7 @@ class Nanoc::Extra::FilesystemToolsTest < Nanoc::TestCase
File.symlink('../bar', 'foo/barlink')
expected_files = ['foo/barlink/y.md', 'foo/x.md']
- actual_files = Nanoc::Extra::FilesystemTools.all_files_in('foo', nil).sort
+ actual_files = Nanoc::DataSources::Filesystem::Tools.all_files_in('foo', nil).sort
assert_equal expected_files, actual_files
end
@@ -72,7 +74,7 @@ class Nanoc::Extra::FilesystemToolsTest < Nanoc::TestCase
# Check
expected_files = ['dir/bar-link', 'dir/foo']
- actual_files = Nanoc::Extra::FilesystemTools.all_files_in('dir', nil).sort
+ actual_files = Nanoc::DataSources::Filesystem::Tools.all_files_in('dir', nil).sort
assert_equal expected_files, actual_files
end
@@ -83,7 +85,7 @@ class Nanoc::Extra::FilesystemToolsTest < Nanoc::TestCase
File.symlink('baz', 'qux')
expected = File.expand_path('foo')
- actual = Nanoc::Extra::FilesystemTools.resolve_symlink('qux')
+ actual = Nanoc::DataSources::Filesystem::Tools.resolve_symlink('qux')
assert_equal expected, actual
end
@@ -94,8 +96,8 @@ class Nanoc::Extra::FilesystemToolsTest < Nanoc::TestCase
File.symlink("symlink-#{i - 1}", "symlink-#{i}")
end
- assert_raises Nanoc::Extra::FilesystemTools::MaxSymlinkDepthExceededError do
- Nanoc::Extra::FilesystemTools.resolve_symlink('symlink-7')
+ assert_raises Nanoc::DataSources::Filesystem::Tools::MaxSymlinkDepthExceededError do
+ Nanoc::DataSources::Filesystem::Tools.resolve_symlink('symlink-7')
end
end
@@ -105,7 +107,7 @@ class Nanoc::Extra::FilesystemToolsTest < Nanoc::TestCase
File.open('dir/.DS_Store', 'w') { |io| io.write('o hai') }
File.open('dir/.htaccess', 'w') { |io| io.write('o hai') }
- actual_files = Nanoc::Extra::FilesystemTools.all_files_in('dir', nil).sort
+ actual_files = Nanoc::DataSources::Filesystem::Tools.all_files_in('dir', nil).sort
assert_equal [], actual_files
end
@@ -114,7 +116,7 @@ class Nanoc::Extra::FilesystemToolsTest < Nanoc::TestCase
FileUtils.mkdir_p('dir')
File.open('dir/.other', 'w') { |io| io.write('o hai') }
- actual_files = Nanoc::Extra::FilesystemTools.all_files_in('dir', '**/.other').sort
+ actual_files = Nanoc::DataSources::Filesystem::Tools.all_files_in('dir', '**/.other').sort
assert_equal ['dir/.other'], actual_files
end
@@ -124,7 +126,7 @@ class Nanoc::Extra::FilesystemToolsTest < Nanoc::TestCase
File.open('dir/.other', 'w') { |io| io.write('o hai') }
File.open('dir/.DS_Store', 'w') { |io| io.write('o hai') }
- actual_files = Nanoc::Extra::FilesystemTools.all_files_in('dir', ['**/.other', '**/.DS_Store']).sort
+ actual_files = Nanoc::DataSources::Filesystem::Tools.all_files_in('dir', ['**/.other', '**/.DS_Store']).sort
assert_equal ['dir/.other', 'dir/.DS_Store'].sort, actual_files.sort
end
@@ -136,7 +138,7 @@ class Nanoc::Extra::FilesystemToolsTest < Nanoc::TestCase
pattern = { dotfiles: '**/.other' }
assert_raises Nanoc::Int::Errors::GenericTrivial, "Do not know how to handle extra_files: #{pattern.inspect}" do
- Nanoc::Extra::FilesystemTools.all_files_in('dir0', pattern)
+ Nanoc::DataSources::Filesystem::Tools.all_files_in('dir0', pattern)
end
end
end
diff --git a/test/extra/deployers/test_fog.rb b/test/deploying/test_fog.rb
similarity index 83%
rename from test/extra/deployers/test_fog.rb
rename to test/deploying/test_fog.rb
index 0723e69..5a2b4be 100644
--- a/test/extra/deployers/test_fog.rb
+++ b/test/deploying/test_fog.rb
@@ -1,7 +1,9 @@
-class Nanoc::Extra::Deployers::FogTest < Nanoc::TestCase
+require 'helper'
+
+class Nanoc::Deploying::Deployers::FogTest < Nanoc::TestCase
def test_read_etags_with_local_provider
if_have 'fog' do
- fog = Nanoc::Extra::Deployers::Fog.new(
+ fog = Nanoc::Deploying::Deployers::Fog.new(
'output/', provider: 'local'
)
@@ -16,7 +18,7 @@ class Nanoc::Extra::Deployers::FogTest < Nanoc::TestCase
def test_read_etags_with_aws_provider
if_have 'fog' do
- fog = Nanoc::Extra::Deployers::Fog.new(
+ fog = Nanoc::Deploying::Deployers::Fog.new(
'output/', provider: 'aws'
)
@@ -36,7 +38,7 @@ class Nanoc::Extra::Deployers::FogTest < Nanoc::TestCase
def test_calc_local_etag_with_local_provider
if_have 'fog' do
- fog = Nanoc::Extra::Deployers::Fog.new(
+ fog = Nanoc::Deploying::Deployers::Fog.new(
'output/', provider: 'local'
)
@@ -49,7 +51,7 @@ class Nanoc::Extra::Deployers::FogTest < Nanoc::TestCase
def test_calc_local_etag_with_aws_provider
if_have 'fog' do
- fog = Nanoc::Extra::Deployers::Fog.new(
+ fog = Nanoc::Deploying::Deployers::Fog.new(
'output/', provider: 'aws'
)
@@ -65,7 +67,7 @@ class Nanoc::Extra::Deployers::FogTest < Nanoc::TestCase
def test_needs_upload_with_missing_remote_etag
if_have 'fog' do
- fog = Nanoc::Extra::Deployers::Fog.new(
+ fog = Nanoc::Deploying::Deployers::Fog.new(
'output/', provider: 'aws'
)
@@ -81,7 +83,7 @@ class Nanoc::Extra::Deployers::FogTest < Nanoc::TestCase
def test_needs_upload_with_different_etags
if_have 'fog' do
- fog = Nanoc::Extra::Deployers::Fog.new(
+ fog = Nanoc::Deploying::Deployers::Fog.new(
'output/', provider: 'aws'
)
@@ -97,7 +99,7 @@ class Nanoc::Extra::Deployers::FogTest < Nanoc::TestCase
def test_needs_upload_with_identical_etags
if_have 'fog' do
- fog = Nanoc::Extra::Deployers::Fog.new(
+ fog = Nanoc::Deploying::Deployers::Fog.new(
'output/', provider: 'aws'
)
diff --git a/test/extra/deployers/test_rsync.rb b/test/deploying/test_rsync.rb
similarity index 76%
rename from test/extra/deployers/test_rsync.rb
rename to test/deploying/test_rsync.rb
index 0ea05ce..6cf0529 100644
--- a/test/extra/deployers/test_rsync.rb
+++ b/test/deploying/test_rsync.rb
@@ -1,7 +1,9 @@
-class Nanoc::Extra::Deployers::RsyncTest < Nanoc::TestCase
+require 'helper'
+
+class Nanoc::Deploying::Deployers::RsyncTest < Nanoc::TestCase
def test_run_without_dst
# Create deployer
- rsync = Nanoc::Extra::Deployers::Rsync.new(
+ rsync = Nanoc::Deploying::Deployers::Rsync.new(
'output/',
{},
)
@@ -22,9 +24,9 @@ class Nanoc::Extra::Deployers::RsyncTest < Nanoc::TestCase
def test_run_with_erroneous_dst
# Create deployer
- rsync = Nanoc::Extra::Deployers::Rsync.new(
+ rsync = Nanoc::Deploying::Deployers::Rsync.new(
'output/',
- { dst: 'asdf/' },
+ dst: 'asdf/',
)
# Mock run_shell_cmd
@@ -43,9 +45,9 @@ class Nanoc::Extra::Deployers::RsyncTest < Nanoc::TestCase
def test_run_everything_okay
# Create deployer
- rsync = Nanoc::Extra::Deployers::Rsync.new(
+ rsync = Nanoc::Deploying::Deployers::Rsync.new(
'output',
- { dst: 'asdf' },
+ dst: 'asdf',
)
# Mock run_shell_cmd
@@ -57,7 +59,7 @@ class Nanoc::Extra::Deployers::RsyncTest < Nanoc::TestCase
rsync.run
# Check args
- opts = Nanoc::Extra::Deployers::Rsync::DEFAULT_OPTIONS
+ opts = Nanoc::Deploying::Deployers::Rsync::DEFAULT_OPTIONS
assert_equal(
['rsync', opts, 'output/', 'asdf'].flatten,
rsync.instance_eval { @shell_cms_args },
@@ -66,7 +68,7 @@ class Nanoc::Extra::Deployers::RsyncTest < Nanoc::TestCase
def test_run_everything_okay_dry
# Create deployer
- rsync = Nanoc::Extra::Deployers::Rsync.new(
+ rsync = Nanoc::Deploying::Deployers::Rsync.new(
'output',
{ dst: 'asdf' },
dry_run: true,
@@ -81,7 +83,7 @@ class Nanoc::Extra::Deployers::RsyncTest < Nanoc::TestCase
rsync.run
# Check args
- opts = Nanoc::Extra::Deployers::Rsync::DEFAULT_OPTIONS
+ opts = Nanoc::Deploying::Deployers::Rsync::DEFAULT_OPTIONS
assert_equal(
['echo', 'rsync', opts, 'output/', 'asdf'].flatten,
rsync.instance_eval { @shell_cms_args },
diff --git a/test/extra/core_ext/test_pathname.rb b/test/extra/core_ext/test_pathname.rb
index 544277f..4d2b537 100644
--- a/test/extra/core_ext/test_pathname.rb
+++ b/test/extra/core_ext/test_pathname.rb
@@ -1,3 +1,5 @@
+require 'helper'
+
class Nanoc::Extra::CoreExtPathnameTest < Nanoc::TestCase
def test_components
assert_equal %w(/ a bb ccc dd e), Pathname.new('/a/bb/ccc/dd/e').__nanoc_components
diff --git a/test/extra/core_ext/test_time.rb b/test/extra/core_ext/test_time.rb
index 5733f5f..735d71c 100644
--- a/test/extra/core_ext/test_time.rb
+++ b/test/extra/core_ext/test_time.rb
@@ -1,8 +1,14 @@
+require 'helper'
+
class Nanoc::ExtraCoreExtTimeTest < Nanoc::TestCase
- def test___nanoc_to_iso8601_date
+ def test___nanoc_to_iso8601_date_utc
assert_equal('2008-05-19', Time.utc(2008, 5, 19, 14, 20, 0, 0).__nanoc_to_iso8601_date)
end
+ def test___nanoc_to_iso8601_date_non_utc
+ assert_equal('2008-05-18', Time.new(2008, 5, 19, 0, 0, 0, '+02:00').__nanoc_to_iso8601_date)
+ end
+
def test___nanoc_to_iso8601_time
assert_equal('2008-05-19T14:20:00Z', Time.utc(2008, 5, 19, 14, 20, 0, 0).__nanoc_to_iso8601_time)
end
diff --git a/test/extra/test_link_collector.rb b/test/extra/test_link_collector.rb
index 5dfe33c..668d399 100644
--- a/test/extra/test_link_collector.rb
+++ b/test/extra/test_link_collector.rb
@@ -1,3 +1,5 @@
+require 'helper'
+
class Nanoc::Extra::LinkCollectorTest < Nanoc::TestCase
def test_all
# Create dummy data
diff --git a/test/extra/test_piper.rb b/test/extra/test_piper.rb
index 60ccd0c..a8a5f39 100644
--- a/test/extra/test_piper.rb
+++ b/test/extra/test_piper.rb
@@ -1,3 +1,5 @@
+require 'helper'
+
class Nanoc::Extra::PiperTest < Nanoc::TestCase
def test_basic
stdout = StringIO.new
diff --git a/test/filters/test_colorize_syntax.rb b/test/filters/colorize_syntax/test_coderay.rb
similarity index 56%
rename from test/filters/test_colorize_syntax.rb
rename to test/filters/colorize_syntax/test_coderay.rb
index d19f040..3bbf580 100644
--- a/test/filters/test_colorize_syntax.rb
+++ b/test/filters/colorize_syntax/test_coderay.rb
@@ -1,4 +1,6 @@
-class Nanoc::Filters::ColorizeSyntaxTest < Nanoc::TestCase
+require 'helper'
+
+class Nanoc::Filters::ColorizeSyntax::CoderayTest < Nanoc::TestCase
CODERAY_PRE = '<div class="CodeRay"><div class="code">'.freeze
CODERAY_POST = '</div></div>'.freeze
@@ -17,56 +19,6 @@ class Nanoc::Filters::ColorizeSyntaxTest < Nanoc::TestCase
end
end
- def test_dummy
- if_have 'nokogiri' do
- # Create filter
- filter = ::Nanoc::Filters::ColorizeSyntax.new
-
- # Get input and expected output
- input = '<pre title="moo"><code class="language-ruby"># comment</code></pre>'
- expected_output = input # because we are using a dummy
-
- # Run filter
- actual_output = filter.setup_and_run(input, default_colorizer: :dummy)
- assert_equal(expected_output, actual_output)
- end
- end
-
- def test_with_frozen_input
- if_have 'nokogiri' do
- input = '<pre title="moo"><code class="language-ruby"># comment</code></pre>'.freeze
- input.freeze
-
- filter = ::Nanoc::Filters::ColorizeSyntax.new
- filter.setup_and_run(input, default_colorizer: :dummy)
- end
- end
-
- def test_full_page
- if_have 'nokogiri' do
- # Create filter
- filter = ::Nanoc::Filters::ColorizeSyntax.new
-
- # Get input and expected output
- input = <<EOS
-<!DOCTYPE html>
-<html>
- <head>
- <title>Foo</title>
- </head>
- <body>
- <pre title="moo"><code class="language-ruby"># comment</code></pre>
- </body>
-</html>
-EOS
- expected_output_regex = %r{^<!DOCTYPE html>\s*<html>\s*<head>\s*<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\s*<title>Foo</title>\s*</head>\s*<body>\s*<pre title="moo"><code class="language-ruby"># comment</code></pre>\s*</body>\s*</html>}
-
- # Run filter
- actual_output = filter.setup_and_run(input, default_colorizer: :dummy, is_fullpage: true)
- assert_match expected_output_regex, actual_output
- end
- end
-
def test_coderay_with_comment
if_have 'coderay', 'nokogiri' do
# Create filter
@@ -132,58 +84,6 @@ EOS
end
end
- def test_pygmentize
- if_have 'nokogiri' do
- skip_unless_have_command 'pygmentize'
-
- # Create filter
- filter = ::Nanoc::Filters::ColorizeSyntax.new
-
- # Get input and expected output
- input = '<pre title="moo"><code class="language-ruby"># comment</code></pre>'
- expected_output = '<pre title="moo"><code class="language-ruby"><span class="c1"># comment</span></code></pre>'
-
- # Run filter
- actual_output = filter.setup_and_run(input, colorizers: { ruby: :pygmentize })
- assert_equal(expected_output, actual_output)
- end
- end
-
- def test_pygmentsrb
- skip 'pygments.rb does not support Windows' if on_windows?
- if_have 'pygments', 'nokogiri' do
- # Create filter
- filter = ::Nanoc::Filters::ColorizeSyntax.new
-
- # Get input and expected output
- input = '<pre title="moo"><code class="language-ruby"># comment…</code></pre>'
- expected_output = '<pre title="moo"><code class="language-ruby"><span class="c1"># comment…</span></code></pre>'
-
- # Run filter
- actual_output = filter.setup_and_run(input, colorizers: { ruby: :pygmentsrb })
- assert_equal(expected_output, actual_output)
- end
- end
-
- def test_simon_highlight
- if_have 'nokogiri' do
- skip_unless_have_command 'highlight'
-
- # Create filter
- filter = ::Nanoc::Filters::ColorizeSyntax.new
-
- # Get input and expected output
- input = %(<pre title="moo"><code class="language-ruby">
-# comment
-</code></pre>)
- expected_output = '<pre title="moo"><code class="language-ruby"><span class="hl slc"># comment</span></code></pre>'
-
- # Run filter
- actual_output = filter.setup_and_run(input, default_colorizer: :simon_highlight)
- assert_equal(expected_output, actual_output)
- end
- end
-
def test_colorize_syntax_with_unknown_syntax
if_have 'coderay', 'nokogiri' do
# Create filter
@@ -226,53 +126,6 @@ EOS
end
end
- def test_colorize_syntax_with_default_colorizer
- skip_unless_have_command 'pygmentize'
-
- if_have 'nokogiri' do
- # Create filter
- filter = ::Nanoc::Filters::ColorizeSyntax.new
-
- # Get input and expected output
- input = '<pre><code class="language-ruby">puts "foo"</code></pre>'
- expected_output = '<pre><code class="language-ruby"><span class="nb">puts</span> <span class="s2">"foo"</span></code></pre>'
-
- # Run filter
- actual_output = filter.setup_and_run(input, default_colorizer: :pygmentize)
- assert_equal(expected_output, actual_output)
- end
- end
-
- def test_colorize_syntax_with_missing_executables
- if_have 'nokogiri' do
- begin
- original_path = ENV['PATH']
- ENV['PATH'] = './blooblooblah'
-
- # Create filter
- filter = ::Nanoc::Filters::ColorizeSyntax.new
-
- # Get input and expected output
- input = '<pre><code class="language-ruby">puts "foo"</code></pre>'
-
- # Run filter
- [:albino, :pygmentize, :simon_highlight].each do |colorizer|
- begin
- input = '<pre><code class="language-ruby">puts "foo"</code></pre>'
- filter.setup_and_run(
- input,
- colorizers: { ruby: colorizer },
- )
- flunk 'expected colorizer to raise if no executable is available'
- rescue
- end
- end
- ensure
- ENV['PATH'] = original_path
- end
- end
- end
-
def test_colorize_syntax_with_non_language_shebang_line
if_have 'coderay', 'nokogiri' do
# Create filter
@@ -389,31 +242,4 @@ EOS
assert_equal(expected_output, actual_output)
end
end
-
- def test_rouge
- if_have 'rouge', 'nokogiri' do
- # Create filter
- filter = ::Nanoc::Filters::ColorizeSyntax.new
-
- # Get input and expected output
- input = <<EOS
-before
-<pre><code class="language-ruby">
- def foo
- end
-</code></pre>
-after
-EOS
- expected_output = <<EOS
-before
-<pre><code class=\"language-ruby\"> <span class=\"k\">def</span> <span class=\"nf\">foo</span>
- <span class=\"k\">end</span></code></pre>
-after
-EOS
-
- # Run filter
- actual_output = filter.setup_and_run(input, default_colorizer: :rouge)
- assert_equal(expected_output, actual_output)
- end
- end
end
diff --git a/test/filters/colorize_syntax/test_common.rb b/test/filters/colorize_syntax/test_common.rb
new file mode 100644
index 0000000..1f0ce14
--- /dev/null
+++ b/test/filters/colorize_syntax/test_common.rb
@@ -0,0 +1,83 @@
+require 'helper'
+
+class Nanoc::Filters::ColorizeSyntax::CommonTest < Nanoc::TestCase
+ def test_dummy
+ if_have 'nokogiri' do
+ # Create filter
+ filter = ::Nanoc::Filters::ColorizeSyntax.new
+
+ # Get input and expected output
+ input = '<pre title="moo"><code class="language-ruby"># comment</code></pre>'
+ expected_output = input # because we are using a dummy
+
+ # Run filter
+ actual_output = filter.setup_and_run(input, default_colorizer: :dummy)
+ assert_equal(expected_output, actual_output)
+ end
+ end
+
+ def test_with_frozen_input
+ if_have 'nokogiri' do
+ input = '<pre title="moo"><code class="language-ruby"># comment</code></pre>'.freeze
+ input.freeze
+
+ filter = ::Nanoc::Filters::ColorizeSyntax.new
+ filter.setup_and_run(input, default_colorizer: :dummy)
+ end
+ end
+
+ def test_full_page
+ if_have 'nokogiri' do
+ # Create filter
+ filter = ::Nanoc::Filters::ColorizeSyntax.new
+
+ # Get input and expected output
+ input = <<EOS
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Foo</title>
+ </head>
+ <body>
+ <pre title="moo"><code class="language-ruby"># comment</code></pre>
+ </body>
+</html>
+EOS
+ expected_output_regex = %r{^<!DOCTYPE html>\s*<html>\s*<head>\s*<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\s*<title>Foo</title>\s*</head>\s*<body>\s*<pre title="moo"><code class="language-ruby"># comment</code></pre>\s*</body>\s*</html>}
+
+ # Run filter
+ actual_output = filter.setup_and_run(input, default_colorizer: :dummy, is_fullpage: true)
+ assert_match expected_output_regex, actual_output
+ end
+ end
+
+ def test_colorize_syntax_with_missing_executables
+ if_have 'nokogiri' do
+ begin
+ original_path = ENV['PATH']
+ ENV['PATH'] = './blooblooblah'
+
+ # Create filter
+ filter = ::Nanoc::Filters::ColorizeSyntax.new
+
+ # Get input and expected output
+ input = '<pre><code class="language-ruby">puts "foo"</code></pre>'
+
+ # Run filter
+ [:albino, :pygmentize, :simon_highlight].each do |colorizer|
+ begin
+ input = '<pre><code class="language-ruby">puts "foo"</code></pre>'
+ filter.setup_and_run(
+ input,
+ colorizers: { ruby: colorizer },
+ )
+ flunk 'expected colorizer to raise if no executable is available'
+ rescue
+ end
+ end
+ ensure
+ ENV['PATH'] = original_path
+ end
+ end
+ end
+end
diff --git a/test/filters/colorize_syntax/test_pygmentize.rb b/test/filters/colorize_syntax/test_pygmentize.rb
new file mode 100644
index 0000000..cc28b32
--- /dev/null
+++ b/test/filters/colorize_syntax/test_pygmentize.rb
@@ -0,0 +1,37 @@
+require 'helper'
+
+class Nanoc::Filters::ColorizeSyntax::PygmentizeTest < Nanoc::TestCase
+ def test_pygmentize
+ if_have 'nokogiri' do
+ skip_unless_have_command 'pygmentize'
+
+ # Create filter
+ filter = ::Nanoc::Filters::ColorizeSyntax.new
+
+ # Get input and expected output
+ input = '<pre title="moo"><code class="language-ruby"># comment</code></pre>'
+ expected_output = '<pre title="moo"><code class="language-ruby"><span class="c1"># comment</span></code></pre>'
+
+ # Run filter
+ actual_output = filter.setup_and_run(input, colorizers: { ruby: :pygmentize })
+ assert_equal(expected_output, actual_output)
+ end
+ end
+
+ def test_colorize_syntax_with_default_colorizer
+ skip_unless_have_command 'pygmentize'
+
+ if_have 'nokogiri' do
+ # Create filter
+ filter = ::Nanoc::Filters::ColorizeSyntax.new
+
+ # Get input and expected output
+ input = '<pre><code class="language-ruby">puts "foo"</code></pre>'
+ expected_output = '<pre><code class="language-ruby"><span class="nb">puts</span> <span class="s2">"foo"</span></code></pre>'
+
+ # Run filter
+ actual_output = filter.setup_and_run(input, default_colorizer: :pygmentize)
+ assert_equal(expected_output, actual_output)
+ end
+ end
+end
diff --git a/test/filters/colorize_syntax/test_pygments.rb b/test/filters/colorize_syntax/test_pygments.rb
new file mode 100644
index 0000000..0f5d9f5
--- /dev/null
+++ b/test/filters/colorize_syntax/test_pygments.rb
@@ -0,0 +1,19 @@
+require 'helper'
+
+class Nanoc::Filters::ColorizeSyntax::PygmentsTest < Nanoc::TestCase
+ def test_pygmentsrb
+ skip 'pygments.rb does not support Windows' if on_windows?
+ if_have 'pygments', 'nokogiri' do
+ # Create filter
+ filter = ::Nanoc::Filters::ColorizeSyntax.new
+
+ # Get input and expected output
+ input = '<pre title="moo"><code class="language-ruby"># comment…</code></pre>'
+ expected_output = '<pre title="moo"><code class="language-ruby"><span class="c1"># comment…</span></code></pre>'
+
+ # Run filter
+ actual_output = filter.setup_and_run(input, colorizers: { ruby: :pygmentsrb })
+ assert_equal(expected_output, actual_output)
+ end
+ end
+end
diff --git a/test/filters/colorize_syntax/test_simon.rb b/test/filters/colorize_syntax/test_simon.rb
new file mode 100644
index 0000000..a9ad0ba
--- /dev/null
+++ b/test/filters/colorize_syntax/test_simon.rb
@@ -0,0 +1,22 @@
+require 'helper'
+
+class Nanoc::Filters::ColorizeSyntax::SimonTest < Nanoc::TestCase
+ def test_simon_highlight
+ if_have 'nokogiri' do
+ skip_unless_have_command 'highlight'
+
+ # Create filter
+ filter = ::Nanoc::Filters::ColorizeSyntax.new
+
+ # Get input and expected output
+ input = %(<pre title="moo"><code class="language-ruby">
+# comment
+</code></pre>)
+ expected_output = '<pre title="moo"><code class="language-ruby"><span class="hl slc"># comment</span></code></pre>'
+
+ # Run filter
+ actual_output = filter.setup_and_run(input, default_colorizer: :simon_highlight)
+ assert_equal(expected_output, actual_output)
+ end
+ end
+end
diff --git a/test/filters/test_asciidoc.rb b/test/filters/test_asciidoc.rb
index 012fe68..1b1859c 100644
--- a/test/filters/test_asciidoc.rb
+++ b/test/filters/test_asciidoc.rb
@@ -1,3 +1,5 @@
+require 'helper'
+
class Nanoc::Filters::AsciiDocTest < Nanoc::TestCase
def test_filter
skip_unless_have_command 'asciidoc'
diff --git a/test/filters/test_bluecloth.rb b/test/filters/test_bluecloth.rb
index 7c804b6..e81fa0f 100644
--- a/test/filters/test_bluecloth.rb
+++ b/test/filters/test_bluecloth.rb
@@ -1,3 +1,5 @@
+require 'helper'
+
class Nanoc::Filters::BlueClothTest < Nanoc::TestCase
def test_filter
if_have 'bluecloth' do
diff --git a/test/filters/test_coffeescript.rb b/test/filters/test_coffeescript.rb
index bf46970..12c982c 100644
--- a/test/filters/test_coffeescript.rb
+++ b/test/filters/test_coffeescript.rb
@@ -1,5 +1,9 @@
+require 'helper'
+
class Nanoc::Filters::CoffeeScriptTest < Nanoc::TestCase
def test_filter
+ skip_v8_on_ruby24
+
if_have 'coffee-script' do
# Create filter
filter = ::Nanoc::Filters::CoffeeScript.new
diff --git a/test/filters/test_erb.rb b/test/filters/test_erb.rb
index 1d4bb05..2405650 100644
--- a/test/filters/test_erb.rb
+++ b/test/filters/test_erb.rb
@@ -1,7 +1,9 @@
+require 'helper'
+
class Nanoc::Filters::ERBTest < Nanoc::TestCase
def test_filter_with_instance_variable
# Create filter
- filter = ::Nanoc::Filters::ERB.new({ location: 'a cheap motel' })
+ filter = ::Nanoc::Filters::ERB.new(location: 'a cheap motel')
# Run filter
result = filter.setup_and_run('<%= "I was hiding in #{@location}." %>')
@@ -10,7 +12,7 @@ class Nanoc::Filters::ERBTest < Nanoc::TestCase
def test_filter_with_instance_method
# Create filter
- filter = ::Nanoc::Filters::ERB.new({ location: 'a cheap motel' })
+ filter = ::Nanoc::Filters::ERB.new(location: 'a cheap motel')
# Run filter
result = filter.setup_and_run('<%= "I was hiding in #{location}." %>')
@@ -45,7 +47,7 @@ class Nanoc::Filters::ERBTest < Nanoc::TestCase
def test_filter_with_yield
# Create filter
- filter = ::Nanoc::Filters::ERB.new({ content: 'a cheap motel' })
+ filter = ::Nanoc::Filters::ERB.new(content: 'a cheap motel')
# Run filter
result = filter.setup_and_run('<%= "I was hiding in #{yield}." %>')
@@ -54,7 +56,7 @@ class Nanoc::Filters::ERBTest < Nanoc::TestCase
def test_filter_with_yield_without_content
# Create filter
- filter = ::Nanoc::Filters::ERB.new({ location: 'a cheap motel' })
+ filter = ::Nanoc::Filters::ERB.new(location: 'a cheap motel')
# Run filter
assert_raises LocalJumpError do
@@ -83,7 +85,7 @@ class Nanoc::Filters::ERBTest < Nanoc::TestCase
def test_trim_mode
# Set up
- filter = ::Nanoc::Filters::ERB.new({ location: 'a cheap motel' })
+ filter = ::Nanoc::Filters::ERB.new(location: 'a cheap motel')
$trim_mode_works = false
# Without
diff --git a/test/filters/test_erubis.rb b/test/filters/test_erubis.rb
index 7fd5780..0c5e726 100644
--- a/test/filters/test_erubis.rb
+++ b/test/filters/test_erubis.rb
@@ -1,8 +1,10 @@
+require 'helper'
+
class Nanoc::Filters::ErubisTest < Nanoc::TestCase
def test_filter_with_instance_variable
if_have 'erubis' do
# Create filter
- filter = ::Nanoc::Filters::Erubis.new({ location: 'a cheap motel' })
+ filter = ::Nanoc::Filters::Erubis.new(location: 'a cheap motel')
# Run filter
result = filter.setup_and_run('<%= "I was hiding in #{@location}." %>')
@@ -13,7 +15,7 @@ class Nanoc::Filters::ErubisTest < Nanoc::TestCase
def test_filter_with_instance_method
if_have 'erubis' do
# Create filter
- filter = ::Nanoc::Filters::Erubis.new({ location: 'a cheap motel' })
+ filter = ::Nanoc::Filters::Erubis.new(location: 'a cheap motel')
# Run filter
result = filter.setup_and_run('<%= "I was hiding in #{location}." %>')
@@ -42,7 +44,7 @@ class Nanoc::Filters::ErubisTest < Nanoc::TestCase
def test_filter_with_yield
if_have 'erubis' do
# Create filter
- filter = ::Nanoc::Filters::Erubis.new({ content: 'a cheap motel' })
+ filter = ::Nanoc::Filters::Erubis.new(content: 'a cheap motel')
# Run filter
result = filter.setup_and_run('<%= "I was hiding in #{yield}." %>')
@@ -53,7 +55,7 @@ class Nanoc::Filters::ErubisTest < Nanoc::TestCase
def test_filter_with_yield_without_content
if_have 'erubis' do
# Create filter
- filter = ::Nanoc::Filters::Erubis.new({ location: 'a cheap motel' })
+ filter = ::Nanoc::Filters::Erubis.new(location: 'a cheap motel')
# Run filter
assert_raises LocalJumpError do
diff --git a/test/filters/test_haml.rb b/test/filters/test_haml.rb
index 1abcc55..f4874b5 100644
--- a/test/filters/test_haml.rb
+++ b/test/filters/test_haml.rb
@@ -1,8 +1,10 @@
+require 'helper'
+
class Nanoc::Filters::HamlTest < Nanoc::TestCase
def test_filter
if_have 'haml' do
# Create filter
- filter = ::Nanoc::Filters::Haml.new({ question: 'Is this the Payne residence?' })
+ filter = ::Nanoc::Filters::Haml.new(question: 'Is this the Payne residence?')
# Run filter (no assigns)
result = filter.setup_and_run('%html')
@@ -21,7 +23,7 @@ class Nanoc::Filters::HamlTest < Nanoc::TestCase
def test_filter_with_params
if_have 'haml' do
# Create filter
- filter = ::Nanoc::Filters::Haml.new({ foo: 'bar' })
+ filter = ::Nanoc::Filters::Haml.new(foo: 'bar')
# Check with HTML5
result = filter.setup_and_run('%img', format: :html5)
@@ -36,7 +38,7 @@ class Nanoc::Filters::HamlTest < Nanoc::TestCase
def test_filter_error
if_have 'haml' do
# Create filter
- filter = ::Nanoc::Filters::Haml.new({ foo: 'bar' })
+ filter = ::Nanoc::Filters::Haml.new(foo: 'bar')
# Run filter
raised = false
@@ -54,7 +56,7 @@ class Nanoc::Filters::HamlTest < Nanoc::TestCase
def test_filter_with_yield
if_have 'haml' do
# Create filter
- filter = ::Nanoc::Filters::Haml.new({ content: 'Is this the Payne residence?' })
+ filter = ::Nanoc::Filters::Haml.new(content: 'Is this the Payne residence?')
# Run filter
result = filter.setup_and_run('%p= yield')
@@ -65,7 +67,7 @@ class Nanoc::Filters::HamlTest < Nanoc::TestCase
def test_filter_with_yield_without_content
if_have 'haml' do
# Create filter
- filter = ::Nanoc::Filters::Haml.new({ location: 'Is this the Payne residence?' })
+ filter = ::Nanoc::Filters::Haml.new(location: 'Is this the Payne residence?')
# Run filter
assert_raises LocalJumpError do
diff --git a/test/filters/test_handlebars.rb b/test/filters/test_handlebars.rb
index d974c9c..5ecdca6 100644
--- a/test/filters/test_handlebars.rb
+++ b/test/filters/test_handlebars.rb
@@ -1,5 +1,9 @@
+require 'helper'
+
class Nanoc::Filters::HandlebarsTest < Nanoc::TestCase
def test_filter
+ skip_v8_on_ruby24
+
if_have 'handlebars' do
# Create data
item = Nanoc::Int::Item.new(
@@ -34,6 +38,8 @@ class Nanoc::Filters::HandlebarsTest < Nanoc::TestCase
end
def test_filter_without_layout
+ skip_v8_on_ruby24
+
if_have 'handlebars' do
# Create data
item = Nanoc::Int::Item.new(
diff --git a/test/filters/test_kramdown.rb b/test/filters/test_kramdown.rb
index 8930604..3f9796f 100644
--- a/test/filters/test_kramdown.rb
+++ b/test/filters/test_kramdown.rb
@@ -1,3 +1,5 @@
+require 'helper'
+
class Nanoc::Filters::KramdownTest < Nanoc::TestCase
def test_filter
if_have 'kramdown' do
@@ -12,15 +14,41 @@ class Nanoc::Filters::KramdownTest < Nanoc::TestCase
def test_warnings
if_have 'kramdown' do
+ # Create item
+ item = Nanoc::Int::Item.new('foo', {}, '/foo.md')
+ item_view = Nanoc::ItemWithRepsView.new(item, nil)
+ item_rep = Nanoc::Int::ItemRep.new(item, :default)
+ item_rep_view = Nanoc::ItemRepView.new(item_rep, nil)
+
# Create filter
- filter = ::Nanoc::Filters::Kramdown.new
+ filter = ::Nanoc::Filters::Kramdown.new(item: item_view, item_rep: item_rep_view)
# Run filter
io = capturing_stdio do
filter.setup_and_run('{:foo}this is bogus')
end
assert_empty io[:stdout]
- assert_equal "kramdown warning: Found span IAL after text - ignoring it\n", io[:stderr]
+ assert_equal "kramdown warning(s) for #{item_rep_view.inspect}\n Found span IAL after text - ignoring it\n", io[:stderr]
+ end
+ end
+
+ def test_warning_filters
+ if_have 'kramdown' do
+ # Create item
+ item = Nanoc::Int::Item.new('foo', {}, '/foo.md')
+ item_view = Nanoc::ItemWithRepsView.new(item, nil)
+ item_rep = Nanoc::Int::ItemRep.new(item, :default)
+ item_rep_view = Nanoc::ItemRepView.new(item_rep, nil)
+
+ # Create filter
+ filter = ::Nanoc::Filters::Kramdown.new(item: item_view, item_rep: item_rep_view)
+
+ # Run filter
+ io = capturing_stdio do
+ filter.setup_and_run("{:foo}this is bogus\n[foo]: http://foo.com\n", warning_filters: 'No link definition')
+ end
+ assert_empty io[:stdout]
+ assert_equal "kramdown warning(s) for #{item_rep_view.inspect}\n Found span IAL after text - ignoring it\n", io[:stderr]
end
end
end
diff --git a/test/filters/test_less.rb b/test/filters/test_less.rb
deleted file mode 100644
index 7e782b4..0000000
--- a/test/filters/test_less.rb
+++ /dev/null
@@ -1,126 +0,0 @@
-class Nanoc::Filters::LessTest < Nanoc::TestCase
- def view_context
- dependency_tracker = Nanoc::Int::DependencyTracker.new(nil)
- Nanoc::ViewContext.new(reps: nil, items: nil, dependency_tracker: dependency_tracker, compiler: nil)
- end
-
- def test_filter
- if_have 'less' do
- # Create item
- @item = Nanoc::ItemWithRepsView.new(Nanoc::Int::Item.new('blah', { content_filename: 'content/foo/bar.txt' }, '/foo/bar/'), view_context)
-
- # Create filter
- filter = ::Nanoc::Filters::Less.new(item: @item, items: [@item])
-
- # Run filter
- result = filter.setup_and_run('.foo { bar: 1 + 1 }')
- assert_match(/\.foo\s*\{\s*bar:\s*2;?\s*\}/, result)
- end
- end
-
- def test_filter_with_paths_relative_to_site_directory
- if_have 'less' do
- # Create file to import
- FileUtils.mkdir_p('content/foo/bar')
- File.open('content/foo/bar/imported_file.less', 'w') { |io| io.write('p { color: red; }') }
-
- # Create item
- @item = Nanoc::ItemWithRepsView.new(Nanoc::Int::Item.new('blah', { content_filename: 'content/foo/bar.txt' }, '/foo/bar/'), view_context)
-
- # Create filter
- filter = ::Nanoc::Filters::Less.new(item: @item, items: [@item])
-
- # Run filter
- result = filter.setup_and_run('@import "content/foo/bar/imported_file.less";')
- assert_match(/p\s*\{\s*color:\s*red;?\s*\}/, result)
- end
- end
-
- def test_filter_with_paths_relative_to_current_file
- if_have 'less' do
- # Create file to import
- FileUtils.mkdir_p('content/foo/bar')
- File.open('content/foo/bar/imported_file.less', 'w') { |io| io.write('p { color: red; }') }
-
- # Create item
- File.open('content/foo/bar.txt', 'w') { |io| io.write('meh') }
- @item = Nanoc::ItemWithRepsView.new(Nanoc::Int::Item.new('blah', { content_filename: 'content/foo/bar.txt' }, '/foo/bar/'), view_context)
-
- # Create filter
- filter = ::Nanoc::Filters::Less.new(item: @item, items: [@item])
-
- # Run filter
- result = filter.setup_and_run('@import "bar/imported_file.less";')
- assert_match(/p\s*\{\s*color:\s*red;?\s*\}/, result)
- end
- end
-
- def test_recompile_includes
- if_have 'less' do
- with_site do |site|
- # Create two less files
- Dir['content/*'].each { |i| FileUtils.rm(i) }
- File.open('content/a.less', 'w') do |io|
- io.write('@import "b.less";')
- end
- File.open('content/b.less', 'w') do |io|
- io.write('p { color: red; }')
- end
-
- # Update rules
- File.open('Rules', 'w') do |io|
- io.write "compile '*' do\n"
- io.write " filter :less\n"
- io.write "end\n"
- io.write "\n"
- io.write "route '/a/' do\n"
- io.write " item.identifier.chop + '.css'\n"
- io.write "end\n"
- io.write "\n"
- io.write "route '/b/' do\n"
- io.write " nil\n"
- io.write "end\n"
- end
-
- # Compile
- site = Nanoc::Int::SiteLoader.new.new_from_cwd
- site.compile
-
- # Check
- assert Dir['output/*'].size == 1
- assert File.file?('output/a.css')
- refute File.file?('output/b.css')
- assert_match(/^p\s*\{\s*color:\s*red;?\s*\}/, File.read('output/a.css'))
-
- # Update included file
- File.open('content/b.less', 'w') do |io|
- io.write('p { color: blue; }')
- end
-
- # Recompile
- site = Nanoc::Int::SiteLoader.new.new_from_cwd
- site.compile
-
- # Recheck
- assert Dir['output/*'].size == 1
- assert File.file?('output/a.css')
- refute File.file?('output/b.css')
- assert_match(/^p\s*\{\s*color:\s*blue;?\s*\}/, File.read('output/a.css'))
- end
- end
- end
-
- def test_compression
- if_have 'less' do
- # Create item
- @item = Nanoc::ItemWithRepsView.new(Nanoc::Int::Item.new('blah', { content_filename: 'content/foo/bar.txt' }, '/foo/bar/'), view_context)
-
- # Create filter
- filter = ::Nanoc::Filters::Less.new(item: @item, items: [@item])
-
- # Run filter with compress option
- result = filter.setup_and_run('.foo { bar: a; } .bar { foo: b; }', compress: true)
- assert_match(/^\.foo\{bar:a\}\n?\.bar\{foo:b\}/, result)
- end
- end
-end
diff --git a/test/filters/test_markaby.rb b/test/filters/test_markaby.rb
index 54d0561..13d1fb5 100644
--- a/test/filters/test_markaby.rb
+++ b/test/filters/test_markaby.rb
@@ -1,3 +1,5 @@
+require 'helper'
+
class Nanoc::Filters::MarkabyTest < Nanoc::TestCase
def test_filter
if_have 'markaby' do
diff --git a/test/filters/test_maruku.rb b/test/filters/test_maruku.rb
index deaeff6..a56eaf6 100644
--- a/test/filters/test_maruku.rb
+++ b/test/filters/test_maruku.rb
@@ -1,3 +1,5 @@
+require 'helper'
+
class Nanoc::Filters::MarukuTest < Nanoc::TestCase
def test_filter
if_have 'maruku' do
diff --git a/test/filters/test_mustache.rb b/test/filters/test_mustache.rb
index e34bbf8..217f420 100644
--- a/test/filters/test_mustache.rb
+++ b/test/filters/test_mustache.rb
@@ -1,3 +1,5 @@
+require 'helper'
+
class Nanoc::Filters::MustacheTest < Nanoc::TestCase
def test_filter
if_have 'mustache' do
@@ -9,7 +11,7 @@ class Nanoc::Filters::MustacheTest < Nanoc::TestCase
)
# Create filter
- filter = ::Nanoc::Filters::Mustache.new({ item: item })
+ filter = ::Nanoc::Filters::Mustache.new(item: item)
# Run filter
result = filter.setup_and_run('The protagonist of {{title}} is {{protagonist}}.')
@@ -28,7 +30,7 @@ class Nanoc::Filters::MustacheTest < Nanoc::TestCase
# Create filter
filter = ::Nanoc::Filters::Mustache.new(
- { content: 'No Payne No Gayne', item: item },
+ content: 'No Payne No Gayne', item: item,
)
# Run filter
diff --git a/test/filters/test_pandoc.rb b/test/filters/test_pandoc.rb
index 427aa3b..a292002 100644
--- a/test/filters/test_pandoc.rb
+++ b/test/filters/test_pandoc.rb
@@ -1,3 +1,5 @@
+require 'helper'
+
class Nanoc::Filters::PandocTest < Nanoc::TestCase
def test_filter
if_have 'pandoc-ruby' do
diff --git a/test/filters/test_rainpress.rb b/test/filters/test_rainpress.rb
index 1e1752f..5445e24 100644
--- a/test/filters/test_rainpress.rb
+++ b/test/filters/test_rainpress.rb
@@ -1,3 +1,5 @@
+require 'helper'
+
class Nanoc::Filters::RainpressTest < Nanoc::TestCase
def test_filter
if_have 'rainpress' do
diff --git a/test/filters/test_rdiscount.rb b/test/filters/test_rdiscount.rb
index 0cfd852..c75dbd6 100644
--- a/test/filters/test_rdiscount.rb
+++ b/test/filters/test_rdiscount.rb
@@ -1,3 +1,5 @@
+require 'helper'
+
class Nanoc::Filters::RDiscountTest < Nanoc::TestCase
def test_filter
if_have 'rdiscount' do
diff --git a/test/filters/test_rdoc.rb b/test/filters/test_rdoc.rb
index 64c9f38..baecf97 100644
--- a/test/filters/test_rdoc.rb
+++ b/test/filters/test_rdoc.rb
@@ -1,3 +1,5 @@
+require 'helper'
+
class Nanoc::Filters::RDocTest < Nanoc::TestCase
def test_filter
# Get filter
diff --git a/test/filters/test_redcarpet.rb b/test/filters/test_redcarpet.rb
index cf1d8ca..68cdf50 100644
--- a/test/filters/test_redcarpet.rb
+++ b/test/filters/test_redcarpet.rb
@@ -1,3 +1,5 @@
+require 'helper'
+
class Nanoc::Filters::RedcarpetTest < Nanoc::TestCase
def test_find
if_have 'redcarpet' do
diff --git a/test/filters/test_redcloth.rb b/test/filters/test_redcloth.rb
index 2269484..5830cde 100644
--- a/test/filters/test_redcloth.rb
+++ b/test/filters/test_redcloth.rb
@@ -1,3 +1,5 @@
+require 'helper'
+
class Nanoc::Filters::RedClothTest < Nanoc::TestCase
def test_filter
if_have 'redcloth' do
diff --git a/test/filters/test_relativize_paths.rb b/test/filters/test_relativize_paths.rb
index 36656e0..928113d 100644
--- a/test/filters/test_relativize_paths.rb
+++ b/test/filters/test_relativize_paths.rb
@@ -1,3 +1,5 @@
+require 'helper'
+
class Nanoc::Filters::RelativizePathsTest < Nanoc::TestCase
def test_filter_html_with_double_quotes
# Create filter with mock item
@@ -347,6 +349,32 @@ EOS
assert_match(/<param (name="movie" )?content="..\/..\/example"/, actual_content)
end
+ def test_filter_form
+ # Create filter with mock item
+ filter = Nanoc::Filters::RelativizePaths.new
+
+ # Mock item
+ filter.instance_eval do
+ @item_rep = Nanoc::Int::ItemRep.new(
+ Nanoc::Int::Item.new(
+ 'content',
+ {},
+ '/foo/bar/baz/',
+ ),
+ :blah,
+ )
+ @item_rep.paths[:last] = '/foo/bar/baz/'
+ end
+
+ # Set content
+ raw_content = %(<form action="/example"></form>)
+ expected_content = %(<form action="../../../example"></form>)
+
+ # Test
+ actual_content = filter.setup_and_run(raw_content, type: :html)
+ assert_equal(expected_content, actual_content)
+ end
+
def test_filter_implicit
# Create filter with mock item
filter = Nanoc::Filters::RelativizePaths.new
diff --git a/test/filters/test_rubypants.rb b/test/filters/test_rubypants.rb
index 37be6d5..23f7f03 100644
--- a/test/filters/test_rubypants.rb
+++ b/test/filters/test_rubypants.rb
@@ -1,3 +1,5 @@
+require 'helper'
+
class Nanoc::Filters::RubyPantsTest < Nanoc::TestCase
def test_filter
if_have 'rubypants' do
diff --git a/test/filters/test_sass.rb b/test/filters/test_sass.rb
index 0808d69..034e889 100644
--- a/test/filters/test_sass.rb
+++ b/test/filters/test_sass.rb
@@ -1,3 +1,5 @@
+require 'helper'
+
class Nanoc::Filters::SassTest < Nanoc::TestCase
def setup
super
@@ -12,7 +14,7 @@ class Nanoc::Filters::SassTest < Nanoc::TestCase
def test_filter
if_have 'sass' do
# Get filter
- filter = create_filter({ foo: 'bar' })
+ filter = create_filter(foo: 'bar')
# Run filter
result = filter.setup_and_run(".foo #bar\n color: #f00")
@@ -23,7 +25,7 @@ class Nanoc::Filters::SassTest < Nanoc::TestCase
def test_filter_with_params
if_have 'sass' do
# Create filter
- filter = create_filter({ foo: 'bar' })
+ filter = create_filter(foo: 'bar')
# Check with compact
result = filter.setup_and_run(".foo #bar\n color: #f00", style: 'compact')
diff --git a/test/filters/test_slim.rb b/test/filters/test_slim.rb
index a86241c..29e4000 100644
--- a/test/filters/test_slim.rb
+++ b/test/filters/test_slim.rb
@@ -1,8 +1,10 @@
+require 'helper'
+
class Nanoc::Filters::SlimTest < Nanoc::TestCase
def test_filter
if_have 'slim' do
# Create filter
- filter = ::Nanoc::Filters::Slim.new({ rabbit: 'The rabbit is on the branch.' })
+ filter = ::Nanoc::Filters::Slim.new(rabbit: 'The rabbit is on the branch.')
# Run filter (no assigns)
result = filter.setup_and_run('html')
@@ -20,7 +22,7 @@ class Nanoc::Filters::SlimTest < Nanoc::TestCase
def test_filter_with_yield
if_have 'slim' do
- filter = ::Nanoc::Filters::Slim.new({ content: 'The rabbit is on the branch.' })
+ filter = ::Nanoc::Filters::Slim.new(content: 'The rabbit is on the branch.')
result = filter.setup_and_run('p = yield')
assert_equal('<p>The rabbit is on the branch.</p>', result)
diff --git a/test/filters/test_typogruby.rb b/test/filters/test_typogruby.rb
index 861b77f..9f5740c 100644
--- a/test/filters/test_typogruby.rb
+++ b/test/filters/test_typogruby.rb
@@ -1,3 +1,5 @@
+require 'helper'
+
class Nanoc::Filters::TypogrubyTest < Nanoc::TestCase
def test_filter
if_have 'typogruby' do
diff --git a/test/filters/test_uglify_js.rb b/test/filters/test_uglify_js.rb
index 107d4a0..1e177db 100644
--- a/test/filters/test_uglify_js.rb
+++ b/test/filters/test_uglify_js.rb
@@ -1,5 +1,9 @@
+require 'helper'
+
class Nanoc::Filters::UglifyJSTest < Nanoc::TestCase
def test_filter
+ skip_v8_on_ruby24
+
if_have 'uglifier' do
# Create filter
filter = ::Nanoc::Filters::UglifyJS.new
@@ -12,6 +16,8 @@ class Nanoc::Filters::UglifyJSTest < Nanoc::TestCase
end
def test_filter_with_options
+ skip_v8_on_ruby24
+
if_have 'uglifier' do
filter = ::Nanoc::Filters::UglifyJS.new
input = "if(donkey) alert('It is a donkey!');"
diff --git a/test/filters/test_xsl.rb b/test/filters/test_xsl.rb
index e87454f..330817f 100644
--- a/test/filters/test_xsl.rb
+++ b/test/filters/test_xsl.rb
@@ -1,3 +1,5 @@
+require 'helper'
+
require 'tempfile'
class Nanoc::Filters::XSLTest < Nanoc::TestCase
@@ -81,13 +83,33 @@ EOS
SAMPLE_XML_OUT_WITH_OMIT_XML_DECL = %r{\A<html>\s*<head>\s*<title>My Report</title>\s*</head>\s*<body>\s*<h1>My Report</h1>\s*</body>\s*</html>\s*\Z}m
+ def setup
+ super
+
+ @dependency_store = Nanoc::Int::DependencyStore.new([])
+ @dependency_tracker = Nanoc::Int::DependencyTracker.new(@dependency_store)
+
+ @base_item = Nanoc::Int::Item.new('base', {}, '/base.md')
+
+ @dependency_tracker.enter(@base_item)
+ end
+
+ def new_view_context
+ Nanoc::ViewContext.new(
+ reps: :__irrelevat_reps,
+ items: :__irrelevat_items,
+ dependency_tracker: @dependency_tracker,
+ compilation_context: :__irrelevat_compiler,
+ )
+ end
+
def test_filter_as_layout
if_have 'nokogiri' do
# Create our data objects
item = Nanoc::Int::Item.new(SAMPLE_XML_IN, {}, '/content/')
- item = Nanoc::ItemWithRepsView.new(item, nil)
+ item = Nanoc::ItemWithRepsView.new(item, new_view_context)
layout = Nanoc::Int::Layout.new(SAMPLE_XSL, {}, '/layout/')
- layout = Nanoc::LayoutView.new(layout, nil)
+ layout = Nanoc::LayoutView.new(layout, new_view_context)
# Create an instance of the filter
assigns = {
@@ -100,6 +122,10 @@ EOS
# Run the filter and validate the results
result = filter.setup_and_run(layout.raw_content)
assert_match SAMPLE_XML_OUT, result
+
+ # Verify dependencies
+ dep = @dependency_store.dependencies_causing_outdatedness_of(@base_item)[0]
+ refute_nil dep
end
end
@@ -107,9 +133,9 @@ EOS
if_have 'nokogiri' do
# Create our data objects
item = Nanoc::Int::Item.new(SAMPLE_XML_IN_WITH_PARAMS, {}, '/content/')
- item = Nanoc::ItemWithRepsView.new(item, nil)
+ item = Nanoc::ItemWithRepsView.new(item, new_view_context)
layout = Nanoc::Int::Layout.new(SAMPLE_XSL_WITH_PARAMS, {}, '/layout/')
- layout = Nanoc::LayoutView.new(layout, nil)
+ layout = Nanoc::LayoutView.new(layout, new_view_context)
# Create an instance of the filter
assigns = {
@@ -122,6 +148,10 @@ EOS
# Run the filter and validate the results
result = filter.setup_and_run(layout.raw_content, foo: 'bar')
assert_match SAMPLE_XML_OUT_WITH_PARAMS, result
+
+ # Verify dependencies
+ dep = @dependency_store.dependencies_causing_outdatedness_of(@base_item)[0]
+ refute_nil dep
end
end
@@ -129,9 +159,9 @@ EOS
if_have 'nokogiri' do
# Create our data objects
item = Nanoc::Int::Item.new(SAMPLE_XML_IN_WITH_OMIT_XML_DECL, {}, '/content/')
- item = Nanoc::ItemWithRepsView.new(item, nil)
+ item = Nanoc::ItemWithRepsView.new(item, new_view_context)
layout = Nanoc::Int::Layout.new(SAMPLE_XSL_WITH_OMIT_XML_DECL, {}, '/layout/')
- layout = Nanoc::LayoutView.new(layout, nil)
+ layout = Nanoc::LayoutView.new(layout, new_view_context)
# Create an instance of the filter
assigns = {
@@ -144,6 +174,10 @@ EOS
# Run the filter and validate the results
result = filter.setup_and_run(layout.raw_content)
assert_match SAMPLE_XML_OUT_WITH_OMIT_XML_DECL, result
+
+ # Verify dependencies
+ dep = @dependency_store.dependencies_causing_outdatedness_of(@base_item)[0]
+ refute_nil dep
end
end
end
diff --git a/test/filters/test_yui_compressor.rb b/test/filters/test_yui_compressor.rb
index 68850ff..14089dc 100644
--- a/test/filters/test_yui_compressor.rb
+++ b/test/filters/test_yui_compressor.rb
@@ -1,3 +1,5 @@
+require 'helper'
+
class Nanoc::Filters::YUICompressorTest < Nanoc::TestCase
def test_filter_javascript
if_have 'yuicompressor' do
@@ -13,10 +15,10 @@ class Nanoc::Filters::YUICompressorTest < Nanoc::TestCase
}
JAVASCRIPT
- result = filter.setup_and_run(sample_js, { type: 'js', munge: true })
+ result = filter.setup_and_run(sample_js, type: 'js', munge: true)
assert_match 'function factorial(c){var a=1;for(var b=2;b<=c;b++){a*=b}return a};', result
- result = filter.setup_and_run(sample_js, { type: 'js', munge: false })
+ result = filter.setup_and_run(sample_js, type: 'js', munge: false)
assert_match 'function factorial(n){var result=1;for(var i=2;i<=n;i++){result*=i}return result};', result
end
end
@@ -31,7 +33,7 @@ class Nanoc::Filters::YUICompressorTest < Nanoc::TestCase
}
CSS
- result = filter.setup_and_run(sample_css, { type: 'css' })
+ result = filter.setup_and_run(sample_css, type: 'css')
assert_match '*{margin:0}', result
end
end
diff --git a/test/fixtures/vcr_cassettes/css_run_error.yml b/test/fixtures/vcr_cassettes/css_run_error.yml
index 6e0cb13..793a154 100644
--- a/test/fixtures/vcr_cassettes/css_run_error.yml
+++ b/test/fixtures/vcr_cassettes/css_run_error.yml
@@ -1,12 +1,17 @@
---
http_interactions:
- request:
- method: get
- uri: http://jigsaw.w3.org/css-validator/validator?output=soap12&profile=css3&text=h1%20%7B%20coxlor:%20rxed%3B%20%7D
+ method: post
+ uri: http://jigsaw.w3.org/css-validator/validator
body:
- encoding: US-ASCII
- string: ''
+ encoding: UTF-8
+ string: "--349832898984244898448024464570528145\r\nContent-Disposition: form-data;
+ name=\"profile\"\r\n\r\ncss3\r\n--349832898984244898448024464570528145\r\nContent-Disposition:
+ form-data; name=\"text\"\r\n\r\nh1 { coxlor: rxed; }\r\n--349832898984244898448024464570528145\r\nContent-Disposition:
+ form-data; name=\"output\"\r\n\r\nsoap12\r\n--349832898984244898448024464570528145--\r\n"
headers:
+ Content-Type:
+ - multipart/form-data; boundary=349832898984244898448024464570528145
Accept-Encoding:
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
Accept:
@@ -21,7 +26,7 @@ http_interactions:
Cache-Control:
- no-cache
Date:
- - Thu, 24 Apr 2014 07:25:20 GMT
+ - Sat, 17 Dec 2016 08:33:23 GMT
Pragma:
- no-cache
Transfer-Encoding:
@@ -42,9 +47,9 @@ http_interactions:
encoding: UTF-8
string: "<?xml version='1.0' encoding=\"utf-8\"?>\n<env:Envelope xmlns:env=\"http://www.w3.org/2003/05/soap-envelope\">\n
\ <env:Body>\n <m:cssvalidationresponse\n env:encodingStyle=\"http://www.w3.org/2003/05/soap-encoding\"\n
- \ xmlns:m=\"http://www.w3.org/2005/07/css-validator\">\n <m:uri>file://localhost/TextArea</m:uri>\n
+ \ xmlns:m=\"http://www.w3.org/2005/07/css-validator\">\n <m:uri>TextArea</m:uri>\n
\ <m:checkedby>http://jigsaw.w3.org/css-validator/</m:checkedby>\n
- \ <m:csslevel>css3</m:csslevel>\n <m:date>2014-04-24T07:25:20Z</m:date>\n
+ \ <m:csslevel>css3</m:csslevel>\n <m:date>2016-12-17T08:33:23Z</m:date>\n
\ <m:validity>false</m:validity>\n <m:result>\n <m:errors
xml:lang=\"en\">\n <m:errorcount>1</m:errorcount>\n \n
\ <m:errorlist>\n <m:uri>file://localhost/TextArea</m:uri>\n
@@ -61,5 +66,5 @@ http_interactions:
\ </m:warnings>\n </m:result>\n </m:cssvalidationresponse>\n
\ </env:Body>\n</env:Envelope>\n\n"
http_version:
- recorded_at: Thu, 24 Apr 2014 07:25:20 GMT
-recorded_with: VCR 2.9.0
+ recorded_at: Sat, 17 Dec 2016 08:33:24 GMT
+recorded_with: VCR 3.0.3
diff --git a/test/fixtures/vcr_cassettes/css_run_ok.yml b/test/fixtures/vcr_cassettes/css_run_ok.yml
index bcad08c..b85e0c1 100644
--- a/test/fixtures/vcr_cassettes/css_run_ok.yml
+++ b/test/fixtures/vcr_cassettes/css_run_ok.yml
@@ -1,12 +1,17 @@
---
http_interactions:
- request:
- method: get
- uri: http://jigsaw.w3.org/css-validator/validator?output=soap12&profile=css3&text=h1%20%7B%20color:%20red%3B%20%7D
+ method: post
+ uri: http://jigsaw.w3.org/css-validator/validator
body:
- encoding: US-ASCII
- string: ''
+ encoding: UTF-8
+ string: "--349832898984244898448024464570528145\r\nContent-Disposition: form-data;
+ name=\"profile\"\r\n\r\ncss3\r\n--349832898984244898448024464570528145\r\nContent-Disposition:
+ form-data; name=\"text\"\r\n\r\nh1 { color: red; }\r\n--349832898984244898448024464570528145\r\nContent-Disposition:
+ form-data; name=\"output\"\r\n\r\nsoap12\r\n--349832898984244898448024464570528145--\r\n"
headers:
+ Content-Type:
+ - multipart/form-data; boundary=349832898984244898448024464570528145
Accept-Encoding:
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
Accept:
@@ -21,7 +26,7 @@ http_interactions:
Cache-Control:
- no-cache
Date:
- - Thu, 24 Apr 2014 07:25:20 GMT
+ - Sat, 17 Dec 2016 08:33:24 GMT
Pragma:
- no-cache
Transfer-Encoding:
@@ -31,7 +36,7 @@ http_interactions:
Content-Type:
- application/soap+xml;charset=utf-8
Server:
- - Jigsaw/2.3.0-beta3
+ - Jigsaw/2.3.0-beta2
Vary:
- Accept-Language
X-W3c-Validator-Errors:
@@ -42,14 +47,14 @@ http_interactions:
encoding: UTF-8
string: "<?xml version='1.0' encoding=\"utf-8\"?>\n<env:Envelope xmlns:env=\"http://www.w3.org/2003/05/soap-envelope\">\n
\ <env:Body>\n <m:cssvalidationresponse\n env:encodingStyle=\"http://www.w3.org/2003/05/soap-encoding\"\n
- \ xmlns:m=\"http://www.w3.org/2005/07/css-validator\">\n <m:uri>file://localhost/TextArea</m:uri>\n
+ \ xmlns:m=\"http://www.w3.org/2005/07/css-validator\">\n <m:uri>TextArea</m:uri>\n
\ <m:checkedby>http://jigsaw.w3.org/css-validator/</m:checkedby>\n
- \ <m:csslevel>css3</m:csslevel>\n <m:date>2014-04-24T07:25:20Z</m:date>\n
+ \ <m:csslevel>css3</m:csslevel>\n <m:date>2016-12-17T08:33:24Z</m:date>\n
\ <m:validity>true</m:validity>\n <m:result>\n <m:errors
xml:lang=\"en\">\n <m:errorcount>0</m:errorcount>\n \n
\ </m:errors>\n <m:warnings xml:lang=\"en\">\n
\ <m:warningcount>0</m:warningcount>\n </m:warnings>\n
\ </m:result>\n </m:cssvalidationresponse>\n </env:Body>\n</env:Envelope>\n\n"
http_version:
- recorded_at: Thu, 24 Apr 2014 07:25:20 GMT
-recorded_with: VCR 2.9.0
+ recorded_at: Sat, 17 Dec 2016 08:33:24 GMT
+recorded_with: VCR 3.0.3
diff --git a/test/fixtures/vcr_cassettes/css_run_parse_error.yml b/test/fixtures/vcr_cassettes/css_run_parse_error.yml
index 83d6134..30d5968 100644
--- a/test/fixtures/vcr_cassettes/css_run_parse_error.yml
+++ b/test/fixtures/vcr_cassettes/css_run_parse_error.yml
@@ -1,12 +1,17 @@
---
http_interactions:
- request:
- method: get
- uri: http://jigsaw.w3.org/css-validator/validator?output=soap12&profile=css3&text=h1%20%7B%20%3B%20%7B
+ method: post
+ uri: http://jigsaw.w3.org/css-validator/validator
body:
- encoding: US-ASCII
- string: ''
+ encoding: UTF-8
+ string: "--349832898984244898448024464570528145\r\nContent-Disposition: form-data;
+ name=\"profile\"\r\n\r\ncss3\r\n--349832898984244898448024464570528145\r\nContent-Disposition:
+ form-data; name=\"text\"\r\n\r\nh1 { ; {\r\n--349832898984244898448024464570528145\r\nContent-Disposition:
+ form-data; name=\"output\"\r\n\r\nsoap12\r\n--349832898984244898448024464570528145--\r\n"
headers:
+ Content-Type:
+ - multipart/form-data; boundary=349832898984244898448024464570528145
Accept-Encoding:
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
Accept:
@@ -21,7 +26,7 @@ http_interactions:
Cache-Control:
- no-cache
Date:
- - Sat, 06 Dec 2014 11:12:00 GMT
+ - Sat, 17 Dec 2016 08:33:24 GMT
Pragma:
- no-cache
Transfer-Encoding:
@@ -31,7 +36,7 @@ http_interactions:
Content-Type:
- application/soap+xml;charset=utf-8
Server:
- - Jigsaw/2.3.0-beta2
+ - Jigsaw/2.3.0-beta3
Vary:
- Accept-Language
X-W3c-Validator-Errors:
@@ -42,9 +47,9 @@ http_interactions:
encoding: UTF-8
string: "<?xml version='1.0' encoding=\"utf-8\"?>\n<env:Envelope xmlns:env=\"http://www.w3.org/2003/05/soap-envelope\">\n
\ <env:Body>\n <m:cssvalidationresponse\n env:encodingStyle=\"http://www.w3.org/2003/05/soap-encoding\"\n
- \ xmlns:m=\"http://www.w3.org/2005/07/css-validator\">\n <m:uri>file://localhost/TextArea</m:uri>\n
+ \ xmlns:m=\"http://www.w3.org/2005/07/css-validator\">\n <m:uri>TextArea</m:uri>\n
\ <m:checkedby>http://jigsaw.w3.org/css-validator/</m:checkedby>\n
- \ <m:csslevel>css3</m:csslevel>\n <m:date>2014-12-06T11:12:00Z</m:date>\n
+ \ <m:csslevel>css3</m:csslevel>\n <m:date>2016-12-17T08:33:24Z</m:date>\n
\ <m:validity>false</m:validity>\n <m:result>\n <m:errors
xml:lang=\"en\">\n <m:errorcount>1</m:errorcount>\n \n
\ <m:errorlist>\n <m:uri>file://localhost/TextArea</m:uri>\n
@@ -61,5 +66,5 @@ http_interactions:
\ </m:warnings>\n </m:result>\n </m:cssvalidationresponse>\n
\ </env:Body>\n</env:Envelope>\n\n"
http_version:
- recorded_at: Sat, 06 Dec 2014 11:12:00 GMT
-recorded_with: VCR 2.9.3
+ recorded_at: Sat, 17 Dec 2016 08:33:24 GMT
+recorded_with: VCR 3.0.3
diff --git a/test/fixtures/vcr_cassettes/html_run_error.yml b/test/fixtures/vcr_cassettes/html_run_error.yml
index ed954c4..faa43d8 100644
--- a/test/fixtures/vcr_cassettes/html_run_error.yml
+++ b/test/fixtures/vcr_cassettes/html_run_error.yml
@@ -2,7 +2,7 @@
http_interactions:
- request:
method: post
- uri: http://validator.w3.org/check
+ uri: https://validator.w3.org/check
body:
encoding: UTF-8
string: "--349832898984244898448024464570528145\r\nContent-Disposition: form-data;
@@ -24,9 +24,9 @@ http_interactions:
message: OK
headers:
Date:
- - Thu, 24 Apr 2014 07:25:20 GMT
+ - Sat, 17 Dec 2016 08:33:18 GMT
Server:
- - Apache/2.2.16 (Debian)
+ - Apache/2.4.10 (Debian)
X-W3c-Validator-Recursion:
- '1'
X-W3c-Validator-Status:
@@ -35,23 +35,38 @@ http_interactions:
- '2'
X-W3c-Validator-Warnings:
- '4'
- Content-Type:
- - application/soap+xml; charset=UTF-8
- Connection:
- - close
Transfer-Encoding:
- chunked
+ Content-Type:
+ - application/soap+xml; charset=UTF-8
+ Strict-Transport-Security:
+ - max-age=15552015; preload
+ Public-Key-Pins:
+ - pin-sha256="cN0QSpPIkuwpT6iP2YjEo1bEwGpH/yiUn6yhdy+HNto="; pin-sha256="WGJkyYjx1QMdMe0UqlyOKXtydPDVrk7sl2fV+nNm1r4=";
+ pin-sha256="LrKdTxZLRTvyHM4/atX2nquX9BeHRZMCxg3cf4rhc2I="; max-age=864000
+ X-Frame-Options:
+ - deny
+ X-Xss-Protection:
+ - 1; mode=block
body:
encoding: UTF-8
string: "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<env:Envelope xmlns:env=\"http://www.w3.org/2003/05/soap-envelope\">\n<env:Body>\n<m:markupvalidationresponse
env:encodingStyle=\"http://www.w3.org/2003/05/soap-encoding\" xmlns:m=\"http://www.w3.org/2005/10/markup-validator\">\n
\ \n <m:uri>output/blah.html</m:uri>\n <m:checkedby>http://validator.w3.org/</m:checkedby>\n
\ <m:doctype></m:doctype>\n <m:charset>utf-8</m:charset>\n <m:validity>false</m:validity>\n
- \ <m:errors>\n <m:errorcount>2</m:errorcount>\n <m:errorlist>\n
- \ \n <m:error>\n <m:line>1</m:line>\n <m:col>1</m:col>\n
- \ <m:message>no document type declaration; will parse without
- validation</m:message>\n <m:messageid>187</m:messageid>\n <m:explanation>
- \ <![CDATA[\n <p class=\"helpwanted\">\n <a\n href=\"feedback.html?uri=;errmsg_id=187#errormsg\"\n\ttitle=\"Suggest
+ \ <m:warnings>\n <m:warningcount>4</m:warningcount>\n <m:warninglist>\n
+ \ \n\n\n\n\n <m:warning><m:messageid>W04</m:messageid><m:message>No
+ Character Encoding Found!\n \n Falling back to \n \n UTF-8.\n
+ \ </m:message></m:warning>\n\n\n\n <m:warning><m:messageid>W06</m:messageid><m:message>Unable
+ to Determine Parse Mode!</m:message></m:warning>\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
+ \ <m:warning><m:messageid>W27</m:messageid><m:message>No Character encoding
+ declared at document level</m:message></m:warning>\n\n\n\n\n\n\n\n </m:warninglist>\n
+ \ </m:warnings>\n <m:errors>\n <m:errorcount>2</m:errorcount>\n
+ \ <m:errorlist>\n\n <m:error>\n <m:line>1</m:line>\n
+ \ <m:col>1</m:col>\n <m:message>no document type
+ declaration; will parse without validation</m:message>\n <m:messageid>187</m:messageid>\n
+ \ <m:explanation> <![CDATA[\n <p class=\"helpwanted\">\n
+ \ <a\n href=\"feedback.html?uri=;errmsg_id=187#errormsg\"\n\ttitle=\"Suggest
improvements on this error message through our feedback channels\" \n >✉</a>\n
\ </p>\n\n <div class=\"ve mid-187\">\n <p>The document type could
not be determined, because the document had no correct DOCTYPE declaration.
@@ -81,14 +96,7 @@ http_interactions:
entry</a>.\n </p>\n </div>\n\n ]]>\n </m:explanation>\n
\ <m:source><![CDATA[<h2>Hi!</h1<strong title=\"Position
where error was detected.\">></strong>]]></m:source>\n </m:error>\n
- \ \n </m:errorlist>\n </m:errors>\n <m:warnings>\n <m:warningcount>4</m:warningcount>\n
- \ <m:warninglist>\n \n\n\n\n <m:warning><m:messageid>W04</m:messageid><m:message>No
- Character Encoding Found!\n \n Falling back to \n \n UTF-8.\n
- \ </m:message></m:warning>\n\n\n\n <m:warning><m:messageid>W06</m:messageid><m:message>Unable
- to Determine Parse Mode!</m:message></m:warning>\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
- \ <m:warning><m:messageid>W27</m:messageid><m:message>No Character encoding
- declared at document level</m:message></m:warning>\n\n\n\n\n\n\n\n \n
- \ </m:warninglist>\n </m:warnings>\n</m:markupvalidationresponse>\n</env:Body>\n</env:Envelope>\n"
+ \ \n </m:errorlist>\n </m:errors>\n</m:markupvalidationresponse>\n</env:Body>\n</env:Envelope>\n"
http_version:
- recorded_at: Thu, 24 Apr 2014 07:25:21 GMT
-recorded_with: VCR 2.9.0
+ recorded_at: Sat, 17 Dec 2016 08:33:19 GMT
+recorded_with: VCR 3.0.3
diff --git a/test/fixtures/vcr_cassettes/html_run_ok.yml b/test/fixtures/vcr_cassettes/html_run_ok.yml
index 45799d8..4728059 100644
--- a/test/fixtures/vcr_cassettes/html_run_ok.yml
+++ b/test/fixtures/vcr_cassettes/html_run_ok.yml
@@ -2,7 +2,7 @@
http_interactions:
- request:
method: post
- uri: http://validator.w3.org/check
+ uri: https://validator.w3.org/check
body:
encoding: UTF-8
string: "--349832898984244898448024464570528145\r\nContent-Disposition: form-data;
@@ -20,37 +20,105 @@ http_interactions:
- Ruby
response:
status:
- code: 200
- message: OK
+ code: 307
+ message: Temporary Redirect
headers:
Date:
- - Thu, 24 Apr 2014 07:25:21 GMT
+ - Sat, 17 Dec 2016 08:36:04 GMT
Server:
- - Apache/2.2.16 (Debian)
- X-W3c-Validator-Recursion:
- - '1'
- X-W3c-Validator-Status:
- - Valid
- X-W3c-Validator-Errors:
- - '0'
- X-W3c-Validator-Warnings:
- - '1'
- Content-Type:
- - application/soap+xml; charset=UTF-8
- Connection:
- - close
+ - Apache/2.4.10 (Debian)
+ Location:
+ - https://validator.w3.org/nu/#file
Transfer-Encoding:
- chunked
+ Strict-Transport-Security:
+ - max-age=15552015; preload
+ Public-Key-Pins:
+ - pin-sha256="cN0QSpPIkuwpT6iP2YjEo1bEwGpH/yiUn6yhdy+HNto="; pin-sha256="WGJkyYjx1QMdMe0UqlyOKXtydPDVrk7sl2fV+nNm1r4=";
+ pin-sha256="LrKdTxZLRTvyHM4/atX2nquX9BeHRZMCxg3cf4rhc2I="; max-age=864000
+ X-Frame-Options:
+ - deny
+ X-Xss-Protection:
+ - 1; mode=block
body:
encoding: UTF-8
- string: "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<env:Envelope xmlns:env=\"http://www.w3.org/2003/05/soap-envelope\">\n<env:Body>\n<m:markupvalidationresponse
+ string: "Status: 302 Found\r\nLocation: https://validator.w3.org/nu/?doc=output%2Fblah.html\r\n\r\nContent-Type:
+ application/soap+xml; charset=UTF-8\nX-W3C-Validator-Recursion: 1\nX-W3C-Validator-Status:
+ Valid\nX-W3C-Validator-Errors: 0\nX-W3C-Validator-Warnings: 1\n\n<?xml version=\"1.0\"
+ encoding=\"UTF-8\"?>\n<env:Envelope xmlns:env=\"http://www.w3.org/2003/05/soap-envelope\">\n<env:Body>\n<m:markupvalidationresponse
env:encodingStyle=\"http://www.w3.org/2003/05/soap-encoding\" xmlns:m=\"http://www.w3.org/2005/10/markup-validator\">\n
\ \n <m:uri>output/blah.html</m:uri>\n <m:checkedby>http://validator.w3.org/</m:checkedby>\n
\ <m:doctype>HTML5</m:doctype>\n <m:charset>utf-8</m:charset>\n <m:validity>true</m:validity>\n
- \ <m:errors>\n <m:errorcount>0</m:errorcount>\n <m:errorlist>\n
- \ \n </m:errorlist>\n </m:errors>\n <m:warnings>\n <m:warningcount>1</m:warningcount>\n
- \ <m:warninglist>\n \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
- \ \n </m:warninglist>\n </m:warnings>\n</m:markupvalidationresponse>\n</env:Body>\n</env:Envelope>\n"
+ \ <m:warnings>\n <m:warningcount>1</m:warningcount>\n <m:warninglist>\n
+ \ <m:warning>\n <m:message>This interface to HTML5
+ document checking is obsolete. Use an interface to https://validator.w3.org/nu/
+ instead.</m:message>\n <m:messageid>obsolete-interface</m:messageid>\n
+ \ <m:explanation> <![CDATA[\n <div class=\"ve
+ obsolete-interface\">\n <p>\n The
+ method you used to check this document relies on an obsolete interface\n that
+ will become permanently unavailable in the near future.\n The
+ currently-supported method is to instead use the W3C Nu Html Checker at\n
+ \ <a href=\"https://validator.w3.org/nu/\">https://validator.w3.org/nu/</a>.\n
+ \ See the documentation on the W3C Nu Html Checker's\n
+ \ <a href=\"https://github.com/validator/validator/wiki/Service:-Input:-GET\">GET
+ interface</a>,\n <a href=\"https://github.com/validator/validator/wiki/Service:-Input:-POST-body\">POST
+ interface</a>,\n and\n <a href=\"https://github.com/validator/validator/wiki/Service:-Common-parameters\">the
+ parameters</a>\n that both interfaces provide, such
+ as the\n <a href=\"https://github.com/validator/validator/wiki/Output%3A-JSON\">out=json</a>,\n
+ \ <a href=\"https://github.com/validator/validator/wiki/Output:-XML\">out=xml</a>,\n
+ \ and\n <a href=\"https://github.com/validator/validator/wiki/Output:-GNU\">out=gnu</a>\n
+ \ parameters/output-formats.\n </p>\n
+ \ <p>\n For comments or questions
+ about this message, send mail to\n <a href=\"mailto:www-validator at w3.org\">www-validator at w3.org</a>\n
+ \ or to\n <a href=\"https://twitter.com/w3c\">@w3c</a>
+ on twitter.\n </p>\n </div>\n ]]>\n
+ \ </m:explanation>\n <m:source></m:source>\n
+ \ </m:warning>\n </m:warninglist>\n </m:warnings>\n <m:errors>\n
+ \ <m:errorcount>0</m:errorcount>\n <m:errorlist>\n\n </m:errorlist>\n
+ \ </m:errors>\n</m:markupvalidationresponse>\n</env:Body>\n</env:Envelope>\n"
+ http_version:
+ recorded_at: Sat, 17 Dec 2016 08:36:05 GMT
+- request:
+ method: post
+ uri: https://validator.w3.org/check
+ body:
+ encoding: UTF-8
+ string: "--349832898984244898448024464570528145\r\nContent-Disposition: form-data;
+ name=\"url\"\r\n\r\nhttps://validator.w3.org/nu/#file\r\n--349832898984244898448024464570528145--\r\n"
+ headers:
+ Content-Type:
+ - multipart/form-data; boundary=349832898984244898448024464570528145
+ Accept-Encoding:
+ - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
+ Accept:
+ - "*/*"
+ User-Agent:
+ - Ruby
+ response:
+ status:
+ code: 302
+ message: Found
+ headers:
+ Date:
+ - Sat, 17 Dec 2016 08:36:05 GMT
+ Server:
+ - Apache/2.4.10 (Debian)
+ Location:
+ - http://validator.w3.org/check?uri=https%3A%2F%2Fvalidator.w3.org%2Fnu%2F%23file
+ Content-Length:
+ - '0'
+ Strict-Transport-Security:
+ - max-age=15552015; preload
+ Public-Key-Pins:
+ - pin-sha256="cN0QSpPIkuwpT6iP2YjEo1bEwGpH/yiUn6yhdy+HNto="; pin-sha256="WGJkyYjx1QMdMe0UqlyOKXtydPDVrk7sl2fV+nNm1r4=";
+ pin-sha256="LrKdTxZLRTvyHM4/atX2nquX9BeHRZMCxg3cf4rhc2I="; max-age=864000
+ X-Frame-Options:
+ - deny
+ X-Xss-Protection:
+ - 1; mode=block
+ body:
+ encoding: UTF-8
+ string: ''
http_version:
- recorded_at: Thu, 24 Apr 2014 07:25:21 GMT
-recorded_with: VCR 2.9.0
+ recorded_at: Sat, 17 Dec 2016 08:36:06 GMT
+recorded_with: VCR 3.0.3
diff --git a/test/helper.rb b/test/helper.rb
index 7725104..d60aa5c 100644
--- a/test/helper.rb
+++ b/test/helper.rb
@@ -28,6 +28,12 @@ module Nanoc::TestHelpers
ENV.key?('DISABLE_NOKOGIRI')
end
+ def skip_v8_on_ruby24
+ if ENV.key?('DISABLE_V8')
+ skip 'V8 specs are disabled (broken on Ruby 2.4)'
+ end
+ end
+
def if_have(*libs)
libs.each do |lib|
if defined?(RUBY_ENGINE) && RUBY_ENGINE == 'jruby' && lib == 'nokogiri' && disable_nokogiri?
diff --git a/test/helpers/test_blogging.rb b/test/helpers/test_blogging.rb
index b8eedd4..0c07a4c 100644
--- a/test/helpers/test_blogging.rb
+++ b/test/helpers/test_blogging.rb
@@ -1,3 +1,5 @@
+require 'helper'
+
class Nanoc::Helpers::BloggingTest < Nanoc::TestCase
include Nanoc::Helpers::Blogging
include Nanoc::Helpers::Text
@@ -93,6 +95,68 @@ class Nanoc::Helpers::BloggingTest < Nanoc::TestCase
end
end
+ def test_atom_feed_updated_is_most_recent
+ if_have 'builder' do
+ # Create items
+ @items = [mock_item, mock_article, mock_article]
+
+ # Create item 1
+ @items[1].stubs(:[]).with(:updated_at).returns(nil)
+ @items[1].stubs(:[]).with(:created_at).returns(Time.parse('2016-12-01 17:20:00 +00:00'))
+ @items[1].expects(:compiled_content).returns('item 1 content')
+
+ # Create item 2
+ @items[2].stubs(:[]).with(:updated_at).returns(nil)
+ @items[2].stubs(:[]).with(:created_at).returns(Time.parse('2016-12-01 18:40:00 +00:00'))
+ @items[2].expects(:compiled_content).returns('item 2 content')
+
+ # Mock site
+ @config = Nanoc::ConfigView.new({ base_url: 'http://example.com' }, nil)
+
+ # Create feed item
+ @item = mock
+ @item.stubs(:[]).with(:title).returns('My Cool Blog')
+ @item.stubs(:[]).with(:author_name).returns('Denis Defreyne')
+ @item.stubs(:[]).with(:author_uri).returns('http://stoneship.org/')
+ @item.stubs(:[]).with(:feed_url).returns(nil)
+ @item.stubs(:path).returns('/journal/feed/')
+
+ # Check
+ assert_match(%r{<title>My Cool Blog</title>\n <updated>2016-12-01T18:40:00Z</updated>}, atom_feed)
+ end
+ end
+
+ def test_atom_feed_updated_is_most_recent_updated_at
+ if_have 'builder' do
+ # Create items
+ @items = [mock_item, mock_article, mock_article]
+
+ # Create item 1
+ @items[1].stubs(:[]).with(:updated_at).returns(Time.parse('2016-12-01 19:20:00 +00:00'))
+ @items[1].stubs(:[]).with(:created_at).returns(Time.parse('2016-12-01 17:20:00 +00:00'))
+ @items[1].expects(:compiled_content).returns('item 1 content')
+
+ # Create item 2
+ @items[2].stubs(:[]).with(:updated_at).returns(Time.parse('2016-12-01 20:40:00 +00:00'))
+ @items[2].stubs(:[]).with(:created_at).returns(Time.parse('2016-12-01 18:40:00 +00:00'))
+ @items[2].expects(:compiled_content).returns('item 2 content')
+
+ # Mock site
+ @config = Nanoc::ConfigView.new({ base_url: 'http://example.com' }, nil)
+
+ # Create feed item
+ @item = mock
+ @item.stubs(:[]).with(:title).returns('My Cool Blog')
+ @item.stubs(:[]).with(:author_name).returns('Denis Defreyne')
+ @item.stubs(:[]).with(:author_uri).returns('http://stoneship.org/')
+ @item.stubs(:[]).with(:feed_url).returns(nil)
+ @item.stubs(:path).returns('/journal/feed/')
+
+ # Check
+ assert_match(%r{<title>My Cool Blog</title>\n <updated>2016-12-01T20:40:00Z</updated>}, atom_feed)
+ end
+ end
+
def test_atom_feed_without_articles
if_have 'builder' do
# Mock items
diff --git a/test/helpers/test_capturing.rb b/test/helpers/test_capturing.rb
index b11926a..1b4fba8 100644
--- a/test/helpers/test_capturing.rb
+++ b/test/helpers/test_capturing.rb
@@ -1,3 +1,5 @@
+require 'helper'
+
class Nanoc::Helpers::CapturingTest < Nanoc::TestCase
include Nanoc::Helpers::Capturing
@@ -12,10 +14,15 @@ class Nanoc::Helpers::CapturingTest < Nanoc::TestCase
reps: item_rep_repo_for(item),
items: :__irrelevant__,
dependency_tracker: :__irrelevant__,
- compiler: :__irrelevant__,
+ compilation_context: :__irrelevant__,
)
end
+ def before
+ super
+ Nanoc::CLI::ErrorHandler.enable
+ end
+
def test_dependencies
with_site do |_site|
# Prepare
diff --git a/test/helpers/test_link_to.rb b/test/helpers/test_link_to.rb
index a54aabe..e75ac46 100644
--- a/test/helpers/test_link_to.rb
+++ b/test/helpers/test_link_to.rb
@@ -1,3 +1,5 @@
+require 'helper'
+
class Nanoc::Helpers::LinkToTest < Nanoc::TestCase
include Nanoc::Helpers::LinkTo
diff --git a/test/helpers/test_xml_sitemap.rb b/test/helpers/test_xml_sitemap.rb
index b4f27d9..e58414f 100644
--- a/test/helpers/test_xml_sitemap.rb
+++ b/test/helpers/test_xml_sitemap.rb
@@ -1,3 +1,5 @@
+require 'helper'
+
class Nanoc::Helpers::XMLSitemapTest < Nanoc::TestCase
include Nanoc::Helpers::XMLSitemap
@@ -6,7 +8,7 @@ class Nanoc::Helpers::XMLSitemapTest < Nanoc::TestCase
@reps = Nanoc::Int::ItemRepRepo.new
dependency_tracker = Nanoc::Int::DependencyTracker.new(nil)
- @view_context = Nanoc::ViewContext.new(reps: @reps, items: nil, dependency_tracker: dependency_tracker, compiler: :__irrelevant__)
+ @view_context = Nanoc::ViewContext.new(reps: @reps, items: nil, dependency_tracker: dependency_tracker, compilation_context: :__irrelevant__)
@items = nil
@item = nil
@@ -30,7 +32,7 @@ class Nanoc::Helpers::XMLSitemapTest < Nanoc::TestCase
@items << item
# Create item 3
- attrs = { mtime: Time.parse('2004-07-12'), changefreq: 'daily', priority: 0.5 }
+ attrs = { mtime: Time.parse('2004-07-12 00:00:00 +02:00'), changefreq: 'daily', priority: 0.5 }
item = Nanoc::ItemWithRepsView.new(Nanoc::Int::Item.new('some content 3', attrs, '/item-three/'), @view_context)
@items << item
create_item_rep(item.unwrap, :three_a, '/item-three/a/')
@@ -70,8 +72,8 @@ class Nanoc::Helpers::XMLSitemapTest < Nanoc::TestCase
assert_equal '0.5', urls[3].css('> priority').inner_text
assert_equal '', urls[0].css('> lastmod').inner_text
assert_equal '', urls[1].css('> lastmod').inner_text
- assert_equal '2004-07-12', urls[2].css('> lastmod').inner_text
- assert_equal '2004-07-12', urls[3].css('> lastmod').inner_text
+ assert_equal '2004-07-11', urls[2].css('> lastmod').inner_text
+ assert_equal '2004-07-11', urls[3].css('> lastmod').inner_text
end
end
diff --git a/test/rule_dsl/test_action_provider.rb b/test/rule_dsl/test_action_provider.rb
index 5bcdff2..733f1d8 100644
--- a/test/rule_dsl/test_action_provider.rb
+++ b/test/rule_dsl/test_action_provider.rb
@@ -1,3 +1,5 @@
+require 'helper'
+
class Nanoc::RuleDSL::ActionProviderTest < Nanoc::TestCase
def new_action_provider(site)
rules_collection = Nanoc::RuleDSL::RulesCollection.new
diff --git a/test/rule_dsl/test_compiler_dsl.rb b/test/rule_dsl/test_compiler_dsl.rb
index 372e06e..07316b6 100644
--- a/test/rule_dsl/test_compiler_dsl.rb
+++ b/test/rule_dsl/test_compiler_dsl.rb
@@ -1,3 +1,5 @@
+require 'helper'
+
class Nanoc::RuleDSL::CompilerDSLTest < Nanoc::TestCase
def test_compile
# TODO: implement
@@ -290,7 +292,7 @@ EOS
end
def test_create_pattern_with_string_with_glob_string_pattern_type
- compiler_dsl = Nanoc::RuleDSL::CompilerDSL.new(nil, { string_pattern_type: 'glob' })
+ compiler_dsl = Nanoc::RuleDSL::CompilerDSL.new(nil, string_pattern_type: 'glob')
pattern = compiler_dsl.create_pattern('/foo/*')
assert pattern.match?('/foo/aaaa')
@@ -299,14 +301,14 @@ EOS
end
def test_create_pattern_with_regex
- compiler_dsl = Nanoc::RuleDSL::CompilerDSL.new(nil, { string_pattern_type: 'glob' })
+ compiler_dsl = Nanoc::RuleDSL::CompilerDSL.new(nil, string_pattern_type: 'glob')
pattern = compiler_dsl.create_pattern(%r{\A/foo/a*/})
assert pattern.match?('/foo/aaaa/')
end
def test_create_pattern_with_string_with_unknown_string_pattern_type
- compiler_dsl = Nanoc::RuleDSL::CompilerDSL.new(nil, { string_pattern_type: 'donkey' })
+ compiler_dsl = Nanoc::RuleDSL::CompilerDSL.new(nil, string_pattern_type: 'donkey')
err = assert_raises(Nanoc::Int::Errors::GenericTrivial) do
compiler_dsl.create_pattern('/foo/*')
@@ -319,7 +321,7 @@ EOS
compiler_dsl = Nanoc::RuleDSL::CompilerDSL.new(nil, {})
actual = compiler_dsl.instance_eval { identifier_to_regex('foo') }
- expected = %r{^/foo/$}
+ expected = %r{^/foo/?$}
assert_equal(expected.to_s, actual.to_s)
assert_equal(expected.source, actual.source)
@@ -333,7 +335,7 @@ EOS
compiler_dsl = Nanoc::RuleDSL::CompilerDSL.new(nil, {})
actual = compiler_dsl.instance_eval { identifier_to_regex('foo/*/bar') }
- expected = %r{^/foo/(.*?)/bar/$}
+ expected = %r{^/foo/(.*?)/bar/?$}
assert_equal(expected.to_s, actual.to_s)
assert_equal(expected.source, actual.source)
@@ -347,7 +349,7 @@ EOS
compiler_dsl = Nanoc::RuleDSL::CompilerDSL.new(nil, {})
actual = compiler_dsl.instance_eval { identifier_to_regex('foo/*/bar/*/qux') }
- expected = %r{^/foo/(.*?)/bar/(.*?)/qux/$}
+ expected = %r{^/foo/(.*?)/bar/(.*?)/qux/?$}
assert_equal(expected.to_s, actual.to_s)
assert_equal(expected.source, actual.source)
@@ -403,7 +405,7 @@ EOS
compiler_dsl = Nanoc::RuleDSL::CompilerDSL.new(nil, {})
actual = compiler_dsl.instance_eval { identifier_to_regex('/foo/+') }
- expected = %r{^/foo/(.+?)/$}
+ expected = %r{^/foo/(.+?)/?$}
assert_equal(expected.to_s, actual.to_s)
assert_equal(expected.source, actual.source)
@@ -414,6 +416,20 @@ EOS
refute('/foo/' =~ actual)
end
+ def test_identifier_to_regex_with_full_identifier
+ # Create compiler DSL
+ compiler_dsl = Nanoc::RuleDSL::CompilerDSL.new(nil, {})
+
+ actual = compiler_dsl.instance_eval { identifier_to_regex('/favicon.ico') }
+ expected = %r{^/favicon\.ico/?$}
+
+ assert_equal(expected.to_s, actual.to_s)
+
+ assert('/favicon.ico' =~ actual)
+ assert('/favicon.ico/' =~ actual)
+ refute('/faviconxico' =~ actual)
+ end
+
def test_dsl_has_no_access_to_compiler
compiler_dsl = Nanoc::RuleDSL::CompilerDSL.new(nil, {})
assert_raises(NameError) do
@@ -423,7 +439,7 @@ EOS
def test_config
$venetian = 'unsnares'
- compiler_dsl = Nanoc::RuleDSL::CompilerDSL.new(nil, { venetian: 'snares' })
+ compiler_dsl = Nanoc::RuleDSL::CompilerDSL.new(nil, venetian: 'snares')
compiler_dsl.instance_eval { $venetian = @config[:venetian] }
assert_equal 'snares', $venetian
end
diff --git a/test/rule_dsl/test_rule.rb b/test/rule_dsl/test_rule.rb
index 4405490..2541fb3 100644
--- a/test/rule_dsl/test_rule.rb
+++ b/test/rule_dsl/test_rule.rb
@@ -1,3 +1,5 @@
+require 'helper'
+
class Nanoc::Int::RuleTest < Nanoc::TestCase
def test_initialize
# TODO: implement
diff --git a/test/rule_dsl/test_rules_collection.rb b/test/rule_dsl/test_rules_collection.rb
index f885041..4512b34 100644
--- a/test/rule_dsl/test_rules_collection.rb
+++ b/test/rule_dsl/test_rules_collection.rb
@@ -1,3 +1,5 @@
+require 'helper'
+
class Nanoc::RuleDSL::RulesCollectionTest < Nanoc::TestCase
def test_compilation_rule_for
# Mock rules
diff --git a/test/test_gem.rb b/test/test_gem.rb
index 4ddad45..5f593d5 100644
--- a/test/test_gem.rb
+++ b/test/test_gem.rb
@@ -1,3 +1,5 @@
+require 'helper'
+
class Nanoc::GemTest < Nanoc::TestCase
def setup
super
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-ruby-extras/nanoc.git
More information about the Pkg-ruby-extras-commits
mailing list