rdoc-markdown 0.21.1 → 0.22.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.
@@ -1,200 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Converts RDoc HTML and normalizes generated Markdown.
4
- class RDoc::Generator::Markdown::Conversion
5
- # Converts RDoc HTML into GitHub-flavored Markdown.
6
- #
7
- # @param input [String] RDoc HTML fragment.
8
- # @param heading_level_offset [Integer] Heading levels to add.
9
- #
10
- # @return [String] Markdown with normalized links and no trailing whitespace.
11
- def self.markdownify(input, heading_level_offset: 0)
12
- fragment = normalized_html_fragment(input, heading_level_offset)
13
- anchor_aliases = tokenize_legacy_anchors(fragment.css("span.legacy-anchor[id]"))
14
- document = fragment.document
15
- fragment.css("a").each { |link| normalize_link(link, document) }
16
- markdown = ReverseMarkdown.convert(fragment, github_flavored: true).dup
17
- markdown.gsub!(Regexp.union(anchor_aliases.keys), anchor_aliases)
18
- normalize_definition_lists(markdown).rstrip
19
- end
20
-
21
- class << self
22
- private
23
-
24
- # Normalizes HTML constructs that reverse_markdown cannot preserve directly.
25
- #
26
- # @param input [String] RDoc HTML fragment.
27
- # @param heading_level_offset [Integer] Heading levels to add to actual HTML headings.
28
- #
29
- # @return [Nokogiri::HTML4::DocumentFragment] Normalized fragment.
30
- def normalized_html_fragment(input, heading_level_offset)
31
- fragment = Nokogiri::HTML.fragment(input)
32
- context_anchors, content_anchors = fragment.css("span.legacy-anchor[id]").partition do |span|
33
- context_anchor?(span)
34
- end
35
- context_anchors.each(&:remove)
36
- content_anchors.each do |span|
37
- heading = span.next_element
38
- heading.add_child(span) if heading&.name&.match?(/\Ah[1-6]\z/)
39
- end
40
- normalize_pre_blocks(fragment)
41
- document = fragment.document
42
- fragment.css("h1, h2, h3, h4, h5, h6").each do |heading|
43
- normalize_heading(heading, document, heading_level_offset)
44
- end
45
- fragment
46
- end
47
-
48
- # Checks whether an anchor identifies the surrounding class or module.
49
- #
50
- # @param span [Nokogiri::XML::Element] Legacy anchor span.
51
- #
52
- # @return [Boolean] Whether the anchor belongs to a top-level context heading.
53
- def context_anchor?(span)
54
- span.next_element&.name == "h1" && span["id"].match?(/\A(?:class|module)-/)
55
- end
56
-
57
- # Preserves simple language classes and literal preformatted text.
58
- #
59
- # @param fragment [Nokogiri::HTML4::DocumentFragment] HTML fragment.
60
- #
61
- # @return [void]
62
- def normalize_pre_blocks(fragment)
63
- fragment.css("pre").each do |pre|
64
- language = pre["class"].to_s[/\A(?!highlight\z)[A-Za-z][A-Za-z0-9_+-]*\z/]
65
- pre["class"] = "brush: #{language};" if language
66
- pre.content = pre.text
67
- end
68
- end
69
-
70
- # Normalizes one heading's level and leading fragment link.
71
- #
72
- # @param heading [Nokogiri::XML::Element] Heading element.
73
- # @param document [Nokogiri::HTML4::Document] Owning document.
74
- # @param heading_level_offset [Integer] Heading levels to add, capped at six.
75
- #
76
- # @return [void]
77
- def normalize_heading(heading, document, heading_level_offset)
78
- heading.name = "h#{[Integer(heading.name.delete_prefix("h")) + heading_level_offset, 6].min}"
79
- link = heading.xpath("./a[starts-with(@href, '#') and string-length(@href) > 1]").find do |anchor|
80
- leading_heading_link?(anchor)
81
- end
82
- return unless link
83
-
84
- id = link["href"].delete_prefix("#")
85
- link.replace(link.children)
86
- return if id == RDoc::Text.to_anchor(heading.text)
87
-
88
- heading.add_child(document.create_element("span", "class" => "legacy-anchor", "id" => id))
89
- end
90
-
91
- # Checks whether a link is the first visible content in its heading.
92
- #
93
- # @param anchor [Nokogiri::XML::Element] Candidate heading link.
94
- #
95
- # @return [Boolean] Whether no visible text precedes the link.
96
- def leading_heading_link?(anchor)
97
- anchor.text.match?(/\S/) &&
98
- anchor.xpath("preceding-sibling::node()").none? { |sibling| sibling.text.match?(/\S/) }
99
- end
100
-
101
- # Replaces legacy anchors with tokens that survive reverse_markdown.
102
- #
103
- # @param anchors [Nokogiri::XML::NodeSet] Legacy anchor spans.
104
- #
105
- # @return [Hash{String => String}] Token-to-anchor HTML lookup.
106
- def tokenize_legacy_anchors(anchors)
107
- anchors.each.with_index.to_h do |span, index|
108
- token = "RDocMarkdownAnchor#{index}End"
109
- anchor = %(<a id="#{span["id"]}"></a>)
110
- span.replace(token)
111
- [token, anchor]
112
- end
113
- end
114
-
115
- # Normalizes one HTML link.
116
- #
117
- # @param link [Nokogiri::XML::Element] Link element.
118
- # @param document [Nokogiri::HTML4::Document] Owning document.
119
- #
120
- # @return [void]
121
- def normalize_link(link, document)
122
- receiver = link.text
123
- href = link["href"].to_s
124
-
125
- if index_reference?(receiver, href)
126
- link.replace(document.create_element("code") { |code| code.content = "#{receiver}[#{href}]" })
127
- elsif href.start_with?("www.")
128
- link["href"] = "https://#{href}"
129
- elsif !href.match?(/\A(?:https?:\/\/|mailto:|#)/i)
130
- link["href"] = normalized_link_target(href)
131
- end
132
- end
133
-
134
- # Checks whether RDoc encoded an indexing expression as a link.
135
- #
136
- # @param receiver [String] Visible receiver text.
137
- # @param href [String] Link target.
138
- #
139
- # @return [Boolean] Whether the link represents Ruby indexing syntax.
140
- def index_reference?(receiver, href)
141
- receiver.match?(/\A(?:[A-Z][A-Za-z0-9_]*(?:::[A-Z][A-Za-z0-9_]*)*|[a-z_][A-Za-z0-9_]*)\z/) &&
142
- href.match?(/\A(?::.+|".+")\z/)
143
- end
144
-
145
- # Rewrites an internal RDoc HTML target to its Markdown equivalent.
146
- #
147
- # @param href [String] Original link target.
148
- #
149
- # @return [String] Normalized target.
150
- def normalized_link_target(href)
151
- href.sub(/\.html(?=[?#]|\z)/i, ".md")
152
- .sub(%r{\A/(?=.+\.md(?:[?#]|\z))}, "")
153
- .sub(%r{\A((?:\.\./)*)(?:files|classes|modules)/(?=.+\.md(?:[?#]|\z))}, '\1')
154
- end
155
-
156
- # Converts RDoc definition-list code blocks into Markdown lists and separates
157
- # raw HTML lists from following Markdown without rewriting fenced examples.
158
- #
159
- # @param markdown [String] Markdown content.
160
- #
161
- # @return [String] Markdown with definition lists normalized.
162
- def normalize_definition_lists(markdown)
163
- markdown.gsub(%r{```[^\n]*\n(.+?)\n```|</dl>\n(?=\S)}m) do
164
- body = Regexp.last_match(1)
165
- next "</dl>\n\n" unless body
166
-
167
- converted = convert_definition_list_block(body)
168
- converted || Regexp.last_match
169
- end
170
- end
171
-
172
- # Converts a single definition-list code block.
173
- #
174
- # @param body [String] Code block body.
175
- #
176
- # @return [String, nil] Converted Markdown, or nil when the block is not a definition list.
177
- def convert_definition_list_block(body)
178
- lines = body.lines
179
- return nil unless lines.all? { |line| definition_list_line?(line) }
180
-
181
- lines.map do |line|
182
- stripped = line.strip
183
- next if stripped.empty?
184
- next "#{stripped.sub(/::\z/, "")}:" if stripped.end_with?("::")
185
-
186
- "- #{stripped.sub(/\A\*\s/, "")}"
187
- end.join("\n")
188
- end
189
-
190
- # Checks whether a line can appear in a converted definition list.
191
- #
192
- # @param line [String] Markdown line.
193
- #
194
- # @return [Boolean] True when the line matches RDoc definition-list output.
195
- def definition_list_line?(line)
196
- stripped = line.strip
197
- stripped.empty? || stripped.end_with?("::") || stripped.match?(/\A\*\s/)
198
- end
199
- end
200
- end
@@ -1,41 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Prevents RDoc from linking to code objects omitted from Markdown output.
4
- module RDoc::Generator::Markdown::CrossrefExtension
5
- # Applies Markdown cross-reference state while rendering a description.
6
- #
7
- # @param cross_reference [RDoc::CrossReference] Resolver scoped to the formatter.
8
- # @param output_object_ids [Set<Integer>] Object IDs emitted by the generator.
9
- #
10
- # @return [Object] Value returned by the block.
11
- def with_markdown_cross_references(cross_reference, output_object_ids)
12
- @markdown_cross_reference = cross_reference
13
- @markdown_output_object_ids = output_object_ids
14
- yield
15
- ensure
16
- @markdown_cross_reference = nil
17
- end
18
-
19
- # Renders a cross-reference only when its owning object is emitted.
20
- #
21
- # @param name [String, nil] Cross-reference target.
22
- # @param text [String] Visible link text.
23
- # @param code [Boolean] Whether to format code objects as code.
24
- # @param rdoc_ref [Boolean] Whether the target uses the rdoc-ref scheme.
25
- #
26
- # @return [String] HTML link or unlinked text.
27
- def link(name, text, code = true, rdoc_ref: false)
28
- return super unless @markdown_cross_reference
29
-
30
- ref = @markdown_cross_reference.resolve(name) if name
31
- return super unless RDoc::CodeObject === ref
32
-
33
- context = ref
34
- context = context.parent until RDoc::ClassModule === context || RDoc::TopLevel === context
35
- return super if @markdown_output_object_ids.include?(context.object_id)
36
-
37
- return text if RDoc::TopLevel === ref || !code
38
-
39
- "<code>#{text}</code>"
40
- end
41
- end