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
@@ -0,0 +1,31 @@
1
+ # encoding: utf-8
2
+
3
+ module Nanoc::Extra::Checking
4
+
5
+ class DSL
6
+
7
+ attr_reader :deploy_checks
8
+
9
+ def self.from_file(filename)
10
+ dsl = self.new
11
+ dsl.instance_eval File.read(filename)
12
+ dsl
13
+ end
14
+
15
+ def initialize
16
+ @deploy_checks = []
17
+ end
18
+
19
+ def check(identifier, &block)
20
+ klass = Class.new(::Nanoc::Extra::Checking::Check)
21
+ klass.send(:define_method, :run, &block)
22
+ klass.send(:identifier, identifier)
23
+ end
24
+
25
+ def deploy_check(*identifiers)
26
+ identifiers.each { |i| @deploy_checks << i }
27
+ end
28
+
29
+ end
30
+
31
+ end
@@ -0,0 +1,19 @@
1
+ # encoding: utf-8
2
+
3
+ module Nanoc::Extra::Checking
4
+
5
+ class Issue
6
+
7
+ attr_reader :description
8
+ attr_reader :subject
9
+ attr_reader :check_class
10
+
11
+ def initialize(desc, subject, check_class)
12
+ @description = desc
13
+ @subject = subject
14
+ @check_class = check_class
15
+ end
16
+
17
+ end
18
+
19
+ end
@@ -0,0 +1,130 @@
1
+ # encoding: utf-8
2
+
3
+ module Nanoc::Extra::Checking
4
+
5
+ # Runner is reponsible for running issue checks.
6
+ #
7
+ # @api private
8
+ class Runner
9
+
10
+ # @param [Nanoc::Site] site The nanoc site this runner is for
11
+ def initialize(site)
12
+ @site = site
13
+ end
14
+
15
+ # Ensures that there is a deployer DSL present.
16
+ #
17
+ # @return [void]
18
+ def require_dsl
19
+ if self.dsl.nil?
20
+ raise Nanoc::Errors::GenericTrivial, "No checks defined (no Checks file present)"
21
+ end
22
+ end
23
+
24
+ # Lists all available checks on stdout.
25
+ #
26
+ # @return [void]
27
+ def list_checks
28
+ puts "Available checks:"
29
+ puts
30
+ puts all_check_classes.map { |i| " " + i.identifier.to_s }.sort.join("\n")
31
+ end
32
+
33
+ # Runs all checks.
34
+ #
35
+ # @return [Boolean] true if successful, false otherwise
36
+ def run_all
37
+ self.run_check_classes(self.all_check_classes)
38
+ end
39
+
40
+ # Runs the checks marked for deployment.
41
+ #
42
+ # @return [Boolean] true if successful, false otherwise
43
+ def run_for_deploy
44
+ return true if self.dsl.nil?
45
+ self.run_check_classes(self.check_classes_named(self.dsl.deploy_checks))
46
+ end
47
+
48
+ # Runs the checks with the given names.
49
+ #
50
+ # @param [Array<Symbol>] check_class_names The names of the checks
51
+ #
52
+ # @return [Boolean] true if successful, false otherwise
53
+ def run_specific(check_class_names)
54
+ self.run_check_classes(self.check_classes_named(check_class_names))
55
+ end
56
+
57
+ protected
58
+
59
+ def dsl
60
+ @dsl_loaded ||= false
61
+ if !@dsl_loaded
62
+ if File.exist?('Checks')
63
+ @dsl = Nanoc::Extra::Checking::DSL.from_file('Checks')
64
+ else
65
+ @dsl = nil
66
+ end
67
+ @dsl_loaded = true
68
+ end
69
+ @dsl
70
+ end
71
+
72
+ def run_check_classes(classes)
73
+ issues = self.run_checks(classes)
74
+ self.print_issues(issues)
75
+ issues.empty? ? true : false
76
+ end
77
+
78
+ def all_check_classes
79
+ Nanoc::Extra::Checking::Check.all.map { |p| p.last }.uniq
80
+ end
81
+
82
+ def check_classes_named(n)
83
+ classes = n.map do |a|
84
+ klass = Nanoc::Extra::Checking::Check.named(a)
85
+ raise Nanoc::Errors::GenericTrivial, "Unknown check: #{a}" if klass.nil?
86
+ klass
87
+ end
88
+ end
89
+
90
+ def run_checks(classes)
91
+ return [] if classes.empty?
92
+
93
+ checks = []
94
+ issues = Set.new
95
+ length = classes.map { |c| c.identifier.to_s.length }.max + 18
96
+ classes.each do |klass|
97
+ print format(" %-#{length}s", "Running #{klass.identifier} check… ")
98
+
99
+ check = klass.new(@site)
100
+ check.run
101
+
102
+ checks << check
103
+ issues.merge(check.issues)
104
+
105
+ # TODO report progress
106
+
107
+ puts check.issues.empty? ? 'ok'.green : 'error'.red
108
+ end
109
+ issues
110
+ end
111
+
112
+ def print_issues(issues)
113
+ require 'colored'
114
+
115
+ return if issues.empty?
116
+ puts "Issues found!"
117
+ issues.group_by { |i| i.subject }.to_a.sort_by { |p| p.first }.each do |pair|
118
+ subject = pair.first
119
+ issues = pair.last
120
+ unless issues.empty?
121
+ puts " #{subject}:"
122
+ issues.each do |i|
123
+ puts " [ #{'ERROR'.red} ] #{i.check_class.identifier} - #{i.description}"
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
129
+
130
+ end
@@ -0,0 +1,57 @@
1
+ # encoding: utf-8
2
+
3
+ require 'set'
4
+
5
+ module ::Nanoc::Extra
6
+
7
+ class LinkCollector
8
+
9
+ def initialize(filenames, mode=nil)
10
+ @filenames = filenames
11
+ @filter = case mode
12
+ when nil
13
+ lambda { |h| true }
14
+ when :external
15
+ lambda { |h| external_href?(h) }
16
+ when :internal
17
+ lambda { |h| !external_href?(h) }
18
+ else
19
+ raise ArgumentError, 'Expected mode argument to be :internal, :external or nil'
20
+ end
21
+ end
22
+
23
+ def filenames_per_href
24
+ require 'nokogiri'
25
+ filenames_per_href = {}
26
+ @filenames.each do |filename|
27
+ hrefs_in_file(filename).each do |href|
28
+ filenames_per_href[href] ||= Set.new
29
+ filenames_per_href[href] << filename
30
+ end
31
+ end
32
+ filenames_per_href
33
+ end
34
+
35
+ def external_href?(href)
36
+ !!(href =~ %r{^(\/\/|[a-z\-]+:)})
37
+ end
38
+
39
+ def hrefs_in_file(filename)
40
+ hrefs_in_file = Set.new
41
+ doc = Nokogiri::HTML(::File.read(filename))
42
+ doc.css('a').each { |e| hrefs_in_file << e[:href] unless e[:href].nil? }
43
+ doc.css('img').each { |e| hrefs_in_file << e[:src] }
44
+
45
+ # Convert protocol-relative urls
46
+ # e.g. //example.com => http://example.com
47
+ hrefs_in_file.map! { |href| href.gsub /^\/\//, 'http://' }
48
+
49
+ # Strip fragment
50
+ hrefs_in_file.map! { |href| href.gsub(/#.*$/, '') }
51
+
52
+ hrefs_in_file.select(&@filter)
53
+ end
54
+
55
+ end
56
+
57
+ end
@@ -49,7 +49,7 @@ module Nanoc::Extra
49
49
  end
50
50
 
51
51
  # Remove empty directories
52
- present_dirs.sort_by{ |d| -d.length }.each do |dir|
52
+ present_dirs.each do |dir|
53
53
  next if Dir.foreach(dir) { |n| break true if n !~ /\A\.\.?\z/ }
54
54
  next if filename_excluded?(dir)
55
55
  self.delete_dir(dir)
@@ -2,276 +2,19 @@
2
2
 
3
3
  module Nanoc::Extra::Validators
4
4
 
5
- # A validator that verifies that all links (`<a href="…">…</a>`) point to a
6
- # location that exists.
5
+ # @deprecated Use the Checking API or the `check` command instead
7
6
  class Links
8
7
 
9
- # @param [String] dir The directory that will be searched for HTML files
10
- # to validate
11
- #
12
- # @param [Array<String>] index_filenames An array of index filenames that
13
- # will be appended to URLs by web servers if a directory is requested
14
- # instead of a file
15
- #
16
- # @option params [Boolean] :internal (false) True if internal links should
17
- # be checked; false if they should not
18
- #
19
- # @option params [Boolean] :external (false) True if external links should
20
- # be checked; false if they should not
21
8
  def initialize(dir, index_filenames, params={})
22
- @dir = dir
23
- @index_filenames = index_filenames
24
9
  @include_internal = params.has_key?(:internal) && params[:internal]
25
10
  @include_external = params.has_key?(:external) && params[:external]
26
11
  end
27
12
 
28
- # Starts the validator. The results will be printed to stdout.
29
- #
30
- # @return [void]
31
13
  def run
32
- require 'nokogiri'
33
-
34
- @delegate = self
35
- links = all_broken_hrefs
36
- if links.empty?
37
- puts "No broken links found!"
38
- else
39
- links.each_pair do |href, origins|
40
- puts "Broken link: #{href} -- referenced from:"
41
- origins.each do |origin|
42
- puts " #{origin}"
43
- end
44
- puts
45
- end
46
- end
47
- end
48
-
49
- private
50
-
51
- # Enumerates all key-value pairs of a given hash in a thread-safe way.
52
- #
53
- # @api private
54
- class ThreadsafeHashEnumerator
55
-
56
- # Creates a new enumerator for the given hash.
57
- #
58
- # @param [Hash] hash The hash for which the enumerator should return
59
- # key-value pairs
60
- def initialize(hash)
61
- @hash = hash
62
- @unprocessed_keys = @hash.keys.dup
63
- @mutex = Mutex.new
64
- end
65
-
66
- # Returns the next key-value pair in the hash.
67
- #
68
- # @return [Array] An array containing the key and the corresponding
69
- # value of teh next key-value pair
70
- def next_pair
71
- @mutex.synchronize do
72
- key = @unprocessed_keys.shift
73
- return (key ? [ key, @hash[key] ] : nil)
74
- end
75
- end
76
-
77
- end
78
-
79
- def all_broken_hrefs
80
- broken_hrefs = {}
81
-
82
- internal_hrefs = {}
83
- external_hrefs = {}
84
-
85
- # Split into internal and external hrefs
86
- all_hrefs_per_filename.each_pair do |filename, hrefs|
87
- hrefs.each do |href|
88
- if is_external_href?(href)
89
- external_hrefs[href] ||= []
90
- external_hrefs[href] << filename
91
- else
92
- internal_hrefs[href] ||= []
93
- internal_hrefs[href] << filename
94
- end
95
- end
96
- end
97
-
98
- # Validate hrefs
99
- validate_internal_hrefs(internal_hrefs, broken_hrefs) if @include_internal
100
- validate_external_hrefs(external_hrefs, broken_hrefs) if @include_external
101
-
102
- # Done
103
- broken_hrefs
104
- end
105
-
106
- def all_files
107
- Dir[@dir + '/**/*.html']
108
- end
109
-
110
- def all_hrefs_per_filename
111
- hrefs = {}
112
- all_files.each do |filename|
113
- hrefs[filename] ||= all_hrefs_in_file(filename)
114
- end
115
- hrefs
116
- end
117
-
118
- def all_hrefs_in_file(filename)
119
- doc = Nokogiri::HTML(::File.read(filename))
120
- doc.css('a').map { |l| l[:href] }.compact
121
- end
122
-
123
- def is_external_href?(href)
124
- !!(href =~ %r{^[a-z\-]+:})
125
- end
126
-
127
- def is_valid_internal_href?(href, origin)
128
- # Skip hrefs that point to self
129
- # FIXME this is ugly and won’t always be correct
130
- return true if href == '.'
131
-
132
- # Remove target
133
- path = href.sub(/#.*$/, '')
134
- return true if path.empty?
135
-
136
- # Make absolute
137
- if path[0, 1] == '/'
138
- path = @dir + path
139
- else
140
- path = ::File.expand_path(path, ::File.dirname(origin))
141
- end
142
-
143
- # Check whether file exists
144
- return true if File.file?(path)
145
-
146
- # Check whether directory with index file exists
147
- return true if File.directory?(path) && @index_filenames.any? { |fn| File.file?(File.join(path, fn)) }
148
-
149
- # Nope :(
150
- return false
151
- end
152
-
153
- def is_valid_external_href?(href)
154
- require 'net/http'
155
- require 'uri'
156
-
157
- # Parse
158
- uri = nil
159
- begin
160
- uri = URI.parse(href)
161
- rescue URI::InvalidURIError
162
- @delegate && @delegate.send(:external_href_validated, href, false)
163
- return false
164
- end
165
-
166
- # Skip non-HTTP URLs
167
- return true if uri.scheme !~ /^https?$/
168
-
169
- # Get status
170
- status = fetch_http_status_for(uri)
171
- is_valid = !status.nil? && status == 200
172
-
173
- # Notify
174
- @delegate && @delegate.send(:external_href_validated, href, is_valid)
175
-
176
- # Done
177
- is_valid
178
- end
179
-
180
- def validate_internal_hrefs(hrefs, broken_hrefs)
181
- hrefs.each_pair do |href, filenames|
182
- filenames.each do |filename|
183
- if !is_valid_internal_href?(href, filename)
184
- broken_hrefs[href] = filenames
185
- end
186
- end
187
- end
188
- end
189
-
190
- def validate_external_hrefs(hrefs, broken_hrefs)
191
- @mutex = Mutex.new
192
-
193
- enum = ThreadsafeHashEnumerator.new(hrefs)
194
-
195
- threads = []
196
- 10.times do
197
- threads << Thread.new do
198
- loop do
199
- # Get next pair
200
- pair = enum.next_pair
201
- break if pair.nil?
202
- href, filenames = pair[0], pair[1]
203
-
204
- # Validate
205
- if !is_valid_external_href?(href)
206
- @mutex.synchronize do
207
- broken_hrefs[href] = filenames
208
- end
209
- end
210
- end
211
- end
212
- end
213
- threads.each { |t| t.join }
214
- end
215
-
216
- def fetch_http_status_for(url, params={})
217
- 5.times do |i|
218
- begin
219
- res = nil
220
- Timeout::timeout(10) do
221
- res = request_url_once(url)
222
- end
223
-
224
- if res.code =~ /^3..$/
225
- return nil if i == 5
226
-
227
- # Find proper location
228
- location = res['Location']
229
- if location !~ /^https?:\/\//
230
- base_url = url.dup
231
- base_url.path = (location =~ /^\// ? '' : '/')
232
- base_url.query = nil
233
- base_url.fragment = nil
234
- location = base_url.to_s + location
235
- end
236
-
237
- url = URI.parse(location)
238
- else
239
- return res.code.to_i
240
- end
241
- rescue
242
- return nil
243
- end
244
- end
245
- end
246
-
247
- def request_url_once(url)
248
- require 'net/https'
249
-
250
- path = (url.path.nil? || url.path.empty? ? '/' : url.path)
251
- req = Net::HTTP::Head.new(path)
252
- http = Net::HTTP.new(url.host, url.port)
253
- if url.instance_of? URI::HTTPS
254
- http.use_ssl = true
255
- http.verify_mode = OpenSSL::SSL::VERIFY_NONE
256
- end
257
- res = http.request(req)
258
- end
259
-
260
- def external_href_validated(href, is_valid)
261
- texts = {
262
- true => 'ok',
263
- false => ' ERROR '
264
- }
265
-
266
- colors = {
267
- true => "\e[32m",
268
- false => "\e[41m\e[37m",
269
- :off => "\033[0m"
270
- }
271
-
272
- @mutex.synchronize do
273
- puts href + ': ' + colors[is_valid] + texts[is_valid] + colors[:off]
274
- end
14
+ checks = []
15
+ checks << 'ilinks' if options[:internal]
16
+ checks << 'elinks' if options[:external]
17
+ Nanoc::CLI.run [ 'check', checks ].flatten
275
18
  end
276
19
 
277
20
  end