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,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../prose/heading_rules'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Markdown
8
+ class FirstHeading < Base
9
+ include RuboCop::Prose::HeadingRules
10
+
11
+ def investigate_document
12
+ return if front_matter_title?
13
+
14
+ first = document.lines.each_with_index.find do |text, index|
15
+ !document.front_matter_line?(index + 1) && !text.strip.empty?
16
+ end
17
+ return unless first
18
+
19
+ number = first.last + 1
20
+ heading = heading_entries.first
21
+ return if heading && heading[:line] == number && heading[:level] == cop_config.fetch('Level', 1)
22
+
23
+ prose_offense(document.line_range(number),
24
+ message: 'Start the document with a heading at the configured level.')
25
+ end
26
+
27
+ private
28
+
29
+ def front_matter_title?
30
+ return false unless cop_config.fetch('FrontMatterTitle', true) && document.front_matter
31
+
32
+ lines = document.front_matter.lines
33
+ metadata = YAML.safe_load(lines[1...-1].join)
34
+ return false unless metadata.is_a?(Hash)
35
+
36
+ title = metadata[cop_config.fetch('TitleKey', 'title')]
37
+ title.is_a?(String) && !title.strip.empty?
38
+ rescue Psych::Exception
39
+ false
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Markdown
6
+ class HardBreakStyle < Base
7
+ extend AutoCorrector
8
+
9
+ MSG = 'Use the configured Markdown hard-break style.'
10
+ STYLES = %w[backslash spaces].freeze
11
+
12
+ def validate_config
13
+ style = cop_config['Style']
14
+ return if STYLES.include?(style)
15
+
16
+ raise ValidationError, 'Markdown/HardBreakStyle: Style must be backslash or spaces.'
17
+ end
18
+
19
+ private
20
+
21
+ def investigate_document
22
+ document.hard_breaks.each { |node| investigate_break(node) }
23
+ end
24
+
25
+ def investigate_break(node)
26
+ number = node.options[:location]
27
+ text = document.line(number)
28
+ marker = marker_for(text)
29
+ return unless marker
30
+
31
+ style, first_column, last_column = marker
32
+ return if style.to_s == cop_config.fetch('Style', 'backslash')
33
+
34
+ range = document.range(number, first_column, number, last_column)
35
+ prose_offense(range) do |corrector|
36
+ next unless safe_to_correct?(style, text, first_column)
37
+
38
+ replacement = cop_config['Style'] == 'backslash' ? '\\' : ' '
39
+ correction_range = if style == :backslash
40
+ document.range(number, last_column - 1, number, last_column)
41
+ else
42
+ range
43
+ end
44
+ corrector.replace(processed_range(correction_range), replacement)
45
+ end
46
+ end
47
+
48
+ def marker_for(text)
49
+ if (spaces = / {2,}\z/.match(text))
50
+ [:spaces, spaces.begin(0), spaces.end(0)]
51
+ elsif (slashes = /\\+\z/.match(text))
52
+ [:backslash, slashes.begin(0), slashes.end(0)]
53
+ end
54
+ end
55
+
56
+ def safe_to_correct?(style, text, first_column)
57
+ return false unless processed_source.raw_source == processed_source.buffer.source
58
+ return true if style == :backslash
59
+
60
+ !text[...first_column].end_with?('\\')
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Markdown
6
+ class HardTabs < Base
7
+ private
8
+
9
+ def investigate_document
10
+ allowed = cop_config['AllowedCodeLanguages'].map(&:downcase)
11
+ exempt = document.nodes(:codeblock).flat_map do |block|
12
+ next [] unless allowed.include?(block.options[:lang].to_s.split.first.to_s.downcase)
13
+
14
+ first = block.options.fetch(:code_start_line)
15
+ (first...(first + block.value.lines.length)).to_a
16
+ end.to_set
17
+ document.lines.each_with_index do |text, index|
18
+ number = index + 1
19
+ next if document.front_matter_line?(number) || exempt.include?(number)
20
+ next if document.code_line?(number) && !cop_config['CodeBlocks']
21
+
22
+ text.to_enum(:scan, /\t/).each do
23
+ position = Regexp.last_match.begin(0)
24
+ prose_offense(document.range(number, position, number, position + 1),
25
+ message: 'Use spaces instead of hard tabs.')
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../prose/heading_rules'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Markdown
8
+ class HeadingBlankLines < Base
9
+ include RuboCop::Prose::HeadingRules
10
+
11
+ def investigate_document
12
+ heading_entries.each do |heading|
13
+ before = blank_count(heading[:line] - 1, -1)
14
+ after = blank_count(heading[:last_line] + 1, 1)
15
+ next unless before < cop_config.fetch('LinesAbove', 1) || after < cop_config.fetch('LinesBelow', 1)
16
+
17
+ prose_offense(heading_range(heading), message: 'Surround headings with the configured blank lines.')
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def blank_count(number, direction)
24
+ count = 0
25
+ while number.between?(1, document.lines.length)
26
+ return Float::INFINITY if document.front_matter_line?(number) && direction.negative?
27
+ break unless heading_blank?(number)
28
+
29
+ count += 1
30
+ number += direction
31
+ end
32
+ number.between?(1, document.lines.length) ? count : Float::INFINITY
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../prose/heading_rules'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Markdown
8
+ class HeadingIncrement < Base
9
+ include RuboCop::Prose::HeadingRules
10
+
11
+ def investigate_document
12
+ previous = nil
13
+ heading_entries.each do |heading|
14
+ if previous && heading[:level] > previous + 1
15
+ prose_offense(heading_range(heading), message: 'Increase heading levels by at most one.')
16
+ end
17
+ previous = heading[:level]
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../prose/heading_rules'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Markdown
8
+ class HeadingIndentation < Base
9
+ include RuboCop::Prose::HeadingRules
10
+
11
+ def investigate_document
12
+ parsed_lines = heading_entries.to_set { |heading| heading[:line] }
13
+ document.prose_lines.each do |text, number|
14
+ next unless text.match?(/\A[ \t]+\S/)
15
+ next unless parsed_lines.include?(number) || dedented_heading?(text, number)
16
+
17
+ prose_offense(document.line_range(number), message: 'Start headings without leading indentation.')
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def dedented_heading?(text, number)
24
+ return false unless text.match?(/\A {1,3}\S/)
25
+
26
+ source = "#{text.lstrip}\n"
27
+ following = document.lines[number]
28
+ if following&.match?(/\A {0,3}(?:=+|-+)[ \t]*\z/) && !document.code_line?(number + 1)
29
+ source += "#{following.lstrip}\n"
30
+ end
31
+ root = RuboCop::Prose::Parser.parse(source).first
32
+ root.children.any? { |node| node.type == :header }
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../prose/heading_rules'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Markdown
8
+ class HeadingPunctuation < Base
9
+ include RuboCop::Prose::HeadingRules
10
+
11
+ def investigate_document
12
+ punctuation = cop_config.fetch('Punctuation', '.,;:!?')
13
+ heading_entries.each do |heading|
14
+ ending = heading[:text].rstrip[-1]
15
+ next unless ending && punctuation.include?(ending)
16
+
17
+ prose_offense(heading_range(heading), message: 'Do not end headings with configured punctuation.')
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../prose/heading_rules'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Markdown
8
+ class HeadingStructure < Base
9
+ include RuboCop::Prose::HeadingRules
10
+
11
+ def investigate_document
12
+ expected = cop_config.fetch('Headings', []).map do |heading|
13
+ match = /\A(\#{1,6}) (.*)\z/.match(heading)
14
+ raise ArgumentError, 'Headings must contain ATX outline strings such as "# Title".' unless match
15
+
16
+ [match[1].length, match[2]]
17
+ end
18
+ return if expected.empty?
19
+
20
+ headings = heading_entries
21
+ headings.each_with_index do |heading, index|
22
+ next if expected[index] == [heading[:level], heading[:text]]
23
+
24
+ prose_offense(heading_range(heading), message: 'Match the configured exact heading outline.')
25
+ end
26
+ return unless expected.length > headings.length
27
+
28
+ prose_offense(heading_end_range, message: 'Add the missing headings from the configured exact outline.')
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../prose/heading_rules'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Markdown
8
+ class HeadingStyle < Base
9
+ include RuboCop::Prose::HeadingRules
10
+
11
+ def investigate_document
12
+ headings = heading_entries
13
+ style = cop_config.fetch('EnforcedStyle', 'consistent')
14
+ style = headings.first&.fetch(:style) if style == 'consistent'
15
+ headings.each do |heading|
16
+ next if heading[:style] == style
17
+
18
+ prose_offense(heading_range(heading), message: "Use #{style} headings.")
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../prose/link_rules'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Markdown
8
+ class ImageAltText < Base
9
+ MSG = 'Provide alternative text for the image.'
10
+
11
+ private
12
+
13
+ def investigate_document
14
+ scanner = document.link_rules
15
+ scanner.links.each do |link|
16
+ next unless link.image && scanner.resolved?(link) && scanner.display_text(link).empty?
17
+
18
+ prose_offense(scanner.link_range(link))
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../prose/link_rules'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Markdown
8
+ class ImagePathSeparators < Base
9
+ extend AutoCorrector
10
+
11
+ MSG = 'Use forward slashes in local image destinations.'
12
+
13
+ private
14
+
15
+ def investigate_document
16
+ scanner = document.link_rules
17
+ reported = Set.new
18
+ scanner.links.each do |link|
19
+ scanner.image_path_separators(link).each do |position, ambiguous|
20
+ next unless reported.add?(position)
21
+
22
+ range = scanner.range(position, position + 1)
23
+ prose_offense(range) do |corrector|
24
+ corrector.replace(processed_range(range), '/') unless ambiguous
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../prose/link_rules'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Markdown
8
+ class LinkFragment < Base
9
+ MSG = 'Link fragment does not identify a heading or HTML anchor in this document.'
10
+
11
+ private
12
+
13
+ def investigate_document
14
+ renderer = cop_config.fetch('Renderer', 'github')
15
+ raise ArgumentError, "Unknown anchor renderer: #{renderer}" unless %w[github gitlab
16
+ kramdown].include?(renderer)
17
+
18
+ scanner = document.link_rules
19
+ anchors = scanner.anchors(renderer, cop_config.fetch('AdditionalAnchors', []))
20
+ scanner.links.each do |link|
21
+ next if link.image || !scanner.resolved?(link)
22
+
23
+ target = scanner.destination(link)
24
+ next unless target&.start_with?('#')
25
+ next if target == '#' && cop_config.fetch('AllowEmptyFragment', true)
26
+
27
+ fragment = URI::DEFAULT_PARSER.unescape(target[1..])
28
+ next if anchors.include?(fragment)
29
+
30
+ prose_offense(scanner.link_range(link))
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../prose/link_rules'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Markdown
8
+ class LinkStyle < Base
9
+ MSG = 'Use an allowed %<kind>s style instead of %<style>s.'
10
+
11
+ private
12
+
13
+ def investigate_document
14
+ scanner = document.link_rules
15
+ scanner.links.each do |link|
16
+ next unless scanner.resolved?(link)
17
+
18
+ key = link.image ? 'AllowedImageStyles' : 'AllowedLinkStyles'
19
+ allowed = cop_config.fetch(key, %w[inline reference autolink])
20
+ next if allowed.include?(link.style) || (link.reference && allowed.include?('reference'))
21
+
22
+ prose_offense(scanner.link_range(link),
23
+ message: format(MSG, kind: link.image ? 'image' : 'link', style: link.style))
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../prose/link_rules'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Markdown
8
+ class LinkTextSpacing < Base
9
+ extend AutoCorrector
10
+
11
+ MSG = 'Remove whitespace at the edges of link text.'
12
+
13
+ private
14
+
15
+ def investigate_document
16
+ scanner = document.link_rules
17
+ scanner.links.each do |link|
18
+ next if link.image || link.style == 'autolink' || !scanner.resolved?(link)
19
+ next if link.label == link.label.strip
20
+
21
+ original = scanner.label_range(link)
22
+ if link.label.strip.empty?
23
+ prose_offense(original)
24
+ next
25
+ end
26
+ prose_offense(original) do |corrector|
27
+ corrector.replace(processed_range(original), link.label.strip)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../prose/list_rules'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Markdown
8
+ class ListBlankLines < Base
9
+ extend AutoCorrector
10
+ include RuboCop::Prose::ListRules
11
+
12
+ private
13
+
14
+ def investigate_document
15
+ @insertions = Set.new
16
+ list_groups do |list, marks, parent|
17
+ next if parent
18
+
19
+ first = marks.first[:line]
20
+ last = node_last_line(list)
21
+ prefix = continuation_prefix(document.line(first)[0...marks.first[:column]])
22
+ add_boundary(first, prefix) unless container_blank?(first - 1) || document.front_matter_line?(first - 1)
23
+ add_boundary(last + 1, prefix) unless container_blank?(last + 1)
24
+ end
25
+ end
26
+
27
+ def add_boundary(number, prefix)
28
+ return unless @insertions.add?(number)
29
+
30
+ range = document.line_range(number)
31
+ prose_offense(range, message: 'Surround lists with blank lines.') do |corrector|
32
+ corrector.insert_before(processed_range(range), "#{prefix.rstrip}\n")
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../prose/list_rules'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Markdown
8
+ class ListContinuationIndentation < Base
9
+ include RuboCop::Prose::ListRules
10
+
11
+ private
12
+
13
+ def investigate_document
14
+ list_groups do |_list, marks, _parent|
15
+ marks.each do |mark|
16
+ item_paragraphs(mark[:item]).each do |paragraph|
17
+ investigate_continuations(mark, paragraph)
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ def item_paragraphs(node)
24
+ return [] if %i[ul ol].include?(node.type)
25
+ return [node] if node.type == :p && node.options[:prose_source]
26
+
27
+ node.children.flat_map { |child| item_paragraphs(child) }
28
+ end
29
+
30
+ def investigate_continuations(mark, paragraph)
31
+ lines = document.line_numbers(paragraph).to_a
32
+ return if lines.length < 2
33
+
34
+ first_line = lines.first
35
+ expected = [mark[:content_column], paragraph_content_column(paragraph, first_line)].max
36
+ lines.drop(1).each do |number|
37
+ next if multiline_atom_line?(number)
38
+
39
+ actual = paragraph_content_column(paragraph, number)
40
+ next unless actual < expected
41
+ next if document.line(number)[actual..].empty?
42
+
43
+ range = document.range(number, actual, number, actual + 1)
44
+ prose_offense(range, message: "Indent list continuation text to column #{expected}.")
45
+ end
46
+ end
47
+
48
+ def paragraph_content_column(paragraph, number)
49
+ source_line = paragraph.options[:prose_source].split("\n", -1).fetch(number - paragraph.options[:location])
50
+ document.content_start(paragraph, number) + source_line[/\A[ \t]*/].length
51
+ end
52
+
53
+ def multiline_atom_line?(number)
54
+ document.multiline_atoms.any? { |atom| number.between?(atom.line, atom.last_line) }
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../prose/list_rules'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Markdown
8
+ class ListIndentation < Base
9
+ include RuboCop::Prose::ListRules
10
+
11
+ private
12
+
13
+ def investigate_document
14
+ list_groups do |_list, marks, _parent|
15
+ expected = marks.first[:indent]
16
+ marks.drop(1).each do |mark|
17
+ next if mark[:indent] == expected
18
+
19
+ prose_offense(document.range(mark[:line], mark[:column], mark[:line],
20
+ mark[:column] + mark[:marker].length),
21
+ message: "Align sibling list markers at indentation #{expected}.")
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../prose/list_rules'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Markdown
8
+ class ListMarkerSpacing < 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
+ multiline = marks.any? { |mark| node_last_line(mark[:item]) > mark[:line] }
17
+ key = "#{list.type == :ul ? 'Unordered' : 'Ordered'}#{multiline ? 'Multi' : 'Single'}"
18
+ spaces = cop_config.fetch(key, 1)
19
+ marks.each do |mark|
20
+ next if mark[:gap].empty? || mark[:gap] == ' ' * spaces
21
+
22
+ first = mark[:column] + mark[:marker].length
23
+ range = document.range(mark[:line], first, mark[:line], mark[:content_column])
24
+ prose_offense(range, message: "Use #{spaces} spaces after this list marker.") do |corrector|
25
+ next unless spacing_correction_safe?(mark, spaces)
26
+
27
+ corrector.replace(processed_range(range), ' ' * spaces)
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ def spacing_correction_safe?(mark, spaces)
34
+ item = mark[:item]
35
+ return false unless node_last_line(item) == mark[:line]
36
+ return false unless item.children.all? { |child| child.type == :p }
37
+
38
+ content = document.line(mark[:line])[mark[:content_column]..]
39
+ spaces.between?(1, 4) && !content.match?(/\A(?:[-+*]|\d+[.)]|>|`{3,}|~{3,}| {4})/)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end