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
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import * as s from '../../schema.js'
|
|
2
|
+
import * as error from '../../error.js'
|
|
3
|
+
import * as delta from '../delta.js'
|
|
4
|
+
import * as fingerprintTrait from '../../trait/fingerprint.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @template {import('../delta.js').DeltaConf} [A={}]
|
|
8
|
+
* @template {import('../delta.js').DeltaConf} [B={}]
|
|
9
|
+
*/
|
|
10
|
+
class TransformResult {
|
|
11
|
+
/**
|
|
12
|
+
* @param {import('../delta.js').DeltaBuilder<A>?} a
|
|
13
|
+
* @param {import('../delta.js').DeltaBuilder<B>?} b
|
|
14
|
+
*/
|
|
15
|
+
constructor (a, b) {
|
|
16
|
+
/**
|
|
17
|
+
* @type {import('../delta.js').DeltaBuilder<A>?}
|
|
18
|
+
*/
|
|
19
|
+
this.a = a
|
|
20
|
+
/**
|
|
21
|
+
* @type {import('../delta.js').DeltaBuilder<B>?}
|
|
22
|
+
*/
|
|
23
|
+
this.b = b
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
isEmpty () {
|
|
27
|
+
return this.a == null && this.b == null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
clear () {
|
|
31
|
+
this.a = null
|
|
32
|
+
this.b = null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {import('../delta.js').DeltaBuilder<A>?} a
|
|
37
|
+
*/
|
|
38
|
+
applyA (a) {
|
|
39
|
+
if (a !== null) {
|
|
40
|
+
if (this.a == null) {
|
|
41
|
+
this.a = a // adopt: `a` is donated to this result (callers must not reuse it)
|
|
42
|
+
} else {
|
|
43
|
+
this.a.apply(a, { move: true }) // ...so the merge may consume `a` rather than freeze-copy it
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return this
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param {import('../delta.js').DeltaBuilder<B>?} b
|
|
51
|
+
*/
|
|
52
|
+
applyB (b) {
|
|
53
|
+
if (b !== null) {
|
|
54
|
+
if (this.b == null) {
|
|
55
|
+
this.b = b // adopt: `b` is donated to this result (callers must not reuse it)
|
|
56
|
+
} else {
|
|
57
|
+
this.b.apply(b, { move: true }) // ...so the merge may consume `b` rather than freeze-copy it
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return this
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
reverse () {
|
|
64
|
+
return new TransformResult(this.b, this.a)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @typedef {TransformResult<any,any>} TransformResultAny
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @template {import('../delta.js').DeltaConf} A
|
|
74
|
+
* @template {import('../delta.js').DeltaConf} B
|
|
75
|
+
* @param {s.Schema<import('../delta.js').Delta<A>>} $a
|
|
76
|
+
* @param {s.Schema<import('../delta.js').Delta<B>>} $b
|
|
77
|
+
* @return {s.Schema<TransformResult<A,B>>}
|
|
78
|
+
*/
|
|
79
|
+
export const $tresult = ($a, $b) => /** @type {any} */ (s.$instanceOf(TransformResult, tr => (tr.a === null || $a.check(tr.a)) && (tr.b === null || $b.check(tr.b))))
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Build a {@link TransformResult}. Exposed so transformers in `./` can construct their results.
|
|
83
|
+
*
|
|
84
|
+
* @template {import('../delta.js').DeltaBuilderAny} [DeltaA=import('../delta.js').DeltaBuilderAny]
|
|
85
|
+
* @template {import('../delta.js').DeltaBuilderAny} [DeltaB=import('../delta.js').DeltaBuilderAny]
|
|
86
|
+
* @param {DeltaA?} a
|
|
87
|
+
* @param {DeltaB?} b
|
|
88
|
+
*/
|
|
89
|
+
export const createTransformResult = (a, b) => new TransformResult(a, b)
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* A stateful, schema-bound mapper between changes of side A and side B, materialized from a
|
|
93
|
+
* {@link Template} via `init()`.
|
|
94
|
+
*
|
|
95
|
+
* ## Deltas are manipulated in place
|
|
96
|
+
*
|
|
97
|
+
* `apply`/`applyA`/`applyB` **mutate their input deltas directly** — they rebase nodes and splice op
|
|
98
|
+
* lists in place, and some transformers `apply(other, { move: true })` the input into an accumulator —
|
|
99
|
+
* rather than editing only through the copy-on-write `apply`/`rebase` surface. This is a deliberate
|
|
100
|
+
* exception to the general rule (a delta is normally edited only via `apply`/`rebase`, which re-clone
|
|
101
|
+
* frozen content on first touch); it keeps the transform allocation-free, but it means a transformer is
|
|
102
|
+
* an *owning* consumer of the delta it is given. **Callers must pass a privately-owned, fully-mutable
|
|
103
|
+
* delta** — never a shared or `done` (frozen) one. A caller holding a shared read delta must
|
|
104
|
+
* {@link import('../delta.js').cloneDeep deep-clone} it first (this is exactly what a
|
|
105
|
+
* {@link import('../rdt.js').Binding} does before routing a change through its transformer).
|
|
106
|
+
*
|
|
107
|
+
* @template {import('../delta.js').DeltaConf} A
|
|
108
|
+
* @template {import('../delta.js').DeltaConf} B
|
|
109
|
+
*/
|
|
110
|
+
export class Transformer {
|
|
111
|
+
/**
|
|
112
|
+
* @param {s.Schema<import('../delta.js').Delta<A>>} $in input delta schema
|
|
113
|
+
* @param {s.Schema<import('../delta.js').Delta<B>>} $out output delta schema (computed by the factory)
|
|
114
|
+
*/
|
|
115
|
+
constructor ($in, $out) {
|
|
116
|
+
/**
|
|
117
|
+
* @type {s.Schema<import('../delta.js').Delta<A>>}
|
|
118
|
+
*/
|
|
119
|
+
this.$in = $in
|
|
120
|
+
/**
|
|
121
|
+
* @type {s.Schema<import('../delta.js').Delta<B>>}
|
|
122
|
+
*/
|
|
123
|
+
this.$out = $out
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @param {TransformResult<A, B>} tin
|
|
128
|
+
* @return {TransformResult<A,B>}
|
|
129
|
+
*/
|
|
130
|
+
apply (tin) {
|
|
131
|
+
const ta = tin.a
|
|
132
|
+
const tb = tin.b
|
|
133
|
+
const ares = ta != null ? this.applyA(ta) : createTransformResult(null, null)
|
|
134
|
+
// transform tb if necessary
|
|
135
|
+
if (tb != null) {
|
|
136
|
+
if (ares.b != null) {
|
|
137
|
+
tb.rebase(ares.b, false)
|
|
138
|
+
}
|
|
139
|
+
const bres = this.applyB(tb) // a fresh, single-use result -> its sides may be consumed (moved)
|
|
140
|
+
if (ares.a) {
|
|
141
|
+
ares.a.apply(bres.a, { move: true })
|
|
142
|
+
} else {
|
|
143
|
+
ares.a = bres.a
|
|
144
|
+
}
|
|
145
|
+
if (ares.b) {
|
|
146
|
+
ares.b.apply(bres.b, { move: true })
|
|
147
|
+
} else {
|
|
148
|
+
ares.b = bres.b
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return ares
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* @param {import('../delta.js').DeltaBuilder<A>} t
|
|
156
|
+
* @return {TransformResult<A,B>}
|
|
157
|
+
*/
|
|
158
|
+
applyA (t) {
|
|
159
|
+
return this.apply(createTransformResult(t, null))
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* @param {import('../delta.js').DeltaBuilder<B>} t
|
|
164
|
+
* @return {TransformResult<A,B>}
|
|
165
|
+
*/
|
|
166
|
+
applyB (t) {
|
|
167
|
+
return this.apply(createTransformResult(null, t))
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Recognize a {@link Transformer} by its `$type` tag. Every value is either a `Transformer` or a
|
|
173
|
+
* {@link Template}, never both, so the two roles can each own a single prototype `$type`.
|
|
174
|
+
*
|
|
175
|
+
* @type {s.Schema<Transformer<any,any>>}
|
|
176
|
+
*/
|
|
177
|
+
export const $transformer = /** @type {s.Schema<Transformer<any,any>>} */ (Transformer.prototype.$type = s.$type('d:transformer', Transformer))
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Like {@link $transformer}, but carries the side-A / side-B delta types for typechecking; the two
|
|
181
|
+
* schema params do not constrain the runtime check.
|
|
182
|
+
*
|
|
183
|
+
* @template {import('../delta.js').DeltaConf} A
|
|
184
|
+
* @template {import('../delta.js').DeltaConf} B
|
|
185
|
+
* @param {s.Schema<import('../delta.js').Delta<A>>|A} _a
|
|
186
|
+
* @param {s.Schema<import('../delta.js').Delta<B>>|A} _b
|
|
187
|
+
* @return {s.Schema<Transformer<A,B>>}
|
|
188
|
+
*/
|
|
189
|
+
/* @__NO_SIDE_EFFECTS__ */
|
|
190
|
+
export const transformerWith = (_a, _b) => /** @type {s.Schema<Transformer<A,B>>} */ ($transformer)
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* A function from an input delta schema to a {@link Template} — the schema-deferred form of a
|
|
194
|
+
* transformer factory. Used for `pipe` stages, recursive composition, and `bind`.
|
|
195
|
+
*
|
|
196
|
+
* @template {import('../delta.js').DeltaConf} In
|
|
197
|
+
* @template {import('../delta.js').DeltaConf} Out
|
|
198
|
+
* @typedef {($in: s.Schema<import('../delta.js').Delta<In>>) => Template<In, Out>} TemplateFactory
|
|
199
|
+
*/
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Force a factory's computed output conf to display fully-resolved (flatten `Omit<…> & {…}` / bare
|
|
203
|
+
* alias artifacts) while staying a valid `DeltaConf` for use as a `Transformer`/`Delta` type
|
|
204
|
+
* argument — conf-aware and depth-limited (nested `Delta<…>` stay intact). Implemented as an
|
|
205
|
+
* overwrite-with-nothing, reusing {@link import('../delta.js').DeltaConfOverwrite}'s
|
|
206
|
+
* prettify+coerce recipe. Apply to every factory's `OUT` typedef whose result is not already
|
|
207
|
+
* produced by `DeltaConfOverwrite`.
|
|
208
|
+
*
|
|
209
|
+
* @template {import('../delta.js').DeltaConf} Conf
|
|
210
|
+
* @typedef {import('../delta.js').DeltaConfOverwrite<Conf, {}>} ResolveOut
|
|
211
|
+
*/
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* The reusable, schema-bound form of a transformer: built by a factory (`attr($d, …)`), it holds the
|
|
215
|
+
* input/output delta schemas (`$in`/`$out`) and materializes a fresh stateful {@link Transformer} via
|
|
216
|
+
* the parameterless `init()`. Concrete templates `extend Template`.
|
|
217
|
+
*
|
|
218
|
+
* Templates are {@link fingerprintTrait.Fingerprintable}: this is what lets them be embedded as values
|
|
219
|
+
* (holes) in a `delta.create(...)` projection spec without collapsing the spec's type.
|
|
220
|
+
*
|
|
221
|
+
* @template {import('../delta.js').DeltaConf} [IN=any]
|
|
222
|
+
* @template {import('../delta.js').DeltaConf} [OUT=any]
|
|
223
|
+
*/
|
|
224
|
+
export class Template {
|
|
225
|
+
/**
|
|
226
|
+
* @param {s.Schema<import('../delta.js').Delta<IN>>} $in input delta schema
|
|
227
|
+
* @param {s.Schema<import('../delta.js').Delta<OUT>>} $out output delta schema (computed by the factory)
|
|
228
|
+
*/
|
|
229
|
+
constructor ($in, $out) {
|
|
230
|
+
/**
|
|
231
|
+
* @type {s.Schema<import('../delta.js').Delta<IN>>}
|
|
232
|
+
*/
|
|
233
|
+
this.$in = $in
|
|
234
|
+
/**
|
|
235
|
+
* @type {s.Schema<import('../delta.js').Delta<OUT>>}
|
|
236
|
+
*/
|
|
237
|
+
this.$out = $out
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Stable, mangle-safe identifier folded into this template's fingerprint. Subclasses with
|
|
242
|
+
* configuration override it so that e.g. `attr('a')` and `attr('b')` fingerprint differently
|
|
243
|
+
* (do NOT use `constructor.name` — class names do not survive mangling).
|
|
244
|
+
*
|
|
245
|
+
* @return {string}
|
|
246
|
+
*/
|
|
247
|
+
get name () { return 'lib0:template' }
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* @return {string}
|
|
251
|
+
*/
|
|
252
|
+
[fingerprintTrait.FingerprintTraitSymbol] () { return this.name }
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Instantiate a fresh stateful {@link Transformer} bound to this template's schema.
|
|
256
|
+
*
|
|
257
|
+
* @return {Transformer<IN, OUT>}
|
|
258
|
+
*/
|
|
259
|
+
/* c8 ignore next 3 */
|
|
260
|
+
init () {
|
|
261
|
+
return error.methodUnimplemented()
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Bind an input schema into the `$d => Template` lambda pattern (the shape used by {@link pipe}
|
|
267
|
+
* stages, {@link import('./children.js').children} handlers, and `bind`) at the top level: calls
|
|
268
|
+
* `fn` with `$d` and returns its {@link Template}. Lets you write `transform($d, $d => pipe($d, …))`
|
|
269
|
+
* — keeping every template construction in the same lambda style — without a separate `const $d`
|
|
270
|
+
* statement. The lambda's `$d` parameter is typed as the input delta schema, and the concrete
|
|
271
|
+
* template subtype `fn` returns is preserved in the result.
|
|
272
|
+
*
|
|
273
|
+
* @template {import('../delta.js').DeltaConf} IN
|
|
274
|
+
* @template {Template<IN, any>} T
|
|
275
|
+
* @param {s.Schema<import('../delta.js').Delta<IN>>} $d
|
|
276
|
+
* @param {($d: s.Schema<import('../delta.js').Delta<IN>>) => T} fn
|
|
277
|
+
* @return {T}
|
|
278
|
+
*/
|
|
279
|
+
export const transform = ($d, fn) => fn($d)
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Rebuild `$in`'s delta schema with a replaced attrs-shape map (keeping name/children/text). Used by
|
|
283
|
+
* the attr-renaming/filtering factories to compute a concrete output schema (`$out`). Falls back to
|
|
284
|
+
* `$deltaAny` when `$in` is loose (not a concrete `$delta(...)`).
|
|
285
|
+
*
|
|
286
|
+
* @param {s.Schema<import('../delta.js').DeltaAny>} $in
|
|
287
|
+
* @param {{[k:string|number]: s.Schema<any>}} newAttrsShape
|
|
288
|
+
* @return {s.Schema<import('../delta.js').DeltaAny>}
|
|
289
|
+
*/
|
|
290
|
+
export const withAttrs = ($in, newAttrsShape) => delta.$$delta.check($in)
|
|
291
|
+
? new delta.$Delta($in.shape.$name, s.$object(newAttrsShape), $in.shape.$children, $in.shape.hasText, false, $in.shape.$formats)
|
|
292
|
+
: delta.$deltaAny
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Rebuild `$in`'s delta schema with a replaced node `name` (keeping attrs/children/text). Falls back
|
|
296
|
+
* to `$deltaAny` when `$in` is loose.
|
|
297
|
+
*
|
|
298
|
+
* @param {s.Schema<import('../delta.js').DeltaAny>} $in
|
|
299
|
+
* @param {string} name
|
|
300
|
+
* @return {s.Schema<import('../delta.js').DeltaAny>}
|
|
301
|
+
*/
|
|
302
|
+
export const withName = ($in, name) => delta.$$delta.check($in)
|
|
303
|
+
? new delta.$Delta(s.$(name), $in.shape.$attrs, $in.shape.$children, $in.shape.hasText, false, $in.shape.$formats)
|
|
304
|
+
: delta.$deltaAny
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* The `{ attrKey: Schema }` map of `$in`'s attrs, or `null` when `$in` is loose / has no object attrs
|
|
308
|
+
* schema. Lets attr-renaming/filtering factories compute a concrete `$out`.
|
|
309
|
+
*
|
|
310
|
+
* @param {s.Schema<import('../delta.js').DeltaAny>} $in
|
|
311
|
+
* @return {{[k:string|number]: s.Schema<any>}?}
|
|
312
|
+
*/
|
|
313
|
+
export const attrsShapeOf = ($in) => delta.$$delta.check($in) && s.$$object.check($in.shape.$attrs)
|
|
314
|
+
? /** @type {{[k:string|number]: s.Schema<any>}} */ ($in.shape.$attrs.shape)
|
|
315
|
+
: null
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Recognize a {@link Template} by its `$type` tag. Used to detect transformer "holes" embedded in a
|
|
319
|
+
* `project` spec.
|
|
320
|
+
*
|
|
321
|
+
* @type {s.Schema<Template>}
|
|
322
|
+
*/
|
|
323
|
+
export const $template = /** @type {s.Schema<Template>} */ (Template.prototype.$type = s.$type('d:template', Template))
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import * as delta from '../delta.js'
|
|
2
|
+
import * as s from '../../schema.js'
|
|
3
|
+
import * as object from '../../object.js'
|
|
4
|
+
import * as math from '../../math.js'
|
|
5
|
+
import { Transformer, Template, createTransformResult } from './core.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* # `fullAttributions`
|
|
9
|
+
*
|
|
10
|
+
* A **stateful** transformer that normalizes the `attribution` dimension: whenever an op changes
|
|
11
|
+
* attribution, it re-emits the **complete accumulated** attribution for that position instead of just
|
|
12
|
+
* the incremental change. So a stream like `insert('a', undefined, { insert: [] })` then
|
|
13
|
+
* `retain(1, undefined, { insertAt: 4 })` is forwarded as `insert('a', …, { insert: [] })` then
|
|
14
|
+
* `retain(1, undefined, { insert: [], insertAt: 4 })`. Content, formats, structure and marks pass
|
|
15
|
+
* through untouched. It is the natural upstream of {@link import('./attribution-to-format.js').attributionToFormat},
|
|
16
|
+
* lifting that transformer's "assume the full attribution is present" requirement.
|
|
17
|
+
*
|
|
18
|
+
* State is a private **content-free attribution overlay** (`this.overlay`): a delta mirroring the
|
|
19
|
+
* document shape but carrying no content — text/scalar runs are `retain(len, _, attr)`, child nodes are
|
|
20
|
+
* `modify(childOverlay, _, nodeAttr)`, node attributes are per-key attr ops — each holding that
|
|
21
|
+
* position's accumulated attribution as an *instruction* (`{k:v}` set, `{k:null}` cleared).
|
|
22
|
+
*
|
|
23
|
+
* Per `applyA(d)`: a single read-only walk over `d` (in step with the overlay) builds one content-free
|
|
24
|
+
* delta `full` carrying the resolved full attribution at exactly `d`'s touched positions. `full` is then
|
|
25
|
+
* used twice — `d.apply(full)` enriches the input *in place* (that is the output: no content is copied,
|
|
26
|
+
* `apply` only splits content-free retains), and `overlay.rebase(d)` + `overlay.apply(full)` update the
|
|
27
|
+
* state, both in place and bounded by the change. `applyB` is passthrough + overlay realign (the view
|
|
28
|
+
* never attributes).
|
|
29
|
+
*
|
|
30
|
+
* Object ownership: `applyA` mutates the input delta's **structure** in place (legitimate — the framework
|
|
31
|
+
* hands a transformer a non-done change builder to consume, cf. `children.js`) and returns it as the
|
|
32
|
+
* output. It never mutates a `format`/`attribution` **object** in place. We do **not** own those objects —
|
|
33
|
+
* they may belong to done (shared) data — so they are read-only here, and every attribution we emit is a
|
|
34
|
+
* freshly allocated object (`mergeLeaves`/`applyFull`, mirroring delta.js's `mergeAttr`). The overlay and
|
|
35
|
+
* the output only ever receive fresh top-level objects (`apply`/`rebase` merge via `mergeAttr`; `clone`
|
|
36
|
+
* shallow-copies via `_cloneAttrs`), so no shared object is updated through a reference. Nested `format`
|
|
37
|
+
* maps and leaf values are shared by reference exactly as delta.js shares them — safe because nothing is
|
|
38
|
+
* written in place.
|
|
39
|
+
*
|
|
40
|
+
* Per-change cost is O(change); the overlay is O(document attribution structure). See {@link fullAttributions}.
|
|
41
|
+
*
|
|
42
|
+
* @module delta/transformer/full-attributions
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Instruction-form shallow merge of `update` into `base`: set every present key, **keep** a `{k:null}`
|
|
47
|
+
* removal verbatim (so it still clears downstream), skip `undefined`. Used for attribution leaves and for
|
|
48
|
+
* the nested `format` map. Mirrors `mergeShallow` with `resolve = false` (delta.js). Allocates a fresh
|
|
49
|
+
* object; never mutates `base`/`update` (they may be done, shared data — read-only).
|
|
50
|
+
*
|
|
51
|
+
* @param {{[k:string]:any}|null|undefined} base
|
|
52
|
+
* @param {{[k:string]:any}} update
|
|
53
|
+
* @return {{[k:string]:any}}
|
|
54
|
+
*/
|
|
55
|
+
const mergeLeaves = (base, update) => {
|
|
56
|
+
const r = s.$objectAny.check(base) ? { ...base } : {}
|
|
57
|
+
for (const k in update) if (update[k] !== undefined) r[k] = update[k] // set, or keep `null` (instruction)
|
|
58
|
+
return r
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The full attribution at a position after applying a tri-state attribution `update` onto the
|
|
63
|
+
* accumulated `base`, in instruction form. `undefined` ⇒ unchanged, `null` ⇒ clear all; otherwise a
|
|
64
|
+
* shallow tri-state merge whose single nested `format` key merges one level. Mirrors `mergeAttr` with
|
|
65
|
+
* `resolve = false` (delta.js) — kept in instruction form so a cleared key survives as `{k:null}` and is
|
|
66
|
+
* re-emitted (the "set present + clear removed" contract). Allocates a fresh object for the merge case;
|
|
67
|
+
* never mutates `base`/`update` (they may be done, shared data — read-only).
|
|
68
|
+
*
|
|
69
|
+
* @param {{[k:string]:any}|null|undefined} base
|
|
70
|
+
* @param {{[k:string]:any}|null|undefined} update
|
|
71
|
+
* @return {{[k:string]:any}|null|undefined}
|
|
72
|
+
*/
|
|
73
|
+
const applyFull = (base, update) => {
|
|
74
|
+
if (update === undefined) return base
|
|
75
|
+
if (update === null) return null
|
|
76
|
+
const r = s.$objectAny.check(base) ? { ...base } : {}
|
|
77
|
+
for (const k in update) {
|
|
78
|
+
const v = update[k]
|
|
79
|
+
if (v === undefined) continue
|
|
80
|
+
if (k === 'format' && s.$objectAny.check(v)) {
|
|
81
|
+
const sub = mergeLeaves(r.format, v)
|
|
82
|
+
if (object.isEmpty(sub)) delete r.format
|
|
83
|
+
else r.format = sub
|
|
84
|
+
} else r[k] = v // leaf set, or `null` kept verbatim (instruction)
|
|
85
|
+
}
|
|
86
|
+
return r
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Normalize a *data* op's attribution (insert/text/setAttr/deleteAttr) for storage in `full`: `null`
|
|
91
|
+
* means "none" (⇒ `undefined`, i.e. a gap), an object is already the complete attribution of new content.
|
|
92
|
+
*
|
|
93
|
+
* @param {{[k:string]:any}|null|undefined} a
|
|
94
|
+
* @return {{[k:string]:any}|undefined}
|
|
95
|
+
*/
|
|
96
|
+
const dataAttr = a => (a == null ? undefined : a)
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Build the content-free `full` delta for change `d` against the read-only `overlay` (the accumulated
|
|
100
|
+
* attribution, or `null` for freshly inserted content). Carries the resolved full attribution at exactly
|
|
101
|
+
* `d`'s touched positions; recurses into child nodes.
|
|
102
|
+
*
|
|
103
|
+
* @param {import('../delta.js').DeltaAny} d
|
|
104
|
+
* @param {import('../delta.js').DeltaAny|null} overlay
|
|
105
|
+
* @return {import('../delta.js').DeltaBuilderAny}
|
|
106
|
+
*/
|
|
107
|
+
const buildFull = (d, overlay) => {
|
|
108
|
+
const full = /** @type {any} */ (delta.create())
|
|
109
|
+
let cur = overlay == null ? null : overlay.children.start
|
|
110
|
+
let off = 0
|
|
111
|
+
/**
|
|
112
|
+
* Advance the read cursor by `n` positions without reading (untouched retains, deletes).
|
|
113
|
+
* @param {number} n
|
|
114
|
+
*/
|
|
115
|
+
const skip = n => {
|
|
116
|
+
let rem = n
|
|
117
|
+
while (rem > 0 && cur != null) {
|
|
118
|
+
const take = math.min(cur.length - off, rem)
|
|
119
|
+
off += take; rem -= take
|
|
120
|
+
if (off >= cur.length) { cur = cur.next; off = 0 }
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Read the attribution run at the cursor (≤ `rem` positions), advancing. Beyond the overlay ⇒ no prior
|
|
125
|
+
* attribution.
|
|
126
|
+
* @param {number} rem
|
|
127
|
+
* @return {[number, {[k:string]:any}|null|undefined]}
|
|
128
|
+
*/
|
|
129
|
+
const readRun = rem => {
|
|
130
|
+
if (cur == null) return [rem, undefined]
|
|
131
|
+
const take = math.min(cur.length - off, rem)
|
|
132
|
+
const attr = /** @type {any} */ (cur).attribution
|
|
133
|
+
off += take
|
|
134
|
+
if (off >= cur.length) { cur = cur.next; off = 0 }
|
|
135
|
+
return [take, attr]
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* The nested overlay + node-attribution at the cursor (a Modify run; else `null`/leaf attr), advancing
|
|
139
|
+
* one position.
|
|
140
|
+
* @return {{ nested: import('../delta.js').DeltaAny|null, attr: {[k:string]:any}|null|undefined }}
|
|
141
|
+
*/
|
|
142
|
+
const readModify = () => {
|
|
143
|
+
const node = cur
|
|
144
|
+
const attr = node == null ? undefined : /** @type {any} */ (node).attribution
|
|
145
|
+
const nested = (node != null && delta.$modifyOp.check(node)) ? node.value : null
|
|
146
|
+
if (cur != null) { off += 1; if (off >= cur.length) { cur = cur.next; off = 0 } }
|
|
147
|
+
return { nested, attr }
|
|
148
|
+
}
|
|
149
|
+
for (const op of d.children) {
|
|
150
|
+
if (delta.$retainOp.check(op)) {
|
|
151
|
+
if (op.attribution === undefined) {
|
|
152
|
+
full.retain(op.retain); skip(op.retain) // untouched ⇒ gap
|
|
153
|
+
} else {
|
|
154
|
+
let rem = op.retain
|
|
155
|
+
while (rem > 0) {
|
|
156
|
+
const [take, runAttr] = readRun(rem)
|
|
157
|
+
full.retain(take, undefined, applyFull(runAttr, op.attribution))
|
|
158
|
+
rem -= take
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
} else if (delta.$textOp.check(op)) {
|
|
162
|
+
full.retain(op.insert.length, undefined, dataAttr(op.attribution)) // new content: attribution already full
|
|
163
|
+
} else if (delta.$insertOp.check(op)) {
|
|
164
|
+
for (const el of op.insert) {
|
|
165
|
+
if (delta.$deltaAny.check(el)) full.modify(buildFull(el, null), undefined, dataAttr(op.attribution))
|
|
166
|
+
else full.retain(1, undefined, dataAttr(op.attribution))
|
|
167
|
+
}
|
|
168
|
+
} else if (delta.$deleteOp.check(op)) {
|
|
169
|
+
skip(op.delete) // positions removed: nothing in `full`
|
|
170
|
+
} else { // $modifyOp
|
|
171
|
+
const { nested, attr } = readModify()
|
|
172
|
+
full.modify(buildFull(op.value, nested), undefined, applyFull(attr, op.attribution))
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
for (const aop of d.attrs) {
|
|
176
|
+
// `setAttr`/`deleteAttr` *replace* the attr op, so their attribution is already complete (pass it
|
|
177
|
+
// through unchanged). Only `modifyAttr` is an instruction that *merges* attribution — that is the one
|
|
178
|
+
// whose increment we expand against the overlay's accumulated attr attribution.
|
|
179
|
+
if (delta.$modifyAttrOp.check(aop)) {
|
|
180
|
+
const oa = overlay == null ? undefined : /** @type {any} */ (overlay).attrs[aop.key]
|
|
181
|
+
const childOverlay = (oa != null && delta.$modifyAttrOp.check(oa) && delta.$deltaAny.check(oa.value)) ? oa.value : null
|
|
182
|
+
full.modifyAttr(aop.key, delta.$deltaAny.check(aop.value) ? buildFull(aop.value, childOverlay) : aop.value, applyFull(oa == null ? undefined : oa.attribution, aop.attribution))
|
|
183
|
+
} else if (delta.$setAttrOp.check(aop)) {
|
|
184
|
+
full.setAttr(aop.key, aop.value, aop.attribution)
|
|
185
|
+
} else if (delta.$deleteAttrOp.check(aop)) {
|
|
186
|
+
full.deleteAttr(aop.key, aop.attribution)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
full.done(false)
|
|
190
|
+
return full
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Stateful transformer holding the accumulated attribution overlay.
|
|
195
|
+
*
|
|
196
|
+
* @extends {Transformer<any,any>}
|
|
197
|
+
*/
|
|
198
|
+
export class FullAttributionsTransformer extends Transformer {
|
|
199
|
+
/**
|
|
200
|
+
* @param {import('../../schema.js').Schema<delta.Delta<any>>} $in
|
|
201
|
+
* @param {import('../../schema.js').Schema<delta.Delta<any>>} $out
|
|
202
|
+
*/
|
|
203
|
+
constructor ($in, $out) {
|
|
204
|
+
super($in, $out)
|
|
205
|
+
/**
|
|
206
|
+
* Content-free overlay: the full accumulated attribution at every document position (instruction
|
|
207
|
+
* form). Mutated in place each change by `rebase` + `apply`.
|
|
208
|
+
*
|
|
209
|
+
* @type {delta.DeltaBuilderAny}
|
|
210
|
+
*/
|
|
211
|
+
this.overlay = delta.create()
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Forward: expand each attribution-bearing op to the complete accumulated attribution. Mutates and
|
|
216
|
+
* returns the input change.
|
|
217
|
+
*
|
|
218
|
+
* @param {delta.DeltaBuilderAny} d
|
|
219
|
+
* @return {import('./core.js').TransformResultAny}
|
|
220
|
+
*/
|
|
221
|
+
applyA (d) {
|
|
222
|
+
const full = buildFull(d, this.overlay)
|
|
223
|
+
// STATE (in place): align/grow the overlay to post-change coords, then accumulate the increments.
|
|
224
|
+
this.overlay.rebase(d, true)
|
|
225
|
+
this.overlay.apply(full, { move: false })
|
|
226
|
+
// OUTPUT (in place): enrich the input — `apply` splits content-free retains; no content is copied.
|
|
227
|
+
d.apply(full, { move: true })
|
|
228
|
+
return createTransformResult(null, d)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Reverse: passthrough; only realign the overlay so positions track view inserts/deletes. The view
|
|
233
|
+
* never attributes, so there is nothing to expand.
|
|
234
|
+
*
|
|
235
|
+
* @param {delta.DeltaBuilderAny} d
|
|
236
|
+
* @return {import('./core.js').TransformResultAny}
|
|
237
|
+
*/
|
|
238
|
+
applyB (d) {
|
|
239
|
+
this.overlay.rebase(d, true)
|
|
240
|
+
return createTransformResult(d, null)
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Template for {@link FullAttributionsTransformer}. The transform touches only the `attribution`
|
|
246
|
+
* dimension (metadata, not part of the delta schema), so the output schema equals the input schema.
|
|
247
|
+
*
|
|
248
|
+
* @template {delta.DeltaConf} [IN=any]
|
|
249
|
+
* @extends {Template<IN, IN>}
|
|
250
|
+
*/
|
|
251
|
+
export class FullAttributions extends Template {
|
|
252
|
+
/**
|
|
253
|
+
* @param {import('../../schema.js').Schema<delta.Delta<IN>>} $d
|
|
254
|
+
*/
|
|
255
|
+
constructor ($d) {
|
|
256
|
+
super($d, $d)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
get name () { return 'lib0:fullAttributions' }
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* @return {Transformer<IN, IN>}
|
|
263
|
+
*/
|
|
264
|
+
init () {
|
|
265
|
+
return new FullAttributionsTransformer(this.$in, this.$out)
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Normalize a delta's `attribution` dimension so every attribution change carries the **complete**
|
|
271
|
+
* accumulated attribution for that position (not just the increment) — see the
|
|
272
|
+
* {@link module:delta/transformer/full-attributions module doc}. Stateful: it keeps a content-free
|
|
273
|
+
* overlay of the accumulated attribution and updates it in place per change. `applyA` mutates and returns
|
|
274
|
+
* its input change. Returns a reusable {@link FullAttributions} template (`.init()` for a standalone
|
|
275
|
+
* transformer); typically piped before
|
|
276
|
+
* {@link import('./attribution-to-format.js').attributionToFormat}.
|
|
277
|
+
*
|
|
278
|
+
* @template {delta.DeltaConf} IN
|
|
279
|
+
* @param {import('../../schema.js').Schema<delta.Delta<IN>>} $d
|
|
280
|
+
* @return {FullAttributions<IN>}
|
|
281
|
+
*/
|
|
282
|
+
export const fullAttributions = ($d) => new FullAttributions($d)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { renameAttrs } from './rename-attrs.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The identity transformer factory: it maps every change verbatim in both directions, so both sides
|
|
5
|
+
* stay bit-for-bit equal (an attr-rename with no renames). The default template factory for
|
|
6
|
+
* {@link import('../rdt.js').bind}; `id($d).init()` builds the transformer.
|
|
7
|
+
*
|
|
8
|
+
* @template {import('../delta.js').DeltaConf} IN
|
|
9
|
+
* @param {import('../../schema.js').Schema<import('../delta.js').Delta<IN>>} $d
|
|
10
|
+
* @return {import('./rename-attrs.js').RenameAttrs<{}, IN>}
|
|
11
|
+
*/
|
|
12
|
+
/* @__NO_SIDE_EFFECTS__ */
|
|
13
|
+
export const id = ($d) => renameAttrs($d, {})
|