rdoc-markdown 0.5.0 → 0.7.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/.erb_lint.yml +40 -0
- data/.erb_linters/no_embedded_assets.rb +29 -0
- data/.erb_linters/non_raw_html.rb +29 -0
- data/.standard.yml +3 -0
- data/.yard-lint.yml +283 -0
- data/AGENTS.md +48 -0
- data/CHANGELOG.md +14 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +175 -39
- data/README.md +79 -1
- data/Rakefile +178 -0
- data/example/Bird.md +15 -9
- data/example/Duck.md +61 -49
- data/example/Object.md +9 -6
- data/example/Waterfowl.md +8 -5
- data/gemfiles/rdoc_head.gemfile +5 -0
- data/lib/markdown.rb +1 -0
- data/lib/rdoc/generator/markdown.rb +624 -94
- data/lib/rdoc/markdown/version.rb +4 -1
- data/lib/templates/classfile.md.erb +56 -35
- data/mutant.yml +15 -0
- data/rdoc-markdown.gemspec +6 -4
- metadata +49 -12
|
@@ -2,65 +2,63 @@
|
|
|
2
2
|
|
|
3
3
|
gem "rdoc"
|
|
4
4
|
|
|
5
|
-
require "pathname"
|
|
6
5
|
require "erb"
|
|
7
6
|
require "reverse_markdown"
|
|
8
|
-
require "unindent"
|
|
9
7
|
require "csv"
|
|
8
|
+
require "cgi"
|
|
10
9
|
|
|
10
|
+
# Generates Markdown output and a CSV search index from an RDoc store.
|
|
11
11
|
class RDoc::Generator::Markdown
|
|
12
12
|
RDoc::RDoc.add_generator self
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
# Defines a constant for directory where templates could be found
|
|
16
|
-
|
|
14
|
+
# Directory containing ERB templates.
|
|
17
15
|
TEMPLATE_DIR = File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "templates"))
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
#
|
|
21
|
-
|
|
17
|
+
# Source store for generated content.
|
|
18
|
+
#
|
|
19
|
+
# @return [RDoc::Store]
|
|
22
20
|
attr_reader :store
|
|
23
21
|
|
|
24
|
-
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
|
|
22
|
+
# Working directory captured when the generator is created.
|
|
23
|
+
#
|
|
24
|
+
# @return [Pathname]
|
|
28
25
|
attr_reader :base_dir
|
|
29
26
|
|
|
30
|
-
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
|
|
27
|
+
# Classes and modules selected for output.
|
|
28
|
+
#
|
|
29
|
+
# @return [Array<RDoc::Context>, nil]
|
|
34
30
|
attr_reader :classes
|
|
35
31
|
|
|
36
|
-
|
|
37
|
-
#
|
|
38
|
-
#
|
|
32
|
+
# Text files selected for output.
|
|
33
|
+
#
|
|
34
|
+
# @return [Array<RDoc::TopLevel>, nil]
|
|
35
|
+
attr_reader :pages
|
|
39
36
|
|
|
37
|
+
# Required by RDoc's generator interface; markdown output has no class subdirectory.
|
|
38
|
+
#
|
|
39
|
+
# @return [nil]
|
|
40
40
|
def class_dir
|
|
41
|
-
nil
|
|
42
41
|
end
|
|
43
42
|
|
|
44
43
|
# this alias is required for rdoc to work
|
|
45
44
|
alias_method :file_dir, :class_dir
|
|
46
45
|
|
|
47
|
-
|
|
48
|
-
#
|
|
49
|
-
|
|
50
|
-
|
|
46
|
+
# Creates a generator for an RDoc store and options.
|
|
47
|
+
#
|
|
48
|
+
# @param store [RDoc::Store] Source documentation store.
|
|
49
|
+
# @param rdoc_options [RDoc::Options] Generator options.
|
|
50
|
+
def initialize(store, rdoc_options)
|
|
51
51
|
@store = store
|
|
52
|
-
@options =
|
|
53
|
-
|
|
54
|
-
@base_dir = Pathname.pwd.expand_path
|
|
52
|
+
@options = rdoc_options
|
|
55
53
|
|
|
56
|
-
@
|
|
54
|
+
@base_dir = Pathname.pwd
|
|
57
55
|
end
|
|
58
56
|
|
|
59
|
-
|
|
60
|
-
#
|
|
61
|
-
|
|
57
|
+
# Writes class files, page files, and the search index.
|
|
58
|
+
#
|
|
59
|
+
# @return [void]
|
|
62
60
|
def generate
|
|
63
|
-
debug("Setting things up
|
|
61
|
+
debug("Setting things up ")
|
|
64
62
|
|
|
65
63
|
setup
|
|
66
64
|
|
|
@@ -68,6 +66,10 @@ class RDoc::Generator::Markdown
|
|
|
68
66
|
|
|
69
67
|
emit_classfiles
|
|
70
68
|
|
|
69
|
+
debug("Generate pages in #{@output_dir}")
|
|
70
|
+
|
|
71
|
+
emit_pagefiles
|
|
72
|
+
|
|
71
73
|
debug("Generate index file in #{@output_dir}")
|
|
72
74
|
|
|
73
75
|
emit_csv_index
|
|
@@ -75,136 +77,664 @@ class RDoc::Generator::Markdown
|
|
|
75
77
|
|
|
76
78
|
private
|
|
77
79
|
|
|
78
|
-
attr_reader :options
|
|
79
|
-
attr_reader :output_dir
|
|
80
|
-
|
|
81
|
-
##
|
|
82
|
-
# This method is used to output debugging information in case rdoc is run with --debug parameter
|
|
80
|
+
attr_reader :options, :output_dir
|
|
83
81
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
82
|
+
# Prints a message when RDoc debug output is enabled.
|
|
83
|
+
#
|
|
84
|
+
# @param str [String] Message to print.
|
|
85
|
+
#
|
|
86
|
+
# @return [void]
|
|
87
|
+
def debug(str)
|
|
88
|
+
# RDoc exposes --debug through this global and does not mirror it on options.
|
|
89
|
+
# standard:disable Style/GlobalVars
|
|
90
|
+
return unless $DEBUG_RDOC
|
|
91
|
+
# standard:enable Style/GlobalVars
|
|
92
|
+
|
|
93
|
+
puts "[rdoc-markdown] #{str}"
|
|
89
94
|
end
|
|
90
95
|
|
|
91
|
-
|
|
92
|
-
# This class emits a search index for generated documentation as sqlite database
|
|
96
|
+
# Writes a CSV search index for generated documentation.
|
|
93
97
|
#
|
|
94
|
-
|
|
95
|
-
def emit_csv_index
|
|
96
|
-
filepath = "#{output_dir}
|
|
98
|
+
# @return [void]
|
|
99
|
+
def emit_csv_index
|
|
100
|
+
filepath = "#{output_dir}/index.csv"
|
|
97
101
|
|
|
98
102
|
CSV.open(filepath, "wb") do |csv|
|
|
99
|
-
|
|
103
|
+
csv << %w[name type path]
|
|
100
104
|
|
|
101
|
-
@classes.
|
|
105
|
+
@classes.each do |klass|
|
|
102
106
|
csv << [
|
|
103
|
-
klass
|
|
107
|
+
display_name(klass),
|
|
104
108
|
klass.type.capitalize,
|
|
105
|
-
|
|
109
|
+
output_path_for(klass)
|
|
106
110
|
]
|
|
107
111
|
|
|
108
|
-
klass.method_list.each do |method|
|
|
109
|
-
next if method.visibility.to_s.eql?("private")
|
|
110
|
-
next if method.visibility.to_s.eql?("protected")
|
|
111
|
-
|
|
112
|
+
klass.method_list.select(&:display?).each do |method|
|
|
112
113
|
csv << [
|
|
113
|
-
"#{klass
|
|
114
|
+
"#{display_name(klass)}.#{method.name}",
|
|
114
115
|
"Method",
|
|
115
|
-
"#{
|
|
116
|
+
"#{output_path_for(klass)}##{method.aref}"
|
|
116
117
|
]
|
|
117
118
|
end
|
|
118
119
|
|
|
119
120
|
klass
|
|
120
121
|
.constants
|
|
121
|
-
.
|
|
122
|
+
.select(&:display?)
|
|
123
|
+
.sort
|
|
122
124
|
.each do |const|
|
|
123
125
|
csv << [
|
|
124
|
-
"#{klass
|
|
126
|
+
"#{display_name(klass)}.#{const.name}",
|
|
125
127
|
"Constant",
|
|
126
|
-
"#{
|
|
128
|
+
"#{output_path_for(klass)}##{const.name}"
|
|
127
129
|
]
|
|
128
130
|
end
|
|
129
131
|
|
|
130
132
|
klass
|
|
131
133
|
.attributes
|
|
132
|
-
.
|
|
134
|
+
.select(&:display?)
|
|
135
|
+
.sort
|
|
133
136
|
.each do |attr|
|
|
134
137
|
csv << [
|
|
135
|
-
"#{klass
|
|
138
|
+
"#{display_name(klass)}.#{attr.name}",
|
|
136
139
|
"Attribute",
|
|
137
|
-
"#{
|
|
140
|
+
"#{output_path_for(klass)}##{attr.aref}"
|
|
138
141
|
]
|
|
139
142
|
end
|
|
140
143
|
end
|
|
144
|
+
|
|
145
|
+
@pages.each do |page|
|
|
146
|
+
csv << [
|
|
147
|
+
page.page_name,
|
|
148
|
+
"Page",
|
|
149
|
+
page_output_path(page)
|
|
150
|
+
]
|
|
151
|
+
end
|
|
141
152
|
end
|
|
142
153
|
end
|
|
143
154
|
|
|
155
|
+
# Writes one Markdown file per selected class or module.
|
|
156
|
+
#
|
|
157
|
+
# @return [void]
|
|
144
158
|
def emit_classfiles
|
|
145
|
-
|
|
146
|
-
|
|
159
|
+
template_content = File.read(File.join(TEMPLATE_DIR, "classfile.md.erb"))
|
|
160
|
+
template = ERB.new(template_content, trim_mode: "-")
|
|
147
161
|
|
|
148
|
-
|
|
162
|
+
@classes.each do |klass|
|
|
163
|
+
result = finalize_markdown(template.result(binding), current_output_path: output_path_for(klass))
|
|
149
164
|
|
|
150
|
-
out_file = Pathname.new("#{output_dir}/#{
|
|
165
|
+
out_file = Pathname.new("#{output_dir}/#{output_path_for(klass)}")
|
|
151
166
|
out_file.dirname.mkpath
|
|
152
|
-
|
|
153
|
-
result = template.result(binding).squeeze(" ")
|
|
154
|
-
|
|
155
167
|
File.write(out_file, result)
|
|
168
|
+
|
|
169
|
+
legacy_paths_for(klass).each do |legacy_path|
|
|
170
|
+
legacy_file = Pathname.new("#{output_dir}/#{legacy_path}")
|
|
171
|
+
legacy_file.dirname.mkpath
|
|
172
|
+
File.write(legacy_file, result)
|
|
173
|
+
end
|
|
156
174
|
end
|
|
157
175
|
end
|
|
158
176
|
|
|
159
|
-
|
|
160
|
-
#
|
|
177
|
+
# Writes one Markdown file per selected text page.
|
|
178
|
+
#
|
|
179
|
+
# @return [void]
|
|
180
|
+
def emit_pagefiles
|
|
181
|
+
@pages.each do |page|
|
|
182
|
+
out_file = Pathname.new("#{output_dir}/#{page_output_path(page)}")
|
|
183
|
+
out_file.dirname.mkpath
|
|
161
184
|
|
|
185
|
+
content = markdownify(page.description)
|
|
186
|
+
File.write(out_file, finalize_markdown(content, current_output_path: page_output_path(page)))
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Converts a qualified object name into a Markdown path.
|
|
191
|
+
#
|
|
192
|
+
# @param class_name [String] Qualified class or module name.
|
|
193
|
+
#
|
|
194
|
+
# @return [String] Relative Markdown path.
|
|
162
195
|
def turn_to_path(class_name)
|
|
163
196
|
"#{class_name.gsub("::", "/")}.md"
|
|
164
197
|
end
|
|
165
198
|
|
|
166
|
-
|
|
167
|
-
#
|
|
199
|
+
# Builds the Markdown output path for an RDoc page.
|
|
200
|
+
#
|
|
201
|
+
# @param page [RDoc::TopLevel] Page object to render.
|
|
202
|
+
#
|
|
203
|
+
# @return [String] Relative Markdown path.
|
|
204
|
+
def page_output_path(page)
|
|
205
|
+
source_path = normalize_input_path_for_output(page.relative_name)
|
|
206
|
+
dirname = File.dirname(source_path)
|
|
207
|
+
basename = "#{File.basename(source_path).tr(".", "_")}.md"
|
|
208
|
+
|
|
209
|
+
return basename if dirname == "."
|
|
210
|
+
|
|
211
|
+
"#{dirname}/#{basename}"
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Returns the normalized display name for a class or module.
|
|
215
|
+
#
|
|
216
|
+
# @param code_object [RDoc::Context] Class or module object.
|
|
217
|
+
#
|
|
218
|
+
# @return [String] Display name used in headings and the index.
|
|
219
|
+
def display_name(code_object)
|
|
220
|
+
class_doc_for(code_object).fetch(:display_name)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Returns the canonical Markdown path for a class or module.
|
|
224
|
+
#
|
|
225
|
+
# @param code_object [RDoc::Context] Class or module object.
|
|
226
|
+
#
|
|
227
|
+
# @return [String] Relative Markdown path.
|
|
228
|
+
def output_path_for(code_object)
|
|
229
|
+
class_doc_for(code_object).fetch(:output_path)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Returns compatibility paths that should mirror the canonical output.
|
|
233
|
+
#
|
|
234
|
+
# @param code_object [RDoc::Context] Class or module object.
|
|
235
|
+
#
|
|
236
|
+
# @return [Array<String>] Legacy Markdown paths.
|
|
237
|
+
def legacy_paths_for(code_object)
|
|
238
|
+
class_doc_for(code_object).fetch(:legacy_paths)
|
|
239
|
+
end
|
|
168
240
|
|
|
169
|
-
#
|
|
241
|
+
# Converts RDoc HTML into GitHub-flavored Markdown.
|
|
242
|
+
#
|
|
243
|
+
# @param input [String] RDoc HTML fragment.
|
|
244
|
+
#
|
|
245
|
+
# @return [String] Markdown with normalized links and no trailing whitespace.
|
|
170
246
|
def markdownify(input)
|
|
171
|
-
#
|
|
172
|
-
# Allowed parameters:
|
|
247
|
+
# ReverseMarkdown supports these unknown-tag modes:
|
|
173
248
|
# - pass_through - (default) Include the unknown tag completely into the result
|
|
174
249
|
# - drop - Drop the unknown tag and its content
|
|
175
250
|
# - bypass - Ignore the unknown tag but try to convert its content
|
|
176
251
|
# - raise - Raise an error to let you know
|
|
177
252
|
|
|
178
|
-
|
|
253
|
+
html = normalize_rdoc_pre_blocks(input)
|
|
254
|
+
|
|
255
|
+
md = ReverseMarkdown.convert(html, github_flavored: true)
|
|
179
256
|
|
|
180
|
-
#
|
|
181
|
-
md.
|
|
257
|
+
# Flatten headings whose visible text is wrapped in a self-link.
|
|
258
|
+
md.gsub!(/^(#+)\s\[([^\]]+)\]\((?:#[^)]+)\)$/) { "#{Regexp.last_match(1)} #{Regexp.last_match(2)}" }
|
|
182
259
|
|
|
183
|
-
# Replace .html to .md extension in all markdown links
|
|
184
|
-
md.gsub(
|
|
185
|
-
|
|
260
|
+
# Replace .html to .md extension in all local markdown links.
|
|
261
|
+
md.gsub!(%r{\]\((?!https?://|mailto:|#)([^)]+?)\.html((?:[?#][^)]+)?)\)}i) do
|
|
262
|
+
"](#{Regexp.last_match(1)}.md#{Regexp.last_match(2)})"
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Turn site-root markdown links into relative links.
|
|
266
|
+
md.gsub!(%r{\]\(/([^)]+?\.md(?:[?#][^)]+)?)\)}) { "](#{Regexp.last_match(1)})" }
|
|
186
267
|
|
|
187
|
-
|
|
268
|
+
# Strip RDoc structural path segments from internal links.
|
|
269
|
+
md.gsub!(%r{\]\(((?:\.\./)*)files/([^)]+?\.md(?:[?#][^)]+)?)\)}) do
|
|
270
|
+
"](#{Regexp.last_match(1)}#{Regexp.last_match(2)})"
|
|
271
|
+
end
|
|
272
|
+
md.gsub!(%r{\]\(((?:\.\./)*)classes/([^)]+?\.md(?:[?#][^)]+)?)\)}) do
|
|
273
|
+
"](#{Regexp.last_match(1)}#{Regexp.last_match(2)})"
|
|
274
|
+
end
|
|
275
|
+
md.gsub!(%r{\]\(((?:\.\./)*)modules/([^)]+?\.md(?:[?#][^)]+)?)\)}) do
|
|
276
|
+
"](#{Regexp.last_match(1)}#{Regexp.last_match(2)})"
|
|
188
277
|
end
|
|
189
278
|
|
|
190
|
-
md
|
|
279
|
+
normalize_definition_list_code_blocks(md).rstrip
|
|
191
280
|
end
|
|
192
281
|
|
|
193
|
-
#
|
|
282
|
+
# Short alias used by ERB templates.
|
|
194
283
|
alias_method :h, :markdownify
|
|
195
284
|
|
|
196
|
-
|
|
197
|
-
#
|
|
198
|
-
#
|
|
285
|
+
# Builds an HTML anchor tag.
|
|
286
|
+
#
|
|
287
|
+
# @param id [String] Fragment identifier for the generated anchor.
|
|
288
|
+
#
|
|
289
|
+
# @return [String] HTML anchor tag.
|
|
290
|
+
def anchor(id)
|
|
291
|
+
%(<a id="#{id}"></a>)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Renders an RDoc object's description as Markdown.
|
|
295
|
+
#
|
|
296
|
+
# @param code_object [RDoc::CodeObject] Object with an RDoc description.
|
|
297
|
+
# @param fallback [String, nil] Text to use when the description is empty.
|
|
298
|
+
# @param heading_level_offset [Integer] Heading levels to add while rendering.
|
|
299
|
+
#
|
|
300
|
+
# @return [String] Rendered description or fallback text.
|
|
301
|
+
def describe(code_object, fallback: nil, heading_level_offset: 0)
|
|
302
|
+
description = code_object.description
|
|
303
|
+
return fallback.to_s if description.empty?
|
|
304
|
+
|
|
305
|
+
shift_headings(markdownify(description), heading_level_offset)
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# Renders a section description as Markdown.
|
|
309
|
+
#
|
|
310
|
+
# @param section [RDoc::Context::Section] RDoc section whose description appears before grouped members.
|
|
311
|
+
# @param heading_level_offset [Integer] Heading levels to add while rendering.
|
|
312
|
+
#
|
|
313
|
+
# @return [String] Rendered section description.
|
|
314
|
+
def section_description(section, heading_level_offset:)
|
|
315
|
+
shift_headings(markdownify(section.description), heading_level_offset)
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Builds the visible method signature used in headings.
|
|
319
|
+
#
|
|
320
|
+
# @param method [RDoc::AnyMethod] Method object to render.
|
|
321
|
+
#
|
|
322
|
+
# @return [String] Normalized method signature.
|
|
323
|
+
def method_signature(method)
|
|
324
|
+
signature = method.param_seq
|
|
325
|
+
return "()" unless signature.match?(/\S/)
|
|
326
|
+
|
|
327
|
+
signature = signature.gsub("->", " -> ")
|
|
328
|
+
signature = signature.gsub(/\s+/, " ").strip
|
|
329
|
+
signature = " #{signature}" if signature.start_with?("->")
|
|
330
|
+
merge_method_signature_arguments(signature, method.params)
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# Merges RDoc parameter names into a type-only signature.
|
|
334
|
+
#
|
|
335
|
+
# @param signature [String] Method signature from RDoc call sequence.
|
|
336
|
+
# @param raw_params [String, nil] Method parameter list from RDoc.
|
|
337
|
+
#
|
|
338
|
+
# @return [String] Signature with names added when safe.
|
|
339
|
+
def merge_method_signature_arguments(signature, raw_params)
|
|
340
|
+
params = normalized_method_params(raw_params)
|
|
341
|
+
|
|
342
|
+
signature_args, signature_suffix = split_signature_arguments_and_suffix(signature)
|
|
343
|
+
return signature if signature_args.nil?
|
|
344
|
+
|
|
345
|
+
param_parts = split_signature_list(params)
|
|
346
|
+
signature_parts = split_signature_list(signature_args)
|
|
347
|
+
return signature unless param_parts.length.eql?(signature_parts.length)
|
|
348
|
+
|
|
349
|
+
param_names = param_parts.map { |part| extract_parameter_name(part) }
|
|
350
|
+
return signature if param_names.any?(&:nil?)
|
|
351
|
+
return signature if signature_parts.zip(param_names).all? { |part, name| signature_part_mentions_name?(part, name) }
|
|
352
|
+
|
|
353
|
+
merged_args = param_parts.zip(signature_parts).map do |param, type|
|
|
354
|
+
separator = param.end_with?(":") ? " " : ": "
|
|
355
|
+
"#{param}#{separator}#{type}"
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
"(#{merged_args.join(", ")})#{signature_suffix}"
|
|
359
|
+
end
|
|
199
360
|
|
|
361
|
+
# Normalizes RDoc's raw parameter string.
|
|
362
|
+
#
|
|
363
|
+
# @param raw_params [String, nil] Parameter list from RDoc.
|
|
364
|
+
#
|
|
365
|
+
# @return [String] Parameter list without outer parentheses.
|
|
366
|
+
def normalized_method_params(raw_params)
|
|
367
|
+
params = raw_params.to_s.strip
|
|
368
|
+
params = params[1...-1] if params.start_with?("(") && params.end_with?(")")
|
|
369
|
+
|
|
370
|
+
params
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
# Splits a parenthesized signature into arguments and suffix.
|
|
374
|
+
#
|
|
375
|
+
# @param signature [String] Method signature.
|
|
376
|
+
#
|
|
377
|
+
# @return [Array<String>, nil] Argument text and suffix, or nil when not parenthesized.
|
|
378
|
+
def split_signature_arguments_and_suffix(signature)
|
|
379
|
+
return unless signature.start_with?("(")
|
|
380
|
+
|
|
381
|
+
depth = 0
|
|
382
|
+
|
|
383
|
+
signature.each_char.with_index do |char, index|
|
|
384
|
+
depth += 1 if char == "("
|
|
385
|
+
|
|
386
|
+
next unless char == ")"
|
|
387
|
+
|
|
388
|
+
depth -= 1
|
|
389
|
+
return [signature[1...index], signature[(index + 1)..]] if depth.zero?
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
# Splits a comma-separated signature list while preserving nested groups.
|
|
394
|
+
#
|
|
395
|
+
# @param list [String] Signature argument list.
|
|
396
|
+
#
|
|
397
|
+
# @return [Array<String>] Signature parts.
|
|
398
|
+
def split_signature_list(list)
|
|
399
|
+
parts = []
|
|
400
|
+
current = +""
|
|
401
|
+
paren_depth = 0
|
|
402
|
+
bracket_depth = 0
|
|
403
|
+
brace_depth = 0
|
|
404
|
+
|
|
405
|
+
list.each_char do |char|
|
|
406
|
+
case char
|
|
407
|
+
when "("
|
|
408
|
+
paren_depth += 1
|
|
409
|
+
when ")"
|
|
410
|
+
paren_depth -= 1
|
|
411
|
+
when "["
|
|
412
|
+
bracket_depth += 1
|
|
413
|
+
when "]"
|
|
414
|
+
bracket_depth -= 1
|
|
415
|
+
when "{"
|
|
416
|
+
brace_depth += 1
|
|
417
|
+
when "}"
|
|
418
|
+
brace_depth -= 1
|
|
419
|
+
when ","
|
|
420
|
+
if paren_depth.zero? && bracket_depth.zero? && brace_depth.zero?
|
|
421
|
+
parts << current.strip
|
|
422
|
+
current = +""
|
|
423
|
+
next
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
current << char
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
parts << current.strip unless current.empty?
|
|
431
|
+
parts
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
# Extracts a bare Ruby parameter name from a parameter fragment.
|
|
435
|
+
#
|
|
436
|
+
# @param parameter [String] Parameter fragment.
|
|
437
|
+
#
|
|
438
|
+
# @return [String, nil] Parameter name, or nil when invalid.
|
|
439
|
+
def extract_parameter_name(parameter)
|
|
440
|
+
match = parameter.match(/\A(?:\*\*|\*|&)?([a-z_]\w*):?\z/)
|
|
441
|
+
match && match[1]
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
# Checks whether a signature fragment already includes a parameter name.
|
|
445
|
+
#
|
|
446
|
+
# @param text [String] Signature fragment.
|
|
447
|
+
# @param name [String] Parameter name.
|
|
448
|
+
#
|
|
449
|
+
# @return [Boolean] True when the name appears as a standalone word.
|
|
450
|
+
def signature_part_mentions_name?(text, name)
|
|
451
|
+
text.match?(/(?<!\w)#{name}(?!\w)/)
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
# Renders a method description or an alias fallback.
|
|
455
|
+
#
|
|
456
|
+
# @param method [RDoc::AnyMethod] Method object to render.
|
|
457
|
+
# @param current_class [RDoc::Context] Class or module currently being rendered.
|
|
458
|
+
#
|
|
459
|
+
# @return [String] Rendered method description.
|
|
460
|
+
def method_description(method, current_class:)
|
|
461
|
+
text = describe(method, heading_level_offset: 4)
|
|
462
|
+
return text unless text.empty?
|
|
463
|
+
|
|
464
|
+
aliased_method = method.is_alias_for
|
|
465
|
+
return "Not documented." unless aliased_method
|
|
466
|
+
|
|
467
|
+
"Alias for: [`#{aliased_method.name}`](#{method_link(aliased_method, current_class: current_class)})"
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
# Applies final whitespace and link normalization before writing Markdown.
|
|
471
|
+
#
|
|
472
|
+
# @param content [String] Markdown content.
|
|
473
|
+
# @param current_output_path [String] Output path for the file being written.
|
|
474
|
+
#
|
|
475
|
+
# @return [String] Final Markdown ending with one newline.
|
|
476
|
+
def finalize_markdown(content, current_output_path:)
|
|
477
|
+
output = content.lines.map(&:rstrip).join("\n")
|
|
478
|
+
output = normalize_internal_links(output, current_output_path: current_output_path)
|
|
479
|
+
output = output.sub(/\n{3,}/, "\n\n")
|
|
480
|
+
"#{output}\n"
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
# Increases Markdown heading levels without exceeding level six.
|
|
484
|
+
#
|
|
485
|
+
# @param markdown [String] Markdown content.
|
|
486
|
+
# @param heading_level_offset [Integer] Heading levels to add.
|
|
487
|
+
#
|
|
488
|
+
# @return [String] Markdown with shifted headings.
|
|
489
|
+
def shift_headings(markdown, heading_level_offset)
|
|
490
|
+
markdown.gsub(/^(#+)(\s)/) do
|
|
491
|
+
hashes = Regexp.last_match(1)
|
|
492
|
+
spaces = Regexp.last_match(2)
|
|
493
|
+
level = [hashes.length + heading_level_offset, 6].min
|
|
494
|
+
"#{"#" * level}#{spaces}"
|
|
495
|
+
end
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
# Converts RDoc definition-list code blocks into Markdown lists.
|
|
499
|
+
#
|
|
500
|
+
# @param markdown [String] Markdown content.
|
|
501
|
+
#
|
|
502
|
+
# @return [String] Markdown with convertible blocks normalized.
|
|
503
|
+
def normalize_definition_list_code_blocks(markdown)
|
|
504
|
+
markdown.gsub(/```\n(.+?)\n```/m) do
|
|
505
|
+
body = Regexp.last_match(1)
|
|
506
|
+
converted = convert_definition_list_block(body)
|
|
507
|
+
converted.nil? ? Regexp.last_match : converted
|
|
508
|
+
end
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
# Converts a single definition-list code block.
|
|
512
|
+
#
|
|
513
|
+
# @param body [String] Code block body.
|
|
514
|
+
#
|
|
515
|
+
# @return [String, nil] Converted Markdown, or nil when the block is not a definition list.
|
|
516
|
+
def convert_definition_list_block(body)
|
|
517
|
+
lines = body.lines
|
|
518
|
+
return nil unless lines.all? { |line| definition_list_line?(line) }
|
|
519
|
+
|
|
520
|
+
lines.map do |line|
|
|
521
|
+
stripped = line.strip
|
|
522
|
+
next if stripped.empty?
|
|
523
|
+
next "#{stripped.sub(/::\z/, "")}:" if stripped.end_with?("::")
|
|
524
|
+
|
|
525
|
+
"- #{stripped.sub(/\A\*\s/, "")}"
|
|
526
|
+
end.join("\n")
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
# Checks whether a line can appear in a converted definition list.
|
|
530
|
+
#
|
|
531
|
+
# @param line [String] Markdown line.
|
|
532
|
+
#
|
|
533
|
+
# @return [Boolean] True when the line matches RDoc definition-list output.
|
|
534
|
+
def definition_list_line?(line)
|
|
535
|
+
stripped = line.strip
|
|
536
|
+
stripped.empty? || stripped.end_with?("::") || stripped.match?(/\A\*\s/)
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
# Builds a Markdown link target for an aliased method.
|
|
540
|
+
#
|
|
541
|
+
# @param method [RDoc::AnyMethod] Target method.
|
|
542
|
+
# @param current_class [RDoc::Context] Class or module currently being rendered.
|
|
543
|
+
#
|
|
544
|
+
# @return [String] Anchor or relative Markdown link target.
|
|
545
|
+
def method_link(method, current_class:)
|
|
546
|
+
target_parent = method.parent
|
|
547
|
+
return "##{method.aref}" if target_parent == current_class
|
|
548
|
+
|
|
549
|
+
"#{output_path_for(target_parent)}##{method.aref}"
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
# Removes RDoc's generated HTML tags from verbatim pre blocks.
|
|
553
|
+
#
|
|
554
|
+
# @param html [String] RDoc HTML fragment.
|
|
555
|
+
#
|
|
556
|
+
# @return [String] HTML fragment with normalized pre blocks.
|
|
557
|
+
def normalize_rdoc_pre_blocks(html)
|
|
558
|
+
html.gsub(%r{<pre\b[^>]*>(?:.+?)</pre>}m) do
|
|
559
|
+
raw = Regexp.last_match(0)
|
|
560
|
+
text = raw.gsub(/<[^>]+>/, "")
|
|
561
|
+
"<pre>#{CGI.unescapeHTML(text)}</pre>"
|
|
562
|
+
end
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
# Rewrites local Markdown links relative to the current output file.
|
|
566
|
+
#
|
|
567
|
+
# @param markdown [String] Markdown content.
|
|
568
|
+
# @param current_output_path [String] Output path for the file being written.
|
|
569
|
+
#
|
|
570
|
+
# @return [String] Markdown with normalized internal links.
|
|
571
|
+
def normalize_internal_links(markdown, current_output_path:)
|
|
572
|
+
current_dir = Pathname.new(current_output_path).dirname
|
|
573
|
+
|
|
574
|
+
markdown.gsub(%r{\]\(([^)]+)\)}) do
|
|
575
|
+
target = Regexp.last_match(1)
|
|
576
|
+
path = target.sub(/[?#].*\z/, "")
|
|
577
|
+
suffix = target[path.length..]
|
|
578
|
+
|
|
579
|
+
resolved = resolve_output_path(path, current_dir)
|
|
580
|
+
rewritten = resolved ? Pathname.new(resolved).relative_path_from(current_dir) : path
|
|
581
|
+
"](#{rewritten}#{suffix})"
|
|
582
|
+
end
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
# Resolves an internal link path against known generated outputs.
|
|
586
|
+
#
|
|
587
|
+
# @param path [String] Link path from Markdown content.
|
|
588
|
+
# @param current_dir [Pathname] Directory of the current output file.
|
|
589
|
+
#
|
|
590
|
+
# @return [String, nil] Resolved output path, or nil when unresolved.
|
|
591
|
+
def resolve_output_path(path, current_dir)
|
|
592
|
+
candidates = [path, path.delete_prefix("#{@root_path_segment}/")]
|
|
593
|
+
|
|
594
|
+
candidates.each do |candidate|
|
|
595
|
+
return candidate if @known_output_paths.include?(candidate)
|
|
596
|
+
end
|
|
597
|
+
|
|
598
|
+
candidates.each do |candidate|
|
|
599
|
+
expanded = current_dir.join(candidate).cleanpath.to_s
|
|
600
|
+
return expanded if @known_output_paths.include?(expanded)
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
nil
|
|
604
|
+
end
|
|
605
|
+
|
|
606
|
+
# Normalizes an input filename into an output-relative source path.
|
|
607
|
+
#
|
|
608
|
+
# @param path [String] RDoc input path.
|
|
609
|
+
#
|
|
610
|
+
# @return [String] Normalized path without root prefixes.
|
|
611
|
+
def normalize_input_path_for_output(path)
|
|
612
|
+
normalized = path.tr("\\", "/").sub(%r{\A\./}, "")
|
|
613
|
+
|
|
614
|
+
root = File.expand_path(@options.root.to_s)
|
|
615
|
+
normalized = normalized.sub(%r{\A#{Regexp.escape(root)}/}, "")
|
|
616
|
+
normalized = normalized.sub(%r{\A/}, "")
|
|
617
|
+
|
|
618
|
+
root_basename = File.basename(root)
|
|
619
|
+
normalized.sub(%r{\A#{Regexp.escape(root_basename)}/}, "")
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
# Looks up resolved class documentation metadata.
|
|
623
|
+
#
|
|
624
|
+
# @param code_object [RDoc::Context] Class or module object.
|
|
625
|
+
#
|
|
626
|
+
# @return [Hash{Symbol => Object}] Metadata for rendering the object.
|
|
627
|
+
def class_doc_for(code_object)
|
|
628
|
+
@class_docs_by_object_id.fetch(code_object.object_id)
|
|
629
|
+
end
|
|
630
|
+
|
|
631
|
+
# Builds canonical class documentation metadata from RDoc objects.
|
|
632
|
+
#
|
|
633
|
+
# @param classes [Array<RDoc::Context>] Classes and modules to normalize.
|
|
634
|
+
#
|
|
635
|
+
# @return [Array<Hash{Symbol => Object}>] Metadata ordered by display name.
|
|
636
|
+
def build_class_docs(classes)
|
|
637
|
+
docs_by_name = {}
|
|
638
|
+
|
|
639
|
+
classes.each do |klass|
|
|
640
|
+
display_name = normalized_full_name(klass.full_name)
|
|
641
|
+
output_path = turn_to_path(display_name)
|
|
642
|
+
legacy_path = turn_to_path(klass.full_name)
|
|
643
|
+
score = class_content_score(klass)
|
|
644
|
+
|
|
645
|
+
candidate = {
|
|
646
|
+
klass: klass,
|
|
647
|
+
display_name: display_name,
|
|
648
|
+
output_path: output_path,
|
|
649
|
+
legacy_paths: [legacy_path],
|
|
650
|
+
score: score
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
existing = docs_by_name[display_name]
|
|
654
|
+
|
|
655
|
+
if existing.nil?
|
|
656
|
+
docs_by_name[display_name] = candidate
|
|
657
|
+
elsif candidate.fetch(:score) > existing.fetch(:score)
|
|
658
|
+
if existing.fetch(:score).positive?
|
|
659
|
+
candidate[:legacy_paths] |= existing.fetch(:legacy_paths)
|
|
660
|
+
end
|
|
661
|
+
docs_by_name[display_name] = candidate
|
|
662
|
+
elsif candidate.fetch(:score).positive?
|
|
663
|
+
existing[:legacy_paths] |= candidate.fetch(:legacy_paths)
|
|
664
|
+
end
|
|
665
|
+
end
|
|
666
|
+
|
|
667
|
+
docs_by_name.values
|
|
668
|
+
.select do |doc|
|
|
669
|
+
doc.fetch(:score).positive? ||
|
|
670
|
+
!synthetic_full_name?(doc.fetch(:klass).full_name)
|
|
671
|
+
end
|
|
672
|
+
.sort_by { |doc| doc.fetch(:display_name) }
|
|
673
|
+
end
|
|
674
|
+
|
|
675
|
+
# Collapses repeated namespace segments from synthetic vendored names.
|
|
676
|
+
#
|
|
677
|
+
# @param full_name [String] Full RDoc object name.
|
|
678
|
+
#
|
|
679
|
+
# @return [String] Normalized object name.
|
|
680
|
+
def normalized_full_name(full_name)
|
|
681
|
+
normalized = full_name
|
|
682
|
+
|
|
683
|
+
loop do
|
|
684
|
+
if normalized =~ /\A([^:]+)(?:::[^:]+)+::\1::(.+)\z/
|
|
685
|
+
normalized = "#{Regexp.last_match(1)}::#{Regexp.last_match(2)}"
|
|
686
|
+
end
|
|
687
|
+
|
|
688
|
+
if normalized =~ /\A(.+?)::\1\z/
|
|
689
|
+
normalized = Regexp.last_match(1)
|
|
690
|
+
end
|
|
691
|
+
|
|
692
|
+
break
|
|
693
|
+
end
|
|
694
|
+
|
|
695
|
+
normalized
|
|
696
|
+
end
|
|
697
|
+
|
|
698
|
+
# Scores how much visible content a class or module has.
|
|
699
|
+
#
|
|
700
|
+
# @param klass [RDoc::Context] Class or module object.
|
|
701
|
+
#
|
|
702
|
+
# @return [Integer] Content score used to choose duplicate docs.
|
|
703
|
+
def class_content_score(klass)
|
|
704
|
+
score = klass.method_list.size + klass.constants.size + klass.attributes.size
|
|
705
|
+
score += 1 unless klass.description.empty?
|
|
706
|
+
score
|
|
707
|
+
end
|
|
708
|
+
|
|
709
|
+
# Checks whether a name appears to contain duplicated root namespaces.
|
|
710
|
+
#
|
|
711
|
+
# @param full_name [String] Full RDoc object name.
|
|
712
|
+
#
|
|
713
|
+
# @return [Boolean] True when the root namespace appears more than once.
|
|
714
|
+
def synthetic_full_name?(full_name)
|
|
715
|
+
parts = full_name.split("::")
|
|
716
|
+
root = parts.first
|
|
717
|
+
parts.count(root) > 1
|
|
718
|
+
end
|
|
719
|
+
|
|
720
|
+
# Prepares sorted objects and link lookup state for generation.
|
|
721
|
+
#
|
|
722
|
+
# @return [void]
|
|
200
723
|
def setup
|
|
201
|
-
|
|
724
|
+
@output_dir = @options.op_dir
|
|
202
725
|
|
|
203
|
-
@
|
|
204
|
-
@
|
|
726
|
+
@class_docs = build_class_docs(@store.all_classes_and_modules.sort)
|
|
727
|
+
@class_docs_by_object_id = @class_docs.to_h { |doc| [doc.fetch(:klass).object_id, doc] }
|
|
728
|
+
@classes = @class_docs.map { |doc| doc.fetch(:klass) }
|
|
729
|
+
@pages = @store.all_files.select(&:text?).select(&:display?).sort_by(&:base_name)
|
|
205
730
|
|
|
206
|
-
|
|
731
|
+
@known_output_paths = Set.new
|
|
732
|
+
@class_docs.each do |doc|
|
|
733
|
+
@known_output_paths << doc.fetch(:output_path)
|
|
734
|
+
doc.fetch(:legacy_paths).each { |path| @known_output_paths << path }
|
|
735
|
+
end
|
|
736
|
+
@pages.each { |page| @known_output_paths << page_output_path(page) }
|
|
207
737
|
|
|
208
|
-
@
|
|
738
|
+
@root_path_segment = Pathname.new(@options.root || ".").basename
|
|
209
739
|
end
|
|
210
740
|
end
|