nanoc3 3.2.0a1 → 3.2.0a2

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 (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