svedit 0.4.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.
- package/README.md +4 -3
- package/dist/Document.svelte.d.ts +20 -4
- package/dist/Document.svelte.d.ts.map +1 -1
- package/dist/Document.svelte.js +74 -37
- package/dist/Svedit.svelte +239 -192
- package/dist/Svedit.svelte.d.ts.map +1 -1
- package/dist/Transaction.svelte.d.ts +12 -15
- package/dist/Transaction.svelte.d.ts.map +1 -1
- package/dist/Transaction.svelte.js +137 -27
- package/dist/commands.svelte.d.ts.map +1 -1
- package/dist/commands.svelte.js +1 -6
- package/dist/util.d.ts +1 -0
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +34 -0
- package/package.json +21 -21
- package/src/lib/Document.svelte.js +74 -37
- package/src/lib/Svedit.svelte +239 -192
- package/src/lib/Transaction.svelte.js +137 -27
- package/src/lib/commands.svelte.js +1 -6
- package/src/lib/util.js +34 -0
|
@@ -47,6 +47,7 @@ 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
|
|
@@ -54,7 +55,8 @@ export default class Transaction {
|
|
|
54
55
|
* tr.set(["page_1", "body", "0", "description"], {text: "Hello world", annotations: []});
|
|
55
56
|
* ```
|
|
56
57
|
*/
|
|
57
|
-
set(path: DocumentPath, value: any):
|
|
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
|
*
|
|
@@ -124,31 +126,26 @@ export default class Transaction {
|
|
|
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.
|
|
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 {
|
|
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.
|
|
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(
|
|
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 {
|
|
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":"
|
|
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,
|
|
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,6 +64,7 @@ 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
|
|
@@ -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
|
/**
|
|
@@ -232,23 +292,60 @@ export default class Transaction {
|
|
|
232
292
|
* Behavior depends on selection type:
|
|
233
293
|
* - For node selections: Removes selected nodes and cascades deletion of unreferenced nodes
|
|
234
294
|
* - For text selections: Removes selected text and adjusts annotations accordingly
|
|
235
|
-
* - For collapsed selections: Deletes the previous character/node
|
|
295
|
+
* - For collapsed selections: Deletes the previous character/node (backward) or next character/node (forward)
|
|
236
296
|
*
|
|
297
|
+
* @param {'backward' | 'forward'} [direction] - Direction of deletion for collapsed selections
|
|
237
298
|
* @returns {Transaction} This transaction instance for method chaining
|
|
238
299
|
*/
|
|
239
|
-
delete_selection() {
|
|
300
|
+
delete_selection(direction = 'backward') {
|
|
240
301
|
if (!this.doc.selection) return this;
|
|
241
302
|
const path = this.doc.selection.path;
|
|
303
|
+
|
|
304
|
+
if (this.doc.selection.type === 'property') {
|
|
305
|
+
this.set(path, '');
|
|
306
|
+
return this;
|
|
307
|
+
}
|
|
308
|
+
|
|
242
309
|
// Get the start and end indices for the selection
|
|
243
310
|
let start = Math.min(this.doc.selection.anchor_offset, this.doc.selection.focus_offset);
|
|
244
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
|
+
}
|
|
245
321
|
|
|
246
|
-
// 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)
|
|
247
324
|
if (start === end) {
|
|
248
|
-
if (start > 0) {
|
|
325
|
+
if (direction === 'backward' && start > 0) {
|
|
249
326
|
start = start - 1;
|
|
250
|
-
} else {
|
|
251
|
-
|
|
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;
|
|
252
349
|
}
|
|
253
350
|
}
|
|
254
351
|
|
|
@@ -344,23 +441,17 @@ export default class Transaction {
|
|
|
344
441
|
/**
|
|
345
442
|
* Inserts an array of nodes at the current node selection.
|
|
346
443
|
*
|
|
347
|
-
* If there's a current selection, it will be deleted first.
|
|
348
|
-
* 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.
|
|
349
445
|
*
|
|
350
|
-
* @param {
|
|
446
|
+
* @param {NodeId[]} node_ids - Array of node IDs to insert
|
|
351
447
|
* @returns {Transaction} This transaction instance for method chaining
|
|
352
448
|
*
|
|
353
|
-
* @note Assumes we only insert new nodes, not reference existing ones
|
|
354
|
-
*
|
|
355
449
|
* @example
|
|
356
450
|
* ```js
|
|
357
|
-
* tr.
|
|
358
|
-
* {id: 'para_1', type: 'paragraph', content: ['Hello', []]},
|
|
359
|
-
* {id: 'para_2', type: 'paragraph', content: ['World', []]}
|
|
360
|
-
* ]);
|
|
451
|
+
* tr.insert_nodes_new(['para_1', 'para_2']);
|
|
361
452
|
* ```
|
|
362
453
|
*/
|
|
363
|
-
insert_nodes(
|
|
454
|
+
insert_nodes(node_ids) {
|
|
364
455
|
if (this.doc.selection.type !== 'node') return this;
|
|
365
456
|
|
|
366
457
|
// Unless cursor is collapsed, delete the selected nodes as a first step
|
|
@@ -382,13 +473,6 @@ export default class Transaction {
|
|
|
382
473
|
node_array.splice(start, end - start);
|
|
383
474
|
}
|
|
384
475
|
|
|
385
|
-
// First create the new nodes (unless they already exists e.g. in case of cut+paste)
|
|
386
|
-
nodes.forEach(node => {
|
|
387
|
-
if (!this.doc.get(node.id)) {
|
|
388
|
-
this.create(node);
|
|
389
|
-
}
|
|
390
|
-
});
|
|
391
|
-
const node_ids = nodes.map(node => node.id);
|
|
392
476
|
node_array.splice(start, 0, ...node_ids);
|
|
393
477
|
|
|
394
478
|
// Update the node_array in the entry
|
|
@@ -398,7 +482,7 @@ export default class Transaction {
|
|
|
398
482
|
type: 'node',
|
|
399
483
|
path: [...this.doc.selection.path],
|
|
400
484
|
anchor_offset: start,
|
|
401
|
-
focus_offset: start +
|
|
485
|
+
focus_offset: start + node_ids.length
|
|
402
486
|
};
|
|
403
487
|
return this;
|
|
404
488
|
}
|
|
@@ -419,7 +503,7 @@ export default class Transaction {
|
|
|
419
503
|
* tr.insert_text('Hello, world!');
|
|
420
504
|
* ```
|
|
421
505
|
*/
|
|
422
|
-
insert_text(replaced_text) {
|
|
506
|
+
insert_text(replaced_text, annotations = [], nodes = {}) {
|
|
423
507
|
if (this.doc.selection.type !== 'text') return this;
|
|
424
508
|
|
|
425
509
|
const annotated_text = structuredClone($state.snapshot(this.doc.get(this.doc.selection.path)));
|
|
@@ -487,6 +571,32 @@ export default class Transaction {
|
|
|
487
571
|
focus_offset: start + get_char_length(replaced_text),
|
|
488
572
|
};
|
|
489
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
|
+
|
|
490
600
|
return this;
|
|
491
601
|
}
|
|
492
602
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"commands.svelte.d.ts","sourceRoot":"","sources":["../src/lib/commands.svelte.js"],"names":[],"mappings":"AAGA,kDAqDC;AAED,iDAsEC;AAED,
|
|
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"}
|
package/dist/commands.svelte.js
CHANGED
|
@@ -117,7 +117,7 @@ export function join_text_node(tr) {
|
|
|
117
117
|
});
|
|
118
118
|
|
|
119
119
|
tr.delete_selection();
|
|
120
|
-
|
|
120
|
+
|
|
121
121
|
// Finally set the cursor position at the join point using the pre-calculated position
|
|
122
122
|
tr.set_selection({
|
|
123
123
|
type: 'text',
|
|
@@ -145,11 +145,6 @@ export function insert_default_node(tr) {
|
|
|
145
145
|
const property_definition = doc.schema[node_array_node.type].properties[property_name];
|
|
146
146
|
const default_type = get_default_node_type(property_definition);
|
|
147
147
|
|
|
148
|
-
// Only proceed if there's exactly one allowed node_type
|
|
149
|
-
// if (!default_type || property_schema.node_types.length !== 1) {
|
|
150
|
-
// return false;
|
|
151
|
-
// }
|
|
152
|
-
|
|
153
148
|
// Use the inserter function if available
|
|
154
149
|
if (doc.config?.inserters?.[default_type]) {
|
|
155
150
|
doc.config.inserters[default_type](tr);
|
package/dist/util.d.ts
CHANGED
|
@@ -122,5 +122,6 @@ export function join_annotated_text(first_text: AnnotatedText, second_text: Anno
|
|
|
122
122
|
* snake_to_pascal('list_item') // Returns: 'ListItem'
|
|
123
123
|
*/
|
|
124
124
|
export function snake_to_pascal(str: string): string;
|
|
125
|
+
export function traverse(node_id: any, schema: any, nodes: any): any[];
|
|
125
126
|
import type { AnnotatedText } from './types.d.ts';
|
|
126
127
|
//# sourceMappingURL=util.d.ts.map
|
package/dist/util.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../src/lib/util.js"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;;;;;;;;;;;GAaG;AACH,qCARW,MAAM,GACJ,MAAM,CAUlB;AAED;;;;;;;;;;;;;;GAcG;AACH,iCATW,MAAM,SACN,MAAM,GACJ,MAAM,CAWlB;AAED;;;;;;;;;;;;;;GAcG;AACH,gCATW,MAAM,SACN,MAAM,QACN,MAAM,GACJ,MAAM,CAUlB;AAED;;;;;;;;;;;;;GAaG;AACH,0CARW,MAAM,gBACN,MAAM,GACJ,MAAM,CAoBlB;AAED;;;;;;;;;;;;;GAaG;AACH,0CARW,MAAM,eACN,MAAM,GACJ,MAAM,CAgBlB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,4DAZW,aAAa,eACb,MAAM,GACJ,CAAC,aAAa,EAAE,aAAa,CAAC,CAyC1C;AAGD;;;;;;;;;;;;;GAaG;AACH,gDARW,aAAa,eACb,aAAa,GACX,aAAa,CAqCzB;AAED;;;;;;;;GAQG;AACH,qCANW,MAAM,GACJ,MAAM,CAUlB;
|
|
1
|
+
{"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../src/lib/util.js"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;;;;;;;;;;;GAaG;AACH,qCARW,MAAM,GACJ,MAAM,CAUlB;AAED;;;;;;;;;;;;;;GAcG;AACH,iCATW,MAAM,SACN,MAAM,GACJ,MAAM,CAWlB;AAED;;;;;;;;;;;;;;GAcG;AACH,gCATW,MAAM,SACN,MAAM,QACN,MAAM,GACJ,MAAM,CAUlB;AAED;;;;;;;;;;;;;GAaG;AACH,0CARW,MAAM,gBACN,MAAM,GACJ,MAAM,CAoBlB;AAED;;;;;;;;;;;;;GAaG;AACH,0CARW,MAAM,eACN,MAAM,GACJ,MAAM,CAgBlB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,4DAZW,aAAa,eACb,MAAM,GACJ,CAAC,aAAa,EAAE,aAAa,CAAC,CAyC1C;AAGD;;;;;;;;;;;;;GAaG;AACH,gDARW,aAAa,eACb,aAAa,GACX,aAAa,CAqCzB;AAED;;;;;;;;GAQG;AACH,qCANW,MAAM,GACJ,MAAM,CAUlB;AAED,uEAgCC;mCA3Q6C,cAAc"}
|
package/dist/util.js
CHANGED
|
@@ -233,3 +233,37 @@ export function snake_to_pascal(str) {
|
|
|
233
233
|
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
234
234
|
.join('');
|
|
235
235
|
}
|
|
236
|
+
|
|
237
|
+
export function traverse(node_id, schema, nodes) {
|
|
238
|
+
const json = [];
|
|
239
|
+
const visited = {};
|
|
240
|
+
const visit = (node) => {
|
|
241
|
+
if (!node || visited[node.id]) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
visited[node.id] = true;
|
|
245
|
+
for (const [property_name, value] of Object.entries(node)) {
|
|
246
|
+
const property_definition = schema[node.type].properties[property_name];
|
|
247
|
+
|
|
248
|
+
if (property_definition?.type === 'node_array') {
|
|
249
|
+
for (const v of value) {
|
|
250
|
+
if (typeof v === 'string') {
|
|
251
|
+
visit(nodes[v]);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
} else if (property_definition?.type === 'node') {
|
|
255
|
+
visit(nodes[value]);
|
|
256
|
+
} else if (property_definition?.type === 'annotated_text') {
|
|
257
|
+
for (const annotation of value.annotations) {
|
|
258
|
+
visit(nodes[annotation.node_id]);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// Finally add the node to the result.
|
|
263
|
+
// Deep clone, to make sure nothing of the original document is referenced.
|
|
264
|
+
json.push(structuredClone(node));
|
|
265
|
+
}
|
|
266
|
+
// Start with the root node (document_id)
|
|
267
|
+
visit(nodes[node_id]);
|
|
268
|
+
return json;
|
|
269
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "svedit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"dev": "vite dev",
|
|
@@ -39,27 +39,27 @@
|
|
|
39
39
|
"svelte": "^5.0.0"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@eslint/compat": "^1.
|
|
43
|
-
"@eslint/js": "^9.
|
|
44
|
-
"@sveltejs/adapter-auto": "^6.
|
|
45
|
-
"@sveltejs/kit": "^2.
|
|
46
|
-
"@sveltejs/package": "^2.
|
|
47
|
-
"@sveltejs/vite-plugin-svelte": "^6.
|
|
48
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
49
|
-
"@typescript-eslint/parser": "^8.
|
|
42
|
+
"@eslint/compat": "^1.4.0",
|
|
43
|
+
"@eslint/js": "^9.36.0",
|
|
44
|
+
"@sveltejs/adapter-auto": "^6.1.0",
|
|
45
|
+
"@sveltejs/kit": "^2.43.7",
|
|
46
|
+
"@sveltejs/package": "^2.5.4",
|
|
47
|
+
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
|
48
|
+
"@typescript-eslint/eslint-plugin": "^8.45.0",
|
|
49
|
+
"@typescript-eslint/parser": "^8.45.0",
|
|
50
50
|
"@vitest/browser": "^3.2.4",
|
|
51
|
-
"eslint": "^9.
|
|
52
|
-
"eslint-config-prettier": "^10.
|
|
53
|
-
"eslint-plugin-svelte": "^3.
|
|
54
|
-
"globals": "^16.
|
|
55
|
-
"prettier": "^3.
|
|
56
|
-
"prettier-plugin-svelte": "^3.
|
|
57
|
-
"publint": "^0.3.
|
|
58
|
-
"svelte": "^5.
|
|
59
|
-
"svelte-check": "^4.
|
|
60
|
-
"vite": "^7.
|
|
61
|
-
"vitest": "^3.2.
|
|
51
|
+
"eslint": "^9.36.0",
|
|
52
|
+
"eslint-config-prettier": "^10.1.8",
|
|
53
|
+
"eslint-plugin-svelte": "^3.12.4",
|
|
54
|
+
"globals": "^16.4.0",
|
|
55
|
+
"prettier": "^3.6.2",
|
|
56
|
+
"prettier-plugin-svelte": "^3.4.0",
|
|
57
|
+
"publint": "^0.3.13",
|
|
58
|
+
"svelte": "^5.39.8",
|
|
59
|
+
"svelte-check": "^4.3.2",
|
|
60
|
+
"vite": "^7.1.7",
|
|
61
|
+
"vitest": "^3.2.4",
|
|
62
62
|
"vitest-browser-svelte": "^1.1.0",
|
|
63
|
-
"nanoid": "^5.1.
|
|
63
|
+
"nanoid": "^5.1.6"
|
|
64
64
|
}
|
|
65
65
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Transaction from './Transaction.svelte.js';
|
|
2
|
-
import { char_slice, get_char_length } from './util.js';
|
|
2
|
+
import { char_slice, get_char_length, traverse } from './util.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* @import {
|
|
@@ -8,13 +8,10 @@ import { char_slice, get_char_length } from './util.js';
|
|
|
8
8
|
* Selection,
|
|
9
9
|
* Annotation,
|
|
10
10
|
* PrimitiveType,
|
|
11
|
-
* DocumentSchemaPrimitive,
|
|
12
11
|
* NodeProperty,
|
|
13
12
|
* NodeArrayProperty,
|
|
14
13
|
* NodeKind,
|
|
15
14
|
* NodeSchema,
|
|
16
|
-
* TextNodeSchema,
|
|
17
|
-
* NonTextNodeSchema,
|
|
18
15
|
* DocumentSchema,
|
|
19
16
|
* SerializedDocument
|
|
20
17
|
* } from './types.d.ts';
|
|
@@ -315,8 +312,8 @@ export default class Document {
|
|
|
315
312
|
get_available_annotation_types() {
|
|
316
313
|
if (this.selection?.type !== 'text') return [];
|
|
317
314
|
const path = this.selection.path;
|
|
318
|
-
const
|
|
319
|
-
return
|
|
315
|
+
const property_definition = this.inspect(path);
|
|
316
|
+
return property_definition.node_types || [];
|
|
320
317
|
}
|
|
321
318
|
|
|
322
319
|
// Helper function to get the currently selected node
|
|
@@ -600,6 +597,47 @@ export default class Document {
|
|
|
600
597
|
return this.schema[node.type].kind;
|
|
601
598
|
}
|
|
602
599
|
|
|
600
|
+
/**
|
|
601
|
+
* Determines whether a node type can be inserted at a given selection.
|
|
602
|
+
* @param {string} node_type - The type of node to insert.
|
|
603
|
+
* @param {Selection} [selection] - The selection at which to insert the node.
|
|
604
|
+
* @returns {boolean} True if the node type can be inserted, false otherwise.
|
|
605
|
+
*/
|
|
606
|
+
can_insert(node_type, selection = this.selection) {
|
|
607
|
+
if (selection?.type === 'node') {
|
|
608
|
+
const property_definition = this.inspect(selection.path);
|
|
609
|
+
if (property_definition.node_types.includes(node_type)) {
|
|
610
|
+
return true;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// No insert position found yet, and root not reached, try one level up if possible
|
|
615
|
+
let next_node_insert_cursor = this.get_next_node_insert_cursor(selection);
|
|
616
|
+
if (!next_node_insert_cursor) return false;
|
|
617
|
+
return this.can_insert(node_type, next_node_insert_cursor);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Compute next possible insert position from a given selection
|
|
622
|
+
*
|
|
623
|
+
* @param {Selection} [selection] - Reference selection
|
|
624
|
+
* @returns {Selection} True if the paste operation was successful, false otherwise
|
|
625
|
+
*/
|
|
626
|
+
get_next_node_insert_cursor(selection = this.selection) {
|
|
627
|
+
// There's no parent path to insert into
|
|
628
|
+
if (!selection || selection.path.length <= 2) {
|
|
629
|
+
return null;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const node_offset = parseInt(String(selection.path.at(-2)), 10) + 1;
|
|
633
|
+
return {
|
|
634
|
+
type: 'node',
|
|
635
|
+
path: selection.path.slice(0, -2),
|
|
636
|
+
anchor_offset: node_offset,
|
|
637
|
+
focus_offset: node_offset,
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
|
|
603
641
|
/**
|
|
604
642
|
* Returns the annotation object that is currently "under the cursor".
|
|
605
643
|
* NOTE: Annotations in Svedit are exclusive, so there can only be one active_annotation
|
|
@@ -628,6 +666,35 @@ export default class Document {
|
|
|
628
666
|
}
|
|
629
667
|
}
|
|
630
668
|
|
|
669
|
+
get_selected_annotated_text() {
|
|
670
|
+
if (this.selection?.type !== 'text') return null;
|
|
671
|
+
|
|
672
|
+
const selection_start = Math.min(this.selection.anchor_offset, this.selection.focus_offset);
|
|
673
|
+
const selection_end = Math.max(this.selection.anchor_offset, this.selection.focus_offset);
|
|
674
|
+
const annotated_text = this.get(this.selection.path);
|
|
675
|
+
const text = char_slice(annotated_text.text, selection_start, selection_end);
|
|
676
|
+
const nodes = {};
|
|
677
|
+
const annotations = annotated_text.annotations.map(a => {
|
|
678
|
+
if (selection_start < a.end_offset && selection_end > a.start_offset) {
|
|
679
|
+
const sub_graph = this.traverse(a.node_id);
|
|
680
|
+
for (const node of sub_graph) {
|
|
681
|
+
if (!nodes[node.id]) {
|
|
682
|
+
nodes[node.id] = node;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
return {
|
|
686
|
+
start_offset: Math.max(a.start_offset - selection_start, 0),
|
|
687
|
+
end_offset: Math.min(a.end_offset - selection_start, selection_end - selection_start),
|
|
688
|
+
node_id: a.node_id
|
|
689
|
+
};
|
|
690
|
+
} else {
|
|
691
|
+
return null;
|
|
692
|
+
}
|
|
693
|
+
}).filter(Boolean);
|
|
694
|
+
|
|
695
|
+
return { text, annotations, nodes };
|
|
696
|
+
}
|
|
697
|
+
|
|
631
698
|
// TODO: think about ways how we can also turn a node selection into plain text.
|
|
632
699
|
get_selected_plain_text() {
|
|
633
700
|
if (this.selection?.type !== 'text') return null;
|
|
@@ -703,37 +770,7 @@ export default class Document {
|
|
|
703
770
|
// IMPORTANT: Leaf nodes must go first, branches second and the root node (entry point) last (depth-first traversal)
|
|
704
771
|
// NOTE: Nodes that are not reachable from the entry point node will not be included in the serialization
|
|
705
772
|
traverse(node_id) {
|
|
706
|
-
|
|
707
|
-
const visited = {};
|
|
708
|
-
const visit = (node) => {
|
|
709
|
-
if (!node || visited[node.id]) {
|
|
710
|
-
return;
|
|
711
|
-
}
|
|
712
|
-
visited[node.id] = true;
|
|
713
|
-
for (const [property_name, value] of Object.entries(node)) {
|
|
714
|
-
const property_schema = this.schema[node.type].properties[property_name];
|
|
715
|
-
|
|
716
|
-
if (property_schema?.type === 'node_array') {
|
|
717
|
-
for (const v of value) {
|
|
718
|
-
if (typeof v === 'string') {
|
|
719
|
-
visit($state.snapshot(this.get(v)));
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
} else if (property_schema?.type === 'node') {
|
|
723
|
-
visit($state.snapshot(this.get(value)));
|
|
724
|
-
} else if (property_schema?.type === 'annotated_text') {
|
|
725
|
-
for (const annotation of value.annotations) {
|
|
726
|
-
visit($state.snapshot(this.get(annotation.node_id)));
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
// Finally add the node to the result.
|
|
731
|
-
// We use a deep clone, so we make sure nothing of the original document is referenced.
|
|
732
|
-
json.push(structuredClone(node));
|
|
733
|
-
}
|
|
734
|
-
// Start with the root node (document_id)
|
|
735
|
-
visit($state.snapshot(this.get(node_id)));
|
|
736
|
-
return json;
|
|
773
|
+
return traverse(node_id, this.schema, $state.snapshot(this.nodes));
|
|
737
774
|
}
|
|
738
775
|
|
|
739
776
|
/**
|