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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c3daa631b2f266f022b572d333bca39c035fca85c6cd3616c855f3cd796f04f7
4
- data.tar.gz: 3e78b24aee5905d444d632d7ff92f348f1ad6bbf1c8747a0c149be1373ec8a6f
3
+ metadata.gz: ac56ce2479d67dd646a069af3e135366bef0eaf66eddf47d607e9d8248d9b738
4
+ data.tar.gz: 727d15a419748305dc44918c85f14db8116d46864ff91ee9c39b10fc2996938b
5
5
  SHA512:
6
- metadata.gz: 02ed290d0f3cbfcb6d8a28f762ff7ee9d2382f391b1b6464a57f9354cbd481cfe8967bd4e1274b70ed97c1526832029efe8e4a997f7477dc3b2256c2e67e54dd
7
- data.tar.gz: cd2fdbe1a09b41dcf83ca2aa63edb827adfae08259c9a48b6a21e59710a8c0feffd9ec3ed7e418eb720904764e5ca69f69855ba9eadaae1b42d6a61ef66e7a3b
6
+ metadata.gz: df438f27e7a0e7210dc7c80b6169b7bfef9adf43af3433ceff01b324dc0693935b20a5c9c6a76166015b40443cb387924bc423796ae9621b82ef0a460ff169d4
7
+ data.tar.gz: d133ccb642a94601be04fa918b5d6c1d5ff64ffc3d175fa3888386172c897b8c82c0b6a0ea26c872127b13e9b4b1fb022f206df5fb332007e95fe118b9ea8b55
data/CHANGELOG.md CHANGED
@@ -1,83 +1,101 @@
1
1
  # Change log
2
2
 
3
- ## master (unreleased)
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
- ### But if I want to use inline disabling some directive like in classic RuboCop?
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
- You can use this tricks
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
- - Preprocess Markdown source into Ruby source preserving line numbers
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
- - Restore Markdown from preprocessed Ruby if it has been autocorrected
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
- - Uses naive Regexp-based approach to extract code blocks from Markdown, support only backticks-style code blocks\*
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
- IgnoredPatterns:
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
- # Revert preprocess changes.
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
- # rubocop:disable Metrics/MethodLength
60
- def call(src)
61
- src.gsub(MD_REGEXP) do |full_match|
62
- m = Regexp.last_match
63
- open_backticks = m[1]
64
- syntax = m[2]
65
- code = m[3]
66
- close_backticks = m[4]
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
- # rubocop:enable Metrics/MethodLength, Metrics/PerceivedComplexity
74
+ end)
69
75
 
70
- def file_finished(file, offenses, ...)
71
- return super unless RuboCop::Markdown.markdown_file?(file)
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
- # Run Preprocess.restore if file has been autocorrected
74
- if @options[:auto_correct] || @options[:autocorrect]
75
- RuboCop::Markdown::Preprocess.restore_and_save!(file)
76
- end
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 ||= begin
86
- marker_comment = "##{RuboCop::Markdown::Preprocess::MARKER}"
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
- src = RuboCop::Markdown::Preprocess.new(path).call(src) if
108
- path && RuboCop::Markdown.markdown_file?(path)
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)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RuboCop
4
4
  module Markdown
5
- VERSION = "2.0.4"
5
+ VERSION = "3.0.0"
6
6
  end
7
7
  end
@@ -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: 2.0.4
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-01-28 00:00:00.000000000 Z
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