svedit 0.12.0 → 0.14.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 (88) hide show
  1. package/README.md +35 -22
  2. package/dist/Command.svelte.d.ts +38 -30
  3. package/dist/Command.svelte.d.ts.map +1 -1
  4. package/dist/Command.svelte.js +271 -326
  5. package/dist/CustomProperty.svelte +3 -5
  6. package/dist/CustomProperty.svelte.d.ts +3 -6
  7. package/dist/CustomProperty.svelte.d.ts.map +1 -1
  8. package/dist/KeyMapper.svelte.d.ts +19 -6
  9. package/dist/KeyMapper.svelte.d.ts.map +1 -1
  10. package/dist/KeyMapper.svelte.js +95 -112
  11. package/dist/Node.svelte +16 -13
  12. package/dist/Node.svelte.d.ts +3 -5
  13. package/dist/Node.svelte.d.ts.map +1 -1
  14. package/dist/NodeArrayProperty.svelte +39 -22
  15. package/dist/NodeArrayProperty.svelte.d.ts +3 -5
  16. package/dist/NodeArrayProperty.svelte.d.ts.map +1 -1
  17. package/dist/NodeCaret.svelte +2 -2
  18. package/dist/NodeCaret.svelte.d.ts +6 -14
  19. package/dist/NodeCaret.svelte.d.ts.map +1 -1
  20. package/dist/NodeGap.svelte +12 -6
  21. package/dist/NodeGap.svelte.d.ts +7 -14
  22. package/dist/NodeGap.svelte.d.ts.map +1 -1
  23. package/dist/NodeGapMarkers.svelte +36 -14
  24. package/dist/NodeGapMarkers.svelte.d.ts +6 -11
  25. package/dist/NodeGapMarkers.svelte.d.ts.map +1 -1
  26. package/dist/NodeSelectionMarkers.svelte +21 -7
  27. package/dist/NodeSelectionMarkers.svelte.d.ts +2 -5
  28. package/dist/NodeSelectionMarkers.svelte.d.ts.map +1 -1
  29. package/dist/Session.svelte.d.ts +81 -129
  30. package/dist/Session.svelte.d.ts.map +1 -1
  31. package/dist/Session.svelte.js +542 -648
  32. package/dist/Svedit.svelte +146 -140
  33. package/dist/Svedit.svelte.d.ts +30 -10
  34. package/dist/Svedit.svelte.d.ts.map +1 -1
  35. package/dist/TextProperty.svelte +37 -23
  36. package/dist/TextProperty.svelte.d.ts +3 -5
  37. package/dist/TextProperty.svelte.d.ts.map +1 -1
  38. package/dist/Transaction.svelte.d.ts +64 -127
  39. package/dist/Transaction.svelte.d.ts.map +1 -1
  40. package/dist/Transaction.svelte.js +839 -1001
  41. package/dist/UnknownNode.svelte +4 -3
  42. package/dist/UnknownNode.svelte.d.ts +5 -9
  43. package/dist/UnknownNode.svelte.d.ts.map +1 -1
  44. package/dist/doc_utils.d.ts +67 -155
  45. package/dist/doc_utils.d.ts.map +1 -1
  46. package/dist/doc_utils.js +619 -863
  47. package/dist/index.d.ts +20 -17
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +1 -17
  50. package/dist/node_visibility.svelte.d.ts +110 -9
  51. package/dist/node_visibility.svelte.d.ts.map +1 -1
  52. package/dist/node_visibility.svelte.js +473 -544
  53. package/dist/paste_utils.d.ts +11 -44
  54. package/dist/paste_utils.d.ts.map +1 -1
  55. package/dist/paste_utils.js +111 -152
  56. package/dist/styles/svedit-colors.css +8 -5
  57. package/dist/transforms.svelte.d.ts +9 -7
  58. package/dist/transforms.svelte.d.ts.map +1 -1
  59. package/dist/transforms.svelte.js +125 -150
  60. package/dist/types.d.ts +294 -309
  61. package/dist/types.d.ts.map +1 -0
  62. package/dist/types.js +1 -0
  63. package/dist/utils.d.ts +40 -119
  64. package/dist/utils.d.ts.map +1 -1
  65. package/dist/utils.js +346 -473
  66. package/package.json +70 -70
  67. package/src/lib/{Command.svelte.js → Command.svelte.ts} +29 -17
  68. package/src/lib/CustomProperty.svelte +3 -5
  69. package/src/lib/{KeyMapper.svelte.js → KeyMapper.svelte.ts} +28 -11
  70. package/src/lib/Node.svelte +16 -13
  71. package/src/lib/NodeArrayProperty.svelte +39 -22
  72. package/src/lib/NodeCaret.svelte +2 -2
  73. package/src/lib/NodeGap.svelte +12 -6
  74. package/src/lib/NodeGapMarkers.svelte +36 -14
  75. package/src/lib/NodeSelectionMarkers.svelte +21 -7
  76. package/src/lib/{Session.svelte.js → Session.svelte.ts} +130 -127
  77. package/src/lib/Svedit.svelte +146 -140
  78. package/src/lib/TextProperty.svelte +37 -23
  79. package/src/lib/{Transaction.svelte.js → Transaction.svelte.ts} +139 -143
  80. package/src/lib/UnknownNode.svelte +4 -3
  81. package/src/lib/{doc_utils.js → doc_utils.ts} +247 -206
  82. package/src/lib/{index.js → index.ts} +48 -0
  83. package/src/lib/{node_visibility.svelte.js → node_visibility.svelte.ts} +68 -92
  84. package/src/lib/{paste_utils.js → paste_utils.ts} +47 -58
  85. package/src/lib/styles/svedit-colors.css +8 -5
  86. package/src/lib/{transforms.svelte.js → transforms.svelte.ts} +39 -21
  87. package/src/lib/{types.d.ts → types.ts} +161 -66
  88. package/src/lib/{utils.js → utils.ts} +110 -129
package/dist/doc_utils.js CHANGED
@@ -3,92 +3,67 @@
3
3
  *
4
4
  * These functions operate on the core document state (schema, doc, selection, config)
5
5
  * without any history management or transaction tracking.
6
- *
7
- * @import { NodeId, DocumentPath, PrimitiveType, NodeProperty, NodeArrayProperty, NodeSchema, DocumentSchema, Selection, Attachment, Mark, Annotation, Document, TextProperty, AnnotatedText, ValidateDocumentSchema } from './types'
8
6
  */
9
-
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
- are_ranges_exclusive
17
- } from './utils.js';
18
-
7
+ import { assert_path_string_segment, is_path_string_segment_valid, get_selection_range, get_char_length, serialize_path, are_ranges_exclusive } from './utils.js';
19
8
  /**
20
9
  * Identity function — keeps schema at runtime & makes IDE infer types.
21
10
  * Similar to your define_schema pattern but for document schemas.
22
- *
23
- * @template {Record<string, NodeSchema>} S
24
- * @param {S & ValidateDocumentSchema<S>} schema - The document schema to validate
25
- * @returns {S} The same schema, but with type information preserved
26
11
  */
27
12
  export function define_document_schema(schema) {
28
- return schema;
13
+ return schema;
29
14
  }
30
-
31
15
  /**
32
16
  * Check if a string represents a valid primitive type.
33
- * @param {string} type - The type string to check
34
- * @returns {type is PrimitiveType} True if it's a valid primitive type
35
17
  */
36
18
  export function is_primitive_type(type) {
37
- return [
38
- 'string',
39
- 'number',
40
- 'boolean',
41
- 'integer',
42
- 'datetime',
43
- 'text',
44
- 'string_array',
45
- 'number_array',
46
- 'boolean_array',
47
- 'integer_array'
48
- ].includes(type);
19
+ return [
20
+ 'string',
21
+ 'number',
22
+ 'boolean',
23
+ 'integer',
24
+ 'datetime',
25
+ 'text',
26
+ 'string_array',
27
+ 'number_array',
28
+ 'boolean_array',
29
+ 'integer_array'
30
+ ].includes(type);
49
31
  }
50
-
51
32
  /**
52
33
  * Get the default node type for a property that references nodes.
53
- * @param {NodeProperty | NodeArrayProperty} property_definition - The property definition
54
- * @returns {string | null} The default node type, or null if none specified
34
+ * Returns null if none specified.
55
35
  */
56
36
  export function get_default_node_type(property_definition) {
57
- if (!property_definition || !property_definition.node_types) {
58
- return null;
59
- }
60
-
61
- return (
62
- property_definition.default_node_type ||
63
- (property_definition.node_types.length === 1 ? property_definition.node_types[0] : null)
64
- );
37
+ if (!property_definition || !property_definition.node_types) {
38
+ return null;
39
+ }
40
+ return (property_definition.default_node_type ||
41
+ (property_definition.node_types.length === 1 ? property_definition.node_types[0] : null));
65
42
  }
66
-
67
- /**
68
- * @param {import('./types').PropertyDefinition} property_definition
69
- * @returns {any}
70
- */
43
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
71
44
  export function get_property_default(property_definition) {
72
- if ('default' in property_definition) return structuredClone(property_definition.default);
73
-
74
- if (property_definition.type === 'string') return '';
75
- if (property_definition.type === 'integer') return 0;
76
- if (property_definition.type === 'number') return 0;
77
- if (property_definition.type === 'boolean') return false;
78
- if (property_definition.type === 'text') return { content: '', marks: [], annotations: [] };
79
- if (property_definition.type === 'node_array') return { nodes: [], marks: [], annotations: [] };
80
- if (
81
- property_definition.type === 'string_array' ||
82
- property_definition.type === 'number_array' ||
83
- property_definition.type === 'boolean_array' ||
84
- property_definition.type === 'integer_array'
85
- ) {
86
- return [];
87
- }
88
-
89
- return undefined;
45
+ if ('default' in property_definition)
46
+ return structuredClone(property_definition.default);
47
+ if (property_definition.type === 'string')
48
+ return '';
49
+ if (property_definition.type === 'integer')
50
+ return 0;
51
+ if (property_definition.type === 'number')
52
+ return 0;
53
+ if (property_definition.type === 'boolean')
54
+ return false;
55
+ if (property_definition.type === 'text')
56
+ return { content: '', marks: [], annotations: [] };
57
+ if (property_definition.type === 'node_array')
58
+ return { nodes: [], marks: [], annotations: [] };
59
+ if (property_definition.type === 'string_array' ||
60
+ property_definition.type === 'number_array' ||
61
+ property_definition.type === 'boolean_array' ||
62
+ property_definition.type === 'integer_array') {
63
+ return [];
64
+ }
65
+ return undefined;
90
66
  }
91
-
92
67
  /**
93
68
  * Fill omitted properties with schema defaults and Svedit's built-in type defaults.
94
69
  *
@@ -97,26 +72,22 @@ export function get_property_default(property_definition) {
97
72
  * schema changes cannot be represented by defaults, such as property renames or
98
73
  * data transformations.
99
74
  *
100
- * @param {any} node - The node to fill defaults for
101
- * @param {DocumentSchema} schema - The document schema
102
- * @returns {any} A shallow copy of the node with cloned default values filled in
75
+ * @returns A shallow copy of the node with cloned default values filled in
103
76
  */
104
77
  export function fill_node_defaults(node, schema) {
105
- const node_schema = schema[node.type];
106
- if (!node_schema) return { ...node };
107
-
108
- const node_with_defaults = { ...node };
109
-
110
- for (const [property_name, property_definition] of Object.entries(node_schema.properties)) {
111
- if (node_with_defaults[property_name] === undefined) {
112
- const property_default = get_property_default(property_definition);
113
- if (property_default !== undefined) node_with_defaults[property_name] = property_default;
114
- }
115
- }
116
-
117
- return node_with_defaults;
78
+ const node_schema = schema[node.type];
79
+ if (!node_schema)
80
+ return { ...node };
81
+ const node_with_defaults = { ...node };
82
+ for (const [property_name, property_definition] of Object.entries(node_schema.properties)) {
83
+ if (node_with_defaults[property_name] === undefined) {
84
+ const property_default = get_property_default(property_definition);
85
+ if (property_default !== undefined)
86
+ node_with_defaults[property_name] = property_default;
87
+ }
88
+ }
89
+ return node_with_defaults;
118
90
  }
119
-
120
91
  /**
121
92
  * Fill omitted properties with schema defaults and Svedit's built-in type defaults across a document.
122
93
  *
@@ -124,585 +95,456 @@ export function fill_node_defaults(node, schema) {
124
95
  * by itself. Call this as one step in an explicit document migration when it is
125
96
  * appropriate.
126
97
  *
127
- * @param {Document} doc - The document to fill defaults for
128
- * @param {DocumentSchema} schema - The document schema
129
- * @returns {Document} A document copy with cloned default values filled in
98
+ * @returns A document copy with cloned default values filled in
130
99
  */
131
100
  export function fill_document_defaults(doc, schema) {
132
- /** @type {Record<string, any>} */
133
- const nodes = {};
134
-
135
- for (const [node_id, node] of Object.entries(doc.nodes)) {
136
- nodes[node_id] = fill_node_defaults(node, schema);
137
- }
138
-
139
- return {
140
- ...doc,
141
- nodes
142
- };
101
+ const nodes = {};
102
+ for (const [node_id, node] of Object.entries(doc.nodes)) {
103
+ nodes[node_id] = fill_node_defaults(node, schema);
104
+ }
105
+ return {
106
+ ...doc,
107
+ nodes
108
+ };
143
109
  }
144
-
145
110
  /**
146
111
  * Validate a document schema to ensure all referenced node types exist.
147
- * @param {DocumentSchema} document_schema - The document schema to validate
148
112
  * @throws {Error} Throws if the document schema is invalid
149
113
  */
150
114
  export function validate_document_schema(document_schema) {
151
- // Check that all referenced node types exist
152
- for (const [node_type, node_schema] of Object.entries(document_schema)) {
153
- for (const [prop_name, prop_def] of Object.entries(node_schema.properties)) {
154
- assert_path_string_segment(prop_name, `Property name "${prop_name}"`);
155
- if (prop_def.type === 'node' || prop_def.type === 'node_array') {
156
- const missing_types = prop_def.node_types.filter(
157
- (ref_type) => !(ref_type in document_schema)
158
- );
159
- if (missing_types.length > 0) {
160
- throw new Error(
161
- `Node type "${node_type}" property "${prop_name}" references unknown node types: ${missing_types.join(', ')}. Available node types: ${Object.keys(document_schema).join(', ')}`
162
- );
163
- }
164
- }
165
- if (prop_def.type === 'text' || prop_def.type === 'node_array') {
166
- // mark_types must reference kind 'mark', annotation_types kind 'annotation'
167
- for (const [key, expected_kind] of [
168
- ['mark_types', 'mark'],
169
- ['annotation_types', 'annotation']
170
- ]) {
171
- const invalid_types = (prop_def[key] ?? []).filter(
172
- (ref_type) => document_schema[ref_type]?.kind !== expected_kind
173
- );
174
- if (invalid_types.length > 0) {
175
- throw new Error(
176
- `Node type "${node_type}" property "${prop_name}" ${key} must reference node types of kind '${expected_kind}', got: ${invalid_types.join(', ')}.`
177
- );
178
- }
179
- }
180
- }
181
- }
182
- }
115
+ // Check that all referenced node types exist
116
+ for (const [node_type, node_schema] of Object.entries(document_schema)) {
117
+ for (const [prop_name, prop_def] of Object.entries(node_schema.properties)) {
118
+ assert_path_string_segment(prop_name, `Property name "${prop_name}"`);
119
+ if (prop_def.type === 'string' && prop_def.values !== undefined) {
120
+ if (!Array.isArray(prop_def.values) ||
121
+ prop_def.values.some((value) => typeof value !== 'string')) {
122
+ throw new Error(`Node type "${node_type}" property "${prop_name}" values must be an array of strings.`);
123
+ }
124
+ if (prop_def.values.length === 0) {
125
+ throw new Error(`Node type "${node_type}" property "${prop_name}" values must not be empty.`);
126
+ }
127
+ if (new Set(prop_def.values).size !== prop_def.values.length) {
128
+ throw new Error(`Node type "${node_type}" property "${prop_name}" values must be unique.`);
129
+ }
130
+ if (prop_def.default !== undefined && !prop_def.values.includes(prop_def.default)) {
131
+ throw new Error(`Node type "${node_type}" property "${prop_name}" default must be one of its allowed values.`);
132
+ }
133
+ }
134
+ if (prop_def.type === 'integer') {
135
+ if (prop_def.default !== undefined && !Number.isInteger(prop_def.default)) {
136
+ throw new Error(`Node type "${node_type}" property "${prop_name}" default must be an integer.`);
137
+ }
138
+ if (prop_def.min !== undefined && !Number.isInteger(prop_def.min)) {
139
+ throw new Error(`Node type "${node_type}" property "${prop_name}" min must be an integer.`);
140
+ }
141
+ if (prop_def.max !== undefined && !Number.isInteger(prop_def.max)) {
142
+ throw new Error(`Node type "${node_type}" property "${prop_name}" max must be an integer.`);
143
+ }
144
+ if (prop_def.min !== undefined &&
145
+ prop_def.max !== undefined &&
146
+ prop_def.min > prop_def.max) {
147
+ throw new Error(`Node type "${node_type}" property "${prop_name}" min must not be greater than max.`);
148
+ }
149
+ if (prop_def.default !== undefined &&
150
+ ((prop_def.min !== undefined && prop_def.default < prop_def.min) ||
151
+ (prop_def.max !== undefined && prop_def.default > prop_def.max))) {
152
+ throw new Error(`Node type "${node_type}" property "${prop_name}" default must be within its min/max range.`);
153
+ }
154
+ }
155
+ if (prop_def.type === 'node' || prop_def.type === 'node_array') {
156
+ const missing_types = prop_def.node_types.filter((ref_type) => !(ref_type in document_schema));
157
+ if (missing_types.length > 0) {
158
+ throw new Error(`Node type "${node_type}" property "${prop_name}" references unknown node types: ${missing_types.join(', ')}. Available node types: ${Object.keys(document_schema).join(', ')}`);
159
+ }
160
+ }
161
+ if (prop_def.type === 'text' || prop_def.type === 'node_array') {
162
+ // mark_types must reference kind 'mark', annotation_types kind 'annotation'
163
+ for (const [key, expected_kind] of [
164
+ ['mark_types', 'mark'],
165
+ ['annotation_types', 'annotation']
166
+ ]) {
167
+ const invalid_types = (prop_def[key] ?? []).filter((ref_type) => document_schema[ref_type]?.kind !== expected_kind);
168
+ if (invalid_types.length > 0) {
169
+ throw new Error(`Node type "${node_type}" property "${prop_name}" ${key} must reference node types of kind '${expected_kind}', got: ${invalid_types.join(', ')}.`);
170
+ }
171
+ }
172
+ }
173
+ }
174
+ }
183
175
  }
184
-
185
176
  /**
186
177
  * Validate that no annotation node type has a registered component.
187
178
  *
188
179
  * Marks render in-place via components; annotations are data-only overlay
189
180
  * ranges that must be interpreted by the app (via CSS classes or props).
190
181
  *
191
- * @param {DocumentSchema} schema - The document schema
192
- * @param {any} config - The Svedit config
193
182
  * @throws {Error} Throws if a kind 'annotation' type has a registered component
194
183
  */
195
184
  export function validate_config_components(schema, config) {
196
- for (const [node_type, node_schema] of Object.entries(schema)) {
197
- if (node_schema.kind === 'annotation' && config?.node_components?.[node_type]) {
198
- throw new Error(
199
- `Annotation type "${node_type}" must not have a registered component. Annotations are data-only; use kind 'mark' for in-place rendered ranges.`
200
- );
201
- }
202
- }
185
+ for (const [node_type, node_schema] of Object.entries(schema)) {
186
+ if (node_schema.kind === 'annotation' && config?.node_components?.[node_type]) {
187
+ throw new Error(`Annotation type "${node_type}" must not have a registered component. Annotations are data-only; use kind 'mark' for in-place rendered ranges.`);
188
+ }
189
+ }
203
190
  }
204
-
205
191
  /**
206
192
  * Validate a primitive value against its schema type.
207
- * @param {PrimitiveType} type - The expected type
208
- * @param {any} value - The value to validate
209
- * @returns {boolean} True if the value matches the type
210
193
  */
194
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
211
195
  function validate_primitive_value(type, value) {
212
- switch (type) {
213
- case 'string':
214
- return typeof value === 'string';
215
- case 'number':
216
- return typeof value === 'number' && !isNaN(value);
217
- case 'boolean':
218
- return typeof value === 'boolean';
219
- case 'integer':
220
- return Number.isInteger(value);
221
- case 'datetime':
222
- return typeof value === 'string' && !isNaN(Date.parse(value));
223
- case 'text':
224
- return (
225
- typeof value === 'object' &&
226
- value !== null &&
227
- typeof value.content === 'string' &&
228
- Array.isArray(value.marks) &&
229
- Array.isArray(value.annotations)
230
- );
231
- case 'string_array':
232
- return Array.isArray(value) && value.every((v) => typeof v === 'string');
233
- case 'number_array':
234
- return Array.isArray(value) && value.every((v) => typeof v === 'number' && !isNaN(v));
235
- case 'boolean_array':
236
- return Array.isArray(value) && value.every((v) => typeof v === 'boolean');
237
- case 'integer_array':
238
- return Array.isArray(value) && value.every((v) => Number.isInteger(v));
239
- default:
240
- return false;
241
- }
196
+ switch (type) {
197
+ case 'string':
198
+ return typeof value === 'string';
199
+ case 'number':
200
+ return typeof value === 'number' && !isNaN(value);
201
+ case 'boolean':
202
+ return typeof value === 'boolean';
203
+ case 'integer':
204
+ return Number.isInteger(value);
205
+ case 'datetime':
206
+ return typeof value === 'string' && !isNaN(Date.parse(value));
207
+ case 'text':
208
+ return (typeof value === 'object' &&
209
+ value !== null &&
210
+ typeof value.content === 'string' &&
211
+ Array.isArray(value.marks) &&
212
+ Array.isArray(value.annotations));
213
+ case 'string_array':
214
+ return Array.isArray(value) && value.every((v) => typeof v === 'string');
215
+ case 'number_array':
216
+ return Array.isArray(value) && value.every((v) => typeof v === 'number' && !isNaN(v));
217
+ case 'boolean_array':
218
+ return Array.isArray(value) && value.every((v) => typeof v === 'boolean');
219
+ case 'integer_array':
220
+ return Array.isArray(value) && value.every((v) => Number.isInteger(v));
221
+ default:
222
+ return false;
223
+ }
242
224
  }
243
-
244
225
  /**
245
226
  * Validate ranges (marks or annotations) for bounds, references, and allowed types.
246
227
  *
247
- * @param {string} node_id - Owner node id, used for error messages
248
- * @param {string} prop_name - Owner property name, used for error messages
249
- * @param {'mark' | 'annotation'} label - Range label, used for error messages
250
- * @param {Array<Attachment>} ranges - Ranges to validate
251
- * @param {number} container_length - Length of the annotated text or node array
252
- * @param {Array<string> | null | undefined} allowed_types - Allowed node types
253
- * @param {Record<string, any>} all_nodes - All document nodes
254
- * @param {boolean} require_references - Whether referenced nodes must already exist
255
228
  * @throws {Error} Throws if ranges are invalid
256
229
  */
257
- function validate_range_array(
258
- node_id,
259
- prop_name,
260
- label,
261
- ranges,
262
- container_length,
263
- allowed_types,
264
- all_nodes,
265
- require_references
266
- ) {
267
- for (const [index, range] of ranges.entries()) {
268
- if (
269
- typeof range !== 'object' ||
270
- range === null ||
271
- !Number.isInteger(range.start_offset) ||
272
- !Number.isInteger(range.end_offset) ||
273
- !is_id_valid(range.node_id)
274
- ) {
275
- throw new Error(
276
- `Node ${node_id} property ${prop_name} has an invalid ${label} at index ${index}. Ranges must have integer start_offset/end_offset and a valid node_id.`
277
- );
278
- }
279
-
280
- if (range.start_offset < 0 || range.end_offset > container_length) {
281
- throw new Error(
282
- `Node ${node_id} property ${prop_name} ${label} ${range.node_id} is out of bounds: ${range.start_offset}-${range.end_offset}, container length is ${container_length}.`
283
- );
284
- }
285
-
286
- if (range.start_offset >= range.end_offset) {
287
- throw new Error(
288
- `Node ${node_id} property ${prop_name} ${label} ${range.node_id} must not be empty or reversed: ${range.start_offset}-${range.end_offset}.`
289
- );
290
- }
291
-
292
- const referenced_node = all_nodes[range.node_id];
293
- if (!referenced_node) {
294
- if (require_references) {
295
- throw new Error(
296
- `Node ${node_id} property ${prop_name} ${label} references missing node ${range.node_id}.`
297
- );
298
- }
299
- continue;
300
- }
301
-
302
- if (allowed_types?.length && !allowed_types.includes(referenced_node.type)) {
303
- throw new Error(
304
- `Node ${node_id} property ${prop_name} ${label} references node ${range.node_id} of type ${referenced_node.type}, but only types [${allowed_types.join(', ')}] are allowed.`
305
- );
306
- }
307
- }
230
+ function validate_range_array(node_id, prop_name, label, ranges, container_length, allowed_types, all_nodes, require_references) {
231
+ for (const [index, range] of ranges.entries()) {
232
+ if (typeof range !== 'object' ||
233
+ range === null ||
234
+ !Number.isInteger(range.start_offset) ||
235
+ !Number.isInteger(range.end_offset) ||
236
+ !is_id_valid(range.node_id)) {
237
+ throw new Error(`Node ${node_id} property ${prop_name} has an invalid ${label} at index ${index}. Ranges must have integer start_offset/end_offset and a valid node_id.`);
238
+ }
239
+ if (range.start_offset < 0 || range.end_offset > container_length) {
240
+ throw new Error(`Node ${node_id} property ${prop_name} ${label} ${range.node_id} is out of bounds: ${range.start_offset}-${range.end_offset}, container length is ${container_length}.`);
241
+ }
242
+ if (range.start_offset >= range.end_offset) {
243
+ throw new Error(`Node ${node_id} property ${prop_name} ${label} ${range.node_id} must not be empty or reversed: ${range.start_offset}-${range.end_offset}.`);
244
+ }
245
+ const referenced_node = all_nodes[range.node_id];
246
+ if (!referenced_node) {
247
+ if (require_references) {
248
+ throw new Error(`Node ${node_id} property ${prop_name} ${label} references missing node ${range.node_id}.`);
249
+ }
250
+ continue;
251
+ }
252
+ if (allowed_types?.length && !allowed_types.includes(referenced_node.type)) {
253
+ throw new Error(`Node ${node_id} property ${prop_name} ${label} references node ${range.node_id} of type ${referenced_node.type}, but only types [${allowed_types.join(', ')}] are allowed.`);
254
+ }
255
+ }
308
256
  }
309
-
310
257
  /**
311
258
  * Validate marks and annotations of an annotated container (text or node array).
312
259
  *
313
260
  * Marks must be mutually exclusive; annotations may overlap.
314
261
  *
315
- * @param {string} node_id - Owner node id, used for error messages
316
- * @param {string} prop_name - Owner property name, used for error messages
317
- * @param {{ marks: Array<Mark>, annotations: Array<Annotation> }} value - Annotated value to validate
318
- * @param {{ mark_types?: string[], annotation_types?: string[] }} prop_def - Property definition
319
- * @param {number} container_length - Length of the annotated text or node array
320
- * @param {Record<string, any>} all_nodes - All document nodes
321
- * @param {boolean} require_references - Whether referenced nodes must already exist
322
262
  * @throws {Error} Throws if marks or annotations are invalid
323
263
  */
324
- function validate_marks_and_annotations(
325
- node_id,
326
- prop_name,
327
- value,
328
- prop_def,
329
- container_length,
330
- all_nodes,
331
- require_references
332
- ) {
333
- validate_range_array(
334
- node_id,
335
- prop_name,
336
- 'mark',
337
- value.marks,
338
- container_length,
339
- prop_def.mark_types,
340
- all_nodes,
341
- require_references
342
- );
343
-
344
- if (!are_ranges_exclusive(value.marks, container_length)) {
345
- throw new Error(
346
- `Node ${node_id} property ${prop_name} has overlapping marks. Marks must be mutually exclusive.`
347
- );
348
- }
349
-
350
- validate_range_array(
351
- node_id,
352
- prop_name,
353
- 'annotation',
354
- value.annotations,
355
- container_length,
356
- prop_def.annotation_types,
357
- all_nodes,
358
- require_references
359
- );
264
+ function validate_marks_and_annotations(node_id, prop_name, value, prop_def, container_length, all_nodes, require_references) {
265
+ validate_range_array(node_id, prop_name, 'mark', value.marks, container_length, prop_def.mark_types, all_nodes, require_references);
266
+ if (!are_ranges_exclusive(value.marks, container_length)) {
267
+ throw new Error(`Node ${node_id} property ${prop_name} has overlapping marks. Marks must be mutually exclusive.`);
268
+ }
269
+ validate_range_array(node_id, prop_name, 'annotation', value.annotations, container_length, prop_def.annotation_types, all_nodes, require_references);
360
270
  }
361
-
362
271
  /**
363
272
  * Validate text property marks and annotations for bounds, references, allowed
364
273
  * types, and mark exclusivity.
365
274
  *
366
- * @param {string} node_id - Owner node id, used for error messages
367
- * @param {string} prop_name - Owner property name, used for error messages
368
- * @param {AnnotatedText} value - Annotated text value to validate
369
- * @param {TextProperty} prop_def - Text property definition
370
- * @param {Record<string, any>} all_nodes - All document nodes
371
- * @param {boolean} require_references - Whether referenced nodes must already exist
372
275
  * @throws {Error} Throws if marks or annotations are invalid
373
276
  */
374
- function validate_text_property(
375
- node_id,
376
- prop_name,
377
- value,
378
- prop_def,
379
- all_nodes,
380
- require_references
381
- ) {
382
- const char_length = get_char_length(value.content);
383
- validate_marks_and_annotations(
384
- node_id,
385
- prop_name,
386
- value,
387
- prop_def,
388
- char_length,
389
- all_nodes,
390
- require_references
391
- );
277
+ function validate_text_property(node_id, prop_name, value, prop_def, all_nodes, require_references) {
278
+ const char_length = get_char_length(value.content);
279
+ validate_marks_and_annotations(node_id, prop_name, value, prop_def, char_length, all_nodes, require_references);
392
280
  }
393
-
394
- /**
395
- * @param {string} id
396
- * @returns {boolean}
397
- */
398
281
  export function is_id_valid(id) {
399
- return typeof id === 'string' && id.length > 0 && is_path_string_segment_valid(id);
282
+ return typeof id === 'string' && id.length > 0 && is_path_string_segment_valid(id);
400
283
  }
401
-
402
284
  /**
403
285
  * Validate a node against its schema.
404
- * @param {any} node - The node to validate
405
- * @param {DocumentSchema} schema - The document schema
406
- * @param {Record<string, any>} all_nodes - All nodes in the document to check references
407
- * @param {{ require_references?: boolean }} [options] - Validation options
286
+ *
287
+ * @param node - The node to validate
288
+ * @param schema - The document schema
289
+ * @param all_nodes - All nodes in the document to check references
290
+ * @param options - Validation options
408
291
  * @throws {Error} Throws if the node is invalid
409
292
  */
410
293
  export function validate_node(node, schema, all_nodes = {}, options = {}) {
411
- const require_references = options.require_references ?? true;
412
- if (!is_id_valid(node.id)) {
413
- throw new Error(`Node ${node.id} has an invalid id.`);
414
- }
415
-
416
- if (!node.type || !schema[node.type]) {
417
- throw new Error(`Node ${node.id} has an invalid type: ${node.type}`);
418
- }
419
-
420
- const node_schema = schema[node.type];
421
-
422
- for (const [prop_name, prop_def] of Object.entries(node_schema.properties)) {
423
- const value = node[prop_name];
424
-
425
- // Check primitive types
426
- if (is_primitive_type(prop_def.type)) {
427
- if (!validate_primitive_value(prop_def.type, value)) {
428
- throw new Error(
429
- `Node ${node.id} has an invalid property: ${prop_name} must be of type ${prop_def.type}.`
430
- );
431
- }
432
- }
433
- if (prop_def.type === 'text') {
434
- validate_text_property(
435
- node.id,
436
- prop_name,
437
- value,
438
- /** @type {TextProperty} */ (prop_def),
439
- all_nodes,
440
- require_references
441
- );
442
- }
443
- // Check node references
444
- if (prop_def.type === 'node') {
445
- if (!is_id_valid(value)) {
446
- throw new Error(
447
- `Node ${node.id} has an invalid property: ${prop_name} must be a valid node id.`
448
- );
449
- }
450
- // Check if referenced node exists and is of allowed type
451
- const referenced_node = all_nodes[value];
452
- if (!referenced_node) {
453
- if (require_references) {
454
- throw new Error(
455
- `Node ${node.id} property ${prop_name} references missing node ${value}.`
456
- );
457
- }
458
- continue;
459
- }
460
- if (!prop_def.node_types.includes(referenced_node.type)) {
461
- throw new Error(
462
- `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.`
463
- );
464
- }
465
- }
466
- // Check node arrays
467
- else if (prop_def.type === 'node_array') {
468
- if (
469
- !value ||
470
- typeof value !== 'object' ||
471
- !Array.isArray(value.nodes) ||
472
- !Array.isArray(value.marks) ||
473
- !Array.isArray(value.annotations)
474
- ) {
475
- throw new Error(
476
- `Node ${node.id} has an invalid property: ${prop_name} must be an object with nodes, marks and annotations.`
477
- );
478
- }
479
-
480
- const node_array_nodes = value.nodes;
481
-
482
- if (!node_array_nodes.every((id) => typeof id === 'string' && is_id_valid(id))) {
483
- throw new Error(
484
- `Node ${node.id} has an invalid property: ${prop_name} must contain valid node ids.`
485
- );
486
- }
487
-
488
- // Check if all referenced nodes exist and are of allowed types
489
- for (const ref_id of node_array_nodes) {
490
- const referenced_node = all_nodes[ref_id];
491
- if (!referenced_node) {
492
- if (require_references) {
493
- throw new Error(
494
- `Node ${node.id} property ${prop_name} references missing node ${ref_id}.`
495
- );
496
- }
497
- continue;
498
- }
499
- if (!prop_def.node_types.includes(referenced_node.type)) {
500
- throw new Error(
501
- `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.`
502
- );
503
- }
504
- }
505
- validate_marks_and_annotations(
506
- node.id,
507
- prop_name,
508
- value,
509
- prop_def,
510
- node_array_nodes.length,
511
- all_nodes,
512
- require_references
513
- );
514
- }
515
- }
294
+ const require_references = options.require_references ?? true;
295
+ if (!is_id_valid(node.id)) {
296
+ throw new Error(`Node ${node.id} has an invalid id.`);
297
+ }
298
+ if (!node.type || !schema[node.type]) {
299
+ throw new Error(`Node ${node.id} has an invalid type: ${node.type}`);
300
+ }
301
+ const node_schema = schema[node.type];
302
+ for (const [prop_name, prop_def] of Object.entries(node_schema.properties)) {
303
+ const value = node[prop_name];
304
+ // Check primitive types
305
+ if (is_primitive_type(prop_def.type)) {
306
+ if (!validate_primitive_value(prop_def.type, value)) {
307
+ throw new Error(`Node ${node.id} has an invalid property: ${prop_name} must be of type ${prop_def.type}.`);
308
+ }
309
+ if (prop_def.type === 'string' && prop_def.values && !prop_def.values.includes(value)) {
310
+ throw new Error(`Node ${node.id} has an invalid property: ${prop_name} must be one of [${prop_def.values.join(', ')}].`);
311
+ }
312
+ if (prop_def.type === 'integer' &&
313
+ ((prop_def.min !== undefined && value < prop_def.min) ||
314
+ (prop_def.max !== undefined && value > prop_def.max))) {
315
+ throw new Error(`Node ${node.id} has an invalid property: ${prop_name} must be between ${prop_def.min ?? '-Infinity'} and ${prop_def.max ?? 'Infinity'}.`);
316
+ }
317
+ }
318
+ if (prop_def.type === 'text') {
319
+ validate_text_property(node.id, prop_name, value, prop_def, all_nodes, require_references);
320
+ }
321
+ // Check node references
322
+ if (prop_def.type === 'node') {
323
+ if (!is_id_valid(value)) {
324
+ throw new Error(`Node ${node.id} has an invalid property: ${prop_name} must be a valid node id.`);
325
+ }
326
+ // Check if referenced node exists and is of allowed type
327
+ const referenced_node = all_nodes[value];
328
+ if (!referenced_node) {
329
+ if (require_references) {
330
+ throw new Error(`Node ${node.id} property ${prop_name} references missing node ${value}.`);
331
+ }
332
+ continue;
333
+ }
334
+ if (!prop_def.node_types.includes(referenced_node.type)) {
335
+ throw new Error(`Node ${node.id} property ${prop_name} references node ${value} of type ${referenced_node.type}, but only types [${prop_def.node_types.join(', ')}] are allowed.`);
336
+ }
337
+ }
338
+ // Check node arrays
339
+ else if (prop_def.type === 'node_array') {
340
+ if (!value ||
341
+ typeof value !== 'object' ||
342
+ !Array.isArray(value.nodes) ||
343
+ !Array.isArray(value.marks) ||
344
+ !Array.isArray(value.annotations)) {
345
+ throw new Error(`Node ${node.id} has an invalid property: ${prop_name} must be an object with nodes, marks and annotations.`);
346
+ }
347
+ const node_array_nodes = value.nodes;
348
+ if (!node_array_nodes.every((id) => typeof id === 'string' && is_id_valid(id))) {
349
+ throw new Error(`Node ${node.id} has an invalid property: ${prop_name} must contain valid node ids.`);
350
+ }
351
+ // Check if all referenced nodes exist and are of allowed types
352
+ for (const ref_id of node_array_nodes) {
353
+ const referenced_node = all_nodes[ref_id];
354
+ if (!referenced_node) {
355
+ if (require_references) {
356
+ throw new Error(`Node ${node.id} property ${prop_name} references missing node ${ref_id}.`);
357
+ }
358
+ continue;
359
+ }
360
+ if (!prop_def.node_types.includes(referenced_node.type)) {
361
+ throw new Error(`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.`);
362
+ }
363
+ }
364
+ validate_marks_and_annotations(node.id, prop_name, value, prop_def, node_array_nodes.length, all_nodes, require_references);
365
+ }
366
+ }
516
367
  }
517
-
518
368
  /**
519
369
  * Validate a document against its schema.
520
370
  *
521
- * @param {Document} doc - The document to validate
522
- * @param {DocumentSchema} schema - The document schema
523
371
  * @throws {Error} Throws if the document is invalid
524
372
  */
525
373
  export function validate_document(doc, schema) {
526
- if (!is_id_valid(doc.document_id)) {
527
- throw new Error(`Document ${doc.document_id} has an invalid id.`);
528
- }
529
- for (const [node_id, node] of Object.entries(doc.nodes)) {
530
- if (!is_id_valid(node_id)) {
531
- throw new Error(`Document node map contains an invalid id: ${node_id}.`);
532
- }
533
- if (node.id !== node_id) {
534
- throw new Error(`Document node map key ${node_id} does not match node id ${node.id}.`);
535
- }
536
- validate_node(node, schema, doc.nodes);
537
- }
374
+ if (!is_id_valid(doc.document_id)) {
375
+ throw new Error(`Document ${doc.document_id} has an invalid id.`);
376
+ }
377
+ for (const [node_id, node] of Object.entries(doc.nodes)) {
378
+ if (!is_id_valid(node_id)) {
379
+ throw new Error(`Document node map contains an invalid id: ${node_id}.`);
380
+ }
381
+ if (node.id !== node_id) {
382
+ throw new Error(`Document node map key ${node_id} does not match node id ${node.id}.`);
383
+ }
384
+ validate_node(node, schema, doc.nodes);
385
+ }
538
386
  }
539
-
540
387
  /**
541
388
  * Gets a value from the document at the specified path.
542
389
  *
543
- * @param {DocumentSchema} schema - The document schema
544
- * @param {Document} doc - The document containing nodes
545
- * @param {DocumentPath|string} path - Array path to the value, or a string node ID
546
- * @returns {any} The value at the specified path
390
+ * @param schema - The document schema
391
+ * @param doc - The document containing nodes
392
+ * @param path - Array path to the value, or a string node ID
393
+ * @returns The value at the specified path
547
394
  */
395
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
548
396
  export function get(schema, doc, path) {
549
- if (typeof path === 'string') {
550
- path = [path];
551
- }
552
- if (!(Array.isArray(path) && path.length >= 1)) {
553
- throw new Error(`Invalid path provided ${JSON.stringify(path)}`);
554
- }
555
-
556
- let val = doc.nodes[path[0]];
557
- let val_type = 'node';
558
-
559
- for (let i = 1; i < path.length; i++) {
560
- const path_segment = path[i];
561
- const path_segment_str = String(path_segment);
562
- if (val_type === 'node') {
563
- if (property_type(schema, val.type, path_segment_str) === 'node_array') {
564
- val = val[path_segment];
565
- val_type = 'node_array';
566
- } else if (property_type(schema, val.type, path_segment_str) === 'text') {
567
- val = val[path_segment];
568
- val_type = 'text';
569
- } else if (property_type(schema, val.type, path_segment_str) === 'node') {
570
- val = doc.nodes[val[path_segment]];
571
- val_type = 'node';
572
- } else if (
573
- ['string_array', 'integer_array'].includes(
574
- property_type(schema, val.type, path_segment_str)
575
- )
576
- ) {
577
- val = val[path_segment];
578
- val_type = 'value_array';
579
- } else {
580
- val = val[path_segment];
581
- val_type = 'value';
582
- }
583
- } else if (val_type === 'node_array') {
584
- if (path_segment === 'nodes') {
585
- val = val.nodes;
586
- val_type = 'node_id_array';
587
- } else if (path_segment === 'marks') {
588
- val = val.marks;
589
- val_type = 'range_array';
590
- } else if (path_segment === 'annotations') {
591
- val = val.annotations;
592
- val_type = 'range_array';
593
- } else if (typeof path_segment === 'number' || /^\d+$/.test(String(path_segment))) {
594
- val = doc.nodes[val.nodes[path_segment]];
595
- val_type = 'node';
596
- } else {
597
- throw new Error(
598
- `Invalid path segment "${path_segment}" for node_array. Use "nodes", "marks" or "annotations".`
599
- );
600
- }
601
- } else if (val_type === 'node_id_array') {
602
- val = doc.nodes[val[path_segment]];
603
- val_type = 'node';
604
- } else if (val_type === 'value_array') {
605
- val = val[path_segment];
606
- val_type = 'value';
607
- } else if (val_type === 'text') {
608
- if (path_segment === 'content') {
609
- val = val.content;
610
- val_type = 'value';
611
- } else if (path_segment === 'marks') {
612
- val = val.marks;
613
- val_type = 'range_array';
614
- } else if (path_segment === 'annotations') {
615
- val = val.annotations;
616
- val_type = 'range_array';
617
- } else {
618
- throw new Error(
619
- `Invalid path segment "${path_segment}" for text. Use "content", "marks" or "annotations".`
620
- );
621
- }
622
- } else if (val_type === 'range_array') {
623
- val = val[path_segment];
624
- val_type = 'range';
625
- } else if (val_type === 'range') {
626
- if (path_segment === 'node_id') {
627
- val = doc.nodes[val.node_id];
628
- val_type = 'node';
629
- } else if (path_segment === 'start_offset') {
630
- val = val.start_offset;
631
- val_type = 'value';
632
- } else if (path_segment === 'end_offset') {
633
- val = val.end_offset;
634
- val_type = 'value';
635
- } else {
636
- throw new Error(
637
- `Invalid path segment "${path_segment}" for range. Use "start_offset", "end_offset", or "node_id".`
638
- );
639
- }
640
- }
641
- }
642
- return val;
397
+ if (typeof path === 'string') {
398
+ path = [path];
399
+ }
400
+ if (!(Array.isArray(path) && path.length >= 1)) {
401
+ throw new Error(`Invalid path provided ${JSON.stringify(path)}`);
402
+ }
403
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
404
+ let val = doc.nodes[path[0]];
405
+ let val_type = 'node';
406
+ for (let i = 1; i < path.length; i++) {
407
+ const path_segment = path[i];
408
+ const path_segment_str = String(path_segment);
409
+ if (val_type === 'node') {
410
+ if (property_type(schema, val.type, path_segment_str) === 'node_array') {
411
+ val = val[path_segment];
412
+ val_type = 'node_array';
413
+ }
414
+ else if (property_type(schema, val.type, path_segment_str) === 'text') {
415
+ val = val[path_segment];
416
+ val_type = 'text';
417
+ }
418
+ else if (property_type(schema, val.type, path_segment_str) === 'node') {
419
+ val = doc.nodes[val[path_segment]];
420
+ val_type = 'node';
421
+ }
422
+ else if (['string_array', 'integer_array'].includes(property_type(schema, val.type, path_segment_str))) {
423
+ val = val[path_segment];
424
+ val_type = 'value_array';
425
+ }
426
+ else {
427
+ val = val[path_segment];
428
+ val_type = 'value';
429
+ }
430
+ }
431
+ else if (val_type === 'node_array') {
432
+ if (path_segment === 'nodes') {
433
+ val = val.nodes;
434
+ val_type = 'node_id_array';
435
+ }
436
+ else if (path_segment === 'marks') {
437
+ val = val.marks;
438
+ val_type = 'range_array';
439
+ }
440
+ else if (path_segment === 'annotations') {
441
+ val = val.annotations;
442
+ val_type = 'range_array';
443
+ }
444
+ else if (typeof path_segment === 'number' || /^\d+$/.test(String(path_segment))) {
445
+ val = doc.nodes[val.nodes[path_segment]];
446
+ val_type = 'node';
447
+ }
448
+ else {
449
+ throw new Error(`Invalid path segment "${path_segment}" for node_array. Use "nodes", "marks" or "annotations".`);
450
+ }
451
+ }
452
+ else if (val_type === 'node_id_array') {
453
+ val = doc.nodes[val[path_segment]];
454
+ val_type = 'node';
455
+ }
456
+ else if (val_type === 'value_array') {
457
+ val = val[path_segment];
458
+ val_type = 'value';
459
+ }
460
+ else if (val_type === 'text') {
461
+ if (path_segment === 'content') {
462
+ val = val.content;
463
+ val_type = 'value';
464
+ }
465
+ else if (path_segment === 'marks') {
466
+ val = val.marks;
467
+ val_type = 'range_array';
468
+ }
469
+ else if (path_segment === 'annotations') {
470
+ val = val.annotations;
471
+ val_type = 'range_array';
472
+ }
473
+ else {
474
+ throw new Error(`Invalid path segment "${path_segment}" for text. Use "content", "marks" or "annotations".`);
475
+ }
476
+ }
477
+ else if (val_type === 'range_array') {
478
+ val = val[path_segment];
479
+ val_type = 'range';
480
+ }
481
+ else if (val_type === 'range') {
482
+ if (path_segment === 'node_id') {
483
+ val = doc.nodes[val.node_id];
484
+ val_type = 'node';
485
+ }
486
+ else if (path_segment === 'start_offset') {
487
+ val = val.start_offset;
488
+ val_type = 'value';
489
+ }
490
+ else if (path_segment === 'end_offset') {
491
+ val = val.end_offset;
492
+ val_type = 'value';
493
+ }
494
+ else {
495
+ throw new Error(`Invalid path segment "${path_segment}" for range. Use "start_offset", "end_offset", or "node_id".`);
496
+ }
497
+ }
498
+ }
499
+ return val;
643
500
  }
644
-
645
501
  /**
646
502
  * Gets the type of a property from the schema.
647
- *
648
- * @param {object} schema - The document schema
649
- * @param {string} type - The node type
650
- * @param {string} property - The property name
651
- * @returns {string} The property type
652
503
  */
653
504
  export function property_type(schema, type, property) {
654
- if (typeof type !== 'string') throw new Error(`Invalid type ${type} provided`);
655
- if (typeof property !== 'string') throw new Error(`Invalid property ${property} provided`);
656
-
657
- if (property === 'type') return 'string';
658
- if (property === 'id') return 'string';
659
-
660
- if (!schema[type]) throw new Error(`Type ${type} not found in schema`);
661
- if (!schema[type].properties[property])
662
- throw new Error(`Property ${property} not found in type ${type}`);
663
-
664
- return schema[type].properties[property].type;
665
- }
666
-
667
- /**
668
- * Determines the kind of a node ('block', 'text', 'mark', or 'annotation').
669
- *
670
- * @param {object} schema - The document schema
671
- * @param {any} node - The node to check
672
- * @returns {'block'|'text'|'mark'|'annotation'} The node kind
505
+ if (typeof type !== 'string')
506
+ throw new Error(`Invalid type ${type} provided`);
507
+ if (typeof property !== 'string')
508
+ throw new Error(`Invalid property ${property} provided`);
509
+ if (property === 'type')
510
+ return 'string';
511
+ if (property === 'id')
512
+ return 'string';
513
+ if (!schema[type])
514
+ throw new Error(`Type ${type} not found in schema`);
515
+ if (!schema[type].properties[property])
516
+ throw new Error(`Property ${property} not found in type ${type}`);
517
+ return schema[type].properties[property].type;
518
+ }
519
+ /**
520
+ * Determines the kind of a node ('document', 'block', 'text', 'mark', or 'annotation').
673
521
  */
674
522
  export function kind(schema, node) {
675
- return schema[node.type].kind;
523
+ return schema[node.type].kind;
676
524
  }
677
-
678
525
  /**
679
526
  * Inspects a path to get metadata about the value at that location.
680
- *
681
- * @param {DocumentSchema} schema - The document schema
682
- * @param {Document} doc - The document containing nodes
683
- * @param {DocumentPath} path - The path to inspect
684
- * @returns {{kind: 'property'|'node', [key: string]: any}} Metadata about the path
685
527
  */
686
528
  export function inspect(schema, doc, path) {
687
- const parent = path.length > 1 ? get(schema, doc, path.slice(0, -1)) : undefined;
688
- if (parent?.type) {
689
- const property_name = path.at(-1);
690
- return {
691
- kind: 'property',
692
- name: property_name,
693
- ...schema[parent.type].properties[property_name]
694
- };
695
- } else {
696
- const node = get(schema, doc, path);
697
- return {
698
- kind: 'node',
699
- id: node.id,
700
- type: node.type,
701
- properties: schema[node.type]
702
- };
703
- }
529
+ const parent = path.length > 1 ? get(schema, doc, path.slice(0, -1)) : undefined;
530
+ if (parent?.type) {
531
+ const property_name = path.at(-1);
532
+ return {
533
+ kind: 'property',
534
+ name: property_name,
535
+ ...schema[parent.type].properties[property_name]
536
+ };
537
+ }
538
+ else {
539
+ const node = get(schema, doc, path);
540
+ return {
541
+ kind: 'node',
542
+ id: node.id,
543
+ type: node.type,
544
+ properties: schema[node.type]
545
+ };
546
+ }
704
547
  }
705
-
706
548
  /**
707
549
  * Creates a mutable draft of a document: a new document object with a new
708
550
  * nodes map, sharing the (immutable) node objects with the original.
@@ -712,16 +554,14 @@ export function inspect(schema, doc, path) {
712
554
  * (fine-grained rendering relies on this) and the original document is
713
555
  * never touched.
714
556
  *
715
- * @param {Document} doc - The document to create a draft of
716
- * @returns {Document} A draft document safe to mutate via apply_op_to_draft
557
+ * @returns A draft document safe to mutate via apply_op_to_draft
717
558
  */
718
559
  export function create_document_draft(doc) {
719
- return {
720
- ...doc,
721
- nodes: { ...doc.nodes }
722
- };
560
+ return {
561
+ ...doc,
562
+ nodes: { ...doc.nodes }
563
+ };
723
564
  }
724
-
725
565
  /**
726
566
  * Applies an operation to a document draft in place.
727
567
  *
@@ -730,89 +570,72 @@ export function create_document_draft(doc) {
730
570
  * previous one-copy-per-op behavior, which made transactions with many ops
731
571
  * (paste, cascade delete) O(N·ops).
732
572
  *
733
- * @param {Document} draft - A draft created via create_document_draft
734
- * @param {Array} op - The operation to apply [type, ...args]
735
- * @returns {Document} The same draft, for convenience
573
+ * @param draft - A draft created via create_document_draft
574
+ * @param op - The operation to apply [type, ...args]
575
+ * @returns The same draft, for convenience
736
576
  */
737
577
  export function apply_op_to_draft(draft, op) {
738
- const [type, ...args] = op;
739
- if (type === 'set') {
740
- const [node_id, property] = args[0];
741
- draft.nodes[node_id] = {
742
- ...draft.nodes[node_id],
743
- [property]: structuredClone(args[1])
744
- };
745
- } else if (type === 'create') {
746
- draft.nodes[args[0].id] = structuredClone(args[0]);
747
- } else if (type === 'delete') {
748
- delete draft.nodes[args[0]];
749
- }
750
- return draft;
578
+ const [type, ...args] = op;
579
+ if (type === 'set') {
580
+ const [node_id, property] = args[0];
581
+ draft.nodes[node_id] = {
582
+ ...draft.nodes[node_id],
583
+ [property]: structuredClone(args[1])
584
+ };
585
+ }
586
+ else if (type === 'create') {
587
+ draft.nodes[args[0].id] = structuredClone(args[0]);
588
+ }
589
+ else if (type === 'delete') {
590
+ delete draft.nodes[args[0]];
591
+ }
592
+ return draft;
751
593
  }
752
-
753
594
  /**
754
595
  * Applies an operation to a document and returns the new document.
755
596
  * Uses copy-on-write semantics.
756
597
  *
757
- * @param {Document} doc - The document to apply the operation to
758
- * @param {Array} op - The operation to apply [type, ...args]
759
- * @returns {Document} The new document with the operation applied
598
+ * @param doc - The document to apply the operation to
599
+ * @param op - The operation to apply [type, ...args]
600
+ * @returns The new document with the operation applied
760
601
  */
761
602
  export function apply_op(doc, op) {
762
- return apply_op_to_draft(create_document_draft(doc), op);
603
+ return apply_op_to_draft(create_document_draft(doc), op);
763
604
  }
764
-
765
605
  /**
766
606
  * Counts how many times a node is referenced in the document.
767
- *
768
- * @param {DocumentSchema} schema - The document schema
769
- * @param {Document} doc - The document containing nodes
770
- * @param {NodeId} node_id - The node ID to count references for
771
- * @returns {number} The number of references
772
607
  */
773
608
  export function count_references(schema, doc, node_id) {
774
- let count = 0;
775
-
776
- for (const node of Object.values(doc.nodes)) {
777
- for (const [property, value] of Object.entries(node)) {
778
- if (property === 'id' || property === 'type') continue;
779
-
780
- const prop_type = property_type(schema, node.type, property);
781
-
782
- if (prop_type === 'node_array') {
783
- count += value.nodes.filter((id) => id === node_id).length;
784
- } else if (prop_type === 'node' && value === node_id) {
785
- count += 1;
786
- }
787
-
788
- if ((prop_type === 'text' || prop_type === 'node_array') && value) {
789
- count += [...value.marks, ...value.annotations].filter(
790
- (range) => range.node_id === node_id
791
- ).length;
792
- }
793
- }
794
- }
795
-
796
- return count;
797
- }
798
-
799
- /**
800
- * @param {DocumentSchema} schema - The document schema
801
- * @param {string} from_type - Current mark type
802
- * @param {string} to_type - Target mark type
803
- * @returns {boolean} Whether a mark can be switched between these types
609
+ let count = 0;
610
+ for (const node of Object.values(doc.nodes)) {
611
+ for (const [property, value] of Object.entries(node)) {
612
+ if (property === 'id' || property === 'type')
613
+ continue;
614
+ const prop_type = property_type(schema, node.type, property);
615
+ if (prop_type === 'node_array') {
616
+ count += value.nodes.filter((id) => id === node_id).length;
617
+ }
618
+ else if (prop_type === 'node' && value === node_id) {
619
+ count += 1;
620
+ }
621
+ if ((prop_type === 'text' || prop_type === 'node_array') && value) {
622
+ count += [...value.marks, ...value.annotations].filter((range) => range.node_id === node_id).length;
623
+ }
624
+ }
625
+ }
626
+ return count;
627
+ }
628
+ /**
629
+ * Whether a mark can be switched between these types.
804
630
  */
805
631
  export function can_switch_mark_type(schema, from_type, to_type) {
806
- const from_schema = schema[from_type];
807
- const to_schema = schema[to_type];
808
- return (
809
- from_schema?.kind === 'mark' &&
810
- to_schema?.kind === 'mark' &&
811
- Object.keys(from_schema.properties ?? {}).length === 0 &&
812
- Object.keys(to_schema.properties ?? {}).length === 0
813
- );
632
+ const from_schema = schema[from_type];
633
+ const to_schema = schema[to_type];
634
+ return (from_schema?.kind === 'mark' &&
635
+ to_schema?.kind === 'mark' &&
636
+ Object.keys(from_schema.properties ?? {}).length === 0 &&
637
+ Object.keys(to_schema.properties ?? {}).length === 0);
814
638
  }
815
-
816
639
  /**
817
640
  * Gets ranges of the given key ('marks' or 'annotations') touched by the
818
641
  * current selection.
@@ -821,259 +644,192 @@ export function can_switch_mark_type(schema, from_type, to_type) {
821
644
  * adjacent boundaries do not count as touching. Collapsed text carets are only
822
645
  * inside a range when they are strictly between start and end; a caret at
823
646
  * either boundary is not active.
824
- *
825
- * @param {DocumentSchema} schema - The document schema
826
- * @param {Document} doc - The document containing nodes
827
- * @param {Selection | null | undefined} selection - The current selection
828
- * @param {'marks' | 'annotations'} key - Which range array to inspect
829
- * @returns {(Attachment & { index: number, node: any })[]} Selected attachment records
830
647
  */
831
648
  function get_selected_ranges(schema, doc, selection, key) {
832
- if (selection?.type !== 'text' && selection?.type !== 'node') return [];
833
- const range = get_selection_range(selection);
834
- if (!range) return [];
835
-
836
- const annotated_prop = get(schema, doc, selection.path);
837
- const ranges = annotated_prop?.[key] ?? [];
838
- const is_collapsed = range.start_offset === range.end_offset;
839
-
840
- return ranges
841
- .map((/** @type {Attachment} */ attachment, index) => {
842
- const node = doc.nodes[attachment.node_id];
843
- return {
844
- ...attachment,
845
- index,
846
- node
847
- };
848
- })
849
- .filter(({ start_offset, end_offset }) => {
850
- if (is_collapsed) {
851
- return start_offset < range.start_offset && end_offset > range.start_offset;
852
- }
853
- return start_offset < range.end_offset && end_offset > range.start_offset;
854
- });
649
+ if (selection?.type !== 'text' && selection?.type !== 'node')
650
+ return [];
651
+ const range = get_selection_range(selection);
652
+ if (!range)
653
+ return [];
654
+ const annotated_prop = get(schema, doc, selection.path);
655
+ const ranges = annotated_prop?.[key] ?? [];
656
+ const is_collapsed = range.start_offset === range.end_offset;
657
+ return ranges
658
+ .map((attachment, index) => {
659
+ const node = doc.nodes[attachment.node_id];
660
+ return {
661
+ ...attachment,
662
+ index,
663
+ node
664
+ };
665
+ })
666
+ .filter(({ start_offset, end_offset }) => {
667
+ if (is_collapsed) {
668
+ return start_offset < range.start_offset && end_offset > range.start_offset;
669
+ }
670
+ return start_offset < range.end_offset && end_offset > range.start_offset;
671
+ });
855
672
  }
856
-
857
673
  /**
858
674
  * Gets marks touched by the current selection.
859
- *
860
- * @param {DocumentSchema} schema - The document schema
861
- * @param {Document} doc - The document containing nodes
862
- * @param {Selection} [selection] - The current selection
863
- * @returns {(Mark & { index: number, node: any })[]} Selected mark records
864
675
  */
865
676
  export function get_selected_marks(schema, doc, selection) {
866
- return get_selected_ranges(schema, doc, selection, 'marks');
677
+ return get_selected_ranges(schema, doc, selection, 'marks');
867
678
  }
868
-
869
679
  /**
870
680
  * Gets annotations touched by the current selection.
871
- *
872
- * @param {DocumentSchema} schema - The document schema
873
- * @param {Document} doc - The document containing nodes
874
- * @param {Selection} [selection] - The current selection
875
- * @returns {(Annotation & { index: number, node: any })[]} Selected annotation records
876
681
  */
877
682
  export function get_selected_annotations(schema, doc, selection) {
878
- return get_selected_ranges(schema, doc, selection, 'annotations');
683
+ return get_selected_ranges(schema, doc, selection, 'annotations');
879
684
  }
880
-
881
685
  /**
882
- * @param {(Attachment & { index: number, node: any })[]} selected_ranges
883
- * @returns {Set<string>} Node types represented in the selected ranges
686
+ * Node types represented in the selected ranges.
884
687
  */
885
688
  export function get_selected_range_types(selected_ranges) {
886
- return new Set(selected_ranges.map(({ node }) => node?.type).filter(Boolean));
689
+ return new Set(selected_ranges.map(({ node }) => node?.type).filter((type) => Boolean(type)));
887
690
  }
888
-
889
691
  /**
890
692
  * Counts references to a node, excluding nodes marked for deletion.
891
- *
892
- * @param {DocumentSchema} schema - The document schema
893
- * @param {Document} doc - The document containing nodes
894
- * @param {NodeId} target_node_id - The node ID to count references for
895
- * @param {Record<NodeId, boolean>} nodes_to_delete - Nodes marked for deletion
896
- * @returns {number} The number of references excluding deleted nodes
897
693
  */
898
694
  export function count_references_excluding_deleted(schema, doc, target_node_id, nodes_to_delete) {
899
- let count = 0;
900
-
901
- for (const node of Object.values(doc.nodes)) {
902
- if (nodes_to_delete[node.id]) continue;
903
-
904
- for (const [property, value] of Object.entries(node)) {
905
- if (property === 'id' || property === 'type') continue;
906
-
907
- const prop_type = property_type(schema, node.type, property);
908
-
909
- if (prop_type === 'node_array') {
910
- count += value.nodes.filter((id) => id === target_node_id).length;
911
- } else if (prop_type === 'node' && value === target_node_id) {
912
- count += 1;
913
- }
914
-
915
- if ((prop_type === 'text' || prop_type === 'node_array') && value) {
916
- count += [...value.marks, ...value.annotations].filter(
917
- (range) => range.node_id === target_node_id
918
- ).length;
919
- }
920
- }
921
- }
922
-
923
- return count;
695
+ let count = 0;
696
+ for (const node of Object.values(doc.nodes)) {
697
+ if (nodes_to_delete[node.id])
698
+ continue;
699
+ for (const [property, value] of Object.entries(node)) {
700
+ if (property === 'id' || property === 'type')
701
+ continue;
702
+ const prop_type = property_type(schema, node.type, property);
703
+ if (prop_type === 'node_array') {
704
+ count += value.nodes.filter((id) => id === target_node_id).length;
705
+ }
706
+ else if (prop_type === 'node' && value === target_node_id) {
707
+ count += 1;
708
+ }
709
+ if ((prop_type === 'text' || prop_type === 'node_array') && value) {
710
+ count += [...value.marks, ...value.annotations].filter((range) => range.node_id === target_node_id).length;
711
+ }
712
+ }
713
+ }
714
+ return count;
924
715
  }
925
-
926
716
  /**
927
717
  * Iterates all node references going out of a node, invoking the visitor
928
718
  * once per occurrence (multiplicity matters for reference counting).
929
719
  * Covers the same reference kinds as count_references_excluding_deleted:
930
720
  * a `node` property, `node_array` .nodes ids, and the mark/annotation
931
721
  * node_ids carried on `text` and `node_array` properties.
932
- *
933
- * @param {DocumentSchema} schema - The document schema
934
- * @param {any} node - The node whose outgoing references to visit
935
- * @param {(referenced_id: NodeId) => void} visit - Called per reference occurrence
936
722
  */
937
723
  export function visit_node_references(schema, node, visit) {
938
- const node_schema = schema[node.type];
939
- if (!node_schema) return;
940
-
941
- for (const [property, prop_def] of Object.entries(node_schema.properties)) {
942
- const value = node[property];
943
- if (value === undefined || value === null) continue;
944
-
945
- const prop_type = prop_def.type;
946
-
947
- if (prop_type === 'node_array') {
948
- for (const id of value.nodes) visit(id);
949
- } else if (prop_type === 'node' && typeof value === 'string') {
950
- visit(value);
951
- }
952
-
953
- if ((prop_type === 'text' || prop_type === 'node_array') && value) {
954
- for (const range of value.marks) visit(range.node_id);
955
- for (const range of value.annotations) visit(range.node_id);
956
- }
957
- }
724
+ const node_schema = schema[node.type];
725
+ if (!node_schema)
726
+ return;
727
+ for (const [property, prop_def] of Object.entries(node_schema.properties)) {
728
+ const value = node[property];
729
+ if (value === undefined || value === null)
730
+ continue;
731
+ const prop_type = prop_def.type;
732
+ if (prop_type === 'node_array') {
733
+ for (const id of value.nodes)
734
+ visit(id);
735
+ }
736
+ else if (prop_type === 'node' && typeof value === 'string') {
737
+ visit(value);
738
+ }
739
+ if ((prop_type === 'text' || prop_type === 'node_array') && value) {
740
+ for (const range of value.marks)
741
+ visit(range.node_id);
742
+ for (const range of value.annotations)
743
+ visit(range.node_id);
744
+ }
745
+ }
958
746
  }
959
-
960
747
  /**
961
748
  * Builds reference counts for every referenced node in one full-document
962
749
  * scan. Used by cascade deletion so the fixpoint can decrement counts
963
750
  * incrementally instead of re-scanning the document per candidate node.
964
- *
965
- * @param {DocumentSchema} schema - The document schema
966
- * @param {Document} doc - The document containing nodes
967
- * @returns {Map<NodeId, number>} Reference count per referenced node id
968
751
  */
969
752
  export function build_reference_counts(schema, doc) {
970
- /** @type {Map<NodeId, number>} */
971
- const counts = new Map();
972
- for (const node of Object.values(doc.nodes)) {
973
- visit_node_references(schema, node, (id) => {
974
- counts.set(id, (counts.get(id) || 0) + 1);
975
- });
976
- }
977
- return counts;
753
+ const counts = new Map();
754
+ for (const node of Object.values(doc.nodes)) {
755
+ visit_node_references(schema, node, (id) => {
756
+ counts.set(id, (counts.get(id) || 0) + 1);
757
+ });
758
+ }
759
+ return counts;
978
760
  }
979
-
980
761
  /**
981
762
  * Gets ids of nodes that reference any of the target nodes.
982
- *
983
- * @param {DocumentSchema} schema - The document schema
984
- * @param {Document} doc - The document containing nodes
985
- * @param {Iterable<NodeId>} target_node_ids - Node IDs to find referrers for
986
- * @returns {NodeId[]} IDs of nodes with references to any target node
987
763
  */
988
764
  export function get_referencing_node_ids(schema, doc, target_node_ids) {
989
- const target_ids = new Set(target_node_ids);
990
- const referencing_node_ids = new Set();
991
- if (target_ids.size === 0) return [];
992
-
993
- for (const node of Object.values(doc.nodes)) {
994
- for (const [property, value] of Object.entries(node)) {
995
- if (property === 'id' || property === 'type') continue;
996
-
997
- const prop_type = property_type(schema, node.type, property);
998
-
999
- if (prop_type === 'node_array') {
1000
- if (value.nodes.some((id) => target_ids.has(id))) {
1001
- referencing_node_ids.add(node.id);
1002
- }
1003
- } else if (prop_type === 'node' && target_ids.has(value)) {
1004
- referencing_node_ids.add(node.id);
1005
- }
1006
-
1007
- if (
1008
- (prop_type === 'text' || prop_type === 'node_array') &&
1009
- value &&
1010
- [...value.marks, ...value.annotations].some((range) =>
1011
- target_ids.has(range.node_id)
1012
- )
1013
- ) {
1014
- referencing_node_ids.add(node.id);
1015
- }
1016
- }
1017
- }
1018
-
1019
- return [...referencing_node_ids];
765
+ const target_ids = new Set(target_node_ids);
766
+ const referencing_node_ids = new Set();
767
+ if (target_ids.size === 0)
768
+ return [];
769
+ for (const node of Object.values(doc.nodes)) {
770
+ for (const [property, value] of Object.entries(node)) {
771
+ if (property === 'id' || property === 'type')
772
+ continue;
773
+ const prop_type = property_type(schema, node.type, property);
774
+ if (prop_type === 'node_array') {
775
+ if (value.nodes.some((id) => target_ids.has(id))) {
776
+ referencing_node_ids.add(node.id);
777
+ }
778
+ }
779
+ else if (prop_type === 'node' && target_ids.has(value)) {
780
+ referencing_node_ids.add(node.id);
781
+ }
782
+ if ((prop_type === 'text' || prop_type === 'node_array') &&
783
+ value &&
784
+ [...value.marks, ...value.annotations].some((range) => target_ids.has(range.node_id))) {
785
+ referencing_node_ids.add(node.id);
786
+ }
787
+ }
788
+ }
789
+ return [...referencing_node_ids];
1020
790
  }
1021
-
1022
791
  /**
1023
792
  * Validates a selection against the current document state.
1024
793
  * Works with any object that implements get() and inspect() (Session or Transaction).
1025
794
  *
1026
- * @param {Selection} selection - The selection to validate
1027
- * @param {{ get: Function, inspect: Function }} session_or_transaction - A Session or Transaction instance
1028
795
  * @throws {Error} Throws if the selection is invalid
1029
796
  */
1030
797
  export function validate_selection(selection, session_or_transaction) {
1031
- if (!selection) return;
1032
-
1033
- const selection_type = selection.type;
1034
- if (!['node', 'text', 'property'].includes(selection_type)) {
1035
- throw new Error(`Invalid selection type: ${selection_type}`);
1036
- }
1037
-
1038
- if (selection_type === 'node') {
1039
- const node_array_prop = session_or_transaction.get(selection.path);
1040
-
1041
- if (!node_array_prop || !Array.isArray(node_array_prop.nodes)) {
1042
- throw new Error('Node selection path must point to a node_array');
1043
- }
1044
-
1045
- const max_offset = node_array_prop.nodes.length;
1046
- if (selection.anchor_offset < 0 || selection.anchor_offset > max_offset) {
1047
- throw new Error(
1048
- `Node selection anchor_offset (${selection.anchor_offset}) is out of bounds. Max is ${max_offset}.`
1049
- );
1050
- }
1051
- if (selection.focus_offset < 0 || selection.focus_offset > max_offset) {
1052
- throw new Error(
1053
- `Node selection focus_offset (${selection.focus_offset}) is out of bounds. Max is ${max_offset}.`
1054
- );
1055
- }
1056
- } else if (selection_type === 'text') {
1057
- const text = session_or_transaction.get(selection.path);
1058
-
1059
- if (!text || typeof text.content !== 'string') {
1060
- throw new Error('Text selection path must point to text');
1061
- }
1062
-
1063
- const char_length = get_char_length(text.content);
1064
- if (selection.anchor_offset < 0 || selection.anchor_offset > char_length) {
1065
- throw new Error(
1066
- `Text selection anchor_offset (${selection.anchor_offset}) is out of bounds. Max is ${char_length}.`
1067
- );
1068
- }
1069
- if (selection.focus_offset < 0 || selection.focus_offset > char_length) {
1070
- throw new Error(
1071
- `Text selection focus_offset (${selection.focus_offset}) is out of bounds. Max is ${char_length}.`
1072
- );
1073
- }
1074
- } else if (selection_type === 'property') {
1075
- if (!session_or_transaction.inspect(selection.path)) {
1076
- throw new Error(`Property selection path not found: ${serialize_path(selection.path)}`);
1077
- }
1078
- }
798
+ if (!selection)
799
+ return;
800
+ const selection_type = selection.type;
801
+ if (!['node', 'text', 'property'].includes(selection_type)) {
802
+ throw new Error(`Invalid selection type: ${selection_type}`);
803
+ }
804
+ if (selection_type === 'node') {
805
+ const node_array_prop = session_or_transaction.get(selection.path);
806
+ if (!node_array_prop || !Array.isArray(node_array_prop.nodes)) {
807
+ throw new Error('Node selection path must point to a node_array');
808
+ }
809
+ const max_offset = node_array_prop.nodes.length;
810
+ if (selection.anchor_offset < 0 || selection.anchor_offset > max_offset) {
811
+ throw new Error(`Node selection anchor_offset (${selection.anchor_offset}) is out of bounds. Max is ${max_offset}.`);
812
+ }
813
+ if (selection.focus_offset < 0 || selection.focus_offset > max_offset) {
814
+ throw new Error(`Node selection focus_offset (${selection.focus_offset}) is out of bounds. Max is ${max_offset}.`);
815
+ }
816
+ }
817
+ else if (selection_type === 'text') {
818
+ const text = session_or_transaction.get(selection.path);
819
+ if (!text || typeof text.content !== 'string') {
820
+ throw new Error('Text selection path must point to text');
821
+ }
822
+ const char_length = get_char_length(text.content);
823
+ if (selection.anchor_offset < 0 || selection.anchor_offset > char_length) {
824
+ throw new Error(`Text selection anchor_offset (${selection.anchor_offset}) is out of bounds. Max is ${char_length}.`);
825
+ }
826
+ if (selection.focus_offset < 0 || selection.focus_offset > char_length) {
827
+ throw new Error(`Text selection focus_offset (${selection.focus_offset}) is out of bounds. Max is ${char_length}.`);
828
+ }
829
+ }
830
+ else if (selection_type === 'property') {
831
+ if (!session_or_transaction.inspect(selection.path)) {
832
+ throw new Error(`Property selection path not found: ${serialize_path(selection.path)}`);
833
+ }
834
+ }
1079
835
  }