@y/y 14.0.0-rc.2 → 14.0.0-rc.21

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.
Files changed (166) hide show
  1. package/README.md +36 -12
  2. package/dist/src/index.d.ts +24 -1
  3. package/dist/src/index.d.ts.map +1 -1
  4. package/dist/src/structs/AbstractStruct.d.ts +14 -17
  5. package/dist/src/structs/AbstractStruct.d.ts.map +1 -1
  6. package/dist/src/structs/GC.d.ts +9 -14
  7. package/dist/src/structs/GC.d.ts.map +1 -1
  8. package/dist/src/structs/Item.d.ts +569 -31
  9. package/dist/src/structs/Item.d.ts.map +1 -1
  10. package/dist/src/structs/Skip.d.ts +9 -11
  11. package/dist/src/structs/Skip.d.ts.map +1 -1
  12. package/dist/src/utils/BlockSet.d.ts +8 -7
  13. package/dist/src/utils/BlockSet.d.ts.map +1 -1
  14. package/dist/src/utils/Doc.d.ts +4 -9
  15. package/dist/src/utils/Doc.d.ts.map +1 -1
  16. package/dist/src/utils/ID.d.ts +0 -1
  17. package/dist/src/utils/ID.d.ts.map +1 -1
  18. package/dist/src/utils/RelativePosition.d.ts +2 -5
  19. package/dist/src/utils/RelativePosition.d.ts.map +1 -1
  20. package/dist/src/utils/Renderer.d.ts +144 -0
  21. package/dist/src/utils/Renderer.d.ts.map +1 -0
  22. package/dist/src/utils/Snapshot.d.ts +7 -11
  23. package/dist/src/utils/Snapshot.d.ts.map +1 -1
  24. package/dist/src/utils/StructStore.d.ts +45 -23
  25. package/dist/src/utils/StructStore.d.ts.map +1 -1
  26. package/dist/src/utils/Transaction.d.ts +11 -21
  27. package/dist/src/utils/Transaction.d.ts.map +1 -1
  28. package/dist/src/utils/UndoManager.d.ts +13 -14
  29. package/dist/src/utils/UndoManager.d.ts.map +1 -1
  30. package/dist/src/utils/UpdateDecoder.d.ts +1 -1
  31. package/dist/src/utils/UpdateDecoder.d.ts.map +1 -1
  32. package/dist/src/utils/UpdateEncoder.d.ts +0 -1
  33. package/dist/src/utils/UpdateEncoder.d.ts.map +1 -1
  34. package/dist/src/utils/YEvent.d.ts +22 -26
  35. package/dist/src/utils/YEvent.d.ts.map +1 -1
  36. package/dist/src/utils/content-helper.d.ts +2 -0
  37. package/dist/src/utils/content-helper.d.ts.map +1 -0
  38. package/dist/src/utils/delta-helpers.d.ts +2 -3
  39. package/dist/src/utils/delta-helpers.d.ts.map +1 -1
  40. package/dist/src/utils/encoding-helpers.d.ts +6 -0
  41. package/dist/src/utils/encoding-helpers.d.ts.map +1 -0
  42. package/dist/src/utils/encoding.d.ts +16 -18
  43. package/dist/src/utils/encoding.d.ts.map +1 -1
  44. package/dist/src/utils/ids.d.ts +331 -0
  45. package/dist/src/utils/ids.d.ts.map +1 -0
  46. package/dist/src/utils/isParentOf.d.ts +1 -2
  47. package/dist/src/utils/isParentOf.d.ts.map +1 -1
  48. package/dist/src/utils/logging.d.ts +0 -1
  49. package/dist/src/utils/logging.d.ts.map +1 -1
  50. package/dist/src/utils/meta.d.ts +15 -64
  51. package/dist/src/utils/meta.d.ts.map +1 -1
  52. package/dist/src/utils/renderer-helpers.d.ts +99 -0
  53. package/dist/src/utils/renderer-helpers.d.ts.map +1 -0
  54. package/dist/src/utils/schemas.d.ts +3 -0
  55. package/dist/src/utils/schemas.d.ts.map +1 -0
  56. package/dist/src/utils/transaction-helpers.d.ts +18 -0
  57. package/dist/src/utils/transaction-helpers.d.ts.map +1 -0
  58. package/dist/src/utils/updates.d.ts +19 -25
  59. package/dist/src/utils/updates.d.ts.map +1 -1
  60. package/dist/src/ytype.d.ts +144 -41
  61. package/dist/src/ytype.d.ts.map +1 -1
  62. package/global.d.ts +53 -0
  63. package/package.json +12 -16
  64. package/src/index.js +32 -124
  65. package/src/structs/AbstractStruct.js +21 -16
  66. package/src/structs/GC.js +15 -20
  67. package/src/structs/Item.js +992 -318
  68. package/src/structs/Skip.js +16 -19
  69. package/src/utils/BlockSet.js +20 -20
  70. package/src/utils/Doc.js +18 -29
  71. package/src/utils/ID.js +0 -2
  72. package/src/utils/RelativePosition.js +15 -25
  73. package/src/utils/{AttributionManager.js → Renderer.js} +42 -197
  74. package/src/utils/Snapshot.js +15 -37
  75. package/src/utils/StructStore.js +89 -227
  76. package/src/utils/Transaction.js +89 -321
  77. package/src/utils/UndoManager.js +157 -19
  78. package/src/utils/UpdateDecoder.js +2 -3
  79. package/src/utils/UpdateEncoder.js +0 -4
  80. package/src/utils/YEvent.js +20 -26
  81. package/src/utils/content-helper.js +0 -0
  82. package/src/utils/delta-helpers.js +21 -42
  83. package/src/utils/encoding-helpers.js +110 -0
  84. package/src/utils/encoding.js +197 -122
  85. package/src/utils/ids.js +1527 -0
  86. package/src/utils/isParentOf.js +2 -4
  87. package/src/utils/logging.js +0 -4
  88. package/src/utils/meta.js +57 -46
  89. package/src/utils/renderer-helpers.js +110 -0
  90. package/src/utils/schemas.js +3 -0
  91. package/src/utils/transaction-helpers.js +413 -0
  92. package/src/utils/updates.js +24 -146
  93. package/src/ytype.js +626 -255
  94. package/tests/testHelper.js +10 -12
  95. package/dist/src/internals.d.ts +0 -36
  96. package/dist/src/internals.d.ts.map +0 -1
  97. package/dist/src/structs/ContentAny.d.ts +0 -67
  98. package/dist/src/structs/ContentAny.d.ts.map +0 -1
  99. package/dist/src/structs/ContentBinary.d.ts +0 -64
  100. package/dist/src/structs/ContentBinary.d.ts.map +0 -1
  101. package/dist/src/structs/ContentDeleted.d.ts +0 -64
  102. package/dist/src/structs/ContentDeleted.d.ts.map +0 -1
  103. package/dist/src/structs/ContentDoc.d.ts +0 -72
  104. package/dist/src/structs/ContentDoc.d.ts.map +0 -1
  105. package/dist/src/structs/ContentEmbed.d.ts +0 -67
  106. package/dist/src/structs/ContentEmbed.d.ts.map +0 -1
  107. package/dist/src/structs/ContentFormat.d.ts +0 -69
  108. package/dist/src/structs/ContentFormat.d.ts.map +0 -1
  109. package/dist/src/structs/ContentJSON.d.ts +0 -70
  110. package/dist/src/structs/ContentJSON.d.ts.map +0 -1
  111. package/dist/src/structs/ContentString.d.ts +0 -70
  112. package/dist/src/structs/ContentString.d.ts.map +0 -1
  113. package/dist/src/structs/ContentType.d.ts +0 -77
  114. package/dist/src/structs/ContentType.d.ts.map +0 -1
  115. package/dist/src/utils/AttributionManager.d.ts +0 -238
  116. package/dist/src/utils/AttributionManager.d.ts.map +0 -1
  117. package/dist/src/utils/IdMap.d.ts +0 -164
  118. package/dist/src/utils/IdMap.d.ts.map +0 -1
  119. package/dist/src/utils/IdSet.d.ts +0 -163
  120. package/dist/src/utils/IdSet.d.ts.map +0 -1
  121. package/dist/tests/IdMap.tests.d.ts +0 -9
  122. package/dist/tests/IdMap.tests.d.ts.map +0 -1
  123. package/dist/tests/IdSet.tests.d.ts +0 -9
  124. package/dist/tests/IdSet.tests.d.ts.map +0 -1
  125. package/dist/tests/attribution.tests.d.ts +0 -9
  126. package/dist/tests/attribution.tests.d.ts.map +0 -1
  127. package/dist/tests/compatibility.tests.d.ts +0 -5
  128. package/dist/tests/compatibility.tests.d.ts.map +0 -1
  129. package/dist/tests/delta.tests.d.ts +0 -7
  130. package/dist/tests/delta.tests.d.ts.map +0 -1
  131. package/dist/tests/doc.tests.d.ts +0 -13
  132. package/dist/tests/doc.tests.d.ts.map +0 -1
  133. package/dist/tests/encoding.tests.d.ts +0 -5
  134. package/dist/tests/encoding.tests.d.ts.map +0 -1
  135. package/dist/tests/index.d.ts +0 -2
  136. package/dist/tests/index.d.ts.map +0 -1
  137. package/dist/tests/relativePositions.tests.d.ts +0 -11
  138. package/dist/tests/relativePositions.tests.d.ts.map +0 -1
  139. package/dist/tests/snapshot.tests.d.ts +0 -14
  140. package/dist/tests/snapshot.tests.d.ts.map +0 -1
  141. package/dist/tests/testHelper.d.ts +0 -171
  142. package/dist/tests/testHelper.d.ts.map +0 -1
  143. package/dist/tests/undo-redo.tests.d.ts +0 -27
  144. package/dist/tests/undo-redo.tests.d.ts.map +0 -1
  145. package/dist/tests/updates.tests.d.ts +0 -26
  146. package/dist/tests/updates.tests.d.ts.map +0 -1
  147. package/dist/tests/y-array.tests.d.ts +0 -43
  148. package/dist/tests/y-array.tests.d.ts.map +0 -1
  149. package/dist/tests/y-map.tests.d.ts +0 -42
  150. package/dist/tests/y-map.tests.d.ts.map +0 -1
  151. package/dist/tests/y-text.tests.d.ts +0 -49
  152. package/dist/tests/y-text.tests.d.ts.map +0 -1
  153. package/dist/tests/y-xml.tests.d.ts +0 -14
  154. package/dist/tests/y-xml.tests.d.ts.map +0 -1
  155. package/src/internals.js +0 -35
  156. package/src/structs/ContentAny.js +0 -115
  157. package/src/structs/ContentBinary.js +0 -93
  158. package/src/structs/ContentDeleted.js +0 -101
  159. package/src/structs/ContentDoc.js +0 -141
  160. package/src/structs/ContentEmbed.js +0 -98
  161. package/src/structs/ContentFormat.js +0 -105
  162. package/src/structs/ContentJSON.js +0 -119
  163. package/src/structs/ContentString.js +0 -113
  164. package/src/structs/ContentType.js +0 -152
  165. package/src/utils/IdMap.js +0 -673
  166. package/src/utils/IdSet.js +0 -825
package/src/ytype.js CHANGED
@@ -1,39 +1,40 @@
1
- import {
2
- cleanupFormattingGap,
3
- createIdSet,
4
- removeEventHandlerListener,
5
- callEventHandlerListeners,
6
- addEventHandlerListener,
7
- createEventHandler,
8
- getState,
9
- isVisible,
10
- ContentType,
11
- createID,
12
- ContentAny,
13
- ContentFormat,
14
- ContentBinary,
15
- ContentJSON,
16
- ContentDeleted,
17
- ContentString,
18
- ContentEmbed,
19
- getItemCleanStart,
20
- noAttributionsManager,
21
- transact,
22
- ContentDoc, UpdateEncoderV1, UpdateEncoderV2, Doc, Snapshot, Transaction, EventHandler, YEvent, Item, createAttributionFromAttributionItems, AbstractAttributionManager // eslint-disable-line
23
- } from './internals.js'
24
-
25
- import * as contentType from './structs/ContentType.js'
26
-
27
- import * as traits from 'lib0/traits'
28
- import * as delta from 'lib0/delta'
1
+ import * as binary from 'lib0/binary'
29
2
  import * as array from 'lib0/array'
30
- import * as map from 'lib0/map'
31
- import * as iterator from 'lib0/iterator'
3
+ import * as delta from 'lib0/delta'
32
4
  import * as error from 'lib0/error'
33
- import * as math from 'lib0/math'
5
+ import * as iterator from 'lib0/iterator'
34
6
  import * as log from 'lib0/logging'
7
+ import * as map from 'lib0/map'
8
+ import * as set from 'lib0/set'
9
+ import * as math from 'lib0/math'
35
10
  import * as object from 'lib0/object'
36
11
  import * as s from 'lib0/schema'
12
+ import * as traits from 'lib0/traits'
13
+ import { ObservableV2 } from 'lib0/observable'
14
+ import {
15
+ Item,
16
+ ContentAny,
17
+ ContentBinary,
18
+ ContentDeleted,
19
+ ContentEmbed,
20
+ ContentFormat,
21
+ ContentJSON,
22
+ ContentString,
23
+ ContentType,
24
+ YXmlFragmentRefID,
25
+ YXmlElementRefID,
26
+ YXmlHookRefID,
27
+ ContentDoc,
28
+ createContentDocFromDoc
29
+ } from './structs/Item.js'
30
+ import { baseRenderer } from './utils/renderer-helpers.js'
31
+ import { removeEventHandlerListener, callEventHandlerListeners, addEventHandlerListener, createEventHandler } from './utils/EventHandler.js'
32
+ import { createID } from './utils/ID.js'
33
+ import { createIdSet, iterateStructsByIdSetWithoutSplits } from './utils/ids.js'
34
+ import { getItemCleanStart, cleanupFormattingGap } from './utils/transaction-helpers.js'
35
+ import { transact } from './utils/Transaction.js'
36
+ import { YEvent } from './utils/YEvent.js'
37
+ import { $ydoc } from './utils/schemas.js'
37
38
 
38
39
  /**
39
40
  * @typedef {Object<string,any>|Array<any>|number|null|string|Uint8Array|BigInt|YType<any>} YValue
@@ -46,6 +47,45 @@ export const warnPrematureAccess = () => { log.warn('Invalid access: Add Yjs typ
46
47
 
47
48
  const maxSearchMarker = 80
48
49
 
50
+ /**
51
+ * @todo SHOULD NOT RETURN AN OBJECT!
52
+ * @param {Array<ContentAttribute<any>>?} attrs
53
+ * @param {boolean} deleted - whether the attributed item is deleted
54
+ * @return {Attribution|undefined} `undefined` when there is no attribution — under lib0's tri-state
55
+ * that means "skip / inherit the builder's attribution context" (NOT `null`, which would clear it).
56
+ */
57
+ export const createAttributionFromAttributionItems = (attrs, deleted) => {
58
+ if (attrs == null) {
59
+ return undefined
60
+ }
61
+ /**
62
+ * @type {Attribution}
63
+ */
64
+ const attribution = {}
65
+ if (deleted) {
66
+ attribution.delete = []
67
+ } else {
68
+ attribution.insert = []
69
+ }
70
+ attrs.forEach(attr => {
71
+ switch (attr.name) {
72
+ // eslint-disable-next-line no-fallthrough
73
+ case 'insert':
74
+ case 'delete': {
75
+ // needs to be non-ambiguous: don't add existing attr if it doesn't match the actual status
76
+ attribution[attr.name]?.push(attr.val)
77
+ break
78
+ }
79
+ default: {
80
+ if (attr.name[0] !== '_') {
81
+ /** @type {any} */ (attribution)[attr.name] = attr.val
82
+ }
83
+ }
84
+ }
85
+ })
86
+ return attribution
87
+ }
88
+
49
89
  /**
50
90
  * A unique timestamp that identifies each marker.
51
91
  *
@@ -60,15 +100,15 @@ export class ItemTextListPosition {
60
100
  * @param {Item|null} left
61
101
  * @param {Item|null} right
62
102
  * @param {number} index
63
- * @param {Map<string,any>} currentAttributes
64
- * @param {AbstractAttributionManager} am
103
+ * @param {Map<string,any>} currentFormats
104
+ * @param {AbstractRenderer} renderer
65
105
  */
66
- constructor (left, right, index, currentAttributes, am) {
106
+ constructor (left, right, index, currentFormats, renderer) {
67
107
  this.left = left
68
108
  this.right = right
69
109
  this.index = index
70
- this.currentAttributes = currentAttributes
71
- this.am = am
110
+ this.currentFormats = currentFormats
111
+ this.renderer = renderer
72
112
  }
73
113
 
74
114
  /**
@@ -81,11 +121,11 @@ export class ItemTextListPosition {
81
121
  switch (this.right.content.constructor) {
82
122
  case ContentFormat:
83
123
  if (!this.right.deleted) {
84
- updateCurrentAttributes(this.currentAttributes, /** @type {ContentFormat} */ (this.right.content))
124
+ updateCurrentFormats(this.currentFormats, /** @type {ContentFormat} */ (this.right.content))
85
125
  }
86
126
  break
87
127
  default:
88
- this.index += this.am.contentLength(this.right)
128
+ this.index += this.renderer.contentLength(this.right)
89
129
  break
90
130
  }
91
131
  this.left = this.right
@@ -96,23 +136,23 @@ export class ItemTextListPosition {
96
136
  * @param {Transaction} transaction
97
137
  * @param {YType} parent
98
138
  * @param {number} length
99
- * @param {Object<string,any>} attributes
139
+ * @param {Object<string,any>} formats
100
140
  *
101
141
  * @function
102
142
  */
103
- formatText (transaction, parent, length, attributes) {
104
- minimizeAttributeChanges(this, attributes)
105
- const negatedAttributes = insertAttributes(transaction, parent, this, attributes)
143
+ formatText (transaction, parent, length, formats) {
144
+ minimizeFormatChanges(this, formats)
145
+ const negatedFormats = insertFormats(transaction, parent, this, formats)
106
146
  // iterate until first non-format or null is found
107
- // delete all formats with attributes[format.key] != null
108
- // also check the attributes after the first non-format as we do not want to insert redundant negated attributes there
147
+ // delete all formats with formats[format.key] != null
148
+ // also check the formats after the first non-format as we do not want to insert redundant negated formats there
109
149
  // eslint-disable-next-line no-labels
110
150
  iterationLoop: while (
111
151
  this.right !== null &&
112
152
  (length > 0 ||
113
153
  (
114
- negatedAttributes.size > 0 &&
115
- ((this.right.deleted && this.am.contentLength(this.right) === 0) || this.right.content.constructor === ContentFormat)
154
+ negatedFormats.size > 0 &&
155
+ ((this.right.deleted && this.renderer.contentLength(this.right) === 0) || this.right.content.constructor === ContentFormat)
116
156
  )
117
157
  )
118
158
  ) {
@@ -120,34 +160,34 @@ export class ItemTextListPosition {
120
160
  case ContentFormat: {
121
161
  if (!this.right.deleted) {
122
162
  const { key, value } = /** @type {ContentFormat} */ (this.right.content)
123
- const attr = attributes[key]
163
+ const attr = formats[key]
124
164
  if (attr !== undefined) {
125
- if (equalAttrs(attr, value)) {
126
- negatedAttributes.delete(key)
165
+ if (equalFormats(attr, value)) {
166
+ negatedFormats.delete(key)
127
167
  } else {
128
168
  if (length === 0) {
129
- // no need to further extend negatedAttributes
169
+ // no need to further extend negatedFormats
130
170
  // eslint-disable-next-line no-labels
131
171
  break iterationLoop
132
172
  }
133
- negatedAttributes.set(key, value)
173
+ negatedFormats.set(key, value)
134
174
  }
135
175
  this.right.delete(transaction)
136
176
  } else {
137
- this.currentAttributes.set(key, value)
177
+ this.currentFormats.set(key, value)
138
178
  }
139
179
  }
140
180
  break
141
181
  }
142
182
  default: {
143
183
  const item = this.right
144
- const rightLen = this.am.contentLength(item)
184
+ const rightLen = this.renderer.contentLength(item)
145
185
  if (length < rightLen) {
146
186
  /**
147
- * @type {Array<import('./internals.js').AttributedContent<any>>}
187
+ * @type {Array<AttributedContent<any>>}
148
188
  */
149
189
  const contents = []
150
- this.am.readContent(contents, item.id.client, item.id.clock, item.deleted, item.content, 0)
190
+ this.renderer.readContent(contents, item.id.client, item.id.clock, item.deleted, item.content, 0)
151
191
  let i = 0
152
192
  for (; i < contents.length && length > 0; i++) {
153
193
  const c = contents[i]
@@ -170,7 +210,7 @@ export class ItemTextListPosition {
170
210
  if (length > 0) {
171
211
  throw new Error('Exceeded content range')
172
212
  }
173
- insertNegatedAttributes(transaction, parent, this, negatedAttributes)
213
+ insertNegatedFormats(transaction, parent, this, negatedFormats)
174
214
  }
175
215
  }
176
216
 
@@ -180,32 +220,32 @@ export class ItemTextListPosition {
180
220
  * @param {Transaction} transaction
181
221
  * @param {YType} parent
182
222
  * @param {ItemTextListPosition} currPos
183
- * @param {Map<string,any>} negatedAttributes
223
+ * @param {Map<string,any>} negatedFormats
184
224
  *
185
225
  * @private
186
226
  * @function
187
227
  */
188
- const insertNegatedAttributes = (transaction, parent, currPos, negatedAttributes) => {
189
- // check if we really need to remove attributes
228
+ const insertNegatedFormats = (transaction, parent, currPos, negatedFormats) => {
229
+ // check if we really need to remove formats
190
230
  while (
191
231
  currPos.right !== null && (
192
- (currPos.right.deleted && (currPos.am === noAttributionsManager || currPos.am.contentLength(currPos.right) === 0)) || (
232
+ (currPos.right.deleted && (currPos.renderer === baseRenderer || currPos.renderer.contentLength(currPos.right) === 0)) || (
193
233
  currPos.right.content.constructor === ContentFormat &&
194
- equalAttrs(negatedAttributes.get(/** @type {ContentFormat} */ (currPos.right.content).key), /** @type {ContentFormat} */ (currPos.right.content).value)
234
+ equalFormats(negatedFormats.get(/** @type {ContentFormat} */ (currPos.right.content).key), /** @type {ContentFormat} */ (currPos.right.content).value)
195
235
  )
196
236
  )
197
237
  ) {
198
238
  if (!currPos.right.deleted) {
199
- negatedAttributes.delete(/** @type {ContentFormat} */ (currPos.right.content).key)
239
+ negatedFormats.delete(/** @type {ContentFormat} */ (currPos.right.content).key)
200
240
  }
201
241
  currPos.forward()
202
242
  }
203
243
  const doc = transaction.doc
204
244
  const ownClientId = doc.clientID
205
- negatedAttributes.forEach((val, key) => {
245
+ negatedFormats.forEach((val, key) => {
206
246
  const left = currPos.left
207
247
  const right = currPos.right
208
- const nextFormat = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentFormat(key, val))
248
+ const nextFormat = new Item(createID(ownClientId, doc.store.getClock(ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentFormat(key, val))
209
249
  nextFormat.integrate(transaction, 0)
210
250
  currPos.right = nextFormat
211
251
  currPos.forward()
@@ -213,34 +253,34 @@ const insertNegatedAttributes = (transaction, parent, currPos, negatedAttributes
213
253
  }
214
254
 
215
255
  /**
216
- * @param {Map<string,any>} currentAttributes
256
+ * @param {Map<string,any>} currentFormats
217
257
  * @param {ContentFormat} format
218
258
  *
219
259
  * @private
220
260
  * @function
221
261
  */
222
- const updateCurrentAttributes = (currentAttributes, format) => {
262
+ const updateCurrentFormats = (currentFormats, format) => {
223
263
  const { key, value } = format
224
264
  if (value === null) {
225
- currentAttributes.delete(key)
265
+ currentFormats.delete(key)
226
266
  } else {
227
- currentAttributes.set(key, value)
267
+ currentFormats.set(key, value)
228
268
  }
229
269
  }
230
270
 
231
271
  /**
232
272
  * @param {ItemTextListPosition} currPos
233
- * @param {Object<string,any>} attributes
273
+ * @param {Object<string,any>} formats
234
274
  *
235
275
  * @private
236
276
  * @function
237
277
  */
238
- const minimizeAttributeChanges = (currPos, attributes) => {
239
- // go right while attributes[right.key] === right.value (or right is deleted)
278
+ const minimizeFormatChanges = (currPos, formats) => {
279
+ // go right while formats[right.key] === right.value (or right is deleted)
240
280
  while (true) {
241
281
  if (currPos.right === null) {
242
282
  break
243
- } else if (currPos.right.deleted ? (currPos.am.contentLength(currPos.right) === 0) : (!currPos.right.deleted && currPos.right.content.constructor === ContentFormat && equalAttrs(attributes[(/** @type {ContentFormat} */ (currPos.right.content)).key] ?? null, /** @type {ContentFormat} */ (currPos.right.content).value))) {
283
+ } else if (currPos.right.deleted ? (currPos.renderer.contentLength(currPos.right) === 0) : (!currPos.right.deleted && currPos.right.content.constructor === ContentFormat && equalFormats(formats[(/** @type {ContentFormat} */ (currPos.right.content)).key] ?? null, /** @type {ContentFormat} */ (currPos.right.content).value))) {
244
284
  //
245
285
  } else {
246
286
  break
@@ -253,30 +293,30 @@ const minimizeAttributeChanges = (currPos, attributes) => {
253
293
  * @param {Transaction} transaction
254
294
  * @param {YType} parent
255
295
  * @param {ItemTextListPosition} currPos
256
- * @param {Object<string,any>} attributes
296
+ * @param {Object<string,any>} formats
257
297
  * @return {Map<string,any>}
258
298
  *
259
299
  * @private
260
300
  * @function
261
301
  **/
262
- const insertAttributes = (transaction, parent, currPos, attributes) => {
302
+ const insertFormats = (transaction, parent, currPos, formats) => {
263
303
  const doc = transaction.doc
264
304
  const ownClientId = doc.clientID
265
- const negatedAttributes = new Map()
305
+ const negatedFormats = new Map()
266
306
  // insert format-start items
267
- for (const key in attributes) {
268
- const val = attributes[key]
269
- const currentVal = currPos.currentAttributes.get(key) ?? null
270
- if (!equalAttrs(currentVal, val)) {
271
- // save negated attribute (set null if currentVal undefined)
272
- negatedAttributes.set(key, currentVal)
307
+ for (const key in formats) {
308
+ const val = formats[key]
309
+ const currentVal = currPos.currentFormats.get(key) ?? null
310
+ if (!equalFormats(currentVal, val)) {
311
+ // save negated format (set null if currentVal undefined)
312
+ negatedFormats.set(key, currentVal)
273
313
  const { left, right } = currPos
274
- currPos.right = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentFormat(key, val))
314
+ currPos.right = new Item(createID(ownClientId, doc.store.getClock(ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentFormat(key, val))
275
315
  currPos.right.integrate(transaction, 0)
276
316
  currPos.forward()
277
317
  }
278
318
  }
279
- return negatedAttributes
319
+ return negatedFormats
280
320
  }
281
321
 
282
322
  /**
@@ -284,31 +324,31 @@ const insertAttributes = (transaction, parent, currPos, attributes) => {
284
324
  * @param {YType} parent
285
325
  * @param {ItemTextListPosition} currPos
286
326
  * @param {import('./structs/Item.js').AbstractContent} content
287
- * @param {Object<string,any>} attributes
327
+ * @param {Object<string,any>} formats
288
328
  *
289
329
  * @private
290
330
  * @function
291
331
  **/
292
- export const insertContent = (transaction, parent, currPos, content, attributes) => {
293
- currPos.currentAttributes.forEach((_val, key) => {
294
- if (attributes[key] === undefined) {
295
- attributes[key] = null
332
+ export const insertContent = (transaction, parent, currPos, content, formats) => {
333
+ currPos.currentFormats.forEach((_val, key) => {
334
+ if (formats[key] === undefined) {
335
+ formats[key] = null
296
336
  }
297
337
  })
298
338
  const doc = transaction.doc
299
339
  const ownClientId = doc.clientID
300
- minimizeAttributeChanges(currPos, attributes)
301
- const negatedAttributes = insertAttributes(transaction, parent, currPos, attributes)
340
+ minimizeFormatChanges(currPos, formats)
341
+ const negatedFormats = insertFormats(transaction, parent, currPos, formats)
302
342
  let { left, right, index } = currPos
303
343
  if (parent._searchMarker) {
304
344
  updateMarkerChanges(parent._searchMarker, currPos.index, content.getLength())
305
345
  }
306
- right = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, content)
346
+ right = new Item(createID(ownClientId, doc.store.getClock(ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, content)
307
347
  right.integrate(transaction, 0)
308
348
  currPos.right = right
309
349
  currPos.index = index
310
350
  currPos.forward()
311
- insertNegatedAttributes(transaction, parent, currPos, negatedAttributes)
351
+ insertNegatedFormats(transaction, parent, currPos, negatedFormats)
312
352
  }
313
353
 
314
354
  /**
@@ -316,27 +356,27 @@ export const insertContent = (transaction, parent, currPos, content, attributes)
316
356
  * @param {YType} parent
317
357
  * @param {ItemTextListPosition} currPos
318
358
  * @param {Array<any>|string} insert
319
- * @param {Object<string,any>} attributes
359
+ * @param {Object<string,any>} formats
320
360
  */
321
- export const insertContentHelper = (transaction, parent, currPos, insert, attributes) => {
361
+ export const insertContentHelper = (transaction, parent, currPos, insert, formats) => {
322
362
  if (s.$string.check(insert)) {
323
- insertContent(transaction, parent, currPos, new ContentString(insert), attributes)
363
+ insertContent(transaction, parent, currPos, new ContentString(insert), formats)
324
364
  } else {
325
365
  insert = insert.map(ins => delta.$deltaAny.check(ins) ? YType.from(ins) : ins)
326
366
  for (let i = 0; i < insert.length;) {
327
367
  const first = insert[i]
328
368
  if (first instanceof YType) {
329
- insertContent(transaction, parent, currPos, new ContentType(first), attributes)
369
+ insertContent(transaction, parent, currPos, new ContentType(first), formats)
330
370
  i++
331
- } else if (first instanceof Doc) {
332
- insertContent(transaction, parent, currPos, new ContentDoc(first), attributes)
371
+ } else if ($ydoc.check(first)) {
372
+ insertContent(transaction, parent, currPos, createContentDocFromDoc(first), formats)
333
373
  i++
334
374
  } else {
335
375
  // insert "any" content
336
376
  // compute slice len
337
377
  let j = i + 1
338
- for (; j < insert.length && !(insert[j] instanceof YType || insert[j] instanceof Doc); j++) { /* nop */ }
339
- insertContent(transaction, parent, currPos, new ContentAny((i === 0 && j === insert.length) ? insert : insert.slice(i, j)), attributes)
378
+ for (; j < insert.length && !(insert[j] instanceof YType || $ydoc.check(insert[j])); j++) { /* nop */ }
379
+ insertContent(transaction, parent, currPos, new ContentAny((i === 0 && j === insert.length) ? insert : insert.slice(i, j)), formats)
340
380
  i = j
341
381
  }
342
382
  }
@@ -354,7 +394,7 @@ export const insertContentHelper = (transaction, parent, currPos, insert, attrib
354
394
  */
355
395
  export const deleteText = (transaction, currPos, length) => {
356
396
  const startLength = length
357
- const startAttrs = map.copy(currPos.currentAttributes)
397
+ const startFormats = map.copy(currPos.currentFormats)
358
398
  const start = currPos.right
359
399
  while (length > 0 && currPos.right !== null) {
360
400
  const item = currPos.right
@@ -364,12 +404,12 @@ export const deleteText = (transaction, currPos, length) => {
364
404
  }
365
405
  length -= item.length
366
406
  item.delete(transaction)
367
- } else if (currPos.am !== noAttributionsManager) {
407
+ } else if (currPos.renderer !== baseRenderer) {
368
408
  /**
369
- * @type {Array<import('./internals.js').AttributedContent<any>>}
409
+ * @type {Array<AttributedContent<any>>}
370
410
  */
371
411
  const contents = []
372
- currPos.am.readContent(contents, item.id.client, item.id.clock, true, item.content, 0)
412
+ currPos.renderer.readContent(contents, item.id.client, item.id.clock, true, item.content, 0)
373
413
  for (let i = 0; i < contents.length; i++) {
374
414
  const c = contents[i]
375
415
  if (c.content.isCountable() && c.attrs != null) {
@@ -390,7 +430,7 @@ export const deleteText = (transaction, currPos, length) => {
390
430
  currPos.forward()
391
431
  }
392
432
  if (start) {
393
- cleanupFormattingGap(transaction, start, currPos.right, startAttrs, currPos.currentAttributes)
433
+ cleanupFormattingGap(transaction, start, currPos.right, startFormats, currPos.currentFormats)
394
434
  }
395
435
  const parent = /** @type {YType<any>} */ (/** @type {Item} */ (currPos.left || currPos.right).parent)
396
436
  if (parent._searchMarker) {
@@ -593,14 +633,25 @@ export const callTypeObservers = (type, transaction, event) => {
593
633
  }
594
634
 
595
635
  /**
596
- * Abstract Yjs Type class
636
+ * Abstract Yjs Type class.
637
+ *
638
+ * A `YType` is a {@link https://github.com/dmonad/lib0 lib0} `RDT` ("replicated data type", see
639
+ * `lib0/delta/rdt.js`): it emits a `'delta'` event whenever its state changes, accepts foreign
640
+ * changes via {@link YType#applyDelta}, exposes its delta {@link YType#$delta schema}, and can be
641
+ * torn down via {@link YType#destroy}. This lets a `YType` be `bind()`-ed to any other RDT (another
642
+ * `YType`, an in-memory delta, a DOM subtree, …). The legacy {@link YType#observe `observe`} /
643
+ * {@link YType#observeDeep `observeDeep`} `YEvent` API continues to work alongside the `'delta'`
644
+ * channel.
645
+ *
597
646
  * @template {delta.DeltaConf} [DConf=any]
647
+ * @extends {ObservableV2<{ delta: (delta: delta.Delta<DConf>) => void, destroy: (type: YType<DConf>) => void }>}
598
648
  */
599
- export class YType {
649
+ export class YType extends ObservableV2 {
600
650
  /**
601
651
  * @param {delta.DeltaConfGetName<DConf>?} name
602
652
  */
603
653
  constructor (name = null) {
654
+ super()
604
655
  /**
605
656
  * @type {delta.DeltaConfGetName<DConf>}
606
657
  */
@@ -637,20 +688,121 @@ export class YType {
637
688
  */
638
689
  this._searchMarker = null
639
690
  /**
640
- * @type {delta.DeltaBuilder<DConf>}
641
- * @private
691
+ * Maintained deep-delta cache backing {@link YType#delta}. `null` until `delta` is first
692
+ * accessed; thereafter kept current on every event of this type (incrementally, by applying the
693
+ * deep change) and re-diffed by {@link YType#useRenderer}. Cleared by {@link YType#clearCache}.
694
+ * @type {delta.DeltaBuilderAny | null}
642
695
  */
643
- this._content = /** @type {delta.DeltaBuilderAny} */ (delta.create())
644
- this._legacyTypeRef = this.name == null ? contentType.YXmlFragmentRefID : contentType.YXmlElementRefID
696
+ this._delta = null
697
+ this._legacyTypeRef = this.name == null ? YXmlFragmentRefID : YXmlElementRefID
645
698
  /**
646
699
  * @type {Array<ArraySearchMarker>|null}
647
700
  */
648
701
  this._searchMarker = []
649
702
  /**
650
- * Whether this YText contains formatting attributes.
703
+ * Whether this YText contains formats.
651
704
  * This flag is updated when a formatting item is integrated (see ContentFormat.integrate)
652
705
  */
653
706
  this._hasFormatting = false
707
+ /**
708
+ * The active default renderer. Used by `toDelta`, `applyDelta`, and the events whenever no
709
+ * explicit renderer is passed. Change it via {@link YType#useRenderer}.
710
+ * @type {AbstractRenderer}
711
+ */
712
+ this._renderer = baseRenderer
713
+ }
714
+
715
+ /**
716
+ * Schema of the deltas this type produces — part of the lib0 `RDT` interface.
717
+ *
718
+ * @type {s.Schema<delta.Delta<DConf>>}
719
+ */
720
+ get $delta () {
721
+ return /** @type {any} */ (delta.$deltaAny)
722
+ }
723
+
724
+ /**
725
+ * The deep delta of this type (the full nested content tree, children rendered as their own
726
+ * deltas).
727
+ *
728
+ * The returned value is the type's **live** maintained cache: it is materialized on first access
729
+ * and then kept current on every event fired on this type (and re-diffed by
730
+ * {@link YType#useRenderer}), so a reference held across edits keeps updating in place. Clone it
731
+ * (e.g. `type.delta.clone()`) if you need a stable snapshot, and call {@link YType#clearCache} to
732
+ * drop the cache.
733
+ *
734
+ * @type {delta.Delta<DConf>}
735
+ */
736
+ get delta () {
737
+ if (this._delta === null) {
738
+ this._delta = this._renderDelta()
739
+ }
740
+ return /** @type {any} */ (this._delta)
741
+ }
742
+
743
+ /**
744
+ * Render the full deep current state into a fresh `isFinal` builder (so subsequent `.apply`s of
745
+ * deep changes update content in place). Uses this type's active renderer.
746
+ *
747
+ * @return {delta.DeltaBuilderAny}
748
+ */
749
+ _renderDelta () {
750
+ const state = /** @type {delta.DeltaBuilderAny} */ (delta.create(this.name))
751
+ state.isFinal = true
752
+ state.apply(this.toDelta({ deep: true }))
753
+ return state
754
+ }
755
+
756
+ /**
757
+ * Discard the cached deep delta backing {@link YType#delta}.
758
+ *
759
+ * After `delta` is first accessed, the cache is updated on every event fired on this type (and
760
+ * re-diffed by {@link YType#useRenderer}). Call this to drop it — e.g. to reclaim memory, or to
761
+ * force an exact recomputation after editing while a non-base renderer is active (the incremental
762
+ * updates can drift from a fresh deep render in that case).
763
+ */
764
+ clearCache () {
765
+ this._delta = null
766
+ }
767
+
768
+ /**
769
+ * Change the default renderer used by this type. After calling `useRenderer(renderer)`, the
770
+ * `toDelta`, `applyDelta`, and event methods all use `renderer` whenever no explicit renderer is
771
+ * passed (an explicit `{ renderer }` argument still overrides it per call).
772
+ *
773
+ * If the deep-delta cache ({@link YType#delta}) is being maintained, or a `'delta'` listener is
774
+ * attached, the content is re-rendered with the new renderer and the difference is emitted on the
775
+ * `'delta'` channel only (a renderer switch is not a CRDT change, so no `YEvent` is produced).
776
+ *
777
+ * @param {AbstractRenderer} renderer
778
+ * @return {this}
779
+ */
780
+ useRenderer (renderer) {
781
+ const prev = this._renderer
782
+ const hasDeltaListeners = (this._observers.get('delta')?.size ?? 0) > 0
783
+ if (renderer !== prev && (this._delta !== null || hasDeltaListeners)) {
784
+ const oldState = this._delta ?? this._renderDelta()
785
+ this._renderer = renderer
786
+ const newState = this._renderDelta()
787
+ if (this._delta !== null) this._delta = newState
788
+ if (hasDeltaListeners) {
789
+ const d = /** @type {any} */ (delta.diff(/** @type {any} */ (oldState), /** @type {any} */ (newState)))
790
+ if (!d.isEmpty()) this.emit('delta', [d])
791
+ }
792
+ } else {
793
+ this._renderer = renderer
794
+ }
795
+ return this
796
+ }
797
+
798
+ /**
799
+ * Tear down this type as an `RDT`: emit the `'destroy'` event and unregister all `'delta'` /
800
+ * `'destroy'` listeners. The CRDT content and the `observe`/`observeDeep` handlers are left
801
+ * untouched — this only releases the RDT/binding observers.
802
+ */
803
+ destroy () {
804
+ this.emit('destroy', [this])
805
+ super.destroy()
654
806
  }
655
807
 
656
808
  /**
@@ -722,6 +874,9 @@ export class YType {
722
874
  _callObserver (transaction, parentSubs) {
723
875
  const event = new YEvent(/** @type {any} */ (this), transaction, parentSubs)
724
876
  callTypeObservers(/** @type {any} */ (this), transaction, event)
877
+ // Note: the RDT `'delta'` channel (and the deep-delta cache) is driven in the transaction
878
+ // cleanup's `changedParentTypes` loop (see Transaction.js) so it bubbles to ancestors like
879
+ // `observeDeep`, not here where only the directly-changed type is visible.
725
880
  if (!transaction.local && this._searchMarker) {
726
881
  this._searchMarker.length = 0
727
882
  }
@@ -782,55 +937,57 @@ export class YType {
782
937
  *
783
938
  * @template {boolean} [Deep=false]
784
939
  *
785
- * @param {AbstractAttributionManager} am
786
940
  * @param {Object} [opts]
787
- * @param {import('./utils/IdSet.js').IdSet?} [opts.itemsToRender]
941
+ * @param {AbstractRenderer} [opts.renderer] - renders the content (with attributions); defaults to this type's active renderer (see {@link YType#useRenderer}), i.e. `baseRenderer` unless changed
942
+ * @param {IdSet?} [opts.itemsToRender]
788
943
  * @param {boolean} [opts.retainInserts] - if true, retain rendered inserts with attributions
789
944
  * @param {boolean} [opts.retainDeletes] - if true, retain rendered+attributed deletes only
790
- * @param {import('./utils/IdSet.js').IdSet?} [opts.deletedItems] - used for computing prevItem in attributes
945
+ * @param {IdSet?} [opts.deletedItems] - used for computing prevItem in attributes
791
946
  * @param {Map<YType,Set<string|null>>|null} [opts.modified] - set of types that should be rendered as modified children
792
947
  * @param {Deep} [opts.deep] - render child types as delta
793
948
  * @return {Deep extends true ? delta.Delta<DConf> : delta.Delta<DeltaConfDeltaToYType<DConf>>} The Delta representation of this type.
794
949
  *
795
950
  * @public
796
951
  */
797
- toDelta (am = noAttributionsManager, opts = {}) {
798
- const { itemsToRender = null, retainInserts = false, retainDeletes = false, deletedItems = null, modified = null, deep = false } = opts
952
+ toDelta (opts = {}) {
953
+ const { renderer = this._renderer, itemsToRender = null, retainInserts = false, retainDeletes = false, deletedItems = null, deep = false } = opts
954
+ const { modified = (deep && itemsToRender) ? computeModifiedFromItems(/** @type {Doc} */ (this.doc).store, itemsToRender) : null } = opts
799
955
  const renderAttrs = modified?.get(this) || null
800
- const renderChildren = !!(modified == null || modified.get(this)?.has(null))
956
+ const renderChildren = modified == null || !modified.has(this) || /** @type {Set<string|null>} */ (modified.get(this)).has(null)
801
957
  /**
802
958
  * @type {delta.DeltaBuilderAny}
803
959
  */
804
960
  const d = /** @type {any} */ (delta.create(this.name))
805
- const optsAll = modified == null ? opts : object.assign({}, opts, { modified: null })
806
- typeMapGetDelta(d, /** @type {any} */ (this), renderAttrs, am, deep, modified, deletedItems, itemsToRender, opts, optsAll)
961
+ const optsAll = object.assign({}, opts, { renderer, modified })
962
+ // opts has been re-computed - do not use opts after this point!
963
+ typeMapGetDelta(d, /** @type {any} */ (this), renderAttrs, renderer, deep, modified, deletedItems, itemsToRender, optsAll, optsAll)
807
964
  if (renderChildren) {
808
965
  /**
809
- * @type {delta.FormattingAttributes}
966
+ * @type {delta.Formats}
810
967
  */
811
- let currentAttributes = {} // saves all current attributes for insert
812
- let usingCurrentAttributes = false
968
+ let currentFormats = {} // saves all current formats for insert
969
+ let usingCurrentFormats = false
813
970
  /**
814
- * @type {delta.FormattingAttributes}
971
+ * @type {delta.Formats}
815
972
  */
816
- let changedAttributes = {} // saves changed attributes for retain
817
- let usingChangedAttributes = false
973
+ let changedFormats = {} // saves changed formats for retain
974
+ let usingChangedFormats = false
818
975
  /**
819
- * Logic for formatting attribute attribution
820
- * Everything that comes after an formatting attribute is formatted by the user that created it.
976
+ * Logic for format attribution
977
+ * Everything that comes after a format is formatted by the user that created it.
821
978
  * Two exceptions:
822
979
  * - the user resets formatting to the previously known formatting that is not attributed
823
- * - the user deletes a formatting attribute and hence restores the previously known formatting
980
+ * - the user deletes a format and hence restores the previously known formatting
824
981
  * that is not attributed.
825
- * @type {delta.FormattingAttributes}
982
+ * @type {delta.Formats}
826
983
  */
827
- const previousUnattributedAttributes = {} // contains previously known unattributed formatting
984
+ const previousUnattributedFormats = {} // contains previously known unattributed formatting
828
985
  /**
829
- * @type {delta.FormattingAttributes}
986
+ * @type {delta.Formats}
830
987
  */
831
- const previousAttributes = {} // The value before changes
988
+ const previousFormats = {} // The value before changes
832
989
  /**
833
- * @type {Array<import('./internals.js').AttributedContent<any>>}
990
+ * @type {Array<AttributedContent<any>>}
834
991
  */
835
992
  const cs = []
836
993
  for (let item = this._start; item !== null; cs.length = 0) {
@@ -844,12 +1001,12 @@ export class YType {
844
1001
  if (ir !== rslice.length - 1) {
845
1002
  itemContent = itemContent.splice(idrange.len)
846
1003
  }
847
- am.readContent(cs, item.id.client, idrange.clock, item.deleted, content, idrange.exists ? 2 : 0)
1004
+ renderer.readContent(cs, item.id.client, idrange.clock, item.deleted, content, idrange.exists ? 2 : 0)
848
1005
  }
849
1006
  }
850
1007
  } else {
851
1008
  for (; item !== null && cs.length < 50; item = item.right) {
852
- am.readContent(cs, item.id.client, item.id.clock, item.deleted, item.content, 1)
1009
+ renderer.readContent(cs, item.id.client, item.id.clock, item.deleted, item.content, 1)
853
1010
  }
854
1011
  }
855
1012
  for (let i = 0; i < cs.length; i++) {
@@ -857,11 +1014,11 @@ export class YType {
857
1014
  // render (attributed) content even if it was deleted
858
1015
  const renderContent = c.render && (!c.deleted || c.attrs != null)
859
1016
  // content that was just deleted. It is not rendered as an insertion, because it doesn't
860
- // have any attributes.
1017
+ // have any formats.
861
1018
  const renderDelete = c.render && c.deleted
862
- // existing content that should be retained, only adding changed attributes
1019
+ // existing content that should be retained, only adding changed formats
863
1020
  const retainContent = !c.render && (!c.deleted || c.attrs != null)
864
- const attribution = (renderContent || c.content.constructor === ContentFormat) ? createAttributionFromAttributionItems(c.attrs, c.deleted) : null
1021
+ const attribution = (renderContent || c.content.constructor === ContentFormat) ? createAttributionFromAttributionItems(c.attrs, c.deleted) : undefined
865
1022
  switch (c.content.constructor) {
866
1023
  case ContentDeleted: {
867
1024
  if (renderDelete) d.delete(c.content.getLength())
@@ -869,18 +1026,26 @@ export class YType {
869
1026
  }
870
1027
  case ContentString:
871
1028
  if (renderContent) {
872
- d.usedAttributes = currentAttributes
873
- usingCurrentAttributes = true
874
1029
  if (c.deleted ? retainDeletes : retainInserts) {
875
- d.retain(/** @type {ContentString} */ (c.content).str.length, null, attribution ?? {})
1030
+ // a retain expresses the format *diff* against existing (cached) content, so use
1031
+ // `changedFormats`: a format removed this change (e.g. its marker was deleted)
1032
+ // is present there as a `null` clear, whereas `currentFormats` (absolute) can
1033
+ // only re-assert present formats and would silently keep a stale one.
1034
+ d.usedFormats = changedFormats
1035
+ usingChangedFormats = true
1036
+ // change render: a retained item with no attribution means its attribution was
1037
+ // removed → emit `null` (clear) rather than `{}` (skip). Present attribution merges.
1038
+ d.retain(/** @type {ContentString} */ (c.content).str.length, undefined, attribution ?? null)
876
1039
  } else {
877
- d.insert(/** @type {ContentString} */ (c.content).str, null, attribution)
1040
+ d.usedFormats = currentFormats
1041
+ usingCurrentFormats = true
1042
+ d.insert(/** @type {ContentString} */ (c.content).str, undefined, attribution)
878
1043
  }
879
1044
  } else if (renderDelete) {
880
1045
  d.delete(c.content.getLength())
881
1046
  } else if (retainContent) {
882
- d.usedAttributes = changedAttributes
883
- usingChangedAttributes = true
1047
+ d.usedFormats = changedFormats
1048
+ usingChangedFormats = true
884
1049
  d.retain(c.content.getLength())
885
1050
  }
886
1051
  break
@@ -890,114 +1055,153 @@ export class YType {
890
1055
  case ContentType:
891
1056
  case ContentBinary:
892
1057
  if (renderContent) {
893
- d.usedAttributes = currentAttributes
894
- usingCurrentAttributes = true
895
1058
  if (c.deleted ? retainDeletes : retainInserts) {
896
- d.retain(c.content.getLength(), null, attribution ?? {})
1059
+ // a retain expresses the format *diff* → use `changedFormats` (see ContentString)
1060
+ d.usedFormats = changedFormats
1061
+ usingChangedFormats = true
1062
+ if (c.deleted && c.content.constructor === ContentType) {
1063
+ // @todo use current transaction instead
1064
+ d.modify(/** @type {any} */ (c.content).type.toDelta(optsAll), undefined, attribution ?? null)
1065
+ } else {
1066
+ d.retain(c.content.getLength(), undefined, attribution ?? null)
1067
+ }
897
1068
  } else if (deep && c.content.constructor === ContentType) {
898
- d.insert([/** @type {any} */(c.content).type.toDelta(am, optsAll)], null, attribution)
1069
+ d.usedFormats = currentFormats
1070
+ usingCurrentFormats = true
1071
+ d.insert([/** @type {any} */(c.content).type.toDelta(optsAll)], undefined, attribution)
899
1072
  } else {
900
- d.insert(c.content.getContent(), null, attribution)
1073
+ d.usedFormats = currentFormats
1074
+ usingCurrentFormats = true
1075
+ d.insert(c.content.getContent(), undefined, attribution)
901
1076
  }
902
1077
  } else if (renderDelete) {
903
1078
  d.delete(1)
904
1079
  } else if (retainContent) {
905
1080
  if (c.content.constructor === ContentType && modified?.has(/** @type {ContentType} */ (c.content).type)) {
906
1081
  // @todo use current transaction instead
907
- d.modify(/** @type {any} */ (c.content).type.toDelta(am, opts))
1082
+ d.modify(/** @type {any} */ (c.content).type.toDelta(optsAll))
908
1083
  } else {
909
- d.usedAttributes = changedAttributes
910
- usingChangedAttributes = true
1084
+ d.usedFormats = changedFormats
1085
+ usingChangedFormats = true
911
1086
  d.retain(1)
912
1087
  }
913
1088
  }
914
1089
  break
915
1090
  case ContentFormat: {
916
1091
  const { key, value } = /** @type {ContentFormat} */ (c.content)
917
- const currAttrVal = currentAttributes[key] ?? null
918
- if (attribution != null && (c.deleted || !object.hasProperty(previousUnattributedAttributes, key))) {
919
- previousUnattributedAttributes[key] = c.deleted ? value : currAttrVal
1092
+ const currFormatVal = currentFormats[key] ?? null
1093
+ if (attribution != null && (c.deleted || !object.hasProperty(previousUnattributedFormats, key))) {
1094
+ previousUnattributedFormats[key] = c.deleted ? value : currFormatVal
920
1095
  }
921
- // @todo write a function "updateCurrentAttributes" and "updateChangedAttributes"
922
- // # Update Attributes
1096
+ // @todo write a function "updateCurrentFormats" and "updateChangedFormats"
1097
+ // # Update Formats
923
1098
  if (renderContent || renderDelete) {
924
1099
  // create fresh references
925
- if (usingCurrentAttributes) {
926
- currentAttributes = object.assign({}, currentAttributes)
927
- usingCurrentAttributes = false
1100
+ if (usingCurrentFormats) {
1101
+ currentFormats = object.assign({}, currentFormats)
1102
+ usingCurrentFormats = false
928
1103
  }
929
- if (usingChangedAttributes) {
930
- usingChangedAttributes = false
931
- changedAttributes = object.assign({}, changedAttributes)
1104
+ if (usingChangedFormats) {
1105
+ usingChangedFormats = false
1106
+ changedFormats = object.assign({}, changedFormats)
932
1107
  }
933
1108
  }
934
1109
  if (renderContent || renderDelete) {
935
1110
  if (c.deleted) {
936
1111
  // content was deleted, but is possibly attributed
937
- if (!equalAttrs(value, currAttrVal)) { // do nothing if nothing changed
938
- if (equalAttrs(currAttrVal, previousAttributes[key] ?? null) && changedAttributes[key] !== undefined) {
939
- delete changedAttributes[key]
1112
+ if (!equalFormats(value, currFormatVal)) { // do nothing if nothing changed
1113
+ if (equalFormats(currFormatVal, previousFormats[key] ?? null) && changedFormats[key] !== undefined) {
1114
+ delete changedFormats[key]
940
1115
  } else {
941
- changedAttributes[key] = currAttrVal
1116
+ changedFormats[key] = currFormatVal
942
1117
  }
943
- // current attributes doesn't change
944
- previousAttributes[key] = value
1118
+ // current formats doesn't change
1119
+ previousFormats[key] = value
945
1120
  }
946
1121
  } else { // !c.deleted
947
1122
  // content was inserted, and is possibly attributed
948
- if (equalAttrs(value, currAttrVal)) {
1123
+ if (equalFormats(value, currFormatVal)) {
949
1124
  // item.delete(transaction)
950
- } else if (equalAttrs(value, previousAttributes[key] ?? null)) {
951
- delete changedAttributes[key]
1125
+ } else if (equalFormats(value, previousFormats[key] ?? null)) {
1126
+ delete changedFormats[key]
952
1127
  } else {
953
- changedAttributes[key] = value
1128
+ changedFormats[key] = value
954
1129
  }
955
1130
  if (value == null) {
956
- delete currentAttributes[key]
1131
+ delete currentFormats[key]
957
1132
  } else {
958
- currentAttributes[key] = value
1133
+ currentFormats[key] = value
959
1134
  }
960
1135
  }
961
1136
  } else if (retainContent && !c.deleted) {
962
- // fresh reference to currentAttributes only
963
- if (usingCurrentAttributes) {
964
- currentAttributes = object.assign({}, currentAttributes)
965
- usingCurrentAttributes = false
1137
+ // fresh reference to currentFormats only
1138
+ if (usingCurrentFormats) {
1139
+ currentFormats = object.assign({}, currentFormats)
1140
+ usingCurrentFormats = false
966
1141
  }
967
- if (usingChangedAttributes && changedAttributes[key] !== undefined) {
968
- usingChangedAttributes = false
969
- changedAttributes = object.assign({}, changedAttributes)
1142
+ if (usingChangedFormats && changedFormats[key] !== undefined) {
1143
+ usingChangedFormats = false
1144
+ changedFormats = object.assign({}, changedFormats)
970
1145
  }
971
1146
  if (value == null) {
972
- delete currentAttributes[key]
1147
+ delete currentFormats[key]
973
1148
  } else {
974
- currentAttributes[key] = value
1149
+ currentFormats[key] = value
975
1150
  }
976
- delete changedAttributes[key]
977
- previousAttributes[key] = value
1151
+ delete changedFormats[key]
1152
+ previousFormats[key] = value
978
1153
  }
979
1154
  // # Update Attributions
980
- if (attribution != null || object.hasProperty(previousUnattributedAttributes, key)) {
1155
+ // A format marker deleted in a change render under an *attributing* renderer nets to no
1156
+ // attribution (its insert+delete suggestion cancels → `attribution == null`), yet it ends
1157
+ // the attributed range it opened: the following retained content must drop the stale
1158
+ // `{ format: { [key]: [] } }` the marker's insertion wrote to the cache. Emit an explicit
1159
+ // `null` leaf for the key (a context-wide `useAttribution(null)` cannot carry a per-key
1160
+ // clear). Conditions: only an attributing render (`renderer !== baseRenderer`; the base
1161
+ // renderer has no attributions to clear), and only when the deletion actually *removes*
1162
+ // the format — i.e. it reverts to no value (`currFormatVal == null`). If it reverts to a
1163
+ // still-present surrounding value (e.g. deleting a `bold:null` marker re-exposes an
1164
+ // enclosing attributed `bold:true`, as when re-bolding), the attribution is preserved.
1165
+ const isDeletedFormatClear = attribution == null && renderer !== baseRenderer && renderDelete && c.deleted && itemsToRender != null && currFormatVal == null && !equalFormats(value, currFormatVal)
1166
+ if (attribution != null || isDeletedFormatClear || object.hasProperty(previousUnattributedFormats, key)) {
981
1167
  /**
982
- * @type {import('./utils/AttributionManager.js').Attribution}
1168
+ * @type {Attribution}
983
1169
  */
984
1170
  const formattingAttribution = object.assign({}, d.usedAttribution)
985
- const changedAttributedAttributes = /** @type {{ [key: string]: Array<any> }} */ (formattingAttribution.format = object.assign({}, formattingAttribution.format ?? {}))
986
- if (attribution == null || equalAttrs(previousUnattributedAttributes[key], currentAttributes[key] ?? null)) {
987
- // an unattributed formatting attribute was found or an attributed formatting
988
- // attribute was found that resets to the previous status
989
- delete changedAttributedAttributes[key]
990
- delete previousUnattributedAttributes[key]
1171
+ const changedAttributedFormats = /** @type {{ [key: string]: Array<any>|null }} */ (formattingAttribution.format = object.assign({}, formattingAttribution.format ?? {}))
1172
+ const sameAsPreviousAttributions = equalFormats(previousUnattributedFormats[key], currentFormats[key] ?? null)
1173
+ if (isDeletedFormatClear) {
1174
+ changedAttributedFormats[key] = null
1175
+ delete previousUnattributedFormats[key]
1176
+ } else if (attribution == null && !sameAsPreviousAttributions) {
1177
+ // skip
1178
+ } else if (attribution == null || sameAsPreviousAttributions) {
1179
+ // an unattributed format was found or an attributed format
1180
+ // was found that resets to the previous status. When this format item is
1181
+ // itself rendered this transaction (`renderContent || renderDelete`) in a change/diff
1182
+ // render (`itemsToRender != null`), it is the END of an attributed format range:
1183
+ // emit an explicit clear (a `null` leaf) so the retained content drops any stale
1184
+ // `{ format: { [key]: [] } }` from the maintained `delta` cache — a bare context-skip
1185
+ // (`delete`) would leave it in place. For a merely-retained (unchanged) boundary, or a
1186
+ // full insert render (removal is already modeled as absence in `currentFormats`),
1187
+ // just drop the key: a change render must not emit ops for unchanged ranges, and
1188
+ // inserts must stay free of a spurious `{ format: { [key]: null } }`.
1189
+ if (attribution != null && itemsToRender != null && (renderContent || renderDelete)) {
1190
+ changedAttributedFormats[key] = null
1191
+ } else {
1192
+ delete changedAttributedFormats[key]
1193
+ }
1194
+ delete previousUnattributedFormats[key]
991
1195
  } else {
992
- const by = changedAttributedAttributes[key] = (changedAttributedAttributes[key]?.slice() ?? [])
1196
+ const by = changedAttributedFormats[key] = (changedAttributedFormats[key]?.slice() ?? [])
993
1197
  by.push(...((c.deleted ? attribution.delete : attribution.insert) ?? []))
994
- const attributedAt = (c.deleted ? attribution.deletedAt : attribution.insertedAt)
1198
+ const attributedAt = (c.deleted ? attribution.deleteAt : attribution.insertAt)
995
1199
  if (attributedAt) formattingAttribution.formatAt = attributedAt
996
1200
  }
997
- if (object.isEmpty(changedAttributedAttributes)) {
1201
+ if (object.isEmpty(changedAttributedFormats)) {
998
1202
  d.useAttribution(null)
999
- } else if (attribution != null) {
1000
- const attributedAt = (c.deleted ? attribution.deletedAt : attribution.insertedAt)
1203
+ } else if (attribution != null || isDeletedFormatClear) {
1204
+ const attributedAt = (c.deleted ? attribution?.deleteAt : attribution?.insertAt)
1001
1205
  if (attributedAt != null) formattingAttribution.formatAt = attributedAt
1002
1206
  d.useAttribution(formattingAttribution)
1003
1207
  }
@@ -1015,28 +1219,33 @@ export class YType {
1015
1219
  * Render the difference to another ydoc (which can be empty) and highlight the differences with
1016
1220
  * attributions.
1017
1221
  *
1018
- * @param {AbstractAttributionManager} am
1222
+ * @param {Object} [opts]
1223
+ * @param {AbstractRenderer} [opts.renderer] - renders the content (with attributions); defaults to this type's active renderer (see {@link YType#useRenderer}), i.e. `baseRenderer` unless changed
1019
1224
  * @return {delta.Delta<DConf>}
1020
1225
  */
1021
- toDeltaDeep (am = noAttributionsManager) {
1022
- return /** @type {any} */ (this.toDelta(am, { deep: true }))
1226
+ toDeltaDeep (opts = {}) {
1227
+ return /** @type {any} */ (this.toDelta({ ...opts, deep: true }))
1023
1228
  }
1024
1229
 
1025
1230
  /**
1026
1231
  * Apply a {@link Delta} on this shared type.
1027
1232
  *
1028
1233
  * @param {delta.DeltaAny} d The changes to apply on this element.
1029
- * @param {AbstractAttributionManager} am
1234
+ * @param {Object} [opts]
1235
+ * @param {AbstractRenderer} [opts.renderer] - renders the content (with attributions); defaults to this type's active renderer (see {@link YType#useRenderer}), i.e. `baseRenderer` unless changed
1236
+ * @return {null} The lib0 `RDT` "fix" of this apply — always `null`: a `YType` accepts every valid
1237
+ * delta as-is and never needs to self-correct.
1030
1238
  *
1031
1239
  * @public
1032
1240
  */
1033
- applyDelta (d, am = noAttributionsManager) {
1241
+ applyDelta (d, { renderer = this._renderer } = {}) {
1242
+ if (d.isEmpty()) return null
1034
1243
  if (this.doc == null) {
1035
1244
  (this._prelim || (this._prelim = /** @type {any} */ (delta.create()))).apply(d)
1036
- } else {
1245
+ } else if (this._item?.deleted !== true) {
1037
1246
  // @todo this was moved here from ytext. Make this more generic
1038
1247
  transact(this.doc, transaction => {
1039
- const currPos = new ItemTextListPosition(null, this._start, 0, new Map(), am)
1248
+ const currPos = new ItemTextListPosition(null, this._start, 0, new Map(), renderer)
1040
1249
  for (const op of d.children) {
1041
1250
  if (delta.$textOp.check(op)) {
1042
1251
  insertContent(transaction, /** @type {any} */ (this), currPos, new ContentString(op.insert), op.format || {})
@@ -1047,11 +1256,10 @@ export class YType {
1047
1256
  } else if (delta.$deleteOp.check(op)) {
1048
1257
  deleteText(transaction, currPos, op.delete)
1049
1258
  } else if (delta.$modifyOp.check(op)) {
1050
- if (currPos.right) {
1051
- /** @type {ContentType} */ (currPos.right.content).type.applyDelta(op.value)
1052
- } else {
1053
- error.unexpectedCase()
1054
- }
1259
+ let item = currPos.right
1260
+ while (item != null && (item.deleted || !item.countable)) { item = item.next }
1261
+ if (item == null || item.content.constructor !== ContentType) { error.unexpectedCase() }
1262
+ /** @type {ContentType} */ (item.content).type.applyDelta(op.value, { renderer })
1055
1263
  currPos.formatText(transaction, /** @type {any} */ (this), 1, op.format || {})
1056
1264
  } else {
1057
1265
  error.unexpectedCase()
@@ -1067,12 +1275,12 @@ export class YType {
1067
1275
  if (!(sub instanceof YType)) {
1068
1276
  error.unexpectedCase()
1069
1277
  }
1070
- sub.applyDelta(op.value)
1278
+ sub.applyDelta(op.value, { renderer })
1071
1279
  }
1072
1280
  }
1073
1281
  })
1074
1282
  }
1075
- return this
1283
+ return null
1076
1284
  }
1077
1285
 
1078
1286
  /**
@@ -1178,7 +1386,7 @@ export class YType {
1178
1386
  *
1179
1387
  * @param {number} index The index to insert content at.
1180
1388
  * @param {Array<delta.DeltaConfGetChildren<DConf>>|delta.DeltaConfGetText<DConf>} content Array of content to append.
1181
- * @param {delta.FormattingAttributes} [format]
1389
+ * @param {delta.Formats} [format]
1182
1390
  */
1183
1391
  insert (index, content, format) {
1184
1392
  this.applyDelta(delta.create().retain(index).insert(/** @type {any} */ (content), format).done())
@@ -1199,7 +1407,7 @@ export class YType {
1199
1407
  *
1200
1408
  * @param {number} index The index to insert content at.
1201
1409
  * @param {number} length The index to insert content at.
1202
- * @param {delta.FormattingAttributes} formats
1410
+ * @param {delta.Formats} formats
1203
1411
  *
1204
1412
  */
1205
1413
  format (index, length, formats) {
@@ -1273,7 +1481,9 @@ export class YType {
1273
1481
  const children = []
1274
1482
  for (const child of dcontent.children) {
1275
1483
  if (delta.$insertOp.check(child)) {
1276
- children.push(...child.insert)
1484
+ for (let i = 0; i < child.insert.length; i++) {
1485
+ children.push(child.insert[i])
1486
+ }
1277
1487
  } else if (delta.$textOp.check(child)) {
1278
1488
  children.push(child.insert)
1279
1489
  }
@@ -1428,8 +1638,8 @@ export class YType {
1428
1638
  _write (encoder) {
1429
1639
  encoder.writeTypeRef(this._legacyTypeRef)
1430
1640
  switch (this._legacyTypeRef) {
1431
- case contentType.YXmlElementRefID:
1432
- case contentType.YXmlHookRefID: {
1641
+ case YXmlElementRefID:
1642
+ case YXmlHookRefID: {
1433
1643
  encoder.writeKey(this.name)
1434
1644
  break
1435
1645
  }
@@ -1438,17 +1648,32 @@ export class YType {
1438
1648
  }
1439
1649
 
1440
1650
  /**
1441
- * @param {import('./utils/UpdateDecoder.js').UpdateDecoderV1 | import('./utils/UpdateDecoder.js').UpdateDecoderV2} decoder
1442
- * @return {YType}
1443
- *
1444
- * @private
1445
- * @function
1651
+ * @template {import('lib0/delta').ReadableDeltaConf} DConf
1652
+ * @param {DConf} _dconf
1653
+ * @return {s.Schema<YType<import('lib0/delta').ReadDeltaConf<DConf>>>}
1446
1654
  */
1447
- export const readYType = decoder => {
1448
- const typeRef = decoder.readTypeRef()
1449
- const ytype = new YType(typeRef === contentType.YXmlElementRefID || typeRef === contentType.YXmlHookRefID ? decoder.readKey() : null)
1450
- ytype._legacyTypeRef = typeRef
1451
- return ytype
1655
+ export const $ytype = _dconf => s.$instanceOf(YType)
1656
+ export const $ytypeAny = s.$instanceOf(YType)
1657
+
1658
+ /**
1659
+ * @param {StructStore} store
1660
+ * @param {IdSet} items
1661
+ */
1662
+ export const computeModifiedFromItems = (store, items) => {
1663
+ /**
1664
+ * @type {Map<YType,Set<null|string>>}
1665
+ */
1666
+ const modified = new Map()
1667
+ iterateStructsByIdSetWithoutSplits(store, items, /** @param {Item | GC | Skip | null} item */ item => {
1668
+ while (item instanceof Item) {
1669
+ const parent = /** @type {YType} */ (item.parent)
1670
+ const conf = map.setIfUndefined(modified, parent, set.create)
1671
+ if (conf.has(item.parentSub)) break // has already been marked as modified
1672
+ conf.add(item.parentSub)
1673
+ item = parent._item
1674
+ }
1675
+ })
1676
+ return modified
1452
1677
  }
1453
1678
 
1454
1679
  /**
@@ -1456,7 +1681,7 @@ export const readYType = decoder => {
1456
1681
  * @param {any} b
1457
1682
  * @return {boolean}
1458
1683
  */
1459
- export const equalAttrs = (a, b) => a === b || (typeof a === 'object' && typeof b === 'object' && a && b && object.equalFlat(a, b))
1684
+ export const equalFormats = (a, b) => a === b || (typeof a === 'object' && typeof b === 'object' && a && b && object.equalFlat(a, b))
1460
1685
 
1461
1686
  /**
1462
1687
  * @template {delta.DeltaConf} DConf
@@ -1561,7 +1786,7 @@ export const typeListInsertGenericsAfter = (transaction, parent, referenceItem,
1561
1786
  let jsonContent = []
1562
1787
  const packJsonContent = () => {
1563
1788
  if (jsonContent.length > 0) {
1564
- left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentAny(jsonContent))
1789
+ left = new Item(createID(ownClientId, store.getClock(ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentAny(jsonContent))
1565
1790
  left.integrate(transaction, 0)
1566
1791
  jsonContent = []
1567
1792
  }
@@ -1586,16 +1811,15 @@ export const typeListInsertGenericsAfter = (transaction, parent, referenceItem,
1586
1811
  switch (c.constructor) {
1587
1812
  case Uint8Array:
1588
1813
  case ArrayBuffer:
1589
- left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentBinary(new Uint8Array(/** @type {Uint8Array} */ (c))))
1590
- left.integrate(transaction, 0)
1591
- break
1592
- case Doc:
1593
- left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentDoc(/** @type {Doc} */ (c)))
1814
+ left = new Item(createID(ownClientId, store.getClock(ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentBinary(new Uint8Array(/** @type {Uint8Array} */ (c))))
1594
1815
  left.integrate(transaction, 0)
1595
1816
  break
1596
1817
  default:
1597
- if (c instanceof YType) {
1598
- left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentType(/** @type {any} */ (c)))
1818
+ if ($ydoc.check(c)) {
1819
+ left = new Item(createID(ownClientId, store.getClock(ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, createContentDocFromDoc(/** @type {Doc} */ (c)))
1820
+ left.integrate(transaction, 0)
1821
+ } else if (c instanceof YType) {
1822
+ left = new Item(createID(ownClientId, store.getClock(ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentType(/** @type {any} */ (c)))
1599
1823
  left.integrate(transaction, 0)
1600
1824
  } else {
1601
1825
  throw new Error('Unexpected content type in insert operation')
@@ -1776,18 +2000,17 @@ export const typeMapSet = (transaction, parent, key, value) => {
1776
2000
  case Uint8Array:
1777
2001
  content = new ContentBinary(/** @type {Uint8Array} */ (value))
1778
2002
  break
1779
- case Doc:
1780
- content = new ContentDoc(/** @type {Doc} */ (value))
1781
- break
1782
2003
  default:
1783
- if (value instanceof YType) {
2004
+ if ($ydoc.check(value)) {
2005
+ content = createContentDocFromDoc(/** @type {Doc} */ (value))
2006
+ } else if (value instanceof YType) {
1784
2007
  content = new ContentType(/** @type {any} */ (value))
1785
2008
  } else {
1786
2009
  throw new Error('Unexpected content type')
1787
2010
  }
1788
2011
  }
1789
2012
  }
1790
- new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, null, null, parent, key, content).integrate(transaction, 0)
2013
+ new Item(createID(ownClientId, doc.store.getClock(ownClientId)), left, left && left.lastId, null, null, parent, key, content).integrate(transaction, 0)
1791
2014
  }
1792
2015
 
1793
2016
  /**
@@ -1838,18 +2061,18 @@ export const typeMapGetAll = (parent) => {
1838
2061
  * @param {TypeDelta} d
1839
2062
  * @param {YType} parent
1840
2063
  * @param {Set<string|null>?} attrsToRender
1841
- * @param {import('./internals.js').AbstractAttributionManager} am
2064
+ * @param {AbstractRenderer} renderer
1842
2065
  * @param {boolean} deep
1843
2066
  * @param {Set<YType>|Map<YType,any>|null} [modified] - set of types that should be rendered as modified children
1844
- * @param {import('./utils/IdSet.js').IdSet?} [deletedItems]
1845
- * @param {import('./utils/IdSet.js').IdSet?} [itemsToRender]
2067
+ * @param {IdSet?} [deletedItems]
2068
+ * @param {IdSet?} [itemsToRender]
1846
2069
  * @param {any} [opts]
1847
2070
  * @param {any} [optsAll]
1848
2071
  *
1849
2072
  * @private
1850
2073
  * @function
1851
2074
  */
1852
- export const typeMapGetDelta = (d, parent, attrsToRender, am, deep, modified, deletedItems, itemsToRender, opts, optsAll) => {
2075
+ export const typeMapGetDelta = (d, parent, attrsToRender, renderer, deep, modified, deletedItems, itemsToRender, opts, optsAll) => {
1853
2076
  // @todo support modified ops!
1854
2077
  /**
1855
2078
  * @param {Item} item
@@ -1857,30 +2080,43 @@ export const typeMapGetDelta = (d, parent, attrsToRender, am, deep, modified, de
1857
2080
  */
1858
2081
  const renderAttrs = (item, key) => {
1859
2082
  /**
1860
- * @type {Array<import('./internals.js').AttributedContent<any>>}
2083
+ * @type {Array<AttributedContent>}
1861
2084
  */
1862
2085
  const cs = []
1863
- am.readContent(cs, item.id.client, item.id.clock, item.deleted, item.content, 1)
1864
- const { deleted, attrs, content, render } = cs[cs.length - 1]
1865
- if (!render) return
2086
+ renderer.readContent(cs, item.id.client, item.id.clock, item.deleted, item.content, 1)
2087
+ if (cs.length === 0) return // the renderer surfaces nothing for this attribute (e.g. a diff renderer hiding an unchanged delete)
2088
+ const { deleted, attrs, content } = cs[cs.length - 1]
1866
2089
  const attribution = createAttributionFromAttributionItems(attrs, deleted)
1867
2090
  let c = array.last(content.getContent())
1868
2091
  if (deleted) {
1869
- if (itemsToRender == null || itemsToRender.hasId(item.lastId)) {
2092
+ if (attribution != null) {
2093
+ // Item surfaced under attribution (suggestion view / diff renderer, either in snapshot mode
2094
+ // or in an event-driven render). The attribute is still observable in the rendered state, so
2095
+ // emit a positive `SetAttrOp` carrying the attribution metadata - matching how content
2096
+ // children are rendered for the same case (positive `InsertOp` with attribution, never
2097
+ // `DeleteOp`).
2098
+ if (itemsToRender == null || itemsToRender.hasId(item.lastId)) {
2099
+ d.setAttr(key, c, attribution)
2100
+ }
2101
+ } else if (itemsToRender != null && itemsToRender.hasId(item.lastId)) {
2102
+ // Hard-deleted attribute within a change render: emit the `deleteAttr` op so consumers (the
2103
+ // `YEvent` delta, RDT bindings, the maintained `delta` cache) can apply the removal. In
2104
+ // full-state mode (`itemsToRender == null`) the attribute is simply omitted (above renders
2105
+ // run with `render === false` for such items, so nothing was emitted before either).
1870
2106
  d.deleteAttr(key, attribution, c)
1871
2107
  }
1872
2108
  } else if (deep && c instanceof YType && modified?.has(c)) {
1873
- d.modifyAttr(key, c.toDelta(am, opts))
2109
+ d.modifyAttr(key, c.toDelta(opts))
1874
2110
  } else {
1875
2111
  // find prev content
1876
2112
  let prevContentItem = item
1877
- // this algorithm is problematic. should check all previous content using am.readcontent
2113
+ // this algorithm is problematic. should check all previous content using renderer.readcontent
1878
2114
  for (; prevContentItem.left !== null && deletedItems?.hasId(prevContentItem.left.lastId); prevContentItem = prevContentItem.left) {
1879
2115
  // nop
1880
2116
  }
1881
2117
  const prevValue = (prevContentItem !== item && itemsToRender?.hasId(prevContentItem.lastId)) ? array.last(prevContentItem.content.getContent()) : undefined
1882
2118
  if (deep && c instanceof YType) {
1883
- c = /** @type {any} */(c).toDelta(am, optsAll)
2119
+ c = /** @type {any} */(c).toDelta(optsAll)
1884
2120
  }
1885
2121
  d.setAttr(key, c, attribution, prevValue)
1886
2122
  }
@@ -1906,6 +2142,17 @@ export const typeMapHas = (parent, key) => {
1906
2142
  return val !== undefined && !val.deleted
1907
2143
  }
1908
2144
 
2145
+ /**
2146
+ * @param {Item} item
2147
+ * @param {Snapshot|undefined} snapshot
2148
+ *
2149
+ * @protected
2150
+ * @function
2151
+ */
2152
+ export const isVisible = (item, snapshot) => snapshot === undefined
2153
+ ? !item.deleted
2154
+ : snapshot.sv.has(item.id.client) && (snapshot.sv.get(item.id.client) || 0) > item.id.clock && !snapshot.ds.hasId(item.id)
2155
+
1909
2156
  /**
1910
2157
  * @param {YType<any>} parent
1911
2158
  * @param {string} key
@@ -1962,3 +2209,127 @@ export const createMapIterator = type => {
1962
2209
  type.doc ?? warnPrematureAccess()
1963
2210
  return iterator.iteratorFilter(type._map.entries(), /** @param {any} entry */ entry => !entry[1].deleted)
1964
2211
  }
2212
+
2213
+ /**
2214
+ * @private
2215
+ *
2216
+ * @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
2217
+ * @return {ContentType}
2218
+ */
2219
+ export const readContentType = decoder => new ContentType(readYType(decoder))
2220
+
2221
+ /**
2222
+ * @private
2223
+ *
2224
+ * @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
2225
+ * @return {ContentString}
2226
+ */
2227
+ export const readContentString = decoder => new ContentString(decoder.readString())
2228
+
2229
+ /**
2230
+ * @private
2231
+ *
2232
+ * @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
2233
+ * @return {ContentJSON}
2234
+ */
2235
+ export const readContentJSON = decoder => {
2236
+ const len = decoder.readLen()
2237
+ const cs = []
2238
+ for (let i = 0; i < len; i++) {
2239
+ const c = decoder.readString()
2240
+ if (c === 'undefined') {
2241
+ cs.push(undefined)
2242
+ } else {
2243
+ cs.push(JSON.parse(c))
2244
+ }
2245
+ }
2246
+ return new ContentJSON(cs)
2247
+ }
2248
+
2249
+ /**
2250
+ * @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
2251
+ * @return {ContentFormat}
2252
+ */
2253
+ export const readContentFormat = decoder => new ContentFormat(decoder.readKey(), decoder.readJSON())
2254
+
2255
+ /**
2256
+ * @private
2257
+ *
2258
+ * @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
2259
+ * @return {ContentEmbed}
2260
+ */
2261
+ export const readContentEmbed = decoder => new ContentEmbed(decoder.readJSON())
2262
+
2263
+ /**
2264
+ * @private
2265
+ *
2266
+ * @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
2267
+ * @return {ContentDoc}
2268
+ */
2269
+ export const readContentDoc = decoder => new ContentDoc(decoder.readString(), decoder.readAny())
2270
+
2271
+ /**
2272
+ * @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
2273
+ * @return {ContentAny}
2274
+ */
2275
+ export const readContentAny = decoder => {
2276
+ const len = decoder.readLen()
2277
+ const cs = []
2278
+ for (let i = 0; i < len; i++) {
2279
+ cs.push(decoder.readAny())
2280
+ }
2281
+ return new ContentAny(cs)
2282
+ }
2283
+
2284
+ /**
2285
+ * @param {UpdateDecoderV1 | UpdateDecoderV2 } decoder
2286
+ * @return {ContentBinary}
2287
+ */
2288
+ export const readContentBinary = decoder => new ContentBinary(decoder.readBuf())
2289
+
2290
+ /**
2291
+ * @private
2292
+ *
2293
+ * @param {UpdateDecoderV1 | UpdateDecoderV2 } decoder
2294
+ * @return {ContentDeleted}
2295
+ */
2296
+ export const readContentDeleted = decoder => new ContentDeleted(decoder.readLen())
2297
+
2298
+ /**
2299
+ * A lookup map for reading Item content.
2300
+ *
2301
+ * @type {Array<function(UpdateDecoderV1 | UpdateDecoderV2):AbstractContent>}
2302
+ */
2303
+ export const contentRefs = [
2304
+ () => { error.unexpectedCase() }, // GC is not ItemContent
2305
+ readContentDeleted, // 1
2306
+ readContentJSON, // 2
2307
+ readContentBinary, // 3
2308
+ readContentString, // 4
2309
+ readContentEmbed, // 5
2310
+ readContentFormat, // 6
2311
+ readContentType, // 7
2312
+ readContentAny, // 8
2313
+ readContentDoc, // 9
2314
+ () => { error.unexpectedCase() } // 10 - Skip is not ItemContent
2315
+ ]
2316
+
2317
+ /**
2318
+ * @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
2319
+ * @param {number} info
2320
+ */
2321
+ export const readItemContent = (decoder, info) => contentRefs[info & binary.BITS5](decoder)
2322
+
2323
+ /**
2324
+ * @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
2325
+ * @return {YType}
2326
+ *
2327
+ * @private
2328
+ * @function
2329
+ */
2330
+ export const readYType = decoder => {
2331
+ const typeRef = decoder.readTypeRef()
2332
+ const ytype = new YType(typeRef === YXmlElementRefID || typeRef === YXmlHookRefID ? decoder.readKey() : null)
2333
+ ytype._legacyTypeRef = typeRef
2334
+ return ytype
2335
+ }