mdl 0.15.0 → 0.17.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 +179 -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: 5f3b2d8b702d0c1f3e4401b5c0a997d76f13368f57fc1be5d52acd2ca9ea35c2
|
|
4
|
+
data.tar.gz: f65a08dd97130930b44e93cb65cc318f218210b99d74ff915cd5842e17dca188
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0a63864cdb9583963ff12d78ccfd5d304b8162c464842140ceae23d5b14afab9d8f4fd60e473634bb0fdf3f2580a68ca8aca451559df058276fc3cb235dc8290
|
|
7
|
+
data.tar.gz: d381798b1c293e45a237368232429e0ab1e9812c1963b674383c61b0301fdcd72f3f34c285bcd58ea5ed1695fa5fd0fbf01514929aa49ded6a4df74472ce1822
|
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,19 @@ 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,
|
|
246
|
+
:treat_links_as_single_words => false
|
|
225
247
|
|
|
226
248
|
check do |doc|
|
|
227
249
|
# Every line in the document that is part of a code block.
|
|
@@ -242,7 +264,32 @@ rule 'MD013', 'Line length' do
|
|
|
242
264
|
end
|
|
243
265
|
end
|
|
244
266
|
end.flatten
|
|
245
|
-
|
|
267
|
+
# Fallback: detect table-like lines not recognized by kramdown
|
|
268
|
+
# (e.g. tables without surrounding blank lines are parsed as paragraphs)
|
|
269
|
+
unless params[:tables]
|
|
270
|
+
doc.lines.each_with_index do |line, i|
|
|
271
|
+
linenum = i + 1
|
|
272
|
+
next if table_lines.include?(linenum)
|
|
273
|
+
next if codeblock_lines.include?(linenum)
|
|
274
|
+
|
|
275
|
+
table_lines << linenum if line.match?(/^\s*\|.*\|/)
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
single_word_lines = doc.matching_lines(/^\s*\S+$/) +
|
|
279
|
+
doc.matching_lines(/^\s*(?:[-*+]|\d+[.)])\s+\S+$/)
|
|
280
|
+
if params[:treat_links_as_single_words]
|
|
281
|
+
link_re = /\[.*\]\([^)]*\)/
|
|
282
|
+
list_re = /^\s*(?:[-*+]|\d+[.)])\s+#{link_re.source}$/
|
|
283
|
+
single_word_lines +=
|
|
284
|
+
doc.matching_lines(/^\s*#{link_re.source}$/) +
|
|
285
|
+
doc.matching_lines(list_re)
|
|
286
|
+
end
|
|
287
|
+
# Every line in the document that is a header.
|
|
288
|
+
header_lines = doc.find_type_elements(:header).map do |e|
|
|
289
|
+
doc.element_linenumber(e)
|
|
290
|
+
end
|
|
291
|
+
overlines = doc.matching_lines(/^.{#{@params[:line_length]}}.+/)
|
|
292
|
+
overlines -= single_word_lines
|
|
246
293
|
if !params[:code_blocks] || params[:ignore_code_blocks]
|
|
247
294
|
overlines -= codeblock_lines
|
|
248
295
|
unless params[:code_blocks]
|
|
@@ -252,6 +299,7 @@ rule 'MD013', 'Line length' do
|
|
|
252
299
|
end
|
|
253
300
|
end
|
|
254
301
|
overlines -= table_lines unless params[:tables]
|
|
302
|
+
overlines -= header_lines unless params[:headings]
|
|
255
303
|
overlines
|
|
256
304
|
end
|
|
257
305
|
end
|
|
@@ -261,8 +309,9 @@ rule 'MD014', 'Dollar signs used before commands without showing output' do
|
|
|
261
309
|
aliases 'commands-show-output'
|
|
262
310
|
check do |doc|
|
|
263
311
|
doc.find_type_elements(:codeblock).select do |e|
|
|
264
|
-
|
|
265
|
-
|
|
312
|
+
lines = e.value.split(/\n+/)
|
|
313
|
+
!lines.empty? &&
|
|
314
|
+
!lines.map { |l| l.match(/^\$\s/) }.include?(nil)
|
|
266
315
|
end.map { |e| doc.element_linenumber(e) }
|
|
267
316
|
end
|
|
268
317
|
end
|
|
@@ -275,6 +324,11 @@ rule 'MD018', 'No space after hash on atx style header' do
|
|
|
275
324
|
doc.header_style(h) == :atx && doc.element_line(h).match(/^#+[^#\s]/)
|
|
276
325
|
end.map { |h| doc.element_linenumber(h) }
|
|
277
326
|
end
|
|
327
|
+
fix do |doc, lines|
|
|
328
|
+
lines.each do |linenum|
|
|
329
|
+
doc.lines[linenum - 1] = doc.lines[linenum - 1].sub(/^(#+)/, '\1 ')
|
|
330
|
+
end
|
|
331
|
+
end
|
|
278
332
|
end
|
|
279
333
|
|
|
280
334
|
rule 'MD019', 'Multiple spaces after hash on atx style header' do
|
|
@@ -285,6 +339,11 @@ rule 'MD019', 'Multiple spaces after hash on atx style header' do
|
|
|
285
339
|
doc.header_style(h) == :atx && doc.element_line(h).match(/^#+\s\s/)
|
|
286
340
|
end.map { |h| doc.element_linenumber(h) }
|
|
287
341
|
end
|
|
342
|
+
fix do |doc, lines|
|
|
343
|
+
lines.each do |linenum|
|
|
344
|
+
doc.lines[linenum - 1] = doc.lines[linenum - 1].sub(/^(#+)\s+/, '\1 ')
|
|
345
|
+
end
|
|
346
|
+
end
|
|
288
347
|
end
|
|
289
348
|
|
|
290
349
|
rule 'MD020', 'No space inside hashes on closed atx style header' do
|
|
@@ -309,6 +368,13 @@ rule 'MD021', 'Multiple spaces inside hashes on closed atx style header' do
|
|
|
309
368
|
|| doc.element_line(h).match(/\s\s#+$/))
|
|
310
369
|
end.map { |h| doc.element_linenumber(h) }
|
|
311
370
|
end
|
|
371
|
+
fix do |doc, lines|
|
|
372
|
+
lines.each do |linenum|
|
|
373
|
+
doc.lines[linenum - 1] = doc.lines[linenum - 1]
|
|
374
|
+
.sub(/^(#+)\s+/, '\1 ')
|
|
375
|
+
.sub(/\s+(#+)$/, ' \1')
|
|
376
|
+
end
|
|
377
|
+
end
|
|
312
378
|
end
|
|
313
379
|
|
|
314
380
|
rule 'MD022', 'Headers should be surrounded by blank lines' do
|
|
@@ -380,6 +446,16 @@ rule 'MD023', 'Headers must start at the beginning of the line' do
|
|
|
380
446
|
end
|
|
381
447
|
errors.sort
|
|
382
448
|
end
|
|
449
|
+
fix do |doc, lines|
|
|
450
|
+
lines.each do |linenum|
|
|
451
|
+
doc.lines[linenum - 1] = doc.lines[linenum - 1].lstrip
|
|
452
|
+
# For setext headers, also lstrip the underline on the next line
|
|
453
|
+
next_line = doc.lines[linenum]
|
|
454
|
+
if next_line&.match?(/^\s+(-+|=+)\s*$/)
|
|
455
|
+
doc.lines[linenum] = next_line.lstrip
|
|
456
|
+
end
|
|
457
|
+
end
|
|
458
|
+
end
|
|
383
459
|
end
|
|
384
460
|
|
|
385
461
|
rule 'MD024', 'Multiple headers with the same content' do
|
|
@@ -444,7 +520,8 @@ rule 'MD026', 'Trailing punctuation in header' do
|
|
|
444
520
|
params :punctuation => '.,;:!?'
|
|
445
521
|
check do |doc|
|
|
446
522
|
doc.find_type(:header).select do |h|
|
|
447
|
-
h[:raw_text].match(/[#{params[:punctuation]}]$/)
|
|
523
|
+
h[:raw_text].match(/[#{params[:punctuation]}]$/) &&
|
|
524
|
+
!h[:raw_text].match(/:[a-zA-Z0-9_+-]+:\s*$/)
|
|
448
525
|
end.map do |h|
|
|
449
526
|
doc.element_linenumber(h)
|
|
450
527
|
end
|
|
@@ -463,12 +540,24 @@ rule 'MD027', 'Multiple spaces after blockquote symbol' do
|
|
|
463
540
|
# element
|
|
464
541
|
errors << linenum if doc.element_line(e).match(/^\s*> /)
|
|
465
542
|
lines.each do |line|
|
|
466
|
-
|
|
543
|
+
# Check extracted text for leading spaces, but verify against the
|
|
544
|
+
# source line to avoid false positives from kramdown text processing
|
|
545
|
+
# (e.g. em-dash conversion creating leading spaces).
|
|
546
|
+
src = doc.lines[linenum - 1]
|
|
547
|
+
if line.start_with?(' ') && src&.match?(/^\s*(?:>\s?)+\s{2,}\S/)
|
|
548
|
+
errors << linenum
|
|
549
|
+
end
|
|
467
550
|
linenum += 1
|
|
468
551
|
end
|
|
469
552
|
end
|
|
470
553
|
errors
|
|
471
554
|
end
|
|
555
|
+
fix do |doc, lines|
|
|
556
|
+
lines.each do |linenum|
|
|
557
|
+
doc.lines[linenum - 1] = doc.lines[linenum - 1].sub(/(>\s*>) +/, '\1 ')
|
|
558
|
+
.sub(/^> +/, '> ')
|
|
559
|
+
end
|
|
560
|
+
end
|
|
472
561
|
end
|
|
473
562
|
|
|
474
563
|
rule 'MD028', 'Blank line inside blockquote' do
|
|
@@ -506,18 +595,16 @@ rule 'MD029', 'Ordered list item prefix' do
|
|
|
506
595
|
doc.find_type_elements(:ol).map do |l|
|
|
507
596
|
doc.find_type_elements(:li, false, l.children)
|
|
508
597
|
.map.with_index do |i, idx|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
end
|
|
598
|
+
line = doc.element_line(i).gsub(/^[\s>]+/, '')
|
|
599
|
+
doc.element_linenumber(i) unless line.start_with?("#{idx + 1}. ")
|
|
512
600
|
end
|
|
513
601
|
end.flatten.compact
|
|
514
602
|
when :one
|
|
515
603
|
doc.find_type_elements(:ol).map do |l|
|
|
516
604
|
doc.find_type_elements(:li, false, l.children)
|
|
517
605
|
end.flatten.map do |i|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
end
|
|
606
|
+
line = doc.element_line(i).gsub(/^[\s>]+/, '')
|
|
607
|
+
doc.element_linenumber(i) unless line.start_with?('1. ')
|
|
521
608
|
end.compact
|
|
522
609
|
end
|
|
523
610
|
end
|
|
@@ -571,7 +658,15 @@ rule 'MD031', 'Fenced code blocks should be surrounded by blank lines' do
|
|
|
571
658
|
next
|
|
572
659
|
end
|
|
573
660
|
|
|
574
|
-
|
|
661
|
+
marker = Regexp.last_match(1)
|
|
662
|
+
# Backtick info strings cannot contain backticks (CommonMark spec),
|
|
663
|
+
# so a line like ```test``` is inline code, not a fence opener.
|
|
664
|
+
if marker[0] == '`' && !in_code
|
|
665
|
+
rest = line.strip[marker.length..]
|
|
666
|
+
next if rest&.include?('`')
|
|
667
|
+
end
|
|
668
|
+
|
|
669
|
+
fence = in_code ? nil : marker
|
|
575
670
|
in_code = !in_code
|
|
576
671
|
if (in_code && !lines[linenum - 1].empty?) ||
|
|
577
672
|
(!in_code && !lines[linenum + 1].empty?)
|
|
@@ -591,12 +686,22 @@ rule 'MD032', 'Lists should be surrounded by blank lines' do
|
|
|
591
686
|
# without surrounding whitespace, so examine the lines directly.
|
|
592
687
|
in_list = false
|
|
593
688
|
in_code = false
|
|
689
|
+
in_comment = false
|
|
594
690
|
fence = nil
|
|
595
691
|
prev_line = ''
|
|
596
692
|
doc.lines.each_with_index do |line, linenum|
|
|
597
693
|
next if line.strip == '{:toc}'
|
|
598
694
|
|
|
599
|
-
|
|
695
|
+
# Track HTML comments
|
|
696
|
+
if !in_comment && line.match?(/<!--/) && !line.match?(/-->/)
|
|
697
|
+
in_comment = true
|
|
698
|
+
elsif in_comment && line.match?(/-->/)
|
|
699
|
+
in_comment = false
|
|
700
|
+
prev_line = ''
|
|
701
|
+
next
|
|
702
|
+
end
|
|
703
|
+
|
|
704
|
+
unless in_code || in_comment
|
|
600
705
|
list_marker = line.strip.match(/^([*+-]|(\d+\.))\s/)
|
|
601
706
|
if list_marker && !in_list && !prev_line.match(/^($|\s)/)
|
|
602
707
|
errors << (linenum + 1)
|
|
@@ -637,7 +742,40 @@ rule 'MD034', 'Bare URL used' do
|
|
|
637
742
|
tags :links, :url
|
|
638
743
|
aliases 'no-bare-urls'
|
|
639
744
|
check do |doc|
|
|
640
|
-
doc.matching_text_element_lines(
|
|
745
|
+
errors = doc.matching_text_element_lines(
|
|
746
|
+
%r{https?://}, %i{a html_element}
|
|
747
|
+
)
|
|
748
|
+
# Text elements inside tables lack location info, so check table
|
|
749
|
+
# text elements separately using the table's location.
|
|
750
|
+
doc.find_type_elements(:table).each do |t|
|
|
751
|
+
table_start = doc.element_linenumber(t)
|
|
752
|
+
next if table_start.nil?
|
|
753
|
+
|
|
754
|
+
doc.find_type_elements_except(:text, [:a], t.children).each do |e|
|
|
755
|
+
next unless e.value.match?(%r{https?://})
|
|
756
|
+
|
|
757
|
+
# Find this text in the table's source lines
|
|
758
|
+
doc.lines[(table_start - 1)..].each_with_index do |line, i|
|
|
759
|
+
linenum = table_start + i
|
|
760
|
+
break unless line&.match?(/^\s*\|/)
|
|
761
|
+
|
|
762
|
+
if line.include?(e.value.strip)
|
|
763
|
+
errors << linenum
|
|
764
|
+
break
|
|
765
|
+
end
|
|
766
|
+
end
|
|
767
|
+
end
|
|
768
|
+
end
|
|
769
|
+
# Filter out false positives where the source line doesn't actually
|
|
770
|
+
# contain a bare URL (kramdown can misparse links containing pipes
|
|
771
|
+
# as tables, reporting wrong line numbers)
|
|
772
|
+
errors.uniq.sort.select do |linenum|
|
|
773
|
+
line = doc.lines[linenum - 1]
|
|
774
|
+
next false if line.nil?
|
|
775
|
+
|
|
776
|
+
# Strip URLs inside markdown links, then check if a bare URL remains
|
|
777
|
+
line.gsub(%r{\]\(https?://[^)]*\)}, '').match?(%r{https?://})
|
|
778
|
+
end
|
|
641
779
|
end
|
|
642
780
|
end
|
|
643
781
|
|
|
@@ -691,9 +829,12 @@ rule 'MD037', 'Spaces inside emphasis markers' do
|
|
|
691
829
|
check do |doc|
|
|
692
830
|
# Kramdown doesn't parse emphasis with spaces, which means we can just
|
|
693
831
|
# look for emphasis patterns inside regular text with spaces just inside
|
|
694
|
-
# them.
|
|
832
|
+
# them. Exclude lines with escaped emphasis markers (\* or \_) since
|
|
833
|
+
# kramdown resolves escapes in text element values.
|
|
695
834
|
(doc.matching_text_element_lines(/\s(\*\*?|__?)\s.+\1/) |
|
|
696
|
-
doc.matching_text_element_lines(/(\*\*?|__?).+\s\1\s/))
|
|
835
|
+
doc.matching_text_element_lines(/(\*\*?|__?).+\s\1\s/))
|
|
836
|
+
.reject { |linenum| doc.lines[linenum - 1]&.match?(/\\[*_]/) }
|
|
837
|
+
.sort
|
|
697
838
|
end
|
|
698
839
|
end
|
|
699
840
|
|
|
@@ -733,7 +874,8 @@ rule 'MD040', 'Fenced code blocks should have a language specified' do
|
|
|
733
874
|
# the class attribute set to language-languagename.
|
|
734
875
|
doc.element_linenumbers(doc.find_type_elements(:codeblock).select do |i|
|
|
735
876
|
!i.attr['class'].to_s.start_with?('language-') &&
|
|
736
|
-
!doc.element_line(i).start_with?(' ')
|
|
877
|
+
!doc.element_line(i).start_with?(' ') &&
|
|
878
|
+
!doc.element_line(i).start_with?("\t")
|
|
737
879
|
end)
|
|
738
880
|
end
|
|
739
881
|
end
|
|
@@ -798,4 +940,7 @@ rule 'MD047', 'File should end with a single newline character' do
|
|
|
798
940
|
error_lines.push(doc.lines.length) unless last_line.nil? || last_line.empty?
|
|
799
941
|
error_lines
|
|
800
942
|
end
|
|
943
|
+
fix do |doc, _lines|
|
|
944
|
+
doc.lines << '' unless doc.lines[-1] && doc.lines[-1].empty?
|
|
945
|
+
end
|
|
801
946
|
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.17.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-06-03 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: []
|