svedit 0.8.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.
Files changed (68) hide show
  1. package/README.md +137 -4
  2. package/dist/AnnotatedTextProperty.svelte +41 -8
  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 +18 -3
  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 +65 -42
  13. package/dist/NodeArrayProperty.svelte.d.ts.map +1 -1
  14. package/dist/NodeCaret.svelte +0 -7
  15. package/dist/NodeGap.svelte +179 -81
  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 +340 -147
  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 +64 -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 +5 -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/styles/svedit-animations.css +8 -0
  40. package/dist/styles/svedit-colors.css +1 -0
  41. package/dist/types.d.ts +46 -3
  42. package/dist/utils.d.ts +52 -0
  43. package/dist/utils.d.ts.map +1 -1
  44. package/dist/utils.js +109 -3
  45. package/package.json +3 -2
  46. package/src/lib/AnnotatedTextProperty.svelte +41 -8
  47. package/src/lib/Command.svelte.js +18 -3
  48. package/src/lib/CustomProperty.svelte +20 -9
  49. package/src/lib/Node.svelte +7 -5
  50. package/src/lib/NodeArrayProperty.svelte +65 -42
  51. package/src/lib/NodeCaret.svelte +0 -7
  52. package/src/lib/NodeGap.svelte +179 -81
  53. package/src/lib/NodeGapMarkers.svelte +251 -84
  54. package/src/lib/NodeSelectionMarkers.svelte +4 -3
  55. package/src/lib/Session.svelte.js +40 -3
  56. package/src/lib/Svedit.svelte +340 -147
  57. package/src/lib/Transaction.svelte.js +103 -61
  58. package/src/lib/doc_utils.js +276 -10
  59. package/src/lib/index.js +5 -1
  60. package/src/lib/node_visibility.svelte.js +718 -0
  61. package/src/lib/styles/svedit-animations.css +8 -0
  62. package/src/lib/styles/svedit-colors.css +1 -0
  63. package/src/lib/types.d.ts +46 -3
  64. package/src/lib/utils.js +109 -3
  65. package/dist/node_gap_computation.svelte.d.ts +0 -5
  66. package/dist/node_gap_computation.svelte.d.ts.map +0 -1
  67. package/dist/node_gap_computation.svelte.js +0 -621
  68. package/src/lib/node_gap_computation.svelte.js +0 -621
@@ -1 +1 @@
1
- {"version":3,"file":"Svedit.svelte.d.ts","sourceRoot":"","sources":["../src/lib/Svedit.svelte.js"],"names":[],"mappings":";;;;;;;AA60CA;;eAAqD;iCAhzC1C,cAAc"}
1
+ {"version":3,"file":"Svedit.svelte.d.ts","sourceRoot":"","sources":["../src/lib/Svedit.svelte.js"],"names":[],"mappings":";;;;;;;AAigDA;;eAAqD;iCAh+C1C,cAAc"}
@@ -34,6 +34,12 @@ export default class Transaction {
34
34
  /** @type {Record<string, any>[]} */
35
35
  inverse_ops: Record<string, any>[];
36
36
  selection_before: Selection;
37
+ /** @type {NodeId[]} */
38
+ created_node_ids: NodeId[];
39
+ /** @type {NodeId[]} */
40
+ modified_node_ids: NodeId[];
41
+ /** @type {NodeId[]} */
42
+ deleted_node_ids: NodeId[];
37
43
  /**
38
44
  * Gets a value from the document at the specified path.
39
45
  *
@@ -107,6 +113,12 @@ export default class Transaction {
107
113
  * @private
108
114
  */
109
115
  private _apply_op;
116
+ /**
117
+ * @param {NodeId[]} node_ids
118
+ * @param {NodeId} node_id
119
+ * @private
120
+ */
121
+ private _track_node_id;
110
122
  /**
111
123
  * Sets a property of a node to a new value.
112
124
  *
@@ -129,9 +141,9 @@ export default class Transaction {
129
141
  * Creates a new node in the document.
130
142
  *
131
143
  * The node must have a valid id and must not already exist in the document.
132
- * The node is validated against the document schema before creation.
144
+ * Omitted properties with schema defaults are filled before validation.
133
145
  *
134
- * @param {any} node - The node object to create (must include id, type, and other properties)
146
+ * @param {any} node - The node object to create (must include id, type, and required properties)
135
147
  * @returns {Transaction} This transaction instance for method chaining
136
148
  * @throws {Error} If the node ID is invalid or if the node already exists
137
149
  *
@@ -150,6 +162,13 @@ export default class Transaction {
150
162
  *
151
163
  * The node's current state is captured for undo support before deletion.
152
164
  *
165
+ * NOTE: This is a force-delete and intentionally does NOT check whether
166
+ * the node is still referenced from elsewhere — callers (e.g.
167
+ * `annotate_text`) sometimes need to remove a node while a stale
168
+ * reference to it still exists in the property they are about to update.
169
+ * For ref-count-aware cleanup, see how `set` calls
170
+ * `_cascade_delete_unreferenced_nodes`.
171
+ *
153
172
  * @param {any} id - The ID of the node to delete
154
173
  * @returns {Transaction} This transaction instance for method chaining
155
174
  *
@@ -197,13 +216,13 @@ export default class Transaction {
197
216
  */
198
217
  annotate_text(annotation_type: any, annotation_properties: any): Transaction;
199
218
  /**
200
- * Deletes the currently selected text or nodes.
219
+ * Deletes the currently selected text, node, or property.
201
220
  *
202
221
  * Behavior depends on selection type:
203
222
  * - For node selections: Removes selected nodes and cascades deletion of unreferenced nodes
204
223
  * - For text selections: Removes selected text and adjusts annotations accordingly
205
224
  * - For collapsed selections: Deletes the previous character/node (backward) or next character/node (forward)
206
- * - Property selections are ignored: Those are best handled handled via commands + keyboard shortcuts.
225
+ * - For property selections: Delegates to the config's handle_property_deletion hook
207
226
  *
208
227
  * @param {'backward' | 'forward'} [direction] - Direction of deletion for collapsed selections
209
228
  * @returns {Transaction} This transaction instance for method chaining
@@ -259,8 +278,8 @@ export default class Transaction {
259
278
  }
260
279
  import type { DocumentSchema } from './types.d.js';
261
280
  import type { Selection } from './types.d.js';
262
- import type { DocumentPath } from './types.d.js';
263
281
  import type { NodeId } from './types.d.js';
282
+ import type { DocumentPath } from './types.d.js';
264
283
  import type { Annotation } from './types.d.js';
265
284
  import type { Document } from './types.d.js';
266
285
  //# sourceMappingURL=Transaction.svelte.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Transaction.svelte.d.ts","sourceRoot":"","sources":["../src/lib/Transaction.svelte.js"],"names":[],"mappings":"AAwBA;;;;;;;;;;;;;;GAcG;AACH;IACC;;;;;;;OAOG;IACH,oBALW,cAAc,OACd,QAAQ,aACR,SAAS,GAAG,IAAI,UAChB,MAAM,EAgBhB;IAbA,uBAAoB;IACpB,qBAAqB;IACrB,KADW,MAAM,CACH;IACd,wBAAwB;IACxB,WADW,SAAS,CACM;IAC1B,YAAoB;IAEpB,oCAAoC;IACpC,KADW,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,CACnB;IACb,oCAAoC;IACpC,aADW,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,CACX;IAErB,4BAAiC;IAGlC;;;;;OAKG;IACH,UAHW,YAAY,GAAC,MAAM,GACjB,GAAG,CAIf;IAED;;;;;;OAMG;IACH,oBAJW,MAAM,YACN,MAAM,GACJ,MAAM,CAIlB;IAED;;;;;OAKG;IACH,WAHW,GAAG,GACD,OAAO,GAAC,MAAM,GAAC,YAAY,CAIvC;IAED;;;;;OAKG;IACH,cAHW,YAAY,GACV;QAAC,IAAI,EAAE,UAAU,GAAC,MAAM,CAAC;QAAC,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;KAAC,CAIzD;IAED;;;;OAIG;IACH,eAFa,MAAM,CAIlB;IAED;;;;;OAKG;IACH,oBAHW,GAAG,QAKb;IAED;;;;;OAKG;IACH,8BAHW,MAAM,GACJ,MAAM,EAAE,CAKpB;IAED;;;;OAIG;IACH,kCAFa,MAAM,EAAE,CAOpB;IAED;;;;;;OAMG;IACH,oCAHW,MAAM,GACJ,UAAU,GAAC,IAAI,CAI3B;IAED;;;;;OAKG;IACH,kBAEC;IAED;;;;;;;;;;;;;;;OAeG;IACH,UAVW,YAAY,SACZ,GAAG,GACD,WAAW,CAmDvB;IAKD,qCAsDC;IAED;;;;;;;;;;;;;;;;;;OAkBG;IACH,aAbW,GAAG,GACD,WAAW,CAyBvB;IAED;;;;;;;;;;;;OAYG;IACH,WARW,GAAG,GACD,WAAW,CAsBvB;IAED;;;;;;OAMG;IACH,yBAJW,SAAS,GACP,WAAW,CAOvB;IAED;;;;;;OAMG;IACH,4BAEC;IAED;;;;;;;;;;;;;;;;;;;OAmBG;IACH,+BAbW,GAAG,yBACH,GAAG,GACD,WAAW,CA+DvB;IAED;;;;;;;;;;;OAWG;IACH,6BAHW,UAAU,GAAG,SAAS,GACpB,WAAW,CA0IvB;IAED;;;;;;;;OAQG;IACH,uBAHW,MAAM,EAAE,GACN,WAAW,CA2BvB;IAED;;;;;;;;;;;;OAYG;IACH,2BALW,MAAM,+BAEN,MAAM,GACJ,WAAW,CA2FvB;IAED;;;;;;;;;OASG;IACH,2CAgCC;IAED;;;;;;;;;;OAUG;IACH,4CAOC;CACD;oCAlyByF,cAAc;+BAAd,cAAc;kCAAd,cAAc;4BAAd,cAAc;gCAAd,cAAc;8BAAd,cAAc"}
1
+ {"version":3,"file":"Transaction.svelte.d.ts","sourceRoot":"","sources":["../src/lib/Transaction.svelte.js"],"names":[],"mappings":"AA0BA;;;;;;;;;;;;;;GAcG;AACH;IACC;;;;;;;OAOG;IACH,oBALW,cAAc,OACd,QAAQ,aACR,SAAS,GAAG,IAAI,UAChB,MAAM,EAuBhB;IApBA,uBAAoB;IACpB,qBAAqB;IACrB,KADW,MAAM,CACH;IACd,wBAAwB;IACxB,WADW,SAAS,CACM;IAC1B,YAAoB;IAEpB,oCAAoC;IACpC,KADW,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,CACnB;IACb,oCAAoC;IACpC,aADW,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,CACX;IAErB,4BAAiC;IAEjC,uBAAuB;IACvB,kBADW,MAAM,EAAE,CACO;IAC1B,uBAAuB;IACvB,mBADW,MAAM,EAAE,CACQ;IAC3B,uBAAuB;IACvB,kBADW,MAAM,EAAE,CACO;IAG3B;;;;;OAKG;IACH,UAHW,YAAY,GAAC,MAAM,GACjB,GAAG,CAIf;IAED;;;;;;OAMG;IACH,oBAJW,MAAM,YACN,MAAM,GACJ,MAAM,CAIlB;IAED;;;;;OAKG;IACH,WAHW,GAAG,GACD,OAAO,GAAC,MAAM,GAAC,YAAY,CAIvC;IAED;;;;;OAKG;IACH,cAHW,YAAY,GACV;QAAC,IAAI,EAAE,UAAU,GAAC,MAAM,CAAC;QAAC,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;KAAC,CAIzD;IAED;;;;OAIG;IACH,eAFa,MAAM,CAIlB;IAED;;;;;OAKG;IACH,oBAHW,GAAG,QAKb;IAGD;;;;;OAKG;IACH,8BAHW,MAAM,GACJ,MAAM,EAAE,CAKpB;IAED;;;;OAIG;IACH,kCAFa,MAAM,EAAE,CAOpB;IAED;;;;;;OAMG;IACH,oCAHW,MAAM,GACJ,UAAU,GAAC,IAAI,CAI3B;IAED;;;;;OAKG;IACH,kBAEC;IAED;;;;OAIG;IACH,uBAEC;IAED;;;;;;;;;;;;;;;OAeG;IACH,UAVW,YAAY,SACZ,GAAG,GACD,WAAW,CA8DvB;IAKD,qCAkCC;IAED;;;;;;;;;;;;;;;;;;OAkBG;IACH,aAbW,GAAG,GACD,WAAW,CA4BvB;IAED;;;;;;;;;;;;;;;;;;;OAmBG;IACH,WARW,GAAG,GACD,WAAW,CAuBvB;IAED;;;;;;OAMG;IACH,yBAJW,SAAS,GACP,WAAW,CAOvB;IAED;;;;;;OAMG;IACH,4BAEC;IAED;;;;;;;;;;;;;;;;;;;OAmBG;IACH,+BAbW,GAAG,yBACH,GAAG,GACD,WAAW,CA+EvB;IAED;;;;;;;;;;;OAWG;IACH,6BAHW,UAAU,GAAG,SAAS,GACpB,WAAW,CA8IvB;IAED;;;;;;;;OAQG;IACH,uBAHW,MAAM,EAAE,GACN,WAAW,CA2BvB;IAED;;;;;;;;;;;;OAYG;IACH,2BALW,MAAM,+BAEN,MAAM,GACJ,WAAW,CA2FvB;IAED;;;;;;;;;OASG;IACH,2CAiCC;IAED;;;;;;;;;;OAUG;IACH,4CAOC;CACD;oCA50ByF,cAAc;+BAAd,cAAc;4BAAd,cAAc;kCAAd,cAAc;gCAAd,cAAc;8BAAd,cAAc"}
@@ -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
  }
@@ -3,10 +3,10 @@
3
3
  * Similar to your define_schema pattern but for document schemas.
4
4
  *
5
5
  * @template {Record<string, NodeSchema>} S
6
- * @param {S} schema - The document schema to validate
6
+ * @param {S & ValidateDocumentSchema<S>} schema - The document schema to validate
7
7
  * @returns {S} The same schema, but with type information preserved
8
8
  */
9
- export function define_document_schema<S extends Record<string, NodeSchema>>(schema: S): S;
9
+ export function define_document_schema<S extends Record<string, NodeSchema>>(schema: S & ValidateDocumentSchema<S>): S;
10
10
  /**
11
11
  * Check if a string represents a valid primitive type.
12
12
  * @param {string} type - The type string to check
@@ -19,20 +19,61 @@ export function is_primitive_type(type: string): type is PrimitiveType;
19
19
  * @returns {string | null} The default node type, or null if none specified
20
20
  */
21
21
  export function get_default_node_type(property_definition: NodeProperty | NodeArrayProperty): string | null;
22
+ /**
23
+ * Fill omitted properties with schema defaults and Svedit's built-in type defaults.
24
+ *
25
+ * This is a convenience helper for schema evolution, not a complete document
26
+ * migration system. Callers are still responsible for proper migrations when
27
+ * schema changes cannot be represented by defaults, such as property renames or
28
+ * data transformations.
29
+ *
30
+ * @param {any} node - The node to fill defaults for
31
+ * @param {DocumentSchema} schema - The document schema
32
+ * @returns {any} A shallow copy of the node with cloned default values filled in
33
+ */
34
+ export function fill_node_defaults(node: any, schema: DocumentSchema): any;
35
+ /**
36
+ * Fill omitted properties with schema defaults and Svedit's built-in type defaults across a document.
37
+ *
38
+ * This does not infer values, rename fields, or make an invalid migration valid
39
+ * by itself. Call this as one step in an explicit document migration when it is
40
+ * appropriate.
41
+ *
42
+ * @param {Document} doc - The document to fill defaults for
43
+ * @param {DocumentSchema} schema - The document schema
44
+ * @returns {Document} A document copy with cloned default values filled in
45
+ */
46
+ export function fill_document_defaults(doc: Document, schema: DocumentSchema): Document;
22
47
  /**
23
48
  * Validate a document schema to ensure all referenced node types exist.
24
49
  * @param {DocumentSchema} document_schema - The document schema to validate
25
50
  * @throws {Error} Throws if the document schema is invalid
26
51
  */
27
52
  export function validate_document_schema(document_schema: DocumentSchema): void;
53
+ /**
54
+ * @param {string} id
55
+ * @returns {boolean}
56
+ */
57
+ export function is_id_valid(id: string): boolean;
28
58
  /**
29
59
  * Validate a node against its schema.
30
60
  * @param {any} node - The node to validate
31
61
  * @param {DocumentSchema} schema - The document schema
32
62
  * @param {Record<string, any>} all_nodes - All nodes in the document to check references
63
+ * @param {{ require_references?: boolean }} [options] - Validation options
33
64
  * @throws {Error} Throws if the node is invalid
34
65
  */
35
- export function validate_node(node: any, schema: DocumentSchema, all_nodes?: Record<string, any>): void;
66
+ export function validate_node(node: any, schema: DocumentSchema, all_nodes?: Record<string, any>, options?: {
67
+ require_references?: boolean;
68
+ }): void;
69
+ /**
70
+ * Validate a document against its schema.
71
+ *
72
+ * @param {Document} doc - The document to validate
73
+ * @param {DocumentSchema} schema - The document schema
74
+ * @throws {Error} Throws if the document is invalid
75
+ */
76
+ export function validate_document(doc: Document, schema: DocumentSchema): void;
36
77
  /**
37
78
  * Gets a value from the document at the specified path.
38
79
  *
@@ -59,6 +100,16 @@ export function property_type(schema: object, type: string, property: string): s
59
100
  * @returns {'block'|'text'|'annotation'} The node kind
60
101
  */
61
102
  export function kind(schema: object, node: any): "block" | "text" | "annotation";
103
+ /**
104
+ * Checks whether an annotation node can be switched to another annotation type
105
+ * without losing data.
106
+ *
107
+ * @param {DocumentSchema} schema - The document schema
108
+ * @param {any} annotation_node - The annotation node to switch
109
+ * @param {string} target_annotation_type - The target annotation type
110
+ * @returns {boolean} True if the annotation can be switched safely
111
+ */
112
+ export function can_switch_annotation_type(schema: DocumentSchema, annotation_node: any, target_annotation_type: string): boolean;
62
113
  /**
63
114
  * Inspects a path to get metadata about the value at that location.
64
115
  *
@@ -114,6 +165,15 @@ export function get_active_annotation(schema: DocumentSchema, doc: Document, sel
114
165
  * @returns {number} The number of references excluding deleted nodes
115
166
  */
116
167
  export function count_references_excluding_deleted(schema: DocumentSchema, doc: Document, target_node_id: NodeId, nodes_to_delete: Record<NodeId, boolean>): number;
168
+ /**
169
+ * Gets ids of nodes that reference any of the target nodes.
170
+ *
171
+ * @param {DocumentSchema} schema - The document schema
172
+ * @param {Document} doc - The document containing nodes
173
+ * @param {Iterable<NodeId>} target_node_ids - Node IDs to find referrers for
174
+ * @returns {NodeId[]} IDs of nodes with references to any target node
175
+ */
176
+ export function get_referencing_node_ids(schema: DocumentSchema, doc: Document, target_node_ids: Iterable<NodeId>): NodeId[];
117
177
  /**
118
178
  * Validates a selection against the current document state.
119
179
  * Works with any object that implements get() and inspect() (Session or Transaction).
@@ -127,6 +187,7 @@ export function validate_selection(selection: Selection, session_or_transaction:
127
187
  inspect: Function;
128
188
  }): void;
129
189
  import type { NodeSchema } from './types';
190
+ import type { ValidateDocumentSchema } from './types';
130
191
  import type { PrimitiveType } from './types';
131
192
  import type { NodeProperty } from './types';
132
193
  import type { NodeArrayProperty } from './types';
@@ -1 +1 @@
1
- {"version":3,"file":"doc_utils.d.ts","sourceRoot":"","sources":["../src/lib/doc_utils.js"],"names":[],"mappings":"AAWA;;;;;;;GAOG;AACH,uCAJ0C,CAAC,SAA7B,MAAM,CAAC,MAAM,EAAE,UAAU,CAAE,UAC9B,CAAC,GACC,CAAC,CAIb;AAED;;;;GAIG;AACH,wCAHW,MAAM,GACJ,IAAI,IAAI,aAAa,CAejC;AAED;;;;GAIG;AACH,2DAHW,YAAY,GAAG,iBAAiB,GAC9B,MAAM,GAAG,IAAI,CAWzB;AAED;;;;GAIG;AACH,0DAHW,cAAc,QAmBxB;AAgDD;;;;;;GAMG;AACH,oCALW,GAAG,UACH,cAAc,cACd,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,QA6D7B;AAED;;;;;;;GAOG;AACH,4BALW,cAAc,OACd,QAAQ,QACR,YAAY,GAAC,MAAM,GACjB,GAAG,CA4Ef;AAED;;;;;;;GAOG;AACH,sCALW,MAAM,QACN,MAAM,YACN,MAAM,GACJ,MAAM,CAclB;AAED;;;;;;GAMG;AACH,6BAJW,MAAM,QACN,GAAG,GACD,OAAO,GAAC,MAAM,GAAC,YAAY,CAIvC;AAED;;;;;;;GAOG;AACH,gCALW,cAAc,OACd,QAAQ,QACR,YAAY,GACV;IAAC,IAAI,EAAE,UAAU,GAAC,MAAM,CAAC;IAAC,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;CAAC,CAoBzD;AAED;;;;;;;GAOG;AACH,8BAJW,QAAQ,cAEN,QAAQ,CAkCpB;AAED;;;;;;;GAOG;AACH,yCALW,cAAc,OACd,QAAQ,WACR,MAAM,GACJ,MAAM,CAoBlB;AAED;;;;;;;;;;;;;GAaG;AACH,8CANW,cAAc,OACd,QAAQ,cACR,SAAS,oBACT,MAAM,GACJ,UAAU,GAAG,IAAI,CAwB7B;AAED;;;;;;;;GAQG;AACH,2DANW,cAAc,OACd,QAAQ,kBACR,MAAM,mBACN,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACrB,MAAM,CA0BlB;AAED;;;;;;;GAOG;AACH,8CAJW,SAAS,0BACT;IAAE,GAAG,WAAW;IAAC,OAAO,WAAU;CAAE,QAoD9C;gCAthBqJ,SAAS;mCAAT,SAAS;kCAAT,SAAS;uCAAT,SAAS;oCAAT,SAAS;8BAAT,SAAS;kCAAT,SAAS;4BAAT,SAAS;+BAAT,SAAS;gCAAT,SAAS"}
1
+ {"version":3,"file":"doc_utils.d.ts","sourceRoot":"","sources":["../src/lib/doc_utils.js"],"names":[],"mappings":"AAiBA;;;;;;;GAOG;AACH,uCAJ0C,CAAC,SAA7B,MAAM,CAAC,MAAM,EAAE,UAAU,CAAE,UAC9B,CAAC,GAAG,uBAAuB,CAAC,CAAC,GAC3B,CAAC,CAIb;AAED;;;;GAIG;AACH,wCAHW,MAAM,GACJ,IAAI,IAAI,aAAa,CAejC;AAED;;;;GAIG;AACH,2DAHW,YAAY,GAAG,iBAAiB,GAC9B,MAAM,GAAG,IAAI,CAWzB;AA2BD;;;;;;;;;;;GAWG;AACH,yCAJW,GAAG,UACH,cAAc,GACZ,GAAG,CAgBf;AAED;;;;;;;;;;GAUG;AACH,4CAJW,QAAQ,UACR,cAAc,GACZ,QAAQ,CAcpB;AAED;;;;GAIG;AACH,0DAHW,cAAc,QAoBxB;AAoHD;;;GAGG;AACH,gCAHW,MAAM,GACJ,OAAO,CAInB;AAED;;;;;;;GAOG;AACH,oCANW,GAAG,UACH,cAAc,cACd,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,YACnB;IAAE,kBAAkB,CAAC,EAAE,OAAO,CAAA;CAAE,QAwF1C;AAED;;;;;;GAMG;AACH,uCAJW,QAAQ,UACR,cAAc,QAUxB;AAED;;;;;;;GAOG;AACH,4BALW,cAAc,OACd,QAAQ,QACR,YAAY,GAAC,MAAM,GACjB,GAAG,CA4Ef;AAED;;;;;;;GAOG;AACH,sCALW,MAAM,QACN,MAAM,YACN,MAAM,GACJ,MAAM,CAclB;AAED;;;;;;GAMG;AACH,6BAJW,MAAM,QACN,GAAG,GACD,OAAO,GAAC,MAAM,GAAC,YAAY,CAIvC;AAED;;;;;;;;GAQG;AACH,mDALW,cAAc,mBACd,GAAG,0BACH,MAAM,GACJ,OAAO,CAgBnB;AAED;;;;;;;GAOG;AACH,gCALW,cAAc,OACd,QAAQ,QACR,YAAY,GACV;IAAC,IAAI,EAAE,UAAU,GAAC,MAAM,CAAC;IAAC,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;CAAC,CAoBzD;AAED;;;;;;;GAOG;AACH,8BAJW,QAAQ,cAEN,QAAQ,CAkCpB;AAED;;;;;;;GAOG;AACH,yCALW,cAAc,OACd,QAAQ,WACR,MAAM,GACJ,MAAM,CAoBlB;AAED;;;;;;;;;;;;;GAaG;AACH,8CANW,cAAc,OACd,QAAQ,cACR,SAAS,oBACT,MAAM,GACJ,UAAU,GAAG,IAAI,CAwB7B;AAED;;;;;;;;GAQG;AACH,2DANW,cAAc,OACd,QAAQ,kBACR,MAAM,mBACN,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACrB,MAAM,CA0BlB;AAED;;;;;;;GAOG;AACH,iDALW,cAAc,OACd,QAAQ,mBACR,QAAQ,CAAC,MAAM,CAAC,GACd,MAAM,EAAE,CA4BpB;AAED;;;;;;;GAOG;AACH,8CAJW,SAAS,0BACT;IAAE,GAAG,WAAW;IAAC,OAAO,WAAU;CAAE,QAoD9C;gCAhyBmN,SAAS;4CAAT,SAAS;mCAAT,SAAS;kCAAT,SAAS;uCAAT,SAAS;oCAAT,SAAS;8BAAT,SAAS;kCAAT,SAAS;4BAAT,SAAS;+BAAT,SAAS;gCAAT,SAAS"}