Skip to content

Fix quadratic bracket scanning in link parsing - #1662

Open
simpleqt wants to merge 1 commit into
Python-Markdown:masterfrom
simpleqt:fix/798-quadratic-bracket-scan
Open

simpleqt wants to merge 1 commit into
Python-Markdown:masterfrom
simpleqt:fix/798-quadratic-bracket-scan

Conversation

@simpleqt

Copy link
Copy Markdown

Problem

LinkInlineProcessor.getText rescanned the entire remaining document on every call to find the matching ] (counting nested brackets), while the inline pattern machinery calls it once per candidate position. Documents with many unclosed [ therefore parse in O(n^2):

input master (90cdcf5) this PR
'[' * 1000 0.113 s 0.003 s
'[' * 2000 0.453 s 0.004 s
'[' * 4000 1.803 s 0.009 s

This is the case @andersk confirmed on 2026-09-16 as still quadratic after #1631 closed a different case (that one, like the merged #1621, addressed other quadratic paths).

Fix

Compute the end index of the bracket pair starting at each position in one linear right-to-left pass (maintaining the "nearest position with balance b-1" map — an open bracket at i matches the first later position whose running balance drops below balance[i]), and cache the resulting dict on the processor keyed by identity of the input string. getText becomes a single dict lookup.

This follows the approach @facelessuser sketched in the issue ("probably not a bad idea to cache some stuff").

Verification

  • Behavior is unchanged: brute-force differential of the old and new getText over ~1.4M (string, index) pairs (random strings from []ab\ up to 12 chars, every start index) — every result identical.
  • Full suite: 807 passed, 110 skipped; tests/test_syntax/extensions/test_md_in_html.py run separately (pre-existing TestSuite collection quirk): 343 passed.
  • New regression tests (TestUnclosedBracketPerformance in tests/test_syntax/inline/test_links.py): fail on master (the 20,000-bracket case takes ~50 s there vs a generous 5 s bound; that path renders wrong), pass in 0.15 s with the fix.
  • flake8 clean on both touched files (max-line-length = 119).

AI assistance disclosure: this patch was produced by an AI coding agent (ZCode) operating under the account owner's direction and review, per the project's AI-assistance policy.

Closes #798

`LinkInlineProcessor.getText` rescanned the whole remaining document on
every call to find the matching `]`, while the inline patterns call it
once per candidate position. Documents with many unclosed `[` therefore
parsed in O(n^2): the input profiled in Python-Markdown#798 (4000 unclosed brackets)
took ~1.8 s to parse, and time grows with the square of the length.

Precompute, in one linear right-to-left pass, the end index of the
bracket pair starting at each position, and cache the result per input
string. Each `getText` call is then a single dict lookup. Output is
unchanged for every input: verified against the old implementation with
a brute-force differential over ~1.4M (string, index) pairs, plus the
full suite (807 passed, 110 skipped, and 343 for md_in_html run
separately).

Fixes Python-Markdown#798

Assisted-by: ZCode (AI coding agent)
Signed-off-by: simpleqt <2015900818@qq.com>
Copilot AI balanced review requested due to automatic review settings October 16, 2026 15:48

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

RE_LINK = re.compile(r'''\(\s*(?:(<[^<>]*>)\s*(?:('[^']*'|"[^"]*")\s*)?\))?''', re.DOTALL | re.UNICODE)
RE_TITLE_CLEAN = re.compile(r'\s')

# Cache of the bracket pairing computed for the last `data` string seen by

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this shared across all instances, even across different Markdown instances?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Parsing [[[[[[… takes quadratic time

3 participants