svedit 0.9.0 → 0.10.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.
Files changed (68) hide show
  1. package/README.md +113 -4
  2. package/dist/AnnotatedTextProperty.svelte +41 -8
  3. package/dist/AnnotatedTextProperty.svelte.d.ts.map +1 -1
  4. package/dist/Command.svelte.d.ts +0 -1
  5. package/dist/Command.svelte.d.ts.map +1 -1
  6. package/dist/Command.svelte.js +18 -3
  7. package/dist/CustomProperty.svelte +20 -9
  8. package/dist/CustomProperty.svelte.d.ts +2 -1
  9. package/dist/CustomProperty.svelte.d.ts.map +1 -1
  10. package/dist/Node.svelte +7 -5
  11. package/dist/Node.svelte.d.ts.map +1 -1
  12. package/dist/NodeArrayProperty.svelte +65 -42
  13. package/dist/NodeArrayProperty.svelte.d.ts.map +1 -1
  14. package/dist/NodeCaret.svelte +0 -7
  15. package/dist/NodeGap.svelte +179 -81
  16. package/dist/NodeGap.svelte.d.ts +0 -2
  17. package/dist/NodeGap.svelte.d.ts.map +1 -1
  18. package/dist/NodeGapMarkers.svelte +251 -84
  19. package/dist/NodeGapMarkers.svelte.d.ts +2 -0
  20. package/dist/NodeGapMarkers.svelte.d.ts.map +1 -1
  21. package/dist/NodeSelectionMarkers.svelte +4 -3
  22. package/dist/NodeSelectionMarkers.svelte.d.ts.map +1 -1
  23. package/dist/Session.svelte.d.ts +12 -1
  24. package/dist/Session.svelte.d.ts.map +1 -1
  25. package/dist/Session.svelte.js +40 -3
  26. package/dist/Svedit.svelte +336 -165
  27. package/dist/Svedit.svelte.d.ts.map +1 -1
  28. package/dist/Transaction.svelte.d.ts +24 -5
  29. package/dist/Transaction.svelte.d.ts.map +1 -1
  30. package/dist/Transaction.svelte.js +103 -61
  31. package/dist/doc_utils.d.ts +64 -3
  32. package/dist/doc_utils.d.ts.map +1 -1
  33. package/dist/doc_utils.js +276 -10
  34. package/dist/index.d.ts +1 -1
  35. package/dist/index.js +5 -1
  36. package/dist/node_visibility.svelte.d.ts +9 -0
  37. package/dist/node_visibility.svelte.d.ts.map +1 -0
  38. package/dist/node_visibility.svelte.js +718 -0
  39. package/dist/styles/svedit-animations.css +8 -0
  40. package/dist/styles/svedit-colors.css +1 -0
  41. package/dist/types.d.ts +46 -3
  42. package/dist/utils.d.ts +52 -0
  43. package/dist/utils.d.ts.map +1 -1
  44. package/dist/utils.js +109 -3
  45. package/package.json +3 -2
  46. package/src/lib/AnnotatedTextProperty.svelte +41 -8
  47. package/src/lib/Command.svelte.js +18 -3
  48. package/src/lib/CustomProperty.svelte +20 -9
  49. package/src/lib/Node.svelte +7 -5
  50. package/src/lib/NodeArrayProperty.svelte +65 -42
  51. package/src/lib/NodeCaret.svelte +0 -7
  52. package/src/lib/NodeGap.svelte +179 -81
  53. package/src/lib/NodeGapMarkers.svelte +251 -84
  54. package/src/lib/NodeSelectionMarkers.svelte +4 -3
  55. package/src/lib/Session.svelte.js +40 -3
  56. package/src/lib/Svedit.svelte +336 -165
  57. package/src/lib/Transaction.svelte.js +103 -61
  58. package/src/lib/doc_utils.js +276 -10
  59. package/src/lib/index.js +5 -1
  60. package/src/lib/node_visibility.svelte.js +718 -0
  61. package/src/lib/styles/svedit-animations.css +8 -0
  62. package/src/lib/styles/svedit-colors.css +1 -0
  63. package/src/lib/types.d.ts +46 -3
  64. package/src/lib/utils.js +109 -3
  65. package/dist/node_gap_computation.svelte.d.ts +0 -5
  66. package/dist/node_gap_computation.svelte.d.ts.map +0 -1
  67. package/dist/node_gap_computation.svelte.js +0 -621
  68. package/src/lib/node_gap_computation.svelte.js +0 -621
@@ -4,17 +4,23 @@
4
4
  * These functions operate on the core document state (schema, doc, selection, config)
5
5
  * without any history management or transaction tracking.
6
6
  *
7
- * @import { NodeId, DocumentPath, PrimitiveType, NodeProperty, NodeArrayProperty, NodeSchema, DocumentSchema, Selection, Annotation, Document } from './types'
7
+ * @import { NodeId, DocumentPath, PrimitiveType, NodeProperty, NodeArrayProperty, NodeSchema, DocumentSchema, Selection, Annotation, Document, AnnotatedTextProperty, AnnotatedText, ValidateDocumentSchema } from './types'
8
8
  */
9
9
 
10
- import { get_selection_range, get_char_length } from './utils.js';
10
+ import {
11
+ assert_path_string_segment,
12
+ is_path_string_segment_valid,
13
+ get_selection_range,
14
+ get_char_length,
15
+ serialize_path
16
+ } from './utils.js';
11
17
 
12
18
  /**
13
19
  * Identity function — keeps schema at runtime & makes IDE infer types.
14
20
  * Similar to your define_schema pattern but for document schemas.
15
21
  *
16
22
  * @template {Record<string, NodeSchema>} S
17
- * @param {S} schema - The document schema to validate
23
+ * @param {S & ValidateDocumentSchema<S>} schema - The document schema to validate
18
24
  * @returns {S} The same schema, but with type information preserved
19
25
  */
20
26
  export function define_document_schema(schema) {
@@ -57,6 +63,84 @@ export function get_default_node_type(property_definition) {
57
63
  );
58
64
  }
59
65
 
66
+ /**
67
+ * @param {import('./types').PropertyDefinition} property_definition
68
+ * @returns {any}
69
+ */
70
+ function get_property_default(property_definition) {
71
+ if ('default' in property_definition) return structuredClone(property_definition.default);
72
+
73
+ if (property_definition.type === 'string') return '';
74
+ if (property_definition.type === 'integer') return 0;
75
+ if (property_definition.type === 'number') return 0;
76
+ if (property_definition.type === 'boolean') return false;
77
+ if (property_definition.type === 'annotated_text') return { text: '', annotations: [] };
78
+ if (property_definition.type === 'node_array') return [];
79
+ if (
80
+ property_definition.type === 'string_array' ||
81
+ property_definition.type === 'number_array' ||
82
+ property_definition.type === 'boolean_array' ||
83
+ property_definition.type === 'integer_array'
84
+ ) {
85
+ return [];
86
+ }
87
+
88
+ return undefined;
89
+ }
90
+
91
+ /**
92
+ * Fill omitted properties with schema defaults and Svedit's built-in type defaults.
93
+ *
94
+ * This is a convenience helper for schema evolution, not a complete document
95
+ * migration system. Callers are still responsible for proper migrations when
96
+ * schema changes cannot be represented by defaults, such as property renames or
97
+ * data transformations.
98
+ *
99
+ * @param {any} node - The node to fill defaults for
100
+ * @param {DocumentSchema} schema - The document schema
101
+ * @returns {any} A shallow copy of the node with cloned default values filled in
102
+ */
103
+ export function fill_node_defaults(node, schema) {
104
+ const node_schema = schema[node.type];
105
+ if (!node_schema) return { ...node };
106
+
107
+ const node_with_defaults = { ...node };
108
+
109
+ for (const [property_name, property_definition] of Object.entries(node_schema.properties)) {
110
+ if (node_with_defaults[property_name] === undefined) {
111
+ const property_default = get_property_default(property_definition);
112
+ if (property_default !== undefined) node_with_defaults[property_name] = property_default;
113
+ }
114
+ }
115
+
116
+ return node_with_defaults;
117
+ }
118
+
119
+ /**
120
+ * Fill omitted properties with schema defaults and Svedit's built-in type defaults across a document.
121
+ *
122
+ * This does not infer values, rename fields, or make an invalid migration valid
123
+ * by itself. Call this as one step in an explicit document migration when it is
124
+ * appropriate.
125
+ *
126
+ * @param {Document} doc - The document to fill defaults for
127
+ * @param {DocumentSchema} schema - The document schema
128
+ * @returns {Document} A document copy with cloned default values filled in
129
+ */
130
+ export function fill_document_defaults(doc, schema) {
131
+ /** @type {Record<string, any>} */
132
+ const nodes = {};
133
+
134
+ for (const [node_id, node] of Object.entries(doc.nodes)) {
135
+ nodes[node_id] = fill_node_defaults(node, schema);
136
+ }
137
+
138
+ return {
139
+ ...doc,
140
+ nodes
141
+ };
142
+ }
143
+
60
144
  /**
61
145
  * Validate a document schema to ensure all referenced node types exist.
62
146
  * @param {DocumentSchema} document_schema - The document schema to validate
@@ -66,6 +150,7 @@ export function validate_document_schema(document_schema) {
66
150
  // Check that all referenced node types exist
67
151
  for (const [node_type, node_schema] of Object.entries(document_schema)) {
68
152
  for (const [prop_name, prop_def] of Object.entries(node_schema.properties)) {
153
+ assert_path_string_segment(prop_name, `Property name "${prop_name}"`);
69
154
  if (prop_def.type === 'node' || prop_def.type === 'node_array') {
70
155
  const missing_types = prop_def.node_types.filter(
71
156
  (ref_type) => !(ref_type in document_schema)
@@ -118,12 +203,88 @@ function validate_primitive_value(type, value) {
118
203
  }
119
204
  }
120
205
 
206
+ /**
207
+ * Validate annotated text annotations for bounds, references, allowed types, and exclusivity.
208
+ *
209
+ * @param {string} node_id - Owner node id, used for error messages
210
+ * @param {string} prop_name - Owner property name, used for error messages
211
+ * @param {AnnotatedText} value - Annotated text value to validate
212
+ * @param {AnnotatedTextProperty} prop_def - Annotated text property definition
213
+ * @param {Record<string, any>} all_nodes - All document nodes
214
+ * @param {boolean} require_references - Whether referenced nodes must already exist
215
+ * @throws {Error} Throws if annotations are invalid
216
+ */
217
+ function validate_annotated_text_property(node_id, prop_name, value, prop_def, all_nodes, require_references) {
218
+ const char_length = get_char_length(value.text);
219
+ const annotations = value.annotations;
220
+
221
+ for (const [index, annotation] of annotations.entries()) {
222
+ if (
223
+ typeof annotation !== 'object' ||
224
+ annotation === null ||
225
+ !Number.isInteger(annotation.start_offset) ||
226
+ !Number.isInteger(annotation.end_offset) ||
227
+ !is_id_valid(annotation.node_id)
228
+ ) {
229
+ throw new Error(
230
+ `Node ${node_id} property ${prop_name} has an invalid annotation at index ${index}. Annotations must have integer start_offset/end_offset and a valid node_id.`
231
+ );
232
+ }
233
+
234
+ if (annotation.start_offset < 0 || annotation.end_offset > char_length) {
235
+ throw new Error(
236
+ `Node ${node_id} property ${prop_name} annotation ${annotation.node_id} is out of bounds: ${annotation.start_offset}-${annotation.end_offset}, text length is ${char_length}.`
237
+ );
238
+ }
239
+
240
+ if (annotation.start_offset >= annotation.end_offset) {
241
+ throw new Error(
242
+ `Node ${node_id} property ${prop_name} annotation ${annotation.node_id} must not be empty or reversed: ${annotation.start_offset}-${annotation.end_offset}.`
243
+ );
244
+ }
245
+
246
+ const referenced_node = all_nodes[annotation.node_id];
247
+ if (!referenced_node) {
248
+ if (require_references) {
249
+ throw new Error(
250
+ `Node ${node_id} property ${prop_name} annotation references missing node ${annotation.node_id}.`
251
+ );
252
+ }
253
+ continue;
254
+ }
255
+
256
+ if (prop_def.node_types?.length && !prop_def.node_types.includes(referenced_node.type)) {
257
+ throw new Error(
258
+ `Node ${node_id} property ${prop_name} annotation references node ${annotation.node_id} of type ${referenced_node.type}, but only types [${prop_def.node_types.join(', ')}] are allowed.`
259
+ );
260
+ }
261
+ }
262
+
263
+ const sorted_annotations = annotations
264
+ .map((annotation, index) => ({ annotation, index }))
265
+ .sort((a, b) =>
266
+ a.annotation.start_offset - b.annotation.start_offset ||
267
+ a.annotation.end_offset - b.annotation.end_offset
268
+ );
269
+
270
+ for (let index = 1; index < sorted_annotations.length; index++) {
271
+ const previous = sorted_annotations[index - 1];
272
+ const current = sorted_annotations[index];
273
+
274
+ if (current.annotation.start_offset < previous.annotation.end_offset) {
275
+ throw new Error(
276
+ `Node ${node_id} property ${prop_name} has overlapping annotations: annotation index ${previous.index} (${previous.annotation.node_id}, ${previous.annotation.start_offset}-${previous.annotation.end_offset}) overlaps annotation index ${current.index} (${current.annotation.node_id}, ${current.annotation.start_offset}-${current.annotation.end_offset}). Annotations must be exclusive.`
277
+ );
278
+ }
279
+ }
280
+ }
281
+
121
282
  /**
122
283
  * @param {string} id
123
284
  * @returns {boolean}
124
285
  */
125
- function is_id_valid(id) {
126
- return typeof id === 'string' && id.length > 0;
286
+ export function is_id_valid(id) {
287
+ return typeof id === 'string' && id.length > 0 && is_path_string_segment_valid(id);
127
288
  }
128
289
 
129
290
  /**
@@ -131,9 +292,11 @@ function is_id_valid(id) {
131
292
  * @param {any} node - The node to validate
132
293
  * @param {DocumentSchema} schema - The document schema
133
294
  * @param {Record<string, any>} all_nodes - All nodes in the document to check references
295
+ * @param {{ require_references?: boolean }} [options] - Validation options
134
296
  * @throws {Error} Throws if the node is invalid
135
297
  */
136
- export function validate_node(node, schema, all_nodes = {}) {
298
+ export function validate_node(node, schema, all_nodes = {}, options = {}) {
299
+ const require_references = options.require_references ?? true;
137
300
  if (!is_id_valid(node.id)) {
138
301
  throw new Error(`Node ${node.id} has an invalid id.`);
139
302
  }
@@ -155,6 +318,16 @@ export function validate_node(node, schema, all_nodes = {}) {
155
318
  );
156
319
  }
157
320
  }
321
+ if (prop_def.type === 'annotated_text') {
322
+ validate_annotated_text_property(
323
+ node.id,
324
+ prop_name,
325
+ value,
326
+ /** @type {AnnotatedTextProperty} */ (prop_def),
327
+ all_nodes,
328
+ require_references
329
+ );
330
+ }
158
331
  // Check node references
159
332
  if (prop_def.type === 'node') {
160
333
  if (!is_id_valid(value)) {
@@ -164,7 +337,15 @@ export function validate_node(node, schema, all_nodes = {}) {
164
337
  }
165
338
  // Check if referenced node exists and is of allowed type
166
339
  const referenced_node = all_nodes[value];
167
- if (referenced_node && !prop_def.node_types.includes(referenced_node.type)) {
340
+ if (!referenced_node) {
341
+ if (require_references) {
342
+ throw new Error(
343
+ `Node ${node.id} property ${prop_name} references missing node ${value}.`
344
+ );
345
+ }
346
+ continue;
347
+ }
348
+ if (!prop_def.node_types.includes(referenced_node.type)) {
168
349
  throw new Error(
169
350
  `Node ${node.id} property ${prop_name} references node ${value} of type ${referenced_node.type}, but only types [${/** @type {NodeProperty} */ (prop_def).node_types.join(', ')}] are allowed.`
170
351
  );
@@ -180,10 +361,18 @@ export function validate_node(node, schema, all_nodes = {}) {
180
361
  `Node ${node.id} has an invalid property: ${prop_name} must be an array of node ids.`
181
362
  );
182
363
  }
183
- // Check if all referenced nodes are of allowed types
364
+ // Check if all referenced nodes exist and are of allowed types
184
365
  for (const ref_id of value) {
185
366
  const referenced_node = all_nodes[ref_id];
186
- if (referenced_node && !prop_def.node_types.includes(referenced_node.type)) {
367
+ if (!referenced_node) {
368
+ if (require_references) {
369
+ throw new Error(
370
+ `Node ${node.id} property ${prop_name} references missing node ${ref_id}.`
371
+ );
372
+ }
373
+ continue;
374
+ }
375
+ if (!prop_def.node_types.includes(referenced_node.type)) {
187
376
  throw new Error(
188
377
  `Node ${node.id} property ${prop_name} references node ${ref_id} of type ${referenced_node.type}, but only types [${prop_def.node_types.join(', ')}] are allowed.`
189
378
  );
@@ -193,6 +382,22 @@ export function validate_node(node, schema, all_nodes = {}) {
193
382
  }
194
383
  }
195
384
 
385
+ /**
386
+ * Validate a document against its schema.
387
+ *
388
+ * @param {Document} doc - The document to validate
389
+ * @param {DocumentSchema} schema - The document schema
390
+ * @throws {Error} Throws if the document is invalid
391
+ */
392
+ export function validate_document(doc, schema) {
393
+ if (!is_id_valid(doc.document_id)) {
394
+ throw new Error(`Document ${doc.document_id} has an invalid id.`);
395
+ }
396
+ for (const node of Object.values(doc.nodes)) {
397
+ validate_node(node, schema, doc.nodes);
398
+ }
399
+ }
400
+
196
401
  /**
197
402
  * Gets a value from the document at the specified path.
198
403
  *
@@ -310,6 +515,31 @@ export function kind(schema, node) {
310
515
  return schema[node.type].kind;
311
516
  }
312
517
 
518
+ /**
519
+ * Checks whether an annotation node can be switched to another annotation type
520
+ * without losing data.
521
+ *
522
+ * @param {DocumentSchema} schema - The document schema
523
+ * @param {any} annotation_node - The annotation node to switch
524
+ * @param {string} target_annotation_type - The target annotation type
525
+ * @returns {boolean} True if the annotation can be switched safely
526
+ */
527
+ export function can_switch_annotation_type(schema, annotation_node, target_annotation_type) {
528
+ if (!annotation_node || annotation_node.type === target_annotation_type) return false;
529
+
530
+ const source_schema = schema[annotation_node.type];
531
+ const target_schema = schema[target_annotation_type];
532
+ if (source_schema?.kind !== 'annotation' || target_schema?.kind !== 'annotation') return false;
533
+
534
+ const source_has_properties = Object.keys(source_schema.properties ?? {}).length > 0;
535
+ const target_has_properties = Object.keys(target_schema.properties ?? {}).length > 0;
536
+ if (source_has_properties || target_has_properties) return false;
537
+
538
+ // eslint-disable-next-line no-unused-vars
539
+ const { id, type, ...extra_properties } = annotation_node;
540
+ return Object.keys(extra_properties).length === 0;
541
+ }
542
+
313
543
  /**
314
544
  * Inspects a path to get metadata about the value at that location.
315
545
  *
@@ -481,6 +711,42 @@ export function count_references_excluding_deleted(schema, doc, target_node_id,
481
711
  return count;
482
712
  }
483
713
 
714
+ /**
715
+ * Gets ids of nodes that reference any of the target nodes.
716
+ *
717
+ * @param {DocumentSchema} schema - The document schema
718
+ * @param {Document} doc - The document containing nodes
719
+ * @param {Iterable<NodeId>} target_node_ids - Node IDs to find referrers for
720
+ * @returns {NodeId[]} IDs of nodes with references to any target node
721
+ */
722
+ export function get_referencing_node_ids(schema, doc, target_node_ids) {
723
+ const target_ids = new Set(target_node_ids);
724
+ const referencing_node_ids = new Set();
725
+ if (target_ids.size === 0) return [];
726
+
727
+ for (const node of Object.values(doc.nodes)) {
728
+ for (const [property, value] of Object.entries(node)) {
729
+ if (property === 'id' || property === 'type') continue;
730
+
731
+ const prop_type = property_type(schema, node.type, property);
732
+
733
+ if (prop_type === 'node_array' && Array.isArray(value)) {
734
+ if (value.some((id) => target_ids.has(id))) {
735
+ referencing_node_ids.add(node.id);
736
+ }
737
+ } else if (prop_type === 'node' && target_ids.has(value)) {
738
+ referencing_node_ids.add(node.id);
739
+ } else if (prop_type === 'annotated_text' && value?.annotations) {
740
+ if (value.annotations.some((annotation) => target_ids.has(annotation.node_id))) {
741
+ referencing_node_ids.add(node.id);
742
+ }
743
+ }
744
+ }
745
+ }
746
+
747
+ return [...referencing_node_ids];
748
+ }
749
+
484
750
  /**
485
751
  * Validates a selection against the current document state.
486
752
  * Works with any object that implements get() and inspect() (Session or Transaction).
@@ -535,7 +801,7 @@ export function validate_selection(selection, session_or_transaction) {
535
801
  }
536
802
  } else if (selection_type === 'property') {
537
803
  if (!session_or_transaction.inspect(selection.path)) {
538
- throw new Error(`Property selection path not found: ${selection.path.join('.')}`);
804
+ throw new Error(`Property selection path not found: ${serialize_path(selection.path)}`);
539
805
  }
540
806
  }
541
807
  }
package/src/lib/index.js CHANGED
@@ -18,8 +18,12 @@ export {
18
18
  define_document_schema,
19
19
  is_primitive_type,
20
20
  get_default_node_type,
21
+ fill_node_defaults,
22
+ fill_document_defaults,
21
23
  validate_document_schema,
22
- validate_node
24
+ validate_document,
25
+ validate_node,
26
+ get_referencing_node_ids
23
27
  } from './doc_utils.js';
24
28
 
25
29
  // Command system