Skip to content

Fix quadratic time for unclosed parentheses in link targets - #1680

Open
ParamTanna wants to merge 1 commit into
Python-Markdown:masterfrom
ParamTanna:fix-getlink-quadratic
Open

ParamTanna wants to merge 1 commit into
Python-Markdown:masterfrom
ParamTanna:fix-getlink-quadratic

Conversation

@ParamTanna

Copy link
Copy Markdown

Description

LinkInlineProcessor.getLink scans forward from the ( of a link target to pair the brackets. The treeprocessor calls it for every [text]( in the text, and when the parentheses never close each call scans to the end, so conversion time grows with the square of the number of unclosed (. Timings for markdown.markdown(s * n) on CPython 3.12:

'[a]('    x4000  (16000 bytes): 2.27s -> 0.015s
'[a](x '  x4000  (24000 bytes): 3.56s -> 0.018s
'[a](<u>' x4000  (28000 bytes): 4.11s -> 0.064s

This is the getLink companion to #1664 (which fixed the same problem in getText and noted that getLink was left out because its quote/title handling is more involved).

The quadratic inputs all turn out to be the ones with no quote before the matching ) — there the scan is just a plain bracket match. Those are now resolved from a table of closing positions built in a single pass per text (_findParenEnds, same monotonic-stack idea as #1664's getText), guarded by a per-text "next quote" table so the fast path only fires when no quote can start a title. Inputs where a quote may begin a title fall back to the original character-by-character loop unchanged; I measured those and they were already linear, so nothing is lost.

To be sure the fast path is behaviour-preserving, I compared the new getLink against the old loop on the raw (href, title, index, handled) tuple at every index of:

  • all 1,111,111 strings up to length 6 over the ten characters [ ] ( ) ' " < > a and space, and
  • 2,000,000 random strings of length 1–60 over a wider alphabet.

The results were identical in every case. The existing test suite passes unchanged (1166 tests), plus two new regression tests. test_many_unclosed_link_parens takes several seconds without the fix and finishes instantly with it.

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 used to assist with the following:

    • Code
    • Code Comments
    • Documentation
    • Changelog Entry
    • Commit Message
    • The Description Above
    • Other: the regression tests, the equivalence/fuzz comparison harness, and the timing script used to find the slow inputs

    Tool used: Claude Code (Claude Opus 4.8). I reviewed and verified all of it before submitting, including running the equivalence comparison and the full test suite myself.

Checklist

`LinkInlineProcessor.getLink` scans forward from the `(` of a link target
to pair the brackets. The treeprocessor calls it for every `[text](` in
the text, and when the parentheses never close each call scans to the end,
so conversion time grows with the square of the number of unclosed `(`.
For example `markdown.markdown('[a](' * n)` on CPython 3.12:

    '[a]('    x4000  (16000 bytes): 2.27s -> 0.015s
    '[a](x '  x4000  (24000 bytes): 3.56s -> 0.018s
    '[a](<u>' x4000  (28000 bytes): 4.11s -> 0.064s

When no quote appears before the matching `)` the scan is a plain bracket
match, so resolve it from a table of closing positions built in a single
pass per text and only fall back to the character-by-character loop when a
quote may start a title (the quote/title inputs were already linear). The
new `getLink` was compared with the old loop at every index of all 1.1M
strings up to length 6 over the 10 characters `[ ] ( ) ' " < > a` and
space, and of 2M longer random strings; the returned tuples were identical
in every case.

This is the `getLink` companion to the `getText` fix in Python-Markdown#1664.
@waylan waylan added the needs-review Needs to be reviewed and/or approved. label Oct 28, 2026
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