Skip to content

Commit 64f4ea1

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`)
1 parent dc7a167 commit 64f4ea1

File tree

2 files changed

+55
-43
lines changed

2 files changed

+55
-43
lines changed

lib/rdoc/markup/to_html_crossref.rb

Lines changed: 47 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -59,21 +59,21 @@ 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+
# Strip '#' from display text for method references (e.g. #method -> method),
63+
# but only when @show_hash is false and only for link text, not for fallback.
64+
display = !@show_hash && name.start_with?('#') ? name[1..] : name
6365

64-
name = name[1..-1] unless @show_hash if name[0, 1] == '#'
65-
66-
if !name.end_with?('+@', '-@') && match = name.match(/(.*[^#:])?@(.*)/)
66+
if !display.end_with?('+@', '-@') && match = display.match(/(.*[^#:])?@(.*)/)
6767
context_name = match[1]
6868
label = RDoc::Text.decode_legacy_label(match[2])
6969
text ||= "#{label} at <code>#{context_name}</code>" if context_name
7070
text ||= label
7171
code = false
7272
else
73-
text ||= name
73+
text ||= display
7474
end
7575

76-
link lookup, text, code, rdoc_ref: rdoc_ref
76+
link(name, text, code, rdoc_ref: rdoc_ref) || name
7777
end
7878

7979
##
@@ -150,6 +150,7 @@ def gen_url(url, text)
150150

151151
##
152152
# Creates an HTML link to +name+ with the given +text+.
153+
# Returns the link HTML string, or +nil+ if the reference could not be resolved.
153154

154155
def link(name, text, code = true, rdoc_ref: false)
155156
if !(name.end_with?('+@', '-@')) and name =~ /(.*[^#:])?@/
@@ -162,56 +163,60 @@ def link(name, text, code = true, rdoc_ref: false)
162163
# Non-text source files (C, Ruby, etc.) don't get HTML pages generated,
163164
# so don't auto-link to them. Explicit rdoc-ref: links are still allowed.
164165
if !rdoc_ref && RDoc::TopLevel === ref && !ref.text?
165-
return text
166+
return
166167
end
167168

168169
case ref
169-
when String then
170+
when String
170171
if rdoc_ref && @warn_missing_rdoc_ref
171172
puts "#{@from_path}: `rdoc-ref:#{name}` can't be resolved for `#{text}`"
172173
end
173-
ref
174+
return
175+
when nil
176+
# A bare label reference like @foo still produces a valid anchor link
177+
return unless label
178+
path = +""
174179
else
175-
path = ref ? ref.as_href(@from_path) : +""
180+
path = ref.as_href(@from_path)
176181

177182
if code and RDoc::CodeObject === ref and !(RDoc::TopLevel === ref)
178183
text = "<code>#{CGI.escapeHTML text}</code>"
179184
end
185+
end
180186

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

217222
def handle_TT(code)

test/rdoc/markup/to_html_crossref_test.rb

Lines changed: 8 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
@@ -435,6 +435,13 @@ def hyper(reference)
435435
"rdoc-ref:#{reference}"
436436
end
437437

438+
def test_handle_regexp_CROSSREF_hash_preserved_for_unresolved
439+
@to.show_hash = false
440+
441+
# #no should not lose its '#' when it doesn't resolve to a method
442+
assert_equal "#no", REGEXP_HANDLING('#no')
443+
end
444+
438445
def tidy(reference)
439446
"{tidy}[rdoc-ref:#{reference}]"
440447
end

0 commit comments

Comments
 (0)