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,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../prose/list_rules'
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Cop
|
|
7
|
+
module Markdown
|
|
8
|
+
class OrderedListNumbering < Base
|
|
9
|
+
extend AutoCorrector
|
|
10
|
+
include RuboCop::Prose::ListRules
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def investigate_document
|
|
15
|
+
list_groups do |list, marks, _parent|
|
|
16
|
+
next unless list.type == :ol
|
|
17
|
+
|
|
18
|
+
first = marks.first[:marker].to_i
|
|
19
|
+
marks.each_with_index do |mark, index|
|
|
20
|
+
expected = cop_config.fetch('EnforcedStyle', 'sequential') == 'ones' ? 1 : first + index
|
|
21
|
+
digits = mark[:marker][/\A\d+/]
|
|
22
|
+
next if digits == expected.to_s
|
|
23
|
+
|
|
24
|
+
range = document.range(mark[:line], mark[:column], mark[:line], mark[:column] + digits.length)
|
|
25
|
+
prose_offense(range, message: "Use #{expected} for this ordered list item.") do |corrector|
|
|
26
|
+
# Changing marker width can change continuation-block indentation.
|
|
27
|
+
corrector.replace(processed_range(range), expected.to_s) if digits.length == expected.to_s.length
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../prose/inline_rules'
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Cop
|
|
7
|
+
module Markdown
|
|
8
|
+
class OutsideSpacing < Base
|
|
9
|
+
WORD_CHARACTER = /[\p{L}\p{M}\p{N}]/u
|
|
10
|
+
AMBIGUOUS_SCRIPT = /[\p{Han}\p{Hiragana}\p{Katakana}\p{Hangul}\p{Thai}\p{Lao}\p{Khmer}\p{Myanmar}]/u
|
|
11
|
+
|
|
12
|
+
def investigate_document
|
|
13
|
+
candidates = code_spans + emphasis_spans
|
|
14
|
+
candidates.each do |first, last, content_first, content_last|
|
|
15
|
+
next unless adjacent_words?(first, content_first, content_last, last)
|
|
16
|
+
|
|
17
|
+
range = ::Parser::Source::Range.new(document.buffer, first, last)
|
|
18
|
+
prose_offense(range, message: 'Separate inline formatting from adjacent prose words.')
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def code_spans
|
|
25
|
+
multiline = RuboCop::Prose::InlineRules.multiline_atoms(document)
|
|
26
|
+
RuboCop::Prose::InlineRules.lines(document).flat_map do |text, number, column|
|
|
27
|
+
line_start = document.line_range(number).begin_pos
|
|
28
|
+
RuboCop::Prose::InlineRules.spans(text).filter_map do |span|
|
|
29
|
+
next unless span.kind == :code && span.begin_pos >= column
|
|
30
|
+
|
|
31
|
+
first = line_start + span.begin_pos
|
|
32
|
+
last = line_start + span.end_pos
|
|
33
|
+
range = ::Parser::Source::Range.new(document.buffer, first, last)
|
|
34
|
+
next if multiline.any? { |atom| atom.overlaps?(range) }
|
|
35
|
+
next if protected_ranges(number, text, line_start).any? { |atom| atom.overlaps?(range) }
|
|
36
|
+
|
|
37
|
+
[first, last, first + span.delimiter.length, last - span.delimiter.length]
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def emphasis_spans
|
|
43
|
+
%i[em strong].flat_map do |type|
|
|
44
|
+
document.emphasis_ranges(type).filter_map do |opening, closing, _delimiter|
|
|
45
|
+
next unless opening.line == closing.line
|
|
46
|
+
|
|
47
|
+
first = opening.begin_pos
|
|
48
|
+
last = closing.end_pos
|
|
49
|
+
number = opening.line
|
|
50
|
+
line_start = document.line_range(number).begin_pos
|
|
51
|
+
next if protected_ranges(number, document.line(number), line_start)
|
|
52
|
+
.any? { |atom| atom.begin_pos < last && first < atom.end_pos }
|
|
53
|
+
|
|
54
|
+
[first, last, opening.end_pos, closing.begin_pos]
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def protected_ranges(number, text, line_start)
|
|
60
|
+
@protected_ranges ||= {}
|
|
61
|
+
@protected_ranges[number] ||= RuboCop::Prose::InlineRules.spans(text).filter_map do |span|
|
|
62
|
+
next if span.kind == :code
|
|
63
|
+
|
|
64
|
+
::Parser::Source::Range.new(document.buffer, line_start + span.begin_pos, line_start + span.end_pos)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def adjacent_words?(first, content_first, content_last, last)
|
|
69
|
+
source = document.buffer.source
|
|
70
|
+
left = [source[first - 1], source[content_first]]
|
|
71
|
+
right = [source[content_last - 1], source[last]]
|
|
72
|
+
[left, right].any? do |outside, inside|
|
|
73
|
+
word_character?(outside) && word_character?(inside) &&
|
|
74
|
+
!ambiguous_script?(outside) && !ambiguous_script?(inside)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def word_character?(character)
|
|
79
|
+
character&.match?(WORD_CHARACTER)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def ambiguous_script?(character)
|
|
83
|
+
character&.match?(AMBIGUOUS_SCRIPT)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../prose/inline_rules'
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Cop
|
|
7
|
+
module Markdown
|
|
8
|
+
class ProperNames < Base
|
|
9
|
+
extend AutoCorrector
|
|
10
|
+
|
|
11
|
+
def investigate_document
|
|
12
|
+
names = Array(cop_config['Names']).reject(&:empty?)
|
|
13
|
+
return if names.empty?
|
|
14
|
+
|
|
15
|
+
alternatives = names.sort_by { |name| -name.length }.map { |name| Regexp.escape(name) }.join('|')
|
|
16
|
+
pattern = /(?<![\p{L}\p{N}_])(?:#{alternatives})(?![\p{L}\p{N}_])/i
|
|
17
|
+
lines = RuboCop::Prose::InlineRules.lines(document)
|
|
18
|
+
if cop_config['CodeBlocks']
|
|
19
|
+
document.nodes(:codeblock).each do |node|
|
|
20
|
+
first = node.options.fetch(:code_start_line)
|
|
21
|
+
node.value.lines.each_index do |index|
|
|
22
|
+
number = first + index
|
|
23
|
+
lines << [document.line(number), number, 0] unless document.front_matter_line?(number)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
lines.each do |text, number, column|
|
|
28
|
+
masked = if document.code_line?(number)
|
|
29
|
+
text
|
|
30
|
+
else
|
|
31
|
+
RuboCop::Prose::InlineRules.mask(text, include_code: cop_config['CodeBlocks'],
|
|
32
|
+
document: document, number: number)
|
|
33
|
+
end
|
|
34
|
+
masked.to_enum(:scan, pattern).each do
|
|
35
|
+
match = Regexp.last_match
|
|
36
|
+
next if match.begin(0) < column
|
|
37
|
+
|
|
38
|
+
expected = names.find { |name| name.casecmp?(match[0]) }
|
|
39
|
+
next if match[0] == expected
|
|
40
|
+
|
|
41
|
+
range = document.range(number, match.begin(0), number, match.end(0))
|
|
42
|
+
prose_offense(range, message: "Use the capitalization `#{expected}`.") do |corrector|
|
|
43
|
+
corrector.replace(processed_range(range), expected)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../prose/inline_rules'
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Cop
|
|
7
|
+
module Markdown
|
|
8
|
+
class RawHtml < Base
|
|
9
|
+
def investigate_document
|
|
10
|
+
allowed = Array(cop_config['AllowedElements']).map(&:downcase)
|
|
11
|
+
offsets = Hash.new(0)
|
|
12
|
+
located = []
|
|
13
|
+
unlocated = []
|
|
14
|
+
descendants = ->(node) { [node] + node.children.flat_map { |child| descendants.call(child) } }
|
|
15
|
+
table_nodes = document.nodes(:table).flat_map { |table| descendants.call(table) }.to_set
|
|
16
|
+
document.nodes(:html_element).each do |node|
|
|
17
|
+
number = node.options[:location]
|
|
18
|
+
if !number || table_nodes.include?(node)
|
|
19
|
+
unlocated << node.value.to_s.downcase
|
|
20
|
+
next
|
|
21
|
+
end
|
|
22
|
+
next unless number && !document.front_matter_line?(number) && !document.code_line?(number)
|
|
23
|
+
|
|
24
|
+
text = document.line(number)
|
|
25
|
+
key = [number, node.value.to_s.downcase]
|
|
26
|
+
match = /<#{Regexp.escape(node.value.to_s)}\b[^>]*(?:>|\z)/i.match(text, offsets[key])
|
|
27
|
+
unless match
|
|
28
|
+
unlocated << node.value.to_s.downcase
|
|
29
|
+
next
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
offsets[key] = match.end(0)
|
|
33
|
+
located << [number, match.begin(0)]
|
|
34
|
+
next if allowed.include?(node.value.to_s.downcase)
|
|
35
|
+
|
|
36
|
+
prose_offense(document.range(number, match.begin(0), number, match.end(0)),
|
|
37
|
+
message: "Raw HTML element <#{node.value}> is not allowed.")
|
|
38
|
+
end
|
|
39
|
+
# GFM table cell span locations are relative to the cell, not the document.
|
|
40
|
+
RuboCop::Prose::InlineRules.lines(document).each do |text, number, _|
|
|
41
|
+
protected = RuboCop::Prose::InlineRules.spans(text).reject { |span| span.kind == :html }
|
|
42
|
+
text.to_enum(:scan, /<([A-Za-z][\w:-]*)\b[^>]*>/).each do
|
|
43
|
+
match = Regexp.last_match
|
|
44
|
+
name = match[1].downcase
|
|
45
|
+
next unless unlocated.include?(name)
|
|
46
|
+
next if located.include?([number, match.begin(0)])
|
|
47
|
+
next if protected.any? { |span| match.begin(0) >= span.begin_pos && match.begin(0) < span.end_pos }
|
|
48
|
+
|
|
49
|
+
unlocated.delete_at(unlocated.index(name))
|
|
50
|
+
next if allowed.include?(name)
|
|
51
|
+
|
|
52
|
+
prose_offense(document.range(number, match.begin(0), number, match.end(0)),
|
|
53
|
+
message: "Raw HTML element <#{match[1]}> is not allowed.")
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../prose/link_rules'
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Cop
|
|
7
|
+
module Markdown
|
|
8
|
+
class ReferenceDefinitions < Base
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def investigate_document
|
|
12
|
+
scanner = document.link_rules
|
|
13
|
+
used = scanner.links.filter_map(&:reference).to_set
|
|
14
|
+
ignored = cop_config.fetch('IgnoredDefinitions', ['//']).map { |label| scanner.normalize(label) }
|
|
15
|
+
seen = {}
|
|
16
|
+
scanner.definitions.each do |definition|
|
|
17
|
+
first = seen[definition.reference]
|
|
18
|
+
seen[definition.reference] ||= definition
|
|
19
|
+
duplicate = first && !(cop_config.fetch('AllowIdenticalDuplicates', false) &&
|
|
20
|
+
scanner.decode(first.destination) == scanner.decode(definition.destination) &&
|
|
21
|
+
first.title == definition.title)
|
|
22
|
+
next if ignored.include?(definition.reference)
|
|
23
|
+
|
|
24
|
+
message = if duplicate && cop_config.fetch('CheckDuplicates', true)
|
|
25
|
+
'Remove or rename the duplicate reference definition.'
|
|
26
|
+
elsif !used.include?(definition.reference) && cop_config.fetch('CheckUnused', false)
|
|
27
|
+
'Reference definition is not used.'
|
|
28
|
+
end
|
|
29
|
+
next unless message
|
|
30
|
+
|
|
31
|
+
prose_offense(scanner.range(definition.begin_pos, definition.end_pos), message: message)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../prose/link_rules'
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Cop
|
|
7
|
+
module Markdown
|
|
8
|
+
class ReversedLink < Base
|
|
9
|
+
MSG = 'Use [link text](destination), not (link text)[destination].'
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def investigate_document
|
|
14
|
+
scanner = document.link_rules
|
|
15
|
+
scanner.reversed_links.each { |first, last| prose_offense(scanner.range(first, last)) }
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../prose/code_rules'
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Cop
|
|
7
|
+
module Markdown
|
|
8
|
+
class ShellPrompts < Base
|
|
9
|
+
extend AutoCorrector
|
|
10
|
+
include RuboCop::Prose::CodeRules
|
|
11
|
+
|
|
12
|
+
def validate_config
|
|
13
|
+
%w[ShellLanguages Prompts].each do |name|
|
|
14
|
+
value = cop_config[name]
|
|
15
|
+
next if value.is_a?(Array) && (name != 'Prompts' || !value.empty?) &&
|
|
16
|
+
value.all? { |entry| entry.is_a?(String) && entry.match?(/\A\S+\z/) }
|
|
17
|
+
|
|
18
|
+
raise ValidationError, "Markdown/ShellPrompts: #{name} must be an array of nonempty strings " \
|
|
19
|
+
'without whitespace (and Prompts must contain at least one marker).'
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def investigate_document
|
|
26
|
+
languages = cop_config.fetch('ShellLanguages')
|
|
27
|
+
code_blocks.each do |node|
|
|
28
|
+
next unless node.options[:fenced] && languages.include?(node.options[:lang])
|
|
29
|
+
|
|
30
|
+
commands = prompted_commands(node)
|
|
31
|
+
next if commands.nil? || commands.empty?
|
|
32
|
+
|
|
33
|
+
safe = prompt_correction_safe?(node, commands)
|
|
34
|
+
commands.each do |_, index, prompt|
|
|
35
|
+
range = code_content_range(node, index, prompt.length + 1)
|
|
36
|
+
next unless range
|
|
37
|
+
|
|
38
|
+
prose_offense(range, message: 'Omit shell prompts when the example contains no output.') do |corrector|
|
|
39
|
+
corrector.remove(processed_range(range)) if safe
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def prompted_commands(node)
|
|
46
|
+
commands = []
|
|
47
|
+
node.value.lines.each_with_index do |line, index|
|
|
48
|
+
line = line.chomp
|
|
49
|
+
prompt = cop_config.fetch('Prompts').find { |marker| line.start_with?("#{marker} ") }
|
|
50
|
+
if prompt
|
|
51
|
+
commands << [line, index, prompt]
|
|
52
|
+
elsif !line.strip.empty? && !line.lstrip.start_with?('#')
|
|
53
|
+
return nil
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
commands
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def prompt_correction_safe?(node, commands)
|
|
60
|
+
return false if commands.any? { |_, _, prompt| prompt.start_with?('#') }
|
|
61
|
+
|
|
62
|
+
details = fence_details(node)
|
|
63
|
+
return false unless details
|
|
64
|
+
|
|
65
|
+
fence = details[:opening][:fence]
|
|
66
|
+
closing = /\A[ \t]{0,3}#{Regexp.escape(fence[0])}{#{fence.length},}[ \t]*\z/
|
|
67
|
+
commands.none? { |command, _, prompt| command.delete_prefix("#{prompt} ").match?(closing) }
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../prose/heading_rules'
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Cop
|
|
7
|
+
module Markdown
|
|
8
|
+
class SingleTopLevelHeading < Base
|
|
9
|
+
include RuboCop::Prose::HeadingRules
|
|
10
|
+
|
|
11
|
+
def investigate_document
|
|
12
|
+
heading_entries.select { |heading| heading[:level] == cop_config.fetch('Level', 1) }.drop(1).each do |heading|
|
|
13
|
+
prose_offense(heading_range(heading), message: 'Use at most one top-level heading.')
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../prose/inline_rules'
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Cop
|
|
7
|
+
module Markdown
|
|
8
|
+
class StrongStyle < Base
|
|
9
|
+
extend AutoCorrector
|
|
10
|
+
|
|
11
|
+
def investigate_document
|
|
12
|
+
expected = cop_config.fetch('Style', 'consistent')
|
|
13
|
+
document.emphasis_ranges(:strong).each do |opening, closing, char|
|
|
14
|
+
actual = char == '*' ? 'asterisk' : 'underscore'
|
|
15
|
+
expected = actual if expected == 'consistent'
|
|
16
|
+
next if actual == expected
|
|
17
|
+
|
|
18
|
+
replacement = expected == 'asterisk' ? '**' : '__'
|
|
19
|
+
before = opening.begin_pos.positive? ? document.source[opening.begin_pos - 1] : nil
|
|
20
|
+
after = document.source[closing.end_pos]
|
|
21
|
+
|
|
22
|
+
prose_offense(opening, message: "Use #{expected} strong emphasis.") do |corrector|
|
|
23
|
+
next if replacement == '__' && (before&.match?(/[[:alnum:]_]/) || after&.match?(/[[:alnum:]_]/))
|
|
24
|
+
|
|
25
|
+
corrector.replace(processed_range(opening), replacement)
|
|
26
|
+
corrector.replace(processed_range(closing), replacement)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Markdown
|
|
6
|
+
class StructuralLineLength < Base
|
|
7
|
+
MSG = 'Markdown source line is too long. [%<length>d/%<max>d]'
|
|
8
|
+
|
|
9
|
+
def validate_config
|
|
10
|
+
%w[Max HeadingMax CodeBlockMax].each do |name|
|
|
11
|
+
value = cop_config[name]
|
|
12
|
+
next if name != 'Max' && value.nil?
|
|
13
|
+
next if value.is_a?(Integer) && value.positive?
|
|
14
|
+
|
|
15
|
+
raise ValidationError, "Markdown/StructuralLineLength: #{name} must be a positive integer."
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def investigate_document
|
|
22
|
+
table_lines = document.nodes(:table).flat_map do |table|
|
|
23
|
+
first = table.options.fetch(:location)
|
|
24
|
+
rows = table.children.sum { |section| section.children.length }
|
|
25
|
+
(first..(first + rows)).to_a
|
|
26
|
+
end.to_set
|
|
27
|
+
headings = document.headings.to_set(&:last)
|
|
28
|
+
document.lines.each_with_index do |text, index|
|
|
29
|
+
number = index + 1
|
|
30
|
+
next if document.front_matter_line?(number)
|
|
31
|
+
|
|
32
|
+
limit = structural_limit(number, headings, table_lines)
|
|
33
|
+
next unless limit && text.length > limit
|
|
34
|
+
|
|
35
|
+
prose_offense(document.range(number, limit), message: format(MSG, length: text.length, max: limit))
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def structural_limit(number, headings, tables)
|
|
40
|
+
if headings.include?(number) && cop_config['Headings']
|
|
41
|
+
cop_config['HeadingMax'] || cop_config['Max']
|
|
42
|
+
elsif document.code_line?(number) && cop_config['CodeBlocks']
|
|
43
|
+
cop_config['CodeBlockMax'] || cop_config['Max']
|
|
44
|
+
elsif tables.include?(number) && cop_config['Tables']
|
|
45
|
+
cop_config['Max']
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Markdown
|
|
6
|
+
class Syntax < Base
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def investigate_document
|
|
10
|
+
document.warnings.reject { |warning| owned_elsewhere?(warning) }
|
|
11
|
+
.group_by { |warning| warning_line(warning) }.each do |number, warnings|
|
|
12
|
+
prose_offense(document.line_range(number), message: warnings.join('; '))
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def owned_elsewhere?(warning)
|
|
17
|
+
owner = if warning.start_with?('No link definition')
|
|
18
|
+
'Markdown/UndefinedReference'
|
|
19
|
+
elsif warning.start_with?('Duplicate link ID')
|
|
20
|
+
'Markdown/ReferenceDefinitions'
|
|
21
|
+
end
|
|
22
|
+
owner && config.cop_enabled?(owner) &&
|
|
23
|
+
processed_source.registry&.any? { |cop| cop.cop_name == owner }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def warning_line(warning)
|
|
27
|
+
number = warning.scan(/\bline (\d+)\b/).last&.first&.to_i || 1
|
|
28
|
+
number.clamp(1, document.lines.length)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../prose/table_rules'
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Cop
|
|
7
|
+
module Markdown
|
|
8
|
+
class TableBlankLines < Base
|
|
9
|
+
extend AutoCorrector
|
|
10
|
+
|
|
11
|
+
def investigate_document
|
|
12
|
+
RuboCop::Prose::TableRules.tables(document).each do |table|
|
|
13
|
+
first = table.rows.first.number
|
|
14
|
+
last = table.rows.last.number
|
|
15
|
+
unless first == 1 || blank?(document.line(first - 1))
|
|
16
|
+
range = document.range(first, 0, first, 0)
|
|
17
|
+
prose_offense(document.line_range(first), message: 'Add a blank line before the table.') do |corrector|
|
|
18
|
+
corrector.insert_before(processed_range(range), blank_prefix(document.line(first)) + newline)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
next if last == document.lines.length || blank?(document.line(last + 1))
|
|
22
|
+
|
|
23
|
+
range = document.range(last + 1, 0, last + 1, 0)
|
|
24
|
+
prose_offense(document.line_range(last), message: 'Add a blank line after the table.') do |corrector|
|
|
25
|
+
corrector.insert_before(processed_range(range), blank_prefix(document.line(last)) + newline)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def blank?(text)
|
|
33
|
+
text.match?(/\A[ \t]*(?:>[ \t]*)*\z/)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def blank_prefix(text)
|
|
37
|
+
text[/\A[ \t]*(?:>[ \t]*)+/].to_s.rstrip
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def newline
|
|
41
|
+
document.source.include?("\r\n") ? "\r\n" : "\n"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../prose/table_rules'
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Cop
|
|
7
|
+
module Markdown
|
|
8
|
+
class TableColumns < Base
|
|
9
|
+
def investigate_document
|
|
10
|
+
RuboCop::Prose::TableRules.tables(document).each do |table|
|
|
11
|
+
count = table.rows.first.cells.length
|
|
12
|
+
table.rows.drop(1).each do |row|
|
|
13
|
+
next if row.cells.length == count
|
|
14
|
+
|
|
15
|
+
prose_offense(document.range(row.number, row.begin_pos, row.number, row.end_pos),
|
|
16
|
+
message: "Expected #{count} table columns, found #{row.cells.length}.")
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../prose/table_rules'
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Cop
|
|
7
|
+
module Markdown
|
|
8
|
+
class TablePipes < Base
|
|
9
|
+
extend AutoCorrector
|
|
10
|
+
|
|
11
|
+
def investigate_document
|
|
12
|
+
RuboCop::Prose::TableRules.tables(document).each do |table|
|
|
13
|
+
style = cop_config.fetch('Style', 'consistent')
|
|
14
|
+
expected = case style
|
|
15
|
+
when 'leading_and_trailing' then [true, true]
|
|
16
|
+
when 'leading_only' then [true, false]
|
|
17
|
+
when 'trailing_only' then [false, true]
|
|
18
|
+
when 'no_leading_or_trailing' then [false, false]
|
|
19
|
+
else [table.rows.first.leading, table.rows.first.trailing]
|
|
20
|
+
end
|
|
21
|
+
table.rows.each do |row|
|
|
22
|
+
next if [row.leading, row.trailing] == expected
|
|
23
|
+
|
|
24
|
+
range = document.range(row.number, row.begin_pos, row.number, row.end_pos)
|
|
25
|
+
prose_offense(range, message: 'Use consistent leading and trailing table pipes.') do |corrector|
|
|
26
|
+
replacement = row.text[row.begin_pos...row.end_pos].dup
|
|
27
|
+
replacement = replacement[1..] if row.leading && !expected[0]
|
|
28
|
+
replacement = replacement[0...-1] if row.trailing && !expected[1]
|
|
29
|
+
replacement = "|#{replacement}" if !row.leading && expected[0]
|
|
30
|
+
replacement = "#{replacement}|" if !row.trailing && expected[1]
|
|
31
|
+
# Without an outer pipe, an empty boundary cell is indistinguishable
|
|
32
|
+
# from an outer pipe; leaving it alone preserves the column count.
|
|
33
|
+
next if !expected[0] && row.cells.first.text.strip.empty?
|
|
34
|
+
next if !expected[1] && row.cells.last.text.strip.empty?
|
|
35
|
+
next if expected.none? && row.cells.length == 1
|
|
36
|
+
|
|
37
|
+
corrector.replace(processed_range(range), replacement)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|