yard-markdown 0.9.0 → 0.9.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 756196562ff8a2cafba54128de039d2f10fffd93816a9fc0abdad8130805e124
4
- data.tar.gz: 3a6f3bdfd92a9a95e632dac284f33151e89c26a1be283deac463f600971774a6
3
+ metadata.gz: 80c8051a8c7527055a46d9dcad2db56a019f54bb99315e25813a010152fd2df3
4
+ data.tar.gz: 7187cf79b4ea2ef53b859cb17971681eefc3981e2d3da9718dead415a2b503f2
5
5
  SHA512:
6
- metadata.gz: 04ea8878df7097ebf384ae8c45110c01036af65ce2004710c3f0c40fc20eb4cc8ccabec5f0dd4877c5abbf8c3305f7b42caa66581c4f884848b389d9106218d8
7
- data.tar.gz: ef8e21cb4d98c319f37eab85291ecd69e808c3c074909adee7376d0fb7ab79e3d62436d86b082c69ef6777fa1eb7706ce9088cd39d648e8fe53fa00daa1ee7f7
6
+ metadata.gz: 71b9f9638c8f7da302a2d8a412524bef3bb77aa7a0ec5f38a8cdc45fa04f4a65e34b0d6e7bb9c0b42e765c474041ec6b9a2845d7136f11552dd64e6f4fc52720
7
+ data.tar.gz: 6e1459ddb1e780dc708d67a1bc13d8f7315bfd8041a07c72207bf66af6bf4d9b91ce9833d7346ec78eef2708eff449932bb6d2743d4e64b8ea5bfe87d3d71e4a
data/CHANGELOG.md CHANGED
@@ -5,6 +5,10 @@ This format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
5
5
 
6
6
  ## Unreleased
7
7
 
8
+ ## 0.9.1
9
+
10
+ - Include full filenames for files, not just titles
11
+
8
12
  ## 0.9.0
9
13
 
10
14
  - Detect Markdown files, copy them into generated documentation, and include them in `index.csv`.
data/README.md CHANGED
@@ -21,10 +21,6 @@ Run `yardoc --format=markdown` to generate markdown documentation.
21
21
 
22
22
  Markdown files in the project tree are detected automatically, copied unchanged into the output, and listed in `index.csv`:
23
23
 
24
- ```bash
25
- yardoc --format=markdown
26
- ```
27
-
28
24
  Markdown files whose basename starts with `_` are ignored automatically.
29
25
 
30
26
  Use YARD's `--exclude` option to omit a separate documentation tree:
@@ -10,7 +10,7 @@ module YARD
10
10
  #
11
11
  # @param value [Object] Raw anchor fragment to encode.
12
12
  # @return [String] Anchor-safe identifier fragment.
13
- def anchor_component(value)
13
+ def self.anchor_component(value)
14
14
  value.to_s.each_char.map do |char|
15
15
  char.match?(/[A-Za-z0-9_-]/) ? char : format("-%X", char.ord)
16
16
  end.join
@@ -20,22 +20,41 @@ module YARD
20
20
  #
21
21
  # @param object [YARD::CodeObjects::Base] Object being rendered.
22
22
  # @return [String] Anchor id for the object's heading.
23
- def aref(object)
24
- type = object.type
25
-
26
- return "class-#{object.path.gsub("::", "-")}" if type == :class
27
- return "module-#{object.path.gsub("::", "-")}" if type == :module
28
- return "constant-#{object.name}" if type == :constant
29
- return "classvariable-#{anchor_component(object.name)}" if type == :classvariable
30
-
31
- scope = (object.scope == :class) ? "c" : "i"
23
+ def self.aref(object)
24
+ aref_for(object, object.type, object.name)
25
+ end
32
26
 
33
- if !object.attr_info.nil?
34
- "attribute-#{scope}-#{object.name}"
27
+ # Dispatches anchor formatting using preloaded object attributes.
28
+ #
29
+ # @param object [YARD::CodeObjects::Base] Object being rendered.
30
+ # @param type [Symbol] YARD object type.
31
+ # @param name [String, Symbol] Object name.
32
+ # @return [String] Anchor id for the object's heading.
33
+ def self.aref_for(object, type, name)
34
+ case type
35
+ when :class, :module
36
+ "#{type}-#{object.path.gsub("::", "-")}"
37
+ when :constant
38
+ "constant-#{name}"
39
+ when :classvariable
40
+ "classvariable-#{anchor_component(name)}"
35
41
  else
36
- "method-#{scope}-#{anchor_component(object.name)}"
42
+ callable_aref(object, name)
37
43
  end
38
44
  end
45
+ private_class_method :aref_for
46
+
47
+ # Returns the anchor id for a method or attribute.
48
+ #
49
+ # @param object [YARD::CodeObjects::MethodObject] Method or attribute being rendered.
50
+ # @param name [String, Symbol] Method or attribute name.
51
+ # @return [String] Anchor id for the callable object's heading.
52
+ def self.callable_aref(object, name)
53
+ scope = (object.scope == :class) ? "c" : "i"
54
+ kind, name = object.attr_info ? ["attribute", name] : ["method", anchor_component(name)]
55
+ "#{kind}-#{scope}-#{name}"
56
+ end
57
+ private_class_method :callable_aref
39
58
  end
40
59
  end
41
60
  end
@@ -10,7 +10,7 @@ module YARD
10
10
  # @param group_order [Array<String>, nil] Preferred ordering for group headings.
11
11
  # @return [String] Markdown for the constants section.
12
12
  def render_constants(constants, group_order)
13
- render_collection("Constants", constants.sort_by { |item| item.name }, group_order) { |item| "`#{item.name}`" }
13
+ render_collection("Constants", constants.sort_by(&:name), group_order) { |item| "`#{item.name}`" }
14
14
  end
15
15
 
16
16
  # Renders the attributes section for an object page.
@@ -19,7 +19,7 @@ module YARD
19
19
  # @param group_order [Array<String>, nil] Preferred ordering for group headings.
20
20
  # @return [String] Markdown for the attributes section.
21
21
  def render_attributes(attrs, group_order)
22
- render_collection("Attributes", attrs, group_order) { |item| "`#{item.name}` [#{attribute_access(item)}]" }
22
+ render_collection("Attributes", attrs, group_order) { |item| "`#{item.name}` [#{MethodPresentationHelper.attribute_access(item)}]" }
23
23
  end
24
24
 
25
25
  # Renders a method section for an object page.
@@ -29,7 +29,7 @@ module YARD
29
29
  # @param group_order [Array<String>, nil] Preferred ordering for group headings.
30
30
  # @return [String] Markdown for the method section.
31
31
  def render_methods(section_title, methods, group_order)
32
- render_collection(section_title, methods, group_order) { |item| "`#{formatted_method_heading(item)}`" }
32
+ render_collection(section_title, methods, group_order) { |item| "`#{MethodPresentationHelper.formatted_method_heading(item)}`" }
33
33
  end
34
34
 
35
35
  # Renders a grouped collection with a caller-provided item label.
@@ -37,25 +37,36 @@ module YARD
37
37
  # @param section_title [String] Section title to render.
38
38
  # @param items [Array<YARD::CodeObjects::Base>] Objects to group and render.
39
39
  # @param group_order [Array<String>, nil] Preferred ordering for group headings.
40
+ # @param label [Proc] Item-label renderer.
41
+ # @yieldparam item [YARD::CodeObjects::Base] Object whose label should be rendered.
40
42
  # @return [String] Markdown for the collection section.
41
- def render_collection(section_title, items, group_order)
42
- lines = ["## #{section_title}"]
43
- groups = grouped_items(items, group_order)
44
- uses_groups = groups.any? { |name, _items| !name.nil? }
45
- item_heading = uses_groups ? "####" : "###"
43
+ def render_collection(section_title, items, group_order, &label)
44
+ groups = SectionAssemblyHelper.grouped_items(items, group_order)
45
+ grouped = groups.any? { |name, _items| name }
46
+ body = groups.map do |name, group_items|
47
+ item_heading = grouped ? "####" : "###"
48
+ item_markdown = group_items.map { |item| render_collection_item(item_heading, item, &label) }.join("\n\n")
49
+ [("### #{name || "General"}" if grouped), item_markdown].compact
50
+ end
46
51
 
47
- groups.each do |group_name, group_items|
48
- lines << "### #{group_name || "General"}" if uses_groups
52
+ ["## #{section_title}", body].join("\n")
53
+ end
49
54
 
50
- lines << group_items.map { |item|
51
- item_lines = [heading_with_anchors("#{item_heading} #{yield(item)}", item)]
52
- append_lines(item_lines, documented_text(item), separated: false)
53
- append_lines(item_lines, render_tags(item), separated: false)
54
- item_lines.join("\n")
55
- }.join("\n\n")
56
- end
55
+ private
57
56
 
58
- lines.join("\n")
57
+ # Renders one collection item with documentation and tags.
58
+ #
59
+ # @param item_heading [String] Markdown heading prefix for the item.
60
+ # @param item [YARD::CodeObjects::Base] Object being rendered.
61
+ # @param label [Proc] Item-label renderer.
62
+ # @yieldparam item [YARD::CodeObjects::Base] Object whose label should be rendered.
63
+ # @return [String] Rendered item Markdown.
64
+ def render_collection_item(item_heading, item, &label)
65
+ [
66
+ heading_with_anchors(item_heading + " " + label.call(item), item),
67
+ documented_text(item),
68
+ TagFormattingHelper.render_tags(item)
69
+ ].reject(&:empty?).join("\n")
59
70
  end
60
71
  end
61
72
  end
@@ -13,9 +13,8 @@ module YARD
13
13
  def documented_text(object)
14
14
  text = rdoc_to_md(object.docstring)
15
15
  return text unless text.empty?
16
- return "" unless object.tags.empty?
17
16
 
18
- "Not documented."
17
+ object.tags.empty? ? "Not documented." : ""
19
18
  end
20
19
 
21
20
  # Converts an RDoc-formatted docstring to Markdown.
@@ -25,17 +24,35 @@ module YARD
25
24
  def rdoc_to_md(docstring)
26
25
  fenced_code_blocks = []
27
26
  placeholder = "YARD_MARKDOWN_FENCED_CODE_BLOCK_%d"
28
- content = docstring.gsub(/^```[^\n]*\n.*?^```[ \t]*$/m) do |block|
29
- fenced_code_blocks << block
30
- format(placeholder, fenced_code_blocks.length - 1)
31
- end
32
-
27
+ content = extract_fenced_code_blocks(docstring, fenced_code_blocks, placeholder)
33
28
  markdown = RDoc::Markup::ToMarkdown.new.convert(content).rstrip
34
- fenced_code_blocks.each_with_index do |block, index|
35
- markdown = markdown.sub(format(placeholder, index), block)
29
+ restore_fenced_code_blocks(markdown, fenced_code_blocks, placeholder)
30
+ end
31
+
32
+ private
33
+
34
+ # Replaces fenced blocks with placeholders before RDoc conversion.
35
+ #
36
+ # @param docstring [String] Raw documentation text.
37
+ # @param fenced_code_blocks [Array<String>] Destination for extracted blocks.
38
+ # @param placeholder [String] Placeholder format string.
39
+ # @return [String] Documentation with fenced blocks replaced.
40
+ def extract_fenced_code_blocks(docstring, fenced_code_blocks, placeholder)
41
+ docstring.gsub(/^```[^\n]*\n.*?^```[ \t]*$/m) do |block|
42
+ format(placeholder, fenced_code_blocks.push(block).length - 1)
36
43
  end
44
+ end
37
45
 
38
- markdown
46
+ # Restores fenced blocks after RDoc conversion.
47
+ #
48
+ # @param markdown [String] Converted Markdown text.
49
+ # @param fenced_code_blocks [Array<String>] Extracted fenced blocks.
50
+ # @param placeholder [String] Placeholder format string.
51
+ # @return [String] Markdown with fenced blocks restored.
52
+ def restore_fenced_code_blocks(markdown, fenced_code_blocks, placeholder)
53
+ fenced_code_blocks.each_with_index.reduce(markdown) do |text, (block, index)|
54
+ text.sub(format(placeholder, index), block)
55
+ end
39
56
  end
40
57
  end
41
58
  end
@@ -4,22 +4,21 @@ module YARD
4
4
  module Markdown
5
5
  # Builds headings and legacy anchors for rendered object sections.
6
6
  module HeadingHelper
7
- include ArefHelper
8
-
9
7
  # Returns the legacy YARD anchor for an object when one exists.
10
8
  #
11
9
  # @param object [YARD::CodeObjects::Base] Object being rendered.
12
10
  # @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"
11
+ def self.legacy_aref(object)
12
+ name = object.name
13
+
14
+ case object.type
15
+ when :constant
16
+ "#{name}-constant"
17
+ when :classvariable
18
+ "#{name}-classvariable"
19
+ when :method
20
+ "#{name}-#{(object.scope == :class) ? "class" : "instance"}_method"
21
+ end
23
22
  end
24
23
 
25
24
  # Returns all anchor tags that should be attached to a heading.
@@ -27,7 +26,7 @@ module YARD
27
26
  # @param object [YARD::CodeObjects::Base] Object being rendered.
28
27
  # @return [Array<String>] HTML anchor tags for the object.
29
28
  def anchor_tags_for(object)
30
- anchors = [aref(object), legacy_aref(object)].compact
29
+ anchors = [ArefHelper.aref(object), HeadingHelper.legacy_aref(object)].compact
31
30
  anchors.map { |id| anchor_tag(id) }
32
31
  end
33
32
 
@@ -10,11 +10,9 @@ module YARD
10
10
  # @param current_path [String] Output path for the current document.
11
11
  # @return [String] Normalized Markdown content with a trailing newline.
12
12
  def finalize_markdown(content, current_path)
13
- output = content.instance_of?(Array) ? content.join("\n") : content
14
- output = output.lines.map(&:rstrip).join("\n")
15
- output = normalize_local_links(output, current_path)
16
- output = output.gsub(/\n{3,}/, "\n\n").strip
17
- "#{output}\n"
13
+ LinkNormalizationHelper.finish_markdown(
14
+ normalize_local_links(LinkNormalizationHelper.normalize_lines(content), current_path)
15
+ )
18
16
  end
19
17
 
20
18
  # Rewrites local Markdown links relative to the current output path.
@@ -26,17 +24,7 @@ module YARD
26
24
  current_dir = Pathname.new(current_path).dirname
27
25
 
28
26
  markdown.gsub(%r{\[(.+?)\]\((?!https?://|mailto:|#)([^)\n]+)\)}) do
29
- label = Regexp.last_match(1)
30
- target = Regexp.last_match(2)
31
- path = target.sub(/[?#].*\z/, "")
32
- suffix = target[path.length..]
33
- rewritten_path = resolve_local_link_target(path, current_dir)
34
-
35
- if rewritten_path.nil?
36
- "`#{label.tr("`", "")}`"
37
- else
38
- "[#{label}](#{rewritten_path}#{suffix})"
39
- end
27
+ normalize_local_link(Regexp.last_match, current_dir)
40
28
  end
41
29
  end
42
30
 
@@ -45,28 +33,37 @@ module YARD
45
33
  # @param path [String] Link target path to resolve.
46
34
  # @param current_dir [Pathname] Directory for the current output file.
47
35
  # @return [YARD::CodeObjects::Base, nil] Matched registry object, if any.
48
- def resolve_registry_object(path, current_dir)
49
- cleaned = path.sub(%r{\A(?:(?:\.\./)+|\./)}, "")
50
- candidates = [path]
51
-
52
- if constant_reference_path?(cleaned)
53
- current_parts = current_dir.to_s.split("/").reject { |part| part.empty? || part == "." }
54
- target_parts = cleaned.split("/")
55
-
56
- current_parts.length.downto(0) do |depth|
57
- candidates << (current_parts.first(depth) + target_parts).join("::")
58
- end
59
- end
36
+ def self.resolve_registry_object(path, current_dir)
37
+ registry_candidates(path, current_dir)
38
+ .map { |candidate| Registry.at(candidate) }
39
+ .compact
40
+ .reject { |object| object.equal?(Registry.root) }
41
+ .first
42
+ end
60
43
 
61
- candidates.each do |candidate|
62
- obj = Registry.at(candidate)
63
- next if obj.nil? || obj.equal?(Registry.root)
44
+ # Returns registry paths that may match a local target.
45
+ #
46
+ # @param path [String] Link target path to resolve.
47
+ # @param current_dir [Pathname] Directory for the current output file.
48
+ # @return [Array<String>] Candidate registry paths in lookup order.
49
+ def self.registry_candidates(path, current_dir)
50
+ cleaned = path.sub(%r{\A(?:(?:\.\./)+|\./)}, "")
51
+ return [path] unless constant_reference_path?(cleaned)
64
52
 
65
- return obj
66
- end
53
+ namespaced_candidates(cleaned, current_dir)
54
+ end
67
55
 
68
- nil
56
+ # Expands a constant target through each enclosing namespace.
57
+ #
58
+ # @param cleaned [String] Target without relative path prefixes.
59
+ # @param current_dir [Pathname] Directory for the current output file.
60
+ # @return [Array<String>] Namespaced registry candidates.
61
+ def self.namespaced_candidates(cleaned, current_dir)
62
+ current_parts = current_dir.to_s.split("/").reject { |part| part.empty? || part == "." }
63
+ target_parts = cleaned.split("/")
64
+ current_parts.length.downto(0).map { |depth| (current_parts.first(depth) + target_parts).join("::") }
69
65
  end
66
+ private_class_method :registry_candidates, :namespaced_candidates
70
67
 
71
68
  # Resolves a local link target to the final relative Markdown path.
72
69
  #
@@ -75,33 +72,15 @@ module YARD
75
72
  # @return [String, nil] Relative Markdown path, or nil when unresolved.
76
73
  def resolve_local_link_target(path, current_dir)
77
74
  normalized = path.sub(%r{\A/+}, "")
78
-
79
- obj = resolve_registry_object(normalized, current_dir)
80
- if obj
81
- object_path = options.serializer.serialized_path(obj)
82
- return relative_output_path(current_dir, object_path)
83
- end
84
-
85
- alias_path = Pathname.new(normalized.sub(/\.html\z/i, "")).cleanpath.to_s
86
- copied_path = options.copied_file_aliases[alias_path]
87
- return relative_output_path(current_dir, copied_path) if copied_path
88
-
89
- if normalized.match?(/\.html\z/i)
90
- normalized = normalized.sub(/\.html\z/i, ".md")
91
- elsif File.extname(normalized).empty?
92
- return nil if unresolved_identifier_target?(normalized)
93
-
94
- normalized = "#{normalized}.md"
95
- end
96
-
97
- relative_output_path(current_dir, normalized)
75
+ target = registry_path(normalized, current_dir) || copied_path(normalized) || LinkNormalizationHelper.markdown_path(normalized)
76
+ LinkNormalizationHelper.relative_output_path(current_dir, target) if target
98
77
  end
99
78
 
100
79
  # Returns whether a path looks like a constant reference.
101
80
  #
102
81
  # @param value [String] Link target to inspect.
103
82
  # @return [Boolean] True when the path resembles a constant name.
104
- def constant_reference_path?(value)
83
+ def self.constant_reference_path?(value)
105
84
  parts = value.split(%r{::|/}).reject(&:empty?)
106
85
  return false if parts.empty?
107
86
 
@@ -112,7 +91,7 @@ module YARD
112
91
  #
113
92
  # @param path [String] Link target to inspect.
114
93
  # @return [Boolean] True when the target should be treated as unresolved.
115
- def unresolved_identifier_target?(path)
94
+ def self.unresolved_identifier_target?(path)
116
95
  cleaned = path.sub(%r{\A(?:(?:\.\./)+|\./)}, "")
117
96
  File.extname(cleaned).empty? && !cleaned.include?("/")
118
97
  end
@@ -122,7 +101,7 @@ module YARD
122
101
  # @param current_dir [Pathname] Directory for the current output file.
123
102
  # @param target_path [String, Pathname] Output path being linked to.
124
103
  # @return [String] Relative path suitable for a Markdown link.
125
- def relative_output_path(current_dir, target_path)
104
+ def self.relative_output_path(current_dir, target_path)
126
105
  target = target_path.to_s
127
106
  return target if target.start_with?("../")
128
107
 
@@ -130,6 +109,72 @@ module YARD
130
109
  rescue
131
110
  target
132
111
  end
112
+
113
+ # Converts supported content into normalized lines.
114
+ #
115
+ # @param content [String, Array<String>] Markdown content.
116
+ # @return [String] Joined content without trailing line whitespace.
117
+ def self.normalize_lines(content)
118
+ text = content.instance_of?(Array) ? content.join("\n") : content
119
+ text.lines.map(&:rstrip).join("\n")
120
+ end
121
+
122
+ # Compacts blank lines and adds the final newline.
123
+ #
124
+ # @param markdown [String] Normalized Markdown content.
125
+ # @return [String] Final Markdown content.
126
+ def self.finish_markdown(markdown)
127
+ "#{markdown.gsub(/\n{3,}/, "\n\n").strip}\n"
128
+ end
129
+
130
+ # Returns the Markdown output path for a non-registry target.
131
+ #
132
+ # @param path [String] Local link target.
133
+ # @return [String, nil] Markdown path, or nil for an unresolved identifier.
134
+ def self.markdown_path(path)
135
+ return path.sub(/\.html\z/i, ".md") if path.match?(/\.html\z/i)
136
+ return path unless File.extname(path).empty?
137
+ return if unresolved_identifier_target?(path)
138
+
139
+ "#{path}.md"
140
+ end
141
+
142
+ private
143
+
144
+ # Rewrites one matched local Markdown link.
145
+ #
146
+ # @param match [MatchData] Local link match.
147
+ # @param current_dir [Pathname] Directory for the current output file.
148
+ # @return [String] Rewritten link or code-formatted label.
149
+ def normalize_local_link(match, current_dir)
150
+ label, target = match.captures
151
+ path, separator, suffix = target.partition(/[?#]/)
152
+ rewritten_path = resolve_local_link_target(path, current_dir)
153
+ return "[#{label}](#{rewritten_path}#{separator}#{suffix})" if rewritten_path
154
+
155
+ "`#{label.tr("`", "")}`"
156
+ end
157
+
158
+ # Finds the serialized path for a registry object.
159
+ #
160
+ # @param path [String] Local link target.
161
+ # @param current_dir [Pathname] Directory for the current output file.
162
+ # @return [String, nil] Serialized object path, if resolved.
163
+ def registry_path(path, current_dir)
164
+ object = LinkNormalizationHelper.resolve_registry_object(path, current_dir)
165
+ return unless object
166
+
167
+ options.serializer.serialized_path(object)
168
+ end
169
+
170
+ # Finds a copied Markdown file by its normalized alias.
171
+ #
172
+ # @param path [String] Local link target.
173
+ # @return [String, nil] Copied file path, if registered.
174
+ def copied_path(path)
175
+ alias_path = Pathname.new(path.sub(/\.html\z/i, "")).cleanpath.to_s
176
+ options.copied_file_aliases[alias_path]
177
+ end
133
178
  end
134
179
  end
135
180
  end
@@ -9,21 +9,9 @@ module YARD
9
9
  # @param object [YARD::CodeObjects::NamespaceObject] Object being rendered.
10
10
  # @return [String] Markdown table containing the object's metadata.
11
11
  def object_metadata(object)
12
- rows = []
13
-
14
- if object.instance_of?(CodeObjects::ClassObject)
15
- rows << ["Inherits", metadata_reference(object.superclass)]
16
- end
17
-
18
- [[:class, "Extended by"], [:instance, "Includes"]].each do |scope, label|
19
- mixins = run_verifier(object.mixins(scope)).sort_by { |item| item.path }
20
- next if mixins.empty?
21
-
22
- rows << [label, mixins.map { |mixin| metadata_reference(mixin) }.join(", ")]
23
- end
24
-
25
- files = object.files.map(&:first).uniq
26
- rows << ["Defined in", files.map { |file| metadata_table_cell(file) }.join(", ")] unless files.empty?
12
+ superclass = object.superclass if object.instance_of?(CodeObjects::ClassObject)
13
+ rows = (superclass ? [["Inherits", metadata_reference(superclass)]] : []) +
14
+ mixin_rows(object) + MetadataSectionHelper.file_rows(object)
27
15
 
28
16
  return "" if rows.empty?
29
17
 
@@ -35,20 +23,57 @@ module YARD
35
23
  # @param target [YARD::CodeObjects::NamespaceObject, YARD::CodeObjects::Proxy] Referenced namespace.
36
24
  # @return [String] Markdown link or plain table-cell text.
37
25
  def metadata_reference(target)
38
- label = metadata_table_cell(target.path)
26
+ path = target.path
27
+ label = MetadataSectionHelper.metadata_table_cell(path)
39
28
  return label unless target.is_a?(CodeObjects::NamespaceObject) && run_verifier([target]).any?
40
29
 
41
- "[#{label}](#{target.path})"
30
+ "[#{label}](#{path})"
42
31
  end
43
32
 
44
33
  # Escapes text for a Markdown table cell.
45
34
  #
46
35
  # @param value [String] Metadata text.
47
36
  # @return [String] GFM table-safe Markdown text.
48
- def metadata_table_cell(value)
37
+ def self.metadata_table_cell(value)
49
38
  value.gsub(/[[:blank:]]*\R[[:blank:]]*/, " ")
50
39
  .gsub(/[\\|]/) { |character| "\\#{character}" }
51
40
  end
41
+
42
+ # Builds the source-file metadata rows.
43
+ #
44
+ # @param object [YARD::CodeObjects::NamespaceObject] Namespace being rendered.
45
+ # @return [Array<Array>] Source-file rows.
46
+ def self.file_rows(object)
47
+ files = object.files.map(&:first).uniq
48
+ return [] if files.empty?
49
+
50
+ [["Defined in", files.map { |file| metadata_table_cell(file) }.join(", ")]]
51
+ end
52
+
53
+ private
54
+
55
+ # Builds class and instance mixin rows.
56
+ #
57
+ # @param object [YARD::CodeObjects::NamespaceObject] Namespace being rendered.
58
+ # @return [Array<Array>] Visible mixin rows.
59
+ def mixin_rows(object)
60
+ [[:class, "Extended by"], [:instance, "Includes"]]
61
+ .map { |scope, label| mixin_row(object, scope, label) }
62
+ .compact
63
+ end
64
+
65
+ # Builds one scoped mixin row.
66
+ #
67
+ # @param object [YARD::CodeObjects::NamespaceObject] Namespace being rendered.
68
+ # @param scope [Symbol] Mixin scope.
69
+ # @param label [String] Metadata row label.
70
+ # @return [Array, nil] Mixin row, if visible mixins exist.
71
+ def mixin_row(object, scope, label)
72
+ mixins = run_verifier(object.mixins(scope)).sort_by(&:path)
73
+ return if mixins.empty?
74
+
75
+ [label, mixins.map { |mixin| metadata_reference(mixin) }.join(", ")]
76
+ end
52
77
  end
53
78
  end
54
79
  end
@@ -8,7 +8,7 @@ module YARD
8
8
  #
9
9
  # @param method_object [YARD::CodeObjects::MethodObject] Method being rendered.
10
10
  # @return [String] Method heading text.
11
- def formatted_method_heading(method_object)
11
+ def self.formatted_method_heading(method_object)
12
12
  name = method_object.name
13
13
  signature = method_signature(method_object)
14
14
  signature = " #{signature}" if name.end_with?("]")
@@ -19,11 +19,9 @@ module YARD
19
19
  #
20
20
  # @param method_object [YARD::CodeObjects::MethodObject] Method being rendered.
21
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}"
22
+ def self.method_signature(method_object)
23
+ rendered = Array(method_object.parameters).map do |name, default|
24
+ default.to_s.empty? ? name : "#{name} = #{default}"
27
25
  end
28
26
 
29
27
  "(#{rendered.join(", ")})"
@@ -33,7 +31,7 @@ module YARD
33
31
  #
34
32
  # @param attribute [YARD::CodeObjects::MethodObject] Attribute reader or writer.
35
33
  # @return [String] Access mode marker such as `R`, `W`, or `RW`.
36
- def attribute_access(attribute)
34
+ def self.attribute_access(attribute)
37
35
  read, write = attribute.attr_info.fetch_values(:read, :write)
38
36
  return "RW" if read && write
39
37
  return "R" if read
@@ -8,7 +8,7 @@ module YARD
8
8
  #
9
9
  # @param object [YARD::CodeObjects::NamespaceObject] Object being rendered.
10
10
  # @return [Array<YARD::CodeObjects::Base>] Constants and class variables.
11
- def constant_listing(object)
11
+ def self.constant_listing(object)
12
12
  constants = object.constants(included: false, inherited: false)
13
13
  constants + object.cvars
14
14
  end
@@ -19,7 +19,7 @@ module YARD
19
19
  # @return [Array<YARD::CodeObjects::MethodObject>] Sorted public methods.
20
20
  def public_method_list(object)
21
21
  prune_method_listing(object.meths(inherited: false, visibility: :public))
22
- .reject { |item| hidden_object?(item) }
22
+ .reject { |item| ObjectListingHelper.hidden_object?(item) }
23
23
  .sort_by { |method_object| method_object.name }
24
24
  end
25
25
 
@@ -44,44 +44,50 @@ module YARD
44
44
  # @param object [YARD::CodeObjects::NamespaceObject] Object being rendered.
45
45
  # @return [Array<YARD::CodeObjects::MethodObject>] Sorted attribute methods.
46
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)
47
+ superclasses = attribute_superclasses(object)
48
+ attributes = superclasses.flat_map { |superclass| attributes_for(superclass) }
49
+ ObjectListingHelper.sort_attributes(attributes)
63
50
  end
64
51
 
65
- # Sorts a listing by scope and case-insensitive name.
52
+ # Sorts attributes by scope and case-insensitive name.
66
53
  #
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
54
+ # @param attributes [Array<YARD::CodeObjects::MethodObject>] Attribute methods collected from eligible ancestors.
55
+ # @return [Array<YARD::CodeObjects::MethodObject>] Sorted attributes.
56
+ def self.sort_attributes(attributes)
57
+ attributes.sort { |left, right| (left.scope <=> right.scope).nonzero? || left.name.to_s.casecmp(right.name.to_s) }
76
58
  end
77
59
 
78
60
  # Returns whether an object is explicitly hidden with `:nodoc:`.
79
61
  #
80
62
  # @param object [YARD::CodeObjects::Base] Listed object whose docstring may start with `:nodoc:`.
81
63
  # @return [Boolean] True when the object should be hidden.
82
- def hidden_object?(object)
64
+ def self.hidden_object?(object)
83
65
  object.docstring.start_with?(":nodoc:")
84
66
  end
67
+
68
+ private
69
+
70
+ # Selects ancestors whose attributes should be embedded.
71
+ #
72
+ # @param object [YARD::CodeObjects::NamespaceObject] Namespace being rendered.
73
+ # @return [Array<YARD::CodeObjects::Base>] Eligible ancestors.
74
+ def attribute_superclasses(object)
75
+ superclasses = object.inheritance_tree(true)
76
+ return superclasses.first(1) if options.embed_mixins.empty?
77
+
78
+ superclasses.select { |superclass| options.embed_mixins_match?(superclass) }
79
+ end
80
+
81
+ # Collects visible attributes from one ancestor.
82
+ #
83
+ # @param superclass [YARD::CodeObjects::Base] Ancestor containing attributes.
84
+ # @return [Array<YARD::CodeObjects::MethodObject>] Visible attributes.
85
+ def attributes_for(superclass)
86
+ superclass.attributes.values.flat_map(&:values).map { |read_write|
87
+ read, write = read_write.fetch_values(:read, :write)
88
+ prune_method_listing([read, write].compact, false).first
89
+ }.compact
90
+ end
85
91
  end
86
92
  end
87
93
  end
@@ -8,7 +8,7 @@ module YARD
8
8
  #
9
9
  # @param content [Object] Section content to render.
10
10
  # @return [String] Section content followed by blank-line spacing.
11
- def render_section_content(content)
11
+ def self.render_section_content(content)
12
12
  text = content.to_s.strip
13
13
  return "" if text.empty?
14
14
 
@@ -20,27 +20,20 @@ module YARD
20
20
  # @param items [Array<#group>] Renderable objects that expose a YARD group name.
21
21
  # @param group_order [Array<String>, nil] Preferred ordering for named groups.
22
22
  # @return [Array<Array>] Ordered pairs of group names and grouped items.
23
- def grouped_items(items, group_order)
23
+ def self.grouped_items(items, group_order)
24
24
  groups = items.group_by(&:group)
25
- names = groups.keys
26
- order = Array(group_order)
27
- ordered_names = (order & names) + (names.compact - order).sort + ([nil] & names)
28
-
29
- ordered_names.map { |name| [name, groups.fetch(name)] }
25
+ ordered_group_names(groups.keys, Array(group_order)).map { |name| [name, groups.fetch(name)] }
30
26
  end
31
27
 
32
- # Appends non-empty content to a mutable list of lines.
28
+ # Orders known group names by configured, alphabetical, and default order.
33
29
  #
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"))
30
+ # @param names [Array<String, nil>] Known group names.
31
+ # @param order [Array<String>] Preferred named-group order.
32
+ # @return [Array<String, nil>] Ordered group names.
33
+ def self.ordered_group_names(names, order)
34
+ (order & names) + (names.compact - order).sort + ([nil] & names)
43
35
  end
36
+ private_class_method :ordered_group_names
44
37
  end
45
38
  end
46
39
  end
@@ -8,43 +8,31 @@ module YARD
8
8
  #
9
9
  # @param object [YARD::CodeObjects::Base] Object whose tags are being rendered.
10
10
  # @return [String] Markdown representation of the object's tags.
11
- def render_tags(object)
11
+ def self.render_tags(object)
12
12
  example_tags, regular_tags = object.tags.partition { |tag| tag.tag_name == "example" }
13
- lines = regular_tags.map { |tag| "- #{format_tag(tag)}" }
14
-
15
- example_tags.each do |tag|
16
- lines << nil unless lines.empty?
17
- title = tag.name.to_s.rstrip.empty? ? "**@example**" : "**@example #{tag.name}**"
18
- lines << title
19
- lines << "```ruby"
20
- lines << tag.text.to_s.rstrip
21
- lines << "```"
13
+ regular = regular_tags.map { |tag| "- #{format_tag(tag)}" }.join("\n")
14
+ examples = example_tags.map do |tag|
15
+ name = tag.name.to_s.rstrip
16
+ title = name.empty? ? "**@example**" : "**@example #{name}**"
17
+ [title, "```ruby", tag.text.to_s.rstrip, "```"].join("\n")
22
18
  end
23
-
24
- lines.join("\n")
19
+ [regular, examples].reject(&:empty?).join("\n\n")
25
20
  end
26
21
 
27
22
  # Formats a non-example YARD tag as a Markdown list item body.
28
23
  #
29
24
  # @param tag [YARD::Tags::Tag] Non-example tag being converted into list item text.
30
25
  # @return [String] Markdown representation of the tag.
31
- def format_tag(tag)
32
- parts = ["**@#{tag.tag_name}**"]
33
- parts << "`#{tag.name}`" unless tag.name.to_s.lstrip.empty?
34
-
35
- cleaned_types = normalized_tag_types(tag.types)
36
- parts << "[#{cleaned_types.join(", ")}]" unless cleaned_types.empty?
37
- parts << tag.text.strip unless tag.text.to_s.lstrip.empty?
38
-
39
- parts.join(" ")
40
- end
41
-
42
- # Normalizes tag type declarations into printable strings.
43
- #
44
- # @param types [Array<Object>, nil] Raw tag types from YARD.
45
- # @return [Array<String>] Cleaned type strings.
46
- def normalized_tag_types(types)
47
- Array(types).map(&:to_s).map(&:strip).reject(&:empty?)
26
+ def self.format_tag(tag)
27
+ name = tag.name.to_s
28
+ text = tag.text.to_s
29
+ types = Array(tag.types).map { |type| type.to_s.strip }.reject(&:empty?)
30
+ [
31
+ "**@#{tag.tag_name}**",
32
+ ("`#{name}`" unless name.lstrip.empty?),
33
+ ("[#{types.join(", ")}]" unless types.empty?),
34
+ (text.strip unless text.lstrip.empty?)
35
+ ].compact.join(" ")
48
36
  end
49
37
  end
50
38
  end
@@ -11,20 +11,47 @@ module YARD
11
11
  # @param checksums [Hash, nil] Previously generated file checksums.
12
12
  # @return [void]
13
13
  def run_generate(checksums)
14
- if options.format == :markdown
15
- existing = options.files.map { |file| File.expand_path(file.filename) }
16
- exclusions = excluded.map { |path| Regexp.new(path, Regexp::IGNORECASE) }
17
- output = File.expand_path(options.serializer.basepath)
18
- markdown_files = Dir.glob("**/*").grep(FILE_PATTERN)
19
- .reject { |file| File.basename(file).start_with?("_") }
20
- .reject { |file| exclusions.any? { |pattern| pattern.match?(file) } }
21
- .reject { |file| File.expand_path(file).start_with?("#{output}/") }
22
- .reject { |file| existing.include?(File.expand_path(file)) }
23
- add_extra_files(markdown_files)
24
- end
14
+ add_extra_files(markdown_files) if options.format == :markdown
25
15
 
26
16
  super
27
17
  end
18
+
19
+ # Returns project Markdown files that YARD has not already discovered.
20
+ #
21
+ # @return [Array<String>] Markdown file paths to add.
22
+ def markdown_files
23
+ Dir.glob("**/*").grep(FILE_PATTERN)
24
+ .reject { |file| File.basename(file).start_with?("_") }
25
+ .reject { |file| excluded_file?(file) }
26
+ .reject { |file| output_file?(file) }
27
+ .reject { |file| existing_file?(file) }
28
+ end
29
+
30
+ # Returns whether a file matches a configured exclusion.
31
+ #
32
+ # @param file [String] Candidate file path.
33
+ # @return [Boolean] True when excluded.
34
+ def excluded_file?(file)
35
+ excluded.any? { |path| Regexp.new(path, Regexp::IGNORECASE).match?(file) }
36
+ end
37
+
38
+ # Returns whether a file is inside the output directory.
39
+ #
40
+ # @param file [String] Candidate file path.
41
+ # @return [Boolean] True when generated output would contain the file.
42
+ def output_file?(file)
43
+ output = File.expand_path(options.serializer.basepath)
44
+ File.expand_path(file).start_with?("#{output}/")
45
+ end
46
+
47
+ # Returns whether YARD has already discovered a file.
48
+ #
49
+ # @param file [String] Candidate file path.
50
+ # @return [Boolean] True when the file already exists in YARD options.
51
+ def existing_file?(file)
52
+ expanded_file = File.expand_path(file)
53
+ options.files.any? { |existing| File.expand_path(existing.filename) == expanded_file }
54
+ end
28
55
  end
29
56
  end
30
57
  end
@@ -3,14 +3,13 @@
3
3
  require "csv"
4
4
 
5
5
  include YARD::Markdown::ObjectListingHelper,
6
- YARD::Markdown::ArefHelper,
7
6
  YARD::Templates::Helpers::ModuleHelper
8
7
 
9
8
  # Prepares the markdown serializer and renders each object page.
10
9
  #
11
10
  # @return [void]
12
11
  def init
13
- options.objects = objects = run_verifier(options.objects).reject { |item| item.name == :root }
12
+ objects = run_verifier(options.objects).reject { |item| item.name == :root }
14
13
  files = Array(options.files).select { |file| file.filename.match?(YARD::Markdown::FILE_PATTERN) }
15
14
  options.copied_file_aliases = files.to_h do |file|
16
15
  path = Pathname.new(file.filename).cleanpath.to_s
@@ -54,11 +53,9 @@ def serialize_index(objects, files)
54
53
  CSV.open(filepath, "wb") do |csv|
55
54
  csv << %w[name type path]
56
55
 
57
- files.each { |file| csv << [file.title, "File", file.filename] }
56
+ files.each { |file| csv << [file.filename, "File", file.filename] }
58
57
 
59
58
  objects.each do |object|
60
- next if object.name == :root
61
-
62
59
  if object.type == :class
63
60
  csv << [object.path, "Class", options.serializer.serialized_path(object)]
64
61
  elsif object.type == :module
@@ -66,7 +63,7 @@ def serialize_index(objects, files)
66
63
  end
67
64
 
68
65
  [
69
- ["Constant", constant_listing(object)],
66
+ ["Constant", YARD::Markdown::ObjectListingHelper.constant_listing(object)],
70
67
  ["Method", public_instance_methods(object)],
71
68
  ["Method", public_class_methods(object)],
72
69
  ["Attribute", attr_listing(object)]
@@ -75,7 +72,7 @@ def serialize_index(objects, files)
75
72
  csv << [
76
73
  "#{object.path}.#{item.name(false)}",
77
74
  type,
78
- options.serializer.serialized_path(object) + "#" + aref(item)
75
+ options.serializer.serialized_path(object) + "#" + YARD::Markdown::ArefHelper.aref(item)
79
76
  ]
80
77
  end
81
78
  end
@@ -1,10 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- include YARD::Markdown::TagFormattingHelper,
4
- YARD::Markdown::SectionAssemblyHelper,
5
- YARD::Markdown::MetadataSectionHelper,
3
+ include YARD::Markdown::MetadataSectionHelper,
6
4
  YARD::Markdown::ObjectListingHelper,
7
- YARD::Markdown::MethodPresentationHelper,
8
5
  YARD::Markdown::LinkNormalizationHelper,
9
6
  YARD::Markdown::HeadingHelper,
10
7
  YARD::Markdown::DocumentationHelper,
@@ -45,48 +42,49 @@ end
45
42
  #
46
43
  # @return [String] Markdown heading section.
47
44
  def header
48
- render_section_content(heading_with_anchors("# #{object.type.to_s.capitalize} #{object.path}", object))
45
+ YARD::Markdown::SectionAssemblyHelper.render_section_content(heading_with_anchors("# #{object.type.to_s.capitalize} #{object.path}", object))
49
46
  end
50
47
 
51
48
  # Renders metadata for the current object.
52
49
  #
53
50
  # @return [String] Markdown metadata section.
54
51
  def metadata
55
- render_section_content(object_metadata(object))
52
+ YARD::Markdown::SectionAssemblyHelper.render_section_content(object_metadata(object))
56
53
  end
57
54
 
58
55
  # Renders the object's docstring as markdown.
59
56
  #
60
57
  # @return [String] Markdown docstring section.
61
58
  def docstring_section
62
- render_section_content(rdoc_to_md(object.docstring))
59
+ YARD::Markdown::SectionAssemblyHelper.render_section_content(rdoc_to_md(object.docstring))
63
60
  end
64
61
 
65
62
  # Renders the object's YARD tags.
66
63
  #
67
64
  # @return [String] Markdown tags section.
68
65
  def tags_section
69
- render_section_content(render_tags(object))
66
+ YARD::Markdown::SectionAssemblyHelper.render_section_content(YARD::Markdown::TagFormattingHelper.render_tags(object))
70
67
  end
71
68
 
72
69
  # Renders the constants section when visible constants are present.
73
70
  #
74
71
  # @return [String] Markdown constants section, or an empty string.
75
72
  def constants_section
76
- constants = constant_listing(object).reject { |item| hidden_object?(item) }
73
+ constants = YARD::Markdown::ObjectListingHelper.constant_listing(object)
74
+ .reject { |item| YARD::Markdown::ObjectListingHelper.hidden_object?(item) }
77
75
  return "" unless constants.any?
78
76
 
79
- render_section_content(render_constants(constants, Array(object.groups)))
77
+ YARD::Markdown::SectionAssemblyHelper.render_section_content(render_constants(constants, Array(object.groups)))
80
78
  end
81
79
 
82
80
  # Renders the attributes section when visible attributes are present.
83
81
  #
84
82
  # @return [String] Markdown attributes section, or an empty string.
85
83
  def attributes_section
86
- attrs = attr_listing(object).reject { |item| hidden_object?(item) }
84
+ attrs = attr_listing(object).reject { |item| YARD::Markdown::ObjectListingHelper.hidden_object?(item) }
87
85
  return "" unless attrs.any?
88
86
 
89
- render_section_content(render_attributes(attrs, Array(object.groups)))
87
+ YARD::Markdown::SectionAssemblyHelper.render_section_content(render_attributes(attrs, Array(object.groups)))
90
88
  end
91
89
 
92
90
  # Renders the public class methods section when methods are present.
@@ -96,7 +94,7 @@ def public_class_methods_section
96
94
  methods = public_class_methods(object)
97
95
  return "" unless methods.any?
98
96
 
99
- render_section_content(render_methods("Public Class Methods", methods, Array(object.groups)))
97
+ YARD::Markdown::SectionAssemblyHelper.render_section_content(render_methods("Public Class Methods", methods, Array(object.groups)))
100
98
  end
101
99
 
102
100
  # Renders the public instance methods section when methods are present.
@@ -106,5 +104,5 @@ def public_instance_methods_section
106
104
  methods = public_instance_methods(object)
107
105
  return "" unless methods.any?
108
106
 
109
- render_section_content(render_methods("Public Instance Methods", methods, Array(object.groups)))
107
+ YARD::Markdown::SectionAssemblyHelper.render_section_content(render_methods("Public Instance Methods", methods, Array(object.groups)))
110
108
  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.9.0
4
+ version: 0.9.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stanislav (Stas) Katkov