Skip to content

Commit f0487a0

Browse files
committed
Allow reference links with backticks
1 parent 9933a0a commit f0487a0

File tree

4 files changed

+135
-4
lines changed

4 files changed

+135
-4
lines changed

docs/changelog.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ See the [Contributing Guide](contributing.md) for details.
1717
* Ensure nested elements inside inline comments are properly unescaped (#1571).
1818
* Make the docs build successfully with mkdocstrings-python 2.0 (#1575).
1919
* Fix infinite loop when multiple bogus or unclosed HTML comments appear in input (#1578).
20+
* Backtick formatting permitted in reference links to match conventional
21+
links (#495).
2022

2123
## [3.10.0] - 2025-11-03
2224

markdown/blockprocessors.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -580,16 +580,35 @@ class ReferenceProcessor(BlockProcessor):
580580
r'^[ ]{0,3}\[([^\[\]]*)\]:[ ]*\n?[ ]*([^\s]+)[ ]*(?:\n[ ]*)?((["\'])(.*)\4[ ]*|\((.*)\)[ ]*)?$', re.MULTILINE
581581
)
582582

583+
def __init__(self, parser: BlockParser):
584+
super().__init__(parser)
585+
586+
from markdown.inlinepatterns import BACKTICK_RE, BacktickInlineProcessor
587+
self.processor = BacktickInlineProcessor(BACKTICK_RE)
588+
583589
def test(self, parent: etree.Element, block: str) -> bool:
584590
return True
585591

586592
def run(self, parent: etree.Element, blocks: list[str]) -> bool:
587593
block = blocks.pop(0)
588594
m = self.RE.search(block)
589595
if m:
590-
id = m.group(1).strip().lower()
596+
id = m.group(1).strip()
591597
link = m.group(2).lstrip('<').rstrip('>')
592598
title = m.group(5) or m.group(6)
599+
600+
# ID may contain backticks that need to be processed, so process
601+
# to the expanded form and use that as the id
602+
bt_m = self.processor.compiled_re.search(id)
603+
while bt_m and bt_m.group(3):
604+
el, start, end = self.processor.handleMatch(bt_m, id)
605+
id = '{}{}{}'.format(
606+
id[:start],
607+
etree.tostring(el, encoding='unicode'),
608+
id[end:]
609+
)
610+
bt_m = self.processor.compiled_re.search(id)
611+
593612
self.parser.md.references[id] = (link, title)
594613
if block[m.end():].strip():
595614
# Add any content after match back to blocks as separate block

markdown/inlinepatterns.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -892,6 +892,21 @@ def handleMatch(self, m: re.Match[str], data: str) -> tuple[etree.Element | None
892892
if not handled:
893893
return None, None, None
894894

895+
# If candidate data contains placeholder string - attempt to expand it
896+
# in a limited way - only going 1 level deep.
897+
if str(util.INLINE_PLACEHOLDER_PREFIX) in id:
898+
inline_processor_index = self.md.treeprocessors.get_index_for_name('inline')
899+
900+
ex_m = util.INLINE_PLACEHOLDER_RE.search(id)
901+
while ex_m and ex_m.group(1) in self.md.treeprocessors[inline_processor_index].stashed_nodes:
902+
value = self.md.treeprocessors[inline_processor_index].stashed_nodes.get(ex_m.group(1))
903+
if isinstance(value, str):
904+
id = id.replace(ex_m.group(0), value)
905+
else:
906+
# An `etree` Element - return rendered version only
907+
id = id.replace(ex_m.group(0), ''.join(etree.tostring(value, encoding='unicode')))
908+
ex_m = util.INLINE_PLACEHOLDER_RE.search(id)
909+
895910
# Clean up line breaks in id
896911
id = self.NEWLINE_CLEANUP_RE.sub(' ', id)
897912
if id not in self.md.references: # ignore undefined refs
@@ -911,10 +926,10 @@ def evalId(self, data: str, index: int, text: str) -> tuple[str | None, int, boo
911926
if not m:
912927
return None, index, False
913928
else:
914-
id = m.group(1).lower()
929+
id = m.group(1)
915930
end = m.end(0)
916931
if not id:
917-
id = text.lower()
932+
id = text
918933
return id, end, True
919934

920935
def makeTag(self, href: str, title: str, text: str) -> etree.Element:
@@ -926,6 +941,7 @@ def makeTag(self, href: str, title: str, text: str) -> etree.Element:
926941
el.set('title', title)
927942

928943
el.text = text
944+
929945
return el
930946

931947

@@ -934,7 +950,7 @@ class ShortReferenceInlineProcessor(ReferenceInlineProcessor):
934950
def evalId(self, data: str, index: int, text: str) -> tuple[str, int, bool]:
935951
"""Evaluate the id of `[ref]`. """
936952

937-
return text.lower(), index, True
953+
return text, index, True
938954

939955

940956
class ImageReferenceInlineProcessor(ReferenceInlineProcessor):

tests/test_syntax/inline/test_links.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,29 @@ def test_angles_and_nonsense_url(self):
164164
'<p><a href="?}]*+|&amp;)">test nonsense</a>.</p>'
165165
)
166166

167+
def test_monospaced_title(self):
168+
self.assertMarkdownRenders(
169+
"""[`test`](link)""",
170+
"""<p><a href="link"><code>test</code></a></p>"""
171+
)
172+
173+
def test_title_containing_monospaced_title(self):
174+
self.assertMarkdownRenders(
175+
"""[some `test`](link)""",
176+
"""<p><a href="link">some <code>test</code></a></p>"""
177+
)
178+
179+
self.assertMarkdownRenders(
180+
"""before [`test` and `test`](link) after""",
181+
"""<p>before <a href="link"><code>test</code> and <code>test</code></a> after</p>"""
182+
)
183+
184+
def test_title_containing_single_backtick(self):
185+
self.assertMarkdownRenders(
186+
"""[some `test](link)""",
187+
"""<p><a href="link">some `test</a></p>"""
188+
)
189+
167190

168191
class TestReferenceLinks(TestCase):
169192

@@ -434,3 +457,74 @@ def test_ref_round_brackets(self):
434457
"""
435458
)
436459
)
460+
461+
def test_ref_link_monospaced_text(self):
462+
self.assertMarkdownRenders(
463+
self.dedent(
464+
"""
465+
[`Text`]
466+
467+
[`Text`]: http://example.com
468+
"""
469+
),
470+
"""<p><a href="http://example.com"><code>Text</code></a></p>"""
471+
)
472+
473+
def test_ref_link_with_containing_monospaced_text(self):
474+
475+
self.assertMarkdownRenders(
476+
self.dedent(
477+
"""
478+
text before [`Text` internal `Text`] text after
479+
480+
[`Text` internal `Text`]: http://example.com
481+
"""
482+
),
483+
"""<p>text before <a href="http://example.com"><code>Text</code> internal <code>Text</code></a> text after</p>"""
484+
)
485+
486+
self.assertMarkdownRenders(
487+
self.dedent(
488+
"""
489+
[some `Text`]
490+
491+
[some `Text`]: http://example.com
492+
"""
493+
),
494+
"""<p><a href="http://example.com">some <code>Text</code></a></p>"""
495+
)
496+
497+
self.assertMarkdownRenders(
498+
self.dedent(
499+
"""
500+
[`Text` after]
501+
502+
[`Text` after]: http://example.com
503+
"""
504+
),
505+
"""<p><a href="http://example.com"><code>Text</code> after</a></p>"""
506+
)
507+
508+
self.assertMarkdownRenders(
509+
self.dedent(
510+
"""
511+
text before [`Text` internal] text after
512+
513+
[`Text` internal]: http://example.com
514+
"""
515+
),
516+
"""<p>text before <a href="http://example.com"><code>Text</code> internal</a> text after</p>"""
517+
)
518+
519+
520+
def test_ref_link_with_single_backtick(self):
521+
self.assertMarkdownRenders(
522+
self.dedent(
523+
"""
524+
[some `Text]
525+
526+
[some `Text]: http://example.com
527+
"""
528+
),
529+
"""<p><a href="http://example.com">some `Text</a></p>"""
530+
)

0 commit comments

Comments
 (0)