Repository: sphinx-doc/sphinx

Issue:
ext.extlinks: Cannot escape angle brackets in link caption
Hello,

I've posted a message on [sphinx-users](https://groups.google.com/forum/#!topic/sphinx-users/Zcbw_5lwU0A) Google Group, but it looks more like a bug to me, so I'll repost it here.

-----

Hello, I'm currently documenting a C++ API using Concepts, using Sphinx 2.0.1.

I have defined the following in my conf.py:

```python
extlinks = {
    'iterconcept': ('https://en.cppreference.com/w/cpp/experimental/ranges/iterator/%s', 'std::'),
}
```

This works fine for concepts with single template parameter:

>:iterconcept:\`RandomAccessIterator\`

However, when there is more than one parameter, it fails horribly:

>:iterconcept:\`Sentinel\<I\> \<Sentinel\>\`

The output is `std::Sentinel` (instead of `std::Sentinel<I>`), and the [link](https://en.cppreference.com/w/cpp/experimental/ranges/iterator/I%3E%20%3CSentinel) contains some invalid characters.

I tried to escape the first angle brackets with backslashes, with no success.

My goal is to get the correct link with `std::Sentinel<I>` rendered with the following [link](https://en.cppreference.com/w/cpp/experimental/ranges/iterator/Sentinel).

Am I missing something obvious?



Discussion:

Patch:
diff --git a/CHANGES b/CHANGES
index 44649c0a4..d36a1d51d 100644
--- a/CHANGES
+++ b/CHANGES
@@ -116,6 +116,7 @@ Bugs fixed
 * #6165: autodoc: ``tab_width`` setting of docutils has been ignored
 * #6311: autosummary: autosummary table gets confused by complex type hints
 * Generated Makefiles lack a final EOL (refs: #6232)
+* #6375: extlinks: Cannot escape angle brackets in link caption
 * #6378: linkcheck: Send commonly used User-Agent
 
 Testing
diff --git a/sphinx/util/nodes.py b/sphinx/util/nodes.py
index 60e0144b0..58c6a6698 100644
--- a/sphinx/util/nodes.py
+++ b/sphinx/util/nodes.py
@@ -31,7 +31,7 @@
 
 
 # \x00 means the "<" was backslash-escaped
-explicit_title_re = re.compile(r'^(.+?)\s*(?<!\x00)<(.*?)>$', re.DOTALL)
+explicit_title_re = re.compile(r'^(.+?)\s*(?<!\x00)<([^<]*?)>$', re.DOTALL)
 caption_ref_re = explicit_title_re  # b/w compat alias
 
 


Test patch:
diff --git a/tests/test_util_nodes.py b/tests/test_util_nodes.py
index c83fda36f..1a2ded447 100644
--- a/tests/test_util_nodes.py
+++ b/tests/test_util_nodes.py
@@ -17,7 +17,7 @@
 from docutils.utils import new_document
 
 from sphinx.transforms import ApplySourceWorkaround
-from sphinx.util.nodes import NodeMatcher, extract_messages, clean_astext
+from sphinx.util.nodes import NodeMatcher, extract_messages, clean_astext, split_explicit_title
 
 
 def _transform(doctree):
@@ -178,3 +178,18 @@ def test_clean_astext():
     node = nodes.paragraph(text='hello world')
     node += nodes.raw('', 'raw text', format='html')
     assert 'hello world' == clean_astext(node)
+
+
+@pytest.mark.parametrize(
+    'title, expected',
+    [
+        # implicit
+        ('hello', (False, 'hello', 'hello')),
+        # explicit
+        ('hello <world>', (True, 'hello', 'world')),
+        # explicit (title having angle brackets)
+        ('hello <world> <sphinx>', (True, 'hello <world>', 'sphinx')),
+    ]
+)
+def test_split_explicit_target(title, expected):
+    assert expected == split_explicit_title(title)