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,526 @@
|
|
|
1
|
+
import * as delta from '../delta.js'
|
|
2
|
+
import * as math from '../../math.js'
|
|
3
|
+
import { Transformer, Template, createTransformResult, $template } from './core.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Resolve a hole template's output conf `OUT` to its projected placement type: a `lib0:value` carrier
|
|
7
|
+
* lifts to its bare scalar; any other node is embedded as a `Delta<OUT>`.
|
|
8
|
+
*
|
|
9
|
+
* @template {delta.DeltaConf} OUT
|
|
10
|
+
* @typedef {OUT extends { name: 'lib0:value', attrs: { value: infer V } } ? V : delta.Delta<OUT>} ResolveHoleOut
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Resolve one spec child element to its projected output type, distributing over a children union: a
|
|
15
|
+
* hole ({@link Template}, whose output conf the spec carries) resolves via its `OUT`, a nested spec
|
|
16
|
+
* delta recurses, and anything else (text, a static scalar/node) is kept verbatim.
|
|
17
|
+
*
|
|
18
|
+
* @template C
|
|
19
|
+
* @typedef {C extends Template<any, infer OUT extends delta.DeltaConf> ? ResolveHoleOut<OUT>
|
|
20
|
+
* : C extends delta.Delta<infer NC extends delta.DeltaConf> ? delta.Delta<ProjectOutput<NC>>
|
|
21
|
+
* : C} ResolveChild
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Resolve a spec's attrs map: a hole attribute value resolves via its output conf; a static value is kept.
|
|
26
|
+
*
|
|
27
|
+
* @template A
|
|
28
|
+
* @typedef {{ [K in keyof A]: A[K] extends Template<any, infer OUT extends delta.DeltaConf> ? ResolveHoleOut<OUT> : A[K] }} ResolveAttrs
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The concrete output conf of a `project($d, spec)` whose spec has conf `SpecConf`: static structure
|
|
33
|
+
* is kept, value holes lift to their scalar, nested specs / node holes recurse. Holes are schema-bound
|
|
34
|
+
* {@link Template}s, so their output conf is already concrete in `SpecConf` (no input param needed).
|
|
35
|
+
*
|
|
36
|
+
* @template {delta.DeltaConf} SpecConf
|
|
37
|
+
* @typedef {import('./core.js').ResolveOut<delta.AsDeltaConf<
|
|
38
|
+
* Omit<SpecConf, 'attrs'|'children'>
|
|
39
|
+
* & (SpecConf extends { attrs: infer A } ? { attrs: ResolveAttrs<A> } : {})
|
|
40
|
+
* & (SpecConf extends { children: infer Ch } ? { children: ResolveChild<Ch> } : {})
|
|
41
|
+
* >>} ProjectOutput
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The scalar (or full replacement value) a `lib0:value` carrier *sets*: its `value` attribute op's
|
|
46
|
+
* value, but **only when that op is a `SetAttrOp`** (a full value set). Returns `undefined` for a
|
|
47
|
+
* `ModifyAttrOp` carrier (an incremental change to a delta-valued attribute — see {@link modifyOf}) or
|
|
48
|
+
* a `DeleteAttrOp`/absent op, so an incremental change is never mistaken for a replacement value.
|
|
49
|
+
*
|
|
50
|
+
* @param {delta.DeltaAny?} carrier
|
|
51
|
+
*/
|
|
52
|
+
const scalarOf = carrier => {
|
|
53
|
+
const op = carrier == null ? undefined : /** @type {any} */ (carrier.attrs).value
|
|
54
|
+
return delta.$setAttrOp.check(op) ? op.value : undefined
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The inner change delta a `lib0:value` carrier *modifies* with: its `value` attribute op's value when
|
|
59
|
+
* that op is a `ModifyAttrOp` (an incremental change to a delta-valued projected attribute), else
|
|
60
|
+
* `undefined`. Routed through the slot's modify channel instead of replacing the slot value.
|
|
61
|
+
*
|
|
62
|
+
* @param {delta.DeltaAny?} carrier
|
|
63
|
+
*/
|
|
64
|
+
const modifyOf = carrier => {
|
|
65
|
+
const op = carrier == null ? undefined : /** @type {any} */ (carrier.attrs).value
|
|
66
|
+
return delta.$modifyAttrOp.check(op) ? /** @type {any} */ (op).value : undefined
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Whether `el` is a `lib0:value` carrier node (the convention produced by the
|
|
71
|
+
* {@link import('./attr.js').attr} transformer). Such a carrier at a child position is lifted to a
|
|
72
|
+
* bare scalar embed; any other node is embedded verbatim (e.g. a `lib0:inline` fragment, which a
|
|
73
|
+
* downstream `inline(['lib0:inline'])` may splice).
|
|
74
|
+
*
|
|
75
|
+
* @param {any} el
|
|
76
|
+
*/
|
|
77
|
+
const isValueNode = el => delta.$deltaAny.check(el) && el.name === 'lib0:value'
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Whether the delta `d` contains a transformer {@link Template} anywhere in its attribute values or
|
|
81
|
+
* (recursively) its inserted child nodes. Used by {@link ProjectionTemplate#init} to decide whether
|
|
82
|
+
* a nested static subtree must be auto-wrapped into a nested {@link project} (it has a hole inside)
|
|
83
|
+
* or embedded verbatim (it is purely static). A plain delta has no `init` method, so `$template`
|
|
84
|
+
* correctly returns false for non-template subtrees.
|
|
85
|
+
*
|
|
86
|
+
* @param {delta.DeltaAny} d
|
|
87
|
+
* @return {boolean}
|
|
88
|
+
*/
|
|
89
|
+
const containsTemplate = d => {
|
|
90
|
+
for (const op of d.attrs) {
|
|
91
|
+
if (delta.$setAttrOp.check(op) && $template.check(op.value)) return true
|
|
92
|
+
}
|
|
93
|
+
for (const op of d.children) {
|
|
94
|
+
if (delta.$insertOp.check(op)) {
|
|
95
|
+
for (const el of op.insert) {
|
|
96
|
+
if ($template.check(el)) return true
|
|
97
|
+
if (delta.$deltaAny.check(el) && containsTemplate(el)) return true
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return false
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Place a carried cursor mark from a value/attr hole's output `rb` (a `lib0:value` carrier whose mark
|
|
106
|
+
* is keyed `'value'`) as a root mark on `out` at `slotKey` - the attribute key or child position the
|
|
107
|
+
* hole occupies. A scalar/attribute slot has no mark channel of its own, so this root mark is the
|
|
108
|
+
* cursor's only home; with several same-id value/attr slots the last placement wins (`Marks` is
|
|
109
|
+
* id-keyed per node, so an id cannot exist twice on one node - true duplication is impossible here).
|
|
110
|
+
* Node holes need none of this: their marks ride nested inside the embedded node. `deleteMarks` ride
|
|
111
|
+
* verbatim (id-keyed).
|
|
112
|
+
*
|
|
113
|
+
* @param {delta.DeltaBuilderAny} out
|
|
114
|
+
* @param {delta.DeltaAny?} rb
|
|
115
|
+
* @param {string|number} slotKey
|
|
116
|
+
*/
|
|
117
|
+
const placeCarrierMark = (out, rb, slotKey) => {
|
|
118
|
+
if (rb == null) return
|
|
119
|
+
if (rb.marks !== null) {
|
|
120
|
+
for (const m of rb.marks) if (m.key === 'value') delta.addRootMark(out, m.copy(slotKey))
|
|
121
|
+
}
|
|
122
|
+
if (rb.deleteMarks !== null) {
|
|
123
|
+
for (const id of rb.deleteMarks) delta.deleteRootMark(out, id)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* One child slot of a {@link ProjectionTransformer}'s fixed output layout: a run of static text, a
|
|
129
|
+
* single static node/embed, or a single hole (filled by a sub-transformer's output).
|
|
130
|
+
*
|
|
131
|
+
* A hole's `carrier` records how its sub-transformer's output is placed: `'value'` lifts a
|
|
132
|
+
* `lib0:value` carrier to a bare scalar embed (1 position, replaced on update); `'node'` embeds a
|
|
133
|
+
* structural node verbatim (a nested `project`'s render, or a non-value carrier) and forwards its
|
|
134
|
+
* incremental change as a `modify`. The kind is fixed once: known at `init` for auto-wrapped nested
|
|
135
|
+
* projects, otherwise sniffed from the first render and cached (`lastScalar` keeps the last lifted
|
|
136
|
+
* scalar so a view deletion of a value slot can be self-healed).
|
|
137
|
+
*
|
|
138
|
+
* @typedef {{ kind: 'text', str: string, format?: any, attribution?: any }
|
|
139
|
+
* | { kind: 'static', el: any, format?: any, attribution?: any }
|
|
140
|
+
* | { kind: 'hole', t: Transformer<any,any>, el: any, carrier?: 'value'|'node', lastScalar?: any, format?: any, attribution?: any }} ChildItem
|
|
141
|
+
*/
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Stateful transformer produced by {@link ProjectionTemplate}. Side A is the data (the
|
|
145
|
+
* *projectionDelta*); side B is the projected structure - the spec with each hole filled by its
|
|
146
|
+
* sub-transformer's output. `project` resolves the `lib0:value` carrier itself: at an *attribute*
|
|
147
|
+
* position (keyed) and at a *child* position (positional, 1->1) it lifts the carrier to its bare
|
|
148
|
+
* scalar. A child hole that is a nested `project` (or any non-`lib0:value` carrier) is embedded as a
|
|
149
|
+
* node. A `lib0:inline` fragment is left intact for a downstream `inline(['lib0:inline'])`.
|
|
150
|
+
*
|
|
151
|
+
* The output layout is *fixed*: every hole occupies exactly one attribute or one child node, and
|
|
152
|
+
* static content is constant. So `applyA` routes data changes into holes, and `applyB` routes view
|
|
153
|
+
* edits at *node*-hole positions back to holes while **self-healing** edits to static content and to
|
|
154
|
+
* value slots (a bare scalar has no `modify` channel, so a value-slot view edit is not round-tripped
|
|
155
|
+
* to data in v1 - use an attribute hole for an editable binding).
|
|
156
|
+
*
|
|
157
|
+
* @extends {Transformer<any,any>}
|
|
158
|
+
*/
|
|
159
|
+
export class ProjectionTransformer extends Transformer {
|
|
160
|
+
/**
|
|
161
|
+
* @param {import('../../schema.js').Schema<delta.Delta<any>>} $in
|
|
162
|
+
* @param {import('../../schema.js').Schema<delta.Delta<any>>} $out
|
|
163
|
+
* @param {string} name
|
|
164
|
+
* @param {Array<{ key: string|number, value: any, attribution?: any }>} staticAttrs
|
|
165
|
+
* @param {Array<{ key: string|number, t: Transformer<any,any> }>} attrHoles
|
|
166
|
+
* @param {Array<ChildItem>} items child slots in output order
|
|
167
|
+
*/
|
|
168
|
+
constructor ($in, $out, name, staticAttrs, attrHoles, items) {
|
|
169
|
+
super($in, $out)
|
|
170
|
+
this.name = name
|
|
171
|
+
this.staticAttrs = staticAttrs
|
|
172
|
+
this.attrHoles = attrHoles
|
|
173
|
+
this.items = items
|
|
174
|
+
// absolute output position of each child slot (static text spans its length, everything else 1)
|
|
175
|
+
let pos = 0
|
|
176
|
+
/**
|
|
177
|
+
* @type {Array<{ start: number, len: number, item: ChildItem }>}
|
|
178
|
+
*/
|
|
179
|
+
this.segs = items.map(item => {
|
|
180
|
+
const len = item.kind === 'text' ? item.str.length : 1
|
|
181
|
+
const seg = { start: pos, len, item }
|
|
182
|
+
pos += len
|
|
183
|
+
return seg
|
|
184
|
+
})
|
|
185
|
+
this.initialized = false
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* @param {delta.DeltaBuilderAny} d a data change (or the full data on the first call)
|
|
190
|
+
* @return {import('./core.js').TransformResultAny}
|
|
191
|
+
*/
|
|
192
|
+
applyA (d) {
|
|
193
|
+
return this.initialized ? this._update(d) : this._render(d)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Lift a child hole's sub-transformer output and place it on `out`. Returns the output count (1).
|
|
198
|
+
* For a value carrier the bare scalar is inserted (and cached for self-heal); otherwise the node is
|
|
199
|
+
* embedded verbatim. The hole's `carrier` kind is sniffed once here and cached.
|
|
200
|
+
*
|
|
201
|
+
* @param {delta.DeltaBuilderAny} out
|
|
202
|
+
* @param {ChildItem & { kind: 'hole' }} item
|
|
203
|
+
* @param {delta.DeltaAny?} rb the sub-transformer's B output
|
|
204
|
+
*/
|
|
205
|
+
_placeHole (out, item, rb) {
|
|
206
|
+
if (item.carrier == null) item.carrier = isValueNode(rb) ? 'value' : 'node'
|
|
207
|
+
if (item.carrier === 'value') {
|
|
208
|
+
const sc = scalarOf(rb)
|
|
209
|
+
const v = sc === undefined ? null : sc
|
|
210
|
+
item.lastScalar = v
|
|
211
|
+
out.insert([v], item.format, item.attribution)
|
|
212
|
+
} else {
|
|
213
|
+
out.insert([rb], item.format, item.attribution)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Initial render: emit the full structure with every hole filled.
|
|
219
|
+
*
|
|
220
|
+
* @param {delta.DeltaBuilderAny} d
|
|
221
|
+
*/
|
|
222
|
+
_render (d) {
|
|
223
|
+
this.initialized = true
|
|
224
|
+
const out = delta.create(/** @type {any} */ (this.name))
|
|
225
|
+
const res = createTransformResult(null, out)
|
|
226
|
+
for (const a of this.staticAttrs) out.setAttr(/** @type {any} */ (a.key), a.value, a.attribution)
|
|
227
|
+
for (const h of this.attrHoles) {
|
|
228
|
+
const r = h.t.applyA(d)
|
|
229
|
+
const sc = scalarOf(r.b)
|
|
230
|
+
if (sc !== undefined) out.setAttr(/** @type {any} */ (h.key), sc)
|
|
231
|
+
placeCarrierMark(out, r.b, h.key) // a cursor on this data attr anchors at the output attr key
|
|
232
|
+
res.applyA(r.a)
|
|
233
|
+
}
|
|
234
|
+
let childPos = 0 // output child position, to anchor a value-hole's carried cursor mark
|
|
235
|
+
for (const item of this.items) {
|
|
236
|
+
if (item.kind === 'text') {
|
|
237
|
+
out.insert(item.str, item.format, item.attribution)
|
|
238
|
+
childPos += item.str.length
|
|
239
|
+
} else if (item.kind === 'static') {
|
|
240
|
+
out.insert([item.el], item.format, item.attribution)
|
|
241
|
+
childPos += 1
|
|
242
|
+
} else {
|
|
243
|
+
const r = item.t.applyA(d)
|
|
244
|
+
this._placeHole(out, item, r.b)
|
|
245
|
+
// value holes lift a bare scalar (no mark channel) - anchor the cursor at the slot position;
|
|
246
|
+
// node holes carry their marks nested inside the embedded node (no action needed)
|
|
247
|
+
if (item.carrier === 'value') placeCarrierMark(out, r.b, childPos)
|
|
248
|
+
childPos += 1
|
|
249
|
+
res.applyA(r.a)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return res
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Incremental data change: emit only the hole updates. A value hole is *replaced*
|
|
257
|
+
* (`delete(1).insert([scalar])`, since a scalar embed has no inner structure to modify); a node
|
|
258
|
+
* hole forwards its sub-transformer's incremental change as a `modify`.
|
|
259
|
+
*
|
|
260
|
+
* @param {delta.DeltaBuilderAny} d
|
|
261
|
+
*/
|
|
262
|
+
_update (d) {
|
|
263
|
+
const out = delta.create()
|
|
264
|
+
const res = createTransformResult(null, out)
|
|
265
|
+
for (const h of this.attrHoles) {
|
|
266
|
+
const r = h.t.applyA(d)
|
|
267
|
+
// emit the attr update by the carrier's op KIND (a mark-only change carries no value op at all):
|
|
268
|
+
// SetAttr -> full set/replace; ModifyAttr -> incremental change to a delta-valued attr, forwarded
|
|
269
|
+
// through the modify channel (NOT written as the value); DeleteAttr -> remove the attr.
|
|
270
|
+
const carrierOp = r.b == null ? undefined : /** @type {any} */ (r.b.attrs).value
|
|
271
|
+
if (delta.$setAttrOp.check(carrierOp)) {
|
|
272
|
+
out.setAttr(/** @type {any} */ (h.key), scalarOf(r.b))
|
|
273
|
+
} else if (delta.$modifyAttrOp.check(carrierOp)) {
|
|
274
|
+
out.modifyAttr(/** @type {any} */ (h.key), modifyOf(r.b))
|
|
275
|
+
} else if (delta.$deleteAttrOp.check(carrierOp)) {
|
|
276
|
+
out.deleteAttr(/** @type {any} */ (h.key))
|
|
277
|
+
}
|
|
278
|
+
placeCarrierMark(out, r.b, h.key)
|
|
279
|
+
res.applyA(r.a)
|
|
280
|
+
}
|
|
281
|
+
let emitted = 0 // output position already covered by `out`'s child ops
|
|
282
|
+
for (const seg of this.segs) {
|
|
283
|
+
if (seg.item.kind !== 'hole') continue
|
|
284
|
+
const item = seg.item
|
|
285
|
+
const r = item.t.applyA(d)
|
|
286
|
+
if (item.carrier === 'value') {
|
|
287
|
+
// route by the carrier's op KIND (a mark-only change carries no value op, so the slot is left
|
|
288
|
+
// intact and only the cursor is anchored): SetAttr -> replace the scalar embed; ModifyAttr ->
|
|
289
|
+
// forward an incremental change to a delta-valued embed through the modify channel; DeleteAttr ->
|
|
290
|
+
// reset the slot to its `null` placeholder (matching a fresh render of the now-absent attr).
|
|
291
|
+
const carrierOp = r.b == null ? undefined : /** @type {any} */ (r.b.attrs).value
|
|
292
|
+
if (delta.$setAttrOp.check(carrierOp)) {
|
|
293
|
+
if (seg.start > emitted) out.retain(seg.start - emitted)
|
|
294
|
+
const sc = scalarOf(r.b)
|
|
295
|
+
const v = sc === undefined ? null : sc
|
|
296
|
+
item.lastScalar = v
|
|
297
|
+
out.delete(1)
|
|
298
|
+
out.insert([v])
|
|
299
|
+
emitted = seg.start + 1
|
|
300
|
+
} else if (delta.$modifyAttrOp.check(carrierOp)) {
|
|
301
|
+
if (seg.start > emitted) out.retain(seg.start - emitted)
|
|
302
|
+
out.modify(modifyOf(r.b)) // incremental change to the delta-valued embed
|
|
303
|
+
emitted = seg.start + 1
|
|
304
|
+
} else if (delta.$deleteAttrOp.check(carrierOp)) {
|
|
305
|
+
// the bound data attr was removed: a child value slot has no data channel, so deletion only
|
|
306
|
+
// updates the view - reset the scalar embed to the `null` placeholder a fresh render shows.
|
|
307
|
+
if (seg.start > emitted) out.retain(seg.start - emitted)
|
|
308
|
+
item.lastScalar = null
|
|
309
|
+
out.delete(1)
|
|
310
|
+
out.insert([null])
|
|
311
|
+
emitted = seg.start + 1
|
|
312
|
+
}
|
|
313
|
+
placeCarrierMark(out, r.b, seg.start)
|
|
314
|
+
} else if (r.b != null && !r.b.isEmpty()) {
|
|
315
|
+
// node hole: forward the incremental change as a modify; its marks ride nested
|
|
316
|
+
if (seg.start > emitted) out.retain(seg.start - emitted)
|
|
317
|
+
out.modify(r.b)
|
|
318
|
+
emitted = seg.start + 1
|
|
319
|
+
}
|
|
320
|
+
res.applyA(r.a)
|
|
321
|
+
}
|
|
322
|
+
return res
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Reconstruct the content occupying output positions `[start, start+len)` as insert ops on `out`
|
|
327
|
+
* (used to self-heal a view delete). Static text/nodes are restored from the spec, and a value
|
|
328
|
+
* slot is restored from its last-rendered scalar. Node-hole positions are skipped (their dynamic
|
|
329
|
+
* structure cannot be restored without the current data).
|
|
330
|
+
*
|
|
331
|
+
* @param {delta.DeltaBuilderAny} out
|
|
332
|
+
* @param {number} start
|
|
333
|
+
* @param {number} len
|
|
334
|
+
*/
|
|
335
|
+
_restore (out, start, len) {
|
|
336
|
+
const end = start + len
|
|
337
|
+
for (const seg of this.segs) {
|
|
338
|
+
if (seg.start >= end || seg.start + seg.len <= start) continue
|
|
339
|
+
const from = math.max(start, seg.start)
|
|
340
|
+
const to = math.min(end, seg.start + seg.len)
|
|
341
|
+
const item = seg.item
|
|
342
|
+
if (item.kind === 'text') {
|
|
343
|
+
out.insert(item.str.slice(from - seg.start, to - seg.start), item.format, item.attribution)
|
|
344
|
+
} else if (item.kind === 'static') {
|
|
345
|
+
out.insert([item.el], item.format, item.attribution)
|
|
346
|
+
} else if (item.kind === 'hole' && item.carrier === 'value') {
|
|
347
|
+
out.insert([item.lastScalar], item.format, item.attribution)
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* @param {delta.DeltaBuilderAny} d a view change in the output space
|
|
354
|
+
* @return {import('./core.js').TransformResultAny}
|
|
355
|
+
*/
|
|
356
|
+
applyB (d) {
|
|
357
|
+
const res = createTransformResult(null, null)
|
|
358
|
+
const heal = delta.create()
|
|
359
|
+
let healed = false
|
|
360
|
+
// attributes: route hole-attr edits to their holes, self-heal edits to static attrs
|
|
361
|
+
for (const op of d.attrs) {
|
|
362
|
+
const hole = this.attrHoles.find(h => h.key === op.key)
|
|
363
|
+
if (hole != null) {
|
|
364
|
+
if (delta.$setAttrOp.check(op)) {
|
|
365
|
+
res.applyA(hole.t.applyB(delta.create('lib0:value').setAttr('value', op.value)).a)
|
|
366
|
+
}
|
|
367
|
+
} else {
|
|
368
|
+
const st = this.staticAttrs.find(a => a.key === op.key)
|
|
369
|
+
if (st != null && !(delta.$setAttrOp.check(op) && op.value === st.value)) {
|
|
370
|
+
heal.setAttr(/** @type {any} */ (st.key), st.value)
|
|
371
|
+
healed = true
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
// children: walk with a pre-change position cursor, routing node-hole modifies and self-healing
|
|
376
|
+
// everything else (drift, edited static content, edited value slots)
|
|
377
|
+
let pos = 0
|
|
378
|
+
const segAt = (/** @type {number} */ p) => this.segs.find(s => s.start <= p && p < s.start + s.len)
|
|
379
|
+
for (const op of d.children) {
|
|
380
|
+
if (delta.$retainOp.check(op)) {
|
|
381
|
+
heal.retain(op.retain)
|
|
382
|
+
pos += op.retain
|
|
383
|
+
} else if (delta.$textOp.check(op)) {
|
|
384
|
+
heal.delete(op.insert.length) // inserted text is drift
|
|
385
|
+
healed = true
|
|
386
|
+
} else if (delta.$insertOp.check(op)) {
|
|
387
|
+
heal.delete(op.insert.length) // inserted nodes/embeds are drift
|
|
388
|
+
healed = true
|
|
389
|
+
} else if (delta.$deleteOp.check(op)) {
|
|
390
|
+
this._restore(heal, pos, op.delete) // re-insert the deleted static / value content
|
|
391
|
+
healed = true
|
|
392
|
+
pos += op.delete
|
|
393
|
+
} else if (delta.$modifyOp.check(op)) {
|
|
394
|
+
const seg = segAt(pos)
|
|
395
|
+
if (seg != null && seg.item.kind === 'hole' && seg.item.carrier === 'node') {
|
|
396
|
+
// op.value is an immutable Delta view; transformers consume a DeltaBuilder
|
|
397
|
+
res.applyA(seg.item.t.applyB(delta.clone(op.value)).a)
|
|
398
|
+
heal.retain(1)
|
|
399
|
+
} else {
|
|
400
|
+
// a static node or a value slot was modified: restore it
|
|
401
|
+
heal.delete(1)
|
|
402
|
+
this._restore(heal, pos, 1)
|
|
403
|
+
healed = true
|
|
404
|
+
}
|
|
405
|
+
pos += 1
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
// route view-side cursor marks on ATTRIBUTE holes back to their data attribute (the editable,
|
|
409
|
+
// round-tripping binding): a root mark keyed to an attr hole's output key rides on a `lib0:value`
|
|
410
|
+
// carrier so the attr sub-transformer maps it to the data attribute. Best-effort for the rest -
|
|
411
|
+
// node-hole nested marks already ride on the routed `modify` above; value child slots are
|
|
412
|
+
// display-only (no data channel); marks at static / non-hole positions and view-side mark
|
|
413
|
+
// *deletions* are not routed back (marks are ephemeral cursor state - see the readme).
|
|
414
|
+
if (d.marks !== null) {
|
|
415
|
+
for (const m of d.marks) {
|
|
416
|
+
const hole = this.attrHoles.find(h => h.key === m.key)
|
|
417
|
+
if (hole != null) {
|
|
418
|
+
const carrier = /** @type {delta.DeltaBuilderAny} */ (delta.create('lib0:value'))
|
|
419
|
+
delta.addRootMark(carrier, m.copy('value'))
|
|
420
|
+
res.applyA(hole.t.applyB(carrier).a)
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (healed) {
|
|
425
|
+
heal.done(false)
|
|
426
|
+
res.applyB(heal)
|
|
427
|
+
}
|
|
428
|
+
return res
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Template that projects the *projectionDelta* onto a fixed structure described by `spec` - an
|
|
434
|
+
* ordinary {@link import('../delta.js').create delta} whose attribute values and inserted children
|
|
435
|
+
* may be transformer {@link Template templates} ("holes"). Each hole receives the whole
|
|
436
|
+
* projectionDelta; its output is placed at that position. The spec is a *write-only template*: it is
|
|
437
|
+
* walked once at {@link ProjectionTemplate#init} and never fingerprinted/diffed.
|
|
438
|
+
*
|
|
439
|
+
* Holes resolve themselves: a `lib0:value` carrier (produced by {@link import('./attr.js').attr}) is
|
|
440
|
+
* lifted to a bare scalar; a nested subtree that contains a hole is **auto-wrapped** into a nested
|
|
441
|
+
* `project` at `init`, so deep structure composes through nested templates with the recursion built
|
|
442
|
+
* once (bounded by spec depth). Plain static subtrees are embedded verbatim.
|
|
443
|
+
*
|
|
444
|
+
* @template {delta.DeltaConf} [IN=any]
|
|
445
|
+
* @template {delta.DeltaConf} [SpecConf=any]
|
|
446
|
+
* @extends {Template<IN, ProjectOutput<SpecConf>>}
|
|
447
|
+
*/
|
|
448
|
+
export class ProjectionTemplate extends Template {
|
|
449
|
+
/**
|
|
450
|
+
* @param {import('../../schema.js').Schema<delta.Delta<IN>>} $d
|
|
451
|
+
* @param {delta.Delta<SpecConf>} spec
|
|
452
|
+
*/
|
|
453
|
+
constructor ($d, spec) {
|
|
454
|
+
super($d, /** @type {any} */ (delta.$deltaAny))
|
|
455
|
+
this.spec = spec
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
get name () { return 'lib0:project' }
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* @return {Transformer<IN, ProjectOutput<SpecConf>>}
|
|
462
|
+
*/
|
|
463
|
+
init () {
|
|
464
|
+
const spec = this.spec
|
|
465
|
+
const $d = this.$in
|
|
466
|
+
/**
|
|
467
|
+
* @type {Array<{ key: string|number, value: any, attribution?: any }>}
|
|
468
|
+
*/
|
|
469
|
+
const staticAttrs = []
|
|
470
|
+
/**
|
|
471
|
+
* @type {Array<{ key: string|number, t: Transformer<any,any> }>}
|
|
472
|
+
*/
|
|
473
|
+
const attrHoles = []
|
|
474
|
+
for (const op of spec.attrs) {
|
|
475
|
+
if (delta.$setAttrOp.check(op)) {
|
|
476
|
+
if ($template.check(op.value)) {
|
|
477
|
+
attrHoles.push({ key: op.key, t: /** @type {Template} */ (op.value).init() })
|
|
478
|
+
} else {
|
|
479
|
+
staticAttrs.push({ key: op.key, value: op.value, attribution: op.attribution })
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* @type {Array<ChildItem>}
|
|
485
|
+
*/
|
|
486
|
+
const items = []
|
|
487
|
+
for (const op of spec.children) {
|
|
488
|
+
if (delta.$textOp.check(op)) {
|
|
489
|
+
items.push({ kind: 'text', str: op.insert, format: op.format, attribution: op.attribution })
|
|
490
|
+
} else if (delta.$insertOp.check(op)) {
|
|
491
|
+
for (const el of op.insert) {
|
|
492
|
+
if ($template.check(el)) {
|
|
493
|
+
items.push({ kind: 'hole', t: /** @type {Template} */ (el).init(), el, format: op.format, attribution: op.attribution })
|
|
494
|
+
} else if (delta.$deltaAny.check(el) && containsTemplate(el)) {
|
|
495
|
+
// a static subtree with a hole inside it - auto-wrap as a nested project (a node hole).
|
|
496
|
+
// `el` is an immutable inserted `Delta`, which is exactly what `project` accepts (it only
|
|
497
|
+
// reads attrs/children/name through `ProjectionTemplate.init`).
|
|
498
|
+
const nested = project($d, el).init()
|
|
499
|
+
items.push({ kind: 'hole', t: nested, el, carrier: 'node', format: op.format, attribution: op.attribution })
|
|
500
|
+
} else {
|
|
501
|
+
items.push({ kind: 'static', el, format: op.format, attribution: op.attribution })
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return /** @type {any} */ (new ProjectionTransformer(this.$in, this.$out, /** @type {any} */ (spec.name), staticAttrs, attrHoles, items))
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Project the *projectionDelta* onto a fixed structure described by `spec` — an ordinary
|
|
512
|
+
* {@link import('../delta.js').create delta} whose attribute values and inserted children may be
|
|
513
|
+
* transformer {@link Template templates} ("holes", built with the same `$d`). A child hole whose
|
|
514
|
+
* output is a `lib0:value` carrier is lifted to its scalar; a nested subtree containing a hole is
|
|
515
|
+
* auto-wrapped into a nested `project`. Returns a reusable {@link ProjectionTemplate} whose output
|
|
516
|
+
* type is computed by {@link ProjectOutput} (holes are
|
|
517
|
+
* {@link import('../../trait/fingerprint.js').Fingerprintable}, so the spec carries their types);
|
|
518
|
+
* `.init()` builds the transformer.
|
|
519
|
+
*
|
|
520
|
+
* @template {delta.DeltaConf} IN
|
|
521
|
+
* @template {delta.DeltaConf} SpecConf
|
|
522
|
+
* @param {import('../../schema.js').Schema<delta.Delta<IN>>} $d
|
|
523
|
+
* @param {delta.Delta<SpecConf>} spec
|
|
524
|
+
* @return {ProjectionTemplate<IN, SpecConf>}
|
|
525
|
+
*/
|
|
526
|
+
export const project = ($d, spec) => new ProjectionTemplate($d, spec)
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import * as delta from '../delta.js'
|
|
2
|
+
import { Transformer, Template, createTransformResult, withAttrs, attrsShapeOf } from './core.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @template {{[K:string|number]:string|number}} Renames
|
|
6
|
+
* @template {delta.DeltaConf} IN
|
|
7
|
+
* @typedef {delta.DeltaConfOverwrite<IN,{ attrs: import('../../ts.js').PropsRename<delta.DeltaConfGetAttrs<IN>,Renames> }>} ApplyRenameAttrs
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {delta.DeltaAny?} d
|
|
12
|
+
* @param {{[K:string|number]:string|number}} renames
|
|
13
|
+
* @param {{[K:string|number]:string|number}} revRenames
|
|
14
|
+
* @return {import('./core.js').TransformResultAny}
|
|
15
|
+
*/
|
|
16
|
+
const renameDeltaAttrs = (d, renames, revRenames) => {
|
|
17
|
+
if (d == null) return createTransformResult(null, null)
|
|
18
|
+
const forwardTransform = delta.clone(d)
|
|
19
|
+
for (const attr of forwardTransform.attrs) {
|
|
20
|
+
const key = attr.key
|
|
21
|
+
const r = renames[key]
|
|
22
|
+
const rv = revRenames[key]
|
|
23
|
+
if (r != null) {
|
|
24
|
+
// @ts-ignore
|
|
25
|
+
forwardTransform.attrs[r] = attr
|
|
26
|
+
// delete original
|
|
27
|
+
delete forwardTransform.attrs[key]
|
|
28
|
+
// @ts-ignore
|
|
29
|
+
attr.key = r
|
|
30
|
+
} else if (rv != null) {
|
|
31
|
+
// used in a rename, delete original
|
|
32
|
+
delete forwardTransform.attrs[key]
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// a mark on a renamed attribute follows the rename; a mark on a dropped (rename-target) attribute is
|
|
36
|
+
// dropped. Content-offset (number-key) marks are untouched. Marks rode in on `delta.clone` above.
|
|
37
|
+
delta.remapRootMarks(forwardTransform, key => {
|
|
38
|
+
const r = renames[key]
|
|
39
|
+
if (r != null) return r
|
|
40
|
+
if (revRenames[key] != null) return null
|
|
41
|
+
return key
|
|
42
|
+
})
|
|
43
|
+
return createTransformResult(null, forwardTransform)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Renames node attributes (`a` -> `b`) in both directions. Holds no per-application state, so
|
|
48
|
+
* {@link RenameAttrs} builds a single instance and shares it across every `init`.
|
|
49
|
+
*
|
|
50
|
+
* @extends Transformer<any,any>
|
|
51
|
+
*/
|
|
52
|
+
export class RenameAttrsTransformer extends Transformer {
|
|
53
|
+
/**
|
|
54
|
+
* @param {import('../../schema.js').Schema<delta.Delta<any>>} $in
|
|
55
|
+
* @param {import('../../schema.js').Schema<delta.Delta<any>>} $out
|
|
56
|
+
* @param {{[K:string|number]:string|number}} renames
|
|
57
|
+
*/
|
|
58
|
+
constructor ($in, $out, renames) {
|
|
59
|
+
super($in, $out)
|
|
60
|
+
this.arenames = renames
|
|
61
|
+
/**
|
|
62
|
+
* @type {{[K:string|number]:string|number}}
|
|
63
|
+
*/
|
|
64
|
+
this.brenames = {}
|
|
65
|
+
for (const k in renames) {
|
|
66
|
+
this.brenames[renames[k]] = k
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @param {delta.DeltaAny} deltaA
|
|
72
|
+
* @return {import('./core.js').TransformResultAny}
|
|
73
|
+
*/
|
|
74
|
+
applyA (deltaA) {
|
|
75
|
+
return renameDeltaAttrs(deltaA, this.arenames, this.brenames)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @param {delta.DeltaAny} deltaB
|
|
80
|
+
* @return {import('./core.js').TransformResultAny}
|
|
81
|
+
*/
|
|
82
|
+
applyB (deltaB) {
|
|
83
|
+
return renameDeltaAttrs(deltaB, this.brenames, this.arenames).reverse()
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Compute the output schema of an attribute rename: rename matching attr keys, drop keys that are
|
|
89
|
+
* rename targets, keep the rest (mirrors {@link renameDeltaAttrs} / {@link ApplyRenameAttrs}). Falls
|
|
90
|
+
* back to `$deltaAny` when the input schema is loose.
|
|
91
|
+
*
|
|
92
|
+
* @param {import('../../schema.js').Schema<delta.DeltaAny>} $d
|
|
93
|
+
* @param {{[K:string|number]:string|number}} renames
|
|
94
|
+
* @return {import('../../schema.js').Schema<delta.DeltaAny>}
|
|
95
|
+
*/
|
|
96
|
+
const renamedAttrsOut = ($d, renames) => {
|
|
97
|
+
const m = attrsShapeOf($d)
|
|
98
|
+
if (m == null) return delta.$deltaAny
|
|
99
|
+
/** @type {{[K:string|number]:string|number}} */
|
|
100
|
+
const rev = {}
|
|
101
|
+
for (const k in renames) rev[renames[k]] = k
|
|
102
|
+
/** @type {{[K:string|number]:import('../../schema.js').Schema<any>}} */
|
|
103
|
+
const m2 = {}
|
|
104
|
+
for (const k in m) {
|
|
105
|
+
if (renames[k] != null) m2[renames[k]] = m[k]
|
|
106
|
+
else if (rev[k] == null) m2[k] = m[k]
|
|
107
|
+
}
|
|
108
|
+
return withAttrs($d, m2)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Template that renames node attributes (`a` -> `b`) in both directions.
|
|
113
|
+
*
|
|
114
|
+
* @template {{[K:string|number]:string|number}} Renames
|
|
115
|
+
* @template {delta.DeltaConf} [IN=any]
|
|
116
|
+
* @extends {Template<IN, ApplyRenameAttrs<Renames, IN>>}
|
|
117
|
+
*/
|
|
118
|
+
export class RenameAttrs extends Template {
|
|
119
|
+
/**
|
|
120
|
+
* @param {import('../../schema.js').Schema<delta.Delta<IN>>} $d
|
|
121
|
+
* @param {Renames} renames
|
|
122
|
+
*/
|
|
123
|
+
constructor ($d, renames) {
|
|
124
|
+
super($d, /** @type {any} */ (renamedAttrsOut($d, renames)))
|
|
125
|
+
/**
|
|
126
|
+
* @type {Renames}
|
|
127
|
+
*/
|
|
128
|
+
this.renames = renames
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
get name () { return 'lib0:renameAttrs:' + JSON.stringify(this.renames) }
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* @return {Transformer<IN,ApplyRenameAttrs<Renames,IN>>}
|
|
135
|
+
*/
|
|
136
|
+
init () {
|
|
137
|
+
return /** @type {any} */ (new RenameAttrsTransformer(this.$in, this.$out, this.renames))
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Rename node attributes (`a` -> `b`) in both directions. Returns a reusable {@link RenameAttrs}
|
|
143
|
+
* template (a `project` hole, or `.init()` for a standalone transformer). The `const Renames` param
|
|
144
|
+
* keeps the `{a:'b'}` literal so the output type is precise without a manual const-assertion.
|
|
145
|
+
*
|
|
146
|
+
* @template {delta.DeltaConf} IN
|
|
147
|
+
* @template {{[K:string|number]:string|number}} const Renames
|
|
148
|
+
* @param {import('../../schema.js').Schema<delta.Delta<IN>>} $d
|
|
149
|
+
* @param {Renames} renames
|
|
150
|
+
* @return {RenameAttrs<Renames, IN>}
|
|
151
|
+
*/
|
|
152
|
+
export const renameAttrs = ($d, renames) => new RenameAttrs($d, renames)
|