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,401 @@
|
|
|
1
|
+
import * as delta from '../delta.js'
|
|
2
|
+
import * as s from '../../schema.js'
|
|
3
|
+
import * as math from '../../math.js'
|
|
4
|
+
import * as error from '../../error.js'
|
|
5
|
+
import { Transformer, Template, createTransformResult } from './core.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The literal node-names a `$Delta`'s `$name` schema accepts, or `null` when the name is loose
|
|
9
|
+
* (`$any`/`$string`) — such a schema acts as a wildcard that matches any node name.
|
|
10
|
+
*
|
|
11
|
+
* @param {import('../../schema.js').Schema<any>} $name
|
|
12
|
+
* @return {Array<string>?}
|
|
13
|
+
*/
|
|
14
|
+
const literalNames = $name => s.$$literal.check($name) ? /** @type {Array<string>} */ ($name.shape) : null
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The expanded matcher for a children / attr-value schema.
|
|
18
|
+
*
|
|
19
|
+
* @typedef {{ byName: Map<string, delta.$Delta<any>>, wild: delta.$Delta<any>?, passAny: boolean, $scalar: import('../../schema.js').Schema<any> }} Allowed
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Expand a children / attr-value schema into the matcher conform uses. `byName`/`wild` give the `$Delta`
|
|
24
|
+
* a node conforms to, **matched by NAME** (so a node carrying not-yet-stripped attrs still matches — a
|
|
25
|
+
* full `.check` would be the wrong test, it would reject content conform is meant to strip). `passAny` is
|
|
26
|
+
* set when the schema accepts ANY content (a `$deltaAny` / `$any` member), so such content passes through
|
|
27
|
+
* verbatim with no nested transformer. `$scalar` checks a non-delta (JSON) value.
|
|
28
|
+
*
|
|
29
|
+
* @param {import('../../schema.js').Schema<any>} $s
|
|
30
|
+
* @return {Allowed}
|
|
31
|
+
*/
|
|
32
|
+
const allowedOf = $s => {
|
|
33
|
+
/** @type {Map<string, delta.$Delta<any>>} */
|
|
34
|
+
const byName = new Map()
|
|
35
|
+
/** @type {delta.$Delta<any>?} */
|
|
36
|
+
let wild = null
|
|
37
|
+
let passAny = false
|
|
38
|
+
for (const m of (s.$$union.check($s) ? $s.shape : [$s])) {
|
|
39
|
+
if (m === delta.$deltaAny || s.$$any.check(m)) {
|
|
40
|
+
passAny = true
|
|
41
|
+
} else if (delta.$$delta.check(m)) {
|
|
42
|
+
const $m = /** @type {delta.$Delta<any>} */ (m)
|
|
43
|
+
const names = literalNames($m.shape.$name)
|
|
44
|
+
if (names === null) wild ??= $m
|
|
45
|
+
else for (const n of names) byName.set(n, $m)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return { byName, wild, passAny, $scalar: $s }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** @type {Allowed} an empty matcher (allows nothing) — the placeholder for a pass-through config */
|
|
52
|
+
const NONE = { byName: new Map(), wild: null, passAny: false, $scalar: s.$never }
|
|
53
|
+
/** @type {Map<string|number, Allowed>} a shared empty attr matcher map (never mutated) */
|
|
54
|
+
const NO_ATTRS = new Map()
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The read-only, schema-derived configuration of a conform transformer. Computed once per schema (see
|
|
58
|
+
* {@link configOf}) and shared by every transformer instance built from that schema — many instances are
|
|
59
|
+
* created for the children of one schema, so this avoids re-expanding the schema each time.
|
|
60
|
+
*
|
|
61
|
+
* @typedef {{ passthrough: boolean, hasText: boolean, childAllowed: Allowed, attrsLoose: boolean, attrAllowed: Map<string|number, Allowed> }} Config
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
/** @type {WeakMap<import('../../schema.js').Schema<any>, Config>} */
|
|
65
|
+
const configCache = new WeakMap()
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The cached {@link Config} for `$schema`, computing (and memoizing) it on first use. `$deltaAny` yields
|
|
69
|
+
* a pass-through config (the conform is the identity).
|
|
70
|
+
*
|
|
71
|
+
* @param {import('../../schema.js').Schema<any>} $schema
|
|
72
|
+
* @return {Config}
|
|
73
|
+
*/
|
|
74
|
+
const configOf = $schema => {
|
|
75
|
+
let c = configCache.get($schema)
|
|
76
|
+
if (c === undefined) {
|
|
77
|
+
if ($schema === delta.$deltaAny) {
|
|
78
|
+
c = { passthrough: true, hasText: false, childAllowed: NONE, attrsLoose: false, attrAllowed: NO_ATTRS }
|
|
79
|
+
} else {
|
|
80
|
+
const shape = /** @type {any} */ ($schema).shape
|
|
81
|
+
const attrsLoose = !s.$$object.check(shape.$attrs)
|
|
82
|
+
/** @type {Map<string|number, Allowed>} */
|
|
83
|
+
const attrAllowed = new Map()
|
|
84
|
+
if (!attrsLoose) {
|
|
85
|
+
const attrShape = /** @type {any} */ (shape.$attrs).shape
|
|
86
|
+
for (const key in attrShape) attrAllowed.set(key, allowedOf(attrShape[key]))
|
|
87
|
+
}
|
|
88
|
+
c = { passthrough: false, hasText: shape.hasText, childAllowed: allowedOf(shape.$children), attrsLoose, attrAllowed }
|
|
89
|
+
}
|
|
90
|
+
configCache.set($schema, c)
|
|
91
|
+
}
|
|
92
|
+
return c
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* A fresh nested conform bound to a sub-schema (a kept child or delta-valued attribute), reusing the
|
|
97
|
+
* memoized {@link Config} for that schema.
|
|
98
|
+
*
|
|
99
|
+
* @param {delta.$Delta<any>} $schema
|
|
100
|
+
* @return {ConformTransformer}
|
|
101
|
+
*/
|
|
102
|
+
const nest = $schema => new ConformTransformer(delta.$deltaAny, $schema, configOf($schema))
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Decide what conform does with one piece of new content (a child node or an attribute value):
|
|
106
|
+
* - `null` — drop it (it matches nothing the schema allows),
|
|
107
|
+
* - `true` — pass it through verbatim (a JSON scalar, which can't be modified anyway, or a node matched
|
|
108
|
+
* by a `$deltaAny`/`$any` schema, whose every future edit is valid),
|
|
109
|
+
* - a `ConformTransformer` — keep it but recursively conform it (a delta node matched to a *narrower*
|
|
110
|
+
* `$Delta`, whose future edits must still be stripped).
|
|
111
|
+
*
|
|
112
|
+
* @param {any} node
|
|
113
|
+
* @param {Allowed} allowed
|
|
114
|
+
* @return {ConformTransformer | true | null}
|
|
115
|
+
*/
|
|
116
|
+
const needsTransform = (node, allowed) => {
|
|
117
|
+
if (delta.$deltaAny.check(node)) {
|
|
118
|
+
const $m = (node.name != null ? allowed.byName.get(node.name) : undefined) ?? allowed.wild
|
|
119
|
+
return $m != null ? nest($m) : (allowed.passAny ? true : null)
|
|
120
|
+
}
|
|
121
|
+
return allowed.$scalar.check(node) ? true : null
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Whether `allowed` admits at least one delta value at this position — a `$deltaAny`/`$any` member, a
|
|
126
|
+
* wildcard `$Delta`, or any name-matched `$Delta`. A `modify` / `modifyAttr` op edits a delta child or
|
|
127
|
+
* delta-valued attribute, so it can only conform where this holds; against a scalar/text-only matcher it
|
|
128
|
+
* is necessarily invalid. Used by {@link ConformTransformer#applyB} to reject such modifies.
|
|
129
|
+
*
|
|
130
|
+
* @param {Allowed} allowed
|
|
131
|
+
* @return {boolean}
|
|
132
|
+
*/
|
|
133
|
+
const admitsDelta = allowed => allowed.passAny || allowed.wild != null || allowed.byName.size > 0
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Makes the projected (side-B) delta conform to `$schema`, **recursively**: drops attributes, attribute
|
|
137
|
+
* values, child nodes, and text the schema does not recognize, and descends into kept delta-valued
|
|
138
|
+
* attributes and kept child nodes with a nested `conform` (a fresh instance per kept child/attr, kept in
|
|
139
|
+
* step with the content — modelled on `children` + `inline`). A child is kept iff its node-name matches
|
|
140
|
+
* one of the schema's child `$Delta`s; a non-delta child iff it validates against `$children`; text iff
|
|
141
|
+
* the schema allows text. Everything else is dropped, so the output is guaranteed to satisfy `$schema`.
|
|
142
|
+
*
|
|
143
|
+
* Content that needs no per-position handling (text, scalars, and nodes matched by a `$deltaAny`/`$any`
|
|
144
|
+
* schema) is coalesced into pass-through runs, so a `$deltaAny` schema is a true zero-overhead identity
|
|
145
|
+
* and an incremental edit costs O(change), not O(document).
|
|
146
|
+
*
|
|
147
|
+
* `applyA` (A → B) does the full recursive filtering. `applyB` (B → A) is a lightweight reverse: it
|
|
148
|
+
* validates each B-side op against `$schema` — throwing on any op that would break conformance — and
|
|
149
|
+
* otherwise passes the change through to A verbatim (a conformant B-edit is already valid on A). Full
|
|
150
|
+
* drop-aware B→A position remapping is deferred (see {@link ConformTransformer#applyB}).
|
|
151
|
+
* Marks are best-effort — marks on kept children/attrs ride through the position map; marks anchored to
|
|
152
|
+
* dropped content are dropped.
|
|
153
|
+
*
|
|
154
|
+
* @template {delta.DeltaConf} SchemaConf
|
|
155
|
+
* @template {delta.DeltaConf} [IN=any]
|
|
156
|
+
* @extends {Template<IN, SchemaConf>}
|
|
157
|
+
*/
|
|
158
|
+
export class Conform extends Template {
|
|
159
|
+
/**
|
|
160
|
+
* @param {import('../../schema.js').Schema<delta.Delta<IN>>} $d input delta schema
|
|
161
|
+
* @param {import('../../schema.js').Schema<delta.Delta<SchemaConf>>} $schema the schema the output must match
|
|
162
|
+
*/
|
|
163
|
+
constructor ($d, $schema) {
|
|
164
|
+
if (/** @type {any} */ ($schema) !== delta.$deltaAny) s.assert($schema, delta.$$delta)
|
|
165
|
+
super($d, /** @type {any} */ ($schema))
|
|
166
|
+
this.$schema = $schema
|
|
167
|
+
// expand the schema ONCE here (memoized); every transformer this template builds reuses it
|
|
168
|
+
this.config = configOf($schema)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
get name () { return 'lib0:conform' }
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* @return {Transformer<IN, SchemaConf>}
|
|
175
|
+
*/
|
|
176
|
+
init () {
|
|
177
|
+
return /** @type {any} */ (new ConformTransformer(this.$in, this.$out, this.config))
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Stateful, recursive transformer produced by {@link Conform}. See {@link Conform} for semantics.
|
|
183
|
+
*
|
|
184
|
+
* The A→B layout lives in {@link ConformTransformer#cmap} (the *child* map), a delta reused as a
|
|
185
|
+
* coalesced positional map (like `children`'s `childTs`): a `retain(n)` run = pass-through positions, an
|
|
186
|
+
* `insert([t])` = a child routed through nested conform `t`, a `delete(n)` run = dropped positions
|
|
187
|
+
* (width 0 on B). It is rebuilt only on a structural change, carrying the nested transformers by
|
|
188
|
+
* reference so their state survives; a change with only retains/modifies is routed in place.
|
|
189
|
+
*
|
|
190
|
+
* @extends {Transformer<any,any>}
|
|
191
|
+
*/
|
|
192
|
+
export class ConformTransformer extends Transformer {
|
|
193
|
+
/**
|
|
194
|
+
* @param {import('../../schema.js').Schema<delta.Delta<any>>} $in
|
|
195
|
+
* @param {import('../../schema.js').Schema<delta.Delta<any>>} $out
|
|
196
|
+
* @param {Config} config the schema-derived read-only config (computed once, see {@link configOf})
|
|
197
|
+
*/
|
|
198
|
+
constructor ($in, $out, config) {
|
|
199
|
+
super($in, $out)
|
|
200
|
+
this.config = config
|
|
201
|
+
/** @type {delta.DeltaBuilderAny} the coalesced child layout map */
|
|
202
|
+
this.cmap = delta.create()
|
|
203
|
+
/** @type {Map<string|number, ConformTransformer>} nested conforms for delta-valued attributes */
|
|
204
|
+
this.transformAttrs = new Map()
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Map an A-side change to a conformant B-side change. Unrecognized attributes/values/children/text are
|
|
209
|
+
* dropped; recognized delta children and delta-valued attributes are recursively conformed.
|
|
210
|
+
*
|
|
211
|
+
* @param {delta.DeltaBuilderAny} dA
|
|
212
|
+
* @return {import('./core.js').TransformResultAny}
|
|
213
|
+
*/
|
|
214
|
+
applyA (dA) {
|
|
215
|
+
const cfg = this.config
|
|
216
|
+
if (cfg.passthrough) return createTransformResult(null, dA) // identity fast-path, no walk, no copy
|
|
217
|
+
const out = /** @type {any} */ (delta.create(/** @type {any} */ (dA.name)))
|
|
218
|
+
/** @type {Set<string|number>} */
|
|
219
|
+
const keptAttrKeys = new Set()
|
|
220
|
+
// --- attributes ---
|
|
221
|
+
for (const op of dA.attrs) {
|
|
222
|
+
const key = op.key
|
|
223
|
+
if (cfg.attrsLoose) {
|
|
224
|
+
// loose schema attrs ($any): keep every attribute verbatim (`out` is `any`, so the dynamic-key
|
|
225
|
+
// write is fine; this mirrors how `slice`/`cloneShallow` copy attr ops)
|
|
226
|
+
out.attrs[key] = op.clone()
|
|
227
|
+
keptAttrKeys.add(key)
|
|
228
|
+
continue
|
|
229
|
+
}
|
|
230
|
+
const allowed = cfg.attrAllowed.get(key)
|
|
231
|
+
if (allowed === undefined) continue // unknown attribute -> drop
|
|
232
|
+
if (delta.$setAttrOp.check(op)) {
|
|
233
|
+
const r = needsTransform(op.value, allowed)
|
|
234
|
+
if (r === null) continue
|
|
235
|
+
if (r === true) { out.setAttr(key, op.value, op.attribution); this.transformAttrs.delete(key) } else { this.transformAttrs.set(key, r); out.setAttr(key, r.applyA(op.value).b, op.attribution) }
|
|
236
|
+
keptAttrKeys.add(key)
|
|
237
|
+
} else if (delta.$modifyAttrOp.check(op)) {
|
|
238
|
+
let t = this.transformAttrs.get(key)
|
|
239
|
+
if (t === undefined) {
|
|
240
|
+
// no setAttr was seen first: a $deltaAny attr forwards verbatim; a narrowed delta attr builds
|
|
241
|
+
// its nested conform lazily; a scalar attr has nothing to modify (drop)
|
|
242
|
+
if (allowed.passAny) { out.modifyAttr(key, op.value); keptAttrKeys.add(key); continue }
|
|
243
|
+
const $m = allowed.wild ?? (allowed.byName.size > 0 ? /** @type {delta.$Delta<any>} */ (allowed.byName.values().next().value) : null)
|
|
244
|
+
if ($m != null) { t = nest($m); this.transformAttrs.set(key, t) }
|
|
245
|
+
}
|
|
246
|
+
if (t != null) { out.modifyAttr(key, t.applyA(op.value).b); keptAttrKeys.add(key) }
|
|
247
|
+
} else { // deleteAttr (the only remaining attr-op kind)
|
|
248
|
+
const dop = /** @type {delta.DeleteAttrOp<any>} */ (op)
|
|
249
|
+
out.deleteAttr(key, dop.attribution)
|
|
250
|
+
this.transformAttrs.delete(key)
|
|
251
|
+
keptAttrKeys.add(key)
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
// --- children: one cursor `(src, off)` walks `this.cmap`, editing it in place ---
|
|
255
|
+
// `cmap` is the coalesced A→B child layout: retain = pass-through, insert([t]) = a child routed
|
|
256
|
+
// through nested conform `t`, delete = dropped (B-width 0). retain/modify advance the cursor without
|
|
257
|
+
// touching the map; insert/text/delete edit it in place via delta's @internal child mutators (which
|
|
258
|
+
// keep `cmap.childCnt` in sync). Same-kind insertion grows the run in place (no split), so the map
|
|
259
|
+
// stays coalesced — typing into a pass-through run is O(1) and never fragments. The untouched tail
|
|
260
|
+
// past the cursor is simply left in place (no copy), so a structural edit costs O(change).
|
|
261
|
+
const cmap = this.cmap
|
|
262
|
+
let src = cmap.children.start
|
|
263
|
+
let off = 0
|
|
264
|
+
// move the cursor onto an op boundary so a fresh op can be inserted there
|
|
265
|
+
const splitCursor = () => { if (off > 0 && src != null) { src = delta._splitChildAt(cmap, src, off); off = 0 } }
|
|
266
|
+
// splice one run into the map at the cursor: grow the current op when it is the same kind (no split),
|
|
267
|
+
// else split + insert a fresh op + coalesce. `$kind`/`make` describe a uniform retain/delete run.
|
|
268
|
+
/**
|
|
269
|
+
* @param {import('../../schema.js').Schema<any>} $kind
|
|
270
|
+
* @param {number} n
|
|
271
|
+
* @param {() => delta.RetainOp | delta.DeleteOp<any>} make
|
|
272
|
+
*/
|
|
273
|
+
const addUniform = ($kind, n, make) => {
|
|
274
|
+
if (src != null && $kind.check(src)) { delta._growRun(cmap, /** @type {delta.RetainOp | delta.DeleteOp<any>} */ (src), n); off += n } else { splitCursor(); delta._mergeChildWithPrev(cmap, delta._insertChild(cmap, src, make())) }
|
|
275
|
+
}
|
|
276
|
+
/** @param {number} n */
|
|
277
|
+
const addRetain = n => addUniform(delta.$retainOp, n, () => new delta.RetainOp(n, null, null)) // n pass-through positions
|
|
278
|
+
/** @param {number} n */
|
|
279
|
+
const addDrop = n => addUniform(delta.$deleteOp, n, () => new delta.DeleteOp(n)) // n dropped positions
|
|
280
|
+
/** @param {ConformTransformer} t */
|
|
281
|
+
const addTransformed = t => { // one child routed through nested conform `t`
|
|
282
|
+
if (src != null && delta.$insertOp.check(src)) { delta._spliceInsert(cmap, src, off, [t]); off += 1 } else { splitCursor(); delta._mergeChildWithPrev(cmap, delta._insertChild(cmap, src, new delta.InsertOp([t], null, null))) }
|
|
283
|
+
}
|
|
284
|
+
for (const op of dA.children) {
|
|
285
|
+
if (delta.$retainOp.check(op)) {
|
|
286
|
+
let rem = op.retain
|
|
287
|
+
while (rem > 0 && src != null) {
|
|
288
|
+
const take = math.min(src.length - off, rem)
|
|
289
|
+
if (!delta.$deleteOp.check(src)) out.retain(take, op.format, op.attribution) // pass & transform: width = take
|
|
290
|
+
off += take; rem -= take
|
|
291
|
+
if (off >= src.length) { src = src.next; off = 0 }
|
|
292
|
+
}
|
|
293
|
+
} else if (delta.$modifyOp.check(op)) {
|
|
294
|
+
if (src != null) {
|
|
295
|
+
if (delta.$insertOp.check(src)) out.modify(/** @type {ConformTransformer} */ (src.insert[off]).applyA(delta.clone(op.value)).b, op.format, op.attribution)
|
|
296
|
+
else if (delta.$retainOp.check(src)) out.modify(delta.clone(op.value), op.format, op.attribution) // pass-through child
|
|
297
|
+
// dropped child (delete in map): emit nothing
|
|
298
|
+
off += 1; if (off >= src.length) { src = src.next; off = 0 }
|
|
299
|
+
}
|
|
300
|
+
} else if (delta.$textOp.check(op)) {
|
|
301
|
+
if (cfg.hasText) { out.insert(op.insert, op.format, op.attribution); addRetain(op.insert.length) } else addDrop(op.insert.length)
|
|
302
|
+
} else if (delta.$insertOp.check(op)) {
|
|
303
|
+
for (const el of op.insert) {
|
|
304
|
+
const r = needsTransform(el, cfg.childAllowed)
|
|
305
|
+
if (r === null) addDrop(1) // dropped: no name match, no scalar match
|
|
306
|
+
else if (r === true) { out.insert([el], op.format, op.attribution); addRetain(1) } else { out.insert([r.applyA(el).b], op.format, op.attribution); addTransformed(r) }
|
|
307
|
+
}
|
|
308
|
+
} else { // delete: remove the deleted A-positions from the map
|
|
309
|
+
let rem = /** @type {delta.DeleteOp<any>} */ (op).delete
|
|
310
|
+
while (rem > 0 && src != null) {
|
|
311
|
+
const take = math.min(src.length - off, rem)
|
|
312
|
+
if (!delta.$deleteOp.check(src)) out.delete(take) // pass & transform had B-width; dropped had 0
|
|
313
|
+
if (off === 0 && take === src.length) { const next = src.next; delta._removeChild(cmap, src); src = next } else { delta._shrinkChild(cmap, src, off, take); if (off >= src.length) { src = src.next; off = 0 } }
|
|
314
|
+
rem -= take
|
|
315
|
+
}
|
|
316
|
+
// the deletion may have joined two same-kind runs: merge, keeping the cursor on the survivor
|
|
317
|
+
if (src != null) {
|
|
318
|
+
const prev = src.prev
|
|
319
|
+
const prevLen = prev != null ? prev.length : 0
|
|
320
|
+
if (delta._mergeChildWithPrev(cmap, src)) { src = prev; off += prevLen }
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
// --- marks (best-effort): map a content offset through the coalesced layout (O(#runs)) ---
|
|
325
|
+
if (dA.marks !== null || dA.deleteMarks !== null) {
|
|
326
|
+
delta.mergeRootMarks(out, dA, k => {
|
|
327
|
+
if (typeof k !== 'number') return keptAttrKeys.has(k) ? k : null // attr mark rides iff kept
|
|
328
|
+
let a = 0; let b = 0
|
|
329
|
+
for (const cop of this.cmap.children) {
|
|
330
|
+
if (a >= k) break
|
|
331
|
+
const take = math.min(cop.length, k - a)
|
|
332
|
+
if (!delta.$deleteOp.check(cop)) b += take // pass & transform keep width; drop contributes 0
|
|
333
|
+
a += take
|
|
334
|
+
}
|
|
335
|
+
return b
|
|
336
|
+
})
|
|
337
|
+
}
|
|
338
|
+
out.done(false)
|
|
339
|
+
return createTransformResult(null, out)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Map a B-side change back to A. The conformed view B already satisfies `$schema`, so a change
|
|
344
|
+
* authored on it is also valid on A and passes straight through — the result is `{ a: dB, b: null }`
|
|
345
|
+
* (mirroring `applyA`'s `{ a: null, b: out }`). Before donating it, every op is validated against the
|
|
346
|
+
* schema and any op that would break conformance **throws**: text where the schema forbids text, an
|
|
347
|
+
* `insert` / `setAttr` whose content fails the schema, an unknown attribute key, or a `modify` /
|
|
348
|
+
* `modifyAttr` targeting a position the schema gives no delta.
|
|
349
|
+
*
|
|
350
|
+
* Two things are intentionally out of scope in v1: the nested change of a `modify` is not
|
|
351
|
+
* deep-validated (only that a delta is permissible there), and B→A coordinates are passed through
|
|
352
|
+
* verbatim — exact when nothing was dropped (B is structurally identical to A); a full drop-aware
|
|
353
|
+
* position remapping through `cmap` is deferred.
|
|
354
|
+
*
|
|
355
|
+
* @param {delta.DeltaBuilderAny} dB
|
|
356
|
+
* @return {import('./core.js').TransformResultAny}
|
|
357
|
+
*/
|
|
358
|
+
applyB (dB) {
|
|
359
|
+
const cfg = this.config
|
|
360
|
+
if (cfg.passthrough) return createTransformResult(dB, null) // identity: every op conforms
|
|
361
|
+
if (!cfg.attrsLoose) { // loose attrs ($any) accept any key/value — nothing to validate
|
|
362
|
+
for (const op of dB.attrs) {
|
|
363
|
+
const allowed = cfg.attrAllowed.get(op.key)
|
|
364
|
+
if (allowed === undefined) throw error.create('[lib0/delta] conform: unknown attribute "' + op.key + '"')
|
|
365
|
+
if (delta.$setAttrOp.check(op)) {
|
|
366
|
+
if (!allowed.$scalar.check(op.value)) throw error.create('[lib0/delta] conform: attribute "' + op.key + '" value does not conform to the schema')
|
|
367
|
+
} else if (delta.$modifyAttrOp.check(op)) {
|
|
368
|
+
if (!admitsDelta(allowed)) throw error.create('[lib0/delta] conform: attribute "' + op.key + '" is a scalar and cannot be modified')
|
|
369
|
+
}
|
|
370
|
+
// deleteAttr: removing an attribute always conforms
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
for (const op of dB.children) {
|
|
374
|
+
if (delta.$textOp.check(op)) {
|
|
375
|
+
if (!cfg.hasText) throw error.create('[lib0/delta] conform: text is not allowed by the schema')
|
|
376
|
+
} else if (delta.$insertOp.check(op)) {
|
|
377
|
+
for (const el of op.insert) if (!cfg.childAllowed.$scalar.check(el)) throw error.create('[lib0/delta] conform: inserted content does not conform to the schema')
|
|
378
|
+
} else if (delta.$modifyOp.check(op)) {
|
|
379
|
+
if (!admitsDelta(cfg.childAllowed)) throw error.create('[lib0/delta] conform: the schema has no delta child to modify')
|
|
380
|
+
}
|
|
381
|
+
// retain / delete: structural, no content -> always conform
|
|
382
|
+
}
|
|
383
|
+
return createTransformResult(dB, null) // every op conforms -> donate the change to A verbatim
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Make the projected (side-B) delta conform to `$schema`, recursively dropping every attribute, value,
|
|
389
|
+
* child node, and text run the schema does not recognize and descending into kept delta children /
|
|
390
|
+
* delta-valued attributes. Content the schema accepts passes through with near-zero overhead;
|
|
391
|
+
* `conform($d, delta.$deltaAny)` is the identity. Returns a reusable {@link Conform} template (a
|
|
392
|
+
* `project` hole, or `.init()` for a standalone transformer). `applyB` (B → A) validates a B-side
|
|
393
|
+
* change against `$schema` and passes it through, throwing on any non-conformant op.
|
|
394
|
+
*
|
|
395
|
+
* @template {delta.DeltaConf} IN
|
|
396
|
+
* @template {delta.DeltaConf} SchemaConf
|
|
397
|
+
* @param {import('../../schema.js').Schema<delta.Delta<IN>>} $d
|
|
398
|
+
* @param {import('../../schema.js').Schema<delta.Delta<SchemaConf>>} $schema
|
|
399
|
+
* @return {Conform<SchemaConf, IN>}
|
|
400
|
+
*/
|
|
401
|
+
export const conform = ($d, $schema) => new Conform($d, $schema)
|