Skip to content

Commit 66c1d65

Browse files
authored
Simplify CrossReference resolve (ruby#1571)
Simply return nil if crossref resolve failed. Crossref suppression (already done in another part) is not what `CrossReference#resolve` have to do. So `resolve(name, text)` don't need `text` arg. Cache mechanism of `resolve` will be simple: no string label cache, only cache `ref` or `nil`.
1 parent 7daa431 commit 66c1d65

4 files changed

Lines changed: 96 additions & 78 deletions

File tree

lib/rdoc/cross_reference.rb

Lines changed: 7 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -190,19 +190,13 @@ def resolve_local_symbol(name)
190190
##
191191
# Returns a reference to +name+.
192192
#
193-
# If the reference is found and +name+ is not documented +text+ will be
194-
# returned. If +name+ is escaped +name+ is returned. If +name+ is not
195-
# found +text+ is returned.
193+
# If the reference is found and +name+ is not documented +nil+ will be
194+
# returned. If +name+ is not found +nil+ is returned.
196195

197-
def resolve(name, text)
196+
def resolve(name)
198197
return @seen[name] if @seen.include? name
199198

200-
ref = case name
201-
when /^\\(#{CLASS_REGEXP_STR})$/o then
202-
@context.find_symbol $1
203-
else
204-
@context.find_symbol name
205-
end
199+
ref = @context.find_symbol name
206200

207201
ref = resolve_local_symbol name unless ref
208202

@@ -211,25 +205,11 @@ def resolve(name, text)
211205

212206
ref = nil if RDoc::Alias === ref # external alias, can't link to it
213207

214-
out = if name == '\\' then
215-
name
216-
elsif name =~ /^\\/ then
217-
# we remove the \ only in front of what we know:
218-
# other backslashes are treated later, only outside of <tt>
219-
ref ? $' : name
220-
elsif ref then
221-
if ref.display? then
222-
ref
223-
else
224-
text
225-
end
226-
else
227-
text
228-
end
208+
ref = nil unless ref&.display?
229209

230-
@seen[name] = out
210+
@seen[name] = ref
231211

232-
out
212+
ref
233213
end
234214

235215
end

lib/rdoc/markup/to_html_crossref.rb

Lines changed: 42 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -57,26 +57,23 @@ def init_link_notation_regexp_handlings
5757
##
5858
# Creates a link to the reference +name+ if the name exists. If +text+ is
5959
# given it is used as the link text, otherwise +name+ is used.
60+
# Returns +nil+ if the link target could not be resolved.
6061

6162
def cross_reference(name, text = nil, code = true, rdoc_ref: false)
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
65-
6663
# Strip '#' for link display text (e.g. #method shows as "method" in links)
6764
display = !@show_hash && name.start_with?('#') ? name[1..] : name
6865

6966
if !display.end_with?('+@', '-@') && match = display.match(/(.*[^#:])?@(.*)/)
7067
context_name = match[1]
71-
label = RDoc::Text.decode_legacy_label(match[2])
72-
text ||= "#{label} at <code>#{context_name}</code>" if context_name
68+
label = convert_string(RDoc::Text.decode_legacy_label(match[2]))
69+
text ||= "#{label} at <code>#{convert_string(context_name)}</code>" if context_name
7370
text ||= label
7471
code = false
7572
else
76-
text ||= display
73+
text ||= convert_string(display)
7774
end
7875

79-
link(name, text, code, rdoc_ref: rdoc_ref) || fallback
76+
link(name, text, code, rdoc_ref: rdoc_ref)
8077
end
8178

8279
##
@@ -98,8 +95,7 @@ def handle_regexp_CROSSREF(name)
9895
# cross-references to "new" in text, for instance)
9996
return name if name =~ /\A[a-z]*\z/
10097
end
101-
102-
cross_reference name, rdoc_ref: false
98+
cross_reference(name, rdoc_ref: false) || convert_string(name)
10399
end
104100

105101
##
@@ -111,7 +107,8 @@ def handle_regexp_HYPERLINK(url)
111107

112108
case url
113109
when /\Ardoc-ref:/
114-
cross_reference $', rdoc_ref: true
110+
ref = $'
111+
cross_reference(ref, rdoc_ref: true) || convert_string(ref)
115112
else
116113
super
117114
end
@@ -131,7 +128,8 @@ def handle_regexp_RDOCLINK(url)
131128
if in_tidylink_label?
132129
convert_string(url)
133130
else
134-
cross_reference $', rdoc_ref: true
131+
ref = $'
132+
cross_reference(ref, rdoc_ref: true) || convert_string(ref)
135133
end
136134
else
137135
super
@@ -145,46 +143,46 @@ def handle_regexp_RDOCLINK(url)
145143
def gen_url(url, text)
146144
if url =~ /\Ardoc-ref:/
147145
name = $'
148-
cross_reference name, text, name == text, rdoc_ref: true
146+
cross_reference(name, text, name == text, rdoc_ref: true) || text
149147
else
150148
super
151149
end
152150
end
153151

154152
##
155-
# Creates an HTML link to +name+ with the given +text+.
153+
# Creates an HTML link to +name+ with the given +html_string+.
154+
# +html_string+ should be already escaped and may contain HTML tags.
156155
# Returns the link HTML string, or +nil+ if the reference could not be resolved.
157156

158-
def link(name, text, code = true, rdoc_ref: false)
157+
def link(name, html_string, code = true, rdoc_ref: false)
159158
if !(name.end_with?('+@', '-@')) and name =~ /(.*[^#:])?@/
160159
name = $1
161160
label = $'
162161
end
163162

164-
ref = @cross_reference.resolve name, text if name
163+
ref = @cross_reference.resolve name if name
165164

166165
# Non-text source files (C, Ruby, etc.) don't get HTML pages generated,
167166
# so don't auto-link to them. Explicit rdoc-ref: links are still allowed.
168167
if !rdoc_ref && RDoc::TopLevel === ref && !ref.text?
169168
return
170169
end
171170

172-
case ref
173-
when String
171+
if ref
172+
path = ref.as_href(@from_path)
173+
174+
if code and RDoc::CodeObject === ref and !(RDoc::TopLevel === ref)
175+
html_string = "<code>#{html_string}</code>"
176+
end
177+
elsif name
174178
if rdoc_ref && @warn_missing_rdoc_ref
175-
puts "#{@from_path}: `rdoc-ref:#{name}` can't be resolved for `#{text}`"
179+
puts "#{@from_path}: `rdoc-ref:#{name}` can't be resolved for `#{html_string}`"
176180
end
177181
return
178-
when nil
182+
else
179183
# A bare label reference like @foo still produces a valid anchor link
180184
return unless label
181185
path = +""
182-
else
183-
path = ref.as_href(@from_path)
184-
185-
if code and RDoc::CodeObject === ref and !(RDoc::TopLevel === ref)
186-
text = "<code>#{CGI.escapeHTML text}</code>"
187-
end
188186
end
189187

190188
if label
@@ -219,33 +217,45 @@ def link(name, text, code = true, rdoc_ref: false)
219217
end
220218
end
221219

222-
"<a href=\"#{path}\">#{text}</a>"
220+
"<a href=\"#{path}\">#{html_string}</a>"
223221
end
224222

225223
def handle_TT(code)
226-
emit_inline(tt_cross_reference(code) || "<code>#{CGI.escapeHTML code}</code>")
224+
emit_inline(tt_cross_reference(code) || "<code>#{convert_string(code)}</code>")
227225
end
228226

229227
# Applies additional special handling on top of the one defined in ToHtml.
230228
# When a tidy link is <tt>{Foo}[rdoc-ref:Foo]</tt>, the label part is surrounded by <tt><code></code></tt>.
231229
# TODO: reconsider this workaround.
232230
def apply_tidylink_label_special_handling(label, url)
233-
if url == "rdoc-ref:#{label}" && cross_reference(label).include?('<code>')
231+
if url == "rdoc-ref:#{label}" && cross_reference(label)&.include?('<code>')
234232
"<code>#{convert_string(label)}</code>"
235233
else
236234
super
237235
end
238236
end
239237

238+
# Handles cross-reference and suppressed-crossref inside tt tag.
239+
# Returns nil if code is not an existing cross-reference nor a suppressed-crossref.
240240
def tt_cross_reference(code)
241241
return if in_tidylink_label?
242242

243243
crossref_regexp = @hyperlink_all ? ALL_CROSSREF_REGEXP : CROSSREF_REGEXP
244-
match = crossref_regexp.match(code)
244+
# REGEXP sometimes matches a string that starts with a backslash but is not a
245+
# suppressed cross-reference (for example, `\+`), so the backslash-removed
246+
# part needs to be checked against crossref_regexp.
247+
match = crossref_regexp.match(code.delete_prefix('\\'))
245248
return unless match && match.begin(1).zero?
246249
return unless match.post_match.match?(/\A[[:punct:]\s]*\z/)
247250

248-
ref = cross_reference(code)
249-
ref if ref != code
251+
# cross_reference(file_page) may return a link without code tag.
252+
# We need to check it because this method shouldn't return an html text without code tag.
253+
if code.start_with?('\\')
254+
# Remove leading backslash if crossref exists
255+
"<code>#{convert_string(code[1..])}</code>" if cross_reference(code[1..])&.include?('<code>')
256+
else
257+
html = cross_reference(code)
258+
html if html&.include?('<code>')
259+
end
250260
end
251261
end

test/rdoc/markup/to_html_crossref_test.rb

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,35 @@ def test_convert_CROSSREF_backslash_in_tt
4848
assert_equal para('<code>.bar.hello(\\)</code>'), result
4949
end
5050

51+
def test_convert_suppressed_CROSSREF_in_tt
52+
result = @to.convert '<tt>C1</tt> <tt>\\C1</tt>'
53+
assert_equal para('<a href="C1.html"><code>C1</code></a> <code>C1</code>'), result
54+
55+
result = @to.convert '<tt>C1#m()</tt> <tt>\\C1#m()</tt>'
56+
assert_equal para('<a href="C1.html#method-i-m"><code>C1#m()</code></a> <code>C1#m()</code>'), result
57+
58+
# Keep backshash if crossref doesn't exist
59+
result = @to.convert '<tt>C1#&</tt> <tt>\\n</tt> <tt>\\C1#&</tt>'
60+
assert_equal para('<code>C1#&amp;</code> <code>\\n</code> <code>\\C1#&amp;</code>'), result
61+
end
62+
63+
def test_convert_file_CROSSREF_in_tt
64+
readme = @store.add_file 'README.txt'
65+
readme.parser = RDoc::Parser::Simple
66+
67+
result = @to.convert 'Link to README.txt'
68+
assert_equal para('Link to <a href="README_txt.html">README.txt</a>'), result
69+
70+
# Don't link to file in code. Only CodeObjects are linked in <tt>.
71+
result = @to.convert '<tt>README.txt</tt>'
72+
assert_equal para('<code>README.txt</code>'), result
73+
74+
# Since <tt>README.txt</tt> is not treated as a cross-reference,
75+
# there's nothing to suppress. Backslash shouldn't be removed.
76+
result = @to.convert '<tt>\README.txt</tt>'
77+
assert_equal para('<code>\README.txt</code>'), result
78+
end
79+
5180
def test_convert_CROSSREF_ignored_excluded_words
5281
@to = RDoc::Markup::ToHtmlCrossref.new 'index.html', @c1,
5382
hyperlink_all: true, warn_missing_rdoc_ref: true,
@@ -118,7 +147,7 @@ def test_convert_CROSSREF_section_with_spaces
118147

119148
def test_convert_CROSSREF_legacy_label
120149
result = @to.convert 'C1@What-27s+Here'
121-
assert_equal para("<a href=\"C1.html#class-c1-whats-here\">What's Here at <code>C1</code></a>"), result
150+
assert_equal para("<a href=\"C1.html#class-c1-whats-here\">What&#39;s Here at <code>C1</code></a>"), result
122151
end
123152

124153
def test_convert_CROSSREF_legacy_label_colon
@@ -130,7 +159,7 @@ def test_convert_CROSSREF_legacy_section
130159
@c1.add_section "What's Here"
131160

132161
result = @to.convert "C1@What-27s+Here"
133-
assert_equal para("<a href=\"C1.html#whats-here\">What's Here at <code>C1</code></a>"), result
162+
assert_equal para("<a href=\"C1.html#whats-here\">What&#39;s Here at <code>C1</code></a>"), result
134163
end
135164

136165
def test_convert_CROSSREF_constant
@@ -430,9 +459,9 @@ def test_handle_regexp_CROSSREF_hash_preserved_for_unresolved
430459
assert_equal "#no", REGEXP_HANDLING('#no')
431460
end
432461

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")
462+
def test_cross_reference_returns_nil_for_unresolved
463+
# Fallback of unresolved refs depends on the context, so cross_reference should return nil for unresolved refs
464+
assert_nil @to.cross_reference("Missing", "Foo")
436465
end
437466

438467
private

test/rdoc/rdoc_cross_reference_test.rb

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@ def setup
1313
end
1414

1515
def assert_ref(expected, name)
16-
assert_equal expected, @xref.resolve(name, 'fail')
16+
assert_equal expected, @xref.resolve(name)
1717
end
1818

1919
def refute_ref(name)
20-
assert_equal name, @xref.resolve(name, name)
20+
assert_nil @xref.resolve(name)
2121
end
2222

2323
def test_METHOD_REGEXP_STR
@@ -202,22 +202,21 @@ def assert_resolve_method(x)
202202
end
203203

204204
def test_resolve_no_ref
205-
assert_equal '', @xref.resolve('', '')
205+
refute_ref('')
206206

207-
assert_equal "bogus", @xref.resolve("bogus", "bogus")
208-
assert_equal "\\bogus", @xref.resolve("\\bogus", "\\bogus")
209-
assert_equal "\\\\bogus", @xref.resolve("\\\\bogus", "\\\\bogus")
207+
refute_ref("bogus")
208+
refute_ref("\\bogus")
210209

211-
assert_equal "\\#n", @xref.resolve("\\#n", "fail")
212-
assert_equal "\\#n()", @xref.resolve("\\#n()", "fail")
213-
assert_equal "\\#n(*)", @xref.resolve("\\#n(*)", "fail")
210+
refute_ref("\\#n")
211+
refute_ref("\\#n()")
212+
refute_ref("\\#n(*)")
214213

215-
assert_equal "C1", @xref.resolve("\\C1", "fail")
216-
assert_equal "::C3", @xref.resolve("\\::C3", "fail")
214+
refute_ref("\\C1")
215+
refute_ref("\\::C3")
217216

218-
assert_equal "succeed", @xref.resolve("::C3::H1#n", "succeed")
219-
assert_equal "succeed", @xref.resolve("::C3::H1#n(*)", "succeed")
220-
assert_equal "\\::C3::H1#n", @xref.resolve("\\::C3::H1#n", "fail")
217+
refute_ref("::C3::H1#n")
218+
refute_ref("::C3::H1#n(*)")
219+
refute_ref("\\::C3::H1#n")
221220
end
222221

223222
end

0 commit comments

Comments
 (0)