@axure/y-prosemirror 1.3.7-fork.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1350 @@
1
+ /**
2
+ * @module bindings/prosemirror
3
+ */
4
+
5
+ import { createMutex } from 'lib0/mutex'
6
+ import * as PModel from 'prosemirror-model'
7
+ import { Plugin, Selection, TextSelection } from "prosemirror-state"; // eslint-disable-line
8
+ import * as math from 'lib0/math'
9
+ import * as object from 'lib0/object'
10
+ import * as set from 'lib0/set'
11
+ import { simpleDiff } from 'lib0/diff'
12
+ import * as error from 'lib0/error'
13
+ import { ySyncPluginKey, yUndoPluginKey } from './keys.js'
14
+ import * as Y from 'yjs'
15
+ import {
16
+ absolutePositionToRelativePosition,
17
+ relativePositionToAbsolutePosition
18
+ } from '../lib.js'
19
+ import * as random from 'lib0/random'
20
+ import * as environment from 'lib0/environment'
21
+ import * as dom from 'lib0/dom'
22
+ import * as eventloop from 'lib0/eventloop'
23
+ import * as map from 'lib0/map'
24
+ import * as utils from '../utils.js'
25
+
26
+ /**
27
+ * @typedef {Object} BindingMetadata
28
+ * @property {ProsemirrorMapping} BindingMetadata.mapping
29
+ * @property {Map<import('prosemirror-model').MarkType, boolean>} BindingMetadata.isOMark - is overlapping mark
30
+ */
31
+
32
+ /**
33
+ * @return {BindingMetadata}
34
+ */
35
+ export const createEmptyMeta = () => ({
36
+ mapping: new Map(),
37
+ isOMark: new Map()
38
+ })
39
+
40
+ /**
41
+ * @param {Y.Item} item
42
+ * @param {Y.Snapshot} [snapshot]
43
+ */
44
+ export const isVisible = (item, snapshot) =>
45
+ snapshot === undefined
46
+ ? !item.deleted
47
+ : (snapshot.sv.has(item.id.client) && /** @type {number} */
48
+ (snapshot.sv.get(item.id.client)) > item.id.clock &&
49
+ !Y.isDeleted(snapshot.ds, item.id))
50
+
51
+ /**
52
+ * Either a node if type is YXmlElement or an Array of text nodes if YXmlText
53
+ * @typedef {Map<Y.AbstractType<any>, PModel.Node | Array<PModel.Node>>} ProsemirrorMapping
54
+ */
55
+
56
+ /**
57
+ * @typedef {Object} ColorDef
58
+ * @property {string} ColorDef.light
59
+ * @property {string} ColorDef.dark
60
+ */
61
+
62
+ /**
63
+ * @typedef {Object} YSyncOpts
64
+ * @property {Array<ColorDef>} [YSyncOpts.colors]
65
+ * @property {Map<string,ColorDef>} [YSyncOpts.colorMapping]
66
+ * @property {Y.PermanentUserData|null} [YSyncOpts.permanentUserData]
67
+ * @property {ProsemirrorMapping} [YSyncOpts.mapping]
68
+ * @property {function} [YSyncOpts.onFirstRender] Fired when the content from Yjs is initially rendered to ProseMirror
69
+ */
70
+
71
+ /**
72
+ * @type {Array<ColorDef>}
73
+ */
74
+ const defaultColors = [{ light: '#ecd44433', dark: '#ecd444' }]
75
+
76
+ /**
77
+ * @param {Map<string,ColorDef>} colorMapping
78
+ * @param {Array<ColorDef>} colors
79
+ * @param {string} user
80
+ * @return {ColorDef}
81
+ */
82
+ const getUserColor = (colorMapping, colors, user) => {
83
+ // @todo do not hit the same color twice if possible
84
+ if (!colorMapping.has(user)) {
85
+ if (colorMapping.size < colors.length) {
86
+ const usedColors = set.create()
87
+ colorMapping.forEach((color) => usedColors.add(color))
88
+ colors = colors.filter((color) => !usedColors.has(color))
89
+ }
90
+ colorMapping.set(user, random.oneOf(colors))
91
+ }
92
+ return /** @type {ColorDef} */ (colorMapping.get(user))
93
+ }
94
+
95
+ /**
96
+ * This plugin listens to changes in prosemirror view and keeps yXmlState and view in sync.
97
+ *
98
+ * This plugin also keeps references to the type and the shared document so other plugins can access it.
99
+ * @param {Y.XmlFragment} yXmlFragment
100
+ * @param {YSyncOpts} opts
101
+ * @return {any} Returns a prosemirror plugin that binds to this type
102
+ */
103
+ export const ySyncPlugin = (yXmlFragment, {
104
+ colors = defaultColors,
105
+ colorMapping = new Map(),
106
+ permanentUserData = null,
107
+ onFirstRender = () => {},
108
+ mapping
109
+ } = {}) => {
110
+ let initialContentChanged = false
111
+ const binding = new ProsemirrorBinding(yXmlFragment, mapping)
112
+ const plugin = new Plugin({
113
+ props: {
114
+ editable: (state) => {
115
+ const syncState = ySyncPluginKey.getState(state)
116
+ return syncState.snapshot == null && syncState.prevSnapshot == null
117
+ }
118
+ },
119
+ key: ySyncPluginKey,
120
+ state: {
121
+ /**
122
+ * @returns {any}
123
+ */
124
+ init: (_initargs, _state) => {
125
+ return {
126
+ type: yXmlFragment,
127
+ doc: yXmlFragment.doc,
128
+ binding,
129
+ snapshot: null,
130
+ prevSnapshot: null,
131
+ isChangeOrigin: false,
132
+ isUndoRedoOperation: false,
133
+ addToHistory: true,
134
+ colors,
135
+ colorMapping,
136
+ permanentUserData
137
+ }
138
+ },
139
+ apply: (tr, pluginState) => {
140
+ const change = tr.getMeta(ySyncPluginKey)
141
+ if (change !== undefined) {
142
+ pluginState = Object.assign({}, pluginState)
143
+ for (const key in change) {
144
+ pluginState[key] = change[key]
145
+ }
146
+ }
147
+ pluginState.addToHistory = tr.getMeta('addToHistory') !== false
148
+ // always set isChangeOrigin. If undefined, this is not change origin.
149
+ pluginState.isChangeOrigin = change !== undefined &&
150
+ !!change.isChangeOrigin
151
+ pluginState.isUndoRedoOperation = change !== undefined && !!change.isChangeOrigin && !!change.isUndoRedoOperation
152
+ if (binding.prosemirrorView !== null) {
153
+ if (
154
+ change !== undefined &&
155
+ (change.snapshot != null || change.prevSnapshot != null)
156
+ ) {
157
+ // snapshot changed, rerender next
158
+ eventloop.timeout(0, () => {
159
+ if (binding.prosemirrorView == null) {
160
+ return
161
+ }
162
+ if (change.restore == null) {
163
+ binding._renderSnapshot(
164
+ change.snapshot,
165
+ change.prevSnapshot,
166
+ pluginState
167
+ )
168
+ } else {
169
+ binding._renderSnapshot(
170
+ change.snapshot,
171
+ change.snapshot,
172
+ pluginState
173
+ )
174
+ // reset to current prosemirror state
175
+ delete pluginState.restore
176
+ delete pluginState.snapshot
177
+ delete pluginState.prevSnapshot
178
+ binding.mux(() => {
179
+ binding._prosemirrorChanged(
180
+ binding.prosemirrorView.state.doc
181
+ )
182
+ })
183
+ }
184
+ })
185
+ }
186
+ }
187
+ return pluginState
188
+ }
189
+ },
190
+ view: (view) => {
191
+ binding.initView(view)
192
+ if (mapping == null) {
193
+ // force rerender to update the bindings mapping
194
+ binding._forceRerender()
195
+ }
196
+ onFirstRender()
197
+ return {
198
+ update: () => {
199
+ const pluginState = plugin.getState(view.state)
200
+ if (
201
+ pluginState.snapshot == null && pluginState.prevSnapshot == null
202
+ ) {
203
+ if (
204
+ // If the content doesn't change initially, we don't render anything to Yjs
205
+ // If the content was cleared by a user action, we want to catch the change and
206
+ // represent it in Yjs
207
+ initialContentChanged ||
208
+ view.state.doc.content.findDiffStart(
209
+ view.state.doc.type.createAndFill().content
210
+ ) !== null
211
+ ) {
212
+ initialContentChanged = true
213
+ if (
214
+ pluginState.addToHistory === false &&
215
+ !pluginState.isChangeOrigin
216
+ ) {
217
+ const yUndoPluginState = yUndoPluginKey.getState(view.state)
218
+ /**
219
+ * @type {Y.UndoManager}
220
+ */
221
+ const um = yUndoPluginState && yUndoPluginState.undoManager
222
+ if (um) {
223
+ um.stopCapturing()
224
+ }
225
+ }
226
+ binding.mux(() => {
227
+ /** @type {Y.Doc} */ (pluginState.doc).transact((tr) => {
228
+ tr.meta.set('addToHistory', pluginState.addToHistory)
229
+ binding._prosemirrorChanged(view.state.doc)
230
+ }, ySyncPluginKey)
231
+ })
232
+ }
233
+ }
234
+ },
235
+ destroy: () => {
236
+ binding.destroy()
237
+ }
238
+ }
239
+ }
240
+ })
241
+ return plugin
242
+ }
243
+
244
+ /**
245
+ * @param {import('prosemirror-state').Transaction} tr
246
+ * @param {RecoverableSelection} recoverableSel
247
+ * @param {ProsemirrorBinding} binding
248
+ */
249
+ const restoreRelativeSelection = (tr, recoverableSel, binding) => {
250
+ if (recoverableSel !== null && recoverableSel.valid()) {
251
+ const selection = recoverableSel.restore(binding, tr.doc)
252
+ tr = tr.setSelection(selection)
253
+ }
254
+ }
255
+
256
+ /**
257
+ * @param {ProsemirrorBinding} pmbinding
258
+ * @param {import('prosemirror-state').EditorState} state
259
+ */
260
+ export const getRelativeSelection = (pmbinding, state) => ({
261
+ type: /** @type {any} */ (state.selection).jsonID,
262
+ anchor: absolutePositionToRelativePosition(
263
+ state.selection.anchor,
264
+ pmbinding.type,
265
+ pmbinding.mapping
266
+ ),
267
+ head: absolutePositionToRelativePosition(
268
+ state.selection.head,
269
+ pmbinding.type,
270
+ pmbinding.mapping
271
+ )
272
+ })
273
+
274
+ export const createRecoverableSelection = (pmbinding, state) => {
275
+ const sel = new RecoverableSelection(pmbinding, state.selection)
276
+ state.selection.map(state.doc, sel)
277
+ return sel
278
+ }
279
+
280
+ export class RecoverableSelection {
281
+ constructor (pmbinding, selection, recoverMode = false) {
282
+ this.records = []
283
+ this.pmbinding = pmbinding
284
+ this.selection = selection
285
+ this.recoverMode = recoverMode
286
+ }
287
+
288
+ restore (pmbinding, doc) {
289
+ try {
290
+ return this.selection.map(doc, new RecoveryMapping(pmbinding, this.records))
291
+ } catch {
292
+ const $pos = doc.resolve(this.selection.anchor)
293
+ return Selection.near($pos)
294
+ }
295
+ }
296
+
297
+ valid () {
298
+ return !!this.records.length && this.records.every(r => r.relPos)
299
+ }
300
+
301
+ map (pos) {
302
+ const relPos = absolutePositionToRelativePosition(pos, this.pmbinding.type, this.pmbinding.mapping)
303
+ this.records.push({ pos, relPos })
304
+ return pos
305
+ }
306
+
307
+ mapResult (pos) {
308
+ return { deleted: false, pos: this.map(pos) }
309
+ }
310
+ }
311
+
312
+ export class RecoveryMapping {
313
+ constructor (pmbinding, records) {
314
+ this.pmbinding = pmbinding
315
+ this.records = records
316
+ }
317
+
318
+ map (pos) {
319
+ return this.mapResult(pos).pos
320
+ }
321
+
322
+ mapResult (pos) {
323
+ for (const rec of this.records) {
324
+ if (rec.pos === pos) {
325
+ const mappedPos = relativePositionToAbsolutePosition(this.pmbinding.doc, this.pmbinding.type, rec.relPos, this.pmbinding.mapping)
326
+ if (mappedPos === null) {
327
+ return { deleted: true, pos }
328
+ }
329
+ return { deleted: false, pos: mappedPos }
330
+ }
331
+ }
332
+ throw new Error('not recorded')
333
+ }
334
+ }
335
+
336
+ /**
337
+ * Binding for prosemirror.
338
+ *
339
+ * @protected
340
+ */
341
+ export class ProsemirrorBinding {
342
+ /**
343
+ * @param {Y.XmlFragment} yXmlFragment The bind source
344
+ * @param {ProsemirrorMapping} mapping
345
+ */
346
+ constructor (yXmlFragment, mapping = new Map()) {
347
+ this.type = yXmlFragment
348
+ /**
349
+ * this will be set once the view is created
350
+ * @type {any}
351
+ */
352
+ this.prosemirrorView = null
353
+ this.mux = createMutex()
354
+ this.mapping = mapping
355
+ /**
356
+ * Is overlapping mark - i.e. mark does not exclude itself.
357
+ *
358
+ * @type {Map<import('prosemirror-model').MarkType, boolean>}
359
+ */
360
+ this.isOMark = new Map()
361
+ this._observeFunction = this._typeChanged.bind(this)
362
+ /**
363
+ * @type {Y.Doc}
364
+ */
365
+ // @ts-ignore
366
+ this.doc = yXmlFragment.doc
367
+ /**
368
+ * last selection as relative positions in the Yjs model
369
+ */
370
+ this.beforePatchSelection = null
371
+ /**
372
+ * current selection as relative positions in the Yjs model
373
+ */
374
+ this.beforeTransactionSelection = null
375
+ this.lastProsemirrorState = null
376
+ this.beforeAllTransactions = () => {
377
+ if (this.beforeTransactionSelection === null && this.prosemirrorView != null) {
378
+ this.beforeTransactionSelection = createRecoverableSelection(
379
+ this,
380
+ this.prosemirrorView.state
381
+ )
382
+ }
383
+ }
384
+ this.afterAllTransactions = () => {
385
+ this.beforeTransactionSelection = null
386
+ }
387
+ this._domSelectionInView = null
388
+ }
389
+
390
+ /**
391
+ * Create a transaction for changing the prosemirror state.
392
+ *
393
+ * @returns
394
+ */
395
+ get _tr () {
396
+ return this.prosemirrorView.state.tr.setMeta('addToHistory', false)
397
+ }
398
+
399
+ _isLocalCursorInView () {
400
+ if (!this.prosemirrorView.hasFocus()) return false
401
+ if (environment.isBrowser && this._domSelectionInView === null) {
402
+ // Calculate the domSelectionInView and clear by next tick after all events are finished
403
+ eventloop.timeout(0, () => {
404
+ this._domSelectionInView = null
405
+ })
406
+ this._domSelectionInView = this._isDomSelectionInView()
407
+ }
408
+ return this._domSelectionInView
409
+ }
410
+
411
+ _isDomSelectionInView () {
412
+ const selection = this.prosemirrorView._root.getSelection()
413
+
414
+ if (selection == null || selection.anchorNode == null) return false
415
+
416
+ const range = this.prosemirrorView._root.createRange()
417
+ range.setStart(selection.anchorNode, selection.anchorOffset)
418
+ range.setEnd(selection.focusNode, selection.focusOffset)
419
+
420
+ // This is a workaround for an edgecase where getBoundingClientRect will
421
+ // return zero values if the selection is collapsed at the start of a newline
422
+ // see reference here: https://stackoverflow.com/a/59780954
423
+ const rects = range.getClientRects()
424
+ if (rects.length === 0) {
425
+ // probably buggy newline behavior, explicitly select the node contents
426
+ if (range.startContainer && range.collapsed) {
427
+ range.selectNodeContents(range.startContainer)
428
+ }
429
+ }
430
+
431
+ const bounding = range.getBoundingClientRect()
432
+ const documentElement = dom.doc.documentElement
433
+
434
+ return bounding.bottom >= 0 && bounding.right >= 0 &&
435
+ bounding.left <=
436
+ (window.innerWidth || documentElement.clientWidth || 0) &&
437
+ bounding.top <= (window.innerHeight || documentElement.clientHeight || 0)
438
+ }
439
+
440
+ /**
441
+ * @param {Y.Snapshot} snapshot
442
+ * @param {Y.Snapshot} prevSnapshot
443
+ */
444
+ renderSnapshot (snapshot, prevSnapshot) {
445
+ if (!prevSnapshot) {
446
+ prevSnapshot = Y.createSnapshot(Y.createDeleteSet(), new Map())
447
+ }
448
+ this.prosemirrorView.dispatch(
449
+ this._tr.setMeta(ySyncPluginKey, { snapshot, prevSnapshot })
450
+ )
451
+ }
452
+
453
+ unrenderSnapshot () {
454
+ this.mapping.clear()
455
+ this.mux(() => {
456
+ const fragmentContent = this.type.toArray().map((t) =>
457
+ createNodeFromYElement(
458
+ /** @type {Y.XmlElement} */ (t),
459
+ this.prosemirrorView.state.schema,
460
+ this
461
+ )
462
+ ).filter((n) => n !== null)
463
+ // @ts-ignore
464
+ const tr = this._tr.replace(
465
+ 0,
466
+ this.prosemirrorView.state.doc.content.size,
467
+ new PModel.Slice(PModel.Fragment.from(fragmentContent), 0, 0)
468
+ )
469
+ tr.setMeta(ySyncPluginKey, { snapshot: null, prevSnapshot: null })
470
+ this.prosemirrorView.dispatch(tr)
471
+ })
472
+ }
473
+
474
+ _forceRerender () {
475
+ this.mapping.clear()
476
+ this.mux(() => {
477
+ // If this is a forced rerender, this might neither happen as a pm change nor within a Yjs
478
+ // transaction. Then the "before selection" doesn't exist. In this case, we need to create a
479
+ // relative position before replacing content. Fixes #126
480
+ const sel = this.beforeTransactionSelection !== null ? null : this.prosemirrorView.state.selection
481
+ const fragmentContent = this.type.toArray().map((t) =>
482
+ createNodeFromYElement(
483
+ /** @type {Y.XmlElement} */ (t),
484
+ this.prosemirrorView.state.schema,
485
+ this
486
+ )
487
+ ).filter((n) => n !== null)
488
+ // @ts-ignore
489
+ const tr = this._tr.replace(
490
+ 0,
491
+ this.prosemirrorView.state.doc.content.size,
492
+ new PModel.Slice(PModel.Fragment.from(fragmentContent), 0, 0)
493
+ )
494
+ if (sel) {
495
+ /**
496
+ * If the Prosemirror document we just created from this.type is
497
+ * smaller than the previous document, the selection might be
498
+ * out of bound, which would make Prosemirror throw an error.
499
+ */
500
+ const clampedAnchor = math.min(math.max(sel.anchor, 0), tr.doc.content.size)
501
+ const clampedHead = math.min(math.max(sel.head, 0), tr.doc.content.size)
502
+
503
+ tr.setSelection(TextSelection.create(tr.doc, clampedAnchor, clampedHead))
504
+ }
505
+ this.prosemirrorView.dispatch(
506
+ tr.setMeta(ySyncPluginKey, { isChangeOrigin: true, binding: this })
507
+ )
508
+ })
509
+ }
510
+
511
+ /**
512
+ * @param {Y.Snapshot|Uint8Array} snapshot
513
+ * @param {Y.Snapshot|Uint8Array} prevSnapshot
514
+ * @param {Object} pluginState
515
+ */
516
+ _renderSnapshot (snapshot, prevSnapshot, pluginState) {
517
+ /**
518
+ * The document that contains the full history of this document.
519
+ * @type {Y.Doc}
520
+ */
521
+ let historyDoc = this.doc
522
+ let historyType = this.type
523
+ if (!snapshot) {
524
+ snapshot = Y.snapshot(this.doc)
525
+ }
526
+ if (snapshot instanceof Uint8Array || prevSnapshot instanceof Uint8Array) {
527
+ if (!(snapshot instanceof Uint8Array) || !(prevSnapshot instanceof Uint8Array)) {
528
+ // expected both snapshots to be v2 updates
529
+ error.unexpectedCase()
530
+ }
531
+ historyDoc = new Y.Doc({ gc: false })
532
+ Y.applyUpdateV2(historyDoc, prevSnapshot)
533
+ prevSnapshot = Y.snapshot(historyDoc)
534
+ Y.applyUpdateV2(historyDoc, snapshot)
535
+ snapshot = Y.snapshot(historyDoc)
536
+ if (historyType._item === null) {
537
+ /**
538
+ * If is a root type, we need to find the root key in the initial document
539
+ * and use it to get the history type.
540
+ */
541
+ const rootKey = Array.from(this.doc.share.keys()).find(
542
+ (key) => this.doc.share.get(key) === this.type
543
+ )
544
+ historyType = historyDoc.getXmlFragment(rootKey)
545
+ } else {
546
+ /**
547
+ * If it is a sub type, we use the item id to find the history type.
548
+ */
549
+ const historyStructs =
550
+ historyDoc.store.clients.get(historyType._item.id.client) ?? []
551
+ const itemIndex = Y.findIndexSS(
552
+ historyStructs,
553
+ historyType._item.id.clock
554
+ )
555
+ const item = /** @type {Y.Item} */ (historyStructs[itemIndex])
556
+ const content = /** @type {Y.ContentType} */ (item.content)
557
+ historyType = /** @type {Y.XmlFragment} */ (content.type)
558
+ }
559
+ }
560
+ // clear mapping because we are going to rerender
561
+ this.mapping.clear()
562
+ this.mux(() => {
563
+ historyDoc.transact((transaction) => {
564
+ // before rendering, we are going to sanitize ops and split deleted ops
565
+ // if they were deleted by seperate users.
566
+ /**
567
+ * @type {Y.PermanentUserData}
568
+ */
569
+ const pud = pluginState.permanentUserData
570
+ if (pud) {
571
+ pud.dss.forEach((ds) => {
572
+ Y.iterateDeletedStructs(transaction, ds, (_item) => {})
573
+ })
574
+ }
575
+ /**
576
+ * @param {'removed'|'added'} type
577
+ * @param {Y.ID} id
578
+ */
579
+ const computeYChange = (type, id) => {
580
+ const user = type === 'added'
581
+ ? pud.getUserByClientId(id.client)
582
+ : pud.getUserByDeletedId(id)
583
+ return {
584
+ user,
585
+ type,
586
+ color: getUserColor(
587
+ pluginState.colorMapping,
588
+ pluginState.colors,
589
+ user
590
+ )
591
+ }
592
+ }
593
+ // Create document fragment and render
594
+ const fragmentContent = Y.typeListToArraySnapshot(
595
+ historyType,
596
+ new Y.Snapshot(prevSnapshot.ds, snapshot.sv)
597
+ ).map((t) => {
598
+ if (
599
+ !t._item.deleted || isVisible(t._item, snapshot) ||
600
+ isVisible(t._item, prevSnapshot)
601
+ ) {
602
+ return createNodeFromYElement(
603
+ t,
604
+ this.prosemirrorView.state.schema,
605
+ { mapping: new Map(), isOMark: new Map() },
606
+ snapshot,
607
+ prevSnapshot,
608
+ computeYChange
609
+ )
610
+ } else {
611
+ // No need to render elements that are not visible by either snapshot.
612
+ // If a client adds and deletes content in the same snapshot the element is not visible by either snapshot.
613
+ return null
614
+ }
615
+ }).filter((n) => n !== null)
616
+ // @ts-ignore
617
+ const tr = this._tr.replace(
618
+ 0,
619
+ this.prosemirrorView.state.doc.content.size,
620
+ new PModel.Slice(PModel.Fragment.from(fragmentContent), 0, 0)
621
+ )
622
+ this.prosemirrorView.dispatch(
623
+ tr.setMeta(ySyncPluginKey, { isChangeOrigin: true })
624
+ )
625
+ }, ySyncPluginKey)
626
+ })
627
+ }
628
+
629
+ /**
630
+ * @param {Array<Y.YEvent<any>>} events
631
+ * @param {Y.Transaction} transaction
632
+ */
633
+ _typeChanged (events, transaction) {
634
+ if (this.prosemirrorView == null) return
635
+ const syncState = ySyncPluginKey.getState(this.prosemirrorView.state)
636
+ if (
637
+ events.length === 0 || syncState.snapshot != null ||
638
+ syncState.prevSnapshot != null
639
+ ) {
640
+ // drop out if snapshot is active
641
+ this.renderSnapshot(syncState.snapshot, syncState.prevSnapshot)
642
+ return
643
+ }
644
+ this.mux(() => {
645
+ /**
646
+ * @param {any} _
647
+ * @param {Y.AbstractType<any>} type
648
+ */
649
+ const delType = (_, type) => this.mapping.delete(type)
650
+ Y.iterateDeletedStructs(
651
+ transaction,
652
+ transaction.deleteSet,
653
+ (struct) => {
654
+ if (struct.constructor === Y.Item) {
655
+ const type = /** @type {Y.ContentType} */ (/** @type {Y.Item} */ (struct).content).type
656
+ type && this.mapping.delete(type)
657
+ }
658
+ }
659
+ )
660
+ transaction.changed.forEach(delType)
661
+ transaction.changedParentTypes.forEach(delType)
662
+ const fragmentContent = this.type.toArray().map((t) =>
663
+ createNodeIfNotExists(
664
+ /** @type {Y.XmlElement | Y.XmlHook} */ (t),
665
+ this.prosemirrorView.state.schema,
666
+ this
667
+ )
668
+ ).filter((n) => n !== null)
669
+ // @ts-ignore
670
+ let tr = this._tr.replace(
671
+ 0,
672
+ this.prosemirrorView.state.doc.content.size,
673
+ new PModel.Slice(PModel.Fragment.from(fragmentContent), 0, 0)
674
+ )
675
+ restoreRelativeSelection(tr, this.beforeTransactionSelection, this)
676
+ tr = tr.setMeta(ySyncPluginKey, { isChangeOrigin: true, isUndoRedoOperation: transaction.origin instanceof Y.UndoManager })
677
+ if (
678
+ this.beforeTransactionSelection !== null && this._isLocalCursorInView()
679
+ ) {
680
+ tr.scrollIntoView()
681
+ }
682
+ this.prosemirrorView.dispatch(tr)
683
+ })
684
+ }
685
+
686
+ /**
687
+ * @param {import('prosemirror-model').Node} doc
688
+ */
689
+ _prosemirrorChanged (doc) {
690
+ this.doc.transact(() => {
691
+ this.beforePatchSelection = createRecoverableSelection(this, this.lastProsemirrorState)
692
+ this.lastProsemirrorState = this.prosemirrorView.state
693
+ updateYFragment(this.doc, this.type, doc, this)
694
+ this.beforeTransactionSelection = createRecoverableSelection(
695
+ this,
696
+ this.prosemirrorView.state
697
+ )
698
+ }, ySyncPluginKey)
699
+ }
700
+
701
+ /**
702
+ * View is ready to listen to changes. Register observers.
703
+ * @param {any} prosemirrorView
704
+ */
705
+ initView (prosemirrorView) {
706
+ if (this.prosemirrorView != null) this.destroy()
707
+ this.prosemirrorView = prosemirrorView
708
+ this.lastProsemirrorState = prosemirrorView.state
709
+ this.doc.on('beforeAllTransactions', this.beforeAllTransactions)
710
+ this.doc.on('afterAllTransactions', this.afterAllTransactions)
711
+ this.type.observeDeep(this._observeFunction)
712
+ }
713
+
714
+ destroy () {
715
+ if (this.prosemirrorView == null) return
716
+ this.prosemirrorView = null
717
+ this.type.unobserveDeep(this._observeFunction)
718
+ this.doc.off('beforeAllTransactions', this.beforeAllTransactions)
719
+ this.doc.off('afterAllTransactions', this.afterAllTransactions)
720
+ }
721
+ }
722
+
723
+ /**
724
+ * @private
725
+ * @param {Y.XmlElement | Y.XmlHook} el
726
+ * @param {PModel.Schema} schema
727
+ * @param {BindingMetadata} meta
728
+ * @param {Y.Snapshot} [snapshot]
729
+ * @param {Y.Snapshot} [prevSnapshot]
730
+ * @param {function('removed' | 'added', Y.ID):any} [computeYChange]
731
+ * @return {PModel.Node | null}
732
+ */
733
+ const createNodeIfNotExists = (
734
+ el,
735
+ schema,
736
+ meta,
737
+ snapshot,
738
+ prevSnapshot,
739
+ computeYChange
740
+ ) => {
741
+ const node = /** @type {PModel.Node} */ (meta.mapping.get(el))
742
+ if (node === undefined) {
743
+ if (el instanceof Y.XmlElement) {
744
+ return createNodeFromYElement(
745
+ el,
746
+ schema,
747
+ meta,
748
+ snapshot,
749
+ prevSnapshot,
750
+ computeYChange
751
+ )
752
+ } else {
753
+ throw error.methodUnimplemented() // we are currently not handling hooks
754
+ }
755
+ }
756
+ return node
757
+ }
758
+
759
+ /**
760
+ * @private
761
+ * @param {Y.XmlElement} el
762
+ * @param {any} schema
763
+ * @param {BindingMetadata} meta
764
+ * @param {Y.Snapshot} [snapshot]
765
+ * @param {Y.Snapshot} [prevSnapshot]
766
+ * @param {function('removed' | 'added', Y.ID):any} [computeYChange]
767
+ * @return {PModel.Node | null} Returns node if node could be created. Otherwise it deletes the yjs type and returns null
768
+ */
769
+ export const createNodeFromYElement = (
770
+ el,
771
+ schema,
772
+ meta,
773
+ snapshot,
774
+ prevSnapshot,
775
+ computeYChange
776
+ ) => {
777
+ const children = []
778
+ /**
779
+ * @param {Y.XmlElement | Y.XmlText} type
780
+ */
781
+ const createChildren = (type) => {
782
+ if (type instanceof Y.XmlElement) {
783
+ const n = createNodeIfNotExists(
784
+ type,
785
+ schema,
786
+ meta,
787
+ snapshot,
788
+ prevSnapshot,
789
+ computeYChange
790
+ )
791
+ if (n !== null) {
792
+ children.push(n)
793
+ }
794
+ } else {
795
+ // If the next ytext exists and was created by us, move the content to the current ytext.
796
+ // This is a fix for #160 -- duplication of characters when two Y.Text exist next to each
797
+ // other.
798
+ const nextytext = /** @type {Y.ContentType} */ (type._item.right?.content)?.type
799
+ if (nextytext instanceof Y.Text && !nextytext._item.deleted && nextytext._item.id.client === nextytext.doc.clientID) {
800
+ type.applyDelta([
801
+ { retain: type.length },
802
+ ...nextytext.toDelta()
803
+ ])
804
+ nextytext.doc.transact(tr => {
805
+ nextytext._item.delete(tr)
806
+ })
807
+ }
808
+ // now create the prosemirror text nodes
809
+ const ns = createTextNodesFromYText(
810
+ type,
811
+ schema,
812
+ meta,
813
+ snapshot,
814
+ prevSnapshot,
815
+ computeYChange
816
+ )
817
+ if (ns !== null) {
818
+ ns.forEach((textchild) => {
819
+ if (textchild !== null) {
820
+ children.push(textchild)
821
+ }
822
+ })
823
+ }
824
+ }
825
+ }
826
+ if (snapshot === undefined || prevSnapshot === undefined) {
827
+ el.toArray().forEach(createChildren)
828
+ } else {
829
+ Y.typeListToArraySnapshot(el, new Y.Snapshot(prevSnapshot.ds, snapshot.sv))
830
+ .forEach(createChildren)
831
+ }
832
+ try {
833
+ const attrs = el.getAttributes(snapshot)
834
+ if (snapshot !== undefined) {
835
+ if (!isVisible(/** @type {Y.Item} */ (el._item), snapshot)) {
836
+ attrs.ychange = computeYChange
837
+ ? computeYChange('removed', /** @type {Y.Item} */ (el._item).id)
838
+ : { type: 'removed' }
839
+ } else if (!isVisible(/** @type {Y.Item} */ (el._item), prevSnapshot)) {
840
+ attrs.ychange = computeYChange
841
+ ? computeYChange('added', /** @type {Y.Item} */ (el._item).id)
842
+ : { type: 'added' }
843
+ }
844
+ }
845
+ const node = schema.node(el.nodeName, attrs, children)
846
+ meta.mapping.set(el, node)
847
+ return node
848
+ } catch (e) {
849
+ // an error occured while creating the node. This is probably a result of a concurrent action.
850
+ /** @type {Y.Doc} */ (el.doc).transact((transaction) => {
851
+ /** @type {Y.Item} */ (el._item).delete(transaction)
852
+ }, ySyncPluginKey)
853
+ meta.mapping.delete(el)
854
+ return null
855
+ }
856
+ }
857
+
858
+ /**
859
+ * @private
860
+ * @param {Y.XmlText} text
861
+ * @param {import('prosemirror-model').Schema} schema
862
+ * @param {BindingMetadata} _meta
863
+ * @param {Y.Snapshot} [snapshot]
864
+ * @param {Y.Snapshot} [prevSnapshot]
865
+ * @param {function('removed' | 'added', Y.ID):any} [computeYChange]
866
+ * @return {Array<PModel.Node>|null}
867
+ */
868
+ const createTextNodesFromYText = (
869
+ text,
870
+ schema,
871
+ _meta,
872
+ snapshot,
873
+ prevSnapshot,
874
+ computeYChange
875
+ ) => {
876
+ const nodes = []
877
+ const deltas = text.toDelta(snapshot, prevSnapshot, computeYChange)
878
+ try {
879
+ for (let i = 0; i < deltas.length; i++) {
880
+ const delta = deltas[i]
881
+ nodes.push(schema.text(delta.insert, attributesToMarks(delta.attributes, schema)))
882
+ }
883
+ } catch (e) {
884
+ // an error occured while creating the node. This is probably a result of a concurrent action.
885
+ /** @type {Y.Doc} */ (text.doc).transact((transaction) => {
886
+ /** @type {Y.Item} */ (text._item).delete(transaction)
887
+ }, ySyncPluginKey)
888
+ return null
889
+ }
890
+ // @ts-ignore
891
+ return nodes
892
+ }
893
+
894
+ /**
895
+ * @private
896
+ * @param {Array<any>} nodes prosemirror node
897
+ * @param {BindingMetadata} meta
898
+ * @return {Y.XmlText}
899
+ */
900
+ const createTypeFromTextNodes = (nodes, meta) => {
901
+ const type = new Y.XmlText()
902
+ const delta = nodes.map((node) => ({
903
+ // @ts-ignore
904
+ insert: node.text,
905
+ attributes: marksToAttributes(node.marks, meta)
906
+ }))
907
+ type.applyDelta(delta)
908
+ meta.mapping.set(type, nodes)
909
+ return type
910
+ }
911
+
912
+ /**
913
+ * @private
914
+ * @param {any} node prosemirror node
915
+ * @param {BindingMetadata} meta
916
+ * @return {Y.XmlElement}
917
+ */
918
+ const createTypeFromElementNode = (node, meta) => {
919
+ const type = new Y.XmlElement(node.type.name)
920
+ for (const key in node.attrs) {
921
+ const val = node.attrs[key]
922
+ if (val !== null && key !== 'ychange') {
923
+ type.setAttribute(key, val)
924
+ }
925
+ }
926
+ type.insert(
927
+ 0,
928
+ normalizePNodeContent(node).map((n) =>
929
+ createTypeFromTextOrElementNode(n, meta)
930
+ )
931
+ )
932
+ meta.mapping.set(type, node)
933
+ return type
934
+ }
935
+
936
+ /**
937
+ * @private
938
+ * @param {PModel.Node|Array<PModel.Node>} node prosemirror text node
939
+ * @param {BindingMetadata} meta
940
+ * @return {Y.XmlElement|Y.XmlText}
941
+ */
942
+ const createTypeFromTextOrElementNode = (node, meta) =>
943
+ node instanceof Array
944
+ ? createTypeFromTextNodes(node, meta)
945
+ : createTypeFromElementNode(node, meta)
946
+
947
+ /**
948
+ * @param {any} val
949
+ */
950
+ const isObject = (val) => typeof val === 'object' && val !== null
951
+
952
+ /**
953
+ * @param {any} pattrs
954
+ * @param {any} yattrs
955
+ */
956
+ const equalAttrs = (pattrs, yattrs) => {
957
+ const keys = Object.keys(pattrs).filter((key) => pattrs[key] !== null)
958
+ let eq =
959
+ keys.length ===
960
+ (yattrs == null ? 0 : Object.keys(yattrs).filter((key) => yattrs[key] !== null).length)
961
+ for (let i = 0; i < keys.length && eq; i++) {
962
+ const key = keys[i]
963
+ const l = pattrs[key]
964
+ const r = yattrs[key]
965
+ eq = key === 'ychange' || l === r ||
966
+ (isObject(l) && isObject(r) && equalAttrs(l, r))
967
+ }
968
+ return eq
969
+ }
970
+
971
+ /**
972
+ * @typedef {Array<Array<PModel.Node>|PModel.Node>} NormalizedPNodeContent
973
+ */
974
+
975
+ /**
976
+ * @param {any} pnode
977
+ * @return {NormalizedPNodeContent}
978
+ */
979
+ const normalizePNodeContent = (pnode) => {
980
+ const c = pnode.content.content
981
+ const res = []
982
+ for (let i = 0; i < c.length; i++) {
983
+ const n = c[i]
984
+ if (n.isText) {
985
+ const textNodes = []
986
+ for (let tnode = c[i]; i < c.length && tnode.isText; tnode = c[++i]) {
987
+ textNodes.push(tnode)
988
+ }
989
+ i--
990
+ res.push(textNodes)
991
+ } else {
992
+ res.push(n)
993
+ }
994
+ }
995
+ return res
996
+ }
997
+
998
+ /**
999
+ * @param {Y.XmlText} ytext
1000
+ * @param {Array<any>} ptexts
1001
+ */
1002
+ const equalYTextPText = (ytext, ptexts) => {
1003
+ const delta = ytext.toDelta()
1004
+ return delta.length === ptexts.length &&
1005
+ delta.every(/** @type {(d:any,i:number) => boolean} */ (d, i) =>
1006
+ d.insert === /** @type {any} */ (ptexts[i]).text &&
1007
+ object.keys(d.attributes || {}).length === ptexts[i].marks.length &&
1008
+ object.every(d.attributes, (attr, yattrname) => {
1009
+ const markname = yattr2markname(yattrname)
1010
+ const pmarks = ptexts[i].marks
1011
+ return equalAttrs(attr, pmarks.find(/** @param {any} mark */ mark => mark.type.name === markname)?.attrs)
1012
+ })
1013
+ )
1014
+ }
1015
+
1016
+ /**
1017
+ * @param {Y.XmlElement|Y.XmlText|Y.XmlHook} ytype
1018
+ * @param {any|Array<any>} pnode
1019
+ */
1020
+ const equalYTypePNode = (ytype, pnode) => {
1021
+ if (
1022
+ ytype instanceof Y.XmlElement && !(pnode instanceof Array) &&
1023
+ matchNodeName(ytype, pnode)
1024
+ ) {
1025
+ const normalizedContent = normalizePNodeContent(pnode)
1026
+ return ytype._length === normalizedContent.length &&
1027
+ equalAttrs(ytype.getAttributes(), pnode.attrs) &&
1028
+ ytype.toArray().every((ychild, i) =>
1029
+ equalYTypePNode(ychild, normalizedContent[i])
1030
+ )
1031
+ }
1032
+ return ytype instanceof Y.XmlText && pnode instanceof Array &&
1033
+ equalYTextPText(ytype, pnode)
1034
+ }
1035
+
1036
+ /**
1037
+ * @param {PModel.Node | Array<PModel.Node> | undefined} mapped
1038
+ * @param {PModel.Node | Array<PModel.Node>} pcontent
1039
+ */
1040
+ const mappedIdentity = (mapped, pcontent) =>
1041
+ mapped === pcontent ||
1042
+ (mapped instanceof Array && pcontent instanceof Array &&
1043
+ mapped.length === pcontent.length && mapped.every((a, i) =>
1044
+ pcontent[i] === a
1045
+ ))
1046
+
1047
+ /**
1048
+ * @param {Y.XmlElement} ytype
1049
+ * @param {PModel.Node} pnode
1050
+ * @param {BindingMetadata} meta
1051
+ * @return {{ foundMappedChild: boolean, equalityFactor: number }}
1052
+ */
1053
+ const computeChildEqualityFactor = (ytype, pnode, meta) => {
1054
+ const yChildren = ytype.toArray()
1055
+ const pChildren = normalizePNodeContent(pnode)
1056
+ const pChildCnt = pChildren.length
1057
+ const yChildCnt = yChildren.length
1058
+ const minCnt = math.min(yChildCnt, pChildCnt)
1059
+ let left = 0
1060
+ let right = 0
1061
+ let foundMappedChild = false
1062
+ for (; left < minCnt; left++) {
1063
+ const leftY = yChildren[left]
1064
+ const leftP = pChildren[left]
1065
+ if (mappedIdentity(meta.mapping.get(leftY), leftP)) {
1066
+ foundMappedChild = true // definite (good) match!
1067
+ } else if (!equalYTypePNode(leftY, leftP)) {
1068
+ break
1069
+ }
1070
+ }
1071
+ for (; left + right < minCnt; right++) {
1072
+ const rightY = yChildren[yChildCnt - right - 1]
1073
+ const rightP = pChildren[pChildCnt - right - 1]
1074
+ if (mappedIdentity(meta.mapping.get(rightY), rightP)) {
1075
+ foundMappedChild = true
1076
+ } else if (!equalYTypePNode(rightY, rightP)) {
1077
+ break
1078
+ }
1079
+ }
1080
+ return {
1081
+ equalityFactor: left + right,
1082
+ foundMappedChild
1083
+ }
1084
+ }
1085
+
1086
+ /**
1087
+ * @param {Y.Text} ytext
1088
+ */
1089
+ const ytextTrans = (ytext) => {
1090
+ let str = ''
1091
+ /**
1092
+ * @type {Y.Item|null}
1093
+ */
1094
+ let n = ytext._start
1095
+ const nAttrs = {}
1096
+ while (n !== null) {
1097
+ if (!n.deleted) {
1098
+ if (n.countable && n.content instanceof Y.ContentString) {
1099
+ str += n.content.str
1100
+ } else if (n.content instanceof Y.ContentFormat) {
1101
+ nAttrs[n.content.key] = null
1102
+ }
1103
+ }
1104
+ n = n.right
1105
+ }
1106
+ return {
1107
+ str,
1108
+ nAttrs
1109
+ }
1110
+ }
1111
+
1112
+ /**
1113
+ * @todo test this more
1114
+ *
1115
+ * @param {Y.Text} ytext
1116
+ * @param {Array<any>} ptexts
1117
+ * @param {BindingMetadata} meta
1118
+ */
1119
+ const updateYText = (ytext, ptexts, meta) => {
1120
+ meta.mapping.set(ytext, ptexts)
1121
+ const { nAttrs, str } = ytextTrans(ytext)
1122
+ const content = ptexts.map((p) => ({
1123
+ insert: /** @type {any} */ (p).text,
1124
+ attributes: Object.assign({}, nAttrs, marksToAttributes(p.marks, meta))
1125
+ }))
1126
+ const { insert, remove, index } = simpleDiff(
1127
+ str,
1128
+ content.map((c) => c.insert).join('')
1129
+ )
1130
+ ytext.delete(index, remove)
1131
+ ytext.insert(index, insert)
1132
+ ytext.applyDelta(
1133
+ content.map((c) => ({ retain: c.insert.length, attributes: c.attributes }))
1134
+ )
1135
+ }
1136
+
1137
+ const hashedMarkNameRegex = /(.*)(--[a-zA-Z0-9+/=]{8})$/
1138
+ /**
1139
+ * @param {string} attrName
1140
+ */
1141
+ export const yattr2markname = attrName => hashedMarkNameRegex.exec(attrName)?.[1] ?? attrName
1142
+
1143
+ /**
1144
+ * @todo move this to markstoattributes
1145
+ *
1146
+ * @param {Object<string, any>} attrs
1147
+ * @param {import('prosemirror-model').Schema} schema
1148
+ */
1149
+ export const attributesToMarks = (attrs, schema) => {
1150
+ /**
1151
+ * @type {Array<import('prosemirror-model').Mark>}
1152
+ */
1153
+ const marks = []
1154
+ for (const markName in attrs) {
1155
+ // remove hashes if necessary
1156
+ marks.push(schema.mark(yattr2markname(markName), attrs[markName]))
1157
+ }
1158
+ return marks
1159
+ }
1160
+
1161
+ /**
1162
+ * @param {Array<import('prosemirror-model').Mark>} marks
1163
+ * @param {BindingMetadata} meta
1164
+ */
1165
+ const marksToAttributes = (marks, meta) => {
1166
+ const pattrs = {}
1167
+ marks.forEach((mark) => {
1168
+ if (mark.type.name !== 'ychange') {
1169
+ const isOverlapping = map.setIfUndefined(meta.isOMark, mark.type, () => !mark.type.excludes(mark.type))
1170
+ pattrs[isOverlapping ? `${mark.type.name}--${utils.hashOfJSON(mark.toJSON())}` : mark.type.name] = mark.attrs
1171
+ }
1172
+ })
1173
+ return pattrs
1174
+ }
1175
+
1176
+ /**
1177
+ * Update a yDom node by syncing the current content of the prosemirror node.
1178
+ *
1179
+ * This is a y-prosemirror internal feature that you can use at your own risk.
1180
+ *
1181
+ * @private
1182
+ * @unstable
1183
+ *
1184
+ * @param {{transact: Function}} y
1185
+ * @param {Y.XmlFragment} yDomFragment
1186
+ * @param {any} pNode
1187
+ * @param {BindingMetadata} meta
1188
+ */
1189
+ export const updateYFragment = (y, yDomFragment, pNode, meta) => {
1190
+ if (
1191
+ yDomFragment instanceof Y.XmlElement &&
1192
+ yDomFragment.nodeName !== pNode.type.name
1193
+ ) {
1194
+ throw new Error('node name mismatch!')
1195
+ }
1196
+ meta.mapping.set(yDomFragment, pNode)
1197
+ // update attributes
1198
+ if (yDomFragment instanceof Y.XmlElement) {
1199
+ const yDomAttrs = yDomFragment.getAttributes()
1200
+ const pAttrs = pNode.attrs
1201
+ for (const key in pAttrs) {
1202
+ if (pAttrs[key] !== null) {
1203
+ if (yDomAttrs[key] !== pAttrs[key] && key !== 'ychange') {
1204
+ yDomFragment.setAttribute(key, pAttrs[key])
1205
+ }
1206
+ } else {
1207
+ yDomFragment.removeAttribute(key)
1208
+ }
1209
+ }
1210
+ // remove all keys that are no longer in pAttrs
1211
+ for (const key in yDomAttrs) {
1212
+ if (pAttrs[key] === undefined) {
1213
+ yDomFragment.removeAttribute(key)
1214
+ }
1215
+ }
1216
+ }
1217
+ // update children
1218
+ const pChildren = normalizePNodeContent(pNode)
1219
+ const pChildCnt = pChildren.length
1220
+ const yChildren = yDomFragment.toArray()
1221
+ const yChildCnt = yChildren.length
1222
+ const minCnt = math.min(pChildCnt, yChildCnt)
1223
+ let left = 0
1224
+ let right = 0
1225
+ // find number of matching elements from left
1226
+ for (; left < minCnt; left++) {
1227
+ const leftY = yChildren[left]
1228
+ const leftP = pChildren[left]
1229
+ if (!mappedIdentity(meta.mapping.get(leftY), leftP)) {
1230
+ if (equalYTypePNode(leftY, leftP)) {
1231
+ // update mapping
1232
+ meta.mapping.set(leftY, leftP)
1233
+ } else {
1234
+ break
1235
+ }
1236
+ }
1237
+ }
1238
+ // find number of matching elements from right
1239
+ for (; right + left < minCnt; right++) {
1240
+ const rightY = yChildren[yChildCnt - right - 1]
1241
+ const rightP = pChildren[pChildCnt - right - 1]
1242
+ if (!mappedIdentity(meta.mapping.get(rightY), rightP)) {
1243
+ if (equalYTypePNode(rightY, rightP)) {
1244
+ // update mapping
1245
+ meta.mapping.set(rightY, rightP)
1246
+ } else {
1247
+ break
1248
+ }
1249
+ }
1250
+ }
1251
+ y.transact(() => {
1252
+ // try to compare and update
1253
+ while (yChildCnt - left - right > 0 && pChildCnt - left - right > 0) {
1254
+ const leftY = yChildren[left]
1255
+ const leftP = pChildren[left]
1256
+ const rightY = yChildren[yChildCnt - right - 1]
1257
+ const rightP = pChildren[pChildCnt - right - 1]
1258
+ if (leftY instanceof Y.XmlText && leftP instanceof Array) {
1259
+ if (!equalYTextPText(leftY, leftP)) {
1260
+ updateYText(leftY, leftP, meta)
1261
+ }
1262
+ left += 1
1263
+ } else {
1264
+ let updateLeft = leftY instanceof Y.XmlElement &&
1265
+ matchNodeName(leftY, leftP)
1266
+ let updateRight = rightY instanceof Y.XmlElement &&
1267
+ matchNodeName(rightY, rightP)
1268
+ if (updateLeft && updateRight) {
1269
+ // decide which which element to update
1270
+ const equalityLeft = computeChildEqualityFactor(
1271
+ /** @type {Y.XmlElement} */ (leftY),
1272
+ /** @type {PModel.Node} */ (leftP),
1273
+ meta
1274
+ )
1275
+ const equalityRight = computeChildEqualityFactor(
1276
+ /** @type {Y.XmlElement} */ (rightY),
1277
+ /** @type {PModel.Node} */ (rightP),
1278
+ meta
1279
+ )
1280
+ if (
1281
+ equalityLeft.foundMappedChild && !equalityRight.foundMappedChild
1282
+ ) {
1283
+ updateRight = false
1284
+ } else if (
1285
+ !equalityLeft.foundMappedChild && equalityRight.foundMappedChild
1286
+ ) {
1287
+ updateLeft = false
1288
+ } else if (
1289
+ equalityLeft.equalityFactor < equalityRight.equalityFactor
1290
+ ) {
1291
+ updateLeft = false
1292
+ } else {
1293
+ updateRight = false
1294
+ }
1295
+ }
1296
+ if (updateLeft) {
1297
+ updateYFragment(
1298
+ y,
1299
+ /** @type {Y.XmlFragment} */ (leftY),
1300
+ /** @type {PModel.Node} */ (leftP),
1301
+ meta
1302
+ )
1303
+ left += 1
1304
+ } else if (updateRight) {
1305
+ updateYFragment(
1306
+ y,
1307
+ /** @type {Y.XmlFragment} */ (rightY),
1308
+ /** @type {PModel.Node} */ (rightP),
1309
+ meta
1310
+ )
1311
+ right += 1
1312
+ } else {
1313
+ meta.mapping.delete(yDomFragment.get(left))
1314
+ yDomFragment.delete(left, 1)
1315
+ yDomFragment.insert(left, [
1316
+ createTypeFromTextOrElementNode(leftP, meta)
1317
+ ])
1318
+ left += 1
1319
+ }
1320
+ }
1321
+ }
1322
+ const yDelLen = yChildCnt - left - right
1323
+ if (
1324
+ yChildCnt === 1 && pChildCnt === 0 && yChildren[0] instanceof Y.XmlText
1325
+ ) {
1326
+ meta.mapping.delete(yChildren[0])
1327
+ // Edge case handling https://github.com/yjs/y-prosemirror/issues/108
1328
+ // Only delete the content of the Y.Text to retain remote changes on the same Y.Text object
1329
+ yChildren[0].delete(0, yChildren[0].length)
1330
+ } else if (yDelLen > 0) {
1331
+ yDomFragment.slice(left, left + yDelLen).forEach(type => meta.mapping.delete(type))
1332
+ yDomFragment.delete(left, yDelLen)
1333
+ }
1334
+ if (left + right < pChildCnt) {
1335
+ const ins = []
1336
+ for (let i = left; i < pChildCnt - right; i++) {
1337
+ ins.push(createTypeFromTextOrElementNode(pChildren[i], meta))
1338
+ }
1339
+ yDomFragment.insert(left, ins)
1340
+ }
1341
+ }, ySyncPluginKey)
1342
+ }
1343
+
1344
+ /**
1345
+ * @function
1346
+ * @param {Y.XmlElement} yElement
1347
+ * @param {any} pNode Prosemirror Node
1348
+ */
1349
+ const matchNodeName = (yElement, pNode) =>
1350
+ !(pNode instanceof Array) && yElement.nodeName === pNode.type.name