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 +4 -4
- data/CHANGELOG.md +9 -0
- data/README.md +9 -16
- data/lib/rdoc/discover.rb +3 -1
- data/lib/rdoc/generator/markdown/blocks.rb +268 -0
- data/lib/rdoc/generator/markdown/descriptions.rb +36 -60
- data/lib/rdoc/generator/markdown/formatter.rb +44 -0
- data/lib/rdoc/generator/markdown/html_input.rb +126 -0
- data/lib/rdoc/generator/markdown/index.rb +4 -4
- data/lib/rdoc/generator/markdown/inline.rb +173 -0
- data/lib/rdoc/generator/markdown/inline_html.rb +56 -0
- data/lib/rdoc/generator/markdown/markdown_links.rb +101 -0
- data/lib/rdoc/generator/markdown/paths.rb +10 -41
- data/lib/rdoc/generator/markdown/references.rb +164 -0
- data/lib/rdoc/generator/markdown/signatures.rb +10 -74
- data/lib/rdoc/generator/markdown/syntax.rb +135 -0
- data/lib/rdoc/generator/markdown.rb +28 -46
- data/lib/rdoc/markdown/version.rb +1 -1
- data/lib/templates/classfile.md.erb +10 -5
- metadata +19 -42
- data/lib/markdown.rb +0 -3
- data/lib/rdoc/generator/markdown/conversion.rb +0 -200
- data/lib/rdoc/generator/markdown/crossref.rb +0 -41
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6f862496849b46537b75b6d25d1e313a830a9aaf535854a5233fad42081b0a0c
|
|
4
|
+
data.tar.gz: e7505ea0dadde9f2f6d357d96ce022e9b9cdc3b0a5457a1648a49bffb4c09fd4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
|
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
|
|
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
|
@@ -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]
|
|
38
|
-
def render_description(code_object)
|
|
39
|
-
formatter =
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
104
|
+
# Lists aliases that have generated documentation sections.
|
|
135
105
|
#
|
|
136
|
-
# @param
|
|
137
|
-
# @param link [String, nil] Generated link target.
|
|
106
|
+
# @param method [RDoc::AnyMethod] Canonical method.
|
|
138
107
|
#
|
|
139
|
-
# @return [String]
|
|
140
|
-
def
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
"
|
|
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
|