nanoc-checking 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|