rdoc-markdown 0.17.2 → 0.19.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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +8 -1
  3. data/README.md +19 -82
  4. data/lib/markdown.rb +0 -4
  5. data/lib/rdoc/discover.rb +1 -7
  6. data/lib/rdoc/generator/markdown/conversion.rb +196 -0
  7. data/lib/rdoc/generator/markdown/crossref.rb +13 -5
  8. data/lib/rdoc/generator/markdown/descriptions.rb +163 -0
  9. data/lib/rdoc/generator/markdown/index.rb +47 -0
  10. data/lib/rdoc/generator/markdown/paths.rb +118 -0
  11. data/lib/rdoc/generator/markdown/selection.rb +39 -0
  12. data/lib/rdoc/generator/markdown/signatures.rb +174 -0
  13. data/lib/rdoc/generator/markdown.rb +80 -680
  14. data/lib/rdoc/markdown/version.rb +1 -1
  15. data/lib/templates/classfile.md.erb +14 -11
  16. metadata +19 -43
  17. data/.editorconfig +0 -13
  18. data/.erb_lint.yml +0 -36
  19. data/.erb_linters/no_embedded_assets.rb +0 -29
  20. data/.erb_linters/non_raw_html.rb +0 -29
  21. data/.standard.yml +0 -3
  22. data/.yard-lint.yml +0 -290
  23. data/AGENTS.md +0 -50
  24. data/CODE_OF_CONDUCT.md +0 -84
  25. data/Gemfile +0 -13
  26. data/Gemfile.lock +0 -202
  27. data/Rakefile +0 -296
  28. data/example/Bird.md +0 -21
  29. data/example/Duck.md +0 -56
  30. data/example/Object.md +0 -8
  31. data/example/Waterfowl.md +0 -9
  32. data/example/index.csv +0 -16
  33. data/example/jekyll-seo-tag/Jekyll/SeoTag/AuthorDrop.md +0 -35
  34. data/example/jekyll-seo-tag/Jekyll/SeoTag/Drop.md +0 -108
  35. data/example/jekyll-seo-tag/Jekyll/SeoTag/Filters.md +0 -8
  36. data/example/jekyll-seo-tag/Jekyll/SeoTag/ImageDrop.md +0 -29
  37. data/example/jekyll-seo-tag/Jekyll/SeoTag/JSONLD.md +0 -15
  38. data/example/jekyll-seo-tag/Jekyll/SeoTag/JSONLDDrop.md +0 -34
  39. data/example/jekyll-seo-tag/Jekyll/SeoTag/UrlHelper.md +0 -4
  40. data/example/jekyll-seo-tag/Jekyll/SeoTag.md +0 -48
  41. data/example/jekyll-seo-tag/Jekyll.md +0 -3
  42. data/example/jekyll-seo-tag/Liquid/Tag.md +0 -3
  43. data/example/jekyll-seo-tag/Liquid.md +0 -4
  44. data/example/jekyll-seo-tag/index.csv +0 -60
  45. data/mutant.yml +0 -15
  46. data/rdoc-markdown.gemspec +0 -46
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Resolves source and generated documentation paths.
4
+ module RDoc::Generator::Markdown::Paths
5
+ # Converts a qualified object name into a Markdown path.
6
+ #
7
+ # @param class_name [String] Qualified class or module name.
8
+ #
9
+ # @return [String] Relative Markdown path.
10
+ def turn_to_path(class_name)
11
+ "#{class_name.gsub("::", "/")}.md"
12
+ end
13
+
14
+ # Builds the Markdown output path for an RDoc page.
15
+ #
16
+ # @param page [RDoc::TopLevel] Page object to render.
17
+ #
18
+ # @return [String] Relative Markdown path.
19
+ def page_output_path(page)
20
+ relative_name = page.relative_name
21
+ source_path = normalize_input_path_for_output(relative_name)
22
+ return source_path if relative_name.match?(/\.(?:md|markdown)\z/i)
23
+
24
+ dirname = File.dirname(source_path)
25
+ basename = "#{File.basename(source_path).tr(".", "_")}.md"
26
+
27
+ return basename if dirname == "."
28
+
29
+ "#{dirname}/#{basename}"
30
+ end
31
+
32
+ # Returns the canonical Markdown path for a class or module.
33
+ #
34
+ # @param code_object [RDoc::Context] Class or module object.
35
+ #
36
+ # @return [String] Relative Markdown path.
37
+ def output_path_for(code_object)
38
+ turn_to_path(code_object.full_name)
39
+ end
40
+
41
+ # Rewrites local Markdown links relative to the current output file.
42
+ #
43
+ # @param markdown [String] Markdown content.
44
+ # @param current_output_path [String] Output path for the file being written.
45
+ #
46
+ # @return [String] Markdown with normalized internal links.
47
+ def normalize_internal_links(markdown, current_output_path:)
48
+ RDoc::Generator::Markdown::Paths.rewrite_internal_links(
49
+ markdown,
50
+ current_output_path,
51
+ generation_state
52
+ )
53
+ end
54
+
55
+ private :turn_to_path, :page_output_path, :output_path_for, :normalize_internal_links
56
+
57
+ # Rewrites links using explicit generated-path state.
58
+ #
59
+ # @param markdown [String] Markdown content.
60
+ # @param current_output_path [String] Output path for the current file.
61
+ # @param state [RDoc::Generator::Markdown::GenerationState] Prepared generation state.
62
+ #
63
+ # @return [String] Markdown with normalized internal links.
64
+ def self.rewrite_internal_links(markdown, current_output_path, state)
65
+ current_dir = Pathname.new(current_output_path).dirname
66
+
67
+ markdown.gsub(%r{\]\(([^)]+)\)}) do
68
+ target = Regexp.last_match(1)
69
+ path = target.sub(/[?#].*\z/, "")
70
+ suffix = target[path.length..]
71
+
72
+ resolved = resolve_output_path_from(path, current_dir, state)
73
+ rewritten = resolved ? Pathname.new(resolved).relative_path_from(current_dir) : path
74
+ "](#{rewritten}#{suffix})"
75
+ end
76
+ end
77
+
78
+ # Resolves a link against explicit generated-path state.
79
+ #
80
+ # @param path [String] Link path from Markdown content.
81
+ # @param current_dir [Pathname] Directory of the current output file.
82
+ # @param state [RDoc::Generator::Markdown::GenerationState] Prepared generation state.
83
+ #
84
+ # @return [String, nil] Resolved output path, or nil when unresolved.
85
+ def self.resolve_output_path_from(path, current_dir, state)
86
+ candidates = [path, path.delete_prefix("#{state.root_path_segment}/")]
87
+ candidates += candidates.map { |candidate| candidate.sub(/_(md|markdown)\.md\z/i, '.\1') }
88
+ expanded_candidates = candidates.map { |candidate| current_dir.join(candidate).cleanpath.to_s }
89
+
90
+ (candidates + expanded_candidates).find { |candidate| state.known_output_paths.include?(candidate) }
91
+ end
92
+
93
+ # Normalizes an input filename into an output-relative source path.
94
+ #
95
+ # @param path [String] RDoc input path.
96
+ #
97
+ # @return [String] Normalized path without root prefixes.
98
+ def normalize_input_path_for_output(path)
99
+ RDoc::Generator::Markdown::Paths.normalize_input_path(path, source_dir)
100
+ end
101
+
102
+ private :normalize_input_path_for_output
103
+
104
+ # Normalizes an input path against an explicit source directory.
105
+ #
106
+ # @param path [String] RDoc input path.
107
+ # @param source_dir [String] Absolute documentation source directory.
108
+ #
109
+ # @return [String] Normalized path without root prefixes.
110
+ def self.normalize_input_path(path, source_dir)
111
+ normalized = path.tr("\\", "/").sub(%r{\A\./}, "")
112
+ normalized = normalized.sub(%r{\A#{Regexp.escape(source_dir)}/}, "")
113
+ normalized = normalized.sub(%r{\A/}, "")
114
+
115
+ root_basename = File.basename(source_dir)
116
+ normalized.sub(%r{\A#{Regexp.escape(root_basename)}/}, "")
117
+ end
118
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Selects RDoc objects that produce Markdown output.
4
+ module RDoc::Generator::Markdown::Selection
5
+ # Returns sorted classes and modules with renderable content.
6
+ #
7
+ # @param store [RDoc::Store] Documentation store.
8
+ #
9
+ # @return [Array<RDoc::Context>] Selected classes and modules.
10
+ def self.classes(store)
11
+ store.unique_classes_and_modules.select(&:display?).select { |klass| renderable_class?(klass) }.sort
12
+ end
13
+
14
+ # Checks whether a class or module has content worth rendering.
15
+ #
16
+ # @param klass [RDoc::Context] Candidate class or module.
17
+ #
18
+ # @return [Boolean] Whether the object should produce a page.
19
+ def self.renderable_class?(klass)
20
+ klass.in_files.any? ||
21
+ klass.documented? ||
22
+ klass.includes.any? ||
23
+ klass.method_list.any?(&:display?) ||
24
+ klass.constants.any?(&:display?) ||
25
+ klass.attributes.any?(&:display?) ||
26
+ klass.sections.any? { |section| section.title.to_s.match?(/\S/) || !section.to_document.empty? }
27
+ end
28
+
29
+ # Returns sorted documentation pages supported by the generator.
30
+ #
31
+ # @param store [RDoc::Store] Documentation store.
32
+ #
33
+ # @return [Array<RDoc::TopLevel>] Selected pages.
34
+ def self.pages(store)
35
+ store.all_files.select(&:text?).select(&:display?)
36
+ .select { |page| page.relative_name.match?(/\.(?:md|markdown|rdoc)\z/i) }
37
+ .sort_by(&:base_name)
38
+ end
39
+ end
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Normalizes RDoc and RBS method signatures for headings.
4
+ module RDoc::Generator::Markdown::Signatures
5
+ # Signature opening delimiters and their matching closers.
6
+ DELIMITER_PAIRS = {
7
+ "(" => ")", "[" => "]", "{" => "}"
8
+ }.freeze
9
+
10
+ # Builds the visible method signature used in headings.
11
+ #
12
+ # @param method [RDoc::AnyMethod] Method object to render.
13
+ #
14
+ # @return [String] Normalized method signature.
15
+ def method_signature(method)
16
+ RDoc::Generator::Markdown::Signatures.render_method_signature(method, store)
17
+ end
18
+
19
+ private :method_signature
20
+
21
+ # Builds a method signature from RDoc and store metadata.
22
+ #
23
+ # @param method [RDoc::AnyMethod] Method object to render.
24
+ # @param store [RDoc::Store] Documentation store with sidecar signatures.
25
+ #
26
+ # @return [String] Normalized method signature.
27
+ def self.render_method_signature(method, store)
28
+ signatures = method.type_signature_lines || store.rbs_signature_for(method) || [method.param_seq]
29
+
30
+ signatures = signatures.filter_map do |signature|
31
+ next unless signature&.match?(/\S/)
32
+
33
+ signature = signature.gsub("->", " -> ")
34
+ signature = signature.gsub(/\s+/, " ").strip
35
+ signature = " #{signature}" if signature.start_with?("->")
36
+ merge_method_signature_arguments(signature, method.params)
37
+ end
38
+
39
+ return "()" if signatures.empty?
40
+
41
+ signatures.join(" | ")
42
+ end
43
+
44
+ # Merges RDoc parameter names into a type-only signature.
45
+ #
46
+ # @param signature [String] Method signature from RDoc call sequence.
47
+ # @param raw_params [String, nil] Method parameter list from RDoc.
48
+ #
49
+ # @return [String] Signature with names added when safe.
50
+ def self.merge_method_signature_arguments(signature, raw_params)
51
+ params = normalized_method_params(raw_params)
52
+
53
+ signature_args, signature_suffix = split_signature_arguments_and_suffix(signature)
54
+ return signature unless signature_args
55
+
56
+ param_parts = split_signature_list(params)
57
+ signature_parts = split_signature_list(signature_args)
58
+ return signature unless param_parts.length.eql?(signature_parts.length)
59
+
60
+ merged = merged_signature(param_parts, signature_parts, signature_suffix)
61
+ merged || signature
62
+ end
63
+
64
+ # Merges matching parameter and signature fragments.
65
+ #
66
+ # @param param_parts [Array<String>] RDoc parameter fragments.
67
+ # @param signature_parts [Array<String>] Signature type fragments.
68
+ # @param signature_suffix [String] Text following the argument list.
69
+ #
70
+ # @return [String, nil] Merged signature, or nil when merging is unsafe or unnecessary.
71
+ def self.merged_signature(param_parts, signature_parts, signature_suffix)
72
+ param_names = param_parts.map { |part| extract_parameter_name(part) }
73
+ return if param_names.any?(&:nil?)
74
+ return if signature_parts.zip(param_names).all? { |part, name| signature_part_mentions_name?(part, name) }
75
+
76
+ merged_args = param_parts.zip(signature_parts).map do |param, type|
77
+ separator = param.end_with?(":") ? " " : ": "
78
+ "#{param}#{separator}#{type}"
79
+ end
80
+
81
+ "(#{merged_args.join(", ")})#{signature_suffix}"
82
+ end
83
+
84
+ # Normalizes RDoc's raw parameter string.
85
+ #
86
+ # @param raw_params [String, nil] Parameter list from RDoc.
87
+ #
88
+ # @return [String] Parameter list without outer parentheses.
89
+ def self.normalized_method_params(raw_params)
90
+ params = raw_params.to_s.strip
91
+ params = params[1...-1] if params.start_with?("(") && params.end_with?(")")
92
+
93
+ params
94
+ end
95
+
96
+ # Splits a parenthesized signature into arguments and suffix.
97
+ #
98
+ # @param signature [String] Method signature.
99
+ #
100
+ # @return [Array<String>, nil] Argument text and suffix, or nil when not parenthesized.
101
+ def self.split_signature_arguments_and_suffix(signature)
102
+ return unless signature.start_with?("(")
103
+
104
+ depth = 0
105
+
106
+ signature.each_char.with_index do |char, index|
107
+ depth += 1 if char == "("
108
+
109
+ next unless char == ")"
110
+
111
+ depth -= 1
112
+ return [signature[1...index], signature[(index + 1)..]] if depth.zero?
113
+ end
114
+ end
115
+
116
+ # Splits a comma-separated signature list while preserving nested groups.
117
+ #
118
+ # @param list [String] Signature argument list.
119
+ #
120
+ # @return [Array<String>] Signature parts.
121
+ def self.split_signature_list(list)
122
+ parts = []
123
+ current = +""
124
+ delimiters = []
125
+
126
+ list.each_char do |char|
127
+ if top_level_signature_separator?(char, delimiters)
128
+ parts << current.strip
129
+ current.clear
130
+ else
131
+ closing = DELIMITER_PAIRS[char]
132
+ if closing
133
+ delimiters << closing
134
+ elsif char == delimiters.last
135
+ delimiters.pop
136
+ end
137
+ current << char
138
+ end
139
+ end
140
+
141
+ parts << current.strip unless current.empty?
142
+ parts
143
+ end
144
+
145
+ # Checks whether a character separates top-level signature parts.
146
+ #
147
+ # @param char [String] Current signature character.
148
+ # @param delimiters [Array<String>] Expected closing delimiters.
149
+ #
150
+ # @return [Boolean] Whether the character is a top-level comma.
151
+ def self.top_level_signature_separator?(char, delimiters)
152
+ char == "," && delimiters.empty?
153
+ end
154
+
155
+ # Extracts a bare Ruby parameter name from a parameter fragment.
156
+ #
157
+ # @param parameter [String] Parameter fragment.
158
+ #
159
+ # @return [String, nil] Parameter name, or nil when invalid.
160
+ def self.extract_parameter_name(parameter)
161
+ match = parameter.match(/\A(?:\*\*|\*|&)?([a-z_]\w*):?\z/)
162
+ match && match[1]
163
+ end
164
+
165
+ # Checks whether a signature fragment already includes a parameter name.
166
+ #
167
+ # @param text [String] Signature fragment.
168
+ # @param name [String] Parameter name.
169
+ #
170
+ # @return [Boolean] True when the name appears as a standalone word.
171
+ def self.signature_part_mentions_name?(text, name)
172
+ text.match?(/(?<!\w)#{name}(?!\w)/)
173
+ end
174
+ end