nanoc-checking 1.0.0

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.
@@ -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: []