rubocop-md 2.0.4 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +38 -20
- data/README.md +49 -8
- data/config/default.yml +7 -1
- data/lib/rubocop/cop/markdown/syntax.rb +36 -0
- data/lib/rubocop/markdown/document.rb +111 -0
- data/lib/rubocop/markdown/parser.rb +52 -0
- data/lib/rubocop/markdown/preprocess.rb +13 -46
- data/lib/rubocop/markdown/rubocop_ext.rb +74 -17
- data/lib/rubocop/markdown/version.rb +1 -1
- data/lib/rubocop/markdown.rb +2 -0
- metadata +33 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ac56ce2479d67dd646a069af3e135366bef0eaf66eddf47d607e9d8248d9b738
|
|
4
|
+
data.tar.gz: 727d15a419748305dc44918c85f14db8116d46864ff91ee9c39b10fc2996938b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: df438f27e7a0e7210dc7c80b6169b7bfef9adf43af3433ceff01b324dc0693935b20a5c9c6a76166015b40443cb387924bc423796ae9621b82ef0a460ff169d4
|
|
7
|
+
data.tar.gz: d133ccb642a94601be04fa918b5d6c1d5ff64ffc3d175fa3888386172c897b8c82c0b6a0ea26c872127b13e9b4b1fb022f206df5fb332007e95fe118b9ea8b55
|
data/CHANGELOG.md
CHANGED
|
@@ -1,83 +1,101 @@
|
|
|
1
1
|
# Change log
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 3.0.0 (2026-11-06)
|
|
4
|
+
|
|
5
|
+
- Add opt-in `Markdown/Syntax` to report parser warnings as standard RuboCop offenses instead of writing them separately to stderr ([#50](https://github.com/rubocop/rubocop-md/pull/50), [@dblock][], [@Copilot][]).
|
|
6
|
+
- Exclude leading YAML front matter from Ruby snippet extraction without changing the original source or offense locations ([#57](https://github.com/rubocop/rubocop-md/pull/57), [@dblock][], [@Copilot][]).
|
|
7
|
+
- Preserve the original Markdown buffer for prose plugins and combine original-source edits with Ruby snippet corrections ([#55](https://github.com/rubocop/rubocop-md/pull/55), [@dblock][], [@Copilot][]).
|
|
8
|
+
- Replace standalone regex-based snippet extraction with a reusable Markdown document parser, supporting tilde fences, indented code, and code blocks inside lists and blockquotes while preserving their source prefixes during autocorrection ([#51](https://github.com/rubocop/rubocop-md/pull/51), [@dblock][], [@Copilot][]).
|
|
9
|
+
- Fix false indentation offenses in and after list-nested code blocks ([#28](https://github.com/rubocop/rubocop-md/issues/28), [#51](https://github.com/rubocop/rubocop-md/pull/51), [@dblock][], [@Copilot][]).
|
|
10
|
+
- Fix `rubocop:disable`, `rubocop:todo`, and `rubocop:enable` directives wrapped in spans or HTML comments outside Ruby code blocks ([#52](https://github.com/rubocop/rubocop-md/pull/52), [@dblock][], [@ocvit][], [@Copilot][]).
|
|
4
11
|
|
|
5
12
|
## 2.0.4 (2026-01-28)
|
|
6
13
|
|
|
7
|
-
- Enable caching when no offenses detected.
|
|
14
|
+
- Enable caching when no offenses detected ([@byroot][]).
|
|
8
15
|
|
|
9
16
|
## 2.0.3 (2025-09-30)
|
|
10
17
|
|
|
11
|
-
- Fix compatibility with RuboCop upstream (`get_processed_source` signature)
|
|
18
|
+
- Fix compatibility with RuboCop upstream (`get_processed_source` signature) ([@Rylan12][]).
|
|
12
19
|
|
|
13
20
|
## 2.0.2 (2025-08-20)
|
|
14
21
|
|
|
15
|
-
- Support metadata in code blocks.
|
|
22
|
+
- Support metadata in code blocks ([@balvig][]).
|
|
16
23
|
|
|
17
24
|
## 2.0.1 (2025-04-14)
|
|
18
25
|
|
|
19
|
-
- Remove `Layout/TrailingBlankLines
|
|
26
|
+
- Remove `Layout/TrailingBlankLines` ([@rmm5t][]).
|
|
20
27
|
|
|
21
28
|
## 2.0.0 (2025-02-17)
|
|
22
29
|
|
|
23
|
-
- Migrate to new RuboCop Plugin architecture.
|
|
30
|
+
- Migrate to new RuboCop Plugin architecture ([@koic][]).
|
|
24
31
|
|
|
25
32
|
## 1.2.4 (2024-10-16)
|
|
26
33
|
|
|
27
|
-
- Fix RuboCop warnings.
|
|
34
|
+
- Fix RuboCop warnings ([@koic][]).
|
|
28
35
|
|
|
29
36
|
## 1.2.3 (2024-09-12)
|
|
30
37
|
|
|
31
|
-
- Do no try linting `.mdx` files by default.
|
|
38
|
+
- Do no try linting `.mdx` files by default ([@larouxn][]).
|
|
32
39
|
|
|
33
40
|
## 1.2.2 (2023-12-01) ❄️
|
|
34
41
|
|
|
35
|
-
- Fix analyzing code blocks with backticks.
|
|
42
|
+
- Fix analyzing code blocks with backticks ([@palkan][]).
|
|
36
43
|
|
|
37
44
|
## 1.2.1 (2023-10-20)
|
|
38
45
|
|
|
39
|
-
- Fix incompatibility when loading the plugin from YAML and using other RuboCop options.
|
|
46
|
+
- Fix incompatibility when loading the plugin from YAML and using other RuboCop options ([@prog-supdex][], [@palkan][]).
|
|
40
47
|
|
|
41
48
|
## 1.2.0 (2023-01-31)
|
|
42
49
|
|
|
43
|
-
- Fix parsing compound syntax hints in code snippets.
|
|
50
|
+
- Fix parsing compound syntax hints in code snippets ([@palkan][]).
|
|
44
51
|
|
|
45
|
-
- Drop Ruby 2.5 support.
|
|
52
|
+
- Drop Ruby 2.5 support ([@palkan][]).
|
|
46
53
|
|
|
47
54
|
## 1.1.0 (2022-10-24)
|
|
48
55
|
|
|
49
|
-
- Ignore offenses in non-code source.
|
|
56
|
+
- Ignore offenses in non-code source ([@palkan][]).
|
|
50
57
|
|
|
51
58
|
## 1.0.1 (2020-12-28)
|
|
52
59
|
|
|
53
|
-
- Exclude `EmptyLineBetweenDefs` for MD files.
|
|
60
|
+
- Exclude `EmptyLineBetweenDefs` for MD files ([@palkan][]).
|
|
54
61
|
|
|
55
62
|
## 1.0.0 (2020-12-24)
|
|
56
63
|
|
|
57
|
-
- Drop Ruby 2.4 support and require RuboCop 1.0.
|
|
64
|
+
- Drop Ruby 2.4 support and require RuboCop 1.0 ([@palkan][]).
|
|
58
65
|
|
|
59
66
|
## 0.4.1 (2020-11-05)
|
|
60
67
|
|
|
61
|
-
- Relax required RuboCop version.
|
|
68
|
+
- Relax required RuboCop version ([@sfeuga][]).
|
|
62
69
|
|
|
63
70
|
## 0.4.0 (2020-07-03)
|
|
64
71
|
|
|
65
|
-
- [#10](https://github.com/rubocop-hq/rubocop-md/pull/10): **Drop Ruby 2.3 support** ([@dominicsayers][])
|
|
72
|
+
- [#10](https://github.com/rubocop-hq/rubocop-md/pull/10): **Drop Ruby 2.3 support** ([@dominicsayers][]).
|
|
66
73
|
|
|
67
74
|
## 0.3.2 (2020-03-18)
|
|
68
75
|
|
|
69
|
-
- [#9](https://github.com/rubocop-hq/rubocop-md/pull/9): Add file extensions for Markdown ([@ybiquitous][])
|
|
76
|
+
- [#9](https://github.com/rubocop-hq/rubocop-md/pull/9): Add file extensions for Markdown ([@ybiquitous][]).
|
|
70
77
|
|
|
71
78
|
## 0.3.1 (2019-12-25)
|
|
72
79
|
|
|
73
|
-
- Upgrade to RuboCop 0.78 ([@palkan][])
|
|
80
|
+
- Upgrade to RuboCop 0.78 ([@palkan][]).
|
|
74
81
|
|
|
75
82
|
Change the default config to use the new cop names for (e.g., `Layout/LineLength`).
|
|
76
83
|
|
|
77
84
|
## 0.3.0 (2019-05-14)
|
|
78
85
|
|
|
79
|
-
- **Drop Ruby 2.2 support** ([@palkan][])
|
|
86
|
+
- **Drop Ruby 2.2 support** ([@palkan][]).
|
|
80
87
|
|
|
81
88
|
[@palkan]: https://github.com/palkan
|
|
82
89
|
[@ybiquitous]: https://github.com/ybiquitous
|
|
83
90
|
[@dominicsayers]: https://github.com/dominicsayers
|
|
91
|
+
[@byroot]: https://github.com/byroot
|
|
92
|
+
[@Rylan12]: https://github.com/Rylan12
|
|
93
|
+
[@balvig]: https://github.com/balvig
|
|
94
|
+
[@rmm5t]: https://github.com/rmm5t
|
|
95
|
+
[@koic]: https://github.com/koic
|
|
96
|
+
[@larouxn]: https://github.com/larouxn
|
|
97
|
+
[@prog-supdex]: https://github.com/prog-supdex
|
|
98
|
+
[@sfeuga]: https://github.com/sfeuga
|
|
99
|
+
[@dblock]: https://github.com/dblock
|
|
100
|
+
[@Copilot]: https://github.com/Copilot
|
|
101
|
+
[@ocvit]: https://github.com/ocvit
|
data/README.md
CHANGED
|
@@ -5,12 +5,17 @@
|
|
|
5
5
|
|
|
6
6
|
Run Rubocop against your Markdown files to make sure that code examples follow style guidelines and have valid syntax.
|
|
7
7
|
|
|
8
|
+
<br/>
|
|
9
|
+
|
|
10
|
+
<img src="https://cdn.evilmartians.com/badges/logo-no-label.svg" alt="Evil Martians logo" width="22" height="16" /> <b>RuboCop Markdown</b> is built by <b><a href="https://evilmartians.com/">Evil Martians</a></b>, an American design and engineering consultancy for <b>developer tools, AI, and cybersecurity startups</b>.
|
|
11
|
+
|
|
8
12
|
## Features
|
|
9
13
|
|
|
10
14
|
- Analyzes code blocks within Markdown files
|
|
11
|
-
- Shows correct line numbers in output
|
|
15
|
+
- Shows correct source line numbers and columns in output
|
|
12
16
|
- Preserves specified language (i.e., do not try to analyze "\`\`\`sh")
|
|
13
17
|
- **Supports autocorrect 📝**
|
|
18
|
+
- Parses backtick and tilde fences, indented code, and code blocks inside lists and blockquotes
|
|
14
19
|
|
|
15
20
|
This project was developed to keep [test-prof](https://github.com/test-prof/test-prof) guides consistent with Ruby style guide back in 2017. Since then, many popular Ruby projects adopted it, including:
|
|
16
21
|
|
|
@@ -112,9 +117,34 @@ Lint/Void:
|
|
|
112
117
|
- '*.md'
|
|
113
118
|
```
|
|
114
119
|
|
|
115
|
-
###
|
|
120
|
+
### Code blocks
|
|
121
|
+
|
|
122
|
+
Markdown is parsed with Kramdown's pure-Ruby GFM parser, retaining compatibility with JRuby. Backtick and tilde fences support language tags and additional metadata, including fences longer than three characters. Code blocks inside lists and blockquotes are supported, and autocorrection preserves their Markdown prefixes even when Ruby edits add or remove lines.
|
|
123
|
+
|
|
124
|
+
Indented code blocks are treated as unlabeled snippets: they are checked when `Markdown.Autodetect` is enabled and their contents parse as Ruby. Explicitly non-Ruby language tags are always ignored. `Markdown.WarnInvalid` retains its existing behavior for explicitly Ruby-tagged snippets.
|
|
125
|
+
|
|
126
|
+
Leading YAML front matter, opened with `---` and closed with `---` or `...`, is excluded from snippet extraction so metadata is not mistaken for Ruby code. The original source and offense locations are preserved; unclosed headers and delimiters elsewhere remain ordinary Markdown.
|
|
127
|
+
|
|
128
|
+
The original Markdown remains available through `processed_source.markdown_document.buffer` for prose plugins such as `rubocop-spellcheck`. `processed_source.buffer` retains the preprocessed Ruby source expected by Ruby cops. Prose ranges can be mapped with `markdown_document.processed_range`; diagnostics and corrections from either buffer retain their original Markdown locations and can be combined in the same run.
|
|
129
|
+
|
|
130
|
+
### Markdown syntax diagnostics
|
|
131
|
+
|
|
132
|
+
Enable `Markdown/Syntax` in `.rubocop.yml` to report Kramdown's parser warnings through RuboCop's normal formatters:
|
|
133
|
+
|
|
134
|
+
```yml
|
|
135
|
+
Markdown/Syntax:
|
|
136
|
+
Enabled: true
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
This opt-in cop reports warning-severity offenses for diagnostics such as undefined references and malformed HTML. It does not provide complete Markdown validation or autocorrection. Multiple warnings on the same line are combined into one offense; warnings without a usable location are reported at the start of the file. Normal cop configuration, `--except`, and inline disabling directives apply.
|
|
140
|
+
|
|
141
|
+
Parser warnings are no longer printed separately to stderr. With `Markdown/Syntax` disabled, they are not reported; enabling it can make previously non-blocking parser warnings fail lint. `Markdown.WarnInvalid` continues to control invalid Ruby snippets independently.
|
|
142
|
+
|
|
143
|
+
For Markdown style and prose checks, use [rubocop-prose](https://github.com/dblock/rubocop-prose).
|
|
116
144
|
|
|
117
|
-
|
|
145
|
+
### Disabling cops
|
|
146
|
+
|
|
147
|
+
Use normal `# rubocop:disable`, `# rubocop:todo`, and `# rubocop:enable` directives inside Ruby code blocks. To disable cops across code blocks without displaying the directives in the rendered document, wrap them in hidden spans:
|
|
118
148
|
|
|
119
149
|
````md
|
|
120
150
|
# my_post.md
|
|
@@ -136,20 +166,31 @@ end of snippet
|
|
|
136
166
|
... continuation of article ...
|
|
137
167
|
````
|
|
138
168
|
|
|
169
|
+
HTML comments work too, and you can name individual cops instead of `all`:
|
|
170
|
+
|
|
171
|
+
```md
|
|
172
|
+
<!-- # rubocop:disable Style/EmptyMethod, Style/ArrayJoin -->
|
|
173
|
+
|
|
174
|
+
Your Ruby code blocks here.
|
|
175
|
+
|
|
176
|
+
<!-- # rubocop:enable Style/EmptyMethod, Style/ArrayJoin -->
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Autocorrection preserves the HTML wrappers and does not apply corrections for disabled cops.
|
|
180
|
+
|
|
139
181
|
## How it works?
|
|
140
182
|
|
|
141
|
-
-
|
|
183
|
+
- Parse Markdown once to identify code blocks and retain their source coordinates
|
|
184
|
+
- Preprocess Ruby snippets while preserving line numbers and recording container prefixes
|
|
142
185
|
- Let RuboCop do its job
|
|
143
|
-
-
|
|
186
|
+
- Map Ruby corrections back to the original Markdown, preserving indentation and list/blockquote prefixes
|
|
144
187
|
|
|
145
188
|
## Limitations
|
|
146
189
|
|
|
147
190
|
- RuboCop cache is disabled for Markdown files (because cache knows nothing about preprocessing)
|
|
148
|
-
-
|
|
191
|
+
- Code-block recognition follows Kramdown's GFM parser
|
|
149
192
|
- No language detection included; if you do not specify language for your code blocks, you'd better turn `WarnInvalid` off (see above)
|
|
150
193
|
|
|
151
|
-
\* It should be easy to integrate a _real_ parser (e.g. [Kramdown](https://kramdown.gettalong.org)) and handle all possible syntax. Feel free to open an issue or pull request!
|
|
152
|
-
|
|
153
194
|
## Development
|
|
154
195
|
|
|
155
196
|
After checking out the repo, run `bundle install` to install dependencies. Then, run `rake test` to run the tests.
|
data/config/default.yml
CHANGED
|
@@ -7,6 +7,12 @@ Markdown:
|
|
|
7
7
|
WarnInvalid: true
|
|
8
8
|
Autodetect: true
|
|
9
9
|
|
|
10
|
+
Markdown/Syntax:
|
|
11
|
+
Description: "Report Markdown parser warnings as offenses."
|
|
12
|
+
Enabled: false
|
|
13
|
+
Severity: warning
|
|
14
|
+
VersionAdded: "2.1"
|
|
15
|
+
|
|
10
16
|
# Keep in sync with MARKDOWN_EXTENSIONS
|
|
11
17
|
Layout/CommentIndentation: &markdown_excludes
|
|
12
18
|
Exclude:
|
|
@@ -80,5 +86,5 @@ Style/FrozenStringLiteralComment:
|
|
|
80
86
|
<<: *markdown_excludes
|
|
81
87
|
|
|
82
88
|
Layout/LineLength:
|
|
83
|
-
|
|
89
|
+
AllowedPatterns:
|
|
84
90
|
- !ruby/regexp /^\#/
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Markdown
|
|
6
|
+
# Reports Markdown parser diagnostics through RuboCop's standard formatters.
|
|
7
|
+
class Syntax < RuboCop::Cop::Base
|
|
8
|
+
def on_new_investigation
|
|
9
|
+
super
|
|
10
|
+
document = processed_source.markdown_document
|
|
11
|
+
return unless document
|
|
12
|
+
|
|
13
|
+
warnings_by_line = document.warnings.group_by do |warning|
|
|
14
|
+
warning_line(warning, document)
|
|
15
|
+
end
|
|
16
|
+
warnings_by_line.each do |number, warnings|
|
|
17
|
+
range = document.range(number, 0, number, document.line(number).length)
|
|
18
|
+
add_offense(document.processed_range(range), message: warnings.join("; "))
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def on_other_file
|
|
23
|
+
super
|
|
24
|
+
on_new_investigation
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def warning_line(warning, document)
|
|
30
|
+
number = warning.scan(/\bline (\d+)\b/).last&.first&.to_i
|
|
31
|
+
number&.between?(1, document.buffer.last_line) ? number : 1
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "parser"
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Markdown
|
|
7
|
+
# Original Markdown, its parsed blocks, and coordinates in the Ruby source buffer.
|
|
8
|
+
class Document
|
|
9
|
+
# A deliberate prose edit mapped into the shared Ruby correction buffer.
|
|
10
|
+
class ProseRange < ::Parser::Source::Range; end
|
|
11
|
+
|
|
12
|
+
attr_reader :buffer, :code_prefixes
|
|
13
|
+
attr_accessor :processed_buffer
|
|
14
|
+
|
|
15
|
+
def initialize(source, path)
|
|
16
|
+
@buffer = ::Parser::Source::Buffer.new(path, source: source)
|
|
17
|
+
@code_prefixes = {}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def code_blocks
|
|
21
|
+
return enum_for(__method__) unless block_given?
|
|
22
|
+
|
|
23
|
+
walk(root) { |node| yield node if node.type == :codeblock }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def code_lines(block)
|
|
27
|
+
first = block.options.fetch(:code_start_line)
|
|
28
|
+
block.options.fetch(:code_lines).each_with_index.to_h do |text, index|
|
|
29
|
+
number = first + index
|
|
30
|
+
original = line(number)
|
|
31
|
+
unless original.end_with?(text)
|
|
32
|
+
raise "Cannot map Markdown code block source on line #{number}"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
code_prefixes[number] = original.delete_suffix(text)
|
|
36
|
+
[number, text]
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def corrected_source(corrector)
|
|
41
|
+
rewriter = ::Parser::Source::TreeRewriter.new(buffer)
|
|
42
|
+
corrector.as_replacements.each do |range, replacement|
|
|
43
|
+
text = replacement.dup
|
|
44
|
+
Preprocess.restore!(text)
|
|
45
|
+
prefix = code_prefixes[range.first_line]
|
|
46
|
+
text = text.gsub("\n", "\n#{prefix}") if prefix
|
|
47
|
+
rewriter.replace(original_range(range), text)
|
|
48
|
+
end
|
|
49
|
+
rewriter.process
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def warnings
|
|
53
|
+
parsed.last
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def line(number)
|
|
57
|
+
buffer.source_line(number)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def original_range(processed)
|
|
61
|
+
return processed if processed.source_buffer.equal?(buffer)
|
|
62
|
+
|
|
63
|
+
range(
|
|
64
|
+
processed.first_line, [processed.column - line_offset(processed.first_line), 0].max,
|
|
65
|
+
processed.last_line, [processed.last_column - line_offset(processed.last_line), 0].max
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def processed_range(original)
|
|
70
|
+
return original if original.source_buffer.equal?(processed_buffer)
|
|
71
|
+
|
|
72
|
+
ProseRange.new(
|
|
73
|
+
processed_buffer,
|
|
74
|
+
processed_position(original.first_line, original.column),
|
|
75
|
+
processed_position(original.last_line, original.last_column)
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def range(first_line, first_column, last_line, last_column)
|
|
80
|
+
::Parser::Source::Range.new(
|
|
81
|
+
buffer,
|
|
82
|
+
buffer.line_range(first_line).begin_pos + first_column,
|
|
83
|
+
buffer.line_range(last_line).begin_pos + last_column
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def root
|
|
90
|
+
parsed.first
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def parsed
|
|
94
|
+
@parsed ||= Markdown::Parser.parse(buffer.source, hard_wrap: false)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def walk(node, &block)
|
|
98
|
+
yield node
|
|
99
|
+
node.children.each { |child| walk(child, &block) }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def line_offset(number)
|
|
103
|
+
processed_buffer.source_line(number).length - line(number).length
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def processed_position(number, column)
|
|
107
|
+
processed_buffer.line_range(number).begin_pos + line_offset(number) + column
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "kramdown"
|
|
4
|
+
require "kramdown-parser-gfm"
|
|
5
|
+
|
|
6
|
+
module RuboCop
|
|
7
|
+
module Markdown
|
|
8
|
+
# Retains code-block source coordinates before Kramdown normalizes their content.
|
|
9
|
+
class Parser < Kramdown::Parser::GFM
|
|
10
|
+
# Accept metadata after the language without replacing Kramdown's block parser.
|
|
11
|
+
FENCED_CODEBLOCK_MATCH = /
|
|
12
|
+
^[ ]{0,3}(([~`])\2{2,})[ \t]*((\S+)[^\n]*?)?[ \t]*\n
|
|
13
|
+
(.*?)^[ ]{0,3}\1\2*[ \t]*\n
|
|
14
|
+
/mx.freeze
|
|
15
|
+
|
|
16
|
+
def self.parse(source, options = {})
|
|
17
|
+
source = source.sub(
|
|
18
|
+
/\A(?:\uFEFF)?---[ \t]*\r?\n.*?^(?:---|\.\.\.)[ \t]*(?:\r?\n|\z)/m
|
|
19
|
+
) { |header| header.gsub(/[^\r\n]/, " ") }
|
|
20
|
+
super
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
protected
|
|
24
|
+
|
|
25
|
+
def parse_codeblock_fenced
|
|
26
|
+
first = @src.pos
|
|
27
|
+
result = super
|
|
28
|
+
return result unless result
|
|
29
|
+
|
|
30
|
+
block = @tree.children.last
|
|
31
|
+
indent = @src.string.byteslice(first..)[/\A */].length
|
|
32
|
+
block.options[:code_start_line] = block.options[:location] + 1
|
|
33
|
+
block.options[:code_lines] = block.value.lines(chomp: true).map do |line|
|
|
34
|
+
line.sub(/\A {0,#{indent}}/, "")
|
|
35
|
+
end
|
|
36
|
+
result
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def parse_codeblock
|
|
40
|
+
first = @src.pos
|
|
41
|
+
result = super
|
|
42
|
+
block = @tree.children.last
|
|
43
|
+
raw = @src.string.byteslice(first, @src.pos - first)
|
|
44
|
+
block.options[:code_start_line] = block.options[:location]
|
|
45
|
+
block.options[:code_lines] = raw.lines(chomp: true).map do |line|
|
|
46
|
+
line.sub(Kramdown::Parser::Kramdown::INDENT, "")
|
|
47
|
+
end
|
|
48
|
+
result
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -7,19 +7,6 @@ module RuboCop
|
|
|
7
7
|
# Transform source Markdown file into valid Ruby file
|
|
8
8
|
# by commenting out all non-code lines
|
|
9
9
|
class Preprocess
|
|
10
|
-
# This is a regexp to parse code blocks from .md files.
|
|
11
|
-
#
|
|
12
|
-
# Only recognizes backticks-style code blocks.
|
|
13
|
-
#
|
|
14
|
-
# Try it: https://rubular.com/r/YMqSWiBuh2TKIJ
|
|
15
|
-
MD_REGEXP = /
|
|
16
|
-
^([[:blank:]]*`{3,4}) # Match opening backticks
|
|
17
|
-
([\w[[:blank:]]+]*)?\n # Match the code block syntax
|
|
18
|
-
([\s\S]+?) # Match everything inside the code block
|
|
19
|
-
(^[[:blank:]]*\1[[:blank:]]*\n?) # Match closing backticks
|
|
20
|
-
|(^.*$) # If we are not in a codeblock, match the whole line
|
|
21
|
-
/x.freeze
|
|
22
|
-
|
|
23
10
|
MARKER = "<--rubocop/md-->"
|
|
24
11
|
|
|
25
12
|
# See https://github.com/github/linguist/blob/v5.3.3/lib/linguist/languages.yml#L3925
|
|
@@ -33,18 +20,7 @@ module RuboCop
|
|
|
33
20
|
].freeze
|
|
34
21
|
|
|
35
22
|
class << self
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
# When autocorrect is applied, RuboCop re-writes the file
|
|
39
|
-
# using preproccessed source buffer.
|
|
40
|
-
#
|
|
41
|
-
# We have to restore it.
|
|
42
|
-
def restore_and_save!(file)
|
|
43
|
-
contents = File.read(file)
|
|
44
|
-
restore!(contents)
|
|
45
|
-
File.write(file, contents)
|
|
46
|
-
end
|
|
47
|
-
|
|
23
|
+
# Remove preprocessing markers from correction text.
|
|
48
24
|
def restore!(src)
|
|
49
25
|
src.gsub!(/^#\s*#{MARKER}/m, "")
|
|
50
26
|
end
|
|
@@ -53,32 +29,23 @@ module RuboCop
|
|
|
53
29
|
attr_reader :config
|
|
54
30
|
|
|
55
31
|
def initialize(file)
|
|
32
|
+
@file = file
|
|
56
33
|
@config = Markdown.config_store.for(file)
|
|
57
34
|
end
|
|
58
35
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
syntax
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
markdown = m[5]
|
|
68
|
-
|
|
69
|
-
if markdown
|
|
70
|
-
# We got markdown outside of a codeblock
|
|
71
|
-
comment_lines(markdown)
|
|
72
|
-
elsif ruby_codeblock?(syntax, code)
|
|
73
|
-
# The codeblock we parsed is assumed ruby, keep as is and append markers to backticks
|
|
74
|
-
"#{comment_lines(open_backticks + syntax)}\n#{code}#{comment_lines(close_backticks)}"
|
|
75
|
-
else
|
|
76
|
-
# The codeblock is not relevant, comment it out
|
|
77
|
-
comment_lines(full_match)
|
|
78
|
-
end
|
|
36
|
+
def call(src, document: Document.new(src, @file))
|
|
37
|
+
code = {}
|
|
38
|
+
document.code_blocks.each do |block|
|
|
39
|
+
syntax = block.options[:lang].to_s
|
|
40
|
+
lines = block.options.fetch(:code_lines)
|
|
41
|
+
next unless ruby_codeblock?(syntax, "#{lines.join("\n")}\n")
|
|
42
|
+
|
|
43
|
+
code.merge!(document.code_lines(block))
|
|
79
44
|
end
|
|
45
|
+
src.lines.each_with_index.map do |line, index|
|
|
46
|
+
code.key?(index + 1) ? code.fetch(index + 1) + line[/\r?\n\z/].to_s : comment_lines(line)
|
|
47
|
+
end.join
|
|
80
48
|
end
|
|
81
|
-
# rubocop:enable Metrics/MethodLength
|
|
82
49
|
|
|
83
50
|
private
|
|
84
51
|
|
|
@@ -27,6 +27,13 @@ module RuboCop
|
|
|
27
27
|
def markdown_file?(file)
|
|
28
28
|
MARKDOWN_EXTENSIONS.include?(File.extname(file))
|
|
29
29
|
end
|
|
30
|
+
|
|
31
|
+
def ignored_offense?(cop_name, location)
|
|
32
|
+
return false if MARKDOWN_OFFENSE_COPS.include?(cop_name)
|
|
33
|
+
return false if location.is_a?(Document::ProseRange)
|
|
34
|
+
|
|
35
|
+
location.source_line.start_with?("##{Preprocess::MARKER}")
|
|
36
|
+
end
|
|
30
37
|
end
|
|
31
38
|
end
|
|
32
39
|
end
|
|
@@ -41,7 +48,6 @@ RuboCop::Runner.prepend(Module.new do
|
|
|
41
48
|
|
|
42
49
|
# Only cache markdown files if they have no offenses, 'cause cache doesn't know about processing.
|
|
43
50
|
# NOTE: we should involve preprocessing in RuboCop::CachedData#deserialize_offenses
|
|
44
|
-
# rubocop:disable Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
45
51
|
def file_offense_cache(file, ...)
|
|
46
52
|
if cached_run? && RuboCop::Markdown.markdown_file?(file)
|
|
47
53
|
config = @config_store.for_file(file)
|
|
@@ -65,16 +71,42 @@ RuboCop::Runner.prepend(Module.new do
|
|
|
65
71
|
super
|
|
66
72
|
end
|
|
67
73
|
end
|
|
68
|
-
|
|
74
|
+
end)
|
|
69
75
|
|
|
70
|
-
|
|
71
|
-
|
|
76
|
+
RuboCop::Cop::Base.prepend(Module.new do
|
|
77
|
+
def add_offense(node_or_range, **options, &block)
|
|
78
|
+
location = range_from_node_or_range(node_or_range)
|
|
79
|
+
return if RuboCop::Markdown.ignored_offense?(cop_name, location)
|
|
72
80
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
81
|
+
super
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
def range_for_original(range)
|
|
87
|
+
document = processed_source.markdown_document
|
|
88
|
+
document ? document.original_range(range) : super
|
|
89
|
+
end
|
|
90
|
+
end)
|
|
91
|
+
|
|
92
|
+
RuboCop::Cop::Corrector.prepend(Module.new do
|
|
93
|
+
def initialize(source)
|
|
94
|
+
processed = source.respond_to?(:processed_source) ? source.processed_source : source
|
|
95
|
+
@markdown_document = processed.markdown_document if processed.respond_to?(:markdown_document)
|
|
96
|
+
super(@markdown_document ? @markdown_document.processed_buffer : source)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def rewrite
|
|
100
|
+
@markdown_document ? @markdown_document.corrected_source(self) : super
|
|
101
|
+
end
|
|
77
102
|
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
def to_range(node_or_range)
|
|
106
|
+
if @markdown_document && node_or_range.is_a?(Parser::Source::Range) &&
|
|
107
|
+
node_or_range.source_buffer.equal?(@markdown_document.buffer)
|
|
108
|
+
node_or_range = @markdown_document.processed_range(node_or_range)
|
|
109
|
+
end
|
|
78
110
|
super
|
|
79
111
|
end
|
|
80
112
|
end)
|
|
@@ -82,13 +114,8 @@ end)
|
|
|
82
114
|
RuboCop::Cop::Commissioner::InvestigationReport.prepend(Module.new do
|
|
83
115
|
# Skip offenses reported for ignored MD source (trailing whitespaces, etc.)
|
|
84
116
|
def offenses
|
|
85
|
-
@offenses ||=
|
|
86
|
-
|
|
87
|
-
offenses_per_cop.flatten(1).reject do |offense|
|
|
88
|
-
next if RuboCop::Markdown::MARKDOWN_OFFENSE_COPS.include?(offense.cop_name)
|
|
89
|
-
|
|
90
|
-
offense.location.source_line.start_with?(marker_comment)
|
|
91
|
-
end
|
|
117
|
+
@offenses ||= offenses_per_cop.flatten(1).reject do |offense|
|
|
118
|
+
RuboCop::Markdown.ignored_offense?(offense.cop_name, offense.location)
|
|
92
119
|
end
|
|
93
120
|
end
|
|
94
121
|
end)
|
|
@@ -102,10 +129,40 @@ end)
|
|
|
102
129
|
|
|
103
130
|
# Extend ProcessedSource#parse with pre-processing
|
|
104
131
|
RuboCop::ProcessedSource.prepend(Module.new do
|
|
132
|
+
attr_reader :markdown_document
|
|
133
|
+
|
|
134
|
+
def comments
|
|
135
|
+
return super unless path && RuboCop::Markdown.markdown_file?(path)
|
|
136
|
+
|
|
137
|
+
@markdown_comments ||= super.map { |comment| markdown_directive(comment) || comment }
|
|
138
|
+
end
|
|
139
|
+
|
|
105
140
|
def parse(src, ...)
|
|
106
141
|
# only process Markdown files
|
|
107
|
-
|
|
108
|
-
|
|
142
|
+
if path && RuboCop::Markdown.markdown_file?(path)
|
|
143
|
+
@markdown_comments = nil
|
|
144
|
+
@markdown_document = RuboCop::Markdown::Document.new(src, path)
|
|
145
|
+
src = RuboCop::Markdown::Preprocess.new(path).call(src, document: @markdown_document)
|
|
146
|
+
end
|
|
109
147
|
super
|
|
148
|
+
@markdown_document.processed_buffer = @buffer if @markdown_document
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
private
|
|
152
|
+
|
|
153
|
+
def markdown_directive(comment)
|
|
154
|
+
return unless comment.text.start_with?("##{RuboCop::Markdown::Preprocess::MARKER}")
|
|
155
|
+
|
|
156
|
+
match = comment.text.match(
|
|
157
|
+
/(?:<!--|<span\b[^>]*>)\s*(#\s*rubocop:[^\n]*?)\s*(?:-->|<\/span>)\s*\z/
|
|
158
|
+
)
|
|
159
|
+
return unless match
|
|
160
|
+
|
|
161
|
+
location = comment.source_range
|
|
162
|
+
range = Parser::Source::Range.new(
|
|
163
|
+
location.source_buffer,
|
|
164
|
+
location.begin_pos + match.begin(1), location.begin_pos + match.end(1)
|
|
165
|
+
)
|
|
166
|
+
Parser::Source::Comment.new(range)
|
|
110
167
|
end
|
|
111
168
|
end)
|
data/lib/rubocop/markdown.rb
CHANGED
|
@@ -12,5 +12,7 @@ module RuboCop
|
|
|
12
12
|
require_relative "markdown/preprocess"
|
|
13
13
|
require_relative "markdown/rubocop_ext" if defined?(::RuboCop::ProcessedSource)
|
|
14
14
|
require_relative "markdown/plugin"
|
|
15
|
+
require_relative "markdown/document"
|
|
16
|
+
require_relative "cop/markdown/syntax"
|
|
15
17
|
end
|
|
16
18
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rubocop-md
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 3.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Vladimir Dementyev
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-11-06 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: lint_roller
|
|
@@ -38,6 +38,34 @@ dependencies:
|
|
|
38
38
|
- - ">="
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
40
|
version: 1.72.1
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: kramdown
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '2.4'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '2.4'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: kramdown-parser-gfm
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '1.1'
|
|
62
|
+
type: :runtime
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '1.1'
|
|
41
69
|
- !ruby/object:Gem::Dependency
|
|
42
70
|
name: bundler
|
|
43
71
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -93,7 +121,10 @@ files:
|
|
|
93
121
|
- README.md
|
|
94
122
|
- config/default.yml
|
|
95
123
|
- lib/rubocop-md.rb
|
|
124
|
+
- lib/rubocop/cop/markdown/syntax.rb
|
|
96
125
|
- lib/rubocop/markdown.rb
|
|
126
|
+
- lib/rubocop/markdown/document.rb
|
|
127
|
+
- lib/rubocop/markdown/parser.rb
|
|
97
128
|
- lib/rubocop/markdown/plugin.rb
|
|
98
129
|
- lib/rubocop/markdown/preprocess.rb
|
|
99
130
|
- lib/rubocop/markdown/rubocop_ext.rb
|