mdl 0.11.0 → 0.13.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e82acf8008929f43d28c4be6a794f538d3745d1dd5acf37487ab061d0f968843
4
- data.tar.gz: c4f5b4b055ceb67dd29a0b292fe4c9ecf974ae3724a7db9c359196dafe60efe0
3
+ metadata.gz: bb98b688ab9d9dc7bac3c169f20fefc7c0cb27ce5bf574bec52053d592b7a9c7
4
+ data.tar.gz: 873df51b4011d23617c1d73ab47a1928765fee0502b6f58853e2932bb3d5ec59
5
5
  SHA512:
6
- metadata.gz: 6ed1202471b0bf2df640e0eaaacbfaf23593b3841ebc232ee630d15d4ae7817aedd6b455451612f0f4239762e0ddd6fe7acfa4efd9c61b72b14db87ab7dcce4a
7
- data.tar.gz: 3a8b7a23579bc6345fdec2d79442b238c6e32e2836602827ed626d105f9495b2ad000ac799d130ebe97815dca67f29b4e5ba6c21c8f816c1cf60bca0cf4c501a
6
+ metadata.gz: fea533cf2d4a61c4da291a094e3f94080acee8837919935e789e1b39c98d8bcc416cb9fcb143a10a1f63738ee993d3fe5ce89ca3dc664a9a11712796db8dbc4d
7
+ data.tar.gz: 8dc7470f981b1267dddb1f668afc4f85c742d6c467ae9a31bcaf24618792d6d9c7792a328ff3258d8efd071d9592b4a84600a2825e787b26d1cb5f7e979989af
data/lib/mdl/cli.rb CHANGED
@@ -107,6 +107,12 @@ module MarkdownLint
107
107
  :description => 'JSON output',
108
108
  :boolean => true
109
109
 
110
+ option :sarif,
111
+ :short => '-S',
112
+ :long => '--sarif',
113
+ :description => 'SARIF output',
114
+ :boolean => true
115
+
110
116
  def run(argv = ARGV)
111
117
  parse_options(argv)
112
118
 
@@ -145,10 +151,10 @@ module MarkdownLint
145
151
  end
146
152
 
147
153
  def self.toggle_list(parts, to_sym = false)
148
- parts = parts.split(',') if parts.class == String
149
- if parts.class == Array
154
+ parts = parts.split(',') if parts.instance_of?(String)
155
+ if parts.instance_of?(Array)
150
156
  inc = parts.reject { |p| p.start_with?('~') }
151
- exc = parts.select { |p| p.start_with?('~') }.map { |p| p[1..-1] }
157
+ exc = parts.select { |p| p.start_with?('~') }.map { |p| p[1..] }
152
158
  if to_sym
153
159
  inc.map!(&:to_sym)
154
160
  exc.map!(&:to_sym)
data/lib/mdl/doc.rb CHANGED
@@ -34,7 +34,9 @@ module MarkdownLint
34
34
  else
35
35
  @offset = 0
36
36
  end
37
- @lines = text.split(/\R/)
37
+ # The -1 is to cause split to preserve an extra entry in the array so we
38
+ # can tell if there's a final newline in the file or not.
39
+ @lines = text.split(/\R/, -1)
38
40
  @parsed = Kramdown::Document.new(text, :input => 'MarkdownLint')
39
41
  @elements = @parsed.root.children
40
42
  add_annotations(@elements)
@@ -78,7 +80,7 @@ module MarkdownLint
78
80
 
79
81
  def find_type_elements(type, nested = true, elements = @elements)
80
82
  results = []
81
- type = [type] if type.class == Symbol
83
+ type = [type] if type.instance_of?(Symbol)
82
84
  elements.each do |e|
83
85
  results.push(e) if type.include?(e.type)
84
86
  if nested && !e.children.empty?
@@ -102,8 +104,8 @@ module MarkdownLint
102
104
  type, nested_except = [], elements = @elements
103
105
  )
104
106
  results = []
105
- type = [type] if type.class == Symbol
106
- nested_except = [nested_except] if nested_except.class == Symbol
107
+ type = [type] if type.instance_of?(Symbol)
108
+ nested_except = [nested_except] if nested_except.instance_of?(Symbol)
107
109
  elements.each do |e|
108
110
  results.push(e) if type.include?(e.type)
109
111
  next if nested_except.include?(e.type) || e.children.empty?
@@ -230,7 +232,7 @@ module MarkdownLint
230
232
 
231
233
  lines = e.value.split("\n")
232
234
  lines.each_with_index do |l, i|
233
- matches << first_line + i if regex.match(l)
235
+ matches << (first_line + i) if regex.match(l)
234
236
  end
235
237
  end
236
238
  matches
@@ -266,6 +268,30 @@ module MarkdownLint
266
268
  lines
267
269
  end
268
270
 
271
+ ##
272
+ # Returns the element as plaintext
273
+
274
+ def extract_as_text(element)
275
+ quotes = {
276
+ :rdquo => '"',
277
+ :ldquo => '"',
278
+ :lsquo => "'",
279
+ :rsquo => "'",
280
+ }
281
+ # If anything goes amiss here, e.g. unknown type, then nil will be
282
+ # returned and we'll just not catch that part of the text, which seems
283
+ # like a sensible failure mode.
284
+ element.children.map do |e|
285
+ if e.type == :text || e.type == :codespan
286
+ e.value
287
+ elsif %i{strong em p a}.include?(e.type)
288
+ extract_as_text(e).join("\n")
289
+ elsif e.type == :smart_quote
290
+ quotes[e.value]
291
+ end
292
+ end.join.split("\n")
293
+ end
294
+
269
295
  private
270
296
 
271
297
  ##
@@ -0,0 +1,89 @@
1
+ require 'json'
2
+
3
+ module MarkdownLint
4
+ # SARIF formatter
5
+ #
6
+ # @see https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html
7
+ class SarifFormatter
8
+ class << self
9
+ def generate(rules, results)
10
+ matched_rules_id = results.map { |result| result['rule'] }.uniq
11
+ matched_rules = rules.select { |id, _| matched_rules_id.include?(id) }
12
+ JSON.generate(generate_sarif(matched_rules, results))
13
+ end
14
+
15
+ def generate_sarif(rules, results)
16
+ {
17
+ :'$schema' => 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
18
+ :version => '2.1.0',
19
+ :runs => [
20
+ {
21
+ :tool => {
22
+ :driver => {
23
+ :name => 'Markdown lint',
24
+ :version => MarkdownLint::VERSION,
25
+ :informationUri => 'https://github.com/markdownlint/markdownlint',
26
+ :rules => generate_sarif_rules(rules),
27
+ },
28
+ },
29
+ :results => generate_sarif_results(rules, results),
30
+ }
31
+ ],
32
+ }
33
+ end
34
+
35
+ def generate_sarif_rules(rules)
36
+ rules.map do |id, rule|
37
+ {
38
+ :id => id,
39
+ :name => rule.aliases.first.split('-').map(&:capitalize).join,
40
+ :defaultConfiguration => {
41
+ :level => 'note',
42
+ },
43
+ :properties => {
44
+ :description => rule.description,
45
+ :tags => rule.tags,
46
+ :queryURI => rule.docs_url,
47
+ },
48
+ :shortDescription => {
49
+ :text => rule.description,
50
+ },
51
+ :fullDescription => {
52
+ :text => rule.description,
53
+ },
54
+ :helpUri => rule.docs_url,
55
+ :help => {
56
+ :text => "More info: #{rule.docs_url}",
57
+ :markdown => "[More info](#{rule.docs_url})",
58
+ },
59
+ }
60
+ end
61
+ end
62
+
63
+ def generate_sarif_results(rules, results)
64
+ results.map do |result|
65
+ {
66
+ :ruleId => result['rule'],
67
+ :ruleIndex => rules.find_index { |id, _| id == result['rule'] },
68
+ :message => {
69
+ :text => "#{result['rule']} - #{result['description']}",
70
+ },
71
+ :locations => [
72
+ {
73
+ :physicalLocation => {
74
+ :artifactLocation => {
75
+ :uri => result['filename'],
76
+ :uriBaseId => '%SRCROOT%',
77
+ },
78
+ :region => {
79
+ :startLine => result['line'],
80
+ },
81
+ },
82
+ }
83
+ ],
84
+ }
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
data/lib/mdl/rules.rb CHANGED
@@ -1,3 +1,9 @@
1
+ docs do |id, description|
2
+ url_hash = [id.downcase,
3
+ description.downcase.gsub(/[^a-z]+/, '-')].join('---')
4
+ "https://github.com/markdownlint/markdownlint/blob/main/docs/RULES.md##{url_hash}"
5
+ end
6
+
1
7
  rule 'MD001', 'Header levels should only increment by one level at a time' do
2
8
  tags :headers
3
9
  aliases 'header-increment'
@@ -27,7 +33,7 @@ end
27
33
 
28
34
  rule 'MD003', 'Header style' do
29
35
  # Header styles are things like ### and adding underscores
30
- # See http://daringfireball.net/projects/markdown/syntax#header
36
+ # See https://daringfireball.net/projects/markdown/syntax#header
31
37
  tags :headers
32
38
  aliases 'header-style'
33
39
  # :style can be one of :consistent, :atx, :atx_closed, :setext
@@ -62,7 +68,7 @@ end
62
68
  rule 'MD004', 'Unordered list style' do
63
69
  tags :bullet, :ul
64
70
  aliases 'ul-style'
65
- # :style can be one of :consistent, :asterisk, :plus, :dash
71
+ # :style can be one of :consistent, :asterisk, :plus, :dash, :sublist
66
72
  params :style => :consistent
67
73
  check do |doc|
68
74
  bullets = doc.find_type_elements(:ul).map do |l|
@@ -71,15 +77,30 @@ rule 'MD004', 'Unordered list style' do
71
77
  if bullets.empty?
72
78
  nil
73
79
  else
74
- doc_style = if @params[:style] == :consistent
80
+ doc_style = case @params[:style]
81
+ when :consistent
75
82
  doc.list_style(bullets.first)
83
+ when :sublist
84
+ {}
76
85
  else
77
86
  @params[:style]
78
87
  end
79
- bullets.map do |b|
80
- doc.element_linenumber(b) \
81
- if doc.list_style(b) != doc_style
82
- end.compact
88
+ results = []
89
+ bullets.each do |b|
90
+ if @params[:style] == :sublist
91
+ level = b.options[:element_level]
92
+ if doc_style[level]
93
+ if doc_style[level] != doc.list_style(b)
94
+ results << doc.element_linenumber(b)
95
+ end
96
+ else
97
+ doc_style[level] = doc.list_style(b)
98
+ end
99
+ elsif doc.list_style(b) != doc_style
100
+ results << doc.element_linenumber(b)
101
+ end
102
+ end
103
+ results.compact
83
104
  end
84
105
  end
85
106
  end
@@ -105,7 +126,7 @@ rule 'MD005', 'Inconsistent indentation for list items at the same level' do
105
126
  end
106
127
 
107
128
  rule 'MD006', 'Consider starting bulleted lists at the beginning of the line' do
108
- # Starting at the beginning of the line means that indendation for each
129
+ # Starting at the beginning of the line means that indentation for each
109
130
  # bullet level can be identical.
110
131
  tags :bullet, :ul, :indentation
111
132
  aliases 'ul-start-left'
@@ -119,7 +140,8 @@ end
119
140
  rule 'MD007', 'Unordered list indentation' do
120
141
  tags :bullet, :ul, :indentation
121
142
  aliases 'ul-indent'
122
- params :indent => 2
143
+ # Do not default to < 3, see PR#373 or the comments in RULES.md
144
+ params :indent => 3
123
145
  check do |doc|
124
146
  errors = []
125
147
  indents = doc.find_type(:ul).map do |e|
@@ -139,7 +161,7 @@ end
139
161
  rule 'MD009', 'Trailing spaces' do
140
162
  tags :whitespace
141
163
  aliases 'no-trailing-spaces'
142
- params :br_spaces => 0
164
+ params :br_spaces => 2
143
165
  check do |doc|
144
166
  errors = doc.matching_lines(/\s$/)
145
167
  if params[:br_spaces] > 1
@@ -152,8 +174,20 @@ end
152
174
  rule 'MD010', 'Hard tabs' do
153
175
  tags :whitespace, :hard_tab
154
176
  aliases 'no-hard-tabs'
177
+ params :ignore_code_blocks => false
155
178
  check do |doc|
156
- doc.matching_lines(/\t/)
179
+ # Every line in the document that is part of a code block. Blank lines
180
+ # inside of a code block are acceptable.
181
+ codeblock_lines = doc.find_type_elements(:codeblock).map do |e|
182
+ (doc.element_linenumber(e)..
183
+ doc.element_linenumber(e) + e.value.lines.count).to_a
184
+ end.flatten
185
+
186
+ # Check for lines with hard tab
187
+ hard_tab_lines = doc.matching_lines(/\t/)
188
+ # Remove lines with hard tabs, if they stem from codeblock
189
+ hard_tab_lines -= codeblock_lines if params[:ignore_code_blocks]
190
+ hard_tab_lines
157
191
  end
158
192
  end
159
193
 
@@ -186,7 +220,9 @@ end
186
220
  rule 'MD013', 'Line length' do
187
221
  tags :line_length
188
222
  aliases 'line-length'
189
- params :line_length => 80, :code_blocks => true, :tables => true
223
+ params :line_length => 80, :ignore_code_blocks => false, :code_blocks => true,
224
+ :tables => true
225
+
190
226
  check do |doc|
191
227
  # Every line in the document that is part of a code block.
192
228
  codeblock_lines = doc.find_type_elements(:codeblock).map do |e|
@@ -207,7 +243,14 @@ rule 'MD013', 'Line length' do
207
243
  end
208
244
  end.flatten
209
245
  overlines = doc.matching_lines(/^.{#{@params[:line_length]}}.*\s/)
210
- overlines -= codeblock_lines unless params[:code_blocks]
246
+ if !params[:code_blocks] || params[:ignore_code_blocks]
247
+ overlines -= codeblock_lines
248
+ unless params[:code_blocks]
249
+ warn 'MD013 warning: Parameter :code_blocks is deprecated.'
250
+ warn ' Please replace \":code_blocks => false\" by '\
251
+ '\":ignore_code_blocks => true\" in your configuration.'
252
+ end
253
+ end
211
254
  overlines -= table_lines unless params[:tables]
212
255
  overlines
213
256
  end
@@ -218,7 +261,7 @@ rule 'MD014', 'Dollar signs used before commands without showing output' do
218
261
  aliases 'commands-show-output'
219
262
  check do |doc|
220
263
  doc.find_type_elements(:codeblock).select do |e|
221
- !e.value.empty? and
264
+ !e.value.empty? &&
222
265
  !e.value.split(/\n+/).map { |l| l.match(/^\$\s/) }.include?(nil)
223
266
  end.map { |e| doc.element_linenumber(e) }
224
267
  end
@@ -229,7 +272,7 @@ rule 'MD018', 'No space after hash on atx style header' do
229
272
  aliases 'no-missing-space-atx'
230
273
  check do |doc|
231
274
  doc.find_type_elements(:header).select do |h|
232
- doc.header_style(h) == :atx and doc.element_line(h).match(/^#+[^#\s]/)
275
+ doc.header_style(h) == :atx && doc.element_line(h).match(/^#+[^#\s]/)
233
276
  end.map { |h| doc.element_linenumber(h) }
234
277
  end
235
278
  end
@@ -239,7 +282,7 @@ rule 'MD019', 'Multiple spaces after hash on atx style header' do
239
282
  aliases 'no-multiple-space-atx'
240
283
  check do |doc|
241
284
  doc.find_type_elements(:header).select do |h|
242
- doc.header_style(h) == :atx and doc.element_line(h).match(/^#+\s\s/)
285
+ doc.header_style(h) == :atx && doc.element_line(h).match(/^#+\s\s/)
243
286
  end.map { |h| doc.element_linenumber(h) }
244
287
  end
245
288
  end
@@ -250,8 +293,8 @@ rule 'MD020', 'No space inside hashes on closed atx style header' do
250
293
  check do |doc|
251
294
  doc.find_type_elements(:header).select do |h|
252
295
  doc.header_style(h) == :atx_closed \
253
- and (doc.element_line(h).match(/^#+[^#\s]/) \
254
- or doc.element_line(h).match(/[^#\s\\]#+$/))
296
+ && (doc.element_line(h).match(/^#+[^#\s]/) \
297
+ || doc.element_line(h).match(/[^#\s\\]#+$/))
255
298
  end.map { |h| doc.element_linenumber(h) }
256
299
  end
257
300
  end
@@ -262,8 +305,8 @@ rule 'MD021', 'Multiple spaces inside hashes on closed atx style header' do
262
305
  check do |doc|
263
306
  doc.find_type_elements(:header).select do |h|
264
307
  doc.header_style(h) == :atx_closed \
265
- and (doc.element_line(h).match(/^#+\s\s/) \
266
- or doc.element_line(h).match(/\s\s#+$/))
308
+ && (doc.element_line(h).match(/^#+\s\s/) \
309
+ || doc.element_line(h).match(/\s\s#+$/))
267
310
  end.map { |h| doc.element_linenumber(h) }
268
311
  end
269
312
  end
@@ -297,7 +340,7 @@ rule 'MD022', 'Headers should be surrounded by blank lines' do
297
340
  errors << linenum if line.match(/^\#{1,6}/) && !prev_lines[1].empty?
298
341
  # Next, look for setext style
299
342
  if line.match(/^(-+|=+)\s*$/) && !prev_lines[0].empty?
300
- errors << linenum - 1
343
+ errors << (linenum - 1)
301
344
  end
302
345
  linenum += 1
303
346
  prev_lines << line
@@ -329,7 +372,7 @@ rule 'MD023', 'Headers must start at the beginning of the line' do
329
372
  errors << linenum if line.match(/^\s+\#{1,6}/)
330
373
  # Next, look for setext style
331
374
  if line.match(/^\s+(-+|=+)\s*$/) && !prev_line.empty?
332
- errors << linenum - 1
375
+ errors << (linenum - 1)
333
376
  end
334
377
  linenum += 1
335
378
  prev_line = line
@@ -390,7 +433,7 @@ rule 'MD025', 'Multiple top level headers in the same document' do
390
433
  h[:level] == params[:level]
391
434
  end
392
435
  if !headers.empty? && (doc.element_linenumber(headers[0]) == 1)
393
- headers[1..-1].map { |h| doc.element_linenumber(h) }
436
+ headers[1..].map { |h| doc.element_linenumber(h) }
394
437
  end
395
438
  end
396
439
  end
@@ -415,7 +458,10 @@ rule 'MD027', 'Multiple spaces after blockquote symbol' do
415
458
  errors = []
416
459
  doc.find_type_elements(:blockquote).each do |e|
417
460
  linenum = doc.element_linenumber(e)
418
- lines = doc.extract_text(e, /^\s*> /)
461
+ lines = doc.extract_as_text(e)
462
+ # Handle first line specially as whitespace is stripped from the text
463
+ # element
464
+ errors << linenum if doc.element_line(e).match(/^\s*> /)
419
465
  lines.each do |line|
420
466
  errors << linenum if line.start_with?(' ')
421
467
  linenum += 1
@@ -438,7 +484,7 @@ rule 'MD028', 'Blank line inside blockquote' do
438
484
  # The current location is the start of the second blockquote, so the
439
485
  # line before will be a blank line in between the two, or at least the
440
486
  # lowest blank line if there are more than one.
441
- errors << e.options[:location] - 1
487
+ errors << (e.options[:location] - 1)
442
488
  end
443
489
  check_blockquote(errors, e.children)
444
490
  end
@@ -490,8 +536,13 @@ rule 'MD030', 'Spaces after list markers' do
490
536
  # the items in it have multiple paragraphs/other block items.
491
537
  srule = items.map { |i| i.children.length }.max > 1 ? 'multi' : 'single'
492
538
  items.each do |i|
493
- actual_spaces = doc.element_line(i).gsub(/^> /, '')
494
- .match(/^\s*\S+(\s+)/)[1].length
539
+ line = doc.element_line(i)
540
+ # See #278 - sometimes we think non-printable characters are list
541
+ # items even if they are not, so this ignore those and prevents
542
+ # us from crashing
543
+ next if line.empty?
544
+
545
+ actual_spaces = line.gsub(/^> /, '').match(/^\s*\S+(\s+)/)[1].length
495
546
  required_spaces = params["#{list_type}_#{srule}".to_sym]
496
547
  errors << doc.element_linenumber(i) if required_spaces != actual_spaces
497
548
  end
@@ -548,7 +599,7 @@ rule 'MD032', 'Lists should be surrounded by blank lines' do
548
599
  unless in_code
549
600
  list_marker = line.strip.match(/^([*+\-]|(\d+\.))\s/)
550
601
  if list_marker && !in_list && !prev_line.match(/^($|\s)/)
551
- errors << linenum + 1
602
+ errors << (linenum + 1)
552
603
  elsif !list_marker && in_list && !line.match(/^($|\s)/)
553
604
  errors << linenum
554
605
  end
@@ -571,8 +622,14 @@ end
571
622
  rule 'MD033', 'Inline HTML' do
572
623
  tags :html
573
624
  aliases 'no-inline-html'
625
+ params :allowed_elements => ''
574
626
  check do |doc|
575
627
  doc.element_linenumbers(doc.find_type(:html_element))
628
+ allowed = params[:allowed_elements].delete(" \t\r\n").downcase.split(',')
629
+ errors = doc.find_type_elements(:html_element).reject do |e|
630
+ allowed.include?(e.value)
631
+ end
632
+ doc.element_linenumbers(errors)
576
633
  end
577
634
  end
578
635
 
@@ -648,7 +705,7 @@ rule 'MD038', 'Spaces inside code span elements' do
648
705
  # block that happen to be parsed as code spans.
649
706
  doc.element_linenumbers(
650
707
  doc.find_type_elements(:codespan).select do |i|
651
- i.value.match(/(^\s|\s$)/) and !i.value.include?("\n")
708
+ i.value.match(/(^\s|\s$)/) && !i.value.include?("\n")
652
709
  end,
653
710
  )
654
711
  end
@@ -660,8 +717,8 @@ rule 'MD039', 'Spaces inside link text' do
660
717
  check do |doc|
661
718
  doc.element_linenumbers(
662
719
  doc.find_type_elements(:a).reject { |e| e.children.empty? }.select do |e|
663
- e.children.first.type == :text && e.children.last.type == :text and (
664
- e.children.first.value.start_with?(' ') or
720
+ e.children.first.type == :text && e.children.last.type == :text && (
721
+ e.children.first.value.start_with?(' ') ||
665
722
  e.children.last.value.end_with?(' '))
666
723
  end,
667
724
  )
@@ -675,7 +732,7 @@ rule 'MD040', 'Fenced code blocks should have a language specified' do
675
732
  # Kramdown parses code blocks with language settings as code blocks with
676
733
  # the class attribute set to language-languagename.
677
734
  doc.element_linenumbers(doc.find_type_elements(:codeblock).select do |i|
678
- !i.attr['class'].to_s.start_with?('language-') and
735
+ !i.attr['class'].to_s.start_with?('language-') &&
679
736
  !doc.element_line(i).start_with?(' ')
680
737
  end)
681
738
  end
@@ -731,3 +788,103 @@ rule 'MD046', 'Code block style' do
731
788
  )
732
789
  end
733
790
  end
791
+
792
+ rule 'MD047', 'File should end with a single newline character' do
793
+ tags :blank_lines
794
+ aliases 'single-trailing-newline'
795
+ check do |doc|
796
+ error_lines = []
797
+ last_line = doc.lines[-1]
798
+ error_lines.push(doc.lines.length) unless last_line.nil? || last_line.empty?
799
+ error_lines
800
+ end
801
+ end
802
+
803
+ rule 'MD055', 'Table row doesn\'t begin/end with pipes' do
804
+ tags :tables
805
+ aliases 'table-rows-start-and-end-with-pipes'
806
+ check do |doc|
807
+ error_lines = []
808
+ tables = doc.find_type_elements(:table)
809
+ lines = doc.lines
810
+
811
+ tables.each do |table|
812
+ table_pos = table.options[:location] - 1
813
+ table_rows = get_table_rows(lines, table_pos)
814
+
815
+ table_rows.each_with_index do |line, index|
816
+ if line.length < 2 || line[0] != '|' || line[-1] != '|'
817
+ error_lines << (table_pos + index + 1)
818
+ end
819
+ end
820
+ end
821
+
822
+ error_lines
823
+ end
824
+ end
825
+
826
+ rule 'MD056', 'Table has inconsistent number of columns' do
827
+ tags :tables
828
+ aliases 'inconsistent-columns-in-table'
829
+ check do |doc|
830
+ error_lines = []
831
+ tables = doc.find_type_elements(:table)
832
+ lines = doc.lines
833
+
834
+ tables.each do |table|
835
+ table_pos = table.options[:location] - 1
836
+ table_rows = get_table_rows(lines, table_pos)
837
+
838
+ num_headings = number_of_columns_in_a_table_row(lines[table_pos])
839
+
840
+ table_rows.each_with_index do |line, index|
841
+ if number_of_columns_in_a_table_row(line) != num_headings
842
+ error_lines << (table_pos + index + 1)
843
+ end
844
+ end
845
+ end
846
+
847
+ error_lines
848
+ end
849
+ end
850
+
851
+ rule 'MD057', 'Table has missing or invalid header separation (second row)' do
852
+ tags :tables
853
+ aliases 'table-invalid-second-row'
854
+ check do |doc|
855
+ error_lines = []
856
+ tables = doc.find_type_elements(:table)
857
+ lines = doc.lines
858
+
859
+ tables.each do |table|
860
+ second_row = ''
861
+
862
+ # line number of table start (1-indexed)
863
+ # which is equal to second row's index (0-indexed)
864
+ line_num = table.options[:location]
865
+ second_row = lines[line_num] if line_num < lines.length
866
+
867
+ # This pattern matches if
868
+ # 1) The row starts and stops with | characters
869
+ # 2) Only consists of characters '|', '-', ':' and whitespace
870
+ # 3) Each section between the separators (i.e. '|')
871
+ # a) has at least three consecutive dashes
872
+ # b) can have whitespace at the beginning or the end
873
+ # c) can have colon before and/or after dashes (for alignment)
874
+ # Some examples:
875
+ # |-----|----|-------| --> matches
876
+ # |:---:|:---|-------| --> matches
877
+ # | :------: | ----| --> matches
878
+ # | - - - | - - - | --> does NOT match
879
+ # |::---| --> does NOT match
880
+ # |----:|:--|----| --> does NOT match
881
+ pattern = /^(\|\s*:?-{3,}:?\s*)+\|$/
882
+ unless second_row.match(pattern)
883
+ # Second row is not in the form described by the pattern
884
+ error_lines << (line_num + 1)
885
+ end
886
+ end
887
+
888
+ error_lines
889
+ end
890
+ end
data/lib/mdl/ruleset.rb CHANGED
@@ -3,9 +3,11 @@ module MarkdownLint
3
3
  class Rule
4
4
  attr_accessor :id, :description
5
5
 
6
- def initialize(id, description, block)
6
+ def initialize(id, description, fallback_docs: nil, &block)
7
7
  @id = id
8
8
  @description = description
9
+ @generate_docs = fallback_docs
10
+ @docs_overridden = false
9
11
  @aliases = []
10
12
  @tags = []
11
13
  @params = {}
@@ -31,6 +33,82 @@ module MarkdownLint
31
33
  @params.update(params) unless params.nil?
32
34
  @params
33
35
  end
36
+
37
+ def docs(url = nil, &block)
38
+ if block_given? != url.nil?
39
+ raise ArgumentError, 'Give either a URL or a block, not both'
40
+ end
41
+
42
+ raise 'A docs url is already set within this rule' if @docs_overridden
43
+
44
+ @generate_docs = block_given? ? block : lambda { |_, _| url }
45
+ @docs_overridden = true
46
+ end
47
+
48
+ def docs_url
49
+ @generate_docs&.call(id, description)
50
+ end
51
+
52
+ # This method calculates the number of columns in a table row
53
+ #
54
+ # @param [String] table_row A row of the table in question.
55
+ # @return [Numeric] Number of columns in the row
56
+ def number_of_columns_in_a_table_row(table_row)
57
+ columns = table_row.strip.split('|')
58
+
59
+ if columns.empty?
60
+ # The stripped line consists of zero or more pipe characters
61
+ # and nothing more.
62
+ #
63
+ # Examples of stripped rows:
64
+ # '||' --> one column
65
+ # '|||' --> two columns
66
+ # '|' --> zero columns
67
+ [0, table_row.count('|') - 1].max
68
+ else
69
+ # Number of columns is the number of splited
70
+ # segments with pipe separator. The first segment
71
+ # is ignored when it's empty string because
72
+ # someting like '|1|2|' is split into ['', '1', '2']
73
+ # when using split('|') function.
74
+ #
75
+ # Some examples:
76
+ # '|foo|bar|' --> two columns
77
+ # ' |foo|bar|' --> two columns
78
+ # '|foo|bar' --> two columns
79
+ # 'foo|bar' --> two columns
80
+ columns.size - (columns[0].empty? ? 1 : 0)
81
+ end
82
+ end
83
+
84
+ # This method returns all the rows of a table
85
+ #
86
+ # @param [Array<String>] lines Lines of a doc as an array
87
+ # @param [Numeric] pos Position/index of the table in the array
88
+ # @return [Array<String>] Rows of the table in an array
89
+ def get_table_rows(lines, pos)
90
+ table_rows = []
91
+ while pos < lines.length
92
+ line = lines[pos]
93
+
94
+ # If the previous line is a table and the current line
95
+ # 1) includes pipe character
96
+ # 2) does not start with code block identifiers
97
+ # a) >= 4 spaces
98
+ # b) < 4 spaces and ``` right after
99
+ #
100
+ # it is possibly a table row
101
+ unless line.include?('|') && !line.start_with?(' ') &&
102
+ !line.strip.start_with?('```')
103
+ break
104
+ end
105
+
106
+ table_rows << line
107
+ pos += 1
108
+ end
109
+
110
+ table_rows
111
+ end
34
112
  end
35
113
 
36
114
  # defines a ruleset
@@ -42,7 +120,8 @@ module MarkdownLint
42
120
  end
43
121
 
44
122
  def rule(id, description, &block)
45
- @rules[id] = Rule.new(id, description, block)
123
+ @rules[id] =
124
+ Rule.new(id, description, :fallback_docs => @fallback_docs, &block)
46
125
  end
47
126
 
48
127
  def load(rules_file)
@@ -50,6 +129,14 @@ module MarkdownLint
50
129
  @rules
51
130
  end
52
131
 
132
+ def docs(url = nil, &block)
133
+ if block_given? != url.nil?
134
+ raise ArgumentError, 'Give either a URL or a block, not both'
135
+ end
136
+
137
+ @fallback_docs = block_given? ? block : lambda { |_, _| url }
138
+ end
139
+
53
140
  def load_default
54
141
  load(File.expand_path('rules.rb', __dir__))
55
142
  end
data/lib/mdl/style.rb CHANGED
@@ -58,7 +58,7 @@ module MarkdownLint
58
58
  warn "#{style_file} does not appear to be a built-in style." +
59
59
  ' If you meant to pass in your own style file, it must contain' +
60
60
  " a '/' or end in '.rb'. See https://github.com/markdownlint/" +
61
- 'markdownlint/blob/master/docs/configuration.md'
61
+ 'markdownlint/blob/main/docs/configuration.md'
62
62
  exit(1)
63
63
  end
64
64
  style_file = tmp
@@ -9,3 +9,7 @@ rule 'MD035', :style => '---'
9
9
  # Inline HTML - this isn't forbidden by the style guide, and raw HTML use is
10
10
  # explicitly mentioned in the 'email automatic links' section.
11
11
  exclude_rule 'MD033'
12
+
13
+ # File should end with a single newline character
14
+ # this isn't forbidden by the style guide
15
+ exclude_rule 'MD047'
@@ -8,3 +8,4 @@ exclude_rule 'MD033' # Inline HTML
8
8
  exclude_rule 'MD034' # Bare URL used
9
9
  exclude_rule 'MD040' # Fenced code blocks should have a language specified
10
10
  exclude_rule 'MD041' # First line in file should be a top level header
11
+ exclude_rule 'MD047' # File should end with a single newline character
data/lib/mdl/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module MarkdownLint
2
- VERSION = '0.11.0'.freeze
2
+ VERSION = '0.13.0'.freeze
3
3
  end
data/lib/mdl.rb CHANGED
@@ -1,3 +1,4 @@
1
+ require_relative 'mdl/formatters/sarif'
1
2
  require_relative 'mdl/cli'
2
3
  require_relative 'mdl/config'
3
4
  require_relative 'mdl/doc'
@@ -66,7 +67,8 @@ module MarkdownLint
66
67
  Dir.chdir(filename) do
67
68
  cli.cli_arguments[i] =
68
69
  Mixlib::ShellOut.new("git ls-files '*.md' '*.markdown'")
69
- .run_command.stdout.lines.map(&:strip)
70
+ .run_command.stdout.lines
71
+ .map { |m| File.join(filename, m.strip) }
70
72
  end
71
73
  else
72
74
  cli.cli_arguments[i] = Dir["#{filename}/**/*.{md,markdown}"]
@@ -77,8 +79,15 @@ module MarkdownLint
77
79
 
78
80
  status = 0
79
81
  results = []
82
+ docs_to_print = []
80
83
  cli.cli_arguments.each do |filename|
81
84
  puts "Checking #{filename}..." if Config[:verbose]
85
+ unless filename == '-' || File.exist?(filename)
86
+ warn(
87
+ "#{Errno::ENOENT}: No such file or directory - #{filename}",
88
+ )
89
+ exit 3
90
+ end
82
91
  doc = Doc.new_from_file(filename, Config[:ignore_front_matter])
83
92
  filename = '(stdin)' if filename == '-'
84
93
  if Config[:show_kramdown_warnings]
@@ -95,31 +104,57 @@ module MarkdownLint
95
104
  status = 1
96
105
  error_lines.each do |line|
97
106
  line += doc.offset # Correct line numbers for any yaml front matter
98
- if Config[:json]
107
+ if Config[:json] || Config[:sarif]
99
108
  results << {
100
109
  'filename' => filename,
101
110
  'line' => line,
102
111
  'rule' => id,
103
112
  'aliases' => rule.aliases,
104
113
  'description' => rule.description,
114
+ 'docs' => rule.docs_url,
105
115
  }
106
- elsif Config[:show_aliases]
107
- puts "#{filename}:#{line}: #{rule.aliases.first || id} " +
108
- rule.description.to_s
109
116
  else
110
- puts "#{filename}:#{line}: #{id} #{rule.description}"
117
+ linked_id = linkify(printable_id(rule), rule.docs_url)
118
+ puts "#{filename}:#{line}: #{linked_id} " + rule.description.to_s
111
119
  end
112
120
  end
121
+
122
+ # If we're not in JSON or SARIF mode (URLs are in the object), and we
123
+ # cannot make real links (checking if we have a TTY is an OK heuristic
124
+ # for that) then, instead of making the output ugly with long URLs, we
125
+ # print them at the end. And of course we only want to print each URL
126
+ # once.
127
+ if !Config[:json] && !Config[:sarif] &&
128
+ !$stdout.tty? && !docs_to_print.include?(rule)
129
+ docs_to_print << rule
130
+ end
113
131
  end
114
132
  end
115
133
 
116
134
  if Config[:json]
117
135
  require 'json'
118
136
  puts JSON.generate(results)
119
- elsif status != 0
120
- puts "\nA detailed description of the rules is available at " +
121
- 'https://github.com/markdownlint/markdownlint/blob/master/docs/RULES.md'
137
+ elsif Config[:sarif]
138
+ puts SarifFormatter.generate(rules, results)
139
+ elsif docs_to_print.any?
140
+ puts "\nFurther documentation is available for these failures:"
141
+ docs_to_print.each do |rule|
142
+ puts " - #{printable_id(rule)}: #{rule.docs_url}"
143
+ end
122
144
  end
123
145
  exit status
124
146
  end
147
+
148
+ def self.printable_id(rule)
149
+ return rule.aliases.first if Config[:show_aliases] && rule.aliases.any?
150
+
151
+ rule.id
152
+ end
153
+
154
+ # Creates hyperlinks in terminal emulators, if available: https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
155
+ def self.linkify(text, url)
156
+ return text unless $stdout.tty? && url
157
+
158
+ "\e]8;;#{url}\e\\#{text}\e]8;;\e\\"
159
+ end
125
160
  end
data/mdl.gemspec CHANGED
@@ -9,8 +9,9 @@ Gem::Specification.new do |spec|
9
9
  spec.email = ['mark@mivok.net']
10
10
  spec.summary = 'Markdown lint tool'
11
11
  spec.description = 'Style checker/lint tool for markdown files'
12
- spec.homepage = 'http://github.com/markdownlint/markdownlint'
12
+ spec.homepage = 'https://github.com/markdownlint/markdownlint'
13
13
  spec.license = 'MIT'
14
+ spec.metadata['rubygems_mfa_required'] = 'true'
14
15
 
15
16
  spec.files = %w{LICENSE.txt Gemfile} + Dir.glob('*.gemspec') +
16
17
  Dir.glob('lib/**/*')
@@ -18,7 +19,7 @@ Gem::Specification.new do |spec|
18
19
  spec.executables = %w{mdl}
19
20
  spec.require_paths = ['lib']
20
21
 
21
- spec.required_ruby_version = '>= 2.4'
22
+ spec.required_ruby_version = '>= 2.7'
22
23
 
23
24
  spec.add_dependency 'kramdown', '~> 2.3'
24
25
  spec.add_dependency 'kramdown-parser-gfm', '~> 1.1'
@@ -30,5 +31,5 @@ Gem::Specification.new do |spec|
30
31
  spec.add_development_dependency 'minitest', '~> 5.9'
31
32
  spec.add_development_dependency 'pry', '~> 0.10'
32
33
  spec.add_development_dependency 'rake', '>= 11.2', '< 14'
33
- spec.add_development_dependency 'rubocop', '>= 0.49.0'
34
+ spec.add_development_dependency 'rubocop', '~> 1.28.1'
34
35
  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.11.0
4
+ version: 0.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mark Harrison
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-08-23 00:00:00.000000000 Z
11
+ date: 2023-10-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: kramdown
@@ -164,16 +164,16 @@ dependencies:
164
164
  name: rubocop
165
165
  requirement: !ruby/object:Gem::Requirement
166
166
  requirements:
167
- - - ">="
167
+ - - "~>"
168
168
  - !ruby/object:Gem::Version
169
- version: 0.49.0
169
+ version: 1.28.1
170
170
  type: :development
171
171
  prerelease: false
172
172
  version_requirements: !ruby/object:Gem::Requirement
173
173
  requirements:
174
- - - ">="
174
+ - - "~>"
175
175
  - !ruby/object:Gem::Version
176
- version: 0.49.0
176
+ version: 1.28.1
177
177
  description: Style checker/lint tool for markdown files
178
178
  email:
179
179
  - mark@mivok.net
@@ -189,6 +189,7 @@ files:
189
189
  - lib/mdl/cli.rb
190
190
  - lib/mdl/config.rb
191
191
  - lib/mdl/doc.rb
192
+ - lib/mdl/formatters/sarif.rb
192
193
  - lib/mdl/kramdown_parser.rb
193
194
  - lib/mdl/rules.rb
194
195
  - lib/mdl/ruleset.rb
@@ -199,11 +200,12 @@ files:
199
200
  - lib/mdl/styles/relaxed.rb
200
201
  - lib/mdl/version.rb
201
202
  - mdl.gemspec
202
- homepage: http://github.com/markdownlint/markdownlint
203
+ homepage: https://github.com/markdownlint/markdownlint
203
204
  licenses:
204
205
  - MIT
205
- metadata: {}
206
- post_install_message:
206
+ metadata:
207
+ rubygems_mfa_required: 'true'
208
+ post_install_message:
207
209
  rdoc_options: []
208
210
  require_paths:
209
211
  - lib
@@ -211,15 +213,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
211
213
  requirements:
212
214
  - - ">="
213
215
  - !ruby/object:Gem::Version
214
- version: '2.4'
216
+ version: '2.7'
215
217
  required_rubygems_version: !ruby/object:Gem::Requirement
216
218
  requirements:
217
219
  - - ">="
218
220
  - !ruby/object:Gem::Version
219
221
  version: '0'
220
222
  requirements: []
221
- rubygems_version: 3.1.2
222
- signing_key:
223
+ rubygems_version: 3.3.15
224
+ signing_key:
223
225
  specification_version: 4
224
226
  summary: Markdown lint tool
225
227
  test_files: []