yard-markdown 0.6.0 → 0.7.1

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.
@@ -1,21 +1,45 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'pathname'
4
- require 'rdoc'
5
-
6
- include Helpers::ModuleHelper
7
-
3
+ require "rdoc"
4
+
5
+ class_eval do
6
+ include YARD::Templates::Helpers::ModuleHelper
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
+ end
19
+
20
+ # Registers the sections rendered for a namespace markdown page.
21
+ #
22
+ # @return [void]
8
23
  def init
9
24
  sections :header,
10
- :relationships,
11
- :docstring_section,
12
- :tags_section,
13
- :constants_section,
14
- :attributes_section,
15
- :public_class_methods_section,
16
- :public_instance_methods_section
17
- end
18
-
25
+ :relationships,
26
+ :docstring_section,
27
+ :tags_section,
28
+ :constants_section,
29
+ :attributes_section,
30
+ :public_class_methods_section,
31
+ :public_instance_methods_section
32
+ end
33
+
34
+ # Renders the template and normalizes markdown for top-level page output.
35
+ #
36
+ # @param opts [Hash, nil] Template options passed through to the base template.
37
+ # @option opts [YARD::CodeObjects::NamespaceObject] :object Object being rendered.
38
+ # @param sects [Array<Symbol>] Section names to render.
39
+ # @param start_at [Integer] Starting index within `sects`.
40
+ # @param break_first [Boolean] Whether rendering stops after the first section.
41
+ # @yield Optional block forwarded to the base template renderer.
42
+ # @return [String] Rendered markdown output.
19
43
  def run(opts = nil, sects = sections, start_at = 0, break_first = false, &block)
20
44
  output = super
21
45
  return output unless top_level_render?(sects, start_at, break_first)
@@ -23,462 +47,80 @@ def run(opts = nil, sects = sections, start_at = 0, break_first = false, &block)
23
47
  finalize_markdown(output, options.serializer.serialized_path(object))
24
48
  end
25
49
 
50
+ # Returns whether this invocation is rendering the full top-level page.
51
+ #
52
+ # @param sects [Array<Symbol>] Section names requested for rendering.
53
+ # @param start_at [Integer] Starting index within `sects`.
54
+ # @param break_first [Boolean] Whether rendering stops after the first section.
55
+ # @return [Boolean] True when the whole page is being rendered in one pass.
26
56
  def top_level_render?(sects, start_at, break_first)
27
57
  !break_first && start_at.zero? && sects == sections
28
58
  end
29
59
 
60
+ # Renders the page heading for the current object.
61
+ #
62
+ # @return [String] Markdown heading section.
30
63
  def header
31
64
  render_section_content(heading_with_anchors("# #{object.type.to_s.capitalize} #{object.path}", object))
32
65
  end
33
66
 
67
+ # Renders inheritance and mixin relationships for the current object.
68
+ #
69
+ # @return [String] Markdown relationships section.
34
70
  def relationships
35
71
  render_section_content(object_relationships(object))
36
72
  end
37
73
 
74
+ # Renders the object's docstring as markdown.
75
+ #
76
+ # @return [String] Markdown docstring section.
38
77
  def docstring_section
39
78
  render_section_content(rdoc_to_md(object.docstring))
40
79
  end
41
80
 
81
+ # Renders the object's YARD tags.
82
+ #
83
+ # @return [String] Markdown tags section.
42
84
  def tags_section
43
85
  render_section_content(render_tags(object))
44
86
  end
45
87
 
88
+ # Renders the constants section when visible constants are present.
89
+ #
90
+ # @return [String] Markdown constants section, or an empty string.
46
91
  def constants_section
47
92
  constants = constant_listing(object).reject { |item| hidden_object?(item) }
48
- return '' unless constants.any?
93
+ return "" unless constants.any?
49
94
 
50
95
  render_section_content(render_constants(constants, Array(object.groups)))
51
96
  end
52
97
 
98
+ # Renders the attributes section when visible attributes are present.
99
+ #
100
+ # @return [String] Markdown attributes section, or an empty string.
53
101
  def attributes_section
54
102
  attrs = attr_listing(object).reject { |item| hidden_object?(item) }
55
- return '' unless attrs.any?
103
+ return "" unless attrs.any?
56
104
 
57
105
  render_section_content(render_attributes(attrs, Array(object.groups)))
58
106
  end
59
107
 
108
+ # Renders the public class methods section when methods are present.
109
+ #
110
+ # @return [String] Markdown public class methods section, or an empty string.
60
111
  def public_class_methods_section
61
112
  methods = public_class_methods(object)
62
- return '' unless methods.any?
113
+ return "" unless methods.any?
63
114
 
64
- render_section_content(render_methods('Public Class Methods', methods, Array(object.groups)))
115
+ render_section_content(render_methods("Public Class Methods", methods, Array(object.groups)))
65
116
  end
66
117
 
118
+ # Renders the public instance methods section when methods are present.
119
+ #
120
+ # @return [String] Markdown public instance methods section, or an empty string.
67
121
  def public_instance_methods_section
68
122
  methods = public_instance_methods(object)
69
- return '' unless methods.any?
70
-
71
- render_section_content(render_methods('Public Instance Methods', methods, Array(object.groups)))
72
- 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
123
+ return "" unless methods.any?
481
124
 
482
- def normalize_malformed_local_links(markdown)
483
- markdown.gsub(%r{\[([^\]]+)\]\((?!https?://|mailto:|#)([^)\n]*['"][^)\n]*)\)}, '`\1`')
125
+ render_section_content(render_methods("Public Instance Methods", methods, Array(object.groups)))
484
126
  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.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stanislav (Stas) Katkov
@@ -59,11 +59,16 @@ extensions: []
59
59
  extra_rdoc_files: []
60
60
  files:
61
61
  - ".editorconfig"
62
+ - ".standard.yml"
62
63
  - ".streerc"
64
+ - ".yard-lint.yml"
63
65
  - ".yardopts"
66
+ - AGENTS.md
67
+ - CHANGELOG.md
64
68
  - LICENSE.txt
65
69
  - README.md
66
70
  - Rakefile
71
+ - config/mutant.yml
67
72
  - example/rdoc/Bird.md
68
73
  - example/rdoc/Duck.md
69
74
  - example/rdoc/Waterfowl.md
@@ -75,6 +80,17 @@ files:
75
80
  - example_rdoc.rb
76
81
  - example_yard.rb
77
82
  - lib/yard-markdown.rb
83
+ - lib/yard/markdown/anchor_component_helper.rb
84
+ - lib/yard/markdown/aref_helper.rb
85
+ - lib/yard/markdown/collection_rendering_helper.rb
86
+ - lib/yard/markdown/documentation_helper.rb
87
+ - lib/yard/markdown/heading_helper.rb
88
+ - lib/yard/markdown/link_normalization_helper.rb
89
+ - lib/yard/markdown/method_presentation_helper.rb
90
+ - lib/yard/markdown/object_listing_helper.rb
91
+ - lib/yard/markdown/relationship_section_helper.rb
92
+ - lib/yard/markdown/section_assembly_helper.rb
93
+ - lib/yard/markdown/tag_formatting_helper.rb
78
94
  - sig/yard/markdown.rbs
79
95
  - templates/default/fulldoc/markdown/setup.rb
80
96
  - templates/default/module/markdown/setup.rb
@@ -98,7 +114,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
98
114
  - !ruby/object:Gem::Version
99
115
  version: '0'
100
116
  requirements: []
101
- rubygems_version: 4.0.3
117
+ rubygems_version: 4.0.10
102
118
  specification_version: 4
103
119
  summary: yard plugin to generate markdown documentation
104
120
  test_files: []