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,189 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'inline_rules'
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Prose
|
|
7
|
+
module CapitalizationRules
|
|
8
|
+
WORD = /[\p{L}\p{N}][\p{L}\p{M}\p{N}_]*(?:['’][\p{L}\p{M}\p{N}_]+)*/
|
|
9
|
+
OpeningWord = Struct.new(:word, :begin_pos, :end_pos, :literal, :code, keyword_init: true)
|
|
10
|
+
QUOTES = { '"' => '"', "'" => "'", '“' => '”', '‘' => '’', '«' => '»', '‹' => '›',
|
|
11
|
+
'「' => '」', '『' => '』' }.freeze
|
|
12
|
+
|
|
13
|
+
def opening_word(source, offset = 0, literal: false, depth: 0, base: 0, markdown: true)
|
|
14
|
+
return if depth > 8
|
|
15
|
+
|
|
16
|
+
index = offset
|
|
17
|
+
index += 1 while source[index]&.match?(/[ \t]/)
|
|
18
|
+
character = source[index]
|
|
19
|
+
|
|
20
|
+
if (closing = QUOTES[character]) && source.index(closing, index + 1)
|
|
21
|
+
return opening_word(source, index + 1, literal: true, depth: depth + 1, base: base, markdown: markdown)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
if markdown && character == '`'
|
|
25
|
+
span = InlineRules.spans(source, links: false, html: false).find do |item|
|
|
26
|
+
item.kind == :code && item.begin_pos == index
|
|
27
|
+
end
|
|
28
|
+
if span
|
|
29
|
+
delimiter = span.delimiter.length
|
|
30
|
+
content_start = index + delimiter
|
|
31
|
+
content_end = span.end_pos - delimiter
|
|
32
|
+
content = source[content_start...content_end]
|
|
33
|
+
padding = content[/\A[ \t]*/].to_s.length
|
|
34
|
+
match = WORD.match(content, padding)
|
|
35
|
+
if match && match.begin(0) == padding && match[0].match?(/\p{L}/)
|
|
36
|
+
return opening_match(match, base + content_start, true, true)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
if markdown && character == '[' && source[index - 1] != '!'
|
|
42
|
+
close = InlineRules.balanced_end(source, index, '[', ']')
|
|
43
|
+
if close
|
|
44
|
+
return opening_word(source, index + 1, literal: literal, depth: depth + 1, base: base, markdown: markdown)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
if markdown && ['*', '_'].include?(character)
|
|
49
|
+
delimiter = source[index..][/\A(?:\*{1,3}|_{1,3})/]
|
|
50
|
+
closing = delimiter && source.index(delimiter, index + delimiter.length)
|
|
51
|
+
if closing
|
|
52
|
+
content_start = index + delimiter.length
|
|
53
|
+
inside = source[content_start...closing]
|
|
54
|
+
return opening_word(inside, literal: literal, depth: depth + 1, base: base + content_start,
|
|
55
|
+
markdown: markdown)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
match = WORD.match(source, index)
|
|
60
|
+
return unless match && match.begin(0) == index && match[0].match?(/\p{L}/)
|
|
61
|
+
|
|
62
|
+
opening_match(match, base, literal, false)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def opening_match(match, offset, literal, code)
|
|
66
|
+
OpeningWord.new(word: match[0], begin_pos: offset + match.begin(0), end_pos: offset + match.end(0),
|
|
67
|
+
literal: literal, code: code)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def lowercase_opening?(opening)
|
|
71
|
+
return false unless opening&.word&.match?(/\A\p{Ll}/)
|
|
72
|
+
return false if ignored_word?(opening.word)
|
|
73
|
+
|
|
74
|
+
!cop_config['PreserveMixedCase'] || !opening.word.match?(/\A\p{Ll}[\p{L}\p{M}\p{N}_]*[\p{Lu}\p{Lt}]/)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def ambiguous_identifier_opening?(source, opening)
|
|
78
|
+
opening.word.include?('_') ||
|
|
79
|
+
source[opening.end_pos..].match?(%r{\A(?:[.:/@=][\p{L}\p{N}]|://)})
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def identifier_source?(source)
|
|
83
|
+
source.match?(%r{
|
|
84
|
+
https?://
|
|
85
|
+
| www\.
|
|
86
|
+
| [\p{L}\p{N}_]+_[\p{L}\p{N}_]+
|
|
87
|
+
| [\p{L}\p{N}_]+/[\p{L}\p{N}_]+
|
|
88
|
+
| \b[\p{L}\p{N}_]+\.[A-Za-z0-9]{2,}\b
|
|
89
|
+
}ix)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def validate_config
|
|
93
|
+
styles = cop_config['SupportedStyles']
|
|
94
|
+
unless styles.include?(cop_config['Style'])
|
|
95
|
+
raise RuboCop::ValidationError, "#{cop_name}: Style must be one of #{styles.join(', ')}."
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
options = %w[IgnoredWords]
|
|
99
|
+
options << 'MinorWords' if cop_config.key?('MinorWords')
|
|
100
|
+
options.each do |option|
|
|
101
|
+
words = cop_config[option]
|
|
102
|
+
next if words.is_a?(Array) && words.all? { |word| word.is_a?(String) && word.match?(/\A#{WORD}\z/) }
|
|
103
|
+
|
|
104
|
+
raise RuboCop::ValidationError, "#{cop_name}: #{option} must be an array of individual words."
|
|
105
|
+
end
|
|
106
|
+
return if [true, false].include?(cop_config['PreserveMixedCase'])
|
|
107
|
+
|
|
108
|
+
raise RuboCop::ValidationError, "#{cop_name}: PreserveMixedCase must be true or false."
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
private
|
|
112
|
+
|
|
113
|
+
def correction_range(text, number)
|
|
114
|
+
return if text.empty? || text.include?("\n") || text.include?("\0")
|
|
115
|
+
|
|
116
|
+
source = document.line(number)
|
|
117
|
+
first = source.index(text)
|
|
118
|
+
return unless first && !source.index(text, first + 1)
|
|
119
|
+
|
|
120
|
+
last = first + text.length
|
|
121
|
+
protected = InlineRules.spans(source)
|
|
122
|
+
return if protected.any? { |span| first < span.end_pos && last > span.begin_pos }
|
|
123
|
+
|
|
124
|
+
document.range(number, first, number, last)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def capitalized_text(text, title_case)
|
|
128
|
+
words = text.scan(WORD).grep(/\p{L}/)
|
|
129
|
+
minor_words = cop_config['MinorWords'].map(&:downcase)
|
|
130
|
+
index = -1
|
|
131
|
+
text.gsub(WORD) do |word|
|
|
132
|
+
next word unless word.match?(/\p{L}/)
|
|
133
|
+
|
|
134
|
+
index += 1
|
|
135
|
+
next word if ignored_word?(word) || (!title_case && index.positive?)
|
|
136
|
+
|
|
137
|
+
if title_case && index.positive? && index < words.size - 1 && minor_words.include?(word.downcase)
|
|
138
|
+
word.downcase
|
|
139
|
+
elsif invalid_initial?(word)
|
|
140
|
+
word.sub(/\p{L}/, &:upcase)
|
|
141
|
+
else
|
|
142
|
+
word
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def invalid_initial?(text)
|
|
148
|
+
first = /[\p{L}\0]/.match(text)
|
|
149
|
+
return false unless first && first[0].match?(/\p{Ll}/)
|
|
150
|
+
return false if ignored_word?(text.scan(WORD).find { |word| word.match?(/\p{L}/) })
|
|
151
|
+
|
|
152
|
+
word = text[first.begin(0)..][/\A[\p{L}\p{N}_]+/]
|
|
153
|
+
!cop_config['PreserveMixedCase'] || !word&.match?(/\p{Lu}|\p{Lt}/)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def invalid_title?(text)
|
|
157
|
+
return false if text.include?("\0")
|
|
158
|
+
|
|
159
|
+
words = text.scan(WORD).grep(/\p{L}/)
|
|
160
|
+
minor_words = cop_config['MinorWords'].map(&:downcase)
|
|
161
|
+
words.each_with_index.any? do |word, index|
|
|
162
|
+
next false if ignored_word?(word)
|
|
163
|
+
|
|
164
|
+
if index.positive? && index < words.size - 1 && minor_words.include?(word.downcase)
|
|
165
|
+
word != word.downcase
|
|
166
|
+
else
|
|
167
|
+
invalid_initial?(word)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def ignored_word?(word)
|
|
173
|
+
cop_config['IgnoredWords'].include?(word)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def prose_text(node)
|
|
177
|
+
case node.type
|
|
178
|
+
when :text then node.value.to_s
|
|
179
|
+
when :entity then node.value.char
|
|
180
|
+
when :header, :p, :em, :strong, :a
|
|
181
|
+
node.children.map { |child| prose_text(child) }.join
|
|
182
|
+
else
|
|
183
|
+
# Do not check a later word after opaque leading content.
|
|
184
|
+
"\0"
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
@@ -58,7 +58,7 @@ module RuboCop
|
|
|
58
58
|
def self.spans(text, links: true, html: true)
|
|
59
59
|
result = []
|
|
60
60
|
index = 0
|
|
61
|
-
while index
|
|
61
|
+
while (index = text.index(/[\\`<!\[]/, index))
|
|
62
62
|
if text[index] == '\\' && text[index + 1]&.match?(/[[:punct:]]/)
|
|
63
63
|
result << Span.new(begin_pos: index, end_pos: [index + 2, text.length].min, kind: :escape)
|
|
64
64
|
index += 2
|
|
@@ -132,7 +132,8 @@ module RuboCop
|
|
|
132
132
|
|
|
133
133
|
def self.mask(text, include_code: false, document: nil, number: nil)
|
|
134
134
|
result = text.dup
|
|
135
|
-
|
|
135
|
+
protected = document && text == document.line(number) ? document.inline_spans(number) : spans(text)
|
|
136
|
+
protected.each do |span|
|
|
136
137
|
next if include_code && span.kind == :code
|
|
137
138
|
|
|
138
139
|
result[span.begin_pos...span.end_pos] = ' ' * (span.end_pos - span.begin_pos)
|
|
@@ -160,8 +161,10 @@ module RuboCop
|
|
|
160
161
|
source[range.begin_pos...range.end_pos] = ' ' * range.size
|
|
161
162
|
end
|
|
162
163
|
root = PositionedParser.parse(source, hard_wrap: false, gfm_quirks: %i[paragraph_end no_auto_typographic]).first
|
|
163
|
-
nodes = lambda do |node|
|
|
164
|
-
|
|
164
|
+
nodes = lambda do |node, found = []|
|
|
165
|
+
found << node
|
|
166
|
+
node.children.each { |child| nodes.call(child, found) } unless node.type == :table
|
|
167
|
+
found
|
|
165
168
|
end
|
|
166
169
|
records = nodes.call(root).map { |node| [node, 0, nil] }
|
|
167
170
|
document.nodes(:table).each do |table|
|
|
@@ -169,13 +172,16 @@ module RuboCop
|
|
|
169
172
|
while number <= document.lines.length && !document.line(number).strip.empty?
|
|
170
173
|
# Cell locations are not document-relative; parsing the row as inline prose
|
|
171
174
|
# recovers offsets without changing the table parser or its AST.
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
+
if document.line(number).match?(/[*_]/)
|
|
176
|
+
row = PositionedParser.parse("prose #{document.line(number).tr('|', ';')}",
|
|
177
|
+
hard_wrap: false, gfm_quirks: %i[paragraph_end no_auto_typographic]).first
|
|
178
|
+
records.concat(nodes.call(row).map { |node| [node, number - 1, -6] })
|
|
179
|
+
end
|
|
175
180
|
number += 1
|
|
176
181
|
end
|
|
177
182
|
end
|
|
178
183
|
ranges = { em: [], strong: [] }
|
|
184
|
+
source_lines = {}.compare_by_identity
|
|
179
185
|
records.each do |node, line_offset, column_offset|
|
|
180
186
|
next unless ranges.key?(node.type) && node.options[:prose_delimiters]
|
|
181
187
|
|
|
@@ -187,15 +193,16 @@ module RuboCop
|
|
|
187
193
|
through = text.byteslice(0, last)
|
|
188
194
|
local_line = before.count("\n")
|
|
189
195
|
last_number = number + through.count("\n") - local_line
|
|
190
|
-
|
|
191
|
-
|
|
196
|
+
lines = source_lines[text] ||= text.lines(chomp: true)
|
|
197
|
+
first_text = lines[local_line]
|
|
198
|
+
last_text = lines[through.count("\n")]
|
|
192
199
|
first_prefix = column_offset || document.line(number).rindex(first_text)
|
|
193
200
|
last_prefix = column_offset || document.line(last_number).rindex(last_text)
|
|
194
201
|
next unless first_prefix && last_prefix
|
|
195
202
|
|
|
196
203
|
column = first_prefix + before.rpartition("\n").last.length
|
|
197
204
|
last_column = last_prefix + through.rpartition("\n").last.length
|
|
198
|
-
next if
|
|
205
|
+
next if document.inline_spans(number).any? { |span| span.begin_pos <= column && span.end_pos > column }
|
|
199
206
|
|
|
200
207
|
position = document.range(number, column, number, column + 1)
|
|
201
208
|
next if multiline_atoms(document).any? { |range| range.contains?(position) }
|
|
@@ -22,6 +22,9 @@ module RuboCop
|
|
|
22
22
|
@links = []
|
|
23
23
|
@definitions = []
|
|
24
24
|
@html_anchors = []
|
|
25
|
+
@inline_destinations = {}
|
|
26
|
+
@definition_destinations = {}
|
|
27
|
+
@display_text = {}
|
|
25
28
|
exclude_blocks
|
|
26
29
|
exclude_spans
|
|
27
30
|
scan_definitions
|
|
@@ -93,8 +96,10 @@ module RuboCop
|
|
|
93
96
|
def display_text(link)
|
|
94
97
|
return link.destination if link.style == 'autolink'
|
|
95
98
|
|
|
96
|
-
|
|
97
|
-
|
|
99
|
+
@display_text.fetch(link.label) do
|
|
100
|
+
root = Parser.parse(link.label).first
|
|
101
|
+
@display_text[link.label] = self.class.plain_text(root).gsub(/[[:space:]]+/, ' ').strip
|
|
102
|
+
end
|
|
98
103
|
end
|
|
99
104
|
|
|
100
105
|
def self.plain_text(node)
|
|
@@ -281,7 +286,7 @@ module RuboCop
|
|
|
281
286
|
end
|
|
282
287
|
|
|
283
288
|
def codespan_end(index)
|
|
284
|
-
run = @text
|
|
289
|
+
run = /\G`+/.match(@text, index)[0]
|
|
285
290
|
cursor = index + run.length
|
|
286
291
|
while (match = /`+/.match(@text, cursor))
|
|
287
292
|
return match.end(0) if match[0].length == run.length
|
|
@@ -373,8 +378,9 @@ module RuboCop
|
|
|
373
378
|
|
|
374
379
|
depth = 1
|
|
375
380
|
cursor = index + 1
|
|
376
|
-
|
|
377
|
-
|
|
381
|
+
candidates = opening == '[' ? /[\[\]\n]/ : /[()\n]/
|
|
382
|
+
while (cursor = text.index(candidates, cursor))
|
|
383
|
+
return if text[cursor] == "\n" && /\G\n[ \t\r]*\n/.match?(text, cursor)
|
|
378
384
|
|
|
379
385
|
unless escaped?(cursor)
|
|
380
386
|
depth += 1 if text[cursor] == opening
|
|
@@ -391,8 +397,9 @@ module RuboCop
|
|
|
391
397
|
|
|
392
398
|
depth = 1
|
|
393
399
|
cursor = ending - 1
|
|
394
|
-
while cursor >= 0
|
|
395
|
-
return if @text[cursor] == "\n" &&
|
|
400
|
+
while cursor >= 0 && (cursor = @text.rindex(/[\[\]\n]/, cursor))
|
|
401
|
+
return if @text[cursor] == "\n" && (blank = /\G\n[ \t\r]*\n/.match(@text, cursor)) &&
|
|
402
|
+
blank.end(0) <= ending + 1
|
|
396
403
|
|
|
397
404
|
unless escaped?(cursor)
|
|
398
405
|
depth += 1 if @text[cursor] == ']'
|
|
@@ -405,7 +412,13 @@ module RuboCop
|
|
|
405
412
|
end
|
|
406
413
|
|
|
407
414
|
def unescaped_brackets?(first, last)
|
|
408
|
-
|
|
415
|
+
cursor = first
|
|
416
|
+
while (cursor = @source.index(/[\[\]]/, cursor)) && cursor < last
|
|
417
|
+
return true unless escaped?(cursor)
|
|
418
|
+
|
|
419
|
+
cursor += 1
|
|
420
|
+
end
|
|
421
|
+
false
|
|
409
422
|
end
|
|
410
423
|
|
|
411
424
|
def whitespace(index, text = @text)
|
|
@@ -413,7 +426,7 @@ module RuboCop
|
|
|
413
426
|
while text[cursor]&.match?(/[ \t\r\n]/)
|
|
414
427
|
newline = text[cursor] == "\n"
|
|
415
428
|
cursor += 1
|
|
416
|
-
cursor +=
|
|
429
|
+
cursor += /\G[ \t]*(?:>[ \t]*)+/.match(text, cursor).to_s.length if newline
|
|
417
430
|
end
|
|
418
431
|
return if text[index...cursor].count("\n") > 1
|
|
419
432
|
|
|
@@ -469,6 +482,10 @@ module RuboCop
|
|
|
469
482
|
end
|
|
470
483
|
|
|
471
484
|
def inline_destination(index)
|
|
485
|
+
@inline_destinations.fetch(index) { @inline_destinations[index] = read_inline_destination(index) }
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
def read_inline_destination(index)
|
|
472
489
|
first = whitespace(index + 1, @source)
|
|
473
490
|
return unless first
|
|
474
491
|
|
|
@@ -490,6 +507,10 @@ module RuboCop
|
|
|
490
507
|
end
|
|
491
508
|
|
|
492
509
|
def definition_destination(index)
|
|
510
|
+
@definition_destinations.fetch(index) { @definition_destinations[index] = read_definition_destination(index) }
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
def read_definition_destination(index)
|
|
493
514
|
start = whitespace(index, @source)
|
|
494
515
|
result = read_destination(start) if start
|
|
495
516
|
return unless result
|
|
@@ -497,8 +518,8 @@ module RuboCop
|
|
|
497
518
|
value, finish, destination_first, destination_last = result
|
|
498
519
|
gap = whitespace(finish, @source)
|
|
499
520
|
title = title_end(gap) if gap && gap > finish
|
|
500
|
-
finish = title if title &&
|
|
501
|
-
return unless
|
|
521
|
+
finish = title if title && /\G[ \t]*(?:\r?\n|\z)/.match?(@source, title)
|
|
522
|
+
return unless /\G[ \t]*(?:\r?\n|\z)/.match?(@source, finish)
|
|
502
523
|
return if value.empty? && @source[start] != '<'
|
|
503
524
|
|
|
504
525
|
[value, finish, title && @source[(gap + 1)...(title - 1)], destination_first, destination_last]
|
|
@@ -549,7 +570,7 @@ module RuboCop
|
|
|
549
570
|
links << link
|
|
550
571
|
# Images may supply the accessible text of an outer link.
|
|
551
572
|
nested = cursor + 1
|
|
552
|
-
while !link.image && nested < link.label_end_pos
|
|
573
|
+
while !link.image && (nested = @text.index('[', nested)) && nested < link.label_end_pos
|
|
553
574
|
if @text[nested] == '[' && @text[nested - 1] == '!' && !escaped?(nested - 1)
|
|
554
575
|
image = parse_link(nested)
|
|
555
576
|
links << image if image&.image && image.end_pos <= link.label_end_pos
|
|
@@ -607,7 +628,7 @@ module RuboCop
|
|
|
607
628
|
end
|
|
608
629
|
|
|
609
630
|
cursor = outer.label_begin_pos
|
|
610
|
-
while cursor < outer.label_end_pos
|
|
631
|
+
while (cursor = @text.index('[', cursor)) && cursor < outer.label_end_pos
|
|
611
632
|
if @text[cursor] == '[' && !escaped?(cursor)
|
|
612
633
|
inner = parse_link(cursor)
|
|
613
634
|
return true if inner && !inner.image && resolved?(inner) && inner.end_pos <= outer.label_end_pos
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'table_rules'
|
|
4
|
+
|
|
3
5
|
module RuboCop
|
|
4
6
|
module Prose
|
|
5
7
|
class MarkdownDocument < Document
|
|
@@ -20,6 +22,7 @@ module RuboCop
|
|
|
20
22
|
|
|
21
23
|
super(buffer)
|
|
22
24
|
@front_matter = source[FRONT_MATTER]
|
|
25
|
+
@front_matter_lines = @front_matter&.lines&.length
|
|
23
26
|
@ignore_front_matter = ignore_front_matter
|
|
24
27
|
end
|
|
25
28
|
|
|
@@ -48,6 +51,19 @@ module RuboCop
|
|
|
48
51
|
@link_rules ||= LinkRules.new(self)
|
|
49
52
|
end
|
|
50
53
|
|
|
54
|
+
def tables
|
|
55
|
+
@tables ||= TableRules.tables(self)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def inline_lines
|
|
59
|
+
@inline_lines ||= InlineRules.lines(self)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def inline_spans(number)
|
|
63
|
+
@inline_spans ||= {}
|
|
64
|
+
@inline_spans[number] ||= InlineRules.spans(line(number))
|
|
65
|
+
end
|
|
66
|
+
|
|
51
67
|
def emphasis_ranges(type)
|
|
52
68
|
@emphasis_ranges ||= InlineRules.emphasis(self)
|
|
53
69
|
@emphasis_ranges.fetch(type, [])
|
|
@@ -70,7 +86,7 @@ module RuboCop
|
|
|
70
86
|
end
|
|
71
87
|
|
|
72
88
|
def front_matter_line?(number)
|
|
73
|
-
@ignore_front_matter &&
|
|
89
|
+
@ignore_front_matter && @front_matter_lines && number <= @front_matter_lines
|
|
74
90
|
end
|
|
75
91
|
|
|
76
92
|
def code_line?(number)
|
|
@@ -128,8 +144,11 @@ module RuboCop
|
|
|
128
144
|
|
|
129
145
|
def atomic_ranges(paragraph)
|
|
130
146
|
paragraph.options[:original_ranges] ||= begin
|
|
131
|
-
|
|
132
|
-
|
|
147
|
+
atoms = paragraph.options[:prose_atoms]
|
|
148
|
+
unless atoms.empty?
|
|
149
|
+
inline = ::Parser::Source::Buffer.new('(paragraph)', source: paragraph.options[:prose_source])
|
|
150
|
+
end
|
|
151
|
+
atoms.map do |first, last|
|
|
133
152
|
start_line, start_column = inline.decompose_position(first)
|
|
134
153
|
end_line, end_column = inline.decompose_position(last)
|
|
135
154
|
first_line = paragraph.options[:location] + start_line - 1
|
|
@@ -153,14 +172,17 @@ module RuboCop
|
|
|
153
172
|
end
|
|
154
173
|
end
|
|
155
174
|
|
|
156
|
-
def walk(node)
|
|
157
|
-
|
|
175
|
+
def walk(node, found = [])
|
|
176
|
+
found << node
|
|
177
|
+
node.children.each { |child| walk(child, found) }
|
|
178
|
+
found
|
|
158
179
|
end
|
|
159
180
|
|
|
160
|
-
def find_hard_breaks(node)
|
|
161
|
-
node.children.
|
|
162
|
-
child.type == :br ?
|
|
181
|
+
def find_hard_breaks(node, found = [])
|
|
182
|
+
node.children.each do |child|
|
|
183
|
+
child.type == :br ? found << child : find_hard_breaks(child, found)
|
|
163
184
|
end
|
|
185
|
+
found
|
|
164
186
|
end
|
|
165
187
|
end
|
|
166
188
|
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Prose
|
|
5
|
+
module ProperNameRules
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def investigate_document
|
|
9
|
+
names = Array(cop_config['Names']).reject(&:empty?)
|
|
10
|
+
return if names.empty?
|
|
11
|
+
|
|
12
|
+
alternatives = names.sort_by { |name| -name.length }.map { |name| Regexp.escape(name) }.join('|')
|
|
13
|
+
pattern = /(?<![\p{L}\p{N}_])(?:#{alternatives})(?![\p{L}\p{N}_])/i
|
|
14
|
+
name_lines.each do |text, number, column|
|
|
15
|
+
text.to_enum(:scan, pattern).each do
|
|
16
|
+
match = Regexp.last_match
|
|
17
|
+
next if match.begin(0) < column
|
|
18
|
+
|
|
19
|
+
expected = names.find { |name| name.casecmp?(match[0]) }
|
|
20
|
+
next if match[0] == expected
|
|
21
|
+
|
|
22
|
+
range = document.range(number, match.begin(0), number, match.end(0))
|
|
23
|
+
prose_offense(range, message: "Use the capitalization `#{expected}`.") do |corrector|
|
|
24
|
+
corrector.replace(processed_range(range), expected)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -41,7 +41,7 @@ module RuboCop
|
|
|
41
41
|
def self.tables(document)
|
|
42
42
|
# A malformed body can make GFM reject the entire table. Its header and delimiter
|
|
43
43
|
# still identify the intended table, but only inside parser-recognized prose.
|
|
44
|
-
eligible =
|
|
44
|
+
eligible = document.inline_lines.to_set { |_, number, _| number }
|
|
45
45
|
result = []
|
|
46
46
|
number = 1
|
|
47
47
|
while number < document.lines.length
|
data/lib/rubocop-prose.rb
CHANGED