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.
- checksums.yaml +7 -0
- data/NEWS.md +5 -0
- data/README.md +5 -0
- data/lib/nanoc-checking.rb +3 -0
- data/lib/nanoc/checking.rb +26 -0
- data/lib/nanoc/checking/check.rb +95 -0
- data/lib/nanoc/checking/checks.rb +18 -0
- data/lib/nanoc/checking/checks/css.rb +20 -0
- data/lib/nanoc/checking/checks/external_links.rb +156 -0
- data/lib/nanoc/checking/checks/html.rb +20 -0
- data/lib/nanoc/checking/checks/internal_links.rb +99 -0
- data/lib/nanoc/checking/checks/mixed_content.rb +41 -0
- data/lib/nanoc/checking/checks/stale.rb +45 -0
- data/lib/nanoc/checking/checks/w3c_validator.rb +35 -0
- data/lib/nanoc/checking/command_runners.rb +11 -0
- data/lib/nanoc/checking/command_runners/check.rb +35 -0
- data/lib/nanoc/checking/commands/check.rb +13 -0
- data/lib/nanoc/checking/dsl.rb +29 -0
- data/lib/nanoc/checking/issue.rb +18 -0
- data/lib/nanoc/checking/loader.rb +52 -0
- data/lib/nanoc/checking/runner.rb +138 -0
- data/lib/nanoc/checking/version.rb +7 -0
- metadata +103 -0
checksums.yaml
ADDED
@@ -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
data/README.md
ADDED
@@ -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,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
|
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: []
|