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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0b0bf6bf60ca7328cdc7b7c979b1c7f9eea59f99c35a219da0eddf8c73e529cc
4
- data.tar.gz: 954a6871342a9137babebe806cab333bbf883d835cdc1ff867731145fb52d85f
3
+ metadata.gz: 89252c41ed1144ed4627ab6d25efb43a057f2a5c5da58e6b90df42432566fa8d
4
+ data.tar.gz: 826a02b1eca94b018590ae5835eca468cc670b6cf4e2cff972439ad09bb3c354
5
5
  SHA512:
6
- metadata.gz: 987343bbcf9c9a260e205970c1ea1bba1c5eb8dc5e0d2f3ed03013126efdf58a65085275f24081019d27fe63c02ce0ebbe0dded59e265392833f19cd00b582dd
7
- data.tar.gz: aa09c154c250f31e0f0ad9cefc8b1e243562a836c9d2c1d740aa7d5d20eac38ea91f56dd73c7a6d1d8aa5ab57b0ba41143013372a65eb208411d3b8ebe4b53bf
6
+ metadata.gz: 175f2e8fe298ed7a7370550829343cc55d4e97c1edefd2502c511d0dd9c9d4eea8b2fadb9d9e516d8a96a97639ff2be82322b29b3071713c0d9fb5fe26be2f6b
7
+ data.tar.gz: 3030a4a7b33a38ef07ba389c60b6ce8854beb6c4c2627a6bbe558f1be8e07b9b9a836fead71062fdba1b99b13e154d4637096f78a035f250066733a9684f77ee
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,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
- overlines = doc.matching_lines(/^.{#{@params[:line_length]}}.*\s/)
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
- !e.value.empty? &&
265
- !e.value.split(/\n+/).map { |l| l.match(/^\$\s/) }.include?(nil)
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
- errors << linenum if line.start_with?(' ')
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
- unless doc.element_line(i).strip.start_with?("#{idx + 1}. ")
510
- doc.element_linenumber(i)
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
- unless doc.element_line(i).strip.start_with?('1. ')
519
- doc.element_linenumber(i)
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
- fence = in_code ? nil : Regexp.last_match(1)
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
- unless in_code
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(%r{https?://})
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/)).sort
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
- @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.16.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.16.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-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.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: []