Skip to content

feat: make the internal Slate tree Portable Text-native#2279

Merged
christianhg merged 10 commits intomainfrom
feat/pt-native-tree
Mar 6, 2026
Merged

feat: make the internal Slate tree Portable Text-native#2279
christianhg merged 10 commits intomainfrom
feat/pt-native-tree

Conversation

@christianhg
Copy link
Member

@christianhg christianhg commented Mar 2, 2026

The editor maintained a parallel Portable Text value alongside the internal Slate tree, maintaining both on every operation and translating between the two. This translation layer was a source of bugs (divergence between the two trees, edge cases in the bridge logic) and unnecessary complexity. This PR removes it by making the internal tree structurally match Portable Text, so no translation is needed.

Node identity checks like Text.isText and Element.isElement are now schema-driven, using the type name from the editor schema instead of structural field checks. Block objects and inline objects are no longer Elements with synthetic children: [{text: ''}] — they're stored as-is in the tree as ObjectNodes, a new third node type alongside Element (text blocks) and Text (spans). ObjectNodes are rendered via a dedicated component with a zero-width spacer for DOM selection, replacing Slate's void element rendering.

With the tree now in Portable Text format, the value wrapper that translated between Slate and PT on every operation (~620 lines) is no longer needed and is deleted. Several unused Slate transforms inherited from upstream are also removed.

This is also a prerequisite for container support. The old architecture couldn't support block objects with nested Portable Text content because the value wrapper would need a complete rewrite to translate nested trees, and the synthetic children array on block objects would conflict with real children. With the PT-native tree, a container is just a block object that becomes an Element with real Portable Text children, while non-container block objects stay as ObjectNodes. The schema-driven identity checks make this distinction possible without structural heuristics.

@changeset-bot
Copy link

changeset-bot bot commented Mar 2, 2026

🦋 Changeset detected

Latest commit: 3533195

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 11 packages
Name Type
@portabletext/editor Minor
@portabletext/plugin-character-pair-decorator Major
@portabletext/plugin-emoji-picker Patch
@portabletext/plugin-input-rule Patch
@portabletext/plugin-markdown-shortcuts Major
@portabletext/plugin-one-line Major
@portabletext/plugin-paste-link Major
@portabletext/plugin-sdk-value Major
@portabletext/plugin-typeahead-picker Patch
@portabletext/plugin-typography Patch
@portabletext/toolbar Patch

Not sure what this means? Click here to learn what changesets are.

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

@vercel
Copy link

vercel bot commented Mar 2, 2026

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

Project Deployment Actions Updated (UTC)
portable-text-editor-documentation Ready Ready Preview, Comment Mar 6, 2026 8:04am
portable-text-example-basic Ready Ready Preview, Comment Mar 6, 2026 8:04am
portable-text-playground Ready Ready Preview, Comment Mar 6, 2026 8:04am

Request Review

@christianhg christianhg force-pushed the feat/pt-native-tree branch from 76adaf6 to 5266f4e Compare March 2, 2026 12:21
@christianhg christianhg force-pushed the feat/pt-native-tree branch from 5266f4e to 60d3a83 Compare March 2, 2026 12:43
@christianhg christianhg changed the title feat(editor): remove value wrapper and __inline flag from Slate tree feat: remove value wrapper and __inline flag from Slate tree Mar 2, 2026
@christianhg christianhg force-pushed the feat/pt-native-tree branch from 60d3a83 to e7a1b66 Compare March 2, 2026 12:53
@christianhg christianhg force-pushed the feat/pt-native-tree branch from c6adffb to bb4092c Compare March 2, 2026 14:12
@christianhg christianhg force-pushed the feat/pt-native-tree branch from b295444 to a345988 Compare March 3, 2026 11:36
@christianhg christianhg force-pushed the feat/pt-native-tree branch from a345988 to 256be5b Compare March 3, 2026 11:48
@github-actions
Copy link
Contributor

github-actions bot commented Mar 5, 2026

📦 Bundle Stats — @portabletext/editor

Compared against main (a657a780)

@portabletext/editor

Metric Value vs main (a657a78)
Internal (raw) 816.3 KB -16.2 KB, -1.9%
Internal (gzip) 152.5 KB -3.0 KB, -2.0%
Bundled (raw) 1.42 MB -16.4 KB, -1.1%
Bundled (gzip) 315.4 KB -3.2 KB, -1.0%
Import time 107ms -1ms, -1.3%

@portabletext/editor/behaviors

Metric Value vs main (a657a78)
Internal (raw) 467 B -
Internal (gzip) 207 B -
Bundled (raw) 424 B -
Bundled (gzip) 171 B -
Import time 7ms -0ms, -0.0%

@portabletext/editor/plugins

Metric Value vs main (a657a78)
Internal (raw) 2.5 KB -
Internal (gzip) 910 B -
Bundled (raw) 2.3 KB -
Bundled (gzip) 839 B -
Import time 13ms +0ms, +3.2%

@portabletext/editor/selectors

Metric Value vs main (a657a78)
Internal (raw) 60.2 KB -17 B, -0.0%
Internal (gzip) 9.4 KB -4 B, -0.0%
Bundled (raw) 56.7 KB -
Bundled (gzip) 8.6 KB -
Import time 11ms +0ms, +4.1%

@portabletext/editor/utils

Metric Value vs main (a657a78)
Internal (raw) 24.2 KB -17 B, -0.1%
Internal (gzip) 4.7 KB -4 B, -0.1%
Bundled (raw) 22.2 KB -
Bundled (gzip) 4.4 KB -
Import time 10ms +0ms, +3.1%
Details
  • Import time regressions over 10% are flagged with ⚠️
  • Treemap artifacts are attached to the CI run for detailed size analysis
  • Sizes shown as raw / gzip 🗜️. Internal bytes = own code only. Total bytes = with all dependencies. Import time = Node.js cold-start median.

The internal Slate tree now stores Portable Text nodes directly instead
of wrapping them in Slate-specific structures. Block objects and inline
objects are stored as-is (no synthetic children array, no `__inline` flag),
and the value wrapper that translated between Slate and Portable Text on
every operation is removed entirely.

This introduces `ObjectNode` as a third node type alongside Element (text
blocks) and Text (spans). Node identity checks (Text.isText,
Element.isElement, Node.isObjectNode) are now schema-driven, using the
type name from the editor schema instead of structural field checks.
`insertFragment` is dead code in the PTE architecture - paste goes
through the behavior system (clipboard.paste → insert.blocks), never
through `insertFragment`.
@ecoscript ecoscript bot mentioned this pull request Mar 6, 2026
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.

2 participants