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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +12 -0
- data/LICENSE.txt +21 -0
- data/README.md +181 -0
- data/Rakefile +8 -0
- data/exe/mdlint +7 -0
- data/lib/mdlint/cli.rb +206 -0
- data/lib/mdlint/config.rb +103 -0
- data/lib/mdlint/linter/rule.rb +66 -0
- data/lib/mdlint/linter/rule_engine.rb +48 -0
- data/lib/mdlint/linter/rules/first_line_heading.rb +41 -0
- data/lib/mdlint/linter/rules/heading_increment.rb +36 -0
- data/lib/mdlint/linter/rules/heading_style.rb +31 -0
- data/lib/mdlint/linter/rules/no_multiple_blanks.rb +50 -0
- data/lib/mdlint/linter/rules/no_trailing_spaces.rb +38 -0
- data/lib/mdlint/linter/violation.rb +35 -0
- data/lib/mdlint/linter.rb +28 -0
- data/lib/mdlint/parser/block_parser.rb +585 -0
- data/lib/mdlint/parser/inline_parser.rb +258 -0
- data/lib/mdlint/parser/state.rb +62 -0
- data/lib/mdlint/parser.rb +29 -0
- data/lib/mdlint/renderer/md_renderer.rb +458 -0
- data/lib/mdlint/renderer.rb +13 -0
- data/lib/mdlint/token.rb +65 -0
- data/lib/mdlint/version.rb +5 -0
- data/lib/mdlint.rb +43 -0
- metadata +73 -0
|
@@ -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
|