jazz-tools 0.9.8 → 0.9.10-jazz-bridge-preview.0
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/.turbo/turbo-build.log +5 -5
- package/.turbo/turbo-lint.log +4 -0
- package/.turbo/turbo-test.log +144 -0
- package/CHANGELOG.md +14 -0
- package/dist/{chunk-YD32FKHW.js → chunk-YWGGIF2U.js} +209 -90
- package/dist/chunk-YWGGIF2U.js.map +1 -0
- package/dist/index.native.js +1 -1
- package/dist/index.web.js +1 -1
- package/dist/testing.js +1 -1
- package/package.json +3 -2
- package/src/coValues/coList.ts +14 -1
- package/src/coValues/coMap.ts +54 -2
- package/src/coValues/coPlainText.ts +7 -34
- package/src/coValues/coRichText.ts +194 -34
- package/src/coValues/interfaces.ts +10 -0
- package/src/exports.ts +7 -1
- package/src/internal.ts +4 -4
- package/src/tests/coList.test.ts +20 -0
- package/src/tests/coPlainText.test.ts +145 -17
- package/src/tests/coRichText.test.ts +889 -12
- package/src/tests/groupsAndAccounts.test.ts +3 -3
- package/tsconfig.native.json +1 -1
- package/tsconfig.web.json +1 -1
- package/dist/chunk-YD32FKHW.js.map +0 -1
package/dist/index.native.js
CHANGED
package/dist/index.web.js
CHANGED
package/dist/testing.js
CHANGED
package/package.json
CHANGED
@@ -23,9 +23,10 @@
|
|
23
23
|
},
|
24
24
|
"type": "module",
|
25
25
|
"license": "MIT",
|
26
|
-
"version": "0.9.
|
26
|
+
"version": "0.9.10-jazz-bridge-preview.0",
|
27
27
|
"dependencies": {
|
28
|
-
"
|
28
|
+
"fast-myers-diff": "^3.2.0",
|
29
|
+
"cojson": "0.9.9"
|
29
30
|
},
|
30
31
|
"devDependencies": {
|
31
32
|
"tsup": "8.3.5",
|
package/src/coValues/coList.ts
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
import type { JsonValue, RawCoList } from "cojson";
|
2
2
|
import { RawAccount } from "cojson";
|
3
|
+
import { calcPatch } from "fast-myers-diff";
|
3
4
|
import type {
|
4
5
|
CoValue,
|
5
6
|
CoValueClass,
|
@@ -287,7 +288,6 @@ export class CoList<Item = any> extends Array<Item> implements CoValue {
|
|
287
288
|
|
288
289
|
let appendAfter = Math.max(start - 1, 0);
|
289
290
|
for (const item of toRawItems(items as Item[], this._schema[ItemsSym])) {
|
290
|
-
console.log(this._raw.asArray(), appendAfter);
|
291
291
|
this._raw.append(item, appendAfter);
|
292
292
|
appendAfter++;
|
293
293
|
}
|
@@ -295,6 +295,19 @@ export class CoList<Item = any> extends Array<Item> implements CoValue {
|
|
295
295
|
return deleted;
|
296
296
|
}
|
297
297
|
|
298
|
+
applyDiff(other: Item[]) {
|
299
|
+
const current = this._raw.asArray() as Item[];
|
300
|
+
const cmp = isRefEncoded(this._schema[ItemsSym])
|
301
|
+
? (aIdx: number, bIdx: number) =>
|
302
|
+
(current[aIdx] as CoValue).id === (other[bIdx] as CoValue).id
|
303
|
+
: undefined;
|
304
|
+
for (const [from, to, insert] of [
|
305
|
+
...calcPatch(current, other, cmp),
|
306
|
+
].reverse()) {
|
307
|
+
this.splice(from, to - from, ...insert);
|
308
|
+
}
|
309
|
+
}
|
310
|
+
|
298
311
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
299
312
|
toJSON(_key?: string, seenAbove?: ID<CoValue>[]): any[] {
|
300
313
|
const itemDescriptor = this._schema[ItemsSym] as Schema;
|
package/src/coValues/coMap.ts
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
import {
|
2
2
|
AgentID,
|
3
|
+
CoValueCore,
|
3
4
|
type CoValueUniqueness,
|
4
5
|
CojsonInternalTypes,
|
5
6
|
type JsonValue,
|
@@ -426,7 +427,6 @@ export class CoMap extends CoValueBase implements CoValue {
|
|
426
427
|
* ```ts
|
427
428
|
* const person = await Person.load(
|
428
429
|
* "co_zdsMhHtfG6VNKt7RqPUPvUtN2Ax",
|
429
|
-
* me,
|
430
430
|
* { pet: {} }
|
431
431
|
* );
|
432
432
|
* ```
|
@@ -473,7 +473,6 @@ export class CoMap extends CoValueBase implements CoValue {
|
|
473
473
|
* ```ts
|
474
474
|
* const unsub = Person.subscribe(
|
475
475
|
* "co_zdsMhHtfG6VNKt7RqPUPvUtN2Ax",
|
476
|
-
* me,
|
477
476
|
* { pet: {} },
|
478
477
|
* (person) => console.log(person)
|
479
478
|
* );
|
@@ -534,6 +533,59 @@ export class CoMap extends CoValueBase implements CoValue {
|
|
534
533
|
return cojsonInternals.idforHeader(header, crypto) as ID<M>;
|
535
534
|
}
|
536
535
|
|
536
|
+
static async getUnique<M extends CoMap>(
|
537
|
+
this: CoValueClass<M>,
|
538
|
+
unique: CoValueUniqueness["uniqueness"],
|
539
|
+
ownerID: ID<Account> | ID<Group>,
|
540
|
+
as: Account,
|
541
|
+
): Promise<M> {
|
542
|
+
// TODO: this is broken and won't load the CoMap even if it already exists on peers!!
|
543
|
+
|
544
|
+
as ||= activeAccountContext.get();
|
545
|
+
|
546
|
+
const header = {
|
547
|
+
type: "comap" as const,
|
548
|
+
ruleset: {
|
549
|
+
type: "ownedByGroup" as const,
|
550
|
+
group: ownerID,
|
551
|
+
},
|
552
|
+
meta: null,
|
553
|
+
uniqueness: unique,
|
554
|
+
};
|
555
|
+
|
556
|
+
const crypto = as._raw.core.crypto;
|
557
|
+
const id = cojsonInternals.idforHeader(header, crypto) as ID<M>;
|
558
|
+
|
559
|
+
const existingEntry = as._raw.core.node.coValuesStore.get(id);
|
560
|
+
|
561
|
+
if (existingEntry) {
|
562
|
+
if (existingEntry.state.type === "available") {
|
563
|
+
return (this as CoValueClass<M> & typeof CoMap).fromRaw(
|
564
|
+
existingEntry.state.coValue.getCurrentContent(),
|
565
|
+
);
|
566
|
+
} else if (existingEntry.state.type !== "unknown") {
|
567
|
+
throw new Error(
|
568
|
+
`Can't handle intermediate state for unique CoMap: ${existingEntry.state.type}`,
|
569
|
+
);
|
570
|
+
}
|
571
|
+
}
|
572
|
+
|
573
|
+
as._raw.core.node.coValuesStore.setAsAvailable(
|
574
|
+
id,
|
575
|
+
new CoValueCore(header, as._raw.core.node),
|
576
|
+
);
|
577
|
+
|
578
|
+
const entry = as._raw.core.node.coValuesStore.get(id);
|
579
|
+
|
580
|
+
if (entry.state.type !== "available") {
|
581
|
+
throw new Error("CoValue not found");
|
582
|
+
}
|
583
|
+
|
584
|
+
return (this as CoValueClass<M> & typeof CoMap).fromRaw(
|
585
|
+
entry.state.coValue.getCurrentContent(),
|
586
|
+
);
|
587
|
+
}
|
588
|
+
|
537
589
|
/**
|
538
590
|
* Given an already loaded `CoMap`, ensure that the specified fields are loaded to the specified depth.
|
539
591
|
*
|
@@ -63,6 +63,10 @@ export class CoPlainText extends String implements CoValue {
|
|
63
63
|
return new this({ text, owner: options.owner });
|
64
64
|
}
|
65
65
|
|
66
|
+
get length() {
|
67
|
+
return this._raw.toString().length;
|
68
|
+
}
|
69
|
+
|
66
70
|
toString() {
|
67
71
|
return this._raw.toString();
|
68
72
|
}
|
@@ -113,22 +117,6 @@ export class CoPlainText extends String implements CoValue {
|
|
113
117
|
/**
|
114
118
|
* Load a `CoPlainText` with a given ID, as a given account.
|
115
119
|
*
|
116
|
-
* `depth` specifies which (if any) fields that reference other CoValues to load as well before resolving.
|
117
|
-
* The `DeeplyLoaded` return type guarantees that corresponding referenced CoValues are loaded to the specified depth.
|
118
|
-
*
|
119
|
-
* You can pass `[]` or `{}` for shallowly loading only this CoPlainText, or `{ fieldA: depthA, fieldB: depthB }` for recursively loading referenced CoValues.
|
120
|
-
*
|
121
|
-
* Check out the `load` methods on `CoMap`/`CoList`/`CoStream`/`Group`/`Account` to see which depth structures are valid to nest.
|
122
|
-
*
|
123
|
-
* @example
|
124
|
-
* ```ts
|
125
|
-
* const person = await Person.load(
|
126
|
-
* "co_zdsMhHtfG6VNKt7RqPUPvUtN2Ax",
|
127
|
-
* me,
|
128
|
-
* { pet: {} }
|
129
|
-
* );
|
130
|
-
* ```
|
131
|
-
*
|
132
120
|
* @category Subscription & Loading
|
133
121
|
*/
|
134
122
|
static load<T extends CoPlainText>(
|
@@ -154,31 +142,16 @@ export class CoPlainText extends String implements CoValue {
|
|
154
142
|
// }
|
155
143
|
|
156
144
|
/**
|
157
|
-
* Load and subscribe to a `
|
145
|
+
* Load and subscribe to a `CoPlainText` with a given ID, as a given account.
|
158
146
|
*
|
159
147
|
* Automatically also subscribes to updates to all referenced/nested CoValues as soon as they are accessed in the listener.
|
160
148
|
*
|
161
|
-
* `depth` specifies which (if any) fields that reference other CoValues to load as well before calling `listener` for the first time.
|
162
|
-
* The `DeeplyLoaded` return type guarantees that corresponding referenced CoValues are loaded to the specified depth.
|
163
|
-
*
|
164
|
-
* You can pass `[]` or `{}` for shallowly loading only this CoMap, or `{ fieldA: depthA, fieldB: depthB }` for recursively loading referenced CoValues.
|
165
|
-
*
|
166
149
|
* Check out the `load` methods on `CoMap`/`CoList`/`CoStream`/`Group`/`Account` to see which depth structures are valid to nest.
|
167
150
|
*
|
168
151
|
* Returns an unsubscribe function that you should call when you no longer need updates.
|
169
152
|
*
|
170
153
|
* Also see the `useCoState` hook to reactively subscribe to a CoValue in a React component.
|
171
154
|
*
|
172
|
-
* @example
|
173
|
-
* ```ts
|
174
|
-
* const unsub = Person.subscribe(
|
175
|
-
* "co_zdsMhHtfG6VNKt7RqPUPvUtN2Ax",
|
176
|
-
* me,
|
177
|
-
* { pet: {} },
|
178
|
-
* (person) => console.log(person)
|
179
|
-
* );
|
180
|
-
* ```
|
181
|
-
*
|
182
155
|
* @category Subscription & Loading
|
183
156
|
*/
|
184
157
|
static subscribe<T extends CoPlainText>(
|
@@ -226,9 +199,9 @@ export class CoPlainText extends String implements CoValue {
|
|
226
199
|
// }
|
227
200
|
|
228
201
|
/**
|
229
|
-
* Given an already loaded `
|
202
|
+
* Given an already loaded `CoPlainText`, subscribe to updates to the `CoPlainText` and ensure that the specified fields are loaded to the specified depth.
|
230
203
|
*
|
231
|
-
* Works like `
|
204
|
+
* Works like `CoPlainText.subscribe()`, but you don't need to pass the ID or the account to load as again.
|
232
205
|
*
|
233
206
|
* Returns an unsubscribe function that you should call when you no longer need updates.
|
234
207
|
*
|
@@ -8,6 +8,33 @@ import type { Group } from "./group.js";
|
|
8
8
|
/**
|
9
9
|
* Base class for text annotations and formatting marks.
|
10
10
|
* Represents a mark with start and end positions in text.
|
11
|
+
*
|
12
|
+
* Example text: "Hello world! How are you?"
|
13
|
+
* If we want to mark "world" with bold:
|
14
|
+
*
|
15
|
+
* ```
|
16
|
+
* uncertainty region
|
17
|
+
* ↓
|
18
|
+
* Hello [····]world[····]! How are you?
|
19
|
+
* ↑ ↑ ↑ ↑
|
20
|
+
* | | | |
|
21
|
+
* startAfter | | endBefore
|
22
|
+
* startBefore endAfter
|
23
|
+
* ```
|
24
|
+
*
|
25
|
+
* - startAfter: Position after "Hello " (exclusive boundary)
|
26
|
+
* - startBefore: Position before "world" (inclusive boundary)
|
27
|
+
* - endAfter: Position after "world" (inclusive boundary)
|
28
|
+
* - endBefore: Position before "!" (exclusive boundary)
|
29
|
+
*
|
30
|
+
* The regions marked with [····] are "uncertainty regions" where:
|
31
|
+
* - Text inserted in the left uncertainty region may or may not be part of the mark
|
32
|
+
* - Text inserted in the right uncertainty region may or may not be part of the mark
|
33
|
+
* - Text inserted between startBefore and endAfter is definitely part of the mark
|
34
|
+
*
|
35
|
+
* Positions must satisfy:
|
36
|
+
* 0 ≤ startAfter ≤ startBefore < endAfter ≤ endBefore ≤ textLength
|
37
|
+
* A mark cannot be zero-length, so endAfter must be greater than startBefore.
|
11
38
|
*/
|
12
39
|
export class Mark extends CoMap {
|
13
40
|
startAfter = co.json<TextPos | null>();
|
@@ -15,6 +42,41 @@ export class Mark extends CoMap {
|
|
15
42
|
endAfter = co.json<TextPos>();
|
16
43
|
endBefore = co.json<TextPos | null>();
|
17
44
|
tag = co.string;
|
45
|
+
|
46
|
+
/**
|
47
|
+
* Validates and clamps mark positions to ensure they are in the correct order
|
48
|
+
* @returns Normalized positions or null if invalid
|
49
|
+
*/
|
50
|
+
validatePositions(
|
51
|
+
textLength: number,
|
52
|
+
idxAfter: (pos: TextPos) => number | undefined,
|
53
|
+
idxBefore: (pos: TextPos) => number | undefined,
|
54
|
+
) {
|
55
|
+
if (!textLength) {
|
56
|
+
console.error("Cannot validate positions for empty text");
|
57
|
+
return null;
|
58
|
+
}
|
59
|
+
|
60
|
+
// Get positions with fallbacks
|
61
|
+
const positions = {
|
62
|
+
startAfter: this.startAfter ? (idxBefore(this.startAfter) ?? 0) : 0,
|
63
|
+
startBefore: this.startBefore ? (idxAfter(this.startBefore) ?? 0) : 0,
|
64
|
+
endAfter: this.endAfter
|
65
|
+
? (idxBefore(this.endAfter) ?? textLength)
|
66
|
+
: textLength,
|
67
|
+
endBefore: this.endBefore
|
68
|
+
? (idxAfter(this.endBefore) ?? textLength)
|
69
|
+
: textLength,
|
70
|
+
};
|
71
|
+
|
72
|
+
// Clamp and ensure proper ordering in one step
|
73
|
+
return {
|
74
|
+
startAfter: Math.max(0, positions.startAfter),
|
75
|
+
startBefore: Math.max(positions.startAfter + 1, positions.startBefore),
|
76
|
+
endAfter: Math.min(textLength - 1, positions.endAfter),
|
77
|
+
endBefore: Math.min(textLength, positions.endBefore),
|
78
|
+
};
|
79
|
+
}
|
18
80
|
}
|
19
81
|
|
20
82
|
/**
|
@@ -194,9 +256,21 @@ export class CoRichText extends CoMap {
|
|
194
256
|
>,
|
195
257
|
options?: { markOwner?: Account | Group },
|
196
258
|
) {
|
197
|
-
if (!this.marks) {
|
259
|
+
if (!this.text || !this.marks) {
|
198
260
|
throw new Error("Cannot insert a range without loaded ranges");
|
199
261
|
}
|
262
|
+
|
263
|
+
const textLength = this.length;
|
264
|
+
|
265
|
+
// Clamp positions to text bounds
|
266
|
+
start = Math.max(start, 0);
|
267
|
+
end = Math.min(end, textLength);
|
268
|
+
|
269
|
+
const owner = options?.markOwner || this._owner;
|
270
|
+
if (!owner) {
|
271
|
+
throw new Error("No owner specified for mark");
|
272
|
+
}
|
273
|
+
|
200
274
|
const range = RangeClass.create(
|
201
275
|
{
|
202
276
|
...extraArgs,
|
@@ -205,11 +279,109 @@ export class CoRichText extends CoMap {
|
|
205
279
|
endAfter: this.posBefore(end),
|
206
280
|
endBefore: this.posAfter(end),
|
207
281
|
},
|
208
|
-
{ owner
|
282
|
+
{ owner },
|
209
283
|
);
|
284
|
+
|
210
285
|
this.marks.push(range);
|
211
286
|
}
|
212
287
|
|
288
|
+
/**
|
289
|
+
* Remove a mark at a specific range.
|
290
|
+
*/
|
291
|
+
removeMark<
|
292
|
+
MarkClass extends {
|
293
|
+
new (...args: any[]): Mark;
|
294
|
+
create(init: any, options: { owner: Account | Group }): Mark;
|
295
|
+
},
|
296
|
+
>(
|
297
|
+
start: number,
|
298
|
+
end: number,
|
299
|
+
RangeClass: MarkClass,
|
300
|
+
options: { tag: string },
|
301
|
+
) {
|
302
|
+
if (!this.marks) {
|
303
|
+
throw new Error("Cannot remove marks without loaded marks");
|
304
|
+
}
|
305
|
+
|
306
|
+
// Find marks of the given class that overlap with the range
|
307
|
+
const resolvedMarks = this.resolveMarks();
|
308
|
+
|
309
|
+
for (const mark of resolvedMarks) {
|
310
|
+
// If mark is outside the range, we'll skip it
|
311
|
+
if (mark.endBefore < start || mark.startAfter > end) {
|
312
|
+
continue;
|
313
|
+
}
|
314
|
+
|
315
|
+
// If mark is wrong type, we'll skip it
|
316
|
+
if (options.tag && mark.sourceMark.tag !== options.tag) {
|
317
|
+
continue;
|
318
|
+
}
|
319
|
+
|
320
|
+
const markIndex = this.marks.findIndex((m) => m === mark.sourceMark);
|
321
|
+
if (markIndex === -1) {
|
322
|
+
continue;
|
323
|
+
}
|
324
|
+
|
325
|
+
// If mark is completely inside the range, we'll remove it
|
326
|
+
if (mark.startBefore >= start && mark.endAfter <= end) {
|
327
|
+
// Remove the mark
|
328
|
+
this.marks.splice(markIndex, 1);
|
329
|
+
continue;
|
330
|
+
}
|
331
|
+
|
332
|
+
// If mark starts before and extends after the removal range, update end positions to start of removal
|
333
|
+
if (
|
334
|
+
mark.startBefore < start &&
|
335
|
+
mark.endAfter >= start &&
|
336
|
+
mark.endAfter <= end
|
337
|
+
) {
|
338
|
+
const endAfterPos = this.posBefore(start);
|
339
|
+
const endBeforePos = this.posBefore(start);
|
340
|
+
if (endAfterPos && endBeforePos) {
|
341
|
+
mark.sourceMark.endAfter = endAfterPos;
|
342
|
+
mark.sourceMark.endBefore = endBeforePos;
|
343
|
+
}
|
344
|
+
continue;
|
345
|
+
}
|
346
|
+
|
347
|
+
// If mark starts in removal range and extends beyond it, update start positions to end of removal
|
348
|
+
if (
|
349
|
+
mark.startBefore >= start &&
|
350
|
+
mark.startBefore <= end &&
|
351
|
+
mark.endAfter > end
|
352
|
+
) {
|
353
|
+
const startAfterPos = this.posAfter(end);
|
354
|
+
const startBeforePos = this.posAfter(end);
|
355
|
+
if (startAfterPos && startBeforePos) {
|
356
|
+
mark.sourceMark.startAfter = startAfterPos;
|
357
|
+
mark.sourceMark.startBefore = startBeforePos;
|
358
|
+
}
|
359
|
+
continue;
|
360
|
+
}
|
361
|
+
|
362
|
+
// If removal is inside the mark, we'll split the mark
|
363
|
+
if (mark.startBefore <= start && mark.endAfter >= end) {
|
364
|
+
// Split the mark by shortening it at the start and adding a new mark at the end
|
365
|
+
const endAfterPos = this.posBefore(start);
|
366
|
+
const endBeforePos = this.posBefore(start);
|
367
|
+
if (endAfterPos && endBeforePos) {
|
368
|
+
mark.sourceMark.endAfter = endAfterPos;
|
369
|
+
mark.sourceMark.endBefore = endBeforePos;
|
370
|
+
}
|
371
|
+
this.insertMark(
|
372
|
+
end + 1,
|
373
|
+
mark.endBefore,
|
374
|
+
RangeClass,
|
375
|
+
{},
|
376
|
+
{
|
377
|
+
markOwner: mark.sourceMark._owner || this._owner,
|
378
|
+
},
|
379
|
+
);
|
380
|
+
continue;
|
381
|
+
}
|
382
|
+
}
|
383
|
+
}
|
384
|
+
|
213
385
|
/**
|
214
386
|
* Resolve the positions of all marks.
|
215
387
|
*/
|
@@ -217,35 +389,27 @@ export class CoRichText extends CoMap {
|
|
217
389
|
if (!this.text || !this.marks) {
|
218
390
|
throw new Error("Cannot resolve ranges without loaded text and ranges");
|
219
391
|
}
|
220
|
-
|
392
|
+
|
393
|
+
const textLength = this.length;
|
394
|
+
|
395
|
+
return this.marks.flatMap((mark) => {
|
221
396
|
if (!mark) return [];
|
222
|
-
|
223
|
-
const
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
const endBefore = mark.endBefore
|
231
|
-
? this.idxAfter(mark.endBefore)
|
232
|
-
: endAfter + 1;
|
233
|
-
if (startAfter === undefined || endBefore === undefined) {
|
234
|
-
return [];
|
235
|
-
}
|
397
|
+
|
398
|
+
const positions = mark.validatePositions(
|
399
|
+
textLength,
|
400
|
+
(pos) => this.idxAfter(pos),
|
401
|
+
(pos) => this.idxBefore(pos),
|
402
|
+
);
|
403
|
+
if (!positions) return [];
|
404
|
+
|
236
405
|
return [
|
237
406
|
{
|
238
407
|
sourceMark: mark,
|
239
|
-
|
240
|
-
startBefore,
|
241
|
-
endAfter,
|
242
|
-
endBefore,
|
408
|
+
...positions,
|
243
409
|
tag: mark.tag,
|
244
|
-
from: mark,
|
245
410
|
},
|
246
411
|
];
|
247
412
|
});
|
248
|
-
return ranges;
|
249
413
|
}
|
250
414
|
|
251
415
|
/**
|
@@ -298,10 +462,10 @@ export class CoRichText extends CoMap {
|
|
298
462
|
toTree(tagPrecedence: string[]): TreeNode {
|
299
463
|
const ranges = this.resolveAndDiffuseAndFocusMarks();
|
300
464
|
|
301
|
-
//
|
465
|
+
// Convert a bunch of (potentially overlapping) ranges into a tree
|
302
466
|
// - make sure we include all text in leaves, even if it's not covered by a range
|
303
467
|
// - we split overlapping ranges in a way where the higher precedence (tag earlier in tagPrecedence)
|
304
|
-
// stays intact and the lower
|
468
|
+
// stays intact and the lower precedence tag is split into two ranges, one inside and one outside the higher precedence range
|
305
469
|
|
306
470
|
const text = this.text?.toString() || "";
|
307
471
|
|
@@ -328,14 +492,6 @@ export class CoRichText extends CoMap {
|
|
328
492
|
? splitNode(inOrAfter, range.end)
|
329
493
|
: [undefined, undefined];
|
330
494
|
|
331
|
-
// console.log("split", range.start, range.end, {
|
332
|
-
// before,
|
333
|
-
// inside,
|
334
|
-
// after,
|
335
|
-
// });
|
336
|
-
|
337
|
-
// TODO: also split children
|
338
|
-
|
339
495
|
return [
|
340
496
|
...(before ? [before] : []),
|
341
497
|
...(inside
|
@@ -365,6 +521,10 @@ export class CoRichText extends CoMap {
|
|
365
521
|
};
|
366
522
|
}
|
367
523
|
|
524
|
+
get length() {
|
525
|
+
return this.text?.toString().length || 0;
|
526
|
+
}
|
527
|
+
|
368
528
|
/**
|
369
529
|
* Convert a CoRichText to plain text.
|
370
530
|
*/
|
@@ -400,7 +560,7 @@ export type TreeNode = {
|
|
400
560
|
/**
|
401
561
|
* Split a node at a specific index. So that the node is split into two parts, one before the index, and one after the index.
|
402
562
|
*/
|
403
|
-
function splitNode(
|
563
|
+
export function splitNode(
|
404
564
|
node: TreeNode | TreeLeaf,
|
405
565
|
at: number,
|
406
566
|
): [TreeNode | TreeLeaf | undefined, TreeNode | TreeLeaf | undefined] {
|
@@ -27,6 +27,16 @@ export interface CoValueClass<Value extends CoValue = CoValue> {
|
|
27
27
|
new (...args: any[]): Value;
|
28
28
|
}
|
29
29
|
|
30
|
+
export interface CoValueClassWithLoad<V extends CoValue>
|
31
|
+
extends CoValueClass<V> {
|
32
|
+
load<D extends DepthsIn<V>>(id: ID<V>, depth: D): Promise<DeeplyLoaded<V, D>>;
|
33
|
+
load<D extends DepthsIn<V>>(
|
34
|
+
id: ID<V>,
|
35
|
+
as: Account,
|
36
|
+
depth: D,
|
37
|
+
): Promise<DeeplyLoaded<V, D>>;
|
38
|
+
}
|
39
|
+
|
30
40
|
export interface CoValueFromRaw<V extends CoValue> {
|
31
41
|
fromRaw(raw: V["_raw"]): V;
|
32
42
|
}
|
package/src/exports.ts
CHANGED
@@ -33,6 +33,7 @@ export {
|
|
33
33
|
Marks,
|
34
34
|
type TreeLeaf,
|
35
35
|
type TreeNode,
|
36
|
+
type ResolvedMark,
|
36
37
|
} from "./coValues/coRichText.js";
|
37
38
|
export { ImageDefinition } from "./coValues/extensions/imageDef.js";
|
38
39
|
export { Group } from "./coValues/group.js";
|
@@ -40,7 +41,12 @@ export { CoValueBase } from "./coValues/interfaces.js";
|
|
40
41
|
export { Profile } from "./coValues/profile.js";
|
41
42
|
export { SchemaUnion } from "./coValues/schemaUnion.js";
|
42
43
|
|
43
|
-
export type {
|
44
|
+
export type {
|
45
|
+
CoValueClass,
|
46
|
+
CoValueClassWithLoad,
|
47
|
+
DeeplyLoaded,
|
48
|
+
DepthsIn,
|
49
|
+
} from "./internal.js";
|
44
50
|
|
45
51
|
export {
|
46
52
|
createCoValueObservable,
|
package/src/internal.ts
CHANGED
@@ -1,13 +1,13 @@
|
|
1
|
-
export * from "./implementation/symbols.js";
|
2
|
-
export * from "./implementation/inspect.js";
|
3
1
|
export * from "./coValues/interfaces.js";
|
2
|
+
export * from "./implementation/inspect.js";
|
3
|
+
export * from "./implementation/symbols.js";
|
4
4
|
|
5
|
-
export * from "./
|
5
|
+
export * from "./coValues/deepLoading.js";
|
6
6
|
export * from "./implementation/anonymousJazzAgent.js";
|
7
|
+
export * from "./implementation/errors.js";
|
7
8
|
export * from "./implementation/refs.js";
|
8
9
|
export * from "./implementation/schema.js";
|
9
10
|
export * from "./implementation/subscriptionScope.js";
|
10
|
-
export * from "./coValues/deepLoading.js";
|
11
11
|
|
12
12
|
export * from "./implementation/createContext.js";
|
13
13
|
|
package/src/tests/coList.test.ts
CHANGED
@@ -115,6 +115,26 @@ describe("Simple CoList operations", async () => {
|
|
115
115
|
expect(list.length).toBe(4);
|
116
116
|
expect(list._raw.asArray()).toEqual(["bread", "salt", "pepper", "onion"]);
|
117
117
|
});
|
118
|
+
|
119
|
+
test("applyDiff", () => {
|
120
|
+
const list = TestList.create(["bread", "butter", "onion"], {
|
121
|
+
owner: me,
|
122
|
+
});
|
123
|
+
// replace
|
124
|
+
list.applyDiff(["bread", "margarine", "onion"]);
|
125
|
+
expect(list._raw.asArray()).toEqual(["bread", "margarine", "onion"]);
|
126
|
+
// delete
|
127
|
+
list.applyDiff(["bread", "onion"]);
|
128
|
+
expect(list._raw.asArray()).toEqual(["bread", "onion"]);
|
129
|
+
// insert multiple
|
130
|
+
list.applyDiff(["bread", "margarine", "onion", "cheese"]);
|
131
|
+
expect(list._raw.asArray()).toEqual([
|
132
|
+
"bread",
|
133
|
+
"margarine",
|
134
|
+
"onion",
|
135
|
+
"cheese",
|
136
|
+
]);
|
137
|
+
});
|
118
138
|
});
|
119
139
|
});
|
120
140
|
|