Skip to content

Commit 5be1dce

Browse files
committed
Preserve inline styling inside tidy link labels
1 parent 88db613 commit 5be1dce

File tree

5 files changed

+227
-29
lines changed

5 files changed

+227
-29
lines changed

lib/rdoc/markup/to_html.rb

Lines changed: 159 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -158,19 +158,16 @@ def handle_regexp_RDOCLINK(target)
158158
def handle_regexp_TIDYLINK(target)
159159
text = target.text
160160

161-
return text unless
162-
text =~ /^\{(.*)\}\[(.*?)\]$/ or text =~ /^(\S+)\[(.*?)\]$/
163-
164-
label = $1
165-
url = CGI.escapeHTML($2)
161+
if tidy_link_capturing?
162+
return finish_tidy_link(text)
163+
end
166164

167-
if /^rdoc-image:/ =~ label
168-
label = handle_RDOCLINK(label)
169-
else
170-
label = CGI.escapeHTML(label)
165+
if text.start_with?('{') && !text.include?('}')
166+
start_tidy_link text
167+
return ''
171168
end
172169

173-
gen_url url, label
170+
convert_complete_tidy_link(text)
174171
end
175172

176173
# :section: Visitor
@@ -458,4 +455,156 @@ def to_html(item)
458455
super convert_flow @am.flow item
459456
end
460457

458+
private
459+
460+
def convert_flow(flow)
461+
res = []
462+
463+
flow.each do |item|
464+
case item
465+
when String then
466+
append_flow_fragment res, convert_string(item)
467+
when RDoc::Markup::AttrChanger then
468+
off_tags res, item
469+
on_tags res, item
470+
when RDoc::Markup::RegexpHandling then
471+
append_flow_fragment res, convert_regexp_handling(item)
472+
else
473+
raise "Unknown flow element: #{item.inspect}"
474+
end
475+
end
476+
477+
res.join
478+
end
479+
480+
def append_flow_fragment(res, fragment)
481+
return if fragment.nil? || fragment.empty?
482+
483+
emit_tidy_link_fragment(res, fragment)
484+
end
485+
486+
def append_to_tidy_label(fragment)
487+
@tidy_link_buffer << fragment
488+
end
489+
490+
##
491+
# Matches an entire tidy link with a braced label "{label}[url]".
492+
#
493+
# Capture 1: label contents.
494+
# Capture 2: URL text.
495+
# Capture 3: trailing content.
496+
TIDY_LINK_WITH_BRACES = /\A\{(.*?)\}\[(.*?)\](.*)\z/
497+
498+
##
499+
# Matches the tail of a braced tidy link when the opening brace was
500+
# consumed earlier while accumulating the label text.
501+
#
502+
# Capture 1: remaining label content.
503+
# Capture 2: URL text.
504+
# Capture 3: trailing content.
505+
TIDY_LINK_WITH_BRACES_TAIL = /\A(.*?)\}\[(.*?)\](.*)\z/
506+
507+
##
508+
# Matches a tidy link with a single-word label "label[url]".
509+
#
510+
# Capture 1: the single-word label (no whitespace).
511+
# Capture 2: URL text between the brackets.
512+
TIDY_LINK_SINGLE_WORD = /\A(\S+)\[(.*?)\](.*)\z/
513+
514+
def convert_complete_tidy_link(text)
515+
return text unless
516+
text =~ TIDY_LINK_WITH_BRACES or text =~ TIDY_LINK_SINGLE_WORD
517+
518+
label = $1
519+
url = CGI.escapeHTML($2)
520+
521+
label_html = if /^rdoc-image:/ =~ label
522+
handle_RDOCLINK(label)
523+
else
524+
render_tidy_link_label(label)
525+
end
526+
527+
gen_url url, label_html
528+
end
529+
530+
def emit_tidy_link_fragment(res, fragment)
531+
if tidy_link_capturing?
532+
append_to_tidy_label fragment
533+
else
534+
res << fragment
535+
end
536+
end
537+
538+
def finish_tidy_link(text)
539+
label_tail, url, trailing = extract_tidy_link_parts(text)
540+
541+
append_to_tidy_label CGI.escapeHTML(label_tail) unless label_tail.empty?
542+
543+
return '' unless url
544+
545+
label_html = @tidy_link_buffer
546+
547+
@tidy_link_buffer = nil
548+
549+
link = gen_url(url, label_html)
550+
551+
return link if trailing.empty?
552+
553+
link + CGI.escapeHTML(trailing)
554+
end
555+
556+
def extract_tidy_link_parts(text)
557+
if text =~ TIDY_LINK_WITH_BRACES
558+
[$1, CGI.escapeHTML($2), $3]
559+
elsif text =~ TIDY_LINK_WITH_BRACES_TAIL
560+
[$1, CGI.escapeHTML($2), $3]
561+
elsif text =~ TIDY_LINK_SINGLE_WORD
562+
[$1, CGI.escapeHTML($2), $3]
563+
else
564+
[text, nil, '']
565+
end
566+
end
567+
568+
def on_tags(res, item)
569+
each_attr_tag(item.turn_on) do |tag|
570+
emit_tidy_link_fragment(res, annotate(tag.on))
571+
@in_tt += 1 if tt? tag
572+
end
573+
end
574+
575+
def off_tags(res, item)
576+
each_attr_tag(item.turn_off, true) do |tag|
577+
emit_tidy_link_fragment(res, annotate(tag.off))
578+
@in_tt -= 1 if tt? tag
579+
end
580+
end
581+
582+
def start_tidy_link(text)
583+
@tidy_link_buffer = String.new
584+
append_to_tidy_label CGI.escapeHTML(text.delete_prefix('{'))
585+
end
586+
587+
def tidy_link_capturing?
588+
!!@tidy_link_buffer
589+
end
590+
591+
def render_tidy_link_label(label)
592+
RDoc::Markup::LinkLabelToHtml.render(label, @options, @from_path)
593+
end
594+
end
595+
596+
##
597+
# Formatter dedicated to rendering tidy link labels without mutating the
598+
# calling formatter's state.
599+
600+
class RDoc::Markup::LinkLabelToHtml < RDoc::Markup::ToHtml
601+
def self.render(label, options, from_path)
602+
new(options, from_path).to_html(label)
603+
end
604+
605+
def initialize(options, from_path = nil)
606+
super(options)
607+
608+
self.from_path = from_path if from_path
609+
end
461610
end

lib/rdoc/markup/to_html_crossref.rb

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -184,38 +184,36 @@ def link(name, text, code = true, rdoc_ref: false)
184184
end
185185
end
186186

187-
def convert_flow(flow)
187+
def convert_flow(flow, &block)
188188
res = []
189189

190190
i = 0
191191
while i < flow.size
192192
item = flow[i]
193193
i += 1
194194
case item
195-
when RDoc::Markup::AttrChanger then
195+
when RDoc::Markup::AttrChanger
196196
# Make "+Class#method+" a cross reference
197-
if tt_tag?(item.turn_on) and
198-
String === (str = flow[i]) and
199-
RDoc::Markup::AttrChanger === flow[i+1] and
200-
tt_tag?(flow[i+1].turn_off, true) and
201-
(@options.hyperlink_all ? ALL_CROSSREF_REGEXP : CROSSREF_REGEXP).match?(str) and
202-
(text = cross_reference str) != str
203-
then
204-
text = yield text, res if defined?(yield)
205-
res << text
206-
i += 2
207-
next
197+
if tt_tag?(item.turn_on) && String === (str = flow[i]) && RDoc::Markup::AttrChanger === flow[i+1] &&
198+
tt_tag?(flow[i+1].turn_off, true) && (@options.hyperlink_all ? ALL_CROSSREF_REGEXP : CROSSREF_REGEXP).match?(str)
199+
200+
unless tidy_link_capturing? || (text = cross_reference(str)) == str
201+
text = block.call(text, res) if block
202+
append_flow_fragment res, text
203+
i += 2
204+
next
205+
end
208206
end
209207
off_tags res, item
210208
on_tags res, item
211-
when String then
209+
when String
212210
text = convert_string(item)
213-
text = yield text, res if defined?(yield)
214-
res << text
215-
when RDoc::Markup::RegexpHandling then
211+
text = block.call(text, res) if block
212+
append_flow_fragment res, text
213+
when RDoc::Markup::RegexpHandling
216214
text = convert_regexp_handling(item)
217-
text = yield text, res if defined?(yield)
218-
res << text
215+
text = block.call(text, res) if block
216+
append_flow_fragment res, text
219217
else
220218
raise "Unknown flow element: #{item.inspect}"
221219
end

test/rdoc/rdoc_markdown_test.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1263,6 +1263,26 @@ def test_gfm_table_with_backslashes_in_code_spans
12631263
assert_equal expected, doc
12641264
end
12651265

1266+
def test_markdown_link_with_styled_label
1267+
markdown = <<~MD
1268+
[Link to Foo](https://example.com)
1269+
1270+
[Link to `Foo`](https://example.com)
1271+
1272+
[Link to **Foo**](https://example.com)
1273+
1274+
[Link to `Foo` and `\Bar` and `Baz`](https://example.com)
1275+
MD
1276+
1277+
doc = parse markdown
1278+
html = @to_html.convert doc
1279+
1280+
assert_includes html, '<a href="https://example.com">Link to Foo</a>'
1281+
assert_includes html, '<a href="https://example.com">Link to <code>Foo</code></a>'
1282+
assert_includes html, '<a href="https://example.com">Link to <strong>Foo</strong></a>'
1283+
assert_includes html, '<a href="https://example.com">Link to <code>Foo</code> and <code>Bar</code> and <code>Baz</code></a>'
1284+
end
1285+
12661286
def parse(text)
12671287
@parser.parse text
12681288
end

test/rdoc/rdoc_markup_to_html_crossref_test.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# frozen_string_literal: true
22
require_relative 'xref_test_case'
3+
require 'rdoc/markdown'
34

45
class RDocMarkupToHtmlCrossrefTest < XrefTestCase
56

@@ -270,6 +271,14 @@ def test_handle_regexp_TIDYLINK_label
270271
link, 'C1#m@foo'
271272
end
272273

274+
def test_convert_TIDYLINK_markdown_with_crossrefs
275+
markdown = RDoc::Markdown.parse('[Link to `C1` and `Foo` and `\\C1` and `Bar`](https://example.com)')
276+
277+
result = markdown.accept(@to)
278+
279+
assert_equal para('<a href="https://example.com">Link to <code>C1</code> and <code>Foo</code> and <code>\\C1</code> and <code>Bar</code></a>'), result
280+
end
281+
273282
def test_to_html_CROSSREF_email
274283
@options.hyperlink_all = false
275284

test/rdoc/rdoc_markup_to_html_test.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -734,6 +734,28 @@ def test_convert_TIDYLINK_multiple
734734
assert_equal expected, result
735735
end
736736

737+
def test_convert_TIDYLINK_with_code_label
738+
result = @to.convert '{Link to +Foo+}[https://example.com]'
739+
740+
expected = "\n<p><a href=\"https://example.com\">Link to <code>Foo</code></a></p>\n"
741+
742+
assert_equal expected, result
743+
744+
result = @to.convert '{Link to +Foo+ and +Bar+ and +Baz+}[https://example.com]'
745+
746+
expected = "\n<p><a href=\"https://example.com\">Link to <code>Foo</code> and <code>Bar</code> and <code>Baz</code></a></p>\n"
747+
748+
assert_equal expected, result
749+
end
750+
751+
def test_convert_TIDYLINK_with_bold_label
752+
result = @to.convert '{Link to *Foo*}[https://example.com]'
753+
754+
expected = "\n<p><a href=\"https://example.com\">Link to <strong>Foo</strong></a></p>\n"
755+
756+
assert_equal expected, result
757+
end
758+
737759
def test_convert_TIDYLINK_image
738760
result =
739761
@to.convert '{rdoc-image:path/to/image.jpg}[http://example.com]'

0 commit comments

Comments
 (0)