Skip to content

[Bug]: deserializeMd drops a soft line break that follows an inline node, merging the words around it #5321

Open

Description

@venmakli

Bug type

Regression (worked before, now fails)

Browser surface

No

Owner surface

Plate framework or plugin

Summary

deserializeMd drops a soft line break that comes right after an inline node (link, emphasis, inline code), so the words on either side of the break are joined in the editor value and in the re-serialized Markdown.

Public reproduction URL

https://gist.github.com/venmakli/e5ad6bf8e0c1d8c5515a7465aabd5929

Steps to reproduce

  1. Download the reproduction (package.json and repro.ts).
  2. Run bun install && bun repro.ts.
  3. For a [link](https://x.dev)\nb the text node after the link is "b" instead of "\nb", and serialization writes a [link](https://x.dev)b. The same happens after *em* and `code`. A soft break between two plain words (a\nb) is kept in the value.

Expected behavior

The line ending after the inline node stays in the next text node, as it did in @udecode/plate-markdown 47.0.4: {"text":"\nb"} after the link, the emphasis and the inline code. The same input without an inline node (a\nb) keeps the line ending on 53.3.18 too ("a\nb").

Actual behavior

On @platejs/markdown 53.3.18 (output of repro.ts):

"a [link](https://x.dev)\nb\n"
  value:     [{"children":[{"text":"a "},{"children":[{"text":"link"}],"type":"a","url":"https://x.dev"},{"text":"b"}],"type":"p"}]
  serialize: "a [link](https://x.dev)b\n"
"a *em*\nb\n"
  value:     [{"children":[{"text":"a "},{"italic":true,"text":"em"},{"text":"b"}],"type":"p"}]
  serialize: "a _em_b\n"
"a `code`\nb\n"
  value:     [{"children":[{"text":"a "},{"code":true,"text":"code"},{"text":"b"}],"type":"p"}]
  serialize: "a `code`b\n"
"a\nb\n"
  value:     [{"children":[{"text":"a\nb"}],"type":"p"}]
  serialize: "a\\\nb\n"

The value shows linkb / emb / codeb as single words. After one deserialize → serialize round trip the text of the document changes: link b becomes linkb.

Acceptance criteria

  • After deserialization, a soft line break directly after a link, a mark or inline code stays in the following text node ("\nb" for the inputs above), as on 47.0.4.
  • Serializing that value does not join the words on either side of the break.
  • > Blockquote line1<br>\n> Blockquote line2 still deserializes to a single "\n" between the two lines, as it does on 47.0.5 and 53.3.18.

Regression range

last-known-good: "@udecode/plate-markdown 47.0.4 (published 2025-04-08T14:10Z), with @udecode/plate 46.0.10"
first-known-bad: "@udecode/plate-markdown 47.0.5 (published 2025-04-09T13:17Z); still present in @platejs/markdown 53.3.18"
evidence: >
  The same script, run on both versions, gives {"text":"\nb"} after the link, emphasis and inline code on 47.0.4,
  and {"text":"b"} on 47.0.5. 47.0.5 is the first release after PR #4200 was merged (2025-04-09T13:05Z, merge 5175db079).
  That PR contains commit 3ccdad462 "fixes leading linebreak", which changed the `text` rule from
  `text: mdastNode.value` to `text: mdastNode.value.replace(/^\n/, '')`. The published dist of 47.0.4 has no
  `replace(/^\n/` in the text rule; 47.0.5 has it.

Reproducibility

Always

Plate / Plite / React versions

plate: "@platejs/markdown 53.3.18, platejs 53.3.18, @platejs/link 53.3.5, @platejs/basic-nodes 53.0.0 (mdast-util-from-markdown 2.1.0)"
plite: not used
plite-react: not used
react: 18.3.1 (installed as a peer only; the repro is headless)

Operating system

macOS 15 (Darwin 24.6.0)

Install method

bun 1.4.4: bun install && bun repro.ts

Framework / app setup

Headless createSlateEditor with BaseBasicMarksPlugin, BaseLinkPlugin and MarkdownPlugin, no React rendering.

Affected packages or entry points

@platejs/markdown: editor.api.markdown.deserialize / deserializeMd, default text rule.

Minimal code or config excerpt

import { BaseBasicMarksPlugin } from '@platejs/basic-nodes';
import { BaseLinkPlugin } from '@platejs/link';
import { MarkdownPlugin } from '@platejs/markdown';
import { createSlateEditor } from 'platejs';

const editor = createSlateEditor({
  plugins: [BaseBasicMarksPlugin, BaseLinkPlugin, MarkdownPlugin],
});

for (const input of ['a [link](https://x.dev)\nb\n', 'a *em*\nb\n', 'a `code`\nb\n', 'a\nb\n']) {
  const value = editor.api.markdown.deserialize(input);
  editor.tf.setValue(value);
  console.log(JSON.stringify(input));
  console.log('  value:    ', JSON.stringify(value));
  console.log('  serialize:', JSON.stringify(editor.api.markdown.serialize()));
}

Logs, screenshots, and evidence

# @udecode/plate-markdown 47.0.4 + @udecode/plate 46.0.10 (same inputs, deserialize only)
"a [link](https://x.dev)\nb\n" [{"children":[{"text":"a "},{"children":[{"text":"link"}],"type":"a","url":"https://x.dev"},{"text":"\nb"}],"type":"p"}]
"a *em*\nb\n" [{"children":[{"text":"a "},{"italic":true,"text":"em"},{"text":"\nb"}],"type":"p"}]
"a `code`\nb\n" [{"children":[{"text":"a "},{"code":true,"text":"code"},{"text":"\nb"}],"type":"p"}]

# @udecode/plate-markdown 47.0.5 + @udecode/plate 46.0.10
"a [link](https://x.dev)\nb\n" [{"children":[{"text":"a "},{"children":[{"text":"link"}],"type":"a","url":"https://x.dev"},{"text":"b"}],"type":"p"}]
"a *em*\nb\n" [{"children":[{"text":"a "},{"italic":true,"text":"em"},{"text":"b"}],"type":"p"}]
"a `code`\nb\n" [{"children":[{"text":"a "},{"code":true,"text":"code"},{"text":"b"}],"type":"p"}]

# mdast from mdast-util-from-markdown 2.1.0 for "a [link](https://x.dev)\nb\n": the text after the link is "\nb"
[["text","a "],["link",null],["text","\nb"]]

# "> Blockquote line1<br>\n> Blockquote line2"
# 47.0.4:  [{"children":[{"text":"Blockquote line1"},{"text":"\n"},{"text":"\nBlockquote line2"}],"type":"blockquote"}]
# 53.3.18: [{"children":[{"children":[{"text":"Blockquote line1"},{"text":"\n"},{"text":"Blockquote line2"}],"type":"p"}],"type":"blockquote"}]

Maintainer Codex context

route: N/A (headless)
local command: bun install && bun repro.ts
fixture/data: the four inputs in repro.ts
rule location: >
  v53.3.18 and main (170d73db6, 2026-10-23): packages/markdown/src/lib/rules/defaultRules.ts:46 (LEADING_NEWLINE_REGEX),
  :1000 (text rule). next: packages/platejs/src/markdown/lib/internal/markdownIntrinsics.ts:22, :170.
tests that must keep passing: >
  3ccdad462 added "should deserialize a break followed by a \n to one line break - collapsing break"
  (packages/markdown/src/lib/deserializer/deserializeMd.spec.tsx, input "> Blockquote line1<br>\n> Blockquote line2").
  No test with that name exists in the 42 spec files under packages/markdown/src at v53.3.18; the behavior it asserted
  (a single "\n" between the two lines) is what 53.3.18 outputs today.
known blocker: none

Impact and severity

  • Affected: @platejs/markdown users who deserialize Markdown where a line ends with a link, emphasis, strong or inline code and the paragraph continues on the next line (one sentence or clause per line).
  • Severity: data risk. The change is silent, and the editor shows the merged words right after load.
  • Frequency: always for such input (4/4 inputs with an inline node before the line ending).
  • Consequence: words are joined (linkb), and the first deserialize → serialize round trip writes the joined text back to the Markdown source.

Additional information

Last known good: @udecode/plate-markdown 47.0.4. First known bad: @udecode/plate-markdown 47.0.5. The behavior has the same output on @platejs/markdown 53.3.12 and 53.3.18.

Activity

  1. yulman2207 commented on Nov 1, 2026

    @yulman2207
    Contributor

    I'll take this one. Planning a patch fix in @platejs/markdown targeting main: keep the soft break after inline nodes and only drop the newline that follows a <br>.

  2. added theissue type on Nov 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions