svedit 0.12.0 → 0.14.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 +35 -22
- package/dist/Command.svelte.d.ts +38 -30
- package/dist/Command.svelte.d.ts.map +1 -1
- package/dist/Command.svelte.js +271 -326
- package/dist/CustomProperty.svelte +3 -5
- package/dist/CustomProperty.svelte.d.ts +3 -6
- package/dist/CustomProperty.svelte.d.ts.map +1 -1
- package/dist/KeyMapper.svelte.d.ts +19 -6
- package/dist/KeyMapper.svelte.d.ts.map +1 -1
- package/dist/KeyMapper.svelte.js +95 -112
- package/dist/Node.svelte +16 -13
- package/dist/Node.svelte.d.ts +3 -5
- package/dist/Node.svelte.d.ts.map +1 -1
- package/dist/NodeArrayProperty.svelte +39 -22
- package/dist/NodeArrayProperty.svelte.d.ts +3 -5
- package/dist/NodeArrayProperty.svelte.d.ts.map +1 -1
- package/dist/NodeCaret.svelte +2 -2
- package/dist/NodeCaret.svelte.d.ts +6 -14
- package/dist/NodeCaret.svelte.d.ts.map +1 -1
- package/dist/NodeGap.svelte +12 -6
- package/dist/NodeGap.svelte.d.ts +7 -14
- package/dist/NodeGap.svelte.d.ts.map +1 -1
- package/dist/NodeGapMarkers.svelte +36 -14
- package/dist/NodeGapMarkers.svelte.d.ts +6 -11
- package/dist/NodeGapMarkers.svelte.d.ts.map +1 -1
- package/dist/NodeSelectionMarkers.svelte +21 -7
- package/dist/NodeSelectionMarkers.svelte.d.ts +2 -5
- package/dist/NodeSelectionMarkers.svelte.d.ts.map +1 -1
- package/dist/Session.svelte.d.ts +81 -129
- package/dist/Session.svelte.d.ts.map +1 -1
- package/dist/Session.svelte.js +542 -648
- package/dist/Svedit.svelte +146 -140
- package/dist/Svedit.svelte.d.ts +30 -10
- package/dist/Svedit.svelte.d.ts.map +1 -1
- package/dist/TextProperty.svelte +37 -23
- package/dist/TextProperty.svelte.d.ts +3 -5
- package/dist/TextProperty.svelte.d.ts.map +1 -1
- package/dist/Transaction.svelte.d.ts +64 -127
- package/dist/Transaction.svelte.d.ts.map +1 -1
- package/dist/Transaction.svelte.js +839 -1001
- package/dist/UnknownNode.svelte +4 -3
- package/dist/UnknownNode.svelte.d.ts +5 -9
- package/dist/UnknownNode.svelte.d.ts.map +1 -1
- package/dist/doc_utils.d.ts +67 -155
- package/dist/doc_utils.d.ts.map +1 -1
- package/dist/doc_utils.js +619 -863
- package/dist/index.d.ts +20 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -17
- package/dist/node_visibility.svelte.d.ts +110 -9
- package/dist/node_visibility.svelte.d.ts.map +1 -1
- package/dist/node_visibility.svelte.js +473 -544
- package/dist/paste_utils.d.ts +11 -44
- package/dist/paste_utils.d.ts.map +1 -1
- package/dist/paste_utils.js +111 -152
- package/dist/styles/svedit-colors.css +8 -5
- package/dist/transforms.svelte.d.ts +9 -7
- package/dist/transforms.svelte.d.ts.map +1 -1
- package/dist/transforms.svelte.js +125 -150
- package/dist/types.d.ts +294 -309
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +40 -119
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +346 -473
- package/package.json +70 -70
- package/src/lib/{Command.svelte.js → Command.svelte.ts} +29 -17
- package/src/lib/CustomProperty.svelte +3 -5
- package/src/lib/{KeyMapper.svelte.js → KeyMapper.svelte.ts} +28 -11
- package/src/lib/Node.svelte +16 -13
- package/src/lib/NodeArrayProperty.svelte +39 -22
- package/src/lib/NodeCaret.svelte +2 -2
- package/src/lib/NodeGap.svelte +12 -6
- package/src/lib/NodeGapMarkers.svelte +36 -14
- package/src/lib/NodeSelectionMarkers.svelte +21 -7
- package/src/lib/{Session.svelte.js → Session.svelte.ts} +130 -127
- package/src/lib/Svedit.svelte +146 -140
- package/src/lib/TextProperty.svelte +37 -23
- package/src/lib/{Transaction.svelte.js → Transaction.svelte.ts} +139 -143
- package/src/lib/UnknownNode.svelte +4 -3
- package/src/lib/{doc_utils.js → doc_utils.ts} +247 -206
- package/src/lib/{index.js → index.ts} +48 -0
- package/src/lib/{node_visibility.svelte.js → node_visibility.svelte.ts} +68 -92
- package/src/lib/{paste_utils.js → paste_utils.ts} +47 -58
- package/src/lib/styles/svedit-colors.css +8 -5
- package/src/lib/{transforms.svelte.js → transforms.svelte.ts} +39 -21
- package/src/lib/{types.d.ts → types.ts} +161 -66
- package/src/lib/{utils.js → utils.ts} +110 -129
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import type {
|
|
2
|
+
Attachment,
|
|
3
|
+
Mark,
|
|
4
|
+
Text,
|
|
5
|
+
SelectionRange,
|
|
6
|
+
Selection,
|
|
7
|
+
DocumentSchema,
|
|
8
|
+
DocumentNode,
|
|
9
|
+
DocumentPath
|
|
10
|
+
} from './types.js';
|
|
4
11
|
|
|
5
12
|
const SEGMENTER = new Intl.Segmenter('en', { granularity: 'grapheme' });
|
|
6
13
|
|
|
@@ -8,21 +15,28 @@ const SEGMENTER = new Intl.Segmenter('en', { granularity: 'grapheme' });
|
|
|
8
15
|
* Detect if the virtual keyboard is likely visible.
|
|
9
16
|
*
|
|
10
17
|
* This is a heuristic based on the visual viewport becoming smaller than the
|
|
11
|
-
* layout viewport.
|
|
12
|
-
*
|
|
13
|
-
*
|
|
18
|
+
* layout viewport. A tolerance is required because clientHeight is a rounded
|
|
19
|
+
* integer while visualViewport.height is fractional (e.g. on Firefox with
|
|
20
|
+
* HiDPI scaling), so a strict comparison can misfire on desktop. A real
|
|
21
|
+
* virtual keyboard shrinks the viewport by far more than the tolerance.
|
|
14
22
|
*/
|
|
15
|
-
|
|
23
|
+
const VIRTUAL_KEYBOARD_MIN_HEIGHT = 50;
|
|
24
|
+
|
|
25
|
+
export function is_virtual_keyboard_active(): boolean {
|
|
16
26
|
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
|
17
27
|
return false;
|
|
18
28
|
}
|
|
19
29
|
|
|
30
|
+
if (!is_mobile_browser()) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
20
34
|
const visual_viewport = window.visualViewport;
|
|
21
35
|
if (!visual_viewport) {
|
|
22
36
|
return false;
|
|
23
37
|
}
|
|
24
38
|
|
|
25
|
-
return visual_viewport.height < document.documentElement.clientHeight;
|
|
39
|
+
return visual_viewport.height < document.documentElement.clientHeight - VIRTUAL_KEYBOARD_MIN_HEIGHT;
|
|
26
40
|
}
|
|
27
41
|
|
|
28
42
|
/**
|
|
@@ -30,10 +44,8 @@ export function is_virtual_keyboard_active() {
|
|
|
30
44
|
*
|
|
31
45
|
* This uses the user agent only so touch-capable laptops are not treated as
|
|
32
46
|
* mobile browsers.
|
|
33
|
-
*
|
|
34
|
-
* @returns {boolean} true if mobile browser, false otherwise
|
|
35
47
|
*/
|
|
36
|
-
export function is_mobile_browser() {
|
|
48
|
+
export function is_mobile_browser(): boolean {
|
|
37
49
|
if (typeof navigator === 'undefined') {
|
|
38
50
|
return false;
|
|
39
51
|
}
|
|
@@ -45,9 +57,8 @@ export function is_mobile_browser() {
|
|
|
45
57
|
// ‼️‼️‼️‼️‼️‼️ UNUSED UTILITY BELOW ‼️‼️‼️‼️‼️‼️
|
|
46
58
|
/**
|
|
47
59
|
* Detect if the current browser is Chrome on desktop
|
|
48
|
-
* @returns {boolean} true if Chrome desktop browser, false otherwise
|
|
49
60
|
*/
|
|
50
|
-
export function is_chrome_desktop_browser() {
|
|
61
|
+
export function is_chrome_desktop_browser(): boolean {
|
|
51
62
|
if (typeof window === 'undefined' || typeof navigator === 'undefined') {
|
|
52
63
|
return false;
|
|
53
64
|
}
|
|
@@ -63,15 +74,12 @@ export function is_chrome_desktop_browser() {
|
|
|
63
74
|
* Uses Intl.Segmenter to count grapheme clusters rather than UTF-16 code units,
|
|
64
75
|
* ensuring emojis and other complex Unicode sequences are counted as single characters.
|
|
65
76
|
*
|
|
66
|
-
* @param {string} str - The string to measure
|
|
67
|
-
* @returns {number} The number of visual characters (grapheme clusters)
|
|
68
|
-
*
|
|
69
77
|
* @example
|
|
70
78
|
* get_char_length('Hello') // Returns: 5
|
|
71
79
|
* get_char_length('a😀b') // Returns: 3 (not 4)
|
|
72
80
|
* get_char_length('👋🏽') // Returns: 1 (not 4 - skin tone modifier treated as single char)
|
|
73
81
|
*/
|
|
74
|
-
export function get_char_length(str) {
|
|
82
|
+
export function get_char_length(str: string): number {
|
|
75
83
|
return [...SEGMENTER.segment(str)].length;
|
|
76
84
|
}
|
|
77
85
|
|
|
@@ -82,16 +90,12 @@ export function get_char_length(str) {
|
|
|
82
90
|
* Uses Intl.Segmenter to access grapheme clusters rather than UTF-16 code units,
|
|
83
91
|
* ensuring emojis and other complex Unicode sequences are treated as single characters.
|
|
84
92
|
*
|
|
85
|
-
* @param {string} str - The string to access
|
|
86
|
-
* @param {number} index - Character position (0-based)
|
|
87
|
-
* @returns {string} The character at the specified position, or empty string if index is out of bounds
|
|
88
|
-
*
|
|
89
93
|
* @example
|
|
90
94
|
* get_char_at('Hello', 1) // Returns: 'e'
|
|
91
95
|
* get_char_at('a😀b', 1) // Returns: '😀' (full emoji)
|
|
92
96
|
* get_char_at('👋🏽', 0) // Returns: '👋🏽' (skin tone modifier included)
|
|
93
97
|
*/
|
|
94
|
-
export function get_char_at(str, index) {
|
|
98
|
+
export function get_char_at(str: string, index: number): string {
|
|
95
99
|
const segments = [...SEGMENTER.segment(str)];
|
|
96
100
|
return segments[index].segment;
|
|
97
101
|
}
|
|
@@ -102,16 +106,15 @@ export function get_char_at(str, index) {
|
|
|
102
106
|
* Uses Intl.Segmenter to slice by grapheme clusters rather than UTF-16 code units,
|
|
103
107
|
* ensuring emojis and other complex Unicode sequences remain intact.
|
|
104
108
|
*
|
|
105
|
-
* @param {string} str - The string to slice
|
|
106
|
-
* @param {number} start - Starting character position (inclusive)
|
|
107
|
-
* @param {number} [end] - Ending character position (exclusive). If undefined, slices to end
|
|
108
|
-
* @returns {string} The sliced string with complete characters
|
|
109
|
-
*
|
|
110
109
|
* @example
|
|
111
110
|
* char_slice('Hello 😀 World', 6, 8) // Returns: '😀 ' (emoji stays intact)
|
|
112
111
|
* char_slice('a👋🏽b', 1, 2) // Returns: '👋🏽' (skin tone modifier included)
|
|
113
112
|
*/
|
|
114
|
-
export function char_slice(
|
|
113
|
+
export function char_slice(
|
|
114
|
+
str: string,
|
|
115
|
+
start: number,
|
|
116
|
+
end: number | undefined = undefined
|
|
117
|
+
): string {
|
|
115
118
|
const segments = [...SEGMENTER.segment(str)];
|
|
116
119
|
return segments
|
|
117
120
|
.slice(start, end)
|
|
@@ -125,15 +128,11 @@ export function char_slice(str, start, end = undefined) {
|
|
|
125
128
|
* Converts DOM selection offsets (which use UTF-16 code units) to character-based
|
|
126
129
|
* offsets (grapheme clusters) for consistent text manipulation.
|
|
127
130
|
*
|
|
128
|
-
* @param {string} str - The string to convert offsets within
|
|
129
|
-
* @param {number} utf16_offset - The UTF-16 code unit offset from DOM
|
|
130
|
-
* @returns {number} The corresponding grapheme cluster offset
|
|
131
|
-
*
|
|
132
131
|
* @example
|
|
133
132
|
* // For string "a😀b" where 😀 uses 2 UTF-16 code units
|
|
134
133
|
* utf16_to_char_offset("a😀b", 3) // Returns: 2 (position after emoji)
|
|
135
134
|
*/
|
|
136
|
-
export function utf16_to_char_offset(str, utf16_offset) {
|
|
135
|
+
export function utf16_to_char_offset(str: string, utf16_offset: number): number {
|
|
137
136
|
const segments = [...SEGMENTER.segment(str)];
|
|
138
137
|
let char_offset = 0;
|
|
139
138
|
let utf16_count = 0;
|
|
@@ -154,15 +153,11 @@ export function utf16_to_char_offset(str, utf16_offset) {
|
|
|
154
153
|
* Converts character-based offsets (grapheme clusters) to DOM selection offsets
|
|
155
154
|
* (UTF-16 code units) for proper DOM selection positioning.
|
|
156
155
|
*
|
|
157
|
-
* @param {string} str - The string to convert offsets within
|
|
158
|
-
* @param {number} char_offset - The grapheme cluster offset
|
|
159
|
-
* @returns {number} The corresponding UTF-16 code unit offset for DOM operations
|
|
160
|
-
*
|
|
161
156
|
* @example
|
|
162
157
|
* // For string "a😀b" where 😀 uses 2 UTF-16 code units
|
|
163
158
|
* char_to_utf16_offset("a😀b", 2) // Returns: 3 (UTF-16 position after emoji)
|
|
164
159
|
*/
|
|
165
|
-
export function char_to_utf16_offset(str, char_offset) {
|
|
160
|
+
export function char_to_utf16_offset(str: string, char_offset: number): number {
|
|
166
161
|
const segments = [...SEGMENTER.segment(str)];
|
|
167
162
|
let utf16_offset = 0;
|
|
168
163
|
|
|
@@ -174,15 +169,11 @@ export function char_to_utf16_offset(str, char_offset) {
|
|
|
174
169
|
}
|
|
175
170
|
|
|
176
171
|
/**
|
|
177
|
-
* Splits
|
|
172
|
+
* Splits a text value at the specified character position.
|
|
178
173
|
*
|
|
179
174
|
* Marks and annotations that span the split point will be divided
|
|
180
175
|
* appropriately, with offsets adjusted for each resulting part.
|
|
181
176
|
*
|
|
182
|
-
* @param {AnnotatedText} text_value - Annotated text object
|
|
183
|
-
* @param {number} at_position - Character position where to split (0-based)
|
|
184
|
-
* @returns {[AnnotatedText, AnnotatedText]} Tuple of [left_part, right_part]
|
|
185
|
-
*
|
|
186
177
|
* @example
|
|
187
178
|
* split_text({content: "Hello world", marks: [{start_offset: 6, end_offset: 11, node_id: "strong"}], annotations: []}, 8)
|
|
188
179
|
* // Returns:
|
|
@@ -191,15 +182,23 @@ export function char_to_utf16_offset(str, char_offset) {
|
|
|
191
182
|
* // {content: "rld", marks: [{start_offset: 0, end_offset: 3, node_id: "strong"}], annotations: []}
|
|
192
183
|
* // ]
|
|
193
184
|
*/
|
|
194
|
-
export function split_text(text_value, at_position) {
|
|
185
|
+
export function split_text(text_value: Text, at_position: number): [Text, Text] {
|
|
195
186
|
const { content } = text_value;
|
|
196
187
|
|
|
197
188
|
// Split the text using character-aware slicing
|
|
198
|
-
const left = {
|
|
199
|
-
|
|
189
|
+
const left: Text = {
|
|
190
|
+
content: char_slice(content, 0, at_position),
|
|
191
|
+
marks: [],
|
|
192
|
+
annotations: []
|
|
193
|
+
};
|
|
194
|
+
const right: Text = {
|
|
195
|
+
content: char_slice(content, at_position),
|
|
196
|
+
marks: [],
|
|
197
|
+
annotations: []
|
|
198
|
+
};
|
|
200
199
|
|
|
201
200
|
// Process marks and annotations with the same range logic
|
|
202
|
-
for (const key of ['marks', 'annotations']) {
|
|
201
|
+
for (const key of ['marks', 'annotations'] as const) {
|
|
203
202
|
for (const { start_offset, end_offset, node_id } of text_value[key] ?? []) {
|
|
204
203
|
if (end_offset <= at_position) {
|
|
205
204
|
// Range is entirely in the left part
|
|
@@ -223,23 +222,19 @@ export function split_text(text_value, at_position) {
|
|
|
223
222
|
}
|
|
224
223
|
|
|
225
224
|
/**
|
|
226
|
-
* Joins two
|
|
225
|
+
* Joins two text values into a single text value.
|
|
227
226
|
*
|
|
228
227
|
* Marks and annotations from the second text will have their offsets shifted
|
|
229
228
|
* by the length of the first text. Adjacent ranges referencing the same node
|
|
230
229
|
* will be merged.
|
|
231
230
|
*
|
|
232
|
-
* @param {AnnotatedText} first_text - First annotated text object
|
|
233
|
-
* @param {AnnotatedText} second_text - Second annotated text object
|
|
234
|
-
* @returns {AnnotatedText} Combined annotated text object
|
|
235
|
-
*
|
|
236
231
|
* @example
|
|
237
232
|
* join_text({content: "Hello wo", marks: [{start_offset: 6, end_offset: 8, node_id: "strong"}], annotations: []}, {content: "rld", marks: [{start_offset: 0, end_offset: 3, node_id: "strong"}], annotations: []})
|
|
238
233
|
* // Returns: {content: "Hello world", marks: [{start_offset: 6, end_offset: 11, node_id: "strong"}], annotations: []}
|
|
239
234
|
*/
|
|
240
|
-
export function join_text(first_text, second_text) {
|
|
235
|
+
export function join_text(first_text: Text, second_text: Text): Text {
|
|
241
236
|
// Join the text content
|
|
242
|
-
const joined = {
|
|
237
|
+
const joined: Text = {
|
|
243
238
|
content: first_text.content + second_text.content,
|
|
244
239
|
marks: [],
|
|
245
240
|
annotations: []
|
|
@@ -247,7 +242,7 @@ export function join_text(first_text, second_text) {
|
|
|
247
242
|
|
|
248
243
|
const offset = get_char_length(first_text.content);
|
|
249
244
|
|
|
250
|
-
for (const key of ['marks', 'annotations']) {
|
|
245
|
+
for (const key of ['marks', 'annotations'] as const) {
|
|
251
246
|
// Start with all ranges from the first text (unchanged)
|
|
252
247
|
const joined_ranges = (first_text[key] ?? []).map((range) => ({ ...range }));
|
|
253
248
|
|
|
@@ -284,13 +279,10 @@ export function join_text(first_text, second_text) {
|
|
|
284
279
|
/**
|
|
285
280
|
* Convert snake_case string to PascalCase
|
|
286
281
|
*
|
|
287
|
-
* @param {string} str - The snake_case string to convert
|
|
288
|
-
* @returns {string} The converted PascalCase string
|
|
289
|
-
*
|
|
290
282
|
* @example
|
|
291
283
|
* snake_to_pascal('list_item') // Returns: 'ListItem'
|
|
292
284
|
*/
|
|
293
|
-
export function snake_to_pascal(str) {
|
|
285
|
+
export function snake_to_pascal(str: string): string {
|
|
294
286
|
return str
|
|
295
287
|
.split('_')
|
|
296
288
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
@@ -307,11 +299,8 @@ const PATH_INDEX_SEGMENT_RE = /^(0|[1-9]\d*)$/;
|
|
|
307
299
|
* Path string segments include node ids and schema property names. Keeping
|
|
308
300
|
* them restricted makes serialized paths reversible, valid as HTML ids, and
|
|
309
301
|
* safe to use as CSS anchor-name suffixes.
|
|
310
|
-
*
|
|
311
|
-
* @param {string} segment
|
|
312
|
-
* @returns {boolean}
|
|
313
302
|
*/
|
|
314
|
-
export function is_path_string_segment_valid(segment) {
|
|
303
|
+
export function is_path_string_segment_valid(segment: string): boolean {
|
|
315
304
|
return (
|
|
316
305
|
typeof segment === 'string' &&
|
|
317
306
|
PATH_STRING_SEGMENT_RE.test(segment) &&
|
|
@@ -321,12 +310,8 @@ export function is_path_string_segment_valid(segment) {
|
|
|
321
310
|
|
|
322
311
|
/**
|
|
323
312
|
* Assert that a string can be used as a Svedit path segment.
|
|
324
|
-
*
|
|
325
|
-
* @param {string} segment
|
|
326
|
-
* @param {string} [label]
|
|
327
|
-
* @returns {void}
|
|
328
313
|
*/
|
|
329
|
-
export function assert_path_string_segment(segment, label = 'Path segment') {
|
|
314
|
+
export function assert_path_string_segment(segment: string, label = 'Path segment'): void {
|
|
330
315
|
if (!is_path_string_segment_valid(segment)) {
|
|
331
316
|
throw new Error(
|
|
332
317
|
`${label} must start with a letter or underscore and contain only letters, numbers, underscores, or dashes. It must not contain "${PATH_SEPARATOR}".`
|
|
@@ -341,13 +326,10 @@ export function assert_path_string_segment(segment, label = 'Path segment') {
|
|
|
341
326
|
* The result is reversible and safe to use as a data attribute value, HTML id,
|
|
342
327
|
* or CSS anchor-name suffix.
|
|
343
328
|
*
|
|
344
|
-
* @param {Array<string | number>} path
|
|
345
|
-
* @returns {string}
|
|
346
|
-
*
|
|
347
329
|
* @example
|
|
348
330
|
* serialize_path(['page_1', 'body', 0, 'items']) // Returns: 'page_1__body__0__items'
|
|
349
331
|
*/
|
|
350
|
-
export function serialize_path(path) {
|
|
332
|
+
export function serialize_path(path: DocumentPath): string {
|
|
351
333
|
return path
|
|
352
334
|
.map((segment) => {
|
|
353
335
|
if (typeof segment === 'number') {
|
|
@@ -366,13 +348,10 @@ export function serialize_path(path) {
|
|
|
366
348
|
/**
|
|
367
349
|
* Deserialize a path string produced by serialize_path.
|
|
368
350
|
*
|
|
369
|
-
* @param {string} serialized_path
|
|
370
|
-
* @returns {Array<string | number>}
|
|
371
|
-
*
|
|
372
351
|
* @example
|
|
373
352
|
* deserialize_path('page_1__body__0__items') // Returns: ['page_1', 'body', 0, 'items']
|
|
374
353
|
*/
|
|
375
|
-
export function deserialize_path(serialized_path) {
|
|
354
|
+
export function deserialize_path(serialized_path: string): DocumentPath {
|
|
376
355
|
if (serialized_path === '') return [];
|
|
377
356
|
|
|
378
357
|
return serialized_path.split(PATH_SEPARATOR).map((segment) => {
|
|
@@ -394,12 +373,8 @@ export function deserialize_path(serialized_path) {
|
|
|
394
373
|
|
|
395
374
|
/**
|
|
396
375
|
* Compare two document paths segment-by-segment, preserving segment types.
|
|
397
|
-
*
|
|
398
|
-
* @param {Array<string | number>} a
|
|
399
|
-
* @param {Array<string | number>} b
|
|
400
|
-
* @returns {boolean}
|
|
401
376
|
*/
|
|
402
|
-
export function paths_equal(a, b) {
|
|
377
|
+
export function paths_equal(a: DocumentPath, b: DocumentPath): boolean {
|
|
403
378
|
if (a.length !== b.length) return false;
|
|
404
379
|
return a.every((segment, index) => segment === b[index]);
|
|
405
380
|
}
|
|
@@ -410,15 +385,16 @@ export function paths_equal(a, b) {
|
|
|
410
385
|
* reference bookkeeping) — traverse() deep-clones every visited node,
|
|
411
386
|
* which is wasteful for id-only consumers.
|
|
412
387
|
*
|
|
413
|
-
* @
|
|
414
|
-
* @param {object} schema - The document schema
|
|
415
|
-
* @param {Record<string, any>} nodes - All document nodes
|
|
416
|
-
* @returns {string[]} Node ids in depth-first order (entry point last)
|
|
388
|
+
* @returns Node ids in depth-first order (entry point last)
|
|
417
389
|
*/
|
|
418
|
-
export function traverse_ids(
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
390
|
+
export function traverse_ids(
|
|
391
|
+
node_id: string,
|
|
392
|
+
schema: DocumentSchema,
|
|
393
|
+
nodes: Record<string, DocumentNode>
|
|
394
|
+
): string[] {
|
|
395
|
+
const ids: string[] = [];
|
|
396
|
+
const visited: Record<string, boolean> = {};
|
|
397
|
+
const visit = (node: DocumentNode | undefined) => {
|
|
422
398
|
if (!node || visited[node.id]) {
|
|
423
399
|
return;
|
|
424
400
|
}
|
|
@@ -449,10 +425,14 @@ export function traverse_ids(node_id, schema, nodes) {
|
|
|
449
425
|
return ids;
|
|
450
426
|
}
|
|
451
427
|
|
|
452
|
-
export function traverse(
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
428
|
+
export function traverse(
|
|
429
|
+
node_id: string,
|
|
430
|
+
schema: DocumentSchema,
|
|
431
|
+
nodes: Record<string, DocumentNode>
|
|
432
|
+
): DocumentNode[] {
|
|
433
|
+
const json: DocumentNode[] = [];
|
|
434
|
+
const visited: Record<string, boolean> = {};
|
|
435
|
+
const visit = (node: DocumentNode | undefined) => {
|
|
456
436
|
if (!node || visited[node.id]) {
|
|
457
437
|
return;
|
|
458
438
|
}
|
|
@@ -492,10 +472,9 @@ export function traverse(node_id, schema, nodes) {
|
|
|
492
472
|
/**
|
|
493
473
|
* Extracts the normalized range from a text or node selection.
|
|
494
474
|
* Returns start and end offsets in document order (start <= end), regardless of selection direction.
|
|
495
|
-
*
|
|
496
|
-
* @returns {SelectionRange | null} The normalized selection range, or null if selection is null, undefined, or a property selection
|
|
475
|
+
* Returns null if selection is null, undefined, or a property selection.
|
|
497
476
|
*/
|
|
498
|
-
export function get_selection_range(selection) {
|
|
477
|
+
export function get_selection_range(selection?: Selection | null): SelectionRange | null {
|
|
499
478
|
if (selection && selection.type !== 'property') {
|
|
500
479
|
return {
|
|
501
480
|
start_offset: Math.min(selection.anchor_offset, selection.focus_offset),
|
|
@@ -506,7 +485,7 @@ export function get_selection_range(selection) {
|
|
|
506
485
|
}
|
|
507
486
|
}
|
|
508
487
|
|
|
509
|
-
export function is_selection_collapsed(selection) {
|
|
488
|
+
export function is_selection_collapsed(selection?: Selection | null): boolean {
|
|
510
489
|
if (selection && selection.type !== 'property') {
|
|
511
490
|
return selection.anchor_offset === selection.focus_offset;
|
|
512
491
|
} else {
|
|
@@ -517,16 +496,15 @@ export function is_selection_collapsed(selection) {
|
|
|
517
496
|
/**
|
|
518
497
|
* Adjust ranges (marks or annotations) after deleting a sequence range.
|
|
519
498
|
*
|
|
520
|
-
* Works for both
|
|
499
|
+
* Works for both text values (character offsets) and node arrays
|
|
521
500
|
* (node offsets).
|
|
522
|
-
*
|
|
523
|
-
* @param {Array<import('./types.d.ts').Attachment>} ranges
|
|
524
|
-
* @param {number} start
|
|
525
|
-
* @param {number} end
|
|
526
|
-
* @returns {{ranges: Array<import('./types.d.ts').Attachment>, removed_node_ids: string[]}}
|
|
527
501
|
*/
|
|
528
|
-
export function adjust_ranges_for_deletion(
|
|
529
|
-
|
|
502
|
+
export function adjust_ranges_for_deletion(
|
|
503
|
+
ranges: Array<Attachment>,
|
|
504
|
+
start: number,
|
|
505
|
+
end: number
|
|
506
|
+
): { ranges: Array<Attachment>; removed_node_ids: string[] } {
|
|
507
|
+
const removed_node_ids: string[] = [];
|
|
530
508
|
const deletion_length = end - start;
|
|
531
509
|
|
|
532
510
|
const next_ranges = ranges
|
|
@@ -554,7 +532,7 @@ export function adjust_ranges_for_deletion(ranges, start, end) {
|
|
|
554
532
|
|
|
555
533
|
return { start_offset, end_offset, node_id: range.node_id };
|
|
556
534
|
})
|
|
557
|
-
.filter(
|
|
535
|
+
.filter((range): range is Attachment => range !== null);
|
|
558
536
|
|
|
559
537
|
return { ranges: next_ranges, removed_node_ids };
|
|
560
538
|
}
|
|
@@ -564,13 +542,12 @@ export function adjust_ranges_for_deletion(ranges, start, end) {
|
|
|
564
542
|
*
|
|
565
543
|
* Insertion inside a range extends it. Insertion exactly at either edge
|
|
566
544
|
* stays outside the range.
|
|
567
|
-
*
|
|
568
|
-
* @param {Array<import('./types.d.ts').Attachment>} ranges
|
|
569
|
-
* @param {number} offset
|
|
570
|
-
* @param {number} length
|
|
571
|
-
* @returns {Array<import('./types.d.ts').Attachment>}
|
|
572
545
|
*/
|
|
573
|
-
export function adjust_ranges_for_insertion(
|
|
546
|
+
export function adjust_ranges_for_insertion(
|
|
547
|
+
ranges: Array<Attachment>,
|
|
548
|
+
offset: number,
|
|
549
|
+
length: number
|
|
550
|
+
): Array<Attachment> {
|
|
574
551
|
if (length === 0) return ranges;
|
|
575
552
|
|
|
576
553
|
return ranges.map((range) => {
|
|
@@ -594,12 +571,8 @@ export function adjust_ranges_for_insertion(ranges, offset, length) {
|
|
|
594
571
|
|
|
595
572
|
/**
|
|
596
573
|
* Check whether ranges are non-empty and mutually exclusive.
|
|
597
|
-
*
|
|
598
|
-
* @param {Array<import('./types.d.ts').Attachment>} ranges
|
|
599
|
-
* @param {number} [length]
|
|
600
|
-
* @returns {boolean}
|
|
601
574
|
*/
|
|
602
|
-
export function are_ranges_exclusive(ranges
|
|
575
|
+
export function are_ranges_exclusive(ranges: Array<Attachment>, length = Infinity): boolean {
|
|
603
576
|
const sorted = [...ranges].sort(
|
|
604
577
|
(a, b) => a.start_offset - b.start_offset || a.end_offset - b.end_offset
|
|
605
578
|
);
|
|
@@ -615,17 +588,27 @@ export function are_ranges_exclusive(ranges, length = Infinity) {
|
|
|
615
588
|
);
|
|
616
589
|
}
|
|
617
590
|
|
|
591
|
+
/**
|
|
592
|
+
* A fragment range calculated from a sequence length, marks, and an optional
|
|
593
|
+
* selection highlight range.
|
|
594
|
+
*/
|
|
595
|
+
export type FragmentRange = {
|
|
596
|
+
type: 'content' | 'mark' | 'selection_highlight';
|
|
597
|
+
start_offset: number;
|
|
598
|
+
end_offset: number;
|
|
599
|
+
mark_index?: number;
|
|
600
|
+
node_id?: string;
|
|
601
|
+
};
|
|
602
|
+
|
|
618
603
|
/**
|
|
619
604
|
* Calculates abstract fragment ranges from a length and marks.
|
|
620
|
-
*
|
|
621
|
-
* @param {number} length - Length of the sequence (e.g. text length or array length)
|
|
622
|
-
* @param {Array<import('./types.d.ts').Mark>} marks - Array of marks
|
|
623
|
-
* @param {import('./types.d.ts').SelectionRange} [selection_highlight_range] - Optional selection highlight range
|
|
624
|
-
* @returns {Array<{type: 'content' | 'mark' | 'selection_highlight', start_offset: number, end_offset: number, mark_index?: number, node_id?: string}>}
|
|
625
605
|
*/
|
|
626
|
-
export function calculate_fragment_ranges(
|
|
627
|
-
|
|
628
|
-
|
|
606
|
+
export function calculate_fragment_ranges(
|
|
607
|
+
length: number,
|
|
608
|
+
marks: Array<Mark>,
|
|
609
|
+
selection_highlight_range?: SelectionRange | null
|
|
610
|
+
): Array<FragmentRange> {
|
|
611
|
+
const fragments: Array<FragmentRange> = [];
|
|
629
612
|
let last_index = 0;
|
|
630
613
|
|
|
631
614
|
// Merge marks with selection highlight and sort by start offset
|
|
@@ -644,10 +627,8 @@ export function calculate_fragment_ranges(length, marks, selection_highlight_ran
|
|
|
644
627
|
}
|
|
645
628
|
|
|
646
629
|
if ('node_id' in range) {
|
|
647
|
-
const mark =
|
|
648
|
-
const mark_index =
|
|
649
|
-
/** @type {import('./types.d.ts').Mark & { mark_index?: number }} */ (mark).mark_index ??
|
|
650
|
-
marks.indexOf(mark);
|
|
630
|
+
const mark = range as Mark;
|
|
631
|
+
const mark_index = (mark as Mark & { mark_index?: number }).mark_index ?? marks.indexOf(mark);
|
|
651
632
|
fragments.push({
|
|
652
633
|
type: 'mark',
|
|
653
634
|
start_offset: mark.start_offset,
|