Skip to content

Fix quadratic-time parsing of consecutive reference definitions - #1678

Merged
waylan merged 3 commits into
Python-Markdown:masterfrom
naaz234:reference-defs-single-pass
Oct 29, 2026
Merged

waylan merged 3 commits into
Python-Markdown:masterfrom
naaz234:reference-defs-single-pass

Conversation

@naaz234

@naaz234 naaz234 commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

Description

ReferenceProcessor.run records one reference definition, puts the rest of the block back at the front of blocks and returns. The next pass runs every block processor's test over the remainder again, and BlockQuoteProcessor, HashHeaderProcessor and HRProcessor search the whole block, so a block of n consecutive definitions costs O(n²). On CPython 3.12, 8000 definitions (247 KiB) take 18 s to convert with no extensions. FootnoteBlockProcessor.run in the footnotes extension has the same one-definition-per-pass shape, and 8000 footnote definitions (177 KiB) take 14 s.

Both processors now record a run of definitions in one pass. Only a definition that is directly followed by another definition is taken early. The last one, and any definition that is indented or spans lines, is left for the next pass as before, because the block starting at it can belong to another processor: an indented code block when tab_length is below 4, or a setext header such as [b]: /url followed by === (covered by a new test). The same inputs now take 0.01 s and 0.23 s.

Output is identical to master over about 1.2 million randomly generated documents rendered with no extensions, extra, footnotes, tables, def_list, admonition, and tab_length of 2 and 3. Definitions separated by other text are still handled one per pass, since what follows a definition can change how the earlier processors treat the block.

Timing script used:

import time, markdown
for n in (2000, 4000, 8000):
    s = ''.join(f'[{i}]: http://example.com/{i}\n' for i in range(n))
    t = time.perf_counter(); markdown.markdown(s); print('references', n, round(time.perf_counter() - t, 2))
md = markdown.Markdown(extensions=['footnotes'])
for n in (2000, 4000, 8000):
    s = ''.join(f'[^{i}]: Footnote {i}\n' for i in range(n))
    md.reset(); t = time.perf_counter(); md.convert(s); print('footnotes', n, round(time.perf_counter() - t, 2))

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 the randomized output comparison against master

Checklist

ReferenceProcessor and FootnoteBlockProcessor record one definition per
pass and return the rest of the block to be parsed again, so every
earlier block processor rescans the shrinking block once per definition.
Record a run of definitions in a single pass, leaving the last one for
the next pass when other content follows it so the earlier processors
still see the same blocks they do today. Add regression tests and a
changelog entry.
Comment thread markdown/extensions/footnotes.py Outdated
Comment thread tests/test_syntax/blocks/test_reference_links.py
Use finditer for footnote definitions as for reference definitions, and
no longer leave the last definition of a run, or an indented one, for the
next pass. A setext underline after consecutive reference definitions is
now a paragraph rather than turning the last definition into a header.
Comment thread markdown/blockprocessors.py Outdated
Comment thread markdown/blockprocessors.py Outdated
Comment thread markdown/extensions/footnotes.py
Record each reference definition as it is matched rather than collecting
them first, and add tests covering runs of reference and footnote
definitions next to split definitions, paragraphs, headers, rules,
blockquotes and code blocks.
@waylan
waylan merged commit 0bfd8c3 into Python-Markdown:master Oct 29, 2026
14 checks passed
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