svedit 0.11.0 → 0.12.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/README.md +230 -64
- package/dist/Command.svelte.d.ts +21 -5
- package/dist/Command.svelte.d.ts.map +1 -1
- package/dist/Command.svelte.js +86 -30
- package/dist/KeyMapper.svelte.d.ts.map +1 -1
- package/dist/KeyMapper.svelte.js +18 -6
- package/dist/Node.svelte +29 -1
- package/dist/Node.svelte.d.ts.map +1 -1
- package/dist/NodeArrayProperty.svelte +119 -12
- package/dist/NodeArrayProperty.svelte.d.ts.map +1 -1
- package/dist/NodeGap.svelte +48 -6
- package/dist/NodeGap.svelte.d.ts.map +1 -1
- package/dist/Session.svelte.d.ts +31 -14
- package/dist/Session.svelte.d.ts.map +1 -1
- package/dist/Session.svelte.js +155 -77
- package/dist/Svedit.svelte +110 -89
- package/dist/Svedit.svelte.d.ts.map +1 -1
- package/dist/{AnnotatedTextProperty.svelte → TextProperty.svelte} +45 -44
- package/dist/TextProperty.svelte.d.ts +7 -0
- package/dist/TextProperty.svelte.d.ts.map +1 -0
- package/dist/Transaction.svelte.d.ts +78 -40
- package/dist/Transaction.svelte.d.ts.map +1 -1
- package/dist/Transaction.svelte.js +412 -234
- package/dist/doc_utils.d.ts +96 -21
- package/dist/doc_utils.d.ts.map +1 -1
- package/dist/doc_utils.js +440 -168
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/node_visibility.svelte.d.ts +30 -4
- package/dist/node_visibility.svelte.d.ts.map +1 -1
- package/dist/node_visibility.svelte.js +327 -363
- package/dist/paste_utils.d.ts +3 -2
- package/dist/paste_utils.d.ts.map +1 -1
- package/dist/paste_utils.js +16 -13
- package/dist/transforms.svelte.d.ts.map +1 -1
- package/dist/transforms.svelte.js +5 -6
- package/dist/types.d.ts +112 -42
- package/dist/utils.d.ts +89 -13
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +320 -85
- package/package.json +4 -1
- package/src/lib/Command.svelte.js +86 -30
- package/src/lib/KeyMapper.svelte.js +18 -6
- package/src/lib/Node.svelte +29 -1
- package/src/lib/NodeArrayProperty.svelte +119 -12
- package/src/lib/NodeGap.svelte +48 -6
- package/src/lib/Session.svelte.js +155 -77
- package/src/lib/Svedit.svelte +110 -89
- package/src/lib/{AnnotatedTextProperty.svelte → TextProperty.svelte} +45 -44
- package/src/lib/Transaction.svelte.js +412 -234
- package/src/lib/doc_utils.js +440 -168
- package/src/lib/index.js +1 -1
- package/src/lib/node_visibility.svelte.js +327 -363
- package/src/lib/paste_utils.js +16 -13
- package/src/lib/transforms.svelte.js +5 -6
- package/src/lib/types.d.ts +112 -42
- package/src/lib/utils.js +320 -85
- package/dist/AnnotatedTextProperty.svelte.d.ts +0 -7
- package/dist/AnnotatedTextProperty.svelte.d.ts.map +0 -1
package/src/lib/utils.js
CHANGED
|
@@ -1,24 +1,45 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @import { Annotation, AnnotatedText, SelectionRange, Selection } from './types.d.ts';
|
|
2
|
+
* @import { Attachment, Mark, Annotation, AnnotatedText, SelectionRange, Selection } from './types.d.ts';
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
const SEGMENTER = new Intl.Segmenter('en', { granularity: 'grapheme' });
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* Detect if the
|
|
8
|
+
* Detect if the virtual keyboard is likely visible.
|
|
9
|
+
*
|
|
10
|
+
* This is a heuristic based on the visual viewport becoming smaller than the
|
|
11
|
+
* layout viewport.
|
|
12
|
+
*
|
|
13
|
+
* @returns {boolean} true if the virtual keyboard is likely active, false otherwise
|
|
14
|
+
*/
|
|
15
|
+
export function is_virtual_keyboard_active() {
|
|
16
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const visual_viewport = window.visualViewport;
|
|
21
|
+
if (!visual_viewport) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return visual_viewport.height < document.documentElement.clientHeight;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Detect if the current browser is likely on a mobile device.
|
|
30
|
+
*
|
|
31
|
+
* This uses the user agent only so touch-capable laptops are not treated as
|
|
32
|
+
* mobile browsers.
|
|
33
|
+
*
|
|
9
34
|
* @returns {boolean} true if mobile browser, false otherwise
|
|
10
35
|
*/
|
|
11
36
|
export function is_mobile_browser() {
|
|
12
|
-
if (typeof
|
|
37
|
+
if (typeof navigator === 'undefined') {
|
|
13
38
|
return false;
|
|
14
39
|
}
|
|
15
40
|
|
|
16
41
|
const user_agent = navigator.userAgent;
|
|
17
|
-
return (
|
|
18
|
-
/iPhone|iPad|iPod|Android|Mobile/i.test(user_agent) ||
|
|
19
|
-
'ontouchstart' in window ||
|
|
20
|
-
navigator.maxTouchPoints > 0
|
|
21
|
-
);
|
|
42
|
+
return /iPhone|iPad|iPod|Android|Mobile/i.test(user_agent);
|
|
22
43
|
}
|
|
23
44
|
|
|
24
45
|
// ‼️‼️‼️‼️‼️‼️ UNUSED UTILITY BELOW ‼️‼️‼️‼️‼️‼️
|
|
@@ -155,110 +176,109 @@ export function char_to_utf16_offset(str, char_offset) {
|
|
|
155
176
|
/**
|
|
156
177
|
* Splits an annotated text at the specified character position.
|
|
157
178
|
*
|
|
158
|
-
*
|
|
159
|
-
* with offsets adjusted for each resulting part.
|
|
179
|
+
* Marks and annotations that span the split point will be divided
|
|
180
|
+
* appropriately, with offsets adjusted for each resulting part.
|
|
160
181
|
*
|
|
161
|
-
* @param {AnnotatedText}
|
|
182
|
+
* @param {AnnotatedText} text_value - Annotated text object
|
|
162
183
|
* @param {number} at_position - Character position where to split (0-based)
|
|
163
184
|
* @returns {[AnnotatedText, AnnotatedText]} Tuple of [left_part, right_part]
|
|
164
185
|
*
|
|
165
186
|
* @example
|
|
166
|
-
*
|
|
187
|
+
* split_text({content: "Hello world", marks: [{start_offset: 6, end_offset: 11, node_id: "strong"}], annotations: []}, 8)
|
|
167
188
|
* // Returns:
|
|
168
189
|
* // [
|
|
169
|
-
* // {
|
|
170
|
-
* // {
|
|
190
|
+
* // {content: "Hello wo", marks: [{start_offset: 6, end_offset: 8, node_id: "strong"}], annotations: []},
|
|
191
|
+
* // {content: "rld", marks: [{start_offset: 0, end_offset: 3, node_id: "strong"}], annotations: []}
|
|
171
192
|
* // ]
|
|
172
193
|
*/
|
|
173
|
-
export function
|
|
174
|
-
const {
|
|
194
|
+
export function split_text(text_value, at_position) {
|
|
195
|
+
const { content } = text_value;
|
|
175
196
|
|
|
176
197
|
// Split the text using character-aware slicing
|
|
177
|
-
const
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
node_id
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
// Annotation spans the split point - split it
|
|
199
|
-
left_annotations.push({ start_offset, end_offset: at_position, node_id });
|
|
200
|
-
right_annotations.push({ start_offset: 0, end_offset: end_offset - at_position, node_id });
|
|
198
|
+
const left = { content: char_slice(content, 0, at_position), marks: [], annotations: [] };
|
|
199
|
+
const right = { content: char_slice(content, at_position), marks: [], annotations: [] };
|
|
200
|
+
|
|
201
|
+
// Process marks and annotations with the same range logic
|
|
202
|
+
for (const key of ['marks', 'annotations']) {
|
|
203
|
+
for (const { start_offset, end_offset, node_id } of text_value[key] ?? []) {
|
|
204
|
+
if (end_offset <= at_position) {
|
|
205
|
+
// Range is entirely in the left part
|
|
206
|
+
left[key].push({ start_offset, end_offset, node_id });
|
|
207
|
+
} else if (start_offset >= at_position) {
|
|
208
|
+
// Range is entirely in the right part - shift offsets
|
|
209
|
+
right[key].push({
|
|
210
|
+
start_offset: start_offset - at_position,
|
|
211
|
+
end_offset: end_offset - at_position,
|
|
212
|
+
node_id
|
|
213
|
+
});
|
|
214
|
+
} else {
|
|
215
|
+
// Range spans the split point - split it
|
|
216
|
+
left[key].push({ start_offset, end_offset: at_position, node_id });
|
|
217
|
+
right[key].push({ start_offset: 0, end_offset: end_offset - at_position, node_id });
|
|
218
|
+
}
|
|
201
219
|
}
|
|
202
220
|
}
|
|
203
221
|
|
|
204
|
-
return [
|
|
205
|
-
{ text: left_text, annotations: left_annotations },
|
|
206
|
-
{ text: right_text, annotations: right_annotations }
|
|
207
|
-
];
|
|
222
|
+
return [left, right];
|
|
208
223
|
}
|
|
209
224
|
|
|
210
225
|
/**
|
|
211
226
|
* Joins two annotated texts into a single annotated text.
|
|
212
227
|
*
|
|
213
|
-
*
|
|
214
|
-
* of the first text. Adjacent
|
|
228
|
+
* Marks and annotations from the second text will have their offsets shifted
|
|
229
|
+
* by the length of the first text. Adjacent ranges referencing the same node
|
|
230
|
+
* will be merged.
|
|
215
231
|
*
|
|
216
232
|
* @param {AnnotatedText} first_text - First annotated text object
|
|
217
233
|
* @param {AnnotatedText} second_text - Second annotated text object
|
|
218
234
|
* @returns {AnnotatedText} Combined annotated text object
|
|
219
235
|
*
|
|
220
236
|
* @example
|
|
221
|
-
*
|
|
222
|
-
* // Returns: {
|
|
237
|
+
* join_text({content: "Hello wo", marks: [{start_offset: 6, end_offset: 8, node_id: "strong"}], annotations: []}, {content: "rld", marks: [{start_offset: 0, end_offset: 3, node_id: "strong"}], annotations: []})
|
|
238
|
+
* // Returns: {content: "Hello world", marks: [{start_offset: 6, end_offset: 11, node_id: "strong"}], annotations: []}
|
|
223
239
|
*/
|
|
224
|
-
export function
|
|
225
|
-
const { text: first_text_content, annotations: first_annotations } = first_text;
|
|
226
|
-
const { text: second_text_content, annotations: second_annotations } = second_text;
|
|
227
|
-
|
|
240
|
+
export function join_text(first_text, second_text) {
|
|
228
241
|
// Join the text content
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
// Add annotations from the second text, shifting their offsets
|
|
236
|
-
const offset = get_char_length(first_text_content);
|
|
237
|
-
for (const { start_offset, end_offset, node_id } of second_annotations) {
|
|
238
|
-
/** @type {Annotation} */
|
|
239
|
-
const shifted_annotation = {
|
|
240
|
-
start_offset: start_offset + offset,
|
|
241
|
-
end_offset: end_offset + offset,
|
|
242
|
-
node_id
|
|
243
|
-
};
|
|
242
|
+
const joined = {
|
|
243
|
+
content: first_text.content + second_text.content,
|
|
244
|
+
marks: [],
|
|
245
|
+
annotations: []
|
|
246
|
+
};
|
|
244
247
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
248
|
+
const offset = get_char_length(first_text.content);
|
|
249
|
+
|
|
250
|
+
for (const key of ['marks', 'annotations']) {
|
|
251
|
+
// Start with all ranges from the first text (unchanged)
|
|
252
|
+
const joined_ranges = (first_text[key] ?? []).map((range) => ({ ...range }));
|
|
253
|
+
|
|
254
|
+
// Add ranges from the second text, shifting their offsets
|
|
255
|
+
for (const { start_offset, end_offset, node_id } of second_text[key] ?? []) {
|
|
256
|
+
const shifted_range = {
|
|
257
|
+
start_offset: start_offset + offset,
|
|
258
|
+
end_offset: end_offset + offset,
|
|
259
|
+
node_id
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
// Check if this range can be merged with the last range from the first text
|
|
263
|
+
const last_range = joined_ranges[joined_ranges.length - 1];
|
|
264
|
+
if (
|
|
265
|
+
last_range &&
|
|
266
|
+
last_range.end_offset === shifted_range.start_offset && // ranges are adjacent
|
|
267
|
+
last_range.node_id === shifted_range.node_id
|
|
268
|
+
) {
|
|
269
|
+
// same node_id
|
|
270
|
+
// Merge by extending the end position of the last range
|
|
271
|
+
last_range.end_offset = shifted_range.end_offset;
|
|
272
|
+
} else {
|
|
273
|
+
// Add as separate range
|
|
274
|
+
joined_ranges.push(shifted_range);
|
|
275
|
+
}
|
|
258
276
|
}
|
|
277
|
+
|
|
278
|
+
joined[key] = joined_ranges;
|
|
259
279
|
}
|
|
260
280
|
|
|
261
|
-
return
|
|
281
|
+
return joined;
|
|
262
282
|
}
|
|
263
283
|
|
|
264
284
|
/**
|
|
@@ -384,6 +404,51 @@ export function paths_equal(a, b) {
|
|
|
384
404
|
return a.every((segment, index) => segment === b[index]);
|
|
385
405
|
}
|
|
386
406
|
|
|
407
|
+
/**
|
|
408
|
+
* Like traverse, but returns only node ids in depth-first order and never
|
|
409
|
+
* clones nodes. Use this when only the reachable id set is needed (e.g.
|
|
410
|
+
* reference bookkeeping) — traverse() deep-clones every visited node,
|
|
411
|
+
* which is wasteful for id-only consumers.
|
|
412
|
+
*
|
|
413
|
+
* @param {string} node_id - The ID of the node to start traversing from
|
|
414
|
+
* @param {object} schema - The document schema
|
|
415
|
+
* @param {Record<string, any>} nodes - All document nodes
|
|
416
|
+
* @returns {string[]} Node ids in depth-first order (entry point last)
|
|
417
|
+
*/
|
|
418
|
+
export function traverse_ids(node_id, schema, nodes) {
|
|
419
|
+
const ids = [];
|
|
420
|
+
const visited = {};
|
|
421
|
+
const visit = (node) => {
|
|
422
|
+
if (!node || visited[node.id]) {
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
visited[node.id] = true;
|
|
426
|
+
for (const [property_name, value] of Object.entries(node)) {
|
|
427
|
+
const property_definition = schema[node.type].properties[property_name];
|
|
428
|
+
|
|
429
|
+
if (property_definition?.type === 'node_array') {
|
|
430
|
+
for (const v of value?.nodes || []) {
|
|
431
|
+
if (typeof v === 'string') {
|
|
432
|
+
visit(nodes[v]);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
for (const range of [...(value?.marks || []), ...(value?.annotations || [])]) {
|
|
436
|
+
visit(nodes[range.node_id]);
|
|
437
|
+
}
|
|
438
|
+
} else if (property_definition?.type === 'node') {
|
|
439
|
+
visit(nodes[value]);
|
|
440
|
+
} else if (property_definition?.type === 'text') {
|
|
441
|
+
for (const range of [...(value.marks || []), ...(value.annotations || [])]) {
|
|
442
|
+
visit(nodes[range.node_id]);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
ids.push(node.id);
|
|
447
|
+
};
|
|
448
|
+
visit(nodes[node_id]);
|
|
449
|
+
return ids;
|
|
450
|
+
}
|
|
451
|
+
|
|
387
452
|
export function traverse(node_id, schema, nodes) {
|
|
388
453
|
const json = [];
|
|
389
454
|
const visited = {};
|
|
@@ -396,16 +461,22 @@ export function traverse(node_id, schema, nodes) {
|
|
|
396
461
|
const property_definition = schema[node.type].properties[property_name];
|
|
397
462
|
|
|
398
463
|
if (property_definition?.type === 'node_array') {
|
|
399
|
-
|
|
464
|
+
const node_ids = value?.nodes || [];
|
|
465
|
+
|
|
466
|
+
for (const v of node_ids) {
|
|
400
467
|
if (typeof v === 'string') {
|
|
401
468
|
visit(nodes[v]);
|
|
402
469
|
}
|
|
403
470
|
}
|
|
471
|
+
|
|
472
|
+
for (const range of [...(value?.marks || []), ...(value?.annotations || [])]) {
|
|
473
|
+
visit(nodes[range.node_id]);
|
|
474
|
+
}
|
|
404
475
|
} else if (property_definition?.type === 'node') {
|
|
405
476
|
visit(nodes[value]);
|
|
406
|
-
} else if (property_definition?.type === '
|
|
407
|
-
for (const
|
|
408
|
-
visit(nodes[
|
|
477
|
+
} else if (property_definition?.type === 'text') {
|
|
478
|
+
for (const range of [...(value.marks || []), ...(value.annotations || [])]) {
|
|
479
|
+
visit(nodes[range.node_id]);
|
|
409
480
|
}
|
|
410
481
|
}
|
|
411
482
|
}
|
|
@@ -442,3 +513,167 @@ export function is_selection_collapsed(selection) {
|
|
|
442
513
|
return false;
|
|
443
514
|
}
|
|
444
515
|
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Adjust ranges (marks or annotations) after deleting a sequence range.
|
|
519
|
+
*
|
|
520
|
+
* Works for both annotated text (character offsets) and annotated node arrays
|
|
521
|
+
* (node offsets).
|
|
522
|
+
*
|
|
523
|
+
* @param {Array<import('./types.d.ts').Attachment>} ranges
|
|
524
|
+
* @param {number} start
|
|
525
|
+
* @param {number} end
|
|
526
|
+
* @returns {{ranges: Array<import('./types.d.ts').Attachment>, removed_node_ids: string[]}}
|
|
527
|
+
*/
|
|
528
|
+
export function adjust_ranges_for_deletion(ranges, start, end) {
|
|
529
|
+
const removed_node_ids = [];
|
|
530
|
+
const deletion_length = end - start;
|
|
531
|
+
|
|
532
|
+
const next_ranges = ranges
|
|
533
|
+
.map((range) => {
|
|
534
|
+
if (range.end_offset <= start) return range;
|
|
535
|
+
|
|
536
|
+
let start_offset = range.start_offset;
|
|
537
|
+
if (range.start_offset >= end) {
|
|
538
|
+
start_offset -= deletion_length;
|
|
539
|
+
} else if (range.start_offset > start) {
|
|
540
|
+
start_offset = start;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
let end_offset = range.end_offset;
|
|
544
|
+
if (range.end_offset >= end) {
|
|
545
|
+
end_offset -= deletion_length;
|
|
546
|
+
} else if (range.end_offset > start) {
|
|
547
|
+
end_offset = start;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (start_offset >= end_offset) {
|
|
551
|
+
removed_node_ids.push(range.node_id);
|
|
552
|
+
return null;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
return { start_offset, end_offset, node_id: range.node_id };
|
|
556
|
+
})
|
|
557
|
+
.filter(Boolean);
|
|
558
|
+
|
|
559
|
+
return { ranges: next_ranges, removed_node_ids };
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Adjust ranges (marks or annotations) after inserting into a sequence.
|
|
564
|
+
*
|
|
565
|
+
* Insertion inside a range extends it. Insertion exactly at either edge
|
|
566
|
+
* stays outside the range.
|
|
567
|
+
*
|
|
568
|
+
* @param {Array<import('./types.d.ts').Attachment>} ranges
|
|
569
|
+
* @param {number} offset
|
|
570
|
+
* @param {number} length
|
|
571
|
+
* @returns {Array<import('./types.d.ts').Attachment>}
|
|
572
|
+
*/
|
|
573
|
+
export function adjust_ranges_for_insertion(ranges, offset, length) {
|
|
574
|
+
if (length === 0) return ranges;
|
|
575
|
+
|
|
576
|
+
return ranges.map((range) => {
|
|
577
|
+
if (range.end_offset <= offset) return range;
|
|
578
|
+
|
|
579
|
+
if (range.start_offset < offset) {
|
|
580
|
+
return {
|
|
581
|
+
start_offset: range.start_offset,
|
|
582
|
+
end_offset: range.end_offset + length,
|
|
583
|
+
node_id: range.node_id
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
return {
|
|
588
|
+
start_offset: range.start_offset + length,
|
|
589
|
+
end_offset: range.end_offset + length,
|
|
590
|
+
node_id: range.node_id
|
|
591
|
+
};
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Check whether ranges are non-empty and mutually exclusive.
|
|
597
|
+
*
|
|
598
|
+
* @param {Array<import('./types.d.ts').Attachment>} ranges
|
|
599
|
+
* @param {number} [length]
|
|
600
|
+
* @returns {boolean}
|
|
601
|
+
*/
|
|
602
|
+
export function are_ranges_exclusive(ranges, length = Infinity) {
|
|
603
|
+
const sorted = [...ranges].sort(
|
|
604
|
+
(a, b) => a.start_offset - b.start_offset || a.end_offset - b.end_offset
|
|
605
|
+
);
|
|
606
|
+
|
|
607
|
+
return sorted.every(
|
|
608
|
+
(range, index) =>
|
|
609
|
+
Number.isInteger(range.start_offset) &&
|
|
610
|
+
Number.isInteger(range.end_offset) &&
|
|
611
|
+
range.start_offset >= 0 &&
|
|
612
|
+
range.start_offset < range.end_offset &&
|
|
613
|
+
range.end_offset <= length &&
|
|
614
|
+
(index === 0 || range.start_offset >= sorted[index - 1].end_offset)
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Calculates abstract fragment ranges from a length and marks.
|
|
620
|
+
*
|
|
621
|
+
* @param {number} length - Length of the sequence (e.g. text length or array length)
|
|
622
|
+
* @param {Array<import('./types.d.ts').Mark>} marks - Array of marks
|
|
623
|
+
* @param {import('./types.d.ts').SelectionRange} [selection_highlight_range] - Optional selection highlight range
|
|
624
|
+
* @returns {Array<{type: 'content' | 'mark' | 'selection_highlight', start_offset: number, end_offset: number, mark_index?: number, node_id?: string}>}
|
|
625
|
+
*/
|
|
626
|
+
export function calculate_fragment_ranges(length, marks, selection_highlight_range) {
|
|
627
|
+
/** @type {Array<{type: 'content' | 'mark' | 'selection_highlight', start_offset: number, end_offset: number, mark_index?: number, node_id?: string}>} */
|
|
628
|
+
const fragments = [];
|
|
629
|
+
let last_index = 0;
|
|
630
|
+
|
|
631
|
+
// Merge marks with selection highlight and sort by start offset
|
|
632
|
+
const ranges = [...marks, ...(selection_highlight_range ? [selection_highlight_range] : [])].sort(
|
|
633
|
+
(a, b) => a.start_offset - b.start_offset
|
|
634
|
+
);
|
|
635
|
+
|
|
636
|
+
for (const range of ranges) {
|
|
637
|
+
// Add content before this range
|
|
638
|
+
if (range.start_offset > last_index) {
|
|
639
|
+
fragments.push({
|
|
640
|
+
type: 'content',
|
|
641
|
+
start_offset: last_index,
|
|
642
|
+
end_offset: range.start_offset
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if ('node_id' in range) {
|
|
647
|
+
const mark = /** @type {import('./types.d.ts').Mark} */ (range);
|
|
648
|
+
const mark_index =
|
|
649
|
+
/** @type {import('./types.d.ts').Mark & { mark_index?: number }} */ (mark).mark_index ??
|
|
650
|
+
marks.indexOf(mark);
|
|
651
|
+
fragments.push({
|
|
652
|
+
type: 'mark',
|
|
653
|
+
start_offset: mark.start_offset,
|
|
654
|
+
end_offset: mark.end_offset,
|
|
655
|
+
node_id: mark.node_id,
|
|
656
|
+
mark_index
|
|
657
|
+
});
|
|
658
|
+
} else {
|
|
659
|
+
fragments.push({
|
|
660
|
+
type: 'selection_highlight',
|
|
661
|
+
start_offset: range.start_offset,
|
|
662
|
+
end_offset: range.end_offset
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
last_index = range.end_offset;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// Add any remaining content
|
|
670
|
+
if (last_index < length) {
|
|
671
|
+
fragments.push({
|
|
672
|
+
type: 'content',
|
|
673
|
+
start_offset: last_index,
|
|
674
|
+
end_offset: length
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
return fragments;
|
|
679
|
+
}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
export default AnnotatedTextProperty;
|
|
2
|
-
type AnnotatedTextProperty = {
|
|
3
|
-
$on?(type: string, callback: (e: any) => void): () => void;
|
|
4
|
-
$set?(props: Partial<AnnotatedTextPropertyProps>): void;
|
|
5
|
-
};
|
|
6
|
-
declare const AnnotatedTextProperty: import("svelte").Component<import("./types.d.ts").AnnotatedTextPropertyProps, {}, "">;
|
|
7
|
-
//# sourceMappingURL=AnnotatedTextProperty.svelte.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"AnnotatedTextProperty.svelte.d.ts","sourceRoot":"","sources":["../src/lib/AnnotatedTextProperty.svelte.js"],"names":[],"mappings":";;;;;AAwIA,2HAAoE"}
|