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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1ac5051e1dda30d1dd98035ba682431ac8e32788b4187772eea00e1306f5ca1d
4
- data.tar.gz: 2b60396b0e783bf920602fd49c2d04e4288e2c9b35e1485f85e7a94a5653d046
3
+ metadata.gz: 1ae9e6bd5c948110c746ef7ef8091fb9858d2ef9358f80dacdd424573009906b
4
+ data.tar.gz: b1843497ea7c116a1d466f9b9368929db9d00390391740d45671d2fea3baee81
5
5
  SHA512:
6
- metadata.gz: e054ef7079b1bf87c8203d3db165a43c3b26ffa7fc7fd1301a346060b0b25e60b7868f82550972992291b16875a5ce22512784417f489f990a673aa49fb1f501
7
- data.tar.gz: 3982749d76b7e586a67b6ed6a5b8ef13bcc3ecea6c0877166ac6fe00605979fd2904c5c069d9b1a0d16da9bbac9e98f186c2a21264038f7849648d78166d426e
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 by executing:
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
- A rule must inherit `Marcdouane::Rule`. It must answer to `check!` and
54
- raise a `Marcdouane::Error` with an `ERROR_MESSAGE` and a line
55
- number. Example:
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(line, index) do
66
- raise Marcdouane:Error.new(ERROR_MESSAGE, index + 1) if line =~ /anglois/
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
- It is then expected to be tested throughout the Cucumber feature tests
73
- like such:
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
- return
59
+ exit 0
43
60
  end
44
61
 
45
- exit_code = 0
46
-
47
62
  files.each do |file|
48
- if !File.exist?(file)
49
- $stderr.puts "`#{file}' is missing, or not a valid file."
50
- exit_code = 1
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
- rules.each do |rule|
21
- rule.new(file, options).check!
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
- puts "Done." if verbose
40
+ rule.check!
41
+ end
29
42
 
30
- exit_code
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, style = nil
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
- raw_line = source[line_number]
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
@@ -11,7 +11,7 @@ module Marcdouane
11
11
  .source
12
12
  .lines
13
13
  .each_cons(2)
14
- .each_with_index do |pair, index|
14
+ .with_index do |pair, index|
15
15
  if pair.map(&:strip).all?(&:empty?)
16
16
  error!(index + 2)
17
17
  end
@@ -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
- # Produces an error that will be collected and output by the
39
- # CLI. It must be called with the 0-indexed line-number, and an
40
- # optional `message` override instead of the class's
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
- raise Marcdouane::Error.new(
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
@@ -2,8 +2,4 @@
2
2
 
3
3
  require_relative "rules/rule"
4
4
 
5
- require_relative "rules/ensure_headers_cascade"
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))
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Marcdouane
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.1"
5
5
  end
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
- module Marcdouane
7
- class Error < StandardError
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.0
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