Skip to content

Commit f03e1b5

Browse files
committed
Fix quadratic rendering time for many inline links
Scanning from index 0 after every inline-pattern match rescanned the unprocessed text repeatedly, making conversion quadratic in the number of inline elements. Return the index just past the inserted placeholder so the next scan starts at the unprocessed tail. Fixes #1619.
1 parent 7be0cff commit f03e1b5

3 files changed

Lines changed: 28 additions & 2 deletions

File tree

docs/changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ See the [Contributing Guide](contributing.md) for details.
1515
### Fixed
1616

1717
* 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).
1819

1920
## [3.10.3] - 2026-07-30
2021

markdown/treeprocessors.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -323,12 +323,17 @@ def __applyPattern(
323323
placeholder = self.__stashNode(node, pattern.type())
324324

325325
if new_style:
326+
# Return the index just past the inserted placeholder so the
327+
# next call scans only the unprocessed tail. Scanning from 0
328+
# after every match makes repeated inline patterns quadratic.
326329
return "{}{}{}".format(data[:start],
327-
placeholder, data[end:]), True, 0
330+
placeholder, data[end:]), True, start + len(placeholder)
328331
else: # pragma: no cover
329332
return "{}{}{}{}".format(leftData,
330333
match.group(1),
331-
placeholder, match.groups()[-1]), True, 0
334+
placeholder, match.groups()[-1]), True, (
335+
len(leftData) + len(match.group(1)) + len(placeholder)
336+
)
332337

333338
def __build_ancestors(self, parent: etree.Element | None, parents: list[str]) -> None:
334339
"""Build the ancestor list."""

tests/test_syntax/inline/test_links.py

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

2222
from markdown.test_tools import TestCase
2323

24+
import time
25+
2426

2527
class TestInlineLinks(TestCase):
2628

@@ -434,3 +436,21 @@ def test_ref_round_brackets(self):
434436
"""
435437
)
436438
)
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)