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,335 @@
|
|
|
1
|
+
import * as delta from '../delta.js'
|
|
2
|
+
import * as s from '../../schema.js'
|
|
3
|
+
import * as object from '../../object.js'
|
|
4
|
+
import { Transformer, Template, createTransformResult } from './core.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* # `attributionToFormat`
|
|
8
|
+
*
|
|
9
|
+
* Render a delta's **`attribution`** dimension (provenance — who inserted/deleted/formatted) into a
|
|
10
|
+
* reserved `y-attributed-*` slice of the **`format`** dimension, so a rich-text editor can display
|
|
11
|
+
* provenance as decorations. The reverse direction strips the decorations back out.
|
|
12
|
+
*
|
|
13
|
+
* Forward (`applyA`, data → view), per content op:
|
|
14
|
+
* - `attribution` whose keys start with `insert`/`delete`/`format` ⇒ `format['y-attributed-insert' |
|
|
15
|
+
* 'y-attributed-delete' | 'y-attributed-format']`, computed by the matching `conf` handler.
|
|
16
|
+
*
|
|
17
|
+
* A node's attribute ops carry attribution but have **no format slot**, and a node has no format of
|
|
18
|
+
* its own — its format lives on the *parent* op that holds it. So a node-attr's attribution is lifted
|
|
19
|
+
* to that parent (`insert`/`modify`) op as `format['y-attributed-attrs'] = { <attrKey>: <value> }`,
|
|
20
|
+
* observed at the boundary before recursing into the child ("the parent formats the child").
|
|
21
|
+
*
|
|
22
|
+
* Reverse (`applyB`, view → data) is a pure strip: every `y-attributed-*` format key is removed and
|
|
23
|
+
* everything else passes through. *The view never attributes.*
|
|
24
|
+
*
|
|
25
|
+
* The transform is **structure-preserving** (1:1 positions) and **stateless** — it walks each change
|
|
26
|
+
* op-by-op and recurses through the whole nested delta in one pass (no per-child transformer
|
|
27
|
+
* instances). See {@link attributionToFormat}.
|
|
28
|
+
*
|
|
29
|
+
* @module delta/transformer/attribution-to-format
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
// Reserved, fixed format-key namespace. Every key produced here starts with `Y_PREFIX`, which is also
|
|
33
|
+
// how `applyB` recognises (and strips) them.
|
|
34
|
+
const Y_PREFIX = 'y-attributed-'
|
|
35
|
+
const Y_ATTRS = 'y-attributed-attrs'
|
|
36
|
+
|
|
37
|
+
/** Content-attribution dimension → its namespaced format key. */
|
|
38
|
+
const CONTENT_KEYS = { insert: 'y-attributed-insert', delete: 'y-attributed-delete', format: 'y-attributed-format' }
|
|
39
|
+
const CONTENT_DIMS = /** @type {Array<'insert'|'delete'|'format'>} */ (object.keys(CONTENT_KEYS))
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Maps an attribution object to the format value placed under its namespaced key (or `null` to clear
|
|
43
|
+
* that key, `{}`/`undefined` to leave it unchanged).
|
|
44
|
+
*
|
|
45
|
+
* @typedef {(attribution: import('../delta.js').Attribution) => any} Handler
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Per-dimension handlers. Each fires when the op's attribution has a key with that dimension's prefix
|
|
50
|
+
* (`insert`/`insertAt` → `insert`, etc.); the whole attribution object is passed in. A missing handler
|
|
51
|
+
* means "do not render that dimension".
|
|
52
|
+
*
|
|
53
|
+
* @typedef {{ insert?: Handler, delete?: Handler, format?: Handler, attrs?: Handler }} Conf
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A handler result that means "no change" (skip): `undefined` or an empty plain object `{}`. A bare
|
|
58
|
+
* scalar (string/number) or a non-empty object is a real value; `null` is handled separately (clear).
|
|
59
|
+
*
|
|
60
|
+
* @param {any} r
|
|
61
|
+
* @return {boolean}
|
|
62
|
+
*/
|
|
63
|
+
const isSkip = (r) => r === undefined || (s.$objectAny.check(r) && object.isEmpty(r))
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Build the `y-attributed-*` content-format increment from a content op's `attribution`. `isData`
|
|
67
|
+
* distinguishes a settled data op (`insert`/`text`: `attribution` is an object or `null`=none) from an
|
|
68
|
+
* instruction op (`retain`/`modify`: `undefined`=skip, `null`=clear-all).
|
|
69
|
+
*
|
|
70
|
+
* @param {Conf} conf
|
|
71
|
+
* @param {import('../delta.js').Attribution|null|undefined} attribution
|
|
72
|
+
* @param {boolean} isData
|
|
73
|
+
* @return {{[k:string]:any}|undefined}
|
|
74
|
+
*/
|
|
75
|
+
const contentFmt = (conf, attribution, isData) => {
|
|
76
|
+
if (attribution === undefined) return undefined
|
|
77
|
+
/** @type {{[k:string]:any}} */
|
|
78
|
+
const f = {}
|
|
79
|
+
if (attribution === null) {
|
|
80
|
+
if (isData) return undefined // data "none" — nothing to render
|
|
81
|
+
for (const dim of CONTENT_DIMS) if (conf[dim] != null) f[CONTENT_KEYS[dim]] = null // instruction clear-all
|
|
82
|
+
return object.isEmpty(f) ? undefined : f
|
|
83
|
+
}
|
|
84
|
+
const ks = object.keys(attribution)
|
|
85
|
+
for (const dim of CONTENT_DIMS) {
|
|
86
|
+
const handler = conf[dim]
|
|
87
|
+
if (handler != null && ks.some(k => k.startsWith(dim))) {
|
|
88
|
+
const r = handler(attribution)
|
|
89
|
+
// `null` ⇒ clear the key, but only as an *instruction* — a data op stores resolved formats with
|
|
90
|
+
// no `{k:null}` leaves, so on a data op a `null` result is "nothing to render" (skip).
|
|
91
|
+
if (r === null) { if (!isData) f[CONTENT_KEYS[dim]] = null } else if (!isSkip(r)) f[CONTENT_KEYS[dim]] = r
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return object.isEmpty(f) ? undefined : f
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Build the `y-attributed-attrs` format value from a child node's attribute-op attributions — the
|
|
99
|
+
* lift that lets the *parent* op carry a node's attr provenance (attr ops have no format slot). For a
|
|
100
|
+
* `modifyAttr` op a `null` attribution is an instruction *clear* (⇒ `{ <key>: null }`); for a settled
|
|
101
|
+
* `setAttr`/`deleteAttr` op `null` is "none" (skipped).
|
|
102
|
+
*
|
|
103
|
+
* @param {Conf} conf
|
|
104
|
+
* @param {import('../delta.js').DeltaAny} node
|
|
105
|
+
* @return {{[k:string]:any}|undefined}
|
|
106
|
+
*/
|
|
107
|
+
const attrsFmt = (conf, node) => {
|
|
108
|
+
const handler = conf.attrs
|
|
109
|
+
if (handler == null) return undefined
|
|
110
|
+
/** @type {{[k:string]:any}} */
|
|
111
|
+
const map = {}
|
|
112
|
+
for (const op of node.attrs) {
|
|
113
|
+
const isInstr = delta.$modifyAttrOp.check(op) // modifyAttr carries an attribution *instruction*; setAttr/deleteAttr resolved data
|
|
114
|
+
const a = op.attribution
|
|
115
|
+
if (a === undefined) continue // instruction skip
|
|
116
|
+
if (a === null) {
|
|
117
|
+
if (isInstr) map[op.key] = null // instruction clear (data "none" ⇒ skip)
|
|
118
|
+
continue
|
|
119
|
+
}
|
|
120
|
+
const r = handler(a)
|
|
121
|
+
if (r === null) { if (isInstr) map[op.key] = null } else if (!isSkip(r)) map[op.key] = r
|
|
122
|
+
}
|
|
123
|
+
return object.isEmpty(map) ? undefined : { [Y_ATTRS]: map }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Merge `y-attributed-*` increments onto a content op's existing `format`, preserving real formats
|
|
128
|
+
* (`bold`, …) and the op's tri-state. With no increment, `base` is returned verbatim (so a `null`
|
|
129
|
+
* clear-all or a `{k:null}` removal survives). With increments, a `null`/`undefined` base contributes
|
|
130
|
+
* nothing and only the namespaced keys are emitted.
|
|
131
|
+
*
|
|
132
|
+
* @param {{[k:string]:any}|null|undefined} base op.format (skip `undefined` / clear-all `null` / object)
|
|
133
|
+
* @param {...({[k:string]:any}|undefined)} adds namespaced format increments
|
|
134
|
+
* @return {{[k:string]:any}|null|undefined}
|
|
135
|
+
*/
|
|
136
|
+
const combineFmt = (base, ...adds) => {
|
|
137
|
+
const extra = object.assign({}, ...adds)
|
|
138
|
+
if (object.isEmpty(extra)) return base
|
|
139
|
+
return base == null ? extra : object.assign({}, base, extra)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Remove every reserved `y-attributed-*` key from a `format`. Returns the input unchanged when it has
|
|
144
|
+
* none (preserving `undefined`/`null`/object verbatim), or `undefined` when stripping empties it.
|
|
145
|
+
*
|
|
146
|
+
* @param {{[k:string]:any}|null|undefined} format
|
|
147
|
+
* @return {{[k:string]:any}|null|undefined}
|
|
148
|
+
*/
|
|
149
|
+
const stripY = (format) => {
|
|
150
|
+
if (format == null) return format
|
|
151
|
+
/** @type {{[k:string]:any}} */
|
|
152
|
+
const r = {}
|
|
153
|
+
let stripped = false
|
|
154
|
+
for (const k in format) {
|
|
155
|
+
if (k.startsWith(Y_PREFIX)) stripped = true
|
|
156
|
+
else r[k] = format[k]
|
|
157
|
+
}
|
|
158
|
+
return stripped ? (object.isEmpty(r) ? undefined : r) : format
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Re-emit a node's own attribute ops without attribution. The view never attributes — a node's
|
|
163
|
+
* attr-attribution is rendered on the *parent* op (see {@link attrsFmt}) — so it must not also ride on
|
|
164
|
+
* the attr op. Rebuilt via the builder rather than mutating cloned ops.
|
|
165
|
+
*
|
|
166
|
+
* @param {import('../delta.js').DeltaBuilderAny} out
|
|
167
|
+
*/
|
|
168
|
+
const stripAttrAttributions = (out) => {
|
|
169
|
+
/** @type {Array<any>} */
|
|
170
|
+
const ops = []
|
|
171
|
+
for (const op of out.attrs) ops.push(op)
|
|
172
|
+
for (const op of ops) {
|
|
173
|
+
if (delta.$modifyAttrOp.check(op)) {
|
|
174
|
+
if (op.attribution !== undefined) out.modifyAttr(op.key, op.value)
|
|
175
|
+
} else if (delta.$setAttrOp.check(op)) {
|
|
176
|
+
if (op.attribution != null) out.setAttr(op.key, op.value)
|
|
177
|
+
} else if (op.attribution != null) { // DeleteAttrOp
|
|
178
|
+
out.deleteAttr(op.key)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* The whole transform, both directions, in one stateless recursive pass over the nested delta.
|
|
185
|
+
*
|
|
186
|
+
* @param {Conf} conf
|
|
187
|
+
* @param {import('../delta.js').DeltaAny} d
|
|
188
|
+
* @param {boolean} fwd `true` maps A→B (attribution→format), `false` maps B→A (strip).
|
|
189
|
+
* @return {import('./core.js').TransformResultAny}
|
|
190
|
+
*/
|
|
191
|
+
const transform = (conf, d, fwd) => {
|
|
192
|
+
const out = /** @type {any} */ (delta.cloneShallow(d))
|
|
193
|
+
if (fwd) {
|
|
194
|
+
stripAttrAttributions(out)
|
|
195
|
+
for (const op of d.children) {
|
|
196
|
+
if (delta.$textOp.check(op)) {
|
|
197
|
+
out.insert(op.insert, combineFmt(op.format, contentFmt(conf, op.attribution, true)))
|
|
198
|
+
} else if (delta.$retainOp.check(op)) {
|
|
199
|
+
out.retain(op.retain, combineFmt(op.format, contentFmt(conf, op.attribution, false)))
|
|
200
|
+
} else if (delta.$insertOp.check(op)) {
|
|
201
|
+
// one element at a time: the builder re-coalesces equal formats and splits differing ones, so
|
|
202
|
+
// children needing distinct `y-attributed-attrs` land in their own insert ops automatically
|
|
203
|
+
for (const el of op.insert) {
|
|
204
|
+
if (delta.$deltaAny.check(el)) {
|
|
205
|
+
out.insert([transform(conf, el, true).b], combineFmt(op.format, contentFmt(conf, op.attribution, true), attrsFmt(conf, el)))
|
|
206
|
+
} else {
|
|
207
|
+
out.insert([el], combineFmt(op.format, contentFmt(conf, op.attribution, true)))
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
} else if (delta.$deleteOp.check(op)) {
|
|
211
|
+
out.delete(op.delete)
|
|
212
|
+
} else { // $modifyOp
|
|
213
|
+
out.modify(transform(conf, op.value, true).b, combineFmt(op.format, contentFmt(conf, op.attribution, false), attrsFmt(conf, op.value)))
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
} else {
|
|
217
|
+
for (const op of d.children) {
|
|
218
|
+
if (delta.$textOp.check(op)) {
|
|
219
|
+
out.insert(op.insert, stripY(op.format))
|
|
220
|
+
} else if (delta.$retainOp.check(op)) {
|
|
221
|
+
out.retain(op.retain, stripY(op.format))
|
|
222
|
+
} else if (delta.$insertOp.check(op)) {
|
|
223
|
+
for (const el of op.insert) {
|
|
224
|
+
out.insert([delta.$deltaAny.check(el) ? transform(conf, el, false).a : el], stripY(op.format))
|
|
225
|
+
}
|
|
226
|
+
} else if (delta.$deleteOp.check(op)) {
|
|
227
|
+
out.delete(op.delete)
|
|
228
|
+
} else { // $modifyOp
|
|
229
|
+
out.modify(transform(conf, op.value, false).a, stripY(op.format))
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
out.done(false)
|
|
234
|
+
return fwd ? createTransformResult(null, out) : createTransformResult(out, null)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Stateless transformer (holds only its `conf`); a single shared instance per template suffices.
|
|
239
|
+
*
|
|
240
|
+
* @extends {Transformer<any,any>}
|
|
241
|
+
*/
|
|
242
|
+
export class AttributionToFormatTransformer extends Transformer {
|
|
243
|
+
/**
|
|
244
|
+
* @param {import('../../schema.js').Schema<delta.Delta<any>>} $in
|
|
245
|
+
* @param {import('../../schema.js').Schema<delta.Delta<any>>} $out
|
|
246
|
+
* @param {Conf} conf
|
|
247
|
+
*/
|
|
248
|
+
constructor ($in, $out, conf) {
|
|
249
|
+
super($in, $out)
|
|
250
|
+
this.conf = conf
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* @param {delta.DeltaBuilderAny} d
|
|
255
|
+
* @return {import('./core.js').TransformResultAny}
|
|
256
|
+
*/
|
|
257
|
+
applyA (d) {
|
|
258
|
+
return transform(this.conf, d, true)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* @param {delta.DeltaBuilderAny} d
|
|
263
|
+
* @return {import('./core.js').TransformResultAny}
|
|
264
|
+
*/
|
|
265
|
+
applyB (d) {
|
|
266
|
+
return transform(this.conf, d, false)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Compute the output schema: widen the input `$formats` to admit the four `y-attributed-*` keys (only
|
|
272
|
+
* text-op formats are schema-checked, delta.js `$Delta#check`). Falls back to `$deltaAny` when `$in`
|
|
273
|
+
* is loose. A local rebuild (not `withAttrs`/`withName`, which hardcode `recursiveChildren:false`) so
|
|
274
|
+
* recursive node trees survive.
|
|
275
|
+
*
|
|
276
|
+
* @param {import('../../schema.js').Schema<delta.DeltaAny>} $in
|
|
277
|
+
* @return {import('../../schema.js').Schema<delta.DeltaAny>}
|
|
278
|
+
*/
|
|
279
|
+
const computeOut = ($in) => {
|
|
280
|
+
if (!delta.$$delta.check($in)) return delta.$deltaAny
|
|
281
|
+
const $formats = $in.shape.$formats
|
|
282
|
+
const $wide = s.$$object.check($formats)
|
|
283
|
+
? s.$object(object.assign({}, $formats.shape, { 'y-attributed-insert': s.$any.optional, 'y-attributed-delete': s.$any.optional, 'y-attributed-format': s.$any.optional, [Y_ATTRS]: s.$any.optional }))
|
|
284
|
+
: $formats // `s.$any` (the default) already admits the namespaced keys
|
|
285
|
+
return new delta.$Delta($in.shape.$name, $in.shape.$attrs, $in.shape.$children, $in.shape.hasText, $in.shape.recursiveChildren, $wide)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Template for {@link AttributionToFormatTransformer}.
|
|
290
|
+
*
|
|
291
|
+
* @template {delta.DeltaConf} [IN=any]
|
|
292
|
+
* @extends {Template<IN, any>}
|
|
293
|
+
*/
|
|
294
|
+
export class AttributionToFormat extends Template {
|
|
295
|
+
/**
|
|
296
|
+
* @param {import('../../schema.js').Schema<delta.Delta<IN>>} $d
|
|
297
|
+
* @param {Conf} conf
|
|
298
|
+
*/
|
|
299
|
+
constructor ($d, conf) {
|
|
300
|
+
super($d, /** @type {any} */ (computeOut($d)))
|
|
301
|
+
/**
|
|
302
|
+
* @type {Conf}
|
|
303
|
+
*/
|
|
304
|
+
this.conf = conf
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
get name () { return 'lib0:attributionToFormat' }
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* @return {Transformer<IN, any>}
|
|
311
|
+
*/
|
|
312
|
+
init () {
|
|
313
|
+
return new AttributionToFormatTransformer(this.$in, this.$out, this.conf)
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Render a delta's `attribution` dimension into a reserved `y-attributed-*` format namespace (forward)
|
|
319
|
+
* and strip it back out (reverse) — see the {@link module:delta/transformer/attribution-to-format
|
|
320
|
+
* module doc}. `conf` supplies per-dimension handlers (`insert`/`delete`/`format` for content ops,
|
|
321
|
+
* `attrs` for node-attribute provenance); each maps the op's attribution to the value placed under the
|
|
322
|
+
* namespaced format key (`null` clears it, `{}`/`undefined` leaves it unchanged).
|
|
323
|
+
*
|
|
324
|
+
* The transformer assumes every attribution-bearing op carries the **complete** attribution: the
|
|
325
|
+
* format dimension merges flat/wholesale, so it cannot replicate `attribution.format`'s deep merge,
|
|
326
|
+
* and an incremental update would overwrite a whole `y-attributed-attrs` map or `y-attributed-format`
|
|
327
|
+
* object. Returns a reusable {@link AttributionToFormat} template (`.init()` for a standalone
|
|
328
|
+
* transformer).
|
|
329
|
+
*
|
|
330
|
+
* @template {delta.DeltaConf} IN
|
|
331
|
+
* @param {import('../../schema.js').Schema<delta.Delta<IN>>} $d
|
|
332
|
+
* @param {Conf} conf
|
|
333
|
+
* @return {AttributionToFormat<IN>}
|
|
334
|
+
*/
|
|
335
|
+
export const attributionToFormat = ($d, conf) => new AttributionToFormat($d, conf)
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import * as delta from '../delta.js'
|
|
2
|
+
import * as math from '../../math.js'
|
|
3
|
+
import { Transformer, Template, createTransformResult } from './core.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Stateful transformer that descends one level into a node's child *nodes* and applies a per-child
|
|
7
|
+
* sub-transformer, chosen by `handler`. Attributes and text pass through untouched. Designed to be
|
|
8
|
+
* applied recursively (see {@link children}).
|
|
9
|
+
*
|
|
10
|
+
* The per-child sub-transformer instances are kept in {@link ChildrenTransformer#childTs}, positionally
|
|
11
|
+
* aligned to the parent's child list (children carry no stable id - identity is positional). `children`
|
|
12
|
+
* never changes the child count at its own level, so side A and side B stay aligned position-for-
|
|
13
|
+
* position and the same `childTs` indexes both directions.
|
|
14
|
+
*
|
|
15
|
+
* `childTs` is a sparse positional map kept as a delta: each transformed child is an `insert([t])` op
|
|
16
|
+
* holding its sub-transformer; every other position (text runs, opted-out nodes) is a coalesced
|
|
17
|
+
* `retain(n)` gap. A run of N text characters costs one retain op, not N array slots. It is walked with
|
|
18
|
+
* a forward cursor in step with the change (see {@link ChildrenTransformer#transform}). Transformer
|
|
19
|
+
* instances are carried by reference across rebuilds, so their per-instance state survives.
|
|
20
|
+
*
|
|
21
|
+
* @extends {Transformer<any,any>}
|
|
22
|
+
*/
|
|
23
|
+
export class ChildrenTransformer extends Transformer {
|
|
24
|
+
/**
|
|
25
|
+
* @param {import('../../schema.js').Schema<delta.Delta<any>>} $in
|
|
26
|
+
* @param {import('../../schema.js').Schema<delta.Delta<any>>} $out
|
|
27
|
+
* @param {(child: delta.DeltaAny, $child: import('../../schema.js').Schema<delta.DeltaAny>) => Template | null} handler
|
|
28
|
+
* picks the (schema-bound) template for a child node, or `null` to leave it untransformed. Evaluated
|
|
29
|
+
* once, when the child is inserted, with the derived per-child schema.
|
|
30
|
+
* @param {import('../../schema.js').Schema<delta.DeltaAny>} childSchema per-child-node schema each
|
|
31
|
+
* sub-template is built against (derived from the parent's input schema; `$deltaAny` when not derivable).
|
|
32
|
+
*/
|
|
33
|
+
constructor ($in, $out, handler, childSchema) {
|
|
34
|
+
super($in, $out)
|
|
35
|
+
this.handler = handler
|
|
36
|
+
/**
|
|
37
|
+
* Sparse positional map of sub-transformers: `insert([t])` at each transformed child, `retain(n)`
|
|
38
|
+
* over gaps (text and opted-out nodes).
|
|
39
|
+
*
|
|
40
|
+
* @type {delta.DeltaBuilderAny}
|
|
41
|
+
*/
|
|
42
|
+
this.childTs = delta.create()
|
|
43
|
+
this.childSchema = childSchema
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @param {delta.DeltaBuilderAny} d the change to map
|
|
48
|
+
* @param {boolean} fwd `true` maps side A -> B (applying sub-transformers' `applyA`), `false` maps
|
|
49
|
+
* side B -> A. `children` is 1:1 over children, so the same `childTs` indexes both directions.
|
|
50
|
+
* @return {import('./core.js').TransformResultAny}
|
|
51
|
+
*/
|
|
52
|
+
transform (d, fwd) {
|
|
53
|
+
// `cloneShallow` carries the node's name, attribute ops, and own (root) marks; only the child nodes
|
|
54
|
+
// are (re)built below, and nested children's marks ride on those rebuilt children. It is the shared
|
|
55
|
+
// mark-carrying primitive, so this node's marks cannot be silently dropped. `any` keeps `out` within
|
|
56
|
+
// the shared TS instantiation budget (see {@link import('./inline.js')}).
|
|
57
|
+
const out = /** @type {any} */ (delta.cloneShallow(d))
|
|
58
|
+
// Walk the old sparse map with a forward cursor while rebuilding a fresh one. The cursor tracks an
|
|
59
|
+
// old-side child position (list node + offset within it).
|
|
60
|
+
const newTs = delta.create()
|
|
61
|
+
let srcNode = this.childTs.children.start
|
|
62
|
+
let srcOff = 0
|
|
63
|
+
/**
|
|
64
|
+
* Copy `n` positions from the old map to `newTs`, advancing the cursor. Transformer references are
|
|
65
|
+
* preserved (the array slice is shallow), so their per-instance state survives the rebuild.
|
|
66
|
+
*
|
|
67
|
+
* @param {number} n
|
|
68
|
+
*/
|
|
69
|
+
const carry = n => {
|
|
70
|
+
let rem = n
|
|
71
|
+
while (rem > 0 && srcNode != null) {
|
|
72
|
+
const take = math.min(srcNode.length - srcOff, rem)
|
|
73
|
+
if (delta.$insertOp.check(srcNode)) {
|
|
74
|
+
newTs.insert(srcNode.insert.slice(srcOff, srcOff + take))
|
|
75
|
+
} else {
|
|
76
|
+
newTs.retain(take)
|
|
77
|
+
}
|
|
78
|
+
srcOff += take
|
|
79
|
+
rem -= take
|
|
80
|
+
if (srcOff >= srcNode.length) { srcNode = srcNode.next; srcOff = 0 }
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Advance the cursor `n` positions without copying (a delete drops those transformers).
|
|
85
|
+
*
|
|
86
|
+
* @param {number} n
|
|
87
|
+
*/
|
|
88
|
+
const drop = n => {
|
|
89
|
+
let rem = n
|
|
90
|
+
while (rem > 0 && srcNode != null) {
|
|
91
|
+
const take = math.min(srcNode.length - srcOff, rem)
|
|
92
|
+
srcOff += take
|
|
93
|
+
rem -= take
|
|
94
|
+
if (srcOff >= srcNode.length) { srcNode = srcNode.next; srcOff = 0 }
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// the sub-transformer at the cursor, or null for a gap (text / opted-out node)
|
|
98
|
+
const peek = () => (srcNode != null && delta.$insertOp.check(srcNode)) ? srcNode.insert[srcOff] : null
|
|
99
|
+
for (const op of d.children) {
|
|
100
|
+
if (delta.$retainOp.check(op)) {
|
|
101
|
+
out.retain(op.retain, op.format, op.attribution)
|
|
102
|
+
carry(op.retain)
|
|
103
|
+
} else if (delta.$textOp.check(op)) {
|
|
104
|
+
out.insert(op.insert, op.format, op.attribution)
|
|
105
|
+
newTs.retain(op.insert.length) // text is a gap - no sub-transformer
|
|
106
|
+
} else if (delta.$insertOp.check(op)) {
|
|
107
|
+
for (const el of op.insert) {
|
|
108
|
+
const tmpl = delta.$deltaAny.check(el) ? this.handler(el, this.childSchema) : null
|
|
109
|
+
if (tmpl != null) {
|
|
110
|
+
const t = tmpl.init()
|
|
111
|
+
const r = fwd ? t.applyA(el) : t.applyB(el)
|
|
112
|
+
out.insert([fwd ? r.b : r.a], op.format, op.attribution)
|
|
113
|
+
newTs.insert([t])
|
|
114
|
+
} else {
|
|
115
|
+
out.insert([el], op.format, op.attribution)
|
|
116
|
+
newTs.retain(1) // opted-out node is a gap
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
} else if (delta.$deleteOp.check(op)) {
|
|
120
|
+
out.delete(op.delete)
|
|
121
|
+
drop(op.delete)
|
|
122
|
+
} else if (delta.$modifyOp.check(op)) {
|
|
123
|
+
const t = peek()
|
|
124
|
+
if (t != null) {
|
|
125
|
+
// op.value is an immutable Delta view; transformers consume a DeltaBuilder, so clone it
|
|
126
|
+
const r = fwd ? t.applyA(delta.clone(op.value)) : t.applyB(delta.clone(op.value))
|
|
127
|
+
out.modify(fwd ? r.b : r.a, op.format, op.attribution)
|
|
128
|
+
} else {
|
|
129
|
+
out.modify(delta.clone(op.value), op.format, op.attribution)
|
|
130
|
+
}
|
|
131
|
+
carry(1)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// the change implicitly retains everything after its last op - carry those untouched positions'
|
|
135
|
+
// sub-transformers into the rebuilt map (out, being a change, needs no trailing retain)
|
|
136
|
+
carry(Infinity)
|
|
137
|
+
out.done(false)
|
|
138
|
+
this.childTs = newTs
|
|
139
|
+
return fwd ? createTransformResult(null, out) : createTransformResult(out, null)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* @param {delta.DeltaBuilderAny} da
|
|
144
|
+
*/
|
|
145
|
+
applyA (da) {
|
|
146
|
+
return this.transform(da, true)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* @param {delta.DeltaBuilderAny} db
|
|
151
|
+
*/
|
|
152
|
+
applyB (db) {
|
|
153
|
+
return this.transform(db, false)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Derive the per-child-node schema from the parent's input schema, so each child's sub-transformer is
|
|
159
|
+
* built against a concrete schema instead of `$deltaAny`. Falls back to `$deltaAny`.
|
|
160
|
+
*
|
|
161
|
+
* @param {import('../../schema.js').Schema<delta.DeltaAny>} $in
|
|
162
|
+
* @return {import('../../schema.js').Schema<delta.DeltaAny>}
|
|
163
|
+
*/
|
|
164
|
+
const childSchemaOf = ($in) => {
|
|
165
|
+
if (!delta.$$delta.check($in)) return delta.$deltaAny
|
|
166
|
+
if ($in.shape.recursiveChildren) return $in
|
|
167
|
+
const $ch = $in.shape.$children
|
|
168
|
+
return delta.$$delta.check($ch) ? $ch : delta.$deltaAny
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Template for {@link ChildrenTransformer}.
|
|
173
|
+
*
|
|
174
|
+
* @template {delta.DeltaConf} [IN=any]
|
|
175
|
+
* @extends {Template<IN, any>}
|
|
176
|
+
*/
|
|
177
|
+
export class Children extends Template {
|
|
178
|
+
/**
|
|
179
|
+
* @param {import('../../schema.js').Schema<delta.Delta<IN>>} $d
|
|
180
|
+
* @param {(child: delta.DeltaAny, $child: import('../../schema.js').Schema<delta.DeltaAny>) => Template | null} handler
|
|
181
|
+
*/
|
|
182
|
+
constructor ($d, handler) {
|
|
183
|
+
super($d, /** @type {any} */ (delta.$deltaAny))
|
|
184
|
+
this.handler = handler
|
|
185
|
+
/**
|
|
186
|
+
* @type {import('../../schema.js').Schema<delta.DeltaAny>}
|
|
187
|
+
*/
|
|
188
|
+
this.childSchema = childSchemaOf($d)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
get name () { return 'lib0:children' }
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* @return {Transformer<IN, any>}
|
|
195
|
+
*/
|
|
196
|
+
init () {
|
|
197
|
+
return new ChildrenTransformer(this.$in, this.$out, this.handler, this.childSchema)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Descend one level into a node's child *nodes* and apply a sub-transformer to each, chosen by
|
|
203
|
+
* `handler(childNode, $child)`: return a {@link Template} to transform that child, or `null` to leave
|
|
204
|
+
* it untransformed. The handler receives the derived per-child schema `$child` so it can build
|
|
205
|
+
* schema-bound sub-templates. Attributes and text pass through. The handler is evaluated once, when
|
|
206
|
+
* the child is inserted (returning `null` opts the child out permanently). Returns a reusable
|
|
207
|
+
* {@link Children} template (a `project` hole, or `.init()` for a standalone transformer).
|
|
208
|
+
*
|
|
209
|
+
* Composes recursively - e.g. inline every anonymous node at every depth:
|
|
210
|
+
* ```js
|
|
211
|
+
* const inlineAll = $d => children($d, (_c, $c) => pipe($c, $c1 => inline($c1, [null]), inlineAll))
|
|
212
|
+
* ```
|
|
213
|
+
*
|
|
214
|
+
* @template {delta.DeltaConf} IN
|
|
215
|
+
* @param {import('../../schema.js').Schema<delta.Delta<IN>>} $d
|
|
216
|
+
* @param {(child: delta.DeltaAny, $child: import('../../schema.js').Schema<delta.DeltaAny>) => Template | null} handler
|
|
217
|
+
* @return {Children<IN>}
|
|
218
|
+
*/
|
|
219
|
+
export const children = ($d, handler) => new Children($d, handler)
|