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.
- package/LICENSE +21 -0
- package/README.md +323 -0
- package/dist/AnnotatedStringProperty.svelte +126 -0
- package/dist/AnnotatedStringProperty.svelte.d.ts +7 -0
- package/dist/AnnotatedStringProperty.svelte.d.ts.map +1 -0
- package/dist/CustomProperty.svelte +40 -0
- package/dist/CustomProperty.svelte.d.ts +8 -0
- package/dist/CustomProperty.svelte.d.ts.map +1 -0
- package/dist/Document.svelte.d.ts +191 -0
- package/dist/Document.svelte.d.ts.map +1 -0
- package/dist/Document.svelte.js +681 -0
- package/dist/Node.svelte +41 -0
- package/dist/Node.svelte.d.ts +7 -0
- package/dist/Node.svelte.d.ts.map +1 -0
- package/dist/NodeArrayProperty.svelte +45 -0
- package/dist/NodeArrayProperty.svelte.d.ts +7 -0
- package/dist/NodeArrayProperty.svelte.d.ts.map +1 -0
- package/dist/Svedit.svelte +936 -0
- package/dist/Svedit.svelte.d.ts +12 -0
- package/dist/Svedit.svelte.d.ts.map +1 -0
- package/dist/Transaction.svelte.d.ts +198 -0
- package/dist/Transaction.svelte.d.ts.map +1 -0
- package/dist/Transaction.svelte.js +577 -0
- package/dist/UnknownNode.svelte +12 -0
- package/dist/UnknownNode.svelte.d.ts +12 -0
- package/dist/UnknownNode.svelte.d.ts.map +1 -0
- package/dist/commands.svelte.d.ts +5 -0
- package/dist/commands.svelte.d.ts.map +1 -0
- package/dist/commands.svelte.js +261 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/types.d.ts +264 -0
- package/dist/util.d.ts +4 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +78 -0
- package/package.json +65 -0
- package/src/lib/AnnotatedStringProperty.svelte +126 -0
- package/src/lib/CustomProperty.svelte +40 -0
- package/src/lib/Document.svelte.js +681 -0
- package/src/lib/Node.svelte +41 -0
- package/src/lib/NodeArrayProperty.svelte +45 -0
- package/src/lib/Svedit.svelte +936 -0
- package/src/lib/Transaction.svelte.js +577 -0
- package/src/lib/UnknownNode.svelte +12 -0
- package/src/lib/commands.svelte.js +261 -0
- package/src/lib/index.js +14 -0
- package/src/lib/types.d.ts +264 -0
- 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
|
+
}
|
package/src/lib/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/src/lib/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
|
+
}
|