From f84134daa61e33d18e210f070aa897c3d2f33167 Mon Sep 17 00:00:00 2001 From: Param Tanna Date: Wed, 28 Oct 2026 08:22:55 +0000 Subject: [PATCH] Fix quadratic time for unclosed inline-link parentheses `LinkProcessor.parse_inline_anchor_or_image` runs two O(n) scans per `[...](`: `Markdown._find_balanced` over the destination and `_inline_link_title.search`. The link loop calls it once per `[`, so an unclosed `(` makes conversion quadratic (`"[a](" * 4000`, 16 KB, ~3s; `"![a](" * n` and `"[a](" * n` the same). Both scans only matter when the destination actually closes: - `_find_balanced` returns `len(text)` immediately when no `close_c` remains, using a per-text cache of the last `close_c` position, instead of scanning to the end of the text. - `parse_inline_anchor_or_image` skips the title search when the destination did not close (`text[end_idx - 1] != ")"`), because `_inline_link_title` ends in `\)$` and cannot match otherwise. Both changes are output-preserving: the patched converter was compared against the original over 320,000 random inputs and produced identical output, and the existing test suite's results are unchanged (same set of environment-related failures before and after). `"[a](" * 4000` goes from ~3s to ~0.01s; link-heavy documents are unaffected (`rfind` is linear and cached per text). --- lib/markdown2.py | 18 ++ test/tm-cases/unclosed_link_parens.html | 409 ++++++++++++++++++++++++ test/tm-cases/unclosed_link_parens.text | 409 ++++++++++++++++++++++++ 3 files changed, 836 insertions(+) create mode 100644 test/tm-cases/unclosed_link_parens.html create mode 100644 test/tm-cases/unclosed_link_parens.text diff --git a/lib/markdown2.py b/lib/markdown2.py index 3632a54e..f63f2ef0 100755 --- a/lib/markdown2.py +++ b/lib/markdown2.py @@ -1581,6 +1581,20 @@ def _find_balanced(self, text: str, start: int, open_c: str, close_c: str) -> in out - the same number of open_c and close_c are encountered - or the end of string if it's reached before the balance point is found. """ + # If no `close_c` remains in the text, the loop below runs to the end, + # and the link scanner calls this once per `[...](`, which is quadratic + # for unclosed brackets. Skip the scan using a per-text cache of the + # last `close_c` position. + cache = getattr(self, "_balanced_last_close", None) + if cache is None or cache[0] is not text: + cache = [text, {}] + self._balanced_last_close = cache + last = cache[1].get(close_c) + if last is None: + last = text.rfind(close_c) + cache[1][close_c] = last + if start > last: + return len(text) i = start l = len(text) count = 1 @@ -3157,6 +3171,10 @@ def parse_inline_anchor_or_image(self, text: str, _link_text: str, start_idx: in if has_anglebrackets: end_idx = self.md._find_balanced(text, end_idx+1, "<", ">") end_idx = self.md._find_balanced(text, end_idx, "(", ")") + if text[end_idx - 1] != ")": + # The destination's "(" never closed, so `_inline_link_title` (which + # ends in `\)$`) cannot match; skip the search. + return match = self.md._inline_link_title.search(text, idx, end_idx) if not match: return diff --git a/test/tm-cases/unclosed_link_parens.html b/test/tm-cases/unclosed_link_parens.html new file mode 100644 index 00000000..c6dee763 --- /dev/null +++ b/test/tm-cases/unclosed_link_parens.html @@ -0,0 +1,409 @@ +

[a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

![img](

+ +

[c](unclosed

+ +

[d]( + +

[e](a(b)c

+ +

A valid ok link, a titled link, and a nested one.

diff --git a/test/tm-cases/unclosed_link_parens.text b/test/tm-cases/unclosed_link_parens.text new file mode 100644 index 00000000..e4b5a263 --- /dev/null +++ b/test/tm-cases/unclosed_link_parens.text @@ -0,0 +1,409 @@ +[a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]([a]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +![img]( + +[c](unclosed + +[d](