Skip to content

Avoid rescanning for a bracket close after unclosed brackets - #1695

Open
dafili wants to merge 2 commits into
Python-Markdown:masterfrom
dafili:fix-quadratic-unclosed-brackets
Open

dafili wants to merge 2 commits into
Python-Markdown:masterfrom
dafili:fix-quadratic-unclosed-brackets

Conversation

@dafili

@dafili dafili commented Nov 7, 2026

Copy link
Copy Markdown

Description

LinkInlineProcessor.getText counts brackets from the opening bracket of every [ the link pattern matches until the count returns to zero. When a bracket is never closed the count never returns to zero, the walk runs to the end of the buffer, and the next [ walks the same span again. A run of n unclosed brackets therefore costs O(n^2) with no extensions enabled:

input before after
'[' * 2000 (2 KB) 0.79 s 0.007 s
'[' * 4000 (4 KB) 3.15 s 0.011 s
'[' * 8000 (8 KB) 12.7 s 0.024 s
'[' * 13000 (13 KB) 33.5 s 0.044 s
'[' * 20000 (20 KB) 77.6 s 0.081 s
'[' * 52000 (52 KB) not run 0.36 s

The buffer does not change while matches keep being rejected, so the ] which matches each [ is now computed once per buffer with a stack and cached on the processor, and getText becomes a dictionary lookup. A ] with no [ before it is still ignored, as it was before, so unpaired closers behave exactly as they did.

Nothing about which text and index are returned changes: for a matched bracket pair getText returns the text up to the ] and the index just past it, and for an unclosed bracket it returns the rest of the buffer and the end of the buffer. That is the same output the loop produced.

Output is unchanged. I compared the rendered HTML with master on 1 945 documents: 400 random mixes of brackets, parentheses, backticks, quotes and escapes, 1 500 longer random documents over the same alphabet plus link and image fragments, and 45 hand-written nesting shapes (matched and unmatched runs at depths 1, 2, 3 and 7, closers before openers, 200-deep runs). The md5 of the concatenated output is identical in both runs: 51cb0be444d0524fd90ae0c983141369 and ff96a25b5fee5033bbef14f0dc071d3b. The full test suite passes (1 188 tests, 111 skipped) and flake8 is clean. The new test takes 78 s on master and 0.08 s with this change.

getLink, which walks (/) the same way, is untouched: this only covers the [ direction reported in #798.

AI Assistance Disclosure

  • No AI tools were used in preparing this PR.

  • AI tools were used and I have disclosed which ones, and have fully reviewed and verified their output.

    AI tools were use to assist with the following:

    • Code
    • Code Comments
    • Documentation
    • Changelog Entry
    • Commit Message
    • The Description Above
    • Other: ______________

    An AI agent (nanobot, running on my machine) drafted the stack-based pairing above, the wording of this description and the commit message. I read the diff line by line, ran the timing and rendering comparisons myself on this machine, and can explain the change or rework it in the review.

Checklist

@waylan waylan added the needs-review Needs to be reviewed and/or approved. label Nov 9, 2026
@dafili
dafili force-pushed the fix-quadratic-unclosed-brackets branch from c23c244 to 34f2814 Compare November 9, 2026 14:15
`LinkInlineProcessor.getText` counts brackets from the opening bracket of
each `[` the link pattern matches until the count returns to zero. When a
bracket is never closed the count never returns to zero and the same walk
to the end of the buffer runs again for the next `[`, so a run of n
unclosed brackets costs O(n^2).

The buffer does not change while matches keep being rejected, so the `]`
which matches each `[` is now computed once per buffer with a stack and
cached on the processor; `getText` becomes a dictionary lookup.

Which text and index are returned for a handled and for an unhandled
match is unchanged, so the rendered output is unchanged.

Refs Python-Markdown#798.
`docs/changelog.md` only; the code and test changes are in the parent
commit and are untouched here.
@dafili
dafili force-pushed the fix-quadratic-unclosed-brackets branch from 34f2814 to b22c46c Compare November 9, 2026 14:19
@waylan

waylan commented Nov 9, 2026

Copy link
Copy Markdown
Member

This may be a duplicate of #1664. At a minimum, they are both claiming to address issue #798.

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

Labels

needs-review Needs to be reviewed and/or approved.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants