svedit 0.9.0 → 0.11.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.
Files changed (72) hide show
  1. package/README.md +476 -364
  2. package/dist/AnnotatedTextProperty.svelte +40 -10
  3. package/dist/AnnotatedTextProperty.svelte.d.ts.map +1 -1
  4. package/dist/Command.svelte.d.ts +0 -1
  5. package/dist/Command.svelte.d.ts.map +1 -1
  6. package/dist/Command.svelte.js +48 -6
  7. package/dist/CustomProperty.svelte +20 -9
  8. package/dist/CustomProperty.svelte.d.ts +2 -1
  9. package/dist/CustomProperty.svelte.d.ts.map +1 -1
  10. package/dist/Node.svelte +7 -5
  11. package/dist/Node.svelte.d.ts.map +1 -1
  12. package/dist/NodeArrayProperty.svelte +59 -48
  13. package/dist/NodeArrayProperty.svelte.d.ts.map +1 -1
  14. package/dist/NodeCaret.svelte +0 -7
  15. package/dist/NodeGap.svelte +208 -118
  16. package/dist/NodeGap.svelte.d.ts +0 -2
  17. package/dist/NodeGap.svelte.d.ts.map +1 -1
  18. package/dist/NodeGapMarkers.svelte +251 -84
  19. package/dist/NodeGapMarkers.svelte.d.ts +2 -0
  20. package/dist/NodeGapMarkers.svelte.d.ts.map +1 -1
  21. package/dist/NodeSelectionMarkers.svelte +4 -3
  22. package/dist/NodeSelectionMarkers.svelte.d.ts.map +1 -1
  23. package/dist/Session.svelte.d.ts +12 -1
  24. package/dist/Session.svelte.d.ts.map +1 -1
  25. package/dist/Session.svelte.js +40 -3
  26. package/dist/Svedit.svelte +496 -221
  27. package/dist/Svedit.svelte.d.ts.map +1 -1
  28. package/dist/Transaction.svelte.d.ts +24 -5
  29. package/dist/Transaction.svelte.d.ts.map +1 -1
  30. package/dist/Transaction.svelte.js +103 -61
  31. package/dist/doc_utils.d.ts +69 -3
  32. package/dist/doc_utils.d.ts.map +1 -1
  33. package/dist/doc_utils.js +276 -10
  34. package/dist/index.d.ts +1 -1
  35. package/dist/index.js +6 -1
  36. package/dist/node_visibility.svelte.d.ts +9 -0
  37. package/dist/node_visibility.svelte.d.ts.map +1 -0
  38. package/dist/node_visibility.svelte.js +718 -0
  39. package/dist/paste_utils.d.ts +46 -0
  40. package/dist/paste_utils.d.ts.map +1 -0
  41. package/dist/paste_utils.js +167 -0
  42. package/dist/styles/svedit-animations.css +8 -0
  43. package/dist/styles/svedit-colors.css +1 -0
  44. package/dist/types.d.ts +46 -3
  45. package/dist/utils.d.ts +52 -0
  46. package/dist/utils.d.ts.map +1 -1
  47. package/dist/utils.js +109 -3
  48. package/package.json +3 -2
  49. package/src/lib/AnnotatedTextProperty.svelte +40 -10
  50. package/src/lib/Command.svelte.js +48 -6
  51. package/src/lib/CustomProperty.svelte +20 -9
  52. package/src/lib/Node.svelte +7 -5
  53. package/src/lib/NodeArrayProperty.svelte +59 -48
  54. package/src/lib/NodeCaret.svelte +0 -7
  55. package/src/lib/NodeGap.svelte +208 -118
  56. package/src/lib/NodeGapMarkers.svelte +251 -84
  57. package/src/lib/NodeSelectionMarkers.svelte +4 -3
  58. package/src/lib/Session.svelte.js +40 -3
  59. package/src/lib/Svedit.svelte +496 -221
  60. package/src/lib/Transaction.svelte.js +103 -61
  61. package/src/lib/doc_utils.js +276 -10
  62. package/src/lib/index.js +6 -1
  63. package/src/lib/node_visibility.svelte.js +718 -0
  64. package/src/lib/paste_utils.js +167 -0
  65. package/src/lib/styles/svedit-animations.css +8 -0
  66. package/src/lib/styles/svedit-colors.css +1 -0
  67. package/src/lib/types.d.ts +46 -3
  68. package/src/lib/utils.js +109 -3
  69. package/dist/node_gap_computation.svelte.d.ts +0 -5
  70. package/dist/node_gap_computation.svelte.d.ts.map +0 -1
  71. package/dist/node_gap_computation.svelte.js +0 -621
  72. 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
- for (const removed_node_id of removed_node_ids) {
223
- // NOTE: This implicitly deletes childnodes as well, given that they are no longer referenced.
224
- this.delete(removed_node_id);
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
- const new_node = { ...node, id: new_id };
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 = node[property_name];
248
-
249
- // Apply default values
250
- if (prop_type === 'node_array') {
251
- // [] is the default value for node arrays
252
- new_node[property_name] = Array.isArray(value)
253
- ? value.map((ref_id) => id_map[ref_id])
254
- : [];
255
- } else if (prop_type === 'node') {
256
- // null is the default value for node references
257
- new_node[property_name] = typeof value === 'string' ? id_map[value] : null;
258
- } else if (prop_type === 'annotated_text') {
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
- * The node is validated against the document schema before creation.
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 other properties)
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(node);
322
+ this.validate_node(node_with_defaults);
311
323
 
312
- if (this.get(node.id)) {
313
- throw new Error('Node with id ' + node.id + ' already exists');
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', node];
328
+ const op = ['create', node_with_defaults];
317
329
  this.ops.push(op);
318
- this.inverse_ops.push(['delete', node.id]);
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
- // If there's an annotation of a different type, don't add a new one
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.log(`Annotation type ${annotation_type} is not allowed here.`);
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 nodes.
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
- * - Property selections are ignored: Those are best handled handled via commands + keyboard shortcuts.
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 || this.selection.type === 'property') return this;
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
  }