Skip to content

Comments

research: PT tree as single source of truth#2221

Draft
christianhg wants to merge 6 commits intomainfrom
research/pt-source-of-truth
Draft

research: PT tree as single source of truth#2221
christianhg wants to merge 6 commits intomainfrom
research/pt-source-of-truth

Conversation

@christianhg
Copy link
Member

Research: Making the PT tree the single source of truth

Exploration branch for understanding what it takes to make editor.children hold PT-shaped data directly, eliminating the dual-tree architecture.

Findings so far

See research/00-structural-gaps.md for the full analysis.

10 structural gaps identified. Two genuinely hard problems:

  1. Node classification: Slate binary Element/Text classification vs PT ternary (text blocks, block objects, inline objects)
  2. Selection in voids: Slate selection model assumes Text nodes everywhere. Solution: render-time synthesis.

Status

Active research - commits represent exploration, not production code.

Vendor `slate@0.120.0`, `slate-dom@0.119.0`, and `slate-react@0.120.0` source
from github.com/ianstormtaylor/slate (tag: slate@0.120.0).

Pure copy of src/ directories — no modifications, no git history.

  packages/editor/src/slate/       — 118 files (slate core)
  packages/editor/src/slate-dom/   — 14 files (DOM integration)
  packages/editor/src/slate-react/ — 40 files (React bindings)
Rewire all bare 'slate', 'slate-dom', 'slate-react' imports to relative
paths. Fix module augmentation (declare module '../slate/index' instead
of 'slate'). Remove slate/slate-dom/slate-react npm dependencies, add
their transitive deps (direction, is-hotkey, lodash, etc.).

Fix 583 TypeScript errors from vendored source hitting PTE's strict
tsconfig (@sanity/tsconfig strictest):

Structural fixes:
- Widen DOMEditor/ReactEditor params to Editor (resolves CustomTypes)
- withDOM/withReact generic: <T extends BaseEditor> → <T extends Editor>
- Remove vendored CustomTypes augmentation (PTE's types/slate.ts is source)
- Convert enum to const object (erasableSyntaxOnly)
- Cast create-editor.ts delegates through any (incremental object build)

Mechanical fixes:
- verbatimModuleSyntax: type-only re-exports (biome --fix)
- erasableSyntaxOnly: angle bracket → as assertions, import aliases
- noUncheckedIndexedAccess: non-null assertions on bounded array access
- Unused variables prefixed with _
- override keyword on React lifecycle methods
- JSX namespace imports for React 19
- React 19 ref types (RefObject<T | null>, useRef(undefined))

Lint fixes:
- Rename String component to SlateString (no shadow global)
- Block statements for single-line ifs
- Explicit types for uninitialized let declarations
- biome-ignore for Slate upstream patterns (hooks in conditionals,
  assignment in expressions, non-null after optional chain)

All 317 unit tests pass. Zero type errors. Zero lint errors.
The vendored Slate source files are now raw TypeScript compiled alongside
PTE, which means the React Compiler babel plugin transforms them. Slate's
components use manual React.memo with custom equality functions that the
React Compiler interferes with, causing incorrect re-render optimization
(skipping renders that should happen).

Exclude src/slate/, src/slate-dom/, and src/slate-react/ from
@vitejs/plugin-react so the React Compiler doesn't process them.
esbuild still handles JSX transformation for these files.
lodash is CJS-only and doesn't resolve under Node's ESM module
resolution. Since the editor package is "type": "module", downstream
consumers running tests in Node (e.g. Vitest) fail with
"Cannot find module 'lodash/debounce'".

Replaces the two lodash imports from vendored Slate with minimal
inline implementations and removes lodash from dependencies.
@vercel
Copy link

vercel bot commented Feb 19, 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 Feb 20, 2026 7:47am
portable-text-example-basic Ready Ready Preview, Comment Feb 20, 2026 7:47am
portable-text-playground Ready Ready Preview, Comment Feb 20, 2026 7:47am

Request Review

@changeset-bot
Copy link

changeset-bot bot commented Feb 19, 2026

⚠️ No Changeset found

Latest commit: a6b5e93

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

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

@christianhg christianhg force-pushed the research/pt-source-of-truth branch from ef2f7b9 to 5b7ee00 Compare February 19, 2026 22:02
@christianhg christianhg force-pushed the research/pt-source-of-truth branch from 5651173 to 5b8d72a Compare February 19, 2026 22:23
@christianhg christianhg force-pushed the research/pt-source-of-truth branch from 5b8d72a to 55b7c12 Compare February 19, 2026 22:25
@christianhg christianhg force-pushed the research/pt-source-of-truth branch from 55b7c12 to c863b05 Compare February 19, 2026 22:38
@christianhg christianhg force-pushed the research/pt-source-of-truth branch from 74b662a to ebdccf1 Compare February 20, 2026 04:52
@christianhg christianhg force-pushed the research/pt-source-of-truth branch from ebdccf1 to 4dd271a Compare February 20, 2026 04:53
@christianhg christianhg force-pushed the research/pt-source-of-truth branch from 4dd271a to 126a26e Compare February 20, 2026 05:07
@christianhg christianhg force-pushed the research/pt-source-of-truth branch from 126a26e to 704965b Compare February 20, 2026 05:20
@christianhg christianhg force-pushed the research/pt-source-of-truth branch from 704965b to a133c68 Compare February 20, 2026 05:23
@christianhg christianhg force-pushed the research/pt-source-of-truth branch from a133c68 to 05cee0b Compare February 20, 2026 06:49
Remove the Slate translation layer for void elements (block objects and
inline objects). Previously, toSlateBlock wrapped void element properties
in a 'value' object, added '__inline' markers, and inserted synthetic
'children' arrays with void-child text nodes. fromSlateBlock reversed
this on the way out.

Now void elements are stored directly as PT-shaped data in the Slate
tree. A block object like {_type: 'image', _key: 'abc', src: '...'}
exists in editor.children exactly as-is, with no children property.

Changes across three layers:

Vendored Slate (20 files):
- Element.isElement() uses _type string check instead of children array
- BaseElement.children is optional
- Node.first/last/nodes/string/child/children guard against missing children
- Editor.point() synthesizes {path, offset: 0} for childless voids
- normalizeNode skips void elements entirely
- Transforms handle optional children defensively
- Void rendering synthesizes a virtual text node at render time only

PTE translation layer (12 files):
- toSlateBlock no longer adds children/value/__inline to voids
- fromSlateBlock no longer strips children/value from voids
- apply-operation-to-portable-text strips children from Elements directly
- operation-to-patches generates patches from node properties directly
- applyPatch applies patches to nodes directly (no value wrapper)
- operation.insert.child creates inline objects without children
- Removed VOID_CHILD_KEY usage from sync-machine
- Fixed set_node handler to guard children in text block property deletion

Rendering (3 files):
- element.tsx synthesizes virtual {text: ''} for void spacer rendering
- use-children.tsx uses editor.isTextSpan() for render branching
- schema plugin uses schema-only isInline check (no __inline marker)
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.

1 participant