nanoc 3.4.3 → 3.5.0b1

Sign up to get free protection for your applications and to get access to all the features.
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] || {}