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,818 @@
|
|
|
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
|
+
* The (unexported) delta {@link import('../delta.js').createMark Mark} type, referenced here for the
|
|
7
|
+
* local mark-bucketing annotations.
|
|
8
|
+
*
|
|
9
|
+
* @typedef {ReturnType<typeof delta.createMark>} Mark
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* One entry of the segment layout maintained by {@link InlineTransformer}. A segment is either a
|
|
14
|
+
* coalesced run of pass-through positions (root text + opaque non-inlined nodes - same length in both
|
|
15
|
+
* coordinate spaces) or a single inline node (1 structured position that expands to `inlineLen`
|
|
16
|
+
* inlined positions).
|
|
17
|
+
*/
|
|
18
|
+
class Seg {
|
|
19
|
+
/**
|
|
20
|
+
* @param {boolean} isInline whether this seg is an inline node (its children are spliced into the parent)
|
|
21
|
+
* @param {number} structLen
|
|
22
|
+
* @param {number} inlineLen
|
|
23
|
+
*/
|
|
24
|
+
constructor (isInline, structLen, inlineLen) {
|
|
25
|
+
this.isInline = isInline
|
|
26
|
+
this.structLen = structLen
|
|
27
|
+
this.inlineLen = inlineLen
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Whether `el` is a child node that should be inlined - i.e. a node delta whose `name` is in the
|
|
33
|
+
* configured `names` set (use `null` to select anonymous/typeless nodes).
|
|
34
|
+
*
|
|
35
|
+
* @param {any} el
|
|
36
|
+
* @param {Array<string|null>} names
|
|
37
|
+
* @return {boolean}
|
|
38
|
+
*/
|
|
39
|
+
const isInlineNode = (el, names) => delta.$deltaAny.check(el) && names.includes(el.name)
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Whether any direct child of `d` is an inline node - the only thing this transformer inlines. Called
|
|
43
|
+
* on each incoming change to feed the cached {@link InlineTransformer#hasInline} flag, which drives
|
|
44
|
+
* the fast-path: while no inline node has ever appeared, the structured and inlined representations
|
|
45
|
+
* are identical and a change is its own transform. An inline node nested inside a non-inlined node is
|
|
46
|
+
* opaque (one level) and intentionally not counted.
|
|
47
|
+
*
|
|
48
|
+
* @param {delta.DeltaAny} d
|
|
49
|
+
* @param {Array<string|null>} names
|
|
50
|
+
* @return {boolean}
|
|
51
|
+
*/
|
|
52
|
+
const hasInlineChild = (d, names) => {
|
|
53
|
+
for (const op of d.children) {
|
|
54
|
+
if (delta.$insertOp.check(op)) {
|
|
55
|
+
for (const el of op.insert) {
|
|
56
|
+
if (isInlineNode(el, names)) return true
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return false
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Net change in a parent's direct child count from applying a settled child-change delta: positions
|
|
65
|
+
* inserted minus positions deleted (retain/modify keep the count). Lets an inline node's inlined
|
|
66
|
+
* length be kept current when a `modify` edits it, without re-deriving the node's children.
|
|
67
|
+
*
|
|
68
|
+
* @param {delta.DeltaAny} d
|
|
69
|
+
* @return {number}
|
|
70
|
+
*/
|
|
71
|
+
const netLen = d => {
|
|
72
|
+
let n = 0
|
|
73
|
+
for (const op of d.children) {
|
|
74
|
+
if (delta.$insertOp.check(op) || delta.$textOp.check(op)) {
|
|
75
|
+
n += op.insert.length
|
|
76
|
+
} else if (delta.$deleteOp.check(op)) {
|
|
77
|
+
n -= op.delete
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return n
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Apply a settled *structured* change to the segment layout `segs` in place, advancing it to the
|
|
85
|
+
* layout of the changed state without re-deriving the whole list. The cursor `(si, so)` is a
|
|
86
|
+
* structured position - seg index plus structural offset within that seg. The layout is kept
|
|
87
|
+
* coalesced (no two adjacent pass-through segs, no empty pass-through seg; every inline node is its
|
|
88
|
+
* own length-1 seg, kept even when its inlined length is 0) so it stays identical to a from-scratch
|
|
89
|
+
* derivation of the settled state. One level only: a direct inline node's length is `childCnt`; an
|
|
90
|
+
* inline node nested deeper is an opaque element of length 1.
|
|
91
|
+
*
|
|
92
|
+
* @param {Array<Seg>} segs
|
|
93
|
+
* @param {delta.DeltaAny} change
|
|
94
|
+
* @param {Array<string|null>} names the node names that are inlined (see {@link isInlineNode})
|
|
95
|
+
*/
|
|
96
|
+
const applySegsChange = (segs, change, names) => {
|
|
97
|
+
let si = 0
|
|
98
|
+
let so = 0
|
|
99
|
+
// Move the cursor onto a seg boundary (so === 0), splitting the current pass-through seg if the
|
|
100
|
+
// cursor sits in its interior. A null seg has structLen 1, so the cursor is never in its interior.
|
|
101
|
+
const ensureBoundary = () => {
|
|
102
|
+
if (so === 0) return
|
|
103
|
+
const s = segs[si]
|
|
104
|
+
segs.splice(si + 1, 0, new Seg(false, s.structLen - so, s.inlineLen - so))
|
|
105
|
+
s.structLen = so
|
|
106
|
+
s.inlineLen = so
|
|
107
|
+
si++
|
|
108
|
+
so = 0
|
|
109
|
+
}
|
|
110
|
+
for (const op of change.children) {
|
|
111
|
+
if (delta.$retainOp.check(op)) {
|
|
112
|
+
// formats on a retain never change the layout - just advance the cursor
|
|
113
|
+
let rem = op.retain
|
|
114
|
+
while (rem > 0 && si < segs.length) {
|
|
115
|
+
const room = segs[si].structLen - so
|
|
116
|
+
if (rem < room) {
|
|
117
|
+
so += rem
|
|
118
|
+
rem = 0
|
|
119
|
+
} else {
|
|
120
|
+
rem -= room
|
|
121
|
+
si++
|
|
122
|
+
so = 0
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
} else if (delta.$textOp.check(op) || delta.$insertOp.check(op)) {
|
|
126
|
+
// Build the inserted layout: a text op is one pass-through run; an insert op is a mix of
|
|
127
|
+
// length-1 null segs and pass-through runs that coalesce the opaque (named) elements.
|
|
128
|
+
/** @type {Array<Seg>} */
|
|
129
|
+
const ins = []
|
|
130
|
+
if (delta.$textOp.check(op)) {
|
|
131
|
+
ins.push(new Seg(false, op.insert.length, op.insert.length))
|
|
132
|
+
} else {
|
|
133
|
+
for (const el of op.insert) {
|
|
134
|
+
const last = ins[ins.length - 1]
|
|
135
|
+
if (isInlineNode(el, names)) {
|
|
136
|
+
ins.push(new Seg(true, 1, /** @type {delta.DeltaAny} */ (el).childCnt))
|
|
137
|
+
} else if (last !== undefined && !last.isInline) {
|
|
138
|
+
last.structLen++
|
|
139
|
+
last.inlineLen++
|
|
140
|
+
} else {
|
|
141
|
+
ins.push(new Seg(false, 1, 1))
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (ins.length === 1 && !ins[0].isInline) {
|
|
146
|
+
// fast path - pure pass-through content grows an adjacent run (no split); the common case
|
|
147
|
+
const len = ins[0].structLen
|
|
148
|
+
if (so > 0) {
|
|
149
|
+
segs[si].structLen += len
|
|
150
|
+
segs[si].inlineLen += len
|
|
151
|
+
so += len
|
|
152
|
+
} else if (si > 0 && !segs[si - 1].isInline) {
|
|
153
|
+
segs[si - 1].structLen += len
|
|
154
|
+
segs[si - 1].inlineLen += len
|
|
155
|
+
} else if (si < segs.length && !segs[si].isInline) {
|
|
156
|
+
segs[si].structLen += len
|
|
157
|
+
segs[si].inlineLen += len
|
|
158
|
+
so = len
|
|
159
|
+
} else {
|
|
160
|
+
segs.splice(si, 0, ins[0])
|
|
161
|
+
si++
|
|
162
|
+
}
|
|
163
|
+
} else {
|
|
164
|
+
// general case (the content contains an inline node): land on a boundary, then coalesce the
|
|
165
|
+
// block's pass-through ends into the surrounding runs
|
|
166
|
+
ensureBoundary()
|
|
167
|
+
let block = ins
|
|
168
|
+
if (si > 0 && !segs[si - 1].isInline && !block[0].isInline) {
|
|
169
|
+
segs[si - 1].structLen += block[0].structLen
|
|
170
|
+
segs[si - 1].inlineLen += block[0].inlineLen
|
|
171
|
+
block = block.slice(1)
|
|
172
|
+
}
|
|
173
|
+
let tail = 0
|
|
174
|
+
if (si < segs.length && !segs[si].isInline && block.length > 0 && !block[block.length - 1].isInline) {
|
|
175
|
+
const lastSeg = block[block.length - 1]
|
|
176
|
+
tail = lastSeg.structLen
|
|
177
|
+
segs[si].structLen += lastSeg.structLen
|
|
178
|
+
segs[si].inlineLen += lastSeg.inlineLen
|
|
179
|
+
block = block.slice(0, -1)
|
|
180
|
+
}
|
|
181
|
+
segs.splice(si, 0, ...block)
|
|
182
|
+
si += block.length
|
|
183
|
+
so = tail
|
|
184
|
+
}
|
|
185
|
+
} else if (delta.$deleteOp.check(op)) {
|
|
186
|
+
ensureBoundary()
|
|
187
|
+
let rem = op.delete
|
|
188
|
+
while (rem > 0 && si < segs.length) {
|
|
189
|
+
const s = segs[si]
|
|
190
|
+
if (s.isInline) {
|
|
191
|
+
// the single structured position is the whole inline node
|
|
192
|
+
segs.splice(si, 1)
|
|
193
|
+
rem -= 1
|
|
194
|
+
} else {
|
|
195
|
+
const take = math.min(s.structLen, rem)
|
|
196
|
+
s.structLen -= take
|
|
197
|
+
s.inlineLen -= take
|
|
198
|
+
rem -= take
|
|
199
|
+
if (s.structLen === 0) segs.splice(si, 1)
|
|
200
|
+
// a partial delete leaves rem 0 and the shortened seg in place
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// a deletion can bring two pass-through runs together - coalesce, keeping the cursor on the seam
|
|
204
|
+
if (si > 0 && si < segs.length && !segs[si - 1].isInline && !segs[si].isInline) {
|
|
205
|
+
const leftLen = segs[si - 1].structLen
|
|
206
|
+
segs[si - 1].structLen += segs[si].structLen
|
|
207
|
+
segs[si - 1].inlineLen += segs[si].inlineLen
|
|
208
|
+
segs.splice(si, 1)
|
|
209
|
+
si--
|
|
210
|
+
so = leftLen
|
|
211
|
+
}
|
|
212
|
+
} else if (delta.$modifyOp.check(op)) {
|
|
213
|
+
if (si < segs.length) {
|
|
214
|
+
const s = segs[si]
|
|
215
|
+
if (so === 0 && s.isInline) {
|
|
216
|
+
// editing inside an inline node changes only its inlined length
|
|
217
|
+
s.inlineLen += netLen(op.value)
|
|
218
|
+
si++
|
|
219
|
+
} else {
|
|
220
|
+
// an opaque (named) node - no layout change; step over its single structured position
|
|
221
|
+
so++
|
|
222
|
+
if (so >= s.structLen) {
|
|
223
|
+
si++
|
|
224
|
+
so = 0
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Map a structured root-mark offset to its inlined offset through the seg layout. A mark on a
|
|
234
|
+
* pass-through position keeps its in-run offset; a mark on an inline node's own structured position
|
|
235
|
+
* maps to that node's inlined start (its `childStart`). A key past the end clamps to the inlined end
|
|
236
|
+
* (a change-delta root mark may sit at the trailing implicit-retain boundary).
|
|
237
|
+
*
|
|
238
|
+
* @param {Array<Seg>} segs
|
|
239
|
+
* @param {number} key
|
|
240
|
+
* @return {number}
|
|
241
|
+
*/
|
|
242
|
+
const structOffsetToInlineOffset = (segs, key) => {
|
|
243
|
+
let s = 0
|
|
244
|
+
let inl = 0
|
|
245
|
+
for (const seg of segs) {
|
|
246
|
+
if (key < s + seg.structLen) return seg.isInline ? inl : inl + (key - s)
|
|
247
|
+
s += seg.structLen
|
|
248
|
+
inl += seg.inlineLen
|
|
249
|
+
}
|
|
250
|
+
return inl
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Map an inlined root-mark offset to a structured location. `inner` is `null` for a pass-through
|
|
255
|
+
* position (the mark is a structured root mark at `struct`); otherwise the offset fell inside the
|
|
256
|
+
* inline node at structured position `struct` (seg index `si`), and `inner` is the offset within that
|
|
257
|
+
* node. A key past the end clamps to the structured end.
|
|
258
|
+
*
|
|
259
|
+
* @param {Array<Seg>} segs
|
|
260
|
+
* @param {number} key
|
|
261
|
+
* @return {{ struct: number, inner: number|null, si: number }}
|
|
262
|
+
*/
|
|
263
|
+
const inlineOffsetToStructLocation = (segs, key) => {
|
|
264
|
+
let s = 0
|
|
265
|
+
let inl = 0
|
|
266
|
+
for (let si = 0; si < segs.length; si++) {
|
|
267
|
+
const seg = segs[si]
|
|
268
|
+
if (key < inl + seg.inlineLen) {
|
|
269
|
+
return seg.isInline ? { struct: s, inner: key - inl, si } : { struct: s + (key - inl), inner: null, si }
|
|
270
|
+
}
|
|
271
|
+
s += seg.structLen
|
|
272
|
+
inl += seg.inlineLen
|
|
273
|
+
}
|
|
274
|
+
return { struct: s, inner: null, si: segs.length }
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Build an output delta that passes through the node name and attribute ops of `src` (only the
|
|
279
|
+
* children are transformed by this transformer).
|
|
280
|
+
*
|
|
281
|
+
* reason: returns `any` rather than `DeltaBuilderAny` on purpose. The result is internal scratch the
|
|
282
|
+
* transformer mutates with fluent builder calls; typing it would instantiate a `DeltaConfOverwrite`
|
|
283
|
+
* per call, and across this file that tips the shared TS2589 instantiation budget that the long
|
|
284
|
+
* pipe-type test (delta-transformer.test.js) sits against. The public output type lives on `init`.
|
|
285
|
+
*
|
|
286
|
+
* @param {delta.DeltaAny} src
|
|
287
|
+
* @return {any}
|
|
288
|
+
*/
|
|
289
|
+
const passThrough = src => {
|
|
290
|
+
const d = delta.create(/** @type {any} */ (src.name))
|
|
291
|
+
for (const op of src.attrs) {
|
|
292
|
+
// reason: `attrs` is a mapped type over the conf's attr keys, so a dynamic-key write can't be
|
|
293
|
+
// checked (same limitation delta.slice suppresses when copying attrs).
|
|
294
|
+
// @ts-ignore
|
|
295
|
+
d.attrs[op.key] = op.clone()
|
|
296
|
+
}
|
|
297
|
+
// carry the conservative mark flag (covers attr-value marks the direct assignment above bypasses);
|
|
298
|
+
// marks carried into the rebuilt children below flag it via the builder / addRootMark
|
|
299
|
+
d.maybeHasMarks = src.maybeHasMarks
|
|
300
|
+
return d
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Stateful transformer that inlines child nodes whose name is in the configured `names` set. Side A
|
|
305
|
+
* is the structured representation (inline nodes present, e.g. `<p>some<>text</></p>`), side B is the
|
|
306
|
+
* inlined representation (inline nodes flattened, e.g. `<p>sometext</p>`).
|
|
307
|
+
*
|
|
308
|
+
* The state the user calls "offsets and inline node length" is the segment layout {@link Seg} stored
|
|
309
|
+
* in `segs`: a coalesced run-length view of the structured side where each inline node is one
|
|
310
|
+
* structured position expanding to `inlineLen` inlined positions, and every other position (root text
|
|
311
|
+
* and opaque non-inlined nodes) is a pass-through run. It is kept current incrementally - every mapped
|
|
312
|
+
* change is folded into `segs` by {@link applySegsChange} rather than re-derived from scratch.
|
|
313
|
+
*
|
|
314
|
+
* @extends {Transformer<any,any>}
|
|
315
|
+
*/
|
|
316
|
+
export class InlineTransformer extends Transformer {
|
|
317
|
+
/**
|
|
318
|
+
* @param {import('../../schema.js').Schema<delta.Delta<any>>} $in
|
|
319
|
+
* @param {import('../../schema.js').Schema<delta.Delta<any>>} $out
|
|
320
|
+
* @param {Array<string|null>} names the node names to inline (`null` selects anonymous nodes)
|
|
321
|
+
*/
|
|
322
|
+
constructor ($in, $out, names) {
|
|
323
|
+
super($in, $out)
|
|
324
|
+
/**
|
|
325
|
+
* @type {Array<string|null>}
|
|
326
|
+
*/
|
|
327
|
+
this.names = names
|
|
328
|
+
/**
|
|
329
|
+
* The segment layout of the structured side, maintained incrementally (see {@link applySegsChange}).
|
|
330
|
+
*
|
|
331
|
+
* @type {Array<Seg>}
|
|
332
|
+
*/
|
|
333
|
+
this.segs = []
|
|
334
|
+
/**
|
|
335
|
+
* Whether the structured side has ever contained a direct inline-node child. Monotonic: once a
|
|
336
|
+
* change introduces an inline node it stays set (even if that node is later deleted), which only
|
|
337
|
+
* costs a missed fast-path, never correctness.
|
|
338
|
+
*
|
|
339
|
+
* @type {boolean}
|
|
340
|
+
*/
|
|
341
|
+
this.hasInline = false
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* structured change -> inlined change
|
|
346
|
+
*
|
|
347
|
+
* @param {delta.DeltaBuilderAny} da
|
|
348
|
+
*/
|
|
349
|
+
applyA (da) {
|
|
350
|
+
this.hasInline ||= hasInlineChild(da, this.names)
|
|
351
|
+
if (!this.hasInline) {
|
|
352
|
+
// fast-path: nothing to inline now or after this change - structured === inlined, so the
|
|
353
|
+
// change passes through unchanged. Keep the layout current and hand back the same delta.
|
|
354
|
+
applySegsChange(this.segs, da, this.names)
|
|
355
|
+
return createTransformResult(null, da)
|
|
356
|
+
}
|
|
357
|
+
const segs = this.segs
|
|
358
|
+
const b = passThrough(da)
|
|
359
|
+
let si = 0
|
|
360
|
+
let so = 0
|
|
361
|
+
let srcInline = 0
|
|
362
|
+
let emitted = 0
|
|
363
|
+
// position in the resulting inlined doc that `b` has built up to (retain + insert advance it,
|
|
364
|
+
// delete does not). Used to anchor marks lifted out of a spliced inline node (A.3).
|
|
365
|
+
let outPos = 0
|
|
366
|
+
/**
|
|
367
|
+
* @param {number} target
|
|
368
|
+
*/
|
|
369
|
+
const alignB = target => {
|
|
370
|
+
if (target > emitted) {
|
|
371
|
+
b.retain(target - emitted)
|
|
372
|
+
outPos += target - emitted
|
|
373
|
+
emitted = target
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Advance the structured cursor by `n` positions, returning the inlined length spanned.
|
|
378
|
+
*
|
|
379
|
+
* @param {number} n
|
|
380
|
+
*/
|
|
381
|
+
const advanceStruct = n => {
|
|
382
|
+
let rem = n
|
|
383
|
+
let inl = 0
|
|
384
|
+
while (rem > 0 && si < segs.length) {
|
|
385
|
+
const s = segs[si]
|
|
386
|
+
const take = math.min(s.structLen - so, rem)
|
|
387
|
+
inl += s.isInline ? s.inlineLen : take
|
|
388
|
+
so += take
|
|
389
|
+
rem -= take
|
|
390
|
+
if (so >= s.structLen) {
|
|
391
|
+
si++
|
|
392
|
+
so = 0
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return inl
|
|
396
|
+
}
|
|
397
|
+
for (const op of da.children) {
|
|
398
|
+
if (delta.$retainOp.check(op)) {
|
|
399
|
+
if (op.format === undefined && op.attribution === undefined) {
|
|
400
|
+
srcInline += advanceStruct(op.retain)
|
|
401
|
+
} else {
|
|
402
|
+
// formatted retain: emit the format over the mapped inline range. An inline node's wrapper
|
|
403
|
+
// has no inlined representation, so its format is dropped (consistent with the inliner); root
|
|
404
|
+
// text and opaque nodes carry the format straight through.
|
|
405
|
+
alignB(srcInline)
|
|
406
|
+
let rem = op.retain
|
|
407
|
+
while (rem > 0 && si < segs.length) {
|
|
408
|
+
const s = segs[si]
|
|
409
|
+
const take = math.min(s.structLen - so, rem)
|
|
410
|
+
const inl = s.isInline ? s.inlineLen : take
|
|
411
|
+
b.retain(inl, s.isInline ? undefined : op.format, s.isInline ? undefined : op.attribution)
|
|
412
|
+
emitted += inl
|
|
413
|
+
outPos += inl
|
|
414
|
+
srcInline += inl
|
|
415
|
+
so += take
|
|
416
|
+
rem -= take
|
|
417
|
+
if (so >= s.structLen) { si++; so = 0 }
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
} else if (delta.$textOp.check(op)) {
|
|
421
|
+
alignB(srcInline)
|
|
422
|
+
b.insert(op.insert, op.format, op.attribution)
|
|
423
|
+
outPos += op.insert.length
|
|
424
|
+
} else if (delta.$insertOp.check(op)) {
|
|
425
|
+
alignB(srcInline)
|
|
426
|
+
for (const el of op.insert) {
|
|
427
|
+
if (isInlineNode(el, this.names)) {
|
|
428
|
+
// splice the inline node's children verbatim (one level), then lift the node's own root
|
|
429
|
+
// marks onto the flattened parent at `childStart + innerKey` (A.3). String-keyed (attr)
|
|
430
|
+
// marks on the inline node have no home in the flattened parent and are dropped.
|
|
431
|
+
const node = /** @type {delta.DeltaAny} */ (el)
|
|
432
|
+
const base = outPos
|
|
433
|
+
b.append(node)
|
|
434
|
+
outPos += node.childCnt
|
|
435
|
+
delta.mergeRootMarks(b, node, k => typeof k === 'number' ? base + k : null)
|
|
436
|
+
} else {
|
|
437
|
+
b.insert([el], op.format, op.attribution)
|
|
438
|
+
outPos += 1
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
} else if (delta.$deleteOp.check(op)) {
|
|
442
|
+
alignB(srcInline)
|
|
443
|
+
const delInline = advanceStruct(op.delete)
|
|
444
|
+
b.delete(delInline)
|
|
445
|
+
srcInline += delInline
|
|
446
|
+
emitted += delInline
|
|
447
|
+
} else if (delta.$modifyOp.check(op)) {
|
|
448
|
+
const s = segs[si]
|
|
449
|
+
alignB(srcInline)
|
|
450
|
+
if (s !== undefined && s.isInline) {
|
|
451
|
+
// data side edited inside an inline node: its inner ops act on the node's children, which are
|
|
452
|
+
// spliced into b at the same offset - replay them directly
|
|
453
|
+
const base = outPos
|
|
454
|
+
for (const inner of op.value.children) {
|
|
455
|
+
if (delta.$retainOp.check(inner)) {
|
|
456
|
+
b.retain(inner.retain, inner.format, inner.attribution)
|
|
457
|
+
emitted += inner.retain
|
|
458
|
+
outPos += inner.retain
|
|
459
|
+
} else if (delta.$textOp.check(inner) || delta.$insertOp.check(inner)) {
|
|
460
|
+
b.insert(inner.insert, inner.format, inner.attribution)
|
|
461
|
+
outPos += inner.insert.length
|
|
462
|
+
} else if (delta.$deleteOp.check(inner)) {
|
|
463
|
+
b.delete(inner.delete)
|
|
464
|
+
emitted += inner.delete
|
|
465
|
+
} else if (delta.$modifyOp.check(inner)) {
|
|
466
|
+
b.modify(delta.clone(inner.value), inner.format, inner.attribution)
|
|
467
|
+
emitted += 1
|
|
468
|
+
outPos += 1
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
// marks set inside the inline node by this change lift onto the flattened parent (A.3)
|
|
472
|
+
delta.mergeRootMarks(b, op.value, k => typeof k === 'number' ? base + k : null)
|
|
473
|
+
srcInline += s.inlineLen
|
|
474
|
+
advanceStruct(1)
|
|
475
|
+
} else {
|
|
476
|
+
// opaque (named) node: forward the modify verbatim
|
|
477
|
+
b.modify(delta.clone(op.value), op.format, op.attribution)
|
|
478
|
+
emitted += 1
|
|
479
|
+
outPos += 1
|
|
480
|
+
srcInline += advanceStruct(1)
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
applySegsChange(this.segs, da, this.names)
|
|
485
|
+
// carry the change's own (root) marks, re-anchoring number offsets through the post-change layout;
|
|
486
|
+
// attr-key marks pass through unchanged. deleteMarks ride verbatim (merged with any lifted above).
|
|
487
|
+
delta.mergeRootMarks(b, da, k => typeof k === 'number' ? structOffsetToInlineOffset(this.segs, k) : k)
|
|
488
|
+
b.done(false)
|
|
489
|
+
return createTransformResult(null, b)
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* inlined change -> structured change
|
|
494
|
+
*
|
|
495
|
+
* @param {delta.DeltaBuilderAny} db
|
|
496
|
+
*/
|
|
497
|
+
applyB (db) {
|
|
498
|
+
this.hasInline ||= hasInlineChild(db, this.names)
|
|
499
|
+
if (!this.hasInline) {
|
|
500
|
+
// fast-path: with no inline nodes the inlined and structured coordinate spaces coincide, so the
|
|
501
|
+
// inlined change is already the structured change.
|
|
502
|
+
applySegsChange(this.segs, db, this.names)
|
|
503
|
+
return createTransformResult(db, null)
|
|
504
|
+
}
|
|
505
|
+
const segs = this.segs
|
|
506
|
+
const a = passThrough(db)
|
|
507
|
+
// --- marks: bucket the change's own (root) marks against the (pre-change) layout. A pass-through
|
|
508
|
+
// offset (or an attr-key mark) becomes a structured root mark; an offset inside an inline node is
|
|
509
|
+
// routed into that node - seeded into its modify when the walk opens one (below), else emitted as a
|
|
510
|
+
// mark-only modify (pure cursor move) or collapsed to the node position (mixed change) after the
|
|
511
|
+
// walk. NB: a change that simultaneously restructures content and moves a cursor maps the cursor in
|
|
512
|
+
// the pre-change layout (best-effort - marks are local/ephemeral).
|
|
513
|
+
/** @type {Array<Mark>} */
|
|
514
|
+
const passMarks = []
|
|
515
|
+
/** @type {Map<number, Array<{ innerKey: number, mark: Mark }>>} */
|
|
516
|
+
const innerBySi = new Map()
|
|
517
|
+
if (db.marks !== null) {
|
|
518
|
+
for (const m of db.marks) {
|
|
519
|
+
if (typeof m.key !== 'number') { passMarks.push(m); continue }
|
|
520
|
+
const r = inlineOffsetToStructLocation(segs, m.key)
|
|
521
|
+
if (r.inner === null) {
|
|
522
|
+
passMarks.push(m.copy(r.struct))
|
|
523
|
+
} else {
|
|
524
|
+
const arr = innerBySi.get(r.si) ?? []
|
|
525
|
+
arr.push({ innerKey: r.inner, mark: m })
|
|
526
|
+
innerBySi.set(r.si, arr)
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
// structured start offset of each seg (pre-change), for placing inner marks after the walk
|
|
531
|
+
/** @type {Array<number>} */
|
|
532
|
+
const segStart = []
|
|
533
|
+
for (let i = 0, acc = 0; i < segs.length; i++) { segStart.push(acc); acc += segs[i].structLen }
|
|
534
|
+
let hadChildOps = false
|
|
535
|
+
let si = 0
|
|
536
|
+
let so = 0
|
|
537
|
+
let structBefore = 0
|
|
538
|
+
let emittedStruct = 0
|
|
539
|
+
// An accumulator for edits that fall inside the current inline node. Several inlined ops can
|
|
540
|
+
// target the same inline node, so they must coalesce into a single `modify` op. `openSegIdx` is
|
|
541
|
+
// the seg index it belongs to, or -1 when there is no open modify.
|
|
542
|
+
let openInner = /** @type {any} */ (delta.create())
|
|
543
|
+
let openNodeStruct = 0
|
|
544
|
+
let openInnerEmitted = 0
|
|
545
|
+
let openSegIdx = -1
|
|
546
|
+
/**
|
|
547
|
+
* @param {number} target
|
|
548
|
+
*/
|
|
549
|
+
const alignA = target => {
|
|
550
|
+
if (target > emittedStruct) {
|
|
551
|
+
a.retain(target - emittedStruct)
|
|
552
|
+
emittedStruct = target
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
const flushModify = () => {
|
|
556
|
+
if (openSegIdx !== -1) {
|
|
557
|
+
alignA(openNodeStruct)
|
|
558
|
+
a.modify(openInner.done(false))
|
|
559
|
+
emittedStruct += 1
|
|
560
|
+
openSegIdx = -1
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* @param {number} target inlined offset within the open inline node
|
|
565
|
+
*/
|
|
566
|
+
const alignInner = target => {
|
|
567
|
+
if (target > openInnerEmitted) {
|
|
568
|
+
openInner.retain(target - openInnerEmitted)
|
|
569
|
+
openInnerEmitted = target
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* @param {number} idx
|
|
574
|
+
*/
|
|
575
|
+
const openModifyFor = idx => {
|
|
576
|
+
openInner = /** @type {any} */ (delta.create())
|
|
577
|
+
openNodeStruct = structBefore
|
|
578
|
+
openInnerEmitted = 0
|
|
579
|
+
openSegIdx = idx
|
|
580
|
+
// seed any inner marks bound to this node so they ride on the modify's value (A.3, reverse)
|
|
581
|
+
const im = innerBySi.get(idx)
|
|
582
|
+
if (im !== undefined) {
|
|
583
|
+
for (const { innerKey, mark } of im) delta.addRootMark(openInner, mark.copy(innerKey))
|
|
584
|
+
innerBySi.delete(idx) // consumed - excluded from the post-walk fallback
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
// empty inline nodes (inlineLen 0) are zero-width in inlined coords - step over them. Only at a
|
|
588
|
+
// seg boundary (so === 0); `so` does not change while skipping.
|
|
589
|
+
const skipEmpty = () => {
|
|
590
|
+
if (so !== 0) return
|
|
591
|
+
while (si < segs.length && segs[si].isInline && segs[si].inlineLen === 0) {
|
|
592
|
+
structBefore += segs[si].structLen
|
|
593
|
+
si++
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
for (const op of db.children) {
|
|
597
|
+
hadChildOps = true
|
|
598
|
+
if (delta.$retainOp.check(op)) {
|
|
599
|
+
const fmt = op.format
|
|
600
|
+
const attr = op.attribution
|
|
601
|
+
let rem = op.retain
|
|
602
|
+
while (rem > 0) {
|
|
603
|
+
skipEmpty()
|
|
604
|
+
if (si >= segs.length) break
|
|
605
|
+
const s = segs[si]
|
|
606
|
+
const take = math.min(s.inlineLen - so, rem)
|
|
607
|
+
if (fmt !== undefined || attr !== undefined) {
|
|
608
|
+
// a formatted retain re-formats existing content: pass-through content takes it
|
|
609
|
+
// directly; content inside an inline node is reached through a modify on that node.
|
|
610
|
+
if (!s.isInline) {
|
|
611
|
+
flushModify()
|
|
612
|
+
alignA(structBefore + so)
|
|
613
|
+
a.retain(take, fmt, attr)
|
|
614
|
+
emittedStruct += take
|
|
615
|
+
} else {
|
|
616
|
+
if (openSegIdx !== si) {
|
|
617
|
+
flushModify()
|
|
618
|
+
openModifyFor(si)
|
|
619
|
+
}
|
|
620
|
+
alignInner(so)
|
|
621
|
+
openInner.retain(take, fmt, attr)
|
|
622
|
+
openInnerEmitted += take
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
so += take
|
|
626
|
+
rem -= take
|
|
627
|
+
if (so >= s.inlineLen) {
|
|
628
|
+
structBefore += s.structLen
|
|
629
|
+
si++
|
|
630
|
+
so = 0
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
} else if (delta.$textOp.check(op) || delta.$insertOp.check(op)) {
|
|
634
|
+
skipEmpty()
|
|
635
|
+
const s = segs[si]
|
|
636
|
+
if (s === undefined) {
|
|
637
|
+
flushModify()
|
|
638
|
+
alignA(structBefore)
|
|
639
|
+
a.insert(op.insert, op.format, op.attribution)
|
|
640
|
+
} else if (!s.isInline) {
|
|
641
|
+
flushModify()
|
|
642
|
+
alignA(structBefore + so)
|
|
643
|
+
a.insert(op.insert, op.format, op.attribution)
|
|
644
|
+
} else if (so === 0) {
|
|
645
|
+
// boundary preference: an insert at the start of an inline node lands in the root
|
|
646
|
+
flushModify()
|
|
647
|
+
alignA(structBefore)
|
|
648
|
+
a.insert(op.insert, op.format, op.attribution)
|
|
649
|
+
} else {
|
|
650
|
+
// strict interior: insert into the inline node
|
|
651
|
+
if (openSegIdx !== si) {
|
|
652
|
+
flushModify()
|
|
653
|
+
openModifyFor(si)
|
|
654
|
+
}
|
|
655
|
+
alignInner(so)
|
|
656
|
+
openInner.insert(op.insert, op.format, op.attribution)
|
|
657
|
+
}
|
|
658
|
+
// an insert does not advance the cursor (it adds content not in the original)
|
|
659
|
+
} else if (delta.$deleteOp.check(op)) {
|
|
660
|
+
let rem = op.delete
|
|
661
|
+
while (rem > 0) {
|
|
662
|
+
skipEmpty()
|
|
663
|
+
if (si >= segs.length) break
|
|
664
|
+
const s = segs[si]
|
|
665
|
+
if (!s.isInline) {
|
|
666
|
+
const k = math.min(s.inlineLen - so, rem)
|
|
667
|
+
flushModify()
|
|
668
|
+
alignA(structBefore + so)
|
|
669
|
+
a.delete(k)
|
|
670
|
+
emittedStruct += k
|
|
671
|
+
so += k
|
|
672
|
+
rem -= k
|
|
673
|
+
if (so >= s.inlineLen) {
|
|
674
|
+
structBefore += s.structLen
|
|
675
|
+
si++
|
|
676
|
+
so = 0
|
|
677
|
+
}
|
|
678
|
+
} else if (so === 0 && rem >= s.inlineLen) {
|
|
679
|
+
// the delete covers the whole node from its start - delete it structurally
|
|
680
|
+
flushModify()
|
|
681
|
+
alignA(structBefore)
|
|
682
|
+
a.delete(1)
|
|
683
|
+
emittedStruct += 1
|
|
684
|
+
rem -= s.inlineLen
|
|
685
|
+
structBefore += s.structLen
|
|
686
|
+
si++
|
|
687
|
+
so = 0
|
|
688
|
+
} else {
|
|
689
|
+
// strict interior delete: remove inside the inline node
|
|
690
|
+
const k = math.min(s.inlineLen - so, rem)
|
|
691
|
+
if (openSegIdx !== si) {
|
|
692
|
+
flushModify()
|
|
693
|
+
openModifyFor(si)
|
|
694
|
+
}
|
|
695
|
+
alignInner(so)
|
|
696
|
+
openInner.delete(k)
|
|
697
|
+
openInnerEmitted += k
|
|
698
|
+
so += k
|
|
699
|
+
rem -= k
|
|
700
|
+
if (so >= s.inlineLen) {
|
|
701
|
+
structBefore += s.structLen
|
|
702
|
+
si++
|
|
703
|
+
so = 0
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
} else if (delta.$modifyOp.check(op)) {
|
|
708
|
+
skipEmpty()
|
|
709
|
+
const s = segs[si]
|
|
710
|
+
if (s !== undefined && s.isInline) {
|
|
711
|
+
// modify targets an element inside the inline node
|
|
712
|
+
if (openSegIdx !== si) {
|
|
713
|
+
flushModify()
|
|
714
|
+
openModifyFor(si)
|
|
715
|
+
}
|
|
716
|
+
alignInner(so)
|
|
717
|
+
openInner.modify(delta.clone(op.value), op.format, op.attribution)
|
|
718
|
+
openInnerEmitted += 1
|
|
719
|
+
so += 1
|
|
720
|
+
if (so >= s.inlineLen) {
|
|
721
|
+
structBefore += s.structLen
|
|
722
|
+
si++
|
|
723
|
+
so = 0
|
|
724
|
+
}
|
|
725
|
+
} else if (s !== undefined) {
|
|
726
|
+
flushModify()
|
|
727
|
+
alignA(structBefore + so)
|
|
728
|
+
a.modify(delta.clone(op.value), op.format, op.attribution)
|
|
729
|
+
emittedStruct += 1
|
|
730
|
+
so += 1
|
|
731
|
+
if (so >= s.inlineLen) {
|
|
732
|
+
structBefore += s.structLen
|
|
733
|
+
si++
|
|
734
|
+
so = 0
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
flushModify()
|
|
740
|
+
// place the change's root marks. Pass-through / attr-key marks are structured root marks. Inner
|
|
741
|
+
// marks not consumed by a modify above are emitted as exact mark-only modifies for a pure cursor
|
|
742
|
+
// move, else collapsed to the inline node's structured position (best-effort).
|
|
743
|
+
for (const m of passMarks) delta.addRootMark(a, m)
|
|
744
|
+
if (innerBySi.size > 0) {
|
|
745
|
+
if (!hadChildOps) {
|
|
746
|
+
let cursor = emittedStruct
|
|
747
|
+
for (const [segIdx, marks] of [...innerBySi.entries()].sort((x, y) => x[0] - y[0])) {
|
|
748
|
+
if (segStart[segIdx] > cursor) { a.retain(segStart[segIdx] - cursor); cursor = segStart[segIdx] }
|
|
749
|
+
const mod = /** @type {any} */ (delta.create())
|
|
750
|
+
for (const { innerKey, mark } of marks) delta.addRootMark(mod, mark.copy(innerKey))
|
|
751
|
+
a.modify(mod.done(false))
|
|
752
|
+
cursor += 1
|
|
753
|
+
}
|
|
754
|
+
} else {
|
|
755
|
+
for (const [segIdx, marks] of innerBySi) {
|
|
756
|
+
for (const { mark } of marks) delta.addRootMark(a, mark.copy(segStart[segIdx]))
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
if (db.deleteMarks !== null) {
|
|
761
|
+
for (const id of db.deleteMarks) delta.deleteRootMark(a, id)
|
|
762
|
+
}
|
|
763
|
+
applySegsChange(this.segs, a, this.names)
|
|
764
|
+
a.done(false)
|
|
765
|
+
return createTransformResult(a, null)
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* Template for {@link InlineTransformer}. Typed loosely: the inlined output shape depends on the
|
|
771
|
+
* runtime `names` and document content, so the OUT side is `any`.
|
|
772
|
+
*
|
|
773
|
+
* @template {delta.DeltaConf} [IN=any]
|
|
774
|
+
* @extends {Template<IN, any>}
|
|
775
|
+
*/
|
|
776
|
+
export class Inline extends Template {
|
|
777
|
+
/**
|
|
778
|
+
* @param {import('../../schema.js').Schema<delta.Delta<IN>>} $d
|
|
779
|
+
* @param {Array<string|null>} names the node names to inline (`null` selects anonymous nodes)
|
|
780
|
+
*/
|
|
781
|
+
constructor ($d, names) {
|
|
782
|
+
super($d, /** @type {any} */ (delta.$deltaAny))
|
|
783
|
+
this.names = names
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
get name () { return 'lib0:inline:' + this.names.join(',') }
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* @return {Transformer<IN, any>}
|
|
790
|
+
*/
|
|
791
|
+
init () {
|
|
792
|
+
return new InlineTransformer(this.$in, this.$out, this.names)
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* Inline child nodes whose name is in `names` by splicing each one's children into its parent (one
|
|
798
|
+
* level; e.g. with `[null]`: `<p>some<>text</></p>` <-> `<p>sometext</p>`). Use `null` for
|
|
799
|
+
* anonymous/typeless nodes, or a node name like `'b'`. Returns a reusable {@link Inline} template
|
|
800
|
+
* (a `project` hole, or `.init()` for a standalone transformer).
|
|
801
|
+
*
|
|
802
|
+
* @template {delta.DeltaConf} IN
|
|
803
|
+
* @param {import('../../schema.js').Schema<delta.Delta<IN>>} $d
|
|
804
|
+
* @param {Array<string|null>} names
|
|
805
|
+
* @return {Inline<IN>}
|
|
806
|
+
*/
|
|
807
|
+
export const inline = ($d, names) => new Inline($d, names)
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* Convenience {@link Inline} factory that inlines anonymous ("null") child nodes
|
|
811
|
+
* (`<p>some<>text</></p>` <-> `<p>sometext</p>`) - i.e. `inline($d, [null])`.
|
|
812
|
+
*
|
|
813
|
+
* @template {delta.DeltaConf} IN
|
|
814
|
+
* @param {import('../../schema.js').Schema<delta.Delta<IN>>} $d
|
|
815
|
+
* @return {Inline<IN>}
|
|
816
|
+
*/
|
|
817
|
+
/* @__NO_SIDE_EFFECTS__ */
|
|
818
|
+
export const inlineNullNodes = ($d) => inline($d, [null])
|