nanoc3 3.2.0a1 → 3.2.0a2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. data/ChangeLog +2 -2
  2. data/NEWS.md +14 -0
  3. data/README.md +20 -12
  4. data/doc/yardoc_templates/default/layout/html/footer.erb +10 -0
  5. data/lib/nanoc3/base/compilation/checksum_store.rb +130 -0
  6. data/lib/nanoc3/base/compilation/checksummer.rb +68 -0
  7. data/lib/nanoc3/base/compilation/compiled_content_cache.rb +57 -0
  8. data/lib/nanoc3/base/{compiler.rb → compilation/compiler.rb} +255 -55
  9. data/lib/nanoc3/base/{compiler_dsl.rb → compilation/compiler_dsl.rb} +11 -6
  10. data/lib/nanoc3/base/{dependency_tracker.rb → compilation/dependency_tracker.rb} +62 -92
  11. data/lib/nanoc3/base/{filter.rb → compilation/filter.rb} +0 -0
  12. data/lib/nanoc3/base/compilation/item_rep_proxy.rb +87 -0
  13. data/lib/nanoc3/base/compilation/outdatedness_checker.rb +86 -0
  14. data/lib/nanoc3/base/compilation/outdatedness_reasons.rb +43 -0
  15. data/lib/nanoc3/base/{rule.rb → compilation/rule.rb} +8 -2
  16. data/lib/nanoc3/base/{rule_context.rb → compilation/rule_context.rb} +17 -14
  17. data/lib/nanoc3/base/directed_graph.rb +8 -0
  18. data/lib/nanoc3/base/errors.rb +9 -17
  19. data/lib/nanoc3/base/plugin_registry.rb +1 -1
  20. data/lib/nanoc3/base/result_data/item_rep.rb +447 -0
  21. data/lib/nanoc3/base/{code_snippet.rb → source_data/code_snippet.rb} +7 -22
  22. data/lib/nanoc3/base/{data_source.rb → source_data/data_source.rb} +0 -0
  23. data/lib/nanoc3/base/{item.rb → source_data/item.rb} +20 -30
  24. data/lib/nanoc3/base/{layout.rb → source_data/layout.rb} +7 -26
  25. data/lib/nanoc3/base/source_data/site.rb +314 -0
  26. data/lib/nanoc3/base/store.rb +126 -0
  27. data/lib/nanoc3/base.rb +26 -15
  28. data/lib/nanoc3/cli/base.rb +2 -1
  29. data/lib/nanoc3/cli/commands/compile.rb +116 -48
  30. data/lib/nanoc3/cli/commands/create_item.rb +0 -1
  31. data/lib/nanoc3/cli/commands/create_layout.rb +0 -1
  32. data/lib/nanoc3/cli/commands/create_site.rb +11 -1
  33. data/lib/nanoc3/cli/commands/debug.rb +11 -6
  34. data/lib/nanoc3/cli/commands/info.rb +1 -2
  35. data/lib/nanoc3/cli/commands/update.rb +0 -1
  36. data/lib/nanoc3/cli/commands/watch.rb +27 -32
  37. data/lib/nanoc3/cli/logger.rb +2 -2
  38. data/lib/nanoc3/data_sources/filesystem.rb +1 -6
  39. data/lib/nanoc3/extra/auto_compiler.rb +2 -3
  40. data/lib/nanoc3/extra/deployers/rsync.rb +1 -0
  41. data/lib/nanoc3/extra/validators/links.rb +45 -26
  42. data/lib/nanoc3/filters/asciidoc.rb +58 -0
  43. data/lib/nanoc3/filters/colorize_syntax.rb +47 -9
  44. data/lib/nanoc3/filters/less.rb +6 -0
  45. data/lib/nanoc3/filters/sass.rb +8 -5
  46. data/lib/nanoc3/filters.rb +2 -0
  47. data/lib/nanoc3/helpers/blogging.rb +8 -0
  48. data/lib/nanoc3/helpers/html_escape.rb +37 -7
  49. data/lib/nanoc3/helpers/link_to.rb +15 -4
  50. data/lib/nanoc3/helpers/rendering.rb +6 -2
  51. data/lib/nanoc3/tasks/clean.rb +0 -4
  52. data/lib/nanoc3.rb +1 -1
  53. data/nanoc3.gemspec +42 -0
  54. metadata +35 -19
  55. data/lib/nanoc3/base/checksummer.rb +0 -40
  56. data/lib/nanoc3/base/compiled_content_cache.rb +0 -86
  57. data/lib/nanoc3/base/item_rep.rb +0 -537
  58. data/lib/nanoc3/base/site.rb +0 -490
@@ -17,17 +17,15 @@ module Nanoc3::CLI::Commands
17
17
  end
18
18
 
19
19
  def long_desc
20
- 'Compile all items of the current site. If an identifier is given, ' +
21
- 'only the item with the given identifier will be compiled. ' +
20
+ 'Compile all items of the current site.' +
22
21
  "\n\n" +
23
22
  'By default, only item that are outdated will be compiled. This can ' +
24
23
  'speed up the compilation process quite a bit, but items that include ' +
25
- 'content from other items may have to be recompiled manually. In ' +
26
- 'order to compile items even when they are outdated, use the --force option.'
24
+ 'content from other items may have to be recompiled manually.'
27
25
  end
28
26
 
29
27
  def usage
30
- "nanoc3 compile [options] [identifier]"
28
+ "nanoc3 compile [options]"
31
29
  end
32
30
 
33
31
  def option_definitions
@@ -35,12 +33,12 @@ module Nanoc3::CLI::Commands
35
33
  # --all
36
34
  {
37
35
  :long => 'all', :short => 'a', :argument => :forbidden,
38
- :desc => 'alias for --force (DEPRECATED)'
36
+ :desc => '(ignored)'
39
37
  },
40
38
  # --force
41
39
  {
42
40
  :long => 'force', :short => 'f', :argument => :forbidden,
43
- :desc => 'compile items even when they are not outdated'
41
+ :desc => '(ignored)'
44
42
  }
45
43
  ]
46
44
  end
@@ -49,30 +47,21 @@ module Nanoc3::CLI::Commands
49
47
  # Make sure we are in a nanoc site directory
50
48
  puts "Loading site data..."
51
49
  @base.require_site
52
- @base.site.load_data
53
50
 
54
51
  # Check presence of --all option
55
- if options.has_key?(:all)
56
- $stderr.puts "Warning: the --all option is deprecated; please use --force instead."
57
- end
58
-
59
- # Find item(s) to compile
60
- if arguments.size == 0
61
- item = nil
62
- elsif arguments.size == 1
63
- # Find item
64
- identifier = arguments[0].cleaned_identifier
65
- item = @base.site.items.find { |item| item.identifier == identifier }
66
-
67
- # Ensure item
68
- if item.nil?
69
- $stderr.puts "Unknown item: #{identifier}"
70
- exit 1
71
- end
52
+ if options.has_key?(:all) || options.has_key?(:force)
53
+ $stderr.puts "Warning: the --force option (and its deprecated --all alias) are, as of nanoc 3.2, no longer supported and have no effect."
54
+ end
55
+
56
+ # Warn if trying to compile a single item
57
+ if arguments.size == 1
58
+ $stderr.puts '-' * 80
59
+ $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.'.make_compatible_with_env
60
+ $stderr.puts '-' * 80
72
61
  end
73
62
 
74
63
  # Give feedback
75
- puts "Compiling #{item.nil? ? 'site' : 'item'}..."
64
+ puts "Compiling site..."
76
65
 
77
66
  # Initialize profiling stuff
78
67
  time_before = Time.now
@@ -80,14 +69,14 @@ module Nanoc3::CLI::Commands
80
69
  @filter_times = {}
81
70
  setup_notifications
82
71
 
72
+ # Prepare for generating diffs
73
+ setup_diffs
74
+
83
75
  # Compile
84
- @base.site.compiler.run(
85
- item,
86
- :force => options.has_key?(:all) || options.has_key?(:force)
87
- )
76
+ @base.site.compile
88
77
 
89
78
  # Find reps
90
- reps = @base.site.items.map { |i| i.reps }.flatten
79
+ reps = @base.site.items.map { |i| i.reps }.flatten
91
80
 
92
81
  # Show skipped reps
93
82
  reps.select { |r| !r.compiled? }.each do |rep|
@@ -98,12 +87,12 @@ module Nanoc3::CLI::Commands
98
87
  end
99
88
  end
100
89
 
101
- # Show diff
102
- write_diff_for(reps)
90
+ # Stop diffing
91
+ teardown_diffs
103
92
 
104
93
  # Give general feedback
105
94
  puts
106
- puts "#{item.nil? ? 'Site' : 'Item'} compiled in #{format('%.2f', Time.now - time_before)}s."
95
+ puts "Site compiled in #{format('%.2f', Time.now - time_before)}s."
107
96
 
108
97
  # Give detailed feedback
109
98
  if options.has_key?(:verbose)
@@ -115,6 +104,9 @@ module Nanoc3::CLI::Commands
115
104
 
116
105
  def setup_notifications
117
106
  # File notifications
107
+ Nanoc3::NotificationCenter.on(:will_write_rep) do |rep, snapshot|
108
+ generate_diff_for(rep, snapshot)
109
+ end
118
110
  Nanoc3::NotificationCenter.on(:rep_written) do |rep, path, is_created, is_modified|
119
111
  action = (is_created ? :create : (is_modified ? :update : :identical))
120
112
  duration = Time.now - @rep_times[rep.raw_path] if @rep_times[rep.raw_path]
@@ -144,36 +136,112 @@ module Nanoc3::CLI::Commands
144
136
  end
145
137
  Nanoc3::NotificationCenter.on(:filtering_started) do |rep, filter_name|
146
138
  @filter_times[filter_name] = Time.now
139
+ start_filter_progress(rep, filter_name)
147
140
  end
148
141
  Nanoc3::NotificationCenter.on(:filtering_ended) do |rep, filter_name|
149
142
  @filter_times[filter_name] = Time.now - @filter_times[filter_name]
143
+ stop_filter_progress(rep, filter_name)
150
144
  end
151
145
  end
152
146
 
153
- def write_diff_for(reps)
154
- # Delete diff
147
+ def setup_diffs
148
+ @diff_lock = Mutex.new
149
+ @diff_threads = []
155
150
  FileUtils.rm('output.diff') if File.file?('output.diff')
151
+ end
152
+
153
+ def teardown_diffs
154
+ @diff_threads.each { |t| t.join }
155
+ end
156
156
 
157
- # Don’t generate diffs when diffs are disabled
157
+ def generate_diff_for(rep, snapshot)
158
158
  return if !@base.site.config[:enable_output_diff]
159
+ return if !File.file?(rep.raw_path(:snapshot => snapshot))
160
+ return if rep.binary?
161
+
162
+ # Get old and new content
163
+ old_content = File.read(rep.raw_path(:snapshot => snapshot))
164
+ new_content = rep.compiled_content(:snapshot => snapshot, :force => true)
159
165
 
160
- # Generate diff
161
- full_diff = ''
162
- reps.each do |rep|
163
- diff = rep.diff
164
- next if diff.nil?
166
+ # Check whether there’s a different
167
+ return if old_content == new_content
165
168
 
166
- # Fix header
167
- # FIXME this may break for other lines starting with --- or +++
169
+ require 'thread'
170
+ @diff_threads << Thread.new do
171
+ # Generate diff
172
+ diff = diff_strings(old_content, new_content)
168
173
  diff.sub!(/^--- .*/, '--- ' + rep.raw_path)
169
174
  diff.sub!(/^\+\+\+ .*/, '+++ ' + rep.raw_path)
170
175
 
171
- # Add
172
- full_diff << diff
176
+ # Write diff
177
+ @diff_lock.synchronize do
178
+ File.open('output.diff', 'a') { |io| io.write(diff) }
179
+ end
180
+ end
181
+ end
182
+
183
+ # TODO move this elsewhere
184
+ def diff_strings(a, b)
185
+ require 'tempfile'
186
+ require 'open3'
187
+
188
+ # Create files
189
+ Tempfile.open('old') do |old_file|
190
+ Tempfile.open('new') do |new_file|
191
+ # Write files
192
+ old_file.write(a)
193
+ old_file.flush
194
+ new_file.write(b)
195
+ new_file.flush
196
+
197
+ # Diff
198
+ cmd = [ 'diff', '-u', old_file.path, new_file.path ]
199
+ Open3.popen3(*cmd) do |stdin, stdout, stderr|
200
+ result = stdout.read
201
+ return (result == '' ? nil : result)
202
+ end
203
+ end
173
204
  end
205
+ rescue Errno::ENOENT
206
+ warn 'Failed to run `diff`, so no diff with the previously compiled ' \
207
+ 'content will be available.'
208
+ nil
209
+ end
210
+
211
+ def start_filter_progress(rep, filter_name)
212
+ # Only show progress on terminals
213
+ return if !$stdout.tty?
214
+
215
+ @progress_thread = Thread.new do
216
+ delay = 1.0
217
+ step = 0
218
+
219
+ text = "Running #{filter_name} filter… ".make_compatible_with_env
220
+
221
+ while !Thread.current[:stopped]
222
+ sleep 0.1
223
+
224
+ # Wait for a while before showing text
225
+ delay -= 0.1
226
+ next if delay > 0.05
227
+
228
+ # Print progress
229
+ $stdout.print text + %w( | / - \\ )[step] + "\r"
230
+ step = (step + 1) % 4
231
+ end
232
+
233
+ # Clear text
234
+ if delay < 0.05
235
+ $stdout.print ' ' * (text.length + 1 + 1) + "\r"
236
+ end
237
+ end
238
+ end
239
+
240
+ def stop_filter_progress(rep, filter_name)
241
+ # Only show progress on terminals
242
+ return if !$stdout.tty?
174
243
 
175
- # Write
176
- File.open('output.diff', 'w') { |io| io.write(full_diff) }
244
+ @progress_thread[:stopped] = true
177
245
  end
178
246
 
179
247
  def print_profiling_feedback(reps)
@@ -46,7 +46,6 @@ module Nanoc3::CLI::Commands
46
46
 
47
47
  # Make sure we are in a nanoc site directory
48
48
  @base.require_site
49
- @base.site.load_data
50
49
 
51
50
  # Set VCS if possible
52
51
  @base.set_vcs(options[:vcs])
@@ -46,7 +46,6 @@ module Nanoc3::CLI::Commands
46
46
 
47
47
  # Make sure we are in a nanoc site directory
48
48
  @base.require_site
49
- @base.site.load_data
50
49
 
51
50
  # Set VCS if possible
52
51
  @base.set_vcs(options[:vcs])
@@ -71,6 +71,10 @@ watcher:
71
71
  # A list of single files to watch for changes. As mentioned above, don’t put
72
72
  # any files from the “output/” or “tmp/” directories in here.
73
73
  files_to_watch: [ 'config.yaml', 'Rules' ]
74
+
75
+ # When to send notifications (using Growl or notify-send).
76
+ notify_on_compilation_success: true
77
+ notify_on_compilation_failure: true
74
78
  EOS
75
79
 
76
80
  DEFAULT_RULES = <<EOS
@@ -100,7 +104,13 @@ route '/stylesheet/' do
100
104
  end
101
105
 
102
106
  route '*' do
103
- item.identifier + 'index.html'
107
+ if item.binary?
108
+ # /foo/ -> /foo.ext
109
+ item.identifier.chop + '.' + item[:extension]
110
+ else
111
+ # /foo/ -> /foo/index.html
112
+ item.identifier + 'index.html'
113
+ end
104
114
  end
105
115
 
106
116
  layout '*', :erb
@@ -33,7 +33,6 @@ module Nanoc3::CLI::Commands
33
33
  # Make sure we are in a nanoc site directory
34
34
  print "Loading site data... "
35
35
  @base.require_site
36
- @base.site.load_data
37
36
  puts "done"
38
37
  puts
39
38
 
@@ -43,8 +42,9 @@ module Nanoc3::CLI::Commands
43
42
  layouts = @base.site.layouts
44
43
 
45
44
  # Get dependency tracker
46
- dependency_tracker = @base.site.compiler.dependency_tracker
47
- dependency_tracker.load_graph
45
+ compiler = @base.site.compiler
46
+ compiler.load
47
+ dependency_tracker = compiler.dependency_tracker
48
48
 
49
49
  # Print item dependencies
50
50
  puts '=== Item dependencies ======================================================='
@@ -82,9 +82,9 @@ module Nanoc3::CLI::Commands
82
82
  items.sort_by { |i| i.identifier }.each do |item|
83
83
  item.reps.sort_by { |r| r.name.to_s }.each do |rep|
84
84
  puts "item #{item.identifier}, rep #{rep.name}:"
85
- outdatedness_reason = rep.outdatedness_reason
85
+ outdatedness_reason = compiler.outdatedness_reason_for(rep)
86
86
  if outdatedness_reason
87
- puts " is outdated: #{outdatedness_reason[:type]} (#{outdatedness_reason[:description]})"
87
+ puts " is outdated: #{outdatedness_reason.message}"
88
88
  else
89
89
  puts " is not outdated"
90
90
  end
@@ -97,7 +97,12 @@ module Nanoc3::CLI::Commands
97
97
  puts
98
98
  layouts.each do |layout|
99
99
  puts "layout #{layout.identifier}:"
100
- puts " is #{layout.outdated? ? '' : 'not '}outdated"
100
+ outdatedness_reason = compiler.outdatedness_reason_for(layout)
101
+ if outdatedness_reason
102
+ puts " is outdated: #{outdatedness_reason.message}"
103
+ else
104
+ puts " is not outdated"
105
+ end
101
106
  puts
102
107
  end
103
108
  end
@@ -39,8 +39,7 @@ module Nanoc3::CLI::Commands
39
39
 
40
40
  # Get list of plugins (before and after)
41
41
  plugins_before = Nanoc3::Plugin.all
42
- @base.site
43
- @base.site.load_data if @base.site
42
+ @base.site.code_snippets if @base.site
44
43
  plugins_after = Nanoc3::Plugin.all
45
44
 
46
45
  # Divide list of plugins into builtin and custom
@@ -55,7 +55,6 @@ module Nanoc3::CLI::Commands
55
55
 
56
56
  # Make sure we are in a nanoc site directory
57
57
  @base.require_site
58
- @base.site.load_data
59
58
 
60
59
  # Set VCS if possible
61
60
  @base.set_vcs(options[:vcs])
@@ -32,6 +32,11 @@ module Nanoc3::CLI::Commands
32
32
  def run(options, arguments)
33
33
  require 'fssm'
34
34
 
35
+ Signal.trap("INT") do
36
+ puts
37
+ exit
38
+ end
39
+
35
40
  @notifier = Notifier.new
36
41
 
37
42
  # Define rebuilder
@@ -53,17 +58,27 @@ module Nanoc3::CLI::Commands
53
58
  # Recompile
54
59
  start = Time.now
55
60
  site = Nanoc3::Site.new('.')
56
- site.load_data
57
61
  begin
58
- site.compiler.run
62
+ site.compile
59
63
 
60
64
  # TODO include icon (--image misc/success-icon.png)
61
- @notifier.notify('Compilation complete')
65
+ notify_on_compilation_success = site.config.has_key?(:notify_on_compilation_success) ?
66
+ site.config[:notify_on_compilation_success] :
67
+ true
68
+ if notify_on_compilation_success
69
+ @notifier.notify('Compilation complete')
70
+ end
62
71
 
63
- puts "done in #{((Time.now - start)*10000).round.to_f / 10}ms"
72
+ time_spent = ((Time.now - start)*1000.0).round
73
+ puts "done in #{format '%is %ims', *(time_spent.divmod(1000))}"
64
74
  rescue Exception => e
65
75
  # TODO include icon (--image misc/error-icon.png)
66
- @notifier.notify('Compilation failed')
76
+ notify_on_compilation_failure = site.config.has_key?(:notify_on_compilation_failure) ?
77
+ site.config[:notify_on_compilation_failure] :
78
+ true
79
+ if notify_on_compilation_failure
80
+ @notifier.notify('Compilation failed')
81
+ end
67
82
 
68
83
  puts
69
84
  @base.print_error(e)
@@ -96,35 +111,15 @@ module Nanoc3::CLI::Commands
96
111
  class Notifier
97
112
 
98
113
  # A list of commandline tool names that can be used to send notifications
99
- TOOLS = %w( growlnotify notify_send )
100
-
101
- # Error that is raised when no notifier can be found.
102
- class NoNotifierFound < ::StandardError
103
-
104
- def initialize
105
- super("Could not find a notifier that works on this system. I tried to find #{CrossPlatformNotifier::TOOLS.join(', ')} but found nothing.")
106
- end
114
+ TOOLS = %w( growlnotify notify-send )
107
115
 
108
- end
109
-
110
- # Send a notification.
116
+ # Send a notification. If no notifier is found, no notification will be
117
+ # created.
111
118
  #
112
119
  # @param [String] message The message to include in the notification
113
- #
114
- # @option params [Boolean] :raise (true) true if this method should
115
- # raise an exception if no notifier can be found, false otherwise
116
- def notify(message, params={})
117
- params[:raise] = true if !params.has_key?(:raise)
118
-
119
- if tool.nil?
120
- if params[:raise]
121
- raise NoNotifierFound
122
- else
123
- return
124
- end
125
- end
126
-
127
- send(tool, message, params)
120
+ def notify(message)
121
+ return if tool.nil?
122
+ send(tool.tr('-', '_'), message, params)
128
123
  end
129
124
 
130
125
  private
@@ -138,7 +133,7 @@ module Nanoc3::CLI::Commands
138
133
  end
139
134
 
140
135
  def notify_send(message, params={})
141
- system('notify_send', messsage)
136
+ system('notify-send', message)
142
137
  end
143
138
 
144
139
  end
@@ -28,13 +28,13 @@ module Nanoc3::CLI
28
28
 
29
29
  def initialize
30
30
  @level = :high
31
- @color = true
31
+ @color = $stdout.tty?
32
32
 
33
33
  # Try enabling color support on Windows
34
34
  begin
35
35
  require 'Win32/Console/ANSI' if RUBY_PLATFORM =~/mswin|mingw/
36
36
  rescue LoadError
37
- warn 'The win32console gem is not available. Install it to enable color support on Windows.'
37
+ @color = false
38
38
  end
39
39
  end
40
40
 
@@ -123,15 +123,10 @@ module Nanoc3::DataSources
123
123
  raise RuntimeError, "meta_mtime and content_mtime are both nil"
124
124
  end
125
125
 
126
- # Get checksum
127
- meta_checksum = meta_filename ? Nanoc3::Checksummer.checksum_for(meta_filename) : nil
128
- content_checksum = content_filename ? Nanoc3::Checksummer.checksum_for(content_filename) : nil
129
- checksum = [ meta_checksum, content_checksum ].compact.join('-')
130
-
131
126
  # Create layout object
132
127
  klass.new(
133
128
  content_or_filename, attributes, identifier,
134
- :binary => is_binary, :mtime => mtime, :checksum => checksum
129
+ :binary => is_binary, :mtime => mtime
135
130
  )
136
131
  end
137
132
  end
@@ -43,8 +43,8 @@ module Nanoc3::Extra
43
43
  r.raw_path == site.config[:output_dir] + path
44
44
  end
45
45
 
46
- # Recompile rep
47
- site.compiler.run(rep.item) if rep
46
+ # Recompile
47
+ site.compile if rep
48
48
 
49
49
  # Get paths by appending index filenames
50
50
  if path =~ /\/$/
@@ -82,7 +82,6 @@ module Nanoc3::Extra
82
82
 
83
83
  def build_site
84
84
  @site = Nanoc3::Site.new(@site_path)
85
- @site.load_data
86
85
  end
87
86
 
88
87
  def mime_type_of(path, fallback)
@@ -106,6 +106,7 @@ module Nanoc3::Extra::Deployers
106
106
  # Runs the given shell command. This is a simple wrapper around Kernel#system.
107
107
  def run_shell_cmd(args)
108
108
  system(*args)
109
+ raise "command exited with a nonzero status code #{$?.exitstatus} (command: #{args.join(' ')})" if !$?.success?
109
110
  end
110
111
 
111
112
  end
@@ -48,6 +48,36 @@ module Nanoc3::Extra::Validators
48
48
 
49
49
  private
50
50
 
51
+ # Enumerates all key-value pairs of a given hash in a thread-safe way.
52
+ #
53
+ # This class is a helper class, which means that it is not used directly
54
+ # by nanoc. Future versions of nanoc may no longer contain this class. Do
55
+ # not depend on this class to be available.
56
+ class ThreadsafeHashEnumerator
57
+
58
+ # Creates a new enumerator for the given hash.
59
+ #
60
+ # @param [Hash] hash The hash for which the enumerator should return
61
+ # key-value pairs
62
+ def initialize(hash)
63
+ @hash = hash
64
+ @unprocessed_keys = @hash.keys.dup
65
+ @mutex = Mutex.new
66
+ end
67
+
68
+ # Returns the next key-value pair in the hash.
69
+ #
70
+ # @return [Array] An array containing the key and the corresponding
71
+ # value of teh next key-value pair
72
+ def next_pair
73
+ @mutex.synchronize do
74
+ key = @unprocessed_keys.shift
75
+ return (key ? [ key, @hash[key] ] : nil)
76
+ end
77
+ end
78
+
79
+ end
80
+
51
81
  def all_broken_hrefs
52
82
  broken_hrefs = {}
53
83
 
@@ -127,17 +157,23 @@ module Nanoc3::Extra::Validators
127
157
  require 'uri'
128
158
 
129
159
  # Parse
130
- uri = URI.parse(href)
160
+ uri = nil
161
+ begin
162
+ uri = URI.parse(href)
163
+ rescue URI::InvalidURIError
164
+ @delegate && @delegate.send(:external_href_validated, href, false)
165
+ return false
166
+ end
131
167
 
132
168
  # Skip non-HTTP URLs
133
169
  return true if uri.scheme != 'http'
134
170
 
135
171
  # Get status
136
172
  status = fetch_http_status_for(uri)
137
- is_valid = (status && status >= 200 && status <= 299)
173
+ is_valid = !!(status && status >= 200 && status <= 299)
138
174
 
139
175
  # Notify
140
- @delegate.send(:external_href_validated, href, is_valid)
176
+ @delegate && @delegate.send(:external_href_validated, href, is_valid)
141
177
 
142
178
  # Done
143
179
  is_valid
@@ -153,30 +189,10 @@ module Nanoc3::Extra::Validators
153
189
  end
154
190
  end
155
191
 
156
- # This class is a helper class, which means that it is not used directly
157
- # by nanoc. Future versions of nanoc may no longer contain this class. Do
158
- # not depend on this class to be available.
159
- class EachPairEnumerator
160
-
161
- def initialize(hash)
162
- @hash = hash
163
- @unprocessed_keys = @hash.keys.dup
164
- @mutex = Mutex.new
165
- end
166
-
167
- def next_pair
168
- @mutex.synchronize do
169
- key = @unprocessed_keys.shift
170
- return (key ? [ key, @hash[key] ] : nil)
171
- end
172
- end
173
-
174
- end
175
-
176
192
  def validate_external_hrefs(hrefs, broken_hrefs)
177
193
  @mutex = Mutex.new
178
194
 
179
- enum = EachPairEnumerator.new(hrefs)
195
+ enum = ThreadsafeHashEnumerator.new(hrefs)
180
196
 
181
197
  threads = []
182
198
  10.times do
@@ -202,7 +218,10 @@ module Nanoc3::Extra::Validators
202
218
  def fetch_http_status_for(url, params={})
203
219
  5.times do |i|
204
220
  begin
205
- res = request_url_once(url)
221
+ res = nil
222
+ Timeout::timeout(10) do
223
+ res = request_url_once(url)
224
+ end
206
225
 
207
226
  if res.code =~ /^3..$/
208
227
  url = URI.parse(res['location'])
@@ -211,7 +230,7 @@ module Nanoc3::Extra::Validators
211
230
  return res.code.to_i
212
231
  end
213
232
  rescue
214
- nil
233
+ return nil
215
234
  end
216
235
  end
217
236
  end