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.
- checksums.yaml +4 -4
- data/AGENTS.md +10 -2
- data/CHANGELOG.md +20 -0
- data/CONTRIBUTING.md +17 -7
- data/DESIGN.md +13 -5
- data/README.md +37 -20
- data/RELEASING.md +8 -8
- data/UPGRADING.md +16 -0
- data/assets/README.md +1 -1
- data/config/cops/markdown/code_block_style.yml +1 -1
- data/config/cops/markdown/emphasis_style.yml +4 -0
- data/config/cops/markdown/fence_style.yml +1 -1
- data/config/cops/markdown/first_heading.yml +1 -2
- data/config/cops/markdown/front_matter_keys.yml +5 -0
- data/config/cops/markdown/front_matter_syntax.yml +3 -0
- data/config/cops/markdown/hard_break_style.yml +3 -0
- data/config/cops/markdown/hard_tabs.yml +1 -1
- data/config/cops/markdown/heading_capitalization.yml +31 -0
- data/config/cops/markdown/heading_style.yml +1 -1
- data/config/cops/markdown/{syntax.yml → kramdown_syntax.yml} +1 -1
- data/config/cops/markdown/list_item_capitalization.yml +32 -0
- data/config/cops/markdown/list_item_punctuation.yml +13 -0
- data/config/cops/markdown/list_marker_spacing.yml +6 -5
- data/config/cops/markdown/ordered_list_numbering.yml +1 -1
- data/config/cops/markdown/reference_definitions.yml +2 -3
- data/config/cops/markdown/strong_style.yml +4 -0
- data/config/cops/markdown/structural_line_length.yml +1 -4
- data/config/cops/markdown/table_pipes.yml +6 -0
- data/config/cops/markdown/table_spacing.yml +4 -0
- data/config/cops/markdown/trailing_whitespace.yml +1 -0
- data/config/cops/markdown/unordered_list_marker.yml +1 -1
- data/config/cops/prose/sentence_capitalization.yml +9 -0
- data/config/cops/text/hard_tabs.yml +3 -0
- data/config/cops/text/proper_names.yml +4 -0
- data/config/obsoletion.yml +45 -0
- data/docs/cops/markdown/blockquote_spacing.md +2 -0
- data/docs/cops/markdown/code_block_style.md +11 -2
- data/docs/cops/markdown/duplicate_headings.md +51 -0
- data/docs/cops/markdown/emphasis_style.md +5 -1
- data/docs/cops/markdown/fence_style.md +11 -2
- data/docs/cops/markdown/first_heading.md +29 -3
- data/docs/cops/markdown/front_matter_keys.md +57 -0
- data/docs/cops/markdown/front_matter_syntax.md +32 -0
- data/docs/cops/markdown/hard_break_style.md +10 -5
- data/docs/cops/markdown/hard_tabs.md +3 -3
- data/docs/cops/markdown/heading_capitalization.md +176 -0
- data/docs/cops/markdown/heading_style.md +11 -2
- data/docs/cops/markdown/{syntax.md → kramdown_syntax.md} +6 -2
- data/docs/cops/markdown/list_item_capitalization.md +115 -0
- data/docs/cops/markdown/list_item_punctuation.md +70 -0
- data/docs/cops/markdown/list_marker_spacing.md +57 -7
- data/docs/cops/markdown/ordered_list_numbering.md +10 -1
- data/docs/cops/markdown/proper_names.md +3 -3
- data/docs/cops/markdown/raw_html.md +99 -6
- data/docs/cops/markdown/reference_definitions.md +30 -4
- data/docs/cops/markdown/strong_style.md +5 -1
- data/docs/cops/markdown/structural_line_length.md +31 -7
- data/docs/cops/markdown/table_pipes.md +7 -1
- data/docs/cops/markdown/table_spacing.md +5 -1
- data/docs/cops/markdown/thematic_break_style.md +2 -0
- data/docs/cops/markdown/trailing_whitespace.md +29 -0
- data/docs/cops/markdown/unordered_list_marker.md +11 -2
- data/docs/cops/prose/multiple_blank_lines.md +2 -0
- data/docs/cops/prose/sentence_capitalization.md +46 -0
- data/docs/cops/text/hard_tabs.md +28 -0
- data/docs/cops/text/proper_names.md +36 -0
- data/docs/settings/files.md +1 -1
- data/docs/settings/front_matter.md +2 -2
- data/lib/rubocop/cop/markdown/code_block_style.rb +1 -1
- data/lib/rubocop/cop/markdown/code_span_spacing.rb +2 -2
- data/lib/rubocop/cop/markdown/duplicate_headings.rb +55 -5
- data/lib/rubocop/cop/markdown/emphasis_spacing.rb +1 -1
- data/lib/rubocop/cop/markdown/emphasis_style.rb +7 -0
- data/lib/rubocop/cop/markdown/fence_style.rb +1 -1
- data/lib/rubocop/cop/markdown/first_heading.rb +13 -2
- data/lib/rubocop/cop/markdown/front_matter_keys.rb +70 -0
- data/lib/rubocop/cop/markdown/front_matter_syntax.rb +26 -0
- data/lib/rubocop/cop/markdown/hard_tabs.rb +11 -7
- data/lib/rubocop/cop/markdown/heading_capitalization.rb +43 -0
- data/lib/rubocop/cop/markdown/heading_style.rb +1 -1
- data/lib/rubocop/cop/markdown/{syntax.rb → kramdown_syntax.rb} +1 -1
- data/lib/rubocop/cop/markdown/list_item_capitalization.rb +90 -0
- data/lib/rubocop/cop/markdown/list_item_punctuation.rb +64 -0
- data/lib/rubocop/cop/markdown/list_marker_spacing.rb +13 -4
- data/lib/rubocop/cop/markdown/ordered_list_numbering.rb +1 -1
- data/lib/rubocop/cop/markdown/outside_spacing.rb +6 -6
- data/lib/rubocop/cop/markdown/proper_names.rb +20 -31
- data/lib/rubocop/cop/markdown/raw_html.rb +62 -7
- data/lib/rubocop/cop/markdown/reference_definitions.rb +15 -5
- data/lib/rubocop/cop/markdown/strong_style.rb +7 -0
- data/lib/rubocop/cop/markdown/structural_line_length.rb +7 -6
- data/lib/rubocop/cop/markdown/table_blank_lines.rb +1 -1
- data/lib/rubocop/cop/markdown/table_columns.rb +1 -1
- data/lib/rubocop/cop/markdown/table_pipes.rb +8 -1
- data/lib/rubocop/cop/markdown/table_spacing.rb +8 -1
- data/lib/rubocop/cop/markdown/thematic_break_style.rb +4 -0
- data/lib/rubocop/cop/markdown/trailing_whitespace.rb +6 -0
- data/lib/rubocop/cop/markdown/unordered_list_marker.rb +1 -1
- data/lib/rubocop/cop/prose/sentence_capitalization.rb +100 -0
- data/lib/rubocop/cop/text/hard_tabs.rb +21 -0
- data/lib/rubocop/cop/text/proper_names.rb +20 -0
- data/lib/rubocop/prose/capitalization_rules.rb +189 -0
- data/lib/rubocop/prose/inline_rules.rb +17 -10
- data/lib/rubocop/prose/link_rules.rb +34 -13
- data/lib/rubocop/prose/markdown_document.rb +30 -8
- data/lib/rubocop/prose/proper_name_rules.rb +31 -0
- data/lib/rubocop/prose/structural_blank_lines.rb +1 -1
- data/lib/rubocop/prose/table_rules.rb +1 -1
- data/lib/rubocop/prose/version.rb +1 -1
- data/lib/rubocop-prose.rb +3 -0
- 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 |
|
|
16
|
-
|
|
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('
|
|
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
|
-
|
|
26
|
+
document.inline_lines.flat_map do |_, number, column|
|
|
27
27
|
line_start = document.line_range(number).begin_pos
|
|
28
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
59
|
+
def protected_ranges(number, line_start)
|
|
60
60
|
@protected_ranges ||= {}
|
|
61
|
-
@protected_ranges[number] ||=
|
|
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
|
-
|
|
12
|
-
names = Array(cop_config['Names']).reject(&:empty?)
|
|
13
|
-
return if names.empty?
|
|
13
|
+
private
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
41
|
-
protected =
|
|
42
|
-
text.to_enum(:scan,
|
|
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
|
|
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 &&
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
34
|
+
message = if duplicate
|
|
25
35
|
'Remove or rename the duplicate reference definition.'
|
|
26
|
-
elsif !used.include?(definition.reference) && cop_config
|
|
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[
|
|
10
|
+
%w[Headings CodeBlocks Tables].each do |name|
|
|
11
11
|
value = cop_config[name]
|
|
12
|
-
next if
|
|
12
|
+
next if value == false
|
|
13
13
|
next if value.is_a?(Integer) && value.positive?
|
|
14
14
|
|
|
15
|
-
raise ValidationError,
|
|
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['
|
|
42
|
+
cop_config['Headings']
|
|
42
43
|
elsif document.code_line?(number) && cop_config['CodeBlocks']
|
|
43
|
-
cop_config['
|
|
44
|
+
cop_config['CodeBlocks']
|
|
44
45
|
elsif tables.include?(number) && cop_config['Tables']
|
|
45
|
-
cop_config['
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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('
|
|
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
|