jazz-tools 0.13.10 → 0.13.11
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 +7 -7
- package/CHANGELOG.md +10 -0
- package/dist/{chunk-NFVKGXSH.js → chunk-I7NYAGLP.js} +23 -398
- package/dist/{chunk-NFVKGXSH.js.map → chunk-I7NYAGLP.js.map} +1 -1
- package/dist/coValues/coPlainText.d.ts +7 -0
- package/dist/coValues/coPlainText.d.ts.map +1 -1
- package/dist/coValues/coRichText.d.ts +2 -257
- package/dist/coValues/coRichText.d.ts.map +1 -1
- package/dist/exports.d.ts +1 -1
- package/dist/exports.d.ts.map +1 -1
- package/dist/index.js +1 -3
- package/dist/index.js.map +1 -1
- package/dist/testing.js +1 -1
- package/package.json +2 -2
- package/src/coValues/coPlainText.ts +23 -1
- package/src/coValues/coRichText.ts +2 -660
- package/src/exports.ts +1 -7
- package/src/tests/coPlainText.test.ts +21 -3
- package/dist/tests/coRichText.test.d.ts +0 -2
- package/dist/tests/coRichText.test.d.ts.map +0 -1
- package/src/tests/coRichText.test.ts +0 -937
@@ -1,661 +1,3 @@
|
|
1
|
-
import {
|
2
|
-
import type { Account } from "./account.js";
|
3
|
-
import { CoList } from "./coList.js";
|
4
|
-
import { CoMap, type CoMapInit } from "./coMap.js";
|
5
|
-
import { CoPlainText, type TextPos } from "./coPlainText.js";
|
6
|
-
import type { Group } from "./group.js";
|
1
|
+
import { CoPlainText } from "./coPlainText.js";
|
7
2
|
|
8
|
-
|
9
|
-
* Base class for text annotations and formatting marks.
|
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.
|
38
|
-
*/
|
39
|
-
export class Mark extends CoMap {
|
40
|
-
startAfter = co.json<TextPos | null>();
|
41
|
-
startBefore = co.json<TextPos>();
|
42
|
-
endAfter = co.json<TextPos>();
|
43
|
-
endBefore = co.json<TextPos | null>();
|
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
|
-
}
|
80
|
-
}
|
81
|
-
|
82
|
-
/**
|
83
|
-
* A mark with resolved numeric positions in text.
|
84
|
-
* Contains both position information and reference to the source mark.
|
85
|
-
* @template R Type extending Mark, defaults to Mark
|
86
|
-
*/
|
87
|
-
export type ResolvedMark<R extends Mark = Mark> = {
|
88
|
-
startAfter: number;
|
89
|
-
startBefore: number;
|
90
|
-
endAfter: number;
|
91
|
-
endBefore: number;
|
92
|
-
sourceMark: R;
|
93
|
-
};
|
94
|
-
|
95
|
-
/**
|
96
|
-
* A mark that has been resolved and diffused with certainty information.
|
97
|
-
* Includes start/end positions and indication of boundary certainty.
|
98
|
-
* @template R Type extending Mark, defaults to Mark
|
99
|
-
*/
|
100
|
-
export type ResolvedAndDiffusedMark<R extends Mark = Mark> = {
|
101
|
-
start: number;
|
102
|
-
end: number;
|
103
|
-
side: "uncertainStart" | "certainMiddle" | "uncertainEnd";
|
104
|
-
sourceMark: R;
|
105
|
-
};
|
106
|
-
|
107
|
-
/**
|
108
|
-
* Defines how marks should be focused when resolving positions.
|
109
|
-
* - 'far': Positions marks at furthest valid positions
|
110
|
-
* - 'close': Positions marks at nearest valid positions
|
111
|
-
* - 'closestWhitespace': Positions marks at nearest whitespace
|
112
|
-
*/
|
113
|
-
export type FocusBias = "far" | "close" | "closestWhitespace";
|
114
|
-
|
115
|
-
/**
|
116
|
-
* A mark that has been resolved and focused to specific positions.
|
117
|
-
* Contains simplified position information and reference to source mark.
|
118
|
-
* @template R Type extending Mark, defaults to Mark
|
119
|
-
*/
|
120
|
-
export type ResolvedAndFocusedMark<R extends Mark = Mark> = {
|
121
|
-
start: number;
|
122
|
-
end: number;
|
123
|
-
sourceMark: R;
|
124
|
-
};
|
125
|
-
|
126
|
-
/**
|
127
|
-
* Main class for handling rich text content with marks.
|
128
|
-
* Combines plain text with a list of marks for formatting and annotations.
|
129
|
-
* Provides methods for text manipulation, mark insertion, and tree conversion.
|
130
|
-
*/
|
131
|
-
export class CoRichText extends CoMap {
|
132
|
-
text = co.ref(CoPlainText);
|
133
|
-
marks = co.ref(CoList.Of(co.ref(Mark)));
|
134
|
-
|
135
|
-
/**
|
136
|
-
* Create a CoRichText from plain text.
|
137
|
-
*/
|
138
|
-
static createFromPlainText(
|
139
|
-
text: string,
|
140
|
-
options: { owner: Account | Group },
|
141
|
-
) {
|
142
|
-
return this.create(
|
143
|
-
{
|
144
|
-
text: CoPlainText.create(text, { owner: options.owner }),
|
145
|
-
marks: CoList.Of(co.ref(Mark)).create([], {
|
146
|
-
owner: options.owner,
|
147
|
-
}),
|
148
|
-
},
|
149
|
-
{ owner: options.owner },
|
150
|
-
);
|
151
|
-
}
|
152
|
-
|
153
|
-
/**
|
154
|
-
* Create a CoRichText from plain text and a mark.
|
155
|
-
*/
|
156
|
-
static createFromPlainTextAndMark<
|
157
|
-
MarkClass extends {
|
158
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
159
|
-
new (...args: any[]): Mark;
|
160
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
161
|
-
create(init: any, options: { owner: Account | Group }): Mark;
|
162
|
-
},
|
163
|
-
>(
|
164
|
-
text: string,
|
165
|
-
WrapIn: MarkClass,
|
166
|
-
extraArgs: Omit<
|
167
|
-
CoMapInit<InstanceType<MarkClass>>,
|
168
|
-
"startAfter" | "startBefore" | "endAfter" | "endBefore"
|
169
|
-
>,
|
170
|
-
options: { owner: Account | Group },
|
171
|
-
) {
|
172
|
-
const richtext = this.createFromPlainText(text, options);
|
173
|
-
|
174
|
-
richtext.insertMark(0, text.length, WrapIn, extraArgs);
|
175
|
-
|
176
|
-
return richtext;
|
177
|
-
}
|
178
|
-
|
179
|
-
/**
|
180
|
-
* Insert text at a specific index.
|
181
|
-
*/
|
182
|
-
insertAfter(idx: number, text: string) {
|
183
|
-
if (!this.text)
|
184
|
-
throw new Error("Cannot insert into a CoRichText without loaded text");
|
185
|
-
this.text.insertAfter(idx, text);
|
186
|
-
}
|
187
|
-
|
188
|
-
/**
|
189
|
-
* Delete a range of text.
|
190
|
-
*/
|
191
|
-
deleteRange(range: { from: number; to: number }) {
|
192
|
-
if (!this.text)
|
193
|
-
throw new Error("Cannot delete from a CoRichText without loaded text");
|
194
|
-
this.text.deleteRange(range);
|
195
|
-
}
|
196
|
-
|
197
|
-
/**
|
198
|
-
* Get the position of a specific index.
|
199
|
-
*/
|
200
|
-
posBefore(idx: number): TextPos | undefined {
|
201
|
-
if (!this.text)
|
202
|
-
throw new Error(
|
203
|
-
"Cannot get posBefore in a CoRichText without loaded text",
|
204
|
-
);
|
205
|
-
return this.text.posBefore(idx);
|
206
|
-
}
|
207
|
-
|
208
|
-
/**
|
209
|
-
* Get the position of a specific index.
|
210
|
-
*/
|
211
|
-
posAfter(idx: number): TextPos | undefined {
|
212
|
-
if (!this.text)
|
213
|
-
throw new Error(
|
214
|
-
"Cannot get posAfter in a CoRichText without loaded text",
|
215
|
-
);
|
216
|
-
return this.text.posAfter(idx);
|
217
|
-
}
|
218
|
-
|
219
|
-
/**
|
220
|
-
* Get the index of a specific position.
|
221
|
-
*/
|
222
|
-
idxBefore(pos: TextPos): number | undefined {
|
223
|
-
if (!this.text)
|
224
|
-
throw new Error(
|
225
|
-
"Cannot get idxBefore in a CoRichText without loaded text",
|
226
|
-
);
|
227
|
-
return this.text.idxBefore(pos);
|
228
|
-
}
|
229
|
-
|
230
|
-
/**
|
231
|
-
* Get the index of a specific position.
|
232
|
-
*/
|
233
|
-
idxAfter(pos: TextPos): number | undefined {
|
234
|
-
if (!this.text)
|
235
|
-
throw new Error(
|
236
|
-
"Cannot get idxAfter in a CoRichText without loaded text",
|
237
|
-
);
|
238
|
-
return this.text.idxAfter(pos);
|
239
|
-
}
|
240
|
-
|
241
|
-
/**
|
242
|
-
* Insert a mark at a specific range.
|
243
|
-
*/
|
244
|
-
insertMark<
|
245
|
-
MarkClass extends {
|
246
|
-
new (...args: any[]): Mark;
|
247
|
-
create(init: any, options: { owner: Account | Group }): Mark;
|
248
|
-
},
|
249
|
-
>(
|
250
|
-
start: number,
|
251
|
-
end: number,
|
252
|
-
RangeClass: MarkClass,
|
253
|
-
extraArgs: Omit<
|
254
|
-
CoMapInit<InstanceType<MarkClass>>,
|
255
|
-
"startAfter" | "startBefore" | "endAfter" | "endBefore"
|
256
|
-
>,
|
257
|
-
options?: { markOwner?: Account | Group },
|
258
|
-
) {
|
259
|
-
if (!this.text || !this.marks) {
|
260
|
-
throw new Error("Cannot insert a range without loaded ranges");
|
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
|
-
|
274
|
-
const range = RangeClass.create(
|
275
|
-
{
|
276
|
-
...extraArgs,
|
277
|
-
startAfter: this.posBefore(start),
|
278
|
-
startBefore: this.posAfter(start),
|
279
|
-
endAfter: this.posBefore(end),
|
280
|
-
endBefore: this.posAfter(end),
|
281
|
-
},
|
282
|
-
{ owner },
|
283
|
-
);
|
284
|
-
|
285
|
-
this.marks.push(range);
|
286
|
-
}
|
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
|
-
// @ts-ignore Some Typescript versions flag this as an error
|
376
|
-
{},
|
377
|
-
{
|
378
|
-
markOwner: mark.sourceMark._owner || this._owner,
|
379
|
-
},
|
380
|
-
);
|
381
|
-
continue;
|
382
|
-
}
|
383
|
-
}
|
384
|
-
}
|
385
|
-
|
386
|
-
/**
|
387
|
-
* Resolve the positions of all marks.
|
388
|
-
*/
|
389
|
-
resolveMarks(): ResolvedMark[] {
|
390
|
-
if (!this.text || !this.marks) {
|
391
|
-
throw new Error("Cannot resolve ranges without loaded text and ranges");
|
392
|
-
}
|
393
|
-
|
394
|
-
const textLength = this.length;
|
395
|
-
|
396
|
-
return this.marks.flatMap((mark) => {
|
397
|
-
if (!mark) return [];
|
398
|
-
|
399
|
-
const positions = mark.validatePositions(
|
400
|
-
textLength,
|
401
|
-
(pos) => this.idxAfter(pos),
|
402
|
-
(pos) => this.idxBefore(pos),
|
403
|
-
);
|
404
|
-
if (!positions) return [];
|
405
|
-
|
406
|
-
return [
|
407
|
-
{
|
408
|
-
sourceMark: mark,
|
409
|
-
...positions,
|
410
|
-
tag: mark.tag,
|
411
|
-
},
|
412
|
-
];
|
413
|
-
});
|
414
|
-
}
|
415
|
-
|
416
|
-
/**
|
417
|
-
* Resolve and diffuse the positions of all marks.
|
418
|
-
*/
|
419
|
-
resolveAndDiffuseMarks(): ResolvedAndDiffusedMark[] {
|
420
|
-
return this.resolveMarks().flatMap((range) => [
|
421
|
-
...(range.startAfter < range.startBefore - 1
|
422
|
-
? [
|
423
|
-
{
|
424
|
-
start: range.startAfter,
|
425
|
-
end: range.startBefore - 1,
|
426
|
-
side: "uncertainStart" as const,
|
427
|
-
sourceMark: range.sourceMark,
|
428
|
-
},
|
429
|
-
]
|
430
|
-
: []),
|
431
|
-
{
|
432
|
-
start: range.startBefore - 1,
|
433
|
-
end: range.endAfter + 1,
|
434
|
-
side: "certainMiddle" as const,
|
435
|
-
sourceMark: range.sourceMark,
|
436
|
-
},
|
437
|
-
...(range.endAfter + 1 < range.endBefore
|
438
|
-
? [
|
439
|
-
{
|
440
|
-
start: range.endAfter + 1,
|
441
|
-
end: range.endBefore,
|
442
|
-
side: "uncertainEnd" as const,
|
443
|
-
sourceMark: range.sourceMark,
|
444
|
-
},
|
445
|
-
]
|
446
|
-
: []),
|
447
|
-
]);
|
448
|
-
}
|
449
|
-
|
450
|
-
/**
|
451
|
-
* Resolve, diffuse, and focus the positions of all marks.
|
452
|
-
*/
|
453
|
-
resolveAndDiffuseAndFocusMarks(): ResolvedAndFocusedMark[] {
|
454
|
-
// for now we only keep the certainMiddle ranges
|
455
|
-
return this.resolveAndDiffuseMarks().filter(
|
456
|
-
(range) => range.side === "certainMiddle",
|
457
|
-
);
|
458
|
-
}
|
459
|
-
|
460
|
-
/**
|
461
|
-
* Convert a CoRichText to a tree structure useful for client libraries.
|
462
|
-
*/
|
463
|
-
toTree(tagPrecedence: string[]): TreeNode {
|
464
|
-
const ranges = this.resolveAndDiffuseAndFocusMarks();
|
465
|
-
|
466
|
-
// Convert a bunch of (potentially overlapping) ranges into a tree
|
467
|
-
// - make sure we include all text in leaves, even if it's not covered by a range
|
468
|
-
// - we split overlapping ranges in a way where the higher precedence (tag earlier in tagPrecedence)
|
469
|
-
// stays intact and the lower precedence tag is split into two ranges, one inside and one outside the higher precedence range
|
470
|
-
|
471
|
-
const text = this.text?.toString() || "";
|
472
|
-
|
473
|
-
let currentNodes: (TreeLeaf | TreeNode)[] = [
|
474
|
-
{
|
475
|
-
type: "leaf",
|
476
|
-
start: 0,
|
477
|
-
end: text.length,
|
478
|
-
},
|
479
|
-
];
|
480
|
-
|
481
|
-
const rangesSortedLowToHighPrecedence = ranges.sort((a, b) => {
|
482
|
-
const aPrecedence = tagPrecedence.indexOf(a.sourceMark.tag);
|
483
|
-
const bPrecedence = tagPrecedence.indexOf(b.sourceMark.tag);
|
484
|
-
return bPrecedence - aPrecedence;
|
485
|
-
});
|
486
|
-
|
487
|
-
// for each range, split the current nodes where necessary (no matter if leaf or already a node), wrapping the resulting "inside" parts in a node with the range's tag
|
488
|
-
for (const range of rangesSortedLowToHighPrecedence) {
|
489
|
-
// console.log("currentNodes", currentNodes);
|
490
|
-
const newNodes = currentNodes.flatMap((node) => {
|
491
|
-
const [before, inOrAfter] = splitNode(node, range.start);
|
492
|
-
const [inside, after] = inOrAfter
|
493
|
-
? splitNode(inOrAfter, range.end)
|
494
|
-
: [undefined, undefined];
|
495
|
-
|
496
|
-
return [
|
497
|
-
...(before ? [before] : []),
|
498
|
-
...(inside
|
499
|
-
? [
|
500
|
-
{
|
501
|
-
type: "node" as const,
|
502
|
-
tag: range.sourceMark.tag,
|
503
|
-
start: inside.start,
|
504
|
-
end: inside.end,
|
505
|
-
children: [inside],
|
506
|
-
},
|
507
|
-
]
|
508
|
-
: []),
|
509
|
-
...(after ? [after] : []),
|
510
|
-
];
|
511
|
-
});
|
512
|
-
|
513
|
-
currentNodes = newNodes;
|
514
|
-
}
|
515
|
-
|
516
|
-
return {
|
517
|
-
type: "node",
|
518
|
-
tag: "root",
|
519
|
-
start: 0,
|
520
|
-
end: text.length,
|
521
|
-
children: currentNodes,
|
522
|
-
};
|
523
|
-
}
|
524
|
-
|
525
|
-
get length() {
|
526
|
-
return this.text?.toString().length || 0;
|
527
|
-
}
|
528
|
-
|
529
|
-
/**
|
530
|
-
* Convert a CoRichText to plain text.
|
531
|
-
*/
|
532
|
-
toString() {
|
533
|
-
if (!this.text) return "";
|
534
|
-
return this.text.toString();
|
535
|
-
}
|
536
|
-
}
|
537
|
-
|
538
|
-
/**
|
539
|
-
* Represents a leaf node in the rich text tree structure.
|
540
|
-
* Contains plain text without any marks.
|
541
|
-
*/
|
542
|
-
export type TreeLeaf = {
|
543
|
-
type: "leaf";
|
544
|
-
start: number;
|
545
|
-
end: number;
|
546
|
-
};
|
547
|
-
|
548
|
-
/**
|
549
|
-
* Represents a node in the rich text tree structure.
|
550
|
-
* Can contain other nodes or leaves, and includes formatting information.
|
551
|
-
*/
|
552
|
-
export type TreeNode = {
|
553
|
-
type: "node";
|
554
|
-
tag: string;
|
555
|
-
start: number;
|
556
|
-
end: number;
|
557
|
-
range?: ResolvedAndFocusedMark;
|
558
|
-
children: (TreeNode | TreeLeaf)[];
|
559
|
-
};
|
560
|
-
|
561
|
-
/**
|
562
|
-
* 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.
|
563
|
-
*/
|
564
|
-
export function splitNode(
|
565
|
-
node: TreeNode | TreeLeaf,
|
566
|
-
at: number,
|
567
|
-
): [TreeNode | TreeLeaf | undefined, TreeNode | TreeLeaf | undefined] {
|
568
|
-
if (node.type === "leaf") {
|
569
|
-
return [
|
570
|
-
at > node.start
|
571
|
-
? {
|
572
|
-
type: "leaf",
|
573
|
-
start: node.start,
|
574
|
-
end: Math.min(at, node.end),
|
575
|
-
}
|
576
|
-
: undefined,
|
577
|
-
at < node.end
|
578
|
-
? {
|
579
|
-
type: "leaf",
|
580
|
-
start: Math.max(at, node.start),
|
581
|
-
end: node.end,
|
582
|
-
}
|
583
|
-
: undefined,
|
584
|
-
];
|
585
|
-
} else {
|
586
|
-
const children = node.children;
|
587
|
-
return [
|
588
|
-
at > node.start
|
589
|
-
? {
|
590
|
-
type: "node",
|
591
|
-
tag: node.tag,
|
592
|
-
start: node.start,
|
593
|
-
end: Math.min(at, node.end),
|
594
|
-
children: children
|
595
|
-
.map((child) => splitNode(child, at)[0])
|
596
|
-
.filter((c): c is Exclude<typeof c, undefined> => !!c),
|
597
|
-
}
|
598
|
-
: undefined,
|
599
|
-
at < node.end
|
600
|
-
? {
|
601
|
-
type: "node",
|
602
|
-
tag: node.tag,
|
603
|
-
start: Math.max(at, node.start),
|
604
|
-
end: node.end,
|
605
|
-
children: children
|
606
|
-
.map((child) => splitNode(child, at)[1])
|
607
|
-
.filter((c): c is Exclude<typeof c, undefined> => !!c),
|
608
|
-
}
|
609
|
-
: undefined,
|
610
|
-
];
|
611
|
-
}
|
612
|
-
}
|
613
|
-
|
614
|
-
/**
|
615
|
-
* Heading mark for rich text formatting.
|
616
|
-
*/
|
617
|
-
export class Heading extends Mark {
|
618
|
-
tag = co.literal("heading");
|
619
|
-
level = co.number;
|
620
|
-
}
|
621
|
-
|
622
|
-
/**
|
623
|
-
* Paragraph mark for rich text formatting.
|
624
|
-
*/
|
625
|
-
export class Paragraph extends Mark {
|
626
|
-
tag = co.literal("paragraph");
|
627
|
-
}
|
628
|
-
|
629
|
-
/**
|
630
|
-
* Link mark for rich text formatting.
|
631
|
-
*/
|
632
|
-
export class Link extends Mark {
|
633
|
-
tag = co.literal("link");
|
634
|
-
url = co.string;
|
635
|
-
}
|
636
|
-
|
637
|
-
/**
|
638
|
-
* Strong (bold) mark for rich text formatting.
|
639
|
-
*/
|
640
|
-
export class Strong extends Mark {
|
641
|
-
tag = co.literal("strong");
|
642
|
-
}
|
643
|
-
|
644
|
-
/**
|
645
|
-
* Emphasis (italic) mark for rich text formatting.
|
646
|
-
*/
|
647
|
-
export class Em extends Mark {
|
648
|
-
tag = co.literal("em");
|
649
|
-
}
|
650
|
-
|
651
|
-
/**
|
652
|
-
* Collection of predefined mark types for common text formatting.
|
653
|
-
* Includes marks for headings, paragraphs, links, and text styling.
|
654
|
-
*/
|
655
|
-
export const Marks = {
|
656
|
-
Heading,
|
657
|
-
Paragraph,
|
658
|
-
Link,
|
659
|
-
Strong,
|
660
|
-
Em,
|
661
|
-
};
|
3
|
+
export class CoRichText extends CoPlainText {}
|
package/src/exports.ts
CHANGED
@@ -30,13 +30,7 @@ export {
|
|
30
30
|
export { CoList } from "./coValues/coList.js";
|
31
31
|
export { CoMap, type CoMapInit } from "./coValues/coMap.js";
|
32
32
|
export { CoPlainText, type TextPos } from "./coValues/coPlainText.js";
|
33
|
-
export {
|
34
|
-
CoRichText,
|
35
|
-
Marks,
|
36
|
-
type TreeLeaf,
|
37
|
-
type TreeNode,
|
38
|
-
type ResolvedMark,
|
39
|
-
} from "./coValues/coRichText.js";
|
33
|
+
export { CoRichText } from "./coValues/coRichText.js";
|
40
34
|
export { ImageDefinition } from "./coValues/extensions/imageDef.js";
|
41
35
|
export { Group } from "./coValues/group.js";
|
42
36
|
export { CoValueBase } from "./coValues/interfaces.js";
|