Skip to content

Commit 7be0cff

Browse files
facelessuserwaylan
authored andcommitted
Fix excessive backtracking when matching inline code blocks
Fixes #1617
1 parent bb50627 commit 7be0cff

2 files changed

Lines changed: 64 additions & 8 deletions

File tree

docs/changelog.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ and this project adheres to the
1010
[Python Version Specification](https://packaging.python.org/en/latest/specifications/version-specifiers/).
1111
See the [Contributing Guide](contributing.md) for details.
1212

13+
## [Unreleased]
14+
15+
### Fixed
16+
17+
* Fix an issue with excessive backtracking when matching inline code blocks (#1617).
18+
1319
## [3.10.3] - 2026-07-30
1420

1521
### Fixed

markdown/inlinepatterns.py

Lines changed: 58 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ def build_inlinepatterns(md: Markdown, **kwargs: Any) -> util.Registry[InlinePro
101101
NOIMG = r'(?<!\!)'
102102
""" Match not an image. Partial regular expression which matches if not preceded by `!`. """
103103

104-
BACKTICK_RE = r'(?:(?<!\\)((?:\\{2})+)(?=`+)|(?<!\\)(`+)(.+?)(?<!`)\2(?!`))'
104+
BACKTICK_RE = r'(?:(?<!\\)((?:\\{2})+)(?=`+)|(?<!\\)`)'
105105
""" Match backtick quoted string (`` `e=f()` `` or ``` ``e=f("`")`` ```). """
106106

107107
ESCAPE_RE = r'\\(.)'
@@ -435,28 +435,78 @@ def handleMatch(self, m: re.Match[str], data: str) -> tuple[etree.Element, int,
435435

436436
class BacktickInlineProcessor(InlineProcessor):
437437
""" Return a `<code>` element containing the escaped matching text. """
438+
439+
RE_TICKS = re.compile(r'`+')
440+
438441
def __init__(self, pattern: str):
439442
InlineProcessor.__init__(self, pattern)
440443
self.ESCAPED_BSLASH = '{}{}{}'.format(util.STX, ord('\\'), util.ETX)
441444
self.tag = 'code'
442445
""" The tag of the rendered element. """
443446

444-
def handleMatch(self, m: re.Match[str], data: str) -> tuple[etree.Element | str, int, int]:
447+
def find_code_spans(self, start: int, text: str) -> tuple[int, int] | None:
448+
"""Find code spans."""
449+
450+
# Get the maximum starting ticks
451+
m = self.RE_TICKS.match(text, start)
452+
if m is None: # pragma: no cover
453+
# This is not ever expected to happen.
454+
return None
455+
max_ticks = len(m.group(0))
456+
457+
start = m.end(0)
458+
last = len(text)
459+
longest_span = 0
460+
end = 0
461+
462+
# Find an ending span of backticks that matches our opening
463+
i = start
464+
while i < last:
465+
m = self.RE_TICKS.match(text, i)
466+
if m is None:
467+
i += 1
468+
continue
469+
470+
# Did we find the end?
471+
i = m.end(0)
472+
span_length = len(m.group(0))
473+
if max_ticks == span_length:
474+
return start, i - span_length
475+
476+
# Track the longest span of backticks we find as a fallback.
477+
if span_length > longest_span:
478+
longest_span = span_length
479+
end = i
480+
481+
# Since we didn't find an exact matching start and end,
482+
# adjust start to match the largest end we could calculate.
483+
if longest_span:
484+
return start - (max_ticks - longest_span), end - longest_span
485+
486+
# We could not find a suitable pairing
487+
return None
488+
489+
def handleMatch(self, m: re.Match[str], data: str) -> tuple[etree.Element | str | None, int | None, int | None]:
445490
"""
446491
If the match contains `group(3)` of a pattern, then return a `code`
447492
[`Element`][xml.etree.ElementTree.Element] which contains HTML escaped text (with
448493
[`code_escape`][markdown.util.code_escape]) as an [`AtomicString`][markdown.util.AtomicString].
449494
450-
If the match does not contain `group(3)` then return the text of `group(1)` backslash escaped.
495+
If the match contains `group(1)` then return the text of `group(1)` as backslash escaped.
451496
452497
"""
453-
if m.group(3):
454-
el = etree.Element(self.tag)
455-
el.text = util.AtomicString(util.code_escape(m.group(3).strip()))
456-
return el, m.start(0), m.end(0)
457-
else:
498+
if m.group(1):
458499
return m.group(1).replace('\\\\', self.ESCAPED_BSLASH), m.start(0), m.end(0)
459500

501+
begin = m.start(0)
502+
result = self.find_code_spans(begin, data)
503+
if result is not None:
504+
start, end = result
505+
el = etree.Element(self.tag)
506+
el.text = util.AtomicString(util.code_escape(data[start:end].strip()))
507+
return el, begin, result[1] + (start - begin)
508+
return None, None, None
509+
460510

461511
class DoubleTagPattern(SimpleTagPattern): # pragma: no cover
462512
"""Return a ElementTree element nested in tag2 nested in tag1.

0 commit comments

Comments
 (0)