Skip to content

Fix quadratic time for unclosed brackets in link text - #1664

Open
naaz234 wants to merge 1 commit into
Python-Markdown:masterfrom
naaz234:link-text-unclosed-brackets
Open

naaz234 wants to merge 1 commit into
Python-Markdown:masterfrom
naaz234:link-text-unclosed-brackets

Conversation

@naaz234

@naaz234 naaz234 commented Oct 19, 2026

Copy link
Copy Markdown
Contributor

Description

LinkInlineProcessor.getText (used by the link, image and reference processors) scans forward from a [ until the brackets balance. The treeprocessor calls it for every [ in the text, and when the brackets never close each call scans to the end, so conversion time grows with the square of the number of unclosed brackets. Timings for markdown.markdown(s * n) on CPython 3.11:

'[['   x8000  (16000 bytes): 55.91s -> 0.02s
'!['   x8000  (16000 bytes): 28.95s -> 0.01s
'[ ['  x8000  (24000 bytes): 68.13s -> 0.03s

This now finds the closing bracket for every index in one pass and caches the result for the current text, so repeated calls on the same text are a list lookup. I compared the new getText with the old loop on every index of about 30,000 random strings made of [, ], a and ( (about 400,000 calls) and the results were identical. The new tests pass with and without the change. test_many_unclosed_square_brackets takes about 27 seconds without the fix and finishes instantly with it.

getLink has a similar rescan for unclosed ( but its quote/title handling is more involved, so I left it out of this change.

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: tests, and finding the slow input with a timing script

    Tool used: Claude Code (Claude Opus 5.5). wrote the patch, tests, changelog entry, commit message and this description. I reviewed and checked all of it before submitting.

Checklist

`LinkInlineProcessor.getText` scanned forward from each `[` to find its
matching `]`. The inline treeprocessor calls it for every `[` in the
text, so when the brackets never close each call scanned to the end of
the text and conversion became quadratic in the number of brackets.

Find the closing bracket for every index in a single pass and cache the
result for the current text. Matches and returned text are unchanged.
)

def test_many_unclosed_square_brackets(self):
# Each unclosed bracket used to rescan the rest of the text.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The spell checker seems to not like the word "rescan." It should probably be added to .spell-dict.

@waylan

waylan commented Oct 19, 2026

Copy link
Copy Markdown
Member

I am going to let @facelessuser review this as he is more familiar with the affected code.

@waylan
waylan requested a review from facelessuser October 19, 2026 19:03
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.

2 participants