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,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../prose/table_rules'
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Cop
|
|
7
|
+
module Markdown
|
|
8
|
+
class TableSpacing < Base
|
|
9
|
+
extend AutoCorrector
|
|
10
|
+
|
|
11
|
+
def investigate_document
|
|
12
|
+
RuboCop::Prose::TableRules.tables(document).each do |table|
|
|
13
|
+
style = cop_config.fetch('Style', 'aligned')
|
|
14
|
+
widths = table.rows.each_with_object({}) do |row, sizes|
|
|
15
|
+
row.cells.each_with_index do |cell, index|
|
|
16
|
+
sizes[index] = [sizes.fetch(index, 0), cell.text.strip.length].max
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
table.rows.each do |row|
|
|
20
|
+
row.cells.each_with_index do |cell, index|
|
|
21
|
+
content = cell.text.strip
|
|
22
|
+
left = row.leading || index.positive?
|
|
23
|
+
right = row.trailing || index < row.cells.length - 1
|
|
24
|
+
if content.empty?
|
|
25
|
+
padding = case style
|
|
26
|
+
when 'tight' then ''
|
|
27
|
+
when 'aligned' then ' ' * (widths[index] + (left ? 1 : 0) + (right ? 1 : 0))
|
|
28
|
+
else ' '
|
|
29
|
+
end
|
|
30
|
+
next if cell.text == padding
|
|
31
|
+
|
|
32
|
+
range = document.range(row.number, cell.begin_pos, row.number, cell.end_pos)
|
|
33
|
+
prose_offense(range, message: "Use #{style} table cell spacing.") do |corrector|
|
|
34
|
+
corrector.replace(processed_range(range), padding)
|
|
35
|
+
end
|
|
36
|
+
next
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
before = cell.text[/\A[ \t]*/]
|
|
40
|
+
after = cell.text[/[ \t]*\z/]
|
|
41
|
+
expected_before = left ? ' ' : ''
|
|
42
|
+
padding = style == 'aligned' ? widths[index] - content.length + 1 : 1
|
|
43
|
+
expected_after = right ? ' ' * padding : ''
|
|
44
|
+
valid = case style
|
|
45
|
+
when 'tight' then before.empty? && after.empty?
|
|
46
|
+
else before == expected_before && after == expected_after
|
|
47
|
+
end
|
|
48
|
+
next if valid
|
|
49
|
+
|
|
50
|
+
replacement = style == 'tight' ? content : "#{expected_before}#{content}#{expected_after}"
|
|
51
|
+
range = document.range(row.number, cell.begin_pos, row.number, cell.end_pos)
|
|
52
|
+
prose_offense(range, message: "Use #{style} table cell spacing.") do |corrector|
|
|
53
|
+
corrector.replace(processed_range(range), replacement)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../prose/list_rules'
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Cop
|
|
7
|
+
module Markdown
|
|
8
|
+
class ThematicBreakBlankLines < Base
|
|
9
|
+
extend AutoCorrector
|
|
10
|
+
include RuboCop::Prose::ListRules
|
|
11
|
+
|
|
12
|
+
THEMATIC_BREAK = /(?:\*[ \t]*){3,}|(?:-[ \t]*){3,}|(?:_[ \t]*){3,}/
|
|
13
|
+
|
|
14
|
+
def validate_config
|
|
15
|
+
%w[LinesAbove LinesBelow].each do |name|
|
|
16
|
+
next unless cop_config.key?(name)
|
|
17
|
+
|
|
18
|
+
value = cop_config[name]
|
|
19
|
+
next if value.is_a?(Integer) && value >= 0
|
|
20
|
+
|
|
21
|
+
raise ValidationError, "Markdown/ThematicBreakBlankLines: #{name} must be a nonnegative integer."
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def investigate_document
|
|
28
|
+
@insertions = {}
|
|
29
|
+
document.nodes(:hr).each do |node|
|
|
30
|
+
number = node.options.fetch(:location)
|
|
31
|
+
next if document.front_matter_line?(number)
|
|
32
|
+
|
|
33
|
+
marker = THEMATIC_BREAK.match(document.line(number))
|
|
34
|
+
next unless marker
|
|
35
|
+
|
|
36
|
+
ancestors = node_path(node)
|
|
37
|
+
prefix = document.line(number)[0...marker.begin(0)]
|
|
38
|
+
in_list = ancestors.any? { |ancestor| ancestor.type == :li }
|
|
39
|
+
prefix = if in_list
|
|
40
|
+
continuation_prefix(prefix)
|
|
41
|
+
else
|
|
42
|
+
prefix.rstrip
|
|
43
|
+
end
|
|
44
|
+
range = document.line_range(number)
|
|
45
|
+
require_blanks(number - 1, -1, prefix, cop_config.fetch('LinesAbove', 1), range)
|
|
46
|
+
require_blanks(number + 1, 1, prefix, cop_config.fetch('LinesBelow', 1), range)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
@insertions.values.group_by { |insertion| insertion[:offense_range] }.each do |offense_range, insertions|
|
|
50
|
+
if insertions.all? { |insertion| insertion[:safe] }
|
|
51
|
+
prose_offense(offense_range, message: 'Surround thematic breaks with blank lines.') do |corrector|
|
|
52
|
+
insertions.each do |insertion|
|
|
53
|
+
range = document.line_range(insertion[:line])
|
|
54
|
+
corrector.insert_before(processed_range(range), "#{insertion[:prefix]}#{newline}" * insertion[:count])
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
else
|
|
58
|
+
prose_offense(offense_range, message: 'Surround thematic breaks with blank lines.')
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def require_blanks(number, direction, prefix, required, offense_range)
|
|
64
|
+
return if required.zero?
|
|
65
|
+
|
|
66
|
+
existing = blank_count(number, direction, prefix)
|
|
67
|
+
missing = required - existing
|
|
68
|
+
return unless missing.positive?
|
|
69
|
+
|
|
70
|
+
insertion_line = direction.negative? ? number - existing + 1 : number
|
|
71
|
+
insertion = @insertions[insertion_line] ||= {
|
|
72
|
+
line: insertion_line, prefix: prefix, count: 0, offense_range: offense_range, safe: true
|
|
73
|
+
}
|
|
74
|
+
insertion[:count] = [insertion[:count], missing].max
|
|
75
|
+
insertion[:safe] = false unless prefix == insertion[:prefix]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def blank_count(number, direction, prefix)
|
|
79
|
+
count = 0
|
|
80
|
+
while number.between?(1, document.lines.length)
|
|
81
|
+
return Float::INFINITY if direction.negative? && document.front_matter_line?(number)
|
|
82
|
+
break unless blank_line?(number, prefix)
|
|
83
|
+
|
|
84
|
+
count += 1
|
|
85
|
+
number += direction
|
|
86
|
+
end
|
|
87
|
+
number.between?(1, document.lines.length) ? count : Float::INFINITY
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def blank_line?(number, prefix)
|
|
91
|
+
text = document.line(number)
|
|
92
|
+
return text.match?(/\A[ \t]*\z/) if prefix.empty?
|
|
93
|
+
|
|
94
|
+
pattern = /\A#{Regexp.escape(prefix)}[ \t]*\z/
|
|
95
|
+
text.match?(pattern)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def node_path(target, node = document.root, path = [])
|
|
99
|
+
current_path = path + [node]
|
|
100
|
+
return current_path if node.equal?(target)
|
|
101
|
+
|
|
102
|
+
node.children.each do |child|
|
|
103
|
+
found = node_path(target, child, current_path)
|
|
104
|
+
return found if found
|
|
105
|
+
end
|
|
106
|
+
[]
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def newline
|
|
110
|
+
document.source.include?("\r\n") ? "\r\n" : "\n"
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Markdown
|
|
6
|
+
class ThematicBreakStyle < Base
|
|
7
|
+
extend AutoCorrector
|
|
8
|
+
|
|
9
|
+
def investigate_document
|
|
10
|
+
style = cop_config.fetch('Style', 'consistent')
|
|
11
|
+
styles = configured_styles(style)
|
|
12
|
+
consistent = style == 'consistent'
|
|
13
|
+
expected = nil
|
|
14
|
+
|
|
15
|
+
document.nodes(:hr).each do |node|
|
|
16
|
+
number = node.options[:location]
|
|
17
|
+
next if document.front_matter_line?(number)
|
|
18
|
+
|
|
19
|
+
text = document.line(number)
|
|
20
|
+
match = /(?:\*[ \t]*){3,}|(?:-[ \t]*){3,}|(?:_[ \t]*){3,}/.match(text)
|
|
21
|
+
next unless match
|
|
22
|
+
|
|
23
|
+
actual = match[0].rstrip
|
|
24
|
+
expected ||= actual if consistent
|
|
25
|
+
next if consistent ? actual == expected : styles.include?(actual)
|
|
26
|
+
|
|
27
|
+
range = document.range(number, match.begin(0), number, match.begin(0) + actual.length)
|
|
28
|
+
replacement = if consistent
|
|
29
|
+
expected
|
|
30
|
+
elsif styles.one?
|
|
31
|
+
styles.first
|
|
32
|
+
end
|
|
33
|
+
message = if consistent || styles.one?
|
|
34
|
+
"Use `#{replacement}` for thematic breaks."
|
|
35
|
+
else
|
|
36
|
+
"Use one of #{styles.map { |value| "`#{value}`" }.join(' or ')} for thematic breaks."
|
|
37
|
+
end
|
|
38
|
+
if replacement
|
|
39
|
+
prose_offense(range, message: message) do |corrector|
|
|
40
|
+
corrector.replace(processed_range(range), replacement)
|
|
41
|
+
end
|
|
42
|
+
else
|
|
43
|
+
prose_offense(range, message: message)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def configured_styles(style)
|
|
51
|
+
return [] if style == 'consistent'
|
|
52
|
+
|
|
53
|
+
styles = style.is_a?(Array) ? style : [style]
|
|
54
|
+
valid = !styles.empty? && styles.all? do |value|
|
|
55
|
+
value.is_a?(String) && thematic_break?(value)
|
|
56
|
+
end
|
|
57
|
+
unless valid
|
|
58
|
+
raise ValidationError, [
|
|
59
|
+
'Markdown/ThematicBreakStyle: Style must be `consistent`,',
|
|
60
|
+
'a literal break, or a nonempty array of valid breaks.'
|
|
61
|
+
].join(' ')
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
styles.uniq
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def thematic_break?(value)
|
|
68
|
+
match = /\A(?:(?:\*[ \t]*){3,}|(?:-[ \t]*){3,}|(?:_[ \t]*){3,})\z/.match(value)
|
|
69
|
+
match && match[0].rstrip == value
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Markdown
|
|
6
|
+
class TrailingWhitespace < Base
|
|
7
|
+
extend AutoCorrector
|
|
8
|
+
|
|
9
|
+
MSG = 'Remove unexpected trailing whitespace; preserve Markdown hard breaks.'
|
|
10
|
+
|
|
11
|
+
def validate_config
|
|
12
|
+
value = cop_config['HardBreakSpaces']
|
|
13
|
+
return if value.is_a?(Integer) && value >= 2
|
|
14
|
+
|
|
15
|
+
raise ValidationError, 'Markdown/TrailingWhitespace: HardBreakSpaces must be an integer of at least two.'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def investigate_document
|
|
21
|
+
paragraphs = document.paragraphs.flat_map { |paragraph| document.line_numbers(paragraph).to_a }.to_set
|
|
22
|
+
hard_break_lines = document.hard_breaks.to_set { |node| node.options[:location] }
|
|
23
|
+
document.lines.each_with_index do |text, index|
|
|
24
|
+
number = index + 1
|
|
25
|
+
next if document.front_matter_line?(number)
|
|
26
|
+
next if document.code_line?(number) && !cop_config['CodeBlocks']
|
|
27
|
+
|
|
28
|
+
match = text.match(/[ \t]+\z/)
|
|
29
|
+
next unless match
|
|
30
|
+
|
|
31
|
+
spaces = text[/ +\z/].to_s.length
|
|
32
|
+
hard_break = !text.strip.empty? && paragraphs.include?(number) && spaces >= 2
|
|
33
|
+
style = hard_break_style
|
|
34
|
+
handled_by_style = hard_break_lines.include?(number) && hard_break &&
|
|
35
|
+
style && (style['Style'] == 'backslash' || text[0...-spaces].end_with?('\\'))
|
|
36
|
+
next if handled_by_style
|
|
37
|
+
|
|
38
|
+
keep = hard_break ? ' ' * cop_config['HardBreakSpaces'] : ''
|
|
39
|
+
next if match[0] == keep
|
|
40
|
+
|
|
41
|
+
range = document.range(number, match.begin(0))
|
|
42
|
+
prose_offense(range) do |corrector|
|
|
43
|
+
corrector.replace(processed_range(range), keep) unless document.code_line?(number)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def hard_break_style
|
|
49
|
+
style = config.for_cop('Markdown/HardBreakStyle')
|
|
50
|
+
style if style && style['Enabled']
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Markdown
|
|
6
|
+
class UnclosedFencedCodeBlock < Base
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def investigate_document
|
|
10
|
+
opening = nil
|
|
11
|
+
document.lines.each_with_index do |line, index|
|
|
12
|
+
number = index + 1
|
|
13
|
+
next if document.front_matter_line?(number)
|
|
14
|
+
|
|
15
|
+
match = fence_line(line)
|
|
16
|
+
next unless match
|
|
17
|
+
|
|
18
|
+
if opening
|
|
19
|
+
opening = nil if closing_fence?(match, opening)
|
|
20
|
+
elsif opening_fence?(match)
|
|
21
|
+
opening = match.merge(line: number)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
report_unclosed(opening) if opening
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def fence_line(line)
|
|
28
|
+
index = 0
|
|
29
|
+
quote_depth = 0
|
|
30
|
+
list_indent = 0
|
|
31
|
+
loop do
|
|
32
|
+
start = index
|
|
33
|
+
index += line[index..].to_s[/\A {0,3}/].length
|
|
34
|
+
if line[index] == '>'
|
|
35
|
+
quote_depth += 1
|
|
36
|
+
index += 1
|
|
37
|
+
index += 1 if [' ', "\t"].include?(line[index])
|
|
38
|
+
elsif (list = /\A([-+*]|\d{1,9}[.)])([ \t]+)/.match(line[index..].to_s)) && list[2].length <= 4
|
|
39
|
+
list_indent += index - start + list[1].length + list[2].length
|
|
40
|
+
index += list[0].length
|
|
41
|
+
else
|
|
42
|
+
index = start
|
|
43
|
+
break
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
indent = line[index..].to_s[/\A {0,3}/].length
|
|
48
|
+
index += indent
|
|
49
|
+
fence = /\A(?<fence>`{3,}|~{3,})(?<info>.*)\z/.match(line[index..].to_s)
|
|
50
|
+
return unless fence
|
|
51
|
+
|
|
52
|
+
{ column: index, fence: fence[:fence], info: fence[:info],
|
|
53
|
+
quote_depth: quote_depth, list_indent: list_indent, indent: indent }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def opening_fence?(match)
|
|
57
|
+
match[:fence][0] != '`' || !match[:info].include?('`')
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def closing_fence?(match, opening)
|
|
61
|
+
indent = match[:list_indent] + match[:indent]
|
|
62
|
+
container_matches = if opening[:list_indent].zero?
|
|
63
|
+
match[:list_indent].zero? && indent <= 3
|
|
64
|
+
else
|
|
65
|
+
indent.between?(opening[:list_indent], opening[:list_indent] + 3)
|
|
66
|
+
end
|
|
67
|
+
match[:fence][0] == opening[:fence][0] &&
|
|
68
|
+
match[:fence].length >= opening[:fence].length &&
|
|
69
|
+
match[:info].match?(/\A[ \t]*\z/) &&
|
|
70
|
+
match[:quote_depth] == opening[:quote_depth] &&
|
|
71
|
+
container_matches
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def report_unclosed(opening)
|
|
75
|
+
range = document.range(opening[:line], opening[:column],
|
|
76
|
+
opening[:line], opening[:column] + opening[:fence].length)
|
|
77
|
+
prose_offense(range, message: 'Close this fenced code block explicitly before end of file.')
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../prose/link_rules'
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Cop
|
|
7
|
+
module Markdown
|
|
8
|
+
class UndefinedReference < Base
|
|
9
|
+
MSG = 'Define the link or image reference.'
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def investigate_document
|
|
14
|
+
scanner = document.link_rules
|
|
15
|
+
scanner.links.each do |link|
|
|
16
|
+
next unless link.reference && !scanner.resolved?(link)
|
|
17
|
+
next if link.style == 'shortcut_reference' && !cop_config.fetch('ShortcutSyntax', false)
|
|
18
|
+
|
|
19
|
+
prose_offense(scanner.link_range(link))
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../prose/list_rules'
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Cop
|
|
7
|
+
module Markdown
|
|
8
|
+
class UnorderedListIndentation < Base
|
|
9
|
+
include RuboCop::Prose::ListRules
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def investigate_document
|
|
14
|
+
width = cop_config.fetch('Indent', 2)
|
|
15
|
+
list_groups do |list, marks, parent|
|
|
16
|
+
next unless list.type == :ul
|
|
17
|
+
|
|
18
|
+
expected = expected_indentation(parent, marks.first, width)
|
|
19
|
+
marks.each do |mark|
|
|
20
|
+
next if mark[:indent] == expected
|
|
21
|
+
|
|
22
|
+
prose_offense(document.range(mark[:line], mark[:column], mark[:line], mark[:column] + 1),
|
|
23
|
+
message: "Use #{expected} spaces of unordered list indentation.")
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def expected_indentation(parent, mark, width)
|
|
29
|
+
return 0 unless parent && parent[:quote_depth] == mark[:quote_depth]
|
|
30
|
+
|
|
31
|
+
width = [width, parent[:content_column] - parent[:column]].max if parent[:marker].length > 1
|
|
32
|
+
parent[:indent] + width
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../prose/list_rules'
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Cop
|
|
7
|
+
module Markdown
|
|
8
|
+
class UnorderedListMarker < Base
|
|
9
|
+
extend AutoCorrector
|
|
10
|
+
include RuboCop::Prose::ListRules
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def investigate_document
|
|
15
|
+
style = cop_config.fetch('EnforcedStyle', 'consistent')
|
|
16
|
+
expected = { 'asterisk' => '*', 'plus' => '+', 'dash' => '-' }[style]
|
|
17
|
+
changes = []
|
|
18
|
+
list_groups do |list, marks, _parent|
|
|
19
|
+
next unless list.type == :ul
|
|
20
|
+
|
|
21
|
+
expected ||= marks.first[:marker]
|
|
22
|
+
marks.each do |mark|
|
|
23
|
+
next if mark[:marker] == expected
|
|
24
|
+
|
|
25
|
+
changes << [mark, expected]
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
candidates = changes.group_by { |mark, _target| mark[:line] }.transform_values do |entries|
|
|
29
|
+
text = document.line(entries.first.first[:line]).dup
|
|
30
|
+
entries.each { |mark, target| text[mark[:column]] = target }
|
|
31
|
+
text
|
|
32
|
+
end
|
|
33
|
+
changes.each do |mark, target|
|
|
34
|
+
range = document.range(mark[:line], mark[:column], mark[:line], mark[:column] + 1)
|
|
35
|
+
prose_offense(range, message: "Use `#{target}` for unordered list markers.") do |corrector|
|
|
36
|
+
next if candidates.fetch(mark[:line]).match?(/(?:\A|[> \t])([*-])(?:[ \t]*\1){2,}[ \t]*\z/)
|
|
37
|
+
|
|
38
|
+
corrector.replace(processed_range(range), target)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Prose
|
|
6
|
+
class Base < RuboCop::Cop::Base
|
|
7
|
+
exclude_from_registry
|
|
8
|
+
|
|
9
|
+
def on_new_investigation
|
|
10
|
+
super
|
|
11
|
+
@document = nil
|
|
12
|
+
investigate_document if applicable_document?
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def on_other_file
|
|
16
|
+
on_new_investigation
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def current_corrector
|
|
20
|
+
return super unless processed_source.prose_format
|
|
21
|
+
|
|
22
|
+
@current_corrector ||= Corrector.new(processed_source)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def applicable_document?
|
|
28
|
+
!processed_source.prose_format.nil?
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def document
|
|
32
|
+
@document ||= processed_source.prose_document(configuration: config)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def investigate_document
|
|
36
|
+
document.paragraphs.each { |paragraph| investigate_paragraph(paragraph) }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def range_for_original(range)
|
|
40
|
+
mapping = processed_source.prose_markdown_document
|
|
41
|
+
mapping ? mapping.original_range(range) : range
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def processed_range(range)
|
|
45
|
+
mapping = processed_source.prose_markdown_document
|
|
46
|
+
mapping ? mapping.processed_range(range) : range
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def prose_offense(range, **, &)
|
|
50
|
+
add_offense(processed_range(range), **, &)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Prose
|
|
6
|
+
class MaxLength < Base
|
|
7
|
+
extend AutoCorrector
|
|
8
|
+
|
|
9
|
+
MSG = 'Prose line is too long. [%<length>d/%<max>d]'
|
|
10
|
+
UNWRAP_MSG = 'Keep paragraph text on a single source line.'
|
|
11
|
+
|
|
12
|
+
def validate_config
|
|
13
|
+
value = cop_config['Max']
|
|
14
|
+
return if value == false || (value.is_a?(Integer) && value.positive?)
|
|
15
|
+
|
|
16
|
+
raise ValidationError, 'Prose/MaxLength: Max must be a positive integer or false.'
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def investigate_paragraph(paragraph)
|
|
22
|
+
return unwrap_paragraph(paragraph) if cop_config['Max'] == false
|
|
23
|
+
|
|
24
|
+
document.line_numbers(paragraph).each { |number| inspect_line(paragraph, number) }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def unwrap_paragraph(paragraph)
|
|
28
|
+
document.soft_breaks(paragraph).each do |number|
|
|
29
|
+
previous = document.line(number)
|
|
30
|
+
column = document.content_start(paragraph, number + 1)
|
|
31
|
+
|
|
32
|
+
join = document.range(number, previous.rstrip.length, number + 1, column)
|
|
33
|
+
offense = document.range(number + 1, column)
|
|
34
|
+
prose_offense(offense, message: UNWRAP_MSG) do |corrector|
|
|
35
|
+
corrector.replace(processed_range(join), ' ') if document.reflowable?(paragraph)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def inspect_line(paragraph, number)
|
|
41
|
+
text = document.line(number)
|
|
42
|
+
max = cop_config['Max']
|
|
43
|
+
return if text.length <= max
|
|
44
|
+
|
|
45
|
+
first = document.content_start(paragraph, number)
|
|
46
|
+
return if cop_config['IgnoreURLs'] && text[first..].strip.match?(%r{\A<?https?://\S+>?\z})
|
|
47
|
+
|
|
48
|
+
range = document.range(number, [first, max].max, number, text.length)
|
|
49
|
+
prefix = document.continuation_prefix(paragraph)
|
|
50
|
+
breaks = if document.reflowable?(paragraph)
|
|
51
|
+
wrap_points(text, number, first, prefix, document.atomic_ranges(paragraph))
|
|
52
|
+
else
|
|
53
|
+
[]
|
|
54
|
+
end
|
|
55
|
+
prose_offense(range, message: format(MSG, length: text.length, max: max)) do |corrector|
|
|
56
|
+
breaks.each { |space| corrector.replace(processed_range(space), "\n#{prefix}") }
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def wrap_points(text, number, first, prefix, atomic)
|
|
61
|
+
spaces = breakable_spaces(text, number, first, atomic)
|
|
62
|
+
breaks = []
|
|
63
|
+
start = 0
|
|
64
|
+
width = cop_config['Max']
|
|
65
|
+
while text.length - start > width
|
|
66
|
+
candidates = spaces.select { |space| space.column > start }
|
|
67
|
+
chosen = candidates.reverse.find { |space| space.column - start <= width }
|
|
68
|
+
chosen ||= candidates.first
|
|
69
|
+
break unless chosen
|
|
70
|
+
|
|
71
|
+
breaks << chosen
|
|
72
|
+
start = chosen.last_column
|
|
73
|
+
width = cop_config['Max'] - prefix.length
|
|
74
|
+
end
|
|
75
|
+
breaks
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def breakable_spaces(text, number, first, atomic)
|
|
79
|
+
spaces = []
|
|
80
|
+
text.to_enum(:scan, /[ \t]+/).each do
|
|
81
|
+
match = Regexp.last_match
|
|
82
|
+
next if match.begin(0) <= first || match.end(0) == text.length
|
|
83
|
+
next unless document.breakable_space?(text, match.begin(0), match.end(0))
|
|
84
|
+
|
|
85
|
+
range = document.range(number, match.begin(0), number, match.end(0))
|
|
86
|
+
spaces << range unless atomic.any? { |protected| protected.overlaps?(range) }
|
|
87
|
+
end
|
|
88
|
+
spaces
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|