mdlint 0.1.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,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mdlint
4
+ module Linter
5
+ module Rules
6
+ class HeadingIncrement < Rule
7
+ self.rule_id = "MD001"
8
+ self.description = "Heading levels should only increment by one level at a time"
9
+
10
+ def check(tokens, _source)
11
+ last_level = 0
12
+
13
+ tokens.each do |token|
14
+ next unless token.type == :heading_open
15
+
16
+ level = token.tag[1].to_i
17
+ if last_level > 0 && level > last_level + 1
18
+ add_violation(
19
+ message: "Heading level jumped from h#{last_level} to h#{level}",
20
+ line: (token.map&.first || 0) + 1,
21
+ fixable: false
22
+ )
23
+ end
24
+ last_level = level
25
+ end
26
+
27
+ @violations
28
+ end
29
+
30
+ def fix(tokens, _source)
31
+ tokens
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mdlint
4
+ module Linter
5
+ module Rules
6
+ class HeadingStyle < Rule
7
+ self.rule_id = "MD003"
8
+ self.description = "Heading style should be consistent"
9
+
10
+ def check(tokens, _source)
11
+ tokens.each do |token|
12
+ next unless token.type == :heading_open
13
+
14
+ if token.markup && !token.markup.start_with?("#")
15
+ add_violation(
16
+ message: "Setext-style heading should be ATX-style",
17
+ line: (token.map&.first || 0) + 1,
18
+ fixable: true
19
+ )
20
+ end
21
+ end
22
+ @violations
23
+ end
24
+
25
+ def fix(tokens, _source)
26
+ tokens
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mdlint
4
+ module Linter
5
+ module Rules
6
+ class NoMultipleBlanks < Rule
7
+ self.rule_id = "MD012"
8
+ self.description = "Multiple consecutive blank lines"
9
+
10
+ def check(_tokens, source)
11
+ blank_count = 0
12
+
13
+ source.each_line.with_index(1) do |line, line_num|
14
+ if line.strip.empty?
15
+ blank_count += 1
16
+ if blank_count > 1
17
+ add_violation(
18
+ message: "Multiple consecutive blank lines",
19
+ line: line_num,
20
+ fixable: true
21
+ )
22
+ end
23
+ else
24
+ blank_count = 0
25
+ end
26
+ end
27
+
28
+ @violations
29
+ end
30
+
31
+ def fix(_tokens, source)
32
+ result = []
33
+ blank_count = 0
34
+
35
+ source.each_line do |line|
36
+ if line.strip.empty?
37
+ blank_count += 1
38
+ result << line if blank_count <= 1
39
+ else
40
+ blank_count = 0
41
+ result << line
42
+ end
43
+ end
44
+
45
+ result.join
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mdlint
4
+ module Linter
5
+ module Rules
6
+ class NoTrailingSpaces < Rule
7
+ self.rule_id = "MD009"
8
+ self.description = "Trailing spaces"
9
+
10
+ def check(_tokens, source)
11
+ source.each_line.with_index(1) do |line, line_num|
12
+ trimmed = line.chomp
13
+ next unless trimmed.end_with?(" ") || trimmed.end_with?("\t")
14
+ next if trimmed.end_with?(" ") && line_num < source.lines.count
15
+
16
+ add_violation(
17
+ message: "Trailing spaces",
18
+ line: line_num,
19
+ column: trimmed.rstrip.length + 1,
20
+ fixable: true
21
+ )
22
+ end
23
+ @violations
24
+ end
25
+
26
+ def fix(_tokens, source)
27
+ source.each_line.map do |line|
28
+ if line.end_with?(" \n")
29
+ line
30
+ else
31
+ line.rstrip + "\n"
32
+ end
33
+ end.join
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mdlint
4
+ module Linter
5
+ class Violation
6
+ attr_reader :rule_id, :message, :line, :column, :severity, :fixable
7
+
8
+ def initialize(rule_id:, message:, line:, column: nil, severity: :warning, fixable: false)
9
+ @rule_id = rule_id
10
+ @message = message
11
+ @line = line
12
+ @column = column
13
+ @severity = severity
14
+ @fixable = fixable
15
+ end
16
+
17
+ def to_s
18
+ location = column ? "#{line}:#{column}" : line.to_s
19
+ "[#{rule_id}] #{location}: #{message}"
20
+ end
21
+
22
+ def fixable?
23
+ @fixable
24
+ end
25
+
26
+ def error?
27
+ @severity == :error
28
+ end
29
+
30
+ def warning?
31
+ @severity == :warning
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "linter/violation"
4
+ require_relative "linter/rule"
5
+ require_relative "linter/rule_engine"
6
+ require_relative "linter/rules/heading_style"
7
+ require_relative "linter/rules/heading_increment"
8
+ require_relative "linter/rules/no_trailing_spaces"
9
+ require_relative "linter/rules/no_multiple_blanks"
10
+ require_relative "linter/rules/first_line_heading"
11
+
12
+ module Mdlint
13
+ module Linter
14
+ class << self
15
+ def check(src, options = {})
16
+ tokens = Parser.parse(src)
17
+ engine = RuleEngine.new(options)
18
+ engine.check(tokens, src)
19
+ end
20
+
21
+ def fix(src, options = {})
22
+ tokens = Parser.parse(src)
23
+ engine = RuleEngine.new(options)
24
+ engine.fix(tokens, src)
25
+ end
26
+ end
27
+ end
28
+ end