From a6717a13d412b6a719c47945ab41f540ee290d07 Mon Sep 17 00:00:00 2001 From: Vincent Gao Date: Wed, 24 Jun 2026 11:19:19 +0200 Subject: [PATCH] fix: resolve reference links when label contains code spans When a reference link label (or text in the [text][] shortcut form) contains backtick code spans, the inline backtick processor (priority 190) replaces them with placeholders before the reference processor (priority 170) resolves the link. The stored reference definition key contains the raw backtick text, causing a lookup mismatch. Fix this by: 1. Adding _unescapeId() to resolve inline placeholders in the reference ID back to plain text before lookup. 2. Storing reference definitions under a backtick-stripped key so that code spans with varying backtick counts all normalize to the same key. Fixes the long-standing issue where [label with `code`][] would not resolve even when [label with `code`]: url was defined. --- markdown/blockprocessors.py | 7 +++++++ markdown/inlinepatterns.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/markdown/blockprocessors.py b/markdown/blockprocessors.py index c2d20ddb..72601fb0 100644 --- a/markdown/blockprocessors.py +++ b/markdown/blockprocessors.py @@ -591,6 +591,13 @@ def run(self, parent: etree.Element, blocks: list[str]) -> bool: link = m.group(2).lstrip('<').rstrip('>') title = m.group(5) or m.group(6) self.parser.md.references[id] = (link, title) + # Also store under a backtick-stripped key so that inline + # reference lookups work when the label contains code spans. + # Backtick code spans are processed before reference resolution, + # and the stashed code element does not preserve the backtick count. + id_stripped = id.replace('`', '') + if id_stripped != id: + self.parser.md.references[id_stripped] = (link, title) if block[m.end():].strip(): # Add any content after match back to blocks as separate block blocks.insert(0, block[m.end():].lstrip('\n')) diff --git a/markdown/inlinepatterns.py b/markdown/inlinepatterns.py index 9f24512b..d9d28943 100644 --- a/markdown/inlinepatterns.py +++ b/markdown/inlinepatterns.py @@ -879,6 +879,35 @@ class ReferenceInlineProcessor(LinkInlineProcessor): RE_LINK = re.compile(r'\s?\[([^\]]*)\]', re.DOTALL | re.UNICODE) + def _unescapeId(self, id: str) -> str: + """Unescape inline placeholders in a reference ID for lookup. + + Inline processing (e.g. backtick code spans at priority 190) runs before + reference resolution (priority 170). Code spans are replaced with + placeholders that would not match the raw-text reference definition key. + This method reverses that transformation for reference matching. + + Backtick delimiters are stripped during normalization since the backtick + count is not preserved in the stashed element. Reference definitions are + stored under a similarly backtick-stripped key. + """ + try: + stash = self.md.treeprocessors['inline'].stashed_nodes + except (KeyError, AttributeError): + return id + + def _replace_placeholder(m: re.Match[str]) -> str: + sid = m.group(1) + if sid in stash: + value = stash.get(sid) + if isinstance(value, str): + return value + else: + return ''.join(value.itertext()) + return m.group(0) + + return util.INLINE_PLACEHOLDER_RE.sub(_replace_placeholder, id) + def handleMatch(self, m: re.Match[str], data: str) -> tuple[etree.Element | None, int | None, int | None]: """ Return [`Element`][xml.etree.ElementTree.Element] returned by `makeTag` method or `(None, None, None)`. @@ -894,6 +923,8 @@ def handleMatch(self, m: re.Match[str], data: str) -> tuple[etree.Element | None # Clean up line breaks in id id = self.NEWLINE_CLEANUP_RE.sub(' ', id) + # Unescape inline placeholders (code spans, etc.) for matching + id = self._unescapeId(id) if id not in self.md.references: # ignore undefined refs return None, m.start(0), end