rdoc-markdown 0.21.1 → 0.22.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: 5103b919452fc2a8143563d54ce1c2e7ab3e4aaf2afdfe44d6057fcce9a2e5c4
4
- data.tar.gz: d8abe423253297371c5cb60c722bb24507c40d2a73ee9df37eca75a1b2dab60a
3
+ metadata.gz: 6f862496849b46537b75b6d25d1e313a830a9aaf535854a5233fad42081b0a0c
4
+ data.tar.gz: e7505ea0dadde9f2f6d357d96ce022e9b9cdc3b0a5457a1648a49bffb4c09fd4
5
5
  SHA512:
6
- metadata.gz: 3fa2a3b9e6cc98af9902d054f3ad4989e05639f9f55da4bd2d5e4f5a984d9e97833d1c4d06898439a00907ad783ec7329ad46c2c8ae614fcc46a001ceea444fd
7
- data.tar.gz: dedabad24735933e97d59127de66271a2a628287822e626c6eb4c5bf9631c613c512a05a511918c4a84fed34bffa61dd29166716fe61ae4badf38fa3e9220deb
6
+ metadata.gz: d6fb9ba4435a1b7e3ae3d9da59b2102a47574dc8c33570ca807358bec137116b17474a1721277d3ebe5833e1093b016fcc08a1dfd24d93f71f12b785382259ca
7
+ data.tar.gz: 3f25a645ec86e266b05fc0b934e2aa82f088011a6bf9906b881c796eff4c86684fafe05da70259fbdeb0c0221a2bfaf28262a867a292d58add5f1b4ca329bf5a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.22.0
4
+
5
+ - BREAKING: Use Ruby notation in search-index names: `Duck.MAX_VELOCITY` becomes `Duck::MAX_VELOCITY`, and instance attributes use `Class#attr` instead of `Class.attr`. Consumers matching index names must update these lookups. Class-level attributes retain `Class.attr`; document paths and anchors are unchanged.
6
+ - Replace `reverse_markdown` and its converter fork with RDoc's native `RDoc::Markup::ToMarkdown` formatter.
7
+
8
+ ## 0.21.2
9
+
10
+ - Distinguish instance methods (`Class#method`) from class methods (`Class.method`) in search-index names, without changing their document paths or anchors.
11
+
3
12
  ## 0.21.1
4
13
 
5
14
  - Preserve hash-prefixed comments and literal Markdown headings in code examples when offsetting documentation headings.
data/README.md CHANGED
@@ -1,15 +1,8 @@
1
1
  # RDoc-Markdown
2
2
  RDoc plugin to generate markdown documentation and search index file (CSV).
3
3
 
4
- > [!CAUTION]
5
- > This gem relies on multiple hacks to generate "plausible" markdown documentation. This is **NOT PRODUCTION READY** — use at your own risk.
6
- >
7
- > RDoc maintainers are actively working on markdown support, things will improve with time...
8
-
9
4
  ## Motivation
10
- Markdown has become the de facto documentation standard. We can render markdown files on any device, possibly on a thermometer with a screen. And everyone knows markdown...
11
-
12
- It's a pity that RDoc can't output a proper markdown file. Somebody has to try to build it.
5
+ Markdown has become the de facto documentation standard. Rdoc should be able to output it too.
13
6
 
14
7
  ## Installation
15
8
 
@@ -23,6 +16,12 @@ If Bundler is not being used to manage dependencies, install the gem by executin
23
16
 
24
17
  $ gem install rdoc-markdown
25
18
 
19
+ Comments render directly through RDoc's native Markdown visitor. No
20
+ `reverse_markdown` dependency or converter-fork override is required.
21
+ Commonmarker (`~> 2.8`) is a runtime dependency used for Markdown AST parsing
22
+ and serialization, not HTML rendering. Definition-list HTML is emitted directly;
23
+ native documents never take an HTML-to-Markdown conversion round trip.
24
+
26
25
 
27
26
  ## Usage
28
27
  RDoc will auto-detect the rdoc-markdown plugin if it is installed. You just need to instruct RDoc to produce markdown output instead of standard HTML through the `format` parameter.
@@ -39,7 +38,7 @@ gem build rdoc-markdown.gemspec
39
38
  ```
40
39
 
41
40
  ```
42
- gem install <path-to>/rdoc-markdown-0.1.2.gem
41
+ gem install <path-to>/rdoc-markdown-<version>.gem
43
42
  ```
44
43
 
45
44
  or you can do the same through a Gemfile:
@@ -66,12 +65,6 @@ sections: an anchor appearing only in a code example or a raw HTML block is not
66
65
  enough. This catches method headings swallowed by an unterminated Markdown HTML
67
66
  block, including aliases that link back to those methods.
68
67
 
69
- To lint markdown ERB templates:
70
-
71
- ```
72
- bundle exec rake erb:lint
73
- ```
74
-
75
68
  ### Generate vendored docs
76
69
  Use rake tasks to generate markdown output for vendored projects:
77
70
 
@@ -93,7 +86,7 @@ Find examples in the [/example](/example/) folder.
93
86
 
94
87
  ```
95
88
  gem build rdoc-markdown.gemspec
96
- gem push rdoc-markdown-0.1.2.gem
89
+ gem push rdoc-markdown-<version>.gem
97
90
  ```
98
91
 
99
92
  There is a `./bin/publish.sh` script that does that. However, it assumes that you have the [gum library](https://github.com/charmbracelet/gum) installed.
data/lib/rdoc/discover.rb CHANGED
@@ -1 +1,3 @@
1
- require "markdown"
1
+ # frozen_string_literal: true
2
+
3
+ require "rdoc/generator/markdown"
@@ -0,0 +1,268 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RDoc::Generator::Markdown
4
+ # Native block visitors; nested fragments share context, not output buffers.
5
+ module Blocks
6
+ # Standalone source annotations, not documentation or inline examples.
7
+ LINTER_DIRECTIVE = /^(?:rubocop|standard):(?:disable|enable|todo)(?:[ \t]+[^\r\n]*)?(?:\r?\n|\z)/
8
+
9
+ # Visits nested documents and lists without resetting document state.
10
+ #
11
+ # @param document [RDoc::Markup::Document] Parsed comment document.
12
+ # @return [void]
13
+ def accept_document(document)
14
+ document.each do |part|
15
+ case part
16
+ when RDoc::Markup::Document
17
+ accept_document(part)
18
+ when RDoc::Markup::List
19
+ accept_list(part)
20
+ else
21
+ part.accept(self)
22
+ end
23
+ end
24
+ end
25
+
26
+ # Renders list contents natively, including code inside definition lists.
27
+ #
28
+ # @param list [RDoc::Markup::List] Parsed list.
29
+ # @return [void]
30
+ def accept_list(list)
31
+ type = list.type
32
+ unless [:BULLET, :NUMBER, :LALPHA, :UALPHA, :LABEL, :NOTE].include?(type)
33
+ raise RDoc::Error, "Invalid list type: #{type.inspect}"
34
+ end
35
+
36
+ definition = [:LABEL, :NOTE].include?(type)
37
+ entries = list.items
38
+ return html_list(type, entries) if definition_body && !definition
39
+
40
+ items = entries.each_with_index.map { |item, index| list_item(type, item, index) }.join("\n\n")
41
+ if definition
42
+ res << %(<dl class="rdoc-list #{type.downcase}-list">\n\n#{items}</dl>\n\n)
43
+ else
44
+ res << items << "\n\n"
45
+ end
46
+ end
47
+
48
+ # Quotes every line, preserving nested blocks and literal code.
49
+ #
50
+ # @param quote [RDoc::Markup::BlockQuote] Quoted markup nodes.
51
+ # @return [void]
52
+ def accept_block_quote(quote)
53
+ res << render_parts(quote.parts).lines.map { |line| "> #{line}" } << "\n\n"
54
+ end
55
+
56
+ # Preserves RDoc's namespaced and legacy heading anchors.
57
+ #
58
+ # @param heading [RDoc::Markup::Heading] Comment heading.
59
+ # @return [void]
60
+ def accept_heading(heading)
61
+ text = attributes(heading.text)
62
+ aliases = heading_aliases(heading, text)
63
+ if leading_native_link?(heading, text)
64
+ text = text.sub(/\A\[([^\]]+)\]\((#[^)]+)\)/) do
65
+ target = Regexp.last_match(2).delete_prefix("#")
66
+ aliases << target unless target == heading.aref
67
+ Regexp.last_match(1)
68
+ end
69
+ end
70
+ res << Syntax.heading(heading.level + rendering.heading_offset, text, aliases.uniq) << "\n\n"
71
+ end
72
+
73
+ # Converts embedded HTML using the existing compatibility rules.
74
+ #
75
+ # @param raw [RDoc::Markup::Raw] Raw HTML block.
76
+ # @return [void]
77
+ def accept_raw(raw)
78
+ res << HtmlInput.new(heading_level_offset: rendering.heading_offset).render(raw.parts.join("\n")) << "\n\n"
79
+ end
80
+
81
+ # Emits code without HTML escaping or syntax-highlighting round trips.
82
+ #
83
+ # @param verbatim [RDoc::Markup::Verbatim] Literal code block.
84
+ # @return [void]
85
+ def accept_verbatim(verbatim)
86
+ text = verbatim.text
87
+ language = verbatim.format
88
+ example = definition_list_example(text) unless language || definition_body
89
+ return res << example << "\n\n" if example
90
+ ruby_code = verbatim.ruby? || (!language && Prism.parse_success?(text))
91
+ language = "ruby" if ruby_code
92
+ if definition_body
93
+ klass = %( class="brush: #{CGI.escapeHTML(language.to_s)};") if language
94
+ content = CGI.escapeHTML(text.rstrip)
95
+ content << "\n" if ruby_code
96
+ res << "<pre#{klass}>#{content}</pre>\n\n"
97
+ else
98
+ res << Syntax.code_block(text, language) << "\n\n"
99
+ end
100
+ end
101
+
102
+ # Separates native tables from the paragraph that follows them.
103
+ #
104
+ # @param header [Array<String>] Column headings.
105
+ # @param body [Array<Array<String>>] Table rows.
106
+ # @param aligns [Array<Symbol, nil>] Column alignment hints.
107
+ # @return [void]
108
+ def accept_table(header, body, aligns)
109
+ rows = [header, *body].map { |row| table_row(row) }
110
+ rows.insert(1, aligns.map { "---" })
111
+ res << rows.map { |row| "| #{row.join(" | ")} |\n" } << "\n"
112
+ end
113
+
114
+ # Preserves RDoc's language-aware soft breaks and explicit hard breaks.
115
+ # @param paragraph [RDoc::Markup::Paragraph] Parsed paragraph.
116
+ # @return [void]
117
+ def accept_paragraph(paragraph)
118
+ text = paragraph_text(paragraph)
119
+ text = text.gsub(/(#{RDoc::Text::SPACE_SEPARATED_LETTER_CLASS})?\K(?:\r?\n)+(?=(?(1)(#{RDoc::Text::SPACE_SEPARATED_LETTER_CLASS})?))/o) { defined?($2) && " " }
120
+ # Main exposes Markdown-style headings in paragraphs, but never in verbatim.
121
+ definition = definition_body
122
+ heading = text.match(/\A {0,3}(\#{1,6})(?:[[:blank:]]+|\z)(.*)\z/) unless definition
123
+ markdown = if heading
124
+ Syntax.heading(heading[1].length, attributes(heading[2]), [])
125
+ else
126
+ attributes(text)
127
+ end
128
+ markdown = "<p>#{InlineHtml.render(markdown)}</p>" if definition
129
+ wrap(markdown)
130
+ end
131
+
132
+ # Emits paragraphs without wrapping Markdown links across lines.
133
+ #
134
+ # @param text [String] Inline Markdown.
135
+ # @return [void]
136
+ def wrap(text)
137
+ res << text << "\n\n"
138
+ end
139
+
140
+ private
141
+
142
+ # Removes source annotations without changing their occurrences inside code.
143
+ # @param paragraph [RDoc::Markup::Paragraph] Original parsed paragraph.
144
+ # @return [String] Prose with code and hard breaks preserved.
145
+ def paragraph_text(paragraph)
146
+ text_parts = paragraph.parts.map { |part| RDoc::Markup::Paragraph.new(part).text("<br>\n").encode(Encoding::UTF_8) }
147
+ ranges = Syntax.code_ranges(text_parts.join)
148
+ prefix = +""
149
+ text = text_parts.map do |part|
150
+ offset = prefix.bytesize
151
+ prefix << part
152
+ strip_linter_directives(part, offset, ranges)
153
+ end.join
154
+ text.force_encoding(paragraph.text.encoding) if text.ascii_only?
155
+ text
156
+ end
157
+
158
+ # Strips only annotation matches outside literal inline source.
159
+ # @param text [String] UTF-8 paragraph part.
160
+ # @param offset [Integer] Original part's source byte offset.
161
+ # @param ranges [Array<Range<Integer>>] Literal source ranges.
162
+ # @return [String] Filtered paragraph part.
163
+ def strip_linter_directives(text, offset, ranges)
164
+ text.gsub(LINTER_DIRECTIVE) do |line|
165
+ position = offset + Regexp.last_match.pre_match.bytesize
166
+ line if Syntax.literal_position?(ranges, position)
167
+ end
168
+ end
169
+
170
+ # Restores main's Markdown lists for complete indented term-and-bullet examples.
171
+ # @param text [String] Unclassified verbatim content.
172
+ # @return [String, nil] Markdown list, or nil when any line is ordinary code.
173
+ def definition_list_example(text)
174
+ lines = text.lines.map(&:strip)
175
+ valid_lines = lines.all? { |line| line.empty? || line.end_with?("::") || line.match?(/\A\*\s/) }
176
+ return unless lines.first&.end_with?("::") && valid_lines && lines.grep(/\A\*\s/).any?
177
+
178
+ lines.map { |line| line.sub(/::\z/, ":").sub(/\A\*\s/, "- ") }.join("\n")
179
+ end
180
+
181
+ # Nested lists stay in the final HTML definition container, like main.
182
+ # @param type [Symbol] Validated ordinary list type.
183
+ # @param entries [Array<RDoc::Markup::ListItem>] Native list items.
184
+ # @return [void]
185
+ def html_list(type, entries)
186
+ opening, closing = RDoc::Markup::ToHtml::LIST_TYPE_TO_HTML.fetch(type)
187
+ res << opening
188
+ entries.each { |item| res << "<li>\n\n#{render_parts(item.parts)}\n\n</li>\n\n" }
189
+ res << closing << "\n\n"
190
+ end
191
+
192
+ # Indents continuation blocks by the width of their actual list marker.
193
+ #
194
+ # @param type [Symbol] RDoc list type.
195
+ # @param item [RDoc::Markup::ListItem] Item containing native markup nodes.
196
+ # @param index [Integer] Zero-based item index.
197
+ # @return [String] Markdown list item or HTML definition container.
198
+ def list_item(type, item, index)
199
+ definition = [:LABEL, :NOTE].include?(type)
200
+ body = render_parts(item.parts, definition: definition)
201
+ return definition_item(item.label, body) if definition
202
+
203
+ marker = (type == :BULLET) ? "-" : "#{index + 1}."
204
+ "#{marker} #{body.gsub("\n", "\n#{" " * (marker.length + 1)}")}"
205
+ end
206
+
207
+ # Blank lines allow GFM to parse native Markdown inside definition containers.
208
+ #
209
+ # @param label [String, Array<String>, nil] Definition terms.
210
+ # @param body [String] Native Markdown for the description.
211
+ # @return [String] Definition item with Markdown-enabled HTML boundaries.
212
+ def definition_item(label, body)
213
+ terms = Array(label).map { |term| "<dt>#{InlineHtml.render(attributes(term))}</dt>\n" }.join
214
+ "#{terms}<dd>\n\n#{body}\n\n</dd>\n\n"
215
+ end
216
+
217
+ # Renders nested blocks without resetting shared heading IDs or cross-references.
218
+ #
219
+ # @param parts [Array<Object>] Child markup nodes.
220
+ # @param definition [Boolean, nil] Final HTML definition-body boundary.
221
+ # @return [String] Nested Markdown.
222
+ def render_parts(parts, definition: definition_body)
223
+ visitor = dup
224
+ visitor.definition_body = definition
225
+ RDoc::Markup::Document.new(*parts).accept(visitor).rstrip
226
+ end
227
+
228
+ # Formats table cells in their own inline escaping context.
229
+ # @param cells [Array<String>] RDoc cell contents.
230
+ # @return [Array<String>] GFM-safe cells.
231
+ def table_row(cells)
232
+ cells.map { |cell| attributes(cell).gsub("|", '\\|') }
233
+ end
234
+
235
+ # Computes compatible heading aliases from RDoc's canonical label methods.
236
+ # @param heading [RDoc::Markup::Heading] Parsed heading.
237
+ # @param text [String] Native inline heading Markdown.
238
+ # @return [Array<String>] Legacy and namespaced IDs.
239
+ def heading_aliases(heading, text)
240
+ object = rendering.object
241
+ modern = unique_heading_id(heading.label(object))
242
+ legacy = unique_heading_id(heading.legacy_label(object))
243
+ aliases = []
244
+ aliases << legacy unless RDoc::ClassModule === object && heading.level == 1
245
+ canonical = RDoc::Text.to_anchor(Syntax.node_text(Commonmarker.parse(text.encode(Encoding::UTF_8))))
246
+ aliases << modern unless text.empty? || modern == canonical || leading_native_link?(heading, text)
247
+ aliases
248
+ end
249
+
250
+ # Main drops the outer self-link before native links, but sees Markdown as text.
251
+ # @param heading [RDoc::Markup::Heading] Original heading source.
252
+ # @param text [String] Native inline heading Markdown.
253
+ # @return [Boolean] Whether the first inline node is a native link.
254
+ def leading_native_link?(heading, text)
255
+ text.start_with?("[") && !heading.text.start_with?("[")
256
+ end
257
+
258
+ # Deduplicates IDs across the document and its native child visitors.
259
+ # @param id [String] RDoc heading ID.
260
+ # @return [String] Unique heading ID.
261
+ def unique_heading_id(id)
262
+ ids = rendering.heading_ids
263
+ count = ids[id]
264
+ ids[id] = count + 1
265
+ count.zero? ? id : "#{id}-#{count}"
266
+ end
267
+ end
268
+ end
@@ -33,42 +33,25 @@ module RDoc::Generator::Markdown::Descriptions
33
33
  # Renders an RDoc description with links limited to emitted objects.
34
34
  #
35
35
  # @param code_object [RDoc::CodeObject, RDoc::Context::Section] Object whose description is rendered.
36
+ # @param heading_level_offset [Integer] Levels to add to comment headings.
36
37
  #
37
- # @return [String] HTML description.
38
- def render_description(code_object)
39
- formatter = description_formatter(code_object)
40
- formatter.extend(RDoc::Generator::Markdown::CrossrefExtension)
41
- formatter.with_markdown_cross_references(
42
- RDoc::CrossReference.new(formatter.context),
43
- generation_state.markdown_output_object_ids
44
- ) do
45
- render_formatted_description(code_object, formatter, options.locale)
38
+ # @return [String] Markdown description.
39
+ def render_description(code_object, heading_level_offset:)
40
+ formatter = RDoc::Generator::Markdown::Formatter.new(
41
+ code_object,
42
+ output_object_ids: generation_state.markdown_output_object_ids,
43
+ heading_level_offset: heading_level_offset
44
+ )
45
+ locale = options.locale
46
+ document = if RDoc::Context::Section === code_object
47
+ documents = code_object.comments.map { |comment| localized_comment_document(comment, locale) }
48
+ RDoc::Markup::Document.new(*documents)
49
+ else
50
+ comment = (RDoc::ClassModule === code_object) ? code_object.comment_location : code_object.comment
51
+ comment = RDoc::I18n::Text.new(comment).translate(locale) if locale
52
+ code_object.parse(comment)
46
53
  end
47
- end
48
-
49
- # Returns the formatter appropriate for an object or section.
50
- #
51
- # @param code_object [RDoc::CodeObject, RDoc::Context::Section] Described object.
52
- #
53
- # @return [RDoc::Markup::Formatter] Formatter configured for the object.
54
- def description_formatter(code_object)
55
- return code_object.formatter unless RDoc::Context::Section === code_object
56
-
57
- code_object.parent.formatter.dup.tap { |copy| copy.code_object = code_object }
58
- end
59
-
60
- # Renders an object through its configured formatter.
61
- #
62
- # @param code_object [RDoc::CodeObject, RDoc::Context::Section] Described object.
63
- # @param formatter [RDoc::Markup::Formatter] Formatter receiving parsed markup.
64
- # @param locale [RDoc::I18n::Locale, nil] Translation locale.
65
- #
66
- # @return [String] Formatted HTML description.
67
- def render_formatted_description(code_object, formatter, locale)
68
- return code_object.description unless RDoc::Context::Section === code_object
69
-
70
- documents = code_object.comments.map { |comment| localized_comment_document(comment, locale) }
71
- RDoc::Markup::Document.new(*documents).accept(formatter)
54
+ document.accept(formatter).rstrip
72
55
  end
73
56
 
74
57
  # Parses one section comment, translating a copy when needed.
@@ -93,25 +76,10 @@ module RDoc::Generator::Markdown::Descriptions
93
76
  #
94
77
  # @return [String] Rendered description or fallback text.
95
78
  def describe(code_object, fallback: nil, heading_level_offset: 0)
96
- description = render_description(code_object)
97
- return fallback.to_s if description.empty?
98
-
99
79
  comment = code_object.comment
100
80
  heading_level_offset = 0 if RDoc::Comment === comment && comment.format == "markdown"
101
- RDoc::Generator::Markdown::Conversion.markdownify(description, heading_level_offset: heading_level_offset)
102
- end
103
-
104
- # Renders a section description as Markdown.
105
- #
106
- # @param section [RDoc::Context::Section] RDoc section whose description appears before grouped members.
107
- # @param heading_level_offset [Integer] Heading levels to add while rendering.
108
- #
109
- # @return [String] Rendered section description.
110
- def section_description(section, heading_level_offset:)
111
- RDoc::Generator::Markdown::Conversion.markdownify(
112
- render_description(section),
113
- heading_level_offset: heading_level_offset
114
- )
81
+ description = render_description(code_object, heading_level_offset: heading_level_offset)
82
+ description.empty? ? fallback.to_s : description
115
83
  end
116
84
 
117
85
  # Renders a method description or an alias fallback.
@@ -128,20 +96,27 @@ module RDoc::Generator::Markdown::Descriptions
128
96
  aliased_method = method.is_alias_for
129
97
  return "Not documented." unless aliased_method
130
98
 
131
- alias_description(aliased_method, method_link(aliased_method, current_class: current_class))
99
+ name = aliased_method.name
100
+ link = method_link(aliased_method, current_class: current_class)
101
+ link ? "Alias for: [`#{name}`](#{link})" : "Alias for: `#{name}`"
132
102
  end
133
103
 
134
- # Formats an alias fallback with an optional target link.
104
+ # Lists aliases that have generated documentation sections.
135
105
  #
136
- # @param aliased_method [RDoc::AnyMethod] Alias target.
137
- # @param link [String, nil] Generated link target.
106
+ # @param method [RDoc::AnyMethod] Canonical method.
138
107
  #
139
- # @return [String] Alias description.
140
- def alias_description(aliased_method, link)
141
- name = aliased_method.name
142
- return "Alias for: `#{name}`" unless link
108
+ # @return [String, nil] Linked alias list, or nil when none are emitted.
109
+ def method_aliases(method)
110
+ links = method.aliases.filter_map do |aka|
111
+ # RDoc's module_function copies share alias arrays with their instance methods.
112
+ next unless aka.is_alias_for.equal?(method)
113
+
114
+ link = method_link(aka, current_class: method.parent)
115
+ "[`#{aka.name}`](#{link})" if link
116
+ end
117
+ return if links.empty?
143
118
 
144
- "Alias for: [`#{name}`](#{link})"
119
+ "Also known as: #{links.join(", ")}"
145
120
  end
146
121
 
147
122
  # Builds a Markdown link target for an aliased method.
@@ -154,6 +129,7 @@ module RDoc::Generator::Markdown::Descriptions
154
129
  return unless method.display?
155
130
 
156
131
  target_parent = method.parent
132
+ return unless target_parent
157
133
  return unless target_parent.method_list.include?(method)
158
134
 
159
135
  target_path = generation_state.class_output_paths[target_parent.full_name]
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rdoc/markup/to_markdown"
4
+ require "prism"
5
+ require "commonmarker"
6
+ require_relative "markdown_links"
7
+ require_relative "inline_html"
8
+ require_relative "inline"
9
+ require_relative "blocks"
10
+
11
+ class RDoc::Generator::Markdown
12
+ # One native visitor with separate block, inline, and reference responsibilities.
13
+ class Formatter < RDoc::Markup::ToMarkdown
14
+ include References
15
+ include Inline
16
+ include Blocks
17
+
18
+ # Context shared by child visitors; each child owns its native output buffer.
19
+ Rendering = Data.define(:object, :context, :reference, :output_ids, :heading_offset, :heading_ids)
20
+
21
+ # Immutable render configuration and document-scoped heading IDs.
22
+ # @return [Rendering] Current document context.
23
+ attr_reader :rendering
24
+
25
+ # Child visitors inherit the final HTML boundary of a definition-list body.
26
+ # @return [Boolean, nil] Whether blocks are inside a definition description.
27
+ attr_accessor :definition_body
28
+
29
+ # Block visitors emit their own paragraph spacing.
30
+ alias_method :accept_blank_line, :ignore
31
+
32
+ # Initializes the native formatter and its document-scoped references.
33
+ # @param code_object [RDoc::CodeObject, RDoc::Context::Section] Described object.
34
+ # @param output_object_ids [Set<Integer>] Emitted objects.
35
+ # @param heading_level_offset [Integer] Levels to add to comment headings.
36
+ def initialize(code_object, output_object_ids:, heading_level_offset:)
37
+ super()
38
+ context = (RDoc::Context === code_object) ? code_object : code_object.parent
39
+ @rendering = Rendering.new(code_object, context, RDoc::CrossReference.new(context), output_object_ids, heading_level_offset, Hash.new(0))
40
+ @from_path = context.path
41
+ configure_inline
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RDoc::Generator::Markdown
4
+ # Explicit HTML input boundary; never used to render native RDoc nodes.
5
+ class HtmlInput
6
+ # Native delimiters for the inline HTML tags accepted at this boundary.
7
+ INLINE_MARKERS = {"strong" => "**", "b" => "**", "em" => "*", "i" => "*", "del" => "~~", "s" => "~~"}.freeze
8
+
9
+ # Creates an adapter with the surrounding comment's heading offset.
10
+ # @param heading_level_offset [Integer] Levels to add to HTML headings.
11
+ def initialize(heading_level_offset:)
12
+ @heading_level_offset = heading_level_offset
13
+ end
14
+
15
+ # Preserves HTML-only constructs and adapts supported elements to native GFM.
16
+ # @param html [String] Explicit raw HTML input.
17
+ # @return [String] Markdown fragment.
18
+ def render(html)
19
+ fragment = Nokogiri::HTML.fragment(html)
20
+ fragment.css("span.legacy-anchor[id]").each { |span| position_anchor(span) }
21
+ nodes = fragment.children.reject { |node| node.text? && !node.text.match?(/\S/) }
22
+ render_nodes(nodes).strip
23
+ end
24
+
25
+ private
26
+
27
+ # Keeps legacy spans beside their heading without exposing context aliases.
28
+ # @param span [Nokogiri::XML::Element] Legacy anchor.
29
+ # @return [void]
30
+ def position_anchor(span)
31
+ heading = span.next_element
32
+ if heading&.name == "h1" && span["id"].match?(/\A(?:class|module)-/)
33
+ span.remove
34
+ elsif heading&.name&.match?(/\Ah[1-6]\z/)
35
+ heading.add_child(span)
36
+ end
37
+ end
38
+
39
+ # Formats adjacent input nodes without reparsing emitted Markdown.
40
+ # @param nodes [Nokogiri::XML::NodeSet] Input siblings.
41
+ # @return [String] Native Markdown.
42
+ def render_nodes(nodes)
43
+ nodes.map { |node| render_node(node) }.join
44
+ end
45
+
46
+ # Dispatches explicit HTML blocks and native inline primitives.
47
+ # @param node [Nokogiri::XML::Node] HTML input node.
48
+ # @return [String] Native Markdown or preserved raw HTML.
49
+ def render_node(node)
50
+ return Syntax.escape(node.content.tr("\n", " ")) if node.text?
51
+
52
+ case node.name
53
+ when "p" then paragraph(node)
54
+ when /\Ah[1-6]\z/ then heading(node)
55
+ when "pre" then preformatted(node)
56
+ when "code", "tt" then Syntax.code(node.text)
57
+ when "br" then "<br>"
58
+ when *INLINE_MARKERS.keys, "a", "span", "div" then inline_element(node)
59
+ else preserve_html(node)
60
+ end
61
+ end
62
+
63
+ # Separates paragraphs from preceding standalone anchor spans.
64
+ # @param node [Nokogiri::XML::Element] Paragraph element.
65
+ # @return [String] Paragraph Markdown.
66
+ def paragraph(node)
67
+ prefix = "\n\n" if node.previous_element&.name == "span"
68
+ "#{prefix}#{render_nodes(node.children)}\n\n"
69
+ end
70
+
71
+ # Keeps literal indentation while removing the conventional opening newline.
72
+ # @param node [Nokogiri::XML::Element] Preformatted HTML input.
73
+ # @return [String] Native fenced code.
74
+ def preformatted(node)
75
+ css = node["class"].to_s
76
+ language = css[/\A(?!highlight\z)[A-Za-z][A-Za-z0-9_+-]*\z/] ||
77
+ css[/\Abrush: (\w+);\z/, 1] || node.parent["class"].to_s[/\Ahighlight-(\w+)\z/, 1]
78
+ "#{Syntax.code_block(node.text.delete_prefix("\n"), language)}\n\n"
79
+ end
80
+
81
+ # Converts only known inline tags, preserving explicit anchor identities.
82
+ # @param node [Nokogiri::XML::Element] Inline or transparent container.
83
+ # @return [String] Inline Markdown.
84
+ def inline_element(node)
85
+ name = node.name
86
+ content = render_nodes(node.children)
87
+ marker = INLINE_MARKERS[name]
88
+ return "#{marker}#{content}#{marker}" if marker
89
+
90
+ id = node["id"]
91
+ link = name == "a"
92
+ return Syntax.anchor(id) if id && (link || node["class"] == "legacy-anchor")
93
+ return content unless link
94
+
95
+ href = node["href"]
96
+ href ? Syntax.link(content, href) : content
97
+ end
98
+
99
+ # Leaves unsupported HTML intact while mapping its actual link targets.
100
+ # @param node [Nokogiri::XML::Element] HTML-only construct.
101
+ # @return [String] Preserved HTML block.
102
+ def preserve_html(node)
103
+ node.css("a[href]").each { |link| link["href"] = Syntax.target(link["href"]) }
104
+ "#{node}\n\n"
105
+ end
106
+
107
+ # Flattens leading heading self-links and retains their noncanonical aliases.
108
+ # @param node [Nokogiri::XML::Element] Heading element.
109
+ # @return [String] Native Markdown heading.
110
+ def heading(node)
111
+ spans = node.css("span.legacy-anchor[id]")
112
+ aliases = spans.map { |span| span["id"] }
113
+ spans.remove
114
+ first = node.element_children.first
115
+ href = first&.[]("href").to_s
116
+ if first&.name == "a" && href.match?(/\A#./) &&
117
+ first.text.match?(/\S/) && first.xpath("preceding-sibling::node()").none? { |sibling| sibling.text.match?(/\S/) }
118
+ id = href.delete_prefix("#")
119
+ first.replace(first.children)
120
+ aliases << id unless id == RDoc::Text.to_anchor(node.text)
121
+ end
122
+ text = render_nodes(node.children).strip
123
+ "#{Syntax.heading(Integer(node.name.delete_prefix("h")) + @heading_level_offset, text, aliases)}\n\n"
124
+ end
125
+ end
126
+ end