nanoc 3.4.3 → 3.5.0b1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. data/CONTRIBUTING.md +25 -0
  2. data/Gemfile +3 -1
  3. data/Gemfile.lock +22 -13
  4. data/NEWS.md +27 -0
  5. data/README.md +3 -1
  6. data/lib/nanoc.rb +2 -2
  7. data/lib/nanoc/base/compilation/compiler_dsl.rb +19 -0
  8. data/lib/nanoc/base/core_ext/array.rb +18 -8
  9. data/lib/nanoc/base/core_ext/hash.rb +18 -8
  10. data/lib/nanoc/base/core_ext/pathname.rb +2 -8
  11. data/lib/nanoc/base/plugin_registry.rb +57 -19
  12. data/lib/nanoc/base/result_data/item_rep.rb +3 -15
  13. data/lib/nanoc/base/source_data/item.rb +1 -1
  14. data/lib/nanoc/base/source_data/layout.rb +1 -1
  15. data/lib/nanoc/base/source_data/site.rb +15 -19
  16. data/lib/nanoc/cli.rb +28 -23
  17. data/lib/nanoc/cli/command_runner.rb +4 -0
  18. data/lib/nanoc/cli/commands/autocompile.rb +15 -6
  19. data/lib/nanoc/cli/commands/check.rb +47 -0
  20. data/lib/nanoc/cli/commands/compile.rb +271 -195
  21. data/lib/nanoc/cli/commands/create-site.rb +5 -5
  22. data/lib/nanoc/cli/commands/deploy.rb +16 -4
  23. data/lib/nanoc/cli/commands/prune.rb +3 -3
  24. data/lib/nanoc/cli/commands/show-data.rb +73 -58
  25. data/lib/nanoc/cli/commands/show-rules.rb +1 -1
  26. data/lib/nanoc/cli/commands/validate-css.rb +2 -3
  27. data/lib/nanoc/cli/commands/validate-html.rb +2 -3
  28. data/lib/nanoc/cli/commands/validate-links.rb +5 -11
  29. data/lib/nanoc/cli/commands/view.rb +1 -1
  30. data/lib/nanoc/cli/commands/watch.rb +38 -20
  31. data/lib/nanoc/cli/error_handler.rb +122 -122
  32. data/lib/nanoc/data_sources.rb +2 -0
  33. data/lib/nanoc/data_sources/filesystem_unified.rb +1 -1
  34. data/lib/nanoc/data_sources/filesystem_verbose.rb +1 -1
  35. data/lib/nanoc/data_sources/static.rb +60 -0
  36. data/lib/nanoc/extra.rb +2 -0
  37. data/lib/nanoc/extra/checking.rb +16 -0
  38. data/lib/nanoc/extra/checking/check.rb +33 -0
  39. data/lib/nanoc/extra/checking/checks.rb +19 -0
  40. data/lib/nanoc/extra/checking/checks/css.rb +23 -0
  41. data/lib/nanoc/extra/checking/checks/external_links.rb +149 -0
  42. data/lib/nanoc/extra/checking/checks/html.rb +24 -0
  43. data/lib/nanoc/extra/checking/checks/internal_links.rb +57 -0
  44. data/lib/nanoc/extra/checking/checks/stale.rb +23 -0
  45. data/lib/nanoc/extra/checking/dsl.rb +31 -0
  46. data/lib/nanoc/extra/checking/issue.rb +19 -0
  47. data/lib/nanoc/extra/checking/runner.rb +130 -0
  48. data/lib/nanoc/extra/link_collector.rb +57 -0
  49. data/lib/nanoc/extra/pruner.rb +1 -1
  50. data/lib/nanoc/extra/validators/links.rb +5 -262
  51. data/lib/nanoc/extra/validators/w3c.rb +8 -76
  52. data/lib/nanoc/filters/colorize_syntax.rb +1 -1
  53. data/lib/nanoc/filters/handlebars.rb +2 -2
  54. data/lib/nanoc/filters/less.rb +1 -1
  55. data/lib/nanoc/filters/redcarpet.rb +1 -1
  56. data/lib/nanoc/filters/rubypants.rb +1 -1
  57. data/lib/nanoc/filters/slim.rb +1 -1
  58. data/lib/nanoc/helpers/blogging.rb +163 -104
  59. data/lib/nanoc/helpers/xml_sitemap.rb +9 -3
  60. data/tasks/doc.rake +2 -1
  61. data/test/base/core_ext/array_spec.rb +4 -4
  62. data/test/base/core_ext/hash_spec.rb +7 -7
  63. data/test/base/core_ext/pathname_spec.rb +12 -2
  64. data/test/base/test_compiler_dsl.rb +24 -0
  65. data/test/base/test_item_rep.rb +58 -26
  66. data/test/cli/commands/test_watch.rb +0 -8
  67. data/test/data_sources/test_static.rb +66 -0
  68. data/test/extra/checking/checks/test_css.rb +40 -0
  69. data/test/extra/checking/checks/test_external_links.rb +76 -0
  70. data/test/extra/checking/checks/test_html.rb +40 -0
  71. data/test/extra/checking/checks/test_internal_links.rb +46 -0
  72. data/test/extra/checking/checks/test_stale.rb +49 -0
  73. data/test/extra/checking/test_check.rb +16 -0
  74. data/test/extra/checking/test_dsl.rb +20 -0
  75. data/test/extra/checking/test_runner.rb +15 -0
  76. data/test/extra/test_link_collector.rb +93 -0
  77. data/test/extra/validators/test_links.rb +0 -64
  78. data/test/extra/validators/test_w3c.rb +20 -26
  79. data/test/filters/test_colorize_syntax.rb +15 -0
  80. data/test/filters/test_less.rb +14 -0
  81. data/test/filters/test_pandoc.rb +5 -1
  82. data/test/helpers/test_blogging.rb +52 -8
  83. data/test/helpers/test_xml_sitemap.rb +68 -0
  84. data/test/test_gem.rb +1 -1
  85. metadata +31 -6
@@ -129,27 +129,15 @@ module Nanoc
129
129
  # Check if file will be created
130
130
  is_created = !File.file?(raw_path)
131
131
 
132
- # Calculate characteristics of old content
133
- if File.file?(raw_path)
134
- hash_old = Pathname.new(raw_path).checksum
135
- size_old = File.size(raw_path)
136
- end
137
-
138
132
  # Notify
139
133
  Nanoc::NotificationCenter.post(:will_write_rep, self, snapshot)
140
134
 
141
135
  if self.binary?
142
- # Calculate characteristics of new content
143
- size_new = File.size(temporary_filenames[:last])
144
- hash_new = Pathname.new(temporary_filenames[:last]).checksum if size_old == size_new
145
-
146
136
  # Check whether content was modified
147
- is_modified = (size_old != size_new || hash_old != hash_new)
137
+ is_modified = !File.file?(raw_path) || !FileUtils.identical?(raw_path, temporary_filenames[:last])
148
138
 
149
- # Copy
150
- if is_modified
151
- FileUtils.cp(temporary_filenames[:last], raw_path)
152
- end
139
+ # Always copy (time spent checking modification is not useful)
140
+ FileUtils.cp(temporary_filenames[:last], raw_path)
153
141
  else
154
142
  # Check whether content was modified
155
143
  is_modified = (!File.file?(raw_path) || File.read(raw_path) != @content[:last])
@@ -86,7 +86,7 @@ module Nanoc
86
86
  end
87
87
 
88
88
  # Get rest of params
89
- @attributes = attributes.symbolize_keys
89
+ @attributes = attributes.symbolize_keys_recursively
90
90
  @identifier = identifier.cleaned_identifier.freeze
91
91
 
92
92
  # Set mtime
@@ -35,7 +35,7 @@ module Nanoc
35
35
  # attribute instead.
36
36
  def initialize(raw_content, attributes, identifier, params=nil)
37
37
  @raw_content = raw_content
38
- @attributes = attributes.symbolize_keys
38
+ @attributes = attributes.symbolize_keys_recursively
39
39
  @identifier = identifier.cleaned_identifier.freeze
40
40
 
41
41
  # Set mtime
@@ -181,25 +181,21 @@ module Nanoc
181
181
  def setup_child_parent_links
182
182
  teardown_child_parent_links
183
183
 
184
- items = @items.sort_by { |i| i.identifier }
185
- items.each_with_index do |item, index|
186
- # Get parent
187
- next if index == 0
188
- parent_identifier = item.identifier.sub(/[^\/]+\/$/, '')
189
- parent = nil
190
- (index-1).downto(0) do |candidate_index|
191
- candidate = items[candidate_index]
192
- if candidate.identifier == parent_identifier
193
- parent = candidate
194
- elsif candidate.identifier[0..parent_identifier.size-1] != parent_identifier
195
- break
184
+ item_map = {}
185
+ @items.each do |item|
186
+ item_map[item.identifier] = item
187
+ end
188
+
189
+ @items.each do |item|
190
+ parent_id_end = item.identifier.rindex('/', -2)
191
+ if parent_id_end
192
+ parent_id = item.identifier[0..parent_id_end]
193
+ parent = item_map[parent_id]
194
+ if parent
195
+ item.parent = parent
196
+ parent.children << item
196
197
  end
197
198
  end
198
- next if parent.nil?
199
-
200
- # Link
201
- item.parent = parent
202
- parent.children << item
203
199
  end
204
200
  end
205
201
 
@@ -346,8 +342,8 @@ module Nanoc
346
342
 
347
343
  # Read config from config.yaml in given dir
348
344
  config_path = File.join(dir_or_config_hash, 'config.yaml')
349
- @config = DEFAULT_CONFIG.merge(YAML.load_file(config_path).symbolize_keys)
350
- @config[:data_sources].map! { |ds| ds.symbolize_keys }
345
+ @config = DEFAULT_CONFIG.merge(YAML.load_file(config_path).symbolize_keys_recursively)
346
+ @config[:data_sources].map! { |ds| ds.symbolize_keys_recursively }
351
347
  else
352
348
  # Use passed config hash
353
349
  @config = DEFAULT_CONFIG.merge(dir_or_config_hash)
@@ -53,6 +53,11 @@ module Nanoc::CLI
53
53
  end
54
54
  end
55
55
 
56
+ # @return [Cri::Command] The root command, i.e. the commandline tool itself
57
+ def self.root_command
58
+ @root_command
59
+ end
60
+
56
61
  # Adds the given command to the collection of available commands.
57
62
  #
58
63
  # @param [Cri::Command] cmd The command to add
@@ -80,6 +85,10 @@ protected
80
85
  # Reinit
81
86
  @root_command = nil
82
87
 
88
+ # Add root command
89
+ filename = File.dirname(__FILE__) + "/cli/commands/nanoc.rb"
90
+ @root_command = self.load_command_at(filename)
91
+
83
92
  # Add help command
84
93
  help_cmd = Cri::Command.new_basic_help
85
94
  self.add_command(help_cmd)
@@ -135,14 +144,6 @@ protected
135
144
  cmd
136
145
  end
137
146
 
138
- # @return [Cri::Command] The root command, i.e. the commandline tool itself
139
- def self.root_command
140
- @root_command ||= begin
141
- filename = File.dirname(__FILE__) + "/cli/commands/nanoc.rb"
142
- self.load_command_at(filename)
143
- end
144
- end
145
-
146
147
  # @return [Array] The directory contents
147
148
  def self.recursive_contents_of(path)
148
149
  return [] unless File.directory?(path)
@@ -151,28 +152,32 @@ protected
151
152
  files
152
153
  end
153
154
 
154
- # Wraps `$stdout` and `$stderr` in appropriate cleaning streams.
155
+ # Wraps the given stream in a cleaning stream. The cleaning streams will
156
+ # have the proper stream cleaners configured.
155
157
  #
156
- # @return [void]
157
- def self.setup_cleaning_streams
158
- $stdout = Nanoc::CLI::CleaningStream.new($stdout)
159
- $stderr = Nanoc::CLI::CleaningStream.new($stderr)
158
+ # @param [IO] io The stream to wrap
159
+ #
160
+ # @return [::Nanoc::CLI::CleaningStream]
161
+ def self.wrap_in_cleaning_stream(io)
162
+ cio = ::Nanoc::CLI::CleaningStream.new(io)
160
163
 
161
- if !self.enable_utf8?($stdout)
162
- $stdout.add_stream_cleaner(Nanoc::CLI::StreamCleaners::UTF8)
164
+ if !self.enable_utf8?(io)
165
+ cio.add_stream_cleaner(Nanoc::CLI::StreamCleaners::UTF8)
163
166
  end
164
167
 
165
- if !self.enable_utf8?($stderr)
166
- $stderr.add_stream_cleaner(Nanoc::CLI::StreamCleaners::UTF8)
168
+ if !self.enable_ansi_colors?(io)
169
+ cio.add_stream_cleaner(Nanoc::CLI::StreamCleaners::ANSIColors)
167
170
  end
168
171
 
169
- if !self.enable_ansi_colors?($stdout)
170
- $stdout.add_stream_cleaner(Nanoc::CLI::StreamCleaners::ANSIColors)
171
- end
172
+ cio
173
+ end
172
174
 
173
- if !self.enable_ansi_colors?($stderr)
174
- $stderr.add_stream_cleaner(Nanoc::CLI::StreamCleaners::ANSIColors)
175
- end
175
+ # Wraps `$stdout` and `$stderr` in appropriate cleaning streams.
176
+ #
177
+ # @return [void]
178
+ def self.setup_cleaning_streams
179
+ $stdout = self.wrap_in_cleaning_stream($stdout)
180
+ $stderr = self.wrap_in_cleaning_stream($stderr)
176
181
  end
177
182
 
178
183
  # @return [Boolean] true if UTF-8 support is present, false if not
@@ -51,8 +51,12 @@ module Nanoc::CLI
51
51
  #
52
52
  # @return [void]
53
53
  def require_site
54
+ print "Loading site data… "
54
55
  if site.nil?
56
+ puts "error"
55
57
  raise ::Nanoc::Errors::GenericTrivial, "The current working directory does not seem to be a nanoc site."
58
+ else
59
+ puts "done"
56
60
  end
57
61
  end
58
62
 
@@ -4,12 +4,20 @@ usage 'autocompile [options]'
4
4
  summary 'start the autocompiler'
5
5
  aliases :aco
6
6
  description <<-EOS
7
- Start the autocompiler web server. Unless specified, the web server will run
8
- on port 3000 and listen on all IP addresses. Running the autocompiler requires
9
- the 'mime/types' and 'rack' gems.
7
+ Start the autocompiler web server. Unless overridden with commandline options
8
+ or configuration entries, the web server will run on port 3000 and listen on all
9
+ IP addresses. Running the autocompiler requires the `mime/types` and `rack` gems.
10
+
11
+ To specify the host and/or port options in config.yaml, you can add either (or
12
+ both) of the following:
13
+
14
+ autocompile:
15
+ host: '10.0.2.0' # override the default host
16
+ port: 4000 # override the default port
17
+
10
18
  EOS
11
19
 
12
- required :H, :handler, 'specify the handler to use (webrick/mongrel/...)'
20
+ required :H, :handler, 'specify the handler to use (webrick/mongrel/…)'
13
21
  required :o, :host, 'specify the host to listen on (default: 0.0.0.0)'
14
22
  required :p, :port, 'specify the port to listen on (default: 3000)'
15
23
 
@@ -22,11 +30,12 @@ module Nanoc::CLI::Commands
22
30
 
23
31
  # Make sure we are in a nanoc site directory
24
32
  self.require_site
33
+ autocompile_config = self.site.config[:autocompile] || {}
25
34
 
26
35
  # Set options
27
36
  options_for_rack = {
28
- :Port => (options[:port] || 3000).to_i,
29
- :Host => (options[:host] || '0.0.0.0')
37
+ :Port => (options[:port] || autocompile_config[:port] || 3000).to_i,
38
+ :Host => (options[:host] || autocompile_config[:host] || '0.0.0.0')
30
39
  }
31
40
 
32
41
  # Guess which handler we should use
@@ -0,0 +1,47 @@
1
+ # encoding: utf-8
2
+
3
+ usage 'check [options] [names]'
4
+ summary 'run issue checks'
5
+ description <<-EOS
6
+ Run issue checks on the current site. If the `--all` option is passed, all available issue checks will be run. If the `--deploy` option is passed, the issue checks marked for deployment will be fun.
7
+ EOS
8
+
9
+ flag :a, :all, 'run all checks'
10
+ flag :L, :list, 'list all checks'
11
+ flag :d, :deploy, 'run checks for deployment'
12
+
13
+ module Nanoc::CLI::Commands
14
+
15
+ class Check < ::Nanoc::CLI::CommandRunner
16
+
17
+ def run
18
+ validate_options_and_arguments
19
+ self.require_site
20
+
21
+ runner = Nanoc::Extra::Checking::Runner.new(site)
22
+ if options[:list]
23
+ runner.list_checks
24
+ elsif options[:all]
25
+ runner.run_all
26
+ elsif options[:deploy]
27
+ runner.require_dsl
28
+ runner.run_for_deploy
29
+ else
30
+ runner.run_specific(arguments)
31
+ end
32
+ end
33
+
34
+ protected
35
+
36
+ def validate_options_and_arguments
37
+ if arguments.empty? && !options[:all] && !options[:deploy] && !options[:list]
38
+ raise Nanoc::Errors::GenericTrivial,
39
+ "nothing to do (pass either --all, --deploy or --list or a list of checks)"
40
+ end
41
+ end
42
+
43
+ end
44
+
45
+ end
46
+
47
+ runner Nanoc::CLI::Commands::Check
@@ -22,102 +22,227 @@ option :f, :force, '(ignored)'
22
22
 
23
23
  module Nanoc::CLI::Commands
24
24
 
25
- # FIXME this command is horribly long and complicated and does way too much. plz cleanup thx.
26
25
  class Compile < ::Nanoc::CLI::CommandRunner
27
26
 
28
- def run
29
- # Make sure we are in a nanoc site directory
30
- puts "Loading site data…"
31
- self.require_site
27
+ extend Nanoc::Memoization
28
+
29
+ # Listens to compilation events and reacts to them. This abstract class
30
+ # does not have a real implementation; subclasses should override {#start}
31
+ # and set up notifications to listen to.
32
+ #
33
+ # @abstract Subclasses must override {#start} and may override {#stop}.
34
+ class Listener
35
+
36
+ # Starts the listener. Subclasses should override this method and set up listener notifications.
37
+ #
38
+ # @return [void]
39
+ #
40
+ # @abstract
41
+ def start
42
+ raise NotImplementedError, "Subclasses of Listener should implement #start"
43
+ end
32
44
 
33
- # Check presence of --all option
34
- if options.has_key?(:all) || options.has_key?(:force)
35
- $stderr.puts "Warning: the --force option (and its deprecated --all alias) are, as of nanoc 3.2, no longer supported and have no effect."
45
+ # Stops the listener. The default implementation removes self from all notification center observers.
46
+ #
47
+ # @return [void]
48
+ def stop
36
49
  end
37
50
 
38
- # Warn if trying to compile a single item
39
- if arguments.size == 1
40
- $stderr.puts '-' * 80
41
- $stderr.puts 'Note: As of nanoc 3.2, it is no longer possible to compile a single item. When invoking the “compile” command, all items in the site will be compiled.'
42
- $stderr.puts '-' * 80
51
+ end
52
+
53
+ # Generates diffs for every output file written
54
+ class DiffGenerator < Listener
55
+
56
+ # @see Listener#start
57
+ def start
58
+ require 'tempfile'
59
+ self.setup_diffs
60
+ old_contents = {}
61
+ Nanoc::NotificationCenter.on(:will_write_rep) do |rep, snapshot|
62
+ path = rep.raw_path(:snapshot => snapshot)
63
+ old_contents[rep] = File.file?(path) ? File.read(path) : nil
64
+ end
65
+ Nanoc::NotificationCenter.on(:rep_written) do |rep, path, is_created, is_modified|
66
+ if !rep.binary?
67
+ new_contents = File.file?(path) ? File.read(path) : nil
68
+ if old_contents[rep] && new_contents
69
+ generate_diff_for(rep, old_contents[rep], new_contents)
70
+ end
71
+ old_contents.delete(rep)
72
+ end
73
+ end
43
74
  end
44
75
 
45
- # Give feedback
46
- puts "Compiling site…"
76
+ # @see Listener#stop
77
+ def stop
78
+ super
79
+ self.teardown_diffs
80
+ end
47
81
 
48
- # Initialize profiling stuff
49
- time_before = Time.now
50
- @rep_times = {}
51
- @filter_times = {}
52
- setup_notifications
82
+ protected
53
83
 
54
- # Prepare for generating diffs
55
- setup_diffs
84
+ def setup_diffs
85
+ @diff_lock = Mutex.new
86
+ @diff_threads = []
87
+ FileUtils.rm('output.diff') if File.file?('output.diff')
88
+ end
56
89
 
57
- # Set up GC control
58
- @gc_lock = Mutex.new
59
- @gc_count = 0
90
+ def teardown_diffs
91
+ @diff_threads.each { |t| t.join }
92
+ end
60
93
 
61
- # Compile
62
- self.site.compile
94
+ def generate_diff_for(rep, old_content, new_content)
95
+ return if old_content == new_content
63
96
 
64
- # Find reps
65
- reps = self.site.items.map { |i| i.reps }.flatten
97
+ @diff_threads << Thread.new do
98
+ # Generate diff
99
+ diff = diff_strings(old_content, new_content)
100
+ diff.sub!(/^--- .*/, '--- ' + rep.raw_path)
101
+ diff.sub!(/^\+\+\+ .*/, '+++ ' + rep.raw_path)
66
102
 
67
- # Show skipped reps
68
- reps.select { |r| !r.compiled? }.each do |rep|
69
- rep.raw_paths.each do |snapshot_name, filename|
70
- next if filename.nil?
71
- duration = @rep_times[filename]
72
- Nanoc::CLI::Logger.instance.file(:high, :skip, filename, duration)
103
+ # Write diff
104
+ @diff_lock.synchronize do
105
+ File.open('output.diff', 'a') { |io| io.write(diff) }
106
+ end
73
107
  end
74
108
  end
75
109
 
76
- # Stop diffing
77
- teardown_diffs
110
+ def diff_strings(a, b)
111
+ require 'open3'
112
+
113
+ # Create files
114
+ Tempfile.open('old') do |old_file|
115
+ Tempfile.open('new') do |new_file|
116
+ # Write files
117
+ old_file.write(a)
118
+ old_file.flush
119
+ new_file.write(b)
120
+ new_file.flush
121
+
122
+ # Diff
123
+ cmd = [ 'diff', '-u', old_file.path, new_file.path ]
124
+ Open3.popen3(*cmd) do |stdin, stdout, stderr|
125
+ result = stdout.read
126
+ return (result == '' ? nil : result)
127
+ end
128
+ end
129
+ end
130
+ rescue Errno::ENOENT
131
+ warn 'Failed to run `diff`, so no diff with the previously compiled ' \
132
+ 'content will be available.'
133
+ nil
134
+ end
78
135
 
79
- # Prune
80
- if self.site.config[:prune][:auto_prune]
81
- Nanoc::Extra::Pruner.new(self.site, :exclude => self.prune_config_exclude).run
136
+ end
137
+
138
+ # Records the time spent per filter and per item representation
139
+ class TimingRecorder < Listener
140
+
141
+ # @option params [Array<Nanoc::ItemRep>] :reps The list of item representations in the site
142
+ def initialize(params={})
143
+ @filter_times = {}
144
+
145
+ @reps = params.fetch(:reps)
82
146
  end
83
147
 
84
- # Give general feedback
85
- puts
86
- puts "Site compiled in #{format('%.2f', Time.now - time_before)}s."
148
+ # @see Listener#start
149
+ def start
150
+ Nanoc::NotificationCenter.on(:filtering_started) do |rep, filter_name|
151
+ @filter_times[filter_name] ||= []
152
+ @filter_times[filter_name] << Time.now
153
+ end
154
+ Nanoc::NotificationCenter.on(:filtering_ended) do |rep, filter_name|
155
+ @filter_times[filter_name] << Time.now - @filter_times[filter_name].pop
156
+ end
157
+ end
158
+
159
+ # @see Listener#stop
160
+ def stop
161
+ super
162
+ self.print_profiling_feedback
163
+ end
164
+
165
+ protected
87
166
 
88
- # Give detailed feedback
89
- if options.has_key?(:verbose)
90
- print_profiling_feedback(reps)
167
+ def print_profiling_feedback
168
+ # Get max filter length
169
+ max_filter_name_length = @filter_times.keys.map { |k| k.to_s.size }.max
170
+ return if max_filter_name_length.nil?
171
+
172
+ # Print warning if necessary
173
+ if @reps.any? { |r| !r.compiled? }
174
+ $stderr.puts
175
+ $stderr.puts "Warning: profiling information may not be accurate because " +
176
+ "some items were not compiled."
177
+ end
178
+
179
+ # Print header
180
+ puts
181
+ puts ' ' * max_filter_name_length + ' | count min avg max tot'
182
+ puts '-' * max_filter_name_length + '-+-----------------------------------'
183
+
184
+ @filter_times.to_a.sort_by { |r| r[1] }.each do |row|
185
+ self.print_row(row)
186
+ end
91
187
  end
188
+
189
+ def print_row(row)
190
+ # Extract data
191
+ filter_name, samples = *row
192
+
193
+ # Calculate stats
194
+ count = samples.size
195
+ min = samples.min
196
+ tot = samples.inject { |memo, i| memo + i}
197
+ avg = tot/count
198
+ max = samples.max
199
+
200
+ # Format stats
201
+ count = format('%4d', count)
202
+ min = format('%4.2f', min)
203
+ avg = format('%4.2f', avg)
204
+ max = format('%4.2f', max)
205
+ tot = format('%5.2f', tot)
206
+
207
+ # Output stats
208
+ filter_name = format("%#{max_filter_name_length}s", filter_name)
209
+ puts "#{filter_name} | #{count} #{min}s #{avg}s #{max}s #{tot}s"
210
+ end
211
+
92
212
  end
93
213
 
94
- def setup_notifications
95
- # Diff generation
96
- require 'tempfile'
97
- old_contents = {}
98
- Nanoc::NotificationCenter.on(:will_write_rep) do |rep, snapshot|
99
- path = rep.raw_path(:snapshot => snapshot)
100
- old_contents[rep] = File.file?(path) ? File.read(path) : nil
214
+ # Controls garbage collection so that it only occurs once every 20 items
215
+ class GCController < Listener
216
+
217
+ def initialize
218
+ @gc_count = 0
101
219
  end
102
- Nanoc::NotificationCenter.on(:rep_written) do |rep, path, is_created, is_modified|
103
- if !rep.binary? && self.site.config[:enable_output_diff]
104
- new_contents = File.file?(path) ? File.read(path) : nil
105
- if old_contents[rep] && new_contents
106
- generate_diff_for(rep, old_contents[rep], new_contents)
220
+
221
+ # @see Listener#start
222
+ def start
223
+ Nanoc::NotificationCenter.on(:compilation_started) do |rep|
224
+ if @gc_count % 20 == 0
225
+ GC.enable
226
+ GC.start
227
+ GC.disable
107
228
  end
229
+ @gc_count += 1
108
230
  end
109
231
  end
110
232
 
111
- # File notifications
112
- Nanoc::NotificationCenter.on(:rep_written) do |rep, path, is_created, is_modified|
113
- action = (is_created ? :create : (is_modified ? :update : :identical))
114
- level = (is_created ? :high : (is_modified ? :high : :low))
115
- duration = Time.now - @rep_times[rep.raw_path] if @rep_times[rep.raw_path]
116
- Nanoc::CLI::Logger.instance.file(level, action, path, duration)
233
+ # @see Listener#stop
234
+ def stop
235
+ super
236
+ GC.enable
117
237
  end
118
238
 
119
- # Debug notifications
120
- if self.debug?
239
+ end
240
+
241
+ # Prints debug information (compilation started/ended, filtering started/ended, …)
242
+ class DebugPrinter < Listener
243
+
244
+ # @see Listener#start
245
+ def start
121
246
  Nanoc::NotificationCenter.on(:compilation_started) do |rep|
122
247
  puts "*** Started compilation of #{rep.inspect}"
123
248
  end
@@ -148,165 +273,116 @@ module Nanoc::CLI::Commands
148
273
  end
149
274
  end
150
275
 
151
- # Timing notifications
152
- Nanoc::NotificationCenter.on(:compilation_started) do |rep|
153
- if @gc_count % 20 == 0 && !ENV.has_key?('TRAVIS')
154
- GC.enable
155
- GC.start
156
- GC.disable
157
- end
158
- @gc_count += 1
159
- @rep_times[rep.raw_path] = Time.now
160
- end
161
- Nanoc::NotificationCenter.on(:compilation_ended) do |rep|
162
- @rep_times[rep.raw_path] = Time.now - @rep_times[rep.raw_path]
163
- end
164
- Nanoc::NotificationCenter.on(:filtering_started) do |rep, filter_name|
165
- @filter_times[filter_name] ||= []
166
- @filter_times[filter_name] << Time.now
167
- start_filter_progress(rep, filter_name)
168
- end
169
- Nanoc::NotificationCenter.on(:filtering_ended) do |rep, filter_name|
170
- @filter_times[filter_name] << Time.now - @filter_times[filter_name].pop
171
- stop_filter_progress(rep, filter_name)
172
- end
173
276
  end
174
277
 
175
- def setup_diffs
176
- @diff_lock = Mutex.new
177
- @diff_threads = []
178
- FileUtils.rm('output.diff') if File.file?('output.diff')
179
- end
278
+ # Prints file actions (created, updated, deleted, identical, skipped)
279
+ class FileActionPrinter < Listener
180
280
 
181
- def teardown_diffs
182
- @diff_threads.each { |t| t.join }
183
- end
281
+ # @option params [Array<Nanoc::ItemRep>] :reps The list of item representations in the site
282
+ def initialize(params={})
283
+ @rep_times = {}
184
284
 
185
- def generate_diff_for(rep, old_content, new_content)
186
- return if old_content == new_content
187
-
188
- @diff_threads << Thread.new do
189
- # Generate diff
190
- diff = diff_strings(old_content, new_content)
191
- diff.sub!(/^--- .*/, '--- ' + rep.raw_path)
192
- diff.sub!(/^\+\+\+ .*/, '+++ ' + rep.raw_path)
285
+ @reps = params.fetch(:reps)
286
+ end
193
287
 
194
- # Write diff
195
- @diff_lock.synchronize do
196
- File.open('output.diff', 'a') { |io| io.write(diff) }
288
+ # @see Listener#start
289
+ def start
290
+ Nanoc::NotificationCenter.on(:compilation_started) do |rep|
291
+ @rep_times[rep.raw_path] = Time.now
292
+ end
293
+ Nanoc::NotificationCenter.on(:compilation_ended) do |rep|
294
+ @rep_times[rep.raw_path] = Time.now - @rep_times[rep.raw_path]
295
+ end
296
+ Nanoc::NotificationCenter.on(:rep_written) do |rep, path, is_created, is_modified|
297
+ action = (is_created ? :create : (is_modified ? :update : :identical))
298
+ level = (is_created ? :high : (is_modified ? :high : :low))
299
+ duration = Time.now - @rep_times[rep.raw_path] if @rep_times[rep.raw_path]
300
+ Nanoc::CLI::Logger.instance.file(level, action, path, duration)
197
301
  end
198
302
  end
199
- end
200
303
 
201
- # TODO move this elsewhere
202
- def diff_strings(a, b)
203
- require 'open3'
204
-
205
- # Create files
206
- Tempfile.open('old') do |old_file|
207
- Tempfile.open('new') do |new_file|
208
- # Write files
209
- old_file.write(a)
210
- old_file.flush
211
- new_file.write(b)
212
- new_file.flush
213
-
214
- # Diff
215
- cmd = [ 'diff', '-u', old_file.path, new_file.path ]
216
- Open3.popen3(*cmd) do |stdin, stdout, stderr|
217
- result = stdout.read
218
- return (result == '' ? nil : result)
304
+ # @see Listener#stop
305
+ def stop
306
+ super
307
+ @reps.select { |r| !r.compiled? }.each do |rep|
308
+ rep.raw_paths.each do |snapshot_name, filename|
309
+ next if filename.nil?
310
+ duration = @rep_times[filename]
311
+ Nanoc::CLI::Logger.instance.file(:high, :skip, filename, duration)
219
312
  end
220
313
  end
221
314
  end
222
- rescue Errno::ENOENT
223
- warn 'Failed to run `diff`, so no diff with the previously compiled ' \
224
- 'content will be available.'
225
- nil
226
- end
227
-
228
- def start_filter_progress(rep, filter_name)
229
- # Only show progress on terminals
230
- return if !$stdout.tty?
231
315
 
232
- @progress_thread = Thread.new do
233
- delay = 1.0
234
- step = 0
235
-
236
- text = " running #{filter_name} filter… "
237
-
238
- loop do
239
- if Thread.current[:stopped]
240
- # Clear
241
- if delay < 0.1
242
- $stdout.print ' ' * (text.length + 3) + "\r"
243
- end
316
+ end
244
317
 
245
- break
246
- end
318
+ def run
319
+ self.require_site
320
+ self.check_for_deprecated_usage
321
+ self.setup_listeners
247
322
 
248
- # Show progress
249
- if delay < 0.1
250
- $stdout.print text + %w( | / - \\ )[step] + "\r"
251
- step = (step + 1) % 4
252
- end
323
+ puts "Compiling site…"
324
+ time_before = Time.now
325
+ self.site.compile
326
+ self.prune
327
+ time_after = Time.now
328
+ puts
329
+ puts "Site compiled in #{format('%.2f', time_after - time_before)}s."
330
+ end
253
331
 
254
- sleep 0.1
255
- delay -= 0.1
256
- end
332
+ protected
257
333
 
334
+ def prune
335
+ if self.site.config[:prune][:auto_prune]
336
+ Nanoc::Extra::Pruner.new(self.site, :exclude => self.prune_config_exclude).run
258
337
  end
259
338
  end
260
339
 
261
- def stop_filter_progress(rep, filter_name)
262
- # Only show progress on terminals
263
- return if !$stdout.tty?
340
+ def setup_listeners
341
+ @listeners = []
264
342
 
265
- @progress_thread[:stopped] = true
266
- end
343
+ if self.site.config[:enable_output_diff]
344
+ @listeners << Nanoc::CLI::Commands::Compile::DiffGenerator.new
345
+ end
267
346
 
268
- def print_profiling_feedback(reps)
269
- # Get max filter length
270
- max_filter_name_length = @filter_times.keys.map { |k| k.to_s.size }.max
271
- return if max_filter_name_length.nil?
347
+ if self.debug?
348
+ @listeners << Nanoc::CLI::Commands::Compile::DebugPrinter.new
349
+ end
272
350
 
273
- # Print warning if necessary
274
- if reps.any? { |r| !r.compiled? }
275
- $stderr.puts
276
- $stderr.puts "Warning: profiling information may not be accurate because " +
277
- "some items were not compiled."
351
+ if options.fetch(:verbose, false)
352
+ @listeners << Nanoc::CLI::Commands::Compile::TimingRecorder.new(:reps => self.reps)
278
353
  end
279
354
 
280
- # Print header
281
- puts
282
- puts ' ' * max_filter_name_length + ' | count min avg max tot'
283
- puts '-' * max_filter_name_length + '-+-----------------------------------'
355
+ unless ENV.has_key?('TRAVIS')
356
+ @listeners << Nanoc::CLI::Commands::Compile::GCController.new
357
+ end
284
358
 
285
- @filter_times.to_a.sort_by { |r| r[1] }.each do |row|
286
- # Extract data
287
- filter_name, samples = *row
359
+ @listeners << Nanoc::CLI::Commands::Compile::FileActionPrinter.new(:reps => self.reps)
288
360
 
289
- # Calculate stats
290
- count = samples.size
291
- min = samples.min
292
- tot = samples.inject { |memo, i| memo + i}
293
- avg = tot/count
294
- max = samples.max
361
+ @listeners.each { |s| s.start }
362
+ end
295
363
 
296
- # Format stats
297
- count = format('%4d', count)
298
- min = format('%4.2f', min)
299
- avg = format('%4.2f', avg)
300
- max = format('%4.2f', max)
301
- tot = format('%5.2f', tot)
364
+ def teardown_listeners
365
+ @listeners.each { |s| s.stop }
366
+ end
302
367
 
303
- # Output stats
304
- filter_name = format("%#{max_filter_name_length}s", filter_name)
305
- puts "#{filter_name} | #{count} #{min}s #{avg}s #{max}s #{tot}s"
306
- end
368
+ def reps
369
+ self.site.items.map { |i| i.reps }.flatten
307
370
  end
371
+ memoize :reps
308
372
 
309
- protected
373
+ def check_for_deprecated_usage
374
+ # Check presence of --all option
375
+ if options.has_key?(:all) || options.has_key?(:force)
376
+ $stderr.puts "Warning: the --force option (and its deprecated --all alias) are, as of nanoc 3.2, no longer supported and have no effect."
377
+ end
378
+
379
+ # Warn if trying to compile a single item
380
+ if arguments.size == 1
381
+ $stderr.puts '-' * 80
382
+ $stderr.puts 'Note: As of nanoc 3.2, it is no longer possible to compile a single item. When invoking the “compile” command, all items in the site will be compiled.'
383
+ $stderr.puts '-' * 80
384
+ end
385
+ end
310
386
 
311
387
  def prune_config
312
388
  self.site.config[:prune] || {}