yard-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.
@@ -4,7 +4,21 @@ require 'pathname'
4
4
  require 'rdoc'
5
5
 
6
6
  include Helpers::ModuleHelper
7
-
7
+ include YARD::Markdown::AnchorComponentHelper
8
+ include YARD::Markdown::ArefHelper
9
+ include YARD::Markdown::CollectionRenderingHelper
10
+ include YARD::Markdown::DocumentationHelper
11
+ include YARD::Markdown::HeadingHelper
12
+ include YARD::Markdown::LinkNormalizationHelper
13
+ include YARD::Markdown::MethodPresentationHelper
14
+ include YARD::Markdown::ObjectListingHelper
15
+ include YARD::Markdown::RelationshipSectionHelper
16
+ include YARD::Markdown::SectionAssemblyHelper
17
+ include YARD::Markdown::TagFormattingHelper
18
+
19
+ # Registers the sections rendered for a namespace markdown page.
20
+ #
21
+ # @return [void]
8
22
  def init
9
23
  sections :header,
10
24
  :relationships,
@@ -16,6 +30,15 @@ def init
16
30
  :public_instance_methods_section
17
31
  end
18
32
 
33
+ # Renders the template and normalizes markdown for top-level page output.
34
+ #
35
+ # @param opts [Hash, nil] Template options passed through to the base template.
36
+ # @option opts [YARD::CodeObjects::NamespaceObject] :object Object being rendered.
37
+ # @param sects [Array<Symbol>] Section names to render.
38
+ # @param start_at [Integer] Starting index within `sects`.
39
+ # @param break_first [Boolean] Whether rendering stops after the first section.
40
+ # @yield Optional block forwarded to the base template renderer.
41
+ # @return [String] Rendered markdown output.
19
42
  def run(opts = nil, sects = sections, start_at = 0, break_first = false, &block)
20
43
  output = super
21
44
  return output unless top_level_render?(sects, start_at, break_first)
@@ -23,26 +46,47 @@ def run(opts = nil, sects = sections, start_at = 0, break_first = false, &block)
23
46
  finalize_markdown(output, options.serializer.serialized_path(object))
24
47
  end
25
48
 
49
+ # Returns whether this invocation is rendering the full top-level page.
50
+ #
51
+ # @param sects [Array<Symbol>] Section names requested for rendering.
52
+ # @param start_at [Integer] Starting index within `sects`.
53
+ # @param break_first [Boolean] Whether rendering stops after the first section.
54
+ # @return [Boolean] True when the whole page is being rendered in one pass.
26
55
  def top_level_render?(sects, start_at, break_first)
27
56
  !break_first && start_at.zero? && sects == sections
28
57
  end
29
58
 
59
+ # Renders the page heading for the current object.
60
+ #
61
+ # @return [String] Markdown heading section.
30
62
  def header
31
63
  render_section_content(heading_with_anchors("# #{object.type.to_s.capitalize} #{object.path}", object))
32
64
  end
33
65
 
66
+ # Renders inheritance and mixin relationships for the current object.
67
+ #
68
+ # @return [String] Markdown relationships section.
34
69
  def relationships
35
70
  render_section_content(object_relationships(object))
36
71
  end
37
72
 
73
+ # Renders the object's docstring as markdown.
74
+ #
75
+ # @return [String] Markdown docstring section.
38
76
  def docstring_section
39
77
  render_section_content(rdoc_to_md(object.docstring))
40
78
  end
41
79
 
80
+ # Renders the object's YARD tags.
81
+ #
82
+ # @return [String] Markdown tags section.
42
83
  def tags_section
43
84
  render_section_content(render_tags(object))
44
85
  end
45
86
 
87
+ # Renders the constants section when visible constants are present.
88
+ #
89
+ # @return [String] Markdown constants section, or an empty string.
46
90
  def constants_section
47
91
  constants = constant_listing(object).reject { |item| hidden_object?(item) }
48
92
  return '' unless constants.any?
@@ -50,6 +94,9 @@ def constants_section
50
94
  render_section_content(render_constants(constants, Array(object.groups)))
51
95
  end
52
96
 
97
+ # Renders the attributes section when visible attributes are present.
98
+ #
99
+ # @return [String] Markdown attributes section, or an empty string.
53
100
  def attributes_section
54
101
  attrs = attr_listing(object).reject { |item| hidden_object?(item) }
55
102
  return '' unless attrs.any?
@@ -57,6 +104,9 @@ def attributes_section
57
104
  render_section_content(render_attributes(attrs, Array(object.groups)))
58
105
  end
59
106
 
107
+ # Renders the public class methods section when methods are present.
108
+ #
109
+ # @return [String] Markdown public class methods section, or an empty string.
60
110
  def public_class_methods_section
61
111
  methods = public_class_methods(object)
62
112
  return '' unless methods.any?
@@ -64,421 +114,12 @@ def public_class_methods_section
64
114
  render_section_content(render_methods('Public Class Methods', methods, Array(object.groups)))
65
115
  end
66
116
 
117
+ # Renders the public instance methods section when methods are present.
118
+ #
119
+ # @return [String] Markdown public instance methods section, or an empty string.
67
120
  def public_instance_methods_section
68
121
  methods = public_instance_methods(object)
69
122
  return '' unless methods.any?
70
123
 
71
124
  render_section_content(render_methods('Public Instance Methods', methods, Array(object.groups)))
72
125
  end
73
-
74
- def render_section_content(content)
75
- text = content.to_s.strip
76
- return '' if text.empty?
77
-
78
- "#{text}\n\n"
79
- end
80
-
81
- def object_relationships(object)
82
- lines = []
83
-
84
- lines << "**Inherits:** `#{object.superclass.path}`" if object.is_a?(CodeObjects::ClassObject) && object.superclass
85
-
86
- [[:class, 'Extended by'], [:instance, 'Includes']].each do |scope, label|
87
- mixins = run_verifier(object.mixins(scope)).sort_by { |item| item.path }
88
- next if mixins.empty?
89
-
90
- lines << "**#{label}:** #{mixins.map { |mixin| "`#{mixin.path}`" }.join(', ')}"
91
- end
92
-
93
- lines.join("\n")
94
- end
95
-
96
- def render_constants(constants, group_order)
97
- lines = ['## Constants']
98
- grouped_constants = grouped_items(constants.sort_by { |item| item.name.to_s }, group_order)
99
- uses_groups = grouped_constants.any? { |name, _items| !name.nil? }
100
-
101
- grouped_constants.each do |group_name, items|
102
- if uses_groups
103
- lines << "### #{group_name || 'General'}"
104
- item_heading = '####'
105
- else
106
- item_heading = '###'
107
- end
108
-
109
- items.each_with_index do |item, index|
110
- lines << '' if index.positive?
111
- lines << heading_with_anchors("#{item_heading} `#{item.name(false)}`", item)
112
- append_lines(lines, documented_text(item), separated: false)
113
- append_lines(lines, render_tags(item), separated: false)
114
- end
115
- end
116
-
117
- lines.join("\n")
118
- end
119
-
120
- def render_attributes(attrs, group_order)
121
- lines = ['## Attributes']
122
- grouped_attrs = grouped_items(attrs, group_order)
123
- uses_groups = grouped_attrs.any? { |name, _items| !name.nil? }
124
-
125
- grouped_attrs.each do |group_name, items|
126
- if uses_groups
127
- lines << "### #{group_name || 'General'}"
128
- item_heading = '####'
129
- else
130
- item_heading = '###'
131
- end
132
-
133
- items.each_with_index do |item, index|
134
- lines << '' if index.positive?
135
- lines << heading_with_anchors("#{item_heading} `#{item.name(false)}` [#{attribute_access(item)}]", item)
136
- append_lines(lines, documented_text(item), separated: false)
137
- append_lines(lines, render_tags(item), separated: false)
138
- end
139
- end
140
-
141
- lines.join("\n")
142
- end
143
-
144
- def render_methods(section_title, methods, group_order)
145
- lines = ["## #{section_title}"]
146
- grouped_methods = grouped_items(methods, group_order)
147
- uses_groups = grouped_methods.any? { |name, _items| !name.nil? }
148
-
149
- grouped_methods.each do |group_name, items|
150
- if uses_groups
151
- lines << "### #{group_name || 'General'}"
152
- item_heading = '####'
153
- else
154
- item_heading = '###'
155
- end
156
-
157
- items.each_with_index do |item, index|
158
- lines << '' if index.positive?
159
- lines << heading_with_anchors("#{item_heading} `#{formatted_method_heading(item)}`", item)
160
- append_lines(lines, documented_text(item), separated: false)
161
- append_lines(lines, render_tags(item), separated: false)
162
- end
163
- end
164
-
165
- lines.join("\n")
166
- end
167
-
168
- def formatted_method_heading(method_object)
169
- name = method_object.name(false).to_s
170
- signature = method_signature(method_object)
171
- signature = " #{signature}" if name.end_with?(']') && signature.start_with?('(')
172
- "#{name}#{signature}"
173
- end
174
-
175
- def method_signature(method_object)
176
- return '()' if method_object.parameters.nil? || method_object.parameters.empty?
177
-
178
- rendered = method_object.parameters.map do |name, default|
179
- default.nil? || default.empty? ? name : "#{name} = #{default}"
180
- end
181
-
182
- "(#{rendered.join(', ')})"
183
- end
184
-
185
- def attribute_access(attribute)
186
- info = attribute.attr_info || {}
187
- return 'RW' if info[:read] && info[:write]
188
- return 'R' if info[:read]
189
- return 'W' if info[:write]
190
-
191
- return 'RW' if attribute.reader? && attribute.writer?
192
- return 'R' if attribute.reader?
193
-
194
- 'W'
195
- end
196
-
197
- def documented_text(object)
198
- text = rdoc_to_md(object.docstring)
199
- return text unless text.empty?
200
- return '' unless object.tags.empty?
201
-
202
- 'Not documented.'
203
- end
204
-
205
- def rdoc_to_md(docstring)
206
- text = docstring.to_s
207
- return '' if text.strip.empty?
208
-
209
- RDoc::Markup::ToMarkdown.new.convert(text).to_s.strip
210
- end
211
-
212
- def render_tags(object)
213
- return '' if object.tags.empty?
214
-
215
- lines = []
216
- regular_tags = object.tags.reject { |tag| tag.tag_name == 'example' }
217
- example_tags = object.tags.select { |tag| tag.tag_name == 'example' }
218
-
219
- regular_tags.each do |tag|
220
- lines << "- #{format_tag(tag)}"
221
- end
222
-
223
- example_tags.each do |tag|
224
- lines << '' unless lines.empty?
225
- title = tag.name.to_s.strip.empty? ? '**@example**' : "**@example #{tag.name}**"
226
- lines << title
227
- lines << '```ruby'
228
- lines << tag.text.to_s.rstrip
229
- lines << '```'
230
- end
231
-
232
- lines.join("\n")
233
- end
234
-
235
- def format_tag(tag)
236
- parts = ["**@#{tag.tag_name}**"]
237
- parts << "`#{tag.name}`" unless tag.name.to_s.strip.empty?
238
-
239
- cleaned_types = normalized_tag_types(tag.types)
240
- parts << "[#{cleaned_types.join(', ')}]" unless cleaned_types.empty?
241
- parts << tag.text.to_s.strip unless tag.text.to_s.strip.empty?
242
-
243
- parts.join(' ')
244
- end
245
-
246
- def normalized_tag_types(types)
247
- values = if types.is_a?(Hash)
248
- types.map { |name, value| format_hash_tag_type(name, value) }
249
- else
250
- Array(types)
251
- end
252
-
253
- values.map(&:to_s).map(&:strip).reject(&:empty?)
254
- end
255
-
256
- def format_hash_tag_type(name, value)
257
- key = name.to_s.strip
258
- return '' if key.empty?
259
- return key if value.nil? || value == true || (value.respond_to?(:empty?) && value.empty?)
260
-
261
- "#{key}: #{value}"
262
- end
263
-
264
- def aref(object)
265
- type = object.type
266
-
267
- return "class-#{anchor_component(object.path.gsub('::', '-'))}" if type == :class
268
- return "module-#{anchor_component(object.path.gsub('::', '-'))}" if type == :module
269
- return "constant-#{anchor_component(object.name(false))}" if type == :constant
270
- return "classvariable-#{anchor_component(object.name(false))}" if type == :classvariable
271
-
272
- scope = object.scope == :class ? 'c' : 'i'
273
-
274
- if object.respond_to?(:attr_info) && !object.attr_info.nil?
275
- "attribute-#{scope}-#{anchor_component(object.name(false))}"
276
- else
277
- "method-#{scope}-#{anchor_component(object.name(false))}"
278
- end
279
- end
280
-
281
- def legacy_aref(object)
282
- type = object.type
283
-
284
- return "#{object.name(false)}-constant" if type == :constant
285
- return "#{object.name(false)}-classvariable" if type == :classvariable
286
- return nil unless object.respond_to?(:scope)
287
-
288
- return "#{object.name(false)}-class_method" if object.scope == :class
289
- return "#{object.name(false)}-instance_method" if object.scope == :instance
290
-
291
- nil
292
- end
293
-
294
- def anchor_tags_for(object)
295
- anchors = [aref(object), legacy_aref(object)].compact.uniq
296
- anchors.map { |id| anchor_tag(id) }
297
- end
298
-
299
- def heading_with_anchors(heading, object)
300
- anchors = anchor_tags_for(object)
301
- return heading if anchors.empty?
302
-
303
- "#{heading} #{anchors.join(' ')}"
304
- end
305
-
306
- def anchor_component(value)
307
- value.to_s.each_char.map do |char|
308
- char.match?(/[A-Za-z0-9_-]/) ? char : format('-%X', char.ord)
309
- end.join
310
- end
311
-
312
- def constant_listing(object)
313
- constants = object.constants(included: false, inherited: false)
314
- constants + object.cvars
315
- end
316
-
317
- def public_method_list(object)
318
- prune_method_listing(
319
- object.meths(inherited: false, visibility: [:public]),
320
- included: false
321
- ).reject { |item| hidden_object?(item) }
322
- .sort_by { |m| m.name.to_s }
323
- end
324
-
325
- def public_class_methods(object)
326
- public_method_list(object).select { |o| o.scope == :class }
327
- end
328
-
329
- def public_instance_methods(object)
330
- public_method_list(object).select { |o| o.scope == :instance }
331
- end
332
-
333
- def attr_listing(object)
334
- attrs = []
335
- object.inheritance_tree(true).each do |superclass|
336
- next if superclass.is_a?(CodeObjects::Proxy)
337
- next if !options.embed_mixins.empty? && !options.embed_mixins_match?(superclass)
338
-
339
- %i[class instance].each do |scope|
340
- superclass.attributes[scope].each do |_name, rw|
341
- attr = prune_method_listing([rw[:read], rw[:write]].compact, false).first
342
- attrs << attr if attr
343
- end
344
- end
345
- break if options.embed_mixins.empty?
346
- end
347
- sort_listing(attrs)
348
- end
349
-
350
- def sort_listing(list)
351
- list.sort_by { |o| [o.scope.to_s, o.name.to_s.downcase] }
352
- end
353
-
354
- def grouped_items(items, group_order)
355
- grouped = Hash.new { |hash, key| hash[key] = [] }
356
- items.each { |item| grouped[item.group] << item }
357
-
358
- ordered = []
359
-
360
- Array(group_order).each do |name|
361
- next unless grouped.key?(name)
362
-
363
- ordered << [name, grouped.delete(name)]
364
- end
365
-
366
- grouped.keys.compact.sort.each do |name|
367
- ordered << [name, grouped.delete(name)]
368
- end
369
-
370
- ordered << [nil, grouped.delete(nil)] if grouped.key?(nil)
371
- ordered
372
- end
373
-
374
- def hidden_object?(object)
375
- object.docstring.to_s.strip.start_with?(':nodoc:')
376
- end
377
-
378
- def append_lines(lines, content, separated: true)
379
- return if content.to_s.strip.empty?
380
-
381
- lines << '' if separated && !lines.empty? && !lines.last.empty?
382
- lines.concat(content.to_s.split("\n"))
383
- end
384
-
385
- def anchor_tag(id)
386
- %(<a id="#{id}"></a>)
387
- end
388
-
389
- def finalize_markdown(content, current_path)
390
- output = content.is_a?(Array) ? content.join("\n") : content.to_s
391
- output = output.lines.map(&:rstrip).join("\n")
392
- output = normalize_local_links(output, current_path)
393
- output = normalize_malformed_local_links(output)
394
- output = output.gsub(/\n{3,}/, "\n\n").strip
395
- "#{output}\n"
396
- end
397
-
398
- def normalize_local_links(markdown, current_path)
399
- current_dir = Pathname.new(current_path).dirname
400
-
401
- markdown.gsub(%r{\[(.+?)\]\((?!https?://|mailto:|#)([^)\n]+)\)}) do
402
- label = Regexp.last_match(1)
403
- target = Regexp.last_match(2)
404
- path = target.sub(/[?#].*\z/, '')
405
- suffix = target[path.length..] || ''
406
- rewritten_path = resolve_local_link_target(path, current_dir)
407
-
408
- if rewritten_path.nil?
409
- "`#{label.tr('`', '')}`"
410
- else
411
- "[#{label}](#{rewritten_path}#{suffix})"
412
- end
413
- end
414
- end
415
-
416
- def resolve_registry_object(path, current_dir)
417
- cleaned = path.to_s.sub(%r{\A(?:\.\./)+}, '').delete_prefix('./').delete_prefix('/')
418
- candidates = [path.to_s, path.to_s.tr('/', '::')]
419
-
420
- if constant_reference_path?(cleaned)
421
- current_parts = current_dir.to_s.split('/').reject { |part| part.empty? || part == '.' }
422
- target_parts = cleaned.split(%r{::|/})
423
-
424
- current_parts.length.downto(0) do |depth|
425
- candidates << (current_parts.first(depth) + target_parts).join('::')
426
- end
427
- end
428
-
429
- candidates.uniq.each do |candidate|
430
- obj = Registry.at(candidate)
431
- return obj if obj && obj.name != :root
432
- end
433
-
434
- nil
435
- end
436
-
437
- def resolve_local_link_target(path, current_dir)
438
- normalized = path.to_s.delete_prefix('./')
439
- normalized = normalized.delete_prefix('/')
440
-
441
- obj = resolve_registry_object(normalized, current_dir)
442
- if obj
443
- object_path = options.serializer.serialized_path(obj)
444
- return relative_output_path(current_dir, Pathname.new(object_path).cleanpath)
445
- end
446
-
447
- if normalized.end_with?('.html')
448
- normalized = normalized.sub(/\.html\z/i, '.md')
449
- elsif File.extname(normalized).empty?
450
- return nil if unresolved_identifier_target?(normalized)
451
-
452
- normalized = "#{normalized}.md" if normalized.include?('/')
453
- end
454
-
455
- relative_output_path(current_dir, Pathname.new(normalized).cleanpath)
456
- end
457
-
458
- def constant_reference_path?(value)
459
- parts = value.to_s.split(%r{::|/}).reject(&:empty?)
460
- return false if parts.empty?
461
-
462
- parts.all? { |part| part.match?(/\A[A-Z]\w*\z/) }
463
- end
464
-
465
- def unresolved_identifier_target?(path)
466
- cleaned = path.to_s.sub(%r{\A(?:\.\./)+}, '').delete_prefix('./')
467
- return false if cleaned.include?('/') || !File.extname(cleaned).empty?
468
- return true if cleaned.start_with?(':') || cleaned.match?(/\A\d/)
469
-
470
- cleaned.match?(/\A[a-z_]\w*\z/)
471
- end
472
-
473
- def relative_output_path(current_dir, target_path)
474
- target = target_path.to_s
475
- return target if target.start_with?('../')
476
-
477
- Pathname.new(target).relative_path_from(current_dir).to_s
478
- rescue StandardError
479
- target
480
- end
481
-
482
- def normalize_malformed_local_links(markdown)
483
- markdown.gsub(%r{\[([^\]]+)\]\((?!https?://|mailto:|#)([^)\n]*['"][^)\n]*)\)}, '`\1`')
484
- end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yard-markdown
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stanislav (Stas) Katkov
@@ -60,10 +60,14 @@ extra_rdoc_files: []
60
60
  files:
61
61
  - ".editorconfig"
62
62
  - ".streerc"
63
+ - ".yard-lint.yml"
63
64
  - ".yardopts"
65
+ - AGENTS.md
66
+ - CHANGELOG.md
64
67
  - LICENSE.txt
65
68
  - README.md
66
69
  - Rakefile
70
+ - config/mutant.yml
67
71
  - example/rdoc/Bird.md
68
72
  - example/rdoc/Duck.md
69
73
  - example/rdoc/Waterfowl.md
@@ -75,6 +79,17 @@ files:
75
79
  - example_rdoc.rb
76
80
  - example_yard.rb
77
81
  - lib/yard-markdown.rb
82
+ - lib/yard/markdown/anchor_component_helper.rb
83
+ - lib/yard/markdown/aref_helper.rb
84
+ - lib/yard/markdown/collection_rendering_helper.rb
85
+ - lib/yard/markdown/documentation_helper.rb
86
+ - lib/yard/markdown/heading_helper.rb
87
+ - lib/yard/markdown/link_normalization_helper.rb
88
+ - lib/yard/markdown/method_presentation_helper.rb
89
+ - lib/yard/markdown/object_listing_helper.rb
90
+ - lib/yard/markdown/relationship_section_helper.rb
91
+ - lib/yard/markdown/section_assembly_helper.rb
92
+ - lib/yard/markdown/tag_formatting_helper.rb
78
93
  - sig/yard/markdown.rbs
79
94
  - templates/default/fulldoc/markdown/setup.rb
80
95
  - templates/default/module/markdown/setup.rb
@@ -98,7 +113,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
98
113
  - !ruby/object:Gem::Version
99
114
  version: '0'
100
115
  requirements: []
101
- rubygems_version: 4.0.3
116
+ rubygems_version: 4.0.10
102
117
  specification_version: 4
103
118
  summary: yard plugin to generate markdown documentation
104
119
  test_files: []