@@ -2,11 +2,123 @@ import { configureYProsemirror } from "@y/prosemirror";
22import * as Y from "@y/y" ;
33
44import type { BlockNoteEditor } from "../../editor/BlockNoteEditor.js" ;
5+ import { findTypeInOtherYdoc } from "../utils.js" ;
6+ import { blockMatchNodes } from "./blockMatchNodes.js" ;
57import {
68 decodeFragmentUpdate ,
79 destroyDecodedFragment ,
810} from "./snapshotCodec.js" ;
911
12+ /** Whether the item with this id was already in `baseline`. */
13+ function inBaseline ( baseline : Y . Doc , id : Y . ID ) : boolean {
14+ const last = baseline . store . clients . get ( id . client ) ?. at ( - 1 ) ;
15+ return last !== undefined && id . clock < last . id . clock + last . length ;
16+ }
17+
18+ /** The distinct attributions recorded for `node` and its descendants. */
19+ function subtreeAttributions (
20+ node : Y . Node ,
21+ map : Y . IdMap < any > ,
22+ ) : Y . ContentAttribute < any > [ ] {
23+ const found = new Map < string , Y . ContentAttribute < any > > ( ) ;
24+ for ( const structs of node . doc ! . store . clients . values ( ) ) {
25+ for ( const item of structs ) {
26+ if (
27+ item instanceof Y . Item &&
28+ ( item === node . _item || Y . isParentOf ( node , item ) )
29+ ) {
30+ for ( const range of map . slice (
31+ item . id . client ,
32+ item . id . clock ,
33+ item . length ,
34+ ) ) {
35+ for ( const attr of range . attrs ?? [ ] ) {
36+ found . set ( `${ attr . name } :${ String ( attr . val ) } ` , attr ) ;
37+ }
38+ }
39+ }
40+ }
41+ }
42+ return [ ...found . values ( ) ] ;
43+ }
44+
45+ /**
46+ * Replace each block that changed structurally since `baseline` with a fresh
47+ * copy, where "structurally" is what `blockMatchNodes` treats as a different
48+ * block (e.g. a type change). The old collaboration binding stored such changes
49+ * inside the same container, which a diff renders as schema-invalid content
50+ * that is then dropped. A fresh container diffs as a deleted block next to an
51+ * inserted one, as the current binding stores it. The copy and the deletion
52+ * take over the original's attributions, so the change keeps its author.
53+ */
54+ function splitChangedBlocks (
55+ node : Y . Node ,
56+ baseline : Y . Doc ,
57+ attributions : Y . ContentMap | undefined ,
58+ added : Y . ContentMap ,
59+ ) : void {
60+ for ( let index = 0 ; index < node . length ; index ++ ) {
61+ const child = node . get ( index ) ;
62+ if ( ! ( child instanceof Y . Node ) ) {
63+ continue ;
64+ }
65+ if ( child . name === "blockContainer" && child . _item ) {
66+ // A block created after the baseline has no previous version.
67+ const before = inBaseline ( baseline , child . _item . id )
68+ ? findTypeInOtherYdoc ( child , baseline )
69+ : undefined ;
70+ if (
71+ before &&
72+ ! blockMatchNodes ( before . toDeltaDeep ( ) , child . toDeltaDeep ( ) )
73+ ) {
74+ const inserted = attributions
75+ ? subtreeAttributions ( child , attributions . inserts )
76+ : [ ] ;
77+ const deleted = attributions
78+ ? subtreeAttributions ( child , attributions . deletes )
79+ : [ ] ;
80+ const doc = child . doc ! ;
81+ // A change that only inserted (or only deleted) still has one author
82+ // for both sides of the split, under that side's attribution kind.
83+ function as (
84+ kind : "insert" | "delete" ,
85+ attrs : Y . ContentAttribute < any > [ ] ,
86+ ) {
87+ return attrs . map ( ( attr ) => Y . createContentAttribute ( kind , attr . val ) ) ;
88+ }
89+ const authors = inserted . length ? inserted : deleted ;
90+ function record ( tr : Y . Transaction ) {
91+ if ( authors . length ) {
92+ Y . insertIntoIdMap (
93+ added . inserts ,
94+ Y . createIdMapFromIdSet ( tr . insertSet , as ( "insert" , authors ) ) ,
95+ ) ;
96+ Y . insertIntoIdMap (
97+ added . deletes ,
98+ Y . createIdMapFromIdSet (
99+ tr . deleteSet ,
100+ as ( "delete" , deleted . length ? deleted : authors ) ,
101+ ) ,
102+ ) ;
103+ }
104+ }
105+ doc . on ( "beforeObserverCalls" , record ) ;
106+ try {
107+ doc . transact ( ( ) => {
108+ const copy = child . clone ( ) ;
109+ node . delete ( index ) ;
110+ node . insert ( index , [ copy ] ) ;
111+ } ) ;
112+ } finally {
113+ doc . off ( "beforeObserverCalls" , record ) ;
114+ }
115+ continue ;
116+ }
117+ }
118+ splitChangedBlocks ( child , baseline , attributions , added ) ;
119+ }
120+ }
121+
10122/**
11123 * Decode a snapshot, diff it against a baseline if given, and render it.
12124 *
@@ -33,14 +145,32 @@ export function showSnapshotPreview(
33145 try {
34146 const snapshot = decodeFragmentUpdate ( fragment , snapshotContent ) ;
35147 try {
148+ let renderAttributions = attributions ;
149+ if ( baseline ) {
150+ const added = Y . createContentMap ( ) ;
151+ splitChangedBlocks (
152+ snapshot . fragment ,
153+ baseline . doc ,
154+ attributions ,
155+ added ,
156+ ) ;
157+ if ( attributions ) {
158+ renderAttributions = Y . createContentMap (
159+ Y . mergeIdMaps ( [ attributions . inserts , added . inserts ] ) ,
160+ Y . mergeIdMaps ( [ attributions . deletes , added . deletes ] ) ,
161+ ) ;
162+ }
163+ }
36164 editor . exec (
37165 configureYProsemirror ( {
38166 ytype : snapshot . fragment ,
39167 renderer : baseline
40168 ? Y . createDiffRenderer (
41169 baseline . doc ,
42170 snapshot . doc ,
43- attributions ? { attributions } : undefined ,
171+ renderAttributions
172+ ? { attributions : renderAttributions }
173+ : undefined ,
44174 )
45175 : undefined ,
46176 } ) ,
0 commit comments