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