nanoc-checking 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 86f973e7f2f708f6de4ae3fb3459f0cfd7f3a96e0cd44f0e742ebea4896d6c22
4
+ data.tar.gz: ec723179cd5cb4c89082367f9f96bb68779d302230ed7b99a689ec3706e4c48f
5
+ SHA512:
6
+ metadata.gz: 63d9e7bc90d4310c80aee3fe27fbdb760847823704986fef39d20e5875f97c91289c88cc9b2493f29c386ba947a1be1de2741e4cf016fc137fa74838205819d3
7
+ data.tar.gz: b74f036990496a6cf571398fb00e284c368292097dd94bf0fde6c5a03dd4d9646407ee93d38d07b875fd6435e298059c7789c7e1335d3710f5a159715de01d39
data/NEWS.md ADDED
@@ -0,0 +1,5 @@
1
+ # nanoc-checking news
2
+
3
+ ## 1.0.0 (2020-03-07)
4
+
5
+ Initial release (extracted from nanoc)
@@ -0,0 +1,5 @@
1
+ # nanoc-checking
2
+
3
+ This provides the `check` command and associated functionality for [Nanoc](https://nanoc.ws).
4
+
5
+ For details, see the [Checking correctness of Nanoc sites](https://nanoc.ws/doc/testing/) chapter of the Nanoc documentation.
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'nanoc/checking'
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'nanoc-core'
4
+ require 'nanoc-cli'
5
+
6
+ module Nanoc
7
+ module Checking
8
+ end
9
+ end
10
+
11
+ require_relative 'checking/version'
12
+ require_relative 'checking/check'
13
+ require_relative 'checking/checks'
14
+ require_relative 'checking/command_runners'
15
+ require_relative 'checking/dsl'
16
+ require_relative 'checking/runner'
17
+ require_relative 'checking/loader'
18
+ require_relative 'checking/issue'
19
+
20
+ root = File.dirname(__FILE__)
21
+ checking_command_path = File.join(root, 'checking', 'commands', 'check.rb')
22
+ check_command = Cri::Command.load_file(checking_command_path, infer_name: true)
23
+
24
+ Nanoc::CLI.after_setup do
25
+ Nanoc::CLI.add_command(check_command)
26
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nanoc
4
+ module Checking
5
+ # @api private
6
+ class OutputDirNotFoundError < ::Nanoc::Core::Error
7
+ def initialize(directory_path)
8
+ super("Unable to run check against output directory at “#{directory_path}”: directory does not exist.")
9
+ end
10
+ end
11
+
12
+ # @api private
13
+ class Check < Nanoc::Core::Context
14
+ extend DDPlugin::Plugin
15
+
16
+ DDMemoize.activate(self)
17
+
18
+ attr_reader :issues
19
+
20
+ def self.define(ident, &block)
21
+ klass = Class.new(self) { identifier(ident) }
22
+ klass.send(:define_method, :run) do
23
+ instance_exec(&block)
24
+ end
25
+ end
26
+
27
+ def self.create(site)
28
+ output_dir = site.config.output_dir
29
+ unless File.exist?(output_dir)
30
+ raise Nanoc::Checking::OutputDirNotFoundError.new(output_dir)
31
+ end
32
+
33
+ output_filenames = Dir[output_dir + '/**/*'].select { |f| File.file?(f) }
34
+
35
+ # FIXME: ugly
36
+ compiler = Nanoc::Core::Compiler.new_for(site)
37
+ res = compiler.run_until_reps_built
38
+ reps = res.fetch(:reps)
39
+ view_context =
40
+ Nanoc::Core::ViewContextForShell.new(
41
+ items: site.items,
42
+ reps: reps,
43
+ )
44
+
45
+ context = {
46
+ items: Nanoc::Core::PostCompileItemCollectionView.new(site.items, view_context),
47
+ layouts: Nanoc::Core::LayoutCollectionView.new(site.layouts, view_context),
48
+ config: Nanoc::Core::ConfigView.new(site.config, view_context),
49
+ output_filenames: output_filenames,
50
+ }
51
+
52
+ new(context)
53
+ end
54
+
55
+ def initialize(context)
56
+ super(context)
57
+
58
+ @issues = Set.new
59
+ end
60
+
61
+ def run
62
+ raise NotImplementedError.new('Nanoc::Checking::Check subclasses must implement #run')
63
+ end
64
+
65
+ def add_issue(desc, subject: nil)
66
+ # Simplify subject
67
+ # FIXME: do not depend on working directory
68
+ if subject&.start_with?(Dir.getwd)
69
+ subject = subject[(Dir.getwd.size + 1)..subject.size]
70
+ end
71
+
72
+ @issues << Issue.new(desc, subject, self.class)
73
+ end
74
+
75
+ # @private
76
+ def output_filenames
77
+ super.reject { |f| excluded_patterns.any? { |pat| pat.match?(f) } }
78
+ end
79
+
80
+ # @private
81
+ memoized def excluded_patterns
82
+ @config
83
+ .fetch(:checks, {})
84
+ .fetch(:all, {})
85
+ .fetch(:exclude_files, [])
86
+ .map { |pattern| Regexp.new(pattern) }
87
+ end
88
+
89
+ # @private
90
+ def output_html_filenames
91
+ output_filenames.select { |f| File.extname(f) =~ /\A\.x?html?\z/ }
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api private
4
+ module Nanoc
5
+ module Checking
6
+ module Checks
7
+ end
8
+ end
9
+ end
10
+
11
+ require_relative 'checks/w3c_validator'
12
+
13
+ require_relative 'checks/css'
14
+ require_relative 'checks/external_links'
15
+ require_relative 'checks/html'
16
+ require_relative 'checks/internal_links'
17
+ require_relative 'checks/mixed_content'
18
+ require_relative 'checks/stale'
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nanoc
4
+ module Checking
5
+ module Checks
6
+ # @api private
7
+ class CSS < ::Nanoc::Checking::Checks::W3CValidator
8
+ identifier :css
9
+
10
+ def extension
11
+ 'css'
12
+ end
13
+
14
+ def validator_class
15
+ ::W3CValidators::CSSValidator
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nanoc
4
+ module Checking
5
+ module Checks
6
+ # A validator that verifies that all external links point to a location that exists.
7
+ #
8
+ # @api private
9
+ class ExternalLinks < ::Nanoc::Checking::Check
10
+ identifiers :external_links, :elinks
11
+
12
+ def run
13
+ # Find all broken external hrefs
14
+ # TODO: de-duplicate this (duplicated in internal links check)
15
+ filenames = output_html_filenames.reject { |f| excluded_file?(f) }
16
+ hrefs_with_filenames = ::Nanoc::Extra::LinkCollector.new(filenames, :external).filenames_per_href
17
+ results = select_invalid(hrefs_with_filenames.keys.shuffle)
18
+
19
+ # Report them
20
+ results.each do |res|
21
+ filenames = hrefs_with_filenames[res.href]
22
+ filenames.each do |filename|
23
+ add_issue(
24
+ "broken reference to #{res.href}: #{res.explanation}",
25
+ subject: filename,
26
+ )
27
+ end
28
+ end
29
+ end
30
+
31
+ class Result
32
+ attr_reader :href
33
+ attr_reader :explanation
34
+
35
+ def initialize(href, explanation)
36
+ @href = href
37
+ @explanation = explanation
38
+ end
39
+ end
40
+
41
+ def select_invalid(hrefs)
42
+ ::Parallel.map(hrefs, in_threads: 10) { |href| validate(href) }.compact
43
+ end
44
+
45
+ def validate(href)
46
+ # Parse
47
+ url = nil
48
+ begin
49
+ url = URI.parse(href)
50
+ rescue URI::Error
51
+ return Result.new(href, 'invalid URI')
52
+ end
53
+
54
+ # Skip excluded URLs
55
+ return nil if excluded?(href)
56
+
57
+ # Skip non-HTTP URLs
58
+ return nil if url.scheme !~ /^https?$/
59
+
60
+ # Get status
61
+ res = nil
62
+ last_err = nil
63
+ timeouts = [3, 5, 10, 30, 60]
64
+ 5.times do |i|
65
+ begin
66
+ Timeout.timeout(timeouts[i]) do
67
+ res = request_url_once(url)
68
+ end
69
+ rescue => e
70
+ last_err = e
71
+ next
72
+ end
73
+
74
+ if /^3..$/.match?(res.code)
75
+ if i == 4
76
+ return Result.new(href, 'too many redirects')
77
+ end
78
+
79
+ location = extract_location(res, url)
80
+ return Result.new(href, 'redirection without a target location') if location.nil?
81
+
82
+ if /^30[18]$/.match?(res.code)
83
+ return Result.new(href, "link has moved permanently to '#{location}'")
84
+ end
85
+
86
+ url = URI.parse(location)
87
+ elsif res.code == '200'
88
+ return nil
89
+ else
90
+ return Result.new(href, res.code)
91
+ end
92
+ end
93
+
94
+ if last_err
95
+ Result.new(href, last_err.message)
96
+ else
97
+ raise Nanoc::Core::Errors::InternalInconsistency, 'last_err cannot be nil'
98
+ end
99
+ end
100
+
101
+ def extract_location(res, url)
102
+ location = res['Location']
103
+
104
+ case location
105
+ when nil
106
+ nil
107
+ when /^https?:\/\//
108
+ location
109
+ else
110
+ base_url = url.dup
111
+ base_url.path = (/^\//.match?(location) ? '' : '/')
112
+ base_url.query = nil
113
+ base_url.fragment = nil
114
+ base_url.to_s + location
115
+ end
116
+ end
117
+
118
+ def path_for_url(url)
119
+ path =
120
+ if url.path.nil? || url.path.empty?
121
+ '/'
122
+ else
123
+ url.path
124
+ end
125
+
126
+ if url.query
127
+ path = path + '?' + url.query
128
+ end
129
+
130
+ path
131
+ end
132
+
133
+ def request_url_once(url)
134
+ req = Net::HTTP::Get.new(path_for_url(url))
135
+ req['User-Agent'] = "Mozilla/5.0 Nanoc/#{Nanoc::VERSION} (link rot checker)"
136
+ http = Net::HTTP.new(url.host, url.port)
137
+ if url.instance_of? URI::HTTPS
138
+ http.use_ssl = true
139
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
140
+ end
141
+ http.request(req)
142
+ end
143
+
144
+ def excluded?(href)
145
+ excludes = @config.fetch(:checks, {}).fetch(:external_links, {}).fetch(:exclude, [])
146
+ excludes.any? { |pattern| Regexp.new(pattern).match(href) }
147
+ end
148
+
149
+ def excluded_file?(file)
150
+ excludes = @config.fetch(:checks, {}).fetch(:external_links, {}).fetch(:exclude_files, [])
151
+ excludes.any? { |pattern| Regexp.new(pattern).match(file) }
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nanoc
4
+ module Checking
5
+ module Checks
6
+ # @api private
7
+ class HTML < ::Nanoc::Checking::Checks::W3CValidator
8
+ identifier :html
9
+
10
+ def extension
11
+ '{htm,html}'
12
+ end
13
+
14
+ def validator_class
15
+ ::W3CValidators::NuValidator
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nanoc
4
+ module Checking
5
+ module Checks
6
+ # A check that verifies that all internal links point to a location that exists.
7
+ #
8
+ # @api private
9
+ class InternalLinks < ::Nanoc::Checking::Check
10
+ identifiers :internal_links, :ilinks
11
+
12
+ # Starts the validator. The results will be printed to stdout.
13
+ #
14
+ # Internal links that match a regexp pattern in `@config[:checks][:internal_links][:exclude]` will
15
+ # be skipped.
16
+ #
17
+ # @return [void]
18
+ def run
19
+ # TODO: de-duplicate this (duplicated in external links check)
20
+ filenames = output_html_filenames
21
+ uris = ::Nanoc::Extra::LinkCollector.new(filenames, :internal).filenames_per_href
22
+
23
+ uris.each_pair do |href, fns|
24
+ fns.each do |filename|
25
+ next if valid?(href, filename)
26
+
27
+ add_issue(
28
+ "broken reference to #{href}",
29
+ subject: filename,
30
+ )
31
+ end
32
+ end
33
+ end
34
+
35
+ protected
36
+
37
+ def valid?(href, origin)
38
+ # Skip hrefs that point to self
39
+ # FIXME: this is ugly and won’t always be correct
40
+ return true if href == '.'
41
+
42
+ # Turn file: into output_dir-as-root relative paths
43
+
44
+ output_dir = @config.output_dir
45
+ output_dir += '/' unless output_dir.end_with?('/')
46
+ base_uri = URI("file://#{output_dir}")
47
+ path = href.sub(/#{base_uri}/, '').sub(/file:\/{1,3}/, '')
48
+
49
+ path = "/#{path}" unless path.start_with?('/')
50
+
51
+ # Skip hrefs that are specified in the exclude configuration
52
+ return true if excluded?(path, origin)
53
+
54
+ # Make an absolute path
55
+ path = ::File.join(output_dir, path[1..path.length])
56
+
57
+ # Remove fragment
58
+ path = path.sub(/#.*$/, '')
59
+ return true if path.empty?
60
+
61
+ # Remove query string
62
+ path = path.sub(/\?.*$/, '')
63
+ return true if path.empty?
64
+
65
+ # Decode URL (e.g. '%20' -> ' ')
66
+ path = CGI.unescape(path)
67
+
68
+ # Check whether file exists
69
+ return true if File.file?(path)
70
+
71
+ # Check whether directory with index file exists
72
+ return true if File.directory?(path) && @config[:index_filenames].any? { |fn| File.file?(File.join(path, fn)) }
73
+
74
+ # Nope :(
75
+ false
76
+ end
77
+
78
+ def excluded?(href, origin)
79
+ config = @config.fetch(:checks, {}).fetch(:internal_links, {})
80
+ excluded_target?(href, config) || excluded_origin?(origin, config)
81
+ end
82
+
83
+ def excluded_target?(href, config)
84
+ excludes = config.fetch(:exclude_targets, config.fetch(:exclude, []))
85
+ excludes.any? { |pattern| Regexp.new(pattern).match(href) }
86
+ end
87
+
88
+ def excluded_origin?(origin, config)
89
+ # FIXME: do not depend on current working directory
90
+ origin = File.absolute_path(origin)
91
+
92
+ relative_origin = origin[@config.output_dir.size..-1]
93
+ excludes = config.fetch(:exclude_origins, [])
94
+ excludes.any? { |pattern| Regexp.new(pattern).match(relative_origin) }
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nanoc
4
+ module Checking
5
+ module Checks
6
+ # A check that verifies HTML files do not reference external resources with
7
+ # URLs that would trigger "mixed content" warnings.
8
+ #
9
+ # @api private
10
+ class MixedContent < ::Nanoc::Checking::Check
11
+ identifier :mixed_content
12
+
13
+ PROTOCOL_PATTERN = /^(\w+):\/\//.freeze
14
+
15
+ def run
16
+ filenames = output_html_filenames
17
+ resource_uris_with_filenames = ::Nanoc::Extra::LinkCollector.new(filenames).filenames_per_resource_uri
18
+
19
+ resource_uris_with_filenames.each_pair do |uri, fns|
20
+ next unless guaranteed_insecure?(uri)
21
+
22
+ fns.each do |filename|
23
+ add_issue(
24
+ "mixed content include: #{uri}",
25
+ subject: filename,
26
+ )
27
+ end
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def guaranteed_insecure?(href)
34
+ protocol = PROTOCOL_PATTERN.match(href)
35
+
36
+ protocol && protocol[1].casecmp('http').zero?
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nanoc
4
+ module Checking
5
+ module Checks
6
+ # @api private
7
+ class Stale < ::Nanoc::Checking::Check
8
+ identifier :stale
9
+
10
+ def run
11
+ output_filenames.each do |f|
12
+ next if pruner.filename_excluded?(f)
13
+ next if item_rep_paths.include?(f)
14
+
15
+ add_issue(
16
+ 'file without matching item',
17
+ subject: f,
18
+ )
19
+ end
20
+ end
21
+
22
+ protected
23
+
24
+ def item_rep_paths
25
+ @item_rep_paths ||=
26
+ Set.new(
27
+ @items
28
+ .flat_map(&:reps)
29
+ .map(&:_unwrap)
30
+ .flat_map(&:raw_paths)
31
+ .flat_map(&:values)
32
+ .flatten,
33
+ )
34
+ end
35
+
36
+ def pruner
37
+ exclude_config = @config.fetch(:prune, {}).fetch(:exclude, [])
38
+ # FIXME: specifying reps this way is icky
39
+ reps = Nanoc::Core::ItemRepRepo.new
40
+ @pruner ||= Nanoc::Core::Pruner.new(@config._unwrap, reps, exclude: exclude_config)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nanoc
4
+ module Checking
5
+ module Checks
6
+ # @api private
7
+ class W3CValidator < ::Nanoc::Checking::Check
8
+ def run
9
+ require 'w3c_validators'
10
+ require 'resolv-replace'
11
+
12
+ Dir[@config.output_dir + '/**/*.' + extension].each do |filename|
13
+ results = validator_class.new.validate_file(filename)
14
+ lines = File.readlines(filename)
15
+ results.errors.each do |e|
16
+ line_num = e.line.to_i - 1
17
+ line = lines[line_num]
18
+ message = e.message.gsub(%r{\s+}, ' ').strip.sub(/\s+:$/, '')
19
+ desc = "line #{line_num + 1}: #{message}: #{line}"
20
+ add_issue(desc, subject: filename)
21
+ end
22
+ end
23
+ end
24
+
25
+ def extension
26
+ raise NotImplementedError
27
+ end
28
+
29
+ def validator_class
30
+ raise NotImplementedError
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api private
4
+ module Nanoc
5
+ module Checking
6
+ module CommandRunners
7
+ end
8
+ end
9
+ end
10
+
11
+ require_relative 'command_runners/check'
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nanoc
4
+ module Checking
5
+ module CommandRunners
6
+ class Check < ::Nanoc::CLI::CommandRunner
7
+ def run
8
+ site = load_site
9
+
10
+ runner = Nanoc::Checking::Runner.new(site)
11
+
12
+ if options[:list]
13
+ runner.list_checks
14
+ return
15
+ end
16
+
17
+ success =
18
+ if options[:all]
19
+ runner.run_all
20
+ elsif options[:deploy]
21
+ runner.run_for_deploy
22
+ elsif arguments.any?
23
+ runner.run_specific(arguments)
24
+ else
25
+ runner.run_for_deploy
26
+ end
27
+
28
+ unless success
29
+ raise Nanoc::Core::TrivialError, 'One or more checks failed'
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ usage 'check [options] [names]'
4
+ summary 'run issue checks'
5
+ description "
6
+ Run issue checks on the current site. If the `--all` option is passed, all available issue checks will be run. By default, the issue checks marked for deployment will be run.
7
+ "
8
+
9
+ flag :a, :all, 'run all checks'
10
+ flag :L, :list, 'list all checks'
11
+ flag :d, :deploy, '(deprecated)'
12
+
13
+ runner Nanoc::Checking::CommandRunners::Check
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nanoc
4
+ module Checking
5
+ # @api private
6
+ class DSL
7
+ def self.from_file(filename, enabled_checks:)
8
+ dsl = new(enabled_checks: enabled_checks)
9
+ absolute_filename = File.expand_path(filename)
10
+ dsl.instance_eval(File.read(filename), absolute_filename)
11
+ dsl
12
+ end
13
+
14
+ def initialize(enabled_checks:)
15
+ @enabled_checks = enabled_checks
16
+ end
17
+
18
+ def check(identifier, &block)
19
+ klass = Class.new(::Nanoc::Checking::Check)
20
+ klass.send(:define_method, :run, &block)
21
+ klass.send(:identifier, identifier)
22
+ end
23
+
24
+ def deploy_check(*identifiers)
25
+ identifiers.each { |i| @enabled_checks << i }
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nanoc
4
+ module Checking
5
+ # @api private
6
+ class Issue
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
+ end
17
+ end
18
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nanoc
4
+ module Checking
5
+ # @api private
6
+ class Loader
7
+ CHECKS_FILENAMES = ['Checks', 'Checks.rb', 'checks', 'checks.rb'].freeze
8
+
9
+ def initialize(config:)
10
+ @config = config
11
+ end
12
+
13
+ def run
14
+ dsl
15
+ end
16
+
17
+ def enabled_checks
18
+ (enabled_checks_from_dsl + enabled_checks_from_config).uniq
19
+ end
20
+
21
+ private
22
+
23
+ def dsl_present?
24
+ checks_filename && File.file?(checks_filename)
25
+ end
26
+
27
+ def enabled_checks_from_dsl
28
+ dsl
29
+ @enabled_checks_from_dsl
30
+ end
31
+
32
+ def enabled_checks_from_config
33
+ @config.fetch(:checking, {}).fetch(:enabled_checks, []).map(&:to_sym)
34
+ end
35
+
36
+ def dsl
37
+ @enabled_checks_from_dsl ||= []
38
+
39
+ @dsl ||=
40
+ if dsl_present?
41
+ Nanoc::Checking::DSL.from_file(checks_filename, enabled_checks: @enabled_checks_from_dsl)
42
+ else
43
+ Nanoc::Checking::DSL.new(enabled_checks: @enabled_checks_from_dsl)
44
+ end
45
+ end
46
+
47
+ def checks_filename
48
+ @_checks_filename ||= CHECKS_FILENAMES.find { |f| File.file?(f) }
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nanoc
4
+ module Checking
5
+ # Runner is reponsible for running issue checks.
6
+ #
7
+ # @api private
8
+ class Runner
9
+ # @param [Nanoc::Core::Site] site The Nanoc site this runner is for
10
+ def initialize(site)
11
+ @site = site
12
+ end
13
+
14
+ def any_enabled_checks?
15
+ enabled_checks.any?
16
+ end
17
+
18
+ # Lists all available checks on stdout.
19
+ #
20
+ # @return [void]
21
+ def list_checks
22
+ load_all
23
+
24
+ puts 'Available checks:'
25
+ puts
26
+ puts all_check_classes.map { |i| ' ' + i.identifier.to_s }.sort.join("\n")
27
+ end
28
+
29
+ # Runs all checks.
30
+ #
31
+ # @return [Boolean] true if successful, false otherwise
32
+ def run_all
33
+ load_all
34
+ run_check_classes(all_check_classes)
35
+ end
36
+
37
+ # Runs the checks marked for deployment.
38
+ #
39
+ # @return [Boolean] true if successful, false otherwise
40
+ def run_for_deploy
41
+ # TODO: rename to #run_enabled
42
+ load_all
43
+ run_check_classes(check_classes_named(enabled_checks))
44
+ end
45
+
46
+ # Runs the checks with the given names.
47
+ #
48
+ # @param [Array<Symbol>] check_class_names The names of the checks
49
+ #
50
+ # @return [Boolean] true if successful, false otherwise
51
+ def run_specific(check_class_names)
52
+ load_all
53
+ run_check_classes(check_classes_named(check_class_names))
54
+ end
55
+
56
+ private
57
+
58
+ def loader
59
+ @loader ||= Nanoc::Checking::Loader.new(config: @site.config)
60
+ end
61
+
62
+ def load_all
63
+ loader.run
64
+ end
65
+
66
+ def enabled_checks
67
+ loader.enabled_checks
68
+ end
69
+
70
+ def run_check_classes(classes)
71
+ issues = run_checks(classes)
72
+ print_issues(issues)
73
+ issues.empty? ? true : false
74
+ end
75
+
76
+ def all_check_classes
77
+ Nanoc::Checking::Check.all
78
+ end
79
+
80
+ def check_classes_named(names)
81
+ names.map do |name|
82
+ name = name.to_s.tr('-', '_').to_sym
83
+ klass = Nanoc::Checking::Check.named(name)
84
+ raise Nanoc::Core::TrivialError, "Unknown check: #{name}" if klass.nil?
85
+
86
+ klass
87
+ end
88
+ end
89
+
90
+ def run_checks(classes)
91
+ return [] if classes.empty?
92
+
93
+ # TODO: remove me
94
+ Nanoc::Core::Compiler.new_for(@site).run_until_reps_built
95
+
96
+ checks = []
97
+ issues = Set.new
98
+ length = classes.map { |c| c.identifier.to_s.length }.max + 18
99
+ classes.each do |klass|
100
+ print format(" %-#{length}s", "Running check #{klass.identifier}… ")
101
+
102
+ check = klass.create(@site)
103
+ check.run
104
+
105
+ checks << check
106
+ issues.merge(check.issues)
107
+
108
+ # TODO: report progress
109
+
110
+ puts check.issues.empty? ? 'ok'.green : 'error'.red
111
+ end
112
+ issues
113
+ end
114
+
115
+ def subject_to_s(str)
116
+ str || '(global)'
117
+ end
118
+
119
+ def print_issues(issues)
120
+ require 'colored'
121
+
122
+ return if issues.empty?
123
+
124
+ puts 'Issues found!'
125
+ issues.group_by(&:subject).to_a.sort_by { |s| subject_to_s(s.first) }.each do |pair|
126
+ subject = pair.first
127
+ issues = pair.last
128
+ next if issues.empty?
129
+
130
+ puts " #{subject_to_s(subject)}:"
131
+ issues.each do |i|
132
+ puts " [ #{'ERROR'.red} ] #{i.check_class.identifier} - #{i.description}"
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nanoc
4
+ module Checking
5
+ VERSION = '1.0.0'
6
+ end
7
+ end
metadata ADDED
@@ -0,0 +1,103 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: nanoc-checking
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Denis Defreyne
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-03-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: nanoc-cli
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '4.11'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 4.11.15
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '4.11'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 4.11.15
33
+ - !ruby/object:Gem::Dependency
34
+ name: nanoc-core
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '4.11'
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: 4.11.15
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - "~>"
48
+ - !ruby/object:Gem::Version
49
+ version: '4.11'
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: 4.11.15
53
+ description: Provides checking functionality for Nanoc
54
+ email: denis+rubygems@denis.ws
55
+ executables: []
56
+ extensions: []
57
+ extra_rdoc_files: []
58
+ files:
59
+ - NEWS.md
60
+ - README.md
61
+ - lib/nanoc-checking.rb
62
+ - lib/nanoc/checking.rb
63
+ - lib/nanoc/checking/check.rb
64
+ - lib/nanoc/checking/checks.rb
65
+ - lib/nanoc/checking/checks/css.rb
66
+ - lib/nanoc/checking/checks/external_links.rb
67
+ - lib/nanoc/checking/checks/html.rb
68
+ - lib/nanoc/checking/checks/internal_links.rb
69
+ - lib/nanoc/checking/checks/mixed_content.rb
70
+ - lib/nanoc/checking/checks/stale.rb
71
+ - lib/nanoc/checking/checks/w3c_validator.rb
72
+ - lib/nanoc/checking/command_runners.rb
73
+ - lib/nanoc/checking/command_runners/check.rb
74
+ - lib/nanoc/checking/commands/check.rb
75
+ - lib/nanoc/checking/dsl.rb
76
+ - lib/nanoc/checking/issue.rb
77
+ - lib/nanoc/checking/loader.rb
78
+ - lib/nanoc/checking/runner.rb
79
+ - lib/nanoc/checking/version.rb
80
+ homepage: https://nanoc.ws/
81
+ licenses:
82
+ - MIT
83
+ metadata: {}
84
+ post_install_message:
85
+ rdoc_options: []
86
+ require_paths:
87
+ - lib
88
+ required_ruby_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - "~>"
91
+ - !ruby/object:Gem::Version
92
+ version: '2.4'
93
+ required_rubygems_version: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ requirements: []
99
+ rubygems_version: 3.1.2
100
+ signing_key:
101
+ specification_version: 4
102
+ summary: Checking support for Nanoc
103
+ test_files: []