svedit 0.3.0 → 0.5.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.
@@ -1 +1 @@
1
- {"version":3,"file":"Svedit.svelte.d.ts","sourceRoot":"","sources":["../src/lib/Svedit.svelte.js"],"names":[],"mappings":";;;;;;;AAg3CA;;eAAqD;iCAp2CgE,cAAc"}
1
+ {"version":3,"file":"Svedit.svelte.d.ts","sourceRoot":"","sources":["../src/lib/Svedit.svelte.js"],"names":[],"mappings":";;;;;;;AAk9CA;;eAAqD;iCA97CzC,cAAc"}
@@ -47,14 +47,16 @@ export default class Transaction {
47
47
  *
48
48
  * @param {DocumentPath} path - Array path to the property (e.g., ["node_1", "title"])
49
49
  * @param {any} value - The new value to set
50
+ * @returns {Transaction} This transaction instance for method chaining
50
51
  *
51
52
  * @example
52
53
  * ```js
53
54
  * tr.set(["list_1", "list_items"], [1, 2, 3]);
54
- * tr.set(["page_1", "body", "0", "description"], ["Hello world", []]);
55
+ * tr.set(["page_1", "body", "0", "description"], {text: "Hello world", annotations: []});
55
56
  * ```
56
57
  */
57
- set(path: DocumentPath, value: any): void;
58
+ set(path: DocumentPath, value: any): Transaction;
59
+ build(node_id: any, nodes: any): any;
58
60
  /**
59
61
  * Creates a new node in the document.
60
62
  *
@@ -94,7 +96,7 @@ export default class Transaction {
94
96
  *
95
97
  * @param {Selection} selection - The new selection object
96
98
  * @returns {Transaction} This transaction instance for method chaining
97
- * @todo Check if selection is valid and throw error if not
99
+ * @throws {Error} Throws if the selection is invalid or out of bounds
98
100
  */
99
101
  set_selection(selection: Selection): Transaction;
100
102
  /**
@@ -105,7 +107,7 @@ export default class Transaction {
105
107
  * when conflicting types are applied.
106
108
  *
107
109
  * @param {any} annotation_type - The type of annotation (e.g., 'link', 'bold', 'italic')
108
- * @param {any} annotation_data - Additional data for the annotation (e.g., href for links)
110
+ * @param {any} annotation_properties - Additional data for the annotation (e.g., href for links)
109
111
  * @returns {Transaction} This transaction instance for method chaining
110
112
  *
111
113
  * @example
@@ -117,38 +119,33 @@ export default class Transaction {
117
119
  * tr.annotate_text('emphasis', {});
118
120
  * ```
119
121
  */
120
- annotate_text(annotation_type: any, annotation_data: any): Transaction;
122
+ annotate_text(annotation_type: any, annotation_properties: any): Transaction;
121
123
  /**
122
124
  * Deletes the currently selected text or nodes.
123
125
  *
124
126
  * Behavior depends on selection type:
125
127
  * - For node selections: Removes selected nodes and cascades deletion of unreferenced nodes
126
128
  * - For text selections: Removes selected text and adjusts annotations accordingly
127
- * - For collapsed selections: Deletes the previous character/node
129
+ * - For collapsed selections: Deletes the previous character/node (backward) or next character/node (forward)
128
130
  *
131
+ * @param {'backward' | 'forward'} [direction] - Direction of deletion for collapsed selections
129
132
  * @returns {Transaction} This transaction instance for method chaining
130
133
  */
131
- delete_selection(): Transaction;
134
+ delete_selection(direction?: "backward" | "forward"): Transaction;
132
135
  /**
133
136
  * Inserts an array of nodes at the current node selection.
134
137
  *
135
- * If there's a current selection, it will be deleted first. New nodes are
136
- * created if they don't already exist (e.g., for cut+paste scenarios).
138
+ * If there's a current selection, it will be deleted first.
137
139
  *
138
- * @param {SerializedNode[]} nodes - Array of node objects to insert
140
+ * @param {NodeId[]} node_ids - Array of node IDs to insert
139
141
  * @returns {Transaction} This transaction instance for method chaining
140
142
  *
141
- * @note Assumes we only insert new nodes, not reference existing ones
142
- *
143
143
  * @example
144
144
  * ```js
145
- * tr.insert_nodes([
146
- * {id: 'para_1', type: 'paragraph', content: ['Hello', []]},
147
- * {id: 'para_2', type: 'paragraph', content: ['World', []]}
148
- * ]);
145
+ * tr.insert_nodes_new(['para_1', 'para_2']);
149
146
  * ```
150
147
  */
151
- insert_nodes(nodes: SerializedNode[]): Transaction;
148
+ insert_nodes(node_ids: NodeId[]): Transaction;
152
149
  /**
153
150
  * Inserts text at the current text selection, replacing any selected content.
154
151
  *
@@ -165,7 +162,7 @@ export default class Transaction {
165
162
  * tr.insert_text('Hello, world!');
166
163
  * ```
167
164
  */
168
- insert_text(replaced_text: string): Transaction;
165
+ insert_text(replaced_text: string, annotations?: any[], nodes?: {}): Transaction;
169
166
  /**
170
167
  * Recursively deletes nodes that become unreferenced after the initial deletion.
171
168
  *
@@ -191,5 +188,5 @@ export default class Transaction {
191
188
  }
192
189
  import type { DocumentPath } from './types.js';
193
190
  import type { Selection } from './types.js';
194
- import type { SerializedNode } from './types.js';
191
+ import type { NodeId } from './types.js';
195
192
  //# 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":"AAMA;;;;;;;;;;;;;;GAcG;AACH;IACE;;;;OAIG;IACH,iBAFW,GAAG,EAWb;IARC,SAAc;IAEd,oCAAoC;IACpC,KADW,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,CACnB;IACb,oCAAoC;IACpC,aADW,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,CACX;IAErB,sBAA0C;IAG5C;;;;;OAKG;IACH,UAHW,YAAY,GACV,GAAG,CAIf;IAED;;;;OAIG;IACH,iBAFa,SAAS,CAIrB;IAED;;;;;;;;;;;;;;OAcG;IACH,UATW,YAAY,SACZ,GAAG,QA2Bb;IAED;;;;;;;;;;;;;;;;;;OAkBG;IACH,aAbW,GAAG,GACD,WAAW,CAyBvB;IAED;;;;;;;;;;;;OAYG;IACH,WARW,GAAG,GACD,WAAW,CAcvB;IAED;;;;;;OAMG;IACH,yBAJW,SAAS,GACP,WAAW,CAOvB;IAED;;;;;;;;;;;;;;;;;;;OAmBG;IACH,+BAbW,GAAG,mBACH,GAAG,GACD,WAAW,CAqFvB;IAED;;;;;;;;;OASG;IACH,oBAFa,WAAW,CA+FvB;IAED;;;;;;;;;;;;;;;;;;OAkBG;IACH,oBAbW,cAAc,EAAE,GACd,WAAW,CAqDvB;IAED;;;;;;;;;;;;;;;OAeG;IACH,2BAVW,MAAM,GACJ,WAAW,CA6EvB;IAED;;;;;;;;OAQG;IACH,2CA+BC;IAED;;;;;;;;;;OAUG;IACH,4CAqBC;CACF;kCAzkBmE,YAAY;+BAAZ,YAAY;oCAAZ,YAAY"}
1
+ {"version":3,"file":"Transaction.svelte.d.ts","sourceRoot":"","sources":["../src/lib/Transaction.svelte.js"],"names":[],"mappings":"AAOA;;;;;;;;;;;;;;GAcG;AACH;IACE;;;;OAIG;IACH,iBAFW,GAAG,EAWb;IARC,SAAc;IAEd,oCAAoC;IACpC,KADW,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,CACnB;IACb,oCAAoC;IACpC,aADW,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,CACX;IAErB,sBAA0C;IAG5C;;;;;OAKG;IACH,UAHW,YAAY,GACV,GAAG,CAIf;IAED;;;;OAIG;IACH,iBAFa,SAAS,CAIrB;IAED;;;;;;;;;;;;;;;OAeG;IACH,UAVW,YAAY,SACZ,GAAG,GACD,WAAW,CA4BvB;IAKD,qCAoDC;IAED;;;;;;;;;;;;;;;;;;OAkBG;IACH,aAbW,GAAG,GACD,WAAW,CAyBvB;IAED;;;;;;;;;;;;OAYG;IACH,WARW,GAAG,GACD,WAAW,CAcvB;IAED;;;;;;OAMG;IACH,yBAJW,SAAS,GACP,WAAW,CAOvB;IAED;;;;;;;;;;;;;;;;;;;OAmBG;IACH,+BAbW,GAAG,yBACH,GAAG,GACD,WAAW,CAuDvB;IAED;;;;;;;;;;OAUG;IACH,6BAHW,UAAU,GAAG,SAAS,GACpB,WAAW,CA6IvB;IAED;;;;;;;;;;;;OAYG;IACH,uBARW,MAAM,EAAE,GACN,WAAW,CAyCvB;IAED;;;;;;;;;;;;;;;OAeG;IACH,2BAVW,MAAM,oCACJ,WAAW,CAwGvB;IAED;;;;;;;;OAQG;IACH,2CA+BC;IAED;;;;;;;;;;OAUG;IACH,4CAuBC;CACF;kCAtqBmD,YAAY;+BAAZ,YAAY;4BAAZ,YAAY"}
@@ -1,8 +1,9 @@
1
1
  /**
2
- * @import { NodeId, SerializedNode, Selection, DocumentPath } from './types.js'
2
+ * @import { NodeId, Selection, DocumentPath } from './types.js'
3
3
  */
4
4
 
5
- import { get_char_length, char_slice } from './util.js';
5
+ import { get_char_length, char_slice, traverse } from './util.js';
6
+ import { join_text_node } from './commands.svelte.js';
6
7
 
7
8
  /**
8
9
  * Transaction class for managing atomic document operations with undo/redo support.
@@ -63,11 +64,12 @@ export default class Transaction {
63
64
  *
64
65
  * @param {DocumentPath} path - Array path to the property (e.g., ["node_1", "title"])
65
66
  * @param {any} value - The new value to set
67
+ * @returns {Transaction} This transaction instance for method chaining
66
68
  *
67
69
  * @example
68
70
  * ```js
69
71
  * tr.set(["list_1", "list_items"], [1, 2, 3]);
70
- * tr.set(["page_1", "body", "0", "description"], ["Hello world", []]);
72
+ * tr.set(["page_1", "body", "0", "description"], {text: "Hello world", annotations: []});
71
73
  * ```
72
74
  */
73
75
  set (path, value) {
@@ -89,6 +91,64 @@ export default class Transaction {
89
91
  this.ops.push(op);
90
92
  this.inverse_ops.push(['set', path, previous_value]);
91
93
  this.doc._apply_op(op);
94
+ return this;
95
+ }
96
+
97
+ // Takes a subgraph and constructs new nodes from it
98
+ // NOTE: all ids will be mapped to new unique ids.
99
+ // NOTE: Omitted properties will be populated with default values.
100
+ build(node_id, nodes) {
101
+ const depth_first_nodes = traverse(node_id, this.doc.schema, nodes);
102
+ // This maps original ids to newly generated ids
103
+ const id_map = {};
104
+
105
+ for (const node of depth_first_nodes) {
106
+ const new_id = this.doc.generate_id();
107
+ id_map[node.id] = new_id;
108
+ const new_node = { ...node, id: new_id };
109
+ const node_schema = this.doc.schema[node.type];
110
+
111
+ // Update all property references to use new IDs
112
+ for (const [property_name, property_definition] of Object.entries(node_schema.properties)) {
113
+ const property_type = property_definition.type;
114
+ const value = node[property_name];
115
+
116
+ // Apply default values
117
+ if (property_type === 'node_array') {
118
+ // [] is the default value for node arrays
119
+ new_node[property_name] = Array.isArray(value) ? value.map(ref_id => id_map[ref_id]) : [];
120
+ } else if (property_type === 'node') {
121
+ // null is the default value for node references
122
+ new_node[property_name] = typeof value === 'string' ? id_map[value] : null;
123
+ } else if (property_type === 'annotated_text') {
124
+ if (value) {
125
+ const annotations = value.annotations.map(annotation => {
126
+ const {start_offset, end_offset, node_id} = annotation;
127
+ return {start_offset, end_offset, node_id: id_map[node_id] || node_id};
128
+ });
129
+ new_node[property_name] = {text: value.text, annotations};
130
+ } else {
131
+ new_node[property_name] = {text: '', annotations: []};
132
+ }
133
+ } else if (property_type === 'string') {
134
+ new_node[property_name] = value ?? property_definition.default ?? '';
135
+ } else if (property_type === 'integer') {
136
+ new_node[property_name] = value ?? property_definition.default ?? 0;
137
+ } else if (property_type === 'number') {
138
+ new_node[property_name] = value ?? property_definition.default ?? 0;
139
+ } else if (property_type === 'boolean') {
140
+ new_node[property_name] = value ?? property_definition.default ?? false;
141
+ } else if (['integer_array', 'number_array'].includes(property_type)) {
142
+ new_node[property_name] = value ?? property_definition.default ?? [];
143
+ } else if (property_type === 'string_array') {
144
+ new_node[property_name] = value ?? property_definition.default ?? [];
145
+ }
146
+ }
147
+
148
+ this.create(new_node);
149
+ }
150
+
151
+ return id_map[depth_first_nodes.at(-1).id];
92
152
  }
93
153
 
94
154
  /**
@@ -152,10 +212,10 @@ export default class Transaction {
152
212
  *
153
213
  * @param {Selection} selection - The new selection object
154
214
  * @returns {Transaction} This transaction instance for method chaining
155
- * @todo Check if selection is valid and throw error if not
215
+ * @throws {Error} Throws if the selection is invalid or out of bounds
156
216
  */
157
217
  set_selection(selection) {
158
- // TODO: Check if selection is valid and throw error if not
218
+ this.doc._validate_selection(selection);
159
219
  this.doc.selection = selection;
160
220
  return this;
161
221
  }
@@ -168,7 +228,7 @@ export default class Transaction {
168
228
  * when conflicting types are applied.
169
229
  *
170
230
  * @param {any} annotation_type - The type of annotation (e.g., 'link', 'bold', 'italic')
171
- * @param {any} annotation_data - Additional data for the annotation (e.g., href for links)
231
+ * @param {any} annotation_properties - Additional data for the annotation (e.g., href for links)
172
232
  * @returns {Transaction} This transaction instance for method chaining
173
233
  *
174
234
  * @example
@@ -180,64 +240,25 @@ export default class Transaction {
180
240
  * tr.annotate_text('emphasis', {});
181
241
  * ```
182
242
  */
183
- annotate_text(annotation_type, annotation_data) {
243
+ annotate_text(annotation_type, annotation_properties) {
184
244
  if (this.doc.selection.type !== 'text') return this;
185
245
 
186
246
  const { start, end } = this.doc.get_selection_range();
187
- const annotated_string = structuredClone($state.snapshot(this.doc.get(this.doc.selection.path)));
188
- const annotations = annotated_string[1];
189
- const existing_annotations = this.doc.active_annotation();
190
-
191
- // Special annotation type handling should probably be done in a separate function.
192
- // The goal is to keep the core logic simple and allow developer to extend and pick only what they need.
193
- // It could also be abstracted to not check for type (e.g. "link") but for a special attribute
194
- // e.g. "zero-range-updatable" for annotations that are updatable without a range selection change.
195
-
196
- // Special handling for links when there's no selection range
197
- // Links should be updatable by just clicking on them without a range selection
198
- if (annotation_type === 'link' && start === end && existing_annotations) {
199
-
200
- // Use findIndex for deep comparison of annotation properties (comparison of annotation properties rather than object reference via indexOf)
201
- const index = annotations.findIndex(/** @param {any} anno */ (anno) =>
202
- anno[0] === existing_annotations[0] &&
203
- anno[1] === existing_annotations[1] &&
204
- anno[2] === existing_annotations[2]
205
- );
206
- // const index = annotations.indexOf(existing_annotations);
207
-
208
- if (index !== -1) {
209
- if (annotation_data.href === '') {
210
- // Remove the annotation if the href is empty
211
- annotations.splice(index, 1);
212
- } else {
213
- annotations[index] = [
214
- existing_annotations[0],
215
- existing_annotations[1],
216
- 'link',
217
- { ...existing_annotations[3], ...annotation_data }
218
- ];
219
- }
220
-
221
- this.set(this.doc.selection.path, annotated_string);
222
- return this;
223
- }
224
- }
247
+ const annotated_text = structuredClone($state.snapshot(this.doc.get(this.doc.selection.path)));
248
+ const annotations = annotated_text.annotations;
249
+ const existing_annotation = this.doc.active_annotation();
250
+ const existing_annotation_same_type = this.doc.active_annotation(annotation_type);
225
251
 
226
- // Regular annotation handling
227
- if (start === end) {
228
- // For non-link annotations: You can not annotate text if the selection is collapsed.
229
- return this;
230
- }
231
-
232
- if (existing_annotations) {
252
+ if (existing_annotation) {
233
253
  // If there's an existing annotation of the same type, remove it
234
- if (existing_annotations[2] === annotation_type) {
254
+ if (existing_annotation_same_type) {
235
255
  const index = annotations.findIndex(/** @param {any} anno */ (anno) =>
236
- anno[0] === existing_annotations[0] &&
237
- anno[1] === existing_annotations[1] &&
238
- anno[2] === existing_annotations[2]
256
+ anno.start_offset === existing_annotation.start_offset &&
257
+ anno.end_offset === existing_annotation.end_offset
239
258
  );
240
259
  if (index !== -1) {
260
+ // Remove the annotation node from the graph
261
+ this.delete(annotations[index].node_id);
241
262
  annotations.splice(index, 1);
242
263
  }
243
264
  } else {
@@ -245,14 +266,23 @@ export default class Transaction {
245
266
  return this;
246
267
  }
247
268
  } else {
269
+ if (!this.doc.available_annotation_types.includes(annotation_type)) {
270
+ console.log(`Annotation type ${annotation_type} is not allowed here.`);
271
+ return this;
272
+ }
273
+
274
+ const new_annotation_node = {
275
+ id: this.doc.generate_id(),
276
+ type: annotation_type,
277
+ ...annotation_properties,
278
+ }
279
+ this.create(new_annotation_node);
248
280
  // If there's no existing annotation, add the new one
249
- annotations.push([start, end, annotation_type, annotation_data]);
281
+ annotations.push({start_offset: start, end_offset: end, node_id: new_annotation_node.id});
250
282
  }
251
283
 
252
- // Update the annotated string
253
- this.set(this.doc.selection.path, annotated_string);
254
- // Not needed anymore as doc.apply makes sure selection is rerendered
255
- // this.doc.selection = { ...this.doc.selection };
284
+ // Update the annotated text
285
+ this.set(this.doc.selection.path, annotated_text);
256
286
  return this;
257
287
  }
258
288
 
@@ -262,23 +292,60 @@ export default class Transaction {
262
292
  * Behavior depends on selection type:
263
293
  * - For node selections: Removes selected nodes and cascades deletion of unreferenced nodes
264
294
  * - For text selections: Removes selected text and adjusts annotations accordingly
265
- * - For collapsed selections: Deletes the previous character/node
295
+ * - For collapsed selections: Deletes the previous character/node (backward) or next character/node (forward)
266
296
  *
297
+ * @param {'backward' | 'forward'} [direction] - Direction of deletion for collapsed selections
267
298
  * @returns {Transaction} This transaction instance for method chaining
268
299
  */
269
- delete_selection() {
300
+ delete_selection(direction = 'backward') {
270
301
  if (!this.doc.selection) return this;
271
302
  const path = this.doc.selection.path;
303
+
304
+ if (this.doc.selection.type === 'property') {
305
+ this.set(path, '');
306
+ return this;
307
+ }
308
+
272
309
  // Get the start and end indices for the selection
273
310
  let start = Math.min(this.doc.selection.anchor_offset, this.doc.selection.focus_offset);
274
311
  let end = Math.max(this.doc.selection.anchor_offset, this.doc.selection.focus_offset);
312
+ let length;
313
+
314
+ if (this.doc.selection?.type === 'text') {
315
+ const text_content = this.doc.get(this.doc.selection.path).text;
316
+ length = get_char_length(text_content);
317
+ } else if (this.doc.selection?.type === 'node') {
318
+ const node_array = this.doc.get(this.doc.selection.path);
319
+ length = node_array.length;
320
+ }
275
321
 
276
- // If selection is collapsed we delete the previous node
322
+ // If selection is collapsed we delete the previous char/node (backward)
323
+ // or the next char/node (forward)
277
324
  if (start === end) {
278
- if (start > 0) {
325
+ if (direction === 'backward' && start > 0) {
279
326
  start = start - 1;
280
- } else {
281
- return this; // cursor is at the very beginning, do nothing.
327
+ } else if (direction === 'forward' && end < length) {
328
+ end = end + 1;
329
+ } else if (direction === 'backward' && start === 0) {
330
+ join_text_node(this);
331
+ return this;
332
+ } else if (direction === 'forward' && end === length) {
333
+ // At end of text - try to join with next text node
334
+ const node_index = parseInt(String(this.doc.selection.path.at(-2)), 10);
335
+ const successor_node = this.doc.get([...this.doc.selection.path.slice(0, -2), node_index + 1]);
336
+ // Check if next node is a text node
337
+ if (successor_node && this.doc.kind(successor_node) === 'text') {
338
+ // Set selection to beginning of next text node
339
+ this.set_selection({
340
+ type: 'text',
341
+ path: [...this.doc.selection.path.slice(0, -2), node_index + 1, 'content'],
342
+ anchor_offset: 0,
343
+ focus_offset: 0
344
+ });
345
+ // Use join_text_node to merge with previous node
346
+ join_text_node(this);
347
+ }
348
+ return this;
282
349
  }
283
350
  }
284
351
 
@@ -309,13 +376,15 @@ export default class Transaction {
309
376
  let text = structuredClone($state.snapshot(this.doc.get(path)));
310
377
 
311
378
  // Update the text content using character-based operations
312
- const original_text = text[0];
313
- text[0] = char_slice(original_text, 0, start) + char_slice(original_text, end, get_char_length(original_text));
379
+ const original_text = text.text;
380
+ text.text = char_slice(original_text, 0, start) + char_slice(original_text, end, get_char_length(original_text));
314
381
 
382
+ // To mark annotation nodes for deletion.
383
+ const _deleted_nodes = [];
315
384
  // Update annotation offsets for deletion
316
385
  const deletion_length = end - start;
317
- const new_annotations = text[1].map(/** @param {any} annotation */ (annotation) => {
318
- const [annotation_start, annotation_end, type, annotation_data] = annotation;
386
+ const new_annotations = text.annotations.map(/** @param {any} annotation */ (annotation) => {
387
+ const {start_offset: annotation_start, end_offset: annotation_end, node_id} = annotation;
319
388
 
320
389
  // Adjust start offset
321
390
  let new_start = annotation_start;
@@ -341,16 +410,24 @@ export default class Transaction {
341
410
 
342
411
  // Remove annotation if it becomes invalid (start >= end)
343
412
  if (new_start >= new_end) {
413
+ console.log('deleting annotation');
414
+ _deleted_nodes.push(node_id);
344
415
  return false;
345
416
  }
346
417
 
347
- return [new_start, new_end, type, annotation_data];
418
+ return {start_offset: new_start, end_offset: new_end, node_id};
348
419
  }).filter(Boolean);
349
420
 
350
- text[1] = new_annotations;
421
+ text.annotations = new_annotations;
351
422
 
352
423
  // Update the text in the entry (this implicitly records an op via this.set)
353
424
  this.set(path, text);
425
+
426
+ // Remove annotation nodes that were implicitly deleted.
427
+ for (const node_id of _deleted_nodes) {
428
+ this.delete(node_id);
429
+ }
430
+
354
431
  this.set_selection({
355
432
  type: 'text',
356
433
  path,
@@ -364,23 +441,17 @@ export default class Transaction {
364
441
  /**
365
442
  * Inserts an array of nodes at the current node selection.
366
443
  *
367
- * If there's a current selection, it will be deleted first. New nodes are
368
- * created if they don't already exist (e.g., for cut+paste scenarios).
444
+ * If there's a current selection, it will be deleted first.
369
445
  *
370
- * @param {SerializedNode[]} nodes - Array of node objects to insert
446
+ * @param {NodeId[]} node_ids - Array of node IDs to insert
371
447
  * @returns {Transaction} This transaction instance for method chaining
372
448
  *
373
- * @note Assumes we only insert new nodes, not reference existing ones
374
- *
375
449
  * @example
376
450
  * ```js
377
- * tr.insert_nodes([
378
- * {id: 'para_1', type: 'paragraph', content: ['Hello', []]},
379
- * {id: 'para_2', type: 'paragraph', content: ['World', []]}
380
- * ]);
451
+ * tr.insert_nodes_new(['para_1', 'para_2']);
381
452
  * ```
382
453
  */
383
- insert_nodes(nodes) {
454
+ insert_nodes(node_ids) {
384
455
  if (this.doc.selection.type !== 'node') return this;
385
456
 
386
457
  // Unless cursor is collapsed, delete the selected nodes as a first step
@@ -402,13 +473,6 @@ export default class Transaction {
402
473
  node_array.splice(start, end - start);
403
474
  }
404
475
 
405
- // First create the new nodes (unless they already exists e.g. in case of cut+paste)
406
- nodes.forEach(node => {
407
- if (!this.doc.get(node.id)) {
408
- this.create(node);
409
- }
410
- });
411
- const node_ids = nodes.map(node => node.id);
412
476
  node_array.splice(start, 0, ...node_ids);
413
477
 
414
478
  // Update the node_array in the entry
@@ -418,7 +482,7 @@ export default class Transaction {
418
482
  type: 'node',
419
483
  path: [...this.doc.selection.path],
420
484
  anchor_offset: start,
421
- focus_offset: start + nodes.length
485
+ focus_offset: start + node_ids.length
422
486
  };
423
487
  return this;
424
488
  }
@@ -439,21 +503,21 @@ export default class Transaction {
439
503
  * tr.insert_text('Hello, world!');
440
504
  * ```
441
505
  */
442
- insert_text(replaced_text) {
506
+ insert_text(replaced_text, annotations = [], nodes = {}) {
443
507
  if (this.doc.selection.type !== 'text') return this;
444
508
 
445
- const annotated_string = structuredClone($state.snapshot(this.doc.get(this.doc.selection.path)));
509
+ const annotated_text = structuredClone($state.snapshot(this.doc.get(this.doc.selection.path)));
446
510
  const { start, end } = this.doc.get_selection_range();
447
511
 
448
512
  // Transform the plain text string using character-based operations
449
- const text = annotated_string[0];
450
- annotated_string[0] = char_slice(text, 0, start) + replaced_text + char_slice(text, end, get_char_length(text));
513
+ const text = annotated_text.text;
514
+ annotated_text.text = char_slice(text, 0, start) + replaced_text + char_slice(text, end, get_char_length(text));
451
515
 
452
- // Transform the annotations (annotated_string[1])
453
- // NOTE: Annotations are stored as [start_offset, end_offset, type]
516
+ // Transform the annotations (annotated_text.annotations)
517
+ // NOTE: Annotations are stored as {start_offset, end_offset, node_id}
454
518
  const delta = get_char_length(replaced_text) - (end - start);
455
- const new_annotations = annotated_string[1].map(/** @param {any} annotation */ (annotation) => {
456
- const [annotation_start, annotation_end, type, annotation_data] = annotation;
519
+ const new_annotations = annotated_text.annotations.map(/** @param {any} annotation */ (annotation) => {
520
+ const {start_offset: annotation_start, end_offset: annotation_end, node_id} = annotation;
457
521
 
458
522
  // Case 1: annotation is wrapped in start and end (remove it)
459
523
  if (start <= annotation_start && end >= annotation_end) {
@@ -462,7 +526,7 @@ export default class Transaction {
462
526
 
463
527
  // Case 2: text inserted before the annotation
464
528
  if (end <= annotation_start) {
465
- return [annotation_start + delta, annotation_end + delta, type, annotation_data];
529
+ return {start_offset: annotation_start + delta, end_offset: annotation_end + delta, node_id};
466
530
  }
467
531
 
468
532
  // Case 3: text inserted after the annotation
@@ -474,19 +538,19 @@ export default class Transaction {
474
538
  // NOTE: replaced_text will not be part of the annotation, we treat it the same as
475
539
  // a cursor right after the annotation
476
540
  if (start > annotation_start && start < annotation_end && end >= annotation_end) {
477
- return [annotation_start, start, type, annotation_data];
541
+ return {start_offset: annotation_start, end_offset: start, node_id};
478
542
  }
479
543
 
480
544
  // Case 5: text inserted inside an annotation
481
545
  if (start >= annotation_start && start <= annotation_end && end < annotation_end && end >= annotation_start) {
482
- return [annotation_start, annotation_end + delta, type, annotation_data];
546
+ return {start_offset: annotation_start, end_offset: annotation_end + delta, node_id};
483
547
  }
484
548
 
485
549
  // Case 6: annotation is partly selected towards left
486
550
  // NOTE: replaced_text will not be part of the annotation, we treat it the same as
487
551
  // a cursor right before the annotation
488
552
  if (start < annotation_start && end > annotation_start && end < annotation_end) {
489
- return [start + get_char_length(replaced_text), annotation_end + delta, type, annotation_data];
553
+ return {start_offset: start + get_char_length(replaced_text), end_offset: annotation_end + delta, node_id};
490
554
  }
491
555
 
492
556
  // Unhandled edge case:
@@ -496,7 +560,8 @@ export default class Transaction {
496
560
  // return annotation;
497
561
  }).filter(Boolean);
498
562
 
499
- this.set(this.doc.selection.path, [annotated_string[0], new_annotations]); // this will update the current state and create a history entry
563
+ annotated_text.annotations = new_annotations;
564
+ this.set(this.doc.selection.path, annotated_text); // this will update the current state and create a history entry
500
565
 
501
566
  // Setting the selection automatically triggers a re-render of the corresponding DOMSelection.
502
567
  const new_selection = {
@@ -506,6 +571,32 @@ export default class Transaction {
506
571
  focus_offset: start + get_char_length(replaced_text),
507
572
  };
508
573
  this.doc.selection = new_selection;
574
+
575
+ // Now we apply annotations if there are any, but only if there's no active annotation
576
+ // at the current collapsed cursor
577
+ if (!this.doc.active_annotation() && annotations.length > 0) {
578
+ const new_annotations = annotations.map(annotation => {
579
+ const original_annotation_node = nodes[annotation.node_id];
580
+ const text_property_definition = this.doc.inspect(this.doc.selection.path);
581
+ // console.log('original_annotation_node.type', original_annotation_node.type);
582
+ // console.log('text_property_definition', text_property_definition);
583
+ if (text_property_definition.node_types.includes(original_annotation_node.type)) {
584
+ const new_annotation_node_id = this.build(annotation.node_id, nodes);
585
+ return {
586
+ start_offset: start + annotation.start_offset,
587
+ end_offset: start + annotation.end_offset,
588
+ node_id: new_annotation_node_id
589
+ };
590
+ } else {
591
+ // Annotation type not allowed in new context, skip it.
592
+ return null;
593
+ }
594
+ }).filter(Boolean);
595
+ const next_annotated_text = structuredClone(annotated_text);
596
+ next_annotated_text.annotations = next_annotated_text.annotations.concat(new_annotations);
597
+ this.set(this.doc.selection.path, next_annotated_text); // this will update the current state and create a history entry
598
+ }
599
+
509
600
  return this;
510
601
  }
511
602
 
@@ -578,6 +669,8 @@ export default class Transaction {
578
669
  count += value.filter(id => id === target_node_id).length;
579
670
  } else if (prop_type === 'node' && value === target_node_id) {
580
671
  count += 1;
672
+ } else if (prop_type === 'annotated_text' && value && value.annotations) {
673
+ count += value.annotations.filter(annotation => annotation.node_id === target_node_id).length;
581
674
  }
582
675
  }
583
676
  }
@@ -1 +1 @@
1
- {"version":3,"file":"commands.svelte.d.ts","sourceRoot":"","sources":["../src/lib/commands.svelte.js"],"names":[],"mappings":"AAGA,kDAqDC;AAED,iDA8DC;AAED,sDA6BC;AAED,6CA6GC"}
1
+ {"version":3,"file":"commands.svelte.d.ts","sourceRoot":"","sources":["../src/lib/commands.svelte.js"],"names":[],"mappings":"AAGA,kDAqDC;AAED,iDAsEC;AAED,sDAwBC;AAED,6CA6GC"}