mdl 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (92) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +13 -0
  3. data/.travis.yml +7 -0
  4. data/Gemfile +2 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +79 -0
  7. data/Rakefile +8 -0
  8. data/bin/mdl +10 -0
  9. data/docs/RULES.md +609 -0
  10. data/docs/creating_rules.md +83 -0
  11. data/docs/creating_styles.md +47 -0
  12. data/example/markdown_spec.md +897 -0
  13. data/lib/mdl.rb +72 -0
  14. data/lib/mdl/cli.rb +89 -0
  15. data/lib/mdl/config.rb +9 -0
  16. data/lib/mdl/doc.rb +252 -0
  17. data/lib/mdl/kramdown_parser.rb +29 -0
  18. data/lib/mdl/rules.rb +393 -0
  19. data/lib/mdl/ruleset.rb +47 -0
  20. data/lib/mdl/style.rb +50 -0
  21. data/lib/mdl/styles/all.rb +1 -0
  22. data/lib/mdl/styles/cirosantilli.rb +6 -0
  23. data/lib/mdl/styles/default.rb +1 -0
  24. data/lib/mdl/styles/relaxed.rb +6 -0
  25. data/lib/mdl/version.rb +3 -0
  26. data/mdl.gemspec +31 -0
  27. data/test/rule_tests/atx_closed_header_spacing.md +17 -0
  28. data/test/rule_tests/atx_header_spacing.md +5 -0
  29. data/test/rule_tests/blockquote_blank_lines.md +31 -0
  30. data/test/rule_tests/blockquote_spaces.md +21 -0
  31. data/test/rule_tests/bulleted_list_2_space_indent.md +6 -0
  32. data/test/rule_tests/bulleted_list_2_space_indent_style.rb +2 -0
  33. data/test/rule_tests/bulleted_list_4_space_indent.md +3 -0
  34. data/test/rule_tests/bulleted_list_not_at_beginning_of_line.md +14 -0
  35. data/test/rule_tests/code_block_dollar.md +22 -0
  36. data/test/rule_tests/consecutive_blank_lines.md +11 -0
  37. data/test/rule_tests/consistent_bullet_styles_asterisk.md +3 -0
  38. data/test/rule_tests/consistent_bullet_styles_dash.md +3 -0
  39. data/test/rule_tests/consistent_bullet_styles_plus.md +3 -0
  40. data/test/rule_tests/empty_doc.md +0 -0
  41. data/test/rule_tests/fenced_code_blocks.md +21 -0
  42. data/test/rule_tests/first_header_bad_atx.md +1 -0
  43. data/test/rule_tests/first_header_bad_setext.md +2 -0
  44. data/test/rule_tests/first_header_good_atx.md +1 -0
  45. data/test/rule_tests/first_header_good_setext.md +2 -0
  46. data/test/rule_tests/header_duplicate_content.md +11 -0
  47. data/test/rule_tests/header_multiple_toplevel.md +3 -0
  48. data/test/rule_tests/header_mutliple_h1_no_toplevel.md +5 -0
  49. data/test/rule_tests/header_trailing_punctuation.md +11 -0
  50. data/test/rule_tests/header_trailing_punctuation_customized.md +14 -0
  51. data/test/rule_tests/header_trailing_punctuation_customized_style.rb +2 -0
  52. data/test/rule_tests/headers_bad.md +7 -0
  53. data/test/rule_tests/headers_good.md +5 -0
  54. data/test/rule_tests/headers_surrounding_space_atx.md +9 -0
  55. data/test/rule_tests/headers_surrounding_space_setext.md +15 -0
  56. data/test/rule_tests/headers_with_spaces_at_the_beginning.md +9 -0
  57. data/test/rule_tests/inconsistent_bullet_indent_same_level.md +4 -0
  58. data/test/rule_tests/inconsistent_bullet_styles_asterisk.md +3 -0
  59. data/test/rule_tests/inconsistent_bullet_styles_dash.md +3 -0
  60. data/test/rule_tests/inconsistent_bullet_styles_plus.md +3 -0
  61. data/test/rule_tests/incorrect_bullet_style_asterisk.md +3 -0
  62. data/test/rule_tests/incorrect_bullet_style_asterisk_style.rb +2 -0
  63. data/test/rule_tests/incorrect_bullet_style_dash.md +3 -0
  64. data/test/rule_tests/incorrect_bullet_style_dash_style.rb +2 -0
  65. data/test/rule_tests/incorrect_bullet_style_plus.md +3 -0
  66. data/test/rule_tests/incorrect_bullet_style_plus_style.rb +2 -0
  67. data/test/rule_tests/incorrect_header_atx.md +6 -0
  68. data/test/rule_tests/incorrect_header_atx_closed.md +6 -0
  69. data/test/rule_tests/incorrect_header_atx_closed_style.rb +2 -0
  70. data/test/rule_tests/incorrect_header_atx_style.rb +2 -0
  71. data/test/rule_tests/incorrect_header_setext.md +6 -0
  72. data/test/rule_tests/incorrect_header_setext_style.rb +2 -0
  73. data/test/rule_tests/long_lines.md +3 -0
  74. data/test/rule_tests/long_lines_100.md +7 -0
  75. data/test/rule_tests/long_lines_100_style.rb +2 -0
  76. data/test/rule_tests/mixed_header_types_atx.md +6 -0
  77. data/test/rule_tests/mixed_header_types_atx_closed.md +6 -0
  78. data/test/rule_tests/mixed_header_types_setext.md +6 -0
  79. data/test/rule_tests/ordered_list_item_prefix.md +13 -0
  80. data/test/rule_tests/ordered_list_item_prefix_ordered.md +13 -0
  81. data/test/rule_tests/ordered_list_item_prefix_ordered_style.rb +2 -0
  82. data/test/rule_tests/reversed_link.md +7 -0
  83. data/test/rule_tests/spaces_after_list_marker.md +74 -0
  84. data/test/rule_tests/spaces_after_list_marker_style.rb +3 -0
  85. data/test/rule_tests/whitespace issues.md +3 -0
  86. data/test/setup_tests.rb +5 -0
  87. data/test/test_ruledocs.rb +45 -0
  88. data/test/test_rules.rb +56 -0
  89. data/tools/README.md +3 -0
  90. data/tools/test_location.rb +20 -0
  91. data/tools/view_markdown.rb +11 -0
  92. metadata +314 -0
@@ -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
@@ -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
@@ -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