svedit 0.12.0 → 0.13.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 +33 -21
- package/dist/Command.svelte.d.ts +38 -30
- package/dist/Command.svelte.d.ts.map +1 -1
- package/dist/Command.svelte.js +271 -326
- package/dist/CustomProperty.svelte +3 -5
- package/dist/CustomProperty.svelte.d.ts +3 -6
- package/dist/CustomProperty.svelte.d.ts.map +1 -1
- package/dist/KeyMapper.svelte.d.ts +19 -6
- package/dist/KeyMapper.svelte.d.ts.map +1 -1
- package/dist/KeyMapper.svelte.js +95 -112
- package/dist/Node.svelte +16 -13
- package/dist/Node.svelte.d.ts +3 -5
- package/dist/Node.svelte.d.ts.map +1 -1
- package/dist/NodeArrayProperty.svelte +39 -22
- package/dist/NodeArrayProperty.svelte.d.ts +3 -5
- package/dist/NodeArrayProperty.svelte.d.ts.map +1 -1
- package/dist/NodeCaret.svelte +1 -1
- package/dist/NodeCaret.svelte.d.ts +6 -14
- package/dist/NodeCaret.svelte.d.ts.map +1 -1
- package/dist/NodeGap.svelte +12 -6
- package/dist/NodeGap.svelte.d.ts +7 -14
- package/dist/NodeGap.svelte.d.ts.map +1 -1
- package/dist/NodeGapMarkers.svelte +19 -10
- package/dist/NodeGapMarkers.svelte.d.ts +6 -11
- package/dist/NodeGapMarkers.svelte.d.ts.map +1 -1
- package/dist/NodeSelectionMarkers.svelte +20 -6
- package/dist/NodeSelectionMarkers.svelte.d.ts +2 -5
- package/dist/NodeSelectionMarkers.svelte.d.ts.map +1 -1
- package/dist/Session.svelte.d.ts +81 -129
- package/dist/Session.svelte.d.ts.map +1 -1
- package/dist/Session.svelte.js +542 -648
- package/dist/Svedit.svelte +144 -138
- package/dist/Svedit.svelte.d.ts +30 -10
- package/dist/Svedit.svelte.d.ts.map +1 -1
- package/dist/TextProperty.svelte +32 -18
- package/dist/TextProperty.svelte.d.ts +3 -5
- package/dist/TextProperty.svelte.d.ts.map +1 -1
- package/dist/Transaction.svelte.d.ts +64 -127
- package/dist/Transaction.svelte.d.ts.map +1 -1
- package/dist/Transaction.svelte.js +839 -1001
- package/dist/UnknownNode.svelte +4 -3
- package/dist/UnknownNode.svelte.d.ts +5 -9
- package/dist/UnknownNode.svelte.d.ts.map +1 -1
- package/dist/doc_utils.d.ts +67 -155
- package/dist/doc_utils.d.ts.map +1 -1
- package/dist/doc_utils.js +619 -863
- package/dist/index.d.ts +20 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -17
- package/dist/node_visibility.svelte.d.ts +110 -9
- package/dist/node_visibility.svelte.d.ts.map +1 -1
- package/dist/node_visibility.svelte.js +473 -544
- package/dist/paste_utils.d.ts +11 -44
- package/dist/paste_utils.d.ts.map +1 -1
- package/dist/paste_utils.js +111 -152
- package/dist/transforms.svelte.d.ts +9 -7
- package/dist/transforms.svelte.d.ts.map +1 -1
- package/dist/transforms.svelte.js +125 -150
- package/dist/types.d.ts +277 -309
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +40 -119
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +346 -473
- package/package.json +69 -70
- package/src/lib/{Command.svelte.js → Command.svelte.ts} +29 -17
- package/src/lib/CustomProperty.svelte +3 -5
- package/src/lib/{KeyMapper.svelte.js → KeyMapper.svelte.ts} +28 -11
- package/src/lib/Node.svelte +16 -13
- package/src/lib/NodeArrayProperty.svelte +39 -22
- package/src/lib/NodeCaret.svelte +1 -1
- package/src/lib/NodeGap.svelte +12 -6
- package/src/lib/NodeGapMarkers.svelte +19 -10
- package/src/lib/NodeSelectionMarkers.svelte +20 -6
- package/src/lib/{Session.svelte.js → Session.svelte.ts} +130 -127
- package/src/lib/Svedit.svelte +144 -138
- package/src/lib/TextProperty.svelte +32 -18
- package/src/lib/{Transaction.svelte.js → Transaction.svelte.ts} +139 -143
- package/src/lib/UnknownNode.svelte +4 -3
- package/src/lib/{doc_utils.js → doc_utils.ts} +247 -206
- package/src/lib/{index.js → index.ts} +47 -0
- package/src/lib/{node_visibility.svelte.js → node_visibility.svelte.ts} +68 -92
- package/src/lib/{paste_utils.js → paste_utils.ts} +47 -58
- package/src/lib/{transforms.svelte.js → transforms.svelte.ts} +39 -21
- package/src/lib/{types.d.ts → types.ts} +143 -66
- package/src/lib/{utils.js → utils.ts} +110 -129
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @import { NodeId, Selection, Document, DocumentSchema, DocumentPath, Attachment, Mark, Annotation } from './types.d.js'
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
1
|
import {
|
|
6
2
|
get_char_length,
|
|
7
3
|
char_slice,
|
|
@@ -32,15 +28,28 @@ import {
|
|
|
32
28
|
get_selected_range_types,
|
|
33
29
|
validate_selection
|
|
34
30
|
} from './doc_utils.js';
|
|
31
|
+
import type { SelectedAttachment } from './doc_utils.js';
|
|
32
|
+
import type {
|
|
33
|
+
NodeId,
|
|
34
|
+
NodeKind,
|
|
35
|
+
Selection,
|
|
36
|
+
Document,
|
|
37
|
+
DocumentSchema,
|
|
38
|
+
DocumentNode,
|
|
39
|
+
DocumentPath,
|
|
40
|
+
Attachment,
|
|
41
|
+
Mark,
|
|
42
|
+
Annotation,
|
|
43
|
+
DocumentOperation,
|
|
44
|
+
DynamicRecord,
|
|
45
|
+
Inspection,
|
|
46
|
+
SessionConfig
|
|
47
|
+
} from './types.js';
|
|
35
48
|
|
|
36
49
|
/**
|
|
37
50
|
* Check whether a selection-relative range fits within a container length.
|
|
38
|
-
*
|
|
39
|
-
* @param {Attachment} range
|
|
40
|
-
* @param {number} length
|
|
41
|
-
* @returns {boolean}
|
|
42
51
|
*/
|
|
43
|
-
function is_range_within_bounds(range, length) {
|
|
52
|
+
function is_range_within_bounds(range: Attachment, length: number): boolean {
|
|
44
53
|
return (
|
|
45
54
|
Number.isInteger(range.start_offset) &&
|
|
46
55
|
Number.isInteger(range.end_offset) &&
|
|
@@ -66,39 +75,52 @@ function is_range_within_bounds(range, length) {
|
|
|
66
75
|
* ```
|
|
67
76
|
*/
|
|
68
77
|
export default class Transaction {
|
|
78
|
+
schema: DocumentSchema;
|
|
79
|
+
doc: Document;
|
|
80
|
+
// NOTE: Typed non-null for historical reasons — most methods assume a
|
|
81
|
+
// selection is present. A null selection is cast here and would surface at
|
|
82
|
+
// runtime exactly as before the TypeScript conversion.
|
|
83
|
+
selection: Selection;
|
|
84
|
+
config: SessionConfig;
|
|
85
|
+
ops: DocumentOperation[];
|
|
86
|
+
inverse_ops: DocumentOperation[];
|
|
87
|
+
selection_before: Selection | null;
|
|
88
|
+
created_node_ids: NodeId[];
|
|
89
|
+
modified_node_ids: NodeId[];
|
|
90
|
+
deleted_node_ids: NodeId[];
|
|
91
|
+
changed_node_types: boolean;
|
|
92
|
+
|
|
69
93
|
/**
|
|
70
94
|
* Creates a new Transaction with the given state.
|
|
71
95
|
*
|
|
72
|
-
* @param
|
|
73
|
-
* @param
|
|
74
|
-
* @param
|
|
75
|
-
* @param
|
|
96
|
+
* @param schema - The document schema
|
|
97
|
+
* @param doc - The document state {document_id, nodes}
|
|
98
|
+
* @param selection - The current selection
|
|
99
|
+
* @param config - The document config (including generate_id)
|
|
76
100
|
*/
|
|
77
|
-
constructor(
|
|
101
|
+
constructor(
|
|
102
|
+
schema: DocumentSchema,
|
|
103
|
+
doc: Document,
|
|
104
|
+
selection: Selection | null,
|
|
105
|
+
config: SessionConfig
|
|
106
|
+
) {
|
|
78
107
|
this.schema = schema;
|
|
79
108
|
// The transaction works on a draft: one shallow copy of the nodes map
|
|
80
109
|
// up front, then ops mutate the draft in place (copying node objects
|
|
81
110
|
// on write). This keeps the original document untouched and node
|
|
82
111
|
// identity stable for unchanged nodes, without the previous
|
|
83
112
|
// one-full-map-copy-per-op cost.
|
|
84
|
-
/** @type {object} */
|
|
85
113
|
this.doc = create_document_draft(doc);
|
|
86
|
-
|
|
87
|
-
this.selection = selection;
|
|
114
|
+
this.selection = selection as Selection;
|
|
88
115
|
this.config = config;
|
|
89
116
|
// Here we track the ops during the transaction
|
|
90
|
-
/** @type {Record<string, any>[]} */
|
|
91
117
|
this.ops = [];
|
|
92
|
-
/** @type {Record<string, any>[]} */
|
|
93
118
|
this.inverse_ops = [];
|
|
94
119
|
// Remember the selection before the transaction started
|
|
95
120
|
this.selection_before = selection;
|
|
96
121
|
|
|
97
|
-
/** @type {NodeId[]} */
|
|
98
122
|
this.created_node_ids = [];
|
|
99
|
-
/** @type {NodeId[]} */
|
|
100
123
|
this.modified_node_ids = [];
|
|
101
|
-
/** @type {NodeId[]} */
|
|
102
124
|
this.deleted_node_ids = [];
|
|
103
125
|
// True when any op set a node's 'type' property — referrers must be
|
|
104
126
|
// re-validated against type constraints in that case.
|
|
@@ -108,50 +130,39 @@ export default class Transaction {
|
|
|
108
130
|
/**
|
|
109
131
|
* Gets a value from the document at the specified path.
|
|
110
132
|
*
|
|
111
|
-
* @param
|
|
112
|
-
* @returns
|
|
133
|
+
* @param path - The path to the value in the document, or a string node ID
|
|
134
|
+
* @returns The value at the specified path
|
|
113
135
|
*/
|
|
114
|
-
|
|
136
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
137
|
+
get(path: DocumentPath | string): any {
|
|
115
138
|
return doc_get(this.schema, this.doc, path);
|
|
116
139
|
}
|
|
117
140
|
|
|
118
141
|
/**
|
|
119
142
|
* Gets the type of a property from the schema.
|
|
120
|
-
*
|
|
121
|
-
* @param {string} type - The node type
|
|
122
|
-
* @param {string} property - The property name
|
|
123
|
-
* @returns {string} The property type
|
|
124
143
|
*/
|
|
125
|
-
property_type(type, property) {
|
|
144
|
+
property_type(type: string, property: string): string {
|
|
126
145
|
return doc_property_type(this.schema, type, property);
|
|
127
146
|
}
|
|
128
147
|
|
|
129
148
|
/**
|
|
130
|
-
* Determines the kind of a node ('block', 'text', 'mark', or 'annotation').
|
|
131
|
-
*
|
|
132
|
-
* @param {any} node - The node to check
|
|
133
|
-
* @returns {'block'|'text'|'mark'|'annotation'} The node kind
|
|
149
|
+
* Determines the kind of a node ('document', 'block', 'text', 'mark', or 'annotation').
|
|
134
150
|
*/
|
|
135
|
-
kind(node) {
|
|
151
|
+
kind(node: DocumentNode): NodeKind {
|
|
136
152
|
return doc_kind(this.schema, node);
|
|
137
153
|
}
|
|
138
154
|
|
|
139
155
|
/**
|
|
140
156
|
* Inspects a path to get metadata about the value at that location.
|
|
141
|
-
*
|
|
142
|
-
* @param {DocumentPath} path - The path to inspect
|
|
143
|
-
* @returns {{kind: 'property'|'node', [key: string]: any}} Metadata about the path
|
|
144
157
|
*/
|
|
145
|
-
inspect(path) {
|
|
158
|
+
inspect(path: DocumentPath): Inspection {
|
|
146
159
|
return doc_inspect(this.schema, this.doc, path);
|
|
147
160
|
}
|
|
148
161
|
|
|
149
162
|
/**
|
|
150
163
|
* Generates a new unique ID using the config's generate_id function.
|
|
151
|
-
*
|
|
152
|
-
* @returns {string} A new unique ID
|
|
153
164
|
*/
|
|
154
|
-
generate_id() {
|
|
165
|
+
generate_id(): string {
|
|
155
166
|
const id = this.config?.generate_id ? this.config.generate_id() : `node_${crypto.randomUUID()}`;
|
|
156
167
|
if (!is_id_valid(id)) {
|
|
157
168
|
throw new Error(
|
|
@@ -164,29 +175,23 @@ export default class Transaction {
|
|
|
164
175
|
/**
|
|
165
176
|
* Validates a node against the document schema.
|
|
166
177
|
*
|
|
167
|
-
* @param {any} node - The node to validate
|
|
168
178
|
* @throws {Error} Throws if the node is invalid
|
|
169
179
|
*/
|
|
170
|
-
validate_node(node) {
|
|
180
|
+
validate_node(node: DocumentNode): void {
|
|
171
181
|
validate_node(node, this.schema, this.doc.nodes, { require_references: false });
|
|
172
182
|
}
|
|
173
183
|
|
|
174
184
|
/**
|
|
175
185
|
* Gets all nodes referenced by a given node (recursively).
|
|
176
|
-
*
|
|
177
|
-
* @param {NodeId} node_id - The node ID to get references for
|
|
178
|
-
* @returns {NodeId[]} Array of referenced node IDs
|
|
179
186
|
*/
|
|
180
|
-
get_referenced_nodes(node_id) {
|
|
187
|
+
get_referenced_nodes(node_id: NodeId): NodeId[] {
|
|
181
188
|
return traverse_ids(node_id, this.schema, this.doc.nodes).slice(0, -1);
|
|
182
189
|
}
|
|
183
190
|
|
|
184
191
|
/**
|
|
185
192
|
* Gets the available mark types for the current selection.
|
|
186
|
-
*
|
|
187
|
-
* @returns {string[]} Array of available mark type names
|
|
188
193
|
*/
|
|
189
|
-
get available_mark_types() {
|
|
194
|
+
get available_mark_types(): string[] {
|
|
190
195
|
if (this.selection?.type !== 'text' && this.selection?.type !== 'node') return [];
|
|
191
196
|
const property_definition = this.inspect(this.selection.path);
|
|
192
197
|
return property_definition.mark_types || [];
|
|
@@ -194,10 +199,8 @@ export default class Transaction {
|
|
|
194
199
|
|
|
195
200
|
/**
|
|
196
201
|
* Gets the available annotation types for the current selection.
|
|
197
|
-
*
|
|
198
|
-
* @returns {string[]} Array of available annotation type names
|
|
199
202
|
*/
|
|
200
|
-
get available_annotation_types() {
|
|
203
|
+
get available_annotation_types(): string[] {
|
|
201
204
|
if (this.selection?.type !== 'text' && this.selection?.type !== 'node') return [];
|
|
202
205
|
const property_definition = this.inspect(this.selection.path);
|
|
203
206
|
return property_definition.annotation_types || [];
|
|
@@ -205,46 +208,34 @@ export default class Transaction {
|
|
|
205
208
|
|
|
206
209
|
/**
|
|
207
210
|
* Returns marks touched by the current selection.
|
|
208
|
-
*
|
|
209
|
-
* @returns {Array<Mark & { index: number, node: any }>}
|
|
210
211
|
*/
|
|
211
|
-
get selected_marks() {
|
|
212
|
+
get selected_marks(): SelectedAttachment[] {
|
|
212
213
|
return get_selected_marks(this.schema, this.doc, this.selection);
|
|
213
214
|
}
|
|
214
215
|
|
|
215
|
-
get active_mark() {
|
|
216
|
+
get active_mark(): SelectedAttachment | null {
|
|
216
217
|
return this.selected_marks.length === 1 ? this.selected_marks[0] : null;
|
|
217
218
|
}
|
|
218
219
|
|
|
219
220
|
/**
|
|
220
221
|
* Returns annotations touched by the current selection.
|
|
221
|
-
*
|
|
222
|
-
* @returns {Array<Annotation & { index: number, node: any }>}
|
|
223
222
|
*/
|
|
224
|
-
get selected_annotations() {
|
|
223
|
+
get selected_annotations(): SelectedAttachment[] {
|
|
225
224
|
return get_selected_annotations(this.schema, this.doc, this.selection);
|
|
226
225
|
}
|
|
227
226
|
|
|
228
|
-
get active_annotation() {
|
|
227
|
+
get active_annotation(): SelectedAttachment | null {
|
|
229
228
|
return this.selected_annotations.length === 1 ? this.selected_annotations[0] : null;
|
|
230
229
|
}
|
|
231
230
|
|
|
232
231
|
/**
|
|
233
232
|
* Applies an operation to the document (internal).
|
|
234
|
-
*
|
|
235
|
-
* @param {Array} op - The operation to apply
|
|
236
|
-
* @private
|
|
237
233
|
*/
|
|
238
|
-
_apply_op(op) {
|
|
234
|
+
private _apply_op(op: DocumentOperation): void {
|
|
239
235
|
apply_op_to_draft(this.doc, op);
|
|
240
236
|
}
|
|
241
237
|
|
|
242
|
-
|
|
243
|
-
* @param {NodeId[]} node_ids
|
|
244
|
-
* @param {NodeId} node_id
|
|
245
|
-
* @private
|
|
246
|
-
*/
|
|
247
|
-
_track_node_id(node_ids, node_id) {
|
|
238
|
+
private _track_node_id(node_ids: NodeId[], node_id: NodeId): void {
|
|
248
239
|
if (!node_ids.includes(node_id)) node_ids.push(node_id);
|
|
249
240
|
}
|
|
250
241
|
|
|
@@ -254,9 +245,9 @@ export default class Transaction {
|
|
|
254
245
|
* This is the core operation for modifying document properties. It records
|
|
255
246
|
* both the forward operation and its inverse for undo support.
|
|
256
247
|
*
|
|
257
|
-
* @param
|
|
258
|
-
* @param
|
|
259
|
-
* @returns
|
|
248
|
+
* @param path - Array path to the property (e.g., ["node_1", "title"])
|
|
249
|
+
* @param value - The new value to set
|
|
250
|
+
* @returns This transaction instance for method chaining
|
|
260
251
|
*
|
|
261
252
|
* @example
|
|
262
253
|
* ```js
|
|
@@ -264,7 +255,8 @@ export default class Transaction {
|
|
|
264
255
|
* tr.set(["page_1", "body", "0", "description"], {content: "Hello world", marks: [], annotations: []});
|
|
265
256
|
* ```
|
|
266
257
|
*/
|
|
267
|
-
|
|
258
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
259
|
+
set(path: DocumentPath, value: any): this {
|
|
268
260
|
const path_info = this.inspect(path);
|
|
269
261
|
if (path_info?.kind !== 'property') {
|
|
270
262
|
throw new Error(
|
|
@@ -286,8 +278,7 @@ export default class Transaction {
|
|
|
286
278
|
|
|
287
279
|
// Collect node IDs that might need to be deleted after the set operation
|
|
288
280
|
const prop_type = this.property_type(node.type, property_key_str);
|
|
289
|
-
|
|
290
|
-
let removed_node_ids = [];
|
|
281
|
+
let removed_node_ids: NodeId[] = [];
|
|
291
282
|
|
|
292
283
|
if (prop_type === 'node' && typeof previous_value === 'string' && previous_value !== value) {
|
|
293
284
|
removed_node_ids = [previous_value];
|
|
@@ -297,10 +288,10 @@ export default class Transaction {
|
|
|
297
288
|
const next_node_ids = new Set(value.nodes);
|
|
298
289
|
|
|
299
290
|
// Only include node IDs that were in previous_value but are not in the new value
|
|
300
|
-
removed_node_ids = previous_node_ids.filter((id) => !next_node_ids.has(id));
|
|
291
|
+
removed_node_ids = previous_node_ids.filter((id: NodeId) => !next_node_ids.has(id));
|
|
301
292
|
}
|
|
302
293
|
|
|
303
|
-
const op = ['set', normalized_path, value];
|
|
294
|
+
const op: DocumentOperation = ['set', normalized_path, value];
|
|
304
295
|
this.ops.push(op);
|
|
305
296
|
this.inverse_ops.push(['set', normalized_path, previous_value]);
|
|
306
297
|
this._apply_op(op);
|
|
@@ -324,15 +315,15 @@ export default class Transaction {
|
|
|
324
315
|
// Takes a subgraph and constructs new nodes from it
|
|
325
316
|
// NOTE: all ids will be mapped to new unique ids.
|
|
326
317
|
// NOTE: Omitted properties will be populated with default values.
|
|
327
|
-
build(node_id, nodes) {
|
|
318
|
+
build(node_id: NodeId, nodes: Record<string, DocumentNode>): NodeId {
|
|
328
319
|
const depth_first_nodes = traverse(node_id, this.schema, nodes);
|
|
329
320
|
// This maps original ids to newly generated ids
|
|
330
|
-
const id_map = {};
|
|
321
|
+
const id_map: Record<string, string> = {};
|
|
331
322
|
|
|
332
323
|
for (const node of depth_first_nodes) {
|
|
333
324
|
const new_id = this.generate_id();
|
|
334
325
|
id_map[node.id] = new_id;
|
|
335
|
-
let new_node = { ...node, id: new_id };
|
|
326
|
+
let new_node: DocumentNode = { ...node, id: new_id };
|
|
336
327
|
const node_schema = this.schema[node.type];
|
|
337
328
|
|
|
338
329
|
// Update all property references to use new IDs
|
|
@@ -340,14 +331,14 @@ export default class Transaction {
|
|
|
340
331
|
const prop_type = property_definition.type;
|
|
341
332
|
const value = new_node[property_name];
|
|
342
333
|
|
|
343
|
-
const remap_ranges = (ranges) =>
|
|
334
|
+
const remap_ranges = (ranges: Attachment[] | undefined) =>
|
|
344
335
|
(ranges ?? []).map(({ start_offset, end_offset, node_id }) => {
|
|
345
336
|
return { start_offset, end_offset, node_id: id_map[node_id] || node_id };
|
|
346
337
|
});
|
|
347
338
|
|
|
348
339
|
if (prop_type === 'node_array' && value && typeof value === 'object') {
|
|
349
340
|
new_node[property_name] = {
|
|
350
|
-
nodes: value.nodes.map((ref_id) => id_map[ref_id]),
|
|
341
|
+
nodes: value.nodes.map((ref_id: NodeId) => id_map[ref_id]),
|
|
351
342
|
marks: remap_ranges(value.marks),
|
|
352
343
|
annotations: remap_ranges(value.annotations)
|
|
353
344
|
};
|
|
@@ -366,7 +357,7 @@ export default class Transaction {
|
|
|
366
357
|
this.create(new_node);
|
|
367
358
|
}
|
|
368
359
|
|
|
369
|
-
return id_map[depth_first_nodes.at(-1)
|
|
360
|
+
return id_map[depth_first_nodes.at(-1)!.id];
|
|
370
361
|
}
|
|
371
362
|
|
|
372
363
|
/**
|
|
@@ -375,8 +366,8 @@ export default class Transaction {
|
|
|
375
366
|
* The node must have a valid id and must not already exist in the document.
|
|
376
367
|
* Omitted properties with schema defaults are filled before validation.
|
|
377
368
|
*
|
|
378
|
-
* @param
|
|
379
|
-
* @returns
|
|
369
|
+
* @param node - The node object to create (must include id, type, and required properties)
|
|
370
|
+
* @returns This transaction instance for method chaining
|
|
380
371
|
* @throws {Error} If the node ID is invalid or if the node already exists
|
|
381
372
|
*
|
|
382
373
|
* @example
|
|
@@ -388,7 +379,7 @@ export default class Transaction {
|
|
|
388
379
|
* });
|
|
389
380
|
* ```
|
|
390
381
|
*/
|
|
391
|
-
create(node) {
|
|
382
|
+
create(node: DocumentNode): this {
|
|
392
383
|
const node_with_defaults = fill_node_defaults(node, this.schema);
|
|
393
384
|
|
|
394
385
|
// Validate node against schema
|
|
@@ -398,7 +389,7 @@ export default class Transaction {
|
|
|
398
389
|
throw new Error('Node with id ' + node_with_defaults.id + ' already exists');
|
|
399
390
|
}
|
|
400
391
|
|
|
401
|
-
const op = ['create', node_with_defaults];
|
|
392
|
+
const op: DocumentOperation = ['create', node_with_defaults];
|
|
402
393
|
this.ops.push(op);
|
|
403
394
|
this.inverse_ops.push(['delete', node_with_defaults.id]);
|
|
404
395
|
this._apply_op(op);
|
|
@@ -418,15 +409,15 @@ export default class Transaction {
|
|
|
418
409
|
* For ref-count-aware cleanup, see how `set` calls
|
|
419
410
|
* `_cascade_delete_unreferenced_nodes`.
|
|
420
411
|
*
|
|
421
|
-
* @param
|
|
422
|
-
* @returns
|
|
412
|
+
* @param id - The ID of the node to delete
|
|
413
|
+
* @returns This transaction instance for method chaining
|
|
423
414
|
*
|
|
424
415
|
* @example
|
|
425
416
|
* ```js
|
|
426
417
|
* tr.delete('node_123');
|
|
427
418
|
* ```
|
|
428
419
|
*/
|
|
429
|
-
delete(id) {
|
|
420
|
+
delete(id: NodeId): this {
|
|
430
421
|
const previous_value = this.get(id);
|
|
431
422
|
if (!previous_value) {
|
|
432
423
|
console.warn(`Deletion of node ${id} skipped, as it does not exist.`);
|
|
@@ -434,7 +425,7 @@ export default class Transaction {
|
|
|
434
425
|
}
|
|
435
426
|
// Get nodes referenced by this node BEFORE deleting it.
|
|
436
427
|
const referenced_nodes = this.get_referenced_nodes(id);
|
|
437
|
-
const op = ['delete', id];
|
|
428
|
+
const op: DocumentOperation = ['delete', id];
|
|
438
429
|
this.ops.push(op);
|
|
439
430
|
this.inverse_ops.push(['create', previous_value]);
|
|
440
431
|
this._apply_op(op);
|
|
@@ -447,11 +438,11 @@ export default class Transaction {
|
|
|
447
438
|
/**
|
|
448
439
|
* Sets the document selection.
|
|
449
440
|
*
|
|
450
|
-
* @param
|
|
451
|
-
* @returns
|
|
441
|
+
* @param selection - The new selection object
|
|
442
|
+
* @returns This transaction instance for method chaining
|
|
452
443
|
* @throws {Error} Throws if the selection is invalid or out of bounds
|
|
453
444
|
*/
|
|
454
|
-
set_selection(selection) {
|
|
445
|
+
set_selection(selection: Selection): this {
|
|
455
446
|
this._validate_selection(selection);
|
|
456
447
|
this.selection = selection;
|
|
457
448
|
return this;
|
|
@@ -460,11 +451,9 @@ export default class Transaction {
|
|
|
460
451
|
/**
|
|
461
452
|
* Validates a selection against the current document state.
|
|
462
453
|
*
|
|
463
|
-
* @param {Selection} selection - The selection to validate
|
|
464
454
|
* @throws {Error} Throws if the selection is invalid
|
|
465
|
-
* @private
|
|
466
455
|
*/
|
|
467
|
-
_validate_selection(selection) {
|
|
456
|
+
private _validate_selection(selection: Selection | null): void {
|
|
468
457
|
validate_selection(selection, this);
|
|
469
458
|
}
|
|
470
459
|
|
|
@@ -474,9 +463,9 @@ export default class Transaction {
|
|
|
474
463
|
* Marks are mutually exclusive. Whole marks are toggled, removed, or
|
|
475
464
|
* switched; ranges are never truncated or expanded by this command.
|
|
476
465
|
*
|
|
477
|
-
* @param
|
|
478
|
-
* @param
|
|
479
|
-
* @returns
|
|
466
|
+
* @param mark_type - The type of mark (e.g., 'link', 'strong', 'emphasis')
|
|
467
|
+
* @param mark_properties - Additional data for the mark (e.g., href for links)
|
|
468
|
+
* @returns This transaction instance for method chaining
|
|
480
469
|
*
|
|
481
470
|
* @example
|
|
482
471
|
* ```js
|
|
@@ -487,7 +476,7 @@ export default class Transaction {
|
|
|
487
476
|
* tr.toggle_mark('emphasis', {});
|
|
488
477
|
* ```
|
|
489
478
|
*/
|
|
490
|
-
toggle_mark(mark_type, mark_properties) {
|
|
479
|
+
toggle_mark(mark_type: string, mark_properties?: DynamicRecord): this {
|
|
491
480
|
if (this.selection.type !== 'text' && this.selection.type !== 'node') return this;
|
|
492
481
|
if (this.selection.type === 'node' && is_selection_collapsed(this.selection)) return this;
|
|
493
482
|
if (!this.available_mark_types.includes(mark_type)) {
|
|
@@ -535,7 +524,7 @@ export default class Transaction {
|
|
|
535
524
|
const selected_indices = new Set(selected_marks.map(({ index }) => index));
|
|
536
525
|
const removed_node_ids = selected_marks.map(({ node_id }) => node_id);
|
|
537
526
|
annotated_value.marks = annotated_value.marks.filter(
|
|
538
|
-
(_, index) => !selected_indices.has(index)
|
|
527
|
+
(_: Attachment, index: number) => !selected_indices.has(index)
|
|
539
528
|
);
|
|
540
529
|
this.set(this.selection.path, annotated_value);
|
|
541
530
|
this._cascade_delete_unreferenced_nodes(removed_node_ids);
|
|
@@ -569,9 +558,9 @@ export default class Transaction {
|
|
|
569
558
|
* same-type annotations may still overlap when created through lower-level
|
|
570
559
|
* APIs.
|
|
571
560
|
*
|
|
572
|
-
* @param
|
|
573
|
-
* @param
|
|
574
|
-
* @returns
|
|
561
|
+
* @param annotation_type - The type of annotation (e.g., 'comment', 'marker')
|
|
562
|
+
* @param annotation_properties - Additional data for the annotation
|
|
563
|
+
* @returns This transaction instance for method chaining
|
|
575
564
|
*
|
|
576
565
|
* @example
|
|
577
566
|
* ```js
|
|
@@ -579,7 +568,7 @@ export default class Transaction {
|
|
|
579
568
|
* tr.toggle_annotation('comment', { text: 'Please review' });
|
|
580
569
|
* ```
|
|
581
570
|
*/
|
|
582
|
-
toggle_annotation(annotation_type, annotation_properties) {
|
|
571
|
+
toggle_annotation(annotation_type: string, annotation_properties?: DynamicRecord): this {
|
|
583
572
|
if (this.selection.type !== 'text' && this.selection.type !== 'node') return this;
|
|
584
573
|
if (this.selection.type === 'node' && is_selection_collapsed(this.selection)) return this;
|
|
585
574
|
if (!this.available_annotation_types.includes(annotation_type)) {
|
|
@@ -621,7 +610,7 @@ export default class Transaction {
|
|
|
621
610
|
const selected_indices = new Set(selected_annotations.map(({ index }) => index));
|
|
622
611
|
const removed_node_ids = selected_annotations.map(({ node_id }) => node_id);
|
|
623
612
|
annotated_value.annotations = annotated_value.annotations.filter(
|
|
624
|
-
(_, index) => !selected_indices.has(index)
|
|
613
|
+
(_: Attachment, index: number) => !selected_indices.has(index)
|
|
625
614
|
);
|
|
626
615
|
this.set(this.selection.path, annotated_value);
|
|
627
616
|
this._cascade_delete_unreferenced_nodes(removed_node_ids);
|
|
@@ -637,10 +626,10 @@ export default class Transaction {
|
|
|
637
626
|
* - For collapsed selections: Deletes the previous character/node (backward) or next character/node (forward)
|
|
638
627
|
* - For property selections: Delegates to the config's handle_property_deletion hook
|
|
639
628
|
*
|
|
640
|
-
* @param
|
|
641
|
-
* @returns
|
|
629
|
+
* @param direction - Direction of deletion for collapsed selections
|
|
630
|
+
* @returns This transaction instance for method chaining
|
|
642
631
|
*/
|
|
643
|
-
delete_selection(direction = 'backward') {
|
|
632
|
+
delete_selection(direction: 'backward' | 'forward' = 'backward'): this {
|
|
644
633
|
if (!this.selection) return this;
|
|
645
634
|
|
|
646
635
|
if (this.selection.type === 'property') {
|
|
@@ -653,7 +642,7 @@ export default class Transaction {
|
|
|
653
642
|
// Get the start and end indices for the selection
|
|
654
643
|
let start = Math.min(this.selection.anchor_offset, this.selection.focus_offset);
|
|
655
644
|
let end = Math.max(this.selection.anchor_offset, this.selection.focus_offset);
|
|
656
|
-
let length;
|
|
645
|
+
let length = 0;
|
|
657
646
|
|
|
658
647
|
if (this.selection?.type === 'text') {
|
|
659
648
|
const text_content = this.get(this.selection.path).content;
|
|
@@ -675,7 +664,7 @@ export default class Transaction {
|
|
|
675
664
|
return this;
|
|
676
665
|
} else if (direction === 'forward' && end === length) {
|
|
677
666
|
// At end of text - try to join with next text node
|
|
678
|
-
const node_index =
|
|
667
|
+
const node_index = this.selection.path.at(-2) as number;
|
|
679
668
|
const successor_node = this.get([...this.selection.path.slice(0, -2), node_index + 1]);
|
|
680
669
|
// Check if next node is a text node
|
|
681
670
|
if (successor_node && this.kind(successor_node) === 'text') {
|
|
@@ -724,7 +713,7 @@ export default class Transaction {
|
|
|
724
713
|
};
|
|
725
714
|
} else if (this.selection.type === 'text') {
|
|
726
715
|
const path = this.selection.path;
|
|
727
|
-
|
|
716
|
+
const text = structuredClone($state.snapshot(this.get(path)));
|
|
728
717
|
|
|
729
718
|
// Update the text content using character-based operations
|
|
730
719
|
const original_text = text.content;
|
|
@@ -763,13 +752,18 @@ export default class Transaction {
|
|
|
763
752
|
* If the selection is expanded (not collapsed), first deletes the selected nodes
|
|
764
753
|
* before inserting the new ones.
|
|
765
754
|
*
|
|
766
|
-
* @param
|
|
767
|
-
* @param
|
|
768
|
-
* @param
|
|
769
|
-
* @param
|
|
770
|
-
* @returns
|
|
755
|
+
* @param node_ids - Array of node IDs to insert
|
|
756
|
+
* @param marks - Selection-relative marks to restore
|
|
757
|
+
* @param annotations - Selection-relative annotations to restore
|
|
758
|
+
* @param nodes - Node graph for restored marks and annotations
|
|
759
|
+
* @returns This transaction instance for method chaining
|
|
771
760
|
*/
|
|
772
|
-
insert_nodes(
|
|
761
|
+
insert_nodes(
|
|
762
|
+
node_ids: NodeId[],
|
|
763
|
+
marks: Array<Mark> = [],
|
|
764
|
+
annotations: Array<Annotation> = [],
|
|
765
|
+
nodes: Record<NodeId, DocumentNode> = {}
|
|
766
|
+
): this {
|
|
773
767
|
if (this.selection.type !== 'node') return this;
|
|
774
768
|
|
|
775
769
|
// Unless caret is collapsed, delete the selected nodes as a first step
|
|
@@ -781,7 +775,7 @@ export default class Transaction {
|
|
|
781
775
|
const current_value = structuredClone($state.snapshot(this.get(path)));
|
|
782
776
|
const node_array = [...current_value.nodes];
|
|
783
777
|
|
|
784
|
-
|
|
778
|
+
const start = Math.min(this.selection.anchor_offset, this.selection.focus_offset);
|
|
785
779
|
|
|
786
780
|
let next_marks = adjust_ranges_for_insertion(current_value.marks, start, node_ids.length);
|
|
787
781
|
let next_annotations = adjust_ranges_for_insertion(
|
|
@@ -816,7 +810,7 @@ export default class Transaction {
|
|
|
816
810
|
node_id: new_mark_node_id
|
|
817
811
|
};
|
|
818
812
|
})
|
|
819
|
-
.filter(
|
|
813
|
+
.filter((mark): mark is Attachment => mark !== null);
|
|
820
814
|
const combined_marks = next_marks.concat(restored_marks);
|
|
821
815
|
if (are_ranges_exclusive(combined_marks)) {
|
|
822
816
|
next_marks = combined_marks;
|
|
@@ -841,7 +835,7 @@ export default class Transaction {
|
|
|
841
835
|
node_id: new_annotation_node_id
|
|
842
836
|
};
|
|
843
837
|
})
|
|
844
|
-
.filter(
|
|
838
|
+
.filter((annotation): annotation is Attachment => annotation !== null);
|
|
845
839
|
next_annotations = next_annotations.concat(restored_annotations);
|
|
846
840
|
|
|
847
841
|
this.set(path, { nodes: node_array, marks: next_marks, annotations: next_annotations });
|
|
@@ -856,13 +850,18 @@ export default class Transaction {
|
|
|
856
850
|
* - Shifting ranges that come after the insertion point
|
|
857
851
|
* - Optionally applying new marks and annotations to the inserted text
|
|
858
852
|
*
|
|
859
|
-
* @param
|
|
860
|
-
* @param
|
|
861
|
-
* @param
|
|
862
|
-
* @param
|
|
863
|
-
* @returns
|
|
853
|
+
* @param replaced_text - The text to insert
|
|
854
|
+
* @param marks - Optional marks to apply to the inserted text
|
|
855
|
+
* @param annotations - Optional annotations to apply to the inserted text
|
|
856
|
+
* @param nodes - Optional node definitions for mark and annotation nodes
|
|
857
|
+
* @returns This transaction instance for method chaining
|
|
864
858
|
*/
|
|
865
|
-
insert_text(
|
|
859
|
+
insert_text(
|
|
860
|
+
replaced_text: string,
|
|
861
|
+
marks: Array<Mark> = [],
|
|
862
|
+
annotations: Array<Annotation> = [],
|
|
863
|
+
nodes: Record<NodeId, DocumentNode> = {}
|
|
864
|
+
): this {
|
|
866
865
|
if (this.selection?.type !== 'text') return this;
|
|
867
866
|
|
|
868
867
|
// Unless selection is collapsed, delete the selected content
|
|
@@ -872,7 +871,7 @@ export default class Transaction {
|
|
|
872
871
|
}
|
|
873
872
|
|
|
874
873
|
const text_value = structuredClone($state.snapshot(this.get(this.selection.path)));
|
|
875
|
-
const range = get_selection_range(this.selection)
|
|
874
|
+
const range = get_selection_range(this.selection)!;
|
|
876
875
|
|
|
877
876
|
// Transform the plain text string using character-based operations
|
|
878
877
|
const current_text = text_value.content;
|
|
@@ -893,9 +892,8 @@ export default class Transaction {
|
|
|
893
892
|
this.set(this.selection.path, text_value); // this will update the current state and create a history entry
|
|
894
893
|
|
|
895
894
|
// Setting the selection automatically triggers a re-render of the corresponding DOMSelection.
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
type: /** @type {const} */ ('text'),
|
|
895
|
+
const new_selection: Selection = {
|
|
896
|
+
type: 'text',
|
|
899
897
|
path: this.selection.path,
|
|
900
898
|
anchor_offset: range.start_offset + get_char_length(replaced_text),
|
|
901
899
|
focus_offset: range.start_offset + get_char_length(replaced_text)
|
|
@@ -922,7 +920,7 @@ export default class Transaction {
|
|
|
922
920
|
}
|
|
923
921
|
return null;
|
|
924
922
|
})
|
|
925
|
-
.filter(
|
|
923
|
+
.filter((mark): mark is Attachment => mark !== null);
|
|
926
924
|
const combined_marks = text_value.marks.concat(restored_marks);
|
|
927
925
|
if (are_ranges_exclusive(combined_marks)) {
|
|
928
926
|
next_text.marks = combined_marks;
|
|
@@ -950,7 +948,7 @@ export default class Transaction {
|
|
|
950
948
|
}
|
|
951
949
|
return null;
|
|
952
950
|
})
|
|
953
|
-
.filter(
|
|
951
|
+
.filter((annotation): annotation is Attachment => annotation !== null);
|
|
954
952
|
if (restored_annotations.length > 0) {
|
|
955
953
|
next_text.annotations = text_value.annotations.concat(restored_annotations);
|
|
956
954
|
text_changed = true;
|
|
@@ -971,10 +969,9 @@ export default class Transaction {
|
|
|
971
969
|
* references are removed. Uses reference counting to determine which
|
|
972
970
|
* nodes are safe to delete.
|
|
973
971
|
*
|
|
974
|
-
* @param
|
|
975
|
-
* @private
|
|
972
|
+
* @param potentially_orphaned_nodes - Array of node IDs to check
|
|
976
973
|
*/
|
|
977
|
-
_cascade_delete_unreferenced_nodes(potentially_orphaned_nodes) {
|
|
974
|
+
private _cascade_delete_unreferenced_nodes(potentially_orphaned_nodes: NodeId[]): void {
|
|
978
975
|
if (potentially_orphaned_nodes.length === 0) return;
|
|
979
976
|
|
|
980
977
|
// Build reference counts in ONE full-document scan, then maintain them
|
|
@@ -984,8 +981,7 @@ export default class Transaction {
|
|
|
984
981
|
// O(M·N) instead of O(N + M).
|
|
985
982
|
const ref_counts = build_reference_counts(this.schema, this.doc);
|
|
986
983
|
|
|
987
|
-
|
|
988
|
-
const nodes_to_delete = {};
|
|
984
|
+
const nodes_to_delete: Record<NodeId, boolean> = {};
|
|
989
985
|
const to_check = [...potentially_orphaned_nodes];
|
|
990
986
|
|
|
991
987
|
while (to_check.length > 0) {
|
|
@@ -1013,7 +1009,7 @@ export default class Transaction {
|
|
|
1013
1009
|
for (const node_id of Object.keys(nodes_to_delete)) {
|
|
1014
1010
|
const previous_value = this.get([node_id]);
|
|
1015
1011
|
if (previous_value) {
|
|
1016
|
-
const op = ['delete', node_id];
|
|
1012
|
+
const op: DocumentOperation = ['delete', node_id];
|
|
1017
1013
|
this.ops.push(op);
|
|
1018
1014
|
this.inverse_ops.push(['create', previous_value]);
|
|
1019
1015
|
this._apply_op(op);
|