rdoc-markdown 0.6.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 +164 -34
- data/README.md +7 -0
- data/Rakefile +12 -8
- data/example/Bird.md +1 -1
- data/gemfiles/rdoc_head.gemfile +5 -0
- data/lib/markdown.rb +1 -0
- data/lib/rdoc/generator/markdown.rb +409 -239
- data/lib/rdoc/markdown/version.rb +4 -1
- data/mutant.yml +15 -0
- data/rdoc-markdown.gemspec +27 -25
- metadata +41 -4
|
@@ -1,62 +1,64 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
gem
|
|
3
|
+
gem "rdoc"
|
|
4
4
|
|
|
5
|
-
require
|
|
6
|
-
require
|
|
7
|
-
require
|
|
8
|
-
require
|
|
9
|
-
require 'cgi'
|
|
5
|
+
require "erb"
|
|
6
|
+
require "reverse_markdown"
|
|
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
|
-
|
|
14
|
+
# Directory containing ERB templates.
|
|
15
|
+
TEMPLATE_DIR = File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "templates"))
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
attr_reader :store, :base_dir, :classes, :pages
|
|
17
|
+
# Source store for generated content.
|
|
18
|
+
#
|
|
19
|
+
# @return [RDoc::Store]
|
|
20
|
+
attr_reader :store
|
|
23
21
|
|
|
24
|
-
|
|
25
|
-
#
|
|
26
|
-
#
|
|
22
|
+
# Working directory captured when the generator is created.
|
|
23
|
+
#
|
|
24
|
+
# @return [Pathname]
|
|
25
|
+
attr_reader :base_dir
|
|
27
26
|
|
|
28
|
-
|
|
29
|
-
#
|
|
30
|
-
#
|
|
27
|
+
# Classes and modules selected for output.
|
|
28
|
+
#
|
|
29
|
+
# @return [Array<RDoc::Context>, nil]
|
|
30
|
+
attr_reader :classes
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
#
|
|
34
|
-
#
|
|
32
|
+
# Text files selected for output.
|
|
33
|
+
#
|
|
34
|
+
# @return [Array<RDoc::TopLevel>, nil]
|
|
35
|
+
attr_reader :pages
|
|
35
36
|
|
|
37
|
+
# Required by RDoc's generator interface; markdown output has no class subdirectory.
|
|
38
|
+
#
|
|
39
|
+
# @return [nil]
|
|
36
40
|
def class_dir
|
|
37
|
-
nil
|
|
38
41
|
end
|
|
39
42
|
|
|
40
43
|
# this alias is required for rdoc to work
|
|
41
|
-
|
|
44
|
+
alias_method :file_dir, :class_dir
|
|
42
45
|
|
|
43
|
-
|
|
44
|
-
#
|
|
45
|
-
|
|
46
|
-
|
|
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)
|
|
47
51
|
@store = store
|
|
48
|
-
@options =
|
|
52
|
+
@options = rdoc_options
|
|
49
53
|
|
|
50
|
-
@base_dir = Pathname.pwd
|
|
51
|
-
|
|
52
|
-
@classes = nil
|
|
54
|
+
@base_dir = Pathname.pwd
|
|
53
55
|
end
|
|
54
56
|
|
|
55
|
-
|
|
56
|
-
#
|
|
57
|
-
|
|
57
|
+
# Writes class files, page files, and the search index.
|
|
58
|
+
#
|
|
59
|
+
# @return [void]
|
|
58
60
|
def generate
|
|
59
|
-
debug("Setting things up
|
|
61
|
+
debug("Setting things up ")
|
|
60
62
|
|
|
61
63
|
setup
|
|
62
64
|
|
|
@@ -77,27 +79,30 @@ class RDoc::Generator::Markdown
|
|
|
77
79
|
|
|
78
80
|
attr_reader :options, :output_dir
|
|
79
81
|
|
|
80
|
-
|
|
81
|
-
#
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
84
90
|
return unless $DEBUG_RDOC
|
|
91
|
+
# standard:enable Style/GlobalVars
|
|
85
92
|
|
|
86
|
-
puts "[rdoc-markdown] #{str}"
|
|
87
|
-
yield if block_given?
|
|
93
|
+
puts "[rdoc-markdown] #{str}"
|
|
88
94
|
end
|
|
89
95
|
|
|
90
|
-
|
|
91
|
-
# This class emits a search index for generated documentation as sqlite database
|
|
96
|
+
# Writes a CSV search index for generated documentation.
|
|
92
97
|
#
|
|
98
|
+
# @return [void]
|
|
99
|
+
def emit_csv_index
|
|
100
|
+
filepath = "#{output_dir}/index.csv"
|
|
93
101
|
|
|
94
|
-
|
|
95
|
-
filepath = "#{output_dir}/#{name}"
|
|
96
|
-
|
|
97
|
-
CSV.open(filepath, 'wb') do |csv|
|
|
102
|
+
CSV.open(filepath, "wb") do |csv|
|
|
98
103
|
csv << %w[name type path]
|
|
99
104
|
|
|
100
|
-
@classes.
|
|
105
|
+
@classes.each do |klass|
|
|
101
106
|
csv << [
|
|
102
107
|
display_name(klass),
|
|
103
108
|
klass.type.capitalize,
|
|
@@ -107,7 +112,7 @@ class RDoc::Generator::Markdown
|
|
|
107
112
|
klass.method_list.select(&:display?).each do |method|
|
|
108
113
|
csv << [
|
|
109
114
|
"#{display_name(klass)}.#{method.name}",
|
|
110
|
-
|
|
115
|
+
"Method",
|
|
111
116
|
"#{output_path_for(klass)}##{method.aref}"
|
|
112
117
|
]
|
|
113
118
|
end
|
|
@@ -115,11 +120,11 @@ class RDoc::Generator::Markdown
|
|
|
115
120
|
klass
|
|
116
121
|
.constants
|
|
117
122
|
.select(&:display?)
|
|
118
|
-
.
|
|
123
|
+
.sort
|
|
119
124
|
.each do |const|
|
|
120
125
|
csv << [
|
|
121
126
|
"#{display_name(klass)}.#{const.name}",
|
|
122
|
-
|
|
127
|
+
"Constant",
|
|
123
128
|
"#{output_path_for(klass)}##{const.name}"
|
|
124
129
|
]
|
|
125
130
|
end
|
|
@@ -127,11 +132,11 @@ class RDoc::Generator::Markdown
|
|
|
127
132
|
klass
|
|
128
133
|
.attributes
|
|
129
134
|
.select(&:display?)
|
|
130
|
-
.
|
|
135
|
+
.sort
|
|
131
136
|
.each do |attr|
|
|
132
137
|
csv << [
|
|
133
138
|
"#{display_name(klass)}.#{attr.name}",
|
|
134
|
-
|
|
139
|
+
"Attribute",
|
|
135
140
|
"#{output_path_for(klass)}##{attr.aref}"
|
|
136
141
|
]
|
|
137
142
|
end
|
|
@@ -140,16 +145,19 @@ class RDoc::Generator::Markdown
|
|
|
140
145
|
@pages.each do |page|
|
|
141
146
|
csv << [
|
|
142
147
|
page.page_name,
|
|
143
|
-
|
|
148
|
+
"Page",
|
|
144
149
|
page_output_path(page)
|
|
145
150
|
]
|
|
146
151
|
end
|
|
147
152
|
end
|
|
148
153
|
end
|
|
149
154
|
|
|
155
|
+
# Writes one Markdown file per selected class or module.
|
|
156
|
+
#
|
|
157
|
+
# @return [void]
|
|
150
158
|
def emit_classfiles
|
|
151
|
-
template_content = File.read(File.join(TEMPLATE_DIR,
|
|
152
|
-
template = ERB.new(template_content, trim_mode:
|
|
159
|
+
template_content = File.read(File.join(TEMPLATE_DIR, "classfile.md.erb"))
|
|
160
|
+
template = ERB.new(template_content, trim_mode: "-")
|
|
153
161
|
|
|
154
162
|
@classes.each do |klass|
|
|
155
163
|
result = finalize_markdown(template.result(binding), current_output_path: output_path_for(klass))
|
|
@@ -166,73 +174,88 @@ class RDoc::Generator::Markdown
|
|
|
166
174
|
end
|
|
167
175
|
end
|
|
168
176
|
|
|
177
|
+
# Writes one Markdown file per selected text page.
|
|
178
|
+
#
|
|
179
|
+
# @return [void]
|
|
169
180
|
def emit_pagefiles
|
|
170
181
|
@pages.each do |page|
|
|
171
182
|
out_file = Pathname.new("#{output_dir}/#{page_output_path(page)}")
|
|
172
183
|
out_file.dirname.mkpath
|
|
173
184
|
|
|
174
|
-
content = markdownify(page.description
|
|
185
|
+
content = markdownify(page.description)
|
|
175
186
|
File.write(out_file, finalize_markdown(content, current_output_path: page_output_path(page)))
|
|
176
187
|
end
|
|
177
188
|
end
|
|
178
189
|
|
|
179
|
-
|
|
180
|
-
#
|
|
181
|
-
|
|
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.
|
|
182
195
|
def turn_to_path(class_name)
|
|
183
|
-
"#{class_name.gsub(
|
|
196
|
+
"#{class_name.gsub("::", "/")}.md"
|
|
184
197
|
end
|
|
185
198
|
|
|
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.
|
|
186
204
|
def page_output_path(page)
|
|
187
|
-
source_path = normalize_input_path_for_output(page.relative_name
|
|
205
|
+
source_path = normalize_input_path_for_output(page.relative_name)
|
|
188
206
|
dirname = File.dirname(source_path)
|
|
189
|
-
basename = "#{File.basename(source_path).tr(
|
|
207
|
+
basename = "#{File.basename(source_path).tr(".", "_")}.md"
|
|
190
208
|
|
|
191
|
-
return basename if dirname ==
|
|
209
|
+
return basename if dirname == "."
|
|
192
210
|
|
|
193
211
|
"#{dirname}/#{basename}"
|
|
194
212
|
end
|
|
195
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.
|
|
196
219
|
def display_name(code_object)
|
|
197
|
-
|
|
198
|
-
class_doc ? class_doc[:display_name] : code_object.full_name
|
|
220
|
+
class_doc_for(code_object).fetch(:display_name)
|
|
199
221
|
end
|
|
200
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.
|
|
201
228
|
def output_path_for(code_object)
|
|
202
|
-
|
|
203
|
-
class_doc ? class_doc[:output_path] : turn_to_path(code_object.full_name)
|
|
229
|
+
class_doc_for(code_object).fetch(:output_path)
|
|
204
230
|
end
|
|
205
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.
|
|
206
237
|
def legacy_paths_for(code_object)
|
|
207
|
-
|
|
208
|
-
class_doc ? class_doc[:legacy_paths] : []
|
|
238
|
+
class_doc_for(code_object).fetch(:legacy_paths)
|
|
209
239
|
end
|
|
210
240
|
|
|
211
|
-
|
|
212
|
-
#
|
|
213
|
-
|
|
214
|
-
#
|
|
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.
|
|
215
246
|
def markdownify(input)
|
|
216
|
-
#
|
|
217
|
-
# Allowed parameters:
|
|
247
|
+
# ReverseMarkdown supports these unknown-tag modes:
|
|
218
248
|
# - pass_through - (default) Include the unknown tag completely into the result
|
|
219
249
|
# - drop - Drop the unknown tag and its content
|
|
220
250
|
# - bypass - Ignore the unknown tag but try to convert its content
|
|
221
251
|
# - raise - Raise an error to let you know
|
|
222
252
|
|
|
223
|
-
html = normalize_rdoc_pre_blocks(input
|
|
253
|
+
html = normalize_rdoc_pre_blocks(input)
|
|
224
254
|
|
|
225
|
-
md =
|
|
226
|
-
|
|
227
|
-
# unindent multiline strings
|
|
228
|
-
md = unindent_text(md)
|
|
229
|
-
|
|
230
|
-
# Remove RDoc navigation links from generated headings.
|
|
231
|
-
md.gsub!(/(#+\s+[^\n]+?)\s*\[¶\]\([^)]+\)(?:\s*\[↑\]\(#top\))?/) { Regexp.last_match(1) }
|
|
232
|
-
md.gsub!(/\s+\[↑\]\(#top\)$/, '')
|
|
255
|
+
md = ReverseMarkdown.convert(html, github_flavored: true)
|
|
233
256
|
|
|
234
257
|
# Flatten headings whose visible text is wrapped in a self-link.
|
|
235
|
-
md.gsub!(/^(#+)\s
|
|
258
|
+
md.gsub!(/^(#+)\s\[([^\]]+)\]\((?:#[^)]+)\)$/) { "#{Regexp.last_match(1)} #{Regexp.last_match(2)}" }
|
|
236
259
|
|
|
237
260
|
# Replace .html to .md extension in all local markdown links.
|
|
238
261
|
md.gsub!(%r{\]\((?!https?://|mailto:|#)([^)]+?)\.html((?:[?#][^)]+)?)\)}i) do
|
|
@@ -253,173 +276,320 @@ class RDoc::Generator::Markdown
|
|
|
253
276
|
"](#{Regexp.last_match(1)}#{Regexp.last_match(2)})"
|
|
254
277
|
end
|
|
255
278
|
|
|
256
|
-
md
|
|
257
|
-
md = normalize_definition_list_code_blocks(md)
|
|
258
|
-
md.lines.map(&:rstrip).join("\n").strip
|
|
279
|
+
normalize_definition_list_code_blocks(md).rstrip
|
|
259
280
|
end
|
|
260
281
|
|
|
261
|
-
#
|
|
262
|
-
|
|
282
|
+
# Short alias used by ERB templates.
|
|
283
|
+
alias_method :h, :markdownify
|
|
263
284
|
|
|
285
|
+
# Builds an HTML anchor tag.
|
|
286
|
+
#
|
|
287
|
+
# @param id [String] Fragment identifier for the generated anchor.
|
|
288
|
+
#
|
|
289
|
+
# @return [String] HTML anchor tag.
|
|
264
290
|
def anchor(id)
|
|
265
291
|
%(<a id="#{id}"></a>)
|
|
266
292
|
end
|
|
267
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.
|
|
268
301
|
def describe(code_object, fallback: nil, heading_level_offset: 0)
|
|
269
|
-
description = code_object.description
|
|
270
|
-
return fallback.to_s if description.
|
|
302
|
+
description = code_object.description
|
|
303
|
+
return fallback.to_s if description.empty?
|
|
271
304
|
|
|
272
305
|
shift_headings(markdownify(description), heading_level_offset)
|
|
273
306
|
end
|
|
274
307
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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)
|
|
280
316
|
end
|
|
281
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.
|
|
282
323
|
def method_signature(method)
|
|
283
|
-
signature = method.param_seq
|
|
284
|
-
return
|
|
324
|
+
signature = method.param_seq
|
|
325
|
+
return "()" unless signature.match?(/\S/)
|
|
285
326
|
|
|
286
|
-
signature = signature.gsub(
|
|
287
|
-
signature = signature.gsub(/\s+/,
|
|
288
|
-
signature = " #{signature}" if signature.start_with?(
|
|
289
|
-
signature
|
|
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)
|
|
290
331
|
end
|
|
291
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
|
|
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.
|
|
292
460
|
def method_description(method, current_class:)
|
|
293
|
-
text = describe(method,
|
|
461
|
+
text = describe(method, heading_level_offset: 4)
|
|
294
462
|
return text unless text.empty?
|
|
295
463
|
|
|
296
|
-
aliased_method = method.
|
|
297
|
-
return
|
|
464
|
+
aliased_method = method.is_alias_for
|
|
465
|
+
return "Not documented." unless aliased_method
|
|
298
466
|
|
|
299
467
|
"Alias for: [`#{aliased_method.name}`](#{method_link(aliased_method, current_class: current_class)})"
|
|
300
468
|
end
|
|
301
469
|
|
|
302
|
-
|
|
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:)
|
|
303
477
|
output = content.lines.map(&:rstrip).join("\n")
|
|
304
|
-
output = normalize_internal_links(output, current_output_path: current_output_path)
|
|
305
|
-
output.
|
|
306
|
-
"#{output
|
|
478
|
+
output = normalize_internal_links(output, current_output_path: current_output_path)
|
|
479
|
+
output = output.sub(/\n{3,}/, "\n\n")
|
|
480
|
+
"#{output}\n"
|
|
307
481
|
end
|
|
308
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.
|
|
309
489
|
def shift_headings(markdown, heading_level_offset)
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
markdown.gsub(/^(#+)(\s+)/) do
|
|
490
|
+
markdown.gsub(/^(#+)(\s)/) do
|
|
313
491
|
hashes = Regexp.last_match(1)
|
|
314
492
|
spaces = Regexp.last_match(2)
|
|
315
493
|
level = [hashes.length + heading_level_offset, 6].min
|
|
316
|
-
"#{
|
|
494
|
+
"#{"#" * level}#{spaces}"
|
|
317
495
|
end
|
|
318
496
|
end
|
|
319
497
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
end
|
|
326
|
-
|
|
327
|
-
section.description.to_s
|
|
328
|
-
rescue NoMethodError
|
|
329
|
-
comments = section.respond_to?(:comments) ? section.comments : nil
|
|
330
|
-
return '' if comments.nil? || comments.empty?
|
|
331
|
-
|
|
332
|
-
comments.map { |comment| comment.respond_to?(:text) ? comment.text : comment.to_s }.join("\n")
|
|
333
|
-
end
|
|
334
|
-
|
|
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.
|
|
335
503
|
def normalize_definition_list_code_blocks(markdown)
|
|
336
|
-
markdown.gsub(/```\n(
|
|
504
|
+
markdown.gsub(/```\n(.+?)\n```/m) do
|
|
337
505
|
body = Regexp.last_match(1)
|
|
338
506
|
converted = convert_definition_list_block(body)
|
|
339
|
-
converted.nil? ? Regexp.last_match
|
|
507
|
+
converted.nil? ? Regexp.last_match : converted
|
|
340
508
|
end
|
|
341
509
|
end
|
|
342
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.
|
|
343
516
|
def convert_definition_list_block(body)
|
|
344
|
-
lines = body.lines
|
|
345
|
-
return nil if lines.empty?
|
|
346
|
-
return nil unless lines.any? { |line| line.strip.end_with?('::') }
|
|
517
|
+
lines = body.lines
|
|
347
518
|
return nil unless lines.all? { |line| definition_list_line?(line) }
|
|
348
519
|
|
|
349
|
-
lines.
|
|
520
|
+
lines.map do |line|
|
|
350
521
|
stripped = line.strip
|
|
351
|
-
next
|
|
352
|
-
next "#{stripped.sub(/::\z/,
|
|
522
|
+
next if stripped.empty?
|
|
523
|
+
next "#{stripped.sub(/::\z/, "")}:" if stripped.end_with?("::")
|
|
353
524
|
|
|
354
|
-
"- #{stripped.sub(
|
|
525
|
+
"- #{stripped.sub(/\A\*\s/, "")}"
|
|
355
526
|
end.join("\n")
|
|
356
527
|
end
|
|
357
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.
|
|
358
534
|
def definition_list_line?(line)
|
|
359
535
|
stripped = line.strip
|
|
360
|
-
stripped.empty? || stripped.end_with?(
|
|
536
|
+
stripped.empty? || stripped.end_with?("::") || stripped.match?(/\A\*\s/)
|
|
361
537
|
end
|
|
362
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.
|
|
363
545
|
def method_link(method, current_class:)
|
|
364
546
|
target_parent = method.parent
|
|
365
547
|
return "##{method.aref}" if target_parent == current_class
|
|
366
548
|
|
|
367
|
-
|
|
368
|
-
current_path = output_path_for(current_class)
|
|
369
|
-
"#{relative_output_path(current_path, target_path)}##{method.aref}"
|
|
370
|
-
end
|
|
371
|
-
|
|
372
|
-
def relative_output_path(from_path, to_path)
|
|
373
|
-
from_dir = Pathname.new(from_path).dirname
|
|
374
|
-
Pathname.new(to_path).relative_path_from(from_dir).to_s
|
|
549
|
+
"#{output_path_for(target_parent)}##{method.aref}"
|
|
375
550
|
end
|
|
376
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.
|
|
377
557
|
def normalize_rdoc_pre_blocks(html)
|
|
378
|
-
html.gsub(%r{<pre\b[^>]*>(
|
|
379
|
-
raw = Regexp.last_match(
|
|
380
|
-
text = raw
|
|
381
|
-
.gsub(%r{<br\s*/?>}i, "\n")
|
|
382
|
-
.gsub(/<[^>]+>/, '')
|
|
558
|
+
html.gsub(%r{<pre\b[^>]*>(?:.+?)</pre>}m) do
|
|
559
|
+
raw = Regexp.last_match(0)
|
|
560
|
+
text = raw.gsub(/<[^>]+>/, "")
|
|
383
561
|
"<pre>#{CGI.unescapeHTML(text)}</pre>"
|
|
384
562
|
end
|
|
385
563
|
end
|
|
386
564
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
end
|
|
394
|
-
|
|
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.
|
|
395
571
|
def normalize_internal_links(markdown, current_output_path:)
|
|
396
|
-
return markdown if @known_output_paths.nil? || @known_output_paths.empty?
|
|
397
|
-
|
|
398
572
|
current_dir = Pathname.new(current_output_path).dirname
|
|
399
573
|
|
|
400
|
-
markdown.gsub(%r{\]\((
|
|
574
|
+
markdown.gsub(%r{\]\(([^)]+)\)}) do
|
|
401
575
|
target = Regexp.last_match(1)
|
|
402
|
-
path = target.sub(/[?#].*\z/,
|
|
403
|
-
suffix = target[path.length..]
|
|
576
|
+
path = target.sub(/[?#].*\z/, "")
|
|
577
|
+
suffix = target[path.length..]
|
|
404
578
|
|
|
405
579
|
resolved = resolve_output_path(path, current_dir)
|
|
406
|
-
rewritten = resolved ? Pathname.new(resolved).relative_path_from(current_dir)
|
|
580
|
+
rewritten = resolved ? Pathname.new(resolved).relative_path_from(current_dir) : path
|
|
407
581
|
"](#{rewritten}#{suffix})"
|
|
408
582
|
end
|
|
409
583
|
end
|
|
410
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.
|
|
411
591
|
def resolve_output_path(path, current_dir)
|
|
412
|
-
|
|
413
|
-
candidates = [normalized_path]
|
|
414
|
-
|
|
415
|
-
stripped = normalized_path.sub(%r{\A(?:files|classes|modules)/}, '')
|
|
416
|
-
candidates << stripped unless stripped == normalized_path
|
|
417
|
-
|
|
418
|
-
if @root_path_segment && stripped.start_with?("#{@root_path_segment}/")
|
|
419
|
-
candidates << stripped.delete_prefix("#{@root_path_segment}/")
|
|
420
|
-
end
|
|
421
|
-
|
|
422
|
-
candidates = candidates.flat_map { |candidate| candidate_with_parent_reductions(candidate) }.uniq
|
|
592
|
+
candidates = [path, path.delete_prefix("#{@root_path_segment}/")]
|
|
423
593
|
|
|
424
594
|
candidates.each do |candidate|
|
|
425
595
|
return candidate if @known_output_paths.include?(candidate)
|
|
@@ -433,33 +603,36 @@ class RDoc::Generator::Markdown
|
|
|
433
603
|
nil
|
|
434
604
|
end
|
|
435
605
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
reduced = reduced.delete_prefix('../')
|
|
442
|
-
reductions << reduced
|
|
443
|
-
end
|
|
444
|
-
|
|
445
|
-
reductions.uniq.reject(&:empty?)
|
|
446
|
-
end
|
|
447
|
-
|
|
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.
|
|
448
611
|
def normalize_input_path_for_output(path)
|
|
449
|
-
normalized = path.
|
|
450
|
-
normalized = normalized.sub(%r{\A/}, '')
|
|
612
|
+
normalized = path.tr("\\", "/").sub(%r{\A\./}, "")
|
|
451
613
|
|
|
452
|
-
root = File.expand_path(@options.root
|
|
453
|
-
normalized = normalized.sub(%r{\A#{Regexp.escape(root)}/},
|
|
614
|
+
root = File.expand_path(@options.root.to_s)
|
|
615
|
+
normalized = normalized.sub(%r{\A#{Regexp.escape(root)}/}, "")
|
|
616
|
+
normalized = normalized.sub(%r{\A/}, "")
|
|
454
617
|
|
|
455
618
|
root_basename = File.basename(root)
|
|
456
|
-
normalized.sub(%r{\A#{Regexp.escape(root_basename)}/},
|
|
619
|
+
normalized.sub(%r{\A#{Regexp.escape(root_basename)}/}, "")
|
|
457
620
|
end
|
|
458
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.
|
|
459
627
|
def class_doc_for(code_object)
|
|
460
|
-
@class_docs_by_object_id
|
|
628
|
+
@class_docs_by_object_id.fetch(code_object.object_id)
|
|
461
629
|
end
|
|
462
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.
|
|
463
636
|
def build_class_docs(classes)
|
|
464
637
|
docs_by_name = {}
|
|
465
638
|
|
|
@@ -473,7 +646,7 @@ class RDoc::Generator::Markdown
|
|
|
473
646
|
klass: klass,
|
|
474
647
|
display_name: display_name,
|
|
475
648
|
output_path: output_path,
|
|
476
|
-
legacy_paths:
|
|
649
|
+
legacy_paths: [legacy_path],
|
|
477
650
|
score: score
|
|
478
651
|
}
|
|
479
652
|
|
|
@@ -481,44 +654,39 @@ class RDoc::Generator::Markdown
|
|
|
481
654
|
|
|
482
655
|
if existing.nil?
|
|
483
656
|
docs_by_name[display_name] = candidate
|
|
484
|
-
elsif candidate
|
|
485
|
-
if existing
|
|
486
|
-
candidate[:legacy_paths] |= existing
|
|
657
|
+
elsif candidate.fetch(:score) > existing.fetch(:score)
|
|
658
|
+
if existing.fetch(:score).positive?
|
|
659
|
+
candidate[:legacy_paths] |= existing.fetch(:legacy_paths)
|
|
487
660
|
end
|
|
488
661
|
docs_by_name[display_name] = candidate
|
|
489
|
-
elsif candidate
|
|
490
|
-
existing[:legacy_paths] |= candidate
|
|
662
|
+
elsif candidate.fetch(:score).positive?
|
|
663
|
+
existing[:legacy_paths] |= candidate.fetch(:legacy_paths)
|
|
491
664
|
end
|
|
492
665
|
end
|
|
493
666
|
|
|
494
667
|
docs_by_name.values
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
668
|
+
.select do |doc|
|
|
669
|
+
doc.fetch(:score).positive? ||
|
|
670
|
+
!synthetic_full_name?(doc.fetch(:klass).full_name)
|
|
498
671
|
end
|
|
499
|
-
|
|
500
|
-
.map { |doc| doc.tap { |d| d[:legacy_paths].uniq! } }
|
|
672
|
+
.sort_by { |doc| doc.fetch(:display_name) }
|
|
501
673
|
end
|
|
502
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.
|
|
503
680
|
def normalized_full_name(full_name)
|
|
504
|
-
normalized = full_name
|
|
681
|
+
normalized = full_name
|
|
505
682
|
|
|
506
683
|
loop do
|
|
507
|
-
break unless normalized
|
|
508
|
-
|
|
509
|
-
if normalized =~ /\A(.+?)::\1::(.+)\z/
|
|
510
|
-
normalized = "#{::Regexp.last_match(1)}::#{::Regexp.last_match(2)}"
|
|
511
|
-
next
|
|
512
|
-
end
|
|
513
|
-
|
|
514
684
|
if normalized =~ /\A([^:]+)(?:::[^:]+)+::\1::(.+)\z/
|
|
515
|
-
normalized = "#{
|
|
516
|
-
next
|
|
685
|
+
normalized = "#{Regexp.last_match(1)}::#{Regexp.last_match(2)}"
|
|
517
686
|
end
|
|
518
687
|
|
|
519
688
|
if normalized =~ /\A(.+?)::\1\z/
|
|
520
689
|
normalized = Regexp.last_match(1)
|
|
521
|
-
next
|
|
522
690
|
end
|
|
523
691
|
|
|
524
692
|
break
|
|
@@ -527,44 +695,46 @@ class RDoc::Generator::Markdown
|
|
|
527
695
|
normalized
|
|
528
696
|
end
|
|
529
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.
|
|
530
703
|
def class_content_score(klass)
|
|
531
704
|
score = klass.method_list.size + klass.constants.size + klass.attributes.size
|
|
532
|
-
score += 1 unless klass.description.
|
|
705
|
+
score += 1 unless klass.description.empty?
|
|
533
706
|
score
|
|
534
707
|
end
|
|
535
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.
|
|
536
714
|
def synthetic_full_name?(full_name)
|
|
537
|
-
parts = full_name.split(
|
|
538
|
-
return false if parts.size < 3
|
|
539
|
-
|
|
715
|
+
parts = full_name.split("::")
|
|
540
716
|
root = parts.first
|
|
541
717
|
parts.count(root) > 1
|
|
542
718
|
end
|
|
543
719
|
|
|
544
|
-
|
|
545
|
-
#
|
|
546
|
-
#
|
|
547
|
-
|
|
720
|
+
# Prepares sorted objects and link lookup state for generation.
|
|
721
|
+
#
|
|
722
|
+
# @return [void]
|
|
548
723
|
def setup
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
@output_dir = Pathname.new(@options.op_dir).expand_path(@base_dir)
|
|
552
|
-
@output_dir.mkpath
|
|
553
|
-
|
|
554
|
-
return unless @store
|
|
724
|
+
@output_dir = @options.op_dir
|
|
555
725
|
|
|
556
726
|
@class_docs = build_class_docs(@store.all_classes_and_modules.sort)
|
|
557
|
-
@class_docs_by_object_id = @class_docs.to_h { |doc| [doc
|
|
558
|
-
@classes = @class_docs.map { |doc| doc
|
|
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) }
|
|
559
729
|
@pages = @store.all_files.select(&:text?).select(&:display?).sort_by(&:base_name)
|
|
560
730
|
|
|
561
731
|
@known_output_paths = Set.new
|
|
562
732
|
@class_docs.each do |doc|
|
|
563
|
-
@known_output_paths << doc
|
|
564
|
-
doc
|
|
733
|
+
@known_output_paths << doc.fetch(:output_path)
|
|
734
|
+
doc.fetch(:legacy_paths).each { |path| @known_output_paths << path }
|
|
565
735
|
end
|
|
566
736
|
@pages.each { |page| @known_output_paths << page_output_path(page) }
|
|
567
737
|
|
|
568
|
-
@root_path_segment = Pathname.new(@options.root ||
|
|
738
|
+
@root_path_segment = Pathname.new(@options.root || ".").basename
|
|
569
739
|
end
|
|
570
740
|
end
|