svedit 0.1.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 (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +323 -0
  3. package/dist/AnnotatedStringProperty.svelte +126 -0
  4. package/dist/AnnotatedStringProperty.svelte.d.ts +7 -0
  5. package/dist/AnnotatedStringProperty.svelte.d.ts.map +1 -0
  6. package/dist/CustomProperty.svelte +40 -0
  7. package/dist/CustomProperty.svelte.d.ts +8 -0
  8. package/dist/CustomProperty.svelte.d.ts.map +1 -0
  9. package/dist/Document.svelte.d.ts +191 -0
  10. package/dist/Document.svelte.d.ts.map +1 -0
  11. package/dist/Document.svelte.js +681 -0
  12. package/dist/Node.svelte +41 -0
  13. package/dist/Node.svelte.d.ts +7 -0
  14. package/dist/Node.svelte.d.ts.map +1 -0
  15. package/dist/NodeArrayProperty.svelte +45 -0
  16. package/dist/NodeArrayProperty.svelte.d.ts +7 -0
  17. package/dist/NodeArrayProperty.svelte.d.ts.map +1 -0
  18. package/dist/Svedit.svelte +936 -0
  19. package/dist/Svedit.svelte.d.ts +12 -0
  20. package/dist/Svedit.svelte.d.ts.map +1 -0
  21. package/dist/Transaction.svelte.d.ts +198 -0
  22. package/dist/Transaction.svelte.d.ts.map +1 -0
  23. package/dist/Transaction.svelte.js +577 -0
  24. package/dist/UnknownNode.svelte +12 -0
  25. package/dist/UnknownNode.svelte.d.ts +12 -0
  26. package/dist/UnknownNode.svelte.d.ts.map +1 -0
  27. package/dist/commands.svelte.d.ts +5 -0
  28. package/dist/commands.svelte.d.ts.map +1 -0
  29. package/dist/commands.svelte.js +261 -0
  30. package/dist/index.d.ts +10 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +14 -0
  33. package/dist/types.d.ts +264 -0
  34. package/dist/util.d.ts +4 -0
  35. package/dist/util.d.ts.map +1 -0
  36. package/dist/util.js +78 -0
  37. package/package.json +65 -0
  38. package/src/lib/AnnotatedStringProperty.svelte +126 -0
  39. package/src/lib/CustomProperty.svelte +40 -0
  40. package/src/lib/Document.svelte.js +681 -0
  41. package/src/lib/Node.svelte +41 -0
  42. package/src/lib/NodeArrayProperty.svelte +45 -0
  43. package/src/lib/Svedit.svelte +936 -0
  44. package/src/lib/Transaction.svelte.js +577 -0
  45. package/src/lib/UnknownNode.svelte +12 -0
  46. package/src/lib/commands.svelte.js +261 -0
  47. package/src/lib/index.js +14 -0
  48. package/src/lib/types.d.ts +264 -0
  49. package/src/lib/util.js +78 -0
@@ -0,0 +1,261 @@
1
+ import { split_annotated_string, join_annotated_string } from './util.js';
2
+ import { get_default_node_type } from './Document.svelte.js';
3
+
4
+ export function break_text_node(tr) {
5
+ const doc = tr.doc;
6
+ // Keep a reference of the original selection (before any transforms are applied)
7
+ const selection = doc.selection;
8
+ // First we need to ensure we have a text selection
9
+ if (selection.type !== 'text') return false;
10
+
11
+ // Next, we need to determine if the enclosing node is a pure text node (e.g. paragraph),
12
+ // which is wrapped inside a node_array (e.g. page.body)
13
+
14
+ // Owner of the text property (e.g. paragraph)
15
+ const node = doc.get(selection.path.slice(0, -1));
16
+ if (doc.kind(node) !== 'text') return false;
17
+ const is_inside_node_array = doc.inspect(selection.path.slice(0, -2))?.type === 'node_array';
18
+ // console.log('is_inside_node_array', is_inside_node_array);
19
+ if (!is_inside_node_array) return false; // Do nothing if we're not inside a node_array
20
+ const node_array_prop = selection.path.at(-3);
21
+ // console.log('node_array_prop', node_array_prop);
22
+ // Get the node that owns the node_array property (e.g. a page.body)
23
+ const node_array_node = doc.get(selection.path.slice(0, -3));
24
+ // console.log('node_array_node', $state.snapshot(node_array_node));
25
+
26
+ // Delete selection unless collapsed
27
+ if (selection.anchor_offset !== selection.focus_offset) {
28
+ tr.delete_selection();
29
+ }
30
+
31
+ const split_at_position = tr.doc.selection.anchor_offset;
32
+ const content = tr.doc.get(selection.path);
33
+ const [left_text, right_text] = split_annotated_string(content, split_at_position);
34
+
35
+ tr.set([node.id, 'content'], left_text);
36
+
37
+ const node_insert_position = {
38
+ type: 'node',
39
+ path: tr.doc.selection.path.slice(0, -2),
40
+ anchor_offset: parseInt(tr.doc.selection.path.at(-2), 10) + 1,
41
+ focus_offset: parseInt(tr.doc.selection.path.at(-2), 10) + 1,
42
+ };
43
+
44
+ // TODO: Only use default_node_type when cursor is at the end of
45
+ const node_array_property_definition = doc.schema[node_array_node.type][node_array_prop];
46
+ const target_node_type = get_default_node_type(node_array_property_definition);
47
+
48
+ if (!target_node_type) {
49
+ console.warn('Cannot determine target node type for break_text_node - no default_ref_type and multiple node_types');
50
+ return false;
51
+ }
52
+
53
+ tr.set_selection(node_insert_position);
54
+
55
+ doc.config.inserters[target_node_type](tr, right_text);
56
+ }
57
+
58
+ export function join_text_node(tr) {
59
+ const doc = tr.doc;
60
+ // Keep a reference of the original selection (before any transforms are applied)
61
+ const selection = doc.selection;
62
+ // First we need to ensure we have a text selection
63
+ if (selection.type !== 'text') return false;
64
+
65
+ const node = doc.get(selection.path.slice(0, -1));
66
+ if (doc.kind(node) !== 'text') return false;
67
+ const is_inside_node_array = doc.inspect(selection.path.slice(0, -2))?.type === 'node_array';
68
+ // console.log('is_inside_node_array', is_inside_node_array);
69
+ if (!is_inside_node_array) return false; // Do nothing if we're not inside a node_array
70
+
71
+ const node_index = parseInt(doc.selection.path.at(-2), 10);
72
+
73
+ // Determine if we can join with the previous node
74
+ let can_join = false;
75
+ let predecessor_node = null;
76
+
77
+ if (node_index > 0) {
78
+ const previous_text_path = [...doc.selection.path.slice(0, -2), node_index - 1];
79
+ predecessor_node = doc.get(previous_text_path);
80
+ can_join = doc.kind(predecessor_node) === 'text';
81
+ }
82
+
83
+ // Special behavior: if we can't join and current node is empty, delete it
84
+ if (!can_join && node.content[0] === '') {
85
+ tr.set_selection({
86
+ type: 'node',
87
+ path: doc.selection.path.slice(0, -2),
88
+ anchor_offset: node_index,
89
+ focus_offset: node_index + 1,
90
+ });
91
+ tr.delete_selection();
92
+ return true;
93
+ }
94
+
95
+ // If we can't join for any reason, return false
96
+ if (!can_join) {
97
+ return false;
98
+ }
99
+
100
+ // Normal joining logic - both nodes are text nodes
101
+ const previous_text_path = [...doc.selection.path.slice(0, -2), node_index - 1];
102
+ const joined_text = join_annotated_string(predecessor_node.content, node.content);
103
+
104
+ tr.set_selection({
105
+ type: 'node',
106
+ path: doc.selection.path.slice(0, -2),
107
+ anchor_offset: node_index,
108
+ focus_offset: node_index + 1,
109
+ });
110
+
111
+ tr.delete_selection();
112
+ tr.set_selection({
113
+ type: 'text',
114
+ path: [...previous_text_path, 'content'],
115
+ anchor_offset: predecessor_node.content[0].length,
116
+ focus_offset: predecessor_node.content[0].length,
117
+ });
118
+ tr.set([predecessor_node.id, 'content'], joined_text);
119
+ }
120
+
121
+ export function insert_default_node(tr) {
122
+ const doc = tr.doc;
123
+ const selection = doc.selection;
124
+
125
+ // Only work with collapsed node selections
126
+ if (selection?.type !== 'node' || selection.anchor_offset !== selection.focus_offset) {
127
+ return false;
128
+ }
129
+
130
+ const path = selection.path;
131
+ const node_array_node = doc.get(path.slice(0, -1));
132
+ const property_name = path.at(-1);
133
+
134
+ // Get the definition for this property
135
+ const property_definition = doc.schema[node_array_node.type][property_name];
136
+ const default_type = get_default_node_type(property_definition);
137
+
138
+ // Only proceed if there's exactly one allowed node_type
139
+ // if (!default_type || property_schema.node_types.length !== 1) {
140
+ // return false;
141
+ // }
142
+
143
+ // Use the inserter function if available
144
+ if (doc.config?.inserters?.[default_type]) {
145
+ doc.config.inserters[default_type](tr);
146
+ return true;
147
+ } else {
148
+ throw new Error(`No inserter function available for default node type '${default_type}'`);
149
+ }
150
+ }
151
+
152
+ export function select_all(tr) {
153
+ const doc = tr.doc;
154
+ const selection = doc.selection;
155
+
156
+ if (!selection) {
157
+ return false;
158
+ }
159
+
160
+ if (selection.type === 'text') {
161
+ const text_content = doc.get(selection.path);
162
+ const text_length = text_content[0].length;
163
+
164
+ // Check if all text is already selected
165
+ const is_all_text_selected =
166
+ Math.min(selection.anchor_offset, selection.focus_offset) === 0 &&
167
+ Math.max(selection.anchor_offset, selection.focus_offset) === text_length;
168
+
169
+ if (!is_all_text_selected) {
170
+ // Select all text in the current text node
171
+ tr.set_selection({
172
+ type: 'text',
173
+ path: selection.path,
174
+ anchor_offset: 0,
175
+ focus_offset: text_length
176
+ });
177
+ return true;
178
+ } else {
179
+ // All text is selected, move up to select the containing node
180
+ const node_path = selection.path.slice(0, -1); // Remove the property name (e.g., 'content')
181
+
182
+ // Check if we have enough path segments and if we're inside a node_array
183
+ if (node_path.length >= 2) {
184
+ const is_inside_node_array = doc.inspect(node_path.slice(0, -1))?.type === 'node_array';
185
+
186
+ if (is_inside_node_array) {
187
+ const node_index = parseInt(node_path.at(-1));
188
+ tr.set_selection({
189
+ type: 'node',
190
+ path: node_path.slice(0, -1),
191
+ anchor_offset: node_index,
192
+ focus_offset: node_index + 1
193
+ });
194
+ return true;
195
+ }
196
+ }
197
+ // Stop expanding - text is not in a selectable node_array
198
+ }
199
+ } else if (selection.type === 'node') {
200
+ const node_array_path = selection.path;
201
+ const node_array = doc.get(node_array_path);
202
+
203
+ // Check if the entire node_array is already selected
204
+ const is_entire_node_array_selected =
205
+ Math.min(selection.anchor_offset, selection.focus_offset) === 0 &&
206
+ Math.max(selection.anchor_offset, selection.focus_offset) === node_array.length;
207
+
208
+ if (!is_entire_node_array_selected) {
209
+ // Select the entire node_array
210
+ tr.set_selection({
211
+ type: 'node',
212
+ path: node_array_path,
213
+ anchor_offset: 0,
214
+ focus_offset: node_array.length
215
+ });
216
+ return true;
217
+ } else {
218
+ // Entire node_array is selected, try to move up to parent node_array
219
+ const parent_path = node_array_path.slice(0, -1);
220
+
221
+ // Check if we have enough path segments and if parent is a valid node_array
222
+ if (parent_path.length >= 2) {
223
+ const is_parent_node_array = doc.inspect(parent_path.slice(0, -1))?.type === 'node_array';
224
+
225
+ if (is_parent_node_array) {
226
+ const parent_node_index = parseInt(parent_path.at(-1));
227
+ tr.set_selection({
228
+ type: 'node',
229
+ path: parent_path.slice(0, -1),
230
+ anchor_offset: parent_node_index,
231
+ focus_offset: parent_node_index + 1
232
+ });
233
+ return true;
234
+ }
235
+ }
236
+ // Stop expanding - we've reached the top level
237
+ }
238
+ } else if (selection.type === 'property') {
239
+ // For property selections, select the containing node
240
+ const node_path = selection.path.slice(0, -1);
241
+
242
+ // Check if we have enough path segments and if we're inside a node_array
243
+ if (node_path.length >= 2) {
244
+ const is_inside_node_array = doc.inspect(node_path.slice(0, -1))?.type === 'node_array';
245
+
246
+ if (is_inside_node_array) {
247
+ const node_index = parseInt(node_path.at(-1));
248
+ tr.set_selection({
249
+ type: 'node',
250
+ path: node_path.slice(0, -1),
251
+ anchor_offset: node_index,
252
+ focus_offset: node_index + 1
253
+ });
254
+ return true;
255
+ }
256
+ }
257
+ // Stop expanding - property is not in a selectable node_array
258
+ }
259
+
260
+ return false;
261
+ }
@@ -0,0 +1,10 @@
1
+ export { default as Svedit } from "./Svedit.svelte";
2
+ export { default as AnnotatedStringProperty } from "./AnnotatedStringProperty.svelte";
3
+ export { default as CustomProperty } from "./CustomProperty.svelte";
4
+ export { default as Node } from "./Node.svelte";
5
+ export { default as NodeArrayProperty } from "./NodeArrayProperty.svelte";
6
+ export { default as Transaction } from "./Transaction.svelte.js";
7
+ export * from "./commands.svelte.js";
8
+ export * from "./util.js";
9
+ export { default as Document, define_document_schema } from "./Document.svelte.js";
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/lib/index.js"],"names":[],"mappings":""}
package/dist/index.js ADDED
@@ -0,0 +1,14 @@
1
+ // Main exports for the svedit library
2
+ export { default as Svedit } from './Svedit.svelte';
3
+ export { default as AnnotatedStringProperty } from './AnnotatedStringProperty.svelte';
4
+ export { default as CustomProperty } from './CustomProperty.svelte';
5
+ export { default as Node } from './Node.svelte';
6
+ export { default as NodeArrayProperty } from './NodeArrayProperty.svelte';
7
+
8
+ // Core classes and utilities
9
+ export { default as Document, define_document_schema } from './Document.svelte.js';
10
+ export { default as Transaction } from './Transaction.svelte.js';
11
+
12
+ // Commands and utilities
13
+ export * from './commands.svelte.js';
14
+ export * from './util.js';
@@ -0,0 +1,264 @@
1
+ import type Document from './Document.svelte.js';
2
+ import type Svedit from './Svedit.svelte';
3
+
4
+ // ===== SVELTE TYPE IMPORTS =====
5
+
6
+ /**
7
+ * Import Svelte's Snippet type for properly typing children in components
8
+ */
9
+ import type { Component, Snippet } from 'svelte';
10
+
11
+ // ===== SELECTION TYPE DEFINITIONS =====
12
+
13
+ /**
14
+ * A unique node identifier (ideally UUID)
15
+ */
16
+ export type NodeId = string;
17
+
18
+ /**
19
+ * Array of IDs, property names (strings), or indexes (integers) that identify a node or property in the document
20
+ */
21
+ export type DocumentPath = Array<string | number>;
22
+
23
+ /**
24
+ * Text selection within a text property
25
+ */
26
+ export type TextSelection = {
27
+ type: "text";
28
+ path: DocumentPath;
29
+ anchor_offset: number;
30
+ focus_offset: number;
31
+ };
32
+
33
+ /**
34
+ * Node selection within a node array
35
+ */
36
+ export type NodeSelection = {
37
+ type: "node";
38
+ path: DocumentPath;
39
+ anchor_offset: number;
40
+ focus_offset: number;
41
+ };
42
+
43
+ /**
44
+ * Property selection within a node
45
+ */
46
+ export type PropertySelection = {
47
+ type: "property";
48
+ path: DocumentPath;
49
+ };
50
+
51
+ /**
52
+ * Union type for all possible selection types
53
+ */
54
+ export type Selection = TextSelection | NodeSelection | PropertySelection;
55
+
56
+ // ===== SCHEMA TYPE DEFINITIONS =====
57
+
58
+ /**
59
+ * Basic scalar types supported by the schema system.
60
+ */
61
+ export type ScalarType = "string" | "number" | "boolean" | "integer" | "datetime";
62
+
63
+ /**
64
+ * Array types for collections of scalar values.
65
+ */
66
+ export type ArrayType = "string_array" | "number_array" | "boolean_array" | "integer_array";
67
+
68
+ /**
69
+ * Special types for rich content.
70
+ */
71
+ export type RichType = "annotated_string";
72
+
73
+ /**
74
+ * Node reference types for linking to other nodes.
75
+ */
76
+ export type ReferenceType = "node" | "node_array";
77
+
78
+ /**
79
+ * All primitive types that can be used in property definitions.
80
+ */
81
+ export type PrimitiveType = ScalarType | ArrayType | RichType;
82
+
83
+ /**
84
+ * All possible property types in schemas.
85
+ */
86
+ export type PropertyType = PrimitiveType | ReferenceType;
87
+
88
+ /**
89
+ * Document schema primitive types - all possible property types in document schemas.
90
+ */
91
+ export type DocumentSchemaPrimitive = "string" | "number" | "boolean" | "integer" | "datetime" | "string_array" | "number_array" | "boolean_array" | "integer_array" | "annotated_string" | "node" | "node_array";
92
+
93
+ /**
94
+ * Maps document schema types to their JavaScript runtime types.
95
+ */
96
+ export type DocumentSchemaValueToJs<T> =
97
+ T extends "string" ? string :
98
+ T extends "number" ? number :
99
+ T extends "boolean" ? boolean :
100
+ T extends "integer" ? number :
101
+ T extends "datetime" ? string :
102
+ T extends "string_array" ? Array<string> :
103
+ T extends "number_array" ? Array<number> :
104
+ T extends "boolean_array" ? Array<boolean> :
105
+ T extends "integer_array" ? Array<number> :
106
+ T extends "annotated_string" ? [string, Array<any>] :
107
+ T extends "node" ? string :
108
+ T extends "node_array" ? Array<string> :
109
+ never;
110
+
111
+ /**
112
+ * Converts a document node schema definition to its inferred JS type.
113
+ * Handles the {type: "..."} wrapper structure used in document schemas.
114
+ */
115
+ export type DocumentNodeToJs<S extends Record<string, {type: DocumentSchemaPrimitive}>> =
116
+ { id: string, type: string } & { [K in keyof S]: DocumentSchemaValueToJs<S[K]["type"]> };
117
+
118
+ /**
119
+ * A property that stores a primitive value.
120
+ */
121
+ export type PrimitiveProperty = {
122
+ [K in PrimitiveType]: { type: K }
123
+ }[PrimitiveType];
124
+
125
+ /**
126
+ * A property that stores a reference to a single node.
127
+ */
128
+ export type NodeProperty = {
129
+ type: "node";
130
+ node_types: string[];
131
+ default_node_type?: string;
132
+ };
133
+
134
+ /**
135
+ * A property that stores an array of node references.
136
+ */
137
+ export type NodeArrayProperty = {
138
+ type: "node_array";
139
+ node_types: string[];
140
+ default_node_type?: string;
141
+ };
142
+
143
+ /**
144
+ * Union type for all possible property definitions.
145
+ */
146
+ export type PropertyDefinition = PrimitiveProperty | NodeProperty | NodeArrayProperty;
147
+
148
+ /**
149
+ * A node schema defines the structure of a specific node type.
150
+ * Maps property names to their definitions.
151
+ */
152
+ export type NodeSchema = Record<string, PropertyDefinition>;
153
+
154
+ /**
155
+ * A document schema defines all node types available in a document.
156
+ * Maps node type names to their schemas.
157
+ */
158
+ export type DocumentSchema = Record<string, NodeSchema>;
159
+
160
+ /**
161
+ * A serialized node in the document format.
162
+ * Must have id and type properties, with other properties defined by the schema.
163
+ */
164
+ export type SerializedNode = {
165
+ id: string;
166
+ type: string;
167
+ [key: string]: any;
168
+ };
169
+
170
+ /**
171
+ * The document serialization format - an array of serialized nodes.
172
+ * Nodes must be ordered so that referenced nodes come before nodes that reference them.
173
+ */
174
+ export type SerializedDocument = SerializedNode[];
175
+
176
+ /**
177
+ * Props for the AnnotatedStringProperty component
178
+ */
179
+ export type AnnotatedStringPropertyProps = {
180
+ /** The full path to the property */
181
+ path: DocumentPath;
182
+ /** The `class` attribute on the content element */
183
+ class?: string;
184
+ /** A placeholder to be rendered for empty content */
185
+ placeholder?: string;
186
+ };
187
+
188
+ /**
189
+ * Props for the CustomProperty component
190
+ */
191
+ export type CustomPropertyProps = {
192
+ /** The full path to the property */
193
+ path: DocumentPath;
194
+ /** The `class` attribute on the content element */
195
+ class?: string;
196
+ /** The content of the custom property (e.g. an image) */
197
+ children: Snippet;
198
+ };
199
+
200
+ /**
201
+ * Props for the NodeArray component
202
+ */
203
+ export type NodeArrayPropertyProps = {
204
+ /** The full path to the property */
205
+ path: DocumentPath;
206
+ /** The `class` attribute on the container element */
207
+ class?: string;
208
+ };
209
+
210
+ /**
211
+ * Props for the Node component
212
+ */
213
+ export type NodeProps = {
214
+ /** The full path to the node */
215
+ path: DocumentPath;
216
+ /** The type-specific content of the node */
217
+ children: Snippet;
218
+ };
219
+
220
+ /**
221
+ * Props for the CustomProperty component
222
+ */
223
+ export type CustomPropertyProps = {
224
+ /** The full path to the property */
225
+ path: DocumentPath;
226
+ /** The `class` attribute on the content element */
227
+ class?: string;
228
+ /** The content of the custom property (e.g. an image) */
229
+ children: Snippet;
230
+ };
231
+
232
+ /**
233
+ * Props for the CustomProperty component
234
+ */
235
+ export type SveditProps = {
236
+ /** The full path to the property */
237
+ doc: Document,
238
+ /** Determines wether the document should be editable or read-only. */
239
+ editable?: boolean,
240
+ /** The path to the root element (e.g. ['page_1']) */
241
+ path: DocumentPath,
242
+ /** The `class` attribute on the content element */
243
+ class?: string;
244
+ };
245
+
246
+ /**
247
+ * Represents an annotation in an annotated string
248
+ * Format: [start_offset, end_offset, type, options?]
249
+ */
250
+ export type Annotation = [number, number, string, Record<string, any>?];
251
+
252
+ /**
253
+ * Represents a fragment of annotated text content
254
+ */
255
+ export type AnnotationFragment = {
256
+ /** The annotation type (e.g., 'link', 'emphasis', 'strong') */
257
+ type: string;
258
+ /** The text content of the annotation */
259
+ content: string;
260
+ /** Index of the annotation in the original array */
261
+ annotation_index: number;
262
+ /** Optional properties from the annotation's options object */
263
+ data?: any;
264
+ };
package/dist/util.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ export function split_annotated_string(text_with_annotations: any, at_position: any): any[][];
2
+ export function join_annotated_string(first_text: any, second_text: any): any[];
3
+ export function snake_to_pascal(str: any): any;
4
+ //# sourceMappingURL=util.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../src/lib/util.js"],"names":[],"mappings":"AAMA,8FA6BC;AAKD,gFA8BC;AAED,+CAKC"}
package/dist/util.js ADDED
@@ -0,0 +1,78 @@
1
+ // split_annotated_string(["Hello world", [[6,11, "strong"]]], 8)
2
+ // =>
3
+ // [
4
+ // ["Hello wo", [[6,8, "strong"]]]
5
+ // ["rld", [[0,3, "strong"]]]
6
+ // ]
7
+ export function split_annotated_string(text_with_annotations, at_position) {
8
+ const [text, annotations] = text_with_annotations;
9
+
10
+ // Split the text
11
+ const left_text = text.slice(0, at_position);
12
+ const right_text = text.slice(at_position);
13
+
14
+ const left_annotations = [];
15
+ const right_annotations = [];
16
+
17
+ // Process each annotation
18
+ for (const [start, end, type, data] of annotations) {
19
+ if (end <= at_position) {
20
+ // Annotation is entirely in the left part
21
+ left_annotations.push([start, end, type, data]);
22
+ } else if (start >= at_position) {
23
+ // Annotation is entirely in the right part - shift offsets
24
+ right_annotations.push([start - at_position, end - at_position, type, data]);
25
+ } else {
26
+ // Annotation spans the split point - split it
27
+ left_annotations.push([start, at_position, type, data]);
28
+ right_annotations.push([0, end - at_position, type, data]);
29
+ }
30
+ }
31
+
32
+ return [
33
+ [left_text, left_annotations],
34
+ [right_text, right_annotations]
35
+ ];
36
+ }
37
+
38
+
39
+ // join_annotated_string(["Hello wo", [[6,8, "strong"]]], ["rld", [[0,3, "strong"]]])
40
+ // => ["Hello world", [[6,11, "strong"]]]
41
+ export function join_annotated_string(first_text, second_text) {
42
+ const [first_text_content, first_annotations] = first_text;
43
+ const [second_text_content, second_annotations] = second_text;
44
+
45
+ // Join the text content
46
+ const joined_text = first_text_content + second_text_content;
47
+
48
+ // Start with all annotations from the first text (unchanged)
49
+ const joined_annotations = [...first_annotations];
50
+
51
+ // Add annotations from the second text, shifting their offsets
52
+ const offset = first_text_content.length;
53
+ for (const [start, end, type, data] of second_annotations) {
54
+ const shifted_annotation = [start + offset, end + offset, type, data];
55
+
56
+ // Check if this annotation can be merged with the last annotation from first text
57
+ const last_annotation = joined_annotations[joined_annotations.length - 1];
58
+ if (last_annotation &&
59
+ last_annotation[1] === shifted_annotation[0] && // annotations are adjacent
60
+ last_annotation[2] === shifted_annotation[2] && // same type
61
+ JSON.stringify(last_annotation[3]) === JSON.stringify(shifted_annotation[3])) { // same data
62
+ // Merge by extending the end position of the last annotation
63
+ last_annotation[1] = shifted_annotation[1];
64
+ } else {
65
+ // Add as separate annotation
66
+ joined_annotations.push(shifted_annotation);
67
+ }
68
+ }
69
+
70
+ return [joined_text, joined_annotations];
71
+ }
72
+
73
+ export function snake_to_pascal(str) {
74
+ return str
75
+ .split('_')
76
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
77
+ .join('');
78
+ }