fix: Avoid O(n^2) backtracking in HTML block close and tilde interrupt regexes - #4014
Merged
Merged
Conversation
The close branches end in `[^\n]*\n+`; the trailing `\n+` requires a newline, so at EOF the close can't match and the engine retries every split of the lazy `[\s\S]*?` before falling through to `$`, which is O(n^2). `\n*` closes on first match and consumes identical text whenever a trailing newline is present (the `[^\n]*` was added in markedjs#3991).
The backtick branch is guarded by a lookahead but `~{3,}` isn't, and it
overlaps the following `[^\n]*`, so a long newline-less tilde run
backtracks quadratically. Since `~{3,}` is always followed by `[^\n]*`,
`~~~` matches the same strings without the overlap. The real fences
tokenizer is left untouched.
|
@hong4rc is attempting to deploy a commit to the MarkedJS Team on Vercel. A member of the Team first needs to authorize it. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This was referenced Jul 13, 2026
styfle
approved these changes
Jul 14, 2026
github-actions Bot
pushed a commit
that referenced
this pull request
Jul 21, 2026
## [18.0.7](v18.0.6...v18.0.7) (2026-07-21) ### Bug Fixes * Avoid O(n^2) backtracking in HTML block close and tilde interrupt regexes ([#4014](#4014)) ([f945fc5](f945fc5)), closes [#3991](#3991) * Avoid O(n^2) masked source rebuild in inline tokenizer ([#4017](#4017)) ([9154f8f](9154f8f)) * keep empty list after blockquote as a sibling block ([#4004](#4004)) ([3f144a0](3f144a0)) * preserve code spans adjacent to tildes ([#4012](#4012)) ([0de7188](0de7188)) * Recognize setext headings whose first line starts with # ([#4015](#4015)) ([f056437](f056437)), closes [#1](#1) * treat a line of only tabs as a blank line between paragraphs ([#4007](#4007)) ([bc2f121](bc2f121))
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #4013 — two more spots in
src/rules.tswhere a block-level regex backtracks quadratically on a long single line. Split into one commit each.The first is the HTML block close. #3991 rewrote the close of the closing-tag, processing-instruction, declaration, and CDATA branches to
[^\n]*\n+so trailing text on the close line is kept. Keeping the trailing text is right, but the\n+isn't: it requires at least one newline, so when the input ends without a trailing newline the close can never match and the engine retries every split point of the preceding lazy[\s\S]*?before finally falling through to$. That's O(n²):Switching
\n+back to\n*closes on the first match. Whenever a trailing newline is present (the normal case)\n*and\n+consume exactly the same text, so output is unchanged — #3991's own trailing-text fixtures still pass.The second is the paragraph/table/blockquote interrupt check. Its fence sub-pattern is
(?:`{3,}(?=[^`\n]*\n)|~{3,})[^\n]*\n. The backtick branch is guarded by a lookahead, but~{3,}isn't, and it overlaps with the following[^\n]*, so a long run of tildes with no newline backtracks quadratically:Because
~{3,}is always immediately followed by[^\n]*,~~~[^\n]*matches exactly the same strings, so replacing~{3,}with~~~removes the overlap without changing what matches. The real fenced-code tokenizer (which captures the fence length via a group and a$alternative) is left untouched — only the three interrupt copies change.Both changes are behavior-preserving: the full spec and unit suites pass with identical output, and I added a
quadratic_*.cjsguard next to #4013's for each.