Skip to content
Merged
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
Comment thread
waylan marked this conversation as resolved.
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