rdoc-markdown 0.17.2 → 0.19.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 +8 -1
- data/README.md +19 -82
- data/lib/markdown.rb +0 -4
- data/lib/rdoc/discover.rb +1 -7
- data/lib/rdoc/generator/markdown/conversion.rb +196 -0
- data/lib/rdoc/generator/markdown/crossref.rb +13 -5
- data/lib/rdoc/generator/markdown/descriptions.rb +163 -0
- data/lib/rdoc/generator/markdown/index.rb +47 -0
- data/lib/rdoc/generator/markdown/paths.rb +118 -0
- data/lib/rdoc/generator/markdown/selection.rb +39 -0
- data/lib/rdoc/generator/markdown/signatures.rb +174 -0
- data/lib/rdoc/generator/markdown.rb +80 -680
- data/lib/rdoc/markdown/version.rb +1 -1
- data/lib/templates/classfile.md.erb +14 -11
- metadata +19 -43
- data/.editorconfig +0 -13
- data/.erb_lint.yml +0 -36
- data/.erb_linters/no_embedded_assets.rb +0 -29
- data/.erb_linters/non_raw_html.rb +0 -29
- data/.standard.yml +0 -3
- data/.yard-lint.yml +0 -290
- data/AGENTS.md +0 -50
- data/CODE_OF_CONDUCT.md +0 -84
- data/Gemfile +0 -13
- data/Gemfile.lock +0 -202
- data/Rakefile +0 -296
- data/example/Bird.md +0 -21
- data/example/Duck.md +0 -56
- data/example/Object.md +0 -8
- data/example/Waterfowl.md +0 -9
- data/example/index.csv +0 -16
- data/example/jekyll-seo-tag/Jekyll/SeoTag/AuthorDrop.md +0 -35
- data/example/jekyll-seo-tag/Jekyll/SeoTag/Drop.md +0 -108
- data/example/jekyll-seo-tag/Jekyll/SeoTag/Filters.md +0 -8
- data/example/jekyll-seo-tag/Jekyll/SeoTag/ImageDrop.md +0 -29
- data/example/jekyll-seo-tag/Jekyll/SeoTag/JSONLD.md +0 -15
- data/example/jekyll-seo-tag/Jekyll/SeoTag/JSONLDDrop.md +0 -34
- data/example/jekyll-seo-tag/Jekyll/SeoTag/UrlHelper.md +0 -4
- data/example/jekyll-seo-tag/Jekyll/SeoTag.md +0 -48
- data/example/jekyll-seo-tag/Jekyll.md +0 -3
- data/example/jekyll-seo-tag/Liquid/Tag.md +0 -3
- data/example/jekyll-seo-tag/Liquid.md +0 -4
- data/example/jekyll-seo-tag/index.csv +0 -60
- data/mutant.yml +0 -15
- data/rdoc-markdown.gemspec +0 -46
|
@@ -4,77 +4,37 @@ require "erb"
|
|
|
4
4
|
require "reverse_markdown"
|
|
5
5
|
require "csv"
|
|
6
6
|
require "fileutils"
|
|
7
|
-
require "optparse"
|
|
8
7
|
|
|
9
8
|
# Generates Markdown output and a CSV search index from an RDoc store.
|
|
10
9
|
class RDoc::Generator::Markdown
|
|
11
10
|
RDoc::RDoc.add_generator self
|
|
12
11
|
|
|
12
|
+
require_relative "markdown/conversion"
|
|
13
13
|
require_relative "markdown/crossref"
|
|
14
|
+
require_relative "markdown/descriptions"
|
|
15
|
+
require_relative "markdown/index"
|
|
16
|
+
require_relative "markdown/paths"
|
|
17
|
+
require_relative "markdown/selection"
|
|
18
|
+
require_relative "markdown/signatures"
|
|
19
|
+
|
|
20
|
+
include Descriptions
|
|
21
|
+
include Index
|
|
22
|
+
include Paths
|
|
23
|
+
include Signatures
|
|
14
24
|
|
|
15
25
|
# Directory containing ERB templates.
|
|
16
26
|
TEMPLATE_DIR = File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "templates"))
|
|
17
27
|
|
|
18
|
-
#
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
@markdown_unknown_tags = :pass_through
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
# Loads markdown generator options from serialized RDoc options.
|
|
32
|
-
#
|
|
33
|
-
# @param map [Psych::Coder] Serialized RDoc options.
|
|
34
|
-
#
|
|
35
|
-
# @return [void]
|
|
36
|
-
def init_with(map)
|
|
37
|
-
super
|
|
38
|
-
@markdown_unknown_tags = map["markdown_unknown_tags"] if map.map.key?("markdown_unknown_tags")
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
# Applies markdown generator options from a loaded .rdoc_options hash.
|
|
42
|
-
#
|
|
43
|
-
# @param map [Hash] Loaded RDoc options.
|
|
44
|
-
#
|
|
45
|
-
# @return [void]
|
|
46
|
-
def override(map)
|
|
47
|
-
super
|
|
48
|
-
@markdown_unknown_tags = map.fetch("markdown_unknown_tags") if map.key?("markdown_unknown_tags")
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
# Registers markdown generator-specific RDoc options.
|
|
53
|
-
#
|
|
54
|
-
# @param rdoc_options [RDoc::Options] RDoc options object.
|
|
55
|
-
#
|
|
56
|
-
# @return [void]
|
|
57
|
-
def self.setup_options(rdoc_options)
|
|
58
|
-
rdoc_options.option_parser.on(
|
|
59
|
-
"--markdown-unknown-tags=MODE",
|
|
60
|
-
"How to handle unknown HTML tags: #{MARKDOWN_UNKNOWN_TAGS.join(", ")}."
|
|
61
|
-
) do |value|
|
|
62
|
-
rdoc_options.markdown_unknown_tags = value.to_sym
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
# Validates the configured reverse_markdown unknown-tag mode.
|
|
67
|
-
#
|
|
68
|
-
# @param value [Symbol] Unknown-tag mode.
|
|
69
|
-
#
|
|
70
|
-
# @return [Symbol] Validated unknown-tag mode.
|
|
71
|
-
def self.validate_markdown_unknown_tags(value)
|
|
72
|
-
return value if MARKDOWN_UNKNOWN_TAGS.include?(value)
|
|
73
|
-
|
|
74
|
-
expected = MARKDOWN_UNKNOWN_TAGS.map { |mode| ":#{mode}" }.join(", ")
|
|
75
|
-
raise OptionParser::InvalidArgument,
|
|
76
|
-
"invalid markdown_unknown_tags: #{value.inspect} (expected one of: #{expected})"
|
|
77
|
-
end
|
|
28
|
+
# Prepared objects and lookup tables used during one generation run.
|
|
29
|
+
GenerationState = Data.define(
|
|
30
|
+
:output_dir,
|
|
31
|
+
:classes,
|
|
32
|
+
:pages,
|
|
33
|
+
:class_output_paths,
|
|
34
|
+
:markdown_output_object_ids,
|
|
35
|
+
:known_output_paths,
|
|
36
|
+
:root_path_segment
|
|
37
|
+
)
|
|
78
38
|
|
|
79
39
|
# Source store for generated content.
|
|
80
40
|
#
|
|
@@ -84,12 +44,16 @@ class RDoc::Generator::Markdown
|
|
|
84
44
|
# Classes and modules selected for output.
|
|
85
45
|
#
|
|
86
46
|
# @return [Array<RDoc::Context>, nil]
|
|
87
|
-
|
|
47
|
+
def classes
|
|
48
|
+
generation_state&.classes
|
|
49
|
+
end
|
|
88
50
|
|
|
89
51
|
# Text files selected for output.
|
|
90
52
|
#
|
|
91
53
|
# @return [Array<RDoc::TopLevel>, nil]
|
|
92
|
-
|
|
54
|
+
def pages
|
|
55
|
+
generation_state&.pages
|
|
56
|
+
end
|
|
93
57
|
|
|
94
58
|
# Creates a generator for an RDoc store and options.
|
|
95
59
|
#
|
|
@@ -99,7 +63,6 @@ class RDoc::Generator::Markdown
|
|
|
99
63
|
@store = store
|
|
100
64
|
@options = rdoc_options
|
|
101
65
|
@source_dir = File.expand_path(rdoc_options.root.to_s)
|
|
102
|
-
@markdown_unknown_tags = self.class.validate_markdown_unknown_tags(rdoc_options.markdown_unknown_tags)
|
|
103
66
|
end
|
|
104
67
|
|
|
105
68
|
# Writes class files, page files, and the search index.
|
|
@@ -107,25 +70,44 @@ class RDoc::Generator::Markdown
|
|
|
107
70
|
# @return [void]
|
|
108
71
|
def generate
|
|
109
72
|
debug("Setting things up ")
|
|
110
|
-
|
|
111
73
|
setup
|
|
112
74
|
|
|
113
|
-
debug("Generate documentation in #{
|
|
114
|
-
|
|
75
|
+
debug("Generate documentation in #{output_dir}")
|
|
115
76
|
emit_classfiles
|
|
116
77
|
|
|
117
|
-
debug("Generate pages in #{
|
|
118
|
-
|
|
78
|
+
debug("Generate pages in #{output_dir}")
|
|
119
79
|
emit_pagefiles
|
|
120
80
|
|
|
121
|
-
debug("Generate index file in #{
|
|
122
|
-
|
|
81
|
+
debug("Generate index file in #{output_dir}")
|
|
123
82
|
emit_csv_index
|
|
124
83
|
end
|
|
125
84
|
|
|
126
85
|
private
|
|
127
86
|
|
|
128
|
-
attr_reader :options, :
|
|
87
|
+
attr_reader :generation_state, :options, :source_dir
|
|
88
|
+
|
|
89
|
+
# Builds an HTML anchor tag.
|
|
90
|
+
#
|
|
91
|
+
# @param id [String] Fragment identifier for the generated anchor.
|
|
92
|
+
#
|
|
93
|
+
# @return [String] HTML anchor tag.
|
|
94
|
+
def anchor(id)
|
|
95
|
+
%(<a id="#{id}"></a>)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Applies final whitespace and link normalization before writing Markdown.
|
|
99
|
+
#
|
|
100
|
+
# @param content [String] Markdown content.
|
|
101
|
+
# @param current_output_path [String] Output path for the file being written.
|
|
102
|
+
#
|
|
103
|
+
# @return [String] Final Markdown ending with one newline.
|
|
104
|
+
def finalize_markdown(content, current_output_path:)
|
|
105
|
+
normalized = normalize_internal_links(
|
|
106
|
+
content.lines.map(&:rstrip).join("\n"),
|
|
107
|
+
current_output_path: current_output_path
|
|
108
|
+
).sub(/\n{3,}/, "\n\n").gsub(/^(#+ .+)\n\n/, "\\1\n")
|
|
109
|
+
"#{normalized}\n"
|
|
110
|
+
end
|
|
129
111
|
|
|
130
112
|
# Prints a message when RDoc debug output is enabled.
|
|
131
113
|
#
|
|
@@ -141,65 +123,6 @@ class RDoc::Generator::Markdown
|
|
|
141
123
|
puts "[rdoc-markdown] #{str}"
|
|
142
124
|
end
|
|
143
125
|
|
|
144
|
-
# Writes a CSV search index for generated documentation.
|
|
145
|
-
#
|
|
146
|
-
# @return [void]
|
|
147
|
-
def emit_csv_index
|
|
148
|
-
filepath = "#{output_dir}/index.csv"
|
|
149
|
-
|
|
150
|
-
CSV.open(filepath, "wb") do |csv|
|
|
151
|
-
csv << %w[name type path]
|
|
152
|
-
|
|
153
|
-
@classes.each do |klass|
|
|
154
|
-
csv << [
|
|
155
|
-
klass.full_name,
|
|
156
|
-
klass.type.capitalize,
|
|
157
|
-
output_path_for(klass)
|
|
158
|
-
]
|
|
159
|
-
|
|
160
|
-
klass.method_list.select(&:display?).each do |method|
|
|
161
|
-
csv << [
|
|
162
|
-
"#{klass.full_name}.#{method.name}",
|
|
163
|
-
"Method",
|
|
164
|
-
"#{output_path_for(klass)}##{method.aref}"
|
|
165
|
-
]
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
klass
|
|
169
|
-
.constants
|
|
170
|
-
.select(&:display?)
|
|
171
|
-
.sort
|
|
172
|
-
.each do |const|
|
|
173
|
-
csv << [
|
|
174
|
-
"#{klass.full_name}.#{const.name}",
|
|
175
|
-
"Constant",
|
|
176
|
-
"#{output_path_for(klass)}##{const.name}"
|
|
177
|
-
]
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
klass
|
|
181
|
-
.attributes
|
|
182
|
-
.select(&:display?)
|
|
183
|
-
.sort
|
|
184
|
-
.each do |attr|
|
|
185
|
-
csv << [
|
|
186
|
-
"#{klass.full_name}.#{attr.name}",
|
|
187
|
-
"Attribute",
|
|
188
|
-
"#{output_path_for(klass)}##{attr.aref}"
|
|
189
|
-
]
|
|
190
|
-
end
|
|
191
|
-
end
|
|
192
|
-
|
|
193
|
-
@pages.each do |page|
|
|
194
|
-
csv << [
|
|
195
|
-
page.page_name,
|
|
196
|
-
"File",
|
|
197
|
-
page_output_path(page)
|
|
198
|
-
]
|
|
199
|
-
end
|
|
200
|
-
end
|
|
201
|
-
end
|
|
202
|
-
|
|
203
126
|
# Writes one Markdown file per selected class or module.
|
|
204
127
|
#
|
|
205
128
|
# @return [void]
|
|
@@ -207,7 +130,7 @@ class RDoc::Generator::Markdown
|
|
|
207
130
|
template_content = File.read(File.join(TEMPLATE_DIR, "classfile.md.erb"))
|
|
208
131
|
template = ERB.new(template_content, trim_mode: "-")
|
|
209
132
|
|
|
210
|
-
|
|
133
|
+
classes.each do |klass|
|
|
211
134
|
content = template.result(binding)
|
|
212
135
|
output_path = output_path_for(klass)
|
|
213
136
|
out_file = Pathname.new("#{output_dir}/#{output_path}")
|
|
@@ -220,569 +143,46 @@ class RDoc::Generator::Markdown
|
|
|
220
143
|
#
|
|
221
144
|
# @return [void]
|
|
222
145
|
def emit_pagefiles
|
|
223
|
-
|
|
224
|
-
|
|
146
|
+
pages.each do |page|
|
|
147
|
+
output_path = page_output_path(page)
|
|
148
|
+
out_file = Pathname.new("#{output_dir}/#{output_path}")
|
|
225
149
|
out_file.dirname.mkpath
|
|
226
150
|
|
|
227
|
-
next FileUtils.cp(File.expand_path(page.absolute_name,
|
|
228
|
-
|
|
229
|
-
content = markdownify(render_description(page))
|
|
230
|
-
File.write(out_file, finalize_markdown(
|
|
231
|
-
content,
|
|
232
|
-
current_output_path: page_output_path(page)
|
|
233
|
-
))
|
|
234
|
-
end
|
|
235
|
-
end
|
|
236
|
-
|
|
237
|
-
# Converts a qualified object name into a Markdown path.
|
|
238
|
-
#
|
|
239
|
-
# @param class_name [String] Qualified class or module name.
|
|
240
|
-
#
|
|
241
|
-
# @return [String] Relative Markdown path.
|
|
242
|
-
def turn_to_path(class_name)
|
|
243
|
-
"#{class_name.gsub("::", "/")}.md"
|
|
244
|
-
end
|
|
245
|
-
|
|
246
|
-
# Builds the Markdown output path for an RDoc page.
|
|
247
|
-
#
|
|
248
|
-
# @param page [RDoc::TopLevel] Page object to render.
|
|
249
|
-
#
|
|
250
|
-
# @return [String] Relative Markdown path.
|
|
251
|
-
def page_output_path(page)
|
|
252
|
-
source_path = normalize_input_path_for_output(page.relative_name)
|
|
253
|
-
return source_path if page.relative_name.match?(/\.(?:md|markdown)\z/i)
|
|
254
|
-
|
|
255
|
-
dirname = File.dirname(source_path)
|
|
256
|
-
basename = "#{File.basename(source_path).tr(".", "_")}.md"
|
|
257
|
-
|
|
258
|
-
return basename if dirname == "."
|
|
259
|
-
|
|
260
|
-
"#{dirname}/#{basename}"
|
|
261
|
-
end
|
|
262
|
-
|
|
263
|
-
# Returns the canonical Markdown path for a class or module.
|
|
264
|
-
#
|
|
265
|
-
# @param code_object [RDoc::Context] Class or module object.
|
|
266
|
-
#
|
|
267
|
-
# @return [String] Relative Markdown path.
|
|
268
|
-
def output_path_for(code_object)
|
|
269
|
-
turn_to_path(code_object.full_name)
|
|
270
|
-
end
|
|
271
|
-
|
|
272
|
-
# Renders a class or module reference, linking it when its documentation is emitted.
|
|
273
|
-
#
|
|
274
|
-
# @param target [RDoc::ClassModule, String] Resolved RDoc object or unresolved name.
|
|
275
|
-
# @param label [String] Visible reference text.
|
|
276
|
-
#
|
|
277
|
-
# @return [String] Markdown text or link.
|
|
278
|
-
def metadata_reference(target, label)
|
|
279
|
-
output_path = @class_output_paths[target.full_name] if target.respond_to?(:full_name)
|
|
280
|
-
cell = metadata_table_cell(label)
|
|
281
|
-
return cell unless output_path
|
|
282
|
-
|
|
283
|
-
"[#{cell}](#{output_path})"
|
|
284
|
-
end
|
|
285
|
-
|
|
286
|
-
# Escapes text for a Markdown table cell.
|
|
287
|
-
#
|
|
288
|
-
# @param value [String] Metadata text.
|
|
289
|
-
#
|
|
290
|
-
# @return [String] GFM table-safe Markdown text.
|
|
291
|
-
def metadata_table_cell(value)
|
|
292
|
-
value.gsub(/[[:blank:]]*\R[[:blank:]]*/, " ")
|
|
293
|
-
.gsub(/[\\|]/) { |character| "\\#{character}" }
|
|
294
|
-
end
|
|
295
|
-
|
|
296
|
-
# Converts RDoc HTML into GitHub-flavored Markdown.
|
|
297
|
-
#
|
|
298
|
-
# @param input [String] RDoc HTML fragment.
|
|
299
|
-
#
|
|
300
|
-
# @return [String] Markdown with normalized links and no trailing whitespace.
|
|
301
|
-
def markdownify(input)
|
|
302
|
-
# ReverseMarkdown supports these unknown-tag modes:
|
|
303
|
-
# - pass_through - (default) Include the unknown tag completely into the result
|
|
304
|
-
# - drop - Drop the unknown tag and its content
|
|
305
|
-
# - bypass - Ignore the unknown tag but try to convert its content
|
|
306
|
-
# - raise - Raise an error to let you know
|
|
307
|
-
|
|
308
|
-
fragment = Nokogiri::HTML.fragment(input)
|
|
309
|
-
fragment.css("span.legacy-anchor[id]").select do |span|
|
|
310
|
-
span.next_element&.name == "h1" && span["id"].match?(/\A(?:class|module)-/)
|
|
311
|
-
end.each(&:remove)
|
|
312
|
-
fragment.css("span.legacy-anchor[id]").each do |span|
|
|
313
|
-
heading = span.next_element
|
|
314
|
-
heading.add_child(span) if heading&.name&.match?(/\Ah[1-6]\z/)
|
|
315
|
-
end
|
|
316
|
-
|
|
317
|
-
fragment.css("pre").each do |pre|
|
|
318
|
-
language = pre["class"].to_s[/\A(?!highlight\z)[A-Za-z][A-Za-z0-9_+-]*\z/]
|
|
319
|
-
pre["class"] = "brush: #{language};" if language
|
|
320
|
-
pre.inner_html = pre.text
|
|
321
|
-
end
|
|
322
|
-
|
|
323
|
-
fragment.css("h1, h2, h3, h4, h5, h6").each do |heading|
|
|
324
|
-
link = heading.xpath("./a[starts-with(@href, '#') and string-length(@href) > 1]").find do |anchor|
|
|
325
|
-
anchor.text.match?(/\S/) &&
|
|
326
|
-
anchor.xpath("preceding-sibling::node()").none? { |sibling| sibling.text.match?(/\S/) }
|
|
327
|
-
end
|
|
328
|
-
next unless link
|
|
329
|
-
|
|
330
|
-
id = link["href"].delete_prefix("#")
|
|
331
|
-
link.replace(link.children)
|
|
332
|
-
next if id == RDoc::Text.to_anchor(heading.text)
|
|
333
|
-
|
|
334
|
-
heading.add_child(fragment.document.create_element("span", "class" => "legacy-anchor", "id" => id))
|
|
335
|
-
end
|
|
336
|
-
|
|
337
|
-
anchor_aliases = fragment.css("span.legacy-anchor[id]").map.with_index do |span, index|
|
|
338
|
-
token = "RDocMarkdownAnchor#{index}End"
|
|
339
|
-
id = span["id"]
|
|
340
|
-
span.replace(token)
|
|
341
|
-
[token, id]
|
|
342
|
-
end
|
|
343
|
-
|
|
344
|
-
fragment.css("a").each do |link|
|
|
345
|
-
receiver = link.text
|
|
346
|
-
href = link["href"].to_s
|
|
347
|
-
|
|
348
|
-
if receiver.match?(/\A(?:[A-Z][A-Za-z0-9_]*(?:::[A-Z][A-Za-z0-9_]*)*|[a-z_][A-Za-z0-9_]*)\z/) &&
|
|
349
|
-
href.match?(/\A(?::.+|".+")\z/)
|
|
350
|
-
link.replace(fragment.document.create_element("code") { |code| code.content = "#{receiver}[#{href}]" })
|
|
351
|
-
elsif href.start_with?("www.")
|
|
352
|
-
link["href"] = "https://#{href}"
|
|
353
|
-
elsif !href.match?(/\A(?:https?:\/\/|mailto:|#)/i)
|
|
354
|
-
href = href.sub(/\.html(?=[?#]|\z)/i, ".md")
|
|
355
|
-
href = href.sub(%r{\A/(?=.+\.md(?:[?#]|\z))}, "")
|
|
356
|
-
href = href.sub(%r{\A((?:\.\./)*)(?:files|classes|modules)/(?=.+\.md(?:[?#]|\z))}, '\1')
|
|
357
|
-
link["href"] = href
|
|
358
|
-
end
|
|
359
|
-
end
|
|
360
|
-
|
|
361
|
-
md = ReverseMarkdown.convert(
|
|
362
|
-
fragment,
|
|
363
|
-
github_flavored: true,
|
|
364
|
-
unknown_tags: @markdown_unknown_tags
|
|
365
|
-
).dup
|
|
366
|
-
anchor_aliases.each do |token, id|
|
|
367
|
-
md.gsub!(token, %(<a id="#{id}"></a>))
|
|
368
|
-
end
|
|
369
|
-
|
|
370
|
-
normalize_definition_list_code_blocks(md).rstrip
|
|
371
|
-
end
|
|
372
|
-
|
|
373
|
-
# Short alias used by ERB templates.
|
|
374
|
-
alias_method :h, :markdownify
|
|
375
|
-
|
|
376
|
-
# Builds an HTML anchor tag.
|
|
377
|
-
#
|
|
378
|
-
# @param id [String] Fragment identifier for the generated anchor.
|
|
379
|
-
#
|
|
380
|
-
# @return [String] HTML anchor tag.
|
|
381
|
-
def anchor(id)
|
|
382
|
-
%(<a id="#{id}"></a>)
|
|
383
|
-
end
|
|
384
|
-
|
|
385
|
-
# Renders an RDoc description with links limited to emitted objects.
|
|
386
|
-
#
|
|
387
|
-
# @param code_object [RDoc::CodeObject, RDoc::Context::Section] Object whose description is rendered.
|
|
388
|
-
#
|
|
389
|
-
# @return [String] HTML description.
|
|
390
|
-
def render_description(code_object)
|
|
391
|
-
section = RDoc::Context::Section === code_object
|
|
392
|
-
formatter = if section
|
|
393
|
-
code_object.parent.formatter.dup.tap { |copy| copy.code_object = code_object }
|
|
394
|
-
else
|
|
395
|
-
code_object.formatter
|
|
396
|
-
end
|
|
397
|
-
formatter.extend(CrossrefExtension)
|
|
398
|
-
begin
|
|
399
|
-
formatter.markdown_cross_reference = RDoc::CrossReference.new(formatter.context)
|
|
400
|
-
formatter.markdown_output_object_ids = @markdown_output_object_ids
|
|
401
|
-
if section
|
|
402
|
-
locale = options.locale
|
|
403
|
-
documents = code_object.comments.map do |comment|
|
|
404
|
-
next comment.parse unless locale && !comment.text.empty?
|
|
405
|
-
|
|
406
|
-
comment = comment.dup
|
|
407
|
-
comment.text = RDoc::I18n::Text.new(comment).translate(locale)
|
|
408
|
-
comment.parse
|
|
409
|
-
end
|
|
410
|
-
RDoc::Markup::Document.new(*documents).accept(formatter)
|
|
411
|
-
else
|
|
412
|
-
code_object.description
|
|
413
|
-
end
|
|
414
|
-
ensure
|
|
415
|
-
formatter.markdown_cross_reference = nil
|
|
416
|
-
end
|
|
417
|
-
end
|
|
418
|
-
|
|
419
|
-
# Renders an RDoc object's description as Markdown.
|
|
420
|
-
#
|
|
421
|
-
# @param code_object [RDoc::CodeObject] Object with an RDoc description.
|
|
422
|
-
# @param fallback [String, nil] Text to use when the description is empty.
|
|
423
|
-
# @param heading_level_offset [Integer] Heading levels to add while rendering.
|
|
424
|
-
#
|
|
425
|
-
# @return [String] Rendered description or fallback text.
|
|
426
|
-
def describe(code_object, fallback: nil, heading_level_offset: 0)
|
|
427
|
-
description = render_description(code_object)
|
|
428
|
-
return fallback.to_s if description.empty?
|
|
151
|
+
next FileUtils.cp(File.expand_path(page.absolute_name, source_dir), out_file) if page.relative_name.match?(/\.(?:md|markdown)\z/i)
|
|
429
152
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
# Renders a section description as Markdown.
|
|
434
|
-
#
|
|
435
|
-
# @param section [RDoc::Context::Section] RDoc section whose description appears before grouped members.
|
|
436
|
-
# @param heading_level_offset [Integer] Heading levels to add while rendering.
|
|
437
|
-
#
|
|
438
|
-
# @return [String] Rendered section description.
|
|
439
|
-
def section_description(section, heading_level_offset:)
|
|
440
|
-
shift_headings(markdownify(render_description(section)), heading_level_offset)
|
|
441
|
-
end
|
|
442
|
-
|
|
443
|
-
# Builds the visible method signature used in headings.
|
|
444
|
-
#
|
|
445
|
-
# @param method [RDoc::AnyMethod] Method object to render.
|
|
446
|
-
#
|
|
447
|
-
# @return [String] Normalized method signature.
|
|
448
|
-
def method_signature(method)
|
|
449
|
-
signatures = method.type_signature_lines || @store.rbs_signature_for(method) || [method.param_seq]
|
|
450
|
-
|
|
451
|
-
signatures = signatures.filter_map do |signature|
|
|
452
|
-
next unless signature&.match?(/\S/)
|
|
453
|
-
|
|
454
|
-
signature = signature.gsub("->", " -> ")
|
|
455
|
-
signature = signature.gsub(/\s+/, " ").strip
|
|
456
|
-
signature = " #{signature}" if signature.start_with?("->")
|
|
457
|
-
merge_method_signature_arguments(signature, method.params)
|
|
458
|
-
end
|
|
459
|
-
|
|
460
|
-
return "()" if signatures.empty?
|
|
461
|
-
|
|
462
|
-
signatures.join(" | ")
|
|
463
|
-
end
|
|
464
|
-
|
|
465
|
-
# Merges RDoc parameter names into a type-only signature.
|
|
466
|
-
#
|
|
467
|
-
# @param signature [String] Method signature from RDoc call sequence.
|
|
468
|
-
# @param raw_params [String, nil] Method parameter list from RDoc.
|
|
469
|
-
#
|
|
470
|
-
# @return [String] Signature with names added when safe.
|
|
471
|
-
def merge_method_signature_arguments(signature, raw_params)
|
|
472
|
-
params = normalized_method_params(raw_params)
|
|
473
|
-
|
|
474
|
-
signature_args, signature_suffix = split_signature_arguments_and_suffix(signature)
|
|
475
|
-
return signature if signature_args.nil?
|
|
476
|
-
|
|
477
|
-
param_parts = split_signature_list(params)
|
|
478
|
-
signature_parts = split_signature_list(signature_args)
|
|
479
|
-
return signature unless param_parts.length.eql?(signature_parts.length)
|
|
480
|
-
|
|
481
|
-
param_names = param_parts.map { |part| extract_parameter_name(part) }
|
|
482
|
-
return signature if param_names.any?(&:nil?)
|
|
483
|
-
return signature if signature_parts.zip(param_names).all? { |part, name| signature_part_mentions_name?(part, name) }
|
|
484
|
-
|
|
485
|
-
merged_args = param_parts.zip(signature_parts).map do |param, type|
|
|
486
|
-
separator = param.end_with?(":") ? " " : ": "
|
|
487
|
-
"#{param}#{separator}#{type}"
|
|
488
|
-
end
|
|
489
|
-
|
|
490
|
-
"(#{merged_args.join(", ")})#{signature_suffix}"
|
|
491
|
-
end
|
|
492
|
-
|
|
493
|
-
# Normalizes RDoc's raw parameter string.
|
|
494
|
-
#
|
|
495
|
-
# @param raw_params [String, nil] Parameter list from RDoc.
|
|
496
|
-
#
|
|
497
|
-
# @return [String] Parameter list without outer parentheses.
|
|
498
|
-
def normalized_method_params(raw_params)
|
|
499
|
-
params = raw_params.to_s.strip
|
|
500
|
-
params = params[1...-1] if params.start_with?("(") && params.end_with?(")")
|
|
501
|
-
|
|
502
|
-
params
|
|
503
|
-
end
|
|
504
|
-
|
|
505
|
-
# Splits a parenthesized signature into arguments and suffix.
|
|
506
|
-
#
|
|
507
|
-
# @param signature [String] Method signature.
|
|
508
|
-
#
|
|
509
|
-
# @return [Array<String>, nil] Argument text and suffix, or nil when not parenthesized.
|
|
510
|
-
def split_signature_arguments_and_suffix(signature)
|
|
511
|
-
return unless signature.start_with?("(")
|
|
512
|
-
|
|
513
|
-
depth = 0
|
|
514
|
-
|
|
515
|
-
signature.each_char.with_index do |char, index|
|
|
516
|
-
depth += 1 if char == "("
|
|
517
|
-
|
|
518
|
-
next unless char == ")"
|
|
519
|
-
|
|
520
|
-
depth -= 1
|
|
521
|
-
return [signature[1...index], signature[(index + 1)..]] if depth.zero?
|
|
522
|
-
end
|
|
523
|
-
end
|
|
524
|
-
|
|
525
|
-
# Splits a comma-separated signature list while preserving nested groups.
|
|
526
|
-
#
|
|
527
|
-
# @param list [String] Signature argument list.
|
|
528
|
-
#
|
|
529
|
-
# @return [Array<String>] Signature parts.
|
|
530
|
-
def split_signature_list(list)
|
|
531
|
-
parts = []
|
|
532
|
-
current = +""
|
|
533
|
-
paren_depth = 0
|
|
534
|
-
bracket_depth = 0
|
|
535
|
-
brace_depth = 0
|
|
536
|
-
|
|
537
|
-
list.each_char do |char|
|
|
538
|
-
case char
|
|
539
|
-
when "("
|
|
540
|
-
paren_depth += 1
|
|
541
|
-
when ")"
|
|
542
|
-
paren_depth -= 1
|
|
543
|
-
when "["
|
|
544
|
-
bracket_depth += 1
|
|
545
|
-
when "]"
|
|
546
|
-
bracket_depth -= 1
|
|
547
|
-
when "{"
|
|
548
|
-
brace_depth += 1
|
|
549
|
-
when "}"
|
|
550
|
-
brace_depth -= 1
|
|
551
|
-
when ","
|
|
552
|
-
if paren_depth.zero? && bracket_depth.zero? && brace_depth.zero?
|
|
553
|
-
parts << current.strip
|
|
554
|
-
current = +""
|
|
555
|
-
next
|
|
556
|
-
end
|
|
557
|
-
end
|
|
558
|
-
|
|
559
|
-
current << char
|
|
560
|
-
end
|
|
561
|
-
|
|
562
|
-
parts << current.strip unless current.empty?
|
|
563
|
-
parts
|
|
564
|
-
end
|
|
565
|
-
|
|
566
|
-
# Extracts a bare Ruby parameter name from a parameter fragment.
|
|
567
|
-
#
|
|
568
|
-
# @param parameter [String] Parameter fragment.
|
|
569
|
-
#
|
|
570
|
-
# @return [String, nil] Parameter name, or nil when invalid.
|
|
571
|
-
def extract_parameter_name(parameter)
|
|
572
|
-
match = parameter.match(/\A(?:\*\*|\*|&)?([a-z_]\w*):?\z/)
|
|
573
|
-
match && match[1]
|
|
574
|
-
end
|
|
575
|
-
|
|
576
|
-
# Checks whether a signature fragment already includes a parameter name.
|
|
577
|
-
#
|
|
578
|
-
# @param text [String] Signature fragment.
|
|
579
|
-
# @param name [String] Parameter name.
|
|
580
|
-
#
|
|
581
|
-
# @return [Boolean] True when the name appears as a standalone word.
|
|
582
|
-
def signature_part_mentions_name?(text, name)
|
|
583
|
-
text.match?(/(?<!\w)#{name}(?!\w)/)
|
|
584
|
-
end
|
|
585
|
-
|
|
586
|
-
# Renders a method description or an alias fallback.
|
|
587
|
-
#
|
|
588
|
-
# @param method [RDoc::AnyMethod] Method object to render.
|
|
589
|
-
# @param current_class [RDoc::Context] Class or module currently being rendered.
|
|
590
|
-
#
|
|
591
|
-
# @return [String] Rendered method description.
|
|
592
|
-
def method_description(method, current_class:)
|
|
593
|
-
text = describe(method, heading_level_offset: 4)
|
|
594
|
-
return text unless text.empty?
|
|
595
|
-
|
|
596
|
-
aliased_method = method.is_alias_for
|
|
597
|
-
return "Not documented." unless aliased_method
|
|
598
|
-
|
|
599
|
-
link = method_link(aliased_method, current_class: current_class)
|
|
600
|
-
return "Alias for: `#{aliased_method.name}`" unless link
|
|
601
|
-
|
|
602
|
-
"Alias for: [`#{aliased_method.name}`](#{link})"
|
|
603
|
-
end
|
|
604
|
-
|
|
605
|
-
# Applies final whitespace and link normalization before writing Markdown.
|
|
606
|
-
#
|
|
607
|
-
# @param content [String] Markdown content.
|
|
608
|
-
# @param current_output_path [String] Output path for the file being written.
|
|
609
|
-
#
|
|
610
|
-
# @return [String] Final Markdown ending with one newline.
|
|
611
|
-
def finalize_markdown(content, current_output_path:)
|
|
612
|
-
output = content.lines.map(&:rstrip).join("\n")
|
|
613
|
-
output = normalize_internal_links(output, current_output_path: current_output_path)
|
|
614
|
-
output = output.sub(/\n{3,}/, "\n\n")
|
|
615
|
-
output = output.gsub(/^(#+ .+)\n\n/, "\\1\n")
|
|
616
|
-
"#{output}\n"
|
|
617
|
-
end
|
|
618
|
-
|
|
619
|
-
# Increases Markdown heading levels without exceeding level six.
|
|
620
|
-
#
|
|
621
|
-
# @param markdown [String] Markdown content.
|
|
622
|
-
# @param heading_level_offset [Integer] Heading levels to add.
|
|
623
|
-
#
|
|
624
|
-
# @return [String] Markdown with shifted headings.
|
|
625
|
-
def shift_headings(markdown, heading_level_offset)
|
|
626
|
-
markdown.gsub(/^(#+)(\s)/) do
|
|
627
|
-
hashes = Regexp.last_match(1)
|
|
628
|
-
spaces = Regexp.last_match(2)
|
|
629
|
-
level = [hashes.length + heading_level_offset, 6].min
|
|
630
|
-
"#{"#" * level}#{spaces}"
|
|
631
|
-
end
|
|
632
|
-
end
|
|
633
|
-
|
|
634
|
-
# Converts RDoc definition-list code blocks into Markdown lists.
|
|
635
|
-
#
|
|
636
|
-
# @param markdown [String] Markdown content.
|
|
637
|
-
#
|
|
638
|
-
# @return [String] Markdown with convertible blocks normalized.
|
|
639
|
-
def normalize_definition_list_code_blocks(markdown)
|
|
640
|
-
markdown.gsub(/```[^\n]*\n(.+?)\n```/m) do
|
|
641
|
-
body = Regexp.last_match(1)
|
|
642
|
-
converted = convert_definition_list_block(body)
|
|
643
|
-
converted.nil? ? Regexp.last_match : converted
|
|
644
|
-
end
|
|
645
|
-
end
|
|
646
|
-
|
|
647
|
-
# Converts a single definition-list code block.
|
|
648
|
-
#
|
|
649
|
-
# @param body [String] Code block body.
|
|
650
|
-
#
|
|
651
|
-
# @return [String, nil] Converted Markdown, or nil when the block is not a definition list.
|
|
652
|
-
def convert_definition_list_block(body)
|
|
653
|
-
lines = body.lines
|
|
654
|
-
return nil unless lines.all? { |line| definition_list_line?(line) }
|
|
655
|
-
|
|
656
|
-
lines.map do |line|
|
|
657
|
-
stripped = line.strip
|
|
658
|
-
next if stripped.empty?
|
|
659
|
-
next "#{stripped.sub(/::\z/, "")}:" if stripped.end_with?("::")
|
|
660
|
-
|
|
661
|
-
"- #{stripped.sub(/\A\*\s/, "")}"
|
|
662
|
-
end.join("\n")
|
|
663
|
-
end
|
|
664
|
-
|
|
665
|
-
# Checks whether a line can appear in a converted definition list.
|
|
666
|
-
#
|
|
667
|
-
# @param line [String] Markdown line.
|
|
668
|
-
#
|
|
669
|
-
# @return [Boolean] True when the line matches RDoc definition-list output.
|
|
670
|
-
def definition_list_line?(line)
|
|
671
|
-
stripped = line.strip
|
|
672
|
-
stripped.empty? || stripped.end_with?("::") || stripped.match?(/\A\*\s/)
|
|
673
|
-
end
|
|
674
|
-
|
|
675
|
-
# Builds a Markdown link target for an aliased method.
|
|
676
|
-
#
|
|
677
|
-
# @param method [RDoc::AnyMethod] Target method.
|
|
678
|
-
# @param current_class [RDoc::Context] Class or module currently being rendered.
|
|
679
|
-
#
|
|
680
|
-
# @return [String, nil] Anchor or relative Markdown link target, or nil when the target page is omitted.
|
|
681
|
-
def method_link(method, current_class:)
|
|
682
|
-
return unless method.display?
|
|
683
|
-
|
|
684
|
-
target_parent = method.parent
|
|
685
|
-
target_path = @class_output_paths[target_parent.full_name]
|
|
686
|
-
return unless target_path
|
|
687
|
-
return "##{method.aref}" if target_parent == current_class
|
|
688
|
-
|
|
689
|
-
"#{target_path}##{method.aref}"
|
|
690
|
-
end
|
|
691
|
-
|
|
692
|
-
# Rewrites local Markdown links relative to the current output file.
|
|
693
|
-
#
|
|
694
|
-
# @param markdown [String] Markdown content.
|
|
695
|
-
# @param current_output_path [String] Output path for the file being written.
|
|
696
|
-
#
|
|
697
|
-
# @return [String] Markdown with normalized internal links.
|
|
698
|
-
def normalize_internal_links(markdown, current_output_path:)
|
|
699
|
-
current_dir = Pathname.new(current_output_path).dirname
|
|
700
|
-
|
|
701
|
-
markdown.gsub(%r{\]\(([^)]+)\)}) do
|
|
702
|
-
target = Regexp.last_match(1)
|
|
703
|
-
path = target.sub(/[?#].*\z/, "")
|
|
704
|
-
suffix = target[path.length..]
|
|
705
|
-
|
|
706
|
-
resolved = resolve_output_path(path, current_dir)
|
|
707
|
-
rewritten = resolved ? Pathname.new(resolved).relative_path_from(current_dir) : path
|
|
708
|
-
"](#{rewritten}#{suffix})"
|
|
709
|
-
end
|
|
710
|
-
end
|
|
711
|
-
|
|
712
|
-
# Resolves an internal link path against known generated outputs.
|
|
713
|
-
#
|
|
714
|
-
# @param path [String] Link path from Markdown content.
|
|
715
|
-
# @param current_dir [Pathname] Directory of the current output file.
|
|
716
|
-
#
|
|
717
|
-
# @return [String, nil] Resolved output path, or nil when unresolved.
|
|
718
|
-
def resolve_output_path(path, current_dir)
|
|
719
|
-
candidates = [path, path.delete_prefix("#{@root_path_segment}/")]
|
|
720
|
-
candidates += candidates.map { |candidate| candidate.sub(/_(md|markdown)\.md\z/i, '.\1') }
|
|
721
|
-
|
|
722
|
-
candidates.each do |candidate|
|
|
723
|
-
return candidate if @known_output_paths.include?(candidate)
|
|
724
|
-
end
|
|
725
|
-
|
|
726
|
-
candidates.each do |candidate|
|
|
727
|
-
expanded = current_dir.join(candidate).cleanpath.to_s
|
|
728
|
-
return expanded if @known_output_paths.include?(expanded)
|
|
153
|
+
content = Conversion.markdownify(render_description(page))
|
|
154
|
+
File.write(out_file, finalize_markdown(content, current_output_path: output_path))
|
|
729
155
|
end
|
|
730
|
-
|
|
731
|
-
nil
|
|
732
|
-
end
|
|
733
|
-
|
|
734
|
-
# Normalizes an input filename into an output-relative source path.
|
|
735
|
-
#
|
|
736
|
-
# @param path [String] RDoc input path.
|
|
737
|
-
#
|
|
738
|
-
# @return [String] Normalized path without root prefixes.
|
|
739
|
-
def normalize_input_path_for_output(path)
|
|
740
|
-
normalized = path.tr("\\", "/").sub(%r{\A\./}, "")
|
|
741
|
-
|
|
742
|
-
root = @source_dir
|
|
743
|
-
normalized = normalized.sub(%r{\A#{Regexp.escape(root)}/}, "")
|
|
744
|
-
normalized = normalized.sub(%r{\A/}, "")
|
|
745
|
-
|
|
746
|
-
root_basename = File.basename(root)
|
|
747
|
-
normalized.sub(%r{\A#{Regexp.escape(root_basename)}/}, "")
|
|
748
156
|
end
|
|
749
157
|
|
|
750
158
|
# Prepares sorted objects and link lookup state for generation.
|
|
751
159
|
#
|
|
752
160
|
# @return [void]
|
|
753
161
|
def setup
|
|
754
|
-
|
|
755
|
-
unless
|
|
162
|
+
output_dir = options.op_dir
|
|
163
|
+
unless output_dir.instance_of?(String)
|
|
756
164
|
raise TypeError, "RDoc markdown output directory must be a String"
|
|
757
165
|
end
|
|
758
166
|
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
klass.includes.any? ||
|
|
763
|
-
klass.method_list.any?(&:display?) ||
|
|
764
|
-
klass.constants.any?(&:display?) ||
|
|
765
|
-
klass.attributes.any?(&:display?) ||
|
|
766
|
-
klass.sections.any? { |section| section.title.to_s.match?(/\S/) || !section.to_document.empty? }
|
|
767
|
-
end.sort
|
|
768
|
-
@class_output_paths = @classes.to_h { |klass| [klass.full_name, output_path_for(klass)] }
|
|
769
|
-
@pages = @store.all_files.select(&:text?).select(&:display?)
|
|
770
|
-
.select { |page| page.relative_name.match?(/\.(?:md|markdown|rdoc)\z/i) }
|
|
771
|
-
.sort_by(&:base_name)
|
|
772
|
-
@markdown_output_object_ids = (@classes + @pages).map(&:object_id)
|
|
773
|
-
@known_output_paths = @class_output_paths.values
|
|
774
|
-
@pages.each { |page| @known_output_paths << page_output_path(page) }
|
|
167
|
+
classes = Selection.classes(store)
|
|
168
|
+
class_output_paths = classes.to_h { |klass| [klass.full_name, output_path_for(klass)] }
|
|
169
|
+
pages = Selection.pages(store)
|
|
775
170
|
|
|
776
|
-
@
|
|
171
|
+
@generation_state = GenerationState.new(
|
|
172
|
+
output_dir: output_dir,
|
|
173
|
+
classes: classes,
|
|
174
|
+
pages: pages,
|
|
175
|
+
class_output_paths: class_output_paths,
|
|
176
|
+
markdown_output_object_ids: (classes + pages).map(&:object_id),
|
|
177
|
+
known_output_paths: class_output_paths.values + pages.map { |page| page_output_path(page) },
|
|
178
|
+
root_path_segment: Pathname.new(options.root || ".").basename
|
|
179
|
+
)
|
|
777
180
|
end
|
|
778
|
-
end
|
|
779
|
-
|
|
780
|
-
# RDoc configuration extended with markdown generator options.
|
|
781
|
-
class RDoc::Options
|
|
782
|
-
prepend RDoc::Generator::Markdown::OptionsExtension
|
|
783
181
|
|
|
784
|
-
#
|
|
182
|
+
# Returns the prepared output directory.
|
|
785
183
|
#
|
|
786
|
-
# @return [
|
|
787
|
-
|
|
184
|
+
# @return [String] Output directory.
|
|
185
|
+
def output_dir
|
|
186
|
+
generation_state.output_dir
|
|
187
|
+
end
|
|
788
188
|
end
|