rubocop-prose 0.1.0 → 0.2.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 (111) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +10 -2
  3. data/CHANGELOG.md +20 -0
  4. data/CONTRIBUTING.md +17 -7
  5. data/DESIGN.md +13 -5
  6. data/README.md +37 -20
  7. data/RELEASING.md +8 -8
  8. data/UPGRADING.md +16 -0
  9. data/assets/README.md +1 -1
  10. data/config/cops/markdown/code_block_style.yml +1 -1
  11. data/config/cops/markdown/emphasis_style.yml +4 -0
  12. data/config/cops/markdown/fence_style.yml +1 -1
  13. data/config/cops/markdown/first_heading.yml +1 -2
  14. data/config/cops/markdown/front_matter_keys.yml +5 -0
  15. data/config/cops/markdown/front_matter_syntax.yml +3 -0
  16. data/config/cops/markdown/hard_break_style.yml +3 -0
  17. data/config/cops/markdown/hard_tabs.yml +1 -1
  18. data/config/cops/markdown/heading_capitalization.yml +31 -0
  19. data/config/cops/markdown/heading_style.yml +1 -1
  20. data/config/cops/markdown/{syntax.yml → kramdown_syntax.yml} +1 -1
  21. data/config/cops/markdown/list_item_capitalization.yml +32 -0
  22. data/config/cops/markdown/list_item_punctuation.yml +13 -0
  23. data/config/cops/markdown/list_marker_spacing.yml +6 -5
  24. data/config/cops/markdown/ordered_list_numbering.yml +1 -1
  25. data/config/cops/markdown/reference_definitions.yml +2 -3
  26. data/config/cops/markdown/strong_style.yml +4 -0
  27. data/config/cops/markdown/structural_line_length.yml +1 -4
  28. data/config/cops/markdown/table_pipes.yml +6 -0
  29. data/config/cops/markdown/table_spacing.yml +4 -0
  30. data/config/cops/markdown/trailing_whitespace.yml +1 -0
  31. data/config/cops/markdown/unordered_list_marker.yml +1 -1
  32. data/config/cops/prose/sentence_capitalization.yml +9 -0
  33. data/config/cops/text/hard_tabs.yml +3 -0
  34. data/config/cops/text/proper_names.yml +4 -0
  35. data/config/obsoletion.yml +45 -0
  36. data/docs/cops/markdown/blockquote_spacing.md +2 -0
  37. data/docs/cops/markdown/code_block_style.md +11 -2
  38. data/docs/cops/markdown/duplicate_headings.md +51 -0
  39. data/docs/cops/markdown/emphasis_style.md +5 -1
  40. data/docs/cops/markdown/fence_style.md +11 -2
  41. data/docs/cops/markdown/first_heading.md +29 -3
  42. data/docs/cops/markdown/front_matter_keys.md +57 -0
  43. data/docs/cops/markdown/front_matter_syntax.md +32 -0
  44. data/docs/cops/markdown/hard_break_style.md +10 -5
  45. data/docs/cops/markdown/hard_tabs.md +3 -3
  46. data/docs/cops/markdown/heading_capitalization.md +176 -0
  47. data/docs/cops/markdown/heading_style.md +11 -2
  48. data/docs/cops/markdown/{syntax.md → kramdown_syntax.md} +6 -2
  49. data/docs/cops/markdown/list_item_capitalization.md +115 -0
  50. data/docs/cops/markdown/list_item_punctuation.md +70 -0
  51. data/docs/cops/markdown/list_marker_spacing.md +57 -7
  52. data/docs/cops/markdown/ordered_list_numbering.md +10 -1
  53. data/docs/cops/markdown/proper_names.md +3 -3
  54. data/docs/cops/markdown/raw_html.md +99 -6
  55. data/docs/cops/markdown/reference_definitions.md +30 -4
  56. data/docs/cops/markdown/strong_style.md +5 -1
  57. data/docs/cops/markdown/structural_line_length.md +31 -7
  58. data/docs/cops/markdown/table_pipes.md +7 -1
  59. data/docs/cops/markdown/table_spacing.md +5 -1
  60. data/docs/cops/markdown/thematic_break_style.md +2 -0
  61. data/docs/cops/markdown/trailing_whitespace.md +29 -0
  62. data/docs/cops/markdown/unordered_list_marker.md +11 -2
  63. data/docs/cops/prose/multiple_blank_lines.md +2 -0
  64. data/docs/cops/prose/sentence_capitalization.md +46 -0
  65. data/docs/cops/text/hard_tabs.md +28 -0
  66. data/docs/cops/text/proper_names.md +36 -0
  67. data/docs/settings/files.md +1 -1
  68. data/docs/settings/front_matter.md +2 -2
  69. data/lib/rubocop/cop/markdown/code_block_style.rb +1 -1
  70. data/lib/rubocop/cop/markdown/code_span_spacing.rb +2 -2
  71. data/lib/rubocop/cop/markdown/duplicate_headings.rb +55 -5
  72. data/lib/rubocop/cop/markdown/emphasis_spacing.rb +1 -1
  73. data/lib/rubocop/cop/markdown/emphasis_style.rb +7 -0
  74. data/lib/rubocop/cop/markdown/fence_style.rb +1 -1
  75. data/lib/rubocop/cop/markdown/first_heading.rb +13 -2
  76. data/lib/rubocop/cop/markdown/front_matter_keys.rb +70 -0
  77. data/lib/rubocop/cop/markdown/front_matter_syntax.rb +26 -0
  78. data/lib/rubocop/cop/markdown/hard_tabs.rb +11 -7
  79. data/lib/rubocop/cop/markdown/heading_capitalization.rb +43 -0
  80. data/lib/rubocop/cop/markdown/heading_style.rb +1 -1
  81. data/lib/rubocop/cop/markdown/{syntax.rb → kramdown_syntax.rb} +1 -1
  82. data/lib/rubocop/cop/markdown/list_item_capitalization.rb +90 -0
  83. data/lib/rubocop/cop/markdown/list_item_punctuation.rb +64 -0
  84. data/lib/rubocop/cop/markdown/list_marker_spacing.rb +13 -4
  85. data/lib/rubocop/cop/markdown/ordered_list_numbering.rb +1 -1
  86. data/lib/rubocop/cop/markdown/outside_spacing.rb +6 -6
  87. data/lib/rubocop/cop/markdown/proper_names.rb +20 -31
  88. data/lib/rubocop/cop/markdown/raw_html.rb +62 -7
  89. data/lib/rubocop/cop/markdown/reference_definitions.rb +15 -5
  90. data/lib/rubocop/cop/markdown/strong_style.rb +7 -0
  91. data/lib/rubocop/cop/markdown/structural_line_length.rb +7 -6
  92. data/lib/rubocop/cop/markdown/table_blank_lines.rb +1 -1
  93. data/lib/rubocop/cop/markdown/table_columns.rb +1 -1
  94. data/lib/rubocop/cop/markdown/table_pipes.rb +8 -1
  95. data/lib/rubocop/cop/markdown/table_spacing.rb +8 -1
  96. data/lib/rubocop/cop/markdown/thematic_break_style.rb +4 -0
  97. data/lib/rubocop/cop/markdown/trailing_whitespace.rb +6 -0
  98. data/lib/rubocop/cop/markdown/unordered_list_marker.rb +1 -1
  99. data/lib/rubocop/cop/prose/sentence_capitalization.rb +100 -0
  100. data/lib/rubocop/cop/text/hard_tabs.rb +21 -0
  101. data/lib/rubocop/cop/text/proper_names.rb +20 -0
  102. data/lib/rubocop/prose/capitalization_rules.rb +189 -0
  103. data/lib/rubocop/prose/inline_rules.rb +17 -10
  104. data/lib/rubocop/prose/link_rules.rb +34 -13
  105. data/lib/rubocop/prose/markdown_document.rb +30 -8
  106. data/lib/rubocop/prose/proper_name_rules.rb +31 -0
  107. data/lib/rubocop/prose/structural_blank_lines.rb +1 -1
  108. data/lib/rubocop/prose/table_rules.rb +1 -1
  109. data/lib/rubocop/prose/version.rb +1 -1
  110. data/lib/rubocop-prose.rb +3 -0
  111. metadata +32 -4
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../prose/list_rules'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Markdown
8
+ class ListItemPunctuation < Base
9
+ include RuboCop::Prose::ListRules
10
+
11
+ TERMINAL_PUNCTUATION = /[.!?…]+\z/
12
+ CLOSING_QUOTES = /["'’”»›]+\z/
13
+
14
+ private
15
+
16
+ def investigate_document
17
+ list_groups do |list, marks, _parent|
18
+ next unless marks.length == list.children.count { |child| child.type == :li }
19
+
20
+ items = marks.map { |mark| [mark, terminal_punctuation(mark[:item])] }
21
+ next if items.any? { |_mark, punctuation| punctuation.nil? }
22
+
23
+ style = cop_config.fetch('Style', 'consistent')
24
+ expected = style == 'consistent' ? items.first.last != false : style == 'always'
25
+ items.each do |mark, punctuation|
26
+ message = item_message(style, punctuation, expected)
27
+ next unless message
28
+
29
+ prose_offense(document.range(mark[:line], mark[:column], mark[:line],
30
+ mark[:column] + mark[:marker].length),
31
+ message: message)
32
+ end
33
+ end
34
+ end
35
+
36
+ def item_message(style, punctuation, expected)
37
+ return punctuation_message(style) if (punctuation != false) != expected
38
+ return unless punctuation
39
+ return if punctuation.each_char.all? { |char| cop_config['AllowedPunctuation'].include?(char) }
40
+
41
+ 'Use only configured terminal punctuation.'
42
+ end
43
+
44
+ def punctuation_message(style)
45
+ case style
46
+ when 'always' then 'End list items with terminal punctuation.'
47
+ when 'never' then 'Do not end list items with terminal punctuation.'
48
+ else 'Use terminal punctuation consistently with sibling list items.'
49
+ end
50
+ end
51
+
52
+ def terminal_punctuation(item)
53
+ return unless item.children.one? && item.children.first.type == :p
54
+
55
+ paragraph = item.children.first
56
+ return unless paragraph.options[:prose_source]
57
+
58
+ text = document.plain_text(paragraph).rstrip.sub(CLOSING_QUOTES, '')
59
+ text[TERMINAL_PUNCTUATION] || false
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -9,14 +9,23 @@ module RuboCop
9
9
  extend AutoCorrector
10
10
  include RuboCop::Prose::ListRules
11
11
 
12
+ def validate_config
13
+ unless %w[fixed aligned].include?(cop_config['Style'])
14
+ raise ValidationError, "#{cop_name}: Style must be fixed or aligned."
15
+ end
16
+ return if cop_config['MinSpaces'].is_a?(Integer) && cop_config['MinSpaces'].positive?
17
+
18
+ raise ValidationError, "#{cop_name}: MinSpaces must be a positive integer."
19
+ end
20
+
12
21
  private
13
22
 
14
23
  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)
24
+ list_groups do |_list, marks, _parent|
25
+ widest = marks.map { |mark| mark[:marker].length }.max
19
26
  marks.each do |mark|
27
+ spaces = cop_config['MinSpaces']
28
+ spaces += widest - mark[:marker].length if cop_config['Style'] == 'aligned'
20
29
  next if mark[:gap].empty? || mark[:gap] == ' ' * spaces
21
30
 
22
31
  first = mark[:column] + mark[:marker].length
@@ -17,7 +17,7 @@ module RuboCop
17
17
 
18
18
  first = marks.first[:marker].to_i
19
19
  marks.each_with_index do |mark, index|
20
- expected = cop_config.fetch('EnforcedStyle', 'sequential') == 'ones' ? 1 : first + index
20
+ expected = cop_config.fetch('Style', 'sequential') == 'ones' ? 1 : first + index
21
21
  digits = mark[:marker][/\A\d+/]
22
22
  next if digits == expected.to_s
23
23
 
@@ -23,16 +23,16 @@ module RuboCop
23
23
 
24
24
  def code_spans
25
25
  multiline = RuboCop::Prose::InlineRules.multiline_atoms(document)
26
- RuboCop::Prose::InlineRules.lines(document).flat_map do |text, number, column|
26
+ document.inline_lines.flat_map do |_, number, column|
27
27
  line_start = document.line_range(number).begin_pos
28
- RuboCop::Prose::InlineRules.spans(text).filter_map do |span|
28
+ document.inline_spans(number).filter_map do |span|
29
29
  next unless span.kind == :code && span.begin_pos >= column
30
30
 
31
31
  first = line_start + span.begin_pos
32
32
  last = line_start + span.end_pos
33
33
  range = ::Parser::Source::Range.new(document.buffer, first, last)
34
34
  next if multiline.any? { |atom| atom.overlaps?(range) }
35
- next if protected_ranges(number, text, line_start).any? { |atom| atom.overlaps?(range) }
35
+ next if protected_ranges(number, line_start).any? { |atom| atom.overlaps?(range) }
36
36
 
37
37
  [first, last, first + span.delimiter.length, last - span.delimiter.length]
38
38
  end
@@ -48,7 +48,7 @@ module RuboCop
48
48
  last = closing.end_pos
49
49
  number = opening.line
50
50
  line_start = document.line_range(number).begin_pos
51
- next if protected_ranges(number, document.line(number), line_start)
51
+ next if protected_ranges(number, line_start)
52
52
  .any? { |atom| atom.begin_pos < last && first < atom.end_pos }
53
53
 
54
54
  [first, last, opening.end_pos, closing.begin_pos]
@@ -56,9 +56,9 @@ module RuboCop
56
56
  end
57
57
  end
58
58
 
59
- def protected_ranges(number, text, line_start)
59
+ def protected_ranges(number, line_start)
60
60
  @protected_ranges ||= {}
61
- @protected_ranges[number] ||= RuboCop::Prose::InlineRules.spans(text).filter_map do |span|
61
+ @protected_ranges[number] ||= document.inline_spans(number).filter_map do |span|
62
62
  next if span.kind == :code
63
63
 
64
64
  ::Parser::Source::Range.new(document.buffer, line_start + span.begin_pos, line_start + span.end_pos)
@@ -1,49 +1,38 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../../prose/inline_rules'
4
+ require_relative '../../prose/proper_name_rules'
4
5
 
5
6
  module RuboCop
6
7
  module Cop
7
8
  module Markdown
8
9
  class ProperNames < Base
10
+ include RuboCop::Prose::ProperNameRules
9
11
  extend AutoCorrector
10
12
 
11
- def investigate_document
12
- names = Array(cop_config['Names']).reject(&:empty?)
13
- return if names.empty?
13
+ private
14
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
15
+ def name_lines
16
+ lines = document.inline_lines.map do |text, number, column|
17
+ [masked_markdown_line(text, number), number, column]
26
18
  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
19
+ return lines unless cop_config['CodeBlocks']
40
20
 
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
21
+ document.nodes(:codeblock).each do |node|
22
+ first = node.options.fetch(:code_start_line)
23
+ node.value.lines.each_index do |index|
24
+ number = first + index
25
+ lines << [document.line(number), number, 0] unless document.front_matter_line?(number)
45
26
  end
46
27
  end
28
+ lines
29
+ end
30
+
31
+ def masked_markdown_line(text, number)
32
+ return text if document.code_line?(number)
33
+
34
+ RuboCop::Prose::InlineRules.mask(text, include_code: cop_config['CodeBlocks'],
35
+ document: document, number: number)
47
36
  end
48
37
  end
49
38
  end
@@ -6,8 +6,15 @@ module RuboCop
6
6
  module Cop
7
7
  module Markdown
8
8
  class RawHtml < Base
9
+ OPENING_TAG = /<([A-Za-z][\w:-]*)\b(?:[^"'<>]|"[^"]*"|'[^']*')*(?:>|\z)/
10
+ ATTRIBUTE = /\s+([a-z_:][\w:.-]*)(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'=<>`]+))?/i
11
+
12
+ def validate_config
13
+ allowed_elements
14
+ end
15
+
9
16
  def investigate_document
10
- allowed = Array(cop_config['AllowedElements']).map(&:downcase)
17
+ allowed = allowed_elements
11
18
  offsets = Hash.new(0)
12
19
  located = []
13
20
  unlocated = []
@@ -23,7 +30,7 @@ module RuboCop
23
30
 
24
31
  text = document.line(number)
25
32
  key = [number, node.value.to_s.downcase]
26
- match = /<#{Regexp.escape(node.value.to_s)}\b[^>]*(?:>|\z)/i.match(text, offsets[key])
33
+ match = opening_tag(text, node.value, offsets[key])
27
34
  unless match
28
35
  unlocated << node.value.to_s.downcase
29
36
  next
@@ -31,15 +38,15 @@ module RuboCop
31
38
 
32
39
  offsets[key] = match.end(0)
33
40
  located << [number, match.begin(0)]
34
- next if allowed.include?(node.value.to_s.downcase)
41
+ next if element_allowed?(node.value, match[0], allowed)
35
42
 
36
43
  prose_offense(document.range(number, match.begin(0), number, match.end(0)),
37
44
  message: "Raw HTML element <#{node.value}> is not allowed.")
38
45
  end
39
46
  # 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
47
+ document.inline_lines.each do |text, number, _|
48
+ protected = document.inline_spans(number).reject { |span| span.kind == :html }
49
+ text.to_enum(:scan, OPENING_TAG).each do
43
50
  match = Regexp.last_match
44
51
  name = match[1].downcase
45
52
  next unless unlocated.include?(name)
@@ -47,13 +54,61 @@ module RuboCop
47
54
  next if protected.any? { |span| match.begin(0) >= span.begin_pos && match.begin(0) < span.end_pos }
48
55
 
49
56
  unlocated.delete_at(unlocated.index(name))
50
- next if allowed.include?(name)
57
+ next if element_allowed?(name, match[0], allowed)
51
58
 
52
59
  prose_offense(document.range(number, match.begin(0), number, match.end(0)),
53
60
  message: "Raw HTML element <#{match[1]}> is not allowed.")
54
61
  end
55
62
  end
56
63
  end
64
+
65
+ private
66
+
67
+ def allowed_elements
68
+ configured = cop_config.fetch('AllowedElements', [])
69
+ unless configured.is_a?(Array)
70
+ raise ValidationError,
71
+ 'Markdown/RawHtml: AllowedElements must be an array of element names or attribute rules.'
72
+ end
73
+
74
+ configured.map do |entry|
75
+ if entry.is_a?(String) && entry.match?(/\A[a-z][\w:-]*\z/i)
76
+ [entry.downcase, nil]
77
+ elsif entry.is_a?(Hash) && entry.size == 1
78
+ attribute_rule(*entry.first)
79
+ else
80
+ raise ValidationError, 'Markdown/RawHtml: AllowedElements entries must be element names or ' \
81
+ 'single-element mappings to attribute lists.'
82
+ end
83
+ end
84
+ end
85
+
86
+ def attribute_rule(element, attributes)
87
+ unless element.is_a?(String) && element.match?(/\A[a-z][\w:-]*\z/i)
88
+ raise ValidationError, 'Markdown/RawHtml: AllowedElements rules must use valid element names.'
89
+ end
90
+
91
+ unless attributes.is_a?(Array) &&
92
+ attributes.all? { |attribute| attribute.is_a?(String) && attribute.match?(/\A[a-z_:][\w:.-]*\z/i) }
93
+ raise ValidationError, 'Markdown/RawHtml: Attribute rules must be arrays of attribute names.'
94
+ end
95
+
96
+ [element.downcase, attributes.map(&:downcase)]
97
+ end
98
+
99
+ def opening_tag(text, name, offset)
100
+ pattern = /<#{Regexp.escape(name.to_s)}\b(?:[^"'<>]|"[^"]*"|'[^']*')*(?:>|\z)/i
101
+ pattern.match(text, offset)
102
+ end
103
+
104
+ def element_allowed?(name, tag, allowed)
105
+ present = tag.scan(ATTRIBUTE).flatten.map(&:downcase)
106
+ allowed.any? do |element, attributes|
107
+ next false unless element == name.to_s.downcase
108
+
109
+ attributes.nil? || present.all? { |attribute| attributes.include?(attribute) }
110
+ end
111
+ end
57
112
  end
58
113
  end
59
114
  end
@@ -6,6 +6,15 @@ module RuboCop
6
6
  module Cop
7
7
  module Markdown
8
8
  class ReferenceDefinitions < Base
9
+ def validate_config
10
+ unless %w[forbid allow_identical allow].include?(cop_config['Duplicates'])
11
+ raise ValidationError, "#{cop_name}: Duplicates must be forbid, allow_identical, or allow."
12
+ end
13
+ return if [true, false].include?(cop_config['Unused'])
14
+
15
+ raise ValidationError, "#{cop_name}: Unused must be true or false."
16
+ end
17
+
9
18
  private
10
19
 
11
20
  def investigate_document
@@ -16,14 +25,15 @@ module RuboCop
16
25
  scanner.definitions.each do |definition|
17
26
  first = seen[definition.reference]
18
27
  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)
28
+ duplicate = first && cop_config['Duplicates'] != 'allow' &&
29
+ !(cop_config['Duplicates'] == 'allow_identical' &&
30
+ scanner.decode(first.destination) == scanner.decode(definition.destination) &&
31
+ first.title == definition.title)
22
32
  next if ignored.include?(definition.reference)
23
33
 
24
- message = if duplicate && cop_config.fetch('CheckDuplicates', true)
34
+ message = if duplicate
25
35
  'Remove or rename the duplicate reference definition.'
26
- elsif !used.include?(definition.reference) && cop_config.fetch('CheckUnused', false)
36
+ elsif !used.include?(definition.reference) && cop_config['Unused']
27
37
  'Reference definition is not used.'
28
38
  end
29
39
  next unless message
@@ -8,6 +8,13 @@ module RuboCop
8
8
  class StrongStyle < Base
9
9
  extend AutoCorrector
10
10
 
11
+ def validate_config
12
+ styles = cop_config['SupportedStyles']
13
+ return if styles.include?(cop_config['Style'])
14
+
15
+ raise ValidationError, "#{cop_name}: Style must be one of #{styles.join(', ')}."
16
+ end
17
+
11
18
  def investigate_document
12
19
  expected = cop_config.fetch('Style', 'consistent')
13
20
  document.emphasis_ranges(:strong).each do |opening, closing, char|
@@ -7,12 +7,13 @@ module RuboCop
7
7
  MSG = 'Markdown source line is too long. [%<length>d/%<max>d]'
8
8
 
9
9
  def validate_config
10
- %w[Max HeadingMax CodeBlockMax].each do |name|
10
+ %w[Headings CodeBlocks Tables].each do |name|
11
11
  value = cop_config[name]
12
- next if name != 'Max' && value.nil?
12
+ next if value == false
13
13
  next if value.is_a?(Integer) && value.positive?
14
14
 
15
- raise ValidationError, "Markdown/StructuralLineLength: #{name} must be a positive integer."
15
+ raise ValidationError,
16
+ "Markdown/StructuralLineLength: #{name} must be a positive integer or false."
16
17
  end
17
18
  end
18
19
 
@@ -38,11 +39,11 @@ module RuboCop
38
39
 
39
40
  def structural_limit(number, headings, tables)
40
41
  if headings.include?(number) && cop_config['Headings']
41
- cop_config['HeadingMax'] || cop_config['Max']
42
+ cop_config['Headings']
42
43
  elsif document.code_line?(number) && cop_config['CodeBlocks']
43
- cop_config['CodeBlockMax'] || cop_config['Max']
44
+ cop_config['CodeBlocks']
44
45
  elsif tables.include?(number) && cop_config['Tables']
45
- cop_config['Max']
46
+ cop_config['Tables']
46
47
  end
47
48
  end
48
49
  end
@@ -9,7 +9,7 @@ module RuboCop
9
9
  extend AutoCorrector
10
10
 
11
11
  def investigate_document
12
- RuboCop::Prose::TableRules.tables(document).each do |table|
12
+ document.tables.each do |table|
13
13
  first = table.rows.first.number
14
14
  last = table.rows.last.number
15
15
  unless first == 1 || blank?(document.line(first - 1))
@@ -7,7 +7,7 @@ module RuboCop
7
7
  module Markdown
8
8
  class TableColumns < Base
9
9
  def investigate_document
10
- RuboCop::Prose::TableRules.tables(document).each do |table|
10
+ document.tables.each do |table|
11
11
  count = table.rows.first.cells.length
12
12
  table.rows.drop(1).each do |row|
13
13
  next if row.cells.length == count
@@ -8,8 +8,15 @@ module RuboCop
8
8
  class TablePipes < Base
9
9
  extend AutoCorrector
10
10
 
11
+ def validate_config
12
+ styles = cop_config['SupportedStyles']
13
+ return if styles.include?(cop_config['Style'])
14
+
15
+ raise ValidationError, "#{cop_name}: Style must be one of #{styles.join(', ')}."
16
+ end
17
+
11
18
  def investigate_document
12
- RuboCop::Prose::TableRules.tables(document).each do |table|
19
+ document.tables.each do |table|
13
20
  style = cop_config.fetch('Style', 'consistent')
14
21
  expected = case style
15
22
  when 'leading_and_trailing' then [true, true]
@@ -8,8 +8,15 @@ module RuboCop
8
8
  class TableSpacing < Base
9
9
  extend AutoCorrector
10
10
 
11
+ def validate_config
12
+ styles = cop_config['SupportedStyles']
13
+ return if styles.include?(cop_config['Style'])
14
+
15
+ raise ValidationError, "#{cop_name}: Style must be one of #{styles.join(', ')}."
16
+ end
17
+
11
18
  def investigate_document
12
- RuboCop::Prose::TableRules.tables(document).each do |table|
19
+ document.tables.each do |table|
13
20
  style = cop_config.fetch('Style', 'aligned')
14
21
  widths = table.rows.each_with_object({}) do |row, sizes|
15
22
  row.cells.each_with_index do |cell, index|
@@ -6,6 +6,10 @@ module RuboCop
6
6
  class ThematicBreakStyle < Base
7
7
  extend AutoCorrector
8
8
 
9
+ def validate_config
10
+ configured_styles(cop_config['Style'])
11
+ end
12
+
9
13
  def investigate_document
10
14
  style = cop_config.fetch('Style', 'consistent')
11
15
  styles = configured_styles(style)
@@ -27,6 +27,7 @@ module RuboCop
27
27
 
28
28
  match = text.match(/[ \t]+\z/)
29
29
  next unless match
30
+ next if allowed_empty_blockquote_space?(text, number, match[0])
30
31
 
31
32
  spaces = text[/ +\z/].to_s.length
32
33
  hard_break = !text.strip.empty? && paragraphs.include?(number) && spaces >= 2
@@ -49,6 +50,11 @@ module RuboCop
49
50
  style = config.for_cop('Markdown/HardBreakStyle')
50
51
  style if style && style['Enabled']
51
52
  end
53
+
54
+ def allowed_empty_blockquote_space?(text, number, whitespace)
55
+ cop_config['AllowEmptyBlockquoteSpace'] && whitespace == ' ' &&
56
+ !document.code_line?(number) && text.match?(/\A[ \t]*>(?:[ \t]*>)* \z/)
57
+ end
52
58
  end
53
59
  end
54
60
  end
@@ -12,7 +12,7 @@ module RuboCop
12
12
  private
13
13
 
14
14
  def investigate_document
15
- style = cop_config.fetch('EnforcedStyle', 'consistent')
15
+ style = cop_config.fetch('Style', 'consistent')
16
16
  expected = { 'asterisk' => '*', 'plus' => '+', 'dash' => '-' }[style]
17
17
  changes = []
18
18
  list_groups do |list, marks, _parent|
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../prose/capitalization_rules'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Prose
8
+ class SentenceCapitalization < Base
9
+ extend AutoCorrector
10
+ include RuboCop::Prose::CapitalizationRules
11
+
12
+ MSG = 'Capitalize or rephrase this lowercase paragraph opening without changing its literal text.'
13
+
14
+ def investigate_document
15
+ if document.is_a?(RuboCop::Prose::MarkdownDocument)
16
+ investigate_markdown
17
+ else
18
+ investigate_text
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def investigate_markdown
25
+ list_openings = list_opening_paragraphs
26
+ table_paragraphs = paragraphs_in_tables
27
+ (document.paragraphs + footnote_paragraphs).uniq(&:object_id).each do |paragraph|
28
+ next if list_openings.include?(paragraph.object_id) || table_paragraphs.include?(paragraph.object_id)
29
+
30
+ source = paragraph.options[:prose_source]
31
+ opening = opening_word(source)
32
+ next if opening && !opening.literal && ambiguous_identifier_opening?(source, opening)
33
+
34
+ number = paragraph.options[:location]
35
+ investigate_opening(source, number, opening, document.content_start(paragraph, number))
36
+ end
37
+ end
38
+
39
+ def investigate_text
40
+ document.paragraphs.each do |paragraph|
41
+ number = paragraph.first_line
42
+ source = document.line(number)
43
+ opening = opening_word(source, markdown: false)
44
+ next if opening && !opening.literal && ambiguous_identifier_opening?(source, opening)
45
+
46
+ investigate_opening(source, number, opening)
47
+ end
48
+ end
49
+
50
+ def investigate_opening(source, number, opening, column_offset = 0)
51
+ return unless lowercase_opening?(opening)
52
+ return if source[0...opening.begin_pos].include?("\n")
53
+
54
+ column = column_offset + opening.begin_pos
55
+ range = document.range(number, column, number, column + 1)
56
+ prose_offense(range, message: MSG) do |corrector|
57
+ next if opening.literal
58
+
59
+ corrector.replace(processed_range(range), range.source.upcase)
60
+ end
61
+ end
62
+
63
+ def list_opening_paragraphs
64
+ openings = []
65
+ visit = lambda do |node|
66
+ if node.type == :li
67
+ first = node.children.first
68
+ openings << first.object_id if first&.type == :p && first.options[:prose_source]
69
+ end
70
+ node.children.each { |child| visit.call(child) }
71
+ end
72
+ visit.call(document.root)
73
+ openings
74
+ end
75
+
76
+ def paragraphs_in_tables
77
+ paragraphs = []
78
+ visit = lambda do |node|
79
+ paragraphs << node.object_id if node.type == :p && node.options[:prose_source]
80
+ node.children.each { |child| visit.call(child) }
81
+ end
82
+ document.nodes(:table).each { |table| visit.call(table) }
83
+ paragraphs
84
+ end
85
+
86
+ def footnote_paragraphs
87
+ paragraphs = []
88
+ visit = lambda do |node|
89
+ paragraphs << node if node.type == :p && node.options[:prose_source]
90
+ node.children.each { |child| visit.call(child) }
91
+ end
92
+ document.nodes(:footnote).each do |footnote|
93
+ visit.call(footnote.value) if footnote.value.respond_to?(:children)
94
+ end
95
+ paragraphs
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Text
6
+ class HardTabs < Base
7
+ private
8
+
9
+ def investigate_document
10
+ document.lines.each_with_index do |text, index|
11
+ text.to_enum(:scan, /\t/).each do
12
+ position = Regexp.last_match.begin(0)
13
+ prose_offense(document.range(index + 1, position, index + 1, position + 1),
14
+ message: 'Use spaces instead of hard tabs.')
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../prose/proper_name_rules'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Text
8
+ class ProperNames < Base
9
+ include RuboCop::Prose::ProperNameRules
10
+ extend AutoCorrector
11
+
12
+ private
13
+
14
+ def name_lines
15
+ document.lines.each_with_index.map { |text, index| [text, index + 1, 0] }
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end