Repository: ruby/rdoc (Ruby)

Issue:
Backslash in RDoc monofont causes linking
Bug can be reproduced by generating the RDoc for this script:

```ruby
class Foo
  # This is not linked:
  # <tt>.bar.hello()</tt>
  #
  # But these are linked:
  # <tt>.bar.hello(\)</tt>
  # <tt>.bar.hello(\\\\)</tt>
  def foo

  # Hello from bar
  def bar
  end
end
```

The generated RDoc HTML looks like this. Notice how the backslash causes the monofont to generate a link to `Foo#bar`:

<img width="509" height="462" alt="Image" src="https://github.com/user-attachments/assets/12d1ca0c-8680-476c-a929-d36ad8625f1e" />

I cannot find a way to prevent this link from generating. I think this is a bug, but I might be missing something from the syntax.


Discussion:

Patch:
diff --git a/lib/rdoc/markup/to_html_crossref.rb b/lib/rdoc/markup/to_html_crossref.rb
index c5e4f62c97..dab486aaae 100644
--- a/lib/rdoc/markup/to_html_crossref.rb
+++ b/lib/rdoc/markup/to_html_crossref.rb
@@ -184,38 +184,35 @@ def link(name, text, code = true, rdoc_ref: false)
     end
   end
 
-  def convert_flow(flow)
+  def convert_flow(flow_items, &block)
     res = []
 
     i = 0
-    while i < flow.size
-      item = flow[i]
-      i += 1
+    while i < flow_items.size
+      item = flow_items[i]
+
       case item
-      when RDoc::Markup::AttrChanger then
-        # Make "+Class#method+" a cross reference
-        if tt_tag?(item.turn_on) and
-          String === (str = flow[i]) and
-          RDoc::Markup::AttrChanger === flow[i+1] and
-          tt_tag?(flow[i+1].turn_off, true) and
-          (@options.hyperlink_all ? ALL_CROSSREF_REGEXP : CROSSREF_REGEXP).match?(str) and
-          (text = cross_reference str) != str
-        then
-          text = yield text, res if defined?(yield)
+      when RDoc::Markup::AttrChanger
+        if (text = convert_tt_crossref(flow_items, i))
+          text = block.call(text, res) if block
           res << text
-          i += 2
+          i += 3
           next
         end
+
         off_tags res, item
-        on_tags res, item
-      when String then
+        on_tags  res, item
+        i += 1
+      when String
         text = convert_string(item)
-        text = yield text, res if defined?(yield)
+        text = block.call(text, res) if block
         res << text
-      when RDoc::Markup::RegexpHandling then
+        i += 1
+      when RDoc::Markup::RegexpHandling
         text = convert_regexp_handling(item)
-        text = yield text, res if defined?(yield)
+        text = block.call(text, res) if block
         res << text
+        i += 1
       else
         raise "Unknown flow element: #{item.inspect}"
       end
@@ -223,4 +220,37 @@ def convert_flow(flow)
 
     res.join('')
   end
+
+  private
+
+  ##
+  # Detects <tt>...</tt> spans that contain a single cross-reference candidate.
+  # When the candidate occupies the whole span (aside from trailing
+  # punctuation), the tt markup is replaced by the resolved cross-reference.
+
+  def convert_tt_crossref(flow_items, index)
+    opener = flow_items[index]
+    return unless tt_tag?(opener.turn_on)
+
+    string = flow_items[index + 1]
+    closer = flow_items[index + 2]
+
+    return unless String === string
+    return unless RDoc::Markup::AttrChanger === closer
+    return unless tt_tag?(closer.turn_off, true)
+
+    crossref_regexp = @options.hyperlink_all ? ALL_CROSSREF_REGEXP : CROSSREF_REGEXP
+    match = crossref_regexp.match(string)
+    return unless match
+    return unless match.begin(1).zero?
+
+    trailing = match.post_match
+    # Only convert when the remainder is punctuation/whitespace so other tt text stays literal.
+    return unless trailing.match?(/\A[[:punct:]\s]*\z/)
+
+    text = cross_reference(string)
+    return if text == string
+
+    text
+  end
 end


Test patch:
diff --git a/test/rdoc/rdoc_markup_to_html_crossref_test.rb b/test/rdoc/rdoc_markup_to_html_crossref_test.rb
index a52c7f1506..e6836ea4c2 100644
--- a/test/rdoc/rdoc_markup_to_html_crossref_test.rb
+++ b/test/rdoc/rdoc_markup_to_html_crossref_test.rb
@@ -33,6 +33,24 @@ def test_convert_CROSSREF
     assert_equal para("<code># :stopdoc:</code>:"), result
   end
 
+  def test_convert_CROSSREF_backslash_in_tt
+    @options.hyperlink_all = false
+
+    formatter = RDoc::Markup::ToHtmlCrossref.new(@options, 'C9.html', @c9_b)
+
+    result = formatter.convert '<tt>C1</tt>'
+    assert_equal para('<a href="C1.html"><code>C1</code></a>'), result
+
+    result = formatter.convert '<tt>.bar.hello()</tt>'
+    assert_equal para('<code>.bar.hello()</code>'), result
+
+    result = formatter.convert '<tt>.bar.hello(\)</tt>'
+    assert_equal para('<code>.bar.hello(\)</code>'), result
+
+    result = formatter.convert '<tt>.bar.hello(\\\\)</tt>'
+    assert_equal para('<code>.bar.hello(\\)</code>'), result
+  end
+
   def test_convert_CROSSREF_ignored_excluded_words
     @options.autolink_excluded_words = ['C1']