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
@@ -3,8 +3,6 @@
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
7
 
10
8
  import {
@@ -15,25 +13,44 @@ import {
15
13
  serialize_path,
16
14
  are_ranges_exclusive
17
15
  } from './utils.js';
16
+ import type {
17
+ NodeId,
18
+ DocumentPath,
19
+ PrimitiveType,
20
+ PropertyDefinition,
21
+ NodeProperty,
22
+ NodeArrayProperty,
23
+ NodeSchema,
24
+ NodeKind,
25
+ DocumentSchema,
26
+ Selection,
27
+ Attachment,
28
+ Mark,
29
+ Annotation,
30
+ Document,
31
+ DocumentNode,
32
+ TextProperty,
33
+ Text,
34
+ ValidateDocumentSchema,
35
+ Inspection,
36
+ DocumentOperation,
37
+ SessionConfig
38
+ } from './types.js';
18
39
 
19
40
  /**
20
41
  * Identity function — keeps schema at runtime & makes IDE infer types.
21
42
  * 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
43
  */
27
- export function define_document_schema(schema) {
44
+ export function define_document_schema<S extends Record<string, NodeSchema>>(
45
+ schema: S & ValidateDocumentSchema<S>
46
+ ): S {
28
47
  return schema;
29
48
  }
30
49
 
31
50
  /**
32
51
  * 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
52
  */
36
- export function is_primitive_type(type) {
53
+ export function is_primitive_type(type: string): type is PrimitiveType {
37
54
  return [
38
55
  'string',
39
56
  'number',
@@ -50,10 +67,11 @@ export function is_primitive_type(type) {
50
67
 
51
68
  /**
52
69
  * 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
70
+ * Returns null if none specified.
55
71
  */
56
- export function get_default_node_type(property_definition) {
72
+ export function get_default_node_type(
73
+ property_definition: NodeProperty | NodeArrayProperty
74
+ ): string | null {
57
75
  if (!property_definition || !property_definition.node_types) {
58
76
  return null;
59
77
  }
@@ -64,11 +82,8 @@ export function get_default_node_type(property_definition) {
64
82
  );
65
83
  }
66
84
 
67
- /**
68
- * @param {import('./types').PropertyDefinition} property_definition
69
- * @returns {any}
70
- */
71
- export function get_property_default(property_definition) {
85
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
86
+ export function get_property_default(property_definition: PropertyDefinition): any {
72
87
  if ('default' in property_definition) return structuredClone(property_definition.default);
73
88
 
74
89
  if (property_definition.type === 'string') return '';
@@ -97,11 +112,9 @@ export function get_property_default(property_definition) {
97
112
  * schema changes cannot be represented by defaults, such as property renames or
98
113
  * data transformations.
99
114
  *
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
115
+ * @returns A shallow copy of the node with cloned default values filled in
103
116
  */
104
- export function fill_node_defaults(node, schema) {
117
+ export function fill_node_defaults(node: DocumentNode, schema: DocumentSchema): DocumentNode {
105
118
  const node_schema = schema[node.type];
106
119
  if (!node_schema) return { ...node };
107
120
 
@@ -124,13 +137,10 @@ export function fill_node_defaults(node, schema) {
124
137
  * by itself. Call this as one step in an explicit document migration when it is
125
138
  * appropriate.
126
139
  *
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
140
+ * @returns A document copy with cloned default values filled in
130
141
  */
131
- export function fill_document_defaults(doc, schema) {
132
- /** @type {Record<string, any>} */
133
- const nodes = {};
142
+ export function fill_document_defaults(doc: Document, schema: DocumentSchema): Document {
143
+ const nodes: Record<string, DocumentNode> = {};
134
144
 
135
145
  for (const [node_id, node] of Object.entries(doc.nodes)) {
136
146
  nodes[node_id] = fill_node_defaults(node, schema);
@@ -144,14 +154,73 @@ export function fill_document_defaults(doc, schema) {
144
154
 
145
155
  /**
146
156
  * Validate a document schema to ensure all referenced node types exist.
147
- * @param {DocumentSchema} document_schema - The document schema to validate
148
157
  * @throws {Error} Throws if the document schema is invalid
149
158
  */
150
- export function validate_document_schema(document_schema) {
159
+ export function validate_document_schema(document_schema: DocumentSchema): void {
151
160
  // Check that all referenced node types exist
152
161
  for (const [node_type, node_schema] of Object.entries(document_schema)) {
153
162
  for (const [prop_name, prop_def] of Object.entries(node_schema.properties)) {
154
163
  assert_path_string_segment(prop_name, `Property name "${prop_name}"`);
164
+ if (prop_def.type === 'string' && prop_def.values !== undefined) {
165
+ if (
166
+ !Array.isArray(prop_def.values) ||
167
+ prop_def.values.some((value) => typeof value !== 'string')
168
+ ) {
169
+ throw new Error(
170
+ `Node type "${node_type}" property "${prop_name}" values must be an array of strings.`
171
+ );
172
+ }
173
+ if (prop_def.values.length === 0) {
174
+ throw new Error(
175
+ `Node type "${node_type}" property "${prop_name}" values must not be empty.`
176
+ );
177
+ }
178
+ if (new Set(prop_def.values).size !== prop_def.values.length) {
179
+ throw new Error(
180
+ `Node type "${node_type}" property "${prop_name}" values must be unique.`
181
+ );
182
+ }
183
+ if (prop_def.default !== undefined && !prop_def.values.includes(prop_def.default)) {
184
+ throw new Error(
185
+ `Node type "${node_type}" property "${prop_name}" default must be one of its allowed values.`
186
+ );
187
+ }
188
+ }
189
+ if (prop_def.type === 'integer') {
190
+ if (prop_def.default !== undefined && !Number.isInteger(prop_def.default)) {
191
+ throw new Error(
192
+ `Node type "${node_type}" property "${prop_name}" default must be an integer.`
193
+ );
194
+ }
195
+ if (prop_def.min !== undefined && !Number.isInteger(prop_def.min)) {
196
+ throw new Error(
197
+ `Node type "${node_type}" property "${prop_name}" min must be an integer.`
198
+ );
199
+ }
200
+ if (prop_def.max !== undefined && !Number.isInteger(prop_def.max)) {
201
+ throw new Error(
202
+ `Node type "${node_type}" property "${prop_name}" max must be an integer.`
203
+ );
204
+ }
205
+ if (
206
+ prop_def.min !== undefined &&
207
+ prop_def.max !== undefined &&
208
+ prop_def.min > prop_def.max
209
+ ) {
210
+ throw new Error(
211
+ `Node type "${node_type}" property "${prop_name}" min must not be greater than max.`
212
+ );
213
+ }
214
+ if (
215
+ prop_def.default !== undefined &&
216
+ ((prop_def.min !== undefined && prop_def.default < prop_def.min) ||
217
+ (prop_def.max !== undefined && prop_def.default > prop_def.max))
218
+ ) {
219
+ throw new Error(
220
+ `Node type "${node_type}" property "${prop_name}" default must be within its min/max range.`
221
+ );
222
+ }
223
+ }
155
224
  if (prop_def.type === 'node' || prop_def.type === 'node_array') {
156
225
  const missing_types = prop_def.node_types.filter(
157
226
  (ref_type) => !(ref_type in document_schema)
@@ -167,7 +236,7 @@ export function validate_document_schema(document_schema) {
167
236
  for (const [key, expected_kind] of [
168
237
  ['mark_types', 'mark'],
169
238
  ['annotation_types', 'annotation']
170
- ]) {
239
+ ] as const) {
171
240
  const invalid_types = (prop_def[key] ?? []).filter(
172
241
  (ref_type) => document_schema[ref_type]?.kind !== expected_kind
173
242
  );
@@ -188,11 +257,9 @@ export function validate_document_schema(document_schema) {
188
257
  * Marks render in-place via components; annotations are data-only overlay
189
258
  * ranges that must be interpreted by the app (via CSS classes or props).
190
259
  *
191
- * @param {DocumentSchema} schema - The document schema
192
- * @param {any} config - The Svedit config
193
260
  * @throws {Error} Throws if a kind 'annotation' type has a registered component
194
261
  */
195
- export function validate_config_components(schema, config) {
262
+ export function validate_config_components(schema: DocumentSchema, config: SessionConfig): void {
196
263
  for (const [node_type, node_schema] of Object.entries(schema)) {
197
264
  if (node_schema.kind === 'annotation' && config?.node_components?.[node_type]) {
198
265
  throw new Error(
@@ -204,11 +271,9 @@ export function validate_config_components(schema, config) {
204
271
 
205
272
  /**
206
273
  * 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
274
  */
211
- function validate_primitive_value(type, value) {
275
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
276
+ function validate_primitive_value(type: PrimitiveType, value: any): boolean {
212
277
  switch (type) {
213
278
  case 'string':
214
279
  return typeof value === 'string';
@@ -244,26 +309,18 @@ function validate_primitive_value(type, value) {
244
309
  /**
245
310
  * Validate ranges (marks or annotations) for bounds, references, and allowed types.
246
311
  *
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
312
  * @throws {Error} Throws if ranges are invalid
256
313
  */
257
314
  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
- ) {
315
+ node_id: string,
316
+ prop_name: string,
317
+ label: 'mark' | 'annotation',
318
+ ranges: Array<Attachment>,
319
+ container_length: number,
320
+ allowed_types: Array<string> | null | undefined,
321
+ all_nodes: Record<string, DocumentNode>,
322
+ require_references: boolean
323
+ ): void {
267
324
  for (const [index, range] of ranges.entries()) {
268
325
  if (
269
326
  typeof range !== 'object' ||
@@ -312,24 +369,17 @@ function validate_range_array(
312
369
  *
313
370
  * Marks must be mutually exclusive; annotations may overlap.
314
371
  *
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
372
  * @throws {Error} Throws if marks or annotations are invalid
323
373
  */
324
374
  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
- ) {
375
+ node_id: string,
376
+ prop_name: string,
377
+ value: { marks: Array<Mark>; annotations: Array<Annotation> },
378
+ prop_def: { mark_types?: string[]; annotation_types?: string[] },
379
+ container_length: number,
380
+ all_nodes: Record<string, DocumentNode>,
381
+ require_references: boolean
382
+ ): void {
333
383
  validate_range_array(
334
384
  node_id,
335
385
  prop_name,
@@ -363,22 +413,16 @@ function validate_marks_and_annotations(
363
413
  * Validate text property marks and annotations for bounds, references, allowed
364
414
  * types, and mark exclusivity.
365
415
  *
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
416
  * @throws {Error} Throws if marks or annotations are invalid
373
417
  */
374
418
  function validate_text_property(
375
- node_id,
376
- prop_name,
377
- value,
378
- prop_def,
379
- all_nodes,
380
- require_references
381
- ) {
419
+ node_id: string,
420
+ prop_name: string,
421
+ value: Text,
422
+ prop_def: TextProperty,
423
+ all_nodes: Record<string, DocumentNode>,
424
+ require_references: boolean
425
+ ): void {
382
426
  const char_length = get_char_length(value.content);
383
427
  validate_marks_and_annotations(
384
428
  node_id,
@@ -391,23 +435,25 @@ function validate_text_property(
391
435
  );
392
436
  }
393
437
 
394
- /**
395
- * @param {string} id
396
- * @returns {boolean}
397
- */
398
- export function is_id_valid(id) {
438
+ export function is_id_valid(id: string): boolean {
399
439
  return typeof id === 'string' && id.length > 0 && is_path_string_segment_valid(id);
400
440
  }
401
441
 
402
442
  /**
403
443
  * 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
444
+ *
445
+ * @param node - The node to validate
446
+ * @param schema - The document schema
447
+ * @param all_nodes - All nodes in the document to check references
448
+ * @param options - Validation options
408
449
  * @throws {Error} Throws if the node is invalid
409
450
  */
410
- export function validate_node(node, schema, all_nodes = {}, options = {}) {
451
+ export function validate_node(
452
+ node: DocumentNode,
453
+ schema: DocumentSchema,
454
+ all_nodes: Record<string, DocumentNode> = {},
455
+ options: { require_references?: boolean } = {}
456
+ ): void {
411
457
  const require_references = options.require_references ?? true;
412
458
  if (!is_id_valid(node.id)) {
413
459
  throw new Error(`Node ${node.id} has an invalid id.`);
@@ -429,13 +475,27 @@ export function validate_node(node, schema, all_nodes = {}, options = {}) {
429
475
  `Node ${node.id} has an invalid property: ${prop_name} must be of type ${prop_def.type}.`
430
476
  );
431
477
  }
478
+ if (prop_def.type === 'string' && prop_def.values && !prop_def.values.includes(value)) {
479
+ throw new Error(
480
+ `Node ${node.id} has an invalid property: ${prop_name} must be one of [${prop_def.values.join(', ')}].`
481
+ );
482
+ }
483
+ if (
484
+ prop_def.type === 'integer' &&
485
+ ((prop_def.min !== undefined && value < prop_def.min) ||
486
+ (prop_def.max !== undefined && value > prop_def.max))
487
+ ) {
488
+ throw new Error(
489
+ `Node ${node.id} has an invalid property: ${prop_name} must be between ${prop_def.min ?? '-Infinity'} and ${prop_def.max ?? 'Infinity'}.`
490
+ );
491
+ }
432
492
  }
433
493
  if (prop_def.type === 'text') {
434
494
  validate_text_property(
435
495
  node.id,
436
496
  prop_name,
437
497
  value,
438
- /** @type {TextProperty} */ (prop_def),
498
+ prop_def as TextProperty,
439
499
  all_nodes,
440
500
  require_references
441
501
  );
@@ -459,7 +519,7 @@ export function validate_node(node, schema, all_nodes = {}, options = {}) {
459
519
  }
460
520
  if (!prop_def.node_types.includes(referenced_node.type)) {
461
521
  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.`
522
+ `Node ${node.id} property ${prop_name} references node ${value} of type ${referenced_node.type}, but only types [${(prop_def as NodeProperty).node_types.join(', ')}] are allowed.`
463
523
  );
464
524
  }
465
525
  }
@@ -479,7 +539,7 @@ export function validate_node(node, schema, all_nodes = {}, options = {}) {
479
539
 
480
540
  const node_array_nodes = value.nodes;
481
541
 
482
- if (!node_array_nodes.every((id) => typeof id === 'string' && is_id_valid(id))) {
542
+ if (!node_array_nodes.every((id: unknown) => typeof id === 'string' && is_id_valid(id))) {
483
543
  throw new Error(
484
544
  `Node ${node.id} has an invalid property: ${prop_name} must contain valid node ids.`
485
545
  );
@@ -518,11 +578,9 @@ export function validate_node(node, schema, all_nodes = {}, options = {}) {
518
578
  /**
519
579
  * Validate a document against its schema.
520
580
  *
521
- * @param {Document} doc - The document to validate
522
- * @param {DocumentSchema} schema - The document schema
523
581
  * @throws {Error} Throws if the document is invalid
524
582
  */
525
- export function validate_document(doc, schema) {
583
+ export function validate_document(doc: Document, schema: DocumentSchema): void {
526
584
  if (!is_id_valid(doc.document_id)) {
527
585
  throw new Error(`Document ${doc.document_id} has an invalid id.`);
528
586
  }
@@ -540,12 +598,13 @@ export function validate_document(doc, schema) {
540
598
  /**
541
599
  * Gets a value from the document at the specified path.
542
600
  *
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
601
+ * @param schema - The document schema
602
+ * @param doc - The document containing nodes
603
+ * @param path - Array path to the value, or a string node ID
604
+ * @returns The value at the specified path
547
605
  */
548
- export function get(schema, doc, path) {
606
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
607
+ export function get(schema: DocumentSchema, doc: Document, path: DocumentPath | string): any {
549
608
  if (typeof path === 'string') {
550
609
  path = [path];
551
610
  }
@@ -553,7 +612,8 @@ export function get(schema, doc, path) {
553
612
  throw new Error(`Invalid path provided ${JSON.stringify(path)}`);
554
613
  }
555
614
 
556
- let val = doc.nodes[path[0]];
615
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
616
+ let val: any = doc.nodes[path[0]];
557
617
  let val_type = 'node';
558
618
 
559
619
  for (let i = 1; i < path.length; i++) {
@@ -644,13 +704,8 @@ export function get(schema, doc, path) {
644
704
 
645
705
  /**
646
706
  * 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
707
  */
653
- export function property_type(schema, type, property) {
708
+ export function property_type(schema: DocumentSchema, type: string, property: string): string {
654
709
  if (typeof type !== 'string') throw new Error(`Invalid type ${type} provided`);
655
710
  if (typeof property !== 'string') throw new Error(`Invalid property ${property} provided`);
656
711
 
@@ -665,28 +720,19 @@ export function property_type(schema, type, property) {
665
720
  }
666
721
 
667
722
  /**
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
723
+ * Determines the kind of a node ('document', 'block', 'text', 'mark', or 'annotation').
673
724
  */
674
- export function kind(schema, node) {
725
+ export function kind(schema: DocumentSchema, node: DocumentNode): NodeKind {
675
726
  return schema[node.type].kind;
676
727
  }
677
728
 
678
729
  /**
679
730
  * 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
731
  */
686
- export function inspect(schema, doc, path) {
732
+ export function inspect(schema: DocumentSchema, doc: Document, path: DocumentPath): Inspection {
687
733
  const parent = path.length > 1 ? get(schema, doc, path.slice(0, -1)) : undefined;
688
734
  if (parent?.type) {
689
- const property_name = path.at(-1);
735
+ const property_name = path.at(-1) as string;
690
736
  return {
691
737
  kind: 'property',
692
738
  name: property_name,
@@ -712,10 +758,9 @@ export function inspect(schema, doc, path) {
712
758
  * (fine-grained rendering relies on this) and the original document is
713
759
  * never touched.
714
760
  *
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
761
+ * @returns A draft document safe to mutate via apply_op_to_draft
717
762
  */
718
- export function create_document_draft(doc) {
763
+ export function create_document_draft(doc: Document): Document {
719
764
  return {
720
765
  ...doc,
721
766
  nodes: { ...doc.nodes }
@@ -730,11 +775,11 @@ export function create_document_draft(doc) {
730
775
  * previous one-copy-per-op behavior, which made transactions with many ops
731
776
  * (paste, cascade delete) O(N·ops).
732
777
  *
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
778
+ * @param draft - A draft created via create_document_draft
779
+ * @param op - The operation to apply [type, ...args]
780
+ * @returns The same draft, for convenience
736
781
  */
737
- export function apply_op_to_draft(draft, op) {
782
+ export function apply_op_to_draft(draft: Document, op: DocumentOperation): Document {
738
783
  const [type, ...args] = op;
739
784
  if (type === 'set') {
740
785
  const [node_id, property] = args[0];
@@ -754,23 +799,18 @@ export function apply_op_to_draft(draft, op) {
754
799
  * Applies an operation to a document and returns the new document.
755
800
  * Uses copy-on-write semantics.
756
801
  *
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
802
+ * @param doc - The document to apply the operation to
803
+ * @param op - The operation to apply [type, ...args]
804
+ * @returns The new document with the operation applied
760
805
  */
761
- export function apply_op(doc, op) {
806
+ export function apply_op(doc: Document, op: DocumentOperation): Document {
762
807
  return apply_op_to_draft(create_document_draft(doc), op);
763
808
  }
764
809
 
765
810
  /**
766
811
  * 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
812
  */
773
- export function count_references(schema, doc, node_id) {
813
+ export function count_references(schema: DocumentSchema, doc: Document, node_id: NodeId): number {
774
814
  let count = 0;
775
815
 
776
816
  for (const node of Object.values(doc.nodes)) {
@@ -780,7 +820,7 @@ export function count_references(schema, doc, node_id) {
780
820
  const prop_type = property_type(schema, node.type, property);
781
821
 
782
822
  if (prop_type === 'node_array') {
783
- count += value.nodes.filter((id) => id === node_id).length;
823
+ count += value.nodes.filter((id: NodeId) => id === node_id).length;
784
824
  } else if (prop_type === 'node' && value === node_id) {
785
825
  count += 1;
786
826
  }
@@ -797,12 +837,13 @@ export function count_references(schema, doc, node_id) {
797
837
  }
798
838
 
799
839
  /**
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
840
+ * Whether a mark can be switched between these types.
804
841
  */
805
- export function can_switch_mark_type(schema, from_type, to_type) {
842
+ export function can_switch_mark_type(
843
+ schema: DocumentSchema,
844
+ from_type: string,
845
+ to_type: string
846
+ ): boolean {
806
847
  const from_schema = schema[from_type];
807
848
  const to_schema = schema[to_type];
808
849
  return (
@@ -813,6 +854,12 @@ export function can_switch_mark_type(schema, from_type, to_type) {
813
854
  );
814
855
  }
815
856
 
857
+ /**
858
+ * An attachment (mark or annotation) enriched with its index in the parent
859
+ * attachment array and the resolved payload node.
860
+ */
861
+ export type SelectedAttachment = Attachment & { index: number; node: DocumentNode };
862
+
816
863
  /**
817
864
  * Gets ranges of the given key ('marks' or 'annotations') touched by the
818
865
  * current selection.
@@ -821,14 +868,13 @@ export function can_switch_mark_type(schema, from_type, to_type) {
821
868
  * adjacent boundaries do not count as touching. Collapsed text carets are only
822
869
  * inside a range when they are strictly between start and end; a caret at
823
870
  * 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
871
  */
831
- function get_selected_ranges(schema, doc, selection, key) {
872
+ function get_selected_ranges(
873
+ schema: DocumentSchema,
874
+ doc: Document,
875
+ selection: Selection | null | undefined,
876
+ key: 'marks' | 'annotations'
877
+ ): SelectedAttachment[] {
832
878
  if (selection?.type !== 'text' && selection?.type !== 'node') return [];
833
879
  const range = get_selection_range(selection);
834
880
  if (!range) return [];
@@ -838,7 +884,7 @@ function get_selected_ranges(schema, doc, selection, key) {
838
884
  const is_collapsed = range.start_offset === range.end_offset;
839
885
 
840
886
  return ranges
841
- .map((/** @type {Attachment} */ attachment, index) => {
887
+ .map((attachment: Attachment, index: number) => {
842
888
  const node = doc.nodes[attachment.node_id];
843
889
  return {
844
890
  ...attachment,
@@ -846,7 +892,7 @@ function get_selected_ranges(schema, doc, selection, key) {
846
892
  node
847
893
  };
848
894
  })
849
- .filter(({ start_offset, end_offset }) => {
895
+ .filter(({ start_offset, end_offset }: SelectedAttachment) => {
850
896
  if (is_collapsed) {
851
897
  return start_offset < range.start_offset && end_offset > range.start_offset;
852
898
  }
@@ -856,46 +902,44 @@ function get_selected_ranges(schema, doc, selection, key) {
856
902
 
857
903
  /**
858
904
  * 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
905
  */
865
- export function get_selected_marks(schema, doc, selection) {
906
+ export function get_selected_marks(
907
+ schema: DocumentSchema,
908
+ doc: Document,
909
+ selection?: Selection | null
910
+ ): SelectedAttachment[] {
866
911
  return get_selected_ranges(schema, doc, selection, 'marks');
867
912
  }
868
913
 
869
914
  /**
870
915
  * 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
916
  */
877
- export function get_selected_annotations(schema, doc, selection) {
917
+ export function get_selected_annotations(
918
+ schema: DocumentSchema,
919
+ doc: Document,
920
+ selection?: Selection | null
921
+ ): SelectedAttachment[] {
878
922
  return get_selected_ranges(schema, doc, selection, 'annotations');
879
923
  }
880
924
 
881
925
  /**
882
- * @param {(Attachment & { index: number, node: any })[]} selected_ranges
883
- * @returns {Set<string>} Node types represented in the selected ranges
926
+ * Node types represented in the selected ranges.
884
927
  */
885
- export function get_selected_range_types(selected_ranges) {
886
- return new Set(selected_ranges.map(({ node }) => node?.type).filter(Boolean));
928
+ export function get_selected_range_types(selected_ranges: SelectedAttachment[]): Set<string> {
929
+ return new Set(
930
+ selected_ranges.map(({ node }) => node?.type).filter((type): type is string => Boolean(type))
931
+ );
887
932
  }
888
933
 
889
934
  /**
890
935
  * 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
936
  */
898
- export function count_references_excluding_deleted(schema, doc, target_node_id, nodes_to_delete) {
937
+ export function count_references_excluding_deleted(
938
+ schema: DocumentSchema,
939
+ doc: Document,
940
+ target_node_id: NodeId,
941
+ nodes_to_delete: Record<NodeId, boolean>
942
+ ): number {
899
943
  let count = 0;
900
944
 
901
945
  for (const node of Object.values(doc.nodes)) {
@@ -907,7 +951,7 @@ export function count_references_excluding_deleted(schema, doc, target_node_id,
907
951
  const prop_type = property_type(schema, node.type, property);
908
952
 
909
953
  if (prop_type === 'node_array') {
910
- count += value.nodes.filter((id) => id === target_node_id).length;
954
+ count += value.nodes.filter((id: NodeId) => id === target_node_id).length;
911
955
  } else if (prop_type === 'node' && value === target_node_id) {
912
956
  count += 1;
913
957
  }
@@ -929,12 +973,12 @@ export function count_references_excluding_deleted(schema, doc, target_node_id,
929
973
  * Covers the same reference kinds as count_references_excluding_deleted:
930
974
  * a `node` property, `node_array` .nodes ids, and the mark/annotation
931
975
  * 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
976
  */
937
- export function visit_node_references(schema, node, visit) {
977
+ export function visit_node_references(
978
+ schema: DocumentSchema,
979
+ node: DocumentNode,
980
+ visit: (referenced_id: NodeId) => void
981
+ ): void {
938
982
  const node_schema = schema[node.type];
939
983
  if (!node_schema) return;
940
984
 
@@ -961,14 +1005,9 @@ export function visit_node_references(schema, node, visit) {
961
1005
  * Builds reference counts for every referenced node in one full-document
962
1006
  * scan. Used by cascade deletion so the fixpoint can decrement counts
963
1007
  * 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
1008
  */
969
- export function build_reference_counts(schema, doc) {
970
- /** @type {Map<NodeId, number>} */
971
- const counts = new Map();
1009
+ export function build_reference_counts(schema: DocumentSchema, doc: Document): Map<NodeId, number> {
1010
+ const counts: Map<NodeId, number> = new Map();
972
1011
  for (const node of Object.values(doc.nodes)) {
973
1012
  visit_node_references(schema, node, (id) => {
974
1013
  counts.set(id, (counts.get(id) || 0) + 1);
@@ -979,15 +1018,14 @@ export function build_reference_counts(schema, doc) {
979
1018
 
980
1019
  /**
981
1020
  * 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
1021
  */
988
- export function get_referencing_node_ids(schema, doc, target_node_ids) {
1022
+ export function get_referencing_node_ids(
1023
+ schema: DocumentSchema,
1024
+ doc: Document,
1025
+ target_node_ids: Iterable<NodeId>
1026
+ ): NodeId[] {
989
1027
  const target_ids = new Set(target_node_ids);
990
- const referencing_node_ids = new Set();
1028
+ const referencing_node_ids: Set<NodeId> = new Set();
991
1029
  if (target_ids.size === 0) return [];
992
1030
 
993
1031
  for (const node of Object.values(doc.nodes)) {
@@ -997,7 +1035,7 @@ export function get_referencing_node_ids(schema, doc, target_node_ids) {
997
1035
  const prop_type = property_type(schema, node.type, property);
998
1036
 
999
1037
  if (prop_type === 'node_array') {
1000
- if (value.nodes.some((id) => target_ids.has(id))) {
1038
+ if (value.nodes.some((id: NodeId) => target_ids.has(id))) {
1001
1039
  referencing_node_ids.add(node.id);
1002
1040
  }
1003
1041
  } else if (prop_type === 'node' && target_ids.has(value)) {
@@ -1007,9 +1045,7 @@ export function get_referencing_node_ids(schema, doc, target_node_ids) {
1007
1045
  if (
1008
1046
  (prop_type === 'text' || prop_type === 'node_array') &&
1009
1047
  value &&
1010
- [...value.marks, ...value.annotations].some((range) =>
1011
- target_ids.has(range.node_id)
1012
- )
1048
+ [...value.marks, ...value.annotations].some((range) => target_ids.has(range.node_id))
1013
1049
  ) {
1014
1050
  referencing_node_ids.add(node.id);
1015
1051
  }
@@ -1023,11 +1059,16 @@ export function get_referencing_node_ids(schema, doc, target_node_ids) {
1023
1059
  * Validates a selection against the current document state.
1024
1060
  * Works with any object that implements get() and inspect() (Session or Transaction).
1025
1061
  *
1026
- * @param {Selection} selection - The selection to validate
1027
- * @param {{ get: Function, inspect: Function }} session_or_transaction - A Session or Transaction instance
1028
1062
  * @throws {Error} Throws if the selection is invalid
1029
1063
  */
1030
- export function validate_selection(selection, session_or_transaction) {
1064
+ export function validate_selection(
1065
+ selection: Selection | null | undefined,
1066
+ session_or_transaction: {
1067
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1068
+ get: (path: DocumentPath) => any;
1069
+ inspect: (path: DocumentPath) => Inspection;
1070
+ }
1071
+ ): void {
1031
1072
  if (!selection) return;
1032
1073
 
1033
1074
  const selection_type = selection.type;