diff --git a/docs/changelog.md b/docs/changelog.md index 6db0b5c0..5c704413 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -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 diff --git a/markdown/blockprocessors.py b/markdown/blockprocessors.py index 3ed4cf07..e47ae5bb 100644 --- a/markdown/blockprocessors.py +++ b/markdown/blockprocessors.py @@ -580,6 +580,12 @@ 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 @@ -587,9 +593,22 @@ 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 diff --git a/markdown/inlinepatterns.py b/markdown/inlinepatterns.py index 9f24512b..c23e4760 100644 --- a/markdown/inlinepatterns.py +++ b/markdown/inlinepatterns.py @@ -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 @@ -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: @@ -926,6 +941,7 @@ def makeTag(self, href: str, title: str, text: str) -> etree.Element: el.set('title', title) el.text = text + return el @@ -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): diff --git a/tests/test_syntax/inline/test_links.py b/tests/test_syntax/inline/test_links.py index e57bd995..9005bb57 100644 --- a/tests/test_syntax/inline/test_links.py +++ b/tests/test_syntax/inline/test_links.py @@ -164,6 +164,29 @@ def test_angles_and_nonsense_url(self): '
' ) + def test_monospaced_title(self): + self.assertMarkdownRenders( + """[`test`](link)""", + """""" + ) + + def test_title_containing_monospaced_title(self): + self.assertMarkdownRenders( + """[some `test`](link)""", + """""" + ) + + self.assertMarkdownRenders( + """before [`test` and `test`](link) after""", + """before test and test after
text before Text internal Text text after
text before Text internal text after