diff --git a/lib/rdoc/markup/to_html_crossref.rb b/lib/rdoc/markup/to_html_crossref.rb
index 106810072b..a0e218b657 100644
--- a/lib/rdoc/markup/to_html_crossref.rb
+++ b/lib/rdoc/markup/to_html_crossref.rb
@@ -59,21 +59,24 @@ def init_link_notation_regexp_handlings
# given it is used as the link text, otherwise +name+ is used.
def cross_reference(name, text = nil, code = true, rdoc_ref: false)
- lookup = name
+ # What to show when the reference doesn't resolve to a link:
+ # caller-provided text if any, otherwise the original name (preserving '#').
+ fallback = text || name
- name = name[1..-1] unless @show_hash if name[0, 1] == '#'
+ # Strip '#' for link display text (e.g. #method shows as "method" in links)
+ display = !@show_hash && name.start_with?('#') ? name[1..] : name
- if !name.end_with?('+@', '-@') && match = name.match(/(.*[^#:])?@(.*)/)
+ if !display.end_with?('+@', '-@') && match = display.match(/(.*[^#:])?@(.*)/)
context_name = match[1]
label = RDoc::Text.decode_legacy_label(match[2])
text ||= "#{label} at #{context_name}" if context_name
text ||= label
code = false
else
- text ||= name
+ text ||= display
end
- link lookup, text, code, rdoc_ref: rdoc_ref
+ link(name, text, code, rdoc_ref: rdoc_ref) || fallback
end
##
@@ -150,6 +153,7 @@ def gen_url(url, text)
##
# Creates an HTML link to +name+ with the given +text+.
+ # Returns the link HTML string, or +nil+ if the reference could not be resolved.
def link(name, text, code = true, rdoc_ref: false)
if !(name.end_with?('+@', '-@')) and name =~ /(.*[^#:])?@/
@@ -162,56 +166,60 @@ def link(name, text, code = true, rdoc_ref: false)
# Non-text source files (C, Ruby, etc.) don't get HTML pages generated,
# so don't auto-link to them. Explicit rdoc-ref: links are still allowed.
if !rdoc_ref && RDoc::TopLevel === ref && !ref.text?
- return text
+ return
end
case ref
- when String then
+ when String
if rdoc_ref && @warn_missing_rdoc_ref
puts "#{@from_path}: `rdoc-ref:#{name}` can't be resolved for `#{text}`"
end
- ref
+ return
+ when nil
+ # A bare label reference like @foo still produces a valid anchor link
+ return unless label
+ path = +""
else
- path = ref ? ref.as_href(@from_path) : +""
+ path = ref.as_href(@from_path)
if code and RDoc::CodeObject === ref and !(RDoc::TopLevel === ref)
text = "#{CGI.escapeHTML text}"
end
+ end
- if label
- # Decode legacy labels (e.g., "What-27s+Here" -> "What's Here")
- # then convert to GitHub-style anchor format
- decoded_label = RDoc::Text.decode_legacy_label(label)
- formatted_label = RDoc::Text.to_anchor(decoded_label)
-
- # Case 1: Path already has an anchor (e.g., method link)
- # Input: C1#method@label -> path="C1.html#method-i-m"
- # Output: C1.html#method-i-m-label
- if path =~ /#/
- path << "-#{formatted_label}"
-
- # Case 2: Label matches a section title
- # Input: C1@Section -> path="C1.html", section "Section" exists
- # Output: C1.html#section (uses section.aref for GitHub-style)
- elsif (section = ref&.sections&.find { |s| decoded_label == s.title })
- path << "##{section.aref}"
-
- # Case 3: Ref has an aref (class/module context)
- # Input: C1@heading -> path="C1.html", ref=C1 class
- # Output: C1.html#class-c1-heading
- elsif ref.respond_to?(:aref)
- path << "##{ref.aref}-#{formatted_label}"
-
- # Case 4: No context, just the label (e.g., TopLevel/file)
- # Input: README@section -> path="README_md.html"
- # Output: README_md.html#section
- else
- path << "##{formatted_label}"
- end
+ if label
+ # Decode legacy labels (e.g., "What-27s+Here" -> "What's Here")
+ # then convert to GitHub-style anchor format
+ decoded_label = RDoc::Text.decode_legacy_label(label)
+ formatted_label = RDoc::Text.to_anchor(decoded_label)
+
+ # Case 1: Path already has an anchor (e.g., method link)
+ # Input: C1#method@label -> path="C1.html#method-i-m"
+ # Output: C1.html#method-i-m-label
+ if path =~ /#/
+ path << "-#{formatted_label}"
+
+ # Case 2: Label matches a section title
+ # Input: C1@Section -> path="C1.html", section "Section" exists
+ # Output: C1.html#section (uses section.aref for GitHub-style)
+ elsif (section = ref&.sections&.find { |s| decoded_label == s.title })
+ path << "##{section.aref}"
+
+ # Case 3: Ref has an aref (class/module context)
+ # Input: C1@heading -> path="C1.html", ref=C1 class
+ # Output: C1.html#class-c1-heading
+ elsif ref.respond_to?(:aref)
+ path << "##{ref.aref}-#{formatted_label}"
+
+ # Case 4: No context, just the label (e.g., TopLevel/file)
+ # Input: README@section -> path="README_md.html"
+ # Output: README_md.html#section
+ else
+ path << "##{formatted_label}"
end
-
- "#{text}"
end
+
+ "#{text}"
end
def handle_TT(code)
diff --git a/test/rdoc/markup/to_html_crossref_test.rb b/test/rdoc/markup/to_html_crossref_test.rb
index ff243fc9e0..24e0c97e86 100644
--- a/test/rdoc/markup/to_html_crossref_test.rb
+++ b/test/rdoc/markup/to_html_crossref_test.rb
@@ -407,7 +407,7 @@ def test_convert_RDOCLINK_rdoc_ref_c_file_linked
end
def test_link
- assert_equal 'n', @to.link('n', 'n')
+ assert_nil @to.link('n', 'n')
assert_equal 'm', @to.link('m', 'm')
end
@@ -423,6 +423,20 @@ def test_link_class_method_full
@to.link('Parent::m', 'Parent::m')
end
+ def test_handle_regexp_CROSSREF_hash_preserved_for_unresolved
+ @to.show_hash = false
+
+ # #no should not lose its '#' when it doesn't resolve to a method
+ assert_equal "#no", REGEXP_HANDLING('#no')
+ end
+
+ def test_cross_reference_preserves_explicit_text_for_unresolved
+ # When explicit text is provided, it should be preserved on unresolved refs
+ assert_equal "Foo", @to.cross_reference("Missing", "Foo")
+ end
+
+ private
+
def para(text)
"\n
#{text}
\n" end