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.
- package/README.md +43 -24
- package/dist/{AnnotatedStringProperty.svelte → AnnotatedTextProperty.svelte} +28 -40
- package/dist/AnnotatedTextProperty.svelte.d.ts +7 -0
- package/dist/AnnotatedTextProperty.svelte.d.ts.map +1 -0
- package/dist/Document.svelte.d.ts +56 -17
- package/dist/Document.svelte.d.ts.map +1 -1
- package/dist/Document.svelte.js +251 -102
- package/dist/Svedit.svelte +322 -223
- package/dist/Svedit.svelte.d.ts.map +1 -1
- package/dist/Transaction.svelte.d.ts +16 -19
- package/dist/Transaction.svelte.d.ts.map +1 -1
- package/dist/Transaction.svelte.js +197 -104
- package/dist/commands.svelte.d.ts.map +1 -1
- package/dist/commands.svelte.js +18 -15
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/types.d.ts +66 -21
- package/dist/util.d.ts +17 -16
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +67 -34
- package/package.json +21 -21
- package/src/lib/{AnnotatedStringProperty.svelte → AnnotatedTextProperty.svelte} +28 -40
- package/src/lib/Document.svelte.js +251 -102
- package/src/lib/Svedit.svelte +322 -223
- package/src/lib/Transaction.svelte.js +197 -104
- package/src/lib/commands.svelte.js +18 -15
- package/src/lib/index.js +1 -1
- package/src/lib/types.d.ts +66 -21
- package/src/lib/util.js +67 -34
- package/dist/AnnotatedStringProperty.svelte.d.ts +0 -7
- package/dist/AnnotatedStringProperty.svelte.d.ts.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Svedit.svelte.d.ts","sourceRoot":"","sources":["../src/lib/Svedit.svelte.js"],"names":[],"mappings":";;;;;;;
|
|
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"],
|
|
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
|
*
|
|
@@ -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
|
-
* @
|
|
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}
|
|
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,
|
|
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.
|
|
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,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"],
|
|
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
|
-
* @
|
|
215
|
+
* @throws {Error} Throws if the selection is invalid or out of bounds
|
|
156
216
|
*/
|
|
157
217
|
set_selection(selection) {
|
|
158
|
-
|
|
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}
|
|
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,
|
|
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
|
|
188
|
-
const annotations =
|
|
189
|
-
const
|
|
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
|
-
|
|
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 (
|
|
254
|
+
if (existing_annotation_same_type) {
|
|
235
255
|
const index = annotations.findIndex(/** @param {any} anno */ (anno) =>
|
|
236
|
-
anno
|
|
237
|
-
anno
|
|
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(
|
|
281
|
+
annotations.push({start_offset: start, end_offset: end, node_id: new_annotation_node.id});
|
|
250
282
|
}
|
|
251
283
|
|
|
252
|
-
// Update the annotated
|
|
253
|
-
this.set(this.doc.selection.path,
|
|
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
|
-
|
|
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
|
|
313
|
-
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
|
|
318
|
-
const
|
|
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
|
|
418
|
+
return {start_offset: new_start, end_offset: new_end, node_id};
|
|
348
419
|
}).filter(Boolean);
|
|
349
420
|
|
|
350
|
-
text
|
|
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.
|
|
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 {
|
|
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.
|
|
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(
|
|
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 +
|
|
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
|
|
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 =
|
|
450
|
-
|
|
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 (
|
|
453
|
-
// NOTE: Annotations are stored as
|
|
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 =
|
|
456
|
-
const
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|
|
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"}
|