├── .gitignore ├── bin └── nanoc ├── lib ├── nanoc │ ├── version.rb │ ├── extra │ │ ├── core_ext.rb │ │ ├── checking.rb │ │ ├── checking │ │ │ ├── issue.rb │ │ │ ├── dsl.rb │ │ │ ├── checks │ │ │ │ ├── stale.rb │ │ │ │ ├── css.rb │ │ │ │ ├── html.rb │ │ │ │ ├── mixed_content.rb │ │ │ │ └── internal_links.rb │ │ │ ├── checks.rb │ │ │ └── check.rb │ │ ├── deployers.rb │ │ ├── core_ext │ │ │ ├── time.rb │ │ │ └── pathname.rb │ │ ├── piper.rb │ │ ├── jruby_nokogiri_warner.rb │ │ ├── deployer.rb │ │ ├── deployers │ │ │ └── rsync.rb │ │ ├── link_collector.rb │ │ └── pruner.rb │ ├── base │ │ ├── error.rb │ │ ├── core_ext.rb │ │ ├── views │ │ │ ├── item_collection.rb │ │ │ ├── layout_collection.rb │ │ │ ├── site.rb │ │ │ ├── mutable_config.rb │ │ │ ├── mutable_layout.rb │ │ │ ├── mutable_identifiable_collection.rb │ │ │ ├── mutable_item.rb │ │ │ ├── mutable_layout_collection.rb │ │ │ ├── config.rb │ │ │ ├── layout.rb │ │ │ ├── mutable_item_collection.rb │ │ │ ├── item_rep_collection.rb │ │ │ ├── identifiable_collection.rb │ │ │ └── item_rep.rb │ │ ├── core_ext │ │ │ ├── pathname.rb │ │ │ ├── string.rb │ │ │ ├── array.rb │ │ │ └── hash.rb │ │ ├── source_data │ │ │ ├── configuration.rb │ │ │ └── code_snippet.rb │ │ ├── compilation │ │ │ ├── rule_memory_calculator.rb │ │ │ ├── rule_memory_store.rb │ │ │ ├── checksum_store.rb │ │ │ ├── outdatedness_reasons.rb │ │ │ └── compiled_content_cache.rb │ │ ├── pattern.rb │ │ ├── temp_filename_factory.rb │ │ ├── context.rb │ │ ├── identifiable_collection.rb │ │ ├── memoization.rb │ │ └── checksummer.rb │ ├── cli │ │ ├── stream_cleaners.rb │ │ ├── stream_cleaners │ │ │ ├── ansi_colors.rb │ │ │ ├── utf8.rb │ │ │ └── abstract.rb │ │ ├── ansi_string_colorizer.rb │ │ ├── commands │ │ │ ├── shell.rb │ │ │ ├── nanoc.rb │ │ │ ├── check.rb │ │ │ ├── prune.rb │ │ │ ├── show-rules.rb │ │ │ └── view.rb │ │ ├── logger.rb │ │ └── command_runner.rb │ ├── data_sources.rb │ ├── filters │ │ ├── bluecloth.rb │ │ ├── rubypants.rb │ │ ├── markaby.rb │ │ ├── rainpress.rb │ │ ├── maruku.rb │ │ ├── typogruby.rb │ │ ├── coffeescript.rb │ │ ├── mustache.rb │ │ ├── rdiscount.rb │ │ ├── asciidoc.rb │ │ ├── uglify_js.rb │ │ ├── kramdown.rb │ │ ├── rdoc.rb │ │ ├── sass │ │ │ └── sass_filesystem_importer.rb │ │ ├── yui_compressor.rb │ │ ├── haml.rb │ │ ├── slim.rb │ │ ├── sass.rb │ │ ├── handlebars.rb │ │ ├── erubis.rb │ │ ├── erb.rb │ │ ├── pandoc.rb │ │ ├── xsl.rb │ │ ├── redcloth.rb │ │ └── less.rb │ ├── extra.rb │ ├── helpers.rb │ └── helpers │ │ ├── breadcrumbs.rb │ │ ├── text.rb │ │ ├── html_escape.rb │ │ ├── filtering.rb │ │ └── tagging.rb └── nanoc.rb ├── test ├── cli │ ├── test_logger.rb │ ├── commands │ │ ├── test_info.rb │ │ ├── test_help.rb │ │ └── test_check.rb │ ├── test_cleaning_stream.rb │ └── test_error_handler.rb ├── gem_loader.rb ├── filters │ ├── test_rdoc.rb │ ├── test_markaby.rb │ ├── test_rubypants.rb │ ├── test_asciidoc.rb │ ├── test_bluecloth.rb │ ├── test_maruku.rb │ ├── test_coffeescript.rb │ ├── test_typogruby.rb │ ├── test_rainpress.rb │ ├── test_kramdown.rb │ ├── test_rdiscount.rb │ ├── test_redcloth.rb │ ├── test_uglify_js.rb │ ├── test_slim.rb │ ├── test_mustache.rb │ ├── test_yui_compressor.rb │ ├── test_pandoc.rb │ ├── test_handlebars.rb │ └── test_erubis.rb ├── extra │ ├── core_ext │ │ ├── test_time.rb │ │ └── test_pathname.rb │ ├── checking │ │ ├── test_dsl.rb │ │ ├── test_check.rb │ │ ├── test_runner.rb │ │ └── checks │ │ │ ├── test_html.rb │ │ │ ├── test_css.rb │ │ │ └── test_stale.rb │ ├── test_piper.rb │ └── deployers │ │ └── test_rsync.rb ├── base │ ├── test_item_rep_recorder_proxy.rb │ ├── test_rule.rb │ ├── test_plugin.rb │ ├── test_code_snippet.rb │ ├── test_context.rb │ ├── test_checksum_store.rb │ ├── test_notification_center.rb │ ├── test_store.rb │ ├── core_ext │ │ ├── string_spec.rb │ │ ├── pathname_spec.rb │ │ ├── array_spec.rb │ │ └── hash_spec.rb │ ├── test_layout.rb │ ├── test_memoization.rb │ ├── test_data_source.rb │ ├── test_rule_context.rb │ ├── temp_filename_factory_spec.rb │ ├── test_filter.rb │ └── test_item_array.rb ├── helpers │ ├── test_html_escape.rb │ ├── test_text.rb │ ├── test_breadcrumbs.rb │ └── test_tagging.rb ├── test_gem.rb └── fixtures │ └── vcr_cassettes │ ├── css_run_ok.yml │ └── html_run_ok.yml ├── ChangeLog ├── spec ├── nanoc │ ├── base │ │ ├── views │ │ │ ├── item_collection_spec.rb │ │ │ ├── layout_collection_spec.rb │ │ │ ├── mutable_config_spec.rb │ │ │ ├── mutable_layout_spec.rb │ │ │ ├── mutable_item_spec.rb │ │ │ ├── mutable_identifiable_collection_spec.rb │ │ │ ├── mutable_item_collection_spec.rb │ │ │ ├── mutable_layout_collection_spec.rb │ │ │ ├── item_rep_collection_spec.rb │ │ │ ├── layout_spec.rb │ │ │ └── config_spec.rb │ │ └── pattern_spec.rb │ └── cli │ │ └── commands │ │ └── shell_spec.rb └── spec_helper.rb ├── Rakefile ├── tasks ├── rubocop.rake ├── doc.rake └── test.rake ├── .travis.yml ├── doc ├── yardoc_templates │ └── default │ │ └── layout │ │ └── html │ │ └── footer.erb └── yardoc_handlers │ └── identifier.rb ├── CONTRIBUTING.md ├── LICENSE ├── Gemfile ├── nanoc.gemspec └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | doc/yardoc 2 | .DS_Store 3 | *.gem 4 | /coverage/ 5 | /.yardoc 6 | *~ 7 | /Gemfile.lock 8 | -------------------------------------------------------------------------------- /bin/nanoc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'nanoc' 3 | require 'nanoc/cli' 4 | 5 | Nanoc::CLI.run(ARGV) 6 | -------------------------------------------------------------------------------- /lib/nanoc/version.rb: -------------------------------------------------------------------------------- 1 | module Nanoc 2 | # The current nanoc version. 3 | VERSION = '4.0.0b4' 4 | end 5 | -------------------------------------------------------------------------------- /lib/nanoc/extra/core_ext.rb: -------------------------------------------------------------------------------- 1 | require 'nanoc/extra/core_ext/pathname' 2 | require 'nanoc/extra/core_ext/time' 3 | -------------------------------------------------------------------------------- /test/cli/test_logger.rb: -------------------------------------------------------------------------------- 1 | class Nanoc::CLI::LoggerTest < Nanoc::TestCase 2 | def test_stub 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /lib/nanoc/base/error.rb: -------------------------------------------------------------------------------- 1 | module Nanoc 2 | # Generic error. Superclass for all nanoc-specific errors. 3 | class Error < ::StandardError 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/cli/commands/test_info.rb: -------------------------------------------------------------------------------- 1 | class Nanoc::CLI::Commands::InfoTest < Nanoc::TestCase 2 | def test_run 3 | Nanoc::CLI.run %w( info ) 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /ChangeLog: -------------------------------------------------------------------------------- 1 | For a list of all changes, please see the changelog on the project repository 2 | instead (https://github.com/nanoc/nanoc). For release notes, please see the 3 | NEWS file. 4 | -------------------------------------------------------------------------------- /lib/nanoc/base/core_ext.rb: -------------------------------------------------------------------------------- 1 | require 'nanoc/base/core_ext/array' 2 | require 'nanoc/base/core_ext/hash' 3 | require 'nanoc/base/core_ext/pathname' 4 | require 'nanoc/base/core_ext/string' 5 | -------------------------------------------------------------------------------- /spec/nanoc/base/views/item_collection_spec.rb: -------------------------------------------------------------------------------- 1 | describe Nanoc::ItemCollectionView do 2 | let(:view_class) { Nanoc::ItemView } 3 | it_behaves_like 'an identifiable collection' 4 | end 5 | -------------------------------------------------------------------------------- /spec/nanoc/base/views/layout_collection_spec.rb: -------------------------------------------------------------------------------- 1 | describe Nanoc::LayoutCollectionView do 2 | let(:view_class) { Nanoc::LayoutView } 3 | it_behaves_like 'an identifiable collection' 4 | end 5 | -------------------------------------------------------------------------------- /test/cli/commands/test_help.rb: -------------------------------------------------------------------------------- 1 | class Nanoc::CLI::Commands::HelpTest < Nanoc::TestCase 2 | def test_run 3 | Nanoc::CLI.run %w( help ) 4 | Nanoc::CLI.run %w( help co ) 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/nanoc/base/views/item_collection.rb: -------------------------------------------------------------------------------- 1 | module Nanoc 2 | class ItemCollectionView < ::Nanoc::IdentifiableCollectionView 3 | # @api private 4 | def view_class 5 | Nanoc::ItemView 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/nanoc/base/views/layout_collection.rb: -------------------------------------------------------------------------------- 1 | module Nanoc 2 | class LayoutCollectionView < ::Nanoc::IdentifiableCollectionView 3 | # @api private 4 | def view_class 5 | Nanoc::LayoutView 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/nanoc/base/views/site.rb: -------------------------------------------------------------------------------- 1 | module Nanoc 2 | class SiteView 3 | # @api private 4 | def initialize(site) 5 | @site = site 6 | end 7 | 8 | # @api private 9 | def unwrap 10 | @site 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/gem_loader.rb: -------------------------------------------------------------------------------- 1 | begin 2 | require 'rubygems' 3 | 4 | gemspec = File.expand_path('nanoc.gemspec', Dir.pwd) 5 | Gem::Specification.load(gemspec).dependencies.each do |dep| 6 | gem dep.name, *dep.requirement.as_list 7 | end 8 | rescue LoadError 9 | end 10 | -------------------------------------------------------------------------------- /lib/nanoc/cli/stream_cleaners.rb: -------------------------------------------------------------------------------- 1 | module Nanoc::CLI 2 | # @api private 3 | module StreamCleaners 4 | autoload 'Abstract', 'nanoc/cli/stream_cleaners/abstract' 5 | autoload 'ANSIColors', 'nanoc/cli/stream_cleaners/ansi_colors' 6 | autoload 'UTF8', 'nanoc/cli/stream_cleaners/utf8' 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/nanoc/base/views/mutable_config.rb: -------------------------------------------------------------------------------- 1 | module Nanoc 2 | class MutableConfigView < Nanoc::ConfigView 3 | # Sets the value for the given attribute. 4 | # 5 | # @param [Symbol] key 6 | # 7 | # @see Hash#[]= 8 | def []=(key, value) 9 | @config[key] = value 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/nanoc/base/views/mutable_layout.rb: -------------------------------------------------------------------------------- 1 | module Nanoc 2 | class MutableLayoutView < Nanoc::LayoutView 3 | # Sets the value for the given attribute. 4 | # 5 | # @param [Symbol] key 6 | # 7 | # @see Hash#[]= 8 | def []=(key, value) 9 | unwrap[key] = value 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Set up env 2 | $LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + '/lib')) 3 | require './test/gem_loader.rb' 4 | 5 | # Load nanoc 6 | require 'nanoc' 7 | 8 | # Load tasks 9 | Dir.glob('tasks/**/*.rake').each { |r| Rake.application.add_import r } 10 | 11 | # Set default task 12 | task default: :test 13 | -------------------------------------------------------------------------------- /lib/nanoc/cli/stream_cleaners/ansi_colors.rb: -------------------------------------------------------------------------------- 1 | module Nanoc::CLI::StreamCleaners 2 | # Removes ANSI color escape sequences. 3 | # 4 | # @api private 5 | class ANSIColors < Abstract 6 | # @see Nanoc::CLI::StreamCleaners::Abstract#clean 7 | def clean(s) 8 | s.gsub(/\e\[.+?m/, '') 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /tasks/rubocop.rake: -------------------------------------------------------------------------------- 1 | begin 2 | require 'rubocop/rake_task' 3 | 4 | RuboCop::RakeTask.new(:rubocop) do |task| 5 | task.options = %w( --display-cop-names --format simple ) 6 | task.patterns = ['lib/**/*.rb'] 7 | end 8 | rescue LoadError 9 | warn 'Could not load RuboCop. RuboCop rake tasks will be unavailable.' 10 | end 11 | -------------------------------------------------------------------------------- /test/filters/test_rdoc.rb: -------------------------------------------------------------------------------- 1 | class Nanoc::Filters::RDocTest < Nanoc::TestCase 2 | def test_filter 3 | # Get filter 4 | filter = ::Nanoc::Filters::RDoc.new 5 | 6 | # Run filter 7 | result = filter.setup_and_run('= Foo') 8 | assert_match(%r{\A\s*Foo(.*)?\s*\Z}, result) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/nanoc/base/views/mutable_config_spec.rb: -------------------------------------------------------------------------------- 1 | describe Nanoc::MutableConfigView do 2 | describe '#[]=' do 3 | let(:config) { {} } 4 | let(:view) { described_class.new(config) } 5 | 6 | it 'sets attributes' do 7 | view[:awesomeness] = 'rather high' 8 | expect(config[:awesomeness]).to eq('rather high') 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/filters/test_markaby.rb: -------------------------------------------------------------------------------- 1 | class Nanoc::Filters::MarkabyTest < Nanoc::TestCase 2 | def test_filter 3 | if_have 'markaby' do 4 | # Create filter 5 | filter = ::Nanoc::Filters::Markaby.new 6 | 7 | # Run filter 8 | result = filter.setup_and_run("html do\nend") 9 | assert_equal('', result) 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /test/filters/test_rubypants.rb: -------------------------------------------------------------------------------- 1 | class Nanoc::Filters::RubyPantsTest < Nanoc::TestCase 2 | def test_filter 3 | if_have 'rubypants' do 4 | # Get filter 5 | filter = ::Nanoc::Filters::RubyPants.new 6 | 7 | # Run filter 8 | result = filter.setup_and_run('Wait---what?') 9 | assert_equal('Wait—what?', result) 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/nanoc/extra/checking.rb: -------------------------------------------------------------------------------- 1 | module Nanoc::Extra 2 | # @api private 3 | module Checking 4 | autoload 'Check', 'nanoc/extra/checking/check' 5 | autoload 'DSL', 'nanoc/extra/checking/dsl' 6 | autoload 'Runner', 'nanoc/extra/checking/runner.rb' 7 | autoload 'Issue', 'nanoc/extra/checking/issue' 8 | end 9 | end 10 | 11 | require 'nanoc/extra/checking/checks' 12 | -------------------------------------------------------------------------------- /spec/nanoc/base/views/mutable_layout_spec.rb: -------------------------------------------------------------------------------- 1 | describe Nanoc::MutableLayoutView do 2 | describe '#[]=' do 3 | let(:layout) { Nanoc::Int::Layout.new('content', {}, '/asdf/') } 4 | let(:view) { described_class.new(layout) } 5 | 6 | it 'sets attributes' do 7 | view[:title] = 'Donkey' 8 | expect(view[:title]).to eq('Donkey') 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/nanoc/extra/checking/issue.rb: -------------------------------------------------------------------------------- 1 | module Nanoc::Extra::Checking 2 | # @api private 3 | class Issue 4 | attr_reader :description 5 | attr_reader :subject 6 | attr_reader :check_class 7 | 8 | def initialize(desc, subject, check_class) 9 | @description = desc 10 | @subject = subject 11 | @check_class = check_class 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/nanoc/extra/deployers.rb: -------------------------------------------------------------------------------- 1 | module Nanoc::Extra 2 | # @api private 3 | module Deployers 4 | autoload 'Fog', 'nanoc/extra/deployers/fog' 5 | autoload 'Rsync', 'nanoc/extra/deployers/rsync' 6 | 7 | Nanoc::Extra::Deployer.register '::Nanoc::Extra::Deployers::Fog', :fog 8 | Nanoc::Extra::Deployer.register '::Nanoc::Extra::Deployers::Rsync', :rsync 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/filters/test_asciidoc.rb: -------------------------------------------------------------------------------- 1 | class Nanoc::Filters::AsciiDocTest < Nanoc::TestCase 2 | def test_filter 3 | skip_unless_have_command 'asciidoc' 4 | 5 | # Create filter 6 | filter = ::Nanoc::Filters::AsciiDoc.new 7 | 8 | # Run filter 9 | result = filter.setup_and_run('== Blah blah') 10 | assert_match %r{

Blah blah

}, result 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /test/extra/core_ext/test_time.rb: -------------------------------------------------------------------------------- 1 | class Nanoc::ExtraCoreExtTimeTest < Nanoc::TestCase 2 | def test___nanoc_to_iso8601_date 3 | assert_equal('2008-05-19', Time.utc(2008, 5, 19, 14, 20, 0, 0).__nanoc_to_iso8601_date) 4 | end 5 | 6 | def test___nanoc_to_iso8601_time 7 | assert_equal('2008-05-19T14:20:00Z', Time.utc(2008, 5, 19, 14, 20, 0, 0).__nanoc_to_iso8601_time) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/nanoc/data_sources.rb: -------------------------------------------------------------------------------- 1 | # @api private 2 | module Nanoc::DataSources 3 | autoload 'Filesystem', 'nanoc/data_sources/filesystem' 4 | autoload 'FilesystemUnified', 'nanoc/data_sources/filesystem_unified' 5 | 6 | Nanoc::DataSource.register '::Nanoc::DataSources::FilesystemUnified', :filesystem_unified 7 | Nanoc::DataSource.register '::Nanoc::DataSources::FilesystemUnified', :filesystem 8 | end 9 | -------------------------------------------------------------------------------- /test/filters/test_bluecloth.rb: -------------------------------------------------------------------------------- 1 | class Nanoc::Filters::BlueClothTest < Nanoc::TestCase 2 | def test_filter 3 | if_have 'bluecloth' do 4 | # Create filter 5 | filter = ::Nanoc::Filters::BlueCloth.new 6 | 7 | # Run filter 8 | result = filter.setup_and_run('> Quote') 9 | assert_match %r{
\s*

Quote

\s*
}, result 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /test/filters/test_maruku.rb: -------------------------------------------------------------------------------- 1 | class Nanoc::Filters::MarukuTest < Nanoc::TestCase 2 | def test_filter 3 | if_have 'maruku' do 4 | # Create filter 5 | filter = ::Nanoc::Filters::Maruku.new 6 | 7 | # Run filter 8 | result = filter.setup_and_run('This is _so_ *cool*!') 9 | assert_equal('

This is so cool!

', result.strip) 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /test/extra/core_ext/test_pathname.rb: -------------------------------------------------------------------------------- 1 | class Nanoc::Extra::CoreExtPathnameTest < Nanoc::TestCase 2 | def test_components 3 | assert_equal %w( / a bb ccc dd e ), Pathname.new('/a/bb/ccc/dd/e').__nanoc_components 4 | end 5 | 6 | def test_include_component 7 | assert Pathname.new('/home/ddfreyne/').__nanoc_include_component?('ddfreyne') 8 | refute Pathname.new('/home/ddfreyne/').__nanoc_include_component?('acid') 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/filters/test_coffeescript.rb: -------------------------------------------------------------------------------- 1 | class Nanoc::Filters::CoffeeScriptTest < Nanoc::TestCase 2 | def test_filter 3 | if_have 'coffee-script' do 4 | # Create filter 5 | filter = ::Nanoc::Filters::CoffeeScript.new 6 | 7 | # Run filter (no assigns) 8 | result = filter.setup_and_run('alert 42') 9 | assert_equal('(function() { alert(42); }).call(this); ', result.gsub(/\s+/, ' ')) 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/nanoc/cli/stream_cleaners/utf8.rb: -------------------------------------------------------------------------------- 1 | module Nanoc::CLI::StreamCleaners 2 | # Simplifies output by replacing UTF-8 characters with their ASCII decompositions. 3 | # 4 | # @api private 5 | class UTF8 < Abstract 6 | # @see Nanoc::CLI::StreamCleaners::Abstract#clean 7 | def clean(s) 8 | # FIXME: this decomposition is not generally usable 9 | s.gsub(/“|”/, '"').gsub(/‘|’/, '\'').gsub('…', '...').gsub('©', '(c)') 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/nanoc/extra/core_ext/time.rb: -------------------------------------------------------------------------------- 1 | # @api private 2 | module Nanoc::Extra::TimeExtensions 3 | # @return [String] The time in an ISO-8601 date format. 4 | def __nanoc_to_iso8601_date 5 | strftime('%Y-%m-%d') 6 | end 7 | 8 | # @return [String] The time in an ISO-8601 time format. 9 | def __nanoc_to_iso8601_time 10 | getutc.strftime('%Y-%m-%dT%H:%M:%SZ') 11 | end 12 | end 13 | 14 | # @api private 15 | class Time 16 | include Nanoc::Extra::TimeExtensions 17 | end 18 | -------------------------------------------------------------------------------- /lib/nanoc/base/views/mutable_identifiable_collection.rb: -------------------------------------------------------------------------------- 1 | module Nanoc 2 | class MutableIdentifiableCollectionView < Nanoc::IdentifiableCollectionView 3 | # Deletes every object for which the block evaluates to true. 4 | # 5 | # @yieldparam [#identifier] object 6 | # 7 | # @yieldreturn [Boolean] 8 | # 9 | # @return [self] 10 | def delete_if(&_block) 11 | @objects.delete_if { |o| yield(view_class.new(o)) } 12 | self 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/nanoc/base/core_ext/pathname.rb: -------------------------------------------------------------------------------- 1 | # @api private 2 | module Nanoc::PathnameExtensions 3 | # Calculates the checksum for the file referenced to by this pathname. Any 4 | # change to the file contents will result in a different checksum. 5 | # 6 | # @return [String] The checksum for this file 7 | # 8 | # @api private 9 | def __nanoc_checksum 10 | Nanoc::Int::Checksummer.calc(self) 11 | end 12 | end 13 | 14 | # @api private 15 | class Pathname 16 | include Nanoc::PathnameExtensions 17 | end 18 | -------------------------------------------------------------------------------- /lib/nanoc/filters/bluecloth.rb: -------------------------------------------------------------------------------- 1 | module Nanoc::Filters 2 | # @api private 3 | class BlueCloth < Nanoc::Filter 4 | requires 'bluecloth' 5 | 6 | # Runs the content through [BlueCloth](http://deveiate.org/projects/BlueCloth). 7 | # This method takes no options. 8 | # 9 | # @param [String] content The content to filter 10 | # 11 | # @return [String] The filtered content 12 | def run(content, _params = {}) 13 | ::BlueCloth.new(content).to_html 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /test/cli/commands/test_check.rb: -------------------------------------------------------------------------------- 1 | class Nanoc::CLI::Commands::CheckTest < Nanoc::TestCase 2 | def test_check_stale 3 | with_site do |_site| 4 | FileUtils.mkdir_p('output') 5 | 6 | # Should not raise now 7 | Nanoc::CLI.run %w( check stale ) 8 | 9 | # Should raise now 10 | File.open('output/blah.html', 'w') { |io| io.write 'moo' } 11 | assert_raises Nanoc::Int::Errors::GenericTrivial do 12 | Nanoc::CLI.run %w( check stale ) 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + '/../lib')) 2 | require 'nanoc' 3 | require 'nanoc/cli' 4 | Nanoc::CLI.setup 5 | 6 | RSpec.configure do |c| 7 | c.around(:each) do |example| 8 | Nanoc::CLI::ErrorHandler.disable 9 | example.run 10 | Nanoc::CLI::ErrorHandler.enable 11 | end 12 | 13 | c.around(:each) do |example| 14 | Dir.mktmpdir('nanoc-test') do |dir| 15 | FileUtils.cd(dir) do 16 | example.run 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /test/extra/checking/test_dsl.rb: -------------------------------------------------------------------------------- 1 | class Nanoc::Extra::Checking::DSLTest < Nanoc::TestCase 2 | def test_from_file 3 | with_site do |_site| 4 | File.open('Checks', 'w') { |io| io.write("check :foo do\n\nend\ndeploy_check :bar\n") } 5 | dsl = Nanoc::Extra::Checking::DSL.from_file('Checks') 6 | 7 | # One new check 8 | refute Nanoc::Extra::Checking::Check.named(:foo).nil? 9 | 10 | # One check marked for deployment 11 | assert_equal [:bar], dsl.deploy_checks 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/nanoc/filters/rubypants.rb: -------------------------------------------------------------------------------- 1 | module Nanoc::Filters 2 | # @api private 3 | class RubyPants < Nanoc::Filter 4 | requires 'rubypants' 5 | 6 | # Runs the content through [RubyPants](http://rubydoc.info/gems/rubypants/). 7 | # This method takes no options. 8 | # 9 | # @param [String] content The content to filter 10 | # 11 | # @return [String] The filtered content 12 | def run(content, _params = {}) 13 | # Get result 14 | ::RubyPants.new(content).to_html 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/nanoc/filters/markaby.rb: -------------------------------------------------------------------------------- 1 | module Nanoc::Filters 2 | # @api private 3 | class Markaby < Nanoc::Filter 4 | requires 'markaby' 5 | 6 | # Runs the content through [Markaby](http://markaby.github.io/). 7 | # This method takes no options. 8 | # 9 | # @param [String] content The content to filter 10 | # 11 | # @return [String] The filtered content 12 | def run(content, _params = {}) 13 | # Get result 14 | ::Markaby::Builder.new(assigns).instance_eval(content).to_s 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/nanoc/filters/rainpress.rb: -------------------------------------------------------------------------------- 1 | module Nanoc::Filters 2 | # @api private 3 | class Rainpress < Nanoc::Filter 4 | requires 'rainpress' 5 | 6 | # Runs the content through [Rainpress](http://code.google.com/p/rainpress/). 7 | # Parameters passed to this filter will be passed on to Rainpress. 8 | # 9 | # @param [String] content The content to filter 10 | # 11 | # @return [String] The filtered content 12 | def run(content, params = {}) 13 | ::Rainpress.compress(content, params) 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/nanoc/filters/maruku.rb: -------------------------------------------------------------------------------- 1 | module Nanoc::Filters 2 | # @api private 3 | class Maruku < Nanoc::Filter 4 | requires 'maruku' 5 | 6 | # Runs the content through [Maruku](https://github.com/bhollis/maruku/). 7 | # Parameters passed to this filter will be passed on to Maruku. 8 | # 9 | # @param [String] content The content to filter 10 | # 11 | # @return [String] The filtered content 12 | def run(content, params = {}) 13 | # Get result 14 | ::Maruku.new(content, params).to_html 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/nanoc/base/views/mutable_item.rb: -------------------------------------------------------------------------------- 1 | module Nanoc 2 | class MutableItemView < Nanoc::ItemView 3 | # Sets the value for the given attribute. 4 | # 5 | # @param [Symbol] key 6 | # 7 | # @see Hash#[]= 8 | def []=(key, value) 9 | unwrap[key] = value 10 | end 11 | 12 | # Updates the attributes based on the given hash. 13 | # 14 | # @param [Hash] hash 15 | # 16 | # @return [self] 17 | def update_attributes(hash) 18 | hash.each { |k, v| unwrap[k] = v } 19 | self 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/nanoc/filters/typogruby.rb: -------------------------------------------------------------------------------- 1 | module Nanoc::Filters 2 | # @since 3.2.0 3 | # 4 | # @api private 5 | class Typogruby < Nanoc::Filter 6 | requires 'typogruby' 7 | 8 | # Runs the content through [Typogruby](http://avdgaag.github.com/typogruby/). 9 | # This method takes no options. 10 | # 11 | # @param [String] content The content to filter 12 | # 13 | # @return [String] The filtered content 14 | def run(content, _params = {}) 15 | # Get result 16 | ::Typogruby.improve(content) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/nanoc/extra.rb: -------------------------------------------------------------------------------- 1 | # @api private 2 | module Nanoc::Extra 3 | autoload 'Checking', 'nanoc/extra/checking' 4 | autoload 'FilesystemTools', 'nanoc/extra/filesystem_tools' 5 | autoload 'LinkCollector', 'nanoc/extra/link_collector.rb' 6 | autoload 'Pruner', 'nanoc/extra/pruner' 7 | autoload 'Piper', 'nanoc/extra/piper' 8 | autoload 'JRubyNokogiriWarner', 'nanoc/extra/jruby_nokogiri_warner' 9 | end 10 | 11 | require 'nanoc/extra/core_ext' 12 | require 'nanoc/extra/deployer' 13 | require 'nanoc/extra/deployers' 14 | -------------------------------------------------------------------------------- /lib/nanoc/filters/coffeescript.rb: -------------------------------------------------------------------------------- 1 | module Nanoc::Filters 2 | # @since 3.3.0 3 | # 4 | # @api private 5 | class CoffeeScript < Nanoc::Filter 6 | requires 'coffee-script' 7 | 8 | # Runs the content through [CoffeeScript](http://coffeescript.org/). 9 | # This method takes no options. 10 | # 11 | # @param [String] content The CoffeeScript content to turn into JavaScript 12 | # 13 | # @return [String] The resulting JavaScript 14 | def run(content, _params = {}) 15 | ::CoffeeScript.compile(content) 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/base/test_item_rep_recorder_proxy.rb: -------------------------------------------------------------------------------- 1 | class Nanoc::Int::ItemRepRecorderProxyTest < Nanoc::TestCase 2 | def test_double_names 3 | proxy = Nanoc::Int::ItemRepRecorderProxy.new(mock) 4 | 5 | proxy.snapshot(:foo, stuff: :giraffe) 6 | assert_raises(Nanoc::Int::Errors::CannotCreateMultipleSnapshotsWithSameName) do 7 | proxy.snapshot(:foo, stuff: :donkey) 8 | end 9 | end 10 | 11 | def test_double_params 12 | proxy = Nanoc::Int::ItemRepRecorderProxy.new(mock) 13 | 14 | proxy.snapshot(:foo) 15 | proxy.snapshot(:bar) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.2.2 4 | branches: 5 | only: 6 | - master 7 | before_install: gem install bundler 8 | matrix: 9 | include: 10 | - rvm: jruby-head 11 | jdk: openjdk7 12 | env: DISABLE_NOKOGIRI=1 13 | allow_failures: 14 | - rvm: jruby-head 15 | jdk: openjdk7 16 | env: DISABLE_NOKOGIRI=1 17 | notifications: 18 | irc: 19 | channels: 20 | - "chat.freenode.net#nanoc" 21 | template: 22 | - "%{repository}/%{branch} %{commit} %{author}: %{message}" 23 | use_notice: true 24 | skip_join: true 25 | -------------------------------------------------------------------------------- /tasks/doc.rake: -------------------------------------------------------------------------------- 1 | require 'yard' 2 | 3 | YARD::Rake::YardocTask.new(:doc) do |yard| 4 | yard.files = Dir['lib/**/*.rb'] 5 | yard.options = [ 6 | '--markup', 'markdown', 7 | '--markup-provider', 'kramdown', 8 | '--charset', 'utf-8', 9 | '--readme', 'README.md', 10 | '--files', 'NEWS.md,LICENSE', 11 | '--output-dir', 'doc/yardoc', 12 | '--template-path', 'doc/yardoc_templates', 13 | '--load', 'doc/yardoc_handlers/identifier.rb', 14 | '--query', '@api.text != "private"', 15 | ] 16 | end 17 | -------------------------------------------------------------------------------- /lib/nanoc/base/source_data/configuration.rb: -------------------------------------------------------------------------------- 1 | module Nanoc::Int 2 | # Represents the site configuration. 3 | # 4 | # @api private 5 | class Configuration < ::Hash 6 | # Creates a new configuration with the given hash. 7 | # 8 | # @param [Hash] hash The actual configuration hash 9 | def initialize(hash) 10 | replace(hash) 11 | end 12 | 13 | # Returns an object that can be used for uniquely identifying objects. 14 | # 15 | # @return [Object] An unique reference to this object 16 | def reference 17 | :config 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/nanoc/cli/commands/shell_spec.rb: -------------------------------------------------------------------------------- 1 | describe Nanoc::CLI::Commands::Shell do 2 | describe '#env_for' do 3 | subject { described_class.env_for(site) } 4 | 5 | let(:site) do 6 | double( 7 | :site, 8 | items: [], 9 | layouts: [], 10 | config: nil, 11 | ) 12 | end 13 | 14 | it 'returns views' do 15 | expect(subject[:items]).to be_a(Nanoc::ItemCollectionView) 16 | expect(subject[:layouts]).to be_a(Nanoc::LayoutCollectionView) 17 | expect(subject[:config]).to be_a(Nanoc::ConfigView) 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /test/base/test_rule.rb: -------------------------------------------------------------------------------- 1 | class Nanoc::Int::RuleTest < Nanoc::TestCase 2 | def test_initialize 3 | # TODO: implement 4 | end 5 | 6 | def test_applicable_to 7 | # TODO: implement 8 | end 9 | 10 | def test_apply_to 11 | # TODO: implement 12 | end 13 | 14 | def test_matches 15 | pattern = Nanoc::Int::Pattern.from(%r{/(.*)/(.*)/}) 16 | identifier = '/anything/else/' 17 | expected = %w(anything else) 18 | 19 | rule = Nanoc::Int::Rule.new(pattern, :string, proc {}) 20 | 21 | assert_equal expected, rule.send(:matches, identifier) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/nanoc/helpers.rb: -------------------------------------------------------------------------------- 1 | module Nanoc::Helpers 2 | autoload 'Blogging', 'nanoc/helpers/blogging' 3 | autoload 'Breadcrumbs', 'nanoc/helpers/breadcrumbs' 4 | autoload 'Capturing', 'nanoc/helpers/capturing' 5 | autoload 'Filtering', 'nanoc/helpers/filtering' 6 | autoload 'HTMLEscape', 'nanoc/helpers/html_escape' 7 | autoload 'LinkTo', 'nanoc/helpers/link_to' 8 | autoload 'Rendering', 'nanoc/helpers/rendering' 9 | autoload 'Tagging', 'nanoc/helpers/tagging' 10 | autoload 'Text', 'nanoc/helpers/text' 11 | autoload 'XMLSitemap', 'nanoc/helpers/xml_sitemap' 12 | end 13 | -------------------------------------------------------------------------------- /test/filters/test_typogruby.rb: -------------------------------------------------------------------------------- 1 | class Nanoc::Filters::TypogrubyTest < Nanoc::TestCase 2 | def test_filter 3 | if_have 'typogruby' do 4 | # Get filter 5 | filter = ::Nanoc::Filters::Typogruby.new 6 | 7 | # Run filter 8 | a = '"Typogruby makes HTML look smarter & better, don\'t you think?"' 9 | b = 'Typogruby makes HTML look smarter & better, don’t you think?”' 10 | result = filter.setup_and_run(a) 11 | assert_equal(b, result) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/nanoc/filters/mustache.rb: -------------------------------------------------------------------------------- 1 | module Nanoc::Filters 2 | # @since 3.2.0 3 | # 4 | # @api private 5 | class Mustache < Nanoc::Filter 6 | requires 'mustache' 7 | 8 | # Runs the content through 9 | # [Mustache](http://github.com/defunkt/mustache). This method takes no 10 | # options. 11 | # 12 | # @param [String] content The content to filter 13 | # 14 | # @return [String] The filtered content 15 | def run(content, _params = {}) 16 | context = item.attributes.merge({ yield: assigns[:content] }) 17 | ::Mustache.render(content, context) 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/nanoc/filters/rdiscount.rb: -------------------------------------------------------------------------------- 1 | module Nanoc::Filters 2 | # @api private 3 | class RDiscount < Nanoc::Filter 4 | requires 'rdiscount' 5 | 6 | # Runs the content through [RDiscount](http://github.com/rtomayko/rdiscount). 7 | # 8 | # @option params [Array] :symbol ([]) A list of RDiscount extensions 9 | # 10 | # @param [String] content The content to filter 11 | # 12 | # @return [String] The filtered content 13 | def run(content, params = {}) 14 | extensions = params[:extensions] || [] 15 | 16 | ::RDiscount.new(content, *extensions).to_html 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/nanoc/extra/core_ext/pathname.rb: -------------------------------------------------------------------------------- 1 | module Nanoc::Extra 2 | # @api private 3 | module PathnameExtensions 4 | def __nanoc_components 5 | components = [] 6 | tmp = self 7 | loop do 8 | old = tmp 9 | components << File.basename(tmp) 10 | tmp = File.dirname(tmp) 11 | break if old == tmp 12 | end 13 | components.reverse 14 | end 15 | 16 | def __nanoc_include_component?(component) 17 | __nanoc_components.include?(component) 18 | end 19 | end 20 | end 21 | 22 | # @api private 23 | class ::Pathname 24 | include ::Nanoc::Extra::PathnameExtensions 25 | end 26 | -------------------------------------------------------------------------------- /test/extra/checking/test_check.rb: -------------------------------------------------------------------------------- 1 | class Nanoc::Extra::Checking::CheckTest < Nanoc::TestCase 2 | def test_output_filenames 3 | with_site do |site| 4 | File.open('output/foo.html', 'w') { |io| io.write 'hello' } 5 | check = Nanoc::Extra::Checking::Check.create(site) 6 | assert_equal ['output/foo.html'], check.output_filenames 7 | end 8 | end 9 | 10 | def test_no_output_dir 11 | with_site do |site| 12 | site.config[:output_dir] = 'non-existent' 13 | assert_raises Nanoc::Extra::Checking::OutputDirNotFoundError do 14 | Nanoc::Extra::Checking::Check.create(site) 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/nanoc/filters/asciidoc.rb: -------------------------------------------------------------------------------- 1 | module Nanoc::Filters 2 | # @since 3.2.0 3 | # 4 | # @api private 5 | class AsciiDoc < Nanoc::Filter 6 | # Runs the content through [AsciiDoc](http://www.methods.co.nz/asciidoc/). 7 | # This method takes no options. 8 | # 9 | # @param [String] content The content to filter 10 | # 11 | # @return [String] The filtered content 12 | def run(content, _params = {}) 13 | stdout = StringIO.new 14 | stderr = $stderr 15 | piper = Nanoc::Extra::Piper.new(stdout: stdout, stderr: stderr) 16 | piper.run(%w( asciidoc -o - - ), content) 17 | stdout.string 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/nanoc/filters/uglify_js.rb: -------------------------------------------------------------------------------- 1 | module Nanoc::Filters 2 | # @api private 3 | class UglifyJS < Nanoc::Filter 4 | requires 'uglifier' 5 | 6 | # Runs the content through [UglifyJS](https://github.com/mishoo/UglifyJS2/). 7 | # This method optionally takes options to pass directly to Uglifier. 8 | # 9 | # @param [String] content The content to filter 10 | # 11 | # @option params [Array] :options ([]) A list of options to pass on to Uglifier 12 | # 13 | # @return [String] The filtered content 14 | def run(content, params = {}) 15 | # Add filename to load path 16 | Uglifier.new(params).compile(content) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/nanoc/extra/checking/dsl.rb: -------------------------------------------------------------------------------- 1 | module Nanoc::Extra::Checking 2 | # @api private 3 | class DSL 4 | attr_reader :deploy_checks 5 | 6 | def self.from_file(filename) 7 | dsl = new 8 | dsl.instance_eval File.read(filename) 9 | dsl 10 | end 11 | 12 | def initialize 13 | @deploy_checks = [] 14 | end 15 | 16 | def check(identifier, &block) 17 | klass = Class.new(::Nanoc::Extra::Checking::Check) 18 | klass.send(:define_method, :run, &block) 19 | klass.send(:identifier, identifier) 20 | end 21 | 22 | def deploy_check(*identifiers) 23 | identifiers.each { |i| @deploy_checks << i } 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/nanoc/base/core_ext/string.rb: -------------------------------------------------------------------------------- 1 | # @api private 2 | module Nanoc::StringExtensions 3 | # Transforms string into an actual identifier 4 | # 5 | # @return [String] The identifier generated from the receiver 6 | def __nanoc_cleaned_identifier 7 | "/#{self}/".gsub(/^\/+|\/+$/, '/') 8 | end 9 | 10 | # Calculates the checksum for this string. Any change to this string will 11 | # result in a different checksum. 12 | # 13 | # @return [String] The checksum for this string 14 | # 15 | # @api private 16 | def __nanoc_checksum 17 | Nanoc::Int::Checksummer.calc(self) 18 | end 19 | end 20 | 21 | # @api private 22 | class String 23 | include Nanoc::StringExtensions 24 | end 25 | -------------------------------------------------------------------------------- /lib/nanoc/filters/kramdown.rb: -------------------------------------------------------------------------------- 1 | module Nanoc::Filters 2 | # @api private 3 | class Kramdown < Nanoc::Filter 4 | requires 'kramdown' 5 | 6 | # Runs the content through [Kramdown](http://kramdown.gettalong.org/). 7 | # Parameters passed to this filter will be passed on to Kramdown. 8 | # 9 | # @param [String] content The content to filter 10 | # 11 | # @return [String] The filtered content 12 | def run(content, params = {}) 13 | document = ::Kramdown::Document.new(content, params) 14 | 15 | document.warnings.each do |warning| 16 | $stderr.puts "kramdown warning: #{warning}" 17 | end 18 | 19 | document.to_html 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/nanoc/filters/rdoc.rb: -------------------------------------------------------------------------------- 1 | module Nanoc::Filters 2 | # @api private 3 | class RDoc < Nanoc::Filter 4 | requires 'rdoc' 5 | 6 | def self.setup 7 | gem 'rdoc', '~> 4.0' 8 | super 9 | end 10 | 11 | # Runs the content through [RDoc::Markup](http://docs.seattlerb.org/rdoc/RDoc/Markup.html). 12 | # This method takes no options. 13 | # 14 | # @param [String] content The content to filter 15 | # 16 | # @return [String] The filtered content 17 | def run(content, _params = {}) 18 | options = ::RDoc::Options.new 19 | to_html = ::RDoc::Markup::ToHtml.new(options) 20 | ::RDoc::Markup.new.convert(content, to_html) 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/base/test_plugin.rb: -------------------------------------------------------------------------------- 1 | class Nanoc::PluginTest < Nanoc::TestCase 2 | class SampleFilter < Nanoc::Filter 3 | identifier :_plugin_test_sample_filter 4 | end 5 | 6 | def test_named 7 | # Find existant filter 8 | filter = Nanoc::Filter.named(:erb) 9 | assert(!filter.nil?) 10 | 11 | # Find non-existant filter 12 | filter = Nanoc::Filter.named(:lksdaffhdlkashlgkskahf) 13 | assert(filter.nil?) 14 | end 15 | 16 | def test_register 17 | SampleFilter.send(:identifier, :_plugin_test_sample_filter) 18 | 19 | registry = Nanoc::Int::PluginRegistry.instance 20 | filter = registry.find(Nanoc::Filter, :_plugin_test_sample_filter) 21 | 22 | refute_nil filter 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/nanoc/filters/sass/sass_filesystem_importer.rb: -------------------------------------------------------------------------------- 1 | # @api private 2 | class ::Sass::Importers::Filesystem 3 | alias_method :_orig_find, :_find 4 | 5 | def _find(dir, name, options) 6 | # Find filename 7 | full_filename, _syntax = ::Sass::Util.destructure(find_real_file(dir, name, options)) 8 | return nil if full_filename.nil? 9 | 10 | # Create dependency 11 | filter = options[:nanoc_current_filter] 12 | if filter 13 | item = filter.imported_filename_to_item(full_filename) 14 | item = item.unwrap if item.respond_to?(:unwrap) 15 | filter.depend_on([item]) unless item.nil? 16 | end 17 | 18 | # Call original _find 19 | _orig_find(dir, name, options) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/filters/test_rainpress.rb: -------------------------------------------------------------------------------- 1 | class Nanoc::Filters::RainpressTest < Nanoc::TestCase 2 | def test_filter 3 | if_have 'rainpress' do 4 | # Create filter 5 | filter = ::Nanoc::Filters::Rainpress.new 6 | 7 | # Run filter 8 | result = filter.setup_and_run('body { color: black; }') 9 | assert_equal('body{color:#000}', result) 10 | end 11 | end 12 | 13 | def test_filter_with_options 14 | if_have 'rainpress' do 15 | # Create filter 16 | filter = ::Nanoc::Filters::Rainpress.new 17 | 18 | # Run filter 19 | result = filter.setup_and_run('body { color: #aabbcc; }', colors: false) 20 | assert_equal('body{color:#aabbcc}', result) 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/nanoc/filters/yui_compressor.rb: -------------------------------------------------------------------------------- 1 | module Nanoc::Filters 2 | # @since 3.3.0 3 | # 4 | # @api private 5 | class YUICompressor < Nanoc::Filter 6 | requires 'yuicompressor' 7 | 8 | # Compress Javascript or CSS using [YUICompressor](http://rubydoc.info/gems/yuicompressor). 9 | # This method optionally takes options to pass directly to the 10 | # YUICompressor gem. 11 | # 12 | # @param [String] content JavaScript or CSS input 13 | # 14 | # @param [Hash] params Options passed to YUICompressor 15 | # 16 | # @return [String] Compressed but equivalent JavaScript or CSS 17 | def run(content, params = {}) 18 | ::YUICompressor.compress(content, params) 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/base/test_code_snippet.rb: -------------------------------------------------------------------------------- 1 | class Nanoc::Int::CodeSnippetTest < Nanoc::TestCase 2 | def test_load 3 | # Initialize 4 | $complete_insane_parrot = 'meow' 5 | 6 | # Create code and load it 7 | code_snippet = Nanoc::Int::CodeSnippet.new("$complete_insane_parrot = 'woof'", 'parrot.rb') 8 | code_snippet.load 9 | 10 | # Ensure code is loaded 11 | assert_equal('woof', $complete_insane_parrot) 12 | end 13 | 14 | def test_load_with_toplevel_binding 15 | # Initialize 16 | @foo = 'meow' 17 | 18 | # Create code and load it 19 | code_snippet = Nanoc::Int::CodeSnippet.new("@foo = 'woof'", 'dog.rb') 20 | code_snippet.load 21 | 22 | # Ensure binding is correct 23 | assert_equal('meow', @foo) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/nanoc/extra/checking/checks/stale.rb: -------------------------------------------------------------------------------- 1 | module Nanoc::Extra::Checking::Checks 2 | # @api private 3 | class Stale < ::Nanoc::Extra::Checking::Check 4 | def run 5 | require 'set' 6 | 7 | item_rep_paths = Set.new(@items.map(&:reps).flatten.map(&:raw_path)) 8 | 9 | output_filenames.each do |f| 10 | next if pruner.filename_excluded?(f) 11 | next if item_rep_paths.include?(f) 12 | 13 | add_issue( 14 | 'file without matching item', 15 | subject: f) 16 | end 17 | end 18 | 19 | protected 20 | 21 | def pruner 22 | exclude_config = @config.fetch(:prune, {}).fetch(:exclude, []) 23 | @pruner ||= Nanoc::Extra::Pruner.new(@site, exclude: exclude_config) 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/helpers/test_html_escape.rb: -------------------------------------------------------------------------------- 1 | class Nanoc::Helpers::HTMLEscapeTest < Nanoc::TestCase 2 | include Nanoc::Helpers::HTMLEscape 3 | 4 | def test_html_escape_with_string 5 | assert_equal('<', html_escape('<')) 6 | assert_equal('>', html_escape('>')) 7 | assert_equal('&', html_escape('&')) 8 | assert_equal('"', html_escape('"')) 9 | end 10 | 11 | def test_html_escape_with_block 12 | _erbout = 'moo' 13 | 14 | html_escape do 15 | _erbout << '

Looks like a header

' 16 | end 17 | 18 | assert_equal 'moo<h1>Looks like a header</h1>', _erbout 19 | end 20 | 21 | def test_html_escape_without_string_or_block 22 | assert_raises RuntimeError do 23 | h 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/nanoc/base/views/mutable_layout_collection.rb: -------------------------------------------------------------------------------- 1 | module Nanoc 2 | class MutableLayoutCollectionView < Nanoc::MutableIdentifiableCollectionView 3 | # @api private 4 | def view_class 5 | Nanoc::MutableLayoutView 6 | end 7 | 8 | # Creates a new layout and adds it to the site’s collection of layouts. 9 | # 10 | # @param [String] content The layout content. 11 | # 12 | # @param [Hash] attributes A hash containing this layout's attributes. 13 | # 14 | # @param [Nanoc::Identifier, String] identifier This layout's identifier. 15 | # 16 | # @return [self] 17 | def create(content, attributes, identifier) 18 | @objects << Nanoc::Int::Layout.new(content, attributes, identifier) 19 | self 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/nanoc/cli/stream_cleaners/abstract.rb: -------------------------------------------------------------------------------- 1 | module Nanoc::CLI::StreamCleaners 2 | # Superclass for all stream cleaners. Stream cleaners have a single method, 3 | # {#clean}, that takes a string and returns a cleaned string. Stream cleaners 4 | # can have state, so they can act as a FSM. 5 | # 6 | # @abstract Subclasses must implement {#clean} 7 | # 8 | # @api private 9 | class Abstract 10 | # Returns a cleaned version of the given string. 11 | # 12 | # @param [String] s The string to clean 13 | # 14 | # @return [String] The cleaned string 15 | def clean(s) # rubocop:disable Lint/UnusedMethodArgument 16 | raise NotImplementedError, 'Subclasses of Nanoc::CLI::StreamCleaners::Abstract must implement #clean' 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/base/test_context.rb: -------------------------------------------------------------------------------- 1 | class Nanoc::Int::ContextTest < Nanoc::TestCase 2 | def test_context_with_instance_variable 3 | # Create context 4 | context = Nanoc::Int::Context.new({ foo: 'bar', baz: 'quux' }) 5 | 6 | # Ensure correct evaluation 7 | assert_equal('bar', eval('@foo', context.get_binding)) 8 | end 9 | 10 | def test_context_with_instance_method 11 | # Create context 12 | context = Nanoc::Int::Context.new({ foo: 'bar', baz: 'quux' }) 13 | 14 | # Ensure correct evaluation 15 | assert_equal('bar', eval('foo', context.get_binding)) 16 | end 17 | 18 | def test_example 19 | # Parse 20 | YARD.parse(LIB_DIR + '/nanoc/base/context.rb') 21 | 22 | # Run 23 | assert_examples_correct 'Nanoc::Int::Context#initialize' 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /test/test_gem.rb: -------------------------------------------------------------------------------- 1 | class Nanoc::GemTest < Nanoc::TestCase 2 | def setup 3 | super 4 | FileUtils.cd(@orig_wd) 5 | end 6 | 7 | def test_build 8 | # Require clean environment 9 | Dir['nanoc-*.gem'].each { |f| FileUtils.rm(f) } 10 | 11 | # Build 12 | files_before = Set.new Dir['**/*'] 13 | stdout = StringIO.new 14 | stderr = StringIO.new 15 | piper = Nanoc::Extra::Piper.new(stdout: stdout, stderr: stderr) 16 | piper.run(%w( gem build nanoc.gemspec ), nil) 17 | files_after = Set.new Dir['**/*'] 18 | 19 | # Check new files 20 | diff = files_after - files_before 21 | assert_equal 1, diff.size 22 | assert_match(/^nanoc-.*\.gem$/, diff.to_a[0]) 23 | ensure 24 | Dir['nanoc-*.gem'].each { |f| FileUtils.rm(f) } 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/nanoc/extra/checking/checks/css.rb: -------------------------------------------------------------------------------- 1 | module ::Nanoc::Extra::Checking::Checks 2 | # @api private 3 | class CSS < ::Nanoc::Extra::Checking::Check 4 | identifier :css 5 | 6 | def run 7 | require 'w3c_validators' 8 | 9 | Dir[@config[:output_dir] + '/**/*.css'].each do |filename| 10 | results = ::W3CValidators::CSSValidator.new.validate_file(filename) 11 | lines = File.readlines(filename) 12 | results.errors.each do |e| 13 | line_num = e.line.to_i - 1 14 | line = lines[line_num] 15 | message = e.message.gsub(%r{\s+}, ' ').strip.sub(/\s+:$/, '') 16 | desc = "line #{line_num + 1}: #{message}: #{line}" 17 | add_issue(desc, subject: filename) 18 | end 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/nanoc/filters/haml.rb: -------------------------------------------------------------------------------- 1 | module Nanoc::Filters 2 | # @api private 3 | class Haml < Nanoc::Filter 4 | requires 'haml' 5 | 6 | # Runs the content through [Haml](http://haml-lang.com/). 7 | # Parameters passed to this filter will be passed on to Haml. 8 | # 9 | # @param [String] content The content to filter 10 | # 11 | # @return [String] The filtered content 12 | def run(content, params = {}) 13 | # Get options 14 | options = params.merge(filename: filename) 15 | 16 | # Create context 17 | context = ::Nanoc::Int::Context.new(assigns) 18 | 19 | # Get result 20 | proc = assigns[:content] ? -> { assigns[:content] } : nil 21 | ::Haml::Engine.new(content, options).render(context, assigns, &proc) 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/nanoc/extra/checking/checks/html.rb: -------------------------------------------------------------------------------- 1 | module ::Nanoc::Extra::Checking::Checks 2 | # @api private 3 | class HTML < ::Nanoc::Extra::Checking::Check 4 | identifier :html 5 | 6 | def run 7 | require 'w3c_validators' 8 | 9 | Dir[@config[:output_dir] + '/**/*.{htm,html}'].each do |filename| 10 | results = ::W3CValidators::MarkupValidator.new.validate_file(filename) 11 | lines = File.readlines(filename) 12 | results.errors.each do |e| 13 | line_num = e.line.to_i - 1 14 | line = lines[line_num] 15 | message = e.message.gsub(%r{\s+}, ' ').strip.sub(/\s+:$/, '') 16 | desc = "line #{line_num + 1}: #{message}: #{line}" 17 | add_issue(desc, subject: filename) 18 | end 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/nanoc/cli/ansi_string_colorizer.rb: -------------------------------------------------------------------------------- 1 | module Nanoc::CLI 2 | # A simple ANSI colorizer for strings. When given a string and a list of 3 | # attributes, it returns a colorized string. 4 | # 5 | # @api private 6 | module ANSIStringColorizer 7 | # TODO: complete mapping 8 | MAPPING = { 9 | bold: "\e[1m", 10 | red: "\e[31m", 11 | green: "\e[32m", 12 | yellow: "\e[33m", 13 | blue: "\e[34m" 14 | } 15 | 16 | # @param [String] s The string to colorize 17 | # 18 | # @param [Array] as An array of attributes from `MAPPING` to colorize the 19 | # string with 20 | # 21 | # @return [String] A string colorized using the given attributes 22 | def self.c(s, *as) 23 | as.map { |a| MAPPING[a] }.join('') + s + "\e[0m" 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/helpers/test_text.rb: -------------------------------------------------------------------------------- 1 | class Nanoc::Helpers::TextTest < Nanoc::TestCase 2 | include Nanoc::Helpers::Text 3 | 4 | def test_excerpt_length 5 | assert_equal('...', excerptize('Foo bar baz quux meow woof', length: 3)) 6 | assert_equal('Foo ...', excerptize('Foo bar baz quux meow woof', length: 7)) 7 | assert_equal('Foo bar baz quux meow woof', excerptize('Foo bar baz quux meow woof', length: 26)) 8 | assert_equal('Foo bar baz quux meow woof', excerptize('Foo bar baz quux meow woof', length: 8_623_785)) 9 | end 10 | 11 | def test_excerpt_omission 12 | assert_equal('Foo [continued]', excerptize('Foo bar baz quux meow woof', length: 15, omission: '[continued]')) 13 | end 14 | 15 | def test_strip_html 16 | # TODO: implement 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/nanoc/filters/slim.rb: -------------------------------------------------------------------------------- 1 | module Nanoc::Filters 2 | # @since 3.2.0 3 | # 4 | # @api private 5 | class Slim < Nanoc::Filter 6 | requires 'slim' 7 | 8 | # Runs the content through [Slim](http://slim-lang.com/). 9 | # This method takes no options. 10 | # 11 | # @param [String] content The content to filter 12 | # 13 | # @return [String] The filtered content 14 | def run(content, params = {}) 15 | params = { 16 | disable_capture: true, # Capture managed by nanoc 17 | buffer: '_erbout', # Force slim to output to the buffer used by nanoc 18 | }.merge params 19 | 20 | # Create context 21 | context = ::Nanoc::Int::Context.new(assigns) 22 | 23 | ::Slim::Template.new(params) { content }.render(context) { assigns[:content] } 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/base/test_checksum_store.rb: -------------------------------------------------------------------------------- 1 | class Nanoc::Int::ChecksumStoreTest < Nanoc::TestCase 2 | def test_get_with_existing_object 3 | require 'pstore' 4 | 5 | # Create store 6 | FileUtils.mkdir_p('tmp') 7 | pstore = PStore.new('tmp/checksums') 8 | pstore.transaction do 9 | pstore[:data] = { [:item, '/moo/'] => 'zomg' } 10 | pstore[:version] = 1 11 | end 12 | 13 | # Check 14 | store = Nanoc::Int::ChecksumStore.new 15 | store.load 16 | obj = Nanoc::Int::Item.new('Moo?', {}, '/moo/') 17 | assert_equal 'zomg', store[obj] 18 | end 19 | 20 | def test_get_with_nonexistant_object 21 | store = Nanoc::Int::ChecksumStore.new 22 | store.load 23 | 24 | # Check 25 | obj = Nanoc::Int::Item.new('Moo?', {}, '/animals/cow/') 26 | assert_equal nil, store[obj] 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test/base/test_notification_center.rb: -------------------------------------------------------------------------------- 1 | class Nanoc::Int::NotificationCenterTest < Nanoc::TestCase 2 | def test_post 3 | # Set up notification 4 | Nanoc::Int::NotificationCenter.on :ping_received, :test do 5 | @ping_received = true 6 | end 7 | 8 | # Post 9 | @ping_received = false 10 | Nanoc::Int::NotificationCenter.post :ping_received 11 | assert(@ping_received) 12 | end 13 | 14 | def test_remove 15 | # Set up notification 16 | Nanoc::Int::NotificationCenter.on :ping_received, :test do 17 | @ping_received = true 18 | end 19 | 20 | # Remove observer 21 | Nanoc::Int::NotificationCenter.remove :ping_received, :test 22 | 23 | # Post 24 | @ping_received = false 25 | Nanoc::Int::NotificationCenter.post :ping_received 26 | assert(!@ping_received) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test/filters/test_kramdown.rb: -------------------------------------------------------------------------------- 1 | class Nanoc::Filters::KramdownTest < Nanoc::TestCase 2 | def test_filter 3 | if_have 'kramdown' do 4 | # Create filter 5 | filter = ::Nanoc::Filters::Kramdown.new 6 | 7 | # Run filter 8 | result = filter.setup_and_run('This is _so_ **cool**!') 9 | assert_equal("

This is so cool!

\n", result) 10 | end 11 | end 12 | 13 | def test_warnings 14 | if_have 'kramdown' do 15 | # Create filter 16 | filter = ::Nanoc::Filters::Kramdown.new 17 | 18 | # Run filter 19 | io = capturing_stdio do 20 | filter.setup_and_run('{:foo}this is bogus') 21 | end 22 | assert_empty io[:stdout] 23 | assert_equal "kramdown warning: Found span IAL after text - ignoring it\n", io[:stderr] 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/nanoc/base/views/config.rb: -------------------------------------------------------------------------------- 1 | module Nanoc 2 | class ConfigView 3 | # @api private 4 | NONE = Object.new 5 | 6 | # @api private 7 | def initialize(config) 8 | @config = config 9 | end 10 | 11 | # @api private 12 | def unwrap 13 | @config 14 | end 15 | 16 | # @see Hash#fetch 17 | def fetch(key, fallback = NONE, &_block) 18 | @config.fetch(key) do 19 | if !fallback.equal?(NONE) 20 | fallback 21 | elsif block_given? 22 | yield(key) 23 | else 24 | raise KeyError, "key not found: #{key.inspect}" 25 | end 26 | end 27 | end 28 | 29 | # @see Hash#key? 30 | def key?(key) 31 | @config.key?(key) 32 | end 33 | 34 | # @see Hash#[] 35 | def [](key) 36 | @config[key] 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/nanoc/cli/commands/shell.rb: -------------------------------------------------------------------------------- 1 | usage 'shell' 2 | summary 'open a shell on the nanoc environment' 3 | aliases 'console' 4 | description " 5 | Open an IRB shell on a context that contains @items, @layouts, and @config. 6 | " 7 | 8 | module Nanoc::CLI::Commands 9 | class Shell < ::Nanoc::CLI::CommandRunner 10 | def run 11 | require 'pry' 12 | 13 | require_site 14 | 15 | Nanoc::Int::Context.new(env).pry 16 | end 17 | 18 | protected 19 | 20 | def env 21 | self.class.env_for_site(site) 22 | end 23 | 24 | def self.env_for(site) 25 | { 26 | items: Nanoc::ItemCollectionView.new(site.items), 27 | layouts: Nanoc::LayoutCollectionView.new(site.layouts), 28 | config: Nanoc::ConfigView.new(site.config), 29 | } 30 | end 31 | end 32 | end 33 | 34 | runner Nanoc::CLI::Commands::Shell 35 | -------------------------------------------------------------------------------- /test/base/test_store.rb: -------------------------------------------------------------------------------- 1 | class Nanoc::Int::StoreTest < Nanoc::TestCase 2 | class TestStore < Nanoc::Int::Store 3 | def data 4 | @data 5 | end 6 | 7 | def data=(new_data) 8 | @data = new_data 9 | end 10 | end 11 | 12 | def test_delete_and_reload_on_error 13 | store = TestStore.new('test.db', 1) 14 | 15 | # Create 16 | store.load 17 | store.data = { fun: 'sure' } 18 | store.store 19 | 20 | # Test stored values 21 | store = TestStore.new('test.db', 1) 22 | store.load 23 | assert_equal({ fun: 'sure' }, store.data) 24 | 25 | # Mess up 26 | File.open('test.db', 'w') do |io| 27 | io << 'Damn {}#}%@}$^)@&$&*^#@ broken stores!!!' 28 | end 29 | 30 | # Reload 31 | store = TestStore.new('test.db', 1) 32 | store.load 33 | assert_equal(nil, store.data) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/nanoc/base/views/mutable_item_spec.rb: -------------------------------------------------------------------------------- 1 | describe Nanoc::MutableItemView do 2 | describe '#[]=' do 3 | let(:item) { Nanoc::Int::Item.new('content', {}, '/asdf/') } 4 | let(:view) { described_class.new(item) } 5 | 6 | it 'sets attributes' do 7 | view[:title] = 'Donkey' 8 | expect(view[:title]).to eq('Donkey') 9 | end 10 | end 11 | 12 | describe '#update_attributes' do 13 | let(:item) { Nanoc::Int::Item.new('content', {}, '/asdf/') } 14 | let(:view) { described_class.new(item) } 15 | 16 | let(:update) { { friend: 'Giraffe' } } 17 | 18 | subject { view.update_attributes(update) } 19 | 20 | it 'sets attributes' do 21 | expect { subject }.to change { view[:friend] }.from(nil).to('Giraffe') 22 | end 23 | 24 | it 'returns self' do 25 | expect(subject).to equal(view) 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /doc/yardoc_templates/default/layout/html/footer.erb: -------------------------------------------------------------------------------- 1 | <%= superb %> 2 | 20 | -------------------------------------------------------------------------------- /lib/nanoc/base/views/layout.rb: -------------------------------------------------------------------------------- 1 | module Nanoc 2 | class LayoutView 3 | # @api private 4 | def initialize(layout) 5 | @layout = layout 6 | end 7 | 8 | # @api private 9 | def unwrap 10 | @layout 11 | end 12 | 13 | # @see Object#== 14 | def ==(other) 15 | identifier == other.identifier 16 | end 17 | alias_method :eql?, :== 18 | 19 | # @see Object#hash 20 | def hash 21 | self.class.hash ^ identifier.hash 22 | end 23 | 24 | # @return [Nanoc::Identifier] 25 | def identifier 26 | @layout.identifier 27 | end 28 | 29 | # @see Hash#[] 30 | def [](key) 31 | @layout[key] 32 | end 33 | 34 | # @api private 35 | def reference 36 | @layout.reference 37 | end 38 | 39 | # @api private 40 | def raw_content 41 | @layout.raw_content 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /test/filters/test_rdiscount.rb: -------------------------------------------------------------------------------- 1 | class Nanoc::Filters::RDiscountTest < Nanoc::TestCase 2 | def test_filter 3 | if_have 'rdiscount' do 4 | # Create filter 5 | filter = ::Nanoc::Filters::RDiscount.new 6 | 7 | # Run filter 8 | result = filter.setup_and_run('> Quote') 9 | assert_match(/
\s*

Quote<\/p>\s*<\/blockquote>/, result) 10 | end 11 | end 12 | 13 | def test_with_extensions 14 | if_have 'rdiscount' do 15 | # Create filter 16 | filter = ::Nanoc::Filters::RDiscount.new 17 | 18 | # Run filter 19 | input = "The quotation 'marks' sure make this look sarcastic!" 20 | output_expected = /The quotation ‘marks’ sure make this look sarcastic!/ 21 | output_actual = filter.setup_and_run(input, extensions: [:smart]) 22 | assert_match(output_expected, output_actual) 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/nanoc/base/views/mutable_identifiable_collection_spec.rb: -------------------------------------------------------------------------------- 1 | shared_examples 'a mutable identifiable collection' do 2 | let(:view) { described_class.new(wrapped) } 3 | 4 | let(:config) do 5 | {} 6 | end 7 | 8 | describe '#delete_if' do 9 | let(:wrapped) do 10 | Nanoc::Int::IdentifiableCollection.new(config).tap do |coll| 11 | coll << double(:identifiable, identifier: Nanoc::Identifier.new('/asdf/')) 12 | end 13 | end 14 | 15 | it 'deletes matching' do 16 | view.delete_if { |i| i.identifier == '/asdf/' } 17 | expect(wrapped).to be_empty 18 | end 19 | 20 | it 'deletes no non-matching' do 21 | view.delete_if { |i| i.identifier == '/blah/' } 22 | expect(wrapped).not_to be_empty 23 | end 24 | 25 | it 'returns self' do 26 | ret = view.delete_if { |_i| false } 27 | expect(ret).to equal(view) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/filters/test_redcloth.rb: -------------------------------------------------------------------------------- 1 | class Nanoc::Filters::RedClothTest < Nanoc::TestCase 2 | def test_filter 3 | if_have 'redcloth' do 4 | # Get filter 5 | filter = ::Nanoc::Filters::RedCloth.new 6 | 7 | # Run filter 8 | result = filter.setup_and_run('h1. Foo') 9 | assert_equal('

Foo

', result) 10 | end 11 | end 12 | 13 | def test_filter_with_options 14 | if_have 'redcloth' do 15 | # Get filter 16 | filter = ::Nanoc::Filters::RedCloth.new 17 | 18 | # Run filter without options 19 | result = filter.setup_and_run('I am a member of SPECTRE.') 20 | assert_equal("

I am a member of SPECTRE.

", result) 21 | 22 | # Run filter with options 23 | result = filter.setup_and_run('I am a member of SPECTRE.', no_span_caps: true) 24 | assert_equal('

I am a member of SPECTRE.

', result) 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/nanoc/filters/sass.rb: -------------------------------------------------------------------------------- 1 | module Nanoc::Filters 2 | # @api private 3 | class Sass < Nanoc::Filter 4 | requires 'sass', 'nanoc/filters/sass/sass_filesystem_importer' 5 | 6 | # Runs the content through [Sass](http://sass-lang.com/). 7 | # Parameters passed to this filter will be passed on to Sass. 8 | # 9 | # @param [String] content The content to filter 10 | # 11 | # @return [String] The filtered content 12 | def run(content, params = {}) 13 | options = params.merge({ 14 | nanoc_current_filter: self, 15 | filename: @item && @item.raw_filename, 16 | }) 17 | engine = ::Sass::Engine.new(content, options) 18 | engine.render 19 | end 20 | 21 | def imported_filename_to_item(filename) 22 | @items.find do |i| 23 | i.raw_filename && 24 | Pathname.new(i.raw_filename).realpath == Pathname.new(filename).realpath 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test/filters/test_uglify_js.rb: -------------------------------------------------------------------------------- 1 | class Nanoc::Filters::UglifyJSTest < Nanoc::TestCase 2 | def test_filter 3 | if_have 'uglifier' do 4 | # Create filter 5 | filter = ::Nanoc::Filters::UglifyJS.new 6 | 7 | # Run filter 8 | input = 'foo = 1; (function(bar) { if (true) alert(bar); })(foo)' 9 | result = filter.setup_and_run(input) 10 | assert_match(/foo=1,function\((.)\)\{alert\(\1\)\}\(foo\);/, result) 11 | end 12 | end 13 | 14 | def test_filter_with_options 15 | if_have 'uglifier' do 16 | filter = ::Nanoc::Filters::UglifyJS.new 17 | input = "if(donkey) alert('It is a donkey!');" 18 | 19 | result = filter.setup_and_run(input, output: { beautify: false }) 20 | assert_equal 'donkey&&alert("It is a donkey!");', result 21 | 22 | result = filter.setup_and_run(input, output: { beautify: true }) 23 | assert_equal 'donkey && alert("It is a donkey!");', result 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | Reporting bugs 5 | -------------- 6 | 7 | If you find a bug in nanoc, you should report it! Some information that you should include in your bug report is the nanoc version (`nanoc --version`) and, if relevant, the crash log (`crash.log`). 8 | 9 | For details, check the [*bug reporting* section of the development guide](http://nanoc.ws/development/#reporting-bugs). 10 | 11 | Contributing code 12 | ----------------- 13 | 14 | Pull requests are appreciated! When submitting a PR, be sure to submit it onto the right branch: 15 | 16 | * For bug fixes, use the release branch, e.g. `release-3.6.x` 17 | * For features, use the `master` branch 18 | 19 | When submitting a PR, make sure that your changes have covering tests, that the documentation remains up-to-date and that you retain backwards compatibility. 20 | 21 | For details, check the [*contributing code* section of the development guide](http://nanoc.ws/development/#contributing-code). 22 | -------------------------------------------------------------------------------- /lib/nanoc/filters/handlebars.rb: -------------------------------------------------------------------------------- 1 | module Nanoc::Filters 2 | # @since 3.4.0 3 | # 4 | # @api private 5 | class Handlebars < Nanoc::Filter 6 | requires 'handlebars' 7 | 8 | # Runs the content through 9 | # [Handlebars](http://handlebarsjs.com/) using 10 | # [Handlebars.rb](https://github.com/cowboyd/handlebars.rb). 11 | # This method takes no options. 12 | # 13 | # @param [String] content The content to filter 14 | # 15 | # @return [String] The filtered content 16 | def run(content, _params = {}) 17 | context = item.attributes.dup 18 | context[:item] = assigns[:item].attributes 19 | context[:config] = assigns[:config] 20 | context[:yield] = assigns[:content] 21 | if assigns.key?(:layout) 22 | context[:layout] = assigns[:layout].attributes 23 | end 24 | 25 | handlebars = ::Handlebars::Context.new 26 | template = handlebars.compile(content) 27 | template.call(context) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/nanoc/helpers/breadcrumbs.rb: -------------------------------------------------------------------------------- 1 | module Nanoc::Helpers 2 | # Provides support for breadcrumbs, which allow the user to go up in the 3 | # page hierarchy. 4 | module Breadcrumbs 5 | # Creates a breadcrumb trail leading from the current item to its parent, 6 | # to its parent’s parent, etc, until the root item is reached. This 7 | # function does not require that each intermediate item exist; for 8 | # example, if there is no `/foo/` item, breadcrumbs for a `/foo/bar/` item 9 | # will contain a `nil` element. 10 | # 11 | # @return [Array] The breadcrumbs, starting with the root item and ending 12 | # with the item itself 13 | def breadcrumbs_trail 14 | trail = [] 15 | idx_start = 0 16 | 17 | loop do 18 | idx = @item.identifier.to_s.index('/', idx_start) 19 | break if idx.nil? 20 | 21 | idx_start = idx + 1 22 | identifier = @item.identifier.to_s[0..idx] 23 | trail << @items[identifier] 24 | end 25 | 26 | trail 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /test/filters/test_slim.rb: -------------------------------------------------------------------------------- 1 | class Nanoc::Filters::SlimTest < Nanoc::TestCase 2 | def test_filter 3 | if_have 'slim' do 4 | # Create filter 5 | filter = ::Nanoc::Filters::Slim.new({ rabbit: 'The rabbit is on the branch.' }) 6 | 7 | # Run filter (no assigns) 8 | result = filter.setup_and_run('html') 9 | assert_match(/.*<\/html>/, result) 10 | 11 | # Run filter (assigns without @) 12 | result = filter.setup_and_run('p = rabbit') 13 | assert_equal('

The rabbit is on the branch.

', result) 14 | 15 | # Run filter (assigns with @) 16 | result = filter.setup_and_run('p = @rabbit') 17 | assert_equal('

The rabbit is on the branch.

', result) 18 | end 19 | end 20 | 21 | def test_filter_with_yield 22 | if_have 'slim' do 23 | filter = ::Nanoc::Filters::Slim.new({ content: 'The rabbit is on the branch.' }) 24 | 25 | result = filter.setup_and_run('p = yield') 26 | assert_equal('

The rabbit is on the branch.

', result) 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /test/base/core_ext/string_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'String#__nanoc_cleaned_identifier' do 2 | it 'should not convert already clean paths' do 3 | '/foo/bar/'.__nanoc_cleaned_identifier.must_equal '/foo/bar/' 4 | end 5 | 6 | it 'should prepend slash if necessary' do 7 | 'foo/bar/'.__nanoc_cleaned_identifier.must_equal '/foo/bar/' 8 | end 9 | 10 | it 'should append slash if necessary' do 11 | '/foo/bar'.__nanoc_cleaned_identifier.must_equal '/foo/bar/' 12 | end 13 | 14 | it 'should remove double slashes at start' do 15 | '//foo/bar/'.__nanoc_cleaned_identifier.must_equal '/foo/bar/' 16 | end 17 | 18 | it 'should remove double slashes at end' do 19 | '/foo/bar//'.__nanoc_cleaned_identifier.must_equal '/foo/bar/' 20 | end 21 | end 22 | 23 | describe 'String#__nanoc_checksum' do 24 | it 'should work on empty strings' do 25 | ''.__nanoc_checksum.must_equal 'PfY7essFItpoXa1f6EuB/deyUmQ=' 26 | end 27 | 28 | it 'should work on all strings' do 29 | 'abc'.__nanoc_checksum.must_equal 'NkkYRO+25f6psNSeCYykXKCg3C0=' 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/nanoc/extra/checking/checks/mixed_content.rb: -------------------------------------------------------------------------------- 1 | module Nanoc::Extra::Checking::Checks 2 | # A check that verifies HTML files do not reference external resources with 3 | # URLs that would trigger "mixed content" warnings. 4 | # 5 | # @api private 6 | class MixedContent < ::Nanoc::Extra::Checking::Check 7 | PROTOCOL_PATTERN = /^(\w+):\/\// 8 | 9 | def run 10 | filenames = output_filenames.select { |f| File.extname(f) == '.html' } 11 | resource_uris_with_filenames = ::Nanoc::Extra::LinkCollector.new(filenames).filenames_per_resource_uri 12 | 13 | resource_uris_with_filenames.each_pair do |uri, fns| 14 | next unless guaranteed_insecure?(uri) 15 | fns.each do |filename| 16 | add_issue( 17 | "mixed content include: #{uri}", 18 | subject: filename) 19 | end 20 | end 21 | end 22 | 23 | private 24 | 25 | def guaranteed_insecure?(href) 26 | protocol = PROTOCOL_PATTERN.match(href) 27 | 28 | protocol && protocol[1].downcase == 'http' 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/nanoc/base/views/mutable_item_collection_spec.rb: -------------------------------------------------------------------------------- 1 | describe Nanoc::MutableItemCollectionView do 2 | let(:view_class) { Nanoc::MutableItemView } 3 | it_behaves_like 'an identifiable collection' 4 | it_behaves_like 'a mutable identifiable collection' 5 | 6 | let(:config) do 7 | { string_pattern_type: 'glob' } 8 | end 9 | 10 | describe '#create' do 11 | let(:item) do 12 | Nanoc::Int::Layout.new('content', {}, '/asdf/') 13 | end 14 | 15 | let(:wrapped) do 16 | Nanoc::Int::IdentifiableCollection.new(config).tap do |coll| 17 | coll << item 18 | end 19 | end 20 | 21 | let(:view) { described_class.new(wrapped) } 22 | 23 | it 'creates an object' do 24 | view.create('new content', { title: 'New Page' }, '/new/') 25 | 26 | expect(wrapped.size).to eq(2) 27 | expect(wrapped['/new/'].raw_content).to eq('new content') 28 | end 29 | 30 | it 'returns self' do 31 | ret = view.create('new content', { title: 'New Page' }, '/new/') 32 | expect(ret).to equal(view) 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/nanoc/base/views/mutable_item_collection.rb: -------------------------------------------------------------------------------- 1 | module Nanoc 2 | class MutableItemCollectionView < Nanoc::MutableIdentifiableCollectionView 3 | # @api private 4 | def view_class 5 | Nanoc::MutableItemView 6 | end 7 | 8 | # Creates a new item and adds it to the site’s collection of items. 9 | # 10 | # @param [String] content The uncompiled item content (if it is a textual 11 | # item) or the path to the filename containing the content (if it is a 12 | # binary item). 13 | # 14 | # @param [Hash] attributes A hash containing this item's attributes. 15 | # 16 | # @param [Nanoc::Identifier, String] identifier This item's identifier. 17 | # 18 | # @param [Hash] params Extra parameters. 19 | # 20 | # @option params [Symbol, nil] :binary (true) Whether or not this item is 21 | # binary 22 | # 23 | # @return [self] 24 | def create(content, attributes, identifier, params = {}) 25 | @objects << Nanoc::Int::Item.new(content, attributes, identifier, params) 26 | self 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /test/base/core_ext/pathname_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'Pathname#checksum' do 2 | it 'should work on empty files' do 3 | begin 4 | # Create file 5 | FileUtils.mkdir_p('tmp') 6 | File.open('tmp/myfile', 'w') { |io| io.write('') } 7 | timestamp = Time.at(1_234_569) 8 | File.utime(timestamp, timestamp, 'tmp/myfile') 9 | 10 | # Create checksum 11 | pathname = Pathname.new('tmp/myfile') 12 | pathname.__nanoc_checksum.must_equal 'oU+0fYgGm4EDTl+uErBv8rB9YhU=' 13 | ensure 14 | FileUtils.rm_rf('tmp') 15 | end 16 | end 17 | 18 | it 'should work on all files' do 19 | begin 20 | # Create file 21 | FileUtils.mkdir_p('tmp') 22 | File.open('tmp/myfile', 'w') { |io| io.write('abc') } 23 | timestamp = Time.at(1_234_569) 24 | File.utime(timestamp, timestamp, 'tmp/myfile') 25 | 26 | # Create checksum 27 | pathname = Pathname.new('tmp/myfile') 28 | pathname.__nanoc_checksum.must_equal 'IAoqYXvcDheQjaYmZ8waPtEO8zU=' 29 | ensure 30 | FileUtils.rm_rf('tmp') 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/nanoc/base/views/mutable_layout_collection_spec.rb: -------------------------------------------------------------------------------- 1 | describe Nanoc::MutableLayoutCollectionView do 2 | let(:view_class) { Nanoc::MutableLayoutView } 3 | it_behaves_like 'an identifiable collection' 4 | it_behaves_like 'a mutable identifiable collection' 5 | 6 | let(:config) do 7 | { string_pattern_type: 'glob' } 8 | end 9 | 10 | describe '#create' do 11 | let(:layout) do 12 | Nanoc::Int::Layout.new('content', {}, '/asdf/') 13 | end 14 | 15 | let(:wrapped) do 16 | Nanoc::Int::IdentifiableCollection.new(config).tap do |coll| 17 | coll << layout 18 | end 19 | end 20 | 21 | let(:view) { described_class.new(wrapped) } 22 | 23 | it 'creates an object' do 24 | view.create('new content', { title: 'New Page' }, '/new/') 25 | 26 | expect(wrapped.size).to eq(2) 27 | expect(wrapped['/new/'].raw_content).to eq('new content') 28 | end 29 | 30 | it 'returns self' do 31 | ret = view.create('new content', { title: 'New Page' }, '/new/') 32 | expect(ret).to equal(view) 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/nanoc/filters/erubis.rb: -------------------------------------------------------------------------------- 1 | module Nanoc::Filters 2 | # @api private 3 | class Erubis < Nanoc::Filter 4 | requires 'erubis' 5 | 6 | # The same as `::Erubis::Eruby` but adds `_erbout` as an alias for the 7 | # `_buf` variable, making it compatible with nanoc’s helpers that rely 8 | # on `_erbout`, such as {Nanoc::Helpers::Capturing}. 9 | class ErubisWithErbout < ::Erubis::Eruby 10 | include ::Erubis::ErboutEnhancer 11 | end 12 | 13 | # Runs the content through [Erubis](http://www.kuwata-lab.com/erubis/). 14 | # This method takes no options. 15 | # 16 | # @param [String] content The content to filter 17 | # 18 | # @return [String] The filtered content 19 | def run(content, _params = {}) 20 | # Create context 21 | context = ::Nanoc::Int::Context.new(assigns) 22 | 23 | # Get binding 24 | proc = assigns[:content] ? -> { assigns[:content] } : nil 25 | assigns_binding = context.get_binding(&proc) 26 | 27 | # Get result 28 | ErubisWithErbout.new(content, filename: filename).result(assigns_binding) 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /doc/yardoc_handlers/identifier.rb: -------------------------------------------------------------------------------- 1 | class NanocIdentifierHandler < ::YARD::Handlers::Ruby::AttributeHandler 2 | # e.g. identifier :foo, :bar 3 | 4 | handles method_call(:identifier), method_call(:identifiers) 5 | namespace_only 6 | 7 | def process 8 | identifiers = statement.parameters(false).map { |param| param.jump(:ident)[0] } 9 | namespace['nanoc_identifiers'] = identifiers 10 | end 11 | end 12 | 13 | class NanocRegisterFilterHandler < ::YARD::Handlers::Ruby::AttributeHandler 14 | # e.g. Nanoc::Filter.register '::Nanoc::Filters::AsciiDoc', :asciidoc 15 | 16 | handles method_call(:register) 17 | namespace_only 18 | 19 | def process 20 | target = statement.jump(:const_path_ref) 21 | return if target != s(:const_path_ref, s(:var_ref, s(:const, 'Nanoc')), s(:const, 'Filter')) 22 | 23 | class_name = statement.jump(:string_literal).jump(:tstring_content)[0] 24 | identifier = statement.jump(:symbol_literal).jump(:ident)[0] 25 | 26 | obj = YARD::Registry.at(class_name.sub(/^::/, '')) 27 | obj['nanoc_identifiers'] ||= [] 28 | obj['nanoc_identifiers'] << identifier 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /tasks/test.rake: -------------------------------------------------------------------------------- 1 | require 'rspec/core/rake_task' 2 | 3 | def run_tests(dir_glob) 4 | ENV['ARGS'] ||= '' 5 | ENV['QUIET'] ||= 'true' 6 | 7 | $LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + '/..')) 8 | 9 | # require our test helper so we don't have to in each individual test 10 | require 'test/helper' 11 | 12 | test_files = Dir["#{dir_glob}*_spec.rb"] + Dir["#{dir_glob}test_*.rb"] 13 | test_files.each { |f| require f } 14 | 15 | res = Minitest.run(ENV['ARGS'].split) 16 | exit(res) if res != 0 17 | end 18 | 19 | namespace :test do 20 | # test:all 21 | desc 'Run all tests' 22 | task :all do 23 | run_tests 'test/**/' 24 | end 25 | 26 | # test:... 27 | %w( base cli data_sources extra filters helpers ).each do |dir| 28 | desc "Run all #{dir} tests" 29 | task dir.to_sym do |_task| 30 | run_tests "test/#{dir}/**/" 31 | end 32 | end 33 | end 34 | 35 | RSpec::Core::RakeTask.new(:spec) do |t| 36 | t.rspec_opts = '-r ./spec/spec_helper.rb --color' 37 | t.verbose = false 38 | end 39 | 40 | desc 'Alias for test:all + rubocop' 41 | task test: [:spec, :'test:all', :rubocop] 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2007-2015 Denis Defreyne and contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /lib/nanoc/cli/commands/nanoc.rb: -------------------------------------------------------------------------------- 1 | usage 'nanoc command [options] [arguments]' 2 | summary 'nanoc, a static site compiler written in Ruby' 3 | 4 | opt :l, :color, 'enable color' do 5 | $stdout.remove_stream_cleaner(Nanoc::CLI::StreamCleaners::ANSIColors) 6 | $stderr.remove_stream_cleaner(Nanoc::CLI::StreamCleaners::ANSIColors) 7 | end 8 | 9 | opt :d, :debug, 'enable debugging' do 10 | Nanoc::CLI.debug = true 11 | end 12 | 13 | opt :h, :help, 'show the help message and quit' do |_value, cmd| 14 | puts cmd.help 15 | exit 0 16 | end 17 | 18 | opt :C, :'no-color', 'disable color' do 19 | $stdout.add_stream_cleaner(Nanoc::CLI::StreamCleaners::ANSIColors) 20 | $stderr.add_stream_cleaner(Nanoc::CLI::StreamCleaners::ANSIColors) 21 | end 22 | 23 | opt :V, :verbose, 'make nanoc output more detailed' do 24 | Nanoc::CLI::Logger.instance.level = :low 25 | end 26 | 27 | opt :v, :version, 'show version information and quit' do 28 | puts Nanoc.version_information 29 | exit 0 30 | end 31 | 32 | opt :w, :warn, 'enable warnings' do 33 | $-w = true 34 | end 35 | 36 | run do |_opts, _args, cmd| 37 | cmd.command_named('compile').run([]) 38 | end 39 | -------------------------------------------------------------------------------- /test/base/test_layout.rb: -------------------------------------------------------------------------------- 1 | class Nanoc::Int::LayoutTest < Nanoc::TestCase 2 | def test_initialize 3 | # Make sure attributes are cleaned 4 | layout = Nanoc::Int::Layout.new('content', { 'foo' => 'bar' }, '/foo') 5 | assert_equal({ foo: 'bar' }, layout.attributes) 6 | end 7 | 8 | def test_lookup_with_known_attribute 9 | # Create layout 10 | layout = Nanoc::Int::Layout.new('content', { 'foo' => 'bar' }, '/foo/') 11 | 12 | # Check attributes 13 | assert_equal('bar', layout[:foo]) 14 | end 15 | 16 | def test_lookup_with_unknown_attribute 17 | # Create layout 18 | layout = Nanoc::Int::Layout.new('content', { 'foo' => 'bar' }, '/foo/') 19 | 20 | # Check attributes 21 | assert_equal(nil, layout[:filter]) 22 | end 23 | 24 | def test_dump_and_load 25 | layout = Nanoc::Int::Layout.new( 26 | 'foobar', 27 | { a: { b: 123 } }, 28 | '/foo/') 29 | 30 | layout = Marshal.load(Marshal.dump(layout)) 31 | 32 | assert_equal Nanoc::Identifier.new('/foo/'), layout.identifier 33 | assert_equal 'foobar', layout.raw_content 34 | assert_equal({ a: { b: 123 } }, layout.attributes) 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/nanoc/filters/erb.rb: -------------------------------------------------------------------------------- 1 | module Nanoc::Filters 2 | # @api private 3 | class ERB < Nanoc::Filter 4 | requires 'erb' 5 | 6 | # Runs the content through [ERB](http://ruby-doc.org/stdlib/libdoc/erb/rdoc/classes/ERB.html). 7 | # 8 | # @param [String] content The content to filter 9 | # 10 | # @option params [Integer] :safe_level (nil) The safe level (`$SAFE`) to 11 | # use while running this filter 12 | # 13 | # @option params [String] :trim_mode (nil) The trim mode to use 14 | # 15 | # @return [String] The filtered content 16 | def run(content, params = {}) 17 | # Add locals 18 | assigns.merge!(params[:locals] || {}) 19 | 20 | # Create context 21 | context = ::Nanoc::Int::Context.new(assigns) 22 | 23 | # Get binding 24 | proc = assigns[:content] ? -> { assigns[:content] } : nil 25 | assigns_binding = context.get_binding(&proc) 26 | 27 | # Get result 28 | safe_level = params[:safe_level] 29 | trim_mode = params[:trim_mode] 30 | erb = ::ERB.new(content, safe_level, trim_mode) 31 | erb.filename = filename 32 | erb.result(assigns_binding) 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/nanoc/base/compilation/rule_memory_calculator.rb: -------------------------------------------------------------------------------- 1 | module Nanoc::Int 2 | # Calculates rule memories for objects that can be run through a rule (item 3 | # representations and layouts). 4 | # 5 | # @api private 6 | class RuleMemoryCalculator 7 | extend Nanoc::Int::Memoization 8 | 9 | # @option params [Nanoc::Int::RulesCollection] rules_collection The rules 10 | # collection 11 | def initialize(params = {}) 12 | @rules_collection = params.fetch(:rules_collection) do 13 | raise ArgumentError, 'Required :rules_collection option is missing' 14 | end 15 | end 16 | 17 | # @param [#reference] obj The object to calculate the rule memory for 18 | # 19 | # @return [Array] The caluclated rule memory for the given object 20 | def [](obj) 21 | result = 22 | case obj.type 23 | when :item_rep 24 | @rules_collection.new_rule_memory_for_rep(obj) 25 | when :layout 26 | @rules_collection.new_rule_memory_for_layout(obj) 27 | else 28 | raise "Do not know how to calculate the rule memory for #{obj.inspect}" 29 | end 30 | 31 | result 32 | end 33 | memoize :[] 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | # FIXME: we may be missing some mswin dependencies here 6 | 7 | gem 'adsf' 8 | gem 'bluecloth', platforms: :ruby 9 | gem 'builder' 10 | gem 'coderay' 11 | gem 'compass' 12 | gem 'coffee-script' 13 | gem 'coveralls', require: false 14 | gem 'erubis' 15 | gem 'fog' 16 | gem 'haml' 17 | gem 'handlebars', platforms: :ruby 18 | gem 'kramdown' 19 | gem 'less', '~> 2.0', platforms: :ruby 20 | gem 'listen' 21 | gem 'markaby' 22 | gem 'maruku' 23 | gem 'mime-types' 24 | gem 'minitest', '~> 5.0' 25 | gem 'mocha' 26 | if RUBY_VERSION >= '2.0.0' 27 | gem 'mustache', '~> 1.0' 28 | else 29 | gem 'mustache', '~> 0.99' 30 | end 31 | gem 'nokogiri', '~> 1.6' 32 | gem 'pandoc-ruby' 33 | gem 'pry' 34 | gem 'pygments.rb', platforms: [:ruby, :mswin] 35 | gem 'rack' 36 | gem 'rake' 37 | gem 'rainpress' 38 | gem 'rdiscount', platforms: [:ruby, :mswin] 39 | gem 'rdoc' 40 | gem 'redcarpet', platforms: [:ruby, :mswin] 41 | gem 'RedCloth' 42 | gem 'rouge' 43 | gem 'rspec' 44 | gem 'rubocop' 45 | gem 'rubypants' 46 | gem 'sass', '~> 3.2.2' 47 | gem 'slim' 48 | gem 'typogruby' 49 | gem 'uglifier' 50 | gem 'vcr' 51 | gem 'w3c_validators' 52 | gem 'webmock' 53 | gem 'yuicompressor' 54 | gem 'yard' 55 | -------------------------------------------------------------------------------- /test/filters/test_mustache.rb: -------------------------------------------------------------------------------- 1 | class Nanoc::Filters::MustacheTest < Nanoc::TestCase 2 | def test_filter 3 | if_have 'mustache' do 4 | # Create item 5 | item = Nanoc::Int::Item.new( 6 | 'content', 7 | { title: 'Max Payne', protagonist: 'Max Payne' }, 8 | '/games/max-payne/' 9 | ) 10 | 11 | # Create filter 12 | filter = ::Nanoc::Filters::Mustache.new({ item: item }) 13 | 14 | # Run filter 15 | result = filter.setup_and_run('The protagonist of {{title}} is {{protagonist}}.') 16 | assert_equal('The protagonist of Max Payne is Max Payne.', result) 17 | end 18 | end 19 | 20 | def test_filter_with_yield 21 | if_have 'mustache' do 22 | # Create item 23 | item = Nanoc::Int::Item.new( 24 | 'content', 25 | { title: 'Max Payne', protagonist: 'Max Payne' }, 26 | '/games/max-payne/' 27 | ) 28 | 29 | # Create filter 30 | filter = ::Nanoc::Filters::Mustache.new( 31 | { content: 'No Payne No Gayne', item: item }) 32 | 33 | # Run filter 34 | result = filter.setup_and_run('Max says: {{yield}}.') 35 | assert_equal('Max says: No Payne No Gayne.', result) 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /test/filters/test_yui_compressor.rb: -------------------------------------------------------------------------------- 1 | class Nanoc::Filters::YUICompressorTest < Nanoc::TestCase 2 | def test_filter_javascript 3 | if_have 'yuicompressor' do 4 | filter = ::Nanoc::Filters::YUICompressor.new 5 | 6 | sample_js = <<-JAVASCRIPT 7 | function factorial(n) { 8 | var result = 1; 9 | for (var i = 2; i <= n; i++) { 10 | result *= i 11 | } 12 | return result; 13 | } 14 | JAVASCRIPT 15 | 16 | result = filter.setup_and_run(sample_js, { type: 'js', munge: true }) 17 | assert_match 'function factorial(c){var a=1;for(var b=2;b<=c;b++){a*=b}return a};', result 18 | 19 | result = filter.setup_and_run(sample_js, { type: 'js', munge: false }) 20 | assert_match 'function factorial(n){var result=1;for(var i=2;i<=n;i++){result*=i}return result};', result 21 | end 22 | end 23 | 24 | def test_filter_css 25 | if_have 'yuicompressor' do 26 | filter = ::Nanoc::Filters::YUICompressor.new 27 | 28 | sample_css = <<-CSS 29 | * { 30 | margin: 0; 31 | } 32 | CSS 33 | 34 | result = filter.setup_and_run(sample_css, { type: 'css' }) 35 | assert_match '*{margin:0}', result 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /test/extra/test_piper.rb: -------------------------------------------------------------------------------- 1 | class Nanoc::Extra::PiperTest < Nanoc::TestCase 2 | def test_basic 3 | stdout = StringIO.new 4 | stderr = StringIO.new 5 | 6 | cmd = %w( ls -l ) 7 | 8 | File.open('foo.txt', 'w') { |io| io.write('hi') } 9 | File.open('bar.txt', 'w') { |io| io.write('ho') } 10 | 11 | piper = Nanoc::Extra::Piper.new(stdout: stdout, stderr: stderr) 12 | piper.run(cmd, nil) 13 | 14 | assert_match(/foo\.txt/, stdout.string) 15 | assert_match(/bar\.txt/, stdout.string) 16 | assert stderr.string.empty? 17 | end 18 | 19 | def test_stdin 20 | stdout = StringIO.new 21 | stderr = StringIO.new 22 | 23 | input = 'Hello World!' 24 | cmd = %w( cat ) 25 | 26 | piper = Nanoc::Extra::Piper.new(stdout: stdout, stderr: stderr) 27 | piper.run(cmd, input) 28 | 29 | assert_equal(input, stdout.string) 30 | assert_equal('', stderr.string) 31 | end 32 | 33 | def test_no_such_command 34 | stdout = StringIO.new 35 | stderr = StringIO.new 36 | 37 | cmd = %w( cat kafhawilgoiwaejagoualjdsfilofiewaguihaifeowuiga ) 38 | 39 | piper = Nanoc::Extra::Piper.new(stdout: stdout, stderr: stderr) 40 | assert_raises(Nanoc::Extra::Piper::Error) do 41 | piper.run(cmd, nil) 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /nanoc.gemspec: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.expand_path('../lib/', __FILE__)) 2 | require 'nanoc/version' 3 | 4 | Gem::Specification.new do |s| 5 | s.name = 'nanoc' 6 | s.version = Nanoc::VERSION 7 | s.homepage = 'http://nanoc.ws/' 8 | s.summary = 'a web publishing system written in Ruby for building small to medium-sized websites.' 9 | s.description = 'nanoc is a simple but very flexible static site generator written in Ruby. It operates on local files, and therefore does not run on the server. nanoc “compiles” the local source files into HTML (usually), by evaluating eRuby, Markdown, etc.' 10 | 11 | s.author = 'Denis Defreyne' 12 | s.email = 'denis.defreyne@stoneship.org' 13 | s.license = 'MIT' 14 | 15 | s.files = 16 | Dir['[A-Z]*'] + 17 | Dir['doc/yardoc_{templates,handlers}/**/*'] + 18 | Dir['{bin,lib,tasks,test}/**/*'] + 19 | ['nanoc.gemspec'] 20 | s.executables = ['nanoc'] 21 | s.require_paths = ['lib'] 22 | 23 | s.rdoc_options = ['--main', 'README.md'] 24 | s.extra_rdoc_files = ['ChangeLog', 'LICENSE', 'README.md', 'NEWS.md'] 25 | 26 | s.required_ruby_version = '>= 2.2.0' 27 | 28 | s.add_runtime_dependency('cri', '~> 2.3') 29 | 30 | s.add_development_dependency('bundler', '>= 1.7.10', '< 2.0') 31 | end 32 | -------------------------------------------------------------------------------- /test/extra/checking/test_runner.rb: -------------------------------------------------------------------------------- 1 | class Nanoc::Extra::Checking::RunnerTest < Nanoc::TestCase 2 | def test_run_specific 3 | with_site do |site| 4 | File.open('output/blah', 'w') { |io| io.write('I am stale! Haha!') } 5 | runner = Nanoc::Extra::Checking::Runner.new(site) 6 | runner.run_specific(%w( stale )) 7 | end 8 | end 9 | 10 | def test_run_specific_custom 11 | with_site do |site| 12 | File.open('Checks', 'w') do |io| 13 | io.write('check :my_foo_check do ; puts "I AM FOO!" ; end') 14 | end 15 | 16 | runner = Nanoc::Extra::Checking::Runner.new(site) 17 | ios = capturing_stdio do 18 | runner.run_specific(%w( my_foo_check )) 19 | end 20 | 21 | assert ios[:stdout].include?('I AM FOO!') 22 | end 23 | end 24 | 25 | def test_list_checks 26 | with_site do |site| 27 | File.open('Checks', 'w') do |io| 28 | io.write('check :my_foo_check do ; end') 29 | end 30 | 31 | runner = Nanoc::Extra::Checking::Runner.new(site) 32 | ios = capturing_stdio do 33 | runner.list_checks 34 | end 35 | 36 | assert ios[:stdout].include?('my_foo_check') 37 | assert ios[:stdout].include?('internal_links') 38 | assert ios[:stderr].empty? 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/nanoc/base/views/item_rep_collection.rb: -------------------------------------------------------------------------------- 1 | module Nanoc 2 | class ItemRepCollectionView 3 | include Enumerable 4 | 5 | # @api private 6 | def initialize(item_reps) 7 | @item_reps = item_reps 8 | end 9 | 10 | # @api private 11 | def unwrap 12 | @item_reps 13 | end 14 | 15 | def to_ary 16 | @item_reps.map { |ir| Nanoc::ItemRepView.new(ir) } 17 | end 18 | 19 | # Calls the given block once for each item rep, passing that item rep as a parameter. 20 | # 21 | # @yieldparam [Nanoc::ItemRepView] item rep 22 | # 23 | # @yieldreturn [void] 24 | # 25 | # @return [self] 26 | def each 27 | @item_reps.each { |ir| yield Nanoc::ItemRepView.new(ir) } 28 | self 29 | end 30 | 31 | # @return [Integer] 32 | def size 33 | @item_reps.size 34 | end 35 | 36 | # Return the item rep with the given name, or nil if no item rep exists. 37 | # 38 | # @param [Symbol] rep_name 39 | # 40 | # @return [nil] if no item rep with the given name was found 41 | # 42 | # @return [Nanoc::ItemRepView] if an item rep with the given name was found 43 | def [](rep_name) 44 | res = @item_reps.find { |ir| ir.name == rep_name } 45 | res && Nanoc::ItemRepView.new(res) 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/nanoc/filters/pandoc.rb: -------------------------------------------------------------------------------- 1 | module Nanoc::Filters 2 | # @api private 3 | class Pandoc < Nanoc::Filter 4 | requires 'pandoc-ruby' 5 | 6 | # Runs the content through [Pandoc](http://johnmacfarlane.net/pandoc/) 7 | # using [PandocRuby](https://github.com/alphabetum/pandoc-ruby). 8 | # 9 | # Arguments can be passed to PandocRuby in two ways: 10 | # 11 | # * Use the `:args` option. This approach is more flexible, since it 12 | # allows passing an array instead of a hash. 13 | # 14 | # * Pass the arguments directly to the filter. With this approach, only 15 | # hashes can be passed, which is more limiting than the `:args` approach. 16 | # 17 | # The `:args` approach is recommended. 18 | # 19 | # @example Passing arguments using `:arg` 20 | # 21 | # filter :pandoc, args: [:s, {:f => :markdown, :to => :html}, 'no-wrap', :toc] 22 | # 23 | # @example Passing arguments not using `:arg` 24 | # 25 | # filter :pandoc, :f => :markdown, :to => :html 26 | # 27 | # @param [String] content The content to filter 28 | # 29 | # @return [String] The filtered content 30 | def run(content, params = {}) 31 | args = params.key?(:args) ? params[:args] : params 32 | 33 | PandocRuby.convert(content, *args) 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /test/filters/test_pandoc.rb: -------------------------------------------------------------------------------- 1 | class Nanoc::Filters::PandocTest < Nanoc::TestCase 2 | def test_filter 3 | if_have 'pandoc-ruby' do 4 | skip_unless_have_command 'pandoc' 5 | 6 | # Create filter 7 | filter = ::Nanoc::Filters::Pandoc.new 8 | 9 | # Run filter 10 | result = filter.setup_and_run("# Heading\n") 11 | assert_match(%r{

Heading

\s*}, result) 12 | end 13 | end 14 | 15 | def test_params_old 16 | if_have 'pandoc-ruby' do 17 | skip_unless_have_command 'pandoc' 18 | 19 | # Create filter 20 | filter = ::Nanoc::Filters::Pandoc.new 21 | 22 | # Run filter 23 | args = { f: :markdown, to: :html } 24 | result = filter.setup_and_run("# Heading\n", args) 25 | assert_match(%r{

Heading

\s*}, result) 26 | end 27 | end 28 | 29 | def test_params_new 30 | if_have 'pandoc-ruby' do 31 | skip_unless_have_command 'pandoc' 32 | 33 | # Create filter 34 | filter = ::Nanoc::Filters::Pandoc.new 35 | 36 | # Run filter 37 | args = [:s, { f: :markdown, to: :html }, 'no-wrap', :toc] 38 | result = filter.setup_and_run("# Heading\n", args: args) 39 | assert_match '
', result 40 | assert_match(%r{

Heading

\s*}, result) 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/nanoc.rb: -------------------------------------------------------------------------------- 1 | module Nanoc 2 | # @return [String] A string containing information about this nanoc version 3 | # and its environment (Ruby engine and version, Rubygems version if any). 4 | # 5 | # @api private 6 | def self.version_information 7 | gem_info = defined?(Gem) ? "with RubyGems #{Gem::VERSION}" : 'without RubyGems' 8 | engine = defined?(RUBY_ENGINE) ? RUBY_ENGINE : 'ruby' 9 | res = '' 10 | res << "nanoc #{Nanoc::VERSION} © 2007-2015 Denis Defreyne.\n" 11 | res << "Running #{engine} #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}) on #{RUBY_PLATFORM} #{gem_info}.\n" 12 | res 13 | end 14 | 15 | # @return [Boolean] True if the current platform is Windows, false otherwise. 16 | # 17 | # @api private 18 | def self.on_windows? 19 | RUBY_PLATFORM =~ /windows|bccwin|cygwin|djgpp|mingw|mswin|wince/i 20 | end 21 | end 22 | 23 | # Load general requirements 24 | require 'digest' 25 | require 'enumerator' 26 | require 'fileutils' 27 | require 'forwardable' 28 | require 'pathname' 29 | require 'pstore' 30 | require 'set' 31 | require 'tempfile' 32 | require 'thread' 33 | require 'time' 34 | require 'yaml' 35 | require 'English' 36 | 37 | # Load nanoc 38 | require 'nanoc/version' 39 | require 'nanoc/base' 40 | require 'nanoc/extra' 41 | require 'nanoc/data_sources' 42 | require 'nanoc/filters' 43 | require 'nanoc/helpers' 44 | -------------------------------------------------------------------------------- /lib/nanoc/extra/checking/checks.rb: -------------------------------------------------------------------------------- 1 | # @api private 2 | module Nanoc::Extra::Checking::Checks 3 | autoload 'CSS', 'nanoc/extra/checking/checks/css' 4 | autoload 'ExternalLinks', 'nanoc/extra/checking/checks/external_links' 5 | autoload 'HTML', 'nanoc/extra/checking/checks/html' 6 | autoload 'InternalLinks', 'nanoc/extra/checking/checks/internal_links' 7 | autoload 'Stale', 'nanoc/extra/checking/checks/stale' 8 | autoload 'MixedContent', 'nanoc/extra/checking/checks/mixed_content' 9 | 10 | Nanoc::Extra::Checking::Check.register '::Nanoc::Extra::Checking::Checks::CSS', :css 11 | Nanoc::Extra::Checking::Check.register '::Nanoc::Extra::Checking::Checks::ExternalLinks', :external_links 12 | Nanoc::Extra::Checking::Check.register '::Nanoc::Extra::Checking::Checks::ExternalLinks', :elinks 13 | Nanoc::Extra::Checking::Check.register '::Nanoc::Extra::Checking::Checks::HTML', :html 14 | Nanoc::Extra::Checking::Check.register '::Nanoc::Extra::Checking::Checks::InternalLinks', :internal_links 15 | Nanoc::Extra::Checking::Check.register '::Nanoc::Extra::Checking::Checks::InternalLinks', :ilinks 16 | Nanoc::Extra::Checking::Check.register '::Nanoc::Extra::Checking::Checks::Stale', :stale 17 | Nanoc::Extra::Checking::Check.register '::Nanoc::Extra::Checking::Checks::MixedContent', :mixed_content 18 | end 19 | -------------------------------------------------------------------------------- /lib/nanoc/extra/piper.rb: -------------------------------------------------------------------------------- 1 | require 'open3' 2 | 3 | module Nanoc::Extra 4 | # @api private 5 | class Piper 6 | class Error < ::Nanoc::Int::Errors::Generic 7 | def initialize(command, exit_code) 8 | @command = command 9 | @exit_code = exit_code 10 | end 11 | 12 | def message 13 | "command exited with a nonzero status code #{@exit_code} (command: #{@command.join(' ')})" 14 | end 15 | end 16 | 17 | # @option [IO] :stdout ($stdout) 18 | # @option [IO] :stderr ($stderr) 19 | def initialize(params = {}) 20 | @stdout = params.fetch(:stdout, $stdout) 21 | @stderr = params.fetch(:stderr, $stderr) 22 | end 23 | 24 | # @param [Array] cmd 25 | # 26 | # @param [String, nil] input 27 | def run(cmd, input) 28 | Open3.popen3(*cmd) do |stdin, stdout, stderr, wait_thr| 29 | stdout_thread = Thread.new { @stdout << stdout.read } 30 | stderr_thread = Thread.new { @stderr << stderr.read } 31 | 32 | if input 33 | stdin << input 34 | end 35 | stdin.close 36 | 37 | stdout_thread.join 38 | stderr_thread.join 39 | 40 | exit_status = wait_thr.value 41 | unless exit_status.success? 42 | raise Error.new(cmd, exit_status.to_i) 43 | end 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /test/base/core_ext/array_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'Array#__nanoc_symbolize_keys_recursively' do 2 | it 'should convert keys to symbols' do 3 | array_old = [:abc, 'xyz', { 'foo' => 'bar', :baz => :qux }] 4 | array_new = [:abc, 'xyz', { foo: 'bar', baz: :qux }] 5 | array_old.__nanoc_symbolize_keys_recursively.must_equal array_new 6 | end 7 | end 8 | 9 | describe 'Array#__nanoc_freeze_recursively' do 10 | include Nanoc::TestHelpers 11 | 12 | it 'should prevent first-level elements from being modified' do 13 | array = [:a, [:b, :c], :d] 14 | array.__nanoc_freeze_recursively 15 | 16 | assert_raises_frozen_error do 17 | array[0] = 123 18 | end 19 | end 20 | 21 | it 'should prevent second-level elements from being modified' do 22 | array = [:a, [:b, :c], :d] 23 | array.__nanoc_freeze_recursively 24 | 25 | assert_raises_frozen_error do 26 | array[1][0] = 123 27 | end 28 | end 29 | 30 | it 'should not freeze infinitely' do 31 | a = [] 32 | a << a 33 | 34 | a.__nanoc_freeze_recursively 35 | 36 | assert a.frozen? 37 | assert a[0].frozen? 38 | assert_equal a, a[0] 39 | end 40 | end 41 | 42 | describe 'Array#__nanoc_checksum' do 43 | it 'should work' do 44 | expectation = 'CEUlNvu/3DUmlbtpFRiLHU8oHA0=' 45 | [[:foo, 123]].__nanoc_checksum.must_equal expectation 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/nanoc/extra/jruby_nokogiri_warner.rb: -------------------------------------------------------------------------------- 1 | require 'singleton' 2 | 3 | module Nanoc::Extra 4 | # @api private 5 | class JRubyNokogiriWarner 6 | include Singleton 7 | 8 | TEXT = < 'bar' }` will 11 | # cause `@foo` to have the value `"bar"`, and the instance method `#foo` 12 | # to return the same value `"bar"`. 13 | # 14 | # @param [Hash] hash A list of key-value pairs to make available 15 | # 16 | # @example Defining a context and accessing values 17 | # 18 | # context = Nanoc::Int::Context.new( 19 | # :name => 'Max Payne', 20 | # :location => 'in a cheap motel' 21 | # ) 22 | # context.instance_eval do 23 | # "I am #{name} and I am hiding #{@location}." 24 | # end 25 | # # => "I am Max Payne and I am hiding in a cheap motel." 26 | def initialize(hash) 27 | hash.each_pair do |key, value| 28 | # Build instance variable 29 | instance_variable_set('@' + key.to_s, value) 30 | 31 | # Define method 32 | metaclass = (class << self; self; end) 33 | metaclass.send(:define_method, key) { value } 34 | end 35 | end 36 | 37 | # Returns a binding for this instance. 38 | # 39 | # @return [Binding] A binding for this instance 40 | def get_binding 41 | binding 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/nanoc/extra/checking/check.rb: -------------------------------------------------------------------------------- 1 | module Nanoc::Extra::Checking 2 | # @api private 3 | class OutputDirNotFoundError < Nanoc::Int::Errors::Generic 4 | def initialize(directory_path) 5 | super("Unable to run check against output directory at “#{directory_path}”: directory does not exist.") 6 | end 7 | end 8 | 9 | # @api private 10 | class Check < Nanoc::Int::Context 11 | extend Nanoc::Int::PluginRegistry::PluginMethods 12 | 13 | attr_reader :issues 14 | 15 | def self.create(site) 16 | output_dir = site.config[:output_dir] 17 | unless File.exist?(output_dir) 18 | raise Nanoc::Extra::Checking::OutputDirNotFoundError.new(output_dir) 19 | end 20 | output_filenames = Dir[output_dir + '/**/*'].select { |f| File.file?(f) } 21 | 22 | context = { 23 | items: Nanoc::ItemCollectionView.new(site.items), 24 | layouts: Nanoc::LayoutCollectionView.new(site.layouts), 25 | config: Nanoc::ConfigView.new(site.config), 26 | site: Nanoc::SiteView.new(site), # TODO: remove me 27 | output_filenames: output_filenames, 28 | } 29 | 30 | new(context) 31 | end 32 | 33 | def initialize(context) 34 | super(context) 35 | 36 | @issues = Set.new 37 | end 38 | 39 | def run 40 | raise NotImplementedError.new('Nanoc::Extra::Checking::Check subclasses must implement #run') 41 | end 42 | 43 | def add_issue(desc, params = {}) 44 | subject = params.fetch(:subject, nil) 45 | 46 | @issues << Issue.new(desc, subject, self.class) 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/nanoc/helpers/text.rb: -------------------------------------------------------------------------------- 1 | module Nanoc::Helpers 2 | # Contains several useful text-related helper functions. 3 | module Text 4 | # Returns an excerpt for the given string. HTML tags are ignored, so if 5 | # you don't want them to turn up, they should be stripped from the string 6 | # before passing it to the excerpt function. 7 | # 8 | # @param [String] string The string for which to build an excerpt 9 | # 10 | # @option params [Number] length (25) The maximum number of characters 11 | # this excerpt can contain, including the omission. 12 | # 13 | # @option params [String] omission ("...") The string to append to the 14 | # excerpt when the excerpt is shorter than the original string 15 | # 16 | # @return [String] The excerpt of the given string 17 | def excerptize(string, params = {}) 18 | # Initialize params 19 | params[:length] ||= 25 20 | params[:omission] ||= '...' 21 | 22 | # Get excerpt 23 | length = params[:length] - params[:omission].length 24 | length = 0 if length < 0 25 | (string.length > params[:length] ? string[0...length] + params[:omission] : string) 26 | end 27 | 28 | # Strips all HTML tags out of the given string. 29 | # 30 | # @param [String] string The string from which to strip all HTML 31 | # 32 | # @return [String] The given string with all HTML stripped 33 | def strip_html(string) 34 | # FIXME: will need something more sophisticated than this, because it sucks 35 | string.gsub(/<[^>]*(>+|\s*\z)/m, '').strip 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/nanoc/base/views/item_rep_collection_spec.rb: -------------------------------------------------------------------------------- 1 | describe Nanoc::ItemRepCollectionView do 2 | let(:view) { described_class.new(wrapped) } 3 | 4 | let(:wrapped) do 5 | [ 6 | double(:item_rep, name: :foo), 7 | double(:item_rep, name: :bar), 8 | double(:item_rep, name: :baz), 9 | ] 10 | end 11 | 12 | describe '#unwrap' do 13 | subject { view.unwrap } 14 | 15 | it { should equal(wrapped) } 16 | end 17 | 18 | describe '#each' do 19 | it 'yields' do 20 | actual = [].tap { |res| view.each { |v| res << v } } 21 | expect(actual.size).to eq(3) 22 | end 23 | 24 | it 'returns self' do 25 | expect(view.each { |_i| }).to equal(view) 26 | end 27 | end 28 | 29 | describe '#size' do 30 | subject { view.size } 31 | 32 | it { should == 3 } 33 | end 34 | 35 | describe '#to_ary' do 36 | subject { view.to_ary } 37 | 38 | it 'returns an array of item rep views' do 39 | expect(subject.class).to eq(Array) 40 | expect(subject.size).to eq(3) 41 | expect(subject[0].class).to eql(Nanoc::ItemRepView) 42 | expect(subject[0].name).to eql(:foo) 43 | end 44 | end 45 | 46 | describe '#[]' do 47 | subject { view[name] } 48 | 49 | context 'when not found' do 50 | let(:name) { :donkey } 51 | 52 | it { should be_nil } 53 | end 54 | 55 | context 'when found' do 56 | let(:name) { :foo } 57 | 58 | it 'returns a view' do 59 | expect(subject.class).to eq(Nanoc::ItemRepView) 60 | expect(subject.name).to eq(:foo) 61 | end 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /test/extra/checking/checks/test_html.rb: -------------------------------------------------------------------------------- 1 | class Nanoc::Extra::Checking::Checks::HTMLTest < Nanoc::TestCase 2 | def test_run_ok 3 | VCR.use_cassette('html_run_ok') do 4 | with_site do |site| 5 | # Create files 6 | FileUtils.mkdir_p('output') 7 | File.open('output/blah.html', 'w') { |io| io.write('Hello

Hi!

') } 8 | File.open('output/style.css', 'w') { |io| io.write('h1 { coxlor: rxed; }') } 9 | 10 | # Run check 11 | check = Nanoc::Extra::Checking::Checks::HTML.create(site) 12 | check.run 13 | 14 | # Check 15 | assert check.issues.empty? 16 | end 17 | end 18 | end 19 | 20 | def test_run_error 21 | VCR.use_cassette('html_run_error') do 22 | with_site do |site| 23 | # Create files 24 | FileUtils.mkdir_p('output') 25 | File.open('output/blah.html', 'w') { |io| io.write('

Hi!

') } 26 | File.open('output/style.css', 'w') { |io| io.write('h1 { coxlor: rxed; }') } 27 | 28 | # Run check 29 | check = Nanoc::Extra::Checking::Checks::HTML.create(site) 30 | check.run 31 | 32 | # Check 33 | refute check.issues.empty? 34 | assert_equal 2, check.issues.size 35 | assert_equal 'line 1: no document type declaration; will parse without validation:

Hi!

', check.issues.to_a[0].description 36 | assert_equal 'line 1: end tag for element "H1" which is not open:

Hi!

', check.issues.to_a[1].description 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/nanoc/extra/deployer.rb: -------------------------------------------------------------------------------- 1 | module Nanoc::Extra 2 | # Represents a deployer, an object that allows uploading the compiled site 3 | # to a specific (remote) location. 4 | # 5 | # @abstract Subclass and override {#run} to implement a custom filter. 6 | # 7 | # @api private 8 | class Deployer 9 | extend Nanoc::Int::PluginRegistry::PluginMethods 10 | 11 | # @return [String] The path to the directory that contains the files to 12 | # upload. It should not have a trailing slash. 13 | attr_reader :source_path 14 | 15 | # @return [Hash] The deployer configuration 16 | attr_reader :config 17 | 18 | # @return [Boolean] true if the deployer should only show what would be 19 | # deployed instead of doing the actual deployment 20 | attr_reader :dry_run 21 | alias_method :dry_run?, :dry_run 22 | 23 | # @param [String] source_path The path to the directory that contains the 24 | # files to upload. It should not have a trailing slash. 25 | # 26 | # @return [Hash] config The deployer configuration 27 | # 28 | # @option params [Boolean] :dry_run (false) true if the deployer should 29 | # only show what would be deployed instead actually deploying 30 | def initialize(source_path, config, params = {}) 31 | @source_path = source_path 32 | @config = config 33 | @dry_run = params.fetch(:dry_run) { false } 34 | end 35 | 36 | # Performs the actual deployment. 37 | # 38 | # @abstract 39 | def run 40 | raise NotImplementedError.new('Nanoc::Extra::Deployer subclasses must implement #run') 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/nanoc/base/core_ext/array.rb: -------------------------------------------------------------------------------- 1 | # @api private 2 | module Nanoc::ArrayExtensions 3 | # Returns a new array where all items' keys are recursively converted to 4 | # symbols by calling {Nanoc::ArrayExtensions#__nanoc_symbolize_keys_recursively} or 5 | # {Nanoc::HashExtensions#__nanoc_symbolize_keys_recursively}. 6 | # 7 | # @return [Array] The converted array 8 | def __nanoc_symbolize_keys_recursively 9 | array = [] 10 | each do |element| 11 | array << (element.respond_to?(:__nanoc_symbolize_keys_recursively) ? element.__nanoc_symbolize_keys_recursively : element) 12 | end 13 | array 14 | end 15 | 16 | # Freezes the contents of the array, as well as all array elements. The 17 | # array elements will be frozen using {#__nanoc_freeze_recursively} if they respond 18 | # to that message, or #freeze if they do not. 19 | # 20 | # @see Hash#__nanoc_freeze_recursively 21 | # 22 | # @return [void] 23 | # 24 | # @since 3.2.0 25 | def __nanoc_freeze_recursively 26 | return if self.frozen? 27 | freeze 28 | each do |value| 29 | if value.respond_to?(:__nanoc_freeze_recursively) 30 | value.__nanoc_freeze_recursively 31 | else 32 | value.freeze 33 | end 34 | end 35 | end 36 | 37 | # Calculates the checksum for this array. Any change to this array will 38 | # result in a different checksum. 39 | # 40 | # @return [String] The checksum for this array 41 | # 42 | # @api private 43 | def __nanoc_checksum 44 | Nanoc::Int::Checksummer.calc(self) 45 | end 46 | end 47 | 48 | # @api private 49 | class Array 50 | include Nanoc::ArrayExtensions 51 | end 52 | -------------------------------------------------------------------------------- /test/base/core_ext/hash_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'Hash#__nanoc_symbolize_keys_recursively' do 2 | it 'should convert keys to symbols' do 3 | hash_old = { 'foo' => 'bar' } 4 | hash_new = { foo: 'bar' } 5 | hash_old.__nanoc_symbolize_keys_recursively.must_equal hash_new 6 | end 7 | 8 | it 'should not require string keys' do 9 | hash_old = { Time.now => 'abc' } 10 | hash_new = hash_old 11 | hash_old.__nanoc_symbolize_keys_recursively.must_equal hash_new 12 | end 13 | end 14 | 15 | describe 'Hash#__nanoc_freeze_recursively' do 16 | include Nanoc::TestHelpers 17 | 18 | it 'should prevent first-level elements from being modified' do 19 | hash = { a: { b: :c } } 20 | hash.__nanoc_freeze_recursively 21 | 22 | assert_raises_frozen_error do 23 | hash[:a] = 123 24 | end 25 | end 26 | 27 | it 'should prevent second-level elements from being modified' do 28 | hash = { a: { b: :c } } 29 | hash.__nanoc_freeze_recursively 30 | 31 | assert_raises_frozen_error do 32 | hash[:a][:b] = 123 33 | end 34 | end 35 | 36 | it 'should not freeze infinitely' do 37 | a = {} 38 | a[:x] = a 39 | 40 | a.__nanoc_freeze_recursively 41 | 42 | assert a.frozen? 43 | assert a[:x].frozen? 44 | assert_equal a, a[:x] 45 | end 46 | end 47 | 48 | describe 'Hash#__nanoc_checksum' do 49 | it 'should work' do 50 | expectation = 'wy7gHokc700tqJ/BmJ+EK6/F0bc=' 51 | { foo: 123 }.__nanoc_checksum.must_equal expectation 52 | end 53 | 54 | it 'should not sort keys' do 55 | a = { a: 1, c: 2, b: 3 }.__nanoc_checksum 56 | b = { a: 1, b: 3, c: 2 }.__nanoc_checksum 57 | a.wont_equal b 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/nanoc/base/compilation/outdatedness_reasons.rb: -------------------------------------------------------------------------------- 1 | module Nanoc::Int 2 | # Module that contains all outdatedness reasons. 3 | # 4 | # @api private 5 | module OutdatednessReasons 6 | # A generic outdatedness reason. An outdatedness reason is basically a 7 | # descriptive message that explains why a given object is outdated. 8 | class Generic 9 | # @return [String] A descriptive message for this outdatedness reason 10 | attr_reader :message 11 | 12 | # @param [String] message The descriptive message for this outdatedness 13 | # reason 14 | def initialize(message) 15 | @message = message 16 | end 17 | end 18 | 19 | CodeSnippetsModified = Generic.new( 20 | 'The code snippets have been modified since the last time the site was compiled.') 21 | 22 | ConfigurationModified = Generic.new( 23 | 'The site configuration has been modified since the last time the site was compiled.') 24 | 25 | DependenciesOutdated = Generic.new( 26 | 'This item uses content or attributes that have changed since the last time the site was compiled.') 27 | 28 | NotEnoughData = Generic.new( 29 | 'Not enough data is present to correctly determine whether the item is outdated.') 30 | 31 | NotWritten = Generic.new( 32 | 'This item representation has not yet been written to the output directory (but it does have a path).') 33 | 34 | RulesModified = Generic.new( 35 | 'The rules file has been modified since the last time the site was compiled.') 36 | 37 | SourceModified = Generic.new( 38 | 'The source file of this item has been modified since the last time the site was compiled.') 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/nanoc/base/source_data/code_snippet.rb: -------------------------------------------------------------------------------- 1 | module Nanoc::Int 2 | # Nanoc::Int::CodeSnippet represent a piece of custom code of a nanoc site. 3 | # 4 | # @api private 5 | class CodeSnippet 6 | # A string containing the actual code in this code snippet. 7 | # 8 | # @return [String] 9 | attr_reader :data 10 | 11 | # The filename corresponding to this code snippet. 12 | # 13 | # @return [String] 14 | attr_reader :filename 15 | 16 | # Creates a new code snippet. 17 | # 18 | # @param [String] data The raw source code which will be executed before 19 | # compilation 20 | # 21 | # @param [String] filename The filename corresponding to this code snippet 22 | # 23 | # @param [Time, Hash] _params Extra parameters. Ignored by nanoc; it is 24 | # only included for backwards compatibility. 25 | def initialize(data, filename, _params = nil) 26 | @data = data 27 | @filename = filename 28 | end 29 | 30 | # Loads the code by executing it. 31 | # 32 | # @return [void] 33 | def load 34 | eval(@data, TOPLEVEL_BINDING, @filename) 35 | end 36 | 37 | # Returns an object that can be used for uniquely identifying objects. 38 | # 39 | # @return [Object] An unique reference to this object 40 | def reference 41 | [:code_snippet, filename] 42 | end 43 | 44 | def inspect 45 | "<#{self.class} filename=\"#{filename}\">" 46 | end 47 | 48 | # @return [String] The checksum for this object. If its contents change, 49 | # the checksum will change as well. 50 | def __nanoc_checksum 51 | Nanoc::Int::Checksummer.calc(self) 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /spec/nanoc/base/views/layout_spec.rb: -------------------------------------------------------------------------------- 1 | describe Nanoc::LayoutView do 2 | describe '#== and #eql?' do 3 | let(:layout) { Nanoc::Int::Layout.new('content', {}, '/asdf/') } 4 | let(:view) { described_class.new(layout) } 5 | 6 | context 'comparing with layout with same identifier' do 7 | let(:other) { Nanoc::Int::Layout.new('content', {}, '/asdf/') } 8 | 9 | it 'is equal' do 10 | expect(view).to eq(other) 11 | expect(view).to eql(other) 12 | end 13 | end 14 | 15 | context 'comparing with layout with different identifier' do 16 | let(:other) { Nanoc::Int::Layout.new('content', {}, '/fdsa/') } 17 | 18 | it 'is not equal' do 19 | expect(view).not_to eq(other) 20 | expect(view).not_to eql(other) 21 | end 22 | end 23 | 24 | context 'comparing with layout view with same identifier' do 25 | let(:other) { Nanoc::LayoutView.new(Nanoc::Int::Layout.new('content', {}, '/asdf/')) } 26 | 27 | it 'is equal' do 28 | expect(view).to eq(other) 29 | expect(view).to eql(other) 30 | end 31 | end 32 | 33 | context 'comparing with layout view with different identifier' do 34 | let(:other) { Nanoc::LayoutView.new(Nanoc::Int::Layout.new('content', {}, '/fdsa/')) } 35 | 36 | it 'is not equal' do 37 | expect(view).not_to eq(other) 38 | expect(view).not_to eql(other) 39 | end 40 | end 41 | end 42 | 43 | describe '#hash' do 44 | let(:layout) { double(:layout, identifier: '/foo/') } 45 | let(:view) { described_class.new(layout) } 46 | 47 | subject { view.hash } 48 | 49 | it { should == described_class.hash ^ '/foo/'.hash } 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/nanoc/base/views/config_spec.rb: -------------------------------------------------------------------------------- 1 | describe Nanoc::ConfigView do 2 | let(:config) do 3 | { amount: 9000, animal: 'donkey' } 4 | end 5 | 6 | let(:view) { described_class.new(config) } 7 | 8 | describe '#[]' do 9 | subject { view[key] } 10 | 11 | context 'with existant key' do 12 | let(:key) { :animal } 13 | it { should eql?('donkey') } 14 | end 15 | 16 | context 'with non-existant key' do 17 | let(:key) { :weapon } 18 | it { should eql?(nil) } 19 | end 20 | end 21 | 22 | describe '#fetch' do 23 | context 'with existant key' do 24 | let(:key) { :animal } 25 | 26 | subject { view.fetch(key) } 27 | 28 | it { should eql?('donkey') } 29 | end 30 | 31 | context 'with non-existant key' do 32 | let(:key) { :weapon } 33 | 34 | context 'with fallback' do 35 | subject { view.fetch(key, 'nothing sorry') } 36 | it { should eql?('nothing sorry') } 37 | end 38 | 39 | context 'with block' do 40 | subject { view.fetch(key) { 'nothing sorry' } } 41 | it { should eql?('nothing sorry') } 42 | end 43 | 44 | context 'with no fallback and no block' do 45 | subject { view.fetch(key) } 46 | 47 | it 'raises' do 48 | expect { subject }.to raise_error(KeyError) 49 | end 50 | end 51 | end 52 | end 53 | 54 | describe '#key?' do 55 | subject { view.key?(key) } 56 | 57 | context 'with existant key' do 58 | let(:key) { :animal } 59 | it { should eql?(true) } 60 | end 61 | 62 | context 'with non-existant key' do 63 | let(:key) { :weapon } 64 | it { should eql?(false) } 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /test/base/test_memoization.rb: -------------------------------------------------------------------------------- 1 | class Nanoc::Int::MemoizationTest < Nanoc::TestCase 2 | class Sample1 3 | extend Nanoc::Int::Memoization 4 | 5 | def initialize(value) 6 | @value = value 7 | end 8 | 9 | def run(n) 10 | @value * 10 + n 11 | end 12 | memoize :run 13 | end 14 | 15 | class Sample2 16 | extend Nanoc::Int::Memoization 17 | 18 | def initialize(value) 19 | @value = value 20 | end 21 | 22 | def run(n) 23 | @value * 100 + n 24 | end 25 | memoize :run 26 | end 27 | 28 | class EqualSample 29 | extend Nanoc::Int::Memoization 30 | 31 | def initialize(value) 32 | @value = value 33 | end 34 | 35 | def hash 36 | 4 37 | end 38 | 39 | def eql?(_other) 40 | true 41 | end 42 | 43 | def ==(_other) 44 | true 45 | end 46 | 47 | def run(n) 48 | @value * 10 + n 49 | end 50 | memoize :run 51 | end 52 | 53 | def test 54 | sample1a = Sample1.new(10) 55 | sample1b = Sample1.new(15) 56 | sample2a = Sample2.new(20) 57 | sample2b = Sample2.new(25) 58 | 59 | 3.times do 60 | assert_equal 10 * 10 + 5, sample1a.run(5) 61 | assert_equal 10 * 15 + 7, sample1b.run(7) 62 | assert_equal 100 * 20 + 5, sample2a.run(5) 63 | assert_equal 100 * 25 + 7, sample2b.run(7) 64 | end 65 | end 66 | 67 | def test_equal 68 | sample1 = EqualSample.new(2) 69 | sample2 = EqualSample.new(3) 70 | 71 | 3.times do 72 | assert_equal 2 * 10 + 5, sample1.run(5) 73 | assert_equal 2 * 10 + 3, sample1.run(3) 74 | assert_equal 3 * 10 + 5, sample2.run(5) 75 | assert_equal 3 * 10 + 3, sample2.run(3) 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/nanoc/base/core_ext/hash.rb: -------------------------------------------------------------------------------- 1 | # @api private 2 | module Nanoc::HashExtensions 3 | # Returns a new hash where all keys are recursively converted to symbols by 4 | # calling {Nanoc::ArrayExtensions#__nanoc_symbolize_keys_recursively} or 5 | # {Nanoc::HashExtensions#__nanoc_symbolize_keys_recursively}. 6 | # 7 | # @return [Hash] The converted hash 8 | def __nanoc_symbolize_keys_recursively 9 | hash = {} 10 | each_pair do |key, value| 11 | new_key = key.respond_to?(:to_sym) ? key.to_sym : key 12 | new_value = value.respond_to?(:__nanoc_symbolize_keys_recursively) ? value.__nanoc_symbolize_keys_recursively : value 13 | hash[new_key] = new_value 14 | end 15 | hash 16 | end 17 | 18 | # Freezes the contents of the hash, as well as all hash values. The hash 19 | # values will be frozen using {#__nanoc_freeze_recursively} if they respond to 20 | # that message, or #freeze if they do not. 21 | # 22 | # @see Array#__nanoc_freeze_recursively 23 | # 24 | # @return [void] 25 | # 26 | # @since 3.2.0 27 | def __nanoc_freeze_recursively 28 | return if self.frozen? 29 | freeze 30 | each_pair do |_key, value| 31 | if value.respond_to?(:__nanoc_freeze_recursively) 32 | value.__nanoc_freeze_recursively 33 | else 34 | value.freeze 35 | end 36 | end 37 | end 38 | 39 | # Calculates the checksum for this hash. Any change to this hash will result 40 | # in a different checksum. 41 | # 42 | # @return [String] The checksum for this hash 43 | # 44 | # @api private 45 | def __nanoc_checksum 46 | Nanoc::Int::Checksummer.calc(self) 47 | end 48 | end 49 | 50 | # @api private 51 | class Hash 52 | include Nanoc::HashExtensions 53 | end 54 | -------------------------------------------------------------------------------- /lib/nanoc/helpers/html_escape.rb: -------------------------------------------------------------------------------- 1 | module Nanoc::Helpers 2 | # Contains functionality for HTML-escaping strings. 3 | module HTMLEscape 4 | require 'nanoc/helpers/capturing' 5 | include Nanoc::Helpers::Capturing 6 | 7 | # Returns the HTML-escaped representation of the given string or the given 8 | # block. Only `&`, `<`, `>` and `"` are escaped. When given a block, the 9 | # contents of the block will be escaped and appended to the output buffer, 10 | # `_erbout`. 11 | # 12 | # @example Escaping a string 13 | # 14 | # h('
') 15 | # # => '<br>' 16 | # 17 | # @example Escaping with a block 18 | # 19 | # <% h do %> 20 | #

Hello world!

21 | # <% end %> 22 | # # The buffer will now contain “<h1>Hello <em>world</em>!</h1>” 23 | # 24 | # @param [String] string The string to escape 25 | # 26 | # @return [String] The escaped string 27 | def html_escape(string = nil, &block) 28 | if block_given? 29 | # Capture and escape block 30 | data = capture(&block) 31 | escaped_data = html_escape(data) 32 | 33 | # Append filtered data to buffer 34 | buffer = eval('_erbout', block.binding) 35 | buffer << escaped_data 36 | elsif string 37 | string.gsub('&', '&') 38 | .gsub('<', '<') 39 | .gsub('>', '>') 40 | .gsub('"', '"') 41 | else 42 | raise 'The #html_escape or #h function needs either a ' \ 43 | 'string or a block to HTML-escape, but neither a string nor a block was given' 44 | end 45 | end 46 | 47 | alias_method :h, :html_escape 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/nanoc/filters/xsl.rb: -------------------------------------------------------------------------------- 1 | module Nanoc::Filters 2 | # @since 3.3.0 3 | # 4 | # @api private 5 | class XSL < Nanoc::Filter 6 | requires 'nokogiri' 7 | 8 | # Runs the item content through an [XSLT](http://www.w3.org/TR/xslt) 9 | # stylesheet using [Nokogiri](http://nokogiri.org/). 10 | # 11 | # This filter can only be run for layouts, because it will need both the 12 | # XML to convert (= the item content) as well as the XSLT stylesheet (= 13 | # the layout content). 14 | # 15 | # Additional parameters can be passed to the layout call. These parameters 16 | # will be turned into `xsl:param` elements. 17 | # 18 | # @example Invoking the filter as a layout 19 | # 20 | # compile '/reports/*/' do 21 | # layout 'xsl-report' 22 | # end 23 | # 24 | # layout 'xsl-report', :xsl, :awesome => 'definitely' 25 | # 26 | # @param [String] _content Ignored. As the filter can be run only as a 27 | # layout, the value of the `:content` parameter passed to the class at 28 | # initialization is used as the content to transform. 29 | # 30 | # @param [Hash] params The parameters that will be stored in corresponding 31 | # `xsl:param` elements. 32 | # 33 | # @return [String] The transformed content 34 | def run(_content, params = {}) 35 | Nanoc::Extra::JRubyNokogiriWarner.check_and_warn 36 | 37 | if assigns[:layout].nil? 38 | raise 'The XSL filter can only be run as a layout' 39 | end 40 | 41 | xml = ::Nokogiri::XML(assigns[:content]) 42 | xsl = ::Nokogiri::XSLT(assigns[:layout].raw_content) 43 | 44 | xsl.apply_to(xml, ::Nokogiri::XSLT.quote_params(params)) 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/nanoc/cli/commands/prune.rb: -------------------------------------------------------------------------------- 1 | usage 'prune' 2 | summary 'remove files not managed by nanoc from the output directory' 3 | description <<-EOS 4 | Find all files in the output directory that do not correspond to an item 5 | managed by nanoc and remove them. Since this is a hazardous operation, an 6 | additional `--yes` flag is needed as confirmation. 7 | 8 | Also see the `auto_prune` configuration option in `nanoc.yaml` (`config.yaml` 9 | for older nanoc sites), which will automatically prune after compilation. 10 | EOS 11 | 12 | flag :y, :yes, 'confirm deletion' 13 | flag :n, :'dry-run', 'print files to be deleted instead of actually deleting them' 14 | 15 | module Nanoc::CLI::Commands 16 | class Prune < ::Nanoc::CLI::CommandRunner 17 | def run 18 | load_site 19 | 20 | if options.key?(:yes) 21 | Nanoc::Extra::Pruner.new(site, exclude: prune_config_exclude).run 22 | elsif options.key?(:'dry-run') 23 | Nanoc::Extra::Pruner.new(site, exclude: prune_config_exclude, dry_run: true).run 24 | else 25 | $stderr.puts 'WARNING: Since the prune command is a destructive command, it requires an additional --yes flag in order to work.' 26 | $stderr.puts 27 | $stderr.puts 'Please ensure that the output directory does not contain any files (such as images or stylesheets) that are necessary but are not managed by nanoc. If you want to get a list of all files that would be removed, pass --dry-run.' 28 | exit 1 29 | end 30 | end 31 | 32 | protected 33 | 34 | def prune_config 35 | site.config[:prune] || {} 36 | end 37 | 38 | def prune_config_exclude 39 | prune_config[:exclude] || {} 40 | end 41 | end 42 | end 43 | 44 | runner Nanoc::CLI::Commands::Prune 45 | -------------------------------------------------------------------------------- /lib/nanoc/base/compilation/compiled_content_cache.rb: -------------------------------------------------------------------------------- 1 | module Nanoc::Int 2 | # Represents a cache than can be used to store already compiled content, 3 | # to prevent it from being needlessly recompiled. 4 | # 5 | # @api private 6 | class CompiledContentCache < ::Nanoc::Int::Store 7 | def initialize 8 | super('tmp/compiled_content', 1) 9 | 10 | @cache = {} 11 | end 12 | 13 | # Returns the cached compiled content for the given item 14 | # representation. This cached compiled content is a hash where the keys 15 | # are the snapshot names and the values the compiled content at the 16 | # given snapshot. 17 | # 18 | # @param [Nanoc::Int::ItemRep] rep The item rep to fetch the content for 19 | # 20 | # @return [Hash] A hash containing the cached compiled 21 | # content for the given item representation 22 | def [](rep) 23 | item_cache = @cache[rep.item.identifier] || {} 24 | item_cache[rep.name] 25 | end 26 | 27 | # Sets the compiled content for the given representation. 28 | # 29 | # @param [Nanoc::Int::ItemRep] rep The item representation for which to set 30 | # the compiled content 31 | # 32 | # @param [Hash] content A hash containing the compiled 33 | # content of the given representation 34 | # 35 | # @return [void] 36 | def []=(rep, content) 37 | @cache[rep.item.identifier] ||= {} 38 | @cache[rep.item.identifier][rep.name] = content 39 | end 40 | 41 | # @see Nanoc::Int::Store#unload 42 | def unload 43 | @cache = {} 44 | end 45 | 46 | protected 47 | 48 | def data 49 | @cache 50 | end 51 | 52 | def data=(new_data) 53 | @cache = new_data 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/nanoc/filters/redcloth.rb: -------------------------------------------------------------------------------- 1 | module Nanoc::Filters 2 | # @api private 3 | class RedCloth < Nanoc::Filter 4 | requires 'redcloth' 5 | 6 | # Runs the content through [RedCloth](http://redcloth.org/). This method 7 | # takes the following options: 8 | # 9 | # * `:filter_class` 10 | # * `:filter_html` 11 | # * `:filter_ids` 12 | # * `:filter_style` 13 | # * `:hard_breaks` 14 | # * `:lite_mode` 15 | # * `:no_span_caps` 16 | # * `:sanitize_htm` 17 | # 18 | # Each of these options sets the corresponding attribute on the `RedCloth` 19 | # instance. For example, when the `:hard_breaks => false` option is passed 20 | # to this filter, the filter will call `r.hard_breaks = false` (with `r` 21 | # being the `RedCloth` instance). 22 | # 23 | # @param [String] content The content to filter 24 | # 25 | # @return [String] The filtered content 26 | def run(content, params = {}) 27 | # Create formatter 28 | r = ::RedCloth.new(content) 29 | 30 | # Set options 31 | r.filter_classes = params[:filter_classes] if params.key?(:filter_classes) 32 | r.filter_html = params[:filter_html] if params.key?(:filter_html) 33 | r.filter_ids = params[:filter_ids] if params.key?(:filter_ids) 34 | r.filter_styles = params[:filter_styles] if params.key?(:filter_styles) 35 | r.hard_breaks = params[:hard_breaks] if params.key?(:hard_breaks) 36 | r.lite_mode = params[:lite_mode] if params.key?(:lite_mode) 37 | r.no_span_caps = params[:no_span_caps] if params.key?(:no_span_caps) 38 | r.sanitize_html = params[:sanitize_html] if params.key?(:sanitize_html) 39 | 40 | # Get result 41 | r.to_html 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /test/helpers/test_breadcrumbs.rb: -------------------------------------------------------------------------------- 1 | class Nanoc::Helpers::BreadcrumbsTest < Nanoc::TestCase 2 | include Nanoc::Helpers::Breadcrumbs 3 | 4 | def test_breadcrumbs_trail_at_root 5 | @items = Nanoc::Int::IdentifiableCollection.new({}) 6 | item = Nanoc::Int::Item.new('root', {}, '/') 7 | @items << item 8 | @item = item 9 | 10 | assert_equal [item], breadcrumbs_trail 11 | end 12 | 13 | def test_breadcrumbs_trail_with_1_parent 14 | @items = Nanoc::Int::IdentifiableCollection.new({}) 15 | parent_item = Nanoc::Int::Item.new('parent', {}, '/') 16 | child_item = Nanoc::Int::Item.new('child', {}, '/foo/') 17 | @items << parent_item 18 | @items << child_item 19 | @item = child_item 20 | 21 | assert_equal [parent_item, child_item], breadcrumbs_trail 22 | end 23 | 24 | def test_breadcrumbs_trail_with_many_parents 25 | @items = Nanoc::Int::IdentifiableCollection.new({}) 26 | grandparent_item = Nanoc::Int::Item.new('grandparent', {}, '/') 27 | parent_item = Nanoc::Int::Item.new('parent', {}, '/foo/') 28 | child_item = Nanoc::Int::Item.new('child', {}, '/foo/bar/') 29 | @items << grandparent_item 30 | @items << parent_item 31 | @items << child_item 32 | @item = child_item 33 | 34 | assert_equal [grandparent_item, parent_item, child_item], breadcrumbs_trail 35 | end 36 | 37 | def test_breadcrumbs_trail_with_nils 38 | @items = Nanoc::Int::IdentifiableCollection.new({}) 39 | grandparent_item = Nanoc::Int::Item.new('grandparent', {}, '/') 40 | child_item = Nanoc::Int::Item.new('child', {}, '/foo/bar/') 41 | @items << grandparent_item 42 | @items << child_item 43 | @item = child_item 44 | 45 | assert_equal [grandparent_item, nil, child_item], breadcrumbs_trail 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/nanoc/helpers/filtering.rb: -------------------------------------------------------------------------------- 1 | module Nanoc::Helpers 2 | # Provides functionality for filtering parts of an item or a layout. 3 | module Filtering 4 | require 'nanoc/helpers/capturing' 5 | include Nanoc::Helpers::Capturing 6 | 7 | # Filters the content in the given block and outputs it. This function 8 | # does not return anything; instead, the filtered contents is directly 9 | # appended to the output buffer (`_erbout`). 10 | # 11 | # This function has been tested with ERB and Haml. Other filters may not 12 | # work correctly. 13 | # 14 | # @example Running a filter on a part of an item or layout 15 | # 16 | #

Lorem ipsum dolor sit amet...

17 | # <% filter :rubypants do %> 18 | #

Consectetur adipisicing elit...

19 | # <% end %> 20 | # 21 | # @param [Symbol] filter_name The name of the filter to run on the 22 | # contents of the block 23 | # 24 | # @param [Hash] arguments Arguments to pass to the filter 25 | # 26 | # @return [void] 27 | def filter(filter_name, arguments = {}, &block) 28 | # Capture block 29 | data = capture(&block) 30 | 31 | # Find filter 32 | klass = Nanoc::Filter.named(filter_name) 33 | raise Nanoc::Int::Errors::UnknownFilter.new(filter_name) if klass.nil? 34 | filter = klass.new(@item_rep.unwrap.assigns) 35 | 36 | # Filter captured data 37 | Nanoc::Int::NotificationCenter.post(:filtering_started, @item_rep.unwrap, filter_name) 38 | filtered_data = filter.setup_and_run(data, arguments) 39 | Nanoc::Int::NotificationCenter.post(:filtering_ended, @item_rep.unwrap, filter_name) 40 | 41 | # Append filtered data to buffer 42 | buffer = eval('_erbout', block.binding) 43 | buffer << filtered_data 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /test/base/test_data_source.rb: -------------------------------------------------------------------------------- 1 | class Nanoc::DataSourceTest < Nanoc::TestCase 2 | def test_loading 3 | # Create data source 4 | data_source = Nanoc::DataSource.new(nil, nil, nil, nil) 5 | data_source.expects(:up).times(1) 6 | data_source.expects(:down).times(1) 7 | 8 | # Test nested loading 9 | assert_equal(0, data_source.instance_eval { @references }) 10 | data_source.loading do 11 | assert_equal(1, data_source.instance_eval { @references }) 12 | data_source.loading do 13 | assert_equal(2, data_source.instance_eval { @references }) 14 | end 15 | assert_equal(1, data_source.instance_eval { @references }) 16 | end 17 | assert_equal(0, data_source.instance_eval { @references }) 18 | end 19 | 20 | def test_not_implemented 21 | # Create data source 22 | data_source = Nanoc::DataSource.new(nil, nil, nil, nil) 23 | 24 | # Test optional methods 25 | data_source.up 26 | data_source.down 27 | 28 | # Test methods - loading data 29 | assert_equal [], data_source.items 30 | assert_equal [], data_source.layouts 31 | end 32 | 33 | def test_new_item 34 | data_source = Nanoc::DataSource.new(nil, nil, nil, nil) 35 | 36 | item = data_source.new_item('stuff', { title: 'Stuff!' }, '/asdf/') 37 | assert_equal 'stuff', item.raw_content 38 | assert_equal 'Stuff!', item.attributes[:title] 39 | assert_equal Nanoc::Identifier.new('/asdf/'), item.identifier 40 | end 41 | 42 | def test_new_layout 43 | data_source = Nanoc::DataSource.new(nil, nil, nil, nil) 44 | 45 | layout = data_source.new_layout('stuff', { title: 'Stuff!' }, '/asdf/') 46 | assert_equal 'stuff', layout.raw_content 47 | assert_equal 'Stuff!', layout.attributes[:title] 48 | assert_equal Nanoc::Identifier.new('/asdf/'), layout.identifier 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /test/base/test_rule_context.rb: -------------------------------------------------------------------------------- 1 | class Nanoc::Int::RuleContextTest < Nanoc::TestCase 2 | def test_objects 3 | # Mock everything 4 | config = mock 5 | items = mock 6 | layouts = mock 7 | site = mock 8 | site.stubs(:config).returns(config) 9 | site.stubs(:items).returns(items) 10 | site.stubs(:layouts).returns(layouts) 11 | item = mock 12 | item.stubs(:site).returns(site) 13 | rep = mock 14 | rep.stubs(:item).returns(item) 15 | compiler = Nanoc::Int::Compiler.new(site) 16 | 17 | # Create context 18 | @rule_context = Nanoc::Int::RuleContext.new(rep: rep, compiler: compiler) 19 | 20 | # Check 21 | assert_equal rep, @rule_context.rep 22 | assert_equal item, @rule_context.item 23 | assert_equal site, @rule_context.site 24 | assert_equal config, @rule_context.config 25 | assert_equal layouts, @rule_context.layouts 26 | assert_equal items, @rule_context.items 27 | end 28 | 29 | def test_actions 30 | # Mock everything 31 | config = mock 32 | items = mock 33 | layouts = mock 34 | site = mock 35 | site.stubs(:config).returns(config) 36 | site.stubs(:items).returns(items) 37 | site.stubs(:layouts).returns(layouts) 38 | item = mock 39 | item.stubs(:site).returns(site) 40 | 41 | # Mock rep 42 | rep = mock 43 | rep.stubs(:item).returns(item) 44 | rep.expects(:filter).with(:foo, { bar: 'baz' }) 45 | rep.expects(:layout).with('foo') 46 | rep.expects(:snapshot).with('awesome') 47 | 48 | # Mock compiler 49 | compiler = Nanoc::Int::Compiler.new(site) 50 | 51 | # Create context 52 | @rule_context = Nanoc::Int::RuleContext.new(rep: rep, compiler: compiler) 53 | 54 | # Check 55 | rep.filter :foo, bar: 'baz' 56 | rep.layout 'foo' 57 | rep.snapshot 'awesome' 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/nanoc/filters/less.rb: -------------------------------------------------------------------------------- 1 | module Nanoc::Filters 2 | # @api private 3 | class Less < Nanoc::Filter 4 | requires 'less' 5 | 6 | # Runs the content through [LESS](http://lesscss.org/). 7 | # This method takes no options. 8 | # 9 | # @param [String] content The content to filter 10 | # 11 | # @return [String] The filtered content 12 | def run(content, params = {}) 13 | # Find imports (hacky) 14 | imports = [] 15 | imports.concat(content.scan(/^@import\s+(["'])([^\1]+?)\1;/)) 16 | imports.concat(content.scan(/^@import\s+url\((["']?)([^)]+?)\1\);/)) 17 | imported_filenames = imports.map do |i| 18 | i[1].match(/\.(less|css)$/) ? i[1] : i[1] + '.less' 19 | end 20 | 21 | # Convert to items 22 | imported_items = imported_filenames.map do |filename| 23 | # Find directory for this item 24 | current_dir_pathname = Pathname.new(@item[:content_filename]).dirname.realpath 25 | 26 | # Find absolute pathname for imported item 27 | imported_pathname = Pathname.new(filename) 28 | if imported_pathname.relative? 29 | imported_pathname = current_dir_pathname + imported_pathname 30 | end 31 | next unless imported_pathname.exist? 32 | imported_filename = imported_pathname.realpath 33 | 34 | # Find matching item 35 | @items.find do |i| 36 | next if i[:content_filename].nil? 37 | Pathname.new(i[:content_filename]).realpath == imported_filename 38 | end 39 | end.compact 40 | 41 | # Create dependencies 42 | depend_on(imported_items) 43 | 44 | # Add filename to load path 45 | paths = [File.dirname(@item[:content_filename])] 46 | parser = ::Less::Parser.new(paths: paths) 47 | parser.parse(content).to_css params 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/nanoc/base/identifiable_collection.rb: -------------------------------------------------------------------------------- 1 | module Nanoc::Int 2 | # @api private 3 | class IdentifiableCollection 4 | include Enumerable 5 | 6 | extend Forwardable 7 | 8 | def_delegator :@objects, :each 9 | def_delegator :@objects, :size 10 | def_delegator :@objects, :<< 11 | def_delegator :@objects, :concat 12 | 13 | def initialize(config) 14 | @config = config 15 | 16 | @objects = [] 17 | end 18 | 19 | def freeze 20 | @objects.freeze 21 | build_mapping 22 | super 23 | end 24 | 25 | def [](arg) 26 | case arg 27 | when String 28 | object_with_identifier(arg) || object_matching_glob(arg) 29 | when Regexp 30 | @objects.find { |i| i.identifier.to_s =~ arg } 31 | else 32 | raise ArgumentError, "don’t know how to fetch objects by #{arg.inspect}" 33 | end 34 | end 35 | 36 | def to_a 37 | @objects 38 | end 39 | 40 | def empty? 41 | @objects.empty? 42 | end 43 | 44 | def delete_if(&block) 45 | @objects.delete_if(&block) 46 | end 47 | 48 | protected 49 | 50 | def object_with_identifier(identifier) 51 | if self.frozen? 52 | @mapping[identifier.to_s] 53 | else 54 | @objects.find { |i| i.identifier == identifier } 55 | end 56 | end 57 | 58 | def object_matching_glob(glob) 59 | if use_globs? 60 | pat = Nanoc::Int::Pattern.from(glob) 61 | @objects.find { |i| pat.match?(i.identifier) } 62 | else 63 | nil 64 | end 65 | end 66 | 67 | def build_mapping 68 | @mapping = {} 69 | @objects.each do |object| 70 | @mapping[object.identifier.to_s] = object 71 | end 72 | @mapping.freeze 73 | end 74 | 75 | def use_globs? 76 | @config[:string_pattern_type] == 'glob' 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/nanoc/cli/commands/show-rules.rb: -------------------------------------------------------------------------------- 1 | usage 'show-rules [thing]' 2 | aliases :explain 3 | summary 'describe the rules for each item' 4 | description " 5 | Prints the rules used for all items and layouts in the current site. 6 | " 7 | 8 | module Nanoc::CLI::Commands 9 | class ShowRules < ::Nanoc::CLI::CommandRunner 10 | def run 11 | require_site 12 | 13 | @c = Nanoc::CLI::ANSIStringColorizer 14 | @calc = site.compiler.rule_memory_calculator 15 | 16 | # TODO: explain /foo/ 17 | # TODO: explain content/foo.html 18 | # TODO: explain output/foo/index.html 19 | 20 | site.items.each { |i| explain_item(i) } 21 | site.layouts.each { |l| explain_layout(l) } 22 | end 23 | 24 | protected 25 | 26 | def explain_item(item) 27 | puts "#{@c.c('Item ' + item.identifier, :bold, :yellow)}:" 28 | puts " (from #{item[:filename]})" if item[:filename] 29 | item.reps.each do |rep| 30 | puts " Rep #{rep.name}:" 31 | if @calc[rep].empty? && rep.raw_path.nil? 32 | puts ' (nothing)' 33 | else 34 | @calc[rep].each do |mem| 35 | puts format(' %s %s', 36 | @c.c(format('%-10s', mem[0].to_s), :blue), 37 | mem[1..-1].map(&:inspect).join(', ')) 38 | end 39 | if rep.raw_path 40 | puts format(' %s %s', 41 | @c.c(format('%-10s', 'write to'), :blue), 42 | rep.raw_path) 43 | end 44 | end 45 | end 46 | puts 47 | end 48 | 49 | def explain_layout(layout) 50 | puts "#{@c.c('Layout ' + layout.identifier, :bold, :yellow)}:" 51 | puts " (from #{layout[:filename]})" if layout[:filename] 52 | puts format(' %s %s', 53 | @c.c(format('%-10s', 'filter'), :blue), 54 | @calc[layout].map(&:inspect).join(', ')) 55 | puts 56 | end 57 | end 58 | end 59 | 60 | runner Nanoc::CLI::Commands::ShowRules 61 | -------------------------------------------------------------------------------- /test/filters/test_handlebars.rb: -------------------------------------------------------------------------------- 1 | class Nanoc::Filters::HandlebarsTest < Nanoc::TestCase 2 | def test_filter 3 | if_have 'handlebars' do 4 | # Create data 5 | item = Nanoc::Int::Item.new( 6 | 'content', 7 | { title: 'Max Payne', protagonist: 'Max Payne', location: 'here' }, 8 | '/games/max-payne/') 9 | layout = Nanoc::Int::Layout.new( 10 | 'layout content', 11 | { name: 'Max Payne' }, 12 | '/default/') 13 | config = { animals: 'cats and dogs' } 14 | 15 | # Create filter 16 | assigns = { 17 | item: item, 18 | layout: layout, 19 | config: config, 20 | content: 'No Payne No Gayne' 21 | } 22 | filter = ::Nanoc::Filters::Handlebars.new(assigns) 23 | 24 | # Run filter 25 | result = filter.setup_and_run('{{protagonist}} says: {{yield}}.') 26 | assert_equal('Max Payne says: No Payne No Gayne.', result) 27 | result = filter.setup_and_run('We can’t stop {{item.location}}! This is the {{layout.name}} layout!') 28 | assert_equal('We can’t stop here! This is the Max Payne layout!', result) 29 | result = filter.setup_and_run('It’s raining {{config.animals}} here!') 30 | assert_equal('It’s raining cats and dogs here!', result) 31 | end 32 | end 33 | 34 | def test_filter_without_layout 35 | if_have 'handlebars' do 36 | # Create data 37 | item = Nanoc::Int::Item.new( 38 | 'content', 39 | { title: 'Max Payne', protagonist: 'Max Payne', location: 'here' }, 40 | '/games/max-payne/') 41 | 42 | # Create filter 43 | assigns = { 44 | item: item, 45 | content: 'No Payne No Gayne' 46 | } 47 | filter = ::Nanoc::Filters::Handlebars.new(assigns) 48 | 49 | # Run filter 50 | result = filter.setup_and_run('{{protagonist}} says: {{yield}}.') 51 | assert_equal('Max Payne says: No Payne No Gayne.', result) 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /test/base/temp_filename_factory_spec.rb: -------------------------------------------------------------------------------- 1 | describe Nanoc::Int::TempFilenameFactory do 2 | subject do 3 | Nanoc::Int::TempFilenameFactory.new 4 | end 5 | 6 | let(:prefix) { 'foo' } 7 | 8 | describe '#create' do 9 | it 'should create unique paths' do 10 | path_a = subject.create(prefix) 11 | path_b = subject.create(prefix) 12 | path_a.wont_equal(path_b) 13 | end 14 | 15 | it 'should return absolute paths' do 16 | path = subject.create(prefix) 17 | path.must_match(/\A\//) 18 | end 19 | 20 | it 'should create the containing directory' do 21 | Dir[subject.root_dir + '/**/*'].must_equal([]) 22 | path = subject.create(prefix) 23 | File.directory?(File.dirname(path)).must_equal(true) 24 | end 25 | 26 | it 'should reuse the same path after cleanup' do 27 | path_a = subject.create(prefix) 28 | subject.cleanup(prefix) 29 | path_b = subject.create(prefix) 30 | path_a.must_equal(path_b) 31 | end 32 | end 33 | 34 | describe '#cleanup' do 35 | it 'should remove generated files' do 36 | path_a = subject.create(prefix) 37 | File.file?(path_a).wont_equal(true) # not yet used 38 | 39 | File.open(path_a, 'w') { |io| io << 'hi!' } 40 | File.file?(path_a).must_equal(true) 41 | 42 | subject.cleanup(prefix) 43 | File.file?(path_a).wont_equal(true) 44 | end 45 | 46 | it 'should eventually delete the root directory' do 47 | subject.create(prefix) 48 | File.directory?(subject.root_dir).must_equal(true) 49 | 50 | subject.cleanup(prefix) 51 | File.directory?(subject.root_dir).wont_equal(true) 52 | end 53 | end 54 | 55 | describe 'other instance' do 56 | let(:other_instance) do 57 | Nanoc::Int::TempFilenameFactory.new 58 | end 59 | 60 | it 'should create unique paths across instances' do 61 | path_a = subject.create(prefix) 62 | path_b = other_instance.create(prefix) 63 | path_a.wont_equal(path_b) 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/nanoc/base/views/identifiable_collection.rb: -------------------------------------------------------------------------------- 1 | module Nanoc 2 | class IdentifiableCollectionView 3 | include Enumerable 4 | 5 | # @api private 6 | def initialize(objects) 7 | @objects = objects 8 | end 9 | 10 | # @api private 11 | def unwrap 12 | @objects 13 | end 14 | 15 | # @abstract 16 | # 17 | # @api private 18 | def view_class 19 | raise NotImplementedError 20 | end 21 | 22 | # Calls the given block once for each object, passing that object as a parameter. 23 | # 24 | # @yieldparam [#identifier] object 25 | # 26 | # @yieldreturn [void] 27 | # 28 | # @return [self] 29 | def each 30 | @objects.each { |i| yield view_class.new(i) } 31 | self 32 | end 33 | 34 | # @return [Integer] 35 | def size 36 | @objects.size 37 | end 38 | 39 | # Finds all objects whose identifier matches the given argument. 40 | # 41 | # @param [String, Regex] arg 42 | # 43 | # @return [Enumerable] 44 | def find_all(arg) 45 | pat = Nanoc::Int::Pattern.from(arg) 46 | select { |i| pat.match?(i.identifier) } 47 | end 48 | 49 | # @overload [](string) 50 | # 51 | # Finds the object whose identifier matches the given string. 52 | # 53 | # If the glob syntax is enabled, the string can be a glob, in which case 54 | # this method finds the first object that matches the given glob. 55 | # 56 | # @param [String] string 57 | # 58 | # @return [nil] if no object matches the string 59 | # 60 | # @return [#identifier] if an object was found 61 | # 62 | # @overload [](regex) 63 | # 64 | # Finds the object whose identifier matches the given regular expression. 65 | # 66 | # @param [Regex] regex 67 | # 68 | # @return [nil] if no object matches the regex 69 | # 70 | # @return [#identifier] if an object was found 71 | def [](arg) 72 | res = @objects[arg] 73 | res && view_class.new(res) 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /lib/nanoc/extra/deployers/rsync.rb: -------------------------------------------------------------------------------- 1 | module Nanoc::Extra::Deployers 2 | # A deployer that deploys a site using rsync. 3 | # 4 | # The configuration has should include a `:dst` value, a string containing 5 | # the destination to where rsync should upload its data. It will likely be 6 | # in `host:path` format. It should not end with a slash. For example, 7 | # `"example.com:/var/www/sites/mysite/html"`. 8 | # 9 | # @example A deployment configuration with public and staging configurations 10 | # 11 | # deploy: 12 | # public: 13 | # kind: rsync 14 | # dst: "ectype:sites/stoneship/public" 15 | # staging: 16 | # kind: rsync 17 | # dst: "ectype:sites/stoneship-staging/public" 18 | # options: [ "-glpPrtvz" ] 19 | # 20 | # @api private 21 | class Rsync < ::Nanoc::Extra::Deployer 22 | # Default rsync options 23 | DEFAULT_OPTIONS = [ 24 | '--group', 25 | '--links', 26 | '--perms', 27 | '--partial', 28 | '--progress', 29 | '--recursive', 30 | '--times', 31 | '--verbose', 32 | '--compress', 33 | '--exclude=".hg"', 34 | '--exclude=".svn"', 35 | '--exclude=".git"' 36 | ] 37 | 38 | # @see Nanoc::Extra::Deployer#run 39 | def run 40 | # Get params 41 | src = source_path + '/' 42 | dst = config[:dst] 43 | options = config[:options] || DEFAULT_OPTIONS 44 | 45 | # Validate 46 | raise 'No dst found in deployment configuration' if dst.nil? 47 | raise 'dst requires no trailing slash' if dst[-1, 1] == '/' 48 | 49 | # Run 50 | if dry_run 51 | warn 'Performing a dry-run; no actions will actually be performed' 52 | run_shell_cmd(['echo', 'rsync', options, src, dst].flatten) 53 | else 54 | run_shell_cmd(['rsync', options, src, dst].flatten) 55 | end 56 | end 57 | 58 | private 59 | 60 | def run_shell_cmd(cmd) 61 | piper = Nanoc::Extra::Piper.new(stdout: $stdout, stderr: $stderr) 62 | piper.run(cmd, nil) 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /test/base/test_filter.rb: -------------------------------------------------------------------------------- 1 | class Nanoc::FilterTest < Nanoc::TestCase 2 | def test_initialize 3 | # Create filter 4 | filter = Nanoc::Filter.new 5 | 6 | # Test assigns 7 | assert_equal({}, filter.instance_eval { @assigns }) 8 | end 9 | 10 | def test_assigns 11 | # Create filter 12 | filter = Nanoc::Filter.new({ foo: 'bar' }) 13 | 14 | # Check assigns 15 | assert_equal('bar', filter.assigns[:foo]) 16 | end 17 | 18 | def test_assigns_with_instance_variables 19 | # Create filter 20 | filter = Nanoc::Filter.new({ foo: 'bar' }) 21 | 22 | # Check assigns 23 | assert_equal('bar', filter.instance_eval { @foo }) 24 | end 25 | 26 | def test_assigns_with_instance_methods 27 | # Create filter 28 | filter = Nanoc::Filter.new({ foo: 'bar' }) 29 | 30 | # Check assigns 31 | assert_equal('bar', filter.instance_eval { foo }) 32 | end 33 | 34 | def test_run 35 | # Create filter 36 | filter = Nanoc::Filter.new 37 | 38 | # Make sure an error is raised 39 | assert_raises(NotImplementedError) do 40 | filter.run(nil) 41 | end 42 | end 43 | 44 | def test_filename_item 45 | # Mock items 46 | item = mock 47 | item.expects(:identifier).returns('/foo/bar/baz/') 48 | item_rep = mock 49 | item_rep.expects(:name).returns(:quux) 50 | 51 | # Create filter 52 | filter = Nanoc::Filter.new({ item: item, item_rep: item_rep }) 53 | 54 | # Check filename 55 | assert_equal('item /foo/bar/baz/ (rep quux)', filter.filename) 56 | end 57 | 58 | def test_filename_layout 59 | # Mock items 60 | layout = mock 61 | layout.expects(:identifier).returns('/wohba/') 62 | 63 | # Create filter 64 | filter = Nanoc::Filter.new({ item: mock, item_rep: mock, layout: layout }) 65 | 66 | # Check filename 67 | assert_equal('layout /wohba/', filter.filename) 68 | end 69 | 70 | def test_filename_unknown 71 | # Create filter 72 | filter = Nanoc::Filter.new({}) 73 | 74 | # Check filename 75 | assert_equal('?', filter.filename) 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /lib/nanoc/base/views/item_rep.rb: -------------------------------------------------------------------------------- 1 | module Nanoc 2 | class ItemRepView 3 | # @api private 4 | def initialize(item_rep) 5 | @item_rep = item_rep 6 | end 7 | 8 | # @api private 9 | def unwrap 10 | @item_rep 11 | end 12 | 13 | # @see Object#== 14 | def ==(other) 15 | item.identifier == other.item.identifier && name == other.name 16 | end 17 | alias_method :eql?, :== 18 | 19 | # @see Object#hash 20 | def hash 21 | self.class.hash ^ item.identifier.hash ^ name.hash 22 | end 23 | 24 | # @return [Symbol] 25 | def name 26 | @item_rep.name 27 | end 28 | 29 | # Returns the compiled content. 30 | # 31 | # @option params [String] :snapshot The name of the snapshot from which to 32 | # fetch the compiled content. By default, the returned compiled content 33 | # will be the content compiled right before the first layout call (if 34 | # any). 35 | # 36 | # @return [String] The content at the given snapshot. 37 | def compiled_content(params = {}) 38 | @item_rep.compiled_content(params) 39 | end 40 | 41 | # Returns the item rep’s path, as used when being linked to. It starts 42 | # with a slash and it is relative to the output directory. It does not 43 | # include the path to the output directory. It will not include the 44 | # filename if the filename is an index filename. 45 | # 46 | # @option params [Symbol] :snapshot (:last) The snapshot for which the 47 | # path should be returned. 48 | # 49 | # @return [String] The item rep’s path. 50 | def path(params = {}) 51 | @item_rep.path(params) 52 | end 53 | 54 | # Returns the item that this item rep belongs to. 55 | # 56 | # @return [Nanoc::ItemView] 57 | def item 58 | Nanoc::ItemView.new(@item_rep.item) 59 | end 60 | 61 | # @api private 62 | def raw_path(params = {}) 63 | @item_rep.raw_path(params) 64 | end 65 | 66 | # @api private 67 | def binary? 68 | @item_rep.binary? 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /test/extra/checking/checks/test_css.rb: -------------------------------------------------------------------------------- 1 | class Nanoc::Extra::Checking::Checks::CSSTest < Nanoc::TestCase 2 | def test_run_ok 3 | VCR.use_cassette('css_run_ok') do 4 | with_site do |site| 5 | # Create files 6 | FileUtils.mkdir_p('output') 7 | File.open('output/blah.html', 'w') { |io| io.write('

Hi!

') } 8 | File.open('output/style.css', 'w') { |io| io.write('h1 { color: red; }') } 9 | 10 | # Run check 11 | check = Nanoc::Extra::Checking::Checks::CSS.create(site) 12 | check.run 13 | 14 | # Check 15 | assert check.issues.empty? 16 | end 17 | end 18 | end 19 | 20 | def test_run_error 21 | VCR.use_cassette('css_run_error') do 22 | with_site do |site| 23 | # Create files 24 | FileUtils.mkdir_p('output') 25 | File.open('output/blah.html', 'w') { |io| io.write('

Hi!

') } 26 | File.open('output/style.css', 'w') { |io| io.write('h1 { coxlor: rxed; }') } 27 | 28 | # Run check 29 | check = Nanoc::Extra::Checking::Checks::CSS.create(site) 30 | check.run 31 | 32 | # Check 33 | refute check.issues.empty? 34 | assert_equal 1, check.issues.size 35 | assert_equal 'line 1: Property coxlor doesn\'t exist: h1 { coxlor: rxed; }', 36 | check.issues.to_a[0].description 37 | end 38 | end 39 | end 40 | 41 | def test_run_parse_error 42 | VCR.use_cassette('css_run_parse_error') do 43 | with_site do |site| 44 | # Create files 45 | FileUtils.mkdir_p('output') 46 | File.open('output/blah.html', 'w') { |io| io.write('

Hi!

') } 47 | File.open('output/style.css', 'w') { |io| io.write('h1 { ; {') } 48 | 49 | # Run check 50 | check = Nanoc::Extra::Checking::Checks::CSS.create(site) 51 | check.run 52 | 53 | # Check 54 | refute check.issues.empty? 55 | assert_equal 1, check.issues.size 56 | assert_equal 'line 1: Parse Error: h1 { ; {', 57 | check.issues.to_a[0].description 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/nanoc/base/memoization.rb: -------------------------------------------------------------------------------- 1 | module Nanoc::Int 2 | # Adds support for memoizing functions. 3 | # 4 | # @api private 5 | # 6 | # @since 3.2.0 7 | module Memoization 8 | # Memoizes the method with the given name. The modified method will cache 9 | # the results of the original method, so that calling a method twice with 10 | # the same arguments will short-circuit and return the cached results 11 | # immediately. 12 | # 13 | # Memoization assumes that the current object as well as the function 14 | # arguments are immutable. Mutating the object or the arguments will not 15 | # cause memoized methods to recalculate their results. There is no way to 16 | # un-memoize a result, and calculation results will remain in memory even 17 | # if they are no longer needed. 18 | # 19 | # @example A fast fib function due to memoization 20 | # 21 | # class FibFast 22 | # 23 | # extend Nanoc::Int::Memoization 24 | # 25 | # def run(n) 26 | # if n == 0 27 | # 0 28 | # elsif n == 1 29 | # 1 30 | # else 31 | # run(n-1) + run(n-2) 32 | # end 33 | # end 34 | # memoize :run 35 | # 36 | # end 37 | # 38 | # @param [Symbol, String] method_name The name of the method to memoize 39 | # 40 | # @return [void] 41 | def memoize(method_name) 42 | # Alias 43 | original_method_name = '__nonmemoized_' + method_name.to_s 44 | alias_method original_method_name, method_name 45 | 46 | # Redefine 47 | define_method(method_name) do |*args| 48 | # Get cache 49 | @__memoization_cache ||= {} 50 | @__memoization_cache[method_name] ||= {} 51 | 52 | # Recalculate if necessary 53 | unless @__memoization_cache[method_name].key?(args) 54 | result = send(original_method_name, *args) 55 | @__memoization_cache[method_name][args] = result 56 | end 57 | 58 | # Done 59 | @__memoization_cache[method_name][args] 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /test/cli/test_error_handler.rb: -------------------------------------------------------------------------------- 1 | class Nanoc::CLI::ErrorHandlerTest < Nanoc::TestCase 2 | def setup 3 | super 4 | @handler = Nanoc::CLI::ErrorHandler.new 5 | end 6 | 7 | def test_resolution_for_with_unknown_gem 8 | error = LoadError.new('no such file to load -- afjlrestjlsgrshter') 9 | assert_nil @handler.send(:resolution_for, error) 10 | end 11 | 12 | def test_resolution_for_with_known_gem_without_bundler 13 | def @handler.using_bundler? 14 | false 15 | end 16 | error = LoadError.new('no such file to load -- kramdown') 17 | assert_match(/^Install the 'kramdown' gem using `gem install kramdown`./, @handler.send(:resolution_for, error)) 18 | end 19 | 20 | def test_resolution_for_with_known_gem_with_bundler 21 | def @handler.using_bundler? 22 | true 23 | end 24 | error = LoadError.new('no such file to load -- kramdown') 25 | assert_match(/^Make sure the gem is added to Gemfile/, @handler.send(:resolution_for, error)) 26 | end 27 | 28 | def test_resolution_for_with_not_load_error 29 | error = RuntimeError.new('nuclear meltdown detected') 30 | assert_nil @handler.send(:resolution_for, error) 31 | end 32 | 33 | def test_write_stack_trace_verbose 34 | error = new_error(20) 35 | 36 | stream = StringIO.new 37 | @handler.send(:write_stack_trace, stream, error, verbose: false) 38 | assert_match(/See full crash log for details./, stream.string) 39 | 40 | stream = StringIO.new 41 | @handler.send(:write_stack_trace, stream, error, verbose: false) 42 | assert_match(/See full crash log for details./, stream.string) 43 | 44 | stream = StringIO.new 45 | @handler.send(:write_stack_trace, stream, error, verbose: true) 46 | refute_match(/See full crash log for details./, stream.string) 47 | end 48 | 49 | def new_error(amount_factor) 50 | backtrace_generator = lambda do |af| 51 | if af == 0 52 | raise 'finally!' 53 | else 54 | backtrace_generator.call(af - 1) 55 | end 56 | end 57 | 58 | begin 59 | backtrace_generator.call(amount_factor) 60 | rescue => e 61 | return e 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/nanoc/cli/logger.rb: -------------------------------------------------------------------------------- 1 | require 'singleton' 2 | 3 | module Nanoc::CLI 4 | # Nanoc::CLI::Logger is a singleton class responsible for generating 5 | # feedback in the terminal. 6 | # 7 | # @api private 8 | class Logger 9 | # Maps actions (`:create`, `:update`, `:identical`, `:skip` and `:delete`) 10 | # onto their ANSI color codes. 11 | ACTION_COLORS = { 12 | create: "\e[32m", # green 13 | update: "\e[33m", # yellow 14 | identical: '', # (nothing) 15 | skip: '', # (nothing) 16 | delete: "\e[31m" # red 17 | } 18 | 19 | include Singleton 20 | 21 | # Returns the log level, which can be :high, :low or :off (which will log 22 | # all messages, only high-priority messages, or no messages at all, 23 | # respectively). 24 | # 25 | # @return [Symbol] The log level 26 | attr_accessor :level 27 | 28 | def initialize 29 | @level = :high 30 | end 31 | 32 | # Logs a file-related action. 33 | # 34 | # @param [:high, :low] level The importance of this action 35 | # 36 | # @param [:create, :update, :identical, :skip, :delete] action The kind of file action 37 | # 38 | # @param [String] name The name of the file the action was performed on 39 | # 40 | # @return [void] 41 | def file(level, action, name, duration = nil) 42 | log( 43 | level, 44 | format('%s%12s%s %s%s', 45 | ACTION_COLORS[action.to_sym], 46 | action, 47 | "\e[0m", 48 | duration.nil? ? '' : format('[%2.2fs] ', duration), 49 | name)) 50 | end 51 | 52 | # Logs a message. 53 | # 54 | # @param [:high, :low] level The importance of this message 55 | # 56 | # @param [String] message The message to be logged 57 | # 58 | # @param [#puts] io The stream to which the message should be written 59 | # 60 | # @return [void] 61 | def log(level, message, io = $stdout) 62 | # Don't log when logging is disabled 63 | return if @level == :off 64 | 65 | # Log when level permits it 66 | io.puts(message) if @level == :low || @level == level 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /test/base/test_item_array.rb: -------------------------------------------------------------------------------- 1 | class Nanoc::Int::IdentifiableCollectionTest < Nanoc::TestCase 2 | def setup 3 | super 4 | 5 | @one = Nanoc::Int::Item.new('Item One', {}, '/one/') 6 | @two = Nanoc::Int::Item.new('Item Two', {}, '/two/') 7 | 8 | @items = Nanoc::Int::IdentifiableCollection.new({}) 9 | @items << @one 10 | @items << @two 11 | end 12 | 13 | def test_change_item_identifier 14 | assert_equal @one, @items['/one/'] 15 | assert_nil @items['/foo/'] 16 | 17 | @one.identifier = '/foo/' 18 | 19 | assert_nil @items['/one/'] 20 | assert_equal @one, @items['/foo/'] 21 | end 22 | 23 | def test_enumerable 24 | assert_equal @one, @items.find { |i| i.identifier == '/one/' } 25 | end 26 | 27 | def test_brackets_with_glob 28 | @items = Nanoc::Int::IdentifiableCollection.new({ string_pattern_type: 'glob' }) 29 | @items << @one 30 | @items << @two 31 | 32 | assert_equal @one, @items['/on*/'] 33 | assert_equal @two, @items['/*wo/'] 34 | end 35 | 36 | def test_brackets_with_identifier 37 | assert_equal @one, @items['/one/'] 38 | assert_equal @two, @items['/two/'] 39 | assert_nil @items['/max-payne/'] 40 | end 41 | 42 | def test_brackets_with_malformed_identifier 43 | assert_nil @items['one/'] 44 | assert_nil @items['/one'] 45 | assert_nil @items['one'] 46 | assert_nil @items['//one/'] 47 | end 48 | 49 | def test_brackets_frozen 50 | @items.freeze 51 | 52 | assert_equal @one, @items['/one/'] 53 | assert_nil @items['/tenthousand/'] 54 | end 55 | 56 | def test_regex 57 | foo = Nanoc::Int::Item.new('Item Foo', {}, '/foo/') 58 | @items << foo 59 | 60 | assert_equal @one, @items[/n/] 61 | assert_equal @two, @items[%r{o/}] # not foo 62 | end 63 | 64 | def test_less_than_less_than 65 | assert_nil @items['/foo/'] 66 | 67 | foo = Nanoc::Int::Item.new('Item Foo', {}, '/foo/') 68 | @items << foo 69 | 70 | assert_equal foo, @items['/foo/'] 71 | end 72 | 73 | def test_concat 74 | new_item = Nanoc::Int::Item.new('New item', {}, '/new/') 75 | @items.concat([new_item]) 76 | 77 | assert_equal new_item, @items['/new/'] 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/nanoc/cli/command_runner.rb: -------------------------------------------------------------------------------- 1 | module Nanoc::CLI 2 | # A command runner subclass for nanoc commands that adds nanoc-specific 3 | # convenience methods and error handling. 4 | # 5 | # @api private 6 | class CommandRunner < ::Cri::CommandRunner 7 | # @see http://rubydoc.info/gems/cri/Cri/CommandRunner#call-instance_method 8 | # 9 | # @return [void] 10 | def call 11 | Nanoc::CLI::ErrorHandler.handle_while(command: self) do 12 | run 13 | end 14 | end 15 | 16 | # Gets the site ({Nanoc::Int::Site} instance) in the current directory and 17 | # loads its data. 18 | # 19 | # @return [Nanoc::Int::Site] The site in the current working directory 20 | def site 21 | # Load site if possible 22 | @site ||= nil 23 | if self.is_in_site_dir? && @site.nil? 24 | @site = Nanoc::Int::Site.new('.') 25 | end 26 | 27 | @site 28 | end 29 | 30 | # @return [Boolean] true if the current working directory is a nanoc site 31 | # directory, false otherwise 32 | def in_site_dir? 33 | Nanoc::Int::Site.cwd_is_nanoc_site? 34 | end 35 | alias_method :is_in_site_dir?, :in_site_dir? 36 | 37 | # Asserts that the current working directory contains a site 38 | # ({Nanoc::Int::Site} instance). If no site is present, prints an error 39 | # message and exits. 40 | # 41 | # @return [void] 42 | def require_site 43 | if site.nil? 44 | raise ::Nanoc::Int::Errors::GenericTrivial, 'The current working directory does not seem to be a nanoc site.' 45 | end 46 | end 47 | 48 | # Asserts that the current working directory contains a site (just like 49 | # {#require_site}) and loads the site into memory. 50 | # 51 | # @return [void] 52 | def load_site 53 | require_site 54 | print 'Loading site data… ' 55 | site.load 56 | puts 'done' 57 | end 58 | 59 | # @return [Boolean] true if debug output is enabled, false if not 60 | # 61 | # @see Nanoc::CLI.debug? 62 | def debug? 63 | Nanoc::CLI.debug? 64 | end 65 | 66 | protected 67 | 68 | # @return [Array] The compilation stack. 69 | def stack 70 | (site && site.compiler.stack) || [] 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /test/extra/deployers/test_rsync.rb: -------------------------------------------------------------------------------- 1 | class Nanoc::Extra::Deployers::RsyncTest < Nanoc::TestCase 2 | def test_run_without_dst 3 | # Create deployer 4 | rsync = Nanoc::Extra::Deployers::Rsync.new( 5 | 'output/', 6 | {}) 7 | 8 | # Mock run_shell_cmd 9 | def rsync.run_shell_cmd(args) 10 | @shell_cms_args = args 11 | end 12 | 13 | # Try running 14 | error = assert_raises(RuntimeError) do 15 | rsync.run 16 | end 17 | 18 | # Check error message 19 | assert_equal 'No dst found in deployment configuration', error.message 20 | end 21 | 22 | def test_run_with_erroneous_dst 23 | # Create deployer 24 | rsync = Nanoc::Extra::Deployers::Rsync.new( 25 | 'output/', 26 | { dst: 'asdf/' }) 27 | 28 | # Mock run_shell_cmd 29 | def rsync.run_shell_cmd(args) 30 | @shell_cms_args = args 31 | end 32 | 33 | # Try running 34 | error = assert_raises(RuntimeError) do 35 | rsync.run 36 | end 37 | 38 | # Check error message 39 | assert_equal 'dst requires no trailing slash', error.message 40 | end 41 | 42 | def test_run_everything_okay 43 | # Create deployer 44 | rsync = Nanoc::Extra::Deployers::Rsync.new( 45 | 'output', 46 | { dst: 'asdf' }) 47 | 48 | # Mock run_shell_cmd 49 | def rsync.run_shell_cmd(args) 50 | @shell_cms_args = args 51 | end 52 | 53 | # Run 54 | rsync.run 55 | 56 | # Check args 57 | opts = Nanoc::Extra::Deployers::Rsync::DEFAULT_OPTIONS 58 | assert_equal( 59 | ['rsync', opts, 'output/', 'asdf'].flatten, 60 | rsync.instance_eval { @shell_cms_args } 61 | ) 62 | end 63 | 64 | def test_run_everything_okay_dry 65 | # Create deployer 66 | rsync = Nanoc::Extra::Deployers::Rsync.new( 67 | 'output', 68 | { dst: 'asdf' }, 69 | dry_run: true) 70 | 71 | # Mock run_shell_cmd 72 | def rsync.run_shell_cmd(args) 73 | @shell_cms_args = args 74 | end 75 | 76 | # Run 77 | rsync.run 78 | 79 | # Check args 80 | opts = Nanoc::Extra::Deployers::Rsync::DEFAULT_OPTIONS 81 | assert_equal( 82 | ['echo', 'rsync', opts, 'output/', 'asdf'].flatten, 83 | rsync.instance_eval { @shell_cms_args } 84 | ) 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /test/fixtures/vcr_cassettes/css_run_ok.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: http://jigsaw.w3.org/css-validator/validator?output=soap12&profile=css3&text=h1%20%7B%20color:%20red%3B%20%7D 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | Accept-Encoding: 11 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 12 | Accept: 13 | - "*/*" 14 | User-Agent: 15 | - Ruby 16 | response: 17 | status: 18 | code: 200 19 | message: OK 20 | headers: 21 | Cache-Control: 22 | - no-cache 23 | Date: 24 | - Thu, 24 Apr 2014 07:25:20 GMT 25 | Pragma: 26 | - no-cache 27 | Transfer-Encoding: 28 | - chunked 29 | Content-Language: 30 | - en 31 | Content-Type: 32 | - application/soap+xml;charset=utf-8 33 | Server: 34 | - Jigsaw/2.3.0-beta3 35 | Vary: 36 | - Accept-Language 37 | X-W3c-Validator-Errors: 38 | - '0' 39 | X-W3c-Validator-Status: 40 | - Valid 41 | body: 42 | encoding: UTF-8 43 | string: "\n\n 44 | \ \n \n file://localhost/TextArea\n 46 | \ http://jigsaw.w3.org/css-validator/\n 47 | \ css3\n 2014-04-24T07:25:20Z\n 48 | \ true\n \n \n 0\n \n 50 | \ \n \n 51 | \ 0\n \n 52 | \ \n \n \n\n\n" 53 | http_version: 54 | recorded_at: Thu, 24 Apr 2014 07:25:20 GMT 55 | recorded_with: VCR 2.9.0 56 | -------------------------------------------------------------------------------- /test/extra/checking/checks/test_stale.rb: -------------------------------------------------------------------------------- 1 | class Nanoc::Extra::Checking::Checks::StaleTest < Nanoc::TestCase 2 | def check_class 3 | Nanoc::Extra::Checking::Checks::Stale 4 | end 5 | 6 | def calc_issues 7 | site = Nanoc::Int::Site.new('.') 8 | check = check_class.create(site) 9 | check.run 10 | check.issues 11 | end 12 | 13 | def test_run_ok 14 | with_site do |_site| 15 | assert Dir['content/*'].empty? 16 | assert Dir['output/*'].empty? 17 | 18 | # Empty 19 | FileUtils.mkdir_p('output') 20 | assert calc_issues.empty? 21 | 22 | # One OK file 23 | File.open('content/index.html', 'w') { |io| io.write('stuff') } 24 | File.open('output/index.html', 'w') { |io| io.write('stuff') } 25 | assert calc_issues.empty? 26 | end 27 | end 28 | 29 | def test_run_error 30 | with_site do |_site| 31 | assert Dir['content/*'].empty? 32 | assert Dir['output/*'].empty? 33 | 34 | File.open('content/index.html', 'w') { |io| io.write('stuff') } 35 | File.open('output/WRONG.html', 'w') { |io| io.write('stuff') } 36 | assert_equal 1, calc_issues.count 37 | issue = calc_issues.to_a[0] 38 | assert_equal 'file without matching item', issue.description 39 | assert_equal 'output/WRONG.html', issue.subject 40 | end 41 | end 42 | 43 | def test_run_excluded 44 | with_site do |_site| 45 | assert Dir['content/*'].empty? 46 | assert Dir['output/*'].empty? 47 | 48 | File.open('nanoc.yaml', 'w') { |io| io.write "string_pattern_type: legacy\nprune:\n exclude: [ 'excluded.html' ]" } 49 | File.open('content/index.html', 'w') { |io| io.write('stuff') } 50 | File.open('output/excluded.html', 'w') { |io| io.write('stuff') } 51 | assert calc_issues.empty? 52 | end 53 | end 54 | 55 | def test_run_excluded_with_broken_config 56 | with_site do |_site| 57 | assert Dir['content/*'].empty? 58 | assert Dir['output/*'].empty? 59 | 60 | File.open('nanoc.yaml', 'w') { |io| io.write "string_pattern_type: legacy\nprune:\n blah: meh" } 61 | File.open('content/index.html', 'w') { |io| io.write('stuff') } 62 | File.open('output/excluded.html', 'w') { |io| io.write('stuff') } 63 | refute calc_issues.empty? 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /test/filters/test_erubis.rb: -------------------------------------------------------------------------------- 1 | class Nanoc::Filters::ErubisTest < Nanoc::TestCase 2 | def test_filter_with_instance_variable 3 | if_have 'erubis' do 4 | # Create filter 5 | filter = ::Nanoc::Filters::Erubis.new({ location: 'a cheap motel' }) 6 | 7 | # Run filter 8 | result = filter.setup_and_run('<%= "I was hiding in #{@location}." %>') 9 | assert_equal('I was hiding in a cheap motel.', result) 10 | end 11 | end 12 | 13 | def test_filter_with_instance_method 14 | if_have 'erubis' do 15 | # Create filter 16 | filter = ::Nanoc::Filters::Erubis.new({ location: 'a cheap motel' }) 17 | 18 | # Run filter 19 | result = filter.setup_and_run('<%= "I was hiding in #{location}." %>') 20 | assert_equal('I was hiding in a cheap motel.', result) 21 | end 22 | end 23 | 24 | def test_filter_error 25 | if_have 'erubis' do 26 | # Create filter 27 | filter = ::Nanoc::Filters::Erubis.new 28 | 29 | # Run filter 30 | raised = false 31 | begin 32 | filter.setup_and_run('<%= this isn\'t really ruby so it\'ll break, muahaha %>') 33 | rescue SyntaxError => e 34 | e.message =~ /(.+?):\d+: / 35 | assert_match '?', $1 36 | raised = true 37 | end 38 | assert raised 39 | end 40 | end 41 | 42 | def test_filter_with_yield 43 | if_have 'erubis' do 44 | # Create filter 45 | filter = ::Nanoc::Filters::Erubis.new({ content: 'a cheap motel' }) 46 | 47 | # Run filter 48 | result = filter.setup_and_run('<%= "I was hiding in #{yield}." %>') 49 | assert_equal('I was hiding in a cheap motel.', result) 50 | end 51 | end 52 | 53 | def test_filter_with_yield_without_content 54 | if_have 'erubis' do 55 | # Create filter 56 | filter = ::Nanoc::Filters::Erubis.new({ location: 'a cheap motel' }) 57 | 58 | # Run filter 59 | assert_raises LocalJumpError do 60 | filter.setup_and_run('<%= "I was hiding in #{yield}." %>') 61 | end 62 | end 63 | end 64 | 65 | def test_filter_with_erbout 66 | if_have 'erubis' do 67 | filter = ::Nanoc::Filters::Erubis.new 68 | result = filter.setup_and_run('stuff<% _erbout << _erbout %>') 69 | assert_equal 'stuffstuff', result 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Gem version](http://img.shields.io/gem/v/nanoc.svg)](http://rubygems.org/gems/nanoc) 2 | [![Build status](http://img.shields.io/travis/nanoc/nanoc.svg)](https://travis-ci.org/nanoc/nanoc) 3 | [![Code Climate](http://img.shields.io/codeclimate/github/nanoc/nanoc.svg)](https://codeclimate.com/github/nanoc/nanoc) 4 | [![Code Coverage](http://img.shields.io/coveralls/nanoc/nanoc.svg)](https://coveralls.io/r/nanoc/nanoc) 5 | [![Documentation Coverage](http://inch-ci.org/github/nanoc/nanoc.svg)](http://inch-ci.org/github/nanoc/nanoc/) 6 | 7 | ![nanoc logo](https://avatars1.githubusercontent.com/u/3260163?s=140) 8 | 9 | # nanoc 10 | 11 | nanoc is a flexible static site generator written in Ruby. See the [nanoc web site](http://nanoc.ws) for more information. 12 | 13 | **Please take a moment and [donate](http://pledgie.com/campaigns/9282) to nanoc. A lot of time has gone into developing nanoc, and I would like to keep it going. Your support will ensure that nanoc will continue to improve.** 14 | 15 | ## Contributing 16 | 17 | Contributions are greatly appreciated! Consult the [Development guidelines](http://nanoc.ws/development/) for information on how you can contribute. 18 | 19 | ### Contributors 20 | 21 | Many thanks to everyone who has contributed to nanoc in one way or another: 22 | 23 | Ale Muñoz, Alexander Mankuta, Arnau Siches, Ben Armston, Bil Bas, Brian Candler, Bruno Dufour, Chris Eppstein, Christian Plessl, Colin Barrett, Damien Pollet, Dan Callahan, Daniel Hofstetter, Daniel Mendler, Daniel Wollschlaeger, David Alexander, David Everitt, Dennis Sutch, Devon Luke Buchanan, Dmitry Bilunov, Eric Sunshine, Erik Hollensbe, Fabian Buch, Felix Hanley, Go Maeda, Gregory Pakosz, Grégory Karékinian, Guilherme Garnier, Jack Chu, Jake Benilov, Jasper Van der Jeugt, Jeff Forcier, John Nishinaga, Justin Clift, Justin Hileman, Kevin Lynagh, Louis T., Mathias Bynens, Matt Keveney, Matthew Frazier, Matthias Beyer, Matthias Reitinger, Matthias Vallentin, Michal Cichra, Nelson Chen, Nicky Peeters, Nikhil Marathe, Oliver Byford, Peter Aronoff, Raphael von der Grün, Remko Tronçon, Riley Goodside, Ruben Verborgh, Scott Vokes, Simon South, Spencer Whitt, Stanley Rost, Starr Horne, Stefan Bühler, Stuart Montgomery, Takashi Uchibe, Toon Willems, Tuomas Kareinen, Ursula Kallio, Vincent Driessen, Xavier Shay, Zaiste de Grengolada, Šime Ramov 24 | -------------------------------------------------------------------------------- /lib/nanoc/base/checksummer.rb: -------------------------------------------------------------------------------- 1 | module Nanoc::Int 2 | # Creates checksums for given objects. 3 | # 4 | # A checksum is a string, such as “mL+TaqNsEeiPkWloPgCtAofT1yg=”, that is used 5 | # to determine whether a piece of data has changed. 6 | # 7 | # @api private 8 | class Checksummer 9 | class << self 10 | # @param obj The object to create a checksum for 11 | # 12 | # @return [String] The digest 13 | def calc(obj) 14 | digest = Digest::SHA1.new 15 | update(obj, digest) 16 | digest.base64digest 17 | end 18 | 19 | private 20 | 21 | def update(obj, digest, visited = Set.new) 22 | digest.update(obj.class.to_s) 23 | 24 | if visited.include?(obj) 25 | digest.update('recur') 26 | return 27 | end 28 | 29 | case obj 30 | when String 31 | digest.update(obj) 32 | when Array 33 | obj.each do |el| 34 | digest.update('elem') 35 | update(el, digest, visited + [obj]) 36 | end 37 | when Hash 38 | obj.each do |key, value| 39 | digest.update('key') 40 | update(key, digest, visited + [obj]) 41 | digest.update('value') 42 | update(value, digest, visited + [obj]) 43 | end 44 | when Pathname 45 | filename = obj.to_s 46 | if File.exist?(filename) 47 | stat = File.stat(filename) 48 | digest.update(stat.size.to_s + '-' + stat.mtime.utc.to_s) 49 | else 50 | digest.update('???') 51 | end 52 | when Nanoc::Int::RulesCollection 53 | update(obj.data, digest) 54 | when Nanoc::Int::CodeSnippet 55 | update(obj.data, digest) 56 | when Nanoc::Int::Item, Nanoc::Int::Layout 57 | digest.update('content') 58 | if obj.respond_to?(:binary?) && obj.binary? 59 | update(Pathname.new(obj.raw_filename), digest) 60 | else 61 | update(obj.raw_content, digest) 62 | end 63 | 64 | digest.update('attributes') 65 | update(obj.attributes, digest, visited + [obj]) 66 | else 67 | data = begin 68 | Marshal.dump(obj) 69 | rescue 70 | obj.inspect 71 | end 72 | 73 | digest.update(data) 74 | end 75 | end 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/nanoc/cli/commands/view.rb: -------------------------------------------------------------------------------- 1 | usage 'view [options]' 2 | summary 'start the web server that serves static files' 3 | description <<-EOS 4 | Start the static web server. Unless specified, the web server will run on port 5 | 3000 and listen on all IP addresses. Running this static web server requires 6 | `adsf` (not `asdf`!). 7 | EOS 8 | 9 | required :H, :handler, 'specify the handler to use (webrick/mongrel/...)' 10 | required :o, :host, 'specify the host to listen on (default: 0.0.0.0)' 11 | required :p, :port, 'specify the port to listen on (default: 3000)' 12 | 13 | module Nanoc::CLI::Commands 14 | class View < ::Nanoc::CLI::CommandRunner 15 | DEFAULT_HANDLER_NAME = :thin 16 | 17 | def run 18 | load_adsf 19 | require 'rack' 20 | 21 | # Make sure we are in a nanoc site directory 22 | require_site 23 | 24 | # Set options 25 | options_for_rack = { 26 | Port: (options[:port] || 3000).to_i, 27 | Host: (options[:host] || '0.0.0.0') 28 | } 29 | 30 | # Get handler 31 | if options.key?(:handler) 32 | handler = Rack::Handler.get(options[:handler]) 33 | else 34 | begin 35 | handler = Rack::Handler.get(DEFAULT_HANDLER_NAME) 36 | rescue LoadError 37 | handler = Rack::Handler::WEBrick 38 | end 39 | end 40 | 41 | # Build app 42 | site = self.site 43 | app = Rack::Builder.new do 44 | use Rack::CommonLogger 45 | use Rack::ShowExceptions 46 | use Rack::Lint 47 | use Rack::Head 48 | use Adsf::Rack::IndexFileFinder, root: site.config[:output_dir] 49 | run Rack::File.new(site.config[:output_dir]) 50 | end.to_app 51 | 52 | # Run autocompiler 53 | handler.run(app, options_for_rack) 54 | end 55 | 56 | protected 57 | 58 | def load_adsf 59 | # Load adsf 60 | begin 61 | require 'adsf' 62 | return 63 | rescue LoadError 64 | $stderr.puts "Could not find the required 'adsf' gem, " \ 65 | 'which is necessary for the view command.' 66 | end 67 | 68 | # Check asdf 69 | begin 70 | require 'asdf' 71 | $stderr.puts "You appear to have 'asdf' installed, " \ 72 | "but not 'adsf'. Please install 'adsf' (check the spelling)!" 73 | rescue LoadError 74 | end 75 | 76 | # Done 77 | exit 1 78 | end 79 | end 80 | end 81 | 82 | runner Nanoc::CLI::Commands::View 83 | -------------------------------------------------------------------------------- /lib/nanoc/extra/checking/checks/internal_links.rb: -------------------------------------------------------------------------------- 1 | require 'uri' 2 | 3 | module Nanoc::Extra::Checking::Checks 4 | # A check that verifies that all internal links point to a location that exists. 5 | # 6 | # @api private 7 | class InternalLinks < ::Nanoc::Extra::Checking::Check 8 | # Starts the validator. The results will be printed to stdout. 9 | # 10 | # Internal links that match a regexp pattern in `@config[:checks][:internal_links][:exclude]` will 11 | # be skipped. 12 | # 13 | # @return [void] 14 | def run 15 | # TODO: de-duplicate this (duplicated in external links check) 16 | filenames = output_filenames.select { |f| File.extname(f) == '.html' } 17 | hrefs_with_filenames = ::Nanoc::Extra::LinkCollector.new(filenames, :internal).filenames_per_href 18 | hrefs_with_filenames.each_pair do |href, fns| 19 | fns.each do |filename| 20 | next if valid?(href, filename) 21 | 22 | add_issue( 23 | "broken reference to #{href}", 24 | subject: filename) 25 | end 26 | end 27 | end 28 | 29 | protected 30 | 31 | def valid?(href, origin) 32 | # Skip hrefs that point to self 33 | # FIXME: this is ugly and won’t always be correct 34 | return true if href == '.' 35 | 36 | # Skip hrefs that are specified in the exclude configuration 37 | return true if self.excluded?(href) 38 | 39 | # Remove target 40 | path = href.sub(/#.*$/, '') 41 | return true if path.empty? 42 | 43 | # Remove query string 44 | path = path.sub(/\?.*$/, '') 45 | return true if path.empty? 46 | 47 | # Decode URL (e.g. '%20' -> ' ') 48 | path = URI.unescape(path) 49 | 50 | # Make absolute 51 | if path[0, 1] == '/' 52 | path = @config[:output_dir] + path 53 | else 54 | path = ::File.expand_path(path, ::File.dirname(origin)) 55 | end 56 | 57 | # Check whether file exists 58 | return true if File.file?(path) 59 | 60 | # Check whether directory with index file exists 61 | return true if File.directory?(path) && @config[:index_filenames].any? { |fn| File.file?(File.join(path, fn)) } 62 | 63 | # Nope :( 64 | false 65 | end 66 | 67 | def excluded?(href) 68 | excludes = @config.fetch(:checks, {}).fetch(:internal_links, {}).fetch(:exclude, []) 69 | excludes.any? { |pattern| Regexp.new(pattern).match(href) } 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/nanoc/extra/link_collector.rb: -------------------------------------------------------------------------------- 1 | require 'set' 2 | 3 | module ::Nanoc::Extra 4 | # @api private 5 | class LinkCollector 6 | URI_ATTRS = { 7 | 'a' => :href, 8 | 'audio' => :src, 9 | 'form' => :action, 10 | 'iframe' => :src, 11 | 'img' => :src, 12 | 'link' => :href, 13 | 'script' => :src, 14 | 'video' => :src 15 | } 16 | 17 | def initialize(filenames, mode = nil) 18 | Nanoc::Extra::JRubyNokogiriWarner.check_and_warn 19 | 20 | @filenames = filenames 21 | @filter = 22 | case mode 23 | when nil 24 | ->(_h) { true } 25 | when :external 26 | ->(h) { external_href?(h) } 27 | when :internal 28 | ->(h) { !external_href?(h) } 29 | else 30 | raise ArgumentError, 'Expected mode argument to be :internal, :external or nil' 31 | end 32 | end 33 | 34 | def filenames_per_href 35 | require 'nokogiri' 36 | filenames_per_href = {} 37 | @filenames.each do |filename| 38 | hrefs_in_file(filename).each do |href| 39 | filenames_per_href[href] ||= Set.new 40 | filenames_per_href[href] << filename 41 | end 42 | end 43 | filenames_per_href 44 | end 45 | 46 | def filenames_per_resource_uri 47 | require 'nokogiri' 48 | filenames_per_resource_uri = {} 49 | @filenames.each do |filename| 50 | resource_uris_in_file(filename).each do |resouce_uri| 51 | filenames_per_resource_uri[resouce_uri] ||= Set.new 52 | filenames_per_resource_uri[resouce_uri] << filename 53 | end 54 | end 55 | filenames_per_resource_uri 56 | end 57 | 58 | def external_href?(href) 59 | href =~ %r{^(\/\/|[a-z\-]+:)} 60 | end 61 | 62 | def hrefs_in_file(filename) 63 | uris_in_file filename, %w(a img) 64 | end 65 | 66 | def resource_uris_in_file(filename) 67 | uris_in_file filename, %w(audio form img iframe link script video) 68 | end 69 | 70 | private 71 | 72 | def uris_in_file(filename, tag_names) 73 | uris = Set.new 74 | doc = Nokogiri::HTML(::File.read(filename)) 75 | tag_names.each do |tag_name| 76 | attr = URI_ATTRS[tag_name] 77 | doc.css(tag_name).each do |e| 78 | uris << e[attr] unless e[attr].nil? 79 | end 80 | end 81 | 82 | # Strip fragment 83 | uris.map! { |href| href.gsub(/#.*$/, '') } 84 | 85 | uris.select(&@filter) 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /test/helpers/test_tagging.rb: -------------------------------------------------------------------------------- 1 | class Nanoc::Helpers::TaggingTest < Nanoc::TestCase 2 | include Nanoc::Helpers::Tagging 3 | 4 | def test_tags_for_without_tags 5 | # Create item 6 | item = Nanoc::Int::Item.new('content', {}, '/path/') 7 | 8 | # Check 9 | assert_equal( 10 | '(none)', 11 | tags_for(item, base_url: 'http://example.com/tag/') 12 | ) 13 | end 14 | 15 | def test_tags_for_with_custom_base_url 16 | # Create item 17 | item = Nanoc::Int::Item.new('content', { tags: %w(foo bar) }, '/path/') 18 | 19 | # Check 20 | assert_equal( 21 | "#{link_for_tag('foo', 'http://stoneship.org/tag/')}, " \ 22 | "#{link_for_tag('bar', 'http://stoneship.org/tag/')}", 23 | tags_for(item, base_url: 'http://stoneship.org/tag/') 24 | ) 25 | end 26 | 27 | def test_tags_for_with_custom_none_text 28 | # Create item 29 | item = Nanoc::Int::Item.new('content', { tags: [] }, '/path/') 30 | 31 | # Check 32 | assert_equal( 33 | 'no tags for you, fool', 34 | tags_for(item, none_text: 'no tags for you, fool', base_url: 'http://example.com/tag/') 35 | ) 36 | end 37 | 38 | def test_tags_for_with_custom_separator 39 | # Create item 40 | item = Nanoc::Int::Item.new('content', { tags: %w(foo bar) }, '/path/') 41 | 42 | # Check 43 | assert_equal( 44 | "#{link_for_tag('foo', 'http://example.com/tag/')} ++ " \ 45 | "#{link_for_tag('bar', 'http://example.com/tag/')}", 46 | tags_for(item, separator: ' ++ ', base_url: 'http://example.com/tag/') 47 | ) 48 | end 49 | 50 | def test_items_with_tag 51 | # Create items 52 | @items = [ 53 | Nanoc::Int::Item.new('item 1', { tags: [:foo] }, '/item1/'), 54 | Nanoc::Int::Item.new('item 2', { tags: [:bar] }, '/item2/'), 55 | Nanoc::Int::Item.new('item 3', { tags: [:foo, :bar] }, '/item3/') 56 | ] 57 | 58 | # Find items 59 | items_with_foo_tag = items_with_tag(:foo) 60 | 61 | # Check 62 | assert_equal( 63 | [@items[0], @items[2]], 64 | items_with_foo_tag 65 | ) 66 | end 67 | 68 | def test_link_for_tag 69 | assert_equal( 70 | %(), 71 | link_for_tag('foobar', 'http://stoneship.org/tags/') 72 | ) 73 | end 74 | 75 | def test_link_for_tag_escape 76 | assert_equal( 77 | %(), 78 | link_for_tag('foo&bar', 'http://stoneship.org/tags&stuff/') 79 | ) 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /lib/nanoc/helpers/tagging.rb: -------------------------------------------------------------------------------- 1 | module Nanoc::Helpers 2 | # Provides support for managing tags added to items. 3 | # 4 | # To add tags to items, set the `tags` attribute to an array of tags that 5 | # should be applied to the item. 6 | # 7 | # @example Adding tags to an item 8 | # 9 | # tags: [ 'foo', 'bar', 'baz' ] 10 | module Tagging 11 | require 'nanoc/helpers/html_escape' 12 | include Nanoc::Helpers::HTMLEscape 13 | 14 | # Returns a formatted list of tags for the given item as a string. The 15 | # tags will be linked using the {#link_for_tag} function; the 16 | # HTML-escaping rules for {#link_for_tag} apply here as well. 17 | # 18 | # @option params [String] base_url The URL to which the tag will be appended 19 | # to construct the link URL. This URL must have a trailing slash. 20 | # 21 | # @option params [String] none_text ("(none)") The text to display when 22 | # the item has no tags 23 | # 24 | # @option params [String] separator (", ") The separator to put between 25 | # tags 26 | # 27 | # @return [String] A hyperlinked list of tags for the given item 28 | def tags_for(item, params = {}) 29 | base_url = params.fetch(:base_url) 30 | none_text = params[:none_text] || '(none)' 31 | separator = params[:separator] || ', ' 32 | 33 | if item[:tags].nil? || item[:tags].empty? 34 | none_text 35 | else 36 | item[:tags].map { |tag| link_for_tag(tag, base_url) }.join(separator) 37 | end 38 | end 39 | 40 | # Find all items with the given tag. 41 | # 42 | # @param [String] tag The tag for which to find all items 43 | # 44 | # @return [Array] All items with the given tag 45 | def items_with_tag(tag) 46 | @items.select { |i| (i[:tags] || []).include?(tag) } 47 | end 48 | 49 | # Returns a link to to the specified tag. The link is marked up using the 50 | # rel-tag microformat. The `href` attribute of the link will be HTML- 51 | # escaped, as will the content of the `a` element. 52 | # 53 | # @param [String] tag The name of the tag, which should consist of letters 54 | # and numbers (no spaces, slashes, or other special characters). 55 | # 56 | # @param [String] base_url The URL to which the tag will be appended to 57 | # construct the link URL. This URL must have a trailing slash. 58 | # 59 | # @return [String] A link for the given tag and the given base URL 60 | def link_for_tag(tag, base_url) 61 | %() 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /test/fixtures/vcr_cassettes/html_run_ok.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: post 5 | uri: http://validator.w3.org/check 6 | body: 7 | encoding: UTF-8 8 | string: "--349832898984244898448024464570528145\r\nContent-Disposition: form-data; 9 | name=\"output\"\r\n\r\nsoap12\r\n--349832898984244898448024464570528145\r\nContent-Disposition: 10 | form-data; name=\"uploaded_file\"; filename=\"output/blah.html\"\r\nContent-Type: 11 | text/html\r\n\r\nHello

Hi!

\r\n--349832898984244898448024464570528145--\r\n" 12 | headers: 13 | Content-Type: 14 | - multipart/form-data; boundary=349832898984244898448024464570528145 15 | Accept-Encoding: 16 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 17 | Accept: 18 | - "*/*" 19 | User-Agent: 20 | - Ruby 21 | response: 22 | status: 23 | code: 200 24 | message: OK 25 | headers: 26 | Date: 27 | - Thu, 24 Apr 2014 07:25:21 GMT 28 | Server: 29 | - Apache/2.2.16 (Debian) 30 | X-W3c-Validator-Recursion: 31 | - '1' 32 | X-W3c-Validator-Status: 33 | - Valid 34 | X-W3c-Validator-Errors: 35 | - '0' 36 | X-W3c-Validator-Warnings: 37 | - '1' 38 | Content-Type: 39 | - application/soap+xml; charset=UTF-8 40 | Connection: 41 | - close 42 | Transfer-Encoding: 43 | - chunked 44 | body: 45 | encoding: UTF-8 46 | string: "\n\n\n\n 48 | \ \n output/blah.html\n http://validator.w3.org/\n 49 | \ HTML5\n utf-8\n true\n 50 | \ \n 0\n \n 51 | \ \n \n \n \n 1\n 52 | \ \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 53 | \ \n \n \n\n\n\n" 54 | http_version: 55 | recorded_at: Thu, 24 Apr 2014 07:25:21 GMT 56 | recorded_with: VCR 2.9.0 57 | -------------------------------------------------------------------------------- /lib/nanoc/extra/pruner.rb: -------------------------------------------------------------------------------- 1 | module Nanoc::Extra 2 | # Responsible for finding and deleting files in the site’s output directory 3 | # that are not managed by nanoc. 4 | # 5 | # @api private 6 | class Pruner 7 | # @return [Nanoc::Int::Site] The site this pruner belongs to 8 | attr_reader :site 9 | 10 | # @param [Nanoc::Int::Site] site The site for which a pruner is created 11 | # 12 | # @option params [Boolean] :dry_run (false) true if the files to be deleted 13 | # should only be printed instead of actually deleted, false if the files 14 | # should actually be deleted. 15 | def initialize(site, params = {}) 16 | @site = site 17 | @dry_run = params.fetch(:dry_run) { false } 18 | @exclude = params.fetch(:exclude) { [] } 19 | end 20 | 21 | # Prunes all output files not managed by nanoc. 22 | # 23 | # @return [void] 24 | def run 25 | require 'find' 26 | 27 | # Get compiled files 28 | all_raw_paths = site.items.map do |item| 29 | item.reps.map(&:raw_path) 30 | end 31 | compiled_files = all_raw_paths.flatten.compact.select { |f| File.file?(f) } 32 | 33 | # Get present files and dirs 34 | present_files = [] 35 | present_dirs = [] 36 | Find.find(site.config[:output_dir] + '/') do |f| 37 | present_files << f if File.file?(f) 38 | present_dirs << f if File.directory?(f) 39 | end 40 | 41 | # Remove stray files 42 | stray_files = (present_files - compiled_files) 43 | stray_files.each do |f| 44 | next if filename_excluded?(f) 45 | delete_file(f) 46 | end 47 | 48 | # Remove empty directories 49 | present_dirs.reverse_each do |dir| 50 | next if Dir.foreach(dir) { |n| break true if n !~ /\A\.\.?\z/ } 51 | next if filename_excluded?(dir) 52 | delete_dir(dir) 53 | end 54 | end 55 | 56 | # @param [String] filename The filename to check 57 | # 58 | # @return [Boolean] true if the given file is excluded, false otherwise 59 | def filename_excluded?(filename) 60 | pathname = Pathname.new(filename) 61 | @exclude.any? { |e| pathname.__nanoc_include_component?(e) } 62 | end 63 | 64 | protected 65 | 66 | def delete_file(file) 67 | if @dry_run 68 | puts file 69 | else 70 | Nanoc::CLI::Logger.instance.file(:high, :delete, file) 71 | FileUtils.rm(file) 72 | end 73 | end 74 | 75 | def delete_dir(dir) 76 | if @dry_run 77 | puts dir 78 | else 79 | Nanoc::CLI::Logger.instance.file(:high, :delete, dir) 80 | Dir.rmdir(dir) 81 | end 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /spec/nanoc/base/pattern_spec.rb: -------------------------------------------------------------------------------- 1 | describe Nanoc::Int::Pattern do 2 | describe '.from' do 3 | it 'converts from string' do 4 | pattern = described_class.from('/foo/x[ab]z/bar.*') 5 | expect(pattern.match?('/foo/xaz/bar.html')).to eql(true) 6 | expect(pattern.match?('/foo/xyz/bar.html')).to eql(false) 7 | end 8 | 9 | it 'converts from regex' do 10 | pattern = described_class.from(%r{\A/foo/x[ab]z/bar\..*\z}) 11 | expect(pattern.match?('/foo/xaz/bar.html')).to eql(true) 12 | expect(pattern.match?('/foo/xyz/bar.html')).to eql(false) 13 | end 14 | 15 | it 'converts from pattern' do 16 | pattern = described_class.from('/foo/x[ab]z/bar.*') 17 | pattern = described_class.from(pattern) 18 | expect(pattern.match?('/foo/xaz/bar.html')).to eql(true) 19 | expect(pattern.match?('/foo/xyz/bar.html')).to eql(false) 20 | end 21 | end 22 | end 23 | 24 | describe Nanoc::Int::RegexpPattern do 25 | describe '#match?' do 26 | it 'matches' do 27 | pattern = described_class.new(/the answer is (\d+)/) 28 | 29 | expect(pattern.match?('the answer is 42')).to eql(true) 30 | expect(pattern.match?('the answer is donkey')).to eql(false) 31 | end 32 | end 33 | 34 | describe '#captures' do 35 | it 'returns nil if it does not match' do 36 | pattern = described_class.new(/the answer is (\d+)/) 37 | expect(pattern.captures('the answer is donkey')).to be_nil 38 | end 39 | 40 | it 'returns array if it matches' do 41 | pattern = described_class.new(/the answer is (\d+)/) 42 | expect(pattern.captures('the answer is 42')).to eql(['42']) 43 | end 44 | end 45 | end 46 | 47 | describe Nanoc::Int::StringPattern do 48 | describe '#match?' do 49 | it 'matches simple strings' do 50 | pattern = described_class.new('d*key') 51 | 52 | expect(pattern.match?('donkey')).to eql(true) 53 | expect(pattern.match?('giraffe')).to eql(false) 54 | end 55 | 56 | it 'matches with pathname option' do 57 | pattern = described_class.new('/foo/*/bar/**/*.animal') 58 | 59 | expect(pattern.match?('/foo/x/bar/a/b/donkey.animal')).to eql(true) 60 | expect(pattern.match?('/foo/x/bar/donkey.animal')).to eql(true) 61 | expect(pattern.match?('/foo/x/railroad/donkey.animal')).to eql(false) 62 | end 63 | 64 | it 'matches with extglob option' do 65 | pattern = described_class.new('{b,gl}oat') 66 | 67 | expect(pattern.match?('boat')).to eql(true) 68 | expect(pattern.match?('gloat')).to eql(true) 69 | expect(pattern.match?('stoat')).to eql(false) 70 | end 71 | end 72 | 73 | describe '#captures' do 74 | it 'returns nil' do 75 | pattern = described_class.new('d*key') 76 | expect(pattern.captures('donkey')).to be_nil 77 | end 78 | end 79 | end 80 | --------------------------------------------------------------------------------