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.
- package/README.md +0 -1315
- package/{types → dist}/buffer.d.ts +2 -10
- package/{types → dist}/delta/delta.d.ts +582 -144
- package/dist/delta/position.d.ts +65 -0
- package/dist/delta/rdt/delta.d.ts +57 -0
- package/dist/delta/rdt/dom.d.ts +102 -0
- package/dist/delta/rdt.d.ts +180 -0
- package/dist/delta/transformer/attr.d.ts +64 -0
- package/dist/delta/transformer/attribution-to-format.d.ts +52 -0
- package/dist/delta/transformer/children.d.ts +69 -0
- package/dist/delta/transformer/conform.d.ts +83 -0
- package/dist/delta/transformer/core.d.ts +189 -0
- package/dist/delta/transformer/full-attributions.d.ts +37 -0
- package/dist/delta/transformer/id.d.ts +2 -0
- package/dist/delta/transformer/inline.d.ts +89 -0
- package/dist/delta/transformer/pipe.d.ts +182 -0
- package/dist/delta/transformer/project.d.ts +223 -0
- package/dist/delta/transformer/rename-attrs.d.ts +75 -0
- package/dist/delta/transformer/rename.d.ts +41 -0
- package/dist/delta/transformer/value.d.ts +92 -0
- package/dist/delta/transformer.d.ts +14 -0
- package/dist/diff/patience.d.ts +25 -0
- package/{types → dist}/environment.d.ts +10 -0
- package/dist/hash/fnv1a.d.ts +11 -0
- package/{types → dist}/schema.d.ts +49 -38
- package/{types → dist}/ts.d.ts +4 -0
- package/dist/webcrypto.d.ts +3 -0
- package/package.json +148 -82
- package/src/bin/0serve.js +11 -3
- package/src/broadcastchannel.js +6 -1
- package/src/buffer.js +35 -25
- package/src/decoding.js +1 -1
- package/src/delta/delta.js +2531 -564
- package/src/delta/position.js +165 -0
- package/src/delta/rdt/delta.js +105 -0
- package/src/delta/rdt/dom.js +307 -0
- package/src/delta/rdt.js +255 -0
- package/src/delta/transformer/attr.js +126 -0
- package/src/delta/transformer/attribution-to-format.js +335 -0
- package/src/delta/transformer/children.js +219 -0
- package/src/delta/transformer/conform.js +401 -0
- package/src/delta/transformer/core.js +323 -0
- package/src/delta/transformer/full-attributions.js +282 -0
- package/src/delta/transformer/id.js +13 -0
- package/src/delta/transformer/inline.js +818 -0
- package/src/delta/transformer/pipe.js +272 -0
- package/src/delta/transformer/project.js +526 -0
- package/src/delta/transformer/rename-attrs.js +152 -0
- package/src/delta/transformer/rename.js +84 -0
- package/src/delta/transformer/value.js +214 -0
- package/src/delta/transformer.js +38 -0
- package/src/diff/patience.js +22 -11
- package/src/dom.js +2 -2
- package/src/encoding.js +1 -1
- package/src/environment.js +15 -0
- package/src/hash/fnv1a.js +82 -0
- package/src/number.js +1 -3
- package/src/performance.node.js +3 -3
- package/src/schema.js +231 -177
- package/src/string.js +3 -5
- package/src/testing.js +1 -1
- package/src/trait/equality.js +1 -1
- package/src/trait/fingerprint.js +1 -1
- package/src/ts.js +11 -0
- package/src/bin/gendocs.js +0 -117
- package/src/component.js +0 -414
- package/src/delta/binding.js +0 -372
- package/src/tree.js +0 -546
- package/types/bin/gendocs.d.ts +0 -3
- package/types/component.d.ts +0 -86
- package/types/delta/binding.d.ts +0 -106
- package/types/diff/patience.d.ts +0 -16
- package/types/tree.d.ts +0 -96
- package/types/webcrypto.d.ts +0 -3
- /package/{types → dist}/array.d.ts +0 -0
- /package/{types → dist}/bin/0ecdsa-generate-keypair.d.ts +0 -0
- /package/{types → dist}/bin/0serve.d.ts +0 -0
- /package/{types → dist}/bin/gentesthtml.d.ts +0 -0
- /package/{types → dist}/binary.d.ts +0 -0
- /package/{types → dist}/broadcastchannel.d.ts +0 -0
- /package/{types → dist}/cache.d.ts +0 -0
- /package/{types → dist}/conditions.d.ts +0 -0
- /package/{types → dist}/crypto/aes-gcm.d.ts +0 -0
- /package/{types → dist}/crypto/common.d.ts +0 -0
- /package/{types → dist}/crypto/ecdsa.d.ts +0 -0
- /package/{types → dist}/crypto/jwt.d.ts +0 -0
- /package/{types → dist}/crypto/rsa-oaep.d.ts +0 -0
- /package/{types → dist}/decoding.d.ts +0 -0
- /package/{types → dist}/diff.d.ts +0 -0
- /package/{types → dist}/dom.d.ts +0 -0
- /package/{types → dist}/encoding.d.ts +0 -0
- /package/{types → dist}/error.d.ts +0 -0
- /package/{types → dist}/eventloop.d.ts +0 -0
- /package/{types → dist}/function.d.ts +0 -0
- /package/{types → dist}/hash/rabin-gf2-polynomial.d.ts +0 -0
- /package/{types → dist}/hash/rabin-uncached.d.ts +0 -0
- /package/{types → dist}/hash/rabin.d.ts +0 -0
- /package/{types → dist}/hash/sha256.d.ts +0 -0
- /package/{types → dist}/hash/sha256.node.d.ts +0 -0
- /package/{types → dist}/indexeddb.d.ts +0 -0
- /package/{types → dist}/indexeddbV2.d.ts +0 -0
- /package/{types → dist}/iterator.d.ts +0 -0
- /package/{types → dist}/json.d.ts +0 -0
- /package/{types → dist}/list.d.ts +0 -0
- /package/{types → dist}/logging.common.d.ts +0 -0
- /package/{types → dist}/logging.d.ts +0 -0
- /package/{types → dist}/logging.node.d.ts +0 -0
- /package/{types → dist}/map.d.ts +0 -0
- /package/{types → dist}/math.d.ts +0 -0
- /package/{types → dist}/metric.d.ts +0 -0
- /package/{types → dist}/mutex.d.ts +0 -0
- /package/{types → dist}/number.d.ts +0 -0
- /package/{types → dist}/object.d.ts +0 -0
- /package/{types → dist}/observable.d.ts +0 -0
- /package/{types → dist}/pair.d.ts +0 -0
- /package/{types → dist}/performance.d.ts +0 -0
- /package/{types → dist}/performance.node.d.ts +0 -0
- /package/{types → dist}/pledge.d.ts +0 -0
- /package/{types → dist}/prng/Mt19937.d.ts +0 -0
- /package/{types → dist}/prng/Xoroshiro128plus.d.ts +0 -0
- /package/{types → dist}/prng/Xorshift32.d.ts +0 -0
- /package/{types → dist}/prng.d.ts +0 -0
- /package/{types → dist}/promise.d.ts +0 -0
- /package/{types → dist}/queue.d.ts +0 -0
- /package/{types → dist}/random.d.ts +0 -0
- /package/{types → dist}/set.d.ts +0 -0
- /package/{types → dist}/sort.d.ts +0 -0
- /package/{types → dist}/statistics.d.ts +0 -0
- /package/{types → dist}/storage.d.ts +0 -0
- /package/{types → dist}/string.d.ts +0 -0
- /package/{types → dist}/symbol.d.ts +0 -0
- /package/{types → dist}/testing.d.ts +0 -0
- /package/{types → dist}/time.d.ts +0 -0
- /package/{types → dist}/trait/equality.d.ts +0 -0
- /package/{types → dist}/trait/fingerprint.d.ts +0 -0
- /package/{types → dist}/trait/traits.d.ts +0 -0
- /package/{types → dist}/url.d.ts +0 -0
- /package/{types → dist}/webcrypto.deno.d.ts +0 -0
- /package/{types → dist}/webcrypto.node.d.ts +0 -0
- /package/{types → dist}/webcrypto.react-native.d.ts +0 -0
- /package/{types → dist}/websocket.d.ts +0 -0
package/src/delta/delta.js
CHANGED
|
@@ -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
|
-
*
|
|
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,
|
|
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,
|
|
87
|
-
s.$object({ type: s.$literal('modify'), value: s.$any }),
|
|
88
|
-
s.$object({ type: s.$literal('delete'),
|
|
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 {
|
|
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 {
|
|
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
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
231
|
-
return $insertOp
|
|
232
|
-
}
|
|
233
|
-
|
|
465
|
+
/* c8 ignore start */
|
|
234
466
|
/**
|
|
235
|
-
* @param {ArrayContent}
|
|
467
|
+
* @param {ArrayContent} _newVal
|
|
236
468
|
*/
|
|
237
|
-
_updateInsert (
|
|
238
|
-
//
|
|
239
|
-
|
|
240
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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 {
|
|
402
|
-
* @param {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 {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 {
|
|
492
|
-
* @param {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 {
|
|
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
|
-
|
|
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
|
|
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,
|
|
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),
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
|
|
822
|
-
|
|
823
|
-
|
|
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 =
|
|
844
|
-
export const $textOp =
|
|
845
|
-
export const $deleteOp = /** @type {s.Schema<DeleteOp<any>>} */ (s.$type('deleteOp'))
|
|
846
|
-
export const $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
|
|
851
|
-
export const $deleteAttrOp = /** @type {s.Schema<DeleteAttrOp<any
|
|
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) &&
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
1212
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1820
|
+
/* c8 ignore stop */
|
|
1821
|
+
list.remove(d.children, op)
|
|
1822
|
+
return true
|
|
1240
1823
|
}
|
|
1241
1824
|
|
|
1242
1825
|
/**
|
|
1243
|
-
*
|
|
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 {
|
|
1831
|
+
* @param {DeltaBuilderAny} d
|
|
1832
|
+
* @param {ChildrenOpAny} op
|
|
1833
|
+
* @param {number} offset
|
|
1834
|
+
* @return {ChildrenOpAny}
|
|
1246
1835
|
*/
|
|
1247
|
-
const
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
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
|
-
*
|
|
1259
|
-
* @
|
|
1260
|
-
*
|
|
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 {
|
|
1943
|
+
* @type {Formats?}
|
|
1271
1944
|
*/
|
|
1272
|
-
this.
|
|
1945
|
+
this._usedFormats = null
|
|
1273
1946
|
/**
|
|
1274
1947
|
* @type {Attribution?}
|
|
1275
1948
|
*/
|
|
1276
|
-
this.
|
|
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.
|
|
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
|
-
*
|
|
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
|
-
|
|
2007
|
+
useFormats (attributes) {
|
|
1293
2008
|
modDeltaCheck(this)
|
|
1294
|
-
this.
|
|
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
|
-
|
|
2043
|
+
updateUsedFormats (name, value) {
|
|
1303
2044
|
modDeltaCheck(this)
|
|
1304
2045
|
if (value == null) {
|
|
1305
|
-
this.
|
|
1306
|
-
delete this.
|
|
1307
|
-
if (object.isEmpty(this.
|
|
1308
|
-
this.
|
|
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
|
-
|
|
1311
|
-
|
|
1312
|
-
this.
|
|
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.
|
|
1326
|
-
delete this.
|
|
1327
|
-
if (object.isEmpty(this.
|
|
1328
|
-
this.
|
|
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
|
-
|
|
1331
|
-
|
|
1332
|
-
this.
|
|
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 {
|
|
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
|
|
2092
|
+
insert (insert, formatting, attribution) {
|
|
1348
2093
|
modDeltaCheck(this)
|
|
1349
|
-
const
|
|
1350
|
-
const mergedAttribution =
|
|
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 => (
|
|
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,
|
|
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 */,
|
|
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 {
|
|
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
|
|
2130
|
+
modify (modify, formatting, attribution) {
|
|
1384
2131
|
modDeltaCheck(this)
|
|
1385
|
-
const
|
|
1386
|
-
const mergedAttribution =
|
|
1387
|
-
list.pushEnd(this.children, new ModifyOp(modify,
|
|
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 {
|
|
2179
|
+
* @param {Formats?} [format]
|
|
1395
2180
|
* @param {Attribution?} [attribution]
|
|
1396
2181
|
*/
|
|
1397
|
-
retain (len, format
|
|
1398
|
-
modDeltaCheck(this)
|
|
1399
|
-
const mergedFormats =
|
|
1400
|
-
const mergedAttribution =
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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),
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
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.
|
|
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
|
-
|
|
1517
|
-
//
|
|
1518
|
-
|
|
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
|
-
*
|
|
1538
|
-
* @param {
|
|
1539
|
-
* @return {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
2430
|
+
opsI = opNextUndeleted(opsI)
|
|
1577
2431
|
offset = 0
|
|
1578
2432
|
}
|
|
1579
|
-
|
|
1580
2433
|
if (opsI != null) {
|
|
1581
|
-
if (op.format
|
|
1582
|
-
//
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
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
|
-
|
|
1593
|
-
|
|
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
|
-
|
|
1600
|
-
this.childCnt += remainingLen
|
|
2461
|
+
_insertChild(this, null, scheduleForMerge(new DeleteOp(remainingLen)))
|
|
1601
2462
|
break
|
|
1602
|
-
} else if ($
|
|
1603
|
-
const delLen = opsI.length - offset
|
|
1604
|
-
|
|
1605
|
-
if (
|
|
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 { //
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
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 (
|
|
1648
|
-
|
|
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)
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
opsI
|
|
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
|
-
|
|
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
|
-
|
|
2556
|
+
_removeChild(this, opsI)
|
|
2557
|
+
opsI = opNextUndeleted(opsI)
|
|
2558
|
+
offset = 0
|
|
1661
2559
|
} else {
|
|
1662
|
-
opsI
|
|
2560
|
+
_shrinkChild(this, opsI, 0, 1)
|
|
1663
2561
|
scheduleForMerge(opsI)
|
|
1664
2562
|
}
|
|
1665
|
-
|
|
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
|
-
|
|
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
|
|
1677
|
-
|
|
1678
|
-
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) || $
|
|
2670
|
+
if ($insertOp.check(otherChild) || $textOp.check(otherChild)) {
|
|
1753
2671
|
if (!priority) {
|
|
1754
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
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
|
-
|
|
1812
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
2831
|
+
_insertChild(this, null, child.clone())
|
|
1866
2832
|
}
|
|
1867
|
-
|
|
1868
|
-
|
|
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
|
-
*
|
|
1876
|
-
*
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
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
|
-
* @
|
|
1951
|
-
*
|
|
1952
|
-
*
|
|
1953
|
-
*
|
|
1954
|
-
*
|
|
1955
|
-
*
|
|
1956
|
-
*
|
|
1957
|
-
*
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
*
|
|
1962
|
-
* @
|
|
1963
|
-
* @
|
|
1964
|
-
*
|
|
1965
|
-
*
|
|
1966
|
-
*
|
|
1967
|
-
*
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
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<
|
|
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
|
|
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
|
|
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
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
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
|
-
|
|
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 {
|
|
3731
|
+
* @template {s.Schema<DeltaAny>} Schema
|
|
2051
3732
|
* @overload
|
|
2052
3733
|
* @param {NodeName} nodeName
|
|
2053
|
-
* @param {
|
|
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
|
-
* @
|
|
3909
|
+
* @param {DiffOptions} [options]
|
|
3910
|
+
* @return {Delta<Conf>}
|
|
2164
3911
|
*/
|
|
2165
|
-
export const diff = (d1, d2) => {
|
|
2166
|
-
|
|
2167
|
-
|
|
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
|
-
|
|
2198
|
-
right1
|
|
2199
|
-
|
|
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<
|
|
3951
|
+
* @type {Array<fingerprintTrait.Fingerprintable>}
|
|
2203
3952
|
*/
|
|
2204
|
-
const
|
|
3953
|
+
const cs1 = []
|
|
2205
3954
|
/**
|
|
2206
|
-
* @type {Array<
|
|
3955
|
+
* @type {Array<fingerprintTrait.Fingerprintable>}
|
|
2207
3956
|
*/
|
|
2208
|
-
const
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
left1
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
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
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
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
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
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
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
4028
|
+
// update offset and iterators
|
|
4029
|
+
if (bOffset >= b.length) {
|
|
4030
|
+
b = b.next
|
|
4031
|
+
bOffset = 0
|
|
2285
4032
|
}
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4044
|
+
/* c8 ignore stop */
|
|
2343
4045
|
}
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
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[
|
|
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
|
-
|
|
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
|
|
4077
|
+
for (const { key } of d1.attrs) {
|
|
2363
4078
|
// @ts-ignore
|
|
2364
|
-
if (d2.attrs[
|
|
2365
|
-
d.deleteAttr(
|
|
4079
|
+
if (d2.attrs[key] == null) {
|
|
4080
|
+
d.deleteAttr(key)
|
|
2366
4081
|
}
|
|
2367
4082
|
}
|
|
2368
4083
|
}
|
|
2369
|
-
return
|
|
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
|
}
|