Repository navigation
Description
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
- Download the reproduction (
package.jsonandrepro.ts). - Run
bun install && bun repro.ts. - For
a [link](https://x.dev)\nbthe text node after the link is"b"instead of"\nb", and serialization writesa [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 line2still 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: noneImpact and severity
- Affected:
@platejs/markdownusers 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
yulman2207 commentedon Nov 1, 2026
I'll take this one. Planning a patch fix in
@platejs/markdowntargetingmain: keep the soft break after inline nodes and only drop the newline that follows a<br>.fix(markdown): keep soft break after inline nodes on deserialize (#5329…