svedit 0.11.1 → 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 +260 -82
- package/dist/Command.svelte.d.ts +54 -29
- package/dist/Command.svelte.d.ts.map +1 -1
- package/dist/Command.svelte.js +286 -283
- 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 +40 -9
- package/dist/Node.svelte.d.ts +3 -5
- package/dist/Node.svelte.d.ts.map +1 -1
- package/dist/NodeArrayProperty.svelte +142 -18
- 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 +60 -12
- 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 +85 -116
- package/dist/Session.svelte.d.ts.map +1 -1
- package/dist/Session.svelte.js +543 -571
- package/dist/Svedit.svelte +243 -216
- package/dist/Svedit.svelte.d.ts +30 -10
- package/dist/Svedit.svelte.d.ts.map +1 -1
- package/dist/{AnnotatedTextProperty.svelte → TextProperty.svelte} +71 -56
- package/dist/TextProperty.svelte.d.ts +5 -0
- package/dist/TextProperty.svelte.d.ts.map +1 -0
- package/dist/Transaction.svelte.d.ts +101 -126
- package/dist/Transaction.svelte.d.ts.map +1 -1
- package/dist/Transaction.svelte.js +843 -827
- 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 +109 -122
- package/dist/doc_utils.d.ts.map +1 -1
- package/dist/doc_utils.js +726 -698
- package/dist/index.d.ts +20 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -18
- package/dist/node_visibility.svelte.d.ts +132 -5
- package/dist/node_visibility.svelte.d.ts.map +1 -1
- package/dist/node_visibility.svelte.js +535 -642
- package/dist/paste_utils.d.ts +11 -43
- package/dist/paste_utils.d.ts.map +1 -1
- package/dist/paste_utils.js +111 -149
- package/dist/transforms.svelte.d.ts +9 -7
- package/dist/transforms.svelte.d.ts.map +1 -1
- package/dist/transforms.svelte.js +126 -152
- package/dist/types.d.ts +329 -291
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +79 -95
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +392 -305
- package/package.json +69 -67
- package/src/lib/{Command.svelte.js → Command.svelte.ts} +111 -41
- package/src/lib/CustomProperty.svelte +3 -5
- package/src/lib/{KeyMapper.svelte.js → KeyMapper.svelte.ts} +28 -11
- package/src/lib/Node.svelte +40 -9
- package/src/lib/NodeArrayProperty.svelte +142 -18
- package/src/lib/NodeCaret.svelte +1 -1
- package/src/lib/NodeGap.svelte +60 -12
- package/src/lib/NodeGapMarkers.svelte +19 -10
- package/src/lib/NodeSelectionMarkers.svelte +20 -6
- package/src/lib/Session.svelte.ts +656 -0
- package/src/lib/Svedit.svelte +243 -216
- package/src/lib/{AnnotatedTextProperty.svelte → TextProperty.svelte} +71 -56
- package/src/lib/Transaction.svelte.ts +1020 -0
- package/src/lib/UnknownNode.svelte +4 -3
- package/src/lib/doc_utils.ts +1120 -0
- package/src/lib/{index.js → index.ts} +48 -1
- package/src/lib/node_visibility.svelte.ts +658 -0
- package/src/lib/{paste_utils.js → paste_utils.ts} +51 -59
- package/src/lib/{transforms.svelte.js → transforms.svelte.ts} +44 -27
- package/src/lib/{types.d.ts → types.ts} +250 -103
- package/src/lib/utils.ts +660 -0
- package/dist/AnnotatedTextProperty.svelte.d.ts +0 -7
- package/dist/AnnotatedTextProperty.svelte.d.ts.map +0 -1
- package/src/lib/Session.svelte.js +0 -575
- package/src/lib/Transaction.svelte.js +0 -846
- package/src/lib/doc_utils.js +0 -807
- package/src/lib/node_visibility.svelte.js +0 -718
- package/src/lib/utils.js +0 -465
|
@@ -1,846 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @import { NodeId, Selection, Document, DocumentSchema, DocumentPath, Annotation } from './types.d.js'
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
get_char_length,
|
|
7
|
-
char_slice,
|
|
8
|
-
traverse,
|
|
9
|
-
get_selection_range,
|
|
10
|
-
is_selection_collapsed
|
|
11
|
-
} from './utils.js';
|
|
12
|
-
import { join_text_node } from './transforms.svelte.js';
|
|
13
|
-
import {
|
|
14
|
-
get as doc_get,
|
|
15
|
-
property_type as doc_property_type,
|
|
16
|
-
kind as doc_kind,
|
|
17
|
-
inspect as doc_inspect,
|
|
18
|
-
apply_op,
|
|
19
|
-
count_references_excluding_deleted,
|
|
20
|
-
validate_node,
|
|
21
|
-
fill_node_defaults,
|
|
22
|
-
get_active_annotation,
|
|
23
|
-
validate_selection,
|
|
24
|
-
can_switch_annotation_type
|
|
25
|
-
} from './doc_utils.js';
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Transaction class for managing atomic document operations with undo/redo support.
|
|
29
|
-
*
|
|
30
|
-
* A Transaction provides a way to group multiple document operations (create, delete, set)
|
|
31
|
-
* into a single atomic unit that can be applied or rolled back as one. It maintains
|
|
32
|
-
* both forward operations and their inverse operations for undo functionality.
|
|
33
|
-
*
|
|
34
|
-
* @example
|
|
35
|
-
* ```js
|
|
36
|
-
* const tr = session.tr;
|
|
37
|
-
* tr.set(['node_1', 'title'], 'New Title');
|
|
38
|
-
* tr.create({id: 'node_2', type: 'paragraph', content: 'Hello'});
|
|
39
|
-
* session.apply(tr); // Applies all operations atomically
|
|
40
|
-
* ```
|
|
41
|
-
*/
|
|
42
|
-
export default class Transaction {
|
|
43
|
-
/**
|
|
44
|
-
* Creates a new Transaction with the given state.
|
|
45
|
-
*
|
|
46
|
-
* @param {DocumentSchema} schema - The document schema
|
|
47
|
-
* @param {Document} doc - The document state {document_id, nodes}
|
|
48
|
-
* @param {Selection | null} selection - The current selection
|
|
49
|
-
* @param {object} config - The document config (including generate_id)
|
|
50
|
-
*/
|
|
51
|
-
constructor(schema, doc, selection, config) {
|
|
52
|
-
this.schema = schema;
|
|
53
|
-
/** @type {object} */
|
|
54
|
-
this.doc = doc;
|
|
55
|
-
/** @type {Selection} */
|
|
56
|
-
this.selection = selection;
|
|
57
|
-
this.config = config;
|
|
58
|
-
// Here we track the ops during the transaction
|
|
59
|
-
/** @type {Record<string, any>[]} */
|
|
60
|
-
this.ops = [];
|
|
61
|
-
/** @type {Record<string, any>[]} */
|
|
62
|
-
this.inverse_ops = [];
|
|
63
|
-
// Remember the selection before the transaction started
|
|
64
|
-
this.selection_before = selection;
|
|
65
|
-
|
|
66
|
-
/** @type {NodeId[]} */
|
|
67
|
-
this.created_node_ids = [];
|
|
68
|
-
/** @type {NodeId[]} */
|
|
69
|
-
this.modified_node_ids = [];
|
|
70
|
-
/** @type {NodeId[]} */
|
|
71
|
-
this.deleted_node_ids = [];
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Gets a value from the document at the specified path.
|
|
76
|
-
*
|
|
77
|
-
* @param {DocumentPath|string} path - The path to the value in the document, or a string node ID
|
|
78
|
-
* @returns {any} The value at the specified path
|
|
79
|
-
*/
|
|
80
|
-
get(path) {
|
|
81
|
-
return doc_get(this.schema, this.doc, path);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Gets the type of a property from the schema.
|
|
86
|
-
*
|
|
87
|
-
* @param {string} type - The node type
|
|
88
|
-
* @param {string} property - The property name
|
|
89
|
-
* @returns {string} The property type
|
|
90
|
-
*/
|
|
91
|
-
property_type(type, property) {
|
|
92
|
-
return doc_property_type(this.schema, type, property);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Determines the kind of a node ('block', 'text', or 'annotation').
|
|
97
|
-
*
|
|
98
|
-
* @param {any} node - The node to check
|
|
99
|
-
* @returns {'block'|'text'|'annotation'} The node kind
|
|
100
|
-
*/
|
|
101
|
-
kind(node) {
|
|
102
|
-
return doc_kind(this.schema, node);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Inspects a path to get metadata about the value at that location.
|
|
107
|
-
*
|
|
108
|
-
* @param {DocumentPath} path - The path to inspect
|
|
109
|
-
* @returns {{kind: 'property'|'node', [key: string]: any}} Metadata about the path
|
|
110
|
-
*/
|
|
111
|
-
inspect(path) {
|
|
112
|
-
return doc_inspect(this.schema, this.doc, path);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Generates a new unique ID using the config's generate_id function.
|
|
117
|
-
*
|
|
118
|
-
* @returns {string} A new unique ID
|
|
119
|
-
*/
|
|
120
|
-
generate_id() {
|
|
121
|
-
return this.config.generate_id();
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Validates a node against the document schema.
|
|
126
|
-
*
|
|
127
|
-
* @param {any} node - The node to validate
|
|
128
|
-
* @throws {Error} Throws if the node is invalid
|
|
129
|
-
*/
|
|
130
|
-
validate_node(node) {
|
|
131
|
-
validate_node(node, this.schema, this.doc.nodes, { require_references: false });
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Gets all nodes referenced by a given node (recursively).
|
|
137
|
-
*
|
|
138
|
-
* @param {NodeId} node_id - The node ID to get references for
|
|
139
|
-
* @returns {NodeId[]} Array of referenced node IDs
|
|
140
|
-
*/
|
|
141
|
-
get_referenced_nodes(node_id) {
|
|
142
|
-
const traversed_nodes = traverse(node_id, this.schema, this.doc.nodes);
|
|
143
|
-
return traversed_nodes.slice(0, -1).map((node) => node.id);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Gets the available annotation types for the current selection.
|
|
148
|
-
*
|
|
149
|
-
* @returns {string[]} Array of available annotation type names
|
|
150
|
-
*/
|
|
151
|
-
get available_annotation_types() {
|
|
152
|
-
if (this.selection?.type !== 'text') return [];
|
|
153
|
-
const path = this.selection.path;
|
|
154
|
-
const property_definition = this.inspect(path);
|
|
155
|
-
return property_definition.node_types || [];
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Returns the annotation object that is currently "under the caret".
|
|
160
|
-
* NOTE: Annotations in Svedit are exclusive, so there can only be one active_annotation
|
|
161
|
-
*
|
|
162
|
-
* @param {string} [annotation_type] Optional annotation type to filter by
|
|
163
|
-
* @returns {Annotation|null}
|
|
164
|
-
*/
|
|
165
|
-
active_annotation(annotation_type) {
|
|
166
|
-
return get_active_annotation(this.schema, this.doc, this.selection, annotation_type);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Applies an operation to the document (internal).
|
|
171
|
-
*
|
|
172
|
-
* @param {Array} op - The operation to apply
|
|
173
|
-
* @private
|
|
174
|
-
*/
|
|
175
|
-
_apply_op(op) {
|
|
176
|
-
this.doc = apply_op(this.doc, op);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* @param {NodeId[]} node_ids
|
|
181
|
-
* @param {NodeId} node_id
|
|
182
|
-
* @private
|
|
183
|
-
*/
|
|
184
|
-
_track_node_id(node_ids, node_id) {
|
|
185
|
-
if (!node_ids.includes(node_id)) node_ids.push(node_id);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Sets a property of a node to a new value.
|
|
190
|
-
*
|
|
191
|
-
* This is the core operation for modifying document properties. It records
|
|
192
|
-
* both the forward operation and its inverse for undo support.
|
|
193
|
-
*
|
|
194
|
-
* @param {DocumentPath} path - Array path to the property (e.g., ["node_1", "title"])
|
|
195
|
-
* @param {any} value - The new value to set
|
|
196
|
-
* @returns {Transaction} This transaction instance for method chaining
|
|
197
|
-
*
|
|
198
|
-
* @example
|
|
199
|
-
* ```js
|
|
200
|
-
* tr.set(["list_1", "list_items"], [1, 2, 3]);
|
|
201
|
-
* tr.set(["page_1", "body", "0", "description"], {text: "Hello world", annotations: []});
|
|
202
|
-
* ```
|
|
203
|
-
*/
|
|
204
|
-
set(path, value) {
|
|
205
|
-
const path_info = this.inspect(path);
|
|
206
|
-
if (path_info?.kind !== 'property') {
|
|
207
|
-
throw new Error(
|
|
208
|
-
`Transaction.set requires a path that points to a property, got ${JSON.stringify(path)}`
|
|
209
|
-
);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const node = this.get(path.slice(0, -1));
|
|
213
|
-
|
|
214
|
-
// Turns ["page_1", "body", "0", "description"]
|
|
215
|
-
// into ["paragraph_1", "description"].
|
|
216
|
-
// Important to keep changes of multiple ops invertible.
|
|
217
|
-
const normalized_path = [node.id, path.at(-1)];
|
|
218
|
-
|
|
219
|
-
// Just to be sure, make a deep copy of the old value
|
|
220
|
-
const property_key = path.at(-1);
|
|
221
|
-
const property_key_str = String(property_key);
|
|
222
|
-
const previous_value = structuredClone($state.snapshot(node[property_key_str]));
|
|
223
|
-
|
|
224
|
-
// Collect node IDs that might need to be deleted after the set operation
|
|
225
|
-
const prop_type = this.property_type(node.type, property_key_str);
|
|
226
|
-
/** @type {NodeId[]} */
|
|
227
|
-
let removed_node_ids = [];
|
|
228
|
-
|
|
229
|
-
if (prop_type === 'node' && typeof previous_value === 'string' && previous_value !== value) {
|
|
230
|
-
removed_node_ids = [previous_value];
|
|
231
|
-
} else if (
|
|
232
|
-
prop_type === 'node_array' &&
|
|
233
|
-
Array.isArray(previous_value) &&
|
|
234
|
-
Array.isArray(value)
|
|
235
|
-
) {
|
|
236
|
-
// Only include node IDs that were in previous_value but are not in the new value
|
|
237
|
-
removed_node_ids = previous_value.filter((id) => !value.includes(id));
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
const op = ['set', normalized_path, value];
|
|
241
|
-
this.ops.push(op);
|
|
242
|
-
this.inverse_ops.push(['set', normalized_path, previous_value]);
|
|
243
|
-
this._apply_op(op);
|
|
244
|
-
this._track_node_id(this.modified_node_ids, node.id);
|
|
245
|
-
|
|
246
|
-
// Garbage-collect nodes whose only reference was via this property.
|
|
247
|
-
// We must use a ref-count-aware sweep instead of unconditional deletion
|
|
248
|
-
// because a node ID removed from this array may still be referenced
|
|
249
|
-
// elsewhere — either from a pre-existing reference (e.g. shared nodes)
|
|
250
|
-
// or from a node created earlier in the same transaction (the
|
|
251
|
-
// wrap-in-group pattern: create a wrapper that points at the children,
|
|
252
|
-
// then set the parent array to drop them).
|
|
253
|
-
if (removed_node_ids.length > 0) {
|
|
254
|
-
this._cascade_delete_unreferenced_nodes(removed_node_ids);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
return this;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// Takes a subgraph and constructs new nodes from it
|
|
261
|
-
// NOTE: all ids will be mapped to new unique ids.
|
|
262
|
-
// NOTE: Omitted properties will be populated with default values.
|
|
263
|
-
build(node_id, nodes) {
|
|
264
|
-
const depth_first_nodes = traverse(node_id, this.schema, nodes);
|
|
265
|
-
// This maps original ids to newly generated ids
|
|
266
|
-
const id_map = {};
|
|
267
|
-
|
|
268
|
-
for (const node of depth_first_nodes) {
|
|
269
|
-
const new_id = this.generate_id();
|
|
270
|
-
id_map[node.id] = new_id;
|
|
271
|
-
let new_node = { ...node, id: new_id };
|
|
272
|
-
const node_schema = this.schema[node.type];
|
|
273
|
-
|
|
274
|
-
// Update all property references to use new IDs
|
|
275
|
-
for (const [property_name, property_definition] of Object.entries(node_schema.properties)) {
|
|
276
|
-
const prop_type = property_definition.type;
|
|
277
|
-
const value = new_node[property_name];
|
|
278
|
-
|
|
279
|
-
if (prop_type === 'node_array' && Array.isArray(value)) {
|
|
280
|
-
new_node[property_name] = value.map((ref_id) => id_map[ref_id]);
|
|
281
|
-
} else if (prop_type === 'node' && typeof value === 'string') {
|
|
282
|
-
new_node[property_name] = id_map[value];
|
|
283
|
-
} else if (prop_type === 'annotated_text' && value) {
|
|
284
|
-
const annotations = value.annotations.map((annotation) => {
|
|
285
|
-
const { start_offset, end_offset, node_id } = annotation;
|
|
286
|
-
return { start_offset, end_offset, node_id: id_map[node_id] || node_id };
|
|
287
|
-
});
|
|
288
|
-
new_node[property_name] = { text: value.text, annotations };
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
new_node = fill_node_defaults(new_node, this.schema);
|
|
293
|
-
this.create(new_node);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
return id_map[depth_first_nodes.at(-1).id];
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
/**
|
|
300
|
-
* Creates a new node in the document.
|
|
301
|
-
*
|
|
302
|
-
* The node must have a valid id and must not already exist in the document.
|
|
303
|
-
* Omitted properties with schema defaults are filled before validation.
|
|
304
|
-
*
|
|
305
|
-
* @param {any} node - The node object to create (must include id, type, and required properties)
|
|
306
|
-
* @returns {Transaction} This transaction instance for method chaining
|
|
307
|
-
* @throws {Error} If the node ID is invalid or if the node already exists
|
|
308
|
-
*
|
|
309
|
-
* @example
|
|
310
|
-
* ```js
|
|
311
|
-
* tr.create({
|
|
312
|
-
* id: 'para_123',
|
|
313
|
-
* type: 'paragraph',
|
|
314
|
-
* content: ['Hello world', []]
|
|
315
|
-
* });
|
|
316
|
-
* ```
|
|
317
|
-
*/
|
|
318
|
-
create(node) {
|
|
319
|
-
const node_with_defaults = fill_node_defaults(node, this.schema);
|
|
320
|
-
|
|
321
|
-
// Validate node against schema
|
|
322
|
-
this.validate_node(node_with_defaults);
|
|
323
|
-
|
|
324
|
-
if (this.get(node_with_defaults.id)) {
|
|
325
|
-
throw new Error('Node with id ' + node_with_defaults.id + ' already exists');
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
const op = ['create', node_with_defaults];
|
|
329
|
-
this.ops.push(op);
|
|
330
|
-
this.inverse_ops.push(['delete', node_with_defaults.id]);
|
|
331
|
-
this._apply_op(op);
|
|
332
|
-
this._track_node_id(this.created_node_ids, node_with_defaults.id);
|
|
333
|
-
return this;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
/**
|
|
337
|
-
* Deletes a node from the document by its ID.
|
|
338
|
-
*
|
|
339
|
-
* The node's current state is captured for undo support before deletion.
|
|
340
|
-
*
|
|
341
|
-
* NOTE: This is a force-delete and intentionally does NOT check whether
|
|
342
|
-
* the node is still referenced from elsewhere — callers (e.g.
|
|
343
|
-
* `annotate_text`) sometimes need to remove a node while a stale
|
|
344
|
-
* reference to it still exists in the property they are about to update.
|
|
345
|
-
* For ref-count-aware cleanup, see how `set` calls
|
|
346
|
-
* `_cascade_delete_unreferenced_nodes`.
|
|
347
|
-
*
|
|
348
|
-
* @param {any} id - The ID of the node to delete
|
|
349
|
-
* @returns {Transaction} This transaction instance for method chaining
|
|
350
|
-
*
|
|
351
|
-
* @example
|
|
352
|
-
* ```js
|
|
353
|
-
* tr.delete('node_123');
|
|
354
|
-
* ```
|
|
355
|
-
*/
|
|
356
|
-
delete(id) {
|
|
357
|
-
const previous_value = this.get(id);
|
|
358
|
-
if (!previous_value) {
|
|
359
|
-
console.warn(`Deletion of node ${id} skipped, as it does not exist.`);
|
|
360
|
-
return this;
|
|
361
|
-
}
|
|
362
|
-
// Get nodes referenced by this node BEFORE deleting it.
|
|
363
|
-
const referenced_nodes = this.get_referenced_nodes(id);
|
|
364
|
-
const op = ['delete', id];
|
|
365
|
-
this.ops.push(op);
|
|
366
|
-
this.inverse_ops.push(['create', previous_value]);
|
|
367
|
-
this._apply_op(op);
|
|
368
|
-
this._track_node_id(this.deleted_node_ids, id);
|
|
369
|
-
// Cascade delete any nodes that are now orphaned after removing this node
|
|
370
|
-
this._cascade_delete_unreferenced_nodes(referenced_nodes);
|
|
371
|
-
return this;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
/**
|
|
375
|
-
* Sets the document selection.
|
|
376
|
-
*
|
|
377
|
-
* @param {Selection} selection - The new selection object
|
|
378
|
-
* @returns {Transaction} This transaction instance for method chaining
|
|
379
|
-
* @throws {Error} Throws if the selection is invalid or out of bounds
|
|
380
|
-
*/
|
|
381
|
-
set_selection(selection) {
|
|
382
|
-
this._validate_selection(selection);
|
|
383
|
-
this.selection = selection;
|
|
384
|
-
return this;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
/**
|
|
388
|
-
* Validates a selection against the current document state.
|
|
389
|
-
*
|
|
390
|
-
* @param {Selection} selection - The selection to validate
|
|
391
|
-
* @throws {Error} Throws if the selection is invalid
|
|
392
|
-
* @private
|
|
393
|
-
*/
|
|
394
|
-
_validate_selection(selection) {
|
|
395
|
-
validate_selection(selection, this);
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
/**
|
|
399
|
-
* Adds, updates, or removes text annotations in the current selection.
|
|
400
|
-
*
|
|
401
|
-
* Handles various annotation scenarios including adding new annotations,
|
|
402
|
-
* updating existing ones (especially for links), and removing annotations
|
|
403
|
-
* when conflicting types are applied.
|
|
404
|
-
*
|
|
405
|
-
* @param {any} annotation_type - The type of annotation (e.g., 'link', 'bold', 'italic')
|
|
406
|
-
* @param {any} annotation_properties - Additional data for the annotation (e.g., href for links)
|
|
407
|
-
* @returns {Transaction} This transaction instance for method chaining
|
|
408
|
-
*
|
|
409
|
-
* @example
|
|
410
|
-
* ```js
|
|
411
|
-
* // Add a link annotation
|
|
412
|
-
* tr.annotate_text('link', { href: 'https://example.com' });
|
|
413
|
-
*
|
|
414
|
-
* // Add emphasis
|
|
415
|
-
* tr.annotate_text('emphasis', {});
|
|
416
|
-
* ```
|
|
417
|
-
*/
|
|
418
|
-
annotate_text(annotation_type, annotation_properties) {
|
|
419
|
-
if (this.selection.type !== 'text') return this;
|
|
420
|
-
|
|
421
|
-
const range = get_selection_range(this.selection);
|
|
422
|
-
const annotated_text = structuredClone($state.snapshot(this.get(this.selection.path)));
|
|
423
|
-
const annotations = annotated_text.annotations;
|
|
424
|
-
const existing_annotation = this.active_annotation();
|
|
425
|
-
const existing_annotation_same_type = this.active_annotation(annotation_type);
|
|
426
|
-
const has_annotation_properties =
|
|
427
|
-
annotation_properties && Object.keys(annotation_properties).length > 0;
|
|
428
|
-
|
|
429
|
-
if (existing_annotation) {
|
|
430
|
-
// If there's an existing annotation of the same type, remove it
|
|
431
|
-
if (existing_annotation_same_type) {
|
|
432
|
-
const index = annotations.findIndex(
|
|
433
|
-
/** @param {any} anno */ (anno) =>
|
|
434
|
-
anno.start_offset === existing_annotation.start_offset &&
|
|
435
|
-
anno.end_offset === existing_annotation.end_offset
|
|
436
|
-
);
|
|
437
|
-
if (index !== -1) {
|
|
438
|
-
// Remove the annotation node from the graph
|
|
439
|
-
this.delete(annotations[index].node_id);
|
|
440
|
-
annotations.splice(index, 1);
|
|
441
|
-
}
|
|
442
|
-
} else {
|
|
443
|
-
const existing_annotation_node = this.get(existing_annotation.node_id);
|
|
444
|
-
const can_switch_annotation =
|
|
445
|
-
!has_annotation_properties &&
|
|
446
|
-
this.available_annotation_types.includes(annotation_type) &&
|
|
447
|
-
can_switch_annotation_type(
|
|
448
|
-
this.schema,
|
|
449
|
-
existing_annotation_node,
|
|
450
|
-
annotation_type
|
|
451
|
-
);
|
|
452
|
-
|
|
453
|
-
if (can_switch_annotation) {
|
|
454
|
-
this.set([existing_annotation.node_id, 'type'], annotation_type);
|
|
455
|
-
return this;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
// If there's an annotation of a different incompatible type, don't add a new one
|
|
459
|
-
return this;
|
|
460
|
-
}
|
|
461
|
-
} else {
|
|
462
|
-
if (is_selection_collapsed(this.selection)) {
|
|
463
|
-
return this;
|
|
464
|
-
}
|
|
465
|
-
if (!this.available_annotation_types.includes(annotation_type)) {
|
|
466
|
-
console.warn(`Annotation type ${annotation_type} is not allowed here.`);
|
|
467
|
-
return this;
|
|
468
|
-
}
|
|
469
|
-
const new_annotation_node = {
|
|
470
|
-
id: this.generate_id(),
|
|
471
|
-
type: annotation_type,
|
|
472
|
-
...annotation_properties
|
|
473
|
-
};
|
|
474
|
-
this.create(new_annotation_node);
|
|
475
|
-
// If there's no existing annotation, add the new one
|
|
476
|
-
annotations.push({
|
|
477
|
-
start_offset: range.start_offset,
|
|
478
|
-
end_offset: range.end_offset,
|
|
479
|
-
node_id: new_annotation_node.id
|
|
480
|
-
});
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
// Update the annotated text
|
|
484
|
-
this.set(this.selection.path, annotated_text);
|
|
485
|
-
return this;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
/**
|
|
489
|
-
* Deletes the currently selected text, node, or property.
|
|
490
|
-
*
|
|
491
|
-
* Behavior depends on selection type:
|
|
492
|
-
* - For node selections: Removes selected nodes and cascades deletion of unreferenced nodes
|
|
493
|
-
* - For text selections: Removes selected text and adjusts annotations accordingly
|
|
494
|
-
* - For collapsed selections: Deletes the previous character/node (backward) or next character/node (forward)
|
|
495
|
-
* - For property selections: Delegates to the config's handle_property_deletion hook
|
|
496
|
-
*
|
|
497
|
-
* @param {'backward' | 'forward'} [direction] - Direction of deletion for collapsed selections
|
|
498
|
-
* @returns {Transaction} This transaction instance for method chaining
|
|
499
|
-
*/
|
|
500
|
-
delete_selection(direction = 'backward') {
|
|
501
|
-
if (!this.selection) return this;
|
|
502
|
-
|
|
503
|
-
if (this.selection.type === 'property') {
|
|
504
|
-
this.config.handle_property_deletion?.(this, this.selection.path);
|
|
505
|
-
return this;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
const path = this.selection.path;
|
|
509
|
-
|
|
510
|
-
// Get the start and end indices for the selection
|
|
511
|
-
let start = Math.min(this.selection.anchor_offset, this.selection.focus_offset);
|
|
512
|
-
let end = Math.max(this.selection.anchor_offset, this.selection.focus_offset);
|
|
513
|
-
let length;
|
|
514
|
-
|
|
515
|
-
if (this.selection?.type === 'text') {
|
|
516
|
-
const text_content = this.get(this.selection.path).text;
|
|
517
|
-
length = get_char_length(text_content);
|
|
518
|
-
} else if (this.selection?.type === 'node') {
|
|
519
|
-
const node_array = this.get(this.selection.path);
|
|
520
|
-
length = node_array.length;
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
// If selection is collapsed we delete the previous char/node (backward)
|
|
524
|
-
// or the next char/node (forward)
|
|
525
|
-
if (start === end) {
|
|
526
|
-
if (direction === 'backward' && start > 0) {
|
|
527
|
-
start = start - 1;
|
|
528
|
-
} else if (direction === 'forward' && end < length) {
|
|
529
|
-
end = end + 1;
|
|
530
|
-
} else if (direction === 'backward' && start === 0) {
|
|
531
|
-
join_text_node(this);
|
|
532
|
-
return this;
|
|
533
|
-
} else if (direction === 'forward' && end === length) {
|
|
534
|
-
// At end of text - try to join with next text node
|
|
535
|
-
const node_index = parseInt(String(this.selection.path.at(-2)), 10);
|
|
536
|
-
const successor_node = this.get([...this.selection.path.slice(0, -2), node_index + 1]);
|
|
537
|
-
// Check if next node is a text node
|
|
538
|
-
if (successor_node && this.kind(successor_node) === 'text') {
|
|
539
|
-
// Set selection to beginning of next text node
|
|
540
|
-
this.set_selection({
|
|
541
|
-
type: 'text',
|
|
542
|
-
path: [...this.selection.path.slice(0, -2), node_index + 1, 'content'],
|
|
543
|
-
anchor_offset: 0,
|
|
544
|
-
focus_offset: 0
|
|
545
|
-
});
|
|
546
|
-
// Use join_text_node to merge with previous node
|
|
547
|
-
join_text_node(this);
|
|
548
|
-
}
|
|
549
|
-
return this;
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
if (this.selection.type === 'node') {
|
|
554
|
-
const node_array = [...this.get(path)];
|
|
555
|
-
|
|
556
|
-
// Remove the selected nodes from the node_array
|
|
557
|
-
node_array.splice(start, end - start);
|
|
558
|
-
|
|
559
|
-
// Update the node_array in the entry (this implicitly records an op via this.set)
|
|
560
|
-
// Note: this.set() will automatically cascade delete unreferenced nodes
|
|
561
|
-
this.set(path, node_array);
|
|
562
|
-
|
|
563
|
-
// Update the selection to point to the start of the deleted range
|
|
564
|
-
this.selection = {
|
|
565
|
-
type: 'node',
|
|
566
|
-
path,
|
|
567
|
-
anchor_offset: start,
|
|
568
|
-
focus_offset: start
|
|
569
|
-
};
|
|
570
|
-
} else if (this.selection.type === 'text') {
|
|
571
|
-
const path = this.selection.path;
|
|
572
|
-
let text = structuredClone($state.snapshot(this.get(path)));
|
|
573
|
-
|
|
574
|
-
// Update the text content using character-based operations
|
|
575
|
-
const original_text = text.text;
|
|
576
|
-
text.text =
|
|
577
|
-
char_slice(original_text, 0, start) +
|
|
578
|
-
char_slice(original_text, end, get_char_length(original_text));
|
|
579
|
-
|
|
580
|
-
// To mark annotation nodes for deletion.
|
|
581
|
-
/** @type {string[]} */
|
|
582
|
-
const _deleted_nodes = [];
|
|
583
|
-
const deletion_length = end - start;
|
|
584
|
-
const new_annotations = text.annotations
|
|
585
|
-
.map((/** @type {any} */ annotation) => {
|
|
586
|
-
const annotation_start = annotation.start_offset;
|
|
587
|
-
const annotation_end = annotation.end_offset;
|
|
588
|
-
const node_id = annotation.node_id;
|
|
589
|
-
|
|
590
|
-
// Case 1: Annotation is entirely before the deleted range - keep unchanged
|
|
591
|
-
if (annotation_end <= start) {
|
|
592
|
-
return annotation;
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
// Case 2: Annotation is entirely after the deleted range - shift it
|
|
596
|
-
let new_start = annotation_start;
|
|
597
|
-
if (annotation_start >= end) {
|
|
598
|
-
new_start = annotation_start - deletion_length;
|
|
599
|
-
} else if (annotation_start > start) {
|
|
600
|
-
// Annotation starts inside deleted range
|
|
601
|
-
new_start = start;
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
// Case 3: Annotation overlaps with deleted range - adjust end
|
|
605
|
-
let new_end = annotation_end;
|
|
606
|
-
if (annotation_end >= end) {
|
|
607
|
-
new_end = annotation_end - deletion_length;
|
|
608
|
-
} else if (annotation_end > start) {
|
|
609
|
-
// Annotation ends inside deleted range
|
|
610
|
-
new_end = start;
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
// If annotation is now empty, mark for deletion
|
|
614
|
-
if (new_start >= new_end) {
|
|
615
|
-
_deleted_nodes.push(node_id);
|
|
616
|
-
return null;
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
return { start_offset: new_start, end_offset: new_end, node_id };
|
|
620
|
-
})
|
|
621
|
-
.filter(Boolean);
|
|
622
|
-
|
|
623
|
-
text.annotations = new_annotations;
|
|
624
|
-
|
|
625
|
-
this.set(path, text);
|
|
626
|
-
|
|
627
|
-
// Delete marked annotation nodes only if no other references remain.
|
|
628
|
-
this._cascade_delete_unreferenced_nodes(_deleted_nodes);
|
|
629
|
-
|
|
630
|
-
// Update the selection to the new caret position
|
|
631
|
-
this.selection = {
|
|
632
|
-
type: 'text',
|
|
633
|
-
path,
|
|
634
|
-
anchor_offset: start,
|
|
635
|
-
focus_offset: start
|
|
636
|
-
};
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
return this;
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
/**
|
|
643
|
-
* Inserts nodes at the current node selection position.
|
|
644
|
-
*
|
|
645
|
-
* If the selection is expanded (not collapsed), first deletes the selected nodes
|
|
646
|
-
* before inserting the new ones.
|
|
647
|
-
*
|
|
648
|
-
* @param {NodeId[]} node_ids - Array of node IDs to insert
|
|
649
|
-
* @returns {Transaction} This transaction instance for method chaining
|
|
650
|
-
*/
|
|
651
|
-
insert_nodes(node_ids) {
|
|
652
|
-
if (this.selection.type !== 'node') return this;
|
|
653
|
-
|
|
654
|
-
// Unless caret is collapsed, delete the selected nodes as a first step
|
|
655
|
-
if (this.selection.anchor_offset !== this.selection.focus_offset) {
|
|
656
|
-
this.delete_selection();
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
const path = this.selection.path;
|
|
660
|
-
const node_array = [...this.get(path)];
|
|
661
|
-
|
|
662
|
-
let start = Math.min(this.selection.anchor_offset, this.selection.focus_offset);
|
|
663
|
-
|
|
664
|
-
// Insert the new nodes
|
|
665
|
-
node_array.splice(start, 0, ...node_ids);
|
|
666
|
-
this.set(path, node_array);
|
|
667
|
-
|
|
668
|
-
this.selection = {
|
|
669
|
-
type: 'node',
|
|
670
|
-
path: [...this.selection.path],
|
|
671
|
-
anchor_offset: start,
|
|
672
|
-
focus_offset: start + node_ids.length
|
|
673
|
-
};
|
|
674
|
-
|
|
675
|
-
return this;
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
/**
|
|
679
|
-
* Inserts text at the current text selection position.
|
|
680
|
-
*
|
|
681
|
-
* Handles annotation adjustments when text is inserted, including:
|
|
682
|
-
* - Expanding annotations that contain the insertion point
|
|
683
|
-
* - Shifting annotations that come after the insertion point
|
|
684
|
-
* - Optionally applying new annotations to the inserted text
|
|
685
|
-
*
|
|
686
|
-
* @param {string} replaced_text - The text to insert
|
|
687
|
-
* @param {Array} [annotations] - Optional annotations to apply to the inserted text
|
|
688
|
-
* @param {object} [nodes] - Optional node definitions for annotation nodes
|
|
689
|
-
* @returns {Transaction} This transaction instance for method chaining
|
|
690
|
-
*/
|
|
691
|
-
insert_text(replaced_text, annotations = [], nodes = {}) {
|
|
692
|
-
if (this.selection?.type !== 'text') return this;
|
|
693
|
-
|
|
694
|
-
// Unless selection is collapsed, delete the selected content
|
|
695
|
-
// NOTE: This makes sure wrapped annotations are disposed correctly
|
|
696
|
-
if (!is_selection_collapsed(this.selection)) {
|
|
697
|
-
this.delete_selection();
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
const annotated_text = structuredClone($state.snapshot(this.get(this.selection.path)));
|
|
701
|
-
const range = get_selection_range(this.selection);
|
|
702
|
-
|
|
703
|
-
// Transform the plain text string using character-based operations
|
|
704
|
-
const text = annotated_text.text;
|
|
705
|
-
annotated_text.text =
|
|
706
|
-
char_slice(text, 0, range.start_offset) + replaced_text + char_slice(text, range.end_offset);
|
|
707
|
-
|
|
708
|
-
// Calculate the change in length
|
|
709
|
-
const delta = get_char_length(replaced_text);
|
|
710
|
-
|
|
711
|
-
const new_annotations = annotated_text.annotations.map((/** @type {any} */ annotation) => {
|
|
712
|
-
const annotation_start = annotation.start_offset;
|
|
713
|
-
const annotation_end = annotation.end_offset;
|
|
714
|
-
const node_id = annotation.node_id;
|
|
715
|
-
|
|
716
|
-
// Annotation is entirely before the insertion point
|
|
717
|
-
if (annotation_end <= range.start_offset) {
|
|
718
|
-
return annotation;
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
// Insertion point is inside the annotation (not at the start) - extend it
|
|
722
|
-
if (annotation_start < range.start_offset && annotation_end >= range.start_offset) {
|
|
723
|
-
return {
|
|
724
|
-
start_offset: annotation_start,
|
|
725
|
-
end_offset: annotation_end + delta,
|
|
726
|
-
node_id
|
|
727
|
-
};
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
// Annotation is entirely after the insertion point - shift it
|
|
731
|
-
if (annotation_start >= range.start_offset) {
|
|
732
|
-
return {
|
|
733
|
-
start_offset: annotation_start + delta,
|
|
734
|
-
end_offset: annotation_end + delta,
|
|
735
|
-
node_id
|
|
736
|
-
};
|
|
737
|
-
}
|
|
738
|
-
return annotation;
|
|
739
|
-
});
|
|
740
|
-
|
|
741
|
-
annotated_text.annotations = new_annotations;
|
|
742
|
-
this.set(this.selection.path, annotated_text); // this will update the current state and create a history entry
|
|
743
|
-
|
|
744
|
-
// Setting the selection automatically triggers a re-render of the corresponding DOMSelection.
|
|
745
|
-
/** @type {Selection} */
|
|
746
|
-
const new_selection = {
|
|
747
|
-
type: /** @type {const} */ ('text'),
|
|
748
|
-
path: this.selection.path,
|
|
749
|
-
anchor_offset: range.start_offset + get_char_length(replaced_text),
|
|
750
|
-
focus_offset: range.start_offset + get_char_length(replaced_text)
|
|
751
|
-
};
|
|
752
|
-
this.selection = new_selection;
|
|
753
|
-
|
|
754
|
-
// Now we apply annotations if there are any, but only if there's no active annotation
|
|
755
|
-
// at the current collapsed caret
|
|
756
|
-
if (!this.active_annotation() && annotations.length > 0) {
|
|
757
|
-
const new_annotations = annotations
|
|
758
|
-
.map((annotation) => {
|
|
759
|
-
const original_annotation_node = nodes[annotation.node_id];
|
|
760
|
-
const text_property_definition = this.inspect(this.selection.path);
|
|
761
|
-
// console.log('original_annotation_node.type', original_annotation_node.type);
|
|
762
|
-
// console.log('text_property_definition', text_property_definition);
|
|
763
|
-
if (text_property_definition.node_types.includes(original_annotation_node.type)) {
|
|
764
|
-
const new_annotation_node_id = this.build(annotation.node_id, nodes);
|
|
765
|
-
return {
|
|
766
|
-
start_offset: range.start_offset + annotation.start_offset,
|
|
767
|
-
end_offset: range.start_offset + annotation.end_offset,
|
|
768
|
-
node_id: new_annotation_node_id
|
|
769
|
-
};
|
|
770
|
-
}
|
|
771
|
-
return null;
|
|
772
|
-
})
|
|
773
|
-
.filter(Boolean);
|
|
774
|
-
const next_annotated_text = structuredClone(annotated_text);
|
|
775
|
-
next_annotated_text.annotations = next_annotated_text.annotations.concat(new_annotations);
|
|
776
|
-
this.set(this.selection.path, next_annotated_text); // this will update the current state and create a history entry
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
return this;
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
/**
|
|
783
|
-
* Recursively deletes nodes that are no longer referenced in the document.
|
|
784
|
-
*
|
|
785
|
-
* This handles the cascade deletion of child nodes when their parent
|
|
786
|
-
* references are removed. Uses reference counting to determine which
|
|
787
|
-
* nodes are safe to delete.
|
|
788
|
-
*
|
|
789
|
-
* @param {NodeId[]} potentially_orphaned_nodes - Array of node IDs to check
|
|
790
|
-
* @private
|
|
791
|
-
*/
|
|
792
|
-
_cascade_delete_unreferenced_nodes(potentially_orphaned_nodes) {
|
|
793
|
-
/** @type {Record<NodeId, boolean>} */
|
|
794
|
-
const nodes_to_delete = {};
|
|
795
|
-
const to_check = [...potentially_orphaned_nodes];
|
|
796
|
-
|
|
797
|
-
while (to_check.length > 0) {
|
|
798
|
-
const node_id = to_check.pop();
|
|
799
|
-
if (!node_id || nodes_to_delete[node_id]) continue;
|
|
800
|
-
|
|
801
|
-
// Count references to this node, excluding nodes already marked for deletion
|
|
802
|
-
const ref_count = this._count_references_excluding_deleted(node_id, nodes_to_delete);
|
|
803
|
-
|
|
804
|
-
if (ref_count === 0) {
|
|
805
|
-
// No more references, safe to delete this node
|
|
806
|
-
nodes_to_delete[node_id] = true;
|
|
807
|
-
|
|
808
|
-
// Also check all nodes referenced by this node
|
|
809
|
-
const referenced_nodes = this.get_referenced_nodes(node_id);
|
|
810
|
-
to_check.push(...referenced_nodes);
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
// Now perform the actual deletions
|
|
815
|
-
for (const node_id of Object.keys(nodes_to_delete)) {
|
|
816
|
-
const previous_value = this.get([node_id]);
|
|
817
|
-
if (previous_value) {
|
|
818
|
-
const op = ['delete', node_id];
|
|
819
|
-
this.ops.push(op);
|
|
820
|
-
this.inverse_ops.push(['create', previous_value]);
|
|
821
|
-
this._apply_op(op);
|
|
822
|
-
this._track_node_id(this.deleted_node_ids, node_id);
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
/**
|
|
828
|
-
* Counts references to a node, excluding nodes that have been marked for deletion.
|
|
829
|
-
*
|
|
830
|
-
* This is used during cascade deletion to accurately count remaining references
|
|
831
|
-
* as nodes are being deleted.
|
|
832
|
-
*
|
|
833
|
-
* @param {NodeId} target_node_id - The node ID to count references for
|
|
834
|
-
* @param {Record<NodeId, boolean>} nodes_to_delete - Nodes already marked for deletion
|
|
835
|
-
* @returns {number} The number of references to the target node
|
|
836
|
-
* @private
|
|
837
|
-
*/
|
|
838
|
-
_count_references_excluding_deleted(target_node_id, nodes_to_delete) {
|
|
839
|
-
return count_references_excluding_deleted(
|
|
840
|
-
this.schema,
|
|
841
|
-
this.doc,
|
|
842
|
-
target_node_id,
|
|
843
|
-
nodes_to_delete
|
|
844
|
-
);
|
|
845
|
-
}
|
|
846
|
-
}
|