svedit 0.9.0 → 0.11.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 +476 -364
- package/dist/AnnotatedTextProperty.svelte +40 -10
- package/dist/AnnotatedTextProperty.svelte.d.ts.map +1 -1
- package/dist/Command.svelte.d.ts +0 -1
- package/dist/Command.svelte.d.ts.map +1 -1
- package/dist/Command.svelte.js +48 -6
- package/dist/CustomProperty.svelte +20 -9
- package/dist/CustomProperty.svelte.d.ts +2 -1
- package/dist/CustomProperty.svelte.d.ts.map +1 -1
- package/dist/Node.svelte +7 -5
- package/dist/Node.svelte.d.ts.map +1 -1
- package/dist/NodeArrayProperty.svelte +59 -48
- package/dist/NodeArrayProperty.svelte.d.ts.map +1 -1
- package/dist/NodeCaret.svelte +0 -7
- package/dist/NodeGap.svelte +208 -118
- package/dist/NodeGap.svelte.d.ts +0 -2
- package/dist/NodeGap.svelte.d.ts.map +1 -1
- package/dist/NodeGapMarkers.svelte +251 -84
- package/dist/NodeGapMarkers.svelte.d.ts +2 -0
- package/dist/NodeGapMarkers.svelte.d.ts.map +1 -1
- package/dist/NodeSelectionMarkers.svelte +4 -3
- package/dist/NodeSelectionMarkers.svelte.d.ts.map +1 -1
- package/dist/Session.svelte.d.ts +12 -1
- package/dist/Session.svelte.d.ts.map +1 -1
- package/dist/Session.svelte.js +40 -3
- package/dist/Svedit.svelte +496 -221
- package/dist/Svedit.svelte.d.ts.map +1 -1
- package/dist/Transaction.svelte.d.ts +24 -5
- package/dist/Transaction.svelte.d.ts.map +1 -1
- package/dist/Transaction.svelte.js +103 -61
- package/dist/doc_utils.d.ts +69 -3
- package/dist/doc_utils.d.ts.map +1 -1
- package/dist/doc_utils.js +276 -10
- package/dist/index.d.ts +1 -1
- package/dist/index.js +6 -1
- package/dist/node_visibility.svelte.d.ts +9 -0
- package/dist/node_visibility.svelte.d.ts.map +1 -0
- package/dist/node_visibility.svelte.js +718 -0
- package/dist/paste_utils.d.ts +46 -0
- package/dist/paste_utils.d.ts.map +1 -0
- package/dist/paste_utils.js +167 -0
- package/dist/styles/svedit-animations.css +8 -0
- package/dist/styles/svedit-colors.css +1 -0
- package/dist/types.d.ts +46 -3
- package/dist/utils.d.ts +52 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +109 -3
- package/package.json +3 -2
- package/src/lib/AnnotatedTextProperty.svelte +40 -10
- package/src/lib/Command.svelte.js +48 -6
- package/src/lib/CustomProperty.svelte +20 -9
- package/src/lib/Node.svelte +7 -5
- package/src/lib/NodeArrayProperty.svelte +59 -48
- package/src/lib/NodeCaret.svelte +0 -7
- package/src/lib/NodeGap.svelte +208 -118
- package/src/lib/NodeGapMarkers.svelte +251 -84
- package/src/lib/NodeSelectionMarkers.svelte +4 -3
- package/src/lib/Session.svelte.js +40 -3
- package/src/lib/Svedit.svelte +496 -221
- package/src/lib/Transaction.svelte.js +103 -61
- package/src/lib/doc_utils.js +276 -10
- package/src/lib/index.js +6 -1
- package/src/lib/node_visibility.svelte.js +718 -0
- package/src/lib/paste_utils.js +167 -0
- package/src/lib/styles/svedit-animations.css +8 -0
- package/src/lib/styles/svedit-colors.css +1 -0
- package/src/lib/types.d.ts +46 -3
- package/src/lib/utils.js +109 -3
- package/dist/node_gap_computation.svelte.d.ts +0 -5
- package/dist/node_gap_computation.svelte.d.ts.map +0 -1
- package/dist/node_gap_computation.svelte.js +0 -621
- package/src/lib/node_gap_computation.svelte.js +0 -621
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { DocumentSchema } from './types.d.ts'
|
|
3
|
+
*/
|
|
4
|
+
export function normalize_line_endings(text: any): any;
|
|
5
|
+
export function dedent_plain_text(plain_text: any): any;
|
|
6
|
+
export function split_plain_text_paragraphs(plain_text: any): any;
|
|
7
|
+
export function normalize_plain_text_for_single_line_property(plain_text: any): any;
|
|
8
|
+
/**
|
|
9
|
+
* @param {string | null | undefined} node_type
|
|
10
|
+
* @param {DocumentSchema} schema
|
|
11
|
+
* @returns {string | null}
|
|
12
|
+
*/
|
|
13
|
+
export function get_text_property_name(node_type: string | null | undefined, schema: DocumentSchema): string | null;
|
|
14
|
+
/**
|
|
15
|
+
* @param {any} node
|
|
16
|
+
* @param {DocumentSchema} schema
|
|
17
|
+
* @returns {{ text: string, annotations: any[] } | null}
|
|
18
|
+
*/
|
|
19
|
+
export function get_text_content(node: any, schema: DocumentSchema): {
|
|
20
|
+
text: string;
|
|
21
|
+
annotations: any[];
|
|
22
|
+
} | null;
|
|
23
|
+
/**
|
|
24
|
+
* @param {any} node
|
|
25
|
+
* @param {DocumentSchema} schema
|
|
26
|
+
* @returns {boolean}
|
|
27
|
+
*/
|
|
28
|
+
export function is_text_like_node_payload(node: any, schema: DocumentSchema): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* @param {any} node_array_property_definition
|
|
31
|
+
* @param {DocumentSchema} schema
|
|
32
|
+
* @returns {string | null}
|
|
33
|
+
*/
|
|
34
|
+
export function get_default_text_node(node_array_property_definition: any, schema: DocumentSchema): string | null;
|
|
35
|
+
/**
|
|
36
|
+
* @param {string[]} paragraph_fragments
|
|
37
|
+
* @param {string | null | undefined} node_type
|
|
38
|
+
* @param {DocumentSchema} schema
|
|
39
|
+
* @returns {{ main_nodes: string[], nodes: Record<string, any> } | null}
|
|
40
|
+
*/
|
|
41
|
+
export function create_plain_text_nodes_payload(paragraph_fragments: string[], node_type: string | null | undefined, schema: DocumentSchema): {
|
|
42
|
+
main_nodes: string[];
|
|
43
|
+
nodes: Record<string, any>;
|
|
44
|
+
} | null;
|
|
45
|
+
import type { DocumentSchema } from './types.d.ts';
|
|
46
|
+
//# sourceMappingURL=paste_utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paste_utils.d.ts","sourceRoot":"","sources":["../src/lib/paste_utils.js"],"names":[],"mappings":"AAAA;;GAEG;AAEH,uDAEC;AAED,wDAkCC;AAED,kEAKC;AAED,oFAEC;AAED;;;;GAIG;AACH,kDAJW,MAAM,GAAG,IAAI,GAAG,SAAS,UACzB,cAAc,GACZ,MAAM,GAAG,IAAI,CAYzB;AAED;;;;GAIG;AACH,uCAJW,GAAG,UACH,cAAc,GACZ;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,GAAG,EAAE,CAAA;CAAE,GAAG,IAAI,CAoBvD;AAED;;;;GAIG;AACH,gDAJW,GAAG,UACH,cAAc,GACZ,OAAO,CAMnB;AAED;;;;GAIG;AACH,sEAJW,GAAG,UACH,cAAc,GACZ,MAAM,GAAG,IAAI,CAmBzB;AAED;;;;;GAKG;AACH,qEALW,MAAM,EAAE,aACR,MAAM,GAAG,IAAI,GAAG,SAAS,UACzB,cAAc,GACZ;IAAE,UAAU,EAAE,MAAM,EAAE,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;CAAE,GAAG,IAAI,CA8BvE;oCArKkC,cAAc"}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { DocumentSchema } from './types.d.ts'
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export function normalize_line_endings(text) {
|
|
6
|
+
return text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function dedent_plain_text(plain_text) {
|
|
10
|
+
const lines = normalize_line_endings(plain_text).split('\n');
|
|
11
|
+
if (lines.length < 2) return plain_text;
|
|
12
|
+
|
|
13
|
+
const non_empty_lines = lines.filter((line) => line.trim().length > 0);
|
|
14
|
+
if (non_empty_lines.length === 0) return plain_text;
|
|
15
|
+
|
|
16
|
+
const indented_non_empty_lines = non_empty_lines.filter((line) => /^[\t ]+/.test(line));
|
|
17
|
+
const indented_line_ratio = indented_non_empty_lines.length / non_empty_lines.length;
|
|
18
|
+
if (indented_line_ratio < 0.8) return plain_text;
|
|
19
|
+
|
|
20
|
+
const leading_whitespace_lengths = indented_non_empty_lines
|
|
21
|
+
.map((line) => line.match(/^[\t ]+/)?.[0].length || 0)
|
|
22
|
+
.filter(Boolean);
|
|
23
|
+
if (leading_whitespace_lengths.length === 0) return plain_text;
|
|
24
|
+
|
|
25
|
+
const dedent_size = Math.min(...leading_whitespace_lengths);
|
|
26
|
+
if (dedent_size <= 0) return plain_text;
|
|
27
|
+
|
|
28
|
+
return lines
|
|
29
|
+
.map((line) => {
|
|
30
|
+
if (line.trim().length === 0) return line;
|
|
31
|
+
let removable_count = 0;
|
|
32
|
+
while (removable_count < dedent_size && removable_count < line.length) {
|
|
33
|
+
const char = line[removable_count];
|
|
34
|
+
if (char === ' ' || char === '\t') {
|
|
35
|
+
removable_count += 1;
|
|
36
|
+
} else {
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return line.slice(removable_count);
|
|
41
|
+
})
|
|
42
|
+
.join('\n');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function split_plain_text_paragraphs(plain_text) {
|
|
46
|
+
return normalize_line_endings(plain_text)
|
|
47
|
+
.split(/\n{2,}/)
|
|
48
|
+
.map((fragment) => fragment.trim())
|
|
49
|
+
.filter(Boolean);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function normalize_plain_text_for_single_line_property(plain_text) {
|
|
53
|
+
return normalize_line_endings(plain_text).replace(/\s*\n+\s*/g, ' ');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {string | null | undefined} node_type
|
|
58
|
+
* @param {DocumentSchema} schema
|
|
59
|
+
* @returns {string | null}
|
|
60
|
+
*/
|
|
61
|
+
export function get_text_property_name(node_type, schema) {
|
|
62
|
+
if (!node_type) return null;
|
|
63
|
+
const node_schema = schema[node_type];
|
|
64
|
+
if (!node_schema || node_schema.kind !== 'text') return null;
|
|
65
|
+
if (node_schema.properties?.content?.type === 'annotated_text') return 'content';
|
|
66
|
+
return (
|
|
67
|
+
Object.entries(node_schema.properties).find(([, property_definition]) => {
|
|
68
|
+
return property_definition.type === 'annotated_text';
|
|
69
|
+
})?.[0] || null
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @param {any} node
|
|
75
|
+
* @param {DocumentSchema} schema
|
|
76
|
+
* @returns {{ text: string, annotations: any[] } | null}
|
|
77
|
+
*/
|
|
78
|
+
export function get_text_content(node, schema) {
|
|
79
|
+
if (!node || typeof node !== 'object') return null;
|
|
80
|
+
|
|
81
|
+
const text_property_name = get_text_property_name(node.type, schema);
|
|
82
|
+
if (
|
|
83
|
+
text_property_name &&
|
|
84
|
+
node[text_property_name] &&
|
|
85
|
+
typeof node[text_property_name].text === 'string' &&
|
|
86
|
+
Array.isArray(node[text_property_name].annotations)
|
|
87
|
+
) {
|
|
88
|
+
return node[text_property_name];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (node.content && typeof node.content.text === 'string' && Array.isArray(node.content.annotations)) {
|
|
92
|
+
return node.content;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* @param {any} node
|
|
100
|
+
* @param {DocumentSchema} schema
|
|
101
|
+
* @returns {boolean}
|
|
102
|
+
*/
|
|
103
|
+
export function is_text_like_node_payload(node, schema) {
|
|
104
|
+
if (!node || typeof node !== 'object') return false;
|
|
105
|
+
if (schema[node.type]?.kind === 'text') return true;
|
|
106
|
+
return !!get_text_content(node, schema);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* @param {any} node_array_property_definition
|
|
111
|
+
* @param {DocumentSchema} schema
|
|
112
|
+
* @returns {string | null}
|
|
113
|
+
*/
|
|
114
|
+
export function get_default_text_node(node_array_property_definition, schema) {
|
|
115
|
+
if (
|
|
116
|
+
node_array_property_definition?.type !== 'node_array' ||
|
|
117
|
+
!Array.isArray(node_array_property_definition.node_types)
|
|
118
|
+
) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const default_node_type = node_array_property_definition.default_node_type;
|
|
123
|
+
if (default_node_type && schema[default_node_type]?.kind === 'text') {
|
|
124
|
+
return default_node_type;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
node_array_property_definition.node_types.find((node_type) => schema[node_type]?.kind === 'text') ||
|
|
129
|
+
null
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* @param {string[]} paragraph_fragments
|
|
135
|
+
* @param {string | null | undefined} node_type
|
|
136
|
+
* @param {DocumentSchema} schema
|
|
137
|
+
* @returns {{ main_nodes: string[], nodes: Record<string, any> } | null}
|
|
138
|
+
*/
|
|
139
|
+
export function create_plain_text_nodes_payload(paragraph_fragments, node_type, schema) {
|
|
140
|
+
if (!Array.isArray(paragraph_fragments) || paragraph_fragments.length === 0 || !node_type) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const text_property_name = get_text_property_name(node_type, schema);
|
|
145
|
+
if (!text_property_name) return null;
|
|
146
|
+
|
|
147
|
+
const payload = {
|
|
148
|
+
main_nodes: [],
|
|
149
|
+
nodes: {}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
for (let i = 0; i < paragraph_fragments.length; i++) {
|
|
153
|
+
const fragment = paragraph_fragments[i];
|
|
154
|
+
const node_id = 'fragment_' + i;
|
|
155
|
+
payload.nodes[node_id] = {
|
|
156
|
+
id: node_id,
|
|
157
|
+
type: node_type,
|
|
158
|
+
[text_property_name]: {
|
|
159
|
+
text: fragment,
|
|
160
|
+
annotations: []
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
payload.main_nodes.push(node_id);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return payload;
|
|
167
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -11,12 +11,17 @@ import type { Component, Snippet } from 'svelte';
|
|
|
11
11
|
// ===== SELECTION TYPE DEFINITIONS =====
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* A unique node identifier
|
|
14
|
+
* A unique node identifier.
|
|
15
|
+
*
|
|
16
|
+
* Node ids must be valid Svedit path string segments: they must start with
|
|
17
|
+
* a letter or underscore, contain only letters, numbers, underscores, or
|
|
18
|
+
* dashes, and must not contain `__`.
|
|
15
19
|
*/
|
|
16
20
|
export type NodeId = string;
|
|
17
21
|
|
|
18
22
|
/**
|
|
19
|
-
* Array of IDs, property names (strings), or indexes (integers) that identify a node or property in the document
|
|
23
|
+
* Array of IDs, property names (strings), or indexes (integers) that identify a node or property in the document.
|
|
24
|
+
* String segments must follow the same path segment rules as NodeId.
|
|
20
25
|
*/
|
|
21
26
|
export type DocumentPath = Array<string | number>;
|
|
22
27
|
|
|
@@ -269,7 +274,8 @@ export type PropertyDefinition =
|
|
|
269
274
|
export type NodeKind = 'document' | 'block' | 'text' | 'annotation';
|
|
270
275
|
|
|
271
276
|
/**
|
|
272
|
-
* Schema for text nodes - must have a content property of type annotated_text
|
|
277
|
+
* Schema for text nodes - must have a content property of type annotated_text.
|
|
278
|
+
* Use define_document_schema to also check that content is the only annotated_text property.
|
|
273
279
|
*/
|
|
274
280
|
export type TextNodeSchema = {
|
|
275
281
|
kind: 'text';
|
|
@@ -278,6 +284,43 @@ export type TextNodeSchema = {
|
|
|
278
284
|
} & Record<string, PropertyDefinition>;
|
|
279
285
|
};
|
|
280
286
|
|
|
287
|
+
export type AnnotatedTextPropertyNames<Properties> = {
|
|
288
|
+
[PropertyName in keyof Properties]: Properties[PropertyName] extends { type: 'annotated_text' }
|
|
289
|
+
? PropertyName
|
|
290
|
+
: never;
|
|
291
|
+
}[keyof Properties];
|
|
292
|
+
|
|
293
|
+
export type TextNodeSchemaError<Message extends string> = {
|
|
294
|
+
[SchemaError in `Svedit schema error: ${Message}`]: never;
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
export type TextNodeMissingContentError = TextNodeSchemaError<
|
|
298
|
+
'Text node schemas must define a "content" property of type annotated_text.'
|
|
299
|
+
>;
|
|
300
|
+
|
|
301
|
+
export type TextNodeExtraAnnotatedTextError<ExtraProperty extends string> = TextNodeSchemaError<
|
|
302
|
+
`Text node schemas must not define annotated_text property "${ExtraProperty}". Use "content" as the only annotated_text property.`
|
|
303
|
+
>;
|
|
304
|
+
|
|
305
|
+
export type ValidateTextNodeSchema<Schema> = Schema extends {
|
|
306
|
+
kind: 'text';
|
|
307
|
+
properties: infer Properties;
|
|
308
|
+
}
|
|
309
|
+
? string extends keyof Properties
|
|
310
|
+
? Schema
|
|
311
|
+
: Properties extends { content: AnnotatedTextProperty }
|
|
312
|
+
? Exclude<AnnotatedTextPropertyNames<Properties>, 'content'> extends infer ExtraProperties
|
|
313
|
+
? [ExtraProperties] extends [never]
|
|
314
|
+
? Schema
|
|
315
|
+
: TextNodeExtraAnnotatedTextError<Extract<ExtraProperties, string>>
|
|
316
|
+
: never
|
|
317
|
+
: TextNodeMissingContentError
|
|
318
|
+
: Schema;
|
|
319
|
+
|
|
320
|
+
export type ValidateDocumentSchema<Schema extends Record<string, NodeSchema>> = {
|
|
321
|
+
[NodeType in keyof Schema]: ValidateTextNodeSchema<Schema[NodeType]>;
|
|
322
|
+
};
|
|
323
|
+
|
|
281
324
|
/**
|
|
282
325
|
* Schema for non-text nodes
|
|
283
326
|
*/
|
package/dist/utils.d.ts
CHANGED
|
@@ -129,6 +129,57 @@ export function join_annotated_text(first_text: AnnotatedText, second_text: Anno
|
|
|
129
129
|
* snake_to_pascal('list_item') // Returns: 'ListItem'
|
|
130
130
|
*/
|
|
131
131
|
export function snake_to_pascal(str: string): string;
|
|
132
|
+
/**
|
|
133
|
+
* Check whether a string can be used as a Svedit path segment.
|
|
134
|
+
*
|
|
135
|
+
* Path string segments include node ids and schema property names. Keeping
|
|
136
|
+
* them restricted makes serialized paths reversible, valid as HTML ids, and
|
|
137
|
+
* safe to use as CSS anchor-name suffixes.
|
|
138
|
+
*
|
|
139
|
+
* @param {string} segment
|
|
140
|
+
* @returns {boolean}
|
|
141
|
+
*/
|
|
142
|
+
export function is_path_string_segment_valid(segment: string): boolean;
|
|
143
|
+
/**
|
|
144
|
+
* Assert that a string can be used as a Svedit path segment.
|
|
145
|
+
*
|
|
146
|
+
* @param {string} segment
|
|
147
|
+
* @param {string} [label]
|
|
148
|
+
* @returns {void}
|
|
149
|
+
*/
|
|
150
|
+
export function assert_path_string_segment(segment: string, label?: string): void;
|
|
151
|
+
/**
|
|
152
|
+
* Serialize a document path while preserving whether each segment is a
|
|
153
|
+
* property/key string or an array index number.
|
|
154
|
+
*
|
|
155
|
+
* The result is reversible and safe to use as a data attribute value, HTML id,
|
|
156
|
+
* or CSS anchor-name suffix.
|
|
157
|
+
*
|
|
158
|
+
* @param {Array<string | number>} path
|
|
159
|
+
* @returns {string}
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* serialize_path(['page_1', 'body', 0, 'items']) // Returns: 'page_1__body__0__items'
|
|
163
|
+
*/
|
|
164
|
+
export function serialize_path(path: Array<string | number>): string;
|
|
165
|
+
/**
|
|
166
|
+
* Deserialize a path string produced by serialize_path.
|
|
167
|
+
*
|
|
168
|
+
* @param {string} serialized_path
|
|
169
|
+
* @returns {Array<string | number>}
|
|
170
|
+
*
|
|
171
|
+
* @example
|
|
172
|
+
* deserialize_path('page_1__body__0__items') // Returns: ['page_1', 'body', 0, 'items']
|
|
173
|
+
*/
|
|
174
|
+
export function deserialize_path(serialized_path: string): Array<string | number>;
|
|
175
|
+
/**
|
|
176
|
+
* Compare two document paths segment-by-segment, preserving segment types.
|
|
177
|
+
*
|
|
178
|
+
* @param {Array<string | number>} a
|
|
179
|
+
* @param {Array<string | number>} b
|
|
180
|
+
* @returns {boolean}
|
|
181
|
+
*/
|
|
182
|
+
export function paths_equal(a: Array<string | number>, b: Array<string | number>): boolean;
|
|
132
183
|
export function traverse(node_id: any, schema: any, nodes: any): any[];
|
|
133
184
|
/**
|
|
134
185
|
* Extracts the normalized range from a text or node selection.
|
|
@@ -138,6 +189,7 @@ export function traverse(node_id: any, schema: any, nodes: any): any[];
|
|
|
138
189
|
*/
|
|
139
190
|
export function get_selection_range(selection?: Selection): SelectionRange | null;
|
|
140
191
|
export function is_selection_collapsed(selection: any): boolean;
|
|
192
|
+
export const PATH_SEPARATOR: "__";
|
|
141
193
|
import type { AnnotatedText } from './types.d.ts';
|
|
142
194
|
import type { Selection } from './types.d.ts';
|
|
143
195
|
import type { SelectionRange } from './types.d.ts';
|
package/dist/utils.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/lib/utils.js"],"names":[],"mappings":"AAMA;;;GAGG;AACH,qCAFa,OAAO,CAanB;AAGD;;;GAGG;AACH,6CAFa,OAAO,CAUnB;AAED;;;;;;;;;;;;;GAaG;AACH,qCARW,MAAM,GACJ,MAAM,CASlB;
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/lib/utils.js"],"names":[],"mappings":"AAMA;;;GAGG;AACH,qCAFa,OAAO,CAanB;AAGD;;;GAGG;AACH,6CAFa,OAAO,CAUnB;AAED;;;;;;;;;;;;;GAaG;AACH,qCARW,MAAM,GACJ,MAAM,CASlB;AAGD;;;;;;;;;;;;;;GAcG;AACH,iCATW,MAAM,SACN,MAAM,GACJ,MAAM,CAUlB;AAED;;;;;;;;;;;;;;GAcG;AACH,gCATW,MAAM,SACN,MAAM,QACN,MAAM,GACJ,MAAM,CAYlB;AAED;;;;;;;;;;;;;GAaG;AACH,0CARW,MAAM,gBACN,MAAM,GACJ,MAAM,CAmBlB;AAED;;;;;;;;;;;;;GAaG;AACH,0CARW,MAAM,eACN,MAAM,GACJ,MAAM,CAelB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,4DAZW,aAAa,eACb,MAAM,GACJ,CAAC,aAAa,EAAE,aAAa,CAAC,CA6C1C;AAED;;;;;;;;;;;;;GAaG;AACH,gDARW,aAAa,eACb,aAAa,GACX,aAAa,CA4CzB;AAED;;;;;;;;GAQG;AACH,qCANW,MAAM,GACJ,MAAM,CAUlB;AAMD;;;;;;;;;GASG;AACH,sDAHW,MAAM,GACJ,OAAO,CAQnB;AAED;;;;;;GAMG;AACH,oDAJW,MAAM,UACN,MAAM,GACJ,IAAI,CAQhB;AAED;;;;;;;;;;;;GAYG;AACH,qCANW,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,GACpB,MAAM,CAmBlB;AAED;;;;;;;;GAQG;AACH,kDANW,MAAM,GACJ,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,CAuBlC;AAED;;;;;;GAMG;AACH,+BAJW,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,KACtB,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,GACpB,OAAO,CAKnB;AAED,uEAgCC;AAED;;;;;GAKG;AACH,gDAHW,SAAS,GACP,cAAc,GAAG,IAAI,CAWjC;AAED,gEAMC;AApKD,6BAA8B,IAAI,CAAC;mCAtRsC,cAAc;+BAAd,cAAc;oCAAd,cAAc"}
|
package/dist/utils.js
CHANGED
|
@@ -21,7 +21,7 @@ export function is_mobile_browser() {
|
|
|
21
21
|
);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
// ‼️‼️‼️‼️‼️‼️ UNUSED UTILITY BELOW ‼️‼️‼️‼️‼️‼️
|
|
24
|
+
// ‼️‼️‼️‼️‼️‼️ UNUSED UTILITY BELOW ‼️‼️‼️‼️‼️‼️
|
|
25
25
|
/**
|
|
26
26
|
* Detect if the current browser is Chrome on desktop
|
|
27
27
|
* @returns {boolean} true if Chrome desktop browser, false otherwise
|
|
@@ -54,8 +54,7 @@ export function get_char_length(str) {
|
|
|
54
54
|
return [...SEGMENTER.segment(str)].length;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
// ‼️‼️‼️‼️‼️‼️ UNUSED UTILITY BELOW ‼️‼️‼️‼️‼️‼️
|
|
57
|
+
// ‼️‼️‼️‼️‼️‼️ UNUSED UTILITY BELOW ‼️‼️‼️‼️‼️‼️
|
|
59
58
|
/**
|
|
60
59
|
* Get a single character at the specified position (accounting for multi-byte characters)
|
|
61
60
|
*
|
|
@@ -278,6 +277,113 @@ export function snake_to_pascal(str) {
|
|
|
278
277
|
.join('');
|
|
279
278
|
}
|
|
280
279
|
|
|
280
|
+
export const PATH_SEPARATOR = '__';
|
|
281
|
+
const PATH_STRING_SEGMENT_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;
|
|
282
|
+
const PATH_INDEX_SEGMENT_RE = /^(0|[1-9]\d*)$/;
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Check whether a string can be used as a Svedit path segment.
|
|
286
|
+
*
|
|
287
|
+
* Path string segments include node ids and schema property names. Keeping
|
|
288
|
+
* them restricted makes serialized paths reversible, valid as HTML ids, and
|
|
289
|
+
* safe to use as CSS anchor-name suffixes.
|
|
290
|
+
*
|
|
291
|
+
* @param {string} segment
|
|
292
|
+
* @returns {boolean}
|
|
293
|
+
*/
|
|
294
|
+
export function is_path_string_segment_valid(segment) {
|
|
295
|
+
return (
|
|
296
|
+
typeof segment === 'string' &&
|
|
297
|
+
PATH_STRING_SEGMENT_RE.test(segment) &&
|
|
298
|
+
!segment.includes(PATH_SEPARATOR)
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Assert that a string can be used as a Svedit path segment.
|
|
304
|
+
*
|
|
305
|
+
* @param {string} segment
|
|
306
|
+
* @param {string} [label]
|
|
307
|
+
* @returns {void}
|
|
308
|
+
*/
|
|
309
|
+
export function assert_path_string_segment(segment, label = 'Path segment') {
|
|
310
|
+
if (!is_path_string_segment_valid(segment)) {
|
|
311
|
+
throw new Error(
|
|
312
|
+
`${label} must start with a letter or underscore and contain only letters, numbers, underscores, or dashes. It must not contain "${PATH_SEPARATOR}".`
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Serialize a document path while preserving whether each segment is a
|
|
319
|
+
* property/key string or an array index number.
|
|
320
|
+
*
|
|
321
|
+
* The result is reversible and safe to use as a data attribute value, HTML id,
|
|
322
|
+
* or CSS anchor-name suffix.
|
|
323
|
+
*
|
|
324
|
+
* @param {Array<string | number>} path
|
|
325
|
+
* @returns {string}
|
|
326
|
+
*
|
|
327
|
+
* @example
|
|
328
|
+
* serialize_path(['page_1', 'body', 0, 'items']) // Returns: 'page_1__body__0__items'
|
|
329
|
+
*/
|
|
330
|
+
export function serialize_path(path) {
|
|
331
|
+
return path
|
|
332
|
+
.map((segment) => {
|
|
333
|
+
if (typeof segment === 'number') {
|
|
334
|
+
if (!Number.isInteger(segment) || segment < 0) {
|
|
335
|
+
throw new Error(`Path index must be a non-negative integer: ${segment}`);
|
|
336
|
+
}
|
|
337
|
+
return String(segment);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
assert_path_string_segment(segment);
|
|
341
|
+
return segment;
|
|
342
|
+
})
|
|
343
|
+
.join(PATH_SEPARATOR);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Deserialize a path string produced by serialize_path.
|
|
348
|
+
*
|
|
349
|
+
* @param {string} serialized_path
|
|
350
|
+
* @returns {Array<string | number>}
|
|
351
|
+
*
|
|
352
|
+
* @example
|
|
353
|
+
* deserialize_path('page_1__body__0__items') // Returns: ['page_1', 'body', 0, 'items']
|
|
354
|
+
*/
|
|
355
|
+
export function deserialize_path(serialized_path) {
|
|
356
|
+
if (serialized_path === '') return [];
|
|
357
|
+
|
|
358
|
+
return serialized_path.split(PATH_SEPARATOR).map((segment) => {
|
|
359
|
+
if (segment === '') {
|
|
360
|
+
throw new Error(`Invalid serialized path: ${serialized_path}`);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (/^\d+$/.test(segment)) {
|
|
364
|
+
if (!PATH_INDEX_SEGMENT_RE.test(segment)) {
|
|
365
|
+
throw new Error(`Invalid serialized path index: ${segment}`);
|
|
366
|
+
}
|
|
367
|
+
return Number(segment);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
assert_path_string_segment(segment, 'Serialized path segment');
|
|
371
|
+
return segment;
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Compare two document paths segment-by-segment, preserving segment types.
|
|
377
|
+
*
|
|
378
|
+
* @param {Array<string | number>} a
|
|
379
|
+
* @param {Array<string | number>} b
|
|
380
|
+
* @returns {boolean}
|
|
381
|
+
*/
|
|
382
|
+
export function paths_equal(a, b) {
|
|
383
|
+
if (a.length !== b.length) return false;
|
|
384
|
+
return a.every((segment, index) => segment === b[index]);
|
|
385
|
+
}
|
|
386
|
+
|
|
281
387
|
export function traverse(node_id, schema, nodes) {
|
|
282
388
|
const json = [];
|
|
283
389
|
const visited = {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "svedit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"dev": "vite dev",
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"@sveltejs/kit": "^2.55.0",
|
|
46
46
|
"@sveltejs/package": "^2.5.7",
|
|
47
47
|
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
|
48
|
+
"@types/node": "^25.6.0",
|
|
48
49
|
"@typescript-eslint/eslint-plugin": "^8.57.0",
|
|
49
50
|
"@typescript-eslint/parser": "^8.57.0",
|
|
50
51
|
"@vitest/browser": "^4.1.0",
|
|
@@ -59,8 +60,8 @@
|
|
|
59
60
|
"publint": "^0.3.18",
|
|
60
61
|
"svelte": "^5.53.12",
|
|
61
62
|
"svelte-check": "^4.4.5",
|
|
62
|
-
"vite": "^8.0.0",
|
|
63
63
|
"typescript": "^5.9.3",
|
|
64
|
+
"vite": "^8.0.0",
|
|
64
65
|
"vitest": "^4.1.0",
|
|
65
66
|
"vitest-browser-svelte": "^2.1.0"
|
|
66
67
|
}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
import { getContext } from 'svelte';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
char_slice,
|
|
5
|
+
get_char_length,
|
|
6
|
+
paths_equal,
|
|
7
|
+
serialize_path,
|
|
8
|
+
get_selection_range
|
|
9
|
+
} from './utils.js';
|
|
4
10
|
|
|
5
11
|
/** @import { AnnotatedTextPropertyProps, Annotation, Fragment, SelectionRange } from './types.d.ts'; */
|
|
6
12
|
|
|
@@ -9,10 +15,11 @@
|
|
|
9
15
|
/** @type {AnnotatedTextPropertyProps} */
|
|
10
16
|
let { path, class: css_class, placeholder = '', tag = 'div', style = '', ...rest } = $props();
|
|
11
17
|
|
|
18
|
+
let path_str = $derived(serialize_path(path));
|
|
19
|
+
|
|
12
20
|
let is_focused = $derived.by(() => {
|
|
13
21
|
return (
|
|
14
|
-
svedit.session.selection?.type === 'text' &&
|
|
15
|
-
path.join('.') === svedit.session.selection?.path.join('.')
|
|
22
|
+
svedit.session.selection?.type === 'text' && paths_equal(path, svedit.session.selection.path)
|
|
16
23
|
);
|
|
17
24
|
});
|
|
18
25
|
|
|
@@ -111,12 +118,11 @@
|
|
|
111
118
|
<svelte:element
|
|
112
119
|
this={tag}
|
|
113
120
|
data-type="text"
|
|
114
|
-
data-path={
|
|
115
|
-
style="anchor-name: --{
|
|
121
|
+
data-path={path_str}
|
|
122
|
+
style="anchor-name: --{path_str};{style}"
|
|
116
123
|
class="text svedit-selectable {css_class}"
|
|
117
124
|
class:empty={is_empty}
|
|
118
125
|
class:focused={is_focused}
|
|
119
|
-
class:editable={svedit.editable}
|
|
120
126
|
{placeholder}
|
|
121
127
|
{...rest}
|
|
122
128
|
>
|
|
@@ -125,8 +131,7 @@
|
|
|
125
131
|
class="selection-highlight"
|
|
126
132
|
style="anchor-name: --selection-highlight;">{fragment.content}</span
|
|
127
133
|
>{:else if fragment.type === 'annotation'}
|
|
128
|
-
{@const AnnotationComponent =
|
|
129
|
-
svedit.session.config.node_components[snake_to_pascal(fragment.node.type)]}
|
|
134
|
+
{@const AnnotationComponent = svedit.session.config.node_components[fragment.node.type]}
|
|
130
135
|
<AnnotationComponent
|
|
131
136
|
path={[...path, 'annotations', fragment.annotation_index, 'node_id']}
|
|
132
137
|
content={fragment.content}
|
|
@@ -137,11 +142,13 @@
|
|
|
137
142
|
</svelte:element>
|
|
138
143
|
|
|
139
144
|
<style>
|
|
140
|
-
|
|
145
|
+
/* Editable text base layout; :where() allows easy override without specificity conflicts. */
|
|
146
|
+
:where(.text) {
|
|
141
147
|
white-space: pre-wrap;
|
|
142
148
|
overflow-wrap: anywhere;
|
|
143
149
|
box-sizing: content-box;
|
|
144
150
|
}
|
|
151
|
+
|
|
145
152
|
/* We switch from ::before to ::after when the element is focused. So the the caret is always before the placeholder. */
|
|
146
153
|
[placeholder].empty:not(.focused)::before,
|
|
147
154
|
[placeholder].empty.focused::after {
|
|
@@ -150,13 +157,36 @@
|
|
|
150
157
|
color: color-mix(in oklch, currentcolor 50%, transparent);
|
|
151
158
|
}
|
|
152
159
|
|
|
160
|
+
/* A virtual caret: to fix the caret vertical alignment issue in Chrome and Firefox for empty focused contenteditable with placeholders */
|
|
161
|
+
/* Browser BUG: iOS Safari only considers the caret color set on the top contenteditable element, not on nested elements (e.g. the second selector doesn't work in iOS Safari) */
|
|
162
|
+
:global(.svedit.editable .svedit-canvas:has([placeholder].empty.focused)),
|
|
163
|
+
:global(.svedit.editable) [placeholder].empty.focused {
|
|
164
|
+
caret-color: transparent !important;
|
|
165
|
+
}
|
|
166
|
+
:global(.svedit.editable) [placeholder].empty.focused::before {
|
|
167
|
+
content: '';
|
|
168
|
+
/* we limit width & height to avoid layout shifts in case the text has a lower natural height */
|
|
169
|
+
width: 0px;
|
|
170
|
+
height: 1cap;
|
|
171
|
+
display: inline-block;
|
|
172
|
+
/* we use box-shadow to draw the caret shape, matching the native caret */
|
|
173
|
+
box-shadow:
|
|
174
|
+
0 -0.4cap 0 0.65px var(--svedit-caret-color, AccentColor),
|
|
175
|
+
0 0 0 0.65px var(--svedit-caret-color, AccentColor),
|
|
176
|
+
0 0.4cap 0 0.65px var(--svedit-caret-color, AccentColor);
|
|
177
|
+
animation: var(
|
|
178
|
+
--node-caret-animation,
|
|
179
|
+
node-caret-blink var(--node-caret-blink-duration, 1.1s) ease-in-out infinite
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
153
183
|
/* Hide flickering: in Chrome, the caret jumps from end of placeholder string to start of text property when we focus */
|
|
154
184
|
.text:not(.focused) {
|
|
155
185
|
caret-color: transparent;
|
|
156
186
|
}
|
|
157
187
|
|
|
158
188
|
/* Disable text-transform when editable and focused so users see original text */
|
|
159
|
-
.
|
|
189
|
+
:global(.svedit.editable) .text.focused {
|
|
160
190
|
text-transform: none !important;
|
|
161
191
|
}
|
|
162
192
|
|