lib0 1.0.0-rc.2 → 1.0.0-rc.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/README.md +0 -1315
  2. package/{types → dist}/buffer.d.ts +2 -10
  3. package/{types → dist}/delta/delta.d.ts +582 -144
  4. package/dist/delta/position.d.ts +65 -0
  5. package/dist/delta/rdt/delta.d.ts +57 -0
  6. package/dist/delta/rdt/dom.d.ts +102 -0
  7. package/dist/delta/rdt.d.ts +180 -0
  8. package/dist/delta/transformer/attr.d.ts +64 -0
  9. package/dist/delta/transformer/attribution-to-format.d.ts +52 -0
  10. package/dist/delta/transformer/children.d.ts +69 -0
  11. package/dist/delta/transformer/conform.d.ts +83 -0
  12. package/dist/delta/transformer/core.d.ts +189 -0
  13. package/dist/delta/transformer/full-attributions.d.ts +37 -0
  14. package/dist/delta/transformer/id.d.ts +2 -0
  15. package/dist/delta/transformer/inline.d.ts +89 -0
  16. package/dist/delta/transformer/pipe.d.ts +182 -0
  17. package/dist/delta/transformer/project.d.ts +223 -0
  18. package/dist/delta/transformer/rename-attrs.d.ts +75 -0
  19. package/dist/delta/transformer/rename.d.ts +41 -0
  20. package/dist/delta/transformer/value.d.ts +92 -0
  21. package/dist/delta/transformer.d.ts +14 -0
  22. package/dist/diff/patience.d.ts +25 -0
  23. package/{types → dist}/environment.d.ts +10 -0
  24. package/dist/hash/fnv1a.d.ts +11 -0
  25. package/{types → dist}/schema.d.ts +49 -38
  26. package/{types → dist}/ts.d.ts +4 -0
  27. package/dist/webcrypto.d.ts +3 -0
  28. package/package.json +148 -82
  29. package/src/bin/0serve.js +11 -3
  30. package/src/broadcastchannel.js +6 -1
  31. package/src/buffer.js +35 -25
  32. package/src/decoding.js +1 -1
  33. package/src/delta/delta.js +2531 -564
  34. package/src/delta/position.js +165 -0
  35. package/src/delta/rdt/delta.js +105 -0
  36. package/src/delta/rdt/dom.js +307 -0
  37. package/src/delta/rdt.js +255 -0
  38. package/src/delta/transformer/attr.js +126 -0
  39. package/src/delta/transformer/attribution-to-format.js +335 -0
  40. package/src/delta/transformer/children.js +219 -0
  41. package/src/delta/transformer/conform.js +401 -0
  42. package/src/delta/transformer/core.js +323 -0
  43. package/src/delta/transformer/full-attributions.js +282 -0
  44. package/src/delta/transformer/id.js +13 -0
  45. package/src/delta/transformer/inline.js +818 -0
  46. package/src/delta/transformer/pipe.js +272 -0
  47. package/src/delta/transformer/project.js +526 -0
  48. package/src/delta/transformer/rename-attrs.js +152 -0
  49. package/src/delta/transformer/rename.js +84 -0
  50. package/src/delta/transformer/value.js +214 -0
  51. package/src/delta/transformer.js +38 -0
  52. package/src/diff/patience.js +22 -11
  53. package/src/dom.js +2 -2
  54. package/src/encoding.js +1 -1
  55. package/src/environment.js +15 -0
  56. package/src/hash/fnv1a.js +82 -0
  57. package/src/number.js +1 -3
  58. package/src/performance.node.js +3 -3
  59. package/src/schema.js +231 -177
  60. package/src/string.js +3 -5
  61. package/src/testing.js +1 -1
  62. package/src/trait/equality.js +1 -1
  63. package/src/trait/fingerprint.js +1 -1
  64. package/src/ts.js +11 -0
  65. package/src/bin/gendocs.js +0 -117
  66. package/src/component.js +0 -414
  67. package/src/delta/binding.js +0 -372
  68. package/src/tree.js +0 -546
  69. package/types/bin/gendocs.d.ts +0 -3
  70. package/types/component.d.ts +0 -86
  71. package/types/delta/binding.d.ts +0 -106
  72. package/types/diff/patience.d.ts +0 -16
  73. package/types/tree.d.ts +0 -96
  74. package/types/webcrypto.d.ts +0 -3
  75. /package/{types → dist}/array.d.ts +0 -0
  76. /package/{types → dist}/bin/0ecdsa-generate-keypair.d.ts +0 -0
  77. /package/{types → dist}/bin/0serve.d.ts +0 -0
  78. /package/{types → dist}/bin/gentesthtml.d.ts +0 -0
  79. /package/{types → dist}/binary.d.ts +0 -0
  80. /package/{types → dist}/broadcastchannel.d.ts +0 -0
  81. /package/{types → dist}/cache.d.ts +0 -0
  82. /package/{types → dist}/conditions.d.ts +0 -0
  83. /package/{types → dist}/crypto/aes-gcm.d.ts +0 -0
  84. /package/{types → dist}/crypto/common.d.ts +0 -0
  85. /package/{types → dist}/crypto/ecdsa.d.ts +0 -0
  86. /package/{types → dist}/crypto/jwt.d.ts +0 -0
  87. /package/{types → dist}/crypto/rsa-oaep.d.ts +0 -0
  88. /package/{types → dist}/decoding.d.ts +0 -0
  89. /package/{types → dist}/diff.d.ts +0 -0
  90. /package/{types → dist}/dom.d.ts +0 -0
  91. /package/{types → dist}/encoding.d.ts +0 -0
  92. /package/{types → dist}/error.d.ts +0 -0
  93. /package/{types → dist}/eventloop.d.ts +0 -0
  94. /package/{types → dist}/function.d.ts +0 -0
  95. /package/{types → dist}/hash/rabin-gf2-polynomial.d.ts +0 -0
  96. /package/{types → dist}/hash/rabin-uncached.d.ts +0 -0
  97. /package/{types → dist}/hash/rabin.d.ts +0 -0
  98. /package/{types → dist}/hash/sha256.d.ts +0 -0
  99. /package/{types → dist}/hash/sha256.node.d.ts +0 -0
  100. /package/{types → dist}/indexeddb.d.ts +0 -0
  101. /package/{types → dist}/indexeddbV2.d.ts +0 -0
  102. /package/{types → dist}/iterator.d.ts +0 -0
  103. /package/{types → dist}/json.d.ts +0 -0
  104. /package/{types → dist}/list.d.ts +0 -0
  105. /package/{types → dist}/logging.common.d.ts +0 -0
  106. /package/{types → dist}/logging.d.ts +0 -0
  107. /package/{types → dist}/logging.node.d.ts +0 -0
  108. /package/{types → dist}/map.d.ts +0 -0
  109. /package/{types → dist}/math.d.ts +0 -0
  110. /package/{types → dist}/metric.d.ts +0 -0
  111. /package/{types → dist}/mutex.d.ts +0 -0
  112. /package/{types → dist}/number.d.ts +0 -0
  113. /package/{types → dist}/object.d.ts +0 -0
  114. /package/{types → dist}/observable.d.ts +0 -0
  115. /package/{types → dist}/pair.d.ts +0 -0
  116. /package/{types → dist}/performance.d.ts +0 -0
  117. /package/{types → dist}/performance.node.d.ts +0 -0
  118. /package/{types → dist}/pledge.d.ts +0 -0
  119. /package/{types → dist}/prng/Mt19937.d.ts +0 -0
  120. /package/{types → dist}/prng/Xoroshiro128plus.d.ts +0 -0
  121. /package/{types → dist}/prng/Xorshift32.d.ts +0 -0
  122. /package/{types → dist}/prng.d.ts +0 -0
  123. /package/{types → dist}/promise.d.ts +0 -0
  124. /package/{types → dist}/queue.d.ts +0 -0
  125. /package/{types → dist}/random.d.ts +0 -0
  126. /package/{types → dist}/set.d.ts +0 -0
  127. /package/{types → dist}/sort.d.ts +0 -0
  128. /package/{types → dist}/statistics.d.ts +0 -0
  129. /package/{types → dist}/storage.d.ts +0 -0
  130. /package/{types → dist}/string.d.ts +0 -0
  131. /package/{types → dist}/symbol.d.ts +0 -0
  132. /package/{types → dist}/testing.d.ts +0 -0
  133. /package/{types → dist}/time.d.ts +0 -0
  134. /package/{types → dist}/trait/equality.d.ts +0 -0
  135. /package/{types → dist}/trait/fingerprint.d.ts +0 -0
  136. /package/{types → dist}/trait/traits.d.ts +0 -0
  137. /package/{types → dist}/url.d.ts +0 -0
  138. /package/{types → dist}/webcrypto.deno.d.ts +0 -0
  139. /package/{types → dist}/webcrypto.node.d.ts +0 -0
  140. /package/{types → dist}/webcrypto.react-native.d.ts +0 -0
  141. /package/{types → dist}/websocket.d.ts +0 -0
@@ -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)