rubocop-prose 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/AGENTS.md +13 -0
- data/CHANGELOG.md +82 -0
- data/CONTRIBUTING.md +77 -0
- data/DESIGN.md +51 -0
- data/LICENSE.txt +21 -0
- data/README.md +152 -0
- data/RELEASING.md +32 -0
- data/assets/README.md +3 -0
- data/assets/RUBOCOP-LICENSE +20 -0
- data/assets/logo.gif +0 -0
- data/assets/logo.png +0 -0
- data/assets/logo.svg +80 -0
- data/assets/rubocop-logo.svg +1 -0
- data/config/cops/markdown/atx_heading_space.yml +3 -0
- data/config/cops/markdown/atx_heading_spacing.yml +3 -0
- data/config/cops/markdown/bare_url.yml +7 -0
- data/config/cops/markdown/blockquote_blank_lines.yml +3 -0
- data/config/cops/markdown/blockquote_spacing.yml +3 -0
- data/config/cops/markdown/closed_heading_space.yml +3 -0
- data/config/cops/markdown/closed_heading_spacing.yml +3 -0
- data/config/cops/markdown/code_block_style.yml +8 -0
- data/config/cops/markdown/code_span_spacing.yml +3 -0
- data/config/cops/markdown/compact_lists.yml +3 -0
- data/config/cops/markdown/descriptive_link_text.yml +8 -0
- data/config/cops/markdown/duplicate_headings.yml +4 -0
- data/config/cops/markdown/emphasis_heading.yml +3 -0
- data/config/cops/markdown/emphasis_spacing.yml +3 -0
- data/config/cops/markdown/emphasis_style.yml +4 -0
- data/config/cops/markdown/empty_link.yml +3 -0
- data/config/cops/markdown/fence_blank_lines.yml +6 -0
- data/config/cops/markdown/fence_language.yml +4 -0
- data/config/cops/markdown/fence_style.yml +8 -0
- data/config/cops/markdown/first_heading.yml +6 -0
- data/config/cops/markdown/front_matter.yml +2 -0
- data/config/cops/markdown/hard_break_style.yml +4 -0
- data/config/cops/markdown/hard_tabs.yml +5 -0
- data/config/cops/markdown/heading_blank_lines.yml +5 -0
- data/config/cops/markdown/heading_increment.yml +3 -0
- data/config/cops/markdown/heading_indentation.yml +3 -0
- data/config/cops/markdown/heading_punctuation.yml +4 -0
- data/config/cops/markdown/heading_structure.yml +4 -0
- data/config/cops/markdown/heading_style.yml +9 -0
- data/config/cops/markdown/image_alt_text.yml +3 -0
- data/config/cops/markdown/image_path_separators.yml +3 -0
- data/config/cops/markdown/link_fragment.yml +6 -0
- data/config/cops/markdown/link_style.yml +10 -0
- data/config/cops/markdown/link_text_spacing.yml +3 -0
- data/config/cops/markdown/list_blank_lines.yml +3 -0
- data/config/cops/markdown/list_continuation_indentation.yml +3 -0
- data/config/cops/markdown/list_indentation.yml +3 -0
- data/config/cops/markdown/list_marker_spacing.yml +7 -0
- data/config/cops/markdown/ordered_list_numbering.yml +7 -0
- data/config/cops/markdown/outside_spacing.yml +3 -0
- data/config/cops/markdown/proper_names.yml +5 -0
- data/config/cops/markdown/raw_html.yml +4 -0
- data/config/cops/markdown/reference_definitions.yml +8 -0
- data/config/cops/markdown/reversed_link.yml +3 -0
- data/config/cops/markdown/shell_prompts.yml +13 -0
- data/config/cops/markdown/single_top_level_heading.yml +4 -0
- data/config/cops/markdown/strong_style.yml +4 -0
- data/config/cops/markdown/structural_line_length.yml +9 -0
- data/config/cops/markdown/syntax.yml +3 -0
- data/config/cops/markdown/table_blank_lines.yml +3 -0
- data/config/cops/markdown/table_columns.yml +3 -0
- data/config/cops/markdown/table_pipes.yml +4 -0
- data/config/cops/markdown/table_spacing.yml +4 -0
- data/config/cops/markdown/thematic_break_blank_lines.yml +5 -0
- data/config/cops/markdown/thematic_break_style.yml +4 -0
- data/config/cops/markdown/trailing_whitespace.yml +5 -0
- data/config/cops/markdown/unclosed_fenced_code_block.yml +3 -0
- data/config/cops/markdown/undefined_reference.yml +4 -0
- data/config/cops/markdown/unordered_list_indentation.yml +4 -0
- data/config/cops/markdown/unordered_list_marker.yml +9 -0
- data/config/cops/prose/max_length.yml +5 -0
- data/config/cops/prose/multiple_blank_lines.yml +4 -0
- data/config/cops/prose/trailing_newline.yml +3 -0
- data/config/cops/text/trailing_whitespace.yml +3 -0
- data/config/default.yml +16 -0
- data/docs/cops/markdown/atx_heading_space.md +26 -0
- data/docs/cops/markdown/atx_heading_spacing.md +24 -0
- data/docs/cops/markdown/bare_url.md +30 -0
- data/docs/cops/markdown/blockquote_blank_lines.md +30 -0
- data/docs/cops/markdown/blockquote_spacing.md +30 -0
- data/docs/cops/markdown/closed_heading_space.md +26 -0
- data/docs/cops/markdown/closed_heading_spacing.md +24 -0
- data/docs/cops/markdown/code_block_style.md +37 -0
- data/docs/cops/markdown/code_span_spacing.md +26 -0
- data/docs/cops/markdown/compact_lists.md +29 -0
- data/docs/cops/markdown/descriptive_link_text.md +31 -0
- data/docs/cops/markdown/duplicate_headings.md +35 -0
- data/docs/cops/markdown/emphasis_heading.md +26 -0
- data/docs/cops/markdown/emphasis_spacing.md +24 -0
- data/docs/cops/markdown/emphasis_style.md +34 -0
- data/docs/cops/markdown/empty_link.md +26 -0
- data/docs/cops/markdown/fence_blank_lines.md +43 -0
- data/docs/cops/markdown/fence_language.md +31 -0
- data/docs/cops/markdown/fence_style.md +38 -0
- data/docs/cops/markdown/first_heading.md +31 -0
- data/docs/cops/markdown/hard_break_style.md +50 -0
- data/docs/cops/markdown/hard_tabs.md +30 -0
- data/docs/cops/markdown/heading_blank_lines.md +36 -0
- data/docs/cops/markdown/heading_increment.md +30 -0
- data/docs/cops/markdown/heading_indentation.md +26 -0
- data/docs/cops/markdown/heading_punctuation.md +27 -0
- data/docs/cops/markdown/heading_structure.md +41 -0
- data/docs/cops/markdown/heading_style.md +36 -0
- data/docs/cops/markdown/image_alt_text.md +26 -0
- data/docs/cops/markdown/image_path_separators.md +32 -0
- data/docs/cops/markdown/link_fragment.md +35 -0
- data/docs/cops/markdown/link_style.md +49 -0
- data/docs/cops/markdown/link_text_spacing.md +26 -0
- data/docs/cops/markdown/list_blank_lines.md +31 -0
- data/docs/cops/markdown/list_continuation_indentation.md +28 -0
- data/docs/cops/markdown/list_indentation.md +28 -0
- data/docs/cops/markdown/list_marker_spacing.md +32 -0
- data/docs/cops/markdown/ordered_list_numbering.md +29 -0
- data/docs/cops/markdown/outside_spacing.md +26 -0
- data/docs/cops/markdown/proper_names.md +39 -0
- data/docs/cops/markdown/raw_html.md +27 -0
- data/docs/cops/markdown/reference_definitions.md +36 -0
- data/docs/cops/markdown/reversed_link.md +26 -0
- data/docs/cops/markdown/shell_prompts.md +57 -0
- data/docs/cops/markdown/single_top_level_heading.md +31 -0
- data/docs/cops/markdown/strong_style.md +34 -0
- data/docs/cops/markdown/structural_line_length.md +40 -0
- data/docs/cops/markdown/syntax.md +28 -0
- data/docs/cops/markdown/table_blank_lines.md +34 -0
- data/docs/cops/markdown/table_columns.md +30 -0
- data/docs/cops/markdown/table_pipes.md +38 -0
- data/docs/cops/markdown/table_spacing.md +38 -0
- data/docs/cops/markdown/thematic_break_blank_lines.md +31 -0
- data/docs/cops/markdown/thematic_break_style.md +48 -0
- data/docs/cops/markdown/trailing_whitespace.md +30 -0
- data/docs/cops/markdown/unclosed_fenced_code_block.md +29 -0
- data/docs/cops/markdown/undefined_reference.md +29 -0
- data/docs/cops/markdown/unordered_list_indentation.md +45 -0
- data/docs/cops/markdown/unordered_list_marker.md +36 -0
- data/docs/cops/prose/max_length.md +33 -0
- data/docs/cops/prose/multiple_blank_lines.md +50 -0
- data/docs/cops/prose/trailing_newline.md +26 -0
- data/docs/cops/text/trailing_whitespace.md +26 -0
- data/docs/settings/files.md +33 -0
- data/docs/settings/front_matter.md +12 -0
- data/lib/rubocop/cop/markdown/atx_heading_space.rb +22 -0
- data/lib/rubocop/cop/markdown/atx_heading_spacing.rb +27 -0
- data/lib/rubocop/cop/markdown/bare_url.rb +22 -0
- data/lib/rubocop/cop/markdown/base.rb +17 -0
- data/lib/rubocop/cop/markdown/blockquote_blank_lines.rb +49 -0
- data/lib/rubocop/cop/markdown/blockquote_spacing.rb +76 -0
- data/lib/rubocop/cop/markdown/closed_heading_space.rb +26 -0
- data/lib/rubocop/cop/markdown/closed_heading_spacing.rb +27 -0
- data/lib/rubocop/cop/markdown/code_block_style.rb +80 -0
- data/lib/rubocop/cop/markdown/code_span_spacing.rb +51 -0
- data/lib/rubocop/cop/markdown/compact_lists.rb +43 -0
- data/lib/rubocop/cop/markdown/descriptive_link_text.rb +27 -0
- data/lib/rubocop/cop/markdown/duplicate_headings.rb +28 -0
- data/lib/rubocop/cop/markdown/emphasis_heading.rb +18 -0
- data/lib/rubocop/cop/markdown/emphasis_spacing.rb +30 -0
- data/lib/rubocop/cop/markdown/emphasis_style.rb +34 -0
- data/lib/rubocop/cop/markdown/empty_link.rb +25 -0
- data/lib/rubocop/cop/markdown/fence_blank_lines.rb +72 -0
- data/lib/rubocop/cop/markdown/fence_language.rb +32 -0
- data/lib/rubocop/cop/markdown/fence_style.rb +39 -0
- data/lib/rubocop/cop/markdown/first_heading.rb +44 -0
- data/lib/rubocop/cop/markdown/hard_break_style.rb +65 -0
- data/lib/rubocop/cop/markdown/hard_tabs.rb +32 -0
- data/lib/rubocop/cop/markdown/heading_blank_lines.rb +37 -0
- data/lib/rubocop/cop/markdown/heading_increment.rb +23 -0
- data/lib/rubocop/cop/markdown/heading_indentation.rb +37 -0
- data/lib/rubocop/cop/markdown/heading_punctuation.rb +23 -0
- data/lib/rubocop/cop/markdown/heading_structure.rb +33 -0
- data/lib/rubocop/cop/markdown/heading_style.rb +24 -0
- data/lib/rubocop/cop/markdown/image_alt_text.rb +24 -0
- data/lib/rubocop/cop/markdown/image_path_separators.rb +32 -0
- data/lib/rubocop/cop/markdown/link_fragment.rb +36 -0
- data/lib/rubocop/cop/markdown/link_style.rb +29 -0
- data/lib/rubocop/cop/markdown/link_text_spacing.rb +34 -0
- data/lib/rubocop/cop/markdown/list_blank_lines.rb +38 -0
- data/lib/rubocop/cop/markdown/list_continuation_indentation.rb +59 -0
- data/lib/rubocop/cop/markdown/list_indentation.rb +28 -0
- data/lib/rubocop/cop/markdown/list_marker_spacing.rb +44 -0
- data/lib/rubocop/cop/markdown/ordered_list_numbering.rb +35 -0
- data/lib/rubocop/cop/markdown/outside_spacing.rb +88 -0
- data/lib/rubocop/cop/markdown/proper_names.rb +51 -0
- data/lib/rubocop/cop/markdown/raw_html.rb +60 -0
- data/lib/rubocop/cop/markdown/reference_definitions.rb +37 -0
- data/lib/rubocop/cop/markdown/reversed_link.rb +20 -0
- data/lib/rubocop/cop/markdown/shell_prompts.rb +72 -0
- data/lib/rubocop/cop/markdown/single_top_level_heading.rb +19 -0
- data/lib/rubocop/cop/markdown/strong_style.rb +33 -0
- data/lib/rubocop/cop/markdown/structural_line_length.rb +51 -0
- data/lib/rubocop/cop/markdown/syntax.rb +33 -0
- data/lib/rubocop/cop/markdown/table_blank_lines.rb +46 -0
- data/lib/rubocop/cop/markdown/table_columns.rb +23 -0
- data/lib/rubocop/cop/markdown/table_pipes.rb +45 -0
- data/lib/rubocop/cop/markdown/table_spacing.rb +62 -0
- data/lib/rubocop/cop/markdown/thematic_break_blank_lines.rb +115 -0
- data/lib/rubocop/cop/markdown/thematic_break_style.rb +74 -0
- data/lib/rubocop/cop/markdown/trailing_whitespace.rb +55 -0
- data/lib/rubocop/cop/markdown/unclosed_fenced_code_block.rb +82 -0
- data/lib/rubocop/cop/markdown/undefined_reference.rb +25 -0
- data/lib/rubocop/cop/markdown/unordered_list_indentation.rb +37 -0
- data/lib/rubocop/cop/markdown/unordered_list_marker.rb +45 -0
- data/lib/rubocop/cop/prose/base.rb +55 -0
- data/lib/rubocop/cop/prose/max_length.rb +93 -0
- data/lib/rubocop/cop/prose/multiple_blank_lines.rb +96 -0
- data/lib/rubocop/cop/prose/trailing_newline.rb +26 -0
- data/lib/rubocop/cop/text/base.rb +17 -0
- data/lib/rubocop/cop/text/trailing_whitespace.rb +25 -0
- data/lib/rubocop/prose/code_rules.rb +79 -0
- data/lib/rubocop/prose/document.rb +62 -0
- data/lib/rubocop/prose/file_types.rb +56 -0
- data/lib/rubocop/prose/heading_rules.rb +54 -0
- data/lib/rubocop/prose/inline_rules.rb +213 -0
- data/lib/rubocop/prose/link_rules.rb +630 -0
- data/lib/rubocop/prose/list_rules.rb +65 -0
- data/lib/rubocop/prose/markdown_document.rb +167 -0
- data/lib/rubocop/prose/parser.rb +80 -0
- data/lib/rubocop/prose/plugin.rb +30 -0
- data/lib/rubocop/prose/runner.rb +63 -0
- data/lib/rubocop/prose/source.rb +82 -0
- data/lib/rubocop/prose/structural_blank_lines.rb +90 -0
- data/lib/rubocop/prose/table_rules.rb +71 -0
- data/lib/rubocop/prose/text_document.rb +54 -0
- data/lib/rubocop/prose/version.rb +7 -0
- data/lib/rubocop-prose.rb +30 -0
- metadata +335 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../prose/structural_blank_lines'
|
|
4
|
+
require_relative '../../prose/list_rules'
|
|
5
|
+
|
|
6
|
+
module RuboCop
|
|
7
|
+
module Cop
|
|
8
|
+
module Prose
|
|
9
|
+
class MultipleBlankLines < Base
|
|
10
|
+
extend AutoCorrector
|
|
11
|
+
include RuboCop::Prose::ListRules
|
|
12
|
+
|
|
13
|
+
def validate_config
|
|
14
|
+
value = cop_config['Max']
|
|
15
|
+
return if value.is_a?(Integer) && value >= 0
|
|
16
|
+
|
|
17
|
+
raise ValidationError, 'Prose/MultipleBlankLines: Max must be a non-negative integer.'
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def investigate_document
|
|
23
|
+
consecutive = 0
|
|
24
|
+
sequence = nil
|
|
25
|
+
quote_blank_lines = markdown_quote_blank_lines
|
|
26
|
+
source_lines = document.source.lines
|
|
27
|
+
protected_lines = structural_blank_lines
|
|
28
|
+
document.lines.each_with_index do |text, index|
|
|
29
|
+
number = index + 1
|
|
30
|
+
quote = quote_blank_lines[number]
|
|
31
|
+
blank = quote || (text.strip.empty? && :root)
|
|
32
|
+
if document.ignored_line?(number) || !blank
|
|
33
|
+
consecutive = 0
|
|
34
|
+
sequence = nil
|
|
35
|
+
next
|
|
36
|
+
end
|
|
37
|
+
if sequence != blank
|
|
38
|
+
consecutive = 0
|
|
39
|
+
sequence = blank
|
|
40
|
+
end
|
|
41
|
+
consecutive += 1
|
|
42
|
+
next if consecutive <= cop_config['Max'] || protected_lines.include?(number)
|
|
43
|
+
|
|
44
|
+
first = document.buffer.line_range(number).begin_pos
|
|
45
|
+
removal = ::Parser::Source::Range.new(document.buffer, first, first + source_lines.fetch(index).length)
|
|
46
|
+
prose_offense(document.line_range(number), message: 'Too many consecutive blank lines.') do |corrector|
|
|
47
|
+
corrector.remove(processed_range(removal)) if cop_config['Max'].positive?
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def structural_blank_lines
|
|
53
|
+
return Set.new unless processed_source.prose_format == :markdown
|
|
54
|
+
|
|
55
|
+
RuboCop::Prose::StructuralBlankLines.new(document, config, processed_source).protected_lines
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def markdown_quote_blank_lines
|
|
59
|
+
return {} unless document.is_a?(RuboCop::Prose::MarkdownDocument)
|
|
60
|
+
|
|
61
|
+
quote_blank_lines_in(document.root, nil, 0, {})
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def quote_blank_lines_in(node, quote, depth, lines)
|
|
65
|
+
if node.type == :blockquote
|
|
66
|
+
quote = node
|
|
67
|
+
depth += 1
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
if quote
|
|
71
|
+
node.children.each_with_index do |child, index|
|
|
72
|
+
next unless child.type == :blank
|
|
73
|
+
|
|
74
|
+
previous = node.children[0...index].reverse.find { |sibling| sibling.type != :blank }
|
|
75
|
+
following = node.children[(index + 1)..].find { |sibling| sibling.type != :blank }
|
|
76
|
+
next unless previous && following && following.options[:location]
|
|
77
|
+
|
|
78
|
+
((node_last_line(previous) + 1)...following.options[:location]).each do |number|
|
|
79
|
+
lines[number] = quote if quoted_blank_line?(number, depth)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
node.children.each { |child| quote_blank_lines_in(child, quote, depth, lines) }
|
|
85
|
+
lines
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def quoted_blank_line?(number, depth)
|
|
89
|
+
text = document.line(number)
|
|
90
|
+
prefix = text[/\A[ \t]*(?:>[ \t]*)+/]
|
|
91
|
+
prefix && prefix.count('>') == depth && text[prefix.length..].strip.empty?
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Prose
|
|
6
|
+
class TrailingNewline < Base
|
|
7
|
+
extend AutoCorrector
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def investigate_document
|
|
12
|
+
source = document.source
|
|
13
|
+
return if source.empty?
|
|
14
|
+
|
|
15
|
+
suffix = source.match(/\n*\z/)
|
|
16
|
+
return if suffix[0] == "\n"
|
|
17
|
+
|
|
18
|
+
range = ::Parser::Source::Range.new(document.buffer, suffix.begin(0), source.length)
|
|
19
|
+
prose_offense(range, message: 'End the document with exactly one newline.') do |corrector|
|
|
20
|
+
corrector.replace(processed_range(range), "\n")
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Text
|
|
6
|
+
class TrailingWhitespace < Base
|
|
7
|
+
extend AutoCorrector
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def investigate_document
|
|
12
|
+
document.lines.each_with_index do |text, index|
|
|
13
|
+
match = text.match(/[ \t]+\z/)
|
|
14
|
+
next unless match
|
|
15
|
+
|
|
16
|
+
range = document.range(index + 1, match.begin(0))
|
|
17
|
+
prose_offense(range, message: 'Remove trailing whitespace.') do |corrector|
|
|
18
|
+
corrector.remove(processed_range(range))
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Prose
|
|
5
|
+
module CodeRules
|
|
6
|
+
def code_blocks
|
|
7
|
+
document.nodes(:codeblock)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def fenced_blocks(list_items: true)
|
|
11
|
+
return enum_for(__method__, list_items: list_items) unless block_given?
|
|
12
|
+
|
|
13
|
+
code_blocks.each do |node|
|
|
14
|
+
next if !list_items && code_ancestors(node).any? { |ancestor| ancestor.type == :li }
|
|
15
|
+
|
|
16
|
+
details = fence_details(node)
|
|
17
|
+
yield details if details
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def code_ancestors(target, node = document.root, parents = [])
|
|
22
|
+
return parents if node.equal?(target)
|
|
23
|
+
|
|
24
|
+
node.children.each do |child|
|
|
25
|
+
found = code_ancestors(target, child, parents + [node])
|
|
26
|
+
return found if found
|
|
27
|
+
end
|
|
28
|
+
nil
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def fence_details(node)
|
|
32
|
+
return unless node.options[:fenced]
|
|
33
|
+
|
|
34
|
+
first = node.options[:location]
|
|
35
|
+
opening = fence_match(document.line(first))
|
|
36
|
+
return unless opening
|
|
37
|
+
|
|
38
|
+
last = node.options.fetch(:code_start_line) + node.value.lines.length
|
|
39
|
+
return if last > document.lines.length
|
|
40
|
+
|
|
41
|
+
closing = fence_match(document.line(last))
|
|
42
|
+
return unless closing && closing[:fence][0] == opening[:fence][0] &&
|
|
43
|
+
closing[:fence].length >= opening[:fence].length && closing[:info].strip.empty?
|
|
44
|
+
|
|
45
|
+
{ first: first, last: last, opening: opening, closing: closing }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def fence_match(text)
|
|
49
|
+
/
|
|
50
|
+
\A(?<prefix>(?:[ \t]*>[ \t]?|[ \t]*[-+*][ \t]+|[ \t]*\d+[.)][ \t]+|[ \t]+)*)
|
|
51
|
+
(?<fence>`{3,}|~{3,})(?<info>.*)\z
|
|
52
|
+
/x.match(text)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def fence_range(number, match)
|
|
56
|
+
document.range(number, match.begin(:fence), number, match.end(:fence))
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def code_blank?(number)
|
|
60
|
+
number < 1 || number > document.lines.length ||
|
|
61
|
+
document.line(number).match?(/\A[ \t]*(?:>[ \t]*)*\z/)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def code_continuation_prefix(prefix)
|
|
65
|
+
prefix.gsub(/[-+*]|\d+[.)]/) { |marker| ' ' * marker.length }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def code_content_range(node, index, length)
|
|
69
|
+
number = node.options.fetch(:code_start_line) + index
|
|
70
|
+
content = node.value.lines[index].chomp
|
|
71
|
+
raw = document.line(number)
|
|
72
|
+
return unless raw.end_with?(content)
|
|
73
|
+
|
|
74
|
+
first = raw.length - content.length
|
|
75
|
+
document.range(number, first, number, first + length)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Prose
|
|
5
|
+
class Document
|
|
6
|
+
attr_reader :buffer, :lines
|
|
7
|
+
|
|
8
|
+
def self.format(path, configuration = nil)
|
|
9
|
+
(configuration ? configuration.prose_file_types : FileTypes.default).format(path)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.file?(path, configuration = nil)
|
|
13
|
+
!format(path, configuration).nil?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.build(path, buffer, configuration: nil, format: self.format(path, configuration))
|
|
17
|
+
case format
|
|
18
|
+
when :markdown then MarkdownDocument.new(buffer, configuration: configuration || {})
|
|
19
|
+
when :text then TextDocument.new(buffer)
|
|
20
|
+
else raise ArgumentError, "Unsupported prose document: #{path.inspect}"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def initialize(buffer)
|
|
25
|
+
@buffer = buffer
|
|
26
|
+
@lines = source.lines(chomp: true)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def source
|
|
30
|
+
buffer.source
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def line(number)
|
|
34
|
+
lines.fetch(number - 1)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def range(first_line, first_column = 0, last_line = first_line, last_column = nil)
|
|
38
|
+
last_column ||= line(last_line).length
|
|
39
|
+
::Parser::Source::Range.new(
|
|
40
|
+
buffer, buffer.line_range(first_line).begin_pos + first_column,
|
|
41
|
+
buffer.line_range(last_line).begin_pos + last_column
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def line_range(number)
|
|
46
|
+
range(number)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def ignored_line?(_number)
|
|
50
|
+
false
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def reflowable?(_paragraph)
|
|
54
|
+
true
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def breakable_space?(_text, _first, _last)
|
|
58
|
+
true
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Prose
|
|
5
|
+
class FileTypes
|
|
6
|
+
attr_reader :patterns
|
|
7
|
+
|
|
8
|
+
def self.default
|
|
9
|
+
@default ||= new(Prose.default_configuration)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(configuration)
|
|
13
|
+
@patterns = %i[markdown text].to_h do |format|
|
|
14
|
+
name = format.to_s.capitalize
|
|
15
|
+
settings = configuration.fetch(name, {})
|
|
16
|
+
raise ValidationError, "#{name} must be a mapping." unless settings.is_a?(Hash)
|
|
17
|
+
|
|
18
|
+
values = settings.fetch('Include') { self.class.default.patterns.fetch(format) }
|
|
19
|
+
[format, validate(name, values)]
|
|
20
|
+
end.freeze
|
|
21
|
+
loaded_path = configuration.loaded_path if configuration.respond_to?(:loaded_path)
|
|
22
|
+
@matchers = patterns.transform_values do |values|
|
|
23
|
+
RuboCop::Config.new({ 'AllCops' => { 'Include' => values } }, loaded_path)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def format(path)
|
|
28
|
+
return unless path
|
|
29
|
+
|
|
30
|
+
matches = @matchers.select { |_, matcher| matcher.file_to_include?(path.to_s) }.keys
|
|
31
|
+
raise ValidationError, "#{path.inspect} matches both Markdown/Include and Text/Include." if matches.length > 1
|
|
32
|
+
|
|
33
|
+
matches.first
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def validate(name, values)
|
|
39
|
+
valid = values.is_a?(Array) && values.all? do |value|
|
|
40
|
+
value.is_a?(Regexp) || (value.is_a?(String) && !value.empty?)
|
|
41
|
+
end
|
|
42
|
+
raise ValidationError, "#{name}/Include must be an array of nonempty file patterns." unless valid
|
|
43
|
+
|
|
44
|
+
values.map { |value| value.dup.freeze }.uniq.freeze
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
module Configuration
|
|
49
|
+
def prose_file_types
|
|
50
|
+
@prose_file_types ||= FileTypes.new(self)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
RuboCop::Config.prepend(RuboCop::Prose::Configuration)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Prose
|
|
5
|
+
module HeadingRules
|
|
6
|
+
# Container markers are retained so source columns refer to the original line.
|
|
7
|
+
ATX = /\A(?<prefix>(?: {0,3}>[ \t]?| {0,3}(?:[-+*]|\d+[.)])[ \t]+)* {0,3})(?<marks>\#{1,6})(?!#)(?<rest>.*)\z/
|
|
8
|
+
|
|
9
|
+
def heading_entries
|
|
10
|
+
document.nodes(:header).map do |node|
|
|
11
|
+
number = node.options.fetch(:location)
|
|
12
|
+
text = document.line(number)
|
|
13
|
+
opening = ATX.match(text)
|
|
14
|
+
closing = opening && closing_heading_match(opening[:rest])
|
|
15
|
+
{
|
|
16
|
+
level: node.options.fetch(:level), text: document.plain_text(node), line: number,
|
|
17
|
+
last_line: opening ? number : number + 1, opening: opening, closing: closing,
|
|
18
|
+
style: if opening
|
|
19
|
+
closing ? 'atx_closed' : 'atx'
|
|
20
|
+
else
|
|
21
|
+
'setext'
|
|
22
|
+
end
|
|
23
|
+
}
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def closing_heading_match(rest)
|
|
28
|
+
match = /(?<space>[ \t]+)(?<marks>#+)[ \t]*\z/.match(rest)
|
|
29
|
+
return match if match
|
|
30
|
+
|
|
31
|
+
/\A(?<space>)(?<marks>#+)[ \t]*\z/.match(rest)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def heading_candidates
|
|
35
|
+
document.prose_lines.filter_map do |text, number|
|
|
36
|
+
match = ATX.match(text)
|
|
37
|
+
[match, number] if match
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def heading_range(entry)
|
|
42
|
+
document.line_range(entry.fetch(:line))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def heading_blank?(number)
|
|
46
|
+
document.line(number).match?(/\A(?: {0,3}>[ \t]?)*[ \t]*\z/)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def heading_end_range
|
|
50
|
+
::Parser::Source::Range.new(document.buffer, document.source.length, document.source.length)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Prose
|
|
5
|
+
# Source positions are character offsets, like Document#range.
|
|
6
|
+
module InlineRules
|
|
7
|
+
Span = Struct.new(:begin_pos, :end_pos, :kind, :delimiter, keyword_init: true)
|
|
8
|
+
|
|
9
|
+
class PositionedParser < Parser
|
|
10
|
+
protected
|
|
11
|
+
|
|
12
|
+
def parse_emphasis
|
|
13
|
+
scanner = @src
|
|
14
|
+
parent = @tree
|
|
15
|
+
count = parent.children.length
|
|
16
|
+
first = scanner.pos
|
|
17
|
+
number = scanner.current_line_number
|
|
18
|
+
super
|
|
19
|
+
node = parent.children.last
|
|
20
|
+
return unless parent.children.length > count && %i[em strong].include?(node.type)
|
|
21
|
+
|
|
22
|
+
node.options[:prose_delimiters] = [scanner.string, first, scanner.pos, number]
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.lines(document)
|
|
27
|
+
found = {}
|
|
28
|
+
document.paragraphs.each do |paragraph|
|
|
29
|
+
document.line_numbers(paragraph).each do |number|
|
|
30
|
+
found[number] = document.content_start(paragraph, number)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
document.nodes(:header).each { |node| found[node.options[:location]] = 0 }
|
|
34
|
+
document.nodes(:table).each do |node|
|
|
35
|
+
number = node.options[:location]
|
|
36
|
+
while number <= document.lines.length && !document.line(number).strip.empty?
|
|
37
|
+
found[number] = 0
|
|
38
|
+
number += 1
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
found.sort.filter_map do |number, column|
|
|
42
|
+
next if document.code_line?(number) || document.front_matter_line?(number)
|
|
43
|
+
|
|
44
|
+
[document.line(number), number, column]
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.escaped?(text, index)
|
|
49
|
+
count = 0
|
|
50
|
+
index -= 1
|
|
51
|
+
while index >= 0 && text[index] == '\\'
|
|
52
|
+
count += 1
|
|
53
|
+
index -= 1
|
|
54
|
+
end
|
|
55
|
+
count.odd?
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def self.spans(text, links: true, html: true)
|
|
59
|
+
result = []
|
|
60
|
+
index = 0
|
|
61
|
+
while index < text.length
|
|
62
|
+
if text[index] == '\\' && text[index + 1]&.match?(/[[:punct:]]/)
|
|
63
|
+
result << Span.new(begin_pos: index, end_pos: [index + 2, text.length].min, kind: :escape)
|
|
64
|
+
index += 2
|
|
65
|
+
next
|
|
66
|
+
end
|
|
67
|
+
if text[index] == '`'
|
|
68
|
+
delimiter = text[index..][/\A`+/]
|
|
69
|
+
finish = index + delimiter.length
|
|
70
|
+
closed = false
|
|
71
|
+
while (match = /`+/.match(text, finish))
|
|
72
|
+
if match[0] == delimiter
|
|
73
|
+
result << Span.new(begin_pos: index, end_pos: match.end(0), kind: :code, delimiter: delimiter)
|
|
74
|
+
index = match.end(0)
|
|
75
|
+
closed = true
|
|
76
|
+
break
|
|
77
|
+
end
|
|
78
|
+
finish = match.end(0)
|
|
79
|
+
end
|
|
80
|
+
next if closed
|
|
81
|
+
|
|
82
|
+
index += delimiter.length
|
|
83
|
+
next
|
|
84
|
+
elsif html && text[index] == '<'
|
|
85
|
+
rest = text[index..]
|
|
86
|
+
html = %r{\A<([A-Za-z][\w:-]*)\b[^>]*>.*?</\1\s*>}i.match(rest) ||
|
|
87
|
+
/\A(?:<!--.*?-->|<[^>]+>)/.match(rest)
|
|
88
|
+
if html
|
|
89
|
+
result << Span.new(begin_pos: index, end_pos: index + html[0].length, kind: :html)
|
|
90
|
+
index += html[0].length
|
|
91
|
+
next
|
|
92
|
+
end
|
|
93
|
+
elsif links && (text[index] == '[' || (text[index] == '!' && text[index + 1] == '['))
|
|
94
|
+
finish = link_end(text, index)
|
|
95
|
+
if finish
|
|
96
|
+
result << Span.new(begin_pos: index, end_pos: finish, kind: :link)
|
|
97
|
+
index = finish
|
|
98
|
+
next
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
index += 1
|
|
102
|
+
end
|
|
103
|
+
result
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def self.link_end(text, index)
|
|
107
|
+
index += 1 if text[index] == '!'
|
|
108
|
+
close = balanced_end(text, index, '[', ']')
|
|
109
|
+
return unless close
|
|
110
|
+
return balanced_end(text, close, '(', ')') if text[close] == '('
|
|
111
|
+
return balanced_end(text, close, '[', ']') if text[close] == '['
|
|
112
|
+
|
|
113
|
+
# A shortcut reference may resolve elsewhere in the document.
|
|
114
|
+
close
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def self.balanced_end(text, index, opening, closing)
|
|
118
|
+
depth = 0
|
|
119
|
+
(index...text.length).each do |position|
|
|
120
|
+
next if escaped?(text, position)
|
|
121
|
+
|
|
122
|
+
depth += 1 if text[position] == opening
|
|
123
|
+
depth -= 1 if text[position] == closing
|
|
124
|
+
return position + 1 if depth.zero?
|
|
125
|
+
end
|
|
126
|
+
nil
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def self.multiline_atoms(document)
|
|
130
|
+
document.multiline_atoms
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def self.mask(text, include_code: false, document: nil, number: nil)
|
|
134
|
+
result = text.dup
|
|
135
|
+
spans(text).each do |span|
|
|
136
|
+
next if include_code && span.kind == :code
|
|
137
|
+
|
|
138
|
+
result[span.begin_pos...span.end_pos] = ' ' * (span.end_pos - span.begin_pos)
|
|
139
|
+
end
|
|
140
|
+
if document
|
|
141
|
+
line = document.line_range(number)
|
|
142
|
+
multiline_atoms(document).each do |range|
|
|
143
|
+
next if include_code && range.source.start_with?('`')
|
|
144
|
+
next unless range.overlaps?(line)
|
|
145
|
+
|
|
146
|
+
first = [range.begin_pos, line.begin_pos].max - line.begin_pos
|
|
147
|
+
last = [range.end_pos, line.end_pos].min - line.begin_pos
|
|
148
|
+
result[first...last] = ' ' * (last - first)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
result
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def self.emphasis(document)
|
|
155
|
+
source = document.source.dup
|
|
156
|
+
document.lines.each_with_index do |_, index|
|
|
157
|
+
next unless document.front_matter_line?(index + 1)
|
|
158
|
+
|
|
159
|
+
range = document.buffer.line_range(index + 1)
|
|
160
|
+
source[range.begin_pos...range.end_pos] = ' ' * range.size
|
|
161
|
+
end
|
|
162
|
+
root = PositionedParser.parse(source, hard_wrap: false, gfm_quirks: %i[paragraph_end no_auto_typographic]).first
|
|
163
|
+
nodes = lambda do |node|
|
|
164
|
+
[node] + (node.type == :table ? [] : node.children.flat_map { |child| nodes.call(child) })
|
|
165
|
+
end
|
|
166
|
+
records = nodes.call(root).map { |node| [node, 0, nil] }
|
|
167
|
+
document.nodes(:table).each do |table|
|
|
168
|
+
number = table.options[:location]
|
|
169
|
+
while number <= document.lines.length && !document.line(number).strip.empty?
|
|
170
|
+
# Cell locations are not document-relative; parsing the row as inline prose
|
|
171
|
+
# recovers offsets without changing the table parser or its AST.
|
|
172
|
+
row = PositionedParser.parse("prose #{document.line(number).tr('|', ';')}",
|
|
173
|
+
hard_wrap: false, gfm_quirks: %i[paragraph_end no_auto_typographic]).first
|
|
174
|
+
records.concat(nodes.call(row).map { |node| [node, number - 1, -6] })
|
|
175
|
+
number += 1
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
ranges = { em: [], strong: [] }
|
|
179
|
+
records.each do |node, line_offset, column_offset|
|
|
180
|
+
next unless ranges.key?(node.type) && node.options[:prose_delimiters]
|
|
181
|
+
|
|
182
|
+
text, first, last, number = node.options[:prose_delimiters]
|
|
183
|
+
next unless number
|
|
184
|
+
|
|
185
|
+
number += line_offset
|
|
186
|
+
before = text.byteslice(0, first)
|
|
187
|
+
through = text.byteslice(0, last)
|
|
188
|
+
local_line = before.count("\n")
|
|
189
|
+
last_number = number + through.count("\n") - local_line
|
|
190
|
+
first_text = text.lines(chomp: true)[local_line]
|
|
191
|
+
last_text = text.lines(chomp: true)[through.count("\n")]
|
|
192
|
+
first_prefix = column_offset || document.line(number).rindex(first_text)
|
|
193
|
+
last_prefix = column_offset || document.line(last_number).rindex(last_text)
|
|
194
|
+
next unless first_prefix && last_prefix
|
|
195
|
+
|
|
196
|
+
column = first_prefix + before.rpartition("\n").last.length
|
|
197
|
+
last_column = last_prefix + through.rpartition("\n").last.length
|
|
198
|
+
next if spans(document.line(number)).any? { |span| span.begin_pos <= column && span.end_pos > column }
|
|
199
|
+
|
|
200
|
+
position = document.range(number, column, number, column + 1)
|
|
201
|
+
next if multiline_atoms(document).any? { |range| range.contains?(position) }
|
|
202
|
+
|
|
203
|
+
width = node.type == :strong ? 2 : 1
|
|
204
|
+
ranges.fetch(node.type) << [
|
|
205
|
+
document.range(number, column, number, column + width),
|
|
206
|
+
document.range(last_number, last_column - width, last_number, last_column), node.options[:char]
|
|
207
|
+
]
|
|
208
|
+
end
|
|
209
|
+
ranges.transform_values { |entries| entries.sort_by { |opening, _, _| opening.begin_pos } }
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|