Skip to content

feat: add BlockPathMap with incremental updates#2336

Draft
christianhg wants to merge 3 commits intomainfrom
feat/block-path-map-v2
Draft

feat: add BlockPathMap with incremental updates#2336
christianhg wants to merge 3 commits intomainfrom
feat/block-path-map-v2

Conversation

@christianhg
Copy link
Member

@christianhg christianhg commented Mar 6, 2026

Adds BlockPathMap - a data structure that maps key-based paths to positional paths with O(1) lookup and incremental updates on structural operations. Text edits and selection changes are zero-cost (early return).

On top of the map, 12 traversal utility functions give behavior authors efficient navigation of nested document structures. The traversal utils use the map for O(1) position resolution, then walk the value tree by positional index - O(depth) per navigation, where depth is 2-5 for realistic documents.

Container field discovery is schema-driven: schema.containers tells us which fields hold child blocks, replacing the duck-typing approach of scanning Object.entries. This cherry-picks the container schema type from feat/containers to make it available here.

Navigation stack

Each traversal operation uses four layers:

  1. Map - O(1) position via blockPathMap.get(keyPath)
  2. Arithmetic - O(1) sibling via index increment/decrement
  3. Index walk - O(depth) tree walk using field names from the key-path + indices from the map
  4. Schema - O(1) field names via schema.containers.find()

BlockPathMap

  • get(keyPath) / has(keyPath) / getIndex(keyPath) - O(1) lookups
  • rebuild(value, schema?) - full rebuild, recurses into containers when schema provided
  • applyOperation(op) - incremental updates for insert, remove, split, merge, move, set_node
  • toBlockIndexMap() - backward-compatible Map<string, number> for existing consumers

Traversal utils

Structural navigation: getNode, getParent, getChildren, getNextSibling, getPrevSibling, getAncestors

Cursor-order navigation: getNextBlock, getPrevBlock (depth-first pre-order, skips containers, returns only leaf blocks)

Convenience: isNested, getDepth, isDescendantOf, getContainingContainer

Tests

129 tests total: 46 for BlockPathMap (including deep structure performance tests on 5,100 entries) and 83 for traversal utils on a 4-level deep document (table, row, cell, text/image). All 8 cross-boundary scenarios tested in both directions.

Performance verified: build 5,100 entries in 42ms, lookup 0.003ms, insert 0.008ms, getNextSibling 0.004ms.

@changeset-bot
Copy link

changeset-bot bot commented Mar 6, 2026

🦋 Changeset detected

Latest commit: b83aa6f

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

This PR includes changesets to release 16 packages
Name Type
@portabletext/editor Minor
@portabletext/schema Minor
@portabletext/sanity-bridge 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
@portabletext/block-tools Patch
@portabletext/html Patch
@portabletext/markdown 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 6, 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 9:49pm
portable-text-example-basic Ready Ready Preview, Comment Mar 6, 2026 9:49pm
portable-text-playground Ready Ready Preview, Comment Mar 6, 2026 9:49pm

Request Review

@github-actions
Copy link
Contributor

github-actions bot commented Mar 6, 2026

📦 Bundle Stats — @portabletext/editor

Compared against main (be4d3b81)

@portabletext/editor

Metric Value vs main (be4d3b8)
Internal (raw) 820.5 KB +9.9 KB, +1.2%
Internal (gzip) 154.1 KB +2.6 KB, +1.7%
Bundled (raw) 1.43 MB +10.1 KB, +0.7%
Bundled (gzip) 317.0 KB +2.7 KB, +0.8%
Import time 104ms -0ms, -0.5%

@portabletext/editor/behaviors

Metric Value vs main (be4d3b8)
Internal (raw) 467 B -
Internal (gzip) 207 B -
Bundled (raw) 424 B -
Bundled (gzip) 171 B -
Import time 6ms -0ms, -1.4%

@portabletext/editor/plugins

Metric Value vs main (be4d3b8)
Internal (raw) 2.5 KB -
Internal (gzip) 910 B -
Bundled (raw) 2.3 KB -
Bundled (gzip) 839 B -
Import time 12ms -0ms, -2.1%

@portabletext/editor/selectors

Metric Value vs main (be4d3b8)
Internal (raw) 60.2 KB -
Internal (gzip) 9.4 KB -
Bundled (raw) 56.7 KB -
Bundled (gzip) 8.6 KB -
Import time 10ms -0ms, -4.5%

@portabletext/editor/utils

Metric Value vs main (be4d3b8)
Internal (raw) 24.2 KB -
Internal (gzip) 4.7 KB -
Bundled (raw) 22.2 KB -
Bundled (gzip) 4.4 KB -
Import time 10ms +0ms, +1.6%
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.

christianhg and others added 2 commits March 6, 2026 21:43
…d discovery

Rewrite traversal internals to use BlockPathMap for O(1) position
resolution instead of findIndex scans. Replace duck-typing container
field discovery with schema-driven lookup via schema.containers.

Four-layer navigation stack:
- Map: O(1) position via blockPathMap.get(keyPath)
- Arithmetic: O(1) sibling access via index increment/decrement
- Index walk: O(depth) tree walk using field names + positional indices
- Schema: O(1) field names via schema.containers.find()

Extend BlockPathMap.rebuild() to accept an optional schema parameter
for recursive container indexing. Pass schema through createTestSnapshot.

Add containers to test schema definitions (table, row, cell).
All 83 traversal tests pass unchanged - same inputs, same outputs,
map-connected internals.
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