Skip to content

Fix code spans after unmatched backticks and avoid rescanning - #1686

Open
naaz234 wants to merge 1 commit into
Python-Markdown:masterfrom
naaz234:backtick-unmatched-rescan
Open

naaz234 wants to merge 1 commit into
Python-Markdown:masterfrom
naaz234:backtick-unmatched-rescan

Conversation

@naaz234

@naaz234 naaz234 commented Oct 31, 2026

Copy link
Copy Markdown
Contributor

Description

When an opening run of backticks has no closing run of the same length, BacktickInlineProcessor.find_code_spans pairs the backticks wrongly in two ways, and it rescans the rest of the text in Python for each such run.

The failure path returns start + max_ticks, but start is already past the opening ticks, so max_ticks more characters get skipped. When those are the first ticks of the next run, that run is split and its code span is lost. The fallback to a shorter closing run is also given up as soon as any longer run appears later in the text. Both came in with #1669, and neither matches the regex used before #1618:

input          master                 this PR, and the pre-#1618 regex
``x```y```z    ``x```y```z            ``x<code>y</code>z
``a```b`       ``a`<code>`b</code>    <code>`a```b</code>

The split runs are also what makes the first input below quadratic. Fixing only the skip is not enough though, because an escaped first tick gives the same rescan: in \``` the opening is one tick long but the scan sees a run of two, so no opening ever closes. handleMatchnow records the runs of a text once, by length, andfind_code_spans` takes the largest length up to the opening that still has a later run. That is the scan once approach mentioned in #1654, and an opening now costs no more than its own ticks. Runs are stored as distances from the end of the text because replacing an earlier span with a placeholder does not change those.

markdown.markdown(s * n) at 80 KB on CPython 3.11, in seconds:

s          master    this PR
``a`        32.25       0.01
\``         51.31       0.58
\``a        76.77       0.49
\```a`b     45.12       0.45
`a`          0.37       0.39

What is left on the escaped inputs is the string splice in __applyPattern that every repeated inline match pays, so they still grow faster than linear at larger sizes. \* has no backticks and takes 0.97 s at 80 KB and 3.46 s at 160 KB, against 0.58 s and 2.09 s for ````. I did not change that here.

Output is byte-identical to a build using the pre-#1618 regex on 1,998,100 documents: every string up to length 10 over backtick, backslash, a and space, plus 600,000 random documents, with no extensions and with extra. Master differs on 67,338 of them. Both cases from #1617 that had to stay the same are unchanged, and each is also in that comparison.

find_code_spans now relies on self.runs, which handleMatch fills when m.pos is 0, so calling it directly no longer works without that step. test_end_in_earlier_text_is_ignored covers the reset. test_many_unmatched_openings is timing only: it passes on master in 18 s and here in 0.6 s.

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:

Checklist

`find_code_spans` walked the rest of the text for every opening run of
backticks without an exact closing run. The failure path also returned
`start + max_ticks` although `start` was already past the opening ticks,
which could split the next run and lose its code span, and the fallback
to a shorter closing run was abandoned when a longer run appeared later.

Record the runs of a text once, by length, and use the largest length up
to the opening which still has a later run. This is the pairing of the
regex replaced in Python-Markdown#1618, and an opening now costs no more than its own
ticks. Runs are held as distances from the end of the text as that does
not change when earlier spans are replaced with placeholders.
@waylan waylan added the needs-review Needs to be reviewed and/or approved. label Oct 31, 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