Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion lib/rdoc/code_object/class_module.rb
Original file line number Diff line number Diff line change
Expand Up @@ -673,7 +673,7 @@ def path
# module or class return the name of the latter.

def name_for_path
is_alias_for ? is_alias_for.full_name : full_name
is_alias_for ? is_alias_for.name_for_path : full_name
end

##
Expand Down Expand Up @@ -848,6 +848,13 @@ def type
def update_aliases
constants.each do |const|
next unless cm = const.is_alias_for

# Resolve chained aliases (A = B = C) to the real class/module.
cm = @store.find_class_or_module(cm.full_name) || cm
while (target = cm.is_alias_for)
cm = target
end

cm_alias = cm.dup
cm_alias.name = const.name

Expand Down
90 changes: 49 additions & 41 deletions lib/rdoc/markup/to_html_crossref.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 <code>#{context_name}</code>" 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

##
Expand Down Expand Up @@ -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 =~ /(.*[^#:])?@/
Expand All @@ -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 = "<code>#{CGI.escapeHTML text}</code>"
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

"<a href=\"#{path}\">#{text}</a>"
end

"<a href=\"#{path}\">#{text}</a>"
end

def handle_TT(code)
Expand Down
37 changes: 37 additions & 0 deletions test/rdoc/generator/darkfish_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,43 @@ def test_canonical_url_for_classes
assert_include(content, '<link rel="canonical" href="https://docs.ruby-lang.org/en/master/Klass/Inner.html">')
end

def test_generate_chained_alias_sidebar_links
# Reproduces ruby/rdoc#1664:
# class Original < Base
# DirectAlias = Original # alias of real class
# ChainedAlias = DirectAlias # alias of an alias
#
# ChainedAlias's sidebar link must point to Original.html, not DirectAlias.html
# (which is never generated because aliases don't get their own files).
parent = @top_level.add_module RDoc::NormalModule, 'Parent'
original = parent.add_class RDoc::NormalClass, 'Original'

direct_alias_const = RDoc::Constant.new 'DirectAlias', nil, ''
direct_alias_const.record_location @top_level
direct_alias_const.is_alias_for = original
parent.add_constant direct_alias_const
parent.update_aliases

direct_alias = @store.find_class_or_module 'Parent::DirectAlias'

chained_alias_const = RDoc::Constant.new 'ChainedAlias', nil, ''
chained_alias_const.record_location @top_level
chained_alias_const.is_alias_for = direct_alias
parent.add_constant chained_alias_const
parent.update_aliases

@store.complete :private
@g.generate

assert_file 'Parent/Original.html'
refute File.exist?('Parent/DirectAlias.html'), 'alias should not get its own file'
refute File.exist?('Parent/ChainedAlias.html'), 'chained alias should not get its own file'

index_html = File.binread('index.html')
assert_match %r{href="\./Parent/Original\.html">DirectAlias<}, index_html
assert_match %r{href="\./Parent/Original\.html">ChainedAlias<}, index_html
end

def test_canonical_url_for_rdoc_files
@store.add_file("CONTRIBUTING.rdoc", parser: RDoc::Parser::Simple)

Expand Down
16 changes: 15 additions & 1 deletion test/rdoc/markup/to_html_crossref_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 '<a href="C1.html#method-c-m"><code>m</code></a>', @to.link('m', 'm')
end
Expand All @@ -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<p>#{text}</p>\n"
end
Expand Down
Loading