svedit 0.3.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 +39 -21
- 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 +39 -16
- package/dist/Document.svelte.d.ts.map +1 -1
- package/dist/Document.svelte.js +192 -80
- package/dist/Svedit.svelte +149 -97
- package/dist/Svedit.svelte.d.ts.map +1 -1
- package/dist/Transaction.svelte.d.ts +4 -4
- package/dist/Transaction.svelte.d.ts.map +1 -1
- package/dist/Transaction.svelte.js +60 -77
- package/dist/commands.svelte.d.ts.map +1 -1
- package/dist/commands.svelte.js +18 -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 +16 -16
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +33 -34
- package/package.json +1 -1
- package/src/lib/{AnnotatedStringProperty.svelte → AnnotatedTextProperty.svelte} +28 -40
- package/src/lib/Document.svelte.js +192 -80
- package/src/lib/Svedit.svelte +149 -97
- package/src/lib/Transaction.svelte.js +60 -77
- package/src/lib/commands.svelte.js +18 -10
- package/src/lib/index.js +1 -1
- package/src/lib/types.d.ts +66 -21
- package/src/lib/util.js +33 -34
- package/dist/AnnotatedStringProperty.svelte.d.ts +0 -7
- package/dist/AnnotatedStringProperty.svelte.d.ts.map +0 -1
package/dist/util.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @import { Annotation } from './types.d.ts';
|
|
2
|
+
* @import { Annotation, AnnotatedText } from './types.d.ts';
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -120,25 +120,25 @@ export function char_to_utf16_offset(str, char_offset) {
|
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
/**
|
|
123
|
-
* Splits an annotated
|
|
123
|
+
* Splits an annotated text at the specified character position.
|
|
124
124
|
*
|
|
125
125
|
* Annotations that span the split point will be divided appropriately,
|
|
126
126
|
* with offsets adjusted for each resulting part.
|
|
127
127
|
*
|
|
128
|
-
* @param {
|
|
128
|
+
* @param {AnnotatedText} text_with_annotations - Annotated text object
|
|
129
129
|
* @param {number} at_position - Character position where to split (0-based)
|
|
130
|
-
* @returns {[
|
|
130
|
+
* @returns {[AnnotatedText, AnnotatedText]} Tuple of [left_part, right_part]
|
|
131
131
|
*
|
|
132
132
|
* @example
|
|
133
|
-
*
|
|
133
|
+
* split_annotated_text({text: "Hello world", annotations: [{start_offset: 6, end_offset: 11, node_id: "strong"}]}, 8)
|
|
134
134
|
* // Returns:
|
|
135
135
|
* // [
|
|
136
|
-
* //
|
|
137
|
-
* //
|
|
136
|
+
* // {text: "Hello wo", annotations: [{start_offset: 6, end_offset: 8, node_id: "strong"}]},
|
|
137
|
+
* // {text: "rld", annotations: [{start_offset: 0, end_offset: 3, node_id: "strong"}]}
|
|
138
138
|
* // ]
|
|
139
139
|
*/
|
|
140
|
-
export function
|
|
141
|
-
const
|
|
140
|
+
export function split_annotated_text(text_with_annotations, at_position) {
|
|
141
|
+
const {text, annotations} = text_with_annotations;
|
|
142
142
|
|
|
143
143
|
// Split the text using character-aware slicing
|
|
144
144
|
const left_text = char_slice(text, 0, at_position);
|
|
@@ -150,44 +150,44 @@ export function split_annotated_string(text_with_annotations, at_position) {
|
|
|
150
150
|
const right_annotations = [];
|
|
151
151
|
|
|
152
152
|
// Process each annotation
|
|
153
|
-
for (const
|
|
154
|
-
if (
|
|
153
|
+
for (const {start_offset, end_offset, node_id} of annotations) {
|
|
154
|
+
if (end_offset <= at_position) {
|
|
155
155
|
// Annotation is entirely in the left part
|
|
156
|
-
left_annotations.push(
|
|
157
|
-
} else if (
|
|
156
|
+
left_annotations.push({start_offset, end_offset, node_id});
|
|
157
|
+
} else if (start_offset >= at_position) {
|
|
158
158
|
// Annotation is entirely in the right part - shift offsets
|
|
159
|
-
right_annotations.push(
|
|
159
|
+
right_annotations.push({start_offset: start_offset - at_position, end_offset: end_offset - at_position, node_id});
|
|
160
160
|
} else {
|
|
161
161
|
// Annotation spans the split point - split it
|
|
162
|
-
left_annotations.push(
|
|
163
|
-
right_annotations.push(
|
|
162
|
+
left_annotations.push({start_offset, end_offset: at_position, node_id});
|
|
163
|
+
right_annotations.push({start_offset: 0, end_offset: end_offset - at_position, node_id});
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
return [
|
|
168
|
-
|
|
169
|
-
|
|
168
|
+
{text: left_text, annotations: left_annotations},
|
|
169
|
+
{text: right_text, annotations: right_annotations}
|
|
170
170
|
];
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
|
|
174
174
|
/**
|
|
175
|
-
* Joins two annotated
|
|
175
|
+
* Joins two annotated texts into a single annotated text.
|
|
176
176
|
*
|
|
177
177
|
* Annotations from the second text will have their offsets shifted by the length
|
|
178
178
|
* of the first text. Adjacent annotations of the same type and data will be merged.
|
|
179
179
|
*
|
|
180
|
-
* @param {
|
|
181
|
-
* @param {
|
|
182
|
-
* @returns {
|
|
180
|
+
* @param {AnnotatedText} first_text - First annotated text object
|
|
181
|
+
* @param {AnnotatedText} second_text - Second annotated text object
|
|
182
|
+
* @returns {AnnotatedText} Combined annotated text object
|
|
183
183
|
*
|
|
184
184
|
* @example
|
|
185
|
-
*
|
|
186
|
-
* // Returns:
|
|
185
|
+
* join_annotated_text({text: "Hello wo", annotations: [{start_offset: 6, end_offset: 8, node_id: "strong"}]}, {text: "rld", annotations: [{start_offset: 0, end_offset: 3, node_id: "strong"}]})
|
|
186
|
+
* // Returns: {text: "Hello world", annotations: [{start_offset: 6, end_offset: 11, node_id: "strong"}]}
|
|
187
187
|
*/
|
|
188
|
-
export function
|
|
189
|
-
const
|
|
190
|
-
const
|
|
188
|
+
export function join_annotated_text(first_text, second_text) {
|
|
189
|
+
const {text: first_text_content, annotations: first_annotations} = first_text;
|
|
190
|
+
const {text: second_text_content, annotations: second_annotations} = second_text;
|
|
191
191
|
|
|
192
192
|
// Join the text content
|
|
193
193
|
const joined_text = first_text_content + second_text_content;
|
|
@@ -198,25 +198,24 @@ export function join_annotated_string(first_text, second_text) {
|
|
|
198
198
|
|
|
199
199
|
// Add annotations from the second text, shifting their offsets
|
|
200
200
|
const offset = get_char_length(first_text_content);
|
|
201
|
-
for (const
|
|
201
|
+
for (const {start_offset, end_offset, node_id} of second_annotations) {
|
|
202
202
|
/** @type {Annotation} */
|
|
203
|
-
const shifted_annotation =
|
|
203
|
+
const shifted_annotation = {start_offset: start_offset + offset, end_offset: end_offset + offset, node_id};
|
|
204
204
|
|
|
205
205
|
// Check if this annotation can be merged with the last annotation from first text
|
|
206
206
|
const last_annotation = joined_annotations[joined_annotations.length - 1];
|
|
207
207
|
if (last_annotation &&
|
|
208
|
-
last_annotation
|
|
209
|
-
last_annotation
|
|
210
|
-
JSON.stringify(last_annotation[3]) === JSON.stringify(shifted_annotation[3])) { // same data
|
|
208
|
+
last_annotation.end_offset === shifted_annotation.start_offset && // annotations are adjacent
|
|
209
|
+
last_annotation.node_id === shifted_annotation.node_id) { // same node_id
|
|
211
210
|
// Merge by extending the end position of the last annotation
|
|
212
|
-
last_annotation
|
|
211
|
+
last_annotation.end_offset = shifted_annotation.end_offset;
|
|
213
212
|
} else {
|
|
214
213
|
// Add as separate annotation
|
|
215
214
|
joined_annotations.push(shifted_annotation);
|
|
216
215
|
}
|
|
217
216
|
}
|
|
218
217
|
|
|
219
|
-
return
|
|
218
|
+
return {text: joined_text, annotations: joined_annotations};
|
|
220
219
|
}
|
|
221
220
|
|
|
222
221
|
/**
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
import { getContext } from 'svelte';
|
|
3
|
-
import { char_slice, get_char_length } from './util.js';
|
|
3
|
+
import { char_slice, get_char_length, snake_to_pascal } from './util.js';
|
|
4
4
|
|
|
5
|
-
/** @import {
|
|
5
|
+
/** @import { AnnotatedTextPropertyProps, Annotation, AnnotationFragment } from './types.d.ts'; */
|
|
6
6
|
|
|
7
7
|
const svedit = getContext('svedit');
|
|
8
8
|
|
|
9
|
-
/** @type {
|
|
9
|
+
/** @type {AnnotatedTextPropertyProps} */
|
|
10
10
|
let {
|
|
11
11
|
path,
|
|
12
12
|
class: css_class,
|
|
@@ -20,32 +20,36 @@
|
|
|
20
20
|
/**
|
|
21
21
|
* Converts text with annotations into renderable fragments for display.
|
|
22
22
|
* @param {string} text - The plain text content
|
|
23
|
-
* @param {Array<Annotation>} annotations - Array of annotations where each is
|
|
23
|
+
* @param {Array<Annotation>} annotations - Array of annotations where each is {start_offset, end_offset, node_id}
|
|
24
24
|
* @returns {Array<string|AnnotationFragment>} Array of fragments - strings for plain text, AnnotationFragment objects for annotated content
|
|
25
25
|
*/
|
|
26
|
-
function
|
|
26
|
+
function get_fragments(text, annotations) {
|
|
27
27
|
let fragments = [];
|
|
28
28
|
let last_index = 0;
|
|
29
29
|
|
|
30
30
|
// Sort annotations by start_offset
|
|
31
|
-
const sorted_annotations = [...annotations].sort((a, b) => a
|
|
31
|
+
const sorted_annotations = [...annotations].sort((a, b) => a.start_offset - b.start_offset);
|
|
32
32
|
|
|
33
|
-
for (let [
|
|
33
|
+
for (let [, annotation] of sorted_annotations.entries()) {
|
|
34
34
|
// Add text before the annotation using character-aware slicing
|
|
35
|
-
if (annotation
|
|
36
|
-
fragments.push(char_slice(text, last_index, annotation
|
|
35
|
+
if (annotation.start_offset > last_index) {
|
|
36
|
+
fragments.push(char_slice(text, last_index, annotation.start_offset));
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
// Add the annotated string using character-aware slicing
|
|
40
|
-
const annotated_content = char_slice(text, annotation
|
|
40
|
+
const annotated_content = char_slice(text, annotation.start_offset, annotation.end_offset);
|
|
41
|
+
const node = svedit.doc.get(annotation.node_id);
|
|
42
|
+
if (!node) throw new Error(`Node not found for annotation ${annotation.node_id}`);
|
|
43
|
+
|
|
41
44
|
fragments.push({
|
|
42
|
-
|
|
45
|
+
node,
|
|
43
46
|
content: annotated_content,
|
|
44
|
-
|
|
45
|
-
|
|
47
|
+
// NOTE: We need to provide the original index here, because the source data
|
|
48
|
+
// is the address space.
|
|
49
|
+
annotation_index: annotations.indexOf(annotation),
|
|
46
50
|
});
|
|
47
51
|
|
|
48
|
-
last_index = annotation
|
|
52
|
+
last_index = annotation.end_offset;
|
|
49
53
|
}
|
|
50
54
|
|
|
51
55
|
// Add any remaining text after the last annotation using character-aware slicing
|
|
@@ -56,15 +60,9 @@
|
|
|
56
60
|
return fragments;
|
|
57
61
|
}
|
|
58
62
|
|
|
59
|
-
let fragments = $derived(
|
|
60
|
-
let plain_text = $derived(svedit.doc.get(path)
|
|
63
|
+
let fragments = $derived(get_fragments(svedit.doc.get(path).text, svedit.doc.get(path).annotations));
|
|
64
|
+
let plain_text = $derived(svedit.doc.get(path).text);
|
|
61
65
|
|
|
62
|
-
/**
|
|
63
|
-
* @param {MouseEvent} e - The click event
|
|
64
|
-
*/
|
|
65
|
-
function handle_link_click(e) {
|
|
66
|
-
e.preventDefault();
|
|
67
|
-
}
|
|
68
66
|
</script>
|
|
69
67
|
|
|
70
68
|
{#key plain_text}
|
|
@@ -79,21 +77,10 @@
|
|
|
79
77
|
placeholder={placeholder}
|
|
80
78
|
>
|
|
81
79
|
{#each fragments as fragment, index (index)}
|
|
82
|
-
{#if typeof fragment === 'string'}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
-->{:else if fragment.type === 'strong'}<!--
|
|
87
|
-
--><strong>{fragment.content}</strong><!--
|
|
88
|
-
-->{:else if fragment.type === 'link'}<!--
|
|
89
|
-
--><a
|
|
90
|
-
onclick={handle_link_click}
|
|
91
|
-
style="anchor-name: --{path.join('-') + '-' + fragment.annotation_index};"
|
|
92
|
-
href={fragment.data.href}
|
|
93
|
-
target={fragment.data.target || '_self'}>{fragment.content}</a><!--
|
|
94
|
-
-->{:else}<!--
|
|
95
|
-
-->{fragment.content}<!--
|
|
96
|
-
-->{/if}
|
|
80
|
+
{#if typeof fragment === 'string'}{fragment}{:else}
|
|
81
|
+
{@const AnnotationComponent = svedit.doc.config.node_components[snake_to_pascal(fragment.node.type)]}
|
|
82
|
+
<AnnotationComponent path={[...path, 'annotations', fragment.annotation_index, 'node_id']} content={fragment.content} />
|
|
83
|
+
{/if}
|
|
97
84
|
{/each}<!--
|
|
98
85
|
--><br>
|
|
99
86
|
</div>
|
|
@@ -118,12 +105,13 @@
|
|
|
118
105
|
|
|
119
106
|
/* We can safely hide the <br> element when the placeholder is empty and focused. */
|
|
120
107
|
/* No longer the cursor will be rendered after the placeholder when focused. */
|
|
108
|
+
/* ATTENTION: This hack has the side effect that in Safari Desktop the cursor sometimes disappears. */
|
|
121
109
|
[placeholder].empty.focused br {
|
|
122
110
|
display:none;
|
|
123
111
|
}
|
|
124
112
|
|
|
125
|
-
|
|
113
|
+
/*.text.focused {
|
|
126
114
|
background: none;
|
|
127
|
-
outline: 1px
|
|
128
|
-
}
|
|
115
|
+
outline: 1px solid var(--editing-stroke-color);
|
|
116
|
+
}*/
|
|
129
117
|
</style>
|