lib0 1.0.0-rc.2 → 1.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 (141) hide show
  1. package/README.md +0 -1315
  2. package/{types → dist}/buffer.d.ts +2 -10
  3. package/{types → dist}/delta/delta.d.ts +582 -144
  4. package/dist/delta/position.d.ts +65 -0
  5. package/dist/delta/rdt/delta.d.ts +57 -0
  6. package/dist/delta/rdt/dom.d.ts +102 -0
  7. package/dist/delta/rdt.d.ts +180 -0
  8. package/dist/delta/transformer/attr.d.ts +64 -0
  9. package/dist/delta/transformer/attribution-to-format.d.ts +52 -0
  10. package/dist/delta/transformer/children.d.ts +69 -0
  11. package/dist/delta/transformer/conform.d.ts +83 -0
  12. package/dist/delta/transformer/core.d.ts +189 -0
  13. package/dist/delta/transformer/full-attributions.d.ts +37 -0
  14. package/dist/delta/transformer/id.d.ts +2 -0
  15. package/dist/delta/transformer/inline.d.ts +89 -0
  16. package/dist/delta/transformer/pipe.d.ts +182 -0
  17. package/dist/delta/transformer/project.d.ts +223 -0
  18. package/dist/delta/transformer/rename-attrs.d.ts +75 -0
  19. package/dist/delta/transformer/rename.d.ts +41 -0
  20. package/dist/delta/transformer/value.d.ts +92 -0
  21. package/dist/delta/transformer.d.ts +14 -0
  22. package/dist/diff/patience.d.ts +25 -0
  23. package/{types → dist}/environment.d.ts +10 -0
  24. package/dist/hash/fnv1a.d.ts +11 -0
  25. package/{types → dist}/schema.d.ts +49 -38
  26. package/{types → dist}/ts.d.ts +4 -0
  27. package/dist/webcrypto.d.ts +3 -0
  28. package/package.json +148 -82
  29. package/src/bin/0serve.js +11 -3
  30. package/src/broadcastchannel.js +6 -1
  31. package/src/buffer.js +35 -25
  32. package/src/decoding.js +1 -1
  33. package/src/delta/delta.js +2531 -564
  34. package/src/delta/position.js +165 -0
  35. package/src/delta/rdt/delta.js +105 -0
  36. package/src/delta/rdt/dom.js +307 -0
  37. package/src/delta/rdt.js +255 -0
  38. package/src/delta/transformer/attr.js +126 -0
  39. package/src/delta/transformer/attribution-to-format.js +335 -0
  40. package/src/delta/transformer/children.js +219 -0
  41. package/src/delta/transformer/conform.js +401 -0
  42. package/src/delta/transformer/core.js +323 -0
  43. package/src/delta/transformer/full-attributions.js +282 -0
  44. package/src/delta/transformer/id.js +13 -0
  45. package/src/delta/transformer/inline.js +818 -0
  46. package/src/delta/transformer/pipe.js +272 -0
  47. package/src/delta/transformer/project.js +526 -0
  48. package/src/delta/transformer/rename-attrs.js +152 -0
  49. package/src/delta/transformer/rename.js +84 -0
  50. package/src/delta/transformer/value.js +214 -0
  51. package/src/delta/transformer.js +38 -0
  52. package/src/diff/patience.js +22 -11
  53. package/src/dom.js +2 -2
  54. package/src/encoding.js +1 -1
  55. package/src/environment.js +15 -0
  56. package/src/hash/fnv1a.js +82 -0
  57. package/src/number.js +1 -3
  58. package/src/performance.node.js +3 -3
  59. package/src/schema.js +231 -177
  60. package/src/string.js +3 -5
  61. package/src/testing.js +1 -1
  62. package/src/trait/equality.js +1 -1
  63. package/src/trait/fingerprint.js +1 -1
  64. package/src/ts.js +11 -0
  65. package/src/bin/gendocs.js +0 -117
  66. package/src/component.js +0 -414
  67. package/src/delta/binding.js +0 -372
  68. package/src/tree.js +0 -546
  69. package/types/bin/gendocs.d.ts +0 -3
  70. package/types/component.d.ts +0 -86
  71. package/types/delta/binding.d.ts +0 -106
  72. package/types/diff/patience.d.ts +0 -16
  73. package/types/tree.d.ts +0 -96
  74. package/types/webcrypto.d.ts +0 -3
  75. /package/{types → dist}/array.d.ts +0 -0
  76. /package/{types → dist}/bin/0ecdsa-generate-keypair.d.ts +0 -0
  77. /package/{types → dist}/bin/0serve.d.ts +0 -0
  78. /package/{types → dist}/bin/gentesthtml.d.ts +0 -0
  79. /package/{types → dist}/binary.d.ts +0 -0
  80. /package/{types → dist}/broadcastchannel.d.ts +0 -0
  81. /package/{types → dist}/cache.d.ts +0 -0
  82. /package/{types → dist}/conditions.d.ts +0 -0
  83. /package/{types → dist}/crypto/aes-gcm.d.ts +0 -0
  84. /package/{types → dist}/crypto/common.d.ts +0 -0
  85. /package/{types → dist}/crypto/ecdsa.d.ts +0 -0
  86. /package/{types → dist}/crypto/jwt.d.ts +0 -0
  87. /package/{types → dist}/crypto/rsa-oaep.d.ts +0 -0
  88. /package/{types → dist}/decoding.d.ts +0 -0
  89. /package/{types → dist}/diff.d.ts +0 -0
  90. /package/{types → dist}/dom.d.ts +0 -0
  91. /package/{types → dist}/encoding.d.ts +0 -0
  92. /package/{types → dist}/error.d.ts +0 -0
  93. /package/{types → dist}/eventloop.d.ts +0 -0
  94. /package/{types → dist}/function.d.ts +0 -0
  95. /package/{types → dist}/hash/rabin-gf2-polynomial.d.ts +0 -0
  96. /package/{types → dist}/hash/rabin-uncached.d.ts +0 -0
  97. /package/{types → dist}/hash/rabin.d.ts +0 -0
  98. /package/{types → dist}/hash/sha256.d.ts +0 -0
  99. /package/{types → dist}/hash/sha256.node.d.ts +0 -0
  100. /package/{types → dist}/indexeddb.d.ts +0 -0
  101. /package/{types → dist}/indexeddbV2.d.ts +0 -0
  102. /package/{types → dist}/iterator.d.ts +0 -0
  103. /package/{types → dist}/json.d.ts +0 -0
  104. /package/{types → dist}/list.d.ts +0 -0
  105. /package/{types → dist}/logging.common.d.ts +0 -0
  106. /package/{types → dist}/logging.d.ts +0 -0
  107. /package/{types → dist}/logging.node.d.ts +0 -0
  108. /package/{types → dist}/map.d.ts +0 -0
  109. /package/{types → dist}/math.d.ts +0 -0
  110. /package/{types → dist}/metric.d.ts +0 -0
  111. /package/{types → dist}/mutex.d.ts +0 -0
  112. /package/{types → dist}/number.d.ts +0 -0
  113. /package/{types → dist}/object.d.ts +0 -0
  114. /package/{types → dist}/observable.d.ts +0 -0
  115. /package/{types → dist}/pair.d.ts +0 -0
  116. /package/{types → dist}/performance.d.ts +0 -0
  117. /package/{types → dist}/performance.node.d.ts +0 -0
  118. /package/{types → dist}/pledge.d.ts +0 -0
  119. /package/{types → dist}/prng/Mt19937.d.ts +0 -0
  120. /package/{types → dist}/prng/Xoroshiro128plus.d.ts +0 -0
  121. /package/{types → dist}/prng/Xorshift32.d.ts +0 -0
  122. /package/{types → dist}/prng.d.ts +0 -0
  123. /package/{types → dist}/promise.d.ts +0 -0
  124. /package/{types → dist}/queue.d.ts +0 -0
  125. /package/{types → dist}/random.d.ts +0 -0
  126. /package/{types → dist}/set.d.ts +0 -0
  127. /package/{types → dist}/sort.d.ts +0 -0
  128. /package/{types → dist}/statistics.d.ts +0 -0
  129. /package/{types → dist}/storage.d.ts +0 -0
  130. /package/{types → dist}/string.d.ts +0 -0
  131. /package/{types → dist}/symbol.d.ts +0 -0
  132. /package/{types → dist}/testing.d.ts +0 -0
  133. /package/{types → dist}/time.d.ts +0 -0
  134. /package/{types → dist}/trait/equality.d.ts +0 -0
  135. /package/{types → dist}/trait/fingerprint.d.ts +0 -0
  136. /package/{types → dist}/trait/traits.d.ts +0 -0
  137. /package/{types → dist}/url.d.ts +0 -0
  138. /package/{types → dist}/webcrypto.deno.d.ts +0 -0
  139. /package/{types → dist}/webcrypto.node.d.ts +0 -0
  140. /package/{types → dist}/webcrypto.react-native.d.ts +0 -0
  141. /package/{types → dist}/websocket.d.ts +0 -0
@@ -1,6 +1,21 @@
1
1
  /**
2
2
  * @beta this API is about to change
3
3
  *
4
+ * ## Consumer API
5
+ *
6
+ * This module exports a large surface, but a consumer only needs a handful of it:
7
+ * - **build / apply / inspect:** {@link create} (the constructor) and the {@link DeltaBuilder} methods
8
+ * it returns (`insert`/`delete`/`retain`/`modify`/`setAttr`/`addMark`/…, `apply`, `rebase`, `done`);
9
+ * {@link clone}, {@link slice}, {@link diff}, {@link inverse}, and `toJSON`/`equals`/`isEmpty` on the result.
10
+ * - **schemas:** {@link $delta} (define a typed delta schema) and {@link $deltaAny} (the catch-all).
11
+ * - **types:** `Delta`, `DeltaBuilder`, `DeltaAny`, `DeltaConf` for annotations.
12
+ *
13
+ * Everything else is `@internal` plumbing: the `*Op` classes and `$*Op` schema sentinels exist for the
14
+ * transformer layer's V8-stable dispatch (do not construct them); the `*RootMark*` helpers,
15
+ * {@link cloneShallow}, {@link random}, the merge helpers, and the remaining `$*` schemas are for
16
+ * cross-module / custom-transformer use, not application code. The other `DeltaConf*` typedefs are the
17
+ * inference machinery behind the fluent builder's typed chain — never named directly.
18
+ *
4
19
  * ## Mutability
5
20
  *
6
21
  * Deltas are mutable by default. But references are often shared, by marking a Delta as "done". You
@@ -22,8 +37,22 @@ import * as encoding from '../encoding.js'
22
37
  import * as buffer from '../buffer.js'
23
38
  import * as patience from '../diff/patience.js'
24
39
  import * as prng from '../prng.js'
40
+ import * as rand from '../random.js'
25
41
 
26
42
  /**
43
+ * Provenance metadata on a content/attr op: *who/what* inserted, deleted, or formatted it. The canonical
44
+ * shape below is a convention — apply/diff/equality treat attribution as an **opaque** object (never branching
45
+ * on these field names), so custom keys flow through — with ONE exception: the nested `format` key merges per
46
+ * inner key (one level), so applying `{format:{italic:[…]}}` *adds* italic while keeping a pre-existing
47
+ * `{format:{bold:[…]}}`, and `{format:{bold:null}}` removes just that inner key (an emptied `format` is
48
+ * dropped). Every other key is flat/wholesale. The `format`/`formatAt` part exists only on
49
+ * children (content ops), never on node attribute ops. As an *update value* on `retain`/`modify`/`*Attr`,
50
+ * attribution uses the same unified tri-state as {@link Formats} (`undefined` skip / `null`
51
+ * clear / `{k:v}` set / `{k:null}` remove) — see the delta `readme.md` "Formats & Attributions". The blanket
52
+ * `null`-clear channel caveat on {@link Formats} applies here too; note `rebase` does not
53
+ * reconcile attribution at all (concurrent attribution edits don't converge), so prefer deterministic
54
+ * attribution assignment outside of concurrent editing.
55
+ *
27
56
  * @typedef {{
28
57
  * insert?: string[]
29
58
  * insertAt?: number
@@ -47,7 +76,21 @@ export const $attribution = /* @__PURE__ */(() => s.$object({
47
76
  }))()
48
77
 
49
78
  /**
50
- * @typedef {{ [key: string]: any }} FormattingAttributes
79
+ * Rich-text formatting attributes (`{ bold: true, }`). As an *update value* on `retain`/`modify` it is a
80
+ * unified tri-state (identical to {@link Attribution}): `undefined`/omitted = skip, `null` = clear all,
81
+ * `{k:v}` = set, `{k:null}` = remove key — see the delta `readme.md` "Formats & Attributions".
82
+ *
83
+ * ⚠️ **Blanket `null` clear is a local-only utility — never send it over a channel.** A `null` clear carries
84
+ * no key information, so it cannot be reconciled key-by-key under `rebase`: a clear *always wins* a concurrent
85
+ * edit (priority-independent — that is what makes it converge), which silently DROPS the other side's set. For
86
+ * collaborative / transmitted data, clear keys individually with `{k:null}` removals instead — those reconcile
87
+ * by priority and converge without data loss (this is what {@link diff} emits and what the rebase fuzz uses).
88
+ *
89
+ * @typedef {{ [key: string]: any }} Formats
90
+ */
91
+
92
+ /**
93
+ * @typedef {{ id: string, key: number|string, assoc: 1|-1, attrs?: object }} MarkJSON
51
94
  */
52
95
 
53
96
  /**
@@ -55,16 +98,18 @@ export const $attribution = /* @__PURE__ */(() => s.$object({
55
98
  * type: 'delta',
56
99
  * name?: string,
57
100
  * attrs?: { [Key in string|number]: DeltaAttrOpJSON },
58
- * children?: Array<DeltaListOpJSON>
101
+ * children?: Array<DeltaListOpJSON>,
102
+ * marks?: Array<MarkJSON>,
103
+ * deleteMarks?: Array<string>
59
104
  * }} DeltaJSON
60
105
  */
61
106
 
62
107
  /**
63
- * @typedef {{ type: 'insert', insert: string|Array<any>, format?: { [key: string]: any }, attribution?: Attribution } | { delete: number } | { type: 'retain', retain: number, format?: { [key:string]: any }, attribution?: Attribution } | { type: 'modify', value: object }} DeltaListOpJSON
108
+ * @typedef {{ type: 'insert', insert: string|Array<any>, format?: { [key: string]: any }, attribution?: Attribution } | { delete: number } | { type: 'retain', retain: number, format?: { [key:string]: any }|null, attribution?: Attribution|null } | { type: 'modify', value: object, format?: { [key:string]: any }|null, attribution?: Attribution|null }} DeltaListOpJSON
64
109
  */
65
110
 
66
111
  /**
67
- * @typedef {{ type: 'insert', value: any, prevValue?: any, attribution?: Attribution } | { type: 'delete', prevValue?: any, attribution?: Attribution } | { type: 'modify', value: DeltaJSON }} DeltaAttrOpJSON
112
+ * @typedef {{ type: 'insert', value: any, attribution?: Attribution } | { type: 'delete', attribution?: Attribution } | { type: 'modify', value: DeltaJSON, attribution?: Attribution|null }} DeltaAttrOpJSON
68
113
  */
69
114
 
70
115
  /**
@@ -83,17 +128,182 @@ export const $attribution = /* @__PURE__ */(() => s.$object({
83
128
  * @type {s.Schema<DeltaAttrOpJSON>}
84
129
  */
85
130
  export const $deltaMapChangeJson = /* @__PURE__ */(() => s.$union(
86
- s.$object({ type: s.$literal('insert'), value: s.$any, prevValue: s.$any.optional, attribution: $attribution.optional }),
87
- s.$object({ type: s.$literal('modify'), value: s.$any }),
88
- s.$object({ type: s.$literal('delete'), prevValue: s.$any.optional, attribution: $attribution.optional })
131
+ s.$object({ type: s.$literal('insert'), value: s.$any, attribution: $attribution.optional }),
132
+ s.$object({ type: s.$literal('modify'), value: s.$any, attribution: $attribution.optional }),
133
+ s.$object({ type: s.$literal('delete'), attribution: $attribution.optional })
89
134
  ))()
90
135
 
91
136
  /**
92
- * @template {{[key:string]: any} | null} Attrs
137
+ * @template {{[key:string]: any} | null | undefined} Attrs
93
138
  * @param {Attrs} attrs
94
139
  * @return {Attrs}
95
140
  */
96
141
  const _cloneAttrs = attrs => attrs == null ? attrs : { ...attrs }
142
+ /**
143
+ * Shallow per-key tri-state merge of `update` into `base` (the usual `format`-dimension semantics, also reused
144
+ * as the inner step of {@link mergeAttr}). Per key: `undefined` skips, `null` removes, anything else sets.
145
+ * `resolve` true (data semantics) applies a `null` removal by deleting the key; false (instruction semantics)
146
+ * keeps the `null` verbatim so it still removes when the instruction is later applied. When `resolve` is true
147
+ * the `base` is canonicalised too — a `{k:null}` already present in `base` has nothing to clear on settled
148
+ * data, so it is dropped rather than copied through. (The used-context call sites pass pre-resolved
149
+ * companions — see {@link resolveUsedData} — so for them this is a no-op safety net; it matters for the
150
+ * apply/diff paths that merge instructions onto stored values.)
151
+ *
152
+ * @param {{[k:string]:any}|null|undefined} base
153
+ * @param {{[k:string]:any}} update
154
+ * @param {boolean} resolve
155
+ * @return {{[k:string]:any}}
156
+ */
157
+ const mergeShallow = (base, update, resolve) => {
158
+ const r = /** @type {{[k:string]:any}} */ ({})
159
+ // copy base; when resolving, drop any `{k:null}` removal already in base (settled data carries no `null` leaf)
160
+ if (s.$objectAny.check(base)) for (const k in base) { if (!resolve || base[k] !== null) r[k] = base[k] }
161
+ for (const k in update) {
162
+ const v = update[k]
163
+ if (v === undefined) continue // skip this key
164
+ else if (v !== null) r[k] = v // set this key
165
+ else if (resolve) delete r[k] // data: apply the removal
166
+ else r[k] = null // instruction: keep the `null` removal verbatim for later
167
+ }
168
+ return r
169
+ }
170
+ /**
171
+ * Merge for the `attribution` dimension: the usual shallow tri-state ({@link mergeShallow}) **except** the
172
+ * `format` key — attribution's one structured sub-field — is itself merged per inner key (one level only, same
173
+ * tri-state). This realises "first merge the formats, then merge the whole attribution". The nesting applies to
174
+ * the `format` key alone; every other attribution key (`insert`/`delete` arrays, the `*At` numbers, …) stays a
175
+ * leaf. An emptied `format` is dropped (a settled attribution never stores `format: {}`). When `resolve` is
176
+ * true the `base` is canonicalised too (see {@link mergeShallow}): a `{k:null}` leaf in `base`, and a
177
+ * `{innerK:null}` inside `base.format`, have nothing to clear on settled data, so they are dropped — an
178
+ * emptied `base.format` is removed. This collapses a `useAttribution({format:{bold:null}})`-style context on
179
+ * a data op instead of storing the removal.
180
+ *
181
+ * @param {{[k:string]:any}|null|undefined} base
182
+ * @param {{[k:string]:any}} update
183
+ * @param {boolean} resolve see {@link mergeShallow}
184
+ * @return {{[k:string]:any}}
185
+ */
186
+ const mergeAttr = (base, update, resolve) => {
187
+ // copy base; when resolving, canonicalise it — drop a `{k:null}` leaf and resolve `base.format`'s own
188
+ // inner removals (dropping an emptied `format`). On an instruction merge (`!resolve`) base is copied verbatim.
189
+ const r = /** @type {{[k:string]:any}} */ ({})
190
+ if (s.$objectAny.check(base)) {
191
+ for (const k in base) {
192
+ const bv = base[k]
193
+ if (resolve && bv === null) continue // drop a base leaf removal
194
+ if (resolve && k === 'format' && s.$objectAny.check(bv)) {
195
+ const f = mergeShallow(bv, {}, true) // resolve base.format's own `{innerK:null}` removals
196
+ if (!object.isEmpty(f)) r[k] = f // drop an emptied format
197
+ } else r[k] = bv
198
+ }
199
+ }
200
+ for (const k in update) {
201
+ const v = update[k]
202
+ if (v === undefined) continue // skip this key
203
+ else if (k === 'format' && s.$objectAny.check(v)) { // the one nested key: merge per inner key (one level)
204
+ const sub = mergeShallow(r[k], v, resolve)
205
+ if (object.isEmpty(sub)) delete r[k] // emptied format ⇒ drop the key
206
+ else r[k] = sub
207
+ } else if (v !== null) r[k] = v // set leaf
208
+ else if (resolve) delete r[k] // data: apply the removal
209
+ else r[k] = null // instruction: keep the `null` removal verbatim for later
210
+ }
211
+ return r
212
+ }
213
+ /**
214
+ * Resolve a retain's tri-state `format`/`attribution` instruction against an EMPTY base — used by a
215
+ * `final` (materializing) apply for a retain that extends beyond existing content: a `{k:null}` removal or
216
+ * a `null` clear has nothing to act on, so it collapses to `undefined` (skip); a set survives. `deep`
217
+ * selects the attribution merge (its nested `format` key merges one level, {@link mergeAttr}) vs. the
218
+ * shallow `format` merge ({@link mergeShallow}).
219
+ *
220
+ * @param {{[k:string]:any}|null|undefined} arg
221
+ * @param {boolean} deep
222
+ * @return {{[k:string]:any}|undefined}
223
+ */
224
+ const resolveBeyond = (arg, deep) => {
225
+ if (arg == null) return undefined // undefined = skip; null = clear-all with nothing to clear = no-op
226
+ const m = deep ? mergeAttr(undefined, arg, true) : mergeShallow(undefined, arg, true)
227
+ return object.isEmpty(m) ? undefined : m
228
+ }
229
+ /**
230
+ * Resolve a `used*` context (set via {@link DeltaBuilder#useFormats}/`useAttribution`) for **data**
231
+ * consumption: settled data stores canonical values only
232
+ * (no `null` leaves — see {@link combineData}), while the slot itself keeps removal instructions
233
+ * verbatim for the instruction ops it governs. Returns the SAME object when it is already canonical
234
+ * (the common case — provenance contexts carry no removals), so every data op under one context shares
235
+ * one interned object; otherwise strips the removals ONCE into a shared copy (`{}` collapses to `null`).
236
+ * Never called per op — {@link DeltaBuilder#_getUsedFormatsData} caches the result per context object.
237
+ *
238
+ * @param {{[k:string]:any}|null} used
239
+ * @param {boolean} deep `true` for the `attribution` dimension (its `format` key resolves one level
240
+ * deeper via {@link mergeAttr}); `false` for `format` (shallow, {@link mergeShallow})
241
+ * @return {{[k:string]:any}|null}
242
+ */
243
+ const resolveUsedData = (used, deep) => {
244
+ if (used === null) return null
245
+ let empty = true
246
+ let canonical = true
247
+ for (const k in used) {
248
+ empty = false
249
+ const v = used[k]
250
+ if (v === null) { canonical = false; break } // a top-level removal (incl. `format: null`)
251
+ if (deep && k === 'format' && s.$objectAny.check(v)) {
252
+ // mirror mergeAttr's resolve-mode base handling: an emptied `format` is dropped on data, and an
253
+ // inner `{k:null}` removal is stripped — either makes the context non-canonical for data ops
254
+ let formatCanonical = false // empty `format: {}` ⇒ non-canonical
255
+ for (const ik in v) {
256
+ if (v[ik] === null) { formatCanonical = false; break }
257
+ formatCanonical = true
258
+ }
259
+ if (!formatCanonical) { canonical = false; break }
260
+ }
261
+ }
262
+ if (empty) return null
263
+ if (canonical) return used
264
+ const m = deep ? mergeAttr(used, {}, true) : mergeShallow(used, {}, true)
265
+ return object.isEmpty(m) ? null : m
266
+ }
267
+ /**
268
+ * Combine a builder's `used*` context with a per-call format/attribution argument for an
269
+ * **instruction** op (`retain`/`modify`/`modifyAttr`) under the unified tri-state: `undefined` — or an
270
+ * empty object, an empty update — inherits the context BY IDENTITY (no traversal, no copy; contexts
271
+ * are interned); `null` clears (ignoring the context); a non-empty object merges over the context,
272
+ * keeping `{k:null}` removals verbatim so they still remove when the instruction is later applied.
273
+ * An empty result is a no-op → `undefined` (skip).
274
+ *
275
+ * @param {{[k:string]:any}|null} used the raw context slot (removal instructions intact)
276
+ * @param {{[k:string]:any}|null|undefined} arg
277
+ * @param {boolean} deep `true` for the `attribution` dimension (its `format` key merges per inner key via
278
+ * {@link mergeAttr}); `false` for `format` (shallow, {@link mergeShallow}).
279
+ * @return {{[k:string]:any}|null|undefined}
280
+ */
281
+ const combineInstr = (used, arg, deep) => {
282
+ if (arg === null) return null // explicit clear — must precede the isEmpty check (object.isEmpty(null) is true)
283
+ if (arg === undefined || object.isEmpty(arg)) return used === null || object.isEmpty(used) ? undefined : used
284
+ const r = deep ? mergeAttr(used, arg, false) : mergeShallow(used, arg, false)
285
+ return object.isEmpty(r) ? undefined : r
286
+ }
287
+ /**
288
+ * Combine for a **data** op (`insert`/`text`/`setAttr`/`deleteAttr`): the stored value is a canonical
289
+ * object or `null` ("none") — never a `{k:null}` removal, there is nothing to remove on settled data.
290
+ * `usedData` is the builder's RESOLVED context companion ({@link resolveUsedData}, cached by
291
+ * {@link DeltaBuilder#_getUsedFormatsData}) and thus already canonical (`null` or a non-empty
292
+ * no-`null` object), so `undefined` — or an empty object, an empty update — inherits it BY IDENTITY
293
+ * (no traversal, no copy); `null` clears; a non-empty object merges over it, resolving the argument's
294
+ * own `{k:null}` removals. `{}` results collapse to `null`.
295
+ *
296
+ * @param {{[k:string]:any}|null} usedData
297
+ * @param {{[k:string]:any}|null|undefined} arg
298
+ * @param {boolean} deep see {@link combineInstr}
299
+ * @return {{[k:string]:any}|null}
300
+ */
301
+ const combineData = (usedData, arg, deep) => {
302
+ if (arg === null) return null // explicit clear — must precede the isEmpty check (object.isEmpty(null) is true)
303
+ if (arg === undefined || object.isEmpty(arg)) return usedData
304
+ const r = deep ? mergeAttr(usedData, arg, true) : mergeShallow(usedData, arg, true)
305
+ return object.isEmpty(r) ? null : /** @type {{[k:string]:any}} */ (r)
306
+ }
97
307
  /**
98
308
  * @template {any} MaybeDelta
99
309
  * @param {MaybeDelta} maybeDelta
@@ -101,10 +311,28 @@ const _cloneAttrs = attrs => attrs == null ? attrs : { ...attrs }
101
311
  */
102
312
  const _markMaybeDeltaAsDone = maybeDelta => $deltaAny.check(maybeDelta) ? /** @type {MaybeDelta} */ (maybeDelta.done()) : maybeDelta
103
313
 
314
+ /**
315
+ * Invariants shared by all op classes below (TextOp, InsertOp, DeleteOp,
316
+ * RetainOp, ModifyOp, SetAttrOp, DeleteAttrOp, ModifyAttrOp):
317
+ *
318
+ * - **Only code inside `delta.js` may mutate op fields.** External consumers
319
+ * treat ops as immutable; structural fields are JSDoc-annotated `@readonly`
320
+ * to reinforce this. Mutation is permitted only while the owning Delta is
321
+ * not `done` — every builder entry point routes through `modDeltaCheck`
322
+ * to enforce this at runtime.
323
+ * - **Any mutation of a fingerprinted field MUST null `_fingerprint`.** The
324
+ * fingerprint is a lazy cache; if it has already been computed and the
325
+ * underlying data changes without invalidating it, every subsequent
326
+ * fingerprint read (and any `diff` / equality check that relies on it) is
327
+ * wrong. Fields covered: insert, delete, retain, format, attribution,
328
+ * value, key.
329
+ *
330
+ * @internal not part of the consumer API — a content op; build deltas via {@link create}/{@link DeltaBuilder}.
331
+ */
104
332
  export class TextOp extends list.ListNode {
105
333
  /**
106
334
  * @param {string} insert
107
- * @param {FormattingAttributes|null} format
335
+ * @param {Formats|null} format
108
336
  * @param {Attribution?} attribution
109
337
  */
110
338
  constructor (insert, format, attribution) {
@@ -117,7 +345,7 @@ export class TextOp extends list.ListNode {
117
345
  this.insert = insert
118
346
  /**
119
347
  * @readonly
120
- * @type {FormattingAttributes|null}
348
+ * @type {Formats|null}
121
349
  */
122
350
  this.format = format
123
351
  this.attribution = attribution
@@ -127,10 +355,6 @@ export class TextOp extends list.ListNode {
127
355
  this._fingerprint = null
128
356
  }
129
357
 
130
- get $type () {
131
- return $textOp
132
- }
133
-
134
358
  /**
135
359
  * @param {string} newVal
136
360
  */
@@ -156,6 +380,7 @@ export class TextOp extends list.ListNode {
156
380
  encoding.writeVarUint(encoder, 0) // textOp type: 0
157
381
  encoding.writeVarString(encoder, this.insert)
158
382
  encoding.writeAny(encoder, this.format)
383
+ encoding.writeAny(encoder, this.attribution)
159
384
  })))
160
385
  }
161
386
 
@@ -184,24 +409,34 @@ export class TextOp extends list.ListNode {
184
409
  * @param {TextOp} other
185
410
  */
186
411
  [equalityTrait.EqualityTraitSymbol] (other) {
187
- return fun.equalityDeep(this.insert, other.insert) && fun.equalityDeep(this.format, other.format) && fun.equalityDeep(this.attribution, other.attribution)
412
+ return $textOp.check(other) &&
413
+ fun.equalityDeep(this.insert, other.insert) &&
414
+ (
415
+ (object.isEmpty(this.format) && object.isEmpty(other.format)) || fun.equalityDeep(this.format, other.format)
416
+ ) &&
417
+ fun.equalityDeep(this.attribution, other.attribution)
188
418
  }
189
419
 
190
420
  /**
421
+ * @param {number} [start]
422
+ * @param {number} [end]
423
+ * @param {boolean} [_markAsDone] accepted for a uniform children-op `clone` signature; ignored (text
424
+ * holds no nested deltas to freeze).
191
425
  * @return {TextOp}
192
426
  */
193
- clone (start = 0, end = this.length) {
427
+ clone (start = 0, end = this.length, _markAsDone = true) {
194
428
  return new TextOp(this.insert.slice(start, end), _cloneAttrs(this.format), _cloneAttrs(this.attribution))
195
429
  }
196
430
  }
197
431
 
198
432
  /**
199
433
  * @template {any} ArrayContent
434
+ * @internal not part of the consumer API — a content op; build deltas via {@link create}/{@link DeltaBuilder}.
200
435
  */
201
436
  export class InsertOp extends list.ListNode {
202
437
  /**
203
438
  * @param {Array<ArrayContent>} insert
204
- * @param {FormattingAttributes|null} format
439
+ * @param {Formats|null} format
205
440
  * @param {Attribution?} attribution
206
441
  */
207
442
  constructor (insert, format, attribution) {
@@ -213,7 +448,7 @@ export class InsertOp extends list.ListNode {
213
448
  this.insert = insert
214
449
  /**
215
450
  * @readonly
216
- * @type {FormattingAttributes?}
451
+ * @type {Formats?}
217
452
  */
218
453
  this.format = format
219
454
  /**
@@ -227,18 +462,17 @@ export class InsertOp extends list.ListNode {
227
462
  this._fingerprint = null
228
463
  }
229
464
 
230
- get $type () {
231
- return $insertOp
232
- }
233
-
465
+ /* c8 ignore start */
234
466
  /**
235
- * @param {ArrayContent} newVal
467
+ * @param {ArrayContent} _newVal
236
468
  */
237
- _updateInsert (newVal) {
238
- // @ts-ignore
239
- this.insert = newVal
240
- this._fingerprint = null
469
+ _updateInsert (_newVal) {
470
+ // Mirror of TextOp._updateInsert; not currently called on InsertOp because
471
+ // adjacent inserts are merged in-place via `end.insert.push(...)`. Kept for
472
+ // parity with TextOp's API.
473
+ error.unexpectedCase() // throw if called
241
474
  }
475
+ /* c8 ignore stop */
242
476
 
243
477
  /**
244
478
  * @return {'insert'}
@@ -278,6 +512,7 @@ export class InsertOp extends list.ListNode {
278
512
  encoding.writeVarString(encoder, fingerprintTrait.fingerprint(/** @type {any} */ (ins)))
279
513
  })
280
514
  encoding.writeAny(encoder, this.format)
515
+ encoding.writeAny(encoder, this.attribution)
281
516
  })))
282
517
  }
283
518
 
@@ -305,19 +540,32 @@ export class InsertOp extends list.ListNode {
305
540
  * @param {InsertOp<ArrayContent>} other
306
541
  */
307
542
  [equalityTrait.EqualityTraitSymbol] (other) {
308
- return fun.equalityDeep(this.insert, other.insert) && fun.equalityDeep(this.format, other.format) && fun.equalityDeep(this.attribution, other.attribution)
543
+ return $insertOp.check(other) &&
544
+ fun.equalityDeep(this.insert, other.insert) &&
545
+ (
546
+ (object.isEmpty(this.format) && object.isEmpty(other.format)) || fun.equalityDeep(this.format, other.format)
547
+ ) &&
548
+ fun.equalityDeep(this.attribution, other.attribution)
309
549
  }
310
550
 
311
551
  /**
552
+ * @param {number} [start]
553
+ * @param {number} [end]
554
+ * @param {boolean} [markAsDone] freeze the cloned child deltas (the default — a shared clone must be
555
+ * immutable). Pass `false` only when the caller is the SOLE owner of the cloned range (a *move*, e.g.
556
+ * `splitHere` which `_splice`s the range out of the source), so the content can stay mutable and is
557
+ * not re-cloned on the next modify (see {@link modValue}).
312
558
  * @return {InsertOp<ArrayContent>}
313
559
  */
314
- clone (start = 0, end = this.length) {
315
- return new InsertOp(this.insert.slice(start, end).map(_markMaybeDeltaAsDone), _cloneAttrs(this.format), _cloneAttrs(this.attribution))
560
+ clone (start = 0, end = this.length, markAsDone = true) {
561
+ const insert = this.insert.slice(start, end)
562
+ return new InsertOp(markAsDone ? insert.map(_markMaybeDeltaAsDone) : insert, _cloneAttrs(this.format), _cloneAttrs(this.attribution))
316
563
  }
317
564
  }
318
565
 
319
566
  /**
320
567
  * @template {DeltaConf} [Conf={}]
568
+ * @internal not part of the consumer API — a content op; build deltas via {@link create}/{@link DeltaBuilder}.
321
569
  */
322
570
  export class DeleteOp extends list.ListNode {
323
571
  /**
@@ -326,20 +574,12 @@ export class DeleteOp extends list.ListNode {
326
574
  constructor (len) {
327
575
  super()
328
576
  this.delete = len
329
- /**
330
- * @type {Delta<Conf>?}
331
- */
332
- this.prevValue = null
333
577
  /**
334
578
  * @type {string|null}
335
579
  */
336
580
  this._fingerprint = null
337
581
  }
338
582
 
339
- get $type () {
340
- return $deleteOp
341
- }
342
-
343
583
  /**
344
584
  * @return {'delete'}
345
585
  */
@@ -348,7 +588,7 @@ export class DeleteOp extends list.ListNode {
348
588
  }
349
589
 
350
590
  get length () {
351
- return 0
591
+ return this.delete
352
592
  }
353
593
 
354
594
  get fingerprint () {
@@ -365,7 +605,6 @@ export class DeleteOp extends list.ListNode {
365
605
  * @param {number} len
366
606
  */
367
607
  _splice (_offset, len) {
368
- this.prevValue = /** @type {any} */ (this.prevValue ? slice(this.prevValue, _offset, len) : null)
369
608
  this._fingerprint = null
370
609
  this.delete -= len
371
610
  return this
@@ -382,24 +621,29 @@ export class DeleteOp extends list.ListNode {
382
621
  * @param {DeleteOp} other
383
622
  */
384
623
  [equalityTrait.EqualityTraitSymbol] (other) {
385
- return this.delete === other.delete
624
+ return $deleteOp.check(other) && this.delete === other.delete
386
625
  }
387
626
 
388
627
  /**
389
- * @param {number} start
390
- * @param {number} end
628
+ * @param {number} [start]
629
+ * @param {number} [end]
630
+ * @param {boolean} [_markAsDone] accepted for a uniform children-op `clone` signature; ignored (a
631
+ * delete holds no nested content).
391
632
  * @return {DeleteOp}
392
633
  */
393
- clone (start = 0, end = this.delete) {
634
+ clone (start = 0, end = this.delete, _markAsDone = true) {
394
635
  return new DeleteOp(end - start)
395
636
  }
396
637
  }
397
638
 
639
+ /**
640
+ * @internal not part of the consumer API — a content op; build deltas via {@link create}/{@link DeltaBuilder}.
641
+ */
398
642
  export class RetainOp extends list.ListNode {
399
643
  /**
400
644
  * @param {number} retain
401
- * @param {FormattingAttributes|null} format
402
- * @param {Attribution?} attribution
645
+ * @param {Formats|null|undefined} format tri-state: `undefined` skip / `null` clear / object merge
646
+ * @param {Attribution|null|undefined} attribution tri-state: `undefined` skip / `null` clear / object merge
403
647
  */
404
648
  constructor (retain, format, attribution) {
405
649
  super()
@@ -410,12 +654,12 @@ export class RetainOp extends list.ListNode {
410
654
  this.retain = retain
411
655
  /**
412
656
  * @readonly
413
- * @type {FormattingAttributes?}
657
+ * @type {Formats|null|undefined}
414
658
  */
415
659
  this.format = format
416
660
  /**
417
661
  * @readonly
418
- * @type {Attribution?}
662
+ * @type {Attribution|null|undefined}
419
663
  */
420
664
  this.attribution = attribution
421
665
  /**
@@ -424,10 +668,6 @@ export class RetainOp extends list.ListNode {
424
668
  this._fingerprint = null
425
669
  }
426
670
 
427
- get $type () {
428
- return $retainOp
429
- }
430
-
431
671
  /**
432
672
  * @return {'retain'}
433
673
  */
@@ -444,6 +684,7 @@ export class RetainOp extends list.ListNode {
444
684
  encoding.writeVarUint(encoder, 3) // retainOp type: 3
445
685
  encoding.writeVarUint(encoder, this.retain)
446
686
  encoding.writeAny(encoder, this.format)
687
+ encoding.writeAny(encoder, this.attribution)
447
688
  })))
448
689
  }
449
690
 
@@ -465,17 +706,30 @@ export class RetainOp extends list.ListNode {
465
706
  */
466
707
  toJSON () {
467
708
  const { retain, format, attribution } = this
468
- return object.assign({ type: /** @type {'retain'} */ ('retain'), retain }, format ? { format } : {}, attribution != null ? { attribution } : {})
709
+ // retain is an instruction op: emit `null` (clear) distinctly from `undefined` (skip / omitted)
710
+ return object.assign({ type: /** @type {'retain'} */ ('retain'), retain }, format !== undefined ? { format } : {}, attribution !== undefined ? { attribution } : {})
469
711
  }
470
712
 
471
713
  /**
472
714
  * @param {RetainOp} other
473
715
  */
474
716
  [equalityTrait.EqualityTraitSymbol] (other) {
475
- return this.retain === other.retain && fun.equalityDeep(this.format, other.format) && fun.equalityDeep(this.attribution, other.attribution)
717
+ return $retainOp.check(other) &&
718
+ this.retain === other.retain &&
719
+ (
720
+ (object.isEmpty(this.format) && object.isEmpty(other.format)) || fun.equalityDeep(this.format, other.format)
721
+ ) &&
722
+ fun.equalityDeep(this.attribution, other.attribution)
476
723
  }
477
724
 
478
- clone (start = 0, end = this.retain) {
725
+ /**
726
+ * @param {number} [start]
727
+ * @param {number} [end]
728
+ * @param {boolean} [_markAsDone] accepted for a uniform children-op `clone` signature; ignored (retain
729
+ * holds no nested deltas).
730
+ * @return {RetainOp}
731
+ */
732
+ clone (start = 0, end = this.retain, _markAsDone = true) {
479
733
  return new RetainOp(end - start, _cloneAttrs(this.format), _cloneAttrs(this.attribution))
480
734
  }
481
735
  }
@@ -484,12 +738,13 @@ export class RetainOp extends list.ListNode {
484
738
  * Delta that can be applied on a YType Embed
485
739
  *
486
740
  * @template {Delta} [DTypes=DeltaAny]
741
+ * @internal not part of the consumer API — a content op; build deltas via {@link create}/{@link DeltaBuilder}.
487
742
  */
488
743
  export class ModifyOp extends list.ListNode {
489
744
  /**
490
745
  * @param {DTypes} delta
491
- * @param {FormattingAttributes|null} format
492
- * @param {Attribution?} attribution
746
+ * @param {Formats|null|undefined} format tri-state: `undefined` skip / `null` clear / object merge
747
+ * @param {Attribution|null|undefined} attribution tri-state: `undefined` skip / `null` clear / object merge
493
748
  */
494
749
  constructor (delta, format, attribution) {
495
750
  super()
@@ -500,12 +755,12 @@ export class ModifyOp extends list.ListNode {
500
755
  this.value = delta
501
756
  /**
502
757
  * @readonly
503
- * @type {FormattingAttributes?}
758
+ * @type {Formats|null|undefined}
504
759
  */
505
760
  this.format = format
506
761
  /**
507
762
  * @readonly
508
- * @type {Attribution?}
763
+ * @type {Attribution|null|undefined}
509
764
  */
510
765
  this.attribution = attribution
511
766
  /**
@@ -514,10 +769,6 @@ export class ModifyOp extends list.ListNode {
514
769
  this._fingerprint = null
515
770
  }
516
771
 
517
- get $type () {
518
- return $modifyOp
519
- }
520
-
521
772
  /**
522
773
  * @return {'modify'}
523
774
  */
@@ -533,27 +784,21 @@ export class ModifyOp extends list.ListNode {
533
784
  * @type {DeltaBuilderAny}
534
785
  */
535
786
  get _modValue () {
536
- /**
537
- * @type {any}
538
- */
539
- const d = this.value
540
- this._fingerprint = null
541
- if (d.isDone) {
542
- // @ts-ignore
543
- return (this.value = clone(d))
544
- }
545
- return d
787
+ return modValue(this)
546
788
  }
547
789
 
548
790
  get fingerprint () {
549
- // don't cache fingerprint because we don't know when delta changes
550
791
  return this._fingerprint || (this._fingerprint = buffer.toBase64(encoding.encode(encoder => {
551
792
  encoding.writeVarUint(encoder, 4) // modifyOp type: 4
552
793
  encoding.writeVarString(encoder, this.value.fingerprint)
553
794
  encoding.writeAny(encoder, this.format)
795
+ encoding.writeAny(encoder, this.attribution)
554
796
  })))
555
797
  }
556
798
 
799
+ /* c8 ignore start */
800
+ // ModifyOp has length 1, so callers never pass offset>0 or len>0 — splitHere
801
+ // is a no-op for length-1 ops. Kept for the structural _splice contract.
557
802
  /**
558
803
  * Remove a part of the operation (similar to Array.splice)
559
804
  *
@@ -563,42 +808,57 @@ export class ModifyOp extends list.ListNode {
563
808
  _splice (_offset, _len) {
564
809
  return this
565
810
  }
811
+ /* c8 ignore stop */
566
812
 
567
813
  /**
568
814
  * @return {DeltaListOpJSON}
569
815
  */
570
816
  toJSON () {
571
817
  const { value, attribution, format } = this
572
- return object.assign({ type: /** @type {'modify'} */ ('modify'), value: value.toJSON() }, format ? { format } : {}, attribution != null ? { attribution } : {})
818
+ // modify is an instruction op: emit `null` (clear) distinctly from `undefined` (skip / omitted)
819
+ return object.assign(
820
+ { type: /** @type {'modify'} */ ('modify'), value: value.toJSON() },
821
+ format !== undefined ? { format } : {},
822
+ attribution !== undefined ? { attribution } : {}
823
+ )
573
824
  }
574
825
 
575
826
  /**
576
827
  * @param {ModifyOp<any>} other
577
828
  */
578
829
  [equalityTrait.EqualityTraitSymbol] (other) {
579
- return this.value[equalityTrait.EqualityTraitSymbol](other.value) && fun.equalityDeep(this.format, other.format) && fun.equalityDeep(this.attribution, other.attribution)
830
+ return $modifyOp.check(other) &&
831
+ this.value[equalityTrait.EqualityTraitSymbol](other.value) &&
832
+ (
833
+ (object.isEmpty(this.format) && object.isEmpty(other.format)) || fun.equalityDeep(this.format, other.format)
834
+ ) &&
835
+ fun.equalityDeep(this.attribution, other.attribution)
580
836
  }
581
837
 
582
838
  /**
839
+ * @param {number} [_start] ignored (a modify is atomic); accepted for a uniform children-op signature.
840
+ * @param {number} [_end] ignored.
841
+ * @param {boolean} [markAsDone] freeze the cloned value (default); `false` shares it mutable — see
842
+ * {@link InsertOp#clone}. The op itself is still a fresh node; only its `value` is consumed.
583
843
  * @return {ModifyOp<DTypes>}
584
844
  */
585
- clone () {
586
- return new ModifyOp(/** @type {DTypes} */ (this.value.done()), _cloneAttrs(this.format), _cloneAttrs(this.attribution))
845
+ clone (_start = 0, _end = 1, markAsDone = true) {
846
+ return new ModifyOp(/** @type {DTypes} */ (markAsDone ? this.value.done() : this.value), _cloneAttrs(this.format), _cloneAttrs(this.attribution))
587
847
  }
588
848
  }
589
849
 
590
850
  /**
591
851
  * @template {any} [V=any]
592
852
  * @template {string|number} [K=any]
853
+ * @internal not part of the consumer API — an attribute op; build deltas via {@link create}/{@link DeltaBuilder}.
593
854
  */
594
855
  export class SetAttrOp {
595
856
  /**
596
857
  * @param {K} key
597
858
  * @param {V} value
598
- * @param {V|undefined} prevValue
599
859
  * @param {Attribution?} attribution
600
860
  */
601
- constructor (key, value, prevValue, attribution) {
861
+ constructor (key, value, attribution) {
602
862
  /**
603
863
  * @readonly
604
864
  * @type {K}
@@ -609,11 +869,6 @@ export class SetAttrOp {
609
869
  * @type {V}
610
870
  */
611
871
  this.value = value
612
- /**
613
- * @readonly
614
- * @type {V|undefined}
615
- */
616
- this.prevValue = prevValue
617
872
  /**
618
873
  * @readonly
619
874
  * @type {Attribution?}
@@ -625,10 +880,6 @@ export class SetAttrOp {
625
880
  this._fingerprint = null
626
881
  }
627
882
 
628
- get $type () {
629
- return $setAttrOp
630
- }
631
-
632
883
  /**
633
884
  * @return {'insert'}
634
885
  */
@@ -638,16 +889,7 @@ export class SetAttrOp {
638
889
  * @type {DeltaBuilderAny}
639
890
  */
640
891
  get _modValue () {
641
- /**
642
- * @type {any}
643
- */
644
- const v = this.value
645
- this._fingerprint = null
646
- if ($deltaAny.check(v) && v.isDone) {
647
- // @ts-ignore
648
- return (this.value = clone(v))
649
- }
650
- return v
892
+ return modValue(this)
651
893
  }
652
894
 
653
895
  get fingerprint () {
@@ -661,53 +903,55 @@ export class SetAttrOp {
661
903
  encoding.writeUint8(encoder, 1)
662
904
  encoding.writeAny(encoder, /** @type {any} */ (this.value))
663
905
  }
906
+ encoding.writeAny(encoder, this.attribution)
664
907
  })))
665
908
  }
666
909
 
667
910
  toJSON () {
668
911
  const v = this.value
669
912
  const attribution = this.attribution
670
- return object.assign({
913
+ return object.assign(
914
+ {
671
915
  type: this.type,
672
916
  value: $deltaAny.check(v) ? v.toJSON() : v
673
917
  },
674
918
  attribution != null ? { attribution } : {}
675
- )}
919
+ )
920
+ }
676
921
 
677
922
  /**
678
923
  * @param {SetAttrOp<V>} other
679
924
  */
680
925
  [equalityTrait.EqualityTraitSymbol] (other) {
681
- return this.key === other.key && fun.equalityDeep(this.value, other.value) && fun.equalityDeep(this.attribution, other.attribution)
926
+ return $setAttrOp.check(other) && this.key === other.key && fun.equalityDeep(this.value, other.value) && fun.equalityDeep(this.attribution, other.attribution)
682
927
  }
683
928
 
684
929
  /**
930
+ * Full (frozen) clone. A `move` apply reuses the source op instead of cloning, so there is no
931
+ * shared-mutable variant — see {@link DeltaBuilder#apply}.
932
+ *
685
933
  * @return {SetAttrOp<V,K>}
686
934
  */
687
935
  clone () {
688
- return new SetAttrOp(this.key, _markMaybeDeltaAsDone(this.value), _markMaybeDeltaAsDone(this.prevValue), _cloneAttrs(this.attribution))
936
+ return new SetAttrOp(this.key, _markMaybeDeltaAsDone(this.value), _cloneAttrs(this.attribution))
689
937
  }
690
938
  }
691
939
 
692
940
  /**
693
941
  * @template [V=any]
694
942
  * @template {string|number} [K=string|number]
943
+ * @internal not part of the consumer API — an attribute op; build deltas via {@link create}/{@link DeltaBuilder}.
695
944
  */
696
945
  export class DeleteAttrOp {
697
946
  /**
698
947
  * @param {K} key
699
- * @param {V|undefined} prevValue
700
948
  * @param {Attribution?} attribution
701
949
  */
702
- constructor (key, prevValue, attribution) {
950
+ constructor (key, attribution) {
703
951
  /**
704
952
  * @type {K}
705
953
  */
706
954
  this.key = key
707
- /**
708
- * @type {V|undefined}
709
- */
710
- this.prevValue = prevValue
711
955
  this.attribution = attribution
712
956
  /**
713
957
  * @type {string|null}
@@ -715,10 +959,6 @@ export class DeleteAttrOp {
715
959
  this._fingerprint = null
716
960
  }
717
961
 
718
- get $type () {
719
- return $deleteAttrOp
720
- }
721
-
722
962
  /**
723
963
  * @type {'delete'}
724
964
  */
@@ -730,6 +970,7 @@ export class DeleteAttrOp {
730
970
  return this._fingerprint || (this._fingerprint = buffer.toBase64(encoding.encode(encoder => {
731
971
  encoding.writeVarUint(encoder, 6) // map delete type: 6
732
972
  encoding.writeAny(encoder, this.key)
973
+ encoding.writeAny(encoder, this.attribution)
733
974
  })))
734
975
  }
735
976
 
@@ -738,7 +979,7 @@ export class DeleteAttrOp {
738
979
  */
739
980
  toJSON () {
740
981
  const {
741
- type, attribution,
982
+ type, attribution
742
983
  } = this
743
984
  return object.assign(
744
985
  { type },
@@ -750,24 +991,31 @@ export class DeleteAttrOp {
750
991
  * @param {DeleteAttrOp<V>} other
751
992
  */
752
993
  [equalityTrait.EqualityTraitSymbol] (other) {
753
- return this.key === other.key && fun.equalityDeep(this.attribution, other.attribution)
994
+ return $deleteAttrOp.check(other) && this.key === other.key && fun.equalityDeep(this.attribution, other.attribution)
754
995
  }
755
996
 
997
+ /**
998
+ * Full (frozen) clone; a `move` apply reuses the source op instead — see {@link DeltaBuilder#apply}.
999
+ *
1000
+ * @return {DeleteAttrOp<V,K>}
1001
+ */
756
1002
  clone () {
757
- return new DeleteAttrOp(this.key, _markMaybeDeltaAsDone(this.prevValue), _cloneAttrs(this.attribution))
1003
+ return new DeleteAttrOp(this.key, _cloneAttrs(this.attribution))
758
1004
  }
759
1005
  }
760
1006
 
761
1007
  /**
762
1008
  * @template {DeltaAny} [Modifier=DeltaAny]
763
1009
  * @template {string|number} [K=string]
1010
+ * @internal not part of the consumer API — an attribute op; build deltas via {@link create}/{@link DeltaBuilder}.
764
1011
  */
765
1012
  export class ModifyAttrOp {
766
1013
  /**
767
1014
  * @param {K} key
768
1015
  * @param {Modifier} delta
1016
+ * @param {Attribution|null|undefined} attribution tri-state: `undefined` skip / `null` clear / object merge
769
1017
  */
770
- constructor (key, delta) {
1018
+ constructor (key, delta, attribution) {
771
1019
  /**
772
1020
  * @readonly
773
1021
  * @type {K}
@@ -778,16 +1026,17 @@ export class ModifyAttrOp {
778
1026
  * @type {Modifier}
779
1027
  */
780
1028
  this.value = delta
1029
+ /**
1030
+ * @readonly
1031
+ * @type {Attribution|null|undefined}
1032
+ */
1033
+ this.attribution = attribution
781
1034
  /**
782
1035
  * @type {string|null}
783
1036
  */
784
1037
  this._fingerprint = null
785
1038
  }
786
1039
 
787
- get $type () {
788
- return $modifyAttrOp
789
- }
790
-
791
1040
  /**
792
1041
  * @type {'modify'}
793
1042
  */
@@ -798,6 +1047,7 @@ export class ModifyAttrOp {
798
1047
  encoding.writeVarUint(encoder, 7) // map modify type: 7
799
1048
  encoding.writeAny(encoder, this.key)
800
1049
  encoding.writeVarString(encoder, this.value.fingerprint)
1050
+ encoding.writeAny(encoder, this.attribution)
801
1051
  })))
802
1052
  }
803
1053
 
@@ -805,50 +1055,51 @@ export class ModifyAttrOp {
805
1055
  * @return {DeltaBuilder}
806
1056
  */
807
1057
  get _modValue () {
808
- this._fingerprint = null
809
- if (this.value.isDone) {
810
- // @ts-ignore
811
- this.value = /** @type {any} */ (clone(this.value))
812
- }
813
- // @ts-ignore
814
- return this.value
1058
+ return modValue(this)
815
1059
  }
816
1060
 
817
1061
  /**
818
1062
  * @return {DeltaAttrOpJSON}
819
1063
  */
820
1064
  toJSON () {
821
- return {
822
- type: this.type,
823
- value: this.value.toJSON()
824
- }
1065
+ const attribution = this.attribution
1066
+ return object.assign(
1067
+ {
1068
+ type: this.type,
1069
+ value: this.value.toJSON()
1070
+ },
1071
+ // modifyAttr applies its attribution as an instruction: emit `null` (clear) distinctly from skip
1072
+ attribution !== undefined ? { attribution } : {}
1073
+ )
825
1074
  }
826
1075
 
827
1076
  /**
828
1077
  * @param {ModifyAttrOp<Modifier>} other
829
1078
  */
830
1079
  [equalityTrait.EqualityTraitSymbol] (other) {
831
- return this.key === other.key && this.value[equalityTrait.EqualityTraitSymbol](other.value)
1080
+ return $modifyAttrOp.check(other) && this.key === other.key && this.value[equalityTrait.EqualityTraitSymbol](other.value) && fun.equalityDeep(this.attribution, other.attribution)
832
1081
  }
833
1082
 
834
1083
  /**
1084
+ * Full (frozen) clone; a `move` apply reuses the source op instead — see {@link DeltaBuilder#apply}.
1085
+ *
835
1086
  * @return {ModifyAttrOp<Modifier,K>}
836
1087
  */
837
1088
  clone () {
838
- return new ModifyAttrOp(this.key, /** @type {Modifier} */ (this.value.done()))
1089
+ return new ModifyAttrOp(this.key, /** @type {Modifier} */ (this.value.done()), _cloneAttrs(this.attribution))
839
1090
  }
840
1091
  }
841
1092
 
842
- export const $insertOp = /** @type {s.Schema<InsertOp<any>>} */ (s.$type('insertOp'))
843
- export const $modifyOp = /** @type {s.Schema<ModifyOp>} */ (s.$type('modifyOp'))
844
- export const $textOp = /** @type {s.Schema<TextOp>} */ (s.$type('textOp'))
845
- export const $deleteOp = /** @type {s.Schema<DeleteOp<any>>} */ (s.$type('deleteOp'))
846
- export const $retainOp = /** @type {s.Schema<RetainOp>} */ (s.$type('retainOp'))
1093
+ export const $insertOp = /** @type {s.Schema<InsertOp<any>>} */ (InsertOp.prototype.$type = /** @type {s.Schema<InsertOp<any>>} */ (s.$type('d:insertOp', InsertOp)))
1094
+ export const $modifyOp = ModifyOp.prototype.$type = s.$type('d:modifyOp', ModifyOp)
1095
+ export const $textOp = TextOp.prototype.$type = s.$type('d:textOp', TextOp)
1096
+ export const $deleteOp = /** @type {s.Schema<DeleteOp<any>>} */ (DeleteOp.prototype.$type = s.$type('d:deleteOp', DeleteOp))
1097
+ export const $retainOp = RetainOp.prototype.$type = s.$type('d:retainOp', RetainOp)
847
1098
  export const $anyOp = s.$union($insertOp, $deleteOp, $textOp, $modifyOp)
848
1099
 
849
- export const $setAttrOp = /** @type {s.Schema<SetAttrOp<any>>} */ (s.$type('setAttrOp'))
850
- export const $modifyAttrOp = /** @type {s.Schema<ModifyAttrOp<any,string|number>>} */ (s.$type('modifyAttrOp'))
851
- export const $deleteAttrOp = /** @type {s.Schema<DeleteAttrOp<any,string|number>>} */ (s.$type('deleteAttrOp'))
1100
+ export const $setAttrOp = /** @type {s.Schema<SetAttrOp<any>>} */ (SetAttrOp.prototype.$type =/** @type {s.Schema<SetAttrOp<any>>} */ (s.$type('d:setAttrOp', SetAttrOp)))
1101
+ export const $modifyAttrOp = /** @type {s.Schema<ModifyAttrOp<any>>} */ (ModifyAttrOp.prototype.$type = /** @type {s.Schema<ModifyAttrOp<any>>} */ (s.$type('d:modifyAttrOp', ModifyAttrOp)))
1102
+ export const $deleteAttrOp = /** @type {s.Schema<DeleteAttrOp<any>>} */ (DeleteAttrOp.prototype.$type = /** @type {s.Schema<DeleteAttrOp<any>>} */ (s.$type('d:deleteAttrOp', DeleteAttrOp)))
852
1103
  export const $anyAttrOp = s.$union($setAttrOp, $deleteAttrOp, $modifyAttrOp)
853
1104
 
854
1105
  /**
@@ -863,7 +1114,7 @@ export const $setAttrOpWith = $content => s.$custom(o => $setAttrOp.check(o) &&
863
1114
  * @param {s.Schema<Content>} $content
864
1115
  * @return {s.Schema<InsertOp<Content>>}
865
1116
  */
866
- export const $insertOpWith = $content => s.$custom(o => $insertOp.check(o) && $content.check(o.insert.every(ins => $content.check(ins))))
1117
+ export const $insertOpWith = $content => s.$custom(o => $insertOp.check(o) && o.insert.every(ins => $content.check(ins)))
867
1118
 
868
1119
  /**
869
1120
  * @template {DeltaAny} Modify
@@ -892,6 +1143,94 @@ export const $modifyAttrOpWith = $content => s.$custom(o => $modifyAttrOp.check(
892
1143
  * @typedef {{ [K in (keyof NewAttrs | keyof Attrs)]: (unknown extends Attrs[K] ? never : Attrs[K]) | (unknown extends NewAttrs[K] ? never : NewAttrs[K]) }} MergeAttrs
893
1144
  */
894
1145
 
1146
+ /**
1147
+ * A cursor/selection anchor stored inside a delta tree (see {@link import('./position.js').marksToPositions}).
1148
+ *
1149
+ * - `key` — the *terminal* step of the position: a content offset (number) or an attribute key
1150
+ * (string) within the node that holds the mark.
1151
+ * - `id` — a unique, user-defined identifier.
1152
+ * - `assoc` — the gravity at a boundary (left `-1` / right `1`).
1153
+ * - `attrs` — optional user-supplied data carried with the mark **by reference**: the same
1154
+ * object is shared across the mark, its `copy`/`clone`s, `toJSON`, and every `MarkPos` from
1155
+ * `marksToPositions`, so the caller must treat it as immutable (do not mutate it after attaching).
1156
+ *
1157
+ * A `Mark` is **immutable** — never mutate one in place; to "move" a mark, replace it with a fresh
1158
+ * `Mark` via {@link Mark#copy} (the {@link Marks} set keys by id, so re-adding the same id replaces
1159
+ * it). Immutability lets a `Mark` be shared freely across a delta and its clones. The class is not
1160
+ * exported — construct via {@link createMark} and validate via {@link $mark}.
1161
+ */
1162
+ class Mark {
1163
+ /**
1164
+ * @param {number|string} key
1165
+ * @param {string} [id] unique id; defaults to a fresh GUID
1166
+ * @param {1|-1} [assoc] gravity at a boundary; defaults to right (`1`)
1167
+ * @param {object?} [attrs] optional user data, stored by reference; treat as immutable
1168
+ */
1169
+ constructor (key, id = rand.uuidv4(), assoc = 1, attrs = null) {
1170
+ /**
1171
+ * @readonly
1172
+ * @type {number|string}
1173
+ */
1174
+ this.key = key
1175
+ /**
1176
+ * @readonly
1177
+ * @type {string}
1178
+ */
1179
+ this.id = id
1180
+ /**
1181
+ * @readonly
1182
+ * @type {1|-1}
1183
+ */
1184
+ this.assoc = assoc
1185
+ /**
1186
+ * @readonly
1187
+ * @type {object?}
1188
+ */
1189
+ this.attrs = attrs
1190
+ }
1191
+
1192
+ /**
1193
+ * A copy of this mark, optionally at a different `key` (used to "move" an otherwise-immutable mark).
1194
+ *
1195
+ * @param {number|string} [key]
1196
+ * @return {Mark}
1197
+ */
1198
+ copy (key = this.key) {
1199
+ return new Mark(key, this.id, this.assoc, this.attrs)
1200
+ }
1201
+
1202
+ /**
1203
+ * @return {MarkJSON}
1204
+ */
1205
+ toJSON () {
1206
+ return this.attrs === null
1207
+ ? { id: this.id, key: this.key, assoc: this.assoc }
1208
+ : { id: this.id, key: this.key, assoc: this.assoc, attrs: this.attrs }
1209
+ }
1210
+
1211
+ /**
1212
+ * @param {Mark} other
1213
+ */
1214
+ [equalityTrait.EqualityTraitSymbol] (other) {
1215
+ return $mark.check(other) && this.id === other.id && this.key === other.key && this.assoc === other.assoc && fun.equalityDeep(this.attrs, other.attrs)
1216
+ }
1217
+ }
1218
+
1219
+ export const $mark = /** @type {s.Schema<Mark>} */ (Mark.prototype.$type = s.$type('d:mark', Mark))
1220
+
1221
+ /**
1222
+ * Create a {@link Mark} (use this instead of `new Mark(...)`). `id` defaults to a fresh GUID, `assoc`
1223
+ * to right gravity (`1`), `attrs` to `null`. A `Mark` is stored on a delta node's own
1224
+ * {@link Marks} set (see {@link DeltaBuilder#addMark}).
1225
+ *
1226
+ * @param {number|string} key
1227
+ * @param {string} [id]
1228
+ * @param {1|-1} [assoc]
1229
+ * @param {object?} [attrs]
1230
+ * @return {Mark}
1231
+ */
1232
+ export const createMark = (key, id, assoc, attrs) => new Mark(key, id, assoc, attrs)
1233
+
895
1234
  /**
896
1235
  * @typedef {Delta<any>} DeltaAny
897
1236
  */
@@ -910,6 +1249,16 @@ export const $modifyAttrOpWith = $content => s.$custom(o => $modifyAttrOp.check(
910
1249
  * @property {boolean} [DeltaConf.recursiveAttrs=false]
911
1250
  */
912
1251
 
1252
+ /**
1253
+ * Coerce a computed conf to provably satisfy the `DeltaConf` constraint. Declaration emit re-checks
1254
+ * type arguments eagerly, so a deeply-computed conf (mapped/recursive) passed to `Delta<…>` /
1255
+ * `Transformer<…>` must be wrapped here: the conditional exposes `DeltaConf` as its upper bound,
1256
+ * letting the constraint resolve without forcing TS to expand the (self-referential) body.
1257
+ *
1258
+ * @template T
1259
+ * @typedef {T extends infer DC extends DeltaConf ? DC : never} AsDeltaConf
1260
+ */
1261
+
913
1262
  /**
914
1263
  * @template {DeltaConf} Conf
915
1264
  * @typedef {Conf extends {name:infer Name} ? (unknown extends Name ? any : (Exclude<Name,undefined>)) : any} DeltaConfGetName
@@ -1004,6 +1353,9 @@ class DeltaData {
1004
1353
  * >}
1005
1354
  */
1006
1355
  this.children = /** @type {any} */ (list.create())
1356
+ /**
1357
+ * All child-ops sizes combined. Note that delete-ops also have a length
1358
+ */
1007
1359
  this.childCnt = 0
1008
1360
  /**
1009
1361
  * @type {any}
@@ -1014,6 +1366,39 @@ class DeltaData {
1014
1366
  */
1015
1367
  this._fingerprint = null
1016
1368
  this.isDone = false
1369
+ /**
1370
+ * Is the final document and does not contain delete, modify, or retain ops.
1371
+ */
1372
+ this.isFinal = false
1373
+ /**
1374
+ * Leaf {@link Mark marks} whose cursor sits in THIS node (in a settled delta), or the marks to ADD
1375
+ * to the target node (in a change delta). Local/ephemeral cursor state — NOT part of the document
1376
+ * fingerprint or equality. `null` when there are none. See {@link Marks}.
1377
+ *
1378
+ * @type {Marks?}
1379
+ */
1380
+ this.marks = null
1381
+ /**
1382
+ * Mark ids to DELETE (tombstones). Only present in a (non-settled) change delta — like a
1383
+ * `DeleteAttrOp`, a deletion is dropped from a final delta (a materialized document carries none, at
1384
+ * any depth). `null` when there are none. Marks follow *last-writer-wins*, treating each op as an
1385
+ * absolute per-id assignment (present vs absent): adding a mark cancels a pending delete of the same
1386
+ * id ({@link addMarkTo}) and a delete cancels a present add ({@link deleteMarkTo}), so a node never
1387
+ * holds both an add and a delete for one id. {@link deleteMarkTo} is the sole writer of this set
1388
+ * (and `addMarkTo`'s mirror); see {@link applyMarkOps}.
1389
+ *
1390
+ * @type {Set<string>?}
1391
+ */
1392
+ this.deleteMarks = null
1393
+ /**
1394
+ * Conservative "this subtree might hold a {@link Mark}" flag (own {@link DeltaData#marks} or any
1395
+ * descendant's). Set `true` when a mark is introduced and OR-propagated up to every ancestor; it is
1396
+ * **never** decremented (removal leaves it conservatively `true`). {@link
1397
+ * import('./position.js').marksToPositions} is the sole resetter: it prunes subtrees where this is
1398
+ * `false` (a guaranteed-empty subtree) and lazily clears a stale `true` to `false` when a descended
1399
+ * subtree turns out to hold none. `false` therefore *guarantees* no marks; `true` only *maybe*.
1400
+ */
1401
+ this.maybeHasMarks = false
1017
1402
  }
1018
1403
  }
1019
1404
 
@@ -1027,12 +1412,11 @@ class DeltaData {
1027
1412
  * >}
1028
1413
  */
1029
1414
  export class Delta extends DeltaData {
1030
- get $type () { return $deltaAny }
1031
1415
  /**
1032
1416
  * @type {string}
1033
1417
  */
1034
1418
  get fingerprint () {
1035
- return this._fingerprint || (this._fingerprint = buffer.toBase64(encoding.encode(encoder => {
1419
+ return this._fingerprint || (this._fingerprint = buffer.toBase64(rabin.fingerprint(rabin.StandardIrreducible32, encoding.encode(encoder => {
1036
1420
  encoding.writeUint32(encoder, 0xf2ae5680) // "magic number" that ensures that different types of content don't yield the same fingerprint
1037
1421
  encoding.writeAny(encoder, this.name)
1038
1422
  /**
@@ -1059,8 +1443,7 @@ export class Delta extends DeltaData {
1059
1443
  for (const child of this.children) {
1060
1444
  encoding.writeVarString(encoder, child.fingerprint)
1061
1445
  }
1062
- return buffer.toBase64(rabin.fingerprint(rabin.StandardIrreducible128, encoding.toUint8Array(encoder)))
1063
- })))
1446
+ }))))
1064
1447
  }
1065
1448
 
1066
1449
  [fingerprintTrait.FingerprintTraitSymbol] () {
@@ -1068,7 +1451,7 @@ export class Delta extends DeltaData {
1068
1451
  }
1069
1452
 
1070
1453
  isEmpty () {
1071
- return object.isEmpty(this.attrs) && list.isEmpty(this.children)
1454
+ return object.isEmpty(this.attrs) && list.isEmpty(this.children) && (this.marks === null || this.marks.size === 0) && (this.deleteMarks === null || this.deleteMarks.size === 0)
1072
1455
  }
1073
1456
 
1074
1457
  /**
@@ -1094,7 +1477,9 @@ export class Delta extends DeltaData {
1094
1477
  { type: /** @type {'delta'} */ ('delta') },
1095
1478
  (name != null ? { name } : {}),
1096
1479
  (object.isEmpty(attrs) ? {} : { attrs }),
1097
- (children.length > 0 ? { children } : {})
1480
+ (children.length > 0 ? { children } : {}),
1481
+ (this.marks !== null && this.marks.size > 0 ? { marks: [...this.marks].sort(compareMarksById).map(m => m.toJSON()) } : {}),
1482
+ (this.deleteMarks !== null && this.deleteMarks.size > 0 ? { deleteMarks: [...this.deleteMarks].sort() } : {})
1098
1483
  )
1099
1484
  }
1100
1485
 
@@ -1113,6 +1498,7 @@ export class Delta extends DeltaData {
1113
1498
  [equalityTrait.EqualityTraitSymbol] (other) {
1114
1499
  // @todo it is only necessary to compare finrerprints OR do a deep equality check (remove
1115
1500
  // childCnt as well)
1501
+ // marks are local/ephemeral cursor state and intentionally NOT part of document identity
1116
1502
  return this.name === other.name && fun.equalityDeep(this.attrs, other.attrs) && fun.equalityDeep(this.children, other.children) && this.childCnt === other.childCnt
1117
1503
  }
1118
1504
 
@@ -1142,7 +1528,7 @@ export class Delta extends DeltaData {
1142
1528
  if (!this.isDone) {
1143
1529
  this.isDone = markAsDone
1144
1530
  const cs = this.children
1145
- for (let end = cs.end; end !== null && $retainOp.check(end) && end.format == null && end.attribution == null; end = cs.end) {
1531
+ for (let end = cs.end; end !== null && $retainOp.check(end) && end.format === undefined && end.attribution === undefined; end = cs.end) {
1146
1532
  this.childCnt -= end.length
1147
1533
  list.popEnd(cs)
1148
1534
  }
@@ -1162,6 +1548,7 @@ export class Delta extends DeltaData {
1162
1548
  export const slice = (d, start = 0, end = d.childCnt, currNode = d.children.start) => {
1163
1549
  const cpy = /** @type {DeltaAny} */ (new DeltaBuilder(d.name, d.$schema))
1164
1550
  cpy.origin = d.origin
1551
+ const sliceStart = start // `start` is mutated by the walk below; keep the original for clamping marks
1165
1552
  // copy attrs
1166
1553
  for (const op of d.attrs) {
1167
1554
  // @ts-ignore
@@ -1196,6 +1583,31 @@ export const slice = (d, start = 0, end = d.childCnt, currNode = d.children.star
1196
1583
  remainingLen -= math.min(currNode.length, remainingLen)
1197
1584
  }
1198
1585
  cpy.childCnt = slicedLen - remainingLen
1586
+ // copy marks (only if the subtree maybe has any). A full clone (the `clone` path) copies this node's
1587
+ // own marks verbatim — a change delta's number-keyed root marks point into the TARGET's coordinate
1588
+ // and may sit beyond the change's own (often empty) content, so they must not be clamped. A genuine
1589
+ // partial slice instead clamps number-keyed marks to the sliced range (rebasing `key -= start`).
1590
+ // String/attr marks ride with the copied attrs; child marks ride on the cloned children. The
1591
+ // `maybeHasMarks` flag is copied (conservative); a change-only deleteMarks list is copied wholesale.
1592
+ if (d.maybeHasMarks) {
1593
+ if (d.marks !== null) {
1594
+ const fullCopy = sliceStart === 0 && end >= d.childCnt
1595
+ const cpyMarks = new Marks()
1596
+ for (const m of d.marks) {
1597
+ if (fullCopy || typeof m.key !== 'number') {
1598
+ cpyMarks.add(m)
1599
+ } else if (m.key >= sliceStart && m.key <= end) {
1600
+ cpyMarks.add(m.copy(m.key - sliceStart))
1601
+ }
1602
+ }
1603
+ if (cpyMarks.size > 0) cpy.marks = cpyMarks
1604
+ }
1605
+ cpy.maybeHasMarks = true
1606
+ }
1607
+ // a bulk copy of an already-disjoint source onto a fresh `cpy` (not a conflict-resolving write, so it
1608
+ // bypasses the deleteMarkTo/addMarkTo primitives): `d`'s marks/deleteMarks are disjoint by id, and
1609
+ // clamping only ever drops marks, so `cpy` stays disjoint
1610
+ if (d.deleteMarks !== null) cpy.deleteMarks = new Set(d.deleteMarks)
1199
1611
  // @ts-ignore
1200
1612
  return cpy
1201
1613
  }
@@ -1208,20 +1620,186 @@ export const slice = (d, start = 0, end = d.childCnt, currNode = d.children.star
1208
1620
  export const clone = d => /** @type {any} */ (slice(d, 0, d.childCnt))
1209
1621
 
1210
1622
  /**
1211
- * Try merging this op with the previous op
1212
- * @param {list.List<any>} parent
1623
+ * Deep-clone a value that may be a nested delta: a fresh {@link cloneDeep} of it when it is a delta,
1624
+ * otherwise the value unchanged (plain JSON content is treated as immutable and shared).
1625
+ *
1626
+ * @param {any} v
1627
+ * @return {any}
1628
+ */
1629
+ const _cloneMaybeDeltaDeep = v => $deltaAny.check(v) ? cloneDeep(v) : v
1630
+
1631
+ /**
1632
+ * Deep-clone one content (child) op for {@link cloneDeep}: a fresh op whose nested deltas (an insert's
1633
+ * delta content, a modify's value) are themselves deep-cloned. `format`/`attribution` objects are
1634
+ * retained (shared) — they are always copied before being mutated (see {@link cloneDeep}). Ops without a
1635
+ * nested delta (`text`/`retain`/`delete`) use `op.clone()`.
1636
+ *
1637
+ * @param {ChildrenOpAny} op
1638
+ * @return {ChildrenOpAny}
1639
+ */
1640
+ const _cloneChildOpDeep = op =>
1641
+ $insertOp.check(op)
1642
+ ? new InsertOp(op.insert.map(_cloneMaybeDeltaDeep), op.format, op.attribution)
1643
+ : ($modifyOp.check(op)
1644
+ ? new ModifyOp(cloneDeep(op.value), op.format, op.attribution)
1645
+ : op.clone())
1646
+
1647
+ /**
1648
+ * Deep-clone one attribute op for {@link cloneDeep} — the attribute-dimension mirror of
1649
+ * {@link _cloneChildOpDeep}: a `setAttr`'s value and a `modifyAttr`'s value may be nested
1650
+ * deltas and are deep-cloned.
1651
+ *
1652
+ * @param {AttrOpAny} op
1653
+ * @return {AttrOpAny}
1654
+ */
1655
+ const _cloneAttrOpDeep = op =>
1656
+ $setAttrOp.check(op)
1657
+ ? new SetAttrOp(op.key, _cloneMaybeDeltaDeep(op.value), op.attribution)
1658
+ : ($modifyAttrOp.check(op)
1659
+ ? new ModifyAttrOp(op.key, cloneDeep(op.value), op.attribution)
1660
+ // the only remaining attr op is a deleteAttr
1661
+ : new DeleteAttrOp(op.key, op.attribution))
1662
+
1663
+ /**
1664
+ * A **deep** clone of `d`: like {@link clone}, but every nested delta — an insert's delta content, a
1665
+ * `modify`/`modifyAttr` value, a delta-valued attribute — is itself recursively cloned into a fresh,
1666
+ * **mutable** node, instead of being frozen (`done`) and shared as {@link clone} does. The result and
1667
+ * its whole subtree are therefore independently editable.
1668
+ *
1669
+ * ## What is cloned, and when to reach for this
1670
+ *
1671
+ * A delta owns two kinds of nested state, treated differently on purpose:
1672
+ * - **Ops and (nested) deltas are cloned.** They carry structure edited *in place* — `apply`/`rebase`
1673
+ * splice op lists and mutate op fields — so a shared copy would be corrupted by a later edit of either
1674
+ * side.
1675
+ * - **`format`/`attribution` objects and plain JSON insert values are retained (shared).** They are
1676
+ * always *copied before being changed* (a format/attribution update allocates a new object — see
1677
+ * {@link mergeAttr}), so sharing them is safe and avoids needless allocation.
1678
+ *
1679
+ * {@link clone} suffices for the common case because a delta is only ever edited through `apply`/`rebase`,
1680
+ * which re-clone a `done` nested delta the first time they touch it (see {@link InsertOp#_modValue}) — the
1681
+ * shared, frozen children are never mutated in place. Reach for `cloneDeep` when a nested delta will be
1682
+ * **manipulated directly** instead of via `apply`/`rebase` — most notably by a
1683
+ * {@link import('./transformer/core.js').Transformer transformer}, which walks and mutates a delta's
1684
+ * ops/children in place. A `done` (frozen) delta must always be cloned before such direct manipulation
1685
+ * (a plain `apply`/`rebase` handles the freeze itself, so it needs no pre-clone).
1686
+ *
1687
+ * @template {DeltaConf} Conf
1688
+ * @param {Delta<Conf>} d
1689
+ * @return {DeltaBuilder<Conf>}
1690
+ */
1691
+ export const cloneDeep = d => {
1692
+ const cpy = /** @type {DeltaAny} */ (new DeltaBuilder(d.name, d.$schema))
1693
+ cpy.origin = d.origin
1694
+ cpy.isFinal = d.isFinal
1695
+ for (const op of d.attrs) {
1696
+ // @ts-ignore dynamic attr key (the same limitation `slice` suppresses when copying attrs)
1697
+ cpy.attrs[op.key] = _cloneAttrOpDeep(op)
1698
+ }
1699
+ for (const op of d.children) {
1700
+ list.pushEnd(cpy.children, _cloneChildOpDeep(op))
1701
+ }
1702
+ cpy.childCnt = d.childCnt
1703
+ // marks ride along verbatim — a full clone never clamps number-keyed root marks, and Mark objects are
1704
+ // immutable so they are shared (mirrors slice's full-copy path); a change delta's deleteMarks is copied.
1705
+ if (d.maybeHasMarks) {
1706
+ if (d.marks !== null) {
1707
+ const cpyMarks = new Marks()
1708
+ for (const m of d.marks) cpyMarks.add(m)
1709
+ if (cpyMarks.size > 0) cpy.marks = cpyMarks
1710
+ }
1711
+ cpy.maybeHasMarks = true
1712
+ }
1713
+ if (d.deleteMarks !== null) cpy.deleteMarks = new Set(d.deleteMarks)
1714
+ return /** @type {any} */ (cpy)
1715
+ }
1716
+
1717
+ /**
1718
+ * A *shallow* clone of `d`: a fresh {@link DeltaBuilder} carrying `d`'s name, attribute ops, and own
1719
+ * (root) marks — but **no children**. Content-transforming
1720
+ * {@link import('./transformer/core.js').Transformer transformers} (e.g. `children`, `value`) rebuild
1721
+ * the child list themselves yet must keep this node's own marks; the marks of nested children ride
1722
+ * along on the children the caller rebuilds. The `maybeHasMarks` flag is copied from `d` (conservative)
1723
+ * and the builder OR-propagates the flag as the caller appends children. Because mark-carrying lives
1724
+ * here in the shared primitive, a transformer that builds its output via {@link cloneShallow} cannot
1725
+ * silently drop the node's marks.
1726
+ *
1727
+ * @template {DeltaAny} D
1728
+ * @param {D} d
1729
+ * @return {D extends Delta<infer Conf> ? DeltaBuilder<Conf> : never}
1730
+ * @internal transformer plumbing — drops children; consumers use {@link clone}.
1731
+ */
1732
+ export const cloneShallow = d => {
1733
+ const cpy = /** @type {DeltaAny} */ (new DeltaBuilder(d.name, d.$schema))
1734
+ cpy.origin = d.origin
1735
+ for (const op of d.attrs) {
1736
+ // @ts-ignore (dynamic attr key — the same limitation slice suppresses when copying attrs)
1737
+ cpy.attrs[op.key] = /** @type {any} */ (op.clone())
1738
+ }
1739
+ // root marks / deleteMarks (a delete-mark-only change has no marks but must still ride); `cpy` is
1740
+ // fresh, so merging onto it is a plain copy
1741
+ if (d.marks !== null || d.deleteMarks !== null) mergeRootMarks(cpy, d)
1742
+ // carry the conservative flag (covers attr-value marks even when this node has no root marks of its
1743
+ // own); the builder OR-propagates it further as the caller appends the rebuilt children
1744
+ cpy.maybeHasMarks = d.maybeHasMarks
1745
+ // @ts-ignore
1746
+ return cpy
1747
+ }
1748
+
1749
+ /**
1750
+ * Make an op's modify `value` mutable for in-place editing: clear the op's cached fingerprint and, when
1751
+ * the value is a `done` delta, replace it with a mutable clone. Shared by the `_modValue` getters of
1752
+ * {@link ModifyOp}, {@link ModifyAttrOp}, and {@link SetAttrOp}; the `$deltaAny` guard lets a scalar
1753
+ * `SetAttrOp` value pass through untouched.
1754
+ *
1755
+ * @param {{ value: any, _fingerprint: string|null }} op
1756
+ * @return {any}
1757
+ */
1758
+ const modValue = op => {
1759
+ op._fingerprint = null
1760
+ const v = op.value
1761
+ if ($deltaAny.check(v) && v.isDone) op.value = clone(v)
1762
+ return op.value
1763
+ }
1764
+
1765
+ /*
1766
+ * --- @internal low-level mutation API for a delta's children list ---
1767
+ *
1768
+ * These are the ONLY sanctioned way to structurally edit `d.children` in place. Each keeps the cached
1769
+ * invariant `d.childCnt === Σ op.length` correct and resets the op's `_fingerprint`. Never mutate
1770
+ * `op.retain` / `op.delete` / `op.insert`, call `list.*`, or touch `d.childCnt` directly from outside
1771
+ * delta.js — go through these so the count (which `EqualityTraitSymbol` compares) never drifts.
1772
+ *
1773
+ * Cursor pattern (used by {@link DeltaBuilder#apply}, {@link DeltaBuilder#rebase}, and
1774
+ * `transformer/conform.js`): walk the target with `(op, offset)`; {@link _splitChildAt} before inserting
1775
+ * in the middle of an op so the cursor sits on a boundary; {@link _mergeChildWithPrev} after edits to
1776
+ * coalesce adjacent same-kind runs. `_growRun` / `_spliceInsert` extend an existing run *without* a split,
1777
+ * which keeps same-kind insertion (e.g. typing into a pass-through run) non-fragmenting.
1778
+ */
1779
+
1780
+ /**
1781
+ * Coalesce `op` into its previous sibling when they are the same kind with equal format/attribution,
1782
+ * removing `op`. `d.childCnt` is unchanged (the length moves to the previous op). Returns `true` when it
1783
+ * merged — so a caller holding a cursor on `op` can move it onto the surviving previous op.
1784
+ *
1785
+ * @param {DeltaBuilderAny} d
1213
1786
  * @param {InsertOp<any>|RetainOp|DeleteOp<any>|TextOp|ModifyOp<any>} op
1787
+ * @return {boolean}
1214
1788
  */
1215
- const tryMergeWithPrev = (parent, op) => {
1789
+ export const _mergeChildWithPrev = (d, op) => {
1216
1790
  const prevOp = op.prev
1217
1791
  if (
1218
1792
  prevOp?.constructor !== op.constructor ||
1793
+ $modifyOp.check(op) ||
1219
1794
  (
1220
- (!$deleteOp.check(op) && !$modifyOp.check(op)) && (!fun.equalityDeep(op.format, /** @type {InsertOp<any>} */ (prevOp).format) || !fun.equalityDeep(op.attribution, /** @type {InsertOp<any>} */ (prevOp).attribution))
1795
+ !$deleteOp.check(op) && (
1796
+ !fun.equalityDeep(op.format, /** @type {InsertOp<any>} */ (prevOp).format) ||
1797
+ !fun.equalityDeep(op.attribution, /** @type {InsertOp<any>} */ (prevOp).attribution)
1798
+ )
1221
1799
  )
1222
1800
  ) {
1223
1801
  // constructor mismatch or format/attribution mismatch
1224
- return
1802
+ return false
1225
1803
  }
1226
1804
  // can be merged
1227
1805
  if ($insertOp.check(op)) {
@@ -1233,31 +1811,126 @@ const tryMergeWithPrev = (parent, op) => {
1233
1811
  /** @type {DeleteOp<any>} */ (prevOp).delete += op.delete
1234
1812
  } else if ($textOp.check(op)) {
1235
1813
  /** @type {TextOp} */ (prevOp)._updateInsert(/** @type {TextOp} */ (prevOp).insert + op.insert)
1814
+ /* c8 ignore start */
1236
1815
  } else {
1816
+ // unreachable: the constructor check at the top of the function already
1817
+ // limits `op` to one of the four kinds tested above
1237
1818
  error.unexpectedCase()
1238
1819
  }
1239
- list.remove(parent, op)
1820
+ /* c8 ignore stop */
1821
+ list.remove(d.children, op)
1822
+ return true
1240
1823
  }
1241
1824
 
1242
1825
  /**
1243
- * Ensures that the delta can be edited. clears _fingerprint cache.
1826
+ * Split `op` at `offset`, leaving `op` = `[0, offset)` and a fresh right node `[offset, op.length)`
1827
+ * inserted immediately after it; returns the right node. `d.childCnt` is unchanged (net-zero). The clone
1828
+ * uses `markAsDone=false` because the right node is `_splice`d out of `op` here, so it is the sole owner.
1829
+ * Caller guarantees `0 < offset < op.length`.
1244
1830
  *
1245
- * @param {any} d
1831
+ * @param {DeltaBuilderAny} d
1832
+ * @param {ChildrenOpAny} op
1833
+ * @param {number} offset
1834
+ * @return {ChildrenOpAny}
1246
1835
  */
1247
- const modDeltaCheck = d => {
1248
- if (d.isDone) {
1249
- /**
1250
- * You tried to modify a delta after it has been marked as "done".
1251
- */
1252
- throw error.create("Readonly Delta can't be modified")
1253
- }
1254
- d._fingerprint = null
1836
+ export const _splitChildAt = (d, op, offset) => {
1837
+ const right = /** @type {ChildrenOpAny} */ (op.clone(offset, op.length, false))
1838
+ op._splice(offset, op.length - offset)
1839
+ list.insertBetween(d.children, op, op.next || null, right)
1840
+ return right
1255
1841
  }
1256
1842
 
1257
1843
  /**
1258
- * @template {DeltaConf} [Conf={}]
1259
- * @template {boolean} [FixedConf=false]
1260
- * @extends {Delta<Conf>}
1844
+ * Insert `op` into `d.children` immediately before `ref` (or push at the end when `ref == null`);
1845
+ * `d.childCnt += op.length`. Does not coalesce — call {@link _mergeChildWithPrev} afterwards if needed.
1846
+ *
1847
+ * @param {DeltaBuilderAny} d
1848
+ * @param {ChildrenOpAny?} ref
1849
+ * @param {ChildrenOpAny} op
1850
+ * @return {ChildrenOpAny}
1851
+ */
1852
+ export const _insertChild = (d, ref, op) => {
1853
+ list.insertBetween(d.children, ref == null ? d.children.end : ref.prev, ref, op)
1854
+ d.childCnt += op.length
1855
+ return op
1856
+ }
1857
+
1858
+ /**
1859
+ * Remove child `op` entirely; `d.childCnt -= op.length`.
1860
+ *
1861
+ * @param {DeltaBuilderAny} d
1862
+ * @param {ChildrenOpAny} op
1863
+ */
1864
+ export const _removeChild = (d, op) => {
1865
+ d.childCnt -= op.length
1866
+ list.remove(d.children, op)
1867
+ }
1868
+
1869
+ /**
1870
+ * Drop `len` positions from `op` starting at `offset` (`op._splice`); `d.childCnt -= len`. Does not remove
1871
+ * an emptied op — the caller decides (call {@link _removeChild} when `op.length === 0`), because removal is
1872
+ * usually entangled with the caller's own cursor advance. Retain/Delete ignore `offset` (uniform runs);
1873
+ * Insert drops `insert[offset, offset+len)`.
1874
+ *
1875
+ * @param {DeltaBuilderAny} d
1876
+ * @param {ChildrenOpAny} op
1877
+ * @param {number} offset
1878
+ * @param {number} len
1879
+ */
1880
+ export const _shrinkChild = (d, op, offset, len) => {
1881
+ op._splice(offset, len)
1882
+ d.childCnt -= len
1883
+ }
1884
+
1885
+ /**
1886
+ * Grow a Retain or Delete run `op` by `n` positions in place (the invariant-safe form of `op.retain += n`);
1887
+ * `d.childCnt += n`. Use to extend an existing same-kind run without a split.
1888
+ *
1889
+ * @param {DeltaBuilderAny} d
1890
+ * @param {RetainOp|DeleteOp<any>} op
1891
+ * @param {number} n
1892
+ */
1893
+ export const _growRun = (d, op, n) => {
1894
+ // reason: RetainOp.retain / DeleteOp.delete are @readonly for consumers; the @internal mutators grow them in place, just as _splice shrinks them
1895
+ // @ts-ignore
1896
+ if ($retainOp.check(op)) op.retain += n; else op.delete += n
1897
+ op._fingerprint = null
1898
+ d.childCnt += n
1899
+ }
1900
+
1901
+ /**
1902
+ * Splice `elems` into an Insert run `op` at `offset` in place; `d.childCnt += elems.length`.
1903
+ *
1904
+ * @param {DeltaBuilderAny} d
1905
+ * @param {InsertOp<any>} op
1906
+ * @param {number} offset
1907
+ * @param {Array<any>} elems
1908
+ */
1909
+ export const _spliceInsert = (d, op, offset, elems) => {
1910
+ op.insert.splice(offset, 0, ...elems)
1911
+ op._fingerprint = null
1912
+ d.childCnt += elems.length
1913
+ }
1914
+
1915
+ /**
1916
+ * Ensures that the delta can be edited. clears _fingerprint cache.
1917
+ *
1918
+ * @param {DeltaBuilderAny} d
1919
+ */
1920
+ const modDeltaCheck = d => {
1921
+ if (d.isDone) {
1922
+ /**
1923
+ * You tried to modify a delta after it has been marked as "done".
1924
+ */
1925
+ throw error.create("Readonly Delta can't be modified")
1926
+ }
1927
+ d._fingerprint = null
1928
+ }
1929
+
1930
+ /**
1931
+ * @template {DeltaConf} [Conf={}]
1932
+ * @template {boolean} [FixedConf=false]
1933
+ * @extends {Delta<Conf>}
1261
1934
  */
1262
1935
  export class DeltaBuilder extends Delta {
1263
1936
  /**
@@ -1267,49 +1940,119 @@ export class DeltaBuilder extends Delta {
1267
1940
  constructor (name, $schema) {
1268
1941
  super(name, $schema)
1269
1942
  /**
1270
- * @type {FormattingAttributes?}
1943
+ * @type {Formats?}
1271
1944
  */
1272
- this.usedAttributes = null
1945
+ this._usedFormats = null
1273
1946
  /**
1274
1947
  * @type {Attribution?}
1275
1948
  */
1276
- this.usedAttribution = null
1949
+ this._usedAttribution = null
1950
+ /**
1951
+ * Resolved data-companion cache for the `used*` contexts ({@link resolveUsedData}): what data
1952
+ * ops inherit instead of the raw context. `undefined` = stale, recomputed on the next data-op
1953
+ * consumption ({@link DeltaBuilder#_getUsedFormatsData}) — {@link DeltaBuilder#useFormats} & co
1954
+ * clear it whenever they swap the context (which is why the public slots are read-only).
1955
+ *
1956
+ * @type {Formats|null|undefined}
1957
+ */
1958
+ this._usedFormatsData = undefined
1959
+ /**
1960
+ * @type {Attribution|null|undefined}
1961
+ */
1962
+ this._usedAttributionData = undefined
1277
1963
  }
1278
1964
 
1279
1965
  /**
1966
+ * The ambient formats context inherited by subsequent ops. Read-only: set it via
1967
+ * {@link DeltaBuilder#useFormats} / {@link DeltaBuilder#updateUsedFormats}, which also invalidate
1968
+ * the cached data companion — a direct overwrite would leave it stale.
1969
+ *
1970
+ * @return {Formats?}
1971
+ */
1972
+ get usedFormats () { return this._usedFormats }
1973
+
1974
+ /**
1975
+ * The ambient attribution context inherited by subsequent ops. Read-only — see
1976
+ * {@link DeltaBuilder#usedFormats}; set it via {@link DeltaBuilder#useAttribution} /
1977
+ * {@link DeltaBuilder#updateUsedAttribution}.
1978
+ *
1979
+ * @return {Attribution?}
1980
+ */
1981
+ get usedAttribution () { return this._usedAttribution }
1982
+
1983
+ /**
1984
+ * Set the ambient attribution context inherited by subsequent ops (`undefined`/empty dim args).
1985
+ * The object is interned — instruction ops store it, and data ops store its resolved companion
1986
+ * ({@link resolveUsedData}), BY REFERENCE. Never mutate it after setting; pass a fresh object
1987
+ * instead (same invariant as the objects stored on ops).
1988
+ *
1280
1989
  * @param {Attribution?} attribution
1281
1990
  */
1282
1991
  useAttribution (attribution) {
1283
1992
  modDeltaCheck(this)
1284
- this.usedAttribution = attribution
1993
+ if (this._usedAttribution !== attribution) {
1994
+ this._usedAttribution = attribution
1995
+ this._usedAttributionData = undefined // clear the data companion — recomputed on next consumption
1996
+ }
1285
1997
  return this
1286
1998
  }
1287
1999
 
1288
2000
  /**
1289
- * @param {FormattingAttributes?} attributes
2001
+ * Set the ambient formats context inherited by subsequent ops (`undefined`/empty dim args). The
2002
+ * object is interned — see {@link DeltaBuilder#useAttribution}: never mutate it after setting.
2003
+ *
2004
+ * @param {Formats?} attributes
1290
2005
  * @return {this}
1291
2006
  */
1292
- useAttributes (attributes) {
2007
+ useFormats (attributes) {
1293
2008
  modDeltaCheck(this)
1294
- this.usedAttributes = attributes
2009
+ if (this._usedFormats !== attributes) {
2010
+ this._usedFormats = attributes
2011
+ this._usedFormatsData = undefined // clear the data companion — recomputed on next consumption
2012
+ }
1295
2013
  return this
1296
2014
  }
1297
2015
 
2016
+ /**
2017
+ * The `usedFormats` context resolved for data-op consumption ({@link resolveUsedData}): the slot
2018
+ * object itself when it carries no `{k:null}` removals, one shared stripped copy otherwise.
2019
+ * Cached until the context is swapped — no per-op traversal.
2020
+ *
2021
+ * @return {Formats?}
2022
+ */
2023
+ _getUsedFormatsData () {
2024
+ const d = this._usedFormatsData
2025
+ return d !== undefined ? d : (this._usedFormatsData = /** @type {Formats?} */ (resolveUsedData(this._usedFormats, false)))
2026
+ }
2027
+
2028
+ /**
2029
+ * See {@link DeltaBuilder#_getUsedFormatsData} — the `usedAttribution` analogue (its `format` key
2030
+ * resolves one level deeper).
2031
+ *
2032
+ * @return {Attribution?}
2033
+ */
2034
+ _getUsedAttributionData () {
2035
+ const d = this._usedAttributionData
2036
+ return d !== undefined ? d : (this._usedAttributionData = /** @type {Attribution?} */ (resolveUsedData(this._usedAttribution, true)))
2037
+ }
2038
+
1298
2039
  /**
1299
2040
  * @param {string} name
1300
2041
  * @param {any} value
1301
2042
  */
1302
- updateUsedAttributes (name, value) {
2043
+ updateUsedFormats (name, value) {
1303
2044
  modDeltaCheck(this)
1304
2045
  if (value == null) {
1305
- this.usedAttributes = object.assign({}, this.usedAttributes)
1306
- delete this.usedAttributes?.[name]
1307
- if (object.isEmpty(this.usedAttributes)) {
1308
- this.usedAttributes = null
2046
+ this._usedFormats = object.assign({}, this._usedFormats)
2047
+ delete this._usedFormats?.[name]
2048
+ if (object.isEmpty(this._usedFormats)) {
2049
+ this._usedFormats = null
1309
2050
  }
1310
- } else if (!fun.equalityDeep(this.usedAttributes?.[name], value)) {
1311
- this.usedAttributes = object.assign({}, this.usedAttributes)
1312
- this.usedAttributes[name] = value
2051
+ this._usedFormatsData = undefined // clear the data companion — recomputed on next consumption
2052
+ } else if (!fun.equalityDeep(this._usedFormats?.[name], value)) {
2053
+ this._usedFormats = object.assign({}, this._usedFormats)
2054
+ this._usedFormats[name] = value
2055
+ this._usedFormatsData = undefined // clear the data companion — recomputed on next consumption
1313
2056
  }
1314
2057
  return this
1315
2058
  }
@@ -1322,14 +2065,16 @@ export class DeltaBuilder extends Delta {
1322
2065
  updateUsedAttribution (name, value) {
1323
2066
  modDeltaCheck(this)
1324
2067
  if (value == null) {
1325
- this.usedAttribution = object.assign({}, this.usedAttribution)
1326
- delete this.usedAttribution?.[name]
1327
- if (object.isEmpty(this.usedAttribution)) {
1328
- this.usedAttribution = null
2068
+ this._usedAttribution = object.assign({}, this._usedAttribution)
2069
+ delete this._usedAttribution?.[name]
2070
+ if (object.isEmpty(this._usedAttribution)) {
2071
+ this._usedAttribution = null
1329
2072
  }
1330
- } else if (!fun.equalityDeep(this.usedAttribution?.[name], value)) {
1331
- this.usedAttribution = object.assign({}, this.usedAttribution)
1332
- this.usedAttribution[name] = value
2073
+ this._usedAttributionData = undefined // clear the data companion — recomputed on next consumption
2074
+ } else if (!fun.equalityDeep(this._usedAttribution?.[name], value)) {
2075
+ this._usedAttribution = object.assign({}, this._usedAttribution)
2076
+ this._usedAttribution[name] = value
2077
+ this._usedAttributionData = undefined // clear the data companion — recomputed on next consumption
1333
2078
  }
1334
2079
  return this
1335
2080
  }
@@ -1337,27 +2082,27 @@ export class DeltaBuilder extends Delta {
1337
2082
  /**
1338
2083
  * @template {(FixedConf extends true ? never : (Array<any>|string)) | (DeltaConfGetChildren<Conf> extends infer Children ? (Children extends never ? never : Array<Children>) : never) | DeltaConfGetText<Conf>} NewContent
1339
2084
  * @param {NewContent} insert
1340
- * @param {FormattingAttributes?} [formatting]
2085
+ * @param {Formats?} [formatting]
1341
2086
  * @param {Attribution?} [attribution]
1342
2087
  * @return {DeltaBuilder<FixedConf extends true ? Conf : DeltaConfOverwrite<Conf,
1343
2088
  * (Exclude<NewContent,string> extends never ? {} : {
1344
2089
  * children: Exclude<NewContent,string>[number]|DeltaConfGetChildren<Conf>
1345
2090
  * }) & (Extract<NewContent,string> extends never ? {} : { text: true })>, FixedConf>}
1346
2091
  */
1347
- insert (insert, formatting = null, attribution = null) {
2092
+ insert (insert, formatting, attribution) {
1348
2093
  modDeltaCheck(this)
1349
- const mergedAttributes = mergeAttrs(this.usedAttributes, formatting)
1350
- const mergedAttribution = mergeAttrs(this.usedAttribution, attribution)
2094
+ const mergedFormats = combineData(this._getUsedFormatsData(), formatting, false)
2095
+ const mergedAttribution = combineData(this._getUsedAttributionData(), attribution, true)
1351
2096
  /**
1352
2097
  * @param {TextOp | InsertOp<any>} lastOp
1353
2098
  */
1354
- const checkMergedEquals = lastOp => (mergedAttributes === lastOp.format || fun.equalityDeep(mergedAttributes, lastOp.format)) && (mergedAttribution === lastOp.attribution || fun.equalityDeep(mergedAttribution, lastOp.attribution))
2099
+ const checkMergedEquals = lastOp => (mergedFormats === lastOp.format || fun.equalityDeep(mergedFormats, lastOp.format)) && (mergedAttribution === lastOp.attribution || fun.equalityDeep(mergedAttribution, lastOp.attribution))
1355
2100
  const end = this.children.end
1356
2101
  if (s.$string.check(insert)) {
1357
2102
  if ($textOp.check(end) && checkMergedEquals(end)) {
1358
2103
  end._updateInsert(end.insert + insert)
1359
2104
  } else if (insert.length > 0) {
1360
- list.pushEnd(this.children, new TextOp(insert, object.isEmpty(mergedAttributes) ? null : mergedAttributes, object.isEmpty(mergedAttribution) ? null : mergedAttribution))
2105
+ list.pushEnd(this.children, new TextOp(insert, mergedFormats, mergedAttribution))
1361
2106
  }
1362
2107
  this.childCnt += insert.length
1363
2108
  } else if (arr.isArray(insert)) {
@@ -1366,42 +2111,83 @@ export class DeltaBuilder extends Delta {
1366
2111
  end.insert.push(...insert)
1367
2112
  end._fingerprint = null
1368
2113
  } else if (insert.length > 0) {
1369
- list.pushEnd(this.children, new InsertOp(insert.slice() /* ensures that we don't reuse an existing array */, object.isEmpty(mergedAttributes) ? null : mergedAttributes, object.isEmpty(mergedAttribution) ? null : mergedAttribution))
2114
+ list.pushEnd(this.children, new InsertOp(insert.slice() /* ensures that we don't reuse an existing array */, mergedFormats, mergedAttribution))
1370
2115
  }
1371
2116
  this.childCnt += insert.length
2117
+ // inserting pre-marked content brings its subtree marks along (as apply's insert path does)
2118
+ this.maybeHasMarks ||= elemsMaybeHaveMarks(insert)
1372
2119
  }
1373
2120
  return /** @type {any} */ (this)
1374
2121
  }
1375
2122
 
1376
2123
  /**
1377
- * @template {Extract<DeltaConfGetAllowedChildren<Conf, FixedConf>,Delta>} NewContent
2124
+ * @template {Extract<DeltaConfGetAllowedChildren<Conf, FixedConf>,Delta|DeltaData<any,any,any,any>|DeltaBuilder>} NewContent
1378
2125
  * @param {NewContent} modify
1379
- * @param {FormattingAttributes?} formatting
1380
- * @param {Attribution?} attribution
2126
+ * @param {Formats?} [formatting] tri-state: omit/`undefined` skip, `null` clear, `{k:v}`/`{k:null}` set/remove
2127
+ * @param {Attribution?} [attribution] tri-state: omit/`undefined` skip, `null` clear, `{k:v}`/`{k:null}` set/remove
1381
2128
  * @return {DeltaBuilder<DeltaConfOverwrite<Conf, {children: DeltaConfGetChildren<Conf>|NewContent}>, FixedConf>}
1382
2129
  */
1383
- modify (modify, formatting = null, attribution = null) {
2130
+ modify (modify, formatting, attribution) {
1384
2131
  modDeltaCheck(this)
1385
- const mergedAttributes = mergeAttrs(this.usedAttributes, formatting)
1386
- const mergedAttribution = mergeAttrs(this.usedAttribution, attribution)
1387
- list.pushEnd(this.children, new ModifyOp(modify, object.isEmpty(mergedAttributes) ? null : mergedAttributes, object.isEmpty(mergedAttribution) ? null : mergedAttribution))
2132
+ const mergedFormats = combineInstr(this.usedFormats, formatting, false)
2133
+ const mergedAttribution = combineInstr(this.usedAttribution, attribution, true)
2134
+ list.pushEnd(this.children, new ModifyOp(modify, mergedFormats, mergedAttribution))
1388
2135
  this.childCnt += 1
2136
+ // the modify target carries marks in its subtree (own root marks + nested) - flag conservatively
2137
+ this.maybeHasMarks ||= modify.maybeHasMarks
1389
2138
  return /** @type {any} */ (this)
1390
2139
  }
1391
2140
 
2141
+ /**
2142
+ * Add a cursor/selection {@link Mark} at `pos` — a {@link import('./position.js').Pos}: a `path` of
2143
+ * content-index / attribute-key steps plus an `assoc` and optional immutable `attrs` (user data
2144
+ * carried with the mark). Returns `this` for chaining (like every other builder method). Pass an
2145
+ * explicit `id` to reference the mark later — re-adding the same `id` replaces it (e.g. to move a
2146
+ * cursor or update its `attrs`) and {@link DeltaBuilder#removeMark} deletes it; an anonymous one-shot
2147
+ * mark gets a fresh GUID otherwise.
2148
+ *
2149
+ * @param {import('./position.js').Pos} pos
2150
+ * @param {string} [id]
2151
+ * @return {this}
2152
+ */
2153
+ addMark (pos, id = rand.uuidv4()) {
2154
+ // apply with the default `final = this.isFinal` (do NOT force `{ final: true }`): a fresh change
2155
+ // builder is non-final so a sibling `removeMark` stays transmittable; a final doc collects nothing
2156
+ this.apply(/** @type {Delta<Conf>} */ (markChange(pos, id, false)))
2157
+ return this
2158
+ }
2159
+
2160
+ /**
2161
+ * Remove the mark with `id`, located via `pos` (the position it was added at). On a settled document
2162
+ * that holds the mark this removes it in place; on a fresh/markless builder it records a transmittable
2163
+ * `deleteMarks` change (symmetric to {@link DeltaBuilder#addMark}), so
2164
+ * `delta.create().removeMark(pos, id)` yields the delete-mark change to apply / rebase / transmit.
2165
+ *
2166
+ * @param {import('./position.js').Pos} pos
2167
+ * @param {string} id
2168
+ * @return {this}
2169
+ */
2170
+ removeMark (pos, id) {
2171
+ // apply with the default `final = this.isFinal` (do NOT force `{ final: true }`): on a non-final
2172
+ // change builder this records a transmittable `deleteMarks`; on a final doc it only removes in place
2173
+ this.apply(/** @type {Delta<Conf>} */ (markChange(pos, id, true)))
2174
+ return this
2175
+ }
2176
+
1392
2177
  /**
1393
2178
  * @param {number} len
1394
- * @param {FormattingAttributes?} [format]
2179
+ * @param {Formats?} [format]
1395
2180
  * @param {Attribution?} [attribution]
1396
2181
  */
1397
- retain (len, format = null, attribution = null) {
1398
- modDeltaCheck(this)
1399
- const mergedFormats = mergeAttrs(this.usedAttributes, format)
1400
- const mergedAttribution = mergeAttrs(this.usedAttribution, attribution)
2182
+ retain (len, format, attribution) {
2183
+ modDeltaCheck(this) // this clears _fingerprint
2184
+ const mergedFormats = combineInstr(this.usedFormats, format, false)
2185
+ const mergedAttribution = combineInstr(this.usedAttribution, attribution, true)
1401
2186
  const lastOp = /** @type {RetainOp|InsertOp<any>} */ (this.children.end)
1402
2187
  if ($retainOp.check(lastOp) && fun.equalityDeep(mergedFormats, lastOp.format) && fun.equalityDeep(mergedAttribution, lastOp.attribution)) {
1403
2188
  // @ts-ignore
1404
2189
  lastOp.retain += len
2190
+ lastOp._fingerprint = null
1405
2191
  } else if (len > 0) {
1406
2192
  list.pushEnd(this.children, new RetainOp(len, mergedFormats, mergedAttribution))
1407
2193
  }
@@ -1417,6 +2203,7 @@ export class DeltaBuilder extends Delta {
1417
2203
  const lastOp = /** @type {DeleteOp<any>|InsertOp<any>} */ (this.children.end)
1418
2204
  if ($deleteOp.check(lastOp)) {
1419
2205
  lastOp.delete += len
2206
+ lastOp._fingerprint = null
1420
2207
  } else if (len > 0) {
1421
2208
  list.pushEnd(this.children, new DeleteOp(len))
1422
2209
  }
@@ -1429,29 +2216,30 @@ export class DeltaBuilder extends Delta {
1429
2216
  * @template {DeltaConfGetAllowedAttrs<Conf, FixedConf>[Key]} Val
1430
2217
  * @param {Key} key
1431
2218
  * @param {Val} val
1432
- * @param {Attribution?} attribution
1433
- * @param {Val|undefined} [prevValue]
2219
+ * @param {Attribution?} [attribution] provenance for this attr (data: object or `null`/none)
1434
2220
  * @return {DeltaBuilder<DeltaConfOverwrite<Conf,{attrs:AddToAttrs<DeltaConfGetAttrs<Conf>,Key,Val>}>, FixedConf>}
1435
2221
  */
1436
- setAttr (key, val, attribution = null, prevValue) {
2222
+ setAttr (key, val, attribution) {
1437
2223
  modDeltaCheck(this)
1438
2224
  // @ts-ignore
1439
2225
  this.attrs[key] /** @type {any} */ =
1440
- (new SetAttrOp(/** @type {any} */ (key), val, prevValue, mergeAttrs(this.usedAttribution, attribution)))
2226
+ (new SetAttrOp(/** @type {any} */ (key), val, combineData(this._getUsedAttributionData(), attribution, true)))
2227
+ // a delta-valued attribute carries marks in its subtree - flag conservatively (never decremented)
2228
+ if ($deltaAny.check(val)) this.maybeHasMarks ||= val.maybeHasMarks
1441
2229
  return /** @type {any} */ (this)
1442
2230
  }
1443
2231
 
1444
2232
  /**
1445
2233
  * @template {DeltaConfGetAllowedAttrs<Conf, FixedConf>} NewAttrs
1446
2234
  * @param {NewAttrs} attrs
1447
- * @param {Attribution?} attribution
2235
+ * @param {Attribution?} [attribution] provenance applied to each set attr (data: object or `null`/none)
1448
2236
  * @return {DeltaBuilder<DeltaConfOverwrite<
1449
2237
  * Conf,
1450
2238
  * { attrs: MergeAttrs<DeltaConfGetAttrs<Conf>,NewAttrs> }
1451
2239
  * >, FixedConf>
1452
2240
  * }
1453
2241
  */
1454
- setAttrs (attrs, attribution = null) {
2242
+ setAttrs (attrs, attribution) {
1455
2243
  modDeltaCheck(this)
1456
2244
  for (const k in attrs) {
1457
2245
  this.setAttr(/** @type {any} */ (k), /** @type {any} */ (attrs)[/** @type {any} */ (k)], attribution)
@@ -1462,17 +2250,18 @@ export class DeltaBuilder extends Delta {
1462
2250
  /**
1463
2251
  * @template {Extract<keyof DeltaConfGetAllowedAttrs<Conf, FixedConf>,string|number>} Key
1464
2252
  * @param {Key} key
1465
- * @param {Attribution?} attribution
1466
- * @param {any} [prevValue]
2253
+ * @param {Attribution?} [attribution] provenance for the deletion (data: object or `null`/none)
1467
2254
  * @return {DeltaBuilder<DeltaConfOverwrite<Conf, {
1468
2255
  * attrs: AddToAttrs<DeltaConfGetAttrs<Conf>,Key,never>
1469
2256
  * }>, FixedConf>}
1470
2257
  */
1471
- deleteAttr (key, attribution = null, prevValue) {
2258
+ deleteAttr (key, attribution) {
1472
2259
  modDeltaCheck(this)
2260
+ // dropping a delta-valued attribute drops its subtree's marks; the conservative `maybeHasMarks` flag
2261
+ // is left as-is (never decremented - marksToPositions self-corrects it)
1473
2262
  // @ts-ignore
1474
2263
  this.attrs[key] /** @type {any} */ =
1475
- (new DeleteAttrOp(/** @type {any} */ (key), prevValue, mergeAttrs(this.usedAttribution, attribution)))
2264
+ (new DeleteAttrOp(/** @type {any} */ (key), combineData(this._getUsedAttributionData(), attribution, true)))
1476
2265
  return /** @type {any} */ (this)
1477
2266
  }
1478
2267
 
@@ -1481,41 +2270,79 @@ export class DeltaBuilder extends Delta {
1481
2270
  * @template {Extract<DeltaConfGetAllowedAttrs<Conf, FixedConf>[Key],DeltaAny>} D
1482
2271
  * @param {Key} key
1483
2272
  * @param {D} modify
2273
+ * @param {Attribution?} [attribution] tri-state instruction merged onto the target attr op: omit/`undefined` skip, `null` clear, `{k:v}`/`{k:null}` set/remove
1484
2274
  * @return {DeltaBuilder<DeltaConfOverwrite<Conf,{attrs:AddToAttrs<DeltaConfGetAttrs<Conf>,Key,D>}>, FixedConf>}
1485
2275
  */
1486
- modifyAttr (key, modify) {
2276
+ modifyAttr (key, modify, attribution) {
1487
2277
  modDeltaCheck(this)
1488
- this.attrs[key] = /** @type {any} */ (new ModifyAttrOp(key, modify))
2278
+ const mergedAttribution = combineInstr(this.usedAttribution, attribution, true)
2279
+ this.attrs[key] = /** @type {any} */ (new ModifyAttrOp(key, modify, mergedAttribution))
2280
+ // the modify value carries marks in its subtree - flag conservatively (never decremented)
2281
+ this.maybeHasMarks ||= /** @type {DeltaAny} */ (modify).maybeHasMarks
1489
2282
  return /** @type {any} */ (this)
1490
2283
  }
1491
2284
 
1492
2285
  /**
1493
- * @param {Delta<Conf>} other
2286
+ * Apply other delta on this op. The result is the merged op of both of them.
2287
+ *
2288
+ * a.apply(b.apply(c))
2289
+ *
2290
+ * is equivalent to
2291
+ *
2292
+ * a.apply(b).apply(c)
2293
+ *
2294
+ * If `final = true`, we consider this delta the final state and drop deleteAttrOps from
2295
+ * attributes. (E.g. if `otherOp` deletes an attribute, this op will simply not have the
2296
+ * attribute). Any kind of `delete` op might be considered a bug. A final delta is not idempotent.
2297
+ *
2298
+ * @param {Delta<Conf>?} other
2299
+ * @param {{ final?: boolean, move?: boolean }} [opts] `move`: the caller donates `other` (it is not
2300
+ * read again), so its content is consumed (shared mutable) rather than frozen-cloned — see {@link
2301
+ * InsertOp#clone}. UNSAFE if the caller keeps using `other`.
2302
+ * @return {DeltaBuilder<Conf>}
1494
2303
  */
1495
- apply (other) {
2304
+ apply (other, { final = this.isFinal, move = false } = {}) {
2305
+ if (other == null) return this
1496
2306
  modDeltaCheck(this)
1497
2307
  this.$schema?.expect(other)
2308
+ // `move`: the caller donates `other` (it must not be read again), so content is consumed (shared
2309
+ // mutable) instead of frozen-cloned — no `done()` and no re-clone on a later modify. UNSAFE if the
2310
+ // caller keeps using `other`. `!move` is the default everywhere (today's behavior).
2311
+ const keep = !move
1498
2312
  // apply attrs
1499
2313
  for (const op of other.attrs) {
1500
2314
  // @ts-ignore
1501
2315
  const c = /** @type {SetAttrOp<any,any>|DeleteAttrOp<any>|ModifyAttrOp<any,any>} */ (this.attrs[op.key])
1502
2316
  if ($modifyAttrOp.check(op)) {
1503
2317
  if ($deltaAny.check(c?.value)) {
1504
- c._modValue.apply(op.value)
2318
+ const tgt = c._modValue
2319
+ tgt.apply(op.value, { final, move })
2320
+ this.maybeHasMarks ||= tgt.maybeHasMarks // flag the attr subtree's marks up
2321
+ // modifyAttr carries a tri-state attribution update for the target attr op (merge/clear/skip)
2322
+ if (applyDim(c, 'attribution', op.attribution)) {
2323
+ /** @type {any} */ (c)._fingerprint = null
2324
+ }
1505
2325
  } else {
1506
- // then this is a simple modify
2326
+ // then this is a simple modify (the attribute did not previously hold a delta)
1507
2327
  // @ts-ignore
1508
- this.attrs[op.key] = op.clone()
2328
+ this.attrs[op.key] = move ? op : op.clone()
2329
+ this.maybeHasMarks ||= op.value.maybeHasMarks // flag any marks the new modify value carries
1509
2330
  }
1510
2331
  } else if ($setAttrOp.check(op)) {
2332
+ // a delta-valued attribute carries marks in its subtree - flag conservatively (never decremented)
2333
+ if ($deltaAny.check(op.value)) this.maybeHasMarks ||= op.value.maybeHasMarks
1511
2334
  // @ts-ignore
1512
- op.prevValue = c?.value
1513
- // @ts-ignore
1514
- this.attrs[op.key] = op.clone()
2335
+ this.attrs[op.key] = move ? op : op.clone()
1515
2336
  } else if ($deleteAttrOp.check(op)) {
1516
- op.prevValue = c?.value
1517
- // @ts-ignore
1518
- delete this.attrs[op.key]
2337
+ // removing a delta-valued attribute drops its subtree's marks; the conservative `maybeHasMarks`
2338
+ // flag is left as-is (never decremented - marksToPositions self-corrects it)
2339
+ if (final) {
2340
+ // @ts-ignore
2341
+ delete this.attrs[op.key]
2342
+ } else {
2343
+ // @ts-ignore
2344
+ this.attrs[op.key] = move ? op : op.clone()
2345
+ }
1519
2346
  }
1520
2347
  }
1521
2348
  // apply children
@@ -1523,6 +2350,9 @@ export class DeltaBuilder extends Delta {
1523
2350
  * @type {ChildrenOpAny?}
1524
2351
  */
1525
2352
  let opsI = this.children.start
2353
+ if ($deleteOp.check(opsI)) {
2354
+ opsI = opNextUndeleted(opsI)
2355
+ }
1526
2356
  let offset = 0
1527
2357
  /**
1528
2358
  * At the end, we will try to merge this op, and op.next op with their respective previous op.
@@ -1534,154 +2364,239 @@ export class DeltaBuilder extends Delta {
1534
2364
  */
1535
2365
  const maybeMergeable = []
1536
2366
  /**
1537
- * @template {InsertOp<any>|RetainOp|DeleteOp|TextOp|ModifyOp<any>|null} OP
1538
- * @param {OP} op
1539
- * @return {OP}
2367
+ * Schedule an op to be merged later
2368
+ * @param {ChildrenOpAny?} op
2369
+ * @return {ChildrenOpAny}
1540
2370
  */
1541
2371
  const scheduleForMerge = op => {
1542
2372
  op && maybeMergeable.push(op)
1543
- return op
2373
+ return /** @type {any} */ (op)
2374
+ }
2375
+
2376
+ /**
2377
+ * Split opsI at the current offset - ensuring that we can insert safely at the current position
2378
+ */
2379
+ const splitHere = () => {
2380
+ if (offset > 0 && opsI != null) {
2381
+ opsI = scheduleForMerge(_splitChildAt(this, opsI, offset))
2382
+ offset = 0
2383
+ }
2384
+ }
2385
+ /**
2386
+ * @param {ChildrenOpAny} op
2387
+ */
2388
+ const insertClonedOp = op => {
2389
+ splitHere()
2390
+ // `move` re-parents the source op directly (it is donated); else clone with `keep`. Freshly
2391
+ // built `new DeleteOp(...)` callers are unaliased, so moving them is likewise safe.
2392
+ _insertChild(this, opsI, scheduleForMerge(move ? op : op.clone(0, op.length, keep)))
2393
+ // inserting pre-marked content (e.g. a node that already carries a cursor) brings its marks along
2394
+ if ($insertOp.check(op)) this.maybeHasMarks ||= elemsMaybeHaveMarks(op.insert)
1544
2395
  }
1545
- other.children.forEach(op => {
2396
+ // manual iteration (not `for..of`): a `move` apply re-parents the source op into `this.children`,
2397
+ // rewiring its `.next`; capture the successor first so the walk stays on `other`'s chain.
2398
+ for (let op = /** @type {ChildrenOpAny?} */ (other.children.start), nextOp = op; op != null; op = nextOp) {
2399
+ nextOp = op.next
2400
+ // defensive: the per-branch logic below resets opsI/offset whenever it
2401
+ // consumes an op exactly. This guard catches any path that forgets to.
2402
+ /* c8 ignore start */
2403
+ if (opsI?.length === offset) {
2404
+ opsI = opNextUndeleted(opsI)
2405
+ offset = 0
2406
+ }
2407
+ /* c8 ignore stop */
1546
2408
  if ($textOp.check(op) || $insertOp.check(op)) {
1547
- if (offset === 0) {
1548
- list.insertBetween(this.children, opsI == null ? this.children.end : opsI.prev, opsI, scheduleForMerge(op.clone()))
1549
- } else {
1550
- // @todo inmplement "splitHelper" and "insertHelper" - I'm splitting all the time and
1551
- // forget to update opsI
1552
- if (opsI == null) error.unexpectedCase()
1553
- const cpy = scheduleForMerge(opsI.clone(offset))
1554
- opsI._splice(offset, opsI.length - offset)
1555
- list.insertBetween(this.children, opsI, opsI.next || null, cpy)
1556
- list.insertBetween(this.children, opsI, cpy || null, scheduleForMerge(op.clone()))
1557
- opsI = cpy
1558
- offset = 0
1559
- }
1560
- this.childCnt += op.insert.length
2409
+ insertClonedOp(op)
1561
2410
  } else if ($retainOp.check(op)) {
1562
2411
  let retainLen = op.length
1563
-
1564
- if (offset > 0 && opsI != null && op.format != null && !$deleteOp.check(opsI) && !object.every(op.format, (v, k) => fun.equalityDeep(v, /** @type {InsertOp<any>|RetainOp|ModifyOp} */ (opsI).format?.[k] || null))) {
2412
+ // split the current op when this retain carries a format and/or attribution instruction
2413
+ // (`!== undefined`) that differs from what the existing op holds both unified tri-state. The
2414
+ // split must fire on any differing instruction (incl. a `{k:null}` removal or a `null` clear), not
2415
+ // just one that would change a *data* target: it marks the boundary between the already-advanced
2416
+ // (skipped) prefix and the region this op formats, so the update can't bleed into the prefix.
2417
+ if (offset > 0 && opsI != null && !$deleteOp.check(opsI) && (
2418
+ (op.format !== undefined && !fun.equalityDeep(/** @type {InsertOp<any>|RetainOp|ModifyOp} */ (opsI).format, op.format)) ||
2419
+ (op.attribution !== undefined && !fun.equalityDeep(/** @type {any} */ (opsI).attribution, op.attribution))
2420
+ )) {
1565
2421
  // need to split current op
1566
- const cpy = scheduleForMerge(opsI.clone(offset))
1567
- opsI._splice(offset, opsI.length - offset)
1568
- list.insertBetween(this.children, opsI, opsI.next || null, cpy)
1569
- opsI = cpy
1570
- offset = 0
2422
+ splitHere()
1571
2423
  }
1572
-
1573
2424
  while (opsI != null && opsI.length - offset <= retainLen) {
1574
- op.format != null && updateOpFormat(opsI, op.format)
2425
+ if (op.format !== undefined || op.attribution !== undefined) {
2426
+ updateOpFormat(opsI, op.format, op.attribution)
2427
+ scheduleForMerge(opsI)
2428
+ }
1575
2429
  retainLen -= opsI.length - offset
1576
- opsI = opsI?.next || null
2430
+ opsI = opNextUndeleted(opsI)
1577
2431
  offset = 0
1578
2432
  }
1579
-
1580
2433
  if (opsI != null) {
1581
- if (op.format != null && retainLen > 0) {
1582
- // split current op and apply format
1583
- const cpy = scheduleForMerge(opsI.clone(retainLen))
1584
- opsI._splice(retainLen, opsI.length - retainLen)
1585
- list.insertBetween(this.children, opsI, opsI.next || null, cpy)
1586
- updateOpFormat(opsI, op.format)
1587
- opsI = cpy
2434
+ if ((op.format !== undefined || op.attribution !== undefined) && retainLen > 0) {
2435
+ // accumulate onto the existing offset — the else-branch below uses
2436
+ // `offset += retainLen`, and we must agree with it when prior
2437
+ // iterations have advanced offset into opsI without splitting (e.g.
2438
+ // a format-less retain followed by a same-format retain).
2439
+ offset += retainLen
2440
+ splitHere()
2441
+ updateOpFormat(/** @type {ChildrenOpAny} */ (opsI.prev), op.format, op.attribution)
2442
+ scheduleForMerge(opsI.prev)
1588
2443
  } else {
1589
2444
  offset += retainLen
1590
2445
  }
1591
2446
  } else if (retainLen > 0) {
1592
- list.pushEnd(this.children, scheduleForMerge(new RetainOp(retainLen, op.format, op.attribution)))
1593
- this.childCnt += retainLen
2447
+ // append the retain beyond existing content. On a NON-final/instruction apply keep it verbatim —
2448
+ // a `null` (clear) format/attribution here is a meaningful change op (e.g. a format/attribution
2449
+ // diff applied onto an as-yet-empty change delta). On a FINAL (materializing) apply the
2450
+ // removal/clear has nothing to act on, so resolve it away ({@link resolveBeyond}): a `{k:null}`
2451
+ // removal or `null` clear collapses to `undefined`, a set survives. We still append the (possibly
2452
+ // now-bare) retain so a following op stays positioned; done() trims it iff it is trailing.
2453
+ const fmt = final ? resolveBeyond(op.format, false) : op.format
2454
+ const attr = final ? resolveBeyond(op.attribution, true) : op.attribution
2455
+ _insertChild(this, null, scheduleForMerge(new RetainOp(retainLen, fmt, attr)))
1594
2456
  }
1595
2457
  } else if ($deleteOp.check(op)) {
1596
2458
  let remainingLen = op.delete
1597
2459
  while (remainingLen > 0) {
1598
2460
  if (opsI == null) {
1599
- list.pushEnd(this.children, scheduleForMerge(new DeleteOp(remainingLen)))
1600
- this.childCnt += remainingLen
2461
+ _insertChild(this, null, scheduleForMerge(new DeleteOp(remainingLen)))
1601
2462
  break
1602
- } else if ($deleteOp.check(opsI)) {
1603
- const delLen = opsI.length - offset
1604
- // the same content can't be deleted twice, remove duplicated deletes
1605
- if (delLen >= remainingLen) {
2463
+ } else if ($retainOp.check(opsI)) { // retain ⇒ splice retain, insert delete
2464
+ const delLen = math.min(opsI.length - offset, remainingLen)
2465
+ _shrinkChild(this, opsI, offset, delLen)
2466
+ if (opsI.length === 0) {
2467
+ _removeChild(this, opsI)
2468
+ opsI = opNextUndeleted(opsI)
2469
+ offset = 0
2470
+ }
2471
+ if (opsI?.length === offset) {
2472
+ opsI = opNextUndeleted(opsI)
1606
2473
  offset = 0
1607
- opsI = opsI.next
1608
- } else {
1609
- offset += remainingLen
1610
2474
  }
2475
+ insertClonedOp(new DeleteOp(delLen))
1611
2476
  remainingLen -= delLen
1612
- } else { // insert / embed / retain / modify replace
2477
+ } else if ($modifyOp.check(opsI)) { // modify delete the source position it stands for
2478
+ // a modify op addresses an underlying source position (retain + change), so deleting it
2479
+ // must emit a DeleteOp for that position - exactly like the retain case above. (The
2480
+ // insert/embed branch below instead just cancels the inserted content, because that
2481
+ // content is new and has no source position to delete.)
2482
+ _removeChild(this, opsI)
2483
+ opsI = opNextUndeleted(opsI)
2484
+ offset = 0
2485
+ insertClonedOp(new DeleteOp(1))
2486
+ remainingLen -= 1
2487
+ } else if (!$deleteOp.check(opsI)) { // insert / embed ⇒ replace
1613
2488
  // case1: delete o fully
1614
2489
  // case2: delete some part of beginning
1615
2490
  // case3: delete some part of end
1616
2491
  // case4: delete some part of center
1617
2492
  const delLen = math.min(opsI.length - offset, remainingLen)
1618
- this.childCnt -= delLen
2493
+ // deleting content drops any marks in the removed embeds; the conservative `maybeHasMarks`
2494
+ // flag is left as-is (never decremented - marksToPositions self-corrects it)
1619
2495
  if (opsI.length === delLen) {
1620
- // case 1
2496
+ // case 1: delete opsI fully
2497
+ _removeChild(this, opsI)
2498
+ scheduleForMerge(opsI = opNextUndeleted(opsI))
1621
2499
  offset = 0
1622
- scheduleForMerge(opsI.next)
1623
- list.remove(this.children, opsI)
1624
- opsI = opsI.next
1625
2500
  } else if (offset === 0) {
1626
- // case 2
1627
- offset = 0
1628
- opsI._splice(0, delLen)
2501
+ // case 2: delete from the beginning
2502
+ _shrinkChild(this, opsI, offset, delLen)
1629
2503
  } else if (offset + delLen === opsI.length) {
1630
- // case 3
1631
- opsI._splice(offset, delLen)
2504
+ // case 3: delete to the end
2505
+ _shrinkChild(this, opsI, offset, delLen)
2506
+ opsI = opNextUndeleted(opsI)
1632
2507
  offset = 0
1633
- opsI = opsI.next
1634
2508
  } else {
1635
- // case 4
1636
- opsI._splice(offset, delLen)
2509
+ // case 4: delete from the center
2510
+ _shrinkChild(this, opsI, offset, delLen)
1637
2511
  }
1638
2512
  remainingLen -= delLen
2513
+ /* c8 ignore start */
2514
+ } else {
2515
+ // unreachable: opsI was already typed as retain | non-delete-content | delete above
2516
+ error.unexpectedCase()
1639
2517
  }
2518
+ /* c8 ignore stop */
1640
2519
  }
1641
2520
  } else if ($modifyOp.check(op)) {
1642
- if (opsI == null) {
1643
- list.pushEnd(this.children, op.clone())
1644
- this.childCnt += 1
1645
- return
2521
+ if (opsI != null && (op.format !== undefined || op.attribution !== undefined) && (!$deleteOp.check(opsI) && !$retainOp.check(opsI))) { // retain handles splitting seperately, without copying attrs
2522
+ splitHere()
2523
+ if (opsI.length > 1) {
2524
+ offset = 1
2525
+ splitHere()
2526
+ opsI = /** @type {InsertOp<any>} */ (opsI.prev)
2527
+ }
2528
+ // at this point, opsI is guaranteed to be !deleteOp && !retainOp and of length 1
2529
+ updateOpFormat(opsI, op.format, op.attribution)
2530
+ scheduleForMerge(opsI)
1646
2531
  }
1647
- if ($modifyAttrOp.check(opsI)) {
1648
- opsI._modValue.apply(/** @type {any} */ (op.value))
2532
+ if (opsI == null) {
2533
+ _insertChild(this, null, move ? op : op.clone(0, 1, keep))
2534
+ this.maybeHasMarks ||= /** @type {DeltaAny} */ (op.value).maybeHasMarks // a freshly-inserted modify brings its value's root marks
2535
+ } else if ($modifyOp.check(opsI)) {
2536
+ const tgt = opsI._modValue
2537
+ tgt.apply(/** @type {any} */ (op.value), { final, move })
2538
+ this.maybeHasMarks ||= tgt.maybeHasMarks // flag the child subtree's marks up (incl. the value's own root marks)
2539
+ opsI = opNextUndeleted(opsI)
1649
2540
  } else if ($insertOp.check(opsI)) {
1650
- opsI._modValue(offset).apply(op.value)
1651
- } else if ($retainOp.check(opsI)) {
1652
- if (offset > 0) {
1653
- const cpy = scheduleForMerge(opsI.clone(0, offset)) // skipped len
1654
- opsI._splice(0, offset) // new remainder
1655
- list.insertBetween(this.children, opsI.prev, opsI, cpy) // insert skipped len
2541
+ const tgt = opsI._modValue(offset)
2542
+ tgt.apply(op.value, { final, move })
2543
+ this.maybeHasMarks ||= tgt.maybeHasMarks // flag the child subtree's marks up (incl. the value's own root marks)
2544
+ if (opsI.length === ++offset) {
2545
+ opsI = opNextUndeleted(opsI)
1656
2546
  offset = 0
1657
2547
  }
1658
- list.insertBetween(this.children, opsI.prev, opsI, scheduleForMerge(op.clone())) // insert skipped len
2548
+ } else if ($retainOp.check(opsI)) {
2549
+ splitHere()
2550
+ const insertModOp = scheduleForMerge(move ? op : op.clone(0, 1, keep))
2551
+ this.maybeHasMarks ||= /** @type {DeltaAny} */ (op.value).maybeHasMarks // the inserted modify brings its value's root marks
2552
+ ;(opsI.format !== undefined || opsI.attribution !== undefined) && updateOpFormat(insertModOp, opsI.format, opsI.attribution)
2553
+ // the modify replaces one retain position with a length-1 modify (net childCnt: +1 here, -1 below)
2554
+ _insertChild(this, opsI, insertModOp)
1659
2555
  if (opsI.length === 1) {
1660
- list.remove(this.children, opsI)
2556
+ _removeChild(this, opsI)
2557
+ opsI = opNextUndeleted(opsI)
2558
+ offset = 0
1661
2559
  } else {
1662
- opsI._splice(0, 1)
2560
+ _shrinkChild(this, opsI, 0, 1)
1663
2561
  scheduleForMerge(opsI)
1664
2562
  }
1665
- } else if ($deleteOp.check(opsI)) {
1666
- // nop
2563
+ /* c8 ignore start */
1667
2564
  } else {
2565
+ // remaining branches: opsI is deleteOp or something unknown
2566
+ // both branches are unreachable today: opNextUndeleted skips
2567
+ // delete ops, so opsI is never a delete during iteration; and the four
2568
+ // branches above exhaust the other op kinds. The deleteOp branch is
2569
+ // kept as a defensive no-op (drops a modify that lands in a deleted
2570
+ // region) rather than a throw.
1668
2571
  error.unexpectedCase()
1669
2572
  }
1670
2573
  } else {
1671
2574
  error.unexpectedCase()
1672
2575
  }
1673
- })
1674
- maybeMergeable.forEach(op => {
2576
+ /* c8 ignore stop */
2577
+ }
2578
+ // iterate backwards, to ensure that we merge all content
2579
+ for (let i = maybeMergeable.length - 1; i >= 0; i--) {
2580
+ const op = maybeMergeable[i]
1675
2581
  // check if this is still integrated
1676
- if (op.prev?.next === op) {
1677
- tryMergeWithPrev(this.children, op)
1678
- op.next && tryMergeWithPrev(this.children, op.next)
2582
+ if (op.prev != null ? op.prev.next === op : this.children.start === op) {
2583
+ op.prev && _mergeChildWithPrev(this, op)
2584
+ op.next && _mergeChildWithPrev(this, op.next)
1679
2585
  }
1680
- })
2586
+ }
2587
+ // marks: shift this node's own leaf marks by the change, then fold in root-level mark adds/deletes
2588
+ // (child subtree marks already flagged `maybeHasMarks` incrementally above)
2589
+ shiftMarksByChange(this, other)
2590
+ // pass `final` so a materializing apply (e.g. a deltaRDT's final state) applies deletes in place
2591
+ // but collects no pending `deleteMarks`; a non-final change delta still records them (transmittable)
2592
+ applyMarkOps(this, other.marks, other.deleteMarks, final)
1681
2593
  return this
1682
2594
  }
1683
2595
 
1684
2596
  /**
2597
+ * Rebase this op against a concurrent op. We can apply this op on a doc that already applied
2598
+ * `other` op.
2599
+ *
1685
2600
  * @param {DeltaAny} other
1686
2601
  * @param {boolean} priority
1687
2602
  */
@@ -1720,9 +2635,12 @@ export class DeltaBuilder extends Delta {
1720
2635
  // @ts-ignore
1721
2636
  delete this.attrs[otherOp.key]
1722
2637
  }
2638
+ /* c8 ignore start */
1723
2639
  } else {
2640
+ // unreachable: attr ops are exhaustively setAttr | deleteAttr | modifyAttr
1724
2641
  error.unexpectedCase()
1725
2642
  }
2643
+ /* c8 ignore stop */
1726
2644
  }
1727
2645
  /**
1728
2646
  * Rebase children.
@@ -1749,10 +2667,9 @@ export class DeltaBuilder extends Delta {
1749
2667
  * - insert: transform based on priority
1750
2668
  * - retain/delete/modify: transform next op against other
1751
2669
  */
1752
- if ($insertOp.check(otherChild) || $modifyOp.check(otherChild) || $textOp.check(otherChild)) {
2670
+ if ($insertOp.check(otherChild) || $textOp.check(otherChild)) {
1753
2671
  if (!priority) {
1754
- list.insertBetween(this.children, currChild.prev, currChild, new RetainOp(otherChild.length, null, null))
1755
- this.childCnt += otherChild.length
2672
+ _insertChild(this, currChild, new RetainOp(otherChild.length, undefined, undefined))
1756
2673
  // curr is transformed against other, transform curr against next
1757
2674
  otherOffset = otherChild.length
1758
2675
  } else {
@@ -1773,16 +2690,18 @@ export class DeltaBuilder extends Delta {
1773
2690
  if ($insertOp.check(otherChild) || $textOp.check(otherChild)) {
1774
2691
  // @todo: with all list changes (retain insertions, removal), try to merge the surrounding
1775
2692
  // ops later
1776
- list.insertBetween(this.children, currChild.prev, currChild, new RetainOp(otherChild.length, null, null))
1777
- this.childCnt += otherChild.length
2693
+ _insertChild(this, currChild, new RetainOp(otherChild.length, undefined, undefined))
1778
2694
  // curr is transformed against other, transform curr against next
1779
2695
  otherOffset = otherChild.length
1780
2696
  } else {
1781
2697
  if ($modifyOp.check(otherChild)) {
1782
- /** @type {any} */ (currChild.value).rebase(otherChild, priority)
2698
+ // _modValue (not .value) — ModifyOp.clone() marks its inner delta
2699
+ // as `done`, so a cloned ModifyOp can only be rebased after the
2700
+ // _modValue getter lazy-clones it back to mutable. This recursion also reconciles the
2701
+ // value's own root marks (via rebaseRootMarks), so a nested mark needs no special handling.
2702
+ currChild._modValue.rebase(otherChild.value, priority)
1783
2703
  } else if ($deleteOp.check(otherChild)) {
1784
- list.remove(this.children, currChild)
1785
- this.childCnt -= 1
2704
+ _removeChild(this, currChild)
1786
2705
  }
1787
2706
  currOffset += 1
1788
2707
  otherOffset += 1
@@ -1796,25 +2715,66 @@ export class DeltaBuilder extends Delta {
1796
2715
  * - insert: split curr op and insert retain
1797
2716
  */
1798
2717
  if ($retainOp.check(otherChild) || $modifyOp.check(otherChild)) {
2718
+ // Format reconciliation over the [currOffset..currOffset+maxCommonLen] overlap. Split currChild
2719
+ // around the overlap so the prefix/suffix keep their original format and only the middle piece
2720
+ // changes. Three rules:
2721
+ // 1. A blanket `null` clear ALWAYS wins, priority-independent (this is what lets it converge — both
2722
+ // rebase orderings agree the overlap ends up cleared, at the cost of dropping a concurrent set;
2723
+ // hence `null` is a local-only utility, see the `Formats` docs). currChild's own
2724
+ // `null` clear is left untouched (it wins); if *other* clears, currChild concedes its whole format.
2725
+ // 2. Otherwise (both per-key objects) priority decides: priority=true is a no-op (currChild wins);
2726
+ // for !priority currChild concedes any key otherChild also writes (a `{k:null}` removal is just
2727
+ // such a key, so per-key removals reconcile here and converge without data loss — the recommended
2728
+ // way to clear).
2729
+ // 3. otherChild `undefined` (skip) never changes currChild.
2730
+ if ($retainOp.check(currChild) && currChild.format !== null) {
2731
+ let changed = false
2732
+ /** @type {Formats|null|undefined} */
2733
+ let newFormat
2734
+ if (otherChild.format === null) {
2735
+ // other clears all → clear wins → currChild concedes its entire format
2736
+ if (currChild.format !== undefined) { newFormat = undefined; changed = true }
2737
+ } else if (!priority && currChild.format != null && otherChild.format != null) {
2738
+ /** @type {Formats} */
2739
+ const stripped = {}
2740
+ for (const k in currChild.format) {
2741
+ if (k in otherChild.format) changed = true
2742
+ else stripped[k] = currChild.format[k]
2743
+ }
2744
+ if (changed) newFormat = object.isEmpty(stripped) ? undefined : stripped
2745
+ }
2746
+ if (changed) {
2747
+ // split off the suffix [currOffset+maxCommonLen..length] if any
2748
+ if (currOffset + maxCommonLen < currChild.length) {
2749
+ _splitChildAt(this, currChild, currOffset + maxCommonLen)
2750
+ }
2751
+ // split off the prefix [0..currOffset] if any: currChild becomes the [currOffset..] suffix
2752
+ if (currOffset > 0) {
2753
+ currChild = _splitChildAt(this, currChild, currOffset)
2754
+ currOffset = 0
2755
+ }
2756
+ // currChild now spans exactly the overlap. A conceded format is `undefined` (skip / no format
2757
+ // change), NOT `null` (which would CLEAR the format on apply).
2758
+ /** @type {any} */ (currChild).format = newFormat
2759
+ currChild._fingerprint = null
2760
+ }
2761
+ }
1799
2762
  currOffset += maxCommonLen
1800
2763
  otherOffset += maxCommonLen
1801
2764
  } else if ($deleteOp.check(otherChild)) {
1802
- if ($retainOp.check(currChild)) {
1803
- // @ts-ignore
1804
- currChild.retain -= maxCommonLen
1805
- } else if ($deleteOp.check(currChild)) {
1806
- currChild.delete -= maxCommonLen
1807
- }
1808
- this.childCnt -= maxCommonLen
1809
- } else { // insert/text.check(currOp)
2765
+ // currChild is Retain | Delete here (outer branch); shrink it by the overlap
2766
+ _shrinkChild(this, currChild, currOffset, maxCommonLen)
2767
+ // advance other so subsequent currChild ops see what comes AFTER this
2768
+ // delete; without this we'd loop against the same delete forever and
2769
+ // never reach other's later inserts.
2770
+ otherOffset += maxCommonLen
2771
+ } else { // insert/text.check(otherChild)
1810
2772
  if (currOffset > 0) {
1811
- const leftPart = currChild.clone(currOffset)
1812
- list.insertBetween(this.children, currChild.prev, currChild, leftPart)
1813
- currChild._splice(currOffset, currChild.length - currOffset)
2773
+ // split currChild so the cursor sits on the boundary; continue on the [currOffset..] suffix
2774
+ currChild = _splitChildAt(this, currChild, currOffset)
1814
2775
  currOffset = 0
1815
2776
  }
1816
- list.insertBetween(this.children, currChild.prev, currChild, new RetainOp(otherChild.length, null, null))
1817
- this.childCnt += otherChild.length
2777
+ _insertChild(this, currChild, new RetainOp(otherChild.length, undefined, undefined))
1818
2778
  otherOffset = otherChild.length
1819
2779
  }
1820
2780
  }
@@ -1827,6 +2787,13 @@ export class DeltaBuilder extends Delta {
1827
2787
  otherOffset = 0
1828
2788
  }
1829
2789
  }
2790
+ // marks: reconcile this node's own root mark ops against the concurrent change (also shifting and,
2791
+ // where an anchor was deleted, converting an add to a delete). No count to maintain: `maybeHasMarks`
2792
+ // is conservative (rebase only drops/reconciles marks, never adds), so a stale `true` is harmless
2793
+ // and marksToPositions self-corrects it.
2794
+ if (this.marks !== null || this.deleteMarks !== null) {
2795
+ rebaseRootMarks(this, other, priority)
2796
+ }
1830
2797
  return this
1831
2798
  }
1832
2799
 
@@ -1857,43 +2824,576 @@ export class DeltaBuilder extends Delta {
1857
2824
  * >, FixedConf>}
1858
2825
  */
1859
2826
  append (other) {
1860
- const children = this.children
1861
- const prevLast = children.end
2827
+ const prevLast = this.children.end
1862
2828
  // @todo Investigate. Above is a typescript issue. It is necessary to cast OtherDelta to a Delta first before
1863
2829
  // inferring type, otherwise Children will contain Text.
1864
2830
  for (const child of other.children) {
1865
- list.pushEnd(children, child.clone())
2831
+ _insertChild(this, null, child.clone())
1866
2832
  }
1867
- this.childCnt += other.childCnt
1868
- prevLast?.next && tryMergeWithPrev(children, prevLast.next)
2833
+ // appended children may carry marks in their subtrees - flag conservatively
2834
+ this.maybeHasMarks ||= other.maybeHasMarks
2835
+ prevLast?.next && _mergeChildWithPrev(this, prevLast.next)
1869
2836
  // @ts-ignore
1870
2837
  return this
1871
2838
  }
1872
2839
  }
1873
2840
 
1874
2841
  /**
1875
- * @param {ChildrenOpAny} op
1876
- * @param {{[k:string]:any}} formatUpdate
1877
- */
1878
- const updateOpFormat = (op, formatUpdate) => {
1879
- if (!$deleteOp.check(op)) {
1880
- // apply formatting attributes
1881
- for (const k in formatUpdate) {
1882
- const v = formatUpdate[k]
1883
- if (v != null || $retainOp.check(op)) {
1884
- // never modify formats
1885
- /** @type {any} */ (op).format = object.assign({}, op.format, { [k]: v })
1886
- } else if (op.format != null) {
1887
- const { [k]: _, ...rest } = op.format
1888
- ;/** @type {any} */ (op).format = rest
2842
+ * Apply one tri-state dimension update (`format` or `attribution`) to `op[field]` and report whether it
2843
+ * changed. `undefined` = skip; `null` = clear all keys; an object merges per-key (`{k:v}` sets, `{k:null}`
2844
+ * removes key `k`). `format` and `attribution` are handled identically (the unified model).
2845
+ *
2846
+ * On an **instruction** op (`retain`/`modify`) the merge is kept verbatim — a `{k:null}` removal and a
2847
+ * `null` clear must survive so they still apply when this op is later consumed (an empty merge result `{}`
2848
+ * is a no-op → `undefined`/skip). On a **data** op (`insert`/`text`/attr op) the merge is resolved against
2849
+ * the stored value (empty → `null`/none). (Limitation: a `null` clear already present on an instruction op
2850
+ * does not compose with a subsequent set into a single op — only the set survives. The diff never produces
2851
+ * this; it would need arbitrary clear-then-set change composition.)
2852
+ *
2853
+ * @param {_OpAny} op
2854
+ * @param {'format'|'attribution'} field
2855
+ * @param {{[k:string]:any}|null|undefined} update
2856
+ * @return {boolean} whether `op[field]` was touched
2857
+ */
2858
+ const applyDim = (op, field, update) => {
2859
+ if (update === undefined) return false
2860
+ if (update === null) {
2861
+ /** @type {any} */ (op)[field] = null // clear all
2862
+ } else if ($retainOp.check(op) || $modifyOp.check(op)) {
2863
+ // instruction op: keep the merge verbatim (preserves `{k:null}` removals to apply later). Attribution
2864
+ // additionally merges its nested `format` key one level (see {@link mergeAttr}); format stays shallow.
2865
+ const cur = /** @type {any} */ (op)[field]
2866
+ const merged = field === 'attribution' ? mergeAttr(cur, update, false) : object.assign({}, cur, update)
2867
+ ;/** @type {any} */ (op)[field] = object.isEmpty(merged) ? undefined : merged
2868
+ } else if (field === 'attribution') {
2869
+ // data op, attribution: resolve per key; the nested `format` key merges one level (see {@link mergeAttr})
2870
+ const merged = mergeAttr(/** @type {any} */ (op)[field], update, true)
2871
+ ;/** @type {any} */ (op)[field] = object.isEmpty(merged) ? null : merged
2872
+ } else {
2873
+ // data op (format): resolve per-key against the stored value (`{k:v}` sets, `{k:null}` removes, `{k:undefined}` skips)
2874
+ let f = /** @type {any} */ (op)[field]
2875
+ for (const k in update) {
2876
+ const v = update[k]
2877
+ if (v === undefined) continue // skip this key
2878
+ if (v === null) { // remove this key (no-op when the stored value is already `null`)
2879
+ if (f !== null) {
2880
+ const { [k]: _, ...rest } = f
2881
+ f = object.isEmpty(rest) ? null : rest
2882
+ }
2883
+ } else { // set this key
2884
+ f = object.assign({}, f, { [k]: v })
1889
2885
  }
1890
2886
  }
2887
+ /** @type {any} */ (op)[field] = f
2888
+ }
2889
+ return true
2890
+ }
2891
+
2892
+ /**
2893
+ * Compute the tri-state update that turns stored value `aVal` into `bVal` (used by {@link diff} for both
2894
+ * `format` and `attribution`): `undefined` when unchanged (skip), `null` when fully cleared, else a
2895
+ * per-key object (`{k:v}` for changed/added keys, `{k:null}` for removed keys). With `deep` (attribution),
2896
+ * the nested `format` key is diffed one level deeper so it round-trips through {@link mergeAttr}'s per-inner-key
2897
+ * merge instead of a wholesale replace.
2898
+ *
2899
+ * @param {{[k:string]:any}|null|undefined} aVal
2900
+ * @param {{[k:string]:any}|null|undefined} bVal
2901
+ * @param {boolean} deep `true` for `attribution` (recurse its `format` key), `false` for `format` (shallow)
2902
+ * @return {{[k:string]:any}|null|undefined}
2903
+ */
2904
+ const diffDim = (aVal, bVal, deep) => {
2905
+ if (fun.equalityDeep(aVal, bVal)) return undefined
2906
+ if ((bVal === null || bVal === undefined || object.isEmpty(bVal)) && (deep || aVal == null)) {
2907
+ return null // fully cleared: attribution (`deep`) clears wholesale; format with no prior keys has nothing to enumerate
2908
+ }
2909
+ /** @type {{[k:string]:any}} */
2910
+ const u = {}
2911
+ for (const k in bVal) {
2912
+ if (!fun.equalityDeep(bVal[k], aVal?.[k] ?? null)) {
2913
+ // attribution's nested `format` diffs one level deeper (mirrors mergeAttr); everything else is wholesale
2914
+ u[k] = deep && k === 'format' && s.$objectAny.check(bVal[k]) && s.$objectAny.check(aVal?.[k])
2915
+ ? diffDim(aVal[k], bVal[k], false)
2916
+ : bVal[k]
2917
+ }
2918
+ }
2919
+ // removed keys. For `format` (shallow) a full clear enumerates one `{k:null}` per prior key instead of a
2920
+ // wholesale `format:null` — individual clears rebase without clobbering concurrently-added formats.
2921
+ // (`for..in` is a no-op when `aVal` is nullish; `k in bVal` is guarded against a nullish `bVal`.)
2922
+ for (const k in aVal) { if (bVal == null || !(k in bVal)) u[k] = null }
2923
+ return u
2924
+ }
2925
+
2926
+ /**
2927
+ * Apply a `format` and/or `attribution` update to an op (both unified tri-state — see {@link applyDim}).
2928
+ * `format`/`attribution` are fingerprinted, so any change nulls the op's `_fingerprint`.
2929
+ *
2930
+ * @param {ChildrenOpAny} op
2931
+ * @param {{[k:string]:any}|null|undefined} formatUpdate
2932
+ * @param {{[k:string]:any}|null|undefined} attributionUpdate
2933
+ */
2934
+ const updateOpFormat = (op, formatUpdate, attributionUpdate) => {
2935
+ if ($deleteOp.check(op)) return
2936
+ const changedF = applyDim(op, 'format', formatUpdate)
2937
+ const changedA = applyDim(op, 'attribution', attributionUpdate)
2938
+ if (changedF || changedA) {
2939
+ /** @type {any} */ (op)._fingerprint = null
2940
+ }
2941
+ }
2942
+
2943
+ /**
2944
+ * @param {ChildrenOpAny?} op
2945
+ */
2946
+ const opNextUndeleted = op => {
2947
+ op = op?.next || null
2948
+ while (op != null && $deleteOp.check(op)) {
2949
+ op = op.next
2950
+ }
2951
+ return op
2952
+ }
2953
+
2954
+ /**
2955
+ * Total order on marks by their (unique) id, used to sort a mark list deterministically (e.g. in
2956
+ * {@link Delta#toJSON}).
2957
+ *
2958
+ * @param {Mark} a
2959
+ * @param {Mark} b
2960
+ */
2961
+ const compareMarksById = (a, b) => a.id < b.id ? -1 : 1
2962
+
2963
+ /**
2964
+ * Position-step order for a mark's `key`, mirroring the fingerprint key sort (see {@link Delta#fingerprint}):
2965
+ * numbers (child indices) first, ascending; then string (attribute) keys, lexicographically.
2966
+ *
2967
+ * @param {number|string} a
2968
+ * @param {number|string} b
2969
+ */
2970
+ const cmpKey = (a, b) =>
2971
+ s.$number.check(a) ? (s.$number.check(b) ? a - b : -1) : s.$number.check(b) ? 1 : (a < b ? -1 : a > b ? 1 : 0)
2972
+
2973
+ /**
2974
+ * Total order over the marks of one node: by position-step {@link cmpKey}, tie-broken by the (unique)
2975
+ * id so the order is fully deterministic even when two cursors share a key. Used by {@link sortMarks}.
2976
+ *
2977
+ * @param {Mark} a
2978
+ * @param {Mark} b
2979
+ */
2980
+ const cmpMarkKey = (a, b) => cmpKey(a.key, b.key) || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)
2981
+
2982
+ /**
2983
+ * Sort a {@link Marks} set in place into its canonical {@link cmpMarkKey} order and return it. Called
2984
+ * lazily whenever the set is iterated, so every reader (e.g.
2985
+ * {@link import('./position.js').marksToPositions}) sees one deterministic, position-ordered sequence
2986
+ * regardless of the order marks were added in. Order is not part of a delta's identity, so reordering
2987
+ * is safe even on a `done` delta.
2988
+ *
2989
+ * @param {Marks} marks
2990
+ */
2991
+ const sortMarks = marks => { marks._marks.sort(cmpMarkKey); return marks }
2992
+
2993
+ /**
2994
+ * The set of leaf {@link Mark marks} on a single delta node, deduplicated by id (so adding the same id
2995
+ * again replaces it). An internal data-shape class (never a public/`new`-from-outside class): every
2996
+ * mutation goes through its methods, so a stored {@link Mark} is treated as immutable and is never
2997
+ * manipulated in place — to "move" a mark you replace it with a fresh `Mark`. {@link clone}/{@link
2998
+ * slice} copy the set so a `done` delta's marks are never mutated through a shared reference. Marks are
2999
+ * local/ephemeral cursor state
3000
+ * and not part of a delta's identity, so this set has no fingerprint/equality of its own.
3001
+ */
3002
+ class Marks {
3003
+ constructor () {
3004
+ /**
3005
+ * @type {Array<Mark>}
3006
+ */
3007
+ this._marks = []
3008
+ }
3009
+
3010
+ get size () { return this._marks.length }
3011
+
3012
+ /**
3013
+ * Add or replace `mark` (by id). Returns `true` if it was added as a new mark, `false` if it
3014
+ * replaced an existing one with the same id.
3015
+ *
3016
+ * @param {Mark} mark
3017
+ * @return {boolean}
3018
+ */
3019
+ add (mark) {
3020
+ const i = this._marks.findIndex(m => m.id === mark.id)
3021
+ if (i < 0) {
3022
+ this._marks.push(mark)
3023
+ return true
3024
+ }
3025
+ this._marks[i] = mark
3026
+ return false
3027
+ }
3028
+
3029
+ /**
3030
+ * Remove the mark with `id`. Returns `true` if a mark was present and removed, `false` if absent.
3031
+ *
3032
+ * @param {string} id
3033
+ * @return {boolean}
3034
+ */
3035
+ delete (id) {
3036
+ const i = this._marks.findIndex(m => m.id === id)
3037
+ if (i < 0) return false
3038
+ this._marks.splice(i, 1)
3039
+ return true
3040
+ }
3041
+
3042
+ [Symbol.iterator] () { sortMarks(this); return this._marks[Symbol.iterator]() }
3043
+ }
3044
+
3045
+ /**
3046
+ * Whether any delta element in `insert` maybe carries a mark in its subtree (short-circuits). Used to
3047
+ * OR-propagate the conservative {@link DeltaData#maybeHasMarks} flag when content is inserted.
3048
+ *
3049
+ * @param {Array<any>} insert
3050
+ * @return {boolean}
3051
+ */
3052
+ const elemsMaybeHaveMarks = insert => insert.some(el => $deltaAny.check(el) && el.maybeHasMarks)
3053
+
3054
+ /**
3055
+ * Add (or replace) a {@link Mark} on `node`, flagging {@link DeltaData#maybeHasMarks}. Adding a mark
3056
+ * cancels any pending delete of the same id on `node` (so a node never holds both an add and a delete
3057
+ * for one id — the add wins), keeping the apply path consistent with rebase's add-vs-delete rule.
3058
+ *
3059
+ * @param {DeltaAny} node
3060
+ * @param {Mark} mark
3061
+ */
3062
+ const addMarkTo = (node, mark) => {
3063
+ const marks = node.marks ?? new Marks()
3064
+ marks.add(mark)
3065
+ node.marks = marks
3066
+ node.maybeHasMarks = true
3067
+ if (node.deleteMarks !== null) {
3068
+ node.deleteMarks.delete(mark.id)
3069
+ if (node.deleteMarks.size === 0) node.deleteMarks = null
3070
+ }
3071
+ }
3072
+
3073
+ /**
3074
+ * Mirror of {@link addMarkTo}: assert mark `id` is *absent* on `node` (last-writer-wins). Strips a
3075
+ * present add of `id` (a delete cancels a pending add — newest wins), then, only for a CHANGE delta
3076
+ * (`record`, i.e. a non-final apply), records the transmittable tombstone in `deleteMarks`. A
3077
+ * final/materializing apply passes `record === false`: the mark is removed in place but no tombstone is
3078
+ * collected (a final delta carries none — see {@link DeltaData#deleteMarks}). The `maybeHasMarks` flag is
3079
+ * intentionally left as-is (never decremented - {@link import('./position.js').marksToPositions}
3080
+ * self-corrects it). Together with `addMarkTo` these are the only two writers of `marks`/`deleteMarks`,
3081
+ * so a node can never hold both an add and a delete for one id.
3082
+ *
3083
+ * @param {DeltaAny} node
3084
+ * @param {string} id
3085
+ * @param {boolean} record whether to keep a transmittable `deleteMarks` tombstone (`= !final`)
3086
+ */
3087
+ const deleteMarkTo = (node, id, record) => {
3088
+ if (node.marks !== null) {
3089
+ node.marks.delete(id)
3090
+ if (node.marks.size === 0) node.marks = null
3091
+ }
3092
+ if (record) {
3093
+ const dm = node.deleteMarks ?? new Set()
3094
+ dm.add(id)
3095
+ node.deleteMarks = dm
3096
+ }
3097
+ }
3098
+
3099
+ /**
3100
+ * Apply mark add/delete ops to `target` (flagging `target.maybeHasMarks` on add). `addMarks` may be a
3101
+ * {@link Marks} set or a plain array; `deleteMarks` is a set of ids. The two id-keyed primitives
3102
+ * {@link deleteMarkTo}/{@link addMarkTo} are the only writers of `marks`/`deleteMarks`, so a node can
3103
+ * never end holding both for one id. Deletes are processed before adds so that a single change carrying
3104
+ * both for one id resolves to *add wins* (the add re-runs last and strips the just-recorded tombstone),
3105
+ * matching the rebase policy. `final` marks a document-materializing apply (propagated from
3106
+ * {@link DeltaBuilder#apply}, defaulting to `this.isFinal`): a settled document removes a present mark in
3107
+ * place but records no tombstone, while a non-final change delta records the delete (passed as
3108
+ * `record = !final`) so `create().removeMark(pos, id)` stays transmittable.
3109
+ *
3110
+ * @param {DeltaAny} target
3111
+ * @param {Iterable<Mark>?} addMarks
3112
+ * @param {Set<string>?} deleteMarks
3113
+ * @param {boolean} final whether this is a final (document-materializing) apply
3114
+ */
3115
+ const applyMarkOps = (target, addMarks, deleteMarks, final) => {
3116
+ if (deleteMarks !== null) {
3117
+ for (const id of deleteMarks) deleteMarkTo(target, id, !final)
3118
+ }
3119
+ if (addMarks !== null) {
3120
+ for (const m of addMarks) addMarkTo(target, m)
3121
+ }
3122
+ }
3123
+
3124
+ /**
3125
+ * Re-key `node`'s *root* marks in place: each mark's `key` is mapped through `mapKey` (return `null` to
3126
+ * drop it, an unchanged key to keep it, or a new key to move it). Used after a full {@link clone} by an
3127
+ * attribute-renaming transformer so a mark on a renamed attribute follows the rename (and a mark on a
3128
+ * dropped attribute is dropped). `node.maybeHasMarks` is left set (conservative; never decremented).
3129
+ *
3130
+ * @param {DeltaAny} node
3131
+ * @param {(key: number|string) => number|string|null} mapKey
3132
+ * @internal transformer plumbing (used by mark-carrying transformers), not consumer API.
3133
+ */
3134
+ export const remapRootMarks = (node, mapKey) => {
3135
+ const marks = node.marks
3136
+ if (marks === null) return
3137
+ for (const m of [...marks]) {
3138
+ const k = mapKey(m.key)
3139
+ if (k === m.key) continue
3140
+ deleteMarkTo(node, m.id, false) // relocate a present mark: strip it, but synthesize no tombstone
3141
+ if (k !== null) addMarkTo(node, m.copy(k))
1891
3142
  }
1892
3143
  }
1893
3144
 
3145
+ /**
3146
+ * Add (or replace, by id) a single root {@link Mark} on `node`, flagging `node.maybeHasMarks`. The
3147
+ * public wrapper of the internal `addMarkTo`, used by the restructuring
3148
+ * {@link import('./transformer/core.js').Transformer transformers} (`inline`, `project`) to place a
3149
+ * mark at a position they compute themselves.
3150
+ *
3151
+ * @param {DeltaAny} node
3152
+ * @param {Mark} mark
3153
+ * @internal transformer plumbing (used by `inline`/`project`), not consumer API.
3154
+ */
3155
+ export const addRootMark = (node, mark) => addMarkTo(node, mark)
3156
+
3157
+ /**
3158
+ * Mark id `id` as deleted on `node` (the mirror of {@link addRootMark}): the public wrapper of the
3159
+ * internal {@link deleteMarkTo} with `record === true`, used by the restructuring
3160
+ * {@link import('./transformer/core.js').Transformer transformers} (`inline`, `project`) to carry a mark
3161
+ * *delete* (which is id-keyed, so it needs no position) onto the node they build. Strips a present add of
3162
+ * the same id (last-writer-wins) so the node never holds both.
3163
+ *
3164
+ * @param {DeltaAny} node
3165
+ * @param {string} id
3166
+ * @internal transformer plumbing (used by `inline`/`project`), not consumer API.
3167
+ */
3168
+ export const deleteRootMark = (node, id) => deleteMarkTo(node, id, true)
3169
+
3170
+ /**
3171
+ * Merge `source`'s *root* marks onto `target`, mapping each add mark's `key` through `mapKey` (return
3172
+ * `null` to drop it, a new key to move it via {@link Mark#copy}). `source`'s `deleteMarks` are merged
3173
+ * verbatim through {@link deleteMarkTo} — a delete is keyed only by its (cross-side-stable) id, so it
3174
+ * maps in either direction and strips a conflicting add on `target` (last-writer-wins, never both). Safe
3175
+ * to call repeatedly to accumulate marks from several sources onto one target (e.g.
3176
+ * {@link import('./transformer/inline.js') inline} lifting the marks of every spliced node onto the
3177
+ * flattened parent), and — onto a fresh target — to copy a node's marks across a key remapping (e.g.
3178
+ * {@link cloneShallow}, and the `attr` transformer following an attribute rename). `target.maybeHasMarks`
3179
+ * is flagged for the added marks; marks inside copied delta-valued attributes/children are flagged when
3180
+ * the caller appends them via the builder (and {@link cloneShallow} copies the flag wholesale).
3181
+ *
3182
+ * @param {DeltaAny} target
3183
+ * @param {DeltaAny} source
3184
+ * @param {(key: number|string) => number|string|null} [mapKey]
3185
+ * @internal transformer plumbing (used by `inline`/`attr`/`cloneShallow`), not consumer API.
3186
+ */
3187
+ export const mergeRootMarks = (target, source, mapKey = k => k) => {
3188
+ if (source.marks !== null) {
3189
+ for (const m of source.marks) {
3190
+ const k = mapKey(m.key)
3191
+ if (k !== null) addMarkTo(target, k === m.key ? m : m.copy(k))
3192
+ }
3193
+ }
3194
+ if (source.deleteMarks !== null) {
3195
+ for (const id of source.deleteMarks) deleteMarkTo(target, id, true)
3196
+ }
3197
+ }
3198
+
3199
+ /**
3200
+ * Map a number `key` (a content offset) of a leaf mark on a node through the content ops of `change`,
3201
+ * returning its new offset. An insert before the key pushes it right (tie-broken at the exact offset by
3202
+ * `assoc`). A delete covering the key applies *collapse-to-cut*: the mark slides to the cut point
3203
+ * (`newPos`) — i.e. to *before* any replacement text inserted there — instead of being dropped, so a
3204
+ * cursor whose content is deleted survives at the deletion site. `oldPos` walks the pre-change doc and
3205
+ * `newPos` the post-change doc *independently* — `key` is never mutated, so an insert past the key is
3206
+ * always compared against the mark's original offset even after earlier deletes shifted the content.
3207
+ *
3208
+ * NOTE: collapse-to-cut is intentionally NOT confluent under concurrent {@link DeltaBuilder#rebase} — a
3209
+ * deletion maps a whole range to one point, losing where in the range the cursor was, so a concurrent
3210
+ * insert there lands on different sides on the two replays. Marks are therefore treated as local /
3211
+ * ephemeral cursor state, excluded from a delta's identity ({@link Delta#fingerprint} / equality); only
3212
+ * the document *content* is guaranteed to converge.
3213
+ *
3214
+ * @param {DeltaAny} change
3215
+ * @param {number} key
3216
+ * @param {1|-1} assoc
3217
+ * @return {number}
3218
+ */
3219
+ const shiftMarkKey = (change, key, assoc) => {
3220
+ let oldPos = 0
3221
+ let newPos = 0
3222
+ for (const op of change.children) {
3223
+ if ($retainOp.check(op) || $modifyOp.check(op)) {
3224
+ const n = op.length
3225
+ if (key < oldPos + n) return newPos + (key - oldPos) // mark falls inside this retained run
3226
+ oldPos += n
3227
+ newPos += n
3228
+ } else if ($insertOp.check(op) || $textOp.check(op)) {
3229
+ if (oldPos < key || (oldPos === key && assoc === 1)) newPos += op.length
3230
+ } else if ($deleteOp.check(op)) {
3231
+ const n = op.delete
3232
+ // mark lies in [oldPos, oldPos+n) — its content is being deleted ⇒ collapse to the cut point
3233
+ if (key < oldPos + n) return newPos
3234
+ // right boundary with LEFT gravity anchors to the last deleted char ⇒ also collapses to the cut
3235
+ // (n > 0 guards the degenerate delete(0) rebase can emit, which deletes nothing)
3236
+ if (n > 0 && key === oldPos + n && assoc === -1) return newPos
3237
+ oldPos += n
3238
+ }
3239
+ }
3240
+ return newPos + (key - oldPos)
3241
+ }
3242
+
3243
+ /**
3244
+ * Shift `node`'s own number-keyed leaf marks by the content ops of `change`, collapsing any covered by a
3245
+ * delete to the cut point (see {@link shiftMarkKey}). Attribute-key (string) marks are untouched.
3246
+ *
3247
+ * @param {DeltaAny} node
3248
+ * @param {DeltaAny} change
3249
+ */
3250
+ const shiftMarksByChange = (node, change) => {
3251
+ const marks = node.marks
3252
+ if (marks === null) return
3253
+ /** @type {Array<Mark>} */
3254
+ const shifted = []
3255
+ for (const m of marks) {
3256
+ if (typeof m.key === 'number') {
3257
+ const nk = shiftMarkKey(change, m.key, m.assoc)
3258
+ if (nk !== m.key) shifted.push(m.copy(nk)) // a fresh Mark — never mutate in place
3259
+ }
3260
+ }
3261
+ // re-add shifted marks under their (unchanged) ids ⇒ replace in place (the flag stays set)
3262
+ for (const m of shifted) marks.add(m)
3263
+ }
3264
+
3265
+ /**
3266
+ * Collect the ids of a {@link Marks} set, a plain {@link Mark} array, or an id array (any may be
3267
+ * `null`) into a `Set`, used for the by-id conflict checks during {@link DeltaBuilder#rebase}.
3268
+ *
3269
+ * @param {Iterable<Mark|string>?} marksOrIds
3270
+ * @return {Set<string>}
3271
+ */
3272
+ const markIdSet = marksOrIds => {
3273
+ /** @type {Set<string>} */
3274
+ const ids = new Set()
3275
+ if (marksOrIds !== null) {
3276
+ for (const x of marksOrIds) ids.add(typeof x === 'string' ? x : x.id)
3277
+ }
3278
+ return ids
3279
+ }
3280
+
3281
+ /**
3282
+ * Core mark reconciliation for one node level during {@link DeltaBuilder#rebase}. `adds` are this
3283
+ * side's mark adds (with keys) and `deletes` its delete ids; `otherAddIds`/`otherDelIds` are the
3284
+ * concurrent side's ids and `otherContent` the delta whose content ops shift surviving number keys.
3285
+ *
3286
+ * Rules (each by mark id): add-vs-add → `priority` decides; add-vs-delete → the add wins (a re-placed
3287
+ * cursor is not killed by a stale removal); delete-vs-delete → the duplicate is dropped. A surviving
3288
+ * number-keyed add is shifted by `otherContent`, collapsing to the cut point if its content was
3289
+ * concurrently deleted (see {@link shiftMarkKey}). Cursor positions are best-effort, not guaranteed to
3290
+ * converge (marks are excluded from the document's identity); only content convergence is guaranteed.
3291
+ *
3292
+ * @param {Iterable<Mark>} adds
3293
+ * @param {Array<string>} deletes
3294
+ * @param {Set<string>} otherAddIds
3295
+ * @param {Set<string>} otherDelIds
3296
+ * @param {DeltaAny} otherContent
3297
+ * @param {boolean} priority
3298
+ * @return {{ adds: Array<Mark>, deletes: Array<string> }}
3299
+ */
3300
+ const rebaseMarkOps = (adds, deletes, otherAddIds, otherDelIds, otherContent, priority) => {
3301
+ /** @type {Array<Mark>} */
3302
+ const keptAdds = []
3303
+ /** @type {Array<string>} */
3304
+ const delIds = deletes.slice()
3305
+ for (const m of adds) {
3306
+ if (!priority && otherAddIds.has(m.id)) continue // lost an add-vs-add conflict
3307
+ if (typeof m.key === 'number') {
3308
+ const nk = shiftMarkKey(otherContent, m.key, m.assoc) // collapses to the cut if its content was deleted
3309
+ keptAdds.push(nk === m.key ? m : m.copy(nk))
3310
+ } else {
3311
+ keptAdds.push(m)
3312
+ }
3313
+ }
3314
+ // a delete survives only where the other side neither re-adds (add wins) nor already deletes (dedup)
3315
+ /** @type {Set<string>} */
3316
+ const seen = new Set()
3317
+ /** @type {Array<string>} */
3318
+ const keptDels = []
3319
+ for (const id of delIds) {
3320
+ if (!seen.has(id) && !otherAddIds.has(id) && !otherDelIds.has(id)) {
3321
+ seen.add(id)
3322
+ keptDels.push(id)
3323
+ }
3324
+ }
3325
+ return { adds: keptAdds, deletes: keptDels }
3326
+ }
3327
+
3328
+ /**
3329
+ * Reconcile a node's own root mark add/delete ops against a concurrent change `other` during
3330
+ * {@link DeltaBuilder#rebase}, rebuilding `node.marks`/`node.deleteMarks` from {@link rebaseMarkOps}.
3331
+ * `maybeHasMarks` is left set (conservative; marksToPositions self-corrects a now-empty subtree).
3332
+ *
3333
+ * @param {DeltaAny} node
3334
+ * @param {DeltaAny} other
3335
+ * @param {boolean} priority
3336
+ */
3337
+ const rebaseRootMarks = (node, other, priority) => {
3338
+ const { adds, deletes } = rebaseMarkOps(
3339
+ node.marks !== null ? [...node.marks] : [],
3340
+ node.deleteMarks !== null ? [...node.deleteMarks] : [],
3341
+ markIdSet(other.marks), markIdSet(other.deleteMarks), other, priority
3342
+ )
3343
+ // rebuild both fields wholesale from rebaseMarkOps's provably-disjoint output (concurrent reconciliation
3344
+ // keeps its deliberate add-wins rule, NOT the apply path's last-writer-wins; not a primitive write)
3345
+ if (adds.length === 0) {
3346
+ node.marks = null
3347
+ } else {
3348
+ const m = new Marks()
3349
+ for (const x of adds) m.add(x)
3350
+ node.marks = m
3351
+ }
3352
+ node.deleteMarks = deletes.length === 0 ? null : new Set(deletes)
3353
+ }
3354
+
3355
+ /**
3356
+ * Build a change delta that adds (or, with `isDelete`, removes) the mark for `pos`. The descent is a
3357
+ * `retain`/`modify`/`modifyAttr` chain re-derived from `pos.path`; the mark always rides on the **leaf
3358
+ * delta's own marks** (root marks). Each interior step just wraps the next level in a `modify`
3359
+ * (content index) or `modifyAttr` (attribute key) — apply/rebase carry the wrapped value's root marks
3360
+ * onto/through the target for free.
3361
+ *
3362
+ * @param {import('./position.js').Pos} pos
3363
+ * @param {string} id
3364
+ * @param {boolean} isDelete
3365
+ * @return {DeltaBuilderAny}
3366
+ */
3367
+ const markChange = (pos, id, isDelete) => {
3368
+ const path = pos.path
3369
+ // a mark anchors at the terminal step of its path (a content offset or attribute key); the root
3370
+ // position `[]` has no terminal and cannot carry a mark - reject it instead of recursing forever
3371
+ if (path.length === 0) throw error.create('cannot place a mark at the root position (empty path): a mark needs a terminal content-offset or attribute-key step')
3372
+ const mark = isDelete ? null : createMark(path[path.length - 1], id, pos.assoc, pos.attrs ?? null)
3373
+ /**
3374
+ * @param {number} i
3375
+ * @return {DeltaBuilderAny}
3376
+ */
3377
+ const build = i => {
3378
+ if (i === path.length - 1) {
3379
+ // leaf reached: carry the mark on the delta's own marks
3380
+ const d = /** @type {DeltaBuilderAny} */ (create())
3381
+ if (mark === null) deleteMarkTo(d, id, true) // a change delta records the transmittable delete
3382
+ else addMarkTo(d, mark)
3383
+ return d
3384
+ }
3385
+ const step = path[i]
3386
+ return s.$string.check(step)
3387
+ ? /** @type {DeltaBuilderAny} */ (create()).modifyAttr(step, build(i + 1))
3388
+ : /** @type {DeltaBuilderAny} */ (create()).retain(step).modify(build(i + 1))
3389
+ }
3390
+ return build(0)
3391
+ }
3392
+
1894
3393
  /**
1895
3394
  * @template {DeltaConf} Conf
1896
3395
  * @extends {s.Schema<Delta<Conf>>}
3396
+ * @internal use the {@link $delta} factory; this class is exported only for the `$$delta` predicate.
1897
3397
  */
1898
3398
  export class $Delta extends s.Schema {
1899
3399
  /**
@@ -1932,8 +3432,10 @@ export class $Delta extends s.Schema {
1932
3432
  check (o, err = undefined) {
1933
3433
  const { $name, $attrs, $children, hasText, $formats } = this.shape
1934
3434
  if (!$deltaAny.check(o, err)) {
3435
+ /* c8 ignore next */
1935
3436
  err?.extend(null, 'Delta', o?.constructor.name, 'Constructor match failed')
1936
3437
  } else if (o.name != null && !$name.check(o.name, err)) {
3438
+ /* c8 ignore next */
1937
3439
  err?.extend('Delta.name', $name.toString(), o.name, 'Unexpected node name')
1938
3440
  } else if (list.toArray(o.children).some(c => (!hasText && $textOp.check(c)) || (hasText && $textOp.check(c) && c.format != null && !$formats.check(c.format)) || ($insertOp.check(c) && !c.insert.every(ins => $children.check(ins))))) {
1939
3441
  err?.extend('Delta.children', '', '', 'Children don\'t match the schema')
@@ -1947,47 +3449,96 @@ export class $Delta extends s.Schema {
1947
3449
  }
1948
3450
 
1949
3451
  /**
1950
- * @template {s.Schema<string>|string|Array<string>} [NodeNameSchema=s.Schema<any>]
1951
- * @template {s.Schema<{ [key: string|number]: any }>|{ [key:string|number]:any }} [AttrsSchema=s.Schema<{}>]
1952
- * @template {any} [ChildrenSchema=s.Schema<never>]
1953
- * @template {boolean} [HasText=false]
1954
- * @template {boolean} [RecursiveChildren=false]
1955
- * @template {{ [k:string]:any }} [Formats={[k:string]:any}]
1956
- * @param {object} opts
1957
- * @param {NodeNameSchema?} [opts.name]
1958
- * @param {AttrsSchema?} [opts.attrs] What key-value pairs are included.
1959
- * @param {ChildrenSchema?} [opts.children] The type of content in `insertOp`
1960
- * @param {HasText} [opts.text] Whether this delta contains text using `textOp`
1961
- * @param {Formats} [opts.formats]
1962
- * @param {RecursiveChildren} [opts.recursiveChildren]
1963
- * @return {[s.ReadSchemaUnwrapped<NodeNameSchema>,s.ReadSchemaUnwrapped<AttrsSchema>,s.ReadSchemaUnwrapped<ChildrenSchema>] extends [infer NodeName, infer Attrs, infer Children] ? s.Schema<Delta<PrettifyDeltaConf<(import('../ts.js').TypeIsAny<NodeName, {}, { name: NodeName }> &
1964
- * ({} extends Attrs ? {} : { attrs: Attrs }) &
1965
- * ([Children] extends [never] ? {} : { children: Children }) &
1966
- * (HasText extends false ? {} : { text: HasText }) &
1967
- * (RecursiveChildren extends false ? {} : { recursiveChildren: RecursiveChildren })) extends infer DC extends DeltaConf ? DC : never>>> : never}
1968
- */
1969
- export const $delta = ({ name, attrs, children, text, formats, recursiveChildren: recursive }) => /** @type {any} */ (new $Delta(
1970
- /** @type {any} */ (name == null ? s.$any : s.$(name)),
1971
- /** @type {any} */ (attrs == null ? s.$object({}) : s.$(attrs)),
1972
- /** @type {any} */ (children == null ? s.$never : s.$(children)),
1973
- text ?? false,
1974
- recursive ?? false,
1975
- formats == null ? s.$any : s.$(formats)
1976
- ))
3452
+ * @typedef {{
3453
+ * name?: s.Schema<string>|string|Array<string>,
3454
+ * attrs?: s.Schema<{ [key: string|number]: any }>|{ [key:string|number]:any },
3455
+ * children?: any,
3456
+ * text?: boolean,
3457
+ * recursiveChildren?: boolean,
3458
+ * formats?: { [k:string]: any }
3459
+ * }} ReadableDeltaConf
3460
+ */
3461
+
3462
+ /**
3463
+ * Transforms a ReadableDeltaConf (the input shape of `$delta(spec)`) to a DeltaConf.
3464
+ * @template {ReadableDeltaConf} DConfSpec
3465
+ * @typedef {[
3466
+ * DConfSpec extends {name: infer N} ? s.ReadSchemaUnwrapped<N> : any,
3467
+ * DConfSpec extends {attrs: infer A} ? s.ReadSchemaUnwrapped<A> : {},
3468
+ * DConfSpec extends {children: infer C} ? s.ReadSchemaUnwrapped<C> : never
3469
+ * ] extends [infer NodeName, infer Attrs, infer Children]
3470
+ * ? AsDeltaConf<PrettifyDeltaConf<(
3471
+ * import('../ts.js').TypeIsAny<NodeName, {}, { name: NodeName }> &
3472
+ * ([keyof Attrs] extends [never] ? {} : { attrs: Attrs }) &
3473
+ * ([Children] extends [never] ? {} : { children: Children }) &
3474
+ * (DConfSpec extends {text: true} ? { text: true } : {}) &
3475
+ * (DConfSpec extends {recursiveChildren: true} ? { recursiveChildren: true } : {})
3476
+ * )>>
3477
+ * : never
3478
+ * } ReadDeltaConf
3479
+ */
3480
+
3481
+ /**
3482
+ * Name-first form: a string-literal `name` as the first argument is preserved as a literal type
3483
+ * (generic argument inference), so callers don't need `s.$literal('x')` / `@type {const}` to keep the
3484
+ * node name literal. The remaining options follow in `opts`.
3485
+ *
3486
+ * @template {string} Name
3487
+ * @template {Omit<ReadableDeltaConf, 'name'>} [Opts={}]
3488
+ * @overload
3489
+ * @param {Name} name
3490
+ * @param {Opts} [opts]
3491
+ * @return {s.Schema<Delta<ReadDeltaConf<Opts & { name: Name }>>>}
3492
+ */
3493
+ /**
3494
+ * @template {ReadableDeltaConf} [Conf={}]
3495
+ * @overload
3496
+ * @param {Conf} opts
3497
+ * @return {s.Schema<Delta<ReadDeltaConf<Conf>>>}
3498
+ */
3499
+ /**
3500
+ * @param {ReadableDeltaConf|string} optsOrName
3501
+ * @param {Omit<ReadableDeltaConf, 'name'>} [opts]
3502
+ * @return {any}
3503
+ */
3504
+ export const $delta = (optsOrName, opts) => {
3505
+ const { name, attrs, children, text, formats, recursiveChildren: recursive } =
3506
+ s.$string.check(optsOrName) ? { ...opts, name: optsOrName } : optsOrName
3507
+ return /** @type {any} */ (new $Delta(
3508
+ /** @type {any} */ (name == null ? s.$any : s.$(name)),
3509
+ /** @type {any} */ (attrs == null ? s.$object({}) : s.$(attrs)),
3510
+ /** @type {any} */ (children == null ? s.$never : s.$(children)),
3511
+ text ?? false,
3512
+ recursive ?? false,
3513
+ formats == null ? s.$any : s.$(formats)
3514
+ ))
3515
+ }
1977
3516
 
1978
3517
  export const $$delta = /* @__PURE__ */s.$constructedBy($Delta)
1979
3518
 
1980
- export const $deltaAny = /** @type {s.Schema<DeltaAny>} */ (/* @__PURE__ */s.$type('delta'))
3519
+ export const $deltaAny = /** @type {s.Schema<Delta<any>>} */ (Delta.prototype.$type = s.$type('d:delta', Delta))
1981
3520
  export const $deltaBuilderAny = /** @type {s.Schema<DeltaBuilderAny>} */ (/* @__PURE__ */s.$custom(o => $deltaAny.check(o) && !o.isDone))
1982
3521
 
1983
3522
  /**
1984
- * Helper function to merge attribution and attributes. The latter input "wins".
3523
+ * Helper function to merge attribution and attributes. The latter input "wins". The nested `format` key is
3524
+ * merged per inner key (one level — see {@link mergeAttr}); every other key is replaced wholesale.
3525
+ *
3526
+ * @template {{ [key: string]: any }} T
3527
+ * @param {T | null} a
3528
+ * @param {T | null} b
3529
+ */
3530
+ export const mergeAttributions = (a, b) => a == null
3531
+ ? b
3532
+ : (b == null ? a : /** @type {T} */ (mergeAttr(a, b, true)))
3533
+
3534
+ /**
3535
+ * Helper function to merge formats. The latter input "wins".
1985
3536
  *
1986
3537
  * @template {{ [key: string]: any }} T
1987
3538
  * @param {T | null} a
1988
3539
  * @param {T | null} b
1989
3540
  */
1990
- export const mergeAttrs = (a, b) => object.isEmpty(a)
3541
+ export const mergeFormats = (a, b) => object.isEmpty(a)
1991
3542
  ? (object.isEmpty(b) ? null : b)
1992
3543
  : (object.isEmpty(b) ? a : object.assign({}, a, b))
1993
3544
 
@@ -2003,33 +3554,163 @@ export const mergeDeltas = (a, b) => {
2003
3554
  c.apply(b)
2004
3555
  return /** @type {any} */ (c)
2005
3556
  }
2006
- return a == null ? b : (a || null)
3557
+ return /** @type {D} */ (a || b || null)
3558
+ }
3559
+
3560
+ /**
3561
+ * The node (child delta) at child-position `pos` of `source`, or `null` when `pos` lands on a text
3562
+ * run or a non-delta child. Lets {@link random} target an existing node with a `modify` op.
3563
+ *
3564
+ * @param {DeltaAny} source
3565
+ * @param {number} pos
3566
+ * @return {DeltaAny?}
3567
+ */
3568
+ const childNodeAt = (source, pos) => {
3569
+ let p = 0
3570
+ for (const op of source.children) {
3571
+ if ($textOp.check(op)) {
3572
+ if (pos < p + op.insert.length) return null
3573
+ p += op.insert.length
3574
+ } else if ($insertOp.check(op)) {
3575
+ for (const el of op.insert) {
3576
+ if (p === pos) return $deltaAny.check(el) ? el : null
3577
+ p++
3578
+ }
3579
+ }
3580
+ }
3581
+ /* c8 ignore start */
3582
+ // the sole caller passes pos < source.childCnt (sourceLen >= 1), so a node or text run always
3583
+ // resolves first; this guards an out-of-range pos defensively.
3584
+ return null
3585
+ /* c8 ignore stop */
3586
+ }
3587
+
3588
+ /**
3589
+ * "Find the child schema for this node": the `$Delta` schemas within `$children` (a single schema or
3590
+ * any member of a union) that accept `node`. Only `$Delta` members can drive a recursive `random`
3591
+ * modify, so non-delta children (numbers, strings, `$any`) and non-matching names are dropped.
3592
+ *
3593
+ * @param {s.Schema<any>} $children
3594
+ * @param {DeltaAny} node
3595
+ * @return {Array<s.Schema<DeltaAny>>}
3596
+ */
3597
+ const matchingNodeSchemas = ($children, node) => (s.$$union.check($children) ? $children.shape : [$children])
3598
+ .filter($c => $$delta.check($c) && $c.check(node))
3599
+
3600
+ /**
3601
+ * Random {@link Attribution} for fuzz tests. There is no schema for attributions; the canonical shape is
3602
+ * `(insert|delete + …At)` intersected with an optional format part. The format part exists only on
3603
+ * children (content ops), never on node attribute ops. Sometimes returns `undefined` (skip) or `null`
3604
+ * (clear). The optional `format` part carries a random non-empty subset of the format names, so two random
3605
+ * attributions differ in which inner `format` keys they carry — exercising the per-inner-key set/remove/merge
3606
+ * of {@link mergeAttr}/{@link diffDim} on the diff round-trip.
3607
+ *
3608
+ * @param {prng.PRNG} gen
3609
+ * @param {boolean} withFormat include the children-only `format`/`formatAt` part
3610
+ * @return {Attribution|null|undefined}
3611
+ */
3612
+ const randomAttribution = (gen, withFormat) => {
3613
+ const r = prng.uint32(gen, 0, 5)
3614
+ if (r <= 2) return undefined // ~half: skip (no attribution change)
3615
+ if (r === 3) return null // clear all attribution
3616
+ const user = () => [prng.oneOf(gen, ['alice', 'bob', 'carol'])]
3617
+ /** @type {Attribution} */
3618
+ const attribution = prng.bool(gen)
3619
+ ? { insert: user(), insertAt: prng.uint32(gen, 0, 100) }
3620
+ : { delete: user(), deleteAt: prng.uint32(gen, 0, 100) }
3621
+ if (withFormat && prng.bool(gen)) {
3622
+ // a random non-empty subset of format names (never an empty `format`, per the storage invariant)
3623
+ /** @type {Record<string, string[]>} */
3624
+ const format = {}
3625
+ for (const name of ['bold', 'italic', 'under']) { if (prng.bool(gen)) format[name] = user() }
3626
+ if (!object.isEmpty(format)) {
3627
+ attribution.format = format
3628
+ attribution.formatAt = prng.uint32(gen, 0, 100)
3629
+ }
3630
+ }
3631
+ return attribution
2007
3632
  }
2008
3633
 
2009
3634
  /**
2010
3635
  * @template {DeltaConf} Conf
2011
3636
  * @param {prng.PRNG} gen
2012
3637
  * @param {s.Schema<Delta<Conf>>} $d
3638
+ * @param {object} conf
3639
+ * @param {DeltaAny?} [conf.source]
3640
+ * @param {number} [conf.minChildOps]
3641
+ * @param {number} [conf.maxChildOps]
3642
+ * @param {boolean} [conf.attribution] generate random attributions (off by default; consumes no PRNG
3643
+ * draws when off, so existing seeds are unaffected). Rebase does not converge attributions, so leave
3644
+ * this off for rebase-convergence fuzzing and enable it for diff fuzzing.
2013
3645
  * @return {DeltaBuilder<Conf>}
2014
3646
  */
2015
- export const random = (gen, $d) => {
3647
+ export const random = (gen, $d, conf = {}) => {
3648
+ const { source = null, minChildOps = 1, maxChildOps = 9, attribution = false } = conf
3649
+ let sourceLen = source == null ? 0 : source.childCnt
2016
3650
  const { $name, $attrs, $children, hasText, $formats: $formats_ } = /** @type {$Delta<any>} */ (/** @type {any} */ ($d)).shape
2017
3651
  const d = s.$$any.check($name) ? create($deltaAny) : create(s.random(gen, $name), $deltaAny)
2018
3652
  const $formats = s.$$any.check($formats_) ? s.$null : $formats_
2019
- prng.bool(gen) && d.setAttrs(s.random(gen, $attrs))
2020
- for (let i = prng.uint32(gen, 0, 5); i > 0; i--) {
2021
- if (hasText && prng.bool(gen)) {
2022
- d.insert(prng.word(gen), s.random(gen, $formats))
2023
- } else if (!s.$$never.check($children)) {
2024
- /**
2025
- * @type {Array<any>}
2026
- */
2027
- const ins = []
2028
- let insN = prng.int32(gen, 0, 5)
2029
- while (insN--) {
2030
- ins.push(s.random(gen, $children))
3653
+ const genAttribution = /** @param {boolean} withFormat */ withFormat => attribution ? randomAttribution(gen, withFormat) : undefined
3654
+ // set random attrs (node attribute ops carry the insert/delete part of an attribution only — no format part)
3655
+ prng.bool(gen) && d.setAttrs(s.random(gen, $attrs, random), genAttribution(false))
3656
+ // delete a single attr
3657
+ if (source && !object.isEmpty(source.attrs) && prng.bool(gen)) {
3658
+ d.deleteAttr(prng.oneOf(gen, object.keys(source.attrs)), genAttribution(false))
3659
+ }
3660
+ for (let i = prng.uint32(gen, minChildOps, maxChildOps); i > 0; i--) {
3661
+ /**
3662
+ * @type {Array<function():void>}
3663
+ */
3664
+ const possibleOps = []
3665
+ if (hasText) {
3666
+ possibleOps.push(() => {
3667
+ d.insert(prng.oneOf(gen, ['a', 'b', ' ', '\n', '.']), s.random(gen, $formats), genAttribution(true))
3668
+ })
3669
+ }
3670
+ if (!s.$$never.check($children)) {
3671
+ possibleOps.push(() => {
3672
+ /**
3673
+ * @type {Array<any>}
3674
+ */
3675
+ const ins = []
3676
+ let insN = prng.int32(gen, 0, 5)
3677
+ while (insN--) {
3678
+ ins.push(s.random(gen, $children, random))
3679
+ }
3680
+ d.insert(ins, s.random(gen, $formats), genAttribution(true))
3681
+ })
3682
+ }
3683
+ if (sourceLen > 0) {
3684
+ possibleOps.push(() => {
3685
+ const len = prng.uint32(gen, 1, sourceLen)
3686
+ sourceLen -= len
3687
+ d.delete(len)
3688
+ })
3689
+ possibleOps.push(() => {
3690
+ const len = prng.uint32(gen, 1, sourceLen)
3691
+ sourceLen -= len
3692
+ // a retain's format is a skip (`undefined`) or a per-key set object — never a blanket `null` clear
3693
+ // ($null formats coerce to skip). Blanket clears are a local-only utility and discouraged on a
3694
+ // channel (they "win" a rebase, dropping concurrent edits — see Formats); the diff fuzz
3695
+ // already exercises the recommended per-key `{k:null}` removals (which converge without data loss).
3696
+ d.retain(len, prng.bool(gen) ? undefined : (s.random(gen, $formats) ?? undefined), genAttribution(true))
3697
+ })
3698
+ // if the op we currently point at is a node, it's also a choice to modify it: find the child
3699
+ // schema that matches the node and recursively generate a change against it.
3700
+ const src = /** @type {DeltaAny} */ (source)
3701
+ const node = childNodeAt(src, src.childCnt - sourceLen)
3702
+ const $nodeMatches = node != null ? matchingNodeSchemas($children, node) : []
3703
+ if ($nodeMatches.length > 0) {
3704
+ possibleOps.push(() => {
3705
+ d.modify(random(gen, prng.oneOf(gen, $nodeMatches), { source: node }), undefined, genAttribution(true))
3706
+ sourceLen -= 1
3707
+ })
2031
3708
  }
2032
- d.insert(ins, s.random(gen, $formats))
3709
+ }
3710
+ if (possibleOps.length > 0) {
3711
+ prng.oneOf(gen, possibleOps)()
3712
+ } else {
3713
+ break
2033
3714
  }
2034
3715
  }
2035
3716
  return /** @type {any} */ (d)
@@ -2047,11 +3728,11 @@ export const random = (gen, $d) => {
2047
3728
  */
2048
3729
  /**
2049
3730
  * @template {string} NodeName
2050
- * @template {DeltaConf} Conf
3731
+ * @template {s.Schema<DeltaAny>} Schema
2051
3732
  * @overload
2052
3733
  * @param {NodeName} nodeName
2053
- * @param {s.Schema<Delta<Conf>>} schema
2054
- * @return {DeltaBuilder<Conf, true>}
3734
+ * @param {Schema} schema
3735
+ * @return {Schema extends s.Schema<Delta<infer Conf>> ? DeltaBuilder<Conf, true> : never}
2055
3736
  */
2056
3737
  /**
2057
3738
  * @template {s.Schema<DeltaAny>} Schema
@@ -2067,8 +3748,7 @@ export const random = (gen, $d) => {
2067
3748
  * @param {NodeName} nodeName
2068
3749
  * @param {Attrs} attrs
2069
3750
  * @param {Children} [children]
2070
- * @return {DeltaBuilder<{
2071
- * name: NodeName,
3751
+ * @return {DeltaBuilder<(NodeName extends string ? { name: NodeName } : {}) & {
2072
3752
  * attrs: Attrs extends null ? {} : Attrs,
2073
3753
  * children: Extract<Children,Array<any>> extends Array<infer Ac> ? (unknown extends Ac ? never : Ac) : never,
2074
3754
  * text: Extract<Children,string> extends never ? false : true
@@ -2097,8 +3777,7 @@ export const create = (nodeNameOrSchema, attrsOrSchema, children) => {
2097
3777
  * @overload
2098
3778
  * @param {NodeName} nodeName
2099
3779
  * @param {...Array<Children>} children
2100
- * @return {DeltaBuilder<{
2101
- * name: NodeName,
3780
+ * @return {DeltaBuilder<(NodeName extends string ? { name: NodeName } : {}) & {
2102
3781
  * children: Extract<Children,Array<any>> extends Array<infer Ac> ? (unknown extends Ac ? never : Ac) : never,
2103
3782
  * text: Extract<Children,string> extends never ? false : true
2104
3783
  * }>}
@@ -2132,8 +3811,7 @@ export const create = (nodeNameOrSchema, attrsOrSchema, children) => {
2132
3811
  * @param {NodeName} nodeName
2133
3812
  * @param {Attrs} attrs
2134
3813
  * @param {...Array<Children>} children
2135
- * @return {DeltaBuilder<{
2136
- * name: NodeName,
3814
+ * @return {DeltaBuilder<(NodeName extends string ? { name: NodeName } : {}) & {
2137
3815
  * attrs: Attrs extends null ? {} : Attrs,
2138
3816
  * children: Extract<Children,Array<any>> extends Array<infer Ac> ? (unknown extends Ac ? never : Ac) : never,
2139
3817
  * text: Extract<Children,string> extends never ? false : true
@@ -2156,17 +3834,84 @@ export const from = (...args) => {
2156
3834
  return d
2157
3835
  }
2158
3836
 
3837
+ class _DiffStringWrapper {
3838
+ /**
3839
+ * @param {string} str
3840
+ */
3841
+ constructor (str) {
3842
+ this.str = str
3843
+ /**
3844
+ * @type {string?}
3845
+ */
3846
+ this._fingerprint = null
3847
+ }
3848
+
3849
+ [fingerprintTrait.FingerprintTraitSymbol] () {
3850
+ return this._fingerprint || (this._fingerprint = fingerprintTrait.fingerprint(this.str))
3851
+ }
3852
+ }
3853
+
3854
+ /*
3855
+ * Delta Diffing approach - optimized for performance and creating readable deltas. You can only
3856
+ * diff insertions (InsertOp & TextOp) not delete ops.
3857
+ *
3858
+ * # Children
3859
+ * Diff content first and then figure out the necessary formatting updates
3860
+ * 1. find common prefix & suffix
3861
+ * 2. slice center to fresh delta. split content by coarse regex ($insert ops are split into
3862
+ * individual items)
3863
+ * 3. patience diff on split content and receive set of splice ops
3864
+ * 4. on each splice op: perform another patience diff with a granular regex on strings
3865
+ * 5. reassemble deltas recursively
3866
+ * 6. apply content diff on original delta and find necessary formatting updates
3867
+ * 7. merge content diff and formatting updates
3868
+ */
3869
+
2159
3870
  /**
3871
+ * @typedef {object} DiffOptions
3872
+ * @property {(d1: DeltaAny, d2: DeltaAny) => boolean} [compare] Predicate deciding when two nodes
3873
+ * are paired into a `modify` (vs. replaced wholesale). Defaults to comparing names
3874
+ * (`(d1, d2) => d1.name === d2.name`). Called as `compare(fromNode, toNode)`.
3875
+ * @property {boolean} [clone=false] By default `diff` does **not** produce a fully fresh delta: wherever
3876
+ * it emits content from `d2` — a wholesale-inserted child node, or a delta-valued attribute — it *shares*
3877
+ * that nested delta by reference, so the result aliases `d2`'s subtrees. Set `true` to
3878
+ * {@link cloneDeep deep-clone} every such delta (children **and** attributes) as it is included in the
3879
+ * output, yielding a result that shares no structure with `d2` (and is safe to hand to something that
3880
+ * manipulates deltas in place, e.g. a transformer).
3881
+ */
3882
+
3883
+ /**
3884
+ * Compute a delta that, when applied to `d1`, produces `d2`. Only the children and attributes of
3885
+ * `d1` and `d2` are compared; the top-level node names of `d1` and `d2` are *not*. Diffing
3886
+ * `<div>a</div>` against `<span>a</span>` is valid and yields an empty diff — they have the same
3887
+ * children and attributes, so as far as `diff` is concerned they are equal at the level it cares
3888
+ * about. The top-level name is treated as a document-type marker, not as diffable content.
3889
+ *
3890
+ * Names *are* compared on children: a child node whose name changes between `d1` and `d2` is
3891
+ * replaced wholesale (delete + insert), not converted into a `modify` op. Same-name child nodes
3892
+ * at aligned positions are paired and recursed into via `modify`.
3893
+ *
3894
+ * Pairing is decided by `options.compare`, which defaults to comparing names
3895
+ * (`(d1, d2) => d1.name === d2.name`). Supply a stricter predicate to tighten the granularity —
3896
+ * e.g. to also require their first child to match — in which case nodes that don't satisfy it are
3897
+ * replaced wholesale instead. `options` is forwarded to every child diff, so the chosen granularity
3898
+ * applies consistently all the way down the tree. `compare` is always called as
3899
+ * `compare(fromNode, toNode)` (the node from `d1` first).
3900
+ *
3901
+ * NOTE: `diff` compares **content only** — it short-circuits on the {@link Delta#fingerprint}, which
3902
+ * excludes marks. Two states that differ *only* in their marks diff to an empty change, so cursor marks
3903
+ * do NOT cross a `diff` (e.g. a `Binding`'s initial-state sync). Marks survive on the live
3904
+ * `apply`/`rebase`/transform path; carrying them across a diff-based boundary needs a separate channel.
3905
+ *
2160
3906
  * @template {DeltaConf} Conf
2161
3907
  * @param {Delta<Conf>} d1
2162
3908
  * @param {NoInfer<Delta<Conf>>} d2
2163
- * @return {DeltaBuilder<Conf>}
3909
+ * @param {DiffOptions} [options]
3910
+ * @return {Delta<Conf>}
2164
3911
  */
2165
- export const diff = (d1, d2) => {
2166
- /**
2167
- * @type {DeltaBuilderAny}
2168
- */
2169
- const d = create()
3912
+ export const diff = (d1, d2, options = {}) => {
3913
+ const d = create(d1.name === d2.name ? d1.name : null, $deltaAny)
3914
+ const compare = options?.compare ?? defaultCompare
2170
3915
  if (d1.fingerprint !== d2.fingerprint) {
2171
3916
  /**
2172
3917
  * @type {ChildrenOpAny?}
@@ -2184,6 +3929,8 @@ export const diff = (d1, d2) => {
2184
3929
  * @type {ChildrenOpAny?}
2185
3930
  */
2186
3931
  let right2 = d2.children.end
3932
+ // whether we need to diff formatting
3933
+ let formattingOrAttributionNeedsDiff = false
2187
3934
  let commonPrefixOffset = 0
2188
3935
  // perform a patience sort
2189
3936
  // 1) remove common prefix and suffix
@@ -2194,177 +3941,397 @@ export const diff = (d1, d2) => {
2194
3941
  left1 = left1.next
2195
3942
  left2 = left2.next
2196
3943
  }
2197
- while (right1 !== null && right1 !== left1 && right1.fingerprint === right2?.fingerprint) {
2198
- right1 = right1.prev
2199
- right2 = right2.prev
3944
+ if (left1 !== null && left2 !== null) {
3945
+ while (right1 !== null && right2 !== null && right1 !== left1 && right2 !== left2 && right1.fingerprint === right2.fingerprint) {
3946
+ right1 = right1.prev
3947
+ right2 = right2.prev
3948
+ }
2200
3949
  }
2201
3950
  /**
2202
- * @type {Array<ChildrenOpAny>}
3951
+ * @type {Array<fingerprintTrait.Fingerprintable>}
2203
3952
  */
2204
- const ops1 = []
3953
+ const cs1 = []
2205
3954
  /**
2206
- * @type {Array<ChildrenOpAny>}
3955
+ * @type {Array<fingerprintTrait.Fingerprintable>}
2207
3956
  */
2208
- const ops2 = []
2209
- while (left1 !== null && left1 !== right1?.next) {
2210
- ops1.push(left1)
2211
- left1 = left1.next
2212
- }
2213
- while (left2 !== null && left2 !== right2?.next) {
2214
- ops2.push(left2)
2215
- left2 = left2.next
3957
+ const cs2 = []
3958
+ // if right is null, then we already matched everything
3959
+ if (right1 != null) {
3960
+ while (left1 !== null && left1 !== right1.next) {
3961
+ if ($textOp.check(left1)) {
3962
+ cs1.push(left1.insert)
3963
+ } else if ($insertOp.check(left1)) {
3964
+ cs1.push(...left1.insert.map(ins => typeof ins === 'string' ? new _DiffStringWrapper(ins) : ins))
3965
+ } else {
3966
+ throw error.create('[lib0/delta] diffing deletes unsupported')
3967
+ }
3968
+ formattingOrAttributionNeedsDiff ||= left1.format != null || left1.attribution != null
3969
+ left1 = left1.next
3970
+ }
2216
3971
  }
2217
- const fprints1 = ops1.map(op => op.fingerprint)
2218
- const fprints2 = ops2.map(op => op.fingerprint)
2219
- const changeset = patience.diff(fprints1, fprints2)
2220
- d.retain(commonPrefixOffset)
2221
- for (let i = 0, lastIndex1 = 0, currIndexOffset2 = 0; i < changeset.length; i++) {
2222
- const change = changeset[i]
2223
- d.retain(change.index - lastIndex1)
2224
- // insert minimal diff at curred position in d
2225
- /**
2226
- *
2227
- * @todo it would be better if these would be slices of delta (an actual delta)
2228
- *
2229
- * @param {ChildrenOpAny[]} opsIs
2230
- * @param {ChildrenOpAny[]} opsShould
2231
- */
2232
- const diffAndApply = (opsIs, opsShould) => {
2233
- const d = create($deltaAny)
2234
- // @todo unoptimized implementation. Convert content to array and diff that based on
2235
- // generated fingerprints. We probably could do better and cache more information.
2236
- // - benchmark
2237
- // - cache fingerprints in ops
2238
- /**
2239
- * @type {Array<string|DeltaAny|fingerprintTrait.Fingerprintable>}
2240
- */
2241
- const isContent = opsIs.flatMap(op => $insertOp.check(op) ? op.insert : ($textOp.check(op) ? op.insert.split('') : error.unexpectedCase()))
2242
- /**
2243
- * @type {Array<string|DeltaAny|fingerprintTrait.Fingerprintable>}
2244
- */
2245
- const shouldContent = opsShould.flatMap(op => $insertOp.check(op) ? op.insert : ($textOp.check(op) ? op.insert.split('') : error.unexpectedCase()))
2246
- const isContentFingerprinted = isContent.map(c => s.$string.check(c) ? c : fingerprintTrait.fingerprint(c))
2247
- const shouldContentFingerprinted = shouldContent.map(c => s.$string.check(c) ? c : fingerprintTrait.fingerprint(c))
2248
- const hasFormatting = opsIs.some(op => !$deleteOp.check(op) && op.format != null) || opsShould.some(op => !$deleteOp.check(op) && op.format != null)
2249
- /**
2250
- * @type {{ index: number, insert: Array<string|DeltaAny|fingerprintTrait.Fingerprintable>, remove: Array<string|DeltaAny|fingerprintTrait.Fingerprintable> }[]}
2251
- */
2252
- const cdiff = patience.diff(isContentFingerprinted, shouldContentFingerprinted)
2253
- // overwrite fingerprinted content with actual content
2254
- for (let i = 0, adj = 0; i < cdiff.length; i++) {
2255
- const cd = cdiff[i]
2256
- cd.remove = isContent.slice(cd.index, cd.index + cd.remove.length)
2257
- cd.insert = shouldContent.slice(cd.index + adj, cd.index + adj + cd.insert.length)
2258
- adj += cd.insert.length - cd.remove.length
3972
+ if (right2 != null) {
3973
+ while (left2 !== null && left2 !== right2.next) {
3974
+ if ($textOp.check(left2)) {
3975
+ cs2.push(left2.insert)
3976
+ } else if ($insertOp.check(left2)) {
3977
+ cs2.push(...left2.insert.map(ins => typeof ins === 'string' ? new _DiffStringWrapper(ins) : ins))
3978
+ /* c8 ignore start */
3979
+ } else {
3980
+ // unreachable for valid diff inputs (delete on the rhs would already
3981
+ // have been rejected via the `[lib0/delta] diffing deletes unsupported`
3982
+ // path above)
3983
+ error.unexpectedCase()
2259
3984
  }
2260
- for (let i = 0, lastIndex = 0; i < cdiff.length; i++) {
2261
- const cd = cdiff[i]
2262
- d.retain(cd.index - lastIndex)
2263
- lastIndex = cd.index
2264
- let cdii = 0
2265
- let cdri = 0
2266
- // try to match as much content as possible, preferring to skip over non-deltas
2267
- for (; cdii < cd.insert.length && cdri < cd.remove.length;) {
2268
- const a = cd.insert[cdii]
2269
- const b = cd.remove[cdri]
2270
- if ($deltaAny.check(a) && $deltaAny.check(b) && a.name === b.name) {
2271
- d.modify(diff(b, a))
2272
- cdii++
2273
- cdri++
2274
- } else if ($deltaAny.check(b)) {
2275
- d.insert(s.$string.check(a) ? a : [a])
2276
- cdii++
2277
- } else {
2278
- d.delete(1)
2279
- cdri++
2280
- }
3985
+ /* c8 ignore stop */
3986
+ formattingOrAttributionNeedsDiff ||= left2.format != null || left2.attribution != null
3987
+ left2 = left2.next
3988
+ }
3989
+ }
3990
+ const changeset1 = [{
3991
+ index: commonPrefixOffset,
3992
+ insert: cs2,
3993
+ remove: cs1
3994
+ }]
3995
+ // split by line
3996
+ const changeset2 = diffChangesetWithSeparator(changeset1, /[\n]+/g)
3997
+ // split by alphanumerics and others
3998
+ const changeset3 = diffChangesetWithSeparator(changeset2, patience.smartSplitRegex)
3999
+ // split all
4000
+ const changeset4 = diffChangesetWithSeparator(changeset3, /./g)
4001
+ applyChangesetToDelta(d, changeset4, compare, options)
4002
+ if (formattingOrAttributionNeedsDiff) {
4003
+ const formattingDiff = create()
4004
+ // update opsIs with content diff. then we can figure out the formatting diff.
4005
+ const originalUpdated = clone(d1)
4006
+ originalUpdated.apply(/** @type {DeltaAny} */ (d))
4007
+ let bOffset = 0
4008
+ // update a to match b
4009
+ let a = /** @type {InsertOp<any>|TextOp|null} */ (originalUpdated.children.start)
4010
+ let b = /** @type {InsertOp<any>|TextOp|null} */ (d2.children.start)
4011
+ let aOffset = 0
4012
+ while (a != null && b != null) {
4013
+ if (!$deleteOp.check(b) && !$deleteOp.check(a)) {
4014
+ const aFormat = a.format
4015
+ const bFormat = b.format
4016
+ const minForward = math.min(b.length - bOffset, a.length - aOffset)
4017
+ aOffset += minForward
4018
+ bOffset += minForward
4019
+ // format and attribution diff identically (unified tri-state — see diffDim):
4020
+ // undefined = unchanged (skip), null = cleared, else a per-key `{k:v}`/`{k:null}` update
4021
+ const fupdate = diffDim(aFormat, bFormat, false)
4022
+ const attributionUpdate = diffDim(a.attribution, b.attribution, true)
4023
+ if (fupdate === undefined && attributionUpdate === undefined) {
4024
+ formattingDiff.retain(minForward)
4025
+ } else {
4026
+ formattingDiff.retain(minForward, fupdate, attributionUpdate)
2281
4027
  }
2282
- for (; cdii < cd.insert.length; cdii++) {
2283
- const a = cd.insert[cdii]
2284
- d.insert(s.$string.check(a) ? a : [a])
4028
+ // update offset and iterators
4029
+ if (bOffset >= b.length) {
4030
+ b = b.next
4031
+ bOffset = 0
2285
4032
  }
2286
- d.delete(cd.remove.length - cdri)
2287
- }
2288
- // create the diff for formatting
2289
- if (hasFormatting) {
2290
- const formattingDiff = create()
2291
- // update opsIs with content diff. then we can figure out the formatting diff.
2292
- const isUpdated = create($deltaAny)
2293
- // copy opsIs to fresh delta
2294
- opsIs.forEach(op => {
2295
- isUpdated.childCnt += op.length
2296
- list.pushEnd(isUpdated.children, op.clone())
2297
- })
2298
- isUpdated.apply(d)
2299
- let shouldI = 0
2300
- let shouldOffset = 0
2301
- let isOp = isUpdated.children.start
2302
- let isOffset = 0
2303
- while (shouldI < opsShould.length && isOp != null) {
2304
- const shouldOp = opsShould[shouldI]
2305
- if (!$deleteOp.check(shouldOp) && !$deleteOp.check(isOp)) {
2306
- const isFormat = isOp.format
2307
- const minForward = math.min(shouldOp.length - shouldOffset, isOp.length - isOffset)
2308
- shouldOffset += minForward
2309
- isOffset += minForward
2310
- if (fun.equalityDeep(shouldOp.format, isFormat)) {
2311
- formattingDiff.retain(minForward)
2312
- } else {
2313
- /**
2314
- * @type {FormattingAttributes}
2315
- */
2316
- const fupdate = {}
2317
- shouldOp.format != null && object.forEach(shouldOp.format, (v, k) => {
2318
- if (!fun.equalityDeep(v, isFormat?.[k] || null)) {
2319
- fupdate[k] = v
2320
- }
2321
- })
2322
- isFormat && object.forEach(isFormat, (_, k) => {
2323
- if (shouldOp?.format?.[k] === undefined) {
2324
- fupdate[k] = null
2325
- }
2326
- })
2327
- formattingDiff.retain(minForward, fupdate)
2328
- }
2329
- // update offset and iterators
2330
- if (shouldOffset >= shouldOp.length) {
2331
- shouldI++
2332
- shouldOffset = 0
2333
- }
2334
- if (isOffset >= isOp.length) {
2335
- isOp = isOp.next
2336
- isOffset = 0
2337
- }
2338
- }
4033
+ if (aOffset >= a.length) {
4034
+ a = a.next
4035
+ aOffset = 0
2339
4036
  }
2340
- d.apply(formattingDiff)
4037
+ /* c8 ignore start */
4038
+ } else {
4039
+ // unreachable: by this point both a and b are insert/text (deletes
4040
+ // were rejected upstream and `originalUpdated` is the result of an
4041
+ // apply, which keeps inserts only).
4042
+ error.unexpectedCase()
2341
4043
  }
2342
- return d
4044
+ /* c8 ignore stop */
2343
4045
  }
2344
- const subd = diffAndApply(ops1.slice(change.index, change.index + change.remove.length), ops2.slice(change.index + currIndexOffset2, change.index + currIndexOffset2 + change.insert.length))
2345
- d.append(subd)
2346
- lastIndex1 = change.index + change.remove.length
2347
- currIndexOffset2 += change.insert.length - change.remove.length
4046
+ // @todo instead of applying, we want to first exec d, then formattingDiff - we need a merge
4047
+ // function!
4048
+ d.apply(formattingDiff)
2348
4049
  }
2349
4050
  for (const attr2 of d2.attrs) {
4051
+ const key = attr2.key
2350
4052
  // @ts-ignore
2351
- const attr1 = d1.attrs[attr2.key]
4053
+ const attr1 = d1.attrs[key]
2352
4054
  if (attr1 == null || (attr1.fingerprint !== attr2.fingerprint)) {
2353
4055
  /* c8 ignore else */
2354
4056
  if ($setAttrOp.check(attr2)) {
2355
- d.setAttr(attr2.key, attr2.value)
4057
+ const prevVal = attr1?.value
4058
+ const nextVal = attr2.value
4059
+ if ($deltaAny.check(prevVal) && $deltaAny.check(nextVal) && compare(prevVal, nextVal)) {
4060
+ // modifyAttr carries the *incremental* attribution update (apply merges it onto the target attr
4061
+ // op's attribution); the inner diff updates its value.
4062
+ // reason: diffDim returns an opaque update (a `{k:null}` removal isn't a canonical Attribution),
4063
+ // while modifyAttr's param is typed Attribution for API ergonomics
4064
+ d.modifyAttr(key, diff(prevVal, nextVal, options), /** @type {Attribution|null|undefined} */ (diffDim(attr1?.attribution, attr2.attribution, true)))
4065
+ } else {
4066
+ // setAttr replaces the whole attr, so it carries the new attribution as data (object-or-none).
4067
+ // `nextVal` is shared from `d2`; deep-clone it when the caller wants an independent result.
4068
+ d.setAttr(key, options.clone ? _cloneMaybeDeltaDeep(nextVal) : nextVal, attr2.attribution)
4069
+ }
4070
+ /* c8 ignore start */
2356
4071
  } else {
2357
- /* c8 ignore next 2 */
2358
4072
  error.unexpectedCase()
2359
4073
  }
4074
+ /* c8 ignore stop */
2360
4075
  }
2361
4076
  }
2362
- for (const attr1 of d1.attrs) {
4077
+ for (const { key } of d1.attrs) {
2363
4078
  // @ts-ignore
2364
- if (d2.attrs[attr1.key] == null) {
2365
- d.deleteAttr(attr1.key)
4079
+ if (d2.attrs[key] == null) {
4080
+ d.deleteAttr(key)
2366
4081
  }
2367
4082
  }
2368
4083
  }
2369
- return /** @type {any} */ (d.done(false))
4084
+ return d.done(false)
4085
+ }
4086
+
4087
+ /**
4088
+ * The tri-state update that undoes a `format`/`attribution` instruction `update` against the stored
4089
+ * value `stored` (object or `null`/none) it was applied to: applying the result after `update` restores
4090
+ * `stored`. Merge the instruction onto `stored` the way an apply resolves it ({@link applyDim}), then
4091
+ * diff the merged result back to `stored` ({@link diffDim}). `undefined` (skip) inverts to `undefined`.
4092
+ *
4093
+ * @param {{[k:string]:any}|null|undefined} stored
4094
+ * @param {{[k:string]:any}|null|undefined} update
4095
+ * @param {boolean} deep `true` for the `attribution` dimension (its `format` key merges per inner key —
4096
+ * see {@link mergeAttr}); `false` for `format` (shallow)
4097
+ * @return {{[k:string]:any}|null|undefined}
4098
+ */
4099
+ const inverseDim = (stored, update, deep) => {
4100
+ if (update === undefined) return undefined
4101
+ const merged = update === null ? null : (deep ? mergeAttr(stored, update, true) : mergeShallow(stored, update, true))
4102
+ return diffDim(object.isEmpty(merged) ? null : merged, stored ?? null, deep)
4103
+ }
4104
+
4105
+ /**
4106
+ * Compute a delta that undoes `d` — a change that was applied to the settled document state `base`
4107
+ * (inserts and set attributes only, e.g. an RDT's final state): `base.apply(d).apply(inverse(d, base))`
4108
+ * equals `base`. Inspired by Quill's `Delta#invert`, `base` supplies what the change alone doesn't
4109
+ * carry: deleted content is re-inserted from `base` (with its stored format/attribution), an
4110
+ * overwritten or deleted attribute is restored to its `base` value, and an inverted `retain`/`modify`
4111
+ * instruction is diffed against the `base` op's stored format/attribution.
4112
+ *
4113
+ * Content only: marks are local/ephemeral cursor state and are not inverted (mirroring {@link diff}).
4114
+ * Like {@link diff}, the result *shares* re-inserted children and restored attribute values with `base`.
4115
+ *
4116
+ * The round-trip holds for changes whose content-consuming ops (`retain`/`delete`/`modify`) stay within
4117
+ * `base`'s bounds. Beyond-base ops degrade gracefully: following ops stay positioned, but there is no
4118
+ * stored content to restore — and what a final apply *materializes* beyond content (e.g. a format set
4119
+ * kept as a trailing formatted retain, or an appended modify) cannot be removed by any change, so such
4120
+ * a change is not fully undone.
4121
+ *
4122
+ * @template {DeltaConf} Conf
4123
+ * @param {DeltaAny} d
4124
+ * @param {Delta<Conf>} base
4125
+ * @return {Delta<Conf>}
4126
+ */
4127
+ export const inverse = (d, base) => {
4128
+ const inv = create(d.name === base.name ? d.name : null, $deltaAny)
4129
+ for (const op of d.attrs) {
4130
+ const baseOp = /** @type {AttrOpAny|undefined} */ ((/** @type {any} */ (base.attrs))[op.key])
4131
+ if ($modifyAttrOp.check(op) && $setAttrOp.check(baseOp) && $deltaAny.check(baseOp.value)) {
4132
+ // the modify edited base's nested delta in place: undo it there, and undo the attribution
4133
+ // instruction it merged onto the attr op
4134
+ inv.modifyAttr(op.key, inverse(op.value, baseOp.value), /** @type {Attribution?} */ (inverseDim(baseOp.attribution, op.attribution, true)))
4135
+ } else if ($setAttrOp.check(baseOp)) {
4136
+ // a set/delete (or a modify that replaced a non-delta value) overwrote a settled attr:
4137
+ // restore value + attribution from base
4138
+ inv.setAttr(op.key, baseOp.value, baseOp.attribution)
4139
+ } else if (!$deleteAttrOp.check(op)) {
4140
+ // a set/modify introduced an attr base didn't have: undo by deleting it
4141
+ inv.deleteAttr(op.key)
4142
+ } // else: deleted an attr base didn't have — nothing to restore
4143
+ }
4144
+ // walk base's children alongside d's ops: an insert consumes no base positions; retain/delete/modify
4145
+ // consume their length in base coordinates
4146
+ let baseOp = /** @type {ChildrenOpAny?} */ (base.children.start)
4147
+ let baseOffset = 0
4148
+ /**
4149
+ * Advance the base cursor by `len`, reporting each traversed chunk `(op, start, end)` (stored
4150
+ * format/attribution are uniform per op, so per-op chunks are exact); returns the length that
4151
+ * extends beyond base's content.
4152
+ *
4153
+ * @param {number} len
4154
+ * @param {(op: TextOp|InsertOp<any>, start: number, end: number) => void} f
4155
+ */
4156
+ const forBase = (len, f) => {
4157
+ while (len > 0 && baseOp != null) {
4158
+ const chunk = math.min(len, baseOp.length - baseOffset)
4159
+ f(/** @type {TextOp|InsertOp<any>} */ (baseOp), baseOffset, baseOffset + chunk)
4160
+ len -= chunk
4161
+ baseOffset += chunk
4162
+ if (baseOffset === baseOp.length) {
4163
+ baseOp = baseOp.next
4164
+ baseOffset = 0
4165
+ }
4166
+ }
4167
+ return len
4168
+ }
4169
+ for (const op of d.children) {
4170
+ if ($textOp.check(op) || $insertOp.check(op)) {
4171
+ inv.delete(op.length)
4172
+ } else if ($retainOp.check(op)) {
4173
+ const rest = forBase(op.retain, (bop, start, end) => {
4174
+ inv.retain(end - start, inverseDim(bop.format, op.format, false), /** @type {Attribution?} */ (inverseDim(bop.attribution, op.attribution, true)))
4175
+ })
4176
+ // beyond base there is no stored content to restore — keep following ops positioned (see the
4177
+ // beyond-base note in the fn docs)
4178
+ rest > 0 && inv.retain(rest)
4179
+ } else if ($modifyOp.check(op)) {
4180
+ if (baseOp == null) {
4181
+ // a modify beyond base edited no stored content — keep the position (mirrors retain-beyond)
4182
+ inv.retain(1)
4183
+ } else {
4184
+ const bop = /** @type {InsertOp<any>} */ (baseOp)
4185
+ const node = /** @type {DeltaAny} */ (bop.insert[baseOffset])
4186
+ inv.modify(inverse(/** @type {DeltaAny} */ (op.value), node), inverseDim(bop.format, op.format, false), /** @type {Attribution?} */ (inverseDim(bop.attribution, op.attribution, true)))
4187
+ forBase(1, () => {})
4188
+ }
4189
+ } else if ($deleteOp.check(op)) {
4190
+ // re-insert the deleted base content with its stored format/attribution
4191
+ forBase(op.delete, (bop, start, end) => {
4192
+ inv.insert(bop.insert.slice(start, end), bop.format, bop.attribution)
4193
+ })
4194
+ }
4195
+ }
4196
+ return inv.done(false)
4197
+ }
4198
+
4199
+ /**
4200
+ * Default pairing predicate for {@link diff}: two delta nodes are paired (and recursed into via
4201
+ * `modify`) when their names match.
4202
+ *
4203
+ * @param {DeltaAny} d1
4204
+ * @param {DeltaAny} d2
4205
+ */
4206
+ const defaultCompare = (d1, d2) => d1.name === d2.name
4207
+
4208
+ /**
4209
+ * @param {string|any} c
4210
+ */
4211
+ const contentLen = c => typeof c === 'string' ? c.length : 1
4212
+
4213
+ /**
4214
+ * Apply removes from c.remove to d, mutating c.remove to exclude the applied items
4215
+ *
4216
+ * @param {DeltaBuilderAny} d
4217
+ * @param {Array<any>} crem
4218
+ * @param {number} len
4219
+ */
4220
+ const applyRemoves = (d, crem, len) => { len > 0 && d.delete(crem.splice(0, len).map(contentLen).reduce(math.add, 0)) }
4221
+ /**
4222
+ * Apply inserts from c.insert to d, mutating c.insert to exclude the applied items. A wholesale-inserted
4223
+ * child is a nested delta shared from `d2`; deep-clone it when `cloneChildren` (the diff's `clone` option)
4224
+ * so the result aliases nothing in `d2` (see {@link DiffOptions}).
4225
+ *
4226
+ * @param {DeltaBuilderAny} d
4227
+ * @param {Array<any>} cins
4228
+ * @param {number} len
4229
+ * @param {boolean} [cloneChildren]
4230
+ */
4231
+ const applyInserts = (d, cins, len, cloneChildren) => { len > 0 && cins.splice(0, len).forEach(ins => d.insert(typeof ins === 'string' ? ins : [ins instanceof _DiffStringWrapper ? ins.str : (cloneChildren ? _cloneMaybeDeltaDeep(ins) : ins)])) }
4232
+
4233
+ /**
4234
+ * @param {DeltaBuilderAny} d
4235
+ * @param {Array<{ index: number, remove: Array<any>, insert: Array<any> }>} changeset
4236
+ * @param {(d1: DeltaAny, d2: DeltaAny) => boolean} compare
4237
+ * @param {DiffOptions} options
4238
+ */
4239
+ const applyChangesetToDelta = (d, changeset, compare, options) => {
4240
+ for (let ci = 0, lastIndex = 0; ci < changeset.length; ci++) {
4241
+ const c = changeset[ci]
4242
+ d.retain(c.index - lastIndex)
4243
+ lastIndex = c.index + c.remove.map(contentLen).reduce(math.add, 0)
4244
+ // @todo do patience diff on the delta-names, then perform maximum number of mods instead of
4245
+ // insert/delete
4246
+ while (true) {
4247
+ const cremoveDeltaIndex = c.remove.findIndex(cc => $deltaAny.check(cc))
4248
+ if (cremoveDeltaIndex < 0) break
4249
+ const cremoveDelta = c.remove[cremoveDeltaIndex]
4250
+ const cinsertDeltaIndex = c.insert.findIndex(cc => $deltaAny.check(cc) && compare(cremoveDelta, cc))
4251
+ if (cinsertDeltaIndex < 0) {
4252
+ applyRemoves(d, c.remove, cremoveDeltaIndex + 1)
4253
+ continue
4254
+ }
4255
+ applyRemoves(d, c.remove, cremoveDeltaIndex)
4256
+ applyInserts(d, c.insert, cinsertDeltaIndex, options.clone)
4257
+ d.modify(diff(c.remove[0], c.insert[0], options))
4258
+ c.remove.splice(0, 1)
4259
+ c.insert.splice(0, 1)
4260
+ }
4261
+ applyRemoves(d, c.remove, c.remove.length)
4262
+ applyInserts(d, c.insert, c.insert.length, options.clone)
4263
+ }
4264
+ return d
4265
+ }
4266
+
4267
+ /**
4268
+ * @param {Array<{ index: number, remove: Array<any>, insert: Array<any> }>} changeset
4269
+ * @param {RegExp} separator
4270
+ */
4271
+ export const diffChangesetWithSeparator = (changeset, separator) => {
4272
+ /**
4273
+ * @type {Array<any>}
4274
+ */
4275
+ const next = []
4276
+ changeset.forEach(change => {
4277
+ // @todo actually split here
4278
+ const cs1 = splitContentArrayByRegexp(change.remove, separator)
4279
+ const cs2 = splitContentArrayByRegexp(change.insert, separator)
4280
+ const fp1 = cs1.map(c => typeof c === 'string' ? c : fingerprintTrait.fingerprint(c))
4281
+ const fp2 = cs2.map(c => typeof c === 'string' ? c : fingerprintTrait.fingerprint(c))
4282
+ const changesetF = patience.diff(fp1, fp2)
4283
+ const nextChangeset = fillFingerprintDiffFromContent(changesetF, cs1, cs2)
4284
+ let prevDiffIndex = 0
4285
+ let nextChangeIndex = change.index
4286
+ // adjust indexes for actual content length and add results to `next`
4287
+ nextChangeset.forEach(c => {
4288
+ // adjust equal content
4289
+ for (; prevDiffIndex < c.index; prevDiffIndex += 1) {
4290
+ nextChangeIndex += contentLen(cs1[prevDiffIndex])
4291
+ }
4292
+ // need to adjust index with the actual length of the previous items
4293
+ c.index = nextChangeIndex
4294
+ next.push(c)
4295
+ })
4296
+ })
4297
+ return next
4298
+ }
4299
+
4300
+ /**
4301
+ * @param {Array<any>} cs
4302
+ * @param {RegExp} regexp
4303
+ */
4304
+ const splitContentArrayByRegexp = (cs, regexp) => {
4305
+ /**
4306
+ * @type {Array<any>}
4307
+ */
4308
+ const res = []
4309
+ cs.forEach(c => {
4310
+ if (typeof c === 'string') {
4311
+ res.push(...patience.splitByRegexp(c, regexp, true))
4312
+ } else {
4313
+ res.push(c)
4314
+ }
4315
+ })
4316
+ return res
4317
+ }
4318
+
4319
+ /**
4320
+ * @template C
4321
+ * @param {Array<{ index: number, remove: Array<string>, insert: Array<string> }>} changeset
4322
+ * @param {Array<C>} is
4323
+ * @param {Array<C>} should
4324
+ * @return {Array<{ index: number, remove: Array<C>, insert: Array<C> }>}
4325
+ */
4326
+ const fillFingerprintDiffFromContent = (changeset, is, should) => {
4327
+ // overwrite fingerprinted content with actual content
4328
+ for (let i = 0, adj = 0; i < changeset.length; i++) {
4329
+ const cd = changeset[i]
4330
+ // @ts-ignore
4331
+ cd.remove = is.slice(cd.index, cd.index + cd.remove.length)
4332
+ // @ts-ignore
4333
+ cd.insert = should.slice(cd.index + adj, cd.index + adj + cd.insert.length)
4334
+ adj += cd.insert.length - cd.remove.length
4335
+ }
4336
+ return /** @type {any} */ (changeset)
2370
4337
  }