mdl 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/mdl/cli.rb +9 -3
- data/lib/mdl/doc.rb +31 -5
- data/lib/mdl/formatters/sarif.rb +89 -0
- data/lib/mdl/rules.rb +189 -32
- data/lib/mdl/ruleset.rb +89 -2
- data/lib/mdl/style.rb +1 -1
- data/lib/mdl/styles/cirosantilli.rb +4 -0
- data/lib/mdl/styles/relaxed.rb +1 -0
- data/lib/mdl/version.rb +1 -1
- data/lib/mdl.rb +44 -9
- data/mdl.gemspec +4 -3
- metadata +15 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bb98b688ab9d9dc7bac3c169f20fefc7c0cb27ce5bf574bec52053d592b7a9c7
|
4
|
+
data.tar.gz: 873df51b4011d23617c1d73ab47a1928765fee0502b6f58853e2932bb3d5ec59
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
149
|
-
if parts.
|
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
|
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
|
-
|
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.
|
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.
|
106
|
-
nested_except = [nested_except] if nested_except.
|
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
|
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 =
|
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
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
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
|
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
|
-
|
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 =>
|
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
|
-
|
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, :
|
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
|
-
|
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?
|
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
|
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
|
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
|
-
|
254
|
-
|
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
|
-
|
266
|
-
|
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
|
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.
|
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
|
-
|
494
|
-
|
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$)/)
|
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
|
664
|
-
e.children.first.value.start_with?(' ')
|
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-')
|
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] =
|
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/
|
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'
|
data/lib/mdl/styles/relaxed.rb
CHANGED
@@ -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
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
|
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
|
-
|
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
|
120
|
-
puts
|
121
|
-
|
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 = '
|
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.
|
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', '
|
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.
|
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:
|
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:
|
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:
|
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:
|
203
|
+
homepage: https://github.com/markdownlint/markdownlint
|
203
204
|
licenses:
|
204
205
|
- MIT
|
205
|
-
metadata:
|
206
|
-
|
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.
|
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.
|
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: []
|