svedit 0.9.0 → 0.10.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 +113 -4
- package/dist/AnnotatedTextProperty.svelte +41 -8
- package/dist/AnnotatedTextProperty.svelte.d.ts.map +1 -1
- package/dist/Command.svelte.d.ts +0 -1
- package/dist/Command.svelte.d.ts.map +1 -1
- package/dist/Command.svelte.js +18 -3
- package/dist/CustomProperty.svelte +20 -9
- package/dist/CustomProperty.svelte.d.ts +2 -1
- package/dist/CustomProperty.svelte.d.ts.map +1 -1
- package/dist/Node.svelte +7 -5
- package/dist/Node.svelte.d.ts.map +1 -1
- package/dist/NodeArrayProperty.svelte +65 -42
- package/dist/NodeArrayProperty.svelte.d.ts.map +1 -1
- package/dist/NodeCaret.svelte +0 -7
- package/dist/NodeGap.svelte +179 -81
- package/dist/NodeGap.svelte.d.ts +0 -2
- package/dist/NodeGap.svelte.d.ts.map +1 -1
- package/dist/NodeGapMarkers.svelte +251 -84
- package/dist/NodeGapMarkers.svelte.d.ts +2 -0
- package/dist/NodeGapMarkers.svelte.d.ts.map +1 -1
- package/dist/NodeSelectionMarkers.svelte +4 -3
- package/dist/NodeSelectionMarkers.svelte.d.ts.map +1 -1
- package/dist/Session.svelte.d.ts +12 -1
- package/dist/Session.svelte.d.ts.map +1 -1
- package/dist/Session.svelte.js +40 -3
- package/dist/Svedit.svelte +336 -165
- package/dist/Svedit.svelte.d.ts.map +1 -1
- package/dist/Transaction.svelte.d.ts +24 -5
- package/dist/Transaction.svelte.d.ts.map +1 -1
- package/dist/Transaction.svelte.js +103 -61
- package/dist/doc_utils.d.ts +64 -3
- package/dist/doc_utils.d.ts.map +1 -1
- package/dist/doc_utils.js +276 -10
- package/dist/index.d.ts +1 -1
- package/dist/index.js +5 -1
- package/dist/node_visibility.svelte.d.ts +9 -0
- package/dist/node_visibility.svelte.d.ts.map +1 -0
- package/dist/node_visibility.svelte.js +718 -0
- package/dist/styles/svedit-animations.css +8 -0
- package/dist/styles/svedit-colors.css +1 -0
- package/dist/types.d.ts +46 -3
- package/dist/utils.d.ts +52 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +109 -3
- package/package.json +3 -2
- package/src/lib/AnnotatedTextProperty.svelte +41 -8
- package/src/lib/Command.svelte.js +18 -3
- package/src/lib/CustomProperty.svelte +20 -9
- package/src/lib/Node.svelte +7 -5
- package/src/lib/NodeArrayProperty.svelte +65 -42
- package/src/lib/NodeCaret.svelte +0 -7
- package/src/lib/NodeGap.svelte +179 -81
- package/src/lib/NodeGapMarkers.svelte +251 -84
- package/src/lib/NodeSelectionMarkers.svelte +4 -3
- package/src/lib/Session.svelte.js +40 -3
- package/src/lib/Svedit.svelte +336 -165
- package/src/lib/Transaction.svelte.js +103 -61
- package/src/lib/doc_utils.js +276 -10
- package/src/lib/index.js +5 -1
- package/src/lib/node_visibility.svelte.js +718 -0
- package/src/lib/styles/svedit-animations.css +8 -0
- package/src/lib/styles/svedit-colors.css +1 -0
- package/src/lib/types.d.ts +46 -3
- package/src/lib/utils.js +109 -3
- package/dist/node_gap_computation.svelte.d.ts +0 -5
- package/dist/node_gap_computation.svelte.d.ts.map +0 -1
- package/dist/node_gap_computation.svelte.js +0 -621
- package/src/lib/node_gap_computation.svelte.js +0 -621
|
@@ -18,8 +18,10 @@ import {
|
|
|
18
18
|
apply_op,
|
|
19
19
|
count_references_excluding_deleted,
|
|
20
20
|
validate_node,
|
|
21
|
+
fill_node_defaults,
|
|
21
22
|
get_active_annotation,
|
|
22
|
-
validate_selection
|
|
23
|
+
validate_selection,
|
|
24
|
+
can_switch_annotation_type
|
|
23
25
|
} from './doc_utils.js';
|
|
24
26
|
|
|
25
27
|
/**
|
|
@@ -60,6 +62,13 @@ export default class Transaction {
|
|
|
60
62
|
this.inverse_ops = [];
|
|
61
63
|
// Remember the selection before the transaction started
|
|
62
64
|
this.selection_before = selection;
|
|
65
|
+
|
|
66
|
+
/** @type {NodeId[]} */
|
|
67
|
+
this.created_node_ids = [];
|
|
68
|
+
/** @type {NodeId[]} */
|
|
69
|
+
this.modified_node_ids = [];
|
|
70
|
+
/** @type {NodeId[]} */
|
|
71
|
+
this.deleted_node_ids = [];
|
|
63
72
|
}
|
|
64
73
|
|
|
65
74
|
/**
|
|
@@ -119,9 +128,10 @@ export default class Transaction {
|
|
|
119
128
|
* @throws {Error} Throws if the node is invalid
|
|
120
129
|
*/
|
|
121
130
|
validate_node(node) {
|
|
122
|
-
validate_node(node, this.schema, this.doc.nodes);
|
|
131
|
+
validate_node(node, this.schema, this.doc.nodes, { require_references: false });
|
|
123
132
|
}
|
|
124
133
|
|
|
134
|
+
|
|
125
135
|
/**
|
|
126
136
|
* Gets all nodes referenced by a given node (recursively).
|
|
127
137
|
*
|
|
@@ -166,6 +176,15 @@ export default class Transaction {
|
|
|
166
176
|
this.doc = apply_op(this.doc, op);
|
|
167
177
|
}
|
|
168
178
|
|
|
179
|
+
/**
|
|
180
|
+
* @param {NodeId[]} node_ids
|
|
181
|
+
* @param {NodeId} node_id
|
|
182
|
+
* @private
|
|
183
|
+
*/
|
|
184
|
+
_track_node_id(node_ids, node_id) {
|
|
185
|
+
if (!node_ids.includes(node_id)) node_ids.push(node_id);
|
|
186
|
+
}
|
|
187
|
+
|
|
169
188
|
/**
|
|
170
189
|
* Sets a property of a node to a new value.
|
|
171
190
|
*
|
|
@@ -183,6 +202,13 @@ export default class Transaction {
|
|
|
183
202
|
* ```
|
|
184
203
|
*/
|
|
185
204
|
set(path, value) {
|
|
205
|
+
const path_info = this.inspect(path);
|
|
206
|
+
if (path_info?.kind !== 'property') {
|
|
207
|
+
throw new Error(
|
|
208
|
+
`Transaction.set requires a path that points to a property, got ${JSON.stringify(path)}`
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
186
212
|
const node = this.get(path.slice(0, -1));
|
|
187
213
|
|
|
188
214
|
// Turns ["page_1", "body", "0", "description"]
|
|
@@ -192,9 +218,6 @@ export default class Transaction {
|
|
|
192
218
|
|
|
193
219
|
// Just to be sure, make a deep copy of the old value
|
|
194
220
|
const property_key = path.at(-1);
|
|
195
|
-
if (property_key === undefined) {
|
|
196
|
-
throw new Error('Invalid path: cannot get property key');
|
|
197
|
-
}
|
|
198
221
|
const property_key_str = String(property_key);
|
|
199
222
|
const previous_value = structuredClone($state.snapshot(node[property_key_str]));
|
|
200
223
|
|
|
@@ -218,10 +241,17 @@ export default class Transaction {
|
|
|
218
241
|
this.ops.push(op);
|
|
219
242
|
this.inverse_ops.push(['set', normalized_path, previous_value]);
|
|
220
243
|
this._apply_op(op);
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
244
|
+
this._track_node_id(this.modified_node_ids, node.id);
|
|
245
|
+
|
|
246
|
+
// Garbage-collect nodes whose only reference was via this property.
|
|
247
|
+
// We must use a ref-count-aware sweep instead of unconditional deletion
|
|
248
|
+
// because a node ID removed from this array may still be referenced
|
|
249
|
+
// elsewhere — either from a pre-existing reference (e.g. shared nodes)
|
|
250
|
+
// or from a node created earlier in the same transaction (the
|
|
251
|
+
// wrap-in-group pattern: create a wrapper that points at the children,
|
|
252
|
+
// then set the parent array to drop them).
|
|
253
|
+
if (removed_node_ids.length > 0) {
|
|
254
|
+
this._cascade_delete_unreferenced_nodes(removed_node_ids);
|
|
225
255
|
}
|
|
226
256
|
|
|
227
257
|
return this;
|
|
@@ -238,48 +268,28 @@ export default class Transaction {
|
|
|
238
268
|
for (const node of depth_first_nodes) {
|
|
239
269
|
const new_id = this.generate_id();
|
|
240
270
|
id_map[node.id] = new_id;
|
|
241
|
-
|
|
271
|
+
let new_node = { ...node, id: new_id };
|
|
242
272
|
const node_schema = this.schema[node.type];
|
|
243
273
|
|
|
244
274
|
// Update all property references to use new IDs
|
|
245
275
|
for (const [property_name, property_definition] of Object.entries(node_schema.properties)) {
|
|
246
276
|
const prop_type = property_definition.type;
|
|
247
|
-
const value =
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
new_node[property_name] =
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
if (value) {
|
|
260
|
-
const annotations = value.annotations.map((annotation) => {
|
|
261
|
-
const { start_offset, end_offset, node_id } = annotation;
|
|
262
|
-
return { start_offset, end_offset, node_id: id_map[node_id] || node_id };
|
|
263
|
-
});
|
|
264
|
-
new_node[property_name] = { text: value.text, annotations };
|
|
265
|
-
} else {
|
|
266
|
-
new_node[property_name] = { text: '', annotations: [] };
|
|
267
|
-
}
|
|
268
|
-
} else if (prop_type === 'string') {
|
|
269
|
-
new_node[property_name] = value ?? property_definition.default ?? '';
|
|
270
|
-
} else if (prop_type === 'integer') {
|
|
271
|
-
new_node[property_name] = value ?? property_definition.default ?? 0;
|
|
272
|
-
} else if (prop_type === 'number') {
|
|
273
|
-
new_node[property_name] = value ?? property_definition.default ?? 0;
|
|
274
|
-
} else if (prop_type === 'boolean') {
|
|
275
|
-
new_node[property_name] = value ?? property_definition.default ?? false;
|
|
276
|
-
} else if (['integer_array', 'number_array'].includes(prop_type)) {
|
|
277
|
-
new_node[property_name] = value ?? property_definition.default ?? [];
|
|
278
|
-
} else if (prop_type === 'string_array') {
|
|
279
|
-
new_node[property_name] = value ?? property_definition.default ?? [];
|
|
277
|
+
const value = new_node[property_name];
|
|
278
|
+
|
|
279
|
+
if (prop_type === 'node_array' && Array.isArray(value)) {
|
|
280
|
+
new_node[property_name] = value.map((ref_id) => id_map[ref_id]);
|
|
281
|
+
} else if (prop_type === 'node' && typeof value === 'string') {
|
|
282
|
+
new_node[property_name] = id_map[value];
|
|
283
|
+
} else if (prop_type === 'annotated_text' && value) {
|
|
284
|
+
const annotations = value.annotations.map((annotation) => {
|
|
285
|
+
const { start_offset, end_offset, node_id } = annotation;
|
|
286
|
+
return { start_offset, end_offset, node_id: id_map[node_id] || node_id };
|
|
287
|
+
});
|
|
288
|
+
new_node[property_name] = { text: value.text, annotations };
|
|
280
289
|
}
|
|
281
290
|
}
|
|
282
291
|
|
|
292
|
+
new_node = fill_node_defaults(new_node, this.schema);
|
|
283
293
|
this.create(new_node);
|
|
284
294
|
}
|
|
285
295
|
|
|
@@ -290,9 +300,9 @@ export default class Transaction {
|
|
|
290
300
|
* Creates a new node in the document.
|
|
291
301
|
*
|
|
292
302
|
* The node must have a valid id and must not already exist in the document.
|
|
293
|
-
*
|
|
303
|
+
* Omitted properties with schema defaults are filled before validation.
|
|
294
304
|
*
|
|
295
|
-
* @param {any} node - The node object to create (must include id, type, and
|
|
305
|
+
* @param {any} node - The node object to create (must include id, type, and required properties)
|
|
296
306
|
* @returns {Transaction} This transaction instance for method chaining
|
|
297
307
|
* @throws {Error} If the node ID is invalid or if the node already exists
|
|
298
308
|
*
|
|
@@ -306,17 +316,20 @@ export default class Transaction {
|
|
|
306
316
|
* ```
|
|
307
317
|
*/
|
|
308
318
|
create(node) {
|
|
319
|
+
const node_with_defaults = fill_node_defaults(node, this.schema);
|
|
320
|
+
|
|
309
321
|
// Validate node against schema
|
|
310
|
-
this.validate_node(
|
|
322
|
+
this.validate_node(node_with_defaults);
|
|
311
323
|
|
|
312
|
-
if (this.get(
|
|
313
|
-
throw new Error('Node with id ' +
|
|
324
|
+
if (this.get(node_with_defaults.id)) {
|
|
325
|
+
throw new Error('Node with id ' + node_with_defaults.id + ' already exists');
|
|
314
326
|
}
|
|
315
327
|
|
|
316
|
-
const op = ['create',
|
|
328
|
+
const op = ['create', node_with_defaults];
|
|
317
329
|
this.ops.push(op);
|
|
318
|
-
this.inverse_ops.push(['delete',
|
|
330
|
+
this.inverse_ops.push(['delete', node_with_defaults.id]);
|
|
319
331
|
this._apply_op(op);
|
|
332
|
+
this._track_node_id(this.created_node_ids, node_with_defaults.id);
|
|
320
333
|
return this;
|
|
321
334
|
}
|
|
322
335
|
|
|
@@ -325,6 +338,13 @@ export default class Transaction {
|
|
|
325
338
|
*
|
|
326
339
|
* The node's current state is captured for undo support before deletion.
|
|
327
340
|
*
|
|
341
|
+
* NOTE: This is a force-delete and intentionally does NOT check whether
|
|
342
|
+
* the node is still referenced from elsewhere — callers (e.g.
|
|
343
|
+
* `annotate_text`) sometimes need to remove a node while a stale
|
|
344
|
+
* reference to it still exists in the property they are about to update.
|
|
345
|
+
* For ref-count-aware cleanup, see how `set` calls
|
|
346
|
+
* `_cascade_delete_unreferenced_nodes`.
|
|
347
|
+
*
|
|
328
348
|
* @param {any} id - The ID of the node to delete
|
|
329
349
|
* @returns {Transaction} This transaction instance for method chaining
|
|
330
350
|
*
|
|
@@ -345,6 +365,7 @@ export default class Transaction {
|
|
|
345
365
|
this.ops.push(op);
|
|
346
366
|
this.inverse_ops.push(['create', previous_value]);
|
|
347
367
|
this._apply_op(op);
|
|
368
|
+
this._track_node_id(this.deleted_node_ids, id);
|
|
348
369
|
// Cascade delete any nodes that are now orphaned after removing this node
|
|
349
370
|
this._cascade_delete_unreferenced_nodes(referenced_nodes);
|
|
350
371
|
return this;
|
|
@@ -402,6 +423,8 @@ export default class Transaction {
|
|
|
402
423
|
const annotations = annotated_text.annotations;
|
|
403
424
|
const existing_annotation = this.active_annotation();
|
|
404
425
|
const existing_annotation_same_type = this.active_annotation(annotation_type);
|
|
426
|
+
const has_annotation_properties =
|
|
427
|
+
annotation_properties && Object.keys(annotation_properties).length > 0;
|
|
405
428
|
|
|
406
429
|
if (existing_annotation) {
|
|
407
430
|
// If there's an existing annotation of the same type, remove it
|
|
@@ -417,16 +440,30 @@ export default class Transaction {
|
|
|
417
440
|
annotations.splice(index, 1);
|
|
418
441
|
}
|
|
419
442
|
} else {
|
|
420
|
-
|
|
443
|
+
const existing_annotation_node = this.get(existing_annotation.node_id);
|
|
444
|
+
const can_switch_annotation =
|
|
445
|
+
!has_annotation_properties &&
|
|
446
|
+
this.available_annotation_types.includes(annotation_type) &&
|
|
447
|
+
can_switch_annotation_type(
|
|
448
|
+
this.schema,
|
|
449
|
+
existing_annotation_node,
|
|
450
|
+
annotation_type
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
if (can_switch_annotation) {
|
|
454
|
+
this.set([existing_annotation.node_id, 'type'], annotation_type);
|
|
455
|
+
return this;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// If there's an annotation of a different incompatible type, don't add a new one
|
|
421
459
|
return this;
|
|
422
460
|
}
|
|
423
461
|
} else {
|
|
424
462
|
if (is_selection_collapsed(this.selection)) {
|
|
425
|
-
console.log('Annotations can only be added to expanded text selections.');
|
|
426
463
|
return this;
|
|
427
464
|
}
|
|
428
465
|
if (!this.available_annotation_types.includes(annotation_type)) {
|
|
429
|
-
console.
|
|
466
|
+
console.warn(`Annotation type ${annotation_type} is not allowed here.`);
|
|
430
467
|
return this;
|
|
431
468
|
}
|
|
432
469
|
const new_annotation_node = {
|
|
@@ -449,19 +486,25 @@ export default class Transaction {
|
|
|
449
486
|
}
|
|
450
487
|
|
|
451
488
|
/**
|
|
452
|
-
* Deletes the currently selected text or
|
|
489
|
+
* Deletes the currently selected text, node, or property.
|
|
453
490
|
*
|
|
454
491
|
* Behavior depends on selection type:
|
|
455
492
|
* - For node selections: Removes selected nodes and cascades deletion of unreferenced nodes
|
|
456
493
|
* - For text selections: Removes selected text and adjusts annotations accordingly
|
|
457
494
|
* - For collapsed selections: Deletes the previous character/node (backward) or next character/node (forward)
|
|
458
|
-
* -
|
|
495
|
+
* - For property selections: Delegates to the config's handle_property_deletion hook
|
|
459
496
|
*
|
|
460
497
|
* @param {'backward' | 'forward'} [direction] - Direction of deletion for collapsed selections
|
|
461
498
|
* @returns {Transaction} This transaction instance for method chaining
|
|
462
499
|
*/
|
|
463
500
|
delete_selection(direction = 'backward') {
|
|
464
|
-
if (!this.selection
|
|
501
|
+
if (!this.selection) return this;
|
|
502
|
+
|
|
503
|
+
if (this.selection.type === 'property') {
|
|
504
|
+
this.config.handle_property_deletion?.(this, this.selection.path);
|
|
505
|
+
return this;
|
|
506
|
+
}
|
|
507
|
+
|
|
465
508
|
const path = this.selection.path;
|
|
466
509
|
|
|
467
510
|
// Get the start and end indices for the selection
|
|
@@ -579,13 +622,11 @@ export default class Transaction {
|
|
|
579
622
|
|
|
580
623
|
text.annotations = new_annotations;
|
|
581
624
|
|
|
582
|
-
// Delete marked annotation nodes
|
|
583
|
-
for (const node_id of _deleted_nodes) {
|
|
584
|
-
this.delete(node_id);
|
|
585
|
-
}
|
|
586
|
-
|
|
587
625
|
this.set(path, text);
|
|
588
626
|
|
|
627
|
+
// Delete marked annotation nodes only if no other references remain.
|
|
628
|
+
this._cascade_delete_unreferenced_nodes(_deleted_nodes);
|
|
629
|
+
|
|
589
630
|
// Update the selection to the new caret position
|
|
590
631
|
this.selection = {
|
|
591
632
|
type: 'text',
|
|
@@ -778,6 +819,7 @@ export default class Transaction {
|
|
|
778
819
|
this.ops.push(op);
|
|
779
820
|
this.inverse_ops.push(['create', previous_value]);
|
|
780
821
|
this._apply_op(op);
|
|
822
|
+
this._track_node_id(this.deleted_node_ids, node_id);
|
|
781
823
|
}
|
|
782
824
|
}
|
|
783
825
|
}
|