svedit 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -24
- package/dist/{AnnotatedStringProperty.svelte → AnnotatedTextProperty.svelte} +28 -40
- package/dist/AnnotatedTextProperty.svelte.d.ts +7 -0
- package/dist/AnnotatedTextProperty.svelte.d.ts.map +1 -0
- package/dist/Document.svelte.d.ts +56 -17
- package/dist/Document.svelte.d.ts.map +1 -1
- package/dist/Document.svelte.js +251 -102
- package/dist/Svedit.svelte +322 -223
- package/dist/Svedit.svelte.d.ts.map +1 -1
- package/dist/Transaction.svelte.d.ts +16 -19
- package/dist/Transaction.svelte.d.ts.map +1 -1
- package/dist/Transaction.svelte.js +197 -104
- package/dist/commands.svelte.d.ts.map +1 -1
- package/dist/commands.svelte.js +18 -15
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/types.d.ts +66 -21
- package/dist/util.d.ts +17 -16
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +67 -34
- package/package.json +21 -21
- package/src/lib/{AnnotatedStringProperty.svelte → AnnotatedTextProperty.svelte} +28 -40
- package/src/lib/Document.svelte.js +251 -102
- package/src/lib/Svedit.svelte +322 -223
- package/src/lib/Transaction.svelte.js +197 -104
- package/src/lib/commands.svelte.js +18 -15
- package/src/lib/index.js +1 -1
- package/src/lib/types.d.ts +66 -21
- package/src/lib/util.js +67 -34
- package/dist/AnnotatedStringProperty.svelte.d.ts +0 -7
- package/dist/AnnotatedStringProperty.svelte.d.ts.map +0 -1
package/dist/Document.svelte.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Transaction from './Transaction.svelte.js';
|
|
2
|
-
import { char_slice } from './util.js';
|
|
2
|
+
import { char_slice, get_char_length, traverse } from './util.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* @import {
|
|
@@ -8,9 +8,10 @@ import { char_slice } from './util.js';
|
|
|
8
8
|
* Selection,
|
|
9
9
|
* Annotation,
|
|
10
10
|
* PrimitiveType,
|
|
11
|
-
* DocumentSchemaPrimitive,
|
|
12
11
|
* NodeProperty,
|
|
13
12
|
* NodeArrayProperty,
|
|
13
|
+
* NodeKind,
|
|
14
|
+
* NodeSchema,
|
|
14
15
|
* DocumentSchema,
|
|
15
16
|
* SerializedDocument
|
|
16
17
|
* } from './types.d.ts';
|
|
@@ -20,7 +21,7 @@ import { char_slice } from './util.js';
|
|
|
20
21
|
* Identity function — keeps schema at runtime & makes IDE infer types.
|
|
21
22
|
* Similar to your define_schema pattern but for document schemas.
|
|
22
23
|
*
|
|
23
|
-
* @template {Record<string,
|
|
24
|
+
* @template {Record<string, NodeSchema>} S
|
|
24
25
|
* @param {S} schema - The document schema to validate
|
|
25
26
|
* @returns {S} The same schema, but with type information preserved
|
|
26
27
|
*/
|
|
@@ -36,7 +37,7 @@ export function define_document_schema(schema) {
|
|
|
36
37
|
export function is_primitive_type(type) {
|
|
37
38
|
return [
|
|
38
39
|
'string', 'number', 'boolean', 'integer', 'datetime',
|
|
39
|
-
'
|
|
40
|
+
'annotated_text', 'string_array', 'number_array',
|
|
40
41
|
'boolean_array', 'integer_array'
|
|
41
42
|
].includes(type);
|
|
42
43
|
}
|
|
@@ -63,7 +64,7 @@ export function get_default_node_type(property_definition) {
|
|
|
63
64
|
export function validate_document_schema(document_schema) {
|
|
64
65
|
// Check that all referenced node types exist
|
|
65
66
|
for (const [node_type, node_schema] of Object.entries(document_schema)) {
|
|
66
|
-
for (const [prop_name, prop_def] of Object.entries(node_schema)) {
|
|
67
|
+
for (const [prop_name, prop_def] of Object.entries(node_schema.properties)) {
|
|
67
68
|
if (prop_def.type === 'node' || prop_def.type === 'node_array') {
|
|
68
69
|
const missing_types = prop_def.node_types.filter(ref_type => !(ref_type in document_schema));
|
|
69
70
|
if (missing_types.length > 0) {
|
|
@@ -92,11 +93,11 @@ function validate_primitive_value(type, value) {
|
|
|
92
93
|
return Number.isInteger(value);
|
|
93
94
|
case 'datetime':
|
|
94
95
|
return typeof value === 'string' && !isNaN(Date.parse(value));
|
|
95
|
-
case '
|
|
96
|
-
return
|
|
97
|
-
value
|
|
98
|
-
typeof value
|
|
99
|
-
Array.isArray(value
|
|
96
|
+
case 'annotated_text':
|
|
97
|
+
return typeof value === 'object' &&
|
|
98
|
+
value !== null &&
|
|
99
|
+
typeof value.text === 'string' &&
|
|
100
|
+
Array.isArray(value.annotations);
|
|
100
101
|
case 'string_array':
|
|
101
102
|
return Array.isArray(value) && value.every(v => typeof v === 'string');
|
|
102
103
|
case 'number_array':
|
|
@@ -136,7 +137,7 @@ function validate_node(node, schema, all_nodes = {}) {
|
|
|
136
137
|
|
|
137
138
|
const node_schema = schema[node.type];
|
|
138
139
|
|
|
139
|
-
for (const [prop_name, prop_def] of Object.entries(node_schema)) {
|
|
140
|
+
for (const [prop_name, prop_def] of Object.entries(node_schema.properties)) {
|
|
140
141
|
const value = node[prop_name];
|
|
141
142
|
|
|
142
143
|
// Check primitive types
|
|
@@ -177,7 +178,7 @@ function validate_node(node, schema, all_nodes = {}) {
|
|
|
177
178
|
|
|
178
179
|
export default class Document {
|
|
179
180
|
/** @type {Selection | undefined} */
|
|
180
|
-
selection = $state();
|
|
181
|
+
#selection = $state();
|
|
181
182
|
config = $state();
|
|
182
183
|
document_id = $state();
|
|
183
184
|
nodes = $state();
|
|
@@ -190,6 +191,86 @@ export default class Document {
|
|
|
190
191
|
|
|
191
192
|
// Reactive variable for selected node
|
|
192
193
|
selected_node = $derived(this.get_selected_node());
|
|
194
|
+
layout_node = $derived(this.get_layout_node());
|
|
195
|
+
available_annotation_types = $derived(this.get_available_annotation_types());
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Gets the current selection
|
|
199
|
+
* @returns {Selection | undefined}
|
|
200
|
+
*/
|
|
201
|
+
get selection() {
|
|
202
|
+
return this.#selection;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Sets the selection with validation
|
|
207
|
+
* @param {Selection | undefined} value - The new selection
|
|
208
|
+
* @throws {Error} Throws if the selection is invalid
|
|
209
|
+
*/
|
|
210
|
+
set selection(value) {
|
|
211
|
+
// if (value !== undefined) {
|
|
212
|
+
// this._validate_selection(value);
|
|
213
|
+
// }
|
|
214
|
+
this.#selection = value;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Validates that a selection is within bounds and refers to valid paths.
|
|
219
|
+
*
|
|
220
|
+
* @param {Selection} selection - The selection to validate
|
|
221
|
+
* @throws {Error} Throws if the selection is invalid
|
|
222
|
+
* @private
|
|
223
|
+
*/
|
|
224
|
+
_validate_selection(selection) {
|
|
225
|
+
if (!selection) return; // no selection is a valid selection
|
|
226
|
+
if (selection && !Array.isArray(selection.path)) {
|
|
227
|
+
throw new Error('Selection must have a valid path');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const selection_type = selection.type;
|
|
231
|
+
if (selection_type === 'node') {
|
|
232
|
+
// For node selections, path should point to a node_array
|
|
233
|
+
if (this.inspect(selection.path).type !== 'node_array') {
|
|
234
|
+
throw new Error(`Node selection path does not point to a node array: ${selection.path.join('.')}`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const node_array = this.get(selection.path);
|
|
238
|
+
|
|
239
|
+
// Validate anchor_offset and focus_offset are within bounds
|
|
240
|
+
const max_offset = node_array.length;
|
|
241
|
+
|
|
242
|
+
if (selection.anchor_offset < 0 || selection.anchor_offset > max_offset) {
|
|
243
|
+
throw new Error(`Node selection anchor_offset ${selection.anchor_offset} is out of bounds (0-${max_offset})`);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (selection.focus_offset < 0 || selection.focus_offset > max_offset) {
|
|
247
|
+
throw new Error(`Node selection focus_offset ${selection.focus_offset} is out of bounds (0-${max_offset})`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
} else if (selection_type === 'text') {
|
|
251
|
+
// For text selections, path should point to an annotated_text property
|
|
252
|
+
const annotated_text = this.get(selection.path);
|
|
253
|
+
|
|
254
|
+
// Validate anchor_offset and focus_offset are within text bounds
|
|
255
|
+
const char_length = get_char_length(annotated_text.text);
|
|
256
|
+
|
|
257
|
+
if (selection.anchor_offset < 0 || selection.anchor_offset > char_length) {
|
|
258
|
+
throw new Error(`Text selection anchor_offset ${selection.anchor_offset} is out of bounds (0-${char_length})`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (selection.focus_offset < 0 || selection.focus_offset > char_length) {
|
|
262
|
+
throw new Error(`Text selection focus_offset ${selection.focus_offset} is out of bounds (0-${char_length})`);
|
|
263
|
+
}
|
|
264
|
+
} else if (selection_type === 'property') {
|
|
265
|
+
// For property selections, just validate the path exists
|
|
266
|
+
if (!this.inspect(selection.path)) {
|
|
267
|
+
throw new Error(`Property selection path not found: ${selection.path.join('.')}`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
} else {
|
|
271
|
+
throw new Error(`Unknown selection type: ${selection_type}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
193
274
|
|
|
194
275
|
/**
|
|
195
276
|
* @param {DocumentSchema} schema - The document schema
|
|
@@ -203,7 +284,6 @@ export default class Document {
|
|
|
203
284
|
validate_document_schema(schema);
|
|
204
285
|
|
|
205
286
|
this.schema = schema;
|
|
206
|
-
this.selection = selection;
|
|
207
287
|
this.config = config;
|
|
208
288
|
this.nodes = {};
|
|
209
289
|
|
|
@@ -216,6 +296,9 @@ export default class Document {
|
|
|
216
296
|
|
|
217
297
|
// The last element in the serialized_doc is the document itself (the root node)
|
|
218
298
|
this.document_id = serialized_doc.at(-1)?.id;
|
|
299
|
+
|
|
300
|
+
// Set selection after nodes are initialized so validation can work properly
|
|
301
|
+
this.selection = selection;
|
|
219
302
|
}
|
|
220
303
|
|
|
221
304
|
generate_id() {
|
|
@@ -226,6 +309,13 @@ export default class Document {
|
|
|
226
309
|
}
|
|
227
310
|
}
|
|
228
311
|
|
|
312
|
+
get_available_annotation_types() {
|
|
313
|
+
if (this.selection?.type !== 'text') return [];
|
|
314
|
+
const path = this.selection.path;
|
|
315
|
+
const property_definition = this.inspect(path);
|
|
316
|
+
return property_definition.node_types || [];
|
|
317
|
+
}
|
|
318
|
+
|
|
229
319
|
// Helper function to get the currently selected node
|
|
230
320
|
get_selected_node() {
|
|
231
321
|
if (!this.selection) return null;
|
|
@@ -240,13 +330,37 @@ export default class Document {
|
|
|
240
330
|
return node_id ? this.get(node_id) : null;
|
|
241
331
|
} else {
|
|
242
332
|
// we are assuming we are either in a text or property (=custom) selection
|
|
243
|
-
const
|
|
244
|
-
if (!
|
|
245
|
-
const owner_node = this.get(
|
|
333
|
+
const owner_node_path = this.selection?.path?.slice(0, -1);
|
|
334
|
+
if (!owner_node_path) return null;
|
|
335
|
+
const owner_node = this.get(owner_node_path);
|
|
246
336
|
return owner_node;
|
|
247
337
|
}
|
|
248
338
|
}
|
|
249
339
|
|
|
340
|
+
// NOTE: This code is a bit whacky, but works for now.
|
|
341
|
+
// TODO: Refactor as we settle on a final API
|
|
342
|
+
get_layout_node() {
|
|
343
|
+
if (!this.selected_node) return null;
|
|
344
|
+
|
|
345
|
+
// The selected node already is a layout node
|
|
346
|
+
if (this.selected_node.layout) {
|
|
347
|
+
return this.selected_node;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// We resolve the parent node if available, and return it if it's a layout node.
|
|
351
|
+
// NOTE: We only support one level atm, we may want to implement this recursively
|
|
352
|
+
if (this.selection.type === 'node') {
|
|
353
|
+
const parent_node = this.get(this.selection.path.slice(0, -1));
|
|
354
|
+
return parent_node.layout ? parent_node : null;
|
|
355
|
+
} else {
|
|
356
|
+
// We are either in a text or property (=custom) selection
|
|
357
|
+
const parent_node_path = this.selection?.path?.slice(0, -3);
|
|
358
|
+
if (!parent_node_path) return null;
|
|
359
|
+
const parent_node = this.get(parent_node_path);
|
|
360
|
+
return parent_node.layout ? parent_node : null;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
250
364
|
// Internal unsafe function: Never call this directly
|
|
251
365
|
_apply_op(op) {
|
|
252
366
|
const [type, ...args] = op;
|
|
@@ -352,8 +466,8 @@ export default class Document {
|
|
|
352
466
|
* doc.get(['page_1', 'body', 3, 'list_items', 0]) // => { type: 'list_item', id: 'list_item_1', ... }
|
|
353
467
|
*
|
|
354
468
|
* @example
|
|
355
|
-
* // Get an annotated
|
|
356
|
-
* doc.get(['page_1', 'cover', 'title']) // =>
|
|
469
|
+
* // Get an annotated text property
|
|
470
|
+
* doc.get(['page_1', 'cover', 'title']) // => {text: 'Hello world', annotations: []}
|
|
357
471
|
*/
|
|
358
472
|
get(path) {
|
|
359
473
|
if (typeof path === 'string') {
|
|
@@ -372,6 +486,9 @@ export default class Document {
|
|
|
372
486
|
if (this.property_type(val.type, path_segment) === 'node_array') {
|
|
373
487
|
val = val[path_segment]; // e.g. for the page body ['list_1', 'paragraph_1']
|
|
374
488
|
val_type = 'node_array';
|
|
489
|
+
} else if (this.property_type(val.type, path_segment) === 'annotated_text') {
|
|
490
|
+
val = val[path_segment];
|
|
491
|
+
val_type = 'annotated_text';
|
|
375
492
|
} else if (this.property_type(val.type, path_segment) === 'node') {
|
|
376
493
|
val = this.nodes[val[path_segment]];
|
|
377
494
|
val_type = 'node';
|
|
@@ -379,7 +496,7 @@ export default class Document {
|
|
|
379
496
|
val = val[path_segment];
|
|
380
497
|
val_type = 'value_array';
|
|
381
498
|
} else {
|
|
382
|
-
// Resolve value properties
|
|
499
|
+
// Resolve value properties such as string, integer, datetime
|
|
383
500
|
val = val[path_segment];
|
|
384
501
|
val_type = 'value';
|
|
385
502
|
}
|
|
@@ -390,6 +507,32 @@ export default class Document {
|
|
|
390
507
|
} else if (val_type === 'value_array') {
|
|
391
508
|
val = val[path_segment];
|
|
392
509
|
val_type = 'value';
|
|
510
|
+
} else if (val_type === 'annotated_text') {
|
|
511
|
+
if (path_segment === 'text') {
|
|
512
|
+
val = val.text;
|
|
513
|
+
val_type = 'value';
|
|
514
|
+
} else if (path_segment === 'annotations') {
|
|
515
|
+
val = val.annotations;
|
|
516
|
+
val_type = 'annotation_array';
|
|
517
|
+
} else {
|
|
518
|
+
throw new Error(`Invalid path segment "${path_segment}" for annotated_text. Use "text" or "annotations".`);
|
|
519
|
+
}
|
|
520
|
+
} else if (val_type === 'annotation_array') {
|
|
521
|
+
val = val[path_segment];
|
|
522
|
+
val_type = 'annotation';
|
|
523
|
+
} else if (val_type === 'annotation') {
|
|
524
|
+
if (path_segment === 'node_id') {
|
|
525
|
+
val = this.nodes[val.node_id];
|
|
526
|
+
val_type = 'node';
|
|
527
|
+
} else if (path_segment === 'start_offset') {
|
|
528
|
+
val = val.start_offset;
|
|
529
|
+
val_type = 'value';
|
|
530
|
+
} else if (path_segment === 'end_offset') {
|
|
531
|
+
val = val.end_offset;
|
|
532
|
+
val_type = 'value';
|
|
533
|
+
} else {
|
|
534
|
+
throw new Error(`Invalid path segment "${path_segment}" for annotation. Use "start_offset", "end_offset", or "node_id".`);
|
|
535
|
+
}
|
|
393
536
|
}
|
|
394
537
|
}
|
|
395
538
|
return val;
|
|
@@ -430,7 +573,7 @@ export default class Document {
|
|
|
430
573
|
kind: 'property',
|
|
431
574
|
name: property_name,
|
|
432
575
|
// Merge property spec from schema
|
|
433
|
-
...this.schema[parent.type][property_name]
|
|
576
|
+
...this.schema[parent.type].properties[property_name]
|
|
434
577
|
};
|
|
435
578
|
} else {
|
|
436
579
|
// Parent is a property (or we are at the root), so we are dealing with a node.
|
|
@@ -445,17 +588,54 @@ export default class Document {
|
|
|
445
588
|
}
|
|
446
589
|
|
|
447
590
|
/**
|
|
448
|
-
* Determines the kind of a node ('
|
|
449
|
-
*
|
|
591
|
+
* Determines the kind of a node ('block' for structured blocks, 'text' for pure
|
|
592
|
+
* text nodes or 'annotation' for annotation nodes.
|
|
450
593
|
* @param {any} node
|
|
451
|
-
* @returns {
|
|
594
|
+
* @returns {NodeKind}
|
|
452
595
|
*/
|
|
453
596
|
kind(node) {
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
597
|
+
return this.schema[node.type].kind;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Determines whether a node type can be inserted at a given selection.
|
|
602
|
+
* @param {string} node_type - The type of node to insert.
|
|
603
|
+
* @param {Selection} [selection] - The selection at which to insert the node.
|
|
604
|
+
* @returns {boolean} True if the node type can be inserted, false otherwise.
|
|
605
|
+
*/
|
|
606
|
+
can_insert(node_type, selection = this.selection) {
|
|
607
|
+
if (selection?.type === 'node') {
|
|
608
|
+
const property_definition = this.inspect(selection.path);
|
|
609
|
+
if (property_definition.node_types.includes(node_type)) {
|
|
610
|
+
return true;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// No insert position found yet, and root not reached, try one level up if possible
|
|
615
|
+
let next_node_insert_cursor = this.get_next_node_insert_cursor(selection);
|
|
616
|
+
if (!next_node_insert_cursor) return false;
|
|
617
|
+
return this.can_insert(node_type, next_node_insert_cursor);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Compute next possible insert position from a given selection
|
|
622
|
+
*
|
|
623
|
+
* @param {Selection} [selection] - Reference selection
|
|
624
|
+
* @returns {Selection} True if the paste operation was successful, false otherwise
|
|
625
|
+
*/
|
|
626
|
+
get_next_node_insert_cursor(selection = this.selection) {
|
|
627
|
+
// There's no parent path to insert into
|
|
628
|
+
if (!selection || selection.path.length <= 2) {
|
|
629
|
+
return null;
|
|
458
630
|
}
|
|
631
|
+
|
|
632
|
+
const node_offset = parseInt(String(selection.path.at(-2)), 10) + 1;
|
|
633
|
+
return {
|
|
634
|
+
type: 'node',
|
|
635
|
+
path: selection.path.slice(0, -2),
|
|
636
|
+
anchor_offset: node_offset,
|
|
637
|
+
focus_offset: node_offset,
|
|
638
|
+
};
|
|
459
639
|
}
|
|
460
640
|
|
|
461
641
|
/**
|
|
@@ -469,30 +649,60 @@ export default class Document {
|
|
|
469
649
|
if (this.selection?.type !== 'text') return null;
|
|
470
650
|
|
|
471
651
|
const { start, end } = this.get_selection_range();
|
|
472
|
-
const
|
|
473
|
-
const annotations =
|
|
652
|
+
const annotated_text = this.get(this.selection.path);
|
|
653
|
+
const annotations = annotated_text.annotations;
|
|
474
654
|
|
|
475
|
-
const active_annotation = annotations.find((
|
|
476
|
-
(
|
|
477
|
-
(
|
|
478
|
-
(
|
|
655
|
+
const active_annotation = annotations.find(({start_offset, end_offset}) =>
|
|
656
|
+
(start_offset <= start && end_offset > start) ||
|
|
657
|
+
(start_offset < end && end_offset >= end) ||
|
|
658
|
+
(start_offset >= start && end_offset <= end)
|
|
479
659
|
) || null;
|
|
480
660
|
|
|
481
|
-
if (annotation_type) {
|
|
482
|
-
|
|
661
|
+
if (annotation_type && active_annotation) {
|
|
662
|
+
const annotation_node = this.get(active_annotation?.node_id);
|
|
663
|
+
return annotation_node?.type === annotation_type ? active_annotation : null;
|
|
483
664
|
} else {
|
|
484
665
|
return active_annotation;
|
|
485
666
|
}
|
|
486
667
|
}
|
|
487
668
|
|
|
669
|
+
get_selected_annotated_text() {
|
|
670
|
+
if (this.selection?.type !== 'text') return null;
|
|
671
|
+
|
|
672
|
+
const selection_start = Math.min(this.selection.anchor_offset, this.selection.focus_offset);
|
|
673
|
+
const selection_end = Math.max(this.selection.anchor_offset, this.selection.focus_offset);
|
|
674
|
+
const annotated_text = this.get(this.selection.path);
|
|
675
|
+
const text = char_slice(annotated_text.text, selection_start, selection_end);
|
|
676
|
+
const nodes = {};
|
|
677
|
+
const annotations = annotated_text.annotations.map(a => {
|
|
678
|
+
if (selection_start < a.end_offset && selection_end > a.start_offset) {
|
|
679
|
+
const sub_graph = this.traverse(a.node_id);
|
|
680
|
+
for (const node of sub_graph) {
|
|
681
|
+
if (!nodes[node.id]) {
|
|
682
|
+
nodes[node.id] = node;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
return {
|
|
686
|
+
start_offset: Math.max(a.start_offset - selection_start, 0),
|
|
687
|
+
end_offset: Math.min(a.end_offset - selection_start, selection_end - selection_start),
|
|
688
|
+
node_id: a.node_id
|
|
689
|
+
};
|
|
690
|
+
} else {
|
|
691
|
+
return null;
|
|
692
|
+
}
|
|
693
|
+
}).filter(Boolean);
|
|
694
|
+
|
|
695
|
+
return { text, annotations, nodes };
|
|
696
|
+
}
|
|
697
|
+
|
|
488
698
|
// TODO: think about ways how we can also turn a node selection into plain text.
|
|
489
699
|
get_selected_plain_text() {
|
|
490
700
|
if (this.selection?.type !== 'text') return null;
|
|
491
701
|
|
|
492
702
|
const start = Math.min(this.selection.anchor_offset, this.selection.focus_offset);
|
|
493
703
|
const end = Math.max(this.selection.anchor_offset, this.selection.focus_offset);
|
|
494
|
-
const
|
|
495
|
-
return char_slice(
|
|
704
|
+
const annotated_text = this.get(this.selection.path);
|
|
705
|
+
return char_slice(annotated_text.text, start, end);
|
|
496
706
|
}
|
|
497
707
|
|
|
498
708
|
get_selected_nodes() {
|
|
@@ -504,41 +714,6 @@ export default class Document {
|
|
|
504
714
|
return $state.snapshot(node_array.slice(start, end));
|
|
505
715
|
}
|
|
506
716
|
|
|
507
|
-
move_node_cursor(direction) {
|
|
508
|
-
if (this.selection?.type !== 'node') return;
|
|
509
|
-
const node_array = this.get(this.selection.path);
|
|
510
|
-
|
|
511
|
-
const { start, end } = this.get_selection_range();
|
|
512
|
-
|
|
513
|
-
if (this.selection.anchor_offset !== this.selection.focus_offset) {
|
|
514
|
-
// If selection is not collapsed, collapse it to the right or the left
|
|
515
|
-
if (direction === 'forward') {
|
|
516
|
-
this.selection.focus_offset = end;
|
|
517
|
-
this.selection.anchor_offset = end;
|
|
518
|
-
} else if (direction === 'backward') {
|
|
519
|
-
this.selection.focus_offset = start;
|
|
520
|
-
this.selection.anchor_offset = start;
|
|
521
|
-
}
|
|
522
|
-
} else if (direction === 'forward' && end < node_array.length) {
|
|
523
|
-
this.selection.focus_offset = end + 1;
|
|
524
|
-
this.selection.anchor_offset = end + 1;
|
|
525
|
-
} else if (direction === 'backward' && start > 0) {
|
|
526
|
-
this.selection.focus_offset = start - 1;
|
|
527
|
-
this.selection.anchor_offset = start - 1;
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
expand_node_selection(direction) {
|
|
532
|
-
if (this.selection.type !== 'node') return;
|
|
533
|
-
const node_array = this.get(this.selection.path);
|
|
534
|
-
|
|
535
|
-
if (direction === 'forward') {
|
|
536
|
-
this.selection.focus_offset = Math.min(this.selection.focus_offset + 1, node_array.length);
|
|
537
|
-
} else if (direction === 'backward') {
|
|
538
|
-
this.selection.focus_offset = Math.max(this.selection.focus_offset - 1, 0);
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
|
|
542
717
|
select_parent() {
|
|
543
718
|
if (!this.selection) return;
|
|
544
719
|
if (['text', 'property'].includes(this.selection?.type)) {
|
|
@@ -595,33 +770,7 @@ export default class Document {
|
|
|
595
770
|
// IMPORTANT: Leaf nodes must go first, branches second and the root node (entry point) last (depth-first traversal)
|
|
596
771
|
// NOTE: Nodes that are not reachable from the entry point node will not be included in the serialization
|
|
597
772
|
traverse(node_id) {
|
|
598
|
-
|
|
599
|
-
const visited = {};
|
|
600
|
-
const visit = (node) => {
|
|
601
|
-
if (!node || visited[node.id]) {
|
|
602
|
-
return;
|
|
603
|
-
}
|
|
604
|
-
visited[node.id] = true;
|
|
605
|
-
for (const [, value] of Object.entries(node)) {
|
|
606
|
-
// TODO: Use schema inspection and do this only for properties of type `node_array`
|
|
607
|
-
if (Array.isArray(value)) {
|
|
608
|
-
for (const v of value) {
|
|
609
|
-
if (typeof v === 'string') {
|
|
610
|
-
visit($state.snapshot(this.get(v)));
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
} else if (typeof value === 'string') {
|
|
614
|
-
// TODO: Use schema inspection and do this only for properties of type `ref`
|
|
615
|
-
visit($state.snapshot(this.get(value)));
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
// Finally add the node to the result.
|
|
619
|
-
// We use a deep clone, so we make sure nothing of the original document is referenced.
|
|
620
|
-
json.push(structuredClone(node));
|
|
621
|
-
}
|
|
622
|
-
// Start with the root node (document_id)
|
|
623
|
-
visit($state.snapshot(this.get(node_id)));
|
|
624
|
-
return json;
|
|
773
|
+
return traverse(node_id, this.schema, $state.snapshot(this.nodes));
|
|
625
774
|
}
|
|
626
775
|
|
|
627
776
|
/**
|
|
@@ -633,7 +782,7 @@ export default class Document {
|
|
|
633
782
|
}
|
|
634
783
|
|
|
635
784
|
// property_type('page', 'body') => 'node_array'
|
|
636
|
-
// property_type('paragraph', 'content') => '
|
|
785
|
+
// property_type('paragraph', 'content') => 'annotated_text'
|
|
637
786
|
property_type(type, property) {
|
|
638
787
|
if (typeof type !== 'string') throw new Error(`Invalid type ${type} provided`);
|
|
639
788
|
if (typeof property !== 'string') throw new Error(`Invalid property ${property} provided`);
|
|
@@ -642,9 +791,9 @@ export default class Document {
|
|
|
642
791
|
if (property === 'id') return 'string';
|
|
643
792
|
|
|
644
793
|
if (!this.schema[type]) throw new Error(`Type ${type} not found in schema`);
|
|
645
|
-
if (!this.schema[type][property]) throw new Error(`Property ${property} not found in type ${type}`);
|
|
794
|
+
if (!this.schema[type].properties[property]) throw new Error(`Property ${property} not found in type ${type}`);
|
|
646
795
|
|
|
647
|
-
return this.schema[type][property].type;
|
|
796
|
+
return this.schema[type].properties[property].type;
|
|
648
797
|
}
|
|
649
798
|
|
|
650
799
|
// Count how many times a node is referenced in the document
|