svedit 0.4.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # Svedit
2
2
 
3
- Svedit (think Svelte Edit) is a tiny library for building **rich content editors** with Svelte 5. You can model your content in JSON, render it with custom Svelte components, and edit it directly in the layout.
3
+ Svedit (think Svelte Edit) is a a tiny library for building editable websites in Svelte. You can model your content in JSON, render it with custom Svelte components, and edit it directly in the layout.
4
4
 
5
5
  Try the [demo](https://svedit.vercel.app).
6
6
 
@@ -217,7 +217,8 @@ const document_config = {
217
217
  layout,
218
218
  content
219
219
  };
220
- tr.insert_nodes([new_text]);
220
+ tr.create(new_text);
221
+ tr.insert_nodes([new_text.id]);
221
222
  },
222
223
  }
223
224
  };
@@ -376,4 +377,4 @@ It's early. Expect bugs. Expect missing features. Expect the need for more work
376
377
 
377
378
  ## Credits
378
379
 
379
- Svedit led by [Michael Aufreiter](https://michaelaufreiter.com) with guidance and support from [Johannes Mutter](https://mutter.co).
380
+ Svedit is led by [Michael Aufreiter](https://michaelaufreiter.com) with guidance and support from [Johannes Mutter](https://mutter.co).
@@ -5,13 +5,10 @@
5
5
  * Selection,
6
6
  * Annotation,
7
7
  * PrimitiveType,
8
- * DocumentSchemaPrimitive,
9
8
  * NodeProperty,
10
9
  * NodeArrayProperty,
11
10
  * NodeKind,
12
11
  * NodeSchema,
13
- * TextNodeSchema,
14
- * NonTextNodeSchema,
15
12
  * DocumentSchema,
16
13
  * SerializedDocument
17
14
  * } from './types.d.ts';
@@ -168,6 +165,20 @@ export default class Document {
168
165
  * @returns {NodeKind}
169
166
  */
170
167
  kind(node: any): NodeKind;
168
+ /**
169
+ * Determines whether a node type can be inserted at a given selection.
170
+ * @param {string} node_type - The type of node to insert.
171
+ * @param {Selection} [selection] - The selection at which to insert the node.
172
+ * @returns {boolean} True if the node type can be inserted, false otherwise.
173
+ */
174
+ can_insert(node_type: string, selection?: Selection): boolean;
175
+ /**
176
+ * Compute next possible insert position from a given selection
177
+ *
178
+ * @param {Selection} [selection] - Reference selection
179
+ * @returns {Selection} True if the paste operation was successful, false otherwise
180
+ */
181
+ get_next_node_insert_cursor(selection?: Selection): Selection;
171
182
  /**
172
183
  * Returns the annotation object that is currently "under the cursor".
173
184
  * NOTE: Annotations in Svedit are exclusive, so there can only be one active_annotation
@@ -176,6 +187,11 @@ export default class Document {
176
187
  * @returns {Annotation|null}
177
188
  */
178
189
  active_annotation(annotation_type: string): Annotation | null;
190
+ get_selected_annotated_text(): {
191
+ text: string;
192
+ annotations: any;
193
+ nodes: {};
194
+ };
179
195
  get_selected_plain_text(): string;
180
196
  get_selected_nodes(): any;
181
197
  select_parent(): void;
@@ -190,7 +206,7 @@ export default class Document {
190
206
  * @returns {SerializedDocument} The serialized document array
191
207
  */
192
208
  to_json(): SerializedDocument;
193
- property_type(type: any, property: any): "string" | "number" | "boolean" | "node" | "node_array" | "annotated_text" | "integer" | "datetime" | "string_array" | "number_array" | "boolean_array" | "integer_array";
209
+ property_type(type: any, property: any): "string" | "number" | "boolean" | "node_array" | "node" | "annotated_text" | "integer" | "integer_array" | "number_array" | "string_array" | "datetime" | "boolean_array";
194
210
  count_references(node_id: any): number;
195
211
  /**
196
212
  * @param {NodeId} node_id
@@ -1 +1 @@
1
- {"version":3,"file":"Document.svelte.d.ts","sourceRoot":"","sources":["../src/lib/Document.svelte.js"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;GAiBG;AAEH;;;;;;;GAOG;AACH,uCAJ0C,CAAC,SAA7B,MAAM,CAAC,MAAM,EAAE,UAAU,CAAE,UAC9B,CAAC,GACC,CAAC,CAIb;AAED;;;;GAIG;AACH,wCAHW,MAAM,GACJ,IAAI,IAAI,aAAa,CAQjC;AAED;;;;GAIG;AACH,2DAHW,YAAY,GAAG,iBAAiB,GAC9B,MAAM,GAAG,IAAI,CASzB;AAED;;;;GAIG;AACH,0DAHW,cAAc,QAexB;AAuGD;IAgGE;;;;;;OAMG;IACH,oBANW,cAAc,kBACd,kBAAkB,0BAE1B;QAA4B,SAAS,GAA7B,SAAS;QACK,MAAM,GAApB,GAAG;KACb,EAqBA;IAxHD,YAAkB;IAClB,iBAAuB;IACvB,WAAiB;IACjB,eAAqB;IACrB,sBAA2B;IAG3B,kBAA6C;IAC7C,kBAAkE;IAGnE,mBAAmD;IACnD,iBAA+C;IAC/C,gCAA6E;IAU5E;;;;OAIG;IACH,qBAHW,SAAS,GAAG,SAAS,EAQ/B;IAlBD;;;OAGG;IACH,iBAFa,SAAS,GAAG,SAAS,CAIjC;IAcD;;;;;;OAMG;IACH,4BAiDC;IAaC,uBAAoB;IAkBtB,mBAMC;IAED,sCAKA;IAGD,yBAkBC;IAID,uBAoBC;IAGA,yBAWC;IAED;;;;OAIG;IACH,oBAHW,GAAG,QAKb;IAED;;;;OAIG;IACH,UAFa,WAAW,CASvB;IAED;;;;OAIG;IACH,mBAFW,WAAW,QAkBrB;IAED,aAWC;IAED,aAaC;IAED;;;;;;;;;;;;;;;;;;;OAmBG;IACH,UAlBW,YAAY,GAAC,MAAM,GACjB,GAAG,CAoFf;IAED;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;IACH,cAHW,YAAY,GACV;QAAC,IAAI,EAAE,UAAU,GAAC,MAAM,CAAC;QAAC,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;KAAC,CAuBzD;IAED;;;;;OAKG;IACH,WAHW,GAAG,GACD,QAAQ,CAIpB;IAED;;;;;;OAMG;IACH,mCAHW,MAAM,GACJ,UAAU,GAAC,IAAI,CAqB3B;IAGD,kCAOC;IAED,0BAOC;IAED,sBAmCC;IAED;;;;MAaC;IAKD,8BAgCC;IAED;;;OAGG;IACH,WAFa,kBAAkB,CAI9B;IAID,mNAWC;IAGD,uCAkBC;IAGD;;;OAGG;IACH,8BAHW,MAAM,GACJ,MAAM,EAAE,CASpB;;CACF;gCAxwBS,cAAc;mCAAd,cAAc;kCAAd,cAAc;uCAAd,cAAc;oCAAd,cAAc;+BAAd,cAAc;wBAnBA,yBAAyB;kCAmBvC,cAAc;8BAAd,cAAc;gCAAd,cAAc;wCAAd,cAAc;4BAAd,cAAc"}
1
+ {"version":3,"file":"Document.svelte.d.ts","sourceRoot":"","sources":["../src/lib/Document.svelte.js"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;GAcG;AAEH;;;;;;;GAOG;AACH,uCAJ0C,CAAC,SAA7B,MAAM,CAAC,MAAM,EAAE,UAAU,CAAE,UAC9B,CAAC,GACC,CAAC,CAIb;AAED;;;;GAIG;AACH,wCAHW,MAAM,GACJ,IAAI,IAAI,aAAa,CAQjC;AAED;;;;GAIG;AACH,2DAHW,YAAY,GAAG,iBAAiB,GAC9B,MAAM,GAAG,IAAI,CASzB;AAED;;;;GAIG;AACH,0DAHW,cAAc,QAexB;AAuGD;IAgGE;;;;;;OAMG;IACH,oBANW,cAAc,kBACd,kBAAkB,0BAE1B;QAA4B,SAAS,GAA7B,SAAS;QACK,MAAM,GAApB,GAAG;KACb,EAqBA;IAxHD,YAAkB;IAClB,iBAAuB;IACvB,WAAiB;IACjB,eAAqB;IACrB,sBAA2B;IAG3B,kBAA6C;IAC7C,kBAAkE;IAGnE,mBAAmD;IACnD,iBAA+C;IAC/C,gCAA6E;IAU5E;;;;OAIG;IACH,qBAHW,SAAS,GAAG,SAAS,EAQ/B;IAlBD;;;OAGG;IACH,iBAFa,SAAS,GAAG,SAAS,CAIjC;IAcD;;;;;;OAMG;IACH,4BAiDC;IAaC,uBAAoB;IAkBtB,mBAMC;IAED,sCAKA;IAGD,yBAkBC;IAID,uBAoBC;IAGA,yBAWC;IAED;;;;OAIG;IACH,oBAHW,GAAG,QAKb;IAED;;;;OAIG;IACH,UAFa,WAAW,CASvB;IAED;;;;OAIG;IACH,mBAFW,WAAW,QAkBrB;IAED,aAWC;IAED,aAaC;IAED;;;;;;;;;;;;;;;;;;;OAmBG;IACH,UAlBW,YAAY,GAAC,MAAM,GACjB,GAAG,CAoFf;IAED;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;IACH,cAHW,YAAY,GACV;QAAC,IAAI,EAAE,UAAU,GAAC,MAAM,CAAC;QAAC,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;KAAC,CAuBzD;IAED;;;;;OAKG;IACH,WAHW,GAAG,GACD,QAAQ,CAIpB;IAED;;;;;OAKG;IACH,sBAJW,MAAM,cACN,SAAS,GACP,OAAO,CAcnB;IAED;;;;;OAKG;IACH,wCAHW,SAAS,GACP,SAAS,CAerB;IAED;;;;;;OAMG;IACH,mCAHW,MAAM,GACJ,UAAU,GAAC,IAAI,CAqB3B;IAED;;;;MA2BC;IAGD,kCAOC;IAED,0BAOC;IAED,sBAmCC;IAED;;;;MAaC;IAKD,8BAEC;IAED;;;OAGG;IACH,WAFa,kBAAkB,CAI9B;IAID,mNAWC;IAGD,uCAkBC;IAGD;;;OAGG;IACH,8BAHW,MAAM,GACJ,MAAM,EAAE,CASpB;;CACF;gCAhzBS,cAAc;mCAAd,cAAc;kCAAd,cAAc;uCAAd,cAAc;oCAAd,cAAc;+BAAd,cAAc;wBAhBA,yBAAyB;kCAgBvC,cAAc;8BAAd,cAAc;gCAAd,cAAc;wCAAd,cAAc;4BAAd,cAAc"}
@@ -1,5 +1,5 @@
1
1
  import Transaction from './Transaction.svelte.js';
2
- import { char_slice, get_char_length } from './util.js';
2
+ import { char_slice, get_char_length, traverse } from './util.js';
3
3
 
4
4
  /**
5
5
  * @import {
@@ -8,13 +8,10 @@ import { char_slice, get_char_length } from './util.js';
8
8
  * Selection,
9
9
  * Annotation,
10
10
  * PrimitiveType,
11
- * DocumentSchemaPrimitive,
12
11
  * NodeProperty,
13
12
  * NodeArrayProperty,
14
13
  * NodeKind,
15
14
  * NodeSchema,
16
- * TextNodeSchema,
17
- * NonTextNodeSchema,
18
15
  * DocumentSchema,
19
16
  * SerializedDocument
20
17
  * } from './types.d.ts';
@@ -315,8 +312,8 @@ export default class Document {
315
312
  get_available_annotation_types() {
316
313
  if (this.selection?.type !== 'text') return [];
317
314
  const path = this.selection.path;
318
- const property_schema = this.inspect(path);
319
- return property_schema.node_types || [];
315
+ const property_definition = this.inspect(path);
316
+ return property_definition.node_types || [];
320
317
  }
321
318
 
322
319
  // Helper function to get the currently selected node
@@ -600,6 +597,47 @@ export default class Document {
600
597
  return this.schema[node.type].kind;
601
598
  }
602
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;
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
+ };
639
+ }
640
+
603
641
  /**
604
642
  * Returns the annotation object that is currently "under the cursor".
605
643
  * NOTE: Annotations in Svedit are exclusive, so there can only be one active_annotation
@@ -628,6 +666,35 @@ export default class Document {
628
666
  }
629
667
  }
630
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
+
631
698
  // TODO: think about ways how we can also turn a node selection into plain text.
632
699
  get_selected_plain_text() {
633
700
  if (this.selection?.type !== 'text') return null;
@@ -703,37 +770,7 @@ export default class Document {
703
770
  // IMPORTANT: Leaf nodes must go first, branches second and the root node (entry point) last (depth-first traversal)
704
771
  // NOTE: Nodes that are not reachable from the entry point node will not be included in the serialization
705
772
  traverse(node_id) {
706
- const json = [];
707
- const visited = {};
708
- const visit = (node) => {
709
- if (!node || visited[node.id]) {
710
- return;
711
- }
712
- visited[node.id] = true;
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') {
717
- for (const v of value) {
718
- if (typeof v === 'string') {
719
- visit($state.snapshot(this.get(v)));
720
- }
721
- }
722
- } else if (property_schema?.type === 'node') {
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
- }
728
- }
729
- }
730
- // Finally add the node to the result.
731
- // We use a deep clone, so we make sure nothing of the original document is referenced.
732
- json.push(structuredClone(node));
733
- }
734
- // Start with the root node (document_id)
735
- visit($state.snapshot(this.get(node_id)));
736
- return json;
773
+ return traverse(node_id, this.schema, $state.snapshot(this.nodes));
737
774
  }
738
775
 
739
776
  /**