mdl 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +13 -0
- data/.travis.yml +7 -0
- data/Gemfile +2 -0
- data/LICENSE.txt +22 -0
- data/README.md +79 -0
- data/Rakefile +8 -0
- data/bin/mdl +10 -0
- data/docs/RULES.md +609 -0
- data/docs/creating_rules.md +83 -0
- data/docs/creating_styles.md +47 -0
- data/example/markdown_spec.md +897 -0
- data/lib/mdl.rb +72 -0
- data/lib/mdl/cli.rb +89 -0
- data/lib/mdl/config.rb +9 -0
- data/lib/mdl/doc.rb +252 -0
- data/lib/mdl/kramdown_parser.rb +29 -0
- data/lib/mdl/rules.rb +393 -0
- data/lib/mdl/ruleset.rb +47 -0
- data/lib/mdl/style.rb +50 -0
- data/lib/mdl/styles/all.rb +1 -0
- data/lib/mdl/styles/cirosantilli.rb +6 -0
- data/lib/mdl/styles/default.rb +1 -0
- data/lib/mdl/styles/relaxed.rb +6 -0
- data/lib/mdl/version.rb +3 -0
- data/mdl.gemspec +31 -0
- data/test/rule_tests/atx_closed_header_spacing.md +17 -0
- data/test/rule_tests/atx_header_spacing.md +5 -0
- data/test/rule_tests/blockquote_blank_lines.md +31 -0
- data/test/rule_tests/blockquote_spaces.md +21 -0
- data/test/rule_tests/bulleted_list_2_space_indent.md +6 -0
- data/test/rule_tests/bulleted_list_2_space_indent_style.rb +2 -0
- data/test/rule_tests/bulleted_list_4_space_indent.md +3 -0
- data/test/rule_tests/bulleted_list_not_at_beginning_of_line.md +14 -0
- data/test/rule_tests/code_block_dollar.md +22 -0
- data/test/rule_tests/consecutive_blank_lines.md +11 -0
- data/test/rule_tests/consistent_bullet_styles_asterisk.md +3 -0
- data/test/rule_tests/consistent_bullet_styles_dash.md +3 -0
- data/test/rule_tests/consistent_bullet_styles_plus.md +3 -0
- data/test/rule_tests/empty_doc.md +0 -0
- data/test/rule_tests/fenced_code_blocks.md +21 -0
- data/test/rule_tests/first_header_bad_atx.md +1 -0
- data/test/rule_tests/first_header_bad_setext.md +2 -0
- data/test/rule_tests/first_header_good_atx.md +1 -0
- data/test/rule_tests/first_header_good_setext.md +2 -0
- data/test/rule_tests/header_duplicate_content.md +11 -0
- data/test/rule_tests/header_multiple_toplevel.md +3 -0
- data/test/rule_tests/header_mutliple_h1_no_toplevel.md +5 -0
- data/test/rule_tests/header_trailing_punctuation.md +11 -0
- data/test/rule_tests/header_trailing_punctuation_customized.md +14 -0
- data/test/rule_tests/header_trailing_punctuation_customized_style.rb +2 -0
- data/test/rule_tests/headers_bad.md +7 -0
- data/test/rule_tests/headers_good.md +5 -0
- data/test/rule_tests/headers_surrounding_space_atx.md +9 -0
- data/test/rule_tests/headers_surrounding_space_setext.md +15 -0
- data/test/rule_tests/headers_with_spaces_at_the_beginning.md +9 -0
- data/test/rule_tests/inconsistent_bullet_indent_same_level.md +4 -0
- data/test/rule_tests/inconsistent_bullet_styles_asterisk.md +3 -0
- data/test/rule_tests/inconsistent_bullet_styles_dash.md +3 -0
- data/test/rule_tests/inconsistent_bullet_styles_plus.md +3 -0
- data/test/rule_tests/incorrect_bullet_style_asterisk.md +3 -0
- data/test/rule_tests/incorrect_bullet_style_asterisk_style.rb +2 -0
- data/test/rule_tests/incorrect_bullet_style_dash.md +3 -0
- data/test/rule_tests/incorrect_bullet_style_dash_style.rb +2 -0
- data/test/rule_tests/incorrect_bullet_style_plus.md +3 -0
- data/test/rule_tests/incorrect_bullet_style_plus_style.rb +2 -0
- data/test/rule_tests/incorrect_header_atx.md +6 -0
- data/test/rule_tests/incorrect_header_atx_closed.md +6 -0
- data/test/rule_tests/incorrect_header_atx_closed_style.rb +2 -0
- data/test/rule_tests/incorrect_header_atx_style.rb +2 -0
- data/test/rule_tests/incorrect_header_setext.md +6 -0
- data/test/rule_tests/incorrect_header_setext_style.rb +2 -0
- data/test/rule_tests/long_lines.md +3 -0
- data/test/rule_tests/long_lines_100.md +7 -0
- data/test/rule_tests/long_lines_100_style.rb +2 -0
- data/test/rule_tests/mixed_header_types_atx.md +6 -0
- data/test/rule_tests/mixed_header_types_atx_closed.md +6 -0
- data/test/rule_tests/mixed_header_types_setext.md +6 -0
- data/test/rule_tests/ordered_list_item_prefix.md +13 -0
- data/test/rule_tests/ordered_list_item_prefix_ordered.md +13 -0
- data/test/rule_tests/ordered_list_item_prefix_ordered_style.rb +2 -0
- data/test/rule_tests/reversed_link.md +7 -0
- data/test/rule_tests/spaces_after_list_marker.md +74 -0
- data/test/rule_tests/spaces_after_list_marker_style.rb +3 -0
- data/test/rule_tests/whitespace issues.md +3 -0
- data/test/setup_tests.rb +5 -0
- data/test/test_ruledocs.rb +45 -0
- data/test/test_rules.rb +56 -0
- data/tools/README.md +3 -0
- data/tools/test_location.rb +20 -0
- data/tools/view_markdown.rb +11 -0
- metadata +314 -0
data/lib/mdl/rules.rb
ADDED
@@ -0,0 +1,393 @@
|
|
1
|
+
rule "MD001", "Header levels should only increment by one level at a time" do
|
2
|
+
tags :headers
|
3
|
+
check do |doc|
|
4
|
+
headers = doc.find_type(:header)
|
5
|
+
old_level = nil
|
6
|
+
errors = []
|
7
|
+
headers.each do |h|
|
8
|
+
if old_level and h[:level] > old_level + 1
|
9
|
+
errors << h[:location]
|
10
|
+
end
|
11
|
+
old_level = h[:level]
|
12
|
+
end
|
13
|
+
errors
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
rule "MD002", "First header should be a h1 header" do
|
18
|
+
tags :headers
|
19
|
+
check do |doc|
|
20
|
+
first_header = doc.find_type(:header).first
|
21
|
+
[first_header[:location]] if first_header and first_header[:level] != 1
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
rule "MD003", "Header style" do
|
26
|
+
# Header styles are things like ### and adding underscores
|
27
|
+
# See http://daringfireball.net/projects/markdown/syntax#header
|
28
|
+
tags :headers
|
29
|
+
# :style can be one of :consistent, :atx, :atx_closed, :setext
|
30
|
+
params :style => :consistent
|
31
|
+
check do |doc|
|
32
|
+
headers = doc.find_type_elements(:header)
|
33
|
+
if headers.empty?
|
34
|
+
nil
|
35
|
+
else
|
36
|
+
if @params[:style] == :consistent
|
37
|
+
doc_style = doc.header_style(headers.first)
|
38
|
+
else
|
39
|
+
doc_style = @params[:style]
|
40
|
+
end
|
41
|
+
headers.map { |h| doc.element_linenumber(h) \
|
42
|
+
if doc.header_style(h) != doc_style }.compact
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
rule "MD004", "Unordered list style" do
|
48
|
+
tags :bullet, :ul
|
49
|
+
# :style can be one of :consistent, :asterisk, :plus, :dash
|
50
|
+
params :style => :consistent
|
51
|
+
check do |doc|
|
52
|
+
bullets = doc.find_type_elements(:ul).map {|l|
|
53
|
+
doc.find_type_elements(:li, false, l.children)}.flatten
|
54
|
+
if bullets.empty?
|
55
|
+
nil
|
56
|
+
else
|
57
|
+
if @params[:style] == :consistent
|
58
|
+
doc_style = doc.list_style(bullets.first)
|
59
|
+
else
|
60
|
+
doc_style = @params[:style]
|
61
|
+
end
|
62
|
+
bullets.map { |b| doc.element_linenumber(b) \
|
63
|
+
if doc.list_style(b) != doc_style }.compact
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
rule "MD005", "Inconsistent indentation for list items at the same level" do
|
69
|
+
tags :bullet, :ul, :indentation
|
70
|
+
check do |doc|
|
71
|
+
bullets = doc.find_type(:li)
|
72
|
+
errors = []
|
73
|
+
indent_levels = []
|
74
|
+
bullets.each do |b|
|
75
|
+
indent_level = doc.indent_for(doc.element_line(b))
|
76
|
+
if indent_levels[b[:element_level]].nil?
|
77
|
+
indent_levels[b[:element_level]] = indent_level
|
78
|
+
end
|
79
|
+
if indent_level != indent_levels[b[:element_level]]
|
80
|
+
errors << doc.element_linenumber(b)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
errors
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
rule "MD006", "Consider starting bulleted lists at the beginning of the line" do
|
88
|
+
# Starting at the beginning of the line means that indendation for each
|
89
|
+
# bullet level can be identical.
|
90
|
+
tags :bullet, :ul, :indentation
|
91
|
+
check do |doc|
|
92
|
+
doc.find_type(:ul, false).select{
|
93
|
+
|e| doc.indent_for(doc.element_line(e)) != 0 }.map{ |e| e[:location] }
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
rule "MD007", "Unordered list indentation" do
|
98
|
+
tags :bullet, :ul, :indentation
|
99
|
+
params :indent => 2
|
100
|
+
check do |doc|
|
101
|
+
indents = []
|
102
|
+
errors = []
|
103
|
+
indents = doc.find_type(:ul).map {
|
104
|
+
|e| [doc.indent_for(doc.element_line(e)), doc.element_linenumber(e)] }
|
105
|
+
curr_indent = indents[0][0] unless indents.empty?
|
106
|
+
indents.each do |indent, linenum|
|
107
|
+
if indent > curr_indent and indent - curr_indent != @params[:indent]
|
108
|
+
errors << linenum
|
109
|
+
end
|
110
|
+
curr_indent = indent
|
111
|
+
end
|
112
|
+
errors
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
rule "MD009", "Trailing spaces" do
|
117
|
+
tags :whitespace
|
118
|
+
check do |doc|
|
119
|
+
doc.matching_lines(/\s$/)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
rule "MD010", "Hard tabs" do
|
124
|
+
tags :whitespace, :hard_tab
|
125
|
+
check do |doc|
|
126
|
+
doc.matching_lines(/\t/)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
rule "MD011", "Reversed link syntax" do
|
131
|
+
tags :links
|
132
|
+
check do |doc|
|
133
|
+
doc.matching_text_element_lines(/\([^)]+\)\[[^\]]+\]/)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
rule "MD012", "Multiple consecutive blank lines" do
|
138
|
+
tags :whitespace, :blank_lines
|
139
|
+
check do |doc|
|
140
|
+
# Every line in the document that is part of a code block. Blank lines
|
141
|
+
# inside of a code block are acceptable.
|
142
|
+
codeblock_lines = doc.find_type_elements(:codeblock).map{
|
143
|
+
|e| (doc.element_linenumber(e)..
|
144
|
+
doc.element_linenumber(e) + e.value.count('\n') - 1).to_a }.flatten
|
145
|
+
blank_lines = doc.matching_lines(/^\s*$/)
|
146
|
+
cons_blank_lines = blank_lines.each_cons(2).select{
|
147
|
+
|p, n| n - p == 1}.map{|p, n| n}
|
148
|
+
cons_blank_lines - codeblock_lines
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
rule "MD013", "Line length" do
|
153
|
+
tags :line_length
|
154
|
+
params :line_length => 80
|
155
|
+
check do |doc|
|
156
|
+
doc.matching_lines(/^.{#{@params[:line_length]}}.*\s/)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
rule "MD014", "Dollar signs used before commands without showing output" do
|
161
|
+
tags :code
|
162
|
+
check do |doc|
|
163
|
+
doc.find_type_elements(:codeblock).select{
|
164
|
+
|e| not e.value.split(/\n+/).map{|l| l.match(/^\$\s/)}.include?(nil)
|
165
|
+
}.map{|e| doc.element_linenumber(e)}
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
rule "MD018", "No space after hash on atx style header" do
|
170
|
+
tags :headers, :atx, :spaces
|
171
|
+
check do |doc|
|
172
|
+
doc.find_type_elements(:header).select do |h|
|
173
|
+
doc.header_style(h) == :atx and doc.element_line(h).match(/^#+[^#\s]/)
|
174
|
+
end.map { |h| doc.element_linenumber(h) }
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
rule "MD019", "Multiple spaces after hash on atx style header" do
|
179
|
+
tags :headers, :atx, :spaces
|
180
|
+
check do |doc|
|
181
|
+
doc.find_type_elements(:header).select do |h|
|
182
|
+
doc.header_style(h) == :atx and doc.element_line(h).match(/^#+\s\s/)
|
183
|
+
end.map { |h| doc.element_linenumber(h) }
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
rule "MD020", "No space inside hashes on closed atx style header" do
|
188
|
+
tags :headers, :atx_closed, :spaces
|
189
|
+
check do |doc|
|
190
|
+
doc.find_type_elements(:header).select do |h|
|
191
|
+
doc.header_style(h) == :atx_closed \
|
192
|
+
and (doc.element_line(h).match(/^#+[^#\s]/) \
|
193
|
+
or doc.element_line(h).match(/[^#\s\\]#+$/))
|
194
|
+
end.map { |h| doc.element_linenumber(h) }
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
rule "MD021", "Multiple spaces inside hashes on closed atx style header" do
|
199
|
+
tags :headers, :atx_closed, :spaces
|
200
|
+
check do |doc|
|
201
|
+
doc.find_type_elements(:header).select do |h|
|
202
|
+
doc.header_style(h) == :atx_closed \
|
203
|
+
and (doc.element_line(h).match(/^#+\s\s/) \
|
204
|
+
or doc.element_line(h).match(/\s\s#+$/))
|
205
|
+
end.map { |h| doc.element_linenumber(h) }
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
rule "MD022", "Headers should be surrounded by blank lines" do
|
210
|
+
tags :headers, :blank_lines
|
211
|
+
check do |doc|
|
212
|
+
errors = []
|
213
|
+
doc.find_type_elements(:header).each do |h|
|
214
|
+
header_bad = false
|
215
|
+
linenum = doc.element_linenumber(h)
|
216
|
+
# Check previous line
|
217
|
+
if linenum > 1 and not doc.lines[linenum - 2].empty?
|
218
|
+
header_bad = true
|
219
|
+
end
|
220
|
+
# Check next line
|
221
|
+
next_line_idx = doc.header_style(h) == :setext ? linenum + 1 : linenum
|
222
|
+
next_line = doc.lines[next_line_idx]
|
223
|
+
header_bad = true if not next_line.nil? and not next_line.empty?
|
224
|
+
errors << linenum if header_bad
|
225
|
+
end
|
226
|
+
# Kramdown requires that headers start on a block boundary, so in most
|
227
|
+
# cases it won't pick up a header without a blank line before it. We need
|
228
|
+
# to check regular text and pick out headers ourselves too
|
229
|
+
doc.find_type_elements(:p).each do |p|
|
230
|
+
linenum = doc.element_linenumber(p)
|
231
|
+
text = p.children.select { |e| e.type == :text }.map {|e| e.value }.join
|
232
|
+
lines = text.split("\n")
|
233
|
+
prev_lines = ["", ""]
|
234
|
+
lines.each do |line|
|
235
|
+
# First look for ATX style headers without blank lines before
|
236
|
+
if line.match(/^\#{1,6}/) and not prev_lines[1].empty?
|
237
|
+
errors << linenum
|
238
|
+
end
|
239
|
+
# Next, look for setext style
|
240
|
+
if line.match(/^(-+|=+)\s*$/) and not prev_lines[0].empty?
|
241
|
+
errors << linenum - 1
|
242
|
+
end
|
243
|
+
linenum += 1
|
244
|
+
prev_lines << line
|
245
|
+
prev_lines.shift
|
246
|
+
end
|
247
|
+
end
|
248
|
+
errors.sort
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
rule "MD023", "Headers must start at the beginning of the line" do
|
253
|
+
tags :headers, :spaces
|
254
|
+
check do |doc|
|
255
|
+
errors = []
|
256
|
+
# The only type of header with spaces actually parsed as such is setext
|
257
|
+
# style where only the text is indented. We check for that first.
|
258
|
+
doc.find_type_elements(:header).each do |h|
|
259
|
+
errors << doc.element_linenumber(h) if doc.element_line(h).match(/^\s/)
|
260
|
+
end
|
261
|
+
# Next we have to look for things that aren't parsed as headers because
|
262
|
+
# they start with spaces.
|
263
|
+
doc.find_type_elements(:p).each do |p|
|
264
|
+
linenum = doc.element_linenumber(p)
|
265
|
+
lines = doc.extract_text(p)
|
266
|
+
prev_line = ""
|
267
|
+
lines.each do |line|
|
268
|
+
# First look for ATX style headers
|
269
|
+
if line.match(/^\s+\#{1,6}/)
|
270
|
+
errors << linenum
|
271
|
+
end
|
272
|
+
# Next, look for setext style
|
273
|
+
if line.match(/^\s+(-+|=+)\s*$/) and not prev_line.empty?
|
274
|
+
errors << linenum - 1
|
275
|
+
end
|
276
|
+
linenum += 1
|
277
|
+
prev_line = line
|
278
|
+
end
|
279
|
+
end
|
280
|
+
errors.sort
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
rule "MD024", "Multiple headers with the same content" do
|
285
|
+
tags :headers
|
286
|
+
check do |doc|
|
287
|
+
header_content = Set.new
|
288
|
+
doc.find_type(:header).select do |h|
|
289
|
+
not header_content.add?(h[:raw_text])
|
290
|
+
end.map { |h| doc.element_linenumber(h) }
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
rule "MD025", "Multiple top level headers in the same document" do
|
295
|
+
tags :headers
|
296
|
+
check do |doc|
|
297
|
+
headers = doc.find_type(:header).select { |h| h[:level] == 1 }
|
298
|
+
if not headers.empty? and doc.element_linenumber(headers[0]) == 1
|
299
|
+
headers[1..-1].map { |h| doc.element_linenumber(h) }
|
300
|
+
end
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
rule "MD026", "Trailing punctuation in header" do
|
305
|
+
tags :headers
|
306
|
+
params :punctuation => '.,;:!?'
|
307
|
+
check do |doc|
|
308
|
+
doc.find_type(:header).select {
|
309
|
+
|h| h[:raw_text].match(/[#{params[:punctuation]}]$/) }.map {
|
310
|
+
|h| doc.element_linenumber(h) }
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
rule "MD027", "Multiple spaces after blockquote symbol" do
|
315
|
+
tags :blockquote, :whitespace, :indentation
|
316
|
+
check do |doc|
|
317
|
+
errors = []
|
318
|
+
doc.find_type_elements(:blockquote).each do |e|
|
319
|
+
linenum = doc.element_linenumber(e)
|
320
|
+
lines = doc.extract_text(e, /^\s*> /)
|
321
|
+
lines.each do |line|
|
322
|
+
errors << linenum if line.start_with?(" ")
|
323
|
+
linenum += 1
|
324
|
+
end
|
325
|
+
end
|
326
|
+
errors
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
rule "MD028", "Blank line inside blockquote" do
|
331
|
+
tags :blockquote, :whitespace
|
332
|
+
check do |doc|
|
333
|
+
def check_blockquote(errors, elements)
|
334
|
+
prev = [nil, nil, nil]
|
335
|
+
elements.each do |e|
|
336
|
+
prev.shift
|
337
|
+
prev << e.type
|
338
|
+
if prev == [:blockquote, :blank, :blockquote]
|
339
|
+
# The current location is the start of the second blockquote, so the
|
340
|
+
# line before will be a blank line in between the two, or at least the
|
341
|
+
# lowest blank line if there are more than one.
|
342
|
+
errors << e.options[:location] - 1
|
343
|
+
end
|
344
|
+
check_blockquote(errors, e.children)
|
345
|
+
end
|
346
|
+
end
|
347
|
+
errors = []
|
348
|
+
check_blockquote(errors, doc.elements)
|
349
|
+
errors
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
rule "MD029", "Ordered list item prefix" do
|
354
|
+
tags :ol
|
355
|
+
# Style can be :one or :ordered
|
356
|
+
params :style => :one
|
357
|
+
check do |doc|
|
358
|
+
if params[:style] == :ordered
|
359
|
+
doc.find_type_elements(:ol).map { |l|
|
360
|
+
doc.find_type_elements(:li, false, l.children).map.with_index { |i, idx|
|
361
|
+
doc.element_linenumber(i) \
|
362
|
+
unless doc.element_line(i).strip.start_with?("#{idx+1}. ")
|
363
|
+
}
|
364
|
+
}.flatten.compact
|
365
|
+
elsif params[:style] == :one
|
366
|
+
doc.find_type_elements(:ol).map { |l|
|
367
|
+
doc.find_type_elements(:li, false, l.children) }.flatten.map { |i|
|
368
|
+
doc.element_linenumber(i) \
|
369
|
+
unless doc.element_line(i).strip.start_with?('1. ') }.compact
|
370
|
+
end
|
371
|
+
end
|
372
|
+
end
|
373
|
+
|
374
|
+
rule "MD030", "Spaces after list markers" do
|
375
|
+
tags :ol, :ul, :whitespace
|
376
|
+
params :ul_single => 1, :ol_single => 1, :ul_multi => 1, :ol_multi => 1
|
377
|
+
check do |doc|
|
378
|
+
errors = []
|
379
|
+
doc.find_type_elements([:ul, :ol]).each do |l|
|
380
|
+
list_type = l.type.to_s
|
381
|
+
items = doc.find_type_elements(:li, false, l.children)
|
382
|
+
# The entire list is to use the multi-paragraph spacing rule if any of
|
383
|
+
# the items in it have multiple paragraphs/other block items.
|
384
|
+
srule = items.map { |i| i.children.length }.max > 1 ? "multi" : "single"
|
385
|
+
items.each do |i|
|
386
|
+
actual_spaces = doc.element_line(i).match(/^\s*\S+(\s+)/)[1].length
|
387
|
+
required_spaces = params["#{list_type}_#{srule}".to_sym]
|
388
|
+
errors << doc.element_linenumber(i) if required_spaces != actual_spaces
|
389
|
+
end
|
390
|
+
end
|
391
|
+
errors
|
392
|
+
end
|
393
|
+
end
|
data/lib/mdl/ruleset.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
module MarkdownLint
|
2
|
+
class Rule
|
3
|
+
attr_accessor :id, :description
|
4
|
+
|
5
|
+
def initialize(id, description, block)
|
6
|
+
@id, @description = id, description
|
7
|
+
@tags = []
|
8
|
+
@params = {}
|
9
|
+
instance_eval &block
|
10
|
+
end
|
11
|
+
|
12
|
+
|
13
|
+
def check(&block)
|
14
|
+
@check = block unless block.nil?
|
15
|
+
@check
|
16
|
+
end
|
17
|
+
|
18
|
+
def tags(*t)
|
19
|
+
@tags = t.flatten.map {|i| i.to_sym} unless t.empty?
|
20
|
+
@tags
|
21
|
+
end
|
22
|
+
|
23
|
+
def params(p = nil)
|
24
|
+
@params.update(p) unless p.nil?
|
25
|
+
@params
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class RuleSet
|
30
|
+
attr_reader :rules
|
31
|
+
|
32
|
+
def rule(id, description, &block)
|
33
|
+
@rules = {} if @rules.nil?
|
34
|
+
@rules[id] = Rule.new(id, description, block)
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.load_default
|
38
|
+
self.load(File.expand_path("../rules.rb", __FILE__))
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.load(rules_file)
|
42
|
+
ruleset = new
|
43
|
+
ruleset.instance_eval(File.read(rules_file), rules_file)
|
44
|
+
ruleset.rules
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lib/mdl/style.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module MarkdownLint
|
4
|
+
class Style
|
5
|
+
attr_reader :rules
|
6
|
+
|
7
|
+
def initialize(all_rules)
|
8
|
+
@tagged_rules = {}
|
9
|
+
all_rules.each do |id, r|
|
10
|
+
r.tags.each do |t|
|
11
|
+
@tagged_rules[t] ||= Set.new
|
12
|
+
@tagged_rules[t] << id
|
13
|
+
end
|
14
|
+
end
|
15
|
+
@all_rules = all_rules
|
16
|
+
@rules = Set.new
|
17
|
+
end
|
18
|
+
|
19
|
+
def all
|
20
|
+
@rules.merge(@all_rules.keys)
|
21
|
+
end
|
22
|
+
|
23
|
+
def rule(id, params={})
|
24
|
+
@rules << id
|
25
|
+
@all_rules[id].params(params)
|
26
|
+
end
|
27
|
+
|
28
|
+
def exclude_rule(id)
|
29
|
+
@rules.delete(id)
|
30
|
+
end
|
31
|
+
|
32
|
+
def tag(t)
|
33
|
+
@rules.merge(@tagged_rules[t])
|
34
|
+
end
|
35
|
+
|
36
|
+
def exclude_tag(t)
|
37
|
+
@rules.subtract(@tagged_rules[t])
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.load(style_file, rules)
|
41
|
+
unless style_file.include?("/") or style_file.end_with?(".rb")
|
42
|
+
style_file = File.expand_path("../styles/#{style_file}.rb", __FILE__)
|
43
|
+
end
|
44
|
+
style = new(rules)
|
45
|
+
style.instance_eval(File.read(style_file), style_file)
|
46
|
+
rules.select! {|r| style.rules.include?(r)}
|
47
|
+
style
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|