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
5 changes: 5 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ See the [Contributing Guide](contributing.md) for details.

## [Unreleased]

### Changed

* Inline processors now resume searching after the previous match, improving
performance for repeated inline patterns (#1619).

### Fixed

* Fix an issue with excessive backtracking when matching inline code blocks (#1617).
Expand Down
9 changes: 7 additions & 2 deletions markdown/treeprocessors.py

@waylan waylan Aug 11, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This changes the API for all inline patterns and processors. @facelessuser I think you have a better handle on this part of the code. Any input on that change here?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I kept the search-index behavior change and documented it under Unreleased > Changed in b745bba, treating it as an inline-processor API/behavior change rather than a Fixed-only entry.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do have some mild concerns about this change. We are assuming that because the API changed here without breaking things, others using this API won't be broken.

When we changed the API here in the first time, we did so in a way that was non-breaking, one you could opt into. This was done by providing a different class that could be checked.

It is possible that we may need to employ some method ot indicate this is a different version of the new way. I really need to do some testing to understand the implications of the change. I will have to do some testing with this over in Pymdown Extensions, where we have a number of plugins using the new style, so I can get a better idea of what the impact here is.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that all the tests over on Pymdown Extensions passed with these changes, so that is a good sign.

Original file line number Diff line number Diff line change
Expand Up @@ -323,12 +323,17 @@ def __applyPattern(
placeholder = self.__stashNode(node, pattern.type())

if new_style:
# Return the index just past the inserted placeholder so the
# next call scans only the unprocessed tail. Scanning from 0
# after every match makes repeated inline patterns quadratic.
return "{}{}{}".format(data[:start],
placeholder, data[end:]), True, 0
placeholder, data[end:]), True, start + len(placeholder)
else: # pragma: no cover
return "{}{}{}{}".format(leftData,
match.group(1),
placeholder, match.groups()[-1]), True, 0
placeholder, match.groups()[-1]), True, (
len(leftData) + len(match.group(1)) + len(placeholder)
)

def __build_ancestors(self, parent: etree.Element | None, parents: list[str]) -> None:
"""Build the ancestor list."""
Expand Down
35 changes: 35 additions & 0 deletions tests/test_apis.py
Original file line number Diff line number Diff line change
Expand Up @@ -725,13 +725,48 @@ def testInlineProcessorDoesntCrashWithWrongAtomicString(self):
'<div><p>a &lt;b&gt;atomic&lt;/b&gt; c</p></div>'
)

def testInlineProcessorAdvancesSearchIndex(self):
"""Test that repeated matches resume after the previous match."""
pattern = _InlineProcessorThatRecordsSearchIndex(r'x', self.md)
self.md.inlinePatterns.register(pattern, 'record-search-index', 1000)

self.assertEqual(self.md.convert('xxxx'), '<p>xxxx</p>')
self.assertEqual(pattern.start_indices[0], 0)
self.assertGreater(pattern.start_indices[1], pattern.start_indices[0])


class _InlineProcessorThatReturnsAtomicString(inlinepatterns.InlineProcessor):
""" Return a simple text of `group(1)` of a Pattern. """
def handleMatch(self, m, data):
return markdown.util.AtomicString('<b>atomic</b>'), m.start(0), m.end(0)


class _RecordingPattern:
"""Proxy a compiled pattern while recording the search offsets."""

def __init__(self, pattern, start_indices):
self.pattern = pattern
self.start_indices = start_indices

def finditer(self, data, start_index=0):
self.start_indices.append(start_index)
return self.pattern.finditer(data, start_index)


class _InlineProcessorThatRecordsSearchIndex(inlinepatterns.InlineProcessor):
"""Record each offset passed to the processor's compiled expression."""

def __init__(self, pattern, md):
super().__init__(pattern, md)
self.start_indices = []

def getCompiledRegExp(self):
return _RecordingPattern(self.compiled_re, self.start_indices)

def handleMatch(self, m, data):
return m.group(0), m.start(0), m.end(0)


class TestConfigParsing(unittest.TestCase):
def assertParses(self, value, result):
self.assertIs(markdown.util.parseBoolValue(value, False), result)
Expand Down
Loading