Skip to content

fix(preset-commonmark): keep remarkLineBreak linear on long runs of spaces - #2504

Merged
Saul-Mirone merged 1 commit into
Milkdown:mainfrom
b84330808:fix/remark-line-break-quadratic
Oct 23, 2026
Merged

Saul-Mirone merged 1 commit into
Milkdown:mainfrom
b84330808:fix/remark-line-break-quadratic

Conversation

@b84330808

Copy link
Copy Markdown
Contributor
  • I read the contributing guide
  • I agree to follow the code of conduct

Summary

Closes #2503.

remarkLineBreak splits text nodes with /[\t ]*(?:\r?\n|\r)/g. When a text node has a long run of spaces or tabs with no line ending after it, the regex retries from every position in the run, and each try scans to the end of the run before failing. So it's quadratic in the run length: 'a' + ' '.repeat(50_000) + 'b' takes several seconds, which hits defaultValue, replaceAll and markdown paste.

This adds the lookbehind @malayyka suggested in the issue, /(?<![\t ])[\t ]*(?:\r?\n|\r)/g, so a match can only start at the beginning of a run. A match that starts inside a run only succeeds if the one at the start of the run does too, so the matches themselves don't change. Lookbehind is already used in strong.ts and strike-through.ts, and the build targets ES2018.

I used Claude Code to write this change.

How did you test this change?

Added packages/plugins/preset-commonmark/src/__test__/remark-line-break.spec.ts. It runs the preset's remark plugins on hand-built text nodes, since micromark already strips whitespace before soft line breaks, so parsing markdown wouldn't reach this part of the regex.

  • LF, CRLF, CR, spaces/tabs before a line ending, a leading run, and a run without a line ending all split the same as before (these pass with and without the change).
  • A 50,000-character run of spaces or tabs followed by b\nc splits correctly within 500 ms. On main these take about 4.9 s and 5.4 s and fail; with the change they take about 10 ms.

I also compared the old and new regex on every string up to length 8 over ' ', '\t', '\r', '\n', 'a', plus random inputs, and the match positions were identical. pnpm test:unit, pnpm test:lint and tsc -b for preset-commonmark run clean.

…paces

remarkLineBreak splits text nodes with /[\t ]*(?:\r?\n|\r)/g. On a run
of spaces or tabs with no line ending after it, the regex tries a match
at every position in the run, and each attempt scans to the end of the
run before it fails. Parsing `'a' + ' '.repeat(50_000) + 'b'` took
several seconds.

Add a lookbehind so that a match can start only at the start of a run.
A match inside a run can only succeed when the match at the start of
the run succeeds, so the matches stay the same.

Closes #2503

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@vercel

vercel Bot commented Oct 21, 2026

Copy link
Copy Markdown

@b84330808 is attempting to deploy a commit to the Milkdown Team on Vercel.

A member of the Team first needs to authorize it.

@changeset-bot

changeset-bot Bot commented Oct 21, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 94e98e5

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Oct 23, 2026

Copy link
Copy Markdown

Open in StackBlitz

@milkdown/components

npm i https://pkg.pr.new/@milkdown/components@2504

@milkdown/core

npm i https://pkg.pr.new/@milkdown/core@2504

@milkdown/crepe

npm i https://pkg.pr.new/@milkdown/crepe@2504

@milkdown/ctx

npm i https://pkg.pr.new/@milkdown/ctx@2504

@milkdown/exception

npm i https://pkg.pr.new/@milkdown/exception@2504

@milkdown/kit

npm i https://pkg.pr.new/@milkdown/kit@2504

@milkdown/prose

npm i https://pkg.pr.new/@milkdown/prose@2504

@milkdown/transformer

npm i https://pkg.pr.new/@milkdown/transformer@2504

@milkdown/utils

npm i https://pkg.pr.new/@milkdown/utils@2504

@milkdown/react

npm i https://pkg.pr.new/@milkdown/react@2504

@milkdown/vue

npm i https://pkg.pr.new/@milkdown/vue@2504

@milkdown/plugin-automd

npm i https://pkg.pr.new/@milkdown/plugin-automd@2504

@milkdown/plugin-block

npm i https://pkg.pr.new/@milkdown/plugin-block@2504

@milkdown/plugin-clipboard

npm i https://pkg.pr.new/@milkdown/plugin-clipboard@2504

@milkdown/plugin-collab

npm i https://pkg.pr.new/@milkdown/plugin-collab@2504

@milkdown/plugin-cursor

npm i https://pkg.pr.new/@milkdown/plugin-cursor@2504

@milkdown/plugin-diff

npm i https://pkg.pr.new/@milkdown/plugin-diff@2504

@milkdown/plugin-emoji

npm i https://pkg.pr.new/@milkdown/plugin-emoji@2504

@milkdown/plugin-highlight

npm i https://pkg.pr.new/@milkdown/plugin-highlight@2504

@milkdown/plugin-history

npm i https://pkg.pr.new/@milkdown/plugin-history@2504

@milkdown/plugin-indent

npm i https://pkg.pr.new/@milkdown/plugin-indent@2504

@milkdown/plugin-listener

npm i https://pkg.pr.new/@milkdown/plugin-listener@2504

@milkdown/plugin-prism

npm i https://pkg.pr.new/@milkdown/plugin-prism@2504

@milkdown/plugin-slash

npm i https://pkg.pr.new/@milkdown/plugin-slash@2504

@milkdown/plugin-streaming

npm i https://pkg.pr.new/@milkdown/plugin-streaming@2504

@milkdown/plugin-tooltip

npm i https://pkg.pr.new/@milkdown/plugin-tooltip@2504

@milkdown/plugin-trailing

npm i https://pkg.pr.new/@milkdown/plugin-trailing@2504

@milkdown/plugin-upload

npm i https://pkg.pr.new/@milkdown/plugin-upload@2504

@milkdown/preset-commonmark

npm i https://pkg.pr.new/@milkdown/preset-commonmark@2504

@milkdown/preset-gfm

npm i https://pkg.pr.new/@milkdown/preset-gfm@2504

@milkdown/theme-nord

npm i https://pkg.pr.new/@milkdown/theme-nord@2504

commit: 94e98e5

@Saul-Mirone
Saul-Mirone enabled auto-merge October 23, 2026 15:35
@Saul-Mirone
Saul-Mirone added this pull request to the merge queue Oct 23, 2026
Merged via the queue into Milkdown:main with commit c0f355f Oct 23, 2026
14 of 15 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.

[Bug] remarkLineBreak is quadratic on long runs of spaces or tabs (50k spaces take ~4 s to parse)

2 participants