diff --git a/lib/rdoc/markup/to_html.rb b/lib/rdoc/markup/to_html.rb
index b036dbe53f..a949c53e41 100644
--- a/lib/rdoc/markup/to_html.rb
+++ b/lib/rdoc/markup/to_html.rb
@@ -158,19 +158,16 @@ def handle_regexp_RDOCLINK(target)
def handle_regexp_TIDYLINK(target)
text = target.text
- return text unless
- text =~ /^\{(.*)\}\[(.*?)\]$/ or text =~ /^(\S+)\[(.*?)\]$/
-
- label = $1
- url = CGI.escapeHTML($2)
+ if tidy_link_capturing?
+ return finish_tidy_link(text)
+ end
- if /^rdoc-image:/ =~ label
- label = handle_RDOCLINK(label)
- else
- label = CGI.escapeHTML(label)
+ if text.start_with?('{') && !text.include?('}')
+ start_tidy_link text
+ return ''
end
- gen_url url, label
+ convert_complete_tidy_link(text)
end
# :section: Visitor
@@ -458,4 +455,153 @@ def to_html(item)
super convert_flow @am.flow item
end
+ private
+
+ def convert_flow(flow_items)
+ res = []
+
+ flow_items.each do |item|
+ case item
+ when String
+ append_flow_fragment res, convert_string(item)
+ when RDoc::Markup::AttrChanger
+ off_tags res, item
+ on_tags res, item
+ when RDoc::Markup::RegexpHandling
+ append_flow_fragment res, convert_regexp_handling(item)
+ else
+ raise "Unknown flow element: #{item.inspect}"
+ end
+ end
+
+ res.join
+ end
+
+ def append_flow_fragment(res, fragment)
+ return if fragment.nil? || fragment.empty?
+
+ emit_tidy_link_fragment(res, fragment)
+ end
+
+ def append_to_tidy_label(fragment)
+ @tidy_link_buffer << fragment
+ end
+
+ ##
+ # Matches an entire tidy link with a braced label "{label}[url]".
+ #
+ # Capture 1: label contents.
+ # Capture 2: URL text.
+ # Capture 3: trailing content.
+ TIDY_LINK_WITH_BRACES = /\A\{(.*?)\}\[(.*?)\](.*)\z/
+
+ ##
+ # Matches the tail of a braced tidy link when the opening brace was
+ # consumed earlier while accumulating the label text.
+ #
+ # Capture 1: remaining label content.
+ # Capture 2: URL text.
+ # Capture 3: trailing content.
+ TIDY_LINK_WITH_BRACES_TAIL = /\A(.*?)\}\[(.*?)\](.*)\z/
+
+ ##
+ # Matches a tidy link with a single-word label "label[url]".
+ #
+ # Capture 1: the single-word label (no whitespace).
+ # Capture 2: URL text between the brackets.
+ TIDY_LINK_SINGLE_WORD = /\A(\S+)\[(.*?)\](.*)\z/
+
+ def convert_complete_tidy_link(text)
+ return text unless
+ text =~ TIDY_LINK_WITH_BRACES or text =~ TIDY_LINK_SINGLE_WORD
+
+ label = $1
+ url = CGI.escapeHTML($2)
+
+ label_html = if /^rdoc-image:/ =~ label
+ handle_RDOCLINK(label)
+ else
+ render_tidy_link_label(label)
+ end
+
+ gen_url url, label_html
+ end
+
+ def emit_tidy_link_fragment(res, fragment)
+ if tidy_link_capturing?
+ append_to_tidy_label fragment
+ else
+ res << fragment
+ end
+ end
+
+ def finish_tidy_link(text)
+ label_tail, url, trailing = extract_tidy_link_parts(text)
+ append_to_tidy_label CGI.escapeHTML(label_tail) unless label_tail.empty?
+
+ return '' unless url
+
+ label_html = @tidy_link_buffer
+ @tidy_link_buffer = nil
+ link = gen_url(url, label_html)
+
+ return link if trailing.empty?
+
+ link + CGI.escapeHTML(trailing)
+ end
+
+ def extract_tidy_link_parts(text)
+ if text =~ TIDY_LINK_WITH_BRACES
+ [$1, CGI.escapeHTML($2), $3]
+ elsif text =~ TIDY_LINK_WITH_BRACES_TAIL
+ [$1, CGI.escapeHTML($2), $3]
+ elsif text =~ TIDY_LINK_SINGLE_WORD
+ [$1, CGI.escapeHTML($2), $3]
+ else
+ [text, nil, '']
+ end
+ end
+
+ def on_tags(res, item)
+ each_attr_tag(item.turn_on) do |tag|
+ emit_tidy_link_fragment(res, annotate(tag.on))
+ @in_tt += 1 if tt? tag
+ end
+ end
+
+ def off_tags(res, item)
+ each_attr_tag(item.turn_off, true) do |tag|
+ emit_tidy_link_fragment(res, annotate(tag.off))
+ @in_tt -= 1 if tt? tag
+ end
+ end
+
+ def start_tidy_link(text)
+ @tidy_link_buffer = String.new
+ append_to_tidy_label CGI.escapeHTML(text.delete_prefix('{'))
+ end
+
+ def tidy_link_capturing?
+ !!@tidy_link_buffer
+ end
+
+ def render_tidy_link_label(label)
+ RDoc::Markup::LinkLabelToHtml.render(label, @options, @from_path)
+ end
+end
+
+##
+# Formatter dedicated to rendering tidy link labels without mutating the
+# calling formatter's state.
+
+class RDoc::Markup::LinkLabelToHtml < RDoc::Markup::ToHtml
+ def self.render(label, options, from_path)
+ new(options, from_path).to_html(label)
+ end
+
+ def initialize(options, from_path = nil)
+ super(options)
+
+ self.from_path = from_path if from_path
+ end
end
diff --git a/lib/rdoc/markup/to_html_crossref.rb b/lib/rdoc/markup/to_html_crossref.rb
index dab486aaae..c05a3b5b17 100644
--- a/lib/rdoc/markup/to_html_crossref.rb
+++ b/lib/rdoc/markup/to_html_crossref.rb
@@ -193,9 +193,9 @@ def convert_flow(flow_items, &block)
case item
when RDoc::Markup::AttrChanger
- if (text = convert_tt_crossref(flow_items, i))
+ if !tidy_link_capturing? && (text = convert_tt_crossref(flow_items, i))
text = block.call(text, res) if block
- res << text
+ append_flow_fragment res, text
i += 3
next
end
@@ -206,12 +206,12 @@ def convert_flow(flow_items, &block)
when String
text = convert_string(item)
text = block.call(text, res) if block
- res << text
+ append_flow_fragment res, text
i += 1
when RDoc::Markup::RegexpHandling
text = convert_regexp_handling(item)
text = block.call(text, res) if block
- res << text
+ append_flow_fragment res, text
i += 1
else
raise "Unknown flow element: #{item.inspect}"
diff --git a/test/rdoc/rdoc_markdown_test.rb b/test/rdoc/rdoc_markdown_test.rb
index 19eeb1c9ef..5ca4ddfc0e 100644
--- a/test/rdoc/rdoc_markdown_test.rb
+++ b/test/rdoc/rdoc_markdown_test.rb
@@ -1263,6 +1263,26 @@ def test_gfm_table_with_backslashes_in_code_spans
assert_equal expected, doc
end
+ def test_markdown_link_with_styled_label
+ markdown = <<~MD
+ [Link to Foo](https://example.com)
+
+ [Link to `Foo`](https://example.com)
+
+ [Link to **Foo**](https://example.com)
+
+ [Link to `Foo` and `\Bar` and `Baz`](https://example.com)
+ MD
+
+ doc = parse markdown
+ html = @to_html.convert doc
+
+ assert_includes html, 'Link to Foo'
+ assert_includes html, 'Link to Foo
'
+ assert_includes html, 'Link to Foo'
+ assert_includes html, 'Link to Foo
and Bar
and Baz
'
+ end
+
def parse(text)
@parser.parse text
end
diff --git a/test/rdoc/rdoc_markup_to_html_crossref_test.rb b/test/rdoc/rdoc_markup_to_html_crossref_test.rb
index e6836ea4c2..820aade4d0 100644
--- a/test/rdoc/rdoc_markup_to_html_crossref_test.rb
+++ b/test/rdoc/rdoc_markup_to_html_crossref_test.rb
@@ -1,5 +1,6 @@
# frozen_string_literal: true
require_relative 'xref_test_case'
+require 'rdoc/markdown'
class RDocMarkupToHtmlCrossrefTest < XrefTestCase
@@ -288,6 +289,14 @@ def test_handle_regexp_TIDYLINK_label
link, 'C1#m@foo'
end
+ def test_convert_TIDYLINK_markdown_with_crossrefs
+ markdown = RDoc::Markdown.parse('[Link to `C1` and `Foo` and `\\C1` and `Bar`](https://example.com)')
+
+ result = markdown.accept(@to)
+
+ assert_equal para('Link to C1
and Foo
and \\C1
and Bar
'), result
+ end
+
def test_to_html_CROSSREF_email
@options.hyperlink_all = false
diff --git a/test/rdoc/rdoc_markup_to_html_test.rb b/test/rdoc/rdoc_markup_to_html_test.rb
index e586e03cb9..cb6774679b 100644
--- a/test/rdoc/rdoc_markup_to_html_test.rb
+++ b/test/rdoc/rdoc_markup_to_html_test.rb
@@ -734,6 +734,28 @@ def test_convert_TIDYLINK_multiple
assert_equal expected, result
end
+ def test_convert_TIDYLINK_with_code_label
+ result = @to.convert '{Link to +Foo+}[https://example.com]'
+
+ expected = "\n