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.
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YARD
4
+ module Markdown
5
+ # Builds headings and legacy anchors for rendered object sections.
6
+ module HeadingHelper
7
+ include ArefHelper
8
+
9
+ # Returns the legacy YARD anchor for an object when one exists.
10
+ #
11
+ # @param object [YARD::CodeObjects::Base] Object being rendered.
12
+ # @return [String, nil] Legacy anchor id, if supported.
13
+ def legacy_aref(object)
14
+ type = object.type
15
+
16
+ return "#{object.name}-constant" if type == :constant
17
+ return "#{object.name}-classvariable" if type == :classvariable
18
+ return nil unless object.respond_to?(:scope)
19
+
20
+ return "#{object.name}-class_method" if object.scope == :class
21
+
22
+ "#{object.name}-instance_method"
23
+ end
24
+
25
+ # Returns all anchor tags that should be attached to a heading.
26
+ #
27
+ # @param object [YARD::CodeObjects::Base] Object being rendered.
28
+ # @return [Array<String>] HTML anchor tags for the object.
29
+ def anchor_tags_for(object)
30
+ anchors = [aref(object), legacy_aref(object)].compact
31
+ anchors.map { |id| anchor_tag(id) }
32
+ end
33
+
34
+ # Appends the generated anchor tags to a Markdown heading.
35
+ #
36
+ # @param heading [String] Heading text to decorate.
37
+ # @param object [YARD::CodeObjects::Base] Object being rendered.
38
+ # @return [String] Heading text with embedded anchor tags.
39
+ def heading_with_anchors(heading, object)
40
+ "#{heading} #{anchor_tags_for(object).join(' ')}"
41
+ end
42
+
43
+ # Builds an HTML anchor tag for a generated id.
44
+ #
45
+ # @param id [String] Anchor id value.
46
+ # @return [String] HTML anchor tag.
47
+ def anchor_tag(id)
48
+ %(<a id="#{id}"></a>)
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+
5
+ module YARD
6
+ module Markdown
7
+ # Rewrites generated Markdown links so they point at Markdown output.
8
+ module LinkNormalizationHelper
9
+ # Normalizes generated Markdown before it is written to disk.
10
+ #
11
+ # @param content [String, Array<String>] Markdown content to finalize.
12
+ # @param current_path [String] Output path for the current document.
13
+ # @return [String] Normalized Markdown content with a trailing newline.
14
+ def finalize_markdown(content, current_path)
15
+ output = content.instance_of?(Array) ? content.join("\n") : content
16
+ output = output.lines.map(&:rstrip).join("\n")
17
+ output = normalize_local_links(output, current_path)
18
+ output = normalize_malformed_local_links(output)
19
+ output = output.gsub(/\n{3,}/, "\n\n").strip
20
+ "#{output}\n"
21
+ end
22
+
23
+ # Rewrites local Markdown links relative to the current output path.
24
+ #
25
+ # @param markdown [String] Markdown content to rewrite.
26
+ # @param current_path [String] Output path for the current document.
27
+ # @return [String] Markdown with local links normalized.
28
+ def normalize_local_links(markdown, current_path)
29
+ current_dir = Pathname.new(current_path).dirname
30
+
31
+ markdown.gsub(%r{\[(.+?)\]\((?!https?://|mailto:|#)([^)\n]+)\)}) do
32
+ label = Regexp.last_match(1)
33
+ target = Regexp.last_match(2)
34
+ path = target.sub(/[?#].*\z/, '')
35
+ suffix = target[path.length..]
36
+ rewritten_path = resolve_local_link_target(path, current_dir)
37
+
38
+ if rewritten_path.nil?
39
+ "`#{label.tr('`', '')}`"
40
+ else
41
+ "[#{label}](#{rewritten_path}#{suffix})"
42
+ end
43
+ end
44
+ end
45
+
46
+ # Resolves a local link path to a YARD registry object when possible.
47
+ #
48
+ # @param path [String] Link target path to resolve.
49
+ # @param current_dir [Pathname] Directory for the current output file.
50
+ # @return [YARD::CodeObjects::Base, nil] Matched registry object, if any.
51
+ def resolve_registry_object(path, current_dir)
52
+ cleaned = path.sub(%r{\A(?:(?:\.\./)+|\./)}, '')
53
+ candidates = [path]
54
+
55
+ if constant_reference_path?(cleaned)
56
+ current_parts = current_dir.to_s.split('/').reject { |part| part.empty? || part == '.' }
57
+ target_parts = cleaned.split('/')
58
+
59
+ current_parts.length.downto(0) do |depth|
60
+ candidates << (current_parts.first(depth) + target_parts).join('::')
61
+ end
62
+ end
63
+
64
+ candidates.each do |candidate|
65
+ obj = Registry.at(candidate)
66
+ next if obj.nil? || obj.equal?(Registry.root)
67
+
68
+ return obj
69
+ end
70
+
71
+ nil
72
+ end
73
+
74
+ # Resolves a local link target to the final relative Markdown path.
75
+ #
76
+ # @param path [String] Link target path to resolve.
77
+ # @param current_dir [Pathname] Directory for the current output file.
78
+ # @return [String, nil] Relative Markdown path, or nil when unresolved.
79
+ def resolve_local_link_target(path, current_dir)
80
+ normalized = path.sub(%r{\A/+}, '')
81
+
82
+ obj = resolve_registry_object(normalized, current_dir)
83
+ if obj
84
+ object_path = options.serializer.serialized_path(obj)
85
+ return relative_output_path(current_dir, object_path)
86
+ end
87
+
88
+ if normalized.match?(/\.html\z/i)
89
+ normalized = normalized.sub(/\.html\z/i, '.md')
90
+ elsif File.extname(normalized).empty?
91
+ return nil if unresolved_identifier_target?(normalized)
92
+
93
+ normalized = "#{normalized}.md" if normalized.include?('/')
94
+ end
95
+
96
+ relative_output_path(current_dir, normalized)
97
+ end
98
+
99
+ # Returns whether a path looks like a constant reference.
100
+ #
101
+ # @param value [String] Link target to inspect.
102
+ # @return [Boolean] True when the path resembles a constant name.
103
+ def constant_reference_path?(value)
104
+ parts = value.split(%r{::|/}).reject(&:empty?)
105
+ return false if parts.empty?
106
+
107
+ parts.all? { |part| part.match?(/\A[A-Z]\w*\z/) }
108
+ end
109
+
110
+ # Returns whether a path looks like an unresolved bare identifier.
111
+ #
112
+ # @param path [String] Link target to inspect.
113
+ # @return [Boolean] True when the target should be treated as unresolved.
114
+ def unresolved_identifier_target?(path)
115
+ cleaned = path.sub(%r{\A(?:(?:\.\./)+|\./)}, '')
116
+ return true if cleaned.start_with?(':') || cleaned.match?(/\A\d/)
117
+
118
+ cleaned.match?(/\A[a-z_]\w*\z/)
119
+ end
120
+
121
+ # Computes a relative path from the current output directory.
122
+ #
123
+ # @param current_dir [Pathname] Directory for the current output file.
124
+ # @param target_path [String, Pathname] Output path being linked to.
125
+ # @return [String] Relative path suitable for a Markdown link.
126
+ def relative_output_path(current_dir, target_path)
127
+ target = target_path.to_s
128
+ return target if target.start_with?('../')
129
+
130
+ Pathname.new(target).relative_path_from(current_dir).to_s
131
+ rescue StandardError
132
+ target
133
+ end
134
+
135
+ # Replaces malformed local Markdown links with inline code.
136
+ #
137
+ # @param markdown [String] Markdown content to normalize.
138
+ # @return [String] Markdown with malformed local links replaced.
139
+ def normalize_malformed_local_links(markdown)
140
+ markdown.gsub(%r{\[([^\]]+)\]\((?!https?://|mailto:|#)(?:[^)\n]*['"][^)\n]*)\)}, '`\1`')
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YARD
4
+ module Markdown
5
+ # Formats method and attribute names for Markdown headings.
6
+ module MethodPresentationHelper
7
+ # Builds the display heading for a method.
8
+ #
9
+ # @param method_object [YARD::CodeObjects::MethodObject] Method being rendered.
10
+ # @return [String] Method heading text.
11
+ def formatted_method_heading(method_object)
12
+ name = method_object.name
13
+ signature = method_signature(method_object)
14
+ signature = " #{signature}" if name.end_with?(']')
15
+ "#{name}#{signature}"
16
+ end
17
+
18
+ # Returns the rendered parameter list for a method.
19
+ #
20
+ # @param method_object [YARD::CodeObjects::MethodObject] Method being rendered.
21
+ # @return [String] Parenthesized method signature.
22
+ def method_signature(method_object)
23
+ return '()' if method_object.parameters.nil?
24
+
25
+ rendered = method_object.parameters.map do |name, default|
26
+ default.nil? || default.empty? ? name : "#{name} = #{default}"
27
+ end
28
+
29
+ "(#{rendered.join(', ')})"
30
+ end
31
+
32
+ # Returns the access marker for an attribute.
33
+ #
34
+ # @param attribute [YARD::CodeObjects::MethodObject] Attribute reader or writer.
35
+ # @return [String] Access mode marker such as `R`, `W`, or `RW`.
36
+ def attribute_access(attribute)
37
+ info = attribute.attr_info || {}
38
+ return 'RW' if info[:read] && info[:write]
39
+ return 'R' if info[:read]
40
+ return 'W' if info[:write]
41
+
42
+ return 'RW' if attribute.reader? && attribute.writer?
43
+ return 'R' if attribute.reader?
44
+
45
+ 'W'
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YARD
4
+ module Markdown
5
+ # Collects and sorts the objects shown on a rendered object page.
6
+ module ObjectListingHelper
7
+ # Returns the constants and class variables defined on an object.
8
+ #
9
+ # @param object [YARD::CodeObjects::NamespaceObject] Object being rendered.
10
+ # @return [Array<YARD::CodeObjects::Base>] Constants and class variables.
11
+ def constant_listing(object)
12
+ constants = object.constants(included: false, inherited: false)
13
+ constants + object.cvars
14
+ end
15
+
16
+ # Returns the visible public methods defined directly on an object.
17
+ #
18
+ # @param object [YARD::CodeObjects::NamespaceObject] Object being rendered.
19
+ # @return [Array<YARD::CodeObjects::MethodObject>] Sorted public methods.
20
+ def public_method_list(object)
21
+ prune_method_listing(object.meths(inherited: false, visibility: :public))
22
+ .reject { |item| hidden_object?(item) }
23
+ .sort_by { |method_object| method_object.name }
24
+ end
25
+
26
+ # Returns the public class methods defined directly on an object.
27
+ #
28
+ # @param object [YARD::CodeObjects::NamespaceObject] Object being rendered.
29
+ # @return [Array<YARD::CodeObjects::MethodObject>] Sorted public class methods.
30
+ def public_class_methods(object)
31
+ public_method_list(object).select { |item| item.scope == :class }
32
+ end
33
+
34
+ # Returns the public instance methods defined directly on an object.
35
+ #
36
+ # @param object [YARD::CodeObjects::NamespaceObject] Object being rendered.
37
+ # @return [Array<YARD::CodeObjects::MethodObject>] Sorted public instance methods.
38
+ def public_instance_methods(object)
39
+ public_method_list(object).select { |item| item.scope == :instance }
40
+ end
41
+
42
+ # Returns the visible attribute methods for an object.
43
+ #
44
+ # @param object [YARD::CodeObjects::NamespaceObject] Object being rendered.
45
+ # @return [Array<YARD::CodeObjects::MethodObject>] Sorted attribute methods.
46
+ def attr_listing(object)
47
+ attrs = []
48
+
49
+ object.inheritance_tree(true).each do |superclass|
50
+ next if !options.embed_mixins.empty? && !options.embed_mixins_match?(superclass)
51
+
52
+ %i[class instance].each do |scope|
53
+ superclass.attributes.fetch(scope).each do |_name, rw|
54
+ attr = prune_method_listing([rw.fetch(:read), rw.fetch(:write)].compact, false).first
55
+ attrs << attr if attr
56
+ end
57
+ end
58
+
59
+ break if options.embed_mixins.empty?
60
+ end
61
+
62
+ sort_listing(attrs)
63
+ end
64
+
65
+ # Sorts a listing by scope and case-insensitive name.
66
+ #
67
+ # @param list [Array<YARD::CodeObjects::Base>] Objects to sort.
68
+ # @return [Array<YARD::CodeObjects::Base>] Sorted objects.
69
+ def sort_listing(list)
70
+ list.sort do |left, right|
71
+ scope_comparison = left.scope <=> right.scope
72
+ next scope_comparison unless scope_comparison.zero?
73
+
74
+ left.name.to_s.casecmp(right.name.to_s)
75
+ end
76
+ end
77
+
78
+ # Returns whether an object is explicitly hidden with `:nodoc:`.
79
+ #
80
+ # @param object [YARD::CodeObjects::Base] Listed object whose docstring may start with `:nodoc:`.
81
+ # @return [Boolean] True when the object should be hidden.
82
+ def hidden_object?(object)
83
+ object.docstring.lstrip.start_with?(':nodoc:')
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YARD
4
+ module Markdown
5
+ # Renders inheritance and mixin relationship summaries.
6
+ module RelationshipSectionHelper
7
+ # Returns section content with the expected trailing spacing.
8
+ #
9
+ # @param content [Object] Section content to render.
10
+ # @return [String] Section content followed by blank-line spacing.
11
+ def render_section_content(content)
12
+ text = content.to_s.strip
13
+ return '' if text.empty?
14
+
15
+ "#{text}\n\n"
16
+ end
17
+
18
+ # Returns inheritance and mixin relationships for an object.
19
+ #
20
+ # @param object [YARD::CodeObjects::NamespaceObject] Object being rendered.
21
+ # @return [String] Markdown summary of the object's relationships.
22
+ def object_relationships(object)
23
+ lines = []
24
+
25
+ lines << "**Inherits:** `#{object.superclass}`" if object.instance_of?(CodeObjects::ClassObject)
26
+
27
+ [[:class, 'Extended by'], [:instance, 'Includes']].each do |scope, label|
28
+ mixins = run_verifier(object.mixins(scope)).sort_by { |item| item.path }
29
+ next if mixins.empty?
30
+
31
+ lines << "**#{label}:** #{mixins.map { |mixin| "`#{mixin.path}`" }.join(', ')}"
32
+ end
33
+
34
+ lines.join("\n")
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YARD
4
+ module Markdown
5
+ # Assembles grouped content into ordered Markdown sections.
6
+ module SectionAssemblyHelper
7
+ # Groups items by their YARD group and orders them for rendering.
8
+ #
9
+ # @param items [Array<#group>] Renderable objects that expose a YARD group name.
10
+ # @param group_order [Array<String>, nil] Preferred ordering for named groups.
11
+ # @return [Array<Array>] Ordered pairs of group names and grouped items.
12
+ def grouped_items(items, group_order)
13
+ grouped = Hash.new { |hash, key| hash[key] = [] }
14
+ items.each { |item| grouped[item.group] << item }
15
+
16
+ ordered = []
17
+
18
+ Array(group_order).each do |name|
19
+ next unless grouped.key?(name)
20
+
21
+ ordered << [name, grouped.delete(name)]
22
+ end
23
+
24
+ grouped.keys.compact.sort.each do |name|
25
+ ordered << [name, grouped.delete(name)]
26
+ end
27
+
28
+ ordered << [nil, grouped.delete(nil)] if grouped.key?(nil)
29
+ ordered
30
+ end
31
+
32
+ # Appends non-empty content to a mutable list of lines.
33
+ #
34
+ # @param lines [Array<String>] Destination line buffer.
35
+ # @param content [String] Rendered Markdown block to split into lines.
36
+ # @param separated [Boolean] Whether to insert a blank separator line first.
37
+ # @return [void]
38
+ def append_lines(lines, content, separated: true)
39
+ return if content.lstrip.empty?
40
+
41
+ lines << '' if separated && !lines.empty? && !lines.last.empty?
42
+ lines.concat(content.split("\n"))
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YARD
4
+ module Markdown
5
+ # Formats YARD tags into Markdown list items and fenced examples.
6
+ module TagFormattingHelper
7
+ # Renders all tags for an object as Markdown.
8
+ #
9
+ # @param object [YARD::CodeObjects::Base] Object whose tags are being rendered.
10
+ # @return [String] Markdown representation of the object's tags.
11
+ def render_tags(object)
12
+ lines = []
13
+ regular_tags = object.tags.reject { |tag| tag.tag_name == 'example' }
14
+ example_tags = object.tags.select { |tag| tag.tag_name == 'example' }
15
+
16
+ regular_tags.each do |tag|
17
+ lines << "- #{format_tag(tag)}"
18
+ end
19
+
20
+ example_tags.each do |tag|
21
+ lines << nil unless lines.empty?
22
+ title = tag.name.to_s.rstrip.empty? ? '**@example**' : "**@example #{tag.name}**"
23
+ lines << title
24
+ lines << '```ruby'
25
+ lines << tag.text.to_s.rstrip
26
+ lines << '```'
27
+ end
28
+
29
+ lines.join("\n")
30
+ end
31
+
32
+ # Formats a non-example YARD tag as a Markdown list item body.
33
+ #
34
+ # @param tag [YARD::Tags::Tag] Non-example tag being converted into list item text.
35
+ # @return [String] Markdown representation of the tag.
36
+ def format_tag(tag)
37
+ parts = ["**@#{tag.tag_name}**"]
38
+ parts << "`#{tag.name}`" unless tag.name.to_s.lstrip.empty?
39
+
40
+ cleaned_types = normalized_tag_types(tag.types)
41
+ parts << "[#{cleaned_types.join(', ')}]" unless cleaned_types.empty?
42
+ parts << tag.text.strip unless tag.text.to_s.lstrip.empty?
43
+
44
+ parts.join(' ')
45
+ end
46
+
47
+ # Normalizes tag type declarations into printable strings.
48
+ #
49
+ # @param types [Array<Object>, Hash, nil] Raw tag types from YARD.
50
+ # @return [Array<String>] Cleaned type strings.
51
+ def normalized_tag_types(types)
52
+ values = if types.instance_of?(Hash)
53
+ types.map { |name, value| format_hash_tag_type(name, value) }
54
+ else
55
+ Array(types)
56
+ end
57
+
58
+ values.map(&:to_s).map(&:strip).reject(&:empty?)
59
+ end
60
+
61
+ # Formats a hash-style tag type entry.
62
+ #
63
+ # @param name [String] Type name to format.
64
+ # @param value [Object] Associated type detail.
65
+ # @return [String, nil] Formatted type entry, or nil when blank.
66
+ def format_hash_tag_type(name, value)
67
+ key = name.rstrip
68
+ return nil if key.empty?
69
+ return key if value.nil? || value == true || (value.respond_to?(:empty?) && value.empty?)
70
+
71
+ "#{key}: #{value}"
72
+ end
73
+ end
74
+ end
75
+ end
data/lib/yard-markdown.rb CHANGED
@@ -1,6 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "yard"
4
+ require_relative "yard/markdown/anchor_component_helper"
5
+ require_relative "yard/markdown/aref_helper"
6
+ require_relative "yard/markdown/collection_rendering_helper"
7
+ require_relative "yard/markdown/documentation_helper"
8
+ require_relative "yard/markdown/heading_helper"
9
+ require_relative "yard/markdown/link_normalization_helper"
10
+ require_relative "yard/markdown/method_presentation_helper"
11
+ require_relative "yard/markdown/object_listing_helper"
12
+ require_relative "yard/markdown/relationship_section_helper"
13
+ require_relative "yard/markdown/section_assembly_helper"
14
+ require_relative "yard/markdown/tag_formatting_helper"
4
15
 
5
16
  module YARD
6
17
  module Markdown