Skip to content

Commit d9383ba

Browse files
committed
Preserve # prefix for unresolved cross-references
When `#name` doesn't resolve to a method, the cross-reference handler was stripping the `#` and returning just the name. Now the original text including `#` is restored when the lookup fails. This fixes rendering of text like `#no-space-heading` in Markdown paragraphs, where the `#` was silently dropped in the final HTML. Also refactors `cross_reference` and `link`: - `cross_reference` no longer mutates its `name` parameter; uses a separate `display` variable for `#`-stripped text - `link` returns `nil` for unresolved references instead of returning the bare text, letting the caller decide what to display - Label handling is hoisted out of the `case` branches so it's shared between resolved refs and bare label references (`@foo`) - Unresolved refs with caller-provided text (e.g. tidy links) now preserve the explicit text instead of falling back to the raw name - Move new test methods before `private` helper section to follow file convention
1 parent dc7a167 commit d9383ba

File tree

2 files changed

+64
-42
lines changed

2 files changed

+64
-42
lines changed

lib/rdoc/markup/to_html_crossref.rb

Lines changed: 49 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -59,21 +59,24 @@ def init_link_notation_regexp_handlings
5959
# given it is used as the link text, otherwise +name+ is used.
6060

6161
def cross_reference(name, text = nil, code = true, rdoc_ref: false)
62-
lookup = name
62+
# What to show when the reference doesn't resolve to a link:
63+
# caller-provided text if any, otherwise the original name (preserving '#').
64+
fallback = text || name
6365

64-
name = name[1..-1] unless @show_hash if name[0, 1] == '#'
66+
# Strip '#' for link display text (e.g. #method shows as "method" in links)
67+
display = !@show_hash && name.start_with?('#') ? name[1..] : name
6568

66-
if !name.end_with?('+@', '-@') && match = name.match(/(.*[^#:])?@(.*)/)
69+
if !display.end_with?('+@', '-@') && match = display.match(/(.*[^#:])?@(.*)/)
6770
context_name = match[1]
6871
label = RDoc::Text.decode_legacy_label(match[2])
6972
text ||= "#{label} at <code>#{context_name}</code>" if context_name
7073
text ||= label
7174
code = false
7275
else
73-
text ||= name
76+
text ||= display
7477
end
7578

76-
link lookup, text, code, rdoc_ref: rdoc_ref
79+
link(name, text, code, rdoc_ref: rdoc_ref) || fallback
7780
end
7881

7982
##
@@ -150,6 +153,7 @@ def gen_url(url, text)
150153

151154
##
152155
# Creates an HTML link to +name+ with the given +text+.
156+
# Returns the link HTML string, or +nil+ if the reference could not be resolved.
153157

154158
def link(name, text, code = true, rdoc_ref: false)
155159
if !(name.end_with?('+@', '-@')) and name =~ /(.*[^#:])?@/
@@ -162,56 +166,60 @@ def link(name, text, code = true, rdoc_ref: false)
162166
# Non-text source files (C, Ruby, etc.) don't get HTML pages generated,
163167
# so don't auto-link to them. Explicit rdoc-ref: links are still allowed.
164168
if !rdoc_ref && RDoc::TopLevel === ref && !ref.text?
165-
return text
169+
return
166170
end
167171

168172
case ref
169-
when String then
173+
when String
170174
if rdoc_ref && @warn_missing_rdoc_ref
171175
puts "#{@from_path}: `rdoc-ref:#{name}` can't be resolved for `#{text}`"
172176
end
173-
ref
177+
return
178+
when nil
179+
# A bare label reference like @foo still produces a valid anchor link
180+
return unless label
181+
path = +""
174182
else
175-
path = ref ? ref.as_href(@from_path) : +""
183+
path = ref.as_href(@from_path)
176184

177185
if code and RDoc::CodeObject === ref and !(RDoc::TopLevel === ref)
178186
text = "<code>#{CGI.escapeHTML text}</code>"
179187
end
188+
end
180189

181-
if label
182-
# Decode legacy labels (e.g., "What-27s+Here" -> "What's Here")
183-
# then convert to GitHub-style anchor format
184-
decoded_label = RDoc::Text.decode_legacy_label(label)
185-
formatted_label = RDoc::Text.to_anchor(decoded_label)
186-
187-
# Case 1: Path already has an anchor (e.g., method link)
188-
# Input: C1#method@label -> path="C1.html#method-i-m"
189-
# Output: C1.html#method-i-m-label
190-
if path =~ /#/
191-
path << "-#{formatted_label}"
192-
193-
# Case 2: Label matches a section title
194-
# Input: C1@Section -> path="C1.html", section "Section" exists
195-
# Output: C1.html#section (uses section.aref for GitHub-style)
196-
elsif (section = ref&.sections&.find { |s| decoded_label == s.title })
197-
path << "##{section.aref}"
198-
199-
# Case 3: Ref has an aref (class/module context)
200-
# Input: C1@heading -> path="C1.html", ref=C1 class
201-
# Output: C1.html#class-c1-heading
202-
elsif ref.respond_to?(:aref)
203-
path << "##{ref.aref}-#{formatted_label}"
204-
205-
# Case 4: No context, just the label (e.g., TopLevel/file)
206-
# Input: README@section -> path="README_md.html"
207-
# Output: README_md.html#section
208-
else
209-
path << "##{formatted_label}"
210-
end
190+
if label
191+
# Decode legacy labels (e.g., "What-27s+Here" -> "What's Here")
192+
# then convert to GitHub-style anchor format
193+
decoded_label = RDoc::Text.decode_legacy_label(label)
194+
formatted_label = RDoc::Text.to_anchor(decoded_label)
195+
196+
# Case 1: Path already has an anchor (e.g., method link)
197+
# Input: C1#method@label -> path="C1.html#method-i-m"
198+
# Output: C1.html#method-i-m-label
199+
if path =~ /#/
200+
path << "-#{formatted_label}"
201+
202+
# Case 2: Label matches a section title
203+
# Input: C1@Section -> path="C1.html", section "Section" exists
204+
# Output: C1.html#section (uses section.aref for GitHub-style)
205+
elsif (section = ref&.sections&.find { |s| decoded_label == s.title })
206+
path << "##{section.aref}"
207+
208+
# Case 3: Ref has an aref (class/module context)
209+
# Input: C1@heading -> path="C1.html", ref=C1 class
210+
# Output: C1.html#class-c1-heading
211+
elsif ref.respond_to?(:aref)
212+
path << "##{ref.aref}-#{formatted_label}"
213+
214+
# Case 4: No context, just the label (e.g., TopLevel/file)
215+
# Input: README@section -> path="README_md.html"
216+
# Output: README_md.html#section
217+
else
218+
path << "##{formatted_label}"
211219
end
212-
213-
"<a href=\"#{path}\">#{text}</a>"
214220
end
221+
222+
"<a href=\"#{path}\">#{text}</a>"
215223
end
216224

217225
def handle_TT(code)

test/rdoc/markup/to_html_crossref_test.rb

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -407,7 +407,7 @@ def test_convert_RDOCLINK_rdoc_ref_c_file_linked
407407
end
408408

409409
def test_link
410-
assert_equal 'n', @to.link('n', 'n')
410+
assert_nil @to.link('n', 'n')
411411

412412
assert_equal '<a href="C1.html#method-c-m"><code>m</code></a>', @to.link('m', 'm')
413413
end
@@ -423,6 +423,20 @@ def test_link_class_method_full
423423
@to.link('Parent::m', 'Parent::m')
424424
end
425425

426+
def test_handle_regexp_CROSSREF_hash_preserved_for_unresolved
427+
@to.show_hash = false
428+
429+
# #no should not lose its '#' when it doesn't resolve to a method
430+
assert_equal "#no", REGEXP_HANDLING('#no')
431+
end
432+
433+
def test_cross_reference_preserves_explicit_text_for_unresolved
434+
# When explicit text is provided, it should be preserved on unresolved refs
435+
assert_equal "Foo", @to.cross_reference("Missing", "Foo")
436+
end
437+
438+
private
439+
426440
def para(text)
427441
"\n<p>#{text}</p>\n"
428442
end

0 commit comments

Comments
 (0)