Skip to content

[lexical-markdown] Fix: Prevent Link Transformer from consuming preceding text#8130

Open
Sa-Te wants to merge 4 commits intofacebook:mainfrom
Sa-Te:fix/markdown-link-wrapping
Open

[lexical-markdown] Fix: Prevent Link Transformer from consuming preceding text#8130
Sa-Te wants to merge 4 commits intofacebook:mainfrom
Sa-Te:fix/markdown-link-wrapping

Conversation

@Sa-Te
Copy link
Contributor

@Sa-Te Sa-Te commented Feb 10, 2026

The Markdown link transformer was using the regex match index relative to the entire text content, rather than relative to the specific TextNode being transformed. This caused text preceding the link (e.g., Start link) to be incorrectly sliced out if the match index was non-zero.

Fix:

  • Updated replace in LINK transformer to calculate the match index relative to the current textNode.
  • Added logic to splitText only if the local index > 0.

Test Plan:

  • Added unit test MarkdownTransformers.test.ts covering the scenario where text precedes a markdown link.
  • Verified manually in Playground.

@meta-cla meta-cla bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Feb 10, 2026
@vercel
Copy link

vercel bot commented Feb 10, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
lexical Ready Ready Preview, Comment Feb 16, 2026 0:01am
lexical-playground Ready Ready Preview, Comment Feb 16, 2026 0:01am

Request Review

Comment on lines +684 to +690
const matchText = match[0];

const textContent = textNode.getTextContent();
const localMatchIndex = textContent.indexOf(matchText);
if (localMatchIndex === -1) {
return;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In what scenario would this be any different from this code?

Suggested change
const matchText = match[0];
const textContent = textNode.getTextContent();
const localMatchIndex = textContent.indexOf(matchText);
if (localMatchIndex === -1) {
return;
}
const localMatchIndex = match.index || 0;

Copy link
Contributor Author

@Sa-Te Sa-Te Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried the optimization, but it caused regressions in the LexicalMarkdown import tests. It seems match.index can become stale if other transformers modify the node content during the pass. Reverting to indexOf fixes the regression.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that the importer already splits the text based on the match before calling the replace function so localMatchIndex is always 0

Copy link
Collaborator

@etrepum etrepum left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test passes even if the MarkdownTransformers changes are reverted, so it's not clear that it's fixing anything

@Sa-Te
Copy link
Contributor Author

Sa-Te commented Feb 11, 2026

This test passes even if the MarkdownTransformers changes are reverted, so it's not clear that it's fixing anything

@etrepum Thanks for the review!

I investigated your comment about localMatchIndex always being 0. While that is true for simple cases, I found that relying on match.index (or assuming 0) actually causes 11 regressions in the lexical-markdown unit test suite.

The Issue:
In cases of nested formatting (e.g., Bold Link), a preceding transformer (like Bold) modifies the text node structure before the Link transformer runs. This causes the original match.index to become stale relative to the new text node state.

Evidence:
When I revert to match.index, tests like can import "text link" fail because the index is no longer accurate.

The Fix:
Switching to textNode.getTextContent().indexOf(match[0]) calculates the correct index on the current node state. I verified this locally, and it results in a 100% pass rate (283 tests), fixing the regressions.

@etrepum
Copy link
Collaborator

etrepum commented Feb 11, 2026

I don't see any test failures when reverting your changes, even in the new test, so if this fixes anything it needs additional tests that will fail without your changes.

Relying on match.index indeed does not work (without other refactoring) because the importer splits the TextNode before calling the replace function so the string is already split, at least in the import cases.

@Sa-Te
Copy link
Contributor Author

Sa-Te commented Feb 16, 2026

I don't see any test failures when reverting your changes, even in the new test, so if this fixes anything it needs additional tests that will fail without your changes.

Relying on match.index indeed does not work (without other refactoring) because the importer splits the TextNode before calling the replace function so the string is already split, at least in the import cases.

@etrepum Thanks for the feedback! I've added a new unit test (LINK.replace handles stale match indices) that isolates the specific race condition where match.index becomes stale.

The Issue:
When Markdown formatting is nested (e.g., Bold Link), the Bold transformer runs first and modifies the text node structure. This invalidates the original match.index (which was calculated on the full string) by the time the Link transformer runs.

Without this fix (using match.index): The new test fails, and I also see regressions in 11 existing tests (specifically regarding nested formatting).

With this fix (using indexOf): The new test passes.

This confirms that searching the current text node is necessary to handle these nested cases correctly.

@etrepum
Copy link
Collaborator

etrepum commented Feb 16, 2026

I think these unit tests are a bit too artificial to be useful, they really should be called by the transformer infrastructure (shortcuts/importer) to show that they are working as expected in the environments that they are used from. I don't think these tests are a good simulation of how that works.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. extended-tests Run extended e2e tests on a PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Comments