diff --git a/docs/changelog.md b/docs/changelog.md index 5e6fc0117..074121705 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -18,6 +18,8 @@ See the [Contributing Guide](contributing.md) for details. * For backwards compatibility in some languages, revert the enforcement of CommonMark style punctuation rules (#1657). +* Fix quadratic processing time in the `fenced_code` extension for documents + with many fenced code blocks (#1660). ## [3.11.1] - 2026-10-13 diff --git a/markdown/extensions/fenced_code.py b/markdown/extensions/fenced_code.py index 603f0bc7b..00dacfee4 100644 --- a/markdown/extensions/fenced_code.py +++ b/markdown/extensions/fenced_code.py @@ -102,6 +102,9 @@ def run(self, lines: list[str]) -> list[str]: self.checked_for_deps = True text = "\n".join(lines) + # Collect the output in pieces, as rebuilding the full text after each block is quadratic. + pieces: list[str] = [] + last = 0 index = 0 while 1: m = self.FENCED_BLOCK_RE.search(text, index) @@ -163,12 +166,13 @@ def run(self, lines: list[str]) -> list[str]: code = f'
{code}'
placeholder = self.md.htmlStash.store(code)
- text = f'{text[:m.start()]}\n{placeholder}\n{text[m.end():]}'
+ pieces.append(f'{text[last:m.start()]}\n{placeholder}\n')
# Continue from after the replaced text in the next iteration.
- index = m.start() + 1 + len(placeholder)
+ index = last = m.end()
else:
break
- return text.split("\n")
+ pieces.append(text[last:])
+ return "".join(pieces).split("\n")
def handle_attrs(self, attrs: Iterable[tuple[str, str]]) -> tuple[str, list[str], dict[str, Any]]:
""" Return tuple: `(id, [list, of, classes], {configs})` """