Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ See the [Contributing Guide](contributing.md) for details.
* Ensure nested elements inside inline comments are properly unescaped (#1571).
* Make the docs build successfully with mkdocstrings-python 2.0 (#1575).
* Fix infinite loop when multiple bogus or unclosed HTML comments appear in input (#1578).
* Backtick formatting permitted in reference links to match conventional
links (#495).

## [3.10.0] - 2025-11-03

Expand Down
21 changes: 20 additions & 1 deletion markdown/blockprocessors.py
Original file line number Diff line number Diff line change
Expand Up @@ -580,16 +580,35 @@ class ReferenceProcessor(BlockProcessor):
r'^[ ]{0,3}\[([^\[\]]*)\]:[ ]*\n?[ ]*([^\s]+)[ ]*(?:\n[ ]*)?((["\'])(.*)\4[ ]*|\((.*)\)[ ]*)?$', re.MULTILINE
)

def __init__(self, parser: BlockParser):
super().__init__(parser)

from markdown.inlinepatterns import BACKTICK_RE, BacktickInlineProcessor
self.processor = BacktickInlineProcessor(BACKTICK_RE)

def test(self, parent: etree.Element, block: str) -> bool:
return True

def run(self, parent: etree.Element, blocks: list[str]) -> bool:
block = blocks.pop(0)
m = self.RE.search(block)
if m:
id = m.group(1).strip().lower()
id = m.group(1).strip()
link = m.group(2).lstrip('<').rstrip('>')
title = m.group(5) or m.group(6)

# ID may contain backticks that need to be processed, so process
# to the expanded form and use that as the id
bt_m = self.processor.compiled_re.search(id)
while bt_m and bt_m.group(3):
el, start, end = self.processor.handleMatch(bt_m, id)
id = '{}{}{}'.format(
id[:start],
etree.tostring(el, encoding='unicode'),
id[end:]
)
bt_m = self.processor.compiled_re.search(id)

self.parser.md.references[id] = (link, title)
if block[m.end():].strip():
# Add any content after match back to blocks as separate block
Expand Down
22 changes: 19 additions & 3 deletions markdown/inlinepatterns.py
Original file line number Diff line number Diff line change
Expand Up @@ -892,6 +892,21 @@ def handleMatch(self, m: re.Match[str], data: str) -> tuple[etree.Element | None
if not handled:
return None, None, None

# If candidate data contains placeholder string - attempt to expand it
# in a limited way - only going 1 level deep.
if str(util.INLINE_PLACEHOLDER_PREFIX) in id:
inline_processor_index = self.md.treeprocessors.get_index_for_name('inline')

ex_m = util.INLINE_PLACEHOLDER_RE.search(id)
while ex_m and ex_m.group(1) in self.md.treeprocessors[inline_processor_index].stashed_nodes:
value = self.md.treeprocessors[inline_processor_index].stashed_nodes.get(ex_m.group(1))
if isinstance(value, str):
id = id.replace(ex_m.group(0), value)
else:
# An `etree` Element - return rendered version only
id = id.replace(ex_m.group(0), ''.join(etree.tostring(value, encoding='unicode')))
ex_m = util.INLINE_PLACEHOLDER_RE.search(id)

# Clean up line breaks in id
id = self.NEWLINE_CLEANUP_RE.sub(' ', id)
if id not in self.md.references: # ignore undefined refs
Expand All @@ -911,10 +926,10 @@ def evalId(self, data: str, index: int, text: str) -> tuple[str | None, int, boo
if not m:
return None, index, False
else:
id = m.group(1).lower()
id = m.group(1)
end = m.end(0)
if not id:
id = text.lower()
id = text
return id, end, True

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

el.text = text

return el


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

return text.lower(), index, True
return text, index, True


class ImageReferenceInlineProcessor(ReferenceInlineProcessor):
Expand Down
94 changes: 94 additions & 0 deletions tests/test_syntax/inline/test_links.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,29 @@ def test_angles_and_nonsense_url(self):
'<p><a href="?}]*+|&amp;)">test nonsense</a>.</p>'
)

def test_monospaced_title(self):
self.assertMarkdownRenders(
"""[`test`](link)""",
"""<p><a href="link"><code>test</code></a></p>"""
)

def test_title_containing_monospaced_title(self):
self.assertMarkdownRenders(
"""[some `test`](link)""",
"""<p><a href="link">some <code>test</code></a></p>"""
)

self.assertMarkdownRenders(
"""before [`test` and `test`](link) after""",
"""<p>before <a href="link"><code>test</code> and <code>test</code></a> after</p>"""
)

def test_title_containing_single_backtick(self):
self.assertMarkdownRenders(
"""[some `test](link)""",
"""<p><a href="link">some `test</a></p>"""
)


class TestReferenceLinks(TestCase):

Expand Down Expand Up @@ -434,3 +457,74 @@ def test_ref_round_brackets(self):
"""
)
)

def test_ref_link_monospaced_text(self):
self.assertMarkdownRenders(
self.dedent(
"""
[`Text`]

[`Text`]: http://example.com
"""
),
"""<p><a href="http://example.com"><code>Text</code></a></p>"""
)

def test_ref_link_with_containing_monospaced_text(self):

self.assertMarkdownRenders(
self.dedent(
"""
text before [`Text` internal `Text`] text after

[`Text` internal `Text`]: http://example.com
"""
),
"""<p>text before <a href="http://example.com"><code>Text</code> internal <code>Text</code></a> text after</p>"""
)

self.assertMarkdownRenders(
self.dedent(
"""
[some `Text`]

[some `Text`]: http://example.com
"""
),
"""<p><a href="http://example.com">some <code>Text</code></a></p>"""
)

self.assertMarkdownRenders(
self.dedent(
"""
[`Text` after]

[`Text` after]: http://example.com
"""
),
"""<p><a href="http://example.com"><code>Text</code> after</a></p>"""
)

self.assertMarkdownRenders(
self.dedent(
"""
text before [`Text` internal] text after

[`Text` internal]: http://example.com
"""
),
"""<p>text before <a href="http://example.com"><code>Text</code> internal</a> text after</p>"""
)


def test_ref_link_with_single_backtick(self):
self.assertMarkdownRenders(
self.dedent(
"""
[some `Text]

[some `Text]: http://example.com
"""
),
"""<p><a href="http://example.com">some `Text</a></p>"""
)
Loading