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
|
@@ -4,12 +4,17 @@ Trims trailing whitespace while preserving paragraph hard breaks at the configur
|
|
|
4
4
|
|
|
5
5
|
When [Markdown/HardBreakStyle](hard_break_style.md) is enabled, that cop selects the hard-break syntax and this cop avoids overlapping corrections.
|
|
6
6
|
|
|
7
|
+
Set `AllowEmptyBlockquoteSpace: true` to permit exactly one trailing ASCII space on an otherwise-empty blockquote line, including nested quotes. Bare quote markers remain valid. Two or more trailing spaces, tabs, and mixed trailing whitespace are still rejected; paragraph hard breaks and code-block handling are unchanged.
|
|
8
|
+
|
|
9
|
+
[Markdown/BlockquoteSpacing](blockquote_spacing.md) leaves empty quote markers alone, and [Markdown/BlockquoteBlankLines](blockquote_blank_lines.md) adds bare markers without requiring a trailing space. [Prose/MultipleBlankLines](../prose/multiple_blank_lines.md) still counts empty quote lines toward `Max`, whether they have the allowed space or not.
|
|
10
|
+
|
|
7
11
|
## Configuration
|
|
8
12
|
|
|
9
13
|
```yaml
|
|
10
14
|
Markdown/TrailingWhitespace:
|
|
11
15
|
Enabled: true
|
|
12
16
|
HardBreakSpaces: 2 # Must be an integer of at least 2.
|
|
17
|
+
AllowEmptyBlockquoteSpace: false
|
|
13
18
|
CodeBlocks: false # When true, code whitespace is report-only to preserve embedded strings.
|
|
14
19
|
```
|
|
15
20
|
|
|
@@ -28,3 +33,27 @@ Inputs are shown as Ruby strings so tabs, trailing spaces, and newline character
|
|
|
28
33
|
```ruby
|
|
29
34
|
"A paragraph.\n"
|
|
30
35
|
```
|
|
36
|
+
|
|
37
|
+
## Empty Blockquote Lines
|
|
38
|
+
|
|
39
|
+
For this configuration:
|
|
40
|
+
|
|
41
|
+
```yaml
|
|
42
|
+
Markdown/TrailingWhitespace:
|
|
43
|
+
AllowEmptyBlockquoteSpace: true
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Bad
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
"> First paragraph.\n> \n> Second paragraph.\n"
|
|
50
|
+
"> First paragraph.\n>\t\n> Second paragraph.\n"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Good
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
"> First paragraph.\n> \n> Second paragraph.\n"
|
|
57
|
+
"> > First paragraph.\n> > \n> > Second paragraph.\n"
|
|
58
|
+
"> First paragraph.\n>\n> Second paragraph.\n"
|
|
59
|
+
```
|
|
@@ -9,7 +9,7 @@ Autocorrection changes only the marker and is withheld if it could create a them
|
|
|
9
9
|
```yaml
|
|
10
10
|
Markdown/UnorderedListMarker:
|
|
11
11
|
Enabled: true
|
|
12
|
-
|
|
12
|
+
Style: consistent # consistent, asterisk, plus or dash.
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
## Example
|
|
@@ -18,7 +18,7 @@ For this configuration:
|
|
|
18
18
|
|
|
19
19
|
```yaml
|
|
20
20
|
Markdown/UnorderedListMarker:
|
|
21
|
-
|
|
21
|
+
Style: dash
|
|
22
22
|
```
|
|
23
23
|
|
|
24
24
|
### Bad
|
|
@@ -34,3 +34,12 @@ Markdown/UnorderedListMarker:
|
|
|
34
34
|
- one
|
|
35
35
|
- two
|
|
36
36
|
```
|
|
37
|
+
|
|
38
|
+
## Migrating From 0.1.0
|
|
39
|
+
|
|
40
|
+
Replace `EnforcedStyle` with `Style`:
|
|
41
|
+
|
|
42
|
+
```yaml
|
|
43
|
+
Markdown/UnorderedListMarker:
|
|
44
|
+
Style: dash
|
|
45
|
+
```
|
|
@@ -4,6 +4,8 @@ Removes excess consecutive blank lines while preserving paragraph separation. In
|
|
|
4
4
|
|
|
5
5
|
[Markdown/BlockquoteBlankLines](../markdown/blockquote_blank_lines.md) handles the separate case of adding markers between adjacent blockquotes.
|
|
6
6
|
|
|
7
|
+
Empty quote lines with the single trailing space permitted by [Markdown/TrailingWhitespace](../markdown/trailing_whitespace.md)'s `AllowEmptyBlockquoteSpace` option still count toward `Max`; retained lines keep their original spelling.
|
|
8
|
+
|
|
7
9
|
At Markdown structural boundaries, the effective limit is at least the minimum required by the enabled [heading](../markdown/heading_blank_lines.md), [fence](../markdown/fence_blank_lines.md), [list](../markdown/list_blank_lines.md), or [table](../markdown/table_blank_lines.md) spacing cop in that file's configuration. Shared boundaries use the largest minimum, not their sum. `Max` still applies elsewhere, including document edges where no structural padding is required.
|
|
8
10
|
|
|
9
11
|
## Configuration
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Prose/SentenceCapitalization
|
|
2
|
+
|
|
3
|
+
Checks lowercase words at paragraph openings in Markdown and plain-text files using the single `initial_capital` style. It is an opt-in editorial rule. It checks ordinary prose and reports lowercase openings inside inline code or quoted terms in Markdown, but never changes quoted or code contents. For literal terms, rewrite the sentence around the term rather than changing the literal spelling.
|
|
4
|
+
|
|
5
|
+
The cop checks paragraph openings only; it does not split paragraphs into sentences. In Markdown, it handles paragraphs in blockquotes, footnotes, and list items, but leaves each list item's opening paragraph to [Markdown/ListItemCapitalization](../markdown/list_item_capitalization.md) so consistent lowercase lists remain possible. Subsequent paragraphs in list items are checked.
|
|
6
|
+
|
|
7
|
+
In plain-text files, paragraphs are literal groups of nonblank lines. The cop does not interpret Markdown-like code, emphasis, links, or containers in text files. A quoted paragraph opening is still reported without correction. Plain-text list-item capitalization is not inferred.
|
|
8
|
+
|
|
9
|
+
Headings, tables, code blocks, and ignored front matter are not Markdown paragraphs for this rule. Mixed-case brands such as `eBay` pass by default. `IgnoredWords` exempts exact, case-sensitive words. Native RuboCop suppression directives can silence intentional Markdown exceptions; plain-text files use `IgnoredWords`.
|
|
10
|
+
|
|
11
|
+
Supports unsafe autocorrection with `rubocop -A` for a mapped ordinary prose opening only. A lowercase opening in code, quotation marks, or ambiguous source mapping is report-only. Abbreviations, URLs, decimals, and later sentence boundaries are not analyzed.
|
|
12
|
+
|
|
13
|
+
## Configuration
|
|
14
|
+
|
|
15
|
+
```yaml
|
|
16
|
+
Prose/SentenceCapitalization:
|
|
17
|
+
Enabled: false
|
|
18
|
+
SafeAutoCorrect: false
|
|
19
|
+
Style: initial_capital
|
|
20
|
+
PreserveMixedCase: true
|
|
21
|
+
IgnoredWords: []
|
|
22
|
+
SupportedStyles:
|
|
23
|
+
- initial_capital
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Examples
|
|
27
|
+
|
|
28
|
+
### Bad
|
|
29
|
+
|
|
30
|
+
```markdown
|
|
31
|
+
lowercase prose starts this paragraph.
|
|
32
|
+
|
|
33
|
+
`fixed` requires exactly one space after every marker.
|
|
34
|
+
|
|
35
|
+
"aligned" adds padding after shorter markers.
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Good
|
|
39
|
+
|
|
40
|
+
```markdown
|
|
41
|
+
Lowercase prose starts this paragraph.
|
|
42
|
+
|
|
43
|
+
The `fixed` style requires exactly one space after every marker.
|
|
44
|
+
|
|
45
|
+
The "aligned" style adds padding after shorter markers.
|
|
46
|
+
```
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Text/HardTabs
|
|
2
|
+
|
|
3
|
+
Reports every hard tab in plain-text files without correction. Markdown-like fences, code, and metadata are literal text and receive no exemptions.
|
|
4
|
+
|
|
5
|
+
Use [Markdown/HardTabs](../markdown/hard_tabs.md) for Markdown code-block and language exceptions.
|
|
6
|
+
|
|
7
|
+
## Configuration
|
|
8
|
+
|
|
9
|
+
```yaml
|
|
10
|
+
Text/HardTabs:
|
|
11
|
+
Enabled: true
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Example
|
|
15
|
+
|
|
16
|
+
Inputs are shown as Ruby strings so tabs and newline characters are visible.
|
|
17
|
+
|
|
18
|
+
### Bad
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
"A\tparagraph.\n"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Good
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
"A paragraph.\n"
|
|
28
|
+
```
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Text/ProperNames
|
|
2
|
+
|
|
3
|
+
Corrects configured names to their canonical capitalization in literal plain text. Matching is case-insensitive, uses whole-word boundaries, and treats punctuation in names literally. Markdown-like code, links, URLs, and fences receive no special treatment.
|
|
4
|
+
|
|
5
|
+
Use [Markdown/ProperNames](../markdown/proper_names.md) for Markdown syntax exclusions and code-content options.
|
|
6
|
+
|
|
7
|
+
## Configuration
|
|
8
|
+
|
|
9
|
+
```yaml
|
|
10
|
+
Text/ProperNames:
|
|
11
|
+
Enabled: false
|
|
12
|
+
Names: [] # Canonical spellings; an empty list disables checks.
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Example
|
|
16
|
+
|
|
17
|
+
For this configuration:
|
|
18
|
+
|
|
19
|
+
```yaml
|
|
20
|
+
Text/ProperNames:
|
|
21
|
+
Enabled: true
|
|
22
|
+
Names:
|
|
23
|
+
- GitHub
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Bad
|
|
27
|
+
|
|
28
|
+
```text
|
|
29
|
+
`github` https://github.com
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Good
|
|
33
|
+
|
|
34
|
+
```text
|
|
35
|
+
`GitHub` https://GitHub.com
|
|
36
|
+
```
|
data/docs/settings/files.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
# Front
|
|
1
|
+
# Front Matter
|
|
2
2
|
|
|
3
3
|
By default, Markdown checks ignore a leading header opened with `---` and closed with `---` or `...`. Unclosed headers and delimiters elsewhere remain ordinary Markdown.
|
|
4
4
|
|
|
5
|
-
Set `IgnoreFrontMatter: false` to inspect the header as Markdown, not validate its YAML. RuboCop directives inside metadata remain ignored, and this setting does not change Ruby snippet extraction.
|
|
5
|
+
Set `IgnoreFrontMatter: false` to inspect the header as Markdown, not validate its YAML. Use the opt-in [Markdown/FrontMatterSyntax](../cops/markdown/front_matter_syntax.md) cop to report YAML syntax errors independently of this setting. RuboCop directives inside metadata remain ignored, and this setting does not change Ruby snippet extraction.
|
|
6
6
|
|
|
7
7
|
## Example
|
|
8
8
|
|
|
@@ -12,7 +12,7 @@ module RuboCop
|
|
|
12
12
|
private
|
|
13
13
|
|
|
14
14
|
def investigate_document
|
|
15
|
-
expected = cop_config.fetch('
|
|
15
|
+
expected = cop_config.fetch('Style', 'consistent')
|
|
16
16
|
code_blocks.each do |node|
|
|
17
17
|
actual = node.options[:fenced] ? 'fenced' : 'indented'
|
|
18
18
|
expected = actual if expected == 'consistent'
|
|
@@ -11,8 +11,8 @@ module RuboCop
|
|
|
11
11
|
def investigate_document
|
|
12
12
|
multiline = RuboCop::Prose::InlineRules.multiline_atoms(document)
|
|
13
13
|
.select { |range| range.source.start_with?('`') }
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
document.inline_lines.each do |text, number, column|
|
|
15
|
+
document.inline_spans(number).each do |span|
|
|
16
16
|
next unless span.kind == :code && span.begin_pos >= column
|
|
17
17
|
|
|
18
18
|
position = document.range(number, span.begin_pos, number, span.end_pos)
|
|
@@ -8,20 +8,70 @@ module RuboCop
|
|
|
8
8
|
class DuplicateHeadings < Base
|
|
9
9
|
include RuboCop::Prose::HeadingRules
|
|
10
10
|
|
|
11
|
+
def validate_config
|
|
12
|
+
scopes = ['siblings', 'document', false]
|
|
13
|
+
value = cop_config['Scope']
|
|
14
|
+
return if scopes.include?(value)
|
|
15
|
+
|
|
16
|
+
if value.is_a?(Hash) && value.key?('Default') && scopes.include?(value['Default'])
|
|
17
|
+
covered = []
|
|
18
|
+
valid = value.except('Default').all? do |selector, scope|
|
|
19
|
+
levels = selector_levels(selector)
|
|
20
|
+
next false unless levels && scopes.include?(scope) && !covered.intersect?(levels)
|
|
21
|
+
|
|
22
|
+
covered.concat(levels)
|
|
23
|
+
true
|
|
24
|
+
end
|
|
25
|
+
return if valid
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
raise ValidationError, "#{cop_name}: Scope must be siblings, document, false, or a mapping with Default " \
|
|
29
|
+
'and nonoverlapping levels (1-6), ranges (4-6), or comparisons (>4), ' \
|
|
30
|
+
'set to siblings, document, or false.'
|
|
31
|
+
end
|
|
32
|
+
|
|
11
33
|
def investigate_document
|
|
12
|
-
|
|
34
|
+
document_seen = Set.new
|
|
35
|
+
sibling_seen = Set.new
|
|
13
36
|
parents = []
|
|
14
37
|
heading_entries.each do |heading|
|
|
15
38
|
parents.pop while parents.any? && parents.last[:level] >= heading[:level]
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
39
|
+
key = [parents.last&.fetch(:line), heading[:text]]
|
|
40
|
+
setting = cop_config['Scope']
|
|
41
|
+
scope = effective_scope(setting, heading[:level])
|
|
42
|
+
duplicate = scope == 'document' ? document_seen.include?(heading[:text]) : sibling_seen.include?(key)
|
|
43
|
+
if scope != false && duplicate
|
|
19
44
|
prose_offense(heading_range(heading), message: 'Use unique heading text within the configured scope.')
|
|
20
45
|
end
|
|
21
|
-
|
|
46
|
+
document_seen.add(heading[:text])
|
|
47
|
+
sibling_seen.add(key)
|
|
22
48
|
parents << heading
|
|
23
49
|
end
|
|
24
50
|
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def effective_scope(setting, level)
|
|
55
|
+
return setting unless setting.is_a?(Hash)
|
|
56
|
+
|
|
57
|
+
selected = setting.find do |selector, _scope|
|
|
58
|
+
selector != 'Default' && selector_levels(selector).include?(level)
|
|
59
|
+
end
|
|
60
|
+
selected ? selected.last : setting.fetch('Default')
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def selector_levels(selector)
|
|
64
|
+
return [selector] if selector.is_a?(Integer) && selector.between?(1, 6)
|
|
65
|
+
return unless selector.is_a?(String)
|
|
66
|
+
|
|
67
|
+
if (match = /\A([1-6])-([1-6])\z/.match(selector))
|
|
68
|
+
first, last = match.captures.map(&:to_i)
|
|
69
|
+
(first..last).to_a if first <= last
|
|
70
|
+
elsif (match = /\A([<>])([1-6])\z/.match(selector))
|
|
71
|
+
levels = (1..6).select { |level| match[1] == '>' ? level > match[2].to_i : level < match[2].to_i }
|
|
72
|
+
levels unless levels.empty?
|
|
73
|
+
end
|
|
74
|
+
end
|
|
25
75
|
end
|
|
26
76
|
end
|
|
27
77
|
end
|
|
@@ -9,7 +9,7 @@ module RuboCop
|
|
|
9
9
|
extend AutoCorrector
|
|
10
10
|
|
|
11
11
|
def investigate_document
|
|
12
|
-
|
|
12
|
+
document.inline_lines.each do |text, number, column|
|
|
13
13
|
masked = RuboCop::Prose::InlineRules.mask(text, document: document, number: number)
|
|
14
14
|
masked.to_enum(:scan, /(?<![\w*_])(\*{1,3}|_{1,3})(?![*_])(.+?)\1(?![\w*_])/).each do
|
|
15
15
|
match = Regexp.last_match
|
|
@@ -8,6 +8,13 @@ module RuboCop
|
|
|
8
8
|
class EmphasisStyle < 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(:em).each do |opening, closing, char|
|
|
@@ -12,7 +12,7 @@ module RuboCop
|
|
|
12
12
|
private
|
|
13
13
|
|
|
14
14
|
def investigate_document
|
|
15
|
-
expected = { 'backtick' => '`', 'tilde' => '~' }[cop_config.fetch('
|
|
15
|
+
expected = { 'backtick' => '`', 'tilde' => '~' }[cop_config.fetch('Style', 'consistent')]
|
|
16
16
|
code_blocks.each do |node|
|
|
17
17
|
details = fence_details(node)
|
|
18
18
|
next unless details
|
|
@@ -24,16 +24,27 @@ module RuboCop
|
|
|
24
24
|
message: 'Start the document with a heading at the configured level.')
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
def validate_config
|
|
28
|
+
title_key = cop_config['FrontMatterTitle']
|
|
29
|
+
return if title_key == false || (title_key.is_a?(String) && !title_key.strip.empty?)
|
|
30
|
+
|
|
31
|
+
raise ValidationError,
|
|
32
|
+
"#{cop_name}: FrontMatterTitle must be a nonempty metadata key or false; " \
|
|
33
|
+
'replace `FrontMatterTitle: true` and `TitleKey` with the metadata key, or use false to disable.'
|
|
34
|
+
end
|
|
35
|
+
|
|
27
36
|
private
|
|
28
37
|
|
|
29
38
|
def front_matter_title?
|
|
30
|
-
|
|
39
|
+
title_key = cop_config['FrontMatterTitle']
|
|
40
|
+
return false if title_key == false
|
|
41
|
+
return false unless document.front_matter
|
|
31
42
|
|
|
32
43
|
lines = document.front_matter.lines
|
|
33
44
|
metadata = YAML.safe_load(lines[1...-1].join)
|
|
34
45
|
return false unless metadata.is_a?(Hash)
|
|
35
46
|
|
|
36
|
-
title = metadata[
|
|
47
|
+
title = metadata[title_key]
|
|
37
48
|
title.is_a?(String) && !title.strip.empty?
|
|
38
49
|
rescue Psych::Exception
|
|
39
50
|
false
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'psych'
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Cop
|
|
7
|
+
module Markdown
|
|
8
|
+
class FrontMatterKeys < Base
|
|
9
|
+
def validate_config
|
|
10
|
+
%w[AllowedKeys RequiredKeys].each do |option|
|
|
11
|
+
keys = cop_config[option]
|
|
12
|
+
next if option == 'AllowedKeys' && keys.nil?
|
|
13
|
+
next if keys.is_a?(Array) && keys.all? { |key| key.is_a?(String) && !key.empty? }
|
|
14
|
+
|
|
15
|
+
raise ValidationError, "#{cop_name}: #{option} must be an array of nonempty key names."
|
|
16
|
+
end
|
|
17
|
+
allowed = cop_config['AllowedKeys']
|
|
18
|
+
return if allowed.nil? || (cop_config['RequiredKeys'] - allowed).empty?
|
|
19
|
+
|
|
20
|
+
raise ValidationError, "#{cop_name}: RequiredKeys must be included in AllowedKeys."
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def investigate_document
|
|
26
|
+
return unless document.front_matter
|
|
27
|
+
|
|
28
|
+
parsed = parse_front_matter
|
|
29
|
+
return if parsed == :invalid
|
|
30
|
+
|
|
31
|
+
root = parsed&.root
|
|
32
|
+
if root && !root.is_a?(Psych::Nodes::Mapping)
|
|
33
|
+
prose_offense(node_range(root), message: 'Front matter must be a mapping of metadata keys.')
|
|
34
|
+
return
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
keys = root ? root.children.each_slice(2).map(&:first) : []
|
|
38
|
+
names = keys.filter_map do |key|
|
|
39
|
+
unless key.is_a?(Psych::Nodes::Scalar)
|
|
40
|
+
prose_offense(node_range(key), message: 'Front matter keys must be scalar names.')
|
|
41
|
+
next
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
allowed = cop_config['AllowedKeys']
|
|
45
|
+
if allowed && !allowed.include?(key.value)
|
|
46
|
+
prose_offense(node_range(key), message: "Front matter key `#{key.value}` is not allowed.")
|
|
47
|
+
end
|
|
48
|
+
key.value
|
|
49
|
+
end
|
|
50
|
+
(cop_config['RequiredKeys'] - names).each do |key|
|
|
51
|
+
prose_offense(document.line_range(1), message: "Missing required front matter key `#{key}`.")
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def parse_front_matter
|
|
56
|
+
Psych.parse(document.front_matter.lines[1...-1].join) || nil
|
|
57
|
+
rescue Psych::SyntaxError
|
|
58
|
+
# Syntax diagnostics belong to Markdown/FrontMatterSyntax.
|
|
59
|
+
:invalid
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def node_range(node)
|
|
63
|
+
first = node.start_line + 2
|
|
64
|
+
last = node.end_line + 2
|
|
65
|
+
document.range(first, node.start_column, last, node.end_column)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'psych'
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Cop
|
|
7
|
+
module Markdown
|
|
8
|
+
class FrontMatterSyntax < Base
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def investigate_document
|
|
12
|
+
return unless document.front_matter
|
|
13
|
+
|
|
14
|
+
Psych.parse(document.front_matter.lines[1...-1].join)
|
|
15
|
+
rescue Psych::SyntaxError => e
|
|
16
|
+
line = (e.line + 1).clamp(1, document.lines.length)
|
|
17
|
+
column = (e.column - 1).clamp(0, document.line(line).length)
|
|
18
|
+
prose_offense(
|
|
19
|
+
document.range(line, column, line, [column + 1, document.line(line).length].min),
|
|
20
|
+
message: "Invalid YAML front matter: #{e.problem}."
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -7,13 +7,7 @@ module RuboCop
|
|
|
7
7
|
private
|
|
8
8
|
|
|
9
9
|
def investigate_document
|
|
10
|
-
|
|
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
|
|
10
|
+
exempt = allowed_code_lines
|
|
17
11
|
document.lines.each_with_index do |text, index|
|
|
18
12
|
number = index + 1
|
|
19
13
|
next if document.front_matter_line?(number) || exempt.include?(number)
|
|
@@ -26,6 +20,16 @@ module RuboCop
|
|
|
26
20
|
end
|
|
27
21
|
end
|
|
28
22
|
end
|
|
23
|
+
|
|
24
|
+
def allowed_code_lines
|
|
25
|
+
allowed = cop_config['AllowedCodeLanguages'].map(&:downcase)
|
|
26
|
+
document.nodes(:codeblock).flat_map do |block|
|
|
27
|
+
next [] unless allowed.include?(block.options[:lang].to_s.split.first.to_s.downcase)
|
|
28
|
+
|
|
29
|
+
first = block.options.fetch(:code_start_line)
|
|
30
|
+
(first...(first + block.value.lines.length)).to_a
|
|
31
|
+
end.to_set
|
|
32
|
+
end
|
|
29
33
|
end
|
|
30
34
|
end
|
|
31
35
|
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../prose/capitalization_rules'
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Cop
|
|
7
|
+
module Markdown
|
|
8
|
+
class HeadingCapitalization < Base
|
|
9
|
+
extend AutoCorrector
|
|
10
|
+
include RuboCop::Prose::CapitalizationRules
|
|
11
|
+
|
|
12
|
+
MSG = 'Capitalize the first prose letter of the heading.'
|
|
13
|
+
TITLE_MSG = 'Use title case: capitalize major words and lowercase internal minor words.'
|
|
14
|
+
|
|
15
|
+
def investigate_document
|
|
16
|
+
document.nodes(:header).each do |node|
|
|
17
|
+
text = prose_text(node)
|
|
18
|
+
title_case = cop_config['Style'] == 'title_case'
|
|
19
|
+
number = node.options.fetch(:location)
|
|
20
|
+
opening = heading_opening(number)
|
|
21
|
+
literal_lowercase = opening&.literal && lowercase_opening?(opening)
|
|
22
|
+
invalid = title_case ? invalid_title?(text) || literal_lowercase : lowercase_opening?(opening)
|
|
23
|
+
invalid ||= !title_case && !opening && invalid_initial?(text)
|
|
24
|
+
next unless invalid
|
|
25
|
+
|
|
26
|
+
range = opening&.literal || identifier_source?(document.line(number)) ? nil : correction_range(text, number)
|
|
27
|
+
replacement = capitalized_text(text, title_case)
|
|
28
|
+
prose_offense(document.line_range(number), message: title_case ? TITLE_MSG : MSG) do |corrector|
|
|
29
|
+
corrector.replace(processed_range(range), replacement) if range
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def heading_opening(number)
|
|
35
|
+
source = document.line(number)
|
|
36
|
+
text = source.sub(/\A(?:[ \t]*>[ \t]?)*[ \t]*(?:[-+*]|\d+[.)])?[ \t]*/, '')
|
|
37
|
+
text = text.sub(/\A\#{1,6}[ \t]+/, '').sub(/[ \t]+\#{1,}[ \t]*\z/, '')
|
|
38
|
+
opening_word(text)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -10,7 +10,7 @@ module RuboCop
|
|
|
10
10
|
|
|
11
11
|
def investigate_document
|
|
12
12
|
headings = heading_entries
|
|
13
|
-
style = cop_config.fetch('
|
|
13
|
+
style = cop_config.fetch('Style', 'consistent')
|
|
14
14
|
style = headings.first&.fetch(:style) if style == 'consistent'
|
|
15
15
|
headings.each do |heading|
|
|
16
16
|
next if heading[:style] == style
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../prose/list_rules'
|
|
4
|
+
require_relative '../../prose/capitalization_rules'
|
|
5
|
+
|
|
6
|
+
module RuboCop
|
|
7
|
+
module Cop
|
|
8
|
+
module Markdown
|
|
9
|
+
class ListItemCapitalization < Base
|
|
10
|
+
extend AutoCorrector
|
|
11
|
+
include RuboCop::Prose::ListRules
|
|
12
|
+
include RuboCop::Prose::CapitalizationRules
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def investigate_document
|
|
17
|
+
list_groups do |_list, marks, _parent|
|
|
18
|
+
expected = nil
|
|
19
|
+
marks.each do |mark|
|
|
20
|
+
if cop_config['Style'] == 'title_case'
|
|
21
|
+
investigate_title(mark)
|
|
22
|
+
next
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
initial = prose_initial(mark)
|
|
26
|
+
next unless initial
|
|
27
|
+
|
|
28
|
+
range, style = initial
|
|
29
|
+
next if initial.last && expected.nil? && cop_config['Style'] == 'consistent'
|
|
30
|
+
|
|
31
|
+
expected ||= cop_config['Style'] == 'initial_capital' ? 'uppercase' : style
|
|
32
|
+
next if style == expected
|
|
33
|
+
|
|
34
|
+
prose_offense(range, message: "Use #{expected} initial capitalization in list items.") do |corrector|
|
|
35
|
+
next if initial.last
|
|
36
|
+
|
|
37
|
+
replacement = expected == 'uppercase' ? range.source.upcase : range.source.downcase
|
|
38
|
+
corrector.replace(processed_range(range), replacement)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def investigate_title(mark)
|
|
45
|
+
paragraph = mark[:item].children.first
|
|
46
|
+
return unless paragraph&.type == :p && paragraph.options[:prose_source]
|
|
47
|
+
return unless paragraph.options[:location] == mark[:line]
|
|
48
|
+
|
|
49
|
+
source = paragraph.options[:prose_source]
|
|
50
|
+
checkbox = source[/\A[ \t]*\[[ xX]\][ \t]+/].to_s
|
|
51
|
+
text = prose_text(paragraph).sub(/\A\[[ xX]\][ \t]+/, '')
|
|
52
|
+
opening = opening_word(source[checkbox.length..])
|
|
53
|
+
return unless invalid_title?(text) || (opening&.literal && lowercase_opening?(opening))
|
|
54
|
+
|
|
55
|
+
range = opening&.literal || identifier_source?(source) ? nil : correction_range(text, mark[:line])
|
|
56
|
+
prose_offense(document.line_range(mark[:line]), message: 'Use title case in list items.') do |corrector|
|
|
57
|
+
corrector.replace(processed_range(range), capitalized_text(text, true)) if range
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def prose_initial(mark)
|
|
62
|
+
paragraph = mark[:item].children.first
|
|
63
|
+
return unless paragraph&.type == :p && paragraph.options[:prose_source]
|
|
64
|
+
return unless paragraph.options[:location] == mark[:line]
|
|
65
|
+
|
|
66
|
+
source = paragraph.options[:prose_source]
|
|
67
|
+
checkbox = source[/\A[ \t]*\[[ xX]\][ \t]+/].to_s
|
|
68
|
+
text = source[checkbox.length..]
|
|
69
|
+
opening = opening_word(text)
|
|
70
|
+
word = opening&.word
|
|
71
|
+
return unless word&.match?(/\A[\p{L}\p{M}]+\z/) ||
|
|
72
|
+
(opening&.code && word&.match?(/\A[\p{L}\p{M}][\p{L}\p{M}\p{N}_]*\z/))
|
|
73
|
+
|
|
74
|
+
return if ignored_word?(opening.word)
|
|
75
|
+
return if cop_config['PreserveMixedCase'] && opening.word[1..].match?(/[\p{Lu}\p{Lt}]/)
|
|
76
|
+
|
|
77
|
+
after = text[opening.end_pos..].sub(/\A(?:\*+|_+|`+|["'“”‘’»›\]]+)+/, '')
|
|
78
|
+
return if after.match?(%r{\A(?:[\p{L}\p{M}_]|[._/:-])})
|
|
79
|
+
|
|
80
|
+
offset = checkbox.length + opening.begin_pos
|
|
81
|
+
return if source[0...offset].include?("\n")
|
|
82
|
+
|
|
83
|
+
column = document.content_start(paragraph, mark[:line]) + offset
|
|
84
|
+
range = document.range(mark[:line], column, mark[:line], column + 1)
|
|
85
|
+
[range, opening.word[0].match?(/\p{Ll}/) ? 'lowercase' : 'uppercase', opening.literal]
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|