Skip to content

Commit b745bba

Browse files
committed
Address review feedback on inline processor fix
1 parent f03e1b5 commit b745bba

3 files changed

Lines changed: 40 additions & 21 deletions

File tree

docs/changelog.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,14 @@ See the [Contributing Guide](contributing.md) for details.
1212

1313
## [Unreleased]
1414

15+
### Changed
16+
17+
* Inline processors now resume searching after the previous match, improving
18+
performance for repeated inline patterns (#1619).
19+
1520
### Fixed
1621

1722
* Fix an issue with excessive backtracking when matching inline code blocks (#1617).
18-
* Fix quadratic rendering time when a paragraph contains many inline links (#1619).
1923

2024
## [3.10.3] - 2026-07-30
2125

tests/test_apis.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -725,13 +725,48 @@ def testInlineProcessorDoesntCrashWithWrongAtomicString(self):
725725
'<div><p>a &lt;b&gt;atomic&lt;/b&gt; c</p></div>'
726726
)
727727

728+
def testInlineProcessorAdvancesSearchIndex(self):
729+
"""Test that repeated matches resume after the previous match."""
730+
pattern = _InlineProcessorThatRecordsSearchIndex(r'x', self.md)
731+
self.md.inlinePatterns.register(pattern, 'record-search-index', 1000)
732+
733+
self.assertEqual(self.md.convert('xxxx'), '<p>xxxx</p>')
734+
self.assertEqual(pattern.start_indices[0], 0)
735+
self.assertGreater(pattern.start_indices[1], pattern.start_indices[0])
736+
728737

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

734743

744+
class _RecordingPattern:
745+
"""Proxy a compiled pattern while recording the search offsets."""
746+
747+
def __init__(self, pattern, start_indices):
748+
self.pattern = pattern
749+
self.start_indices = start_indices
750+
751+
def finditer(self, data, start_index=0):
752+
self.start_indices.append(start_index)
753+
return self.pattern.finditer(data, start_index)
754+
755+
756+
class _InlineProcessorThatRecordsSearchIndex(inlinepatterns.InlineProcessor):
757+
"""Record each offset passed to the processor's compiled expression."""
758+
759+
def __init__(self, pattern, md):
760+
super().__init__(pattern, md)
761+
self.start_indices = []
762+
763+
def getCompiledRegExp(self):
764+
return _RecordingPattern(self.compiled_re, self.start_indices)
765+
766+
def handleMatch(self, m, data):
767+
return m.group(0), m.start(0), m.end(0)
768+
769+
735770
class TestConfigParsing(unittest.TestCase):
736771
def assertParses(self, value, result):
737772
self.assertIs(markdown.util.parseBoolValue(value, False), result)

tests/test_syntax/inline/test_links.py

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@
2121

2222
from markdown.test_tools import TestCase
2323

24-
import time
25-
2624

2725
class TestInlineLinks(TestCase):
2826

@@ -436,21 +434,3 @@ def test_ref_round_brackets(self):
436434
"""
437435
)
438436
)
439-
440-
def test_many_repeated_links(self):
441-
# Regression test for #1619: rendering many inline links in one
442-
# paragraph used to rescan the unprocessed text from the start after
443-
# every match, making conversion quadratic in the number of links.
444-
from markdown import Markdown
445-
446-
text = "[link](x)" * 8192
447-
start = time.monotonic()
448-
html = Markdown().convert(text)
449-
elapsed = time.monotonic() - start
450-
451-
# The old implementation takes several seconds (or more on slow CI)
452-
# for this input; the linear implementation finishes well under a
453-
# second on any machine. Allow a generous ceiling to avoid flakes.
454-
self.assertLess(elapsed, 5)
455-
self.assertEqual(html.count("<a "), 8192)
456-
self.assertEqual(html.count("</a>"), 8192)

0 commit comments

Comments
 (0)