mdl 0.16.0 → 0.18.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: 89252c41ed1144ed4627ab6d25efb43a057f2a5c5da58e6b90df42432566fa8d
4
- data.tar.gz: 826a02b1eca94b018590ae5835eca468cc670b6cf4e2cff972439ad09bb3c354
3
+ metadata.gz: 99253a6b08c6d6b023f76b93c7a05e5052194a6c379bf0ef519415715fb5a015
4
+ data.tar.gz: 61658817f7ce0c64b809c2ef86f4aa3fb31071038eefe1a4382f9a3386b66bd4
5
5
  SHA512:
6
- metadata.gz: 175f2e8fe298ed7a7370550829343cc55d4e97c1edefd2502c511d0dd9c9d4eea8b2fadb9d9e516d8a96a97639ff2be82322b29b3071713c0d9fb5fe26be2f6b
7
- data.tar.gz: 3030a4a7b33a38ef07ba389c60b6ce8854beb6c4c2627a6bbe558f1be8e07b9b9a836fead71062fdba1b99b13e154d4637096f78a035f250066733a9684f77ee
6
+ metadata.gz: 85dfb4fece1edff5969af1cad21c30a506ff54f7f0ee1fd770e0d59f29c01a6ef14f52e34d99ceade84a718cdaeb97fd5708601c7c0c99cb28feaeda25769bf3
7
+ data.tar.gz: c4ce1bfb14b52592e23107631f76e537fbe857122abb2715b16c1fee9543fddba0f8bd6781f48d20d87b40616bc9510c2d114b4065ffe58e1da7f88c4eff5b03
data/Gemfile CHANGED
@@ -4,8 +4,8 @@ gemspec
4
4
  group :development, :test do
5
5
  gem 'base64'
6
6
  gem 'bundler', '>= 1.12', '< 5'
7
- gem 'minitest', '~> 5.26', '>= 5.26.2'
8
- gem 'pry', '~> 0.15.2'
7
+ gem 'minitest', '~> 6.0'
8
+ gem 'pry', '~> 0.16.0'
9
9
  gem 'rake', '~> 13.3', '>= 13.3.1'
10
10
  gem 'rubocop', '~> 1.81'
11
11
  end
data/lib/mdl/cli.rb CHANGED
@@ -29,12 +29,31 @@ module MarkdownLint
29
29
  :description => 'Increase verbosity',
30
30
  :boolean => true
31
31
 
32
+ option :html_to_native,
33
+ :short => '-n',
34
+ :long => '--[no-]html-to-native',
35
+ :boolean => true,
36
+ :description => 'Convert HTML elements to native elements'
37
+
32
38
  option :ignore_front_matter,
33
39
  :short => '-i',
34
40
  :long => '--[no-]ignore-front-matter',
35
41
  :boolean => true,
36
42
  :description => 'Ignore YAML front matter'
37
43
 
44
+ option :parse_block_html,
45
+ :short => '-b',
46
+ :long => '--[no-]parse-block-html',
47
+ :boolean => true,
48
+ :description => 'Process kramdown syntax in block HTML tags'
49
+
50
+ option :parse_span_html,
51
+ :short => '-p',
52
+ :long => '--[no-]parse-span-html',
53
+ :boolean => true,
54
+ :default => true,
55
+ :description => 'Process kramdown syntax in span HTML tags'
56
+
38
57
  option :show_kramdown_warnings,
39
58
  :short => '-w',
40
59
  :long => '--[no-]warnings',
data/lib/mdl/doc.rb CHANGED
@@ -26,7 +26,7 @@ module MarkdownLint
26
26
  ##
27
27
  # Create a new document given a string containing the markdown source
28
28
 
29
- def initialize(text, ignore_front_matter = false)
29
+ def initialize(text, ignore_front_matter = false, **)
30
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
@@ -39,7 +39,7 @@ module MarkdownLint
39
39
  # The -1 is to cause split to preserve an extra entry in the array so we
40
40
  # can tell if there's a final newline in the file or not.
41
41
  @lines = text.split(/\R/, -1)
42
- @parsed = Kramdown::Document.new(text, :input => 'MarkdownLint')
42
+ @parsed = Kramdown::Document.new(text, :input => 'MarkdownLint', **)
43
43
  @elements = @parsed.root.children
44
44
  add_annotations(@elements)
45
45
  end
@@ -47,13 +47,14 @@ module MarkdownLint
47
47
  ##
48
48
  # Alternate 'constructor' passing in a filename
49
49
 
50
- def self.new_from_file(filename, ignore_front_matter = false)
50
+ def self.new_from_file(filename, ignore_front_matter = false, **)
51
51
  if filename == '-'
52
- new($stdin.read.scrub, ignore_front_matter)
52
+ new($stdin.read.scrub, ignore_front_matter, **)
53
53
  else
54
54
  new(
55
55
  File.read(filename, :encoding => 'UTF-8').scrub,
56
56
  ignore_front_matter,
57
+ **,
57
58
  )
58
59
  end
59
60
  end
@@ -14,8 +14,14 @@ module Kramdown
14
14
  @block_parsers.insert(i, :codeblock_fenced_gfm)
15
15
  end
16
16
 
17
- # Regular kramdown parser, but with GFM style fenced code blocks
18
- FENCED_CODEBLOCK_MATCH = Kramdown::Parser::GFM::FENCED_CODEBLOCK_MATCH
17
+ # GFM fenced code blocks, extended to allow spaces in the info string
18
+ # (e.g. ```c hlines=2). The GFM regex restricts the info string to a
19
+ # single non-whitespace token, which causes kramdown to miss blocks whose
20
+ # info string contains a space, leading to false positives inside them.
21
+ # Capture groups match GFM's: 1=fence, 2=fence-char, 3=full-info,
22
+ # 4=first-word, 5=content.
23
+ FENCED_CODEBLOCK_MATCH =
24
+ /^ {0,3}(([~`]){3,})\s*?((\S+?)[^\n]*)?\n(.*?)^ {0,3}\1\2*\s*?\n/m
19
25
 
20
26
  # End paragraphs when a fenced code block starts, matching GFM
21
27
  # behavior. Without this, fenced code blocks without a preceding
data/lib/mdl/rules.rb CHANGED
@@ -1,3 +1,7 @@
1
+ require 'uri'
2
+
3
+ URI_REGEXP = URI::RFC2396_PARSER.make_regexp
4
+
1
5
  docs do |id, description|
2
6
  url_hash = [id.downcase,
3
7
  description.downcase.gsub(/[^a-z]+/, '-')].join('---')
@@ -7,9 +11,12 @@ end
7
11
  rule 'MD001', 'Header levels should only increment by one level at a time' do
8
12
  tags :headers
9
13
  aliases 'header-increment'
14
+ params :front_matter_title => /^\s*title\s*[:=]/
10
15
  check do |doc|
11
16
  headers = doc.find_type(:header)
12
- old_level = nil
17
+ has = params[:front_matter_title]
18
+ has &&= doc.front_matter.match(params[:front_matter_title])
19
+ old_level = 1 if has
13
20
  errors = []
14
21
  headers.each do |h|
15
22
  errors << h[:location] if old_level && (h[:level] > old_level + 1)
@@ -22,8 +29,12 @@ end
22
29
  rule 'MD002', 'First header should be a top level header' do
23
30
  tags :headers
24
31
  aliases 'first-header-h1'
25
- params :level => 1
32
+ params :level => 1, :front_matter_title => /^\s*title\s*[:=]/
26
33
  check do |doc|
34
+ has = params[:front_matter_title]
35
+ has &&= doc.front_matter.match(params[:front_matter_title])
36
+ next [] if has
37
+
27
38
  first_header = doc.find_type(:header).first
28
39
  if first_header && (first_header[:level] != @params[:level])
29
40
  [first_header[:location]]
@@ -242,7 +253,8 @@ rule 'MD013', 'Line length' do
242
253
  tags :line_length
243
254
  aliases 'line-length'
244
255
  params :line_length => 80, :ignore_code_blocks => false, :code_blocks => true,
245
- :tables => true, :headings => true
256
+ :tables => true, :headings => true,
257
+ :treat_links_as_single_words => false
246
258
 
247
259
  check do |doc|
248
260
  # Every line in the document that is part of a code block.
@@ -274,12 +286,30 @@ rule 'MD013', 'Line length' do
274
286
  table_lines << linenum if line.match?(/^\s*\|.*\|/)
275
287
  end
276
288
  end
277
- single_word_lines = doc.matching_lines(/^\S+$/)
289
+ single_word_lines = doc.matching_lines(/^\s*\S+$/) +
290
+ doc.matching_lines(/^\s*(?:[-*+]|\d+[.)])\s+\S+$/)
291
+ if params[:treat_links_as_single_words]
292
+ link_re = /\[.*\]\([^)]*\)/
293
+ list_re = /^\s*(?:[-*+]|\d+[.)])\s+#{link_re.source}$/
294
+ single_word_lines +=
295
+ doc.matching_lines(/^\s*#{link_re.source}$/) +
296
+ doc.matching_lines(list_re)
297
+ end
278
298
  # Every line in the document that is a header.
279
299
  header_lines = doc.find_type_elements(:header).map do |e|
280
300
  doc.element_linenumber(e)
281
301
  end
302
+ # Every line that is a link reference:
303
+ #
304
+ # This link is referenced[1] at the bottom
305
+ #
306
+ # [1]: https://example.com
307
+ link_reference_lines = doc.matching_lines(/^\[.*\]: #{URI_REGEXP}$/)
308
+
282
309
  overlines = doc.matching_lines(/^.{#{@params[:line_length]}}.+/)
310
+
311
+ overlines -= link_reference_lines if params[:treat_links_as_single_words]
312
+
283
313
  overlines -= single_word_lines
284
314
  if !params[:code_blocks] || params[:ignore_code_blocks]
285
315
  overlines -= codeblock_lines
@@ -477,7 +507,7 @@ rule 'MD024', 'Multiple headers with the same content' do
477
507
  stack.pop
478
508
  elsif current_level < level
479
509
  stack.push([text])
480
- elsif stack.last.include?(text)
510
+ elsif stack&.last&.include?(text)
481
511
  same_nesting_duplicates.add(header)
482
512
  end
483
513
 
@@ -494,13 +524,16 @@ end
494
524
  rule 'MD025', 'Multiple top level headers in the same document' do
495
525
  tags :headers
496
526
  aliases 'single-h1'
497
- params :level => 1
527
+ params :level => 1, :front_matter_title => /^\s*title\s*[:=]/
498
528
  check do |doc|
499
529
  headers = doc.find_type(:header, false).select do |h|
500
530
  h[:level] == params[:level]
501
531
  end
502
- if !headers.empty? && (doc.element_linenumber(headers[0]) == 1)
503
- headers[1..].map { |h| doc.element_linenumber(h) }
532
+ has = params[:front_matter_title]
533
+ has &&= doc.front_matter.match(params[:front_matter_title])
534
+ if !headers.empty? && (doc.element_linenumber(headers[0]) == 1 || has)
535
+ headers = headers[1..] unless has
536
+ headers.map { |h| doc.element_linenumber(h) }
504
537
  end
505
538
  end
506
539
  end
@@ -703,8 +736,8 @@ rule 'MD032', 'Lists should be surrounded by blank lines' do
703
736
  end
704
737
  line.strip.match(/^(`{3,}|~{3,})/)
705
738
  if Regexp.last_match(1) && (
706
- !in_code || (Regexp.last_match(1).slice(0, fence.length) == fence)
707
- )
739
+ !in_code || (Regexp.last_match(1).slice(0, fence.length) == fence)
740
+ )
708
741
  fence = in_code ? nil : Regexp.last_match(1)
709
742
  in_code = !in_code
710
743
  in_list = false
@@ -732,6 +765,7 @@ end
732
765
  rule 'MD034', 'Bare URL used' do
733
766
  tags :links, :url
734
767
  aliases 'no-bare-urls'
768
+ params :allow_quoted => false
735
769
  check do |doc|
736
770
  errors = doc.matching_text_element_lines(
737
771
  %r{https?://}, %i{a html_element}
@@ -765,7 +799,17 @@ rule 'MD034', 'Bare URL used' do
765
799
  next false if line.nil?
766
800
 
767
801
  # Strip URLs inside markdown links, then check if a bare URL remains
768
- line.gsub(%r{\]\(https?://[^)]*\)}, '').match?(%r{https?://})
802
+ line = line.gsub(%r{\]\(https?://[^)]*\)}, '')
803
+
804
+ if params[:allow_quoted]
805
+ # If allow_quoted is set, also strip any URL directly en-quoted,
806
+ # check if a bare url remains
807
+ line = line
808
+ .gsub(%r{"https?://\S*?"}, '')
809
+ .gsub(%r{'https?://\S*?'}, '')
810
+ end
811
+
812
+ line.match?(%r{https?://})
769
813
  end
770
814
  end
771
815
  end
@@ -874,11 +918,14 @@ end
874
918
  rule 'MD041', 'First line in file should be a top level header' do
875
919
  tags :headers
876
920
  aliases 'first-line-h1'
877
- params :level => 1
921
+ params :level => 1, :front_matter_title => /^\s*title\s*[:=]/
878
922
  check do |doc|
879
923
  first_header = doc.find_type(:header).first
880
- [1] if first_header.nil? || (first_header[:location] != 1) \
881
- || (first_header[:level] != params[:level])
924
+ first = first_header.nil? || (first_header[:location] != 1)
925
+ first ||= first_header[:level] != params[:level]
926
+ has = params[:front_matter_title]
927
+ has &&= doc.front_matter.match(params[:front_matter_title])
928
+ [1] if first && !has
882
929
  end
883
930
  end
884
931
 
data/lib/mdl/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module MarkdownLint
2
- VERSION = '0.16.0'.freeze
2
+ VERSION = '0.18.0'.freeze
3
3
  end
data/lib/mdl.rb CHANGED
@@ -67,9 +67,9 @@ module MarkdownLint
67
67
  if Config[:git_recurse]
68
68
  Dir.chdir(filename) do
69
69
  cli.cli_arguments[i] =
70
- Mixlib::ShellOut.new("git ls-files '*.md' '*.markdown'")
71
- .run_command.stdout.lines
72
- .map { |m| File.join(filename, m.strip) }
70
+ Mixlib::ShellOut.new("git ls-files -z '*.md' '*.markdown'")
71
+ .run_command.stdout.split("\x00")
72
+ .map { |m| File.join(filename, m) }
73
73
  end
74
74
  else
75
75
  cli.cli_arguments[i] = Dir["#{filename}/**/*.{md,markdown}"]
@@ -95,7 +95,12 @@ module MarkdownLint
95
95
  text = original_text.dup
96
96
  end
97
97
 
98
- doc = Doc.new_from_file(filename, Config[:ignore_front_matter])
98
+ options = {
99
+ :parse_block_html => Config[:parse_block_html],
100
+ :parse_span_html => Config[:parse_span_html],
101
+ :html_to_native => Config[:html_to_native],
102
+ }
103
+ doc = Doc.new_from_file(filename, Config[:ignore_front_matter], **options)
99
104
  filename = '(stdin)' if filename == '-'
100
105
  if Config[:show_kramdown_warnings]
101
106
  status = 2 unless doc.parsed.warnings.empty?
@@ -113,7 +118,7 @@ module MarkdownLint
113
118
  if Config[:fix] && filename != '(stdin)' && rule.fix
114
119
  rule.fix.call(doc, error_lines)
115
120
  text = doc.to_s
116
- doc = Doc.new(text.dup, Config[:ignore_front_matter])
121
+ doc = Doc.new(text.dup, Config[:ignore_front_matter], **options)
117
122
  corrected = true
118
123
  end
119
124
 
data/mdl.gemspec CHANGED
@@ -26,4 +26,5 @@ Gem::Specification.new do |spec|
26
26
  spec.add_dependency 'mixlib-cli'
27
27
  spec.add_dependency 'mixlib-config'
28
28
  spec.add_dependency 'mixlib-shellout'
29
+ spec.add_dependency 'uri'
29
30
  end
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.16.0
4
+ version: 0.18.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: 2026-05-29 00:00:00.000000000 Z
11
+ date: 2026-08-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: kramdown
@@ -80,6 +80,20 @@ dependencies:
80
80
  - - ">="
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: uri
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
83
97
  description: Style checker/lint tool for markdown files
84
98
  email:
85
99
  - phil@ipom.com