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})` """