rdoc-markdown 0.21.2 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4 -1
- data/README.md +8 -8
- data/lib/rdoc/discover.rb +3 -1
- data/lib/rdoc/generator/markdown/blocks.rb +268 -0
- data/lib/rdoc/generator/markdown/descriptions.rb +36 -60
- data/lib/rdoc/generator/markdown/formatter.rb +44 -0
- data/lib/rdoc/generator/markdown/html_input.rb +126 -0
- data/lib/rdoc/generator/markdown/index.rb +4 -5
- data/lib/rdoc/generator/markdown/inline.rb +173 -0
- data/lib/rdoc/generator/markdown/inline_html.rb +56 -0
- data/lib/rdoc/generator/markdown/markdown_links.rb +101 -0
- data/lib/rdoc/generator/markdown/paths.rb +10 -41
- data/lib/rdoc/generator/markdown/references.rb +164 -0
- data/lib/rdoc/generator/markdown/signatures.rb +10 -74
- data/lib/rdoc/generator/markdown/syntax.rb +135 -0
- data/lib/rdoc/generator/markdown.rb +28 -46
- data/lib/rdoc/markdown/version.rb +1 -1
- data/lib/templates/classfile.md.erb +10 -5
- metadata +18 -41
- data/lib/markdown.rb +0 -3
- data/lib/rdoc/generator/markdown/conversion.rb +0 -200
- data/lib/rdoc/generator/markdown/crossref.rb +0 -41
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class RDoc::Generator::Markdown
|
|
4
|
+
# Native ToMarkdown inline callbacks and explicit-link label boundaries.
|
|
5
|
+
module Inline
|
|
6
|
+
# Escapes plain content without changing inline code or resolved references.
|
|
7
|
+
# @param text [String] Literal content.
|
|
8
|
+
# @return [String] GFM text.
|
|
9
|
+
def convert_string(text)
|
|
10
|
+
# Final definition HTML keeps literal backslashes before semicolons.
|
|
11
|
+
text = text.gsub("\\;") { "\\\\;" } if definition_body
|
|
12
|
+
Syntax.escape(text)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Emits a plain-text node from RDoc's inline parser.
|
|
16
|
+
# @param text [String] Literal content.
|
|
17
|
+
# @return [void]
|
|
18
|
+
def handle_PLAIN_TEXT(text)
|
|
19
|
+
emit_inline(convert_string(text))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Emits inline code, resolving only valid reference syntax outside link labels.
|
|
23
|
+
# @param text [String] Literal code.
|
|
24
|
+
# @return [void]
|
|
25
|
+
def handle_TT(text)
|
|
26
|
+
emit_inline(code_reference(text) || Syntax.code(text))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Emits a tidy link without permitting nested links in its label.
|
|
30
|
+
# @param nodes [Array<String, Hash>] Native inline label nodes.
|
|
31
|
+
# @param url [String] Link target.
|
|
32
|
+
# @return [void]
|
|
33
|
+
def handle_TIDYLINK(nodes, url)
|
|
34
|
+
first = nodes.first
|
|
35
|
+
label = if nodes.size == 1 && String === first && first.match?(/\A[*^]\d+\z/)
|
|
36
|
+
first[1..]
|
|
37
|
+
else
|
|
38
|
+
dup.inline_nodes(nodes)
|
|
39
|
+
end
|
|
40
|
+
emit_inline(gen_url(url, label))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Renders a label in an independent native inline buffer.
|
|
44
|
+
# @param nodes [Array<String, Hash>] Native inline label nodes.
|
|
45
|
+
# @return [String] Inline Markdown.
|
|
46
|
+
def inline_nodes(nodes)
|
|
47
|
+
@inline_output = +""
|
|
48
|
+
@in_link = true
|
|
49
|
+
@quote_converter ||= RDoc::Markup::ToHtml::QuoteConverter.new
|
|
50
|
+
traverse_inline_nodes(nodes)
|
|
51
|
+
@inline_output
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Uses RDoc's text-only quote policy without routing nodes through ToHtml.
|
|
55
|
+
# @param text [String] Inline RDoc source.
|
|
56
|
+
# @return [String] Inline Markdown with main's punctuation.
|
|
57
|
+
def handle_inline(text)
|
|
58
|
+
@quote_converter = RDoc::Markup::ToHtml::QuoteConverter.new
|
|
59
|
+
@markdown_links = MarkdownLinks.new(text)
|
|
60
|
+
super(@markdown_links.source)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Emits protected Markdown links and literal tag-like code without nested links.
|
|
64
|
+
# @param token [String] Protected Markdown token or literal user input.
|
|
65
|
+
# @return [String] Native Markdown or escaped literal text.
|
|
66
|
+
def handle_regexp_markdown(token)
|
|
67
|
+
link = @markdown_links&.[](token)
|
|
68
|
+
return token unless link
|
|
69
|
+
|
|
70
|
+
node = link.node
|
|
71
|
+
type = node.type
|
|
72
|
+
return Syntax.code(node.string_content) if type == :code
|
|
73
|
+
|
|
74
|
+
label = dup.markdown_label(link.label)
|
|
75
|
+
link_node = type == :link
|
|
76
|
+
target = rendering.reference.resolve(node.url) if link_node
|
|
77
|
+
node.url = target.as_href(rendering.context.path) if target && emitted_reference?(target)
|
|
78
|
+
(in_link_label? && link_node) ? label : MarkdownLinks.render(link, label)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Parses an original Markdown label with RDoc's surrounding inline semantics.
|
|
82
|
+
# @param text [String] Original link label source.
|
|
83
|
+
# @return [String] Inline Markdown without automatic nested links.
|
|
84
|
+
def markdown_label(text)
|
|
85
|
+
@markdown_links = MarkdownLinks.new(text)
|
|
86
|
+
inline_nodes(RDoc::Markup::InlineParser.new(@markdown_links.source).parse)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Preserves the typography aliases used by RDoc's HTML formatter.
|
|
90
|
+
# @param text [String] Matched punctuation or symbol shorthand.
|
|
91
|
+
# @return [String] Unicode punctuation with RDoc's encoding fallback.
|
|
92
|
+
def handle_regexp_HTML_CHARACTERS(text)
|
|
93
|
+
name = RDoc::Markup::ToHtml::HTML_CHARACTER_ALIASES.fetch(text)
|
|
94
|
+
RDoc::Markup::ToHtml::TO_HTML_CHARACTERS[text.encoding].fetch(name)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Converts quotes adjacent to words using RDoc's paragraph-scoped state.
|
|
98
|
+
# @param text [String] Matched quote.
|
|
99
|
+
# @return [String] Converted quote or escaped literal.
|
|
100
|
+
def handle_regexp_QUOTE_AFTER_WORD(text)
|
|
101
|
+
@quote_converter.convert(text, after_word: true) || convert_string(text)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Converts quotes not adjacent to words using the same paragraph state.
|
|
105
|
+
# @param text [String] Matched quote.
|
|
106
|
+
# @return [String] Converted quote or escaped literal.
|
|
107
|
+
def handle_regexp_QUOTE_NOT_AFTER_WORD(text)
|
|
108
|
+
@quote_converter.convert(text, after_word: false) || convert_string(text)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Escapes the visible part after RDoc's selective cross-reference suppression.
|
|
112
|
+
# @param text [String] Suppressed reference prefix.
|
|
113
|
+
# @return [String] Literal Markdown without the suppression backslash.
|
|
114
|
+
def handle_regexp_SUPPRESSED_CROSSREF(text)
|
|
115
|
+
convert_string(text.delete_prefix("\\"))
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Indicates the inline-only boundary where nested links are prohibited.
|
|
119
|
+
# @return [Boolean] Whether this visitor renders an explicit link label.
|
|
120
|
+
def in_link_label?
|
|
121
|
+
@in_link
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Formats native URL schemes and explicit RDoc references.
|
|
125
|
+
# @param url [String] RDoc target.
|
|
126
|
+
# @param label [String] Inline Markdown label.
|
|
127
|
+
# @return [String] Markdown link, image, or unresolved label.
|
|
128
|
+
def gen_url(url, label)
|
|
129
|
+
return explicit_reference(url.delete_prefix("rdoc-ref:"), label) if url.start_with?("rdoc-ref:")
|
|
130
|
+
return label_reference(url, label) if url.start_with?("rdoc-label:")
|
|
131
|
+
|
|
132
|
+
_, target, = parse_url(url)
|
|
133
|
+
unless target.match?(%r{\A(?:[a-z][a-z0-9+.-]*:|//|www\.|#)}i)
|
|
134
|
+
# Like page output paths, every dot in the file name becomes an underscore.
|
|
135
|
+
target = target.sub(%r{[^/]+\.(?:rb|rdoc)(?=[#?]|\z)}i) { |name| "#{name.tr(".", "_")}.md" }
|
|
136
|
+
end
|
|
137
|
+
return "" if target.match?(/\Ahttps?:.+\.(?:png|gif|jpe?g|bmp)\z/)
|
|
138
|
+
|
|
139
|
+
Syntax.link(label, target)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
private
|
|
143
|
+
|
|
144
|
+
# Keeps the optional source anchor without parsing HTML attribute strings.
|
|
145
|
+
# @param url [String] Native rdoc-label URL.
|
|
146
|
+
# @param label [String] Inline Markdown label.
|
|
147
|
+
# @return [String] Anchored Markdown reference.
|
|
148
|
+
def label_reference(url, label)
|
|
149
|
+
target, id = url.delete_prefix("rdoc-label:").split(":", 2)
|
|
150
|
+
if id&.start_with?("foot")
|
|
151
|
+
return %(<sup><a id="#{CGI.escapeHTML(id)}" href="##{CGI.escapeHTML(target)}">#{label}</a></sup>)
|
|
152
|
+
end
|
|
153
|
+
anchor = Syntax.anchor(id) if id
|
|
154
|
+
"#{anchor}#{Syntax.link(label, "##{target}")}"
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Registers native reference handlers on the markup instance initialized by super.
|
|
158
|
+
# @return [void]
|
|
159
|
+
def configure_inline
|
|
160
|
+
@in_link = false
|
|
161
|
+
@markup.add_regexp_handling(/\0RDocMarkdownX*\d+\0/, :markdown)
|
|
162
|
+
# ToRdoc suppresses every backslash escape; main suppresses only references.
|
|
163
|
+
@markup.regexp_handlings.reject! { |_, name| name == :SUPPRESSED_CROSSREF }
|
|
164
|
+
@markup.add_regexp_handling(/\\(?:[#:A-Z]|[a-z]+_[a-z0-9])/, :SUPPRESSED_CROSSREF)
|
|
165
|
+
@markup.add_regexp_handling(/(?:link:|https?:|mailto:|ftp:|irc:|www\.)#{RDoc::Markup::ToHtml::URL_CHARACTERS_REGEXP_STR}+\w/o, :HYPERLINK)
|
|
166
|
+
@markup.add_regexp_handling(Regexp.union(RDoc::Markup::ToHtml::HTML_CHARACTER_ALIASES.keys), :HTML_CHARACTERS)
|
|
167
|
+
@markup.add_regexp_handling(/\b['"`]/, :QUOTE_AFTER_WORD)
|
|
168
|
+
@markup.add_regexp_handling(/\B['"`]/, :QUOTE_NOT_AFTER_WORD)
|
|
169
|
+
@markup.add_regexp_handling(RDoc::CrossReference::CROSSREF_REGEXP, :CROSSREF)
|
|
170
|
+
@markup.add_regexp_handling(RDoc::CrossReference::ALL_CROSSREF_REGEXP, :CROSSREF) if options.hyperlink_all
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class RDoc::Generator::Markdown
|
|
4
|
+
# Emits final inline HTML inside definition containers, without HTML conversion.
|
|
5
|
+
class InlineHtml
|
|
6
|
+
# Native inline formatting tags; transparent AST containers have no wrapper.
|
|
7
|
+
TAGS = {document: nil, paragraph: nil, escaped: nil, emph: "em", strong: "strong", strikethrough: "del"}.freeze
|
|
8
|
+
|
|
9
|
+
# Serializes only inline syntax already produced by the native formatter.
|
|
10
|
+
# @param markdown [String] Escaped native inline Markdown.
|
|
11
|
+
# @return [String] Final inline HTML, with no paragraph wrapper.
|
|
12
|
+
def self.render(markdown)
|
|
13
|
+
new.render_node(Commonmarker.parse(markdown.encode(Encoding::UTF_8), options: {extension: {autolink: false}}))
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Emits one parsed inline node and its children.
|
|
17
|
+
# @param node [Commonmarker::Node] Native inline syntax node.
|
|
18
|
+
# @return [String] Final inline HTML.
|
|
19
|
+
def render_node(node)
|
|
20
|
+
type = node.type
|
|
21
|
+
case type
|
|
22
|
+
when :text, :code
|
|
23
|
+
text = CGI.escapeHTML(node.string_content)
|
|
24
|
+
(type == :code) ? "<code>#{text}</code>" : text
|
|
25
|
+
when :html_inline then node.to_commonmark.delete_suffix("\n")
|
|
26
|
+
when :softbreak then "\n"
|
|
27
|
+
when :linebreak then "<br>\n"
|
|
28
|
+
when :image then image(node)
|
|
29
|
+
else
|
|
30
|
+
content = node.map { |child| render_node(child) }.join
|
|
31
|
+
return %(<a href="#{CGI.escapeHTML(node.url)}"#{title(node)}>#{content}</a>) if type == :link
|
|
32
|
+
|
|
33
|
+
tag = TAGS.fetch(type)
|
|
34
|
+
tag ? "<#{tag}>#{content}</#{tag}>" : content
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
# An image's alt text is plain text, including formatted labels' contents.
|
|
41
|
+
# @param node [Commonmarker::Node] Image node.
|
|
42
|
+
# @return [String] Final image HTML.
|
|
43
|
+
def image(node)
|
|
44
|
+
alt = Syntax.node_text(node)
|
|
45
|
+
%(<img src="#{CGI.escapeHTML(node.url)}" alt="#{CGI.escapeHTML(alt)}"#{title(node)}>)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Escapes optional titles independently from the link or image destination.
|
|
49
|
+
# @param node [Commonmarker::Node] Link or image node.
|
|
50
|
+
# @return [String] Optional HTML title attribute.
|
|
51
|
+
def title(node)
|
|
52
|
+
value = node.title
|
|
53
|
+
%( title="#{CGI.escapeHTML(value)}") unless value.empty?
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class RDoc::Generator::Markdown
|
|
4
|
+
# Protects Markdown links and tag-like code while RDoc parses surrounding markup.
|
|
5
|
+
class MarkdownLinks
|
|
6
|
+
# Keeps the typed node and original RDoc label together at the boundary.
|
|
7
|
+
Link = Data.define(:node, :label)
|
|
8
|
+
|
|
9
|
+
# RDoc source with collision-free tokens protecting Markdown links and code.
|
|
10
|
+
# @return [String] Prepared inline RDoc source.
|
|
11
|
+
attr_reader :source
|
|
12
|
+
|
|
13
|
+
# Finds complete Markdown links without interpreting RDoc code tags as prose.
|
|
14
|
+
# @param text [String] Original inline RDoc source.
|
|
15
|
+
def initialize(text)
|
|
16
|
+
@source = text
|
|
17
|
+
@nodes = {}
|
|
18
|
+
return if !(text.include?("](") || (text.include?("`") && text.include?("<")))
|
|
19
|
+
|
|
20
|
+
@source = text.encode(Encoding::UTF_8)
|
|
21
|
+
@line_offsets = [0]
|
|
22
|
+
@source.each_line { |line| @line_offsets << @line_offsets.last + line.bytesize }
|
|
23
|
+
@literal_ranges = Syntax.native_literal_ranges(@source)
|
|
24
|
+
buffer = @source.b
|
|
25
|
+
inline_edits.reverse_each { |range, token| buffer[range] = token }
|
|
26
|
+
@source = buffer.force_encoding(Encoding::UTF_8)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Looks up only tokens created for this exact input.
|
|
30
|
+
# @param token [String] Inline parser token.
|
|
31
|
+
# @return [Link, nil] Protected Markdown node and its original label.
|
|
32
|
+
def [](token)
|
|
33
|
+
@nodes[token]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Serializes a typed link, retaining titles and destination escaping.
|
|
37
|
+
# @param link [Link] Original link or image node.
|
|
38
|
+
# @param label [String] Natively formatted inline Markdown label.
|
|
39
|
+
# @return [String] Inline Markdown, never rendered HTML.
|
|
40
|
+
def self.render(link, label)
|
|
41
|
+
original = link.node
|
|
42
|
+
prefix = "!" if original.type == :image
|
|
43
|
+
node = Commonmarker.parse("#{prefix}[#{label}](markdown-target)").first_child.first_child
|
|
44
|
+
node.url = Syntax.target(original.url)
|
|
45
|
+
node.title = original.title
|
|
46
|
+
node.to_commonmark.delete_suffix("\n")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
# Uses source byte positions; inline nodes inside outer links stay together.
|
|
52
|
+
# @return [Array<Array(Range, String)>] Nonoverlapping source replacements.
|
|
53
|
+
def inline_edits
|
|
54
|
+
prefix = "RDocMarkdown"
|
|
55
|
+
prefix += "X" while @source.include?(prefix)
|
|
56
|
+
edits = []
|
|
57
|
+
covered_until = nil
|
|
58
|
+
Commonmarker.parse(@source).walk do |node|
|
|
59
|
+
range = inline_range(node)
|
|
60
|
+
next if !range || (covered_until && range.begin < covered_until)
|
|
61
|
+
|
|
62
|
+
token = "\0#{prefix}#{@nodes.length}\0"
|
|
63
|
+
@nodes[token] = Link.new(node, original_label(node))
|
|
64
|
+
edits << [range, token]
|
|
65
|
+
covered_until = range.end
|
|
66
|
+
end
|
|
67
|
+
edits
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Keeps native RDoc links and code-tag boundaries ahead of Markdown spans.
|
|
71
|
+
# @param node [Commonmarker::Node] Candidate Markdown node.
|
|
72
|
+
# @return [Range<Integer>, nil] Replaceable Markdown link or tag-like code range.
|
|
73
|
+
def inline_range(node)
|
|
74
|
+
type = node.type
|
|
75
|
+
return unless [:link, :image, :code].include?(type)
|
|
76
|
+
return if type == :code && !node.string_content.include?("<")
|
|
77
|
+
|
|
78
|
+
range = Syntax.source_range(node, @line_offsets)
|
|
79
|
+
first = range.begin
|
|
80
|
+
bytes = @source.b
|
|
81
|
+
return unless bytes[range].start_with?("[", "![", "`")
|
|
82
|
+
|
|
83
|
+
previous = bytes[first - 1] unless first.zero?
|
|
84
|
+
return if type == :link && previous&.match?(/[A-Za-z0-9}]/)
|
|
85
|
+
return if Syntax.literal_position?(@literal_ranges, first)
|
|
86
|
+
|
|
87
|
+
range
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Retains RDoc label formatting instead of changing its emphasis conventions.
|
|
91
|
+
# @param node [Commonmarker::Node] Markdown link or image.
|
|
92
|
+
# @return [String] Label source, including native formatting delimiters.
|
|
93
|
+
def original_label(node)
|
|
94
|
+
children = node.to_a
|
|
95
|
+
return "" if children.empty?
|
|
96
|
+
|
|
97
|
+
range = Syntax.source_range(children.first, @line_offsets).begin...Syntax.source_range(children.last, @line_offsets).end
|
|
98
|
+
@source.b[range].force_encoding(Encoding::UTF_8)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -2,15 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
# Resolves source and generated documentation paths.
|
|
4
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
5
|
# Builds the Markdown output path for an RDoc page.
|
|
15
6
|
#
|
|
16
7
|
# @param page [RDoc::TopLevel] Page object to render.
|
|
@@ -18,7 +9,7 @@ module RDoc::Generator::Markdown::Paths
|
|
|
18
9
|
# @return [String] Relative Markdown path.
|
|
19
10
|
def page_output_path(page)
|
|
20
11
|
relative_name = page.relative_name
|
|
21
|
-
source_path =
|
|
12
|
+
source_path = RDoc::Generator::Markdown::Paths.normalize_input_path(relative_name, source_dir)
|
|
22
13
|
return source_path if relative_name.match?(/\.(?:md|markdown)\z/i)
|
|
23
14
|
|
|
24
15
|
dirname = File.dirname(source_path)
|
|
@@ -35,26 +26,13 @@ module RDoc::Generator::Markdown::Paths
|
|
|
35
26
|
#
|
|
36
27
|
# @return [String] Relative Markdown path.
|
|
37
28
|
def output_path_for(code_object)
|
|
38
|
-
|
|
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
|
-
)
|
|
29
|
+
"#{code_object.full_name.gsub("::", "/")}.md"
|
|
53
30
|
end
|
|
54
31
|
|
|
55
|
-
private :
|
|
32
|
+
private :page_output_path, :output_path_for
|
|
56
33
|
|
|
57
34
|
# Rewrites links using explicit generated-path state.
|
|
35
|
+
# Code spans and fences are skipped; prose backticks are always escaped.
|
|
58
36
|
#
|
|
59
37
|
# @param markdown [String] Markdown content.
|
|
60
38
|
# @param current_output_path [String] Output path for the current file.
|
|
@@ -64,8 +42,10 @@ module RDoc::Generator::Markdown::Paths
|
|
|
64
42
|
def self.rewrite_internal_links(markdown, current_output_path, state)
|
|
65
43
|
current_dir = Pathname.new(current_output_path).dirname
|
|
66
44
|
|
|
67
|
-
markdown.gsub(
|
|
68
|
-
|
|
45
|
+
markdown.gsub(/(?<!\\)(`+).+?\1|\]\(([^)]+)\)/m) do
|
|
46
|
+
next $& if $1
|
|
47
|
+
|
|
48
|
+
target = $2
|
|
69
49
|
path = target.sub(/[?#].*\z/, "")
|
|
70
50
|
suffix = target[path.length..]
|
|
71
51
|
|
|
@@ -95,27 +75,16 @@ module RDoc::Generator::Markdown::Paths
|
|
|
95
75
|
.find { |candidate| state.known_output_paths.include?(candidate) }
|
|
96
76
|
end
|
|
97
77
|
|
|
98
|
-
# Normalizes an input filename into an output-relative source path.
|
|
99
|
-
#
|
|
100
|
-
# @param path [String] RDoc input path.
|
|
101
|
-
#
|
|
102
|
-
# @return [String] Normalized path without root prefixes.
|
|
103
|
-
def normalize_input_path_for_output(path)
|
|
104
|
-
RDoc::Generator::Markdown::Paths.normalize_input_path(path, source_dir)
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
private :normalize_input_path_for_output
|
|
108
|
-
|
|
109
78
|
# Normalizes an input path against an explicit source directory.
|
|
110
79
|
#
|
|
111
80
|
# @param path [String] RDoc input path.
|
|
112
81
|
# @param source_dir [String] Absolute documentation source directory.
|
|
113
82
|
#
|
|
114
|
-
# @return [String] Normalized path without root prefixes.
|
|
83
|
+
# @return [String] Normalized path without root or parent-directory prefixes.
|
|
115
84
|
def self.normalize_input_path(path, source_dir)
|
|
116
85
|
normalized = path.tr("\\", "/").sub(%r{\A\./}, "")
|
|
117
86
|
normalized = normalized.sub(%r{\A#{Regexp.escape(source_dir)}/}, "")
|
|
118
|
-
normalized = normalized.sub(%r{\A
|
|
87
|
+
normalized = normalized.sub(%r{\A(?:\.\./|/)+}, "")
|
|
119
88
|
|
|
120
89
|
root_basename = File.basename(source_dir)
|
|
121
90
|
normalized.sub(%r{\A#{Regexp.escape(root_basename)}/}, "")
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class RDoc::Generator::Markdown
|
|
4
|
+
# Native reference policy, independent of HTML rendering and output buffers.
|
|
5
|
+
module References
|
|
6
|
+
# Options attached to the RDoc context being documented.
|
|
7
|
+
# @return [RDoc::Options] Current options.
|
|
8
|
+
def options
|
|
9
|
+
rendering.context.options
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Resolves automatic references outside explicit link labels.
|
|
13
|
+
# @param name [String] Reference candidate.
|
|
14
|
+
# @return [String] Linked or literal Markdown.
|
|
15
|
+
def handle_regexp_CROSSREF(name)
|
|
16
|
+
text = convert_string(name)
|
|
17
|
+
return text if in_link_label? || options.autolink_excluded_words&.include?(name)
|
|
18
|
+
return text if name.match?(/@[\w-]+\.[\w-]/)
|
|
19
|
+
|
|
20
|
+
reference_link(name) || ((name.include?("#") && !name.start_with?("#")) ? Syntax.code(name) : text)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Formats automatic URLs without linking URLs inside link labels.
|
|
24
|
+
# @param url [String] URL candidate.
|
|
25
|
+
# @return [String] Linked or literal Markdown.
|
|
26
|
+
def handle_regexp_HYPERLINK(url)
|
|
27
|
+
return convert_string(url) if in_link_label?
|
|
28
|
+
|
|
29
|
+
label = convert_string(url.sub(%r{\A(?:link|https?|mailto|ftp|irc):/*}, ""))
|
|
30
|
+
gen_url(url, label)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Resolves explicit rdoc-ref schemes; other schemes use ToMarkdown.
|
|
34
|
+
# @param url [String] RDoc URL.
|
|
35
|
+
# @return [String] Native Markdown.
|
|
36
|
+
def handle_regexp_RDOCLINK(url)
|
|
37
|
+
return super unless url.start_with?("rdoc-ref:")
|
|
38
|
+
|
|
39
|
+
name = url.delete_prefix("rdoc-ref:")
|
|
40
|
+
in_link_label? ? convert_string(name) : explicit_reference(name)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Reports missing explicit references and retains their visible labels.
|
|
44
|
+
# @param name [String] Reference name.
|
|
45
|
+
# @param label [String, nil] Optional native label.
|
|
46
|
+
# @return [String] Reference or visible fallback.
|
|
47
|
+
def explicit_reference(name, label = nil)
|
|
48
|
+
found = reference_link(name, label: label)
|
|
49
|
+
return found if found
|
|
50
|
+
|
|
51
|
+
text = label || convert_string(name)
|
|
52
|
+
puts "#{rendering.context.path}: `rdoc-ref:#{name}` can't be resolved for `#{text}`" if options.warn_missing_rdoc_ref
|
|
53
|
+
text
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Resolves a reference only when its owning page is emitted.
|
|
57
|
+
# @param name [String] Reference name.
|
|
58
|
+
# @param label [String, nil] Optional native label.
|
|
59
|
+
# @return [String, nil] Native link, unlinked code, or an unresolved reference.
|
|
60
|
+
def reference_link(name, label: nil)
|
|
61
|
+
target_name, section = split_reference(name)
|
|
62
|
+
local = target_name.empty?
|
|
63
|
+
ref = rendering.reference.resolve(target_name)
|
|
64
|
+
return if !ref && !(local && section)
|
|
65
|
+
|
|
66
|
+
text = reference_text(name, label, ref)
|
|
67
|
+
return text if ref && !emitted_reference?(ref)
|
|
68
|
+
|
|
69
|
+
path = ref ? ref.as_href(rendering.context.path) : ""
|
|
70
|
+
path = reference_section(path, ref, section) if section
|
|
71
|
+
Syntax.link(text, path)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
# Main keeps file references inside code spans literal, not prose links.
|
|
77
|
+
# @param text [String] Inline code candidate.
|
|
78
|
+
# @return [String, nil] Code-formatted object link or literal-code fallback.
|
|
79
|
+
def code_reference(text)
|
|
80
|
+
return if in_link_label?
|
|
81
|
+
|
|
82
|
+
name = text.delete_prefix("\\")
|
|
83
|
+
return unless code_reference_syntax?(name)
|
|
84
|
+
|
|
85
|
+
target_name, = split_reference(name)
|
|
86
|
+
return if RDoc::TopLevel === rendering.reference.resolve(target_name)
|
|
87
|
+
|
|
88
|
+
linked = reference_link(name)
|
|
89
|
+
text.start_with?("\\") ? (Syntax.code(name) if linked) : linked
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Separates inline-code display policy from target resolution.
|
|
93
|
+
# @param name [String] Reference name.
|
|
94
|
+
# @param label [String, nil] Explicit native label.
|
|
95
|
+
# @param ref [RDoc::CodeObject, nil] Resolved target.
|
|
96
|
+
# @return [String] Native inline Markdown.
|
|
97
|
+
def reference_text(name, label, ref)
|
|
98
|
+
display = options.show_hash ? name : name.delete_prefix("#")
|
|
99
|
+
target, section = split_reference(display)
|
|
100
|
+
if section
|
|
101
|
+
return label if label
|
|
102
|
+
|
|
103
|
+
title = convert_string(RDoc::Text.decode_legacy_label(section))
|
|
104
|
+
return target.empty? ? title : "#{title} at #{Syntax.code(target)}"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
plain = label ? label.delete("\\") : display
|
|
108
|
+
text = label || convert_string(display)
|
|
109
|
+
(!(RDoc::TopLevel === ref) && (!label || plain == name)) ? Syntax.code(plain) : text
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Matches both explicit method hashes and hyperlink-all method notation.
|
|
113
|
+
# @param name [String] Complete inline code text without suppression.
|
|
114
|
+
# @return [Boolean] Whether a reference starts the span and consumes its words.
|
|
115
|
+
def code_reference_syntax?(name)
|
|
116
|
+
patterns = [RDoc::CrossReference::CROSSREF_REGEXP]
|
|
117
|
+
patterns << RDoc::CrossReference::ALL_CROSSREF_REGEXP if options.hyperlink_all
|
|
118
|
+
patterns.any? do |pattern|
|
|
119
|
+
match = pattern.match(name)
|
|
120
|
+
match&.begin(1) == 0 && match.post_match.match?(/\A[[:punct:]\s]*\z/)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Separates heading labels without mistaking unary operator method names.
|
|
125
|
+
# @param name [String] Reference name.
|
|
126
|
+
# @return [String, Array(String, String, nil)] Target and optional heading label.
|
|
127
|
+
def split_reference(name)
|
|
128
|
+
return name if name.end_with?("+@", "-@")
|
|
129
|
+
|
|
130
|
+
target, separator, section = name.partition("@")
|
|
131
|
+
[target, (section unless separator.empty?)]
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Tests the owning page rather than the individual member's identity.
|
|
135
|
+
# @param ref [RDoc::CodeObject] Resolved target.
|
|
136
|
+
# @return [Boolean] Whether that target has a generated page.
|
|
137
|
+
def emitted_reference?(ref)
|
|
138
|
+
owner = ref
|
|
139
|
+
owner = owner.parent until RDoc::ClassModule === owner || RDoc::TopLevel === owner
|
|
140
|
+
rendering.output_ids.include?(owner.object_id)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Applies RDoc's canonical method, named-section, and page heading anchors.
|
|
144
|
+
# @param path [String] Relative RDoc target.
|
|
145
|
+
# @param ref [RDoc::CodeObject, nil] Target or a local heading reference.
|
|
146
|
+
# @param section [String] Encoded heading label.
|
|
147
|
+
# @return [String] Anchored target.
|
|
148
|
+
def reference_section(path, ref, section)
|
|
149
|
+
decoded = RDoc::Text.decode_legacy_label(section)
|
|
150
|
+
slug = RDoc::Text.to_anchor(decoded)
|
|
151
|
+
return "#{path}-#{slug}" if path.include?("#")
|
|
152
|
+
|
|
153
|
+
named = ref.sections.find { |candidate| candidate.title == decoded } if ref
|
|
154
|
+
anchor = if named
|
|
155
|
+
named.aref
|
|
156
|
+
elsif RDoc::ClassModule === ref
|
|
157
|
+
"#{ref.aref}-#{slug}"
|
|
158
|
+
else
|
|
159
|
+
slug
|
|
160
|
+
end
|
|
161
|
+
"#{path}##{anchor}"
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|