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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0b0bf6bf60ca7328cdc7b7c979b1c7f9eea59f99c35a219da0eddf8c73e529cc
4
- data.tar.gz: 954a6871342a9137babebe806cab333bbf883d835cdc1ff867731145fb52d85f
3
+ metadata.gz: 5f3b2d8b702d0c1f3e4401b5c0a997d76f13368f57fc1be5d52acd2ca9ea35c2
4
+ data.tar.gz: f65a08dd97130930b44e93cb65cc318f218210b99d74ff915cd5842e17dca188
5
5
  SHA512:
6
- metadata.gz: 987343bbcf9c9a260e205970c1ea1bba1c5eb8dc5e0d2f3ed03013126efdf58a65085275f24081019d27fe63c02ce0ebbe0dded59e265392833f19cd00b582dd
7
- data.tar.gz: aa09c154c250f31e0f0ad9cefc8b1e243562a836c9d2c1d740aa7d5d20eac38ea91f56dd73c7a6d1d8aa5ab57b0ba41143013372a65eb208411d3b8ebe4b53bf
6
+ metadata.gz: 0a63864cdb9583963ff12d78ccfd5d304b8162c464842140ceae23d5b14afab9d8f4fd60e473634bb0fdf3f2580a68ca8aca451559df058276fc3cb235dc8290
7
+ data.tar.gz: d381798b1c293e45a237368232429e0ab1e9812c1963b674383c61b0301fdcd72f3f34c285bcd58ea5ed1695fa5fd0fbf01514929aa49ded6a4df74472ce1822
data/Gemfile CHANGED
@@ -3,7 +3,7 @@ gemspec
3
3
 
4
4
  group :development, :test do
5
5
  gem 'base64'
6
- gem 'bundler', '>= 1.12', '< 3'
6
+ gem 'bundler', '>= 1.12', '< 5'
7
7
  gem 'minitest', '~> 5.26', '>= 5.26.2'
8
8
  gem 'pry', '~> 0.15.2'
9
9
  gem 'rake', '~> 13.3', '>= 13.3.1'
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 = /^---\n(.*?)---\n\n?/m
30
+ regex = /\A---\n(.*?)---\n\n?/m
31
31
  if ignore_front_matter && regex.match(text)
32
- @offset = regex.match(text).to_s.split("\n").length
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(File.read(filename, :encoding => 'UTF-8'), ignore_front_matter)
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.select { |text, _linenum| regex.match(text) }
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
  ##
@@ -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
- if indent_levels[b[:element_level]].nil?
118
- indent_levels[b[:element_level]] = indent_level
119
- end
120
- if indent_level != indent_levels[b[:element_level]]
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
- indents = doc.find_type(:ul).map do |e|
148
- [doc.indent_for(doc.element_line(e)), doc.element_linenumber(e)]
149
- end
150
- curr_indent = indents[0][0] unless indents.empty?
151
- indents.each do |indent, linenum|
152
- if (indent > curr_indent) && (indent - curr_indent != @params[:indent])
153
- errors << linenum
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
- overlines = doc.matching_lines(/^.{#{@params[:line_length]}}.*\s/)
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
- !e.value.empty? &&
265
- !e.value.split(/\n+/).map { |l| l.match(/^\$\s/) }.include?(nil)
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
- errors << linenum if line.start_with?(' ')
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
- unless doc.element_line(i).strip.start_with?("#{idx + 1}. ")
510
- doc.element_linenumber(i)
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
- unless doc.element_line(i).strip.start_with?('1. ')
519
- doc.element_linenumber(i)
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
- fence = in_code ? nil : Regexp.last_match(1)
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
- unless in_code
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(%r{https?://})
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/)).sort
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
- @params.update(params) unless params.nil?
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
@@ -1,3 +1,3 @@
1
1
  module MarkdownLint
2
- VERSION = '0.15.0'.freeze
2
+ VERSION = '0.17.0'.freeze
3
3
  end
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
- results << {
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} " + rule.description.to_s
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
- if !Config[:json] && !Config[:sarif] &&
128
- !$stdout.tty? && !docs_to_print.include?(rule)
129
- docs_to_print << rule
130
- end
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 = ['Mark Harrison']
9
- spec.email = ['mark@mivok.net']
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.3'
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.15.0
4
+ version: 0.17.0
5
5
  platform: ruby
6
6
  authors:
7
- - Mark Harrison
7
+ - Phil Dibowitz
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-11-26 00:00:00.000000000 Z
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.3'
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.3'
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
- - mark@mivok.net
85
+ - phil@ipom.com
86
86
  executables:
87
87
  - mdl
88
88
  extensions: []