Skip to content

[Bug]: HTML deserialization of inline-only content returns unwrapped root text nodes, crashing the editor since chunking became default #5061

Description

@harsh863

Bug type

Crash (process/app exits or hangs)

Browser surface

Yes

Summary

deserializeHtml / editor.api.html.deserialize returns bare Text nodes at the document root when the HTML fragment contains only inline content (e.g. first<br><br>second, <b>bold</b> text, <font>x</font> y, or a <div> that unwraps to loose text). This is an invalid Slate value ("top-level nodes must be Elements"), and since chunking was enabled by default in 49.0.5 (#4371), rendering such a value crashes React with:

TypeError: undefined is not iterable (cannot read property Symbol(Symbol.iterator))
    at splitDecorationsByChild (slate-dom)
    at useDecorationsByChild → useChildren → Children (slate-react)

The root cause is in normalizeDescendantsToDocumentFragment (packages/core/src/lib/utils/normalizeDescendantsToDocumentFragment.ts): normalizeDifferentNodeTypes only wraps inline nodes into a defaultElementPlugin block when the fragment mixes block and inline siblings (hasDifferentChildNodes). A fragment that is 100% inline/text passes through unwrapped.

This same signature is reported in #5043 from a different producer (LengthPlugin truncation on paste); the render-side vulnerability to invalid children appears to be shared.

Public reproduction URL

N/A — 3-line repro below, no app setup required.

Steps to reproduce

import { createPlateEditor } from 'platejs/react';

const editor = createPlateEditor({ plugins: [] });

const value = editor.api.html.deserialize({ element: 'first<br><br>second' });
console.log(value);
// → [ { text: 'first\n\nsecond' } ]   ← bare Text node at editor root, no block wrapper

Then either:

  1. Render it (crash):
const editor = createPlateEditor({ plugins: [], value });
render(
  <Plate editor={editor}>
    <PlateContent readOnly />
  </Plate>
);
// → TypeError: undefined is not iterable (cannot read property Symbol(Symbol.iterator))
  1. Or follow the official docs example (silent data loss): the HTML docs recommend editor.tf.setValue(editor.api.html.deserialize({ element })). Slate's default normalization handles a root-level text node by removing it, so the deserialized content is wiped to [] instead of crashing.

Other inline-only inputs that produce the same invalid output: <b>bold</b> plain, text <a href="x">link</a>, <span style="color:red">x</span> y, <font color="#f00">x</font> y, <div>a<br>b</div> (no element parser for div, so its children unwrap to root).

Expected behavior

deserializeHtml always returns a valid Slate Value: root-level runs of inline/text nodes are wrapped in the defaultElementPlugin block (paragraph by default), the same way they already are when the fragment mixes block and inline nodes.

Given 'first<br><br>second':

[{ "type": "p", "children": [{ "text": "first\n\nsecond" }] }]

Actual behavior

[{ "text": "first\n\nsecond" }]

Rendering this crashes (stack above); editor.tf.setValue erases it; chunking: false makes the same value render, confirming the crash is in the chunked render path.

Regression window

  • Deserializer output is unchanged — @udecode/plate@47 produces the same bare text node. But slate-react 0.112 rendered it leniently, so this was latent.
  • Since @udecode/plate-core@49.0.5 (Enable chunking optimization by default #4371, chunking on by default) the same value crashes at render. Verified on platejs@52.3.4 (slate-react 0.123) and platejs@52.3.21 (slate 0.124).

Suggested fix

In normalizeDescendantsToDocumentFragment, wrap inline nodes into the default block even when all top-level descendants are inline — i.e. drop or extend the hasDifferentChildNodes precondition in normalizeDifferentNodeTypes so an all-inline fragment gets the same treatment as a mixed one. Happy to open a PR if that direction is acceptable.

Acceptance criteria

  • editor.api.html.deserialize({ element: 'a<br><br>b' }) returns block-wrapped nodes.
  • Rendering the deserialized value of any HTML fragment does not crash.
  • editor.tf.setValue(editor.api.html.deserialize({ element })) (the documented import flow) does not lose content for inline-only HTML.

Plate / Slate / React versions

platejs: 52.3.4 (also reproduced on 52.3.21)
slate-react: 0.123.0 (also 0.124)
react: 18.3.1

Operating system

macOS (darwin 25.5.0) — also reproduced in jsdom (vitest)

Install method

npm

Framework / app setup

Vite + React 18 component library; repro is framework-independent (fails identically in jsdom).

Affected packages or entry points

  • @platejs/corenormalizeDescendantsToDocumentFragment, deserializeHtml
  • slate-react / slate-dom chunking render path (splitDecorationsByChild) — crashes on invalid children instead of failing gracefully

Additional information

Real-world impact: any app rendering legacy/user-generated HTML fragments (plain text with <br> breaks, email-derived content with <font>/<span>/<div>) through Plate white-screens after upgrading across v49+. Related: #5043 (same crash signature via LengthPlugin truncation).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions