nanoc 3.4.3 → 3.5.0b1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. data/CONTRIBUTING.md +25 -0
  2. data/Gemfile +3 -1
  3. data/Gemfile.lock +22 -13
  4. data/NEWS.md +27 -0
  5. data/README.md +3 -1
  6. data/lib/nanoc.rb +2 -2
  7. data/lib/nanoc/base/compilation/compiler_dsl.rb +19 -0
  8. data/lib/nanoc/base/core_ext/array.rb +18 -8
  9. data/lib/nanoc/base/core_ext/hash.rb +18 -8
  10. data/lib/nanoc/base/core_ext/pathname.rb +2 -8
  11. data/lib/nanoc/base/plugin_registry.rb +57 -19
  12. data/lib/nanoc/base/result_data/item_rep.rb +3 -15
  13. data/lib/nanoc/base/source_data/item.rb +1 -1
  14. data/lib/nanoc/base/source_data/layout.rb +1 -1
  15. data/lib/nanoc/base/source_data/site.rb +15 -19
  16. data/lib/nanoc/cli.rb +28 -23
  17. data/lib/nanoc/cli/command_runner.rb +4 -0
  18. data/lib/nanoc/cli/commands/autocompile.rb +15 -6
  19. data/lib/nanoc/cli/commands/check.rb +47 -0
  20. data/lib/nanoc/cli/commands/compile.rb +271 -195
  21. data/lib/nanoc/cli/commands/create-site.rb +5 -5
  22. data/lib/nanoc/cli/commands/deploy.rb +16 -4
  23. data/lib/nanoc/cli/commands/prune.rb +3 -3
  24. data/lib/nanoc/cli/commands/show-data.rb +73 -58
  25. data/lib/nanoc/cli/commands/show-rules.rb +1 -1
  26. data/lib/nanoc/cli/commands/validate-css.rb +2 -3
  27. data/lib/nanoc/cli/commands/validate-html.rb +2 -3
  28. data/lib/nanoc/cli/commands/validate-links.rb +5 -11
  29. data/lib/nanoc/cli/commands/view.rb +1 -1
  30. data/lib/nanoc/cli/commands/watch.rb +38 -20
  31. data/lib/nanoc/cli/error_handler.rb +122 -122
  32. data/lib/nanoc/data_sources.rb +2 -0
  33. data/lib/nanoc/data_sources/filesystem_unified.rb +1 -1
  34. data/lib/nanoc/data_sources/filesystem_verbose.rb +1 -1
  35. data/lib/nanoc/data_sources/static.rb +60 -0
  36. data/lib/nanoc/extra.rb +2 -0
  37. data/lib/nanoc/extra/checking.rb +16 -0
  38. data/lib/nanoc/extra/checking/check.rb +33 -0
  39. data/lib/nanoc/extra/checking/checks.rb +19 -0
  40. data/lib/nanoc/extra/checking/checks/css.rb +23 -0
  41. data/lib/nanoc/extra/checking/checks/external_links.rb +149 -0
  42. data/lib/nanoc/extra/checking/checks/html.rb +24 -0
  43. data/lib/nanoc/extra/checking/checks/internal_links.rb +57 -0
  44. data/lib/nanoc/extra/checking/checks/stale.rb +23 -0
  45. data/lib/nanoc/extra/checking/dsl.rb +31 -0
  46. data/lib/nanoc/extra/checking/issue.rb +19 -0
  47. data/lib/nanoc/extra/checking/runner.rb +130 -0
  48. data/lib/nanoc/extra/link_collector.rb +57 -0
  49. data/lib/nanoc/extra/pruner.rb +1 -1
  50. data/lib/nanoc/extra/validators/links.rb +5 -262
  51. data/lib/nanoc/extra/validators/w3c.rb +8 -76
  52. data/lib/nanoc/filters/colorize_syntax.rb +1 -1
  53. data/lib/nanoc/filters/handlebars.rb +2 -2
  54. data/lib/nanoc/filters/less.rb +1 -1
  55. data/lib/nanoc/filters/redcarpet.rb +1 -1
  56. data/lib/nanoc/filters/rubypants.rb +1 -1
  57. data/lib/nanoc/filters/slim.rb +1 -1
  58. data/lib/nanoc/helpers/blogging.rb +163 -104
  59. data/lib/nanoc/helpers/xml_sitemap.rb +9 -3
  60. data/tasks/doc.rake +2 -1
  61. data/test/base/core_ext/array_spec.rb +4 -4
  62. data/test/base/core_ext/hash_spec.rb +7 -7
  63. data/test/base/core_ext/pathname_spec.rb +12 -2
  64. data/test/base/test_compiler_dsl.rb +24 -0
  65. data/test/base/test_item_rep.rb +58 -26
  66. data/test/cli/commands/test_watch.rb +0 -8
  67. data/test/data_sources/test_static.rb +66 -0
  68. data/test/extra/checking/checks/test_css.rb +40 -0
  69. data/test/extra/checking/checks/test_external_links.rb +76 -0
  70. data/test/extra/checking/checks/test_html.rb +40 -0
  71. data/test/extra/checking/checks/test_internal_links.rb +46 -0
  72. data/test/extra/checking/checks/test_stale.rb +49 -0
  73. data/test/extra/checking/test_check.rb +16 -0
  74. data/test/extra/checking/test_dsl.rb +20 -0
  75. data/test/extra/checking/test_runner.rb +15 -0
  76. data/test/extra/test_link_collector.rb +93 -0
  77. data/test/extra/validators/test_links.rb +0 -64
  78. data/test/extra/validators/test_w3c.rb +20 -26
  79. data/test/filters/test_colorize_syntax.rb +15 -0
  80. data/test/filters/test_less.rb +14 -0
  81. data/test/filters/test_pandoc.rb +5 -1
  82. data/test/helpers/test_blogging.rb +52 -8
  83. data/test/helpers/test_xml_sitemap.rb +68 -0
  84. data/test/test_gem.rb +1 -1
  85. metadata +31 -6
@@ -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