marcdouane 0.1.0 → 0.1.1
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 +4 -4
- data/README.md +20 -8
- data/Thorfile +71 -0
- data/bin/marcdouane +23 -12
- data/cucumber.yml +1 -0
- data/lib/marcdouane/file_checker.rb +31 -11
- data/lib/marcdouane/rules/consistent_header_style.rb +8 -11
- data/lib/marcdouane/rules/no_consecutive_blank_lines.rb +1 -1
- data/lib/marcdouane/rules/rule.rb +9 -8
- data/lib/marcdouane/rules/single_top_level_header.rb +26 -0
- data/lib/marcdouane/rules.rb +1 -5
- data/lib/marcdouane/version.rb +1 -1
- data/lib/marcdouane.rb +2 -12
- metadata +19 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1ae9e6bd5c948110c746ef7ef8091fb9858d2ef9358f80dacdd424573009906b
|
|
4
|
+
data.tar.gz: b1843497ea7c116a1d466f9b9368929db9d00390391740d45671d2fea3baee81
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 54cd432ee92e94694c9df60c3709bcdbeef5fcfbb0a8fb5ff35f4a84f91be2e3c0f2626c5128391e15525d978af61edbd775888401b82d93f5d4fcd32dd298c8
|
|
7
|
+
data.tar.gz: f89fc35dc945765eb1b54f4a3f864f2af3b827adb1ada8e5e9962c033d545776909a684a61b190dc100ff8436b142b0db0042e3f53384192ab21768223ab45ca
|
data/README.md
CHANGED
|
@@ -12,7 +12,8 @@ Install the gem and add to the application's Gemfile by executing:
|
|
|
12
12
|
bundle add marcdouane
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
If bundler is not being used to manage dependencies, install the gem
|
|
15
|
+
If bundler is not being used to manage dependencies, install the gem
|
|
16
|
+
by executing:
|
|
16
17
|
|
|
17
18
|
```bash
|
|
18
19
|
gem install marcdouane
|
|
@@ -50,9 +51,14 @@ I'm doing it
|
|
|
50
51
|
|
|
51
52
|
## Contributing
|
|
52
53
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
You can generate a new rule with `bundle exec thor generate_rule
|
|
55
|
+
YourRuleName` which will write the class file, require it and add a
|
|
56
|
+
new test-case for it.
|
|
57
|
+
|
|
58
|
+
A rule must inherit `Marcdouane::Rule`. It must answer to `check!`
|
|
59
|
+
and call `error!` to signal a faulty line. The message defaults to the
|
|
60
|
+
class's `ERROR_MESSAGE` but can be overriden through the `message`
|
|
61
|
+
parameter. The line-number is 0-indexed. Example:
|
|
56
62
|
|
|
57
63
|
```ruby
|
|
58
64
|
class CheckAnglois < Marcdouane::Rule
|
|
@@ -62,15 +68,21 @@ class CheckAnglois < Marcdouane::Rule
|
|
|
62
68
|
File
|
|
63
69
|
.read(file)
|
|
64
70
|
.lines
|
|
65
|
-
.each_with_index
|
|
66
|
-
|
|
71
|
+
.each_with_index do |line, index|
|
|
72
|
+
error!(index) if line =~ /anglois/
|
|
67
73
|
end
|
|
68
74
|
end
|
|
69
75
|
end
|
|
70
76
|
```
|
|
71
77
|
|
|
72
|
-
|
|
73
|
-
|
|
78
|
+
This will get the CLI to output:
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
<filename>:<line_number>: [CheckAnglois] Do not mention the English
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
All rules are expected to be tested via the extensive test suite in
|
|
85
|
+
`features/rules.features`, using Cucumber and Aruba like such:
|
|
74
86
|
|
|
75
87
|
```feature
|
|
76
88
|
Feature: Built-in Markdown Rules
|
data/Thorfile
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thor/group"
|
|
4
|
+
|
|
5
|
+
# Generates a new rule for Marcdouane, prompting an error message and
|
|
6
|
+
# filling in some tests too.
|
|
7
|
+
class GenerateRule < Thor::Group
|
|
8
|
+
include Thor::Actions
|
|
9
|
+
|
|
10
|
+
desc "Generate a new rule for Marcdouane"
|
|
11
|
+
|
|
12
|
+
argument :name,
|
|
13
|
+
type: :string,
|
|
14
|
+
desc: "Camel-case identifier of the rule"
|
|
15
|
+
|
|
16
|
+
def create_rule_file
|
|
17
|
+
create_file "lib/marcdouane/rules/#{underscore(name)}.rb" do
|
|
18
|
+
@msg = ask("What is the error message to be displayed on a faulty line?")
|
|
19
|
+
|
|
20
|
+
<<~RB
|
|
21
|
+
# frozen_string_literal: true
|
|
22
|
+
|
|
23
|
+
module Marcdouane
|
|
24
|
+
module Rules
|
|
25
|
+
class #{name} < Rule
|
|
26
|
+
ERROR_MESSAGE = "#{@msg}"
|
|
27
|
+
|
|
28
|
+
def check!; end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
RB
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def add_test_case
|
|
37
|
+
append_to_file "features/rules.feature" do
|
|
38
|
+
<<-CUKE
|
|
39
|
+
|
|
40
|
+
@wip
|
|
41
|
+
Rule: #{@msg}
|
|
42
|
+
Example: FIXME
|
|
43
|
+
Given a file named "foo.md" with:
|
|
44
|
+
"""
|
|
45
|
+
Faulty content
|
|
46
|
+
"""
|
|
47
|
+
When I run `marcdouane check "foo.md"`
|
|
48
|
+
Then it should fail with:
|
|
49
|
+
"""
|
|
50
|
+
foo.md:1: [#{name}] #{@msg}
|
|
51
|
+
"""
|
|
52
|
+
CUKE
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def letsgo
|
|
57
|
+
puts "Done! Use `bundle exec cucumber -t @wip` to get going."
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def underscore(str)
|
|
63
|
+
str.instance_eval do
|
|
64
|
+
gsub("::", "/")
|
|
65
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
66
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
67
|
+
.tr("-", "_")
|
|
68
|
+
.downcase
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
data/bin/marcdouane
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
require "bundler/setup"
|
|
5
4
|
require "dry/cli"
|
|
6
5
|
require "marcdouane"
|
|
7
6
|
|
|
7
|
+
# rubocop:disable Style/Documentation
|
|
8
8
|
module Marcdouane
|
|
9
9
|
module CLI
|
|
10
10
|
module Commands
|
|
@@ -36,32 +36,43 @@ module Marcdouane
|
|
|
36
36
|
required: true,
|
|
37
37
|
desc: "Files to check against"
|
|
38
38
|
|
|
39
|
+
option :"custom-rules",
|
|
40
|
+
type: :string,
|
|
41
|
+
required: false,
|
|
42
|
+
desc: "Path to a file containing custom rules"
|
|
43
|
+
|
|
39
44
|
def call(files: [], **options)
|
|
45
|
+
check_files!(files)
|
|
46
|
+
|
|
47
|
+
if options[:"custom-rules"]
|
|
48
|
+
load options[:"custom-rules"]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
files
|
|
52
|
+
.map { |file| Marcdouane::FileChecker.call(file, options) }
|
|
53
|
+
.then { |codes| exit(codes.max) }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def check_files!(files)
|
|
40
57
|
if files.empty?
|
|
41
58
|
puts "No files provided."
|
|
42
|
-
|
|
59
|
+
exit 0
|
|
43
60
|
end
|
|
44
61
|
|
|
45
|
-
exit_code = 0
|
|
46
|
-
|
|
47
62
|
files.each do |file|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
else
|
|
52
|
-
exit_code = Marcdouane::FileChecker.call(file, options)
|
|
63
|
+
unless File.exist?(file)
|
|
64
|
+
warn "`#{file}' is missing, or not a valid file."
|
|
65
|
+
exit 1
|
|
53
66
|
end
|
|
54
67
|
end
|
|
55
|
-
|
|
56
|
-
exit(exit_code)
|
|
57
68
|
end
|
|
58
69
|
end
|
|
59
70
|
|
|
60
|
-
|
|
61
71
|
register "version", Version, aliases: ["v", "-v", "--version"]
|
|
62
72
|
register "check", Check
|
|
63
73
|
end
|
|
64
74
|
end
|
|
65
75
|
end
|
|
76
|
+
# rubocop:enable Style/Documentation
|
|
66
77
|
|
|
67
78
|
Dry::CLI.new(Marcdouane::CLI::Commands).call
|
data/cucumber.yml
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
default: --publish-quiet
|
|
@@ -6,33 +6,53 @@ require "yaml"
|
|
|
6
6
|
require_relative "rules"
|
|
7
7
|
|
|
8
8
|
module Marcdouane
|
|
9
|
+
# FileChecker grabs all rules available and then calls them on a
|
|
10
|
+
# file, transmitting any options fed to the CLI. and then calls all
|
|
11
|
+
# the rules
|
|
9
12
|
class FileChecker
|
|
10
13
|
class << self
|
|
14
|
+
attr_reader :exit_code
|
|
15
|
+
|
|
11
16
|
def call(file, options)
|
|
12
17
|
verbose = options.fetch(:verbose)
|
|
13
18
|
|
|
14
|
-
parse_config!(options[:config])
|
|
19
|
+
parse_config!(options[:config]) unless options[:config].nil?
|
|
15
20
|
|
|
16
21
|
puts "Checking `#{file}'..." if verbose
|
|
17
22
|
|
|
18
|
-
exit_code = 0
|
|
23
|
+
@exit_code = 0
|
|
24
|
+
|
|
25
|
+
rules
|
|
26
|
+
.map { |klass| run_rule(file, klass, options) }
|
|
27
|
+
.tap { |_codes| puts "Done." if verbose }
|
|
28
|
+
.then { @exit_code }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def run_rule(file, klass, options)
|
|
32
|
+
rule = klass.new(file, options)
|
|
19
33
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
rescue Marcdouane::Error => e
|
|
23
|
-
$stderr.puts("#{file}:#{e.line_number}: #{e.message}")
|
|
34
|
+
rule.subscribe("rule.error") do |event|
|
|
35
|
+
print_error(file, rule, event[:line_number], event[:msg])
|
|
24
36
|
|
|
25
|
-
exit_code = 1
|
|
37
|
+
@exit_code = 1
|
|
26
38
|
end
|
|
27
39
|
|
|
28
|
-
|
|
40
|
+
rule.check!
|
|
41
|
+
end
|
|
29
42
|
|
|
30
|
-
|
|
43
|
+
def print_error(file, rule, line_number, msg)
|
|
44
|
+
warn(
|
|
45
|
+
format(
|
|
46
|
+
"%<file>s:%<line_number>s: [%<rule_class>s] %<message>s",
|
|
47
|
+
file:,
|
|
48
|
+
line_number:,
|
|
49
|
+
rule_class: rule.identifier,
|
|
50
|
+
message: msg
|
|
51
|
+
)
|
|
52
|
+
)
|
|
31
53
|
end
|
|
32
54
|
|
|
33
55
|
def parse_config!(path)
|
|
34
|
-
return if path.nil?
|
|
35
|
-
|
|
36
56
|
config = YAML.load_file(path)
|
|
37
57
|
|
|
38
58
|
config.each do |klass, hash|
|
|
@@ -2,30 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
module Marcdouane
|
|
4
4
|
module Rules
|
|
5
|
+
# Ensure that headers style is consistent: either the '#'-prefix
|
|
6
|
+
# style or the '=' underscore style, but not both.
|
|
5
7
|
class ConsistentHeaderStyle < Rule
|
|
6
8
|
ERROR_MESSAGE = "Use a unique, consistent header style"
|
|
7
9
|
|
|
8
10
|
def check!
|
|
9
|
-
reference_style
|
|
11
|
+
reference_style = nil
|
|
10
12
|
|
|
13
|
+
# we need to dup otherwise seeking directly into the
|
|
14
|
+
# @markdown.source confuses Inkmark and subsequent headers
|
|
15
|
+
# will have nil byte ranges
|
|
11
16
|
source = @markdown.source.lines.dup
|
|
12
17
|
|
|
13
18
|
@markdown.on(:heading) do |header|
|
|
14
19
|
line_number = line_number_from_byte_range(header.byte_range)
|
|
15
20
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if raw_line.start_with?("#")
|
|
19
|
-
style = :normal
|
|
20
|
-
else
|
|
21
|
-
style = :underline
|
|
22
|
-
end
|
|
21
|
+
style = source[line_number].start_with?("#") ? :normal : :underline
|
|
23
22
|
|
|
24
23
|
reference_style ||= style
|
|
25
24
|
|
|
26
|
-
if reference_style != style
|
|
27
|
-
error!(line_number)
|
|
28
|
-
end
|
|
25
|
+
error!(line_number) if reference_style != style
|
|
29
26
|
end
|
|
30
27
|
|
|
31
28
|
@markdown.walk
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "dry/configurable"
|
|
4
|
+
require "dry/events/publisher"
|
|
4
5
|
require "inkmark"
|
|
5
6
|
|
|
6
7
|
module Marcdouane
|
|
@@ -13,6 +14,7 @@ module Marcdouane
|
|
|
13
14
|
# ERROR_MESSAGE when the check fails.
|
|
14
15
|
class Rule
|
|
15
16
|
extend Dry::Configurable
|
|
17
|
+
include Dry::Events::Publisher[:marcdouane] # I don't know what that identifier is for
|
|
16
18
|
|
|
17
19
|
attr_reader :file, :options, :markdown
|
|
18
20
|
|
|
@@ -25,6 +27,8 @@ module Marcdouane
|
|
|
25
27
|
frontmatter: true
|
|
26
28
|
}
|
|
27
29
|
)
|
|
30
|
+
|
|
31
|
+
register_event("rule.error")
|
|
28
32
|
end
|
|
29
33
|
|
|
30
34
|
def line_number_from_byte_range(range)
|
|
@@ -35,17 +39,14 @@ module Marcdouane
|
|
|
35
39
|
self.class.to_s.split("::").last
|
|
36
40
|
end
|
|
37
41
|
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
# optional `message` override instead of the
|
|
41
|
-
# ERROR_MESSAGE.
|
|
42
|
+
# Publishes an error event, picked up by the FileChecker
|
|
43
|
+
# somewhere down the line. It must be called with the 0-indexed
|
|
44
|
+
# line-number, and an optional `message` override instead of the
|
|
45
|
+
# class's ERROR_MESSAGE.
|
|
42
46
|
def error!(machine_line_number, message = nil)
|
|
43
47
|
msg = message || self.class.const_get("ERROR_MESSAGE")
|
|
44
48
|
|
|
45
|
-
|
|
46
|
-
"[#{identifier}] #{msg}",
|
|
47
|
-
machine_line_number + 1
|
|
48
|
-
)
|
|
49
|
+
publish("rule.error", msg: msg, line_number: machine_line_number + 1)
|
|
49
50
|
end
|
|
50
51
|
end
|
|
51
52
|
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Marcdouane
|
|
4
|
+
module Rules
|
|
5
|
+
# Ensure that there is only a single top-level header in the file.
|
|
6
|
+
class SingleTopLevelHeader < Rule
|
|
7
|
+
ERROR_MESSAGE = "A top-level header is already present"
|
|
8
|
+
|
|
9
|
+
def check!
|
|
10
|
+
previously_seen = false
|
|
11
|
+
|
|
12
|
+
@markdown.on(:heading) do |header|
|
|
13
|
+
if header.level == 1
|
|
14
|
+
if previously_seen
|
|
15
|
+
error!(line_number_from_byte_range(header.byte_range))
|
|
16
|
+
else
|
|
17
|
+
previously_seen = true
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
@markdown.walk
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
data/lib/marcdouane/rules.rb
CHANGED
|
@@ -2,8 +2,4 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "rules/rule"
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
require_relative "rules/line_length"
|
|
7
|
-
require_relative "rules/no_consecutive_blank_lines"
|
|
8
|
-
require_relative "rules/start_with_top_level_header"
|
|
9
|
-
require_relative "rules/consistent_header_style"
|
|
5
|
+
Dir["#{File.dirname(__FILE__)}/rules/*.rb"].map(&method(:require))
|
data/lib/marcdouane/version.rb
CHANGED
data/lib/marcdouane.rb
CHANGED
|
@@ -3,15 +3,5 @@
|
|
|
3
3
|
require_relative "marcdouane/version"
|
|
4
4
|
require_relative "marcdouane/file_checker"
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
attr_reader :msg, :line_number
|
|
9
|
-
|
|
10
|
-
def initialize(msg, line_number)
|
|
11
|
-
@msg = msg
|
|
12
|
-
@line_number = line_number
|
|
13
|
-
|
|
14
|
-
super(@msg)
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
end
|
|
6
|
+
# Marcdouane is a pretty Markdown linter
|
|
7
|
+
module Marcdouane; end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: marcdouane
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Stéphane Maniaci
|
|
@@ -37,6 +37,20 @@ dependencies:
|
|
|
37
37
|
- - "~>"
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
39
|
version: '1.4'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: dry-events
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '1.1'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '1.1'
|
|
40
54
|
- !ruby/object:Gem::Dependency
|
|
41
55
|
name: inkmark
|
|
42
56
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -77,7 +91,9 @@ files:
|
|
|
77
91
|
- LICENSE.txt
|
|
78
92
|
- README.md
|
|
79
93
|
- Rakefile
|
|
94
|
+
- Thorfile
|
|
80
95
|
- bin/marcdouane
|
|
96
|
+
- cucumber.yml
|
|
81
97
|
- lib/marcdouane.rb
|
|
82
98
|
- lib/marcdouane/file_checker.rb
|
|
83
99
|
- lib/marcdouane/rules.rb
|
|
@@ -86,6 +102,7 @@ files:
|
|
|
86
102
|
- lib/marcdouane/rules/line_length.rb
|
|
87
103
|
- lib/marcdouane/rules/no_consecutive_blank_lines.rb
|
|
88
104
|
- lib/marcdouane/rules/rule.rb
|
|
105
|
+
- lib/marcdouane/rules/single_top_level_header.rb
|
|
89
106
|
- lib/marcdouane/rules/start_with_top_level_header.rb
|
|
90
107
|
- lib/marcdouane/version.rb
|
|
91
108
|
- logo.jpeg
|
|
@@ -96,6 +113,7 @@ licenses:
|
|
|
96
113
|
metadata:
|
|
97
114
|
homepage_uri: https://github.com/freesteph/marcdouane
|
|
98
115
|
source_code_uri: https://github.com/freesteph/marcdouane
|
|
116
|
+
rubygems_mfa_required: 'true'
|
|
99
117
|
rdoc_options: []
|
|
100
118
|
require_paths:
|
|
101
119
|
- lib
|