svedit 0.2.0 → 0.4.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 +41 -21
- package/dist/AnnotatedTextProperty.svelte +117 -0
- package/dist/AnnotatedTextProperty.svelte.d.ts +7 -0
- package/dist/AnnotatedTextProperty.svelte.d.ts.map +1 -0
- package/dist/CustomProperty.svelte +20 -12
- package/dist/CustomProperty.svelte.d.ts +1 -2
- package/dist/CustomProperty.svelte.d.ts.map +1 -1
- package/dist/Document.svelte.d.ts +40 -17
- package/dist/Document.svelte.d.ts.map +1 -1
- package/dist/Document.svelte.js +192 -77
- package/dist/Node.svelte +18 -14
- package/dist/Node.svelte.d.ts.map +1 -1
- package/dist/NodeArrayProperty.svelte +1 -1
- package/dist/Svedit.svelte +628 -109
- package/dist/Svedit.svelte.d.ts +1 -1
- package/dist/Svedit.svelte.d.ts.map +1 -1
- package/dist/Transaction.svelte.d.ts +4 -7
- package/dist/Transaction.svelte.d.ts.map +1 -1
- package/dist/Transaction.svelte.js +74 -81
- package/dist/commands.svelte.d.ts.map +1 -1
- package/dist/commands.svelte.js +20 -10
- 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 +125 -3
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +191 -34
- package/package.json +1 -1
- package/src/lib/AnnotatedTextProperty.svelte +117 -0
- package/src/lib/CustomProperty.svelte +20 -12
- package/src/lib/Document.svelte.js +192 -77
- package/src/lib/Node.svelte +18 -14
- package/src/lib/NodeArrayProperty.svelte +1 -1
- package/src/lib/Svedit.svelte +628 -109
- package/src/lib/Transaction.svelte.js +74 -81
- package/src/lib/commands.svelte.js +20 -10
- package/src/lib/index.js +1 -1
- package/src/lib/types.d.ts +66 -21
- package/src/lib/util.js +191 -34
- package/dist/AnnotatedStringProperty.svelte +0 -126
- package/dist/AnnotatedStringProperty.svelte.d.ts +0 -7
- package/dist/AnnotatedStringProperty.svelte.d.ts.map +0 -1
- package/src/lib/AnnotatedStringProperty.svelte +0 -126
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Transaction from './Transaction.svelte.js';
|
|
2
|
+
import { char_slice, get_char_length } from './util.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* @import {
|
|
@@ -10,6 +11,10 @@ import Transaction from './Transaction.svelte.js';
|
|
|
10
11
|
* DocumentSchemaPrimitive,
|
|
11
12
|
* NodeProperty,
|
|
12
13
|
* NodeArrayProperty,
|
|
14
|
+
* NodeKind,
|
|
15
|
+
* NodeSchema,
|
|
16
|
+
* TextNodeSchema,
|
|
17
|
+
* NonTextNodeSchema,
|
|
13
18
|
* DocumentSchema,
|
|
14
19
|
* SerializedDocument
|
|
15
20
|
* } from './types.d.ts';
|
|
@@ -19,7 +24,7 @@ import Transaction from './Transaction.svelte.js';
|
|
|
19
24
|
* Identity function — keeps schema at runtime & makes IDE infer types.
|
|
20
25
|
* Similar to your define_schema pattern but for document schemas.
|
|
21
26
|
*
|
|
22
|
-
* @template {Record<string,
|
|
27
|
+
* @template {Record<string, NodeSchema>} S
|
|
23
28
|
* @param {S} schema - The document schema to validate
|
|
24
29
|
* @returns {S} The same schema, but with type information preserved
|
|
25
30
|
*/
|
|
@@ -35,7 +40,7 @@ export function define_document_schema(schema) {
|
|
|
35
40
|
export function is_primitive_type(type) {
|
|
36
41
|
return [
|
|
37
42
|
'string', 'number', 'boolean', 'integer', 'datetime',
|
|
38
|
-
'
|
|
43
|
+
'annotated_text', 'string_array', 'number_array',
|
|
39
44
|
'boolean_array', 'integer_array'
|
|
40
45
|
].includes(type);
|
|
41
46
|
}
|
|
@@ -62,7 +67,7 @@ export function get_default_node_type(property_definition) {
|
|
|
62
67
|
export function validate_document_schema(document_schema) {
|
|
63
68
|
// Check that all referenced node types exist
|
|
64
69
|
for (const [node_type, node_schema] of Object.entries(document_schema)) {
|
|
65
|
-
for (const [prop_name, prop_def] of Object.entries(node_schema)) {
|
|
70
|
+
for (const [prop_name, prop_def] of Object.entries(node_schema.properties)) {
|
|
66
71
|
if (prop_def.type === 'node' || prop_def.type === 'node_array') {
|
|
67
72
|
const missing_types = prop_def.node_types.filter(ref_type => !(ref_type in document_schema));
|
|
68
73
|
if (missing_types.length > 0) {
|
|
@@ -91,11 +96,11 @@ function validate_primitive_value(type, value) {
|
|
|
91
96
|
return Number.isInteger(value);
|
|
92
97
|
case 'datetime':
|
|
93
98
|
return typeof value === 'string' && !isNaN(Date.parse(value));
|
|
94
|
-
case '
|
|
95
|
-
return
|
|
96
|
-
value
|
|
97
|
-
typeof value
|
|
98
|
-
Array.isArray(value
|
|
99
|
+
case 'annotated_text':
|
|
100
|
+
return typeof value === 'object' &&
|
|
101
|
+
value !== null &&
|
|
102
|
+
typeof value.text === 'string' &&
|
|
103
|
+
Array.isArray(value.annotations);
|
|
99
104
|
case 'string_array':
|
|
100
105
|
return Array.isArray(value) && value.every(v => typeof v === 'string');
|
|
101
106
|
case 'number_array':
|
|
@@ -135,7 +140,7 @@ function validate_node(node, schema, all_nodes = {}) {
|
|
|
135
140
|
|
|
136
141
|
const node_schema = schema[node.type];
|
|
137
142
|
|
|
138
|
-
for (const [prop_name, prop_def] of Object.entries(node_schema)) {
|
|
143
|
+
for (const [prop_name, prop_def] of Object.entries(node_schema.properties)) {
|
|
139
144
|
const value = node[prop_name];
|
|
140
145
|
|
|
141
146
|
// Check primitive types
|
|
@@ -176,7 +181,7 @@ function validate_node(node, schema, all_nodes = {}) {
|
|
|
176
181
|
|
|
177
182
|
export default class Document {
|
|
178
183
|
/** @type {Selection | undefined} */
|
|
179
|
-
selection = $state();
|
|
184
|
+
#selection = $state();
|
|
180
185
|
config = $state();
|
|
181
186
|
document_id = $state();
|
|
182
187
|
nodes = $state();
|
|
@@ -189,6 +194,86 @@ export default class Document {
|
|
|
189
194
|
|
|
190
195
|
// Reactive variable for selected node
|
|
191
196
|
selected_node = $derived(this.get_selected_node());
|
|
197
|
+
layout_node = $derived(this.get_layout_node());
|
|
198
|
+
available_annotation_types = $derived(this.get_available_annotation_types());
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Gets the current selection
|
|
202
|
+
* @returns {Selection | undefined}
|
|
203
|
+
*/
|
|
204
|
+
get selection() {
|
|
205
|
+
return this.#selection;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Sets the selection with validation
|
|
210
|
+
* @param {Selection | undefined} value - The new selection
|
|
211
|
+
* @throws {Error} Throws if the selection is invalid
|
|
212
|
+
*/
|
|
213
|
+
set selection(value) {
|
|
214
|
+
// if (value !== undefined) {
|
|
215
|
+
// this._validate_selection(value);
|
|
216
|
+
// }
|
|
217
|
+
this.#selection = value;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Validates that a selection is within bounds and refers to valid paths.
|
|
222
|
+
*
|
|
223
|
+
* @param {Selection} selection - The selection to validate
|
|
224
|
+
* @throws {Error} Throws if the selection is invalid
|
|
225
|
+
* @private
|
|
226
|
+
*/
|
|
227
|
+
_validate_selection(selection) {
|
|
228
|
+
if (!selection) return; // no selection is a valid selection
|
|
229
|
+
if (selection && !Array.isArray(selection.path)) {
|
|
230
|
+
throw new Error('Selection must have a valid path');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const selection_type = selection.type;
|
|
234
|
+
if (selection_type === 'node') {
|
|
235
|
+
// For node selections, path should point to a node_array
|
|
236
|
+
if (this.inspect(selection.path).type !== 'node_array') {
|
|
237
|
+
throw new Error(`Node selection path does not point to a node array: ${selection.path.join('.')}`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const node_array = this.get(selection.path);
|
|
241
|
+
|
|
242
|
+
// Validate anchor_offset and focus_offset are within bounds
|
|
243
|
+
const max_offset = node_array.length;
|
|
244
|
+
|
|
245
|
+
if (selection.anchor_offset < 0 || selection.anchor_offset > max_offset) {
|
|
246
|
+
throw new Error(`Node selection anchor_offset ${selection.anchor_offset} is out of bounds (0-${max_offset})`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (selection.focus_offset < 0 || selection.focus_offset > max_offset) {
|
|
250
|
+
throw new Error(`Node selection focus_offset ${selection.focus_offset} is out of bounds (0-${max_offset})`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
} else if (selection_type === 'text') {
|
|
254
|
+
// For text selections, path should point to an annotated_text property
|
|
255
|
+
const annotated_text = this.get(selection.path);
|
|
256
|
+
|
|
257
|
+
// Validate anchor_offset and focus_offset are within text bounds
|
|
258
|
+
const char_length = get_char_length(annotated_text.text);
|
|
259
|
+
|
|
260
|
+
if (selection.anchor_offset < 0 || selection.anchor_offset > char_length) {
|
|
261
|
+
throw new Error(`Text selection anchor_offset ${selection.anchor_offset} is out of bounds (0-${char_length})`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (selection.focus_offset < 0 || selection.focus_offset > char_length) {
|
|
265
|
+
throw new Error(`Text selection focus_offset ${selection.focus_offset} is out of bounds (0-${char_length})`);
|
|
266
|
+
}
|
|
267
|
+
} else if (selection_type === 'property') {
|
|
268
|
+
// For property selections, just validate the path exists
|
|
269
|
+
if (!this.inspect(selection.path)) {
|
|
270
|
+
throw new Error(`Property selection path not found: ${selection.path.join('.')}`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
} else {
|
|
274
|
+
throw new Error(`Unknown selection type: ${selection_type}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
192
277
|
|
|
193
278
|
/**
|
|
194
279
|
* @param {DocumentSchema} schema - The document schema
|
|
@@ -202,7 +287,6 @@ export default class Document {
|
|
|
202
287
|
validate_document_schema(schema);
|
|
203
288
|
|
|
204
289
|
this.schema = schema;
|
|
205
|
-
this.selection = selection;
|
|
206
290
|
this.config = config;
|
|
207
291
|
this.nodes = {};
|
|
208
292
|
|
|
@@ -215,6 +299,9 @@ export default class Document {
|
|
|
215
299
|
|
|
216
300
|
// The last element in the serialized_doc is the document itself (the root node)
|
|
217
301
|
this.document_id = serialized_doc.at(-1)?.id;
|
|
302
|
+
|
|
303
|
+
// Set selection after nodes are initialized so validation can work properly
|
|
304
|
+
this.selection = selection;
|
|
218
305
|
}
|
|
219
306
|
|
|
220
307
|
generate_id() {
|
|
@@ -225,6 +312,13 @@ export default class Document {
|
|
|
225
312
|
}
|
|
226
313
|
}
|
|
227
314
|
|
|
315
|
+
get_available_annotation_types() {
|
|
316
|
+
if (this.selection?.type !== 'text') return [];
|
|
317
|
+
const path = this.selection.path;
|
|
318
|
+
const property_schema = this.inspect(path);
|
|
319
|
+
return property_schema.node_types || [];
|
|
320
|
+
}
|
|
321
|
+
|
|
228
322
|
// Helper function to get the currently selected node
|
|
229
323
|
get_selected_node() {
|
|
230
324
|
if (!this.selection) return null;
|
|
@@ -239,11 +333,37 @@ export default class Document {
|
|
|
239
333
|
return node_id ? this.get(node_id) : null;
|
|
240
334
|
} else {
|
|
241
335
|
// we are assuming we are either in a text or property (=custom) selection
|
|
242
|
-
const
|
|
336
|
+
const owner_node_path = this.selection?.path?.slice(0, -1);
|
|
337
|
+
if (!owner_node_path) return null;
|
|
338
|
+
const owner_node = this.get(owner_node_path);
|
|
243
339
|
return owner_node;
|
|
244
340
|
}
|
|
245
341
|
}
|
|
246
342
|
|
|
343
|
+
// NOTE: This code is a bit whacky, but works for now.
|
|
344
|
+
// TODO: Refactor as we settle on a final API
|
|
345
|
+
get_layout_node() {
|
|
346
|
+
if (!this.selected_node) return null;
|
|
347
|
+
|
|
348
|
+
// The selected node already is a layout node
|
|
349
|
+
if (this.selected_node.layout) {
|
|
350
|
+
return this.selected_node;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// We resolve the parent node if available, and return it if it's a layout node.
|
|
354
|
+
// NOTE: We only support one level atm, we may want to implement this recursively
|
|
355
|
+
if (this.selection.type === 'node') {
|
|
356
|
+
const parent_node = this.get(this.selection.path.slice(0, -1));
|
|
357
|
+
return parent_node.layout ? parent_node : null;
|
|
358
|
+
} else {
|
|
359
|
+
// We are either in a text or property (=custom) selection
|
|
360
|
+
const parent_node_path = this.selection?.path?.slice(0, -3);
|
|
361
|
+
if (!parent_node_path) return null;
|
|
362
|
+
const parent_node = this.get(parent_node_path);
|
|
363
|
+
return parent_node.layout ? parent_node : null;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
247
367
|
// Internal unsafe function: Never call this directly
|
|
248
368
|
_apply_op(op) {
|
|
249
369
|
const [type, ...args] = op;
|
|
@@ -349,8 +469,8 @@ export default class Document {
|
|
|
349
469
|
* doc.get(['page_1', 'body', 3, 'list_items', 0]) // => { type: 'list_item', id: 'list_item_1', ... }
|
|
350
470
|
*
|
|
351
471
|
* @example
|
|
352
|
-
* // Get an annotated
|
|
353
|
-
* doc.get(['page_1', 'cover', 'title']) // =>
|
|
472
|
+
* // Get an annotated text property
|
|
473
|
+
* doc.get(['page_1', 'cover', 'title']) // => {text: 'Hello world', annotations: []}
|
|
354
474
|
*/
|
|
355
475
|
get(path) {
|
|
356
476
|
if (typeof path === 'string') {
|
|
@@ -369,6 +489,9 @@ export default class Document {
|
|
|
369
489
|
if (this.property_type(val.type, path_segment) === 'node_array') {
|
|
370
490
|
val = val[path_segment]; // e.g. for the page body ['list_1', 'paragraph_1']
|
|
371
491
|
val_type = 'node_array';
|
|
492
|
+
} else if (this.property_type(val.type, path_segment) === 'annotated_text') {
|
|
493
|
+
val = val[path_segment];
|
|
494
|
+
val_type = 'annotated_text';
|
|
372
495
|
} else if (this.property_type(val.type, path_segment) === 'node') {
|
|
373
496
|
val = this.nodes[val[path_segment]];
|
|
374
497
|
val_type = 'node';
|
|
@@ -376,7 +499,7 @@ export default class Document {
|
|
|
376
499
|
val = val[path_segment];
|
|
377
500
|
val_type = 'value_array';
|
|
378
501
|
} else {
|
|
379
|
-
// Resolve value properties
|
|
502
|
+
// Resolve value properties such as string, integer, datetime
|
|
380
503
|
val = val[path_segment];
|
|
381
504
|
val_type = 'value';
|
|
382
505
|
}
|
|
@@ -387,6 +510,32 @@ export default class Document {
|
|
|
387
510
|
} else if (val_type === 'value_array') {
|
|
388
511
|
val = val[path_segment];
|
|
389
512
|
val_type = 'value';
|
|
513
|
+
} else if (val_type === 'annotated_text') {
|
|
514
|
+
if (path_segment === 'text') {
|
|
515
|
+
val = val.text;
|
|
516
|
+
val_type = 'value';
|
|
517
|
+
} else if (path_segment === 'annotations') {
|
|
518
|
+
val = val.annotations;
|
|
519
|
+
val_type = 'annotation_array';
|
|
520
|
+
} else {
|
|
521
|
+
throw new Error(`Invalid path segment "${path_segment}" for annotated_text. Use "text" or "annotations".`);
|
|
522
|
+
}
|
|
523
|
+
} else if (val_type === 'annotation_array') {
|
|
524
|
+
val = val[path_segment];
|
|
525
|
+
val_type = 'annotation';
|
|
526
|
+
} else if (val_type === 'annotation') {
|
|
527
|
+
if (path_segment === 'node_id') {
|
|
528
|
+
val = this.nodes[val.node_id];
|
|
529
|
+
val_type = 'node';
|
|
530
|
+
} else if (path_segment === 'start_offset') {
|
|
531
|
+
val = val.start_offset;
|
|
532
|
+
val_type = 'value';
|
|
533
|
+
} else if (path_segment === 'end_offset') {
|
|
534
|
+
val = val.end_offset;
|
|
535
|
+
val_type = 'value';
|
|
536
|
+
} else {
|
|
537
|
+
throw new Error(`Invalid path segment "${path_segment}" for annotation. Use "start_offset", "end_offset", or "node_id".`);
|
|
538
|
+
}
|
|
390
539
|
}
|
|
391
540
|
}
|
|
392
541
|
return val;
|
|
@@ -427,7 +576,7 @@ export default class Document {
|
|
|
427
576
|
kind: 'property',
|
|
428
577
|
name: property_name,
|
|
429
578
|
// Merge property spec from schema
|
|
430
|
-
...this.schema[parent.type][property_name]
|
|
579
|
+
...this.schema[parent.type].properties[property_name]
|
|
431
580
|
};
|
|
432
581
|
} else {
|
|
433
582
|
// Parent is a property (or we are at the root), so we are dealing with a node.
|
|
@@ -442,17 +591,13 @@ export default class Document {
|
|
|
442
591
|
}
|
|
443
592
|
|
|
444
593
|
/**
|
|
445
|
-
* Determines the kind of a node ('
|
|
446
|
-
*
|
|
594
|
+
* Determines the kind of a node ('block' for structured blocks, 'text' for pure
|
|
595
|
+
* text nodes or 'annotation' for annotation nodes.
|
|
447
596
|
* @param {any} node
|
|
448
|
-
* @returns {
|
|
597
|
+
* @returns {NodeKind}
|
|
449
598
|
*/
|
|
450
599
|
kind(node) {
|
|
451
|
-
|
|
452
|
-
return 'text'
|
|
453
|
-
} else {
|
|
454
|
-
return 'node';
|
|
455
|
-
}
|
|
600
|
+
return this.schema[node.type].kind;
|
|
456
601
|
}
|
|
457
602
|
|
|
458
603
|
/**
|
|
@@ -466,17 +611,18 @@ export default class Document {
|
|
|
466
611
|
if (this.selection?.type !== 'text') return null;
|
|
467
612
|
|
|
468
613
|
const { start, end } = this.get_selection_range();
|
|
469
|
-
const
|
|
470
|
-
const annotations =
|
|
614
|
+
const annotated_text = this.get(this.selection.path);
|
|
615
|
+
const annotations = annotated_text.annotations;
|
|
471
616
|
|
|
472
|
-
const active_annotation = annotations.find((
|
|
473
|
-
(
|
|
474
|
-
(
|
|
475
|
-
(
|
|
617
|
+
const active_annotation = annotations.find(({start_offset, end_offset}) =>
|
|
618
|
+
(start_offset <= start && end_offset > start) ||
|
|
619
|
+
(start_offset < end && end_offset >= end) ||
|
|
620
|
+
(start_offset >= start && end_offset <= end)
|
|
476
621
|
) || null;
|
|
477
622
|
|
|
478
|
-
if (annotation_type) {
|
|
479
|
-
|
|
623
|
+
if (annotation_type && active_annotation) {
|
|
624
|
+
const annotation_node = this.get(active_annotation?.node_id);
|
|
625
|
+
return annotation_node?.type === annotation_type ? active_annotation : null;
|
|
480
626
|
} else {
|
|
481
627
|
return active_annotation;
|
|
482
628
|
}
|
|
@@ -488,8 +634,8 @@ export default class Document {
|
|
|
488
634
|
|
|
489
635
|
const start = Math.min(this.selection.anchor_offset, this.selection.focus_offset);
|
|
490
636
|
const end = Math.max(this.selection.anchor_offset, this.selection.focus_offset);
|
|
491
|
-
const
|
|
492
|
-
return
|
|
637
|
+
const annotated_text = this.get(this.selection.path);
|
|
638
|
+
return char_slice(annotated_text.text, start, end);
|
|
493
639
|
}
|
|
494
640
|
|
|
495
641
|
get_selected_nodes() {
|
|
@@ -501,41 +647,6 @@ export default class Document {
|
|
|
501
647
|
return $state.snapshot(node_array.slice(start, end));
|
|
502
648
|
}
|
|
503
649
|
|
|
504
|
-
move_node_cursor(direction) {
|
|
505
|
-
if (this.selection?.type !== 'node') return;
|
|
506
|
-
const node_array = this.get(this.selection.path);
|
|
507
|
-
|
|
508
|
-
const { start, end } = this.get_selection_range();
|
|
509
|
-
|
|
510
|
-
if (this.selection.anchor_offset !== this.selection.focus_offset) {
|
|
511
|
-
// If selection is not collapsed, collapse it to the right or the left
|
|
512
|
-
if (direction === 'forward') {
|
|
513
|
-
this.selection.focus_offset = end;
|
|
514
|
-
this.selection.anchor_offset = end;
|
|
515
|
-
} else if (direction === 'backward') {
|
|
516
|
-
this.selection.focus_offset = start;
|
|
517
|
-
this.selection.anchor_offset = start;
|
|
518
|
-
}
|
|
519
|
-
} else if (direction === 'forward' && end < node_array.length) {
|
|
520
|
-
this.selection.focus_offset = end + 1;
|
|
521
|
-
this.selection.anchor_offset = end + 1;
|
|
522
|
-
} else if (direction === 'backward' && start > 0) {
|
|
523
|
-
this.selection.focus_offset = start - 1;
|
|
524
|
-
this.selection.anchor_offset = start - 1;
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
expand_node_selection(direction) {
|
|
529
|
-
if (this.selection.type !== 'node') return;
|
|
530
|
-
const node_array = this.get(this.selection.path);
|
|
531
|
-
|
|
532
|
-
if (direction === 'forward') {
|
|
533
|
-
this.selection.focus_offset = Math.min(this.selection.focus_offset + 1, node_array.length);
|
|
534
|
-
} else if (direction === 'backward') {
|
|
535
|
-
this.selection.focus_offset = Math.max(this.selection.focus_offset - 1, 0);
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
|
|
539
650
|
select_parent() {
|
|
540
651
|
if (!this.selection) return;
|
|
541
652
|
if (['text', 'property'].includes(this.selection?.type)) {
|
|
@@ -599,17 +710,21 @@ export default class Document {
|
|
|
599
710
|
return;
|
|
600
711
|
}
|
|
601
712
|
visited[node.id] = true;
|
|
602
|
-
for (const [, value] of Object.entries(node)) {
|
|
603
|
-
|
|
604
|
-
|
|
713
|
+
for (const [property_name, value] of Object.entries(node)) {
|
|
714
|
+
const property_schema = this.schema[node.type].properties[property_name];
|
|
715
|
+
|
|
716
|
+
if (property_schema?.type === 'node_array') {
|
|
605
717
|
for (const v of value) {
|
|
606
718
|
if (typeof v === 'string') {
|
|
607
719
|
visit($state.snapshot(this.get(v)));
|
|
608
720
|
}
|
|
609
721
|
}
|
|
610
|
-
} else if (
|
|
611
|
-
// TODO: Use schema inspection and do this only for properties of type `ref`
|
|
722
|
+
} else if (property_schema?.type === 'node') {
|
|
612
723
|
visit($state.snapshot(this.get(value)));
|
|
724
|
+
} else if (property_schema?.type === 'annotated_text') {
|
|
725
|
+
for (const annotation of value.annotations) {
|
|
726
|
+
visit($state.snapshot(this.get(annotation.node_id)));
|
|
727
|
+
}
|
|
613
728
|
}
|
|
614
729
|
}
|
|
615
730
|
// Finally add the node to the result.
|
|
@@ -630,7 +745,7 @@ export default class Document {
|
|
|
630
745
|
}
|
|
631
746
|
|
|
632
747
|
// property_type('page', 'body') => 'node_array'
|
|
633
|
-
// property_type('paragraph', 'content') => '
|
|
748
|
+
// property_type('paragraph', 'content') => 'annotated_text'
|
|
634
749
|
property_type(type, property) {
|
|
635
750
|
if (typeof type !== 'string') throw new Error(`Invalid type ${type} provided`);
|
|
636
751
|
if (typeof property !== 'string') throw new Error(`Invalid property ${property} provided`);
|
|
@@ -639,9 +754,9 @@ export default class Document {
|
|
|
639
754
|
if (property === 'id') return 'string';
|
|
640
755
|
|
|
641
756
|
if (!this.schema[type]) throw new Error(`Type ${type} not found in schema`);
|
|
642
|
-
if (!this.schema[type][property]) throw new Error(`Property ${property} not found in type ${type}`);
|
|
757
|
+
if (!this.schema[type].properties[property]) throw new Error(`Property ${property} not found in type ${type}`);
|
|
643
758
|
|
|
644
|
-
return this.schema[type][property].type;
|
|
759
|
+
return this.schema[type].properties[property].type;
|
|
645
760
|
}
|
|
646
761
|
|
|
647
762
|
// Count how many times a node is referenced in the document
|
package/src/lib/Node.svelte
CHANGED
|
@@ -18,21 +18,25 @@
|
|
|
18
18
|
let is_first_node_array_child = $derived(is_inside_node_array && parseInt(String(path.at(-1)), 10) === 0);
|
|
19
19
|
</script>
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
21
|
+
{#if svedit.editable}
|
|
22
|
+
<div
|
|
23
|
+
class="node"
|
|
24
|
+
data-node-id={node.id}
|
|
25
|
+
data-path={path.join('.')}
|
|
26
|
+
data-type="node"
|
|
27
|
+
style="anchor-name: --{path.join('-')};"
|
|
28
|
+
>
|
|
29
|
+
{#if is_first_node_array_child}
|
|
30
|
+
<NodeCursorTrap {path} type="position-zero-cursor-trap" />
|
|
31
|
+
{/if}
|
|
32
|
+
{@render children()}
|
|
33
|
+
{#if is_inside_node_array}
|
|
34
|
+
<NodeCursorTrap {path} type="after-node-cursor-trap" />
|
|
35
|
+
{/if}
|
|
36
|
+
</div>
|
|
37
|
+
{:else}
|
|
31
38
|
{@render children()}
|
|
32
|
-
|
|
33
|
-
<NodeCursorTrap {path} type="after-node-cursor-trap" />
|
|
34
|
-
{/if}
|
|
35
|
-
</div>
|
|
39
|
+
{/if}
|
|
36
40
|
|
|
37
41
|
<style>
|
|
38
42
|
div {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
</script>
|
|
19
19
|
|
|
20
20
|
<div class={css_class} data-type="node_array" data-path={path.join('.')}>
|
|
21
|
-
{#if nodes.length === 0}
|
|
21
|
+
{#if nodes.length === 0 && svedit.editable}
|
|
22
22
|
<!--
|
|
23
23
|
Experimental: We'll let .empty-node-array act like a node, so the existing
|
|
24
24
|
code paths for selection mapping will work as expected.
|