rubocop-prose 0.1.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.
Files changed (228) hide show
  1. checksums.yaml +7 -0
  2. data/AGENTS.md +13 -0
  3. data/CHANGELOG.md +82 -0
  4. data/CONTRIBUTING.md +77 -0
  5. data/DESIGN.md +51 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +152 -0
  8. data/RELEASING.md +32 -0
  9. data/assets/README.md +3 -0
  10. data/assets/RUBOCOP-LICENSE +20 -0
  11. data/assets/logo.gif +0 -0
  12. data/assets/logo.png +0 -0
  13. data/assets/logo.svg +80 -0
  14. data/assets/rubocop-logo.svg +1 -0
  15. data/config/cops/markdown/atx_heading_space.yml +3 -0
  16. data/config/cops/markdown/atx_heading_spacing.yml +3 -0
  17. data/config/cops/markdown/bare_url.yml +7 -0
  18. data/config/cops/markdown/blockquote_blank_lines.yml +3 -0
  19. data/config/cops/markdown/blockquote_spacing.yml +3 -0
  20. data/config/cops/markdown/closed_heading_space.yml +3 -0
  21. data/config/cops/markdown/closed_heading_spacing.yml +3 -0
  22. data/config/cops/markdown/code_block_style.yml +8 -0
  23. data/config/cops/markdown/code_span_spacing.yml +3 -0
  24. data/config/cops/markdown/compact_lists.yml +3 -0
  25. data/config/cops/markdown/descriptive_link_text.yml +8 -0
  26. data/config/cops/markdown/duplicate_headings.yml +4 -0
  27. data/config/cops/markdown/emphasis_heading.yml +3 -0
  28. data/config/cops/markdown/emphasis_spacing.yml +3 -0
  29. data/config/cops/markdown/emphasis_style.yml +4 -0
  30. data/config/cops/markdown/empty_link.yml +3 -0
  31. data/config/cops/markdown/fence_blank_lines.yml +6 -0
  32. data/config/cops/markdown/fence_language.yml +4 -0
  33. data/config/cops/markdown/fence_style.yml +8 -0
  34. data/config/cops/markdown/first_heading.yml +6 -0
  35. data/config/cops/markdown/front_matter.yml +2 -0
  36. data/config/cops/markdown/hard_break_style.yml +4 -0
  37. data/config/cops/markdown/hard_tabs.yml +5 -0
  38. data/config/cops/markdown/heading_blank_lines.yml +5 -0
  39. data/config/cops/markdown/heading_increment.yml +3 -0
  40. data/config/cops/markdown/heading_indentation.yml +3 -0
  41. data/config/cops/markdown/heading_punctuation.yml +4 -0
  42. data/config/cops/markdown/heading_structure.yml +4 -0
  43. data/config/cops/markdown/heading_style.yml +9 -0
  44. data/config/cops/markdown/image_alt_text.yml +3 -0
  45. data/config/cops/markdown/image_path_separators.yml +3 -0
  46. data/config/cops/markdown/link_fragment.yml +6 -0
  47. data/config/cops/markdown/link_style.yml +10 -0
  48. data/config/cops/markdown/link_text_spacing.yml +3 -0
  49. data/config/cops/markdown/list_blank_lines.yml +3 -0
  50. data/config/cops/markdown/list_continuation_indentation.yml +3 -0
  51. data/config/cops/markdown/list_indentation.yml +3 -0
  52. data/config/cops/markdown/list_marker_spacing.yml +7 -0
  53. data/config/cops/markdown/ordered_list_numbering.yml +7 -0
  54. data/config/cops/markdown/outside_spacing.yml +3 -0
  55. data/config/cops/markdown/proper_names.yml +5 -0
  56. data/config/cops/markdown/raw_html.yml +4 -0
  57. data/config/cops/markdown/reference_definitions.yml +8 -0
  58. data/config/cops/markdown/reversed_link.yml +3 -0
  59. data/config/cops/markdown/shell_prompts.yml +13 -0
  60. data/config/cops/markdown/single_top_level_heading.yml +4 -0
  61. data/config/cops/markdown/strong_style.yml +4 -0
  62. data/config/cops/markdown/structural_line_length.yml +9 -0
  63. data/config/cops/markdown/syntax.yml +3 -0
  64. data/config/cops/markdown/table_blank_lines.yml +3 -0
  65. data/config/cops/markdown/table_columns.yml +3 -0
  66. data/config/cops/markdown/table_pipes.yml +4 -0
  67. data/config/cops/markdown/table_spacing.yml +4 -0
  68. data/config/cops/markdown/thematic_break_blank_lines.yml +5 -0
  69. data/config/cops/markdown/thematic_break_style.yml +4 -0
  70. data/config/cops/markdown/trailing_whitespace.yml +5 -0
  71. data/config/cops/markdown/unclosed_fenced_code_block.yml +3 -0
  72. data/config/cops/markdown/undefined_reference.yml +4 -0
  73. data/config/cops/markdown/unordered_list_indentation.yml +4 -0
  74. data/config/cops/markdown/unordered_list_marker.yml +9 -0
  75. data/config/cops/prose/max_length.yml +5 -0
  76. data/config/cops/prose/multiple_blank_lines.yml +4 -0
  77. data/config/cops/prose/trailing_newline.yml +3 -0
  78. data/config/cops/text/trailing_whitespace.yml +3 -0
  79. data/config/default.yml +16 -0
  80. data/docs/cops/markdown/atx_heading_space.md +26 -0
  81. data/docs/cops/markdown/atx_heading_spacing.md +24 -0
  82. data/docs/cops/markdown/bare_url.md +30 -0
  83. data/docs/cops/markdown/blockquote_blank_lines.md +30 -0
  84. data/docs/cops/markdown/blockquote_spacing.md +30 -0
  85. data/docs/cops/markdown/closed_heading_space.md +26 -0
  86. data/docs/cops/markdown/closed_heading_spacing.md +24 -0
  87. data/docs/cops/markdown/code_block_style.md +37 -0
  88. data/docs/cops/markdown/code_span_spacing.md +26 -0
  89. data/docs/cops/markdown/compact_lists.md +29 -0
  90. data/docs/cops/markdown/descriptive_link_text.md +31 -0
  91. data/docs/cops/markdown/duplicate_headings.md +35 -0
  92. data/docs/cops/markdown/emphasis_heading.md +26 -0
  93. data/docs/cops/markdown/emphasis_spacing.md +24 -0
  94. data/docs/cops/markdown/emphasis_style.md +34 -0
  95. data/docs/cops/markdown/empty_link.md +26 -0
  96. data/docs/cops/markdown/fence_blank_lines.md +43 -0
  97. data/docs/cops/markdown/fence_language.md +31 -0
  98. data/docs/cops/markdown/fence_style.md +38 -0
  99. data/docs/cops/markdown/first_heading.md +31 -0
  100. data/docs/cops/markdown/hard_break_style.md +50 -0
  101. data/docs/cops/markdown/hard_tabs.md +30 -0
  102. data/docs/cops/markdown/heading_blank_lines.md +36 -0
  103. data/docs/cops/markdown/heading_increment.md +30 -0
  104. data/docs/cops/markdown/heading_indentation.md +26 -0
  105. data/docs/cops/markdown/heading_punctuation.md +27 -0
  106. data/docs/cops/markdown/heading_structure.md +41 -0
  107. data/docs/cops/markdown/heading_style.md +36 -0
  108. data/docs/cops/markdown/image_alt_text.md +26 -0
  109. data/docs/cops/markdown/image_path_separators.md +32 -0
  110. data/docs/cops/markdown/link_fragment.md +35 -0
  111. data/docs/cops/markdown/link_style.md +49 -0
  112. data/docs/cops/markdown/link_text_spacing.md +26 -0
  113. data/docs/cops/markdown/list_blank_lines.md +31 -0
  114. data/docs/cops/markdown/list_continuation_indentation.md +28 -0
  115. data/docs/cops/markdown/list_indentation.md +28 -0
  116. data/docs/cops/markdown/list_marker_spacing.md +32 -0
  117. data/docs/cops/markdown/ordered_list_numbering.md +29 -0
  118. data/docs/cops/markdown/outside_spacing.md +26 -0
  119. data/docs/cops/markdown/proper_names.md +39 -0
  120. data/docs/cops/markdown/raw_html.md +27 -0
  121. data/docs/cops/markdown/reference_definitions.md +36 -0
  122. data/docs/cops/markdown/reversed_link.md +26 -0
  123. data/docs/cops/markdown/shell_prompts.md +57 -0
  124. data/docs/cops/markdown/single_top_level_heading.md +31 -0
  125. data/docs/cops/markdown/strong_style.md +34 -0
  126. data/docs/cops/markdown/structural_line_length.md +40 -0
  127. data/docs/cops/markdown/syntax.md +28 -0
  128. data/docs/cops/markdown/table_blank_lines.md +34 -0
  129. data/docs/cops/markdown/table_columns.md +30 -0
  130. data/docs/cops/markdown/table_pipes.md +38 -0
  131. data/docs/cops/markdown/table_spacing.md +38 -0
  132. data/docs/cops/markdown/thematic_break_blank_lines.md +31 -0
  133. data/docs/cops/markdown/thematic_break_style.md +48 -0
  134. data/docs/cops/markdown/trailing_whitespace.md +30 -0
  135. data/docs/cops/markdown/unclosed_fenced_code_block.md +29 -0
  136. data/docs/cops/markdown/undefined_reference.md +29 -0
  137. data/docs/cops/markdown/unordered_list_indentation.md +45 -0
  138. data/docs/cops/markdown/unordered_list_marker.md +36 -0
  139. data/docs/cops/prose/max_length.md +33 -0
  140. data/docs/cops/prose/multiple_blank_lines.md +50 -0
  141. data/docs/cops/prose/trailing_newline.md +26 -0
  142. data/docs/cops/text/trailing_whitespace.md +26 -0
  143. data/docs/settings/files.md +33 -0
  144. data/docs/settings/front_matter.md +12 -0
  145. data/lib/rubocop/cop/markdown/atx_heading_space.rb +22 -0
  146. data/lib/rubocop/cop/markdown/atx_heading_spacing.rb +27 -0
  147. data/lib/rubocop/cop/markdown/bare_url.rb +22 -0
  148. data/lib/rubocop/cop/markdown/base.rb +17 -0
  149. data/lib/rubocop/cop/markdown/blockquote_blank_lines.rb +49 -0
  150. data/lib/rubocop/cop/markdown/blockquote_spacing.rb +76 -0
  151. data/lib/rubocop/cop/markdown/closed_heading_space.rb +26 -0
  152. data/lib/rubocop/cop/markdown/closed_heading_spacing.rb +27 -0
  153. data/lib/rubocop/cop/markdown/code_block_style.rb +80 -0
  154. data/lib/rubocop/cop/markdown/code_span_spacing.rb +51 -0
  155. data/lib/rubocop/cop/markdown/compact_lists.rb +43 -0
  156. data/lib/rubocop/cop/markdown/descriptive_link_text.rb +27 -0
  157. data/lib/rubocop/cop/markdown/duplicate_headings.rb +28 -0
  158. data/lib/rubocop/cop/markdown/emphasis_heading.rb +18 -0
  159. data/lib/rubocop/cop/markdown/emphasis_spacing.rb +30 -0
  160. data/lib/rubocop/cop/markdown/emphasis_style.rb +34 -0
  161. data/lib/rubocop/cop/markdown/empty_link.rb +25 -0
  162. data/lib/rubocop/cop/markdown/fence_blank_lines.rb +72 -0
  163. data/lib/rubocop/cop/markdown/fence_language.rb +32 -0
  164. data/lib/rubocop/cop/markdown/fence_style.rb +39 -0
  165. data/lib/rubocop/cop/markdown/first_heading.rb +44 -0
  166. data/lib/rubocop/cop/markdown/hard_break_style.rb +65 -0
  167. data/lib/rubocop/cop/markdown/hard_tabs.rb +32 -0
  168. data/lib/rubocop/cop/markdown/heading_blank_lines.rb +37 -0
  169. data/lib/rubocop/cop/markdown/heading_increment.rb +23 -0
  170. data/lib/rubocop/cop/markdown/heading_indentation.rb +37 -0
  171. data/lib/rubocop/cop/markdown/heading_punctuation.rb +23 -0
  172. data/lib/rubocop/cop/markdown/heading_structure.rb +33 -0
  173. data/lib/rubocop/cop/markdown/heading_style.rb +24 -0
  174. data/lib/rubocop/cop/markdown/image_alt_text.rb +24 -0
  175. data/lib/rubocop/cop/markdown/image_path_separators.rb +32 -0
  176. data/lib/rubocop/cop/markdown/link_fragment.rb +36 -0
  177. data/lib/rubocop/cop/markdown/link_style.rb +29 -0
  178. data/lib/rubocop/cop/markdown/link_text_spacing.rb +34 -0
  179. data/lib/rubocop/cop/markdown/list_blank_lines.rb +38 -0
  180. data/lib/rubocop/cop/markdown/list_continuation_indentation.rb +59 -0
  181. data/lib/rubocop/cop/markdown/list_indentation.rb +28 -0
  182. data/lib/rubocop/cop/markdown/list_marker_spacing.rb +44 -0
  183. data/lib/rubocop/cop/markdown/ordered_list_numbering.rb +35 -0
  184. data/lib/rubocop/cop/markdown/outside_spacing.rb +88 -0
  185. data/lib/rubocop/cop/markdown/proper_names.rb +51 -0
  186. data/lib/rubocop/cop/markdown/raw_html.rb +60 -0
  187. data/lib/rubocop/cop/markdown/reference_definitions.rb +37 -0
  188. data/lib/rubocop/cop/markdown/reversed_link.rb +20 -0
  189. data/lib/rubocop/cop/markdown/shell_prompts.rb +72 -0
  190. data/lib/rubocop/cop/markdown/single_top_level_heading.rb +19 -0
  191. data/lib/rubocop/cop/markdown/strong_style.rb +33 -0
  192. data/lib/rubocop/cop/markdown/structural_line_length.rb +51 -0
  193. data/lib/rubocop/cop/markdown/syntax.rb +33 -0
  194. data/lib/rubocop/cop/markdown/table_blank_lines.rb +46 -0
  195. data/lib/rubocop/cop/markdown/table_columns.rb +23 -0
  196. data/lib/rubocop/cop/markdown/table_pipes.rb +45 -0
  197. data/lib/rubocop/cop/markdown/table_spacing.rb +62 -0
  198. data/lib/rubocop/cop/markdown/thematic_break_blank_lines.rb +115 -0
  199. data/lib/rubocop/cop/markdown/thematic_break_style.rb +74 -0
  200. data/lib/rubocop/cop/markdown/trailing_whitespace.rb +55 -0
  201. data/lib/rubocop/cop/markdown/unclosed_fenced_code_block.rb +82 -0
  202. data/lib/rubocop/cop/markdown/undefined_reference.rb +25 -0
  203. data/lib/rubocop/cop/markdown/unordered_list_indentation.rb +37 -0
  204. data/lib/rubocop/cop/markdown/unordered_list_marker.rb +45 -0
  205. data/lib/rubocop/cop/prose/base.rb +55 -0
  206. data/lib/rubocop/cop/prose/max_length.rb +93 -0
  207. data/lib/rubocop/cop/prose/multiple_blank_lines.rb +96 -0
  208. data/lib/rubocop/cop/prose/trailing_newline.rb +26 -0
  209. data/lib/rubocop/cop/text/base.rb +17 -0
  210. data/lib/rubocop/cop/text/trailing_whitespace.rb +25 -0
  211. data/lib/rubocop/prose/code_rules.rb +79 -0
  212. data/lib/rubocop/prose/document.rb +62 -0
  213. data/lib/rubocop/prose/file_types.rb +56 -0
  214. data/lib/rubocop/prose/heading_rules.rb +54 -0
  215. data/lib/rubocop/prose/inline_rules.rb +213 -0
  216. data/lib/rubocop/prose/link_rules.rb +630 -0
  217. data/lib/rubocop/prose/list_rules.rb +65 -0
  218. data/lib/rubocop/prose/markdown_document.rb +167 -0
  219. data/lib/rubocop/prose/parser.rb +80 -0
  220. data/lib/rubocop/prose/plugin.rb +30 -0
  221. data/lib/rubocop/prose/runner.rb +63 -0
  222. data/lib/rubocop/prose/source.rb +82 -0
  223. data/lib/rubocop/prose/structural_blank_lines.rb +90 -0
  224. data/lib/rubocop/prose/table_rules.rb +71 -0
  225. data/lib/rubocop/prose/text_document.rb +54 -0
  226. data/lib/rubocop/prose/version.rb +7 -0
  227. data/lib/rubocop-prose.rb +30 -0
  228. metadata +335 -0
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../prose/list_rules'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Markdown
8
+ class OrderedListNumbering < Base
9
+ extend AutoCorrector
10
+ include RuboCop::Prose::ListRules
11
+
12
+ private
13
+
14
+ def investigate_document
15
+ list_groups do |list, marks, _parent|
16
+ next unless list.type == :ol
17
+
18
+ first = marks.first[:marker].to_i
19
+ marks.each_with_index do |mark, index|
20
+ expected = cop_config.fetch('EnforcedStyle', 'sequential') == 'ones' ? 1 : first + index
21
+ digits = mark[:marker][/\A\d+/]
22
+ next if digits == expected.to_s
23
+
24
+ range = document.range(mark[:line], mark[:column], mark[:line], mark[:column] + digits.length)
25
+ prose_offense(range, message: "Use #{expected} for this ordered list item.") do |corrector|
26
+ # Changing marker width can change continuation-block indentation.
27
+ corrector.replace(processed_range(range), expected.to_s) if digits.length == expected.to_s.length
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../prose/inline_rules'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Markdown
8
+ class OutsideSpacing < Base
9
+ WORD_CHARACTER = /[\p{L}\p{M}\p{N}]/u
10
+ AMBIGUOUS_SCRIPT = /[\p{Han}\p{Hiragana}\p{Katakana}\p{Hangul}\p{Thai}\p{Lao}\p{Khmer}\p{Myanmar}]/u
11
+
12
+ def investigate_document
13
+ candidates = code_spans + emphasis_spans
14
+ candidates.each do |first, last, content_first, content_last|
15
+ next unless adjacent_words?(first, content_first, content_last, last)
16
+
17
+ range = ::Parser::Source::Range.new(document.buffer, first, last)
18
+ prose_offense(range, message: 'Separate inline formatting from adjacent prose words.')
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def code_spans
25
+ multiline = RuboCop::Prose::InlineRules.multiline_atoms(document)
26
+ RuboCop::Prose::InlineRules.lines(document).flat_map do |text, number, column|
27
+ line_start = document.line_range(number).begin_pos
28
+ RuboCop::Prose::InlineRules.spans(text).filter_map do |span|
29
+ next unless span.kind == :code && span.begin_pos >= column
30
+
31
+ first = line_start + span.begin_pos
32
+ last = line_start + span.end_pos
33
+ range = ::Parser::Source::Range.new(document.buffer, first, last)
34
+ next if multiline.any? { |atom| atom.overlaps?(range) }
35
+ next if protected_ranges(number, text, line_start).any? { |atom| atom.overlaps?(range) }
36
+
37
+ [first, last, first + span.delimiter.length, last - span.delimiter.length]
38
+ end
39
+ end
40
+ end
41
+
42
+ def emphasis_spans
43
+ %i[em strong].flat_map do |type|
44
+ document.emphasis_ranges(type).filter_map do |opening, closing, _delimiter|
45
+ next unless opening.line == closing.line
46
+
47
+ first = opening.begin_pos
48
+ last = closing.end_pos
49
+ number = opening.line
50
+ line_start = document.line_range(number).begin_pos
51
+ next if protected_ranges(number, document.line(number), line_start)
52
+ .any? { |atom| atom.begin_pos < last && first < atom.end_pos }
53
+
54
+ [first, last, opening.end_pos, closing.begin_pos]
55
+ end
56
+ end
57
+ end
58
+
59
+ def protected_ranges(number, text, line_start)
60
+ @protected_ranges ||= {}
61
+ @protected_ranges[number] ||= RuboCop::Prose::InlineRules.spans(text).filter_map do |span|
62
+ next if span.kind == :code
63
+
64
+ ::Parser::Source::Range.new(document.buffer, line_start + span.begin_pos, line_start + span.end_pos)
65
+ end
66
+ end
67
+
68
+ def adjacent_words?(first, content_first, content_last, last)
69
+ source = document.buffer.source
70
+ left = [source[first - 1], source[content_first]]
71
+ right = [source[content_last - 1], source[last]]
72
+ [left, right].any? do |outside, inside|
73
+ word_character?(outside) && word_character?(inside) &&
74
+ !ambiguous_script?(outside) && !ambiguous_script?(inside)
75
+ end
76
+ end
77
+
78
+ def word_character?(character)
79
+ character&.match?(WORD_CHARACTER)
80
+ end
81
+
82
+ def ambiguous_script?(character)
83
+ character&.match?(AMBIGUOUS_SCRIPT)
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../prose/inline_rules'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Markdown
8
+ class ProperNames < Base
9
+ extend AutoCorrector
10
+
11
+ def investigate_document
12
+ names = Array(cop_config['Names']).reject(&:empty?)
13
+ return if names.empty?
14
+
15
+ alternatives = names.sort_by { |name| -name.length }.map { |name| Regexp.escape(name) }.join('|')
16
+ pattern = /(?<![\p{L}\p{N}_])(?:#{alternatives})(?![\p{L}\p{N}_])/i
17
+ lines = RuboCop::Prose::InlineRules.lines(document)
18
+ if cop_config['CodeBlocks']
19
+ document.nodes(:codeblock).each do |node|
20
+ first = node.options.fetch(:code_start_line)
21
+ node.value.lines.each_index do |index|
22
+ number = first + index
23
+ lines << [document.line(number), number, 0] unless document.front_matter_line?(number)
24
+ end
25
+ end
26
+ end
27
+ lines.each do |text, number, column|
28
+ masked = if document.code_line?(number)
29
+ text
30
+ else
31
+ RuboCop::Prose::InlineRules.mask(text, include_code: cop_config['CodeBlocks'],
32
+ document: document, number: number)
33
+ end
34
+ masked.to_enum(:scan, pattern).each do
35
+ match = Regexp.last_match
36
+ next if match.begin(0) < column
37
+
38
+ expected = names.find { |name| name.casecmp?(match[0]) }
39
+ next if match[0] == expected
40
+
41
+ range = document.range(number, match.begin(0), number, match.end(0))
42
+ prose_offense(range, message: "Use the capitalization `#{expected}`.") do |corrector|
43
+ corrector.replace(processed_range(range), expected)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../prose/inline_rules'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Markdown
8
+ class RawHtml < Base
9
+ def investigate_document
10
+ allowed = Array(cop_config['AllowedElements']).map(&:downcase)
11
+ offsets = Hash.new(0)
12
+ located = []
13
+ unlocated = []
14
+ descendants = ->(node) { [node] + node.children.flat_map { |child| descendants.call(child) } }
15
+ table_nodes = document.nodes(:table).flat_map { |table| descendants.call(table) }.to_set
16
+ document.nodes(:html_element).each do |node|
17
+ number = node.options[:location]
18
+ if !number || table_nodes.include?(node)
19
+ unlocated << node.value.to_s.downcase
20
+ next
21
+ end
22
+ next unless number && !document.front_matter_line?(number) && !document.code_line?(number)
23
+
24
+ text = document.line(number)
25
+ key = [number, node.value.to_s.downcase]
26
+ match = /<#{Regexp.escape(node.value.to_s)}\b[^>]*(?:>|\z)/i.match(text, offsets[key])
27
+ unless match
28
+ unlocated << node.value.to_s.downcase
29
+ next
30
+ end
31
+
32
+ offsets[key] = match.end(0)
33
+ located << [number, match.begin(0)]
34
+ next if allowed.include?(node.value.to_s.downcase)
35
+
36
+ prose_offense(document.range(number, match.begin(0), number, match.end(0)),
37
+ message: "Raw HTML element <#{node.value}> is not allowed.")
38
+ end
39
+ # GFM table cell span locations are relative to the cell, not the document.
40
+ RuboCop::Prose::InlineRules.lines(document).each do |text, number, _|
41
+ protected = RuboCop::Prose::InlineRules.spans(text).reject { |span| span.kind == :html }
42
+ text.to_enum(:scan, /<([A-Za-z][\w:-]*)\b[^>]*>/).each do
43
+ match = Regexp.last_match
44
+ name = match[1].downcase
45
+ next unless unlocated.include?(name)
46
+ next if located.include?([number, match.begin(0)])
47
+ next if protected.any? { |span| match.begin(0) >= span.begin_pos && match.begin(0) < span.end_pos }
48
+
49
+ unlocated.delete_at(unlocated.index(name))
50
+ next if allowed.include?(name)
51
+
52
+ prose_offense(document.range(number, match.begin(0), number, match.end(0)),
53
+ message: "Raw HTML element <#{match[1]}> is not allowed.")
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../prose/link_rules'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Markdown
8
+ class ReferenceDefinitions < Base
9
+ private
10
+
11
+ def investigate_document
12
+ scanner = document.link_rules
13
+ used = scanner.links.filter_map(&:reference).to_set
14
+ ignored = cop_config.fetch('IgnoredDefinitions', ['//']).map { |label| scanner.normalize(label) }
15
+ seen = {}
16
+ scanner.definitions.each do |definition|
17
+ first = seen[definition.reference]
18
+ seen[definition.reference] ||= definition
19
+ duplicate = first && !(cop_config.fetch('AllowIdenticalDuplicates', false) &&
20
+ scanner.decode(first.destination) == scanner.decode(definition.destination) &&
21
+ first.title == definition.title)
22
+ next if ignored.include?(definition.reference)
23
+
24
+ message = if duplicate && cop_config.fetch('CheckDuplicates', true)
25
+ 'Remove or rename the duplicate reference definition.'
26
+ elsif !used.include?(definition.reference) && cop_config.fetch('CheckUnused', false)
27
+ 'Reference definition is not used.'
28
+ end
29
+ next unless message
30
+
31
+ prose_offense(scanner.range(definition.begin_pos, definition.end_pos), message: message)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../prose/link_rules'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Markdown
8
+ class ReversedLink < Base
9
+ MSG = 'Use [link text](destination), not (link text)[destination].'
10
+
11
+ private
12
+
13
+ def investigate_document
14
+ scanner = document.link_rules
15
+ scanner.reversed_links.each { |first, last| prose_offense(scanner.range(first, last)) }
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../prose/code_rules'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Markdown
8
+ class ShellPrompts < Base
9
+ extend AutoCorrector
10
+ include RuboCop::Prose::CodeRules
11
+
12
+ def validate_config
13
+ %w[ShellLanguages Prompts].each do |name|
14
+ value = cop_config[name]
15
+ next if value.is_a?(Array) && (name != 'Prompts' || !value.empty?) &&
16
+ value.all? { |entry| entry.is_a?(String) && entry.match?(/\A\S+\z/) }
17
+
18
+ raise ValidationError, "Markdown/ShellPrompts: #{name} must be an array of nonempty strings " \
19
+ 'without whitespace (and Prompts must contain at least one marker).'
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def investigate_document
26
+ languages = cop_config.fetch('ShellLanguages')
27
+ code_blocks.each do |node|
28
+ next unless node.options[:fenced] && languages.include?(node.options[:lang])
29
+
30
+ commands = prompted_commands(node)
31
+ next if commands.nil? || commands.empty?
32
+
33
+ safe = prompt_correction_safe?(node, commands)
34
+ commands.each do |_, index, prompt|
35
+ range = code_content_range(node, index, prompt.length + 1)
36
+ next unless range
37
+
38
+ prose_offense(range, message: 'Omit shell prompts when the example contains no output.') do |corrector|
39
+ corrector.remove(processed_range(range)) if safe
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ def prompted_commands(node)
46
+ commands = []
47
+ node.value.lines.each_with_index do |line, index|
48
+ line = line.chomp
49
+ prompt = cop_config.fetch('Prompts').find { |marker| line.start_with?("#{marker} ") }
50
+ if prompt
51
+ commands << [line, index, prompt]
52
+ elsif !line.strip.empty? && !line.lstrip.start_with?('#')
53
+ return nil
54
+ end
55
+ end
56
+ commands
57
+ end
58
+
59
+ def prompt_correction_safe?(node, commands)
60
+ return false if commands.any? { |_, _, prompt| prompt.start_with?('#') }
61
+
62
+ details = fence_details(node)
63
+ return false unless details
64
+
65
+ fence = details[:opening][:fence]
66
+ closing = /\A[ \t]{0,3}#{Regexp.escape(fence[0])}{#{fence.length},}[ \t]*\z/
67
+ commands.none? { |command, _, prompt| command.delete_prefix("#{prompt} ").match?(closing) }
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../prose/heading_rules'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Markdown
8
+ class SingleTopLevelHeading < Base
9
+ include RuboCop::Prose::HeadingRules
10
+
11
+ def investigate_document
12
+ heading_entries.select { |heading| heading[:level] == cop_config.fetch('Level', 1) }.drop(1).each do |heading|
13
+ prose_offense(heading_range(heading), message: 'Use at most one top-level heading.')
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../prose/inline_rules'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Markdown
8
+ class StrongStyle < Base
9
+ extend AutoCorrector
10
+
11
+ def investigate_document
12
+ expected = cop_config.fetch('Style', 'consistent')
13
+ document.emphasis_ranges(:strong).each do |opening, closing, char|
14
+ actual = char == '*' ? 'asterisk' : 'underscore'
15
+ expected = actual if expected == 'consistent'
16
+ next if actual == expected
17
+
18
+ replacement = expected == 'asterisk' ? '**' : '__'
19
+ before = opening.begin_pos.positive? ? document.source[opening.begin_pos - 1] : nil
20
+ after = document.source[closing.end_pos]
21
+
22
+ prose_offense(opening, message: "Use #{expected} strong emphasis.") do |corrector|
23
+ next if replacement == '__' && (before&.match?(/[[:alnum:]_]/) || after&.match?(/[[:alnum:]_]/))
24
+
25
+ corrector.replace(processed_range(opening), replacement)
26
+ corrector.replace(processed_range(closing), replacement)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Markdown
6
+ class StructuralLineLength < Base
7
+ MSG = 'Markdown source line is too long. [%<length>d/%<max>d]'
8
+
9
+ def validate_config
10
+ %w[Max HeadingMax CodeBlockMax].each do |name|
11
+ value = cop_config[name]
12
+ next if name != 'Max' && value.nil?
13
+ next if value.is_a?(Integer) && value.positive?
14
+
15
+ raise ValidationError, "Markdown/StructuralLineLength: #{name} must be a positive integer."
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def investigate_document
22
+ table_lines = document.nodes(:table).flat_map do |table|
23
+ first = table.options.fetch(:location)
24
+ rows = table.children.sum { |section| section.children.length }
25
+ (first..(first + rows)).to_a
26
+ end.to_set
27
+ headings = document.headings.to_set(&:last)
28
+ document.lines.each_with_index do |text, index|
29
+ number = index + 1
30
+ next if document.front_matter_line?(number)
31
+
32
+ limit = structural_limit(number, headings, table_lines)
33
+ next unless limit && text.length > limit
34
+
35
+ prose_offense(document.range(number, limit), message: format(MSG, length: text.length, max: limit))
36
+ end
37
+ end
38
+
39
+ def structural_limit(number, headings, tables)
40
+ if headings.include?(number) && cop_config['Headings']
41
+ cop_config['HeadingMax'] || cop_config['Max']
42
+ elsif document.code_line?(number) && cop_config['CodeBlocks']
43
+ cop_config['CodeBlockMax'] || cop_config['Max']
44
+ elsif tables.include?(number) && cop_config['Tables']
45
+ cop_config['Max']
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Markdown
6
+ class Syntax < Base
7
+ private
8
+
9
+ def investigate_document
10
+ document.warnings.reject { |warning| owned_elsewhere?(warning) }
11
+ .group_by { |warning| warning_line(warning) }.each do |number, warnings|
12
+ prose_offense(document.line_range(number), message: warnings.join('; '))
13
+ end
14
+ end
15
+
16
+ def owned_elsewhere?(warning)
17
+ owner = if warning.start_with?('No link definition')
18
+ 'Markdown/UndefinedReference'
19
+ elsif warning.start_with?('Duplicate link ID')
20
+ 'Markdown/ReferenceDefinitions'
21
+ end
22
+ owner && config.cop_enabled?(owner) &&
23
+ processed_source.registry&.any? { |cop| cop.cop_name == owner }
24
+ end
25
+
26
+ def warning_line(warning)
27
+ number = warning.scan(/\bline (\d+)\b/).last&.first&.to_i || 1
28
+ number.clamp(1, document.lines.length)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../prose/table_rules'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Markdown
8
+ class TableBlankLines < Base
9
+ extend AutoCorrector
10
+
11
+ def investigate_document
12
+ RuboCop::Prose::TableRules.tables(document).each do |table|
13
+ first = table.rows.first.number
14
+ last = table.rows.last.number
15
+ unless first == 1 || blank?(document.line(first - 1))
16
+ range = document.range(first, 0, first, 0)
17
+ prose_offense(document.line_range(first), message: 'Add a blank line before the table.') do |corrector|
18
+ corrector.insert_before(processed_range(range), blank_prefix(document.line(first)) + newline)
19
+ end
20
+ end
21
+ next if last == document.lines.length || blank?(document.line(last + 1))
22
+
23
+ range = document.range(last + 1, 0, last + 1, 0)
24
+ prose_offense(document.line_range(last), message: 'Add a blank line after the table.') do |corrector|
25
+ corrector.insert_before(processed_range(range), blank_prefix(document.line(last)) + newline)
26
+ end
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def blank?(text)
33
+ text.match?(/\A[ \t]*(?:>[ \t]*)*\z/)
34
+ end
35
+
36
+ def blank_prefix(text)
37
+ text[/\A[ \t]*(?:>[ \t]*)+/].to_s.rstrip
38
+ end
39
+
40
+ def newline
41
+ document.source.include?("\r\n") ? "\r\n" : "\n"
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../prose/table_rules'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Markdown
8
+ class TableColumns < Base
9
+ def investigate_document
10
+ RuboCop::Prose::TableRules.tables(document).each do |table|
11
+ count = table.rows.first.cells.length
12
+ table.rows.drop(1).each do |row|
13
+ next if row.cells.length == count
14
+
15
+ prose_offense(document.range(row.number, row.begin_pos, row.number, row.end_pos),
16
+ message: "Expected #{count} table columns, found #{row.cells.length}.")
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../prose/table_rules'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Markdown
8
+ class TablePipes < Base
9
+ extend AutoCorrector
10
+
11
+ def investigate_document
12
+ RuboCop::Prose::TableRules.tables(document).each do |table|
13
+ style = cop_config.fetch('Style', 'consistent')
14
+ expected = case style
15
+ when 'leading_and_trailing' then [true, true]
16
+ when 'leading_only' then [true, false]
17
+ when 'trailing_only' then [false, true]
18
+ when 'no_leading_or_trailing' then [false, false]
19
+ else [table.rows.first.leading, table.rows.first.trailing]
20
+ end
21
+ table.rows.each do |row|
22
+ next if [row.leading, row.trailing] == expected
23
+
24
+ range = document.range(row.number, row.begin_pos, row.number, row.end_pos)
25
+ prose_offense(range, message: 'Use consistent leading and trailing table pipes.') do |corrector|
26
+ replacement = row.text[row.begin_pos...row.end_pos].dup
27
+ replacement = replacement[1..] if row.leading && !expected[0]
28
+ replacement = replacement[0...-1] if row.trailing && !expected[1]
29
+ replacement = "|#{replacement}" if !row.leading && expected[0]
30
+ replacement = "#{replacement}|" if !row.trailing && expected[1]
31
+ # Without an outer pipe, an empty boundary cell is indistinguishable
32
+ # from an outer pipe; leaving it alone preserves the column count.
33
+ next if !expected[0] && row.cells.first.text.strip.empty?
34
+ next if !expected[1] && row.cells.last.text.strip.empty?
35
+ next if expected.none? && row.cells.length == 1
36
+
37
+ corrector.replace(processed_range(range), replacement)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end