mdl 0.15.0 → 0.16.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 +4 -4
- data/Gemfile +1 -1
- data/lib/mdl/cli.rb +9 -0
- data/lib/mdl/doc.rb +18 -6
- data/lib/mdl/kramdown_parser.rb +8 -0
- data/lib/mdl/rules.rb +170 -34
- data/lib/mdl/ruleset.rb +20 -1
- data/lib/mdl/style.rb +2 -0
- data/lib/mdl/version.rb +1 -1
- data/lib/mdl.rb +30 -6
- data/mdl.gemspec +3 -3
- metadata +6 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 89252c41ed1144ed4627ab6d25efb43a057f2a5c5da58e6b90df42432566fa8d
|
|
4
|
+
data.tar.gz: 826a02b1eca94b018590ae5835eca468cc670b6cf4e2cff972439ad09bb3c354
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 175f2e8fe298ed7a7370550829343cc55d4e97c1edefd2502c511d0dd9c9d4eea8b2fadb9d9e516d8a96a97639ff2be82322b29b3071713c0d9fb5fe26be2f6b
|
|
7
|
+
data.tar.gz: 3030a4a7b33a38ef07ba389c60b6ce8854beb6c4c2627a6bbe558f1be8e07b9b9a836fead71062fdba1b99b13e154d4637096f78a035f250066733a9684f77ee
|
data/Gemfile
CHANGED
data/lib/mdl/cli.rb
CHANGED
|
@@ -101,6 +101,12 @@ module MarkdownLint
|
|
|
101
101
|
:proc => proc { puts MarkdownLint::VERSION },
|
|
102
102
|
:exit => 0
|
|
103
103
|
|
|
104
|
+
option :fix,
|
|
105
|
+
:short => '-f',
|
|
106
|
+
:long => '--fix',
|
|
107
|
+
:description => 'Automatically fix what can be fixed',
|
|
108
|
+
:boolean => true
|
|
109
|
+
|
|
104
110
|
option :json,
|
|
105
111
|
:short => '-j',
|
|
106
112
|
:long => '--json',
|
|
@@ -122,6 +128,9 @@ module MarkdownLint
|
|
|
122
128
|
# Only fall back to ~/.mdlrc if we are using the default value for -c
|
|
123
129
|
if filename.nil? && (config[:config_file] == CONFIG_FILE)
|
|
124
130
|
filename = File.expand_path("~/#{CONFIG_FILE}")
|
|
131
|
+
elsif filename.nil? && config[:config_file] != CONFIG_FILE
|
|
132
|
+
warn "Config file '#{config[:config_file]}' not found"
|
|
133
|
+
exit 3
|
|
125
134
|
end
|
|
126
135
|
|
|
127
136
|
if !filename.nil? && File.exist?(filename)
|
data/lib/mdl/doc.rb
CHANGED
|
@@ -11,7 +11,7 @@ module MarkdownLint
|
|
|
11
11
|
# subtract 1 from a line number to get the correct line. The element_line*
|
|
12
12
|
# methods take care of this for you.
|
|
13
13
|
|
|
14
|
-
attr_reader :lines, :parsed, :elements, :offset
|
|
14
|
+
attr_reader :lines, :parsed, :elements, :offset, :front_matter
|
|
15
15
|
|
|
16
16
|
##
|
|
17
17
|
# A Kramdown::Document object containing the parsed markdown document.
|
|
@@ -27,11 +27,13 @@ module MarkdownLint
|
|
|
27
27
|
# Create a new document given a string containing the markdown source
|
|
28
28
|
|
|
29
29
|
def initialize(text, ignore_front_matter = false)
|
|
30
|
-
regex =
|
|
30
|
+
regex = /\A---\n(.*?)---\n\n?/m
|
|
31
31
|
if ignore_front_matter && regex.match(text)
|
|
32
|
-
@
|
|
32
|
+
@front_matter = regex.match(text).to_s
|
|
33
|
+
@offset = @front_matter.count("\n")
|
|
33
34
|
text.sub!(regex, '')
|
|
34
35
|
else
|
|
36
|
+
@front_matter = ''
|
|
35
37
|
@offset = 0
|
|
36
38
|
end
|
|
37
39
|
# The -1 is to cause split to preserve an extra entry in the array so we
|
|
@@ -47,9 +49,12 @@ module MarkdownLint
|
|
|
47
49
|
|
|
48
50
|
def self.new_from_file(filename, ignore_front_matter = false)
|
|
49
51
|
if filename == '-'
|
|
50
|
-
new($stdin.read, ignore_front_matter)
|
|
52
|
+
new($stdin.read.scrub, ignore_front_matter)
|
|
51
53
|
else
|
|
52
|
-
new(
|
|
54
|
+
new(
|
|
55
|
+
File.read(filename, :encoding => 'UTF-8').scrub,
|
|
56
|
+
ignore_front_matter,
|
|
57
|
+
)
|
|
53
58
|
end
|
|
54
59
|
end
|
|
55
60
|
|
|
@@ -211,7 +216,8 @@ module MarkdownLint
|
|
|
211
216
|
# Returns line numbers for lines that match the given regular expression
|
|
212
217
|
|
|
213
218
|
def matching_lines(regex)
|
|
214
|
-
@lines.each_with_index
|
|
219
|
+
@lines.each_with_index
|
|
220
|
+
.select { |text, _linenum| regex.match(text) }
|
|
215
221
|
.map do |i|
|
|
216
222
|
i[1] + 1
|
|
217
223
|
end
|
|
@@ -292,6 +298,12 @@ module MarkdownLint
|
|
|
292
298
|
end.join.split("\n")
|
|
293
299
|
end
|
|
294
300
|
|
|
301
|
+
##
|
|
302
|
+
# Reconstruct the full file content from front matter and lines
|
|
303
|
+
def to_s
|
|
304
|
+
@front_matter + @lines.join("\n")
|
|
305
|
+
end
|
|
306
|
+
|
|
295
307
|
private
|
|
296
308
|
|
|
297
309
|
##
|
data/lib/mdl/kramdown_parser.rb
CHANGED
|
@@ -16,6 +16,14 @@ module Kramdown
|
|
|
16
16
|
|
|
17
17
|
# Regular kramdown parser, but with GFM style fenced code blocks
|
|
18
18
|
FENCED_CODEBLOCK_MATCH = Kramdown::Parser::GFM::FENCED_CODEBLOCK_MATCH
|
|
19
|
+
|
|
20
|
+
# End paragraphs when a fenced code block starts, matching GFM
|
|
21
|
+
# behavior. Without this, fenced code blocks without a preceding
|
|
22
|
+
# blank line are swallowed into the paragraph.
|
|
23
|
+
PARAGRAPH_END = Regexp.union(
|
|
24
|
+
Kramdown::Parser::Kramdown::PARAGRAPH_END,
|
|
25
|
+
Kramdown::Parser::GFM::FENCED_CODEBLOCK_START,
|
|
26
|
+
)
|
|
19
27
|
end
|
|
20
28
|
end
|
|
21
29
|
end
|
data/lib/mdl/rules.rb
CHANGED
|
@@ -111,15 +111,13 @@ rule 'MD005', 'Inconsistent indentation for list items at the same level' do
|
|
|
111
111
|
check do |doc|
|
|
112
112
|
bullets = doc.find_type(:li)
|
|
113
113
|
errors = []
|
|
114
|
-
indent_levels =
|
|
114
|
+
indent_levels = {}
|
|
115
115
|
bullets.each do |b|
|
|
116
116
|
indent_level = doc.indent_for(doc.element_line(b))
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if indent_level != indent_levels[
|
|
121
|
-
errors << doc.element_linenumber(b)
|
|
122
|
-
end
|
|
117
|
+
list_type = b[:parent].type
|
|
118
|
+
key = [b[:element_level], list_type]
|
|
119
|
+
indent_levels[key] = indent_level if indent_levels[key].nil?
|
|
120
|
+
errors << doc.element_linenumber(b) if indent_level != indent_levels[key]
|
|
123
121
|
end
|
|
124
122
|
errors
|
|
125
123
|
end
|
|
@@ -144,16 +142,24 @@ rule 'MD007', 'Unordered list indentation' do
|
|
|
144
142
|
params :indent => 3
|
|
145
143
|
check do |doc|
|
|
146
144
|
errors = []
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
145
|
+
# Check indentation of nested ul elements relative to their parent ul.
|
|
146
|
+
# Only compare parent-child ul pairs to avoid false positives when
|
|
147
|
+
# unrelated lists appear at different indent levels.
|
|
148
|
+
def check_ul_indent(doc, elements, parent_indent, errors, indent)
|
|
149
|
+
elements.each do |e|
|
|
150
|
+
if e.type == :ul
|
|
151
|
+
el_indent = doc.indent_for(doc.element_line(e))
|
|
152
|
+
if parent_indent && (el_indent > parent_indent) &&
|
|
153
|
+
(el_indent - parent_indent != indent)
|
|
154
|
+
errors << doc.element_linenumber(e)
|
|
155
|
+
end
|
|
156
|
+
check_ul_indent(doc, e.children, el_indent, errors, indent)
|
|
157
|
+
else
|
|
158
|
+
check_ul_indent(doc, e.children, parent_indent, errors, indent)
|
|
159
|
+
end
|
|
154
160
|
end
|
|
155
|
-
curr_indent = indent
|
|
156
161
|
end
|
|
162
|
+
check_ul_indent(doc, doc.elements, nil, errors, @params[:indent])
|
|
157
163
|
errors
|
|
158
164
|
end
|
|
159
165
|
end
|
|
@@ -169,6 +175,11 @@ rule 'MD009', 'Trailing spaces' do
|
|
|
169
175
|
end
|
|
170
176
|
errors
|
|
171
177
|
end
|
|
178
|
+
fix do |doc, lines|
|
|
179
|
+
lines.each do |linenum|
|
|
180
|
+
doc.lines[linenum - 1] = doc.lines[linenum - 1].rstrip
|
|
181
|
+
end
|
|
182
|
+
end
|
|
172
183
|
end
|
|
173
184
|
|
|
174
185
|
rule 'MD010', 'Hard tabs' do
|
|
@@ -189,13 +200,18 @@ rule 'MD010', 'Hard tabs' do
|
|
|
189
200
|
hard_tab_lines -= codeblock_lines if params[:ignore_code_blocks]
|
|
190
201
|
hard_tab_lines
|
|
191
202
|
end
|
|
203
|
+
fix do |doc, lines|
|
|
204
|
+
lines.each do |linenum|
|
|
205
|
+
doc.lines[linenum - 1] = doc.lines[linenum - 1].gsub("\t", ' ')
|
|
206
|
+
end
|
|
207
|
+
end
|
|
192
208
|
end
|
|
193
209
|
|
|
194
210
|
rule 'MD011', 'Reversed link syntax' do
|
|
195
211
|
tags :links
|
|
196
212
|
aliases 'no-reversed-links'
|
|
197
213
|
check do |doc|
|
|
198
|
-
doc.matching_text_element_lines(/\([^)]+\)\[[^\]]+\]/)
|
|
214
|
+
doc.matching_text_element_lines(/\([^)]+\)\[[^\]\^]+\]/)
|
|
199
215
|
end
|
|
200
216
|
end
|
|
201
217
|
|
|
@@ -215,13 +231,18 @@ rule 'MD012', 'Multiple consecutive blank lines' do
|
|
|
215
231
|
end.map { |_p, n| n }
|
|
216
232
|
cons_blank_lines - codeblock_lines
|
|
217
233
|
end
|
|
234
|
+
fix do |doc, lines|
|
|
235
|
+
lines.reverse_each do |linenum|
|
|
236
|
+
doc.lines.delete_at(linenum - 1)
|
|
237
|
+
end
|
|
238
|
+
end
|
|
218
239
|
end
|
|
219
240
|
|
|
220
241
|
rule 'MD013', 'Line length' do
|
|
221
242
|
tags :line_length
|
|
222
243
|
aliases 'line-length'
|
|
223
244
|
params :line_length => 80, :ignore_code_blocks => false, :code_blocks => true,
|
|
224
|
-
:tables => true
|
|
245
|
+
:tables => true, :headings => true
|
|
225
246
|
|
|
226
247
|
check do |doc|
|
|
227
248
|
# Every line in the document that is part of a code block.
|
|
@@ -242,7 +263,24 @@ rule 'MD013', 'Line length' do
|
|
|
242
263
|
end
|
|
243
264
|
end
|
|
244
265
|
end.flatten
|
|
245
|
-
|
|
266
|
+
# Fallback: detect table-like lines not recognized by kramdown
|
|
267
|
+
# (e.g. tables without surrounding blank lines are parsed as paragraphs)
|
|
268
|
+
unless params[:tables]
|
|
269
|
+
doc.lines.each_with_index do |line, i|
|
|
270
|
+
linenum = i + 1
|
|
271
|
+
next if table_lines.include?(linenum)
|
|
272
|
+
next if codeblock_lines.include?(linenum)
|
|
273
|
+
|
|
274
|
+
table_lines << linenum if line.match?(/^\s*\|.*\|/)
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
single_word_lines = doc.matching_lines(/^\S+$/)
|
|
278
|
+
# Every line in the document that is a header.
|
|
279
|
+
header_lines = doc.find_type_elements(:header).map do |e|
|
|
280
|
+
doc.element_linenumber(e)
|
|
281
|
+
end
|
|
282
|
+
overlines = doc.matching_lines(/^.{#{@params[:line_length]}}.+/)
|
|
283
|
+
overlines -= single_word_lines
|
|
246
284
|
if !params[:code_blocks] || params[:ignore_code_blocks]
|
|
247
285
|
overlines -= codeblock_lines
|
|
248
286
|
unless params[:code_blocks]
|
|
@@ -252,6 +290,7 @@ rule 'MD013', 'Line length' do
|
|
|
252
290
|
end
|
|
253
291
|
end
|
|
254
292
|
overlines -= table_lines unless params[:tables]
|
|
293
|
+
overlines -= header_lines unless params[:headings]
|
|
255
294
|
overlines
|
|
256
295
|
end
|
|
257
296
|
end
|
|
@@ -261,8 +300,9 @@ rule 'MD014', 'Dollar signs used before commands without showing output' do
|
|
|
261
300
|
aliases 'commands-show-output'
|
|
262
301
|
check do |doc|
|
|
263
302
|
doc.find_type_elements(:codeblock).select do |e|
|
|
264
|
-
|
|
265
|
-
|
|
303
|
+
lines = e.value.split(/\n+/)
|
|
304
|
+
!lines.empty? &&
|
|
305
|
+
!lines.map { |l| l.match(/^\$\s/) }.include?(nil)
|
|
266
306
|
end.map { |e| doc.element_linenumber(e) }
|
|
267
307
|
end
|
|
268
308
|
end
|
|
@@ -275,6 +315,11 @@ rule 'MD018', 'No space after hash on atx style header' do
|
|
|
275
315
|
doc.header_style(h) == :atx && doc.element_line(h).match(/^#+[^#\s]/)
|
|
276
316
|
end.map { |h| doc.element_linenumber(h) }
|
|
277
317
|
end
|
|
318
|
+
fix do |doc, lines|
|
|
319
|
+
lines.each do |linenum|
|
|
320
|
+
doc.lines[linenum - 1] = doc.lines[linenum - 1].sub(/^(#+)/, '\1 ')
|
|
321
|
+
end
|
|
322
|
+
end
|
|
278
323
|
end
|
|
279
324
|
|
|
280
325
|
rule 'MD019', 'Multiple spaces after hash on atx style header' do
|
|
@@ -285,6 +330,11 @@ rule 'MD019', 'Multiple spaces after hash on atx style header' do
|
|
|
285
330
|
doc.header_style(h) == :atx && doc.element_line(h).match(/^#+\s\s/)
|
|
286
331
|
end.map { |h| doc.element_linenumber(h) }
|
|
287
332
|
end
|
|
333
|
+
fix do |doc, lines|
|
|
334
|
+
lines.each do |linenum|
|
|
335
|
+
doc.lines[linenum - 1] = doc.lines[linenum - 1].sub(/^(#+)\s+/, '\1 ')
|
|
336
|
+
end
|
|
337
|
+
end
|
|
288
338
|
end
|
|
289
339
|
|
|
290
340
|
rule 'MD020', 'No space inside hashes on closed atx style header' do
|
|
@@ -309,6 +359,13 @@ rule 'MD021', 'Multiple spaces inside hashes on closed atx style header' do
|
|
|
309
359
|
|| doc.element_line(h).match(/\s\s#+$/))
|
|
310
360
|
end.map { |h| doc.element_linenumber(h) }
|
|
311
361
|
end
|
|
362
|
+
fix do |doc, lines|
|
|
363
|
+
lines.each do |linenum|
|
|
364
|
+
doc.lines[linenum - 1] = doc.lines[linenum - 1]
|
|
365
|
+
.sub(/^(#+)\s+/, '\1 ')
|
|
366
|
+
.sub(/\s+(#+)$/, ' \1')
|
|
367
|
+
end
|
|
368
|
+
end
|
|
312
369
|
end
|
|
313
370
|
|
|
314
371
|
rule 'MD022', 'Headers should be surrounded by blank lines' do
|
|
@@ -380,6 +437,16 @@ rule 'MD023', 'Headers must start at the beginning of the line' do
|
|
|
380
437
|
end
|
|
381
438
|
errors.sort
|
|
382
439
|
end
|
|
440
|
+
fix do |doc, lines|
|
|
441
|
+
lines.each do |linenum|
|
|
442
|
+
doc.lines[linenum - 1] = doc.lines[linenum - 1].lstrip
|
|
443
|
+
# For setext headers, also lstrip the underline on the next line
|
|
444
|
+
next_line = doc.lines[linenum]
|
|
445
|
+
if next_line&.match?(/^\s+(-+|=+)\s*$/)
|
|
446
|
+
doc.lines[linenum] = next_line.lstrip
|
|
447
|
+
end
|
|
448
|
+
end
|
|
449
|
+
end
|
|
383
450
|
end
|
|
384
451
|
|
|
385
452
|
rule 'MD024', 'Multiple headers with the same content' do
|
|
@@ -444,7 +511,8 @@ rule 'MD026', 'Trailing punctuation in header' do
|
|
|
444
511
|
params :punctuation => '.,;:!?'
|
|
445
512
|
check do |doc|
|
|
446
513
|
doc.find_type(:header).select do |h|
|
|
447
|
-
h[:raw_text].match(/[#{params[:punctuation]}]$/)
|
|
514
|
+
h[:raw_text].match(/[#{params[:punctuation]}]$/) &&
|
|
515
|
+
!h[:raw_text].match(/:[a-zA-Z0-9_+-]+:\s*$/)
|
|
448
516
|
end.map do |h|
|
|
449
517
|
doc.element_linenumber(h)
|
|
450
518
|
end
|
|
@@ -463,12 +531,24 @@ rule 'MD027', 'Multiple spaces after blockquote symbol' do
|
|
|
463
531
|
# element
|
|
464
532
|
errors << linenum if doc.element_line(e).match(/^\s*> /)
|
|
465
533
|
lines.each do |line|
|
|
466
|
-
|
|
534
|
+
# Check extracted text for leading spaces, but verify against the
|
|
535
|
+
# source line to avoid false positives from kramdown text processing
|
|
536
|
+
# (e.g. em-dash conversion creating leading spaces).
|
|
537
|
+
src = doc.lines[linenum - 1]
|
|
538
|
+
if line.start_with?(' ') && src&.match?(/^\s*(?:>\s?)+\s{2,}\S/)
|
|
539
|
+
errors << linenum
|
|
540
|
+
end
|
|
467
541
|
linenum += 1
|
|
468
542
|
end
|
|
469
543
|
end
|
|
470
544
|
errors
|
|
471
545
|
end
|
|
546
|
+
fix do |doc, lines|
|
|
547
|
+
lines.each do |linenum|
|
|
548
|
+
doc.lines[linenum - 1] = doc.lines[linenum - 1].sub(/(>\s*>) +/, '\1 ')
|
|
549
|
+
.sub(/^> +/, '> ')
|
|
550
|
+
end
|
|
551
|
+
end
|
|
472
552
|
end
|
|
473
553
|
|
|
474
554
|
rule 'MD028', 'Blank line inside blockquote' do
|
|
@@ -506,18 +586,16 @@ rule 'MD029', 'Ordered list item prefix' do
|
|
|
506
586
|
doc.find_type_elements(:ol).map do |l|
|
|
507
587
|
doc.find_type_elements(:li, false, l.children)
|
|
508
588
|
.map.with_index do |i, idx|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
end
|
|
589
|
+
line = doc.element_line(i).gsub(/^[\s>]+/, '')
|
|
590
|
+
doc.element_linenumber(i) unless line.start_with?("#{idx + 1}. ")
|
|
512
591
|
end
|
|
513
592
|
end.flatten.compact
|
|
514
593
|
when :one
|
|
515
594
|
doc.find_type_elements(:ol).map do |l|
|
|
516
595
|
doc.find_type_elements(:li, false, l.children)
|
|
517
596
|
end.flatten.map do |i|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
end
|
|
597
|
+
line = doc.element_line(i).gsub(/^[\s>]+/, '')
|
|
598
|
+
doc.element_linenumber(i) unless line.start_with?('1. ')
|
|
521
599
|
end.compact
|
|
522
600
|
end
|
|
523
601
|
end
|
|
@@ -571,7 +649,15 @@ rule 'MD031', 'Fenced code blocks should be surrounded by blank lines' do
|
|
|
571
649
|
next
|
|
572
650
|
end
|
|
573
651
|
|
|
574
|
-
|
|
652
|
+
marker = Regexp.last_match(1)
|
|
653
|
+
# Backtick info strings cannot contain backticks (CommonMark spec),
|
|
654
|
+
# so a line like ```test``` is inline code, not a fence opener.
|
|
655
|
+
if marker[0] == '`' && !in_code
|
|
656
|
+
rest = line.strip[marker.length..]
|
|
657
|
+
next if rest&.include?('`')
|
|
658
|
+
end
|
|
659
|
+
|
|
660
|
+
fence = in_code ? nil : marker
|
|
575
661
|
in_code = !in_code
|
|
576
662
|
if (in_code && !lines[linenum - 1].empty?) ||
|
|
577
663
|
(!in_code && !lines[linenum + 1].empty?)
|
|
@@ -591,12 +677,22 @@ rule 'MD032', 'Lists should be surrounded by blank lines' do
|
|
|
591
677
|
# without surrounding whitespace, so examine the lines directly.
|
|
592
678
|
in_list = false
|
|
593
679
|
in_code = false
|
|
680
|
+
in_comment = false
|
|
594
681
|
fence = nil
|
|
595
682
|
prev_line = ''
|
|
596
683
|
doc.lines.each_with_index do |line, linenum|
|
|
597
684
|
next if line.strip == '{:toc}'
|
|
598
685
|
|
|
599
|
-
|
|
686
|
+
# Track HTML comments
|
|
687
|
+
if !in_comment && line.match?(/<!--/) && !line.match?(/-->/)
|
|
688
|
+
in_comment = true
|
|
689
|
+
elsif in_comment && line.match?(/-->/)
|
|
690
|
+
in_comment = false
|
|
691
|
+
prev_line = ''
|
|
692
|
+
next
|
|
693
|
+
end
|
|
694
|
+
|
|
695
|
+
unless in_code || in_comment
|
|
600
696
|
list_marker = line.strip.match(/^([*+-]|(\d+\.))\s/)
|
|
601
697
|
if list_marker && !in_list && !prev_line.match(/^($|\s)/)
|
|
602
698
|
errors << (linenum + 1)
|
|
@@ -637,7 +733,40 @@ rule 'MD034', 'Bare URL used' do
|
|
|
637
733
|
tags :links, :url
|
|
638
734
|
aliases 'no-bare-urls'
|
|
639
735
|
check do |doc|
|
|
640
|
-
doc.matching_text_element_lines(
|
|
736
|
+
errors = doc.matching_text_element_lines(
|
|
737
|
+
%r{https?://}, %i{a html_element}
|
|
738
|
+
)
|
|
739
|
+
# Text elements inside tables lack location info, so check table
|
|
740
|
+
# text elements separately using the table's location.
|
|
741
|
+
doc.find_type_elements(:table).each do |t|
|
|
742
|
+
table_start = doc.element_linenumber(t)
|
|
743
|
+
next if table_start.nil?
|
|
744
|
+
|
|
745
|
+
doc.find_type_elements_except(:text, [:a], t.children).each do |e|
|
|
746
|
+
next unless e.value.match?(%r{https?://})
|
|
747
|
+
|
|
748
|
+
# Find this text in the table's source lines
|
|
749
|
+
doc.lines[(table_start - 1)..].each_with_index do |line, i|
|
|
750
|
+
linenum = table_start + i
|
|
751
|
+
break unless line&.match?(/^\s*\|/)
|
|
752
|
+
|
|
753
|
+
if line.include?(e.value.strip)
|
|
754
|
+
errors << linenum
|
|
755
|
+
break
|
|
756
|
+
end
|
|
757
|
+
end
|
|
758
|
+
end
|
|
759
|
+
end
|
|
760
|
+
# Filter out false positives where the source line doesn't actually
|
|
761
|
+
# contain a bare URL (kramdown can misparse links containing pipes
|
|
762
|
+
# as tables, reporting wrong line numbers)
|
|
763
|
+
errors.uniq.sort.select do |linenum|
|
|
764
|
+
line = doc.lines[linenum - 1]
|
|
765
|
+
next false if line.nil?
|
|
766
|
+
|
|
767
|
+
# Strip URLs inside markdown links, then check if a bare URL remains
|
|
768
|
+
line.gsub(%r{\]\(https?://[^)]*\)}, '').match?(%r{https?://})
|
|
769
|
+
end
|
|
641
770
|
end
|
|
642
771
|
end
|
|
643
772
|
|
|
@@ -691,9 +820,12 @@ rule 'MD037', 'Spaces inside emphasis markers' do
|
|
|
691
820
|
check do |doc|
|
|
692
821
|
# Kramdown doesn't parse emphasis with spaces, which means we can just
|
|
693
822
|
# look for emphasis patterns inside regular text with spaces just inside
|
|
694
|
-
# them.
|
|
823
|
+
# them. Exclude lines with escaped emphasis markers (\* or \_) since
|
|
824
|
+
# kramdown resolves escapes in text element values.
|
|
695
825
|
(doc.matching_text_element_lines(/\s(\*\*?|__?)\s.+\1/) |
|
|
696
|
-
doc.matching_text_element_lines(/(\*\*?|__?).+\s\1\s/))
|
|
826
|
+
doc.matching_text_element_lines(/(\*\*?|__?).+\s\1\s/))
|
|
827
|
+
.reject { |linenum| doc.lines[linenum - 1]&.match?(/\\[*_]/) }
|
|
828
|
+
.sort
|
|
697
829
|
end
|
|
698
830
|
end
|
|
699
831
|
|
|
@@ -733,7 +865,8 @@ rule 'MD040', 'Fenced code blocks should have a language specified' do
|
|
|
733
865
|
# the class attribute set to language-languagename.
|
|
734
866
|
doc.element_linenumbers(doc.find_type_elements(:codeblock).select do |i|
|
|
735
867
|
!i.attr['class'].to_s.start_with?('language-') &&
|
|
736
|
-
!doc.element_line(i).start_with?(' ')
|
|
868
|
+
!doc.element_line(i).start_with?(' ') &&
|
|
869
|
+
!doc.element_line(i).start_with?("\t")
|
|
737
870
|
end)
|
|
738
871
|
end
|
|
739
872
|
end
|
|
@@ -798,4 +931,7 @@ rule 'MD047', 'File should end with a single newline character' do
|
|
|
798
931
|
error_lines.push(doc.lines.length) unless last_line.nil? || last_line.empty?
|
|
799
932
|
error_lines
|
|
800
933
|
end
|
|
934
|
+
fix do |doc, _lines|
|
|
935
|
+
doc.lines << '' unless doc.lines[-1] && doc.lines[-1].empty?
|
|
936
|
+
end
|
|
801
937
|
end
|
data/lib/mdl/ruleset.rb
CHANGED
|
@@ -19,6 +19,11 @@ module MarkdownLint
|
|
|
19
19
|
@check
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
+
def fix(&block)
|
|
23
|
+
@fix = block unless block.nil?
|
|
24
|
+
@fix
|
|
25
|
+
end
|
|
26
|
+
|
|
22
27
|
def tags(*tags)
|
|
23
28
|
@tags = tags.flatten.map(&:to_sym) unless tags.empty?
|
|
24
29
|
@tags
|
|
@@ -30,7 +35,21 @@ module MarkdownLint
|
|
|
30
35
|
end
|
|
31
36
|
|
|
32
37
|
def params(params = nil)
|
|
33
|
-
|
|
38
|
+
unless params.nil?
|
|
39
|
+
# Normalize string values to symbols where the rule's default is
|
|
40
|
+
# a symbol AND the string looks like a simple identifier, so that
|
|
41
|
+
# style files can use either :atx or "atx". Values like "---"
|
|
42
|
+
# (MD035 hr style) are left as strings.
|
|
43
|
+
params = params.each_with_object({}) do |(k, v), h|
|
|
44
|
+
h[k] = if v.is_a?(String) && @params[k].is_a?(Symbol) &&
|
|
45
|
+
v.match?(/\A[a-z][a-z_]*\z/)
|
|
46
|
+
v.to_sym
|
|
47
|
+
else
|
|
48
|
+
v
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
@params.update(params)
|
|
52
|
+
end
|
|
34
53
|
@params
|
|
35
54
|
end
|
|
36
55
|
|
data/lib/mdl/style.rb
CHANGED
|
@@ -38,6 +38,7 @@ module MarkdownLint
|
|
|
38
38
|
|
|
39
39
|
def exclude_rule(id)
|
|
40
40
|
id = @aliases[id] if @aliases[id]
|
|
41
|
+
all if @rules.empty?
|
|
41
42
|
@rules.delete(id)
|
|
42
43
|
end
|
|
43
44
|
|
|
@@ -46,6 +47,7 @@ module MarkdownLint
|
|
|
46
47
|
end
|
|
47
48
|
|
|
48
49
|
def exclude_tag(tag)
|
|
50
|
+
all if @rules.empty?
|
|
49
51
|
@rules.subtract(@tagged_rules[tag])
|
|
50
52
|
end
|
|
51
53
|
|
data/lib/mdl/version.rb
CHANGED
data/lib/mdl.rb
CHANGED
|
@@ -62,6 +62,7 @@ module MarkdownLint
|
|
|
62
62
|
|
|
63
63
|
# Recurse into directories
|
|
64
64
|
cli.cli_arguments.each_with_index do |filename, i|
|
|
65
|
+
filename = filename.chomp('/') if filename.end_with?('/')
|
|
65
66
|
if Dir.exist?(filename)
|
|
66
67
|
if Config[:git_recurse]
|
|
67
68
|
Dir.chdir(filename) do
|
|
@@ -88,6 +89,12 @@ module MarkdownLint
|
|
|
88
89
|
)
|
|
89
90
|
exit 3
|
|
90
91
|
end
|
|
92
|
+
|
|
93
|
+
if Config[:fix] && filename != '-'
|
|
94
|
+
original_text = File.read(filename, :encoding => 'UTF-8').scrub
|
|
95
|
+
text = original_text.dup
|
|
96
|
+
end
|
|
97
|
+
|
|
91
98
|
doc = Doc.new_from_file(filename, Config[:ignore_front_matter])
|
|
92
99
|
filename = '(stdin)' if filename == '-'
|
|
93
100
|
if Config[:show_kramdown_warnings]
|
|
@@ -102,10 +109,19 @@ module MarkdownLint
|
|
|
102
109
|
next if error_lines.nil? || error_lines.empty?
|
|
103
110
|
|
|
104
111
|
status = 1
|
|
112
|
+
corrected = false
|
|
113
|
+
if Config[:fix] && filename != '(stdin)' && rule.fix
|
|
114
|
+
rule.fix.call(doc, error_lines)
|
|
115
|
+
text = doc.to_s
|
|
116
|
+
doc = Doc.new(text.dup, Config[:ignore_front_matter])
|
|
117
|
+
corrected = true
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
suffix = corrected ? ' (corrected)' : ''
|
|
105
121
|
error_lines.each do |line|
|
|
106
122
|
line += doc.offset # Correct line numbers for any yaml front matter
|
|
107
123
|
if Config[:json] || Config[:sarif]
|
|
108
|
-
|
|
124
|
+
result = {
|
|
109
125
|
'filename' => filename,
|
|
110
126
|
'line' => line,
|
|
111
127
|
'rule' => id,
|
|
@@ -113,9 +129,12 @@ module MarkdownLint
|
|
|
113
129
|
'description' => rule.description,
|
|
114
130
|
'docs' => rule.docs_url,
|
|
115
131
|
}
|
|
132
|
+
result['corrected'] = corrected if Config[:fix]
|
|
133
|
+
results << result
|
|
116
134
|
else
|
|
117
135
|
linked_id = linkify(printable_id(rule), rule.docs_url)
|
|
118
|
-
puts "#{filename}:#{line}: #{linked_id} "
|
|
136
|
+
puts "#{filename}:#{line}: #{linked_id} " \
|
|
137
|
+
"#{rule.description}#{suffix}"
|
|
119
138
|
end
|
|
120
139
|
end
|
|
121
140
|
|
|
@@ -124,10 +143,15 @@ module MarkdownLint
|
|
|
124
143
|
# for that) then, instead of making the output ugly with long URLs, we
|
|
125
144
|
# print them at the end. And of course we only want to print each URL
|
|
126
145
|
# once.
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
146
|
+
next unless !Config[:json] && !Config[:sarif] &&
|
|
147
|
+
!$stdout.tty? && rule.docs_url &&
|
|
148
|
+
!docs_to_print.include?(rule)
|
|
149
|
+
|
|
150
|
+
docs_to_print << rule
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
if Config[:fix] && filename != '(stdin)' && text != original_text
|
|
154
|
+
File.write(filename, text)
|
|
131
155
|
end
|
|
132
156
|
end
|
|
133
157
|
|
data/mdl.gemspec
CHANGED
|
@@ -5,8 +5,8 @@ require 'mdl/version'
|
|
|
5
5
|
Gem::Specification.new do |spec|
|
|
6
6
|
spec.name = 'mdl'
|
|
7
7
|
spec.version = MarkdownLint::VERSION
|
|
8
|
-
spec.authors = ['
|
|
9
|
-
spec.email = ['
|
|
8
|
+
spec.authors = ['Phil Dibowitz']
|
|
9
|
+
spec.email = ['phil@ipom.com']
|
|
10
10
|
spec.summary = 'Markdown lint tool'
|
|
11
11
|
spec.description = 'Style checker/lint tool for markdown files'
|
|
12
12
|
spec.homepage = 'https://github.com/markdownlint/markdownlint'
|
|
@@ -21,7 +21,7 @@ Gem::Specification.new do |spec|
|
|
|
21
21
|
|
|
22
22
|
spec.required_ruby_version = '>= 3.2'
|
|
23
23
|
|
|
24
|
-
spec.add_dependency 'kramdown', '~> 2.
|
|
24
|
+
spec.add_dependency 'kramdown', '~> 2.5'
|
|
25
25
|
spec.add_dependency 'kramdown-parser-gfm', '~> 1.1'
|
|
26
26
|
spec.add_dependency 'mixlib-cli'
|
|
27
27
|
spec.add_dependency 'mixlib-config'
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mdl
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.16.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
|
-
-
|
|
7
|
+
- Phil Dibowitz
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-05-29 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: kramdown
|
|
@@ -16,14 +16,14 @@ dependencies:
|
|
|
16
16
|
requirements:
|
|
17
17
|
- - "~>"
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '2.
|
|
19
|
+
version: '2.5'
|
|
20
20
|
type: :runtime
|
|
21
21
|
prerelease: false
|
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
23
|
requirements:
|
|
24
24
|
- - "~>"
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: '2.
|
|
26
|
+
version: '2.5'
|
|
27
27
|
- !ruby/object:Gem::Dependency
|
|
28
28
|
name: kramdown-parser-gfm
|
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -82,7 +82,7 @@ dependencies:
|
|
|
82
82
|
version: '0'
|
|
83
83
|
description: Style checker/lint tool for markdown files
|
|
84
84
|
email:
|
|
85
|
-
-
|
|
85
|
+
- phil@ipom.com
|
|
86
86
|
executables:
|
|
87
87
|
- mdl
|
|
88
88
|
extensions: []
|