svedit 0.13.0 → 0.15.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 +155 -1
- package/dist/NodeCaret.svelte +1 -1
- package/dist/NodeGapMarkers.svelte +17 -4
- package/dist/NodeGapMarkers.svelte.d.ts.map +1 -1
- package/dist/NodeSelectionMarkers.svelte +14 -3
- package/dist/Session.svelte.d.ts.map +1 -1
- package/dist/Session.svelte.js +17 -9
- package/dist/Svedit.svelte +101 -29
- package/dist/Svedit.svelte.d.ts.map +1 -1
- package/dist/TextProperty.svelte +5 -5
- package/dist/Transaction.svelte.d.ts +6 -2
- package/dist/Transaction.svelte.d.ts.map +1 -1
- package/dist/Transaction.svelte.js +7 -6
- package/dist/html_export.d.ts +11 -0
- package/dist/html_export.d.ts.map +1 -0
- package/dist/html_export.js +25 -0
- package/dist/html_paste.d.ts +59 -0
- package/dist/html_paste.d.ts.map +1 -0
- package/dist/html_paste.js +229 -0
- package/dist/html_utils.d.ts +4 -0
- package/dist/html_utils.d.ts.map +1 -0
- package/dist/html_utils.js +16 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/styles/svedit-colors.css +8 -5
- package/dist/types.d.ts +23 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +28 -26
- package/src/lib/NodeCaret.svelte +1 -1
- package/src/lib/NodeGapMarkers.svelte +17 -4
- package/src/lib/NodeSelectionMarkers.svelte +14 -3
- package/src/lib/Session.svelte.ts +16 -11
- package/src/lib/Svedit.svelte +101 -29
- package/src/lib/TextProperty.svelte +5 -5
- package/src/lib/Transaction.svelte.ts +12 -6
- package/src/lib/html_export.ts +37 -0
- package/src/lib/html_paste.ts +279 -0
- package/src/lib/html_utils.ts +17 -0
- package/src/lib/index.ts +15 -0
- package/src/lib/styles/svedit-colors.css +8 -5
- package/src/lib/types.ts +24 -1
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { safe_html_href } from './html_utils.js';
|
|
2
|
+
import type { DocumentNode, DocumentSchema, PropertyDefinition, Text } from './types.js';
|
|
3
|
+
import { get_default_text_node, get_text_property_name } from './paste_utils.js';
|
|
4
|
+
|
|
5
|
+
export type HtmlBlockTag = 'p' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
|
|
6
|
+
export type HtmlBlockMapping = {
|
|
7
|
+
type: string;
|
|
8
|
+
text_property: string;
|
|
9
|
+
properties?: Record<string, unknown>;
|
|
10
|
+
};
|
|
11
|
+
export type HtmlListTag = 'ul' | 'ol';
|
|
12
|
+
export type HtmlContainerMapping = {
|
|
13
|
+
type: string;
|
|
14
|
+
children_property: string;
|
|
15
|
+
properties?: Record<string, unknown>;
|
|
16
|
+
};
|
|
17
|
+
export type HtmlListMapping = HtmlContainerMapping & { item: HtmlBlockMapping };
|
|
18
|
+
export type HtmlMarkMapping = { type: string; properties?: Record<string, unknown> };
|
|
19
|
+
export type HtmlPasteConfig = {
|
|
20
|
+
blocks?: Partial<
|
|
21
|
+
Record<
|
|
22
|
+
HtmlBlockTag,
|
|
23
|
+
HtmlBlockMapping | ((context: { tag: HtmlBlockTag }) => HtmlBlockMapping | null)
|
|
24
|
+
>
|
|
25
|
+
>;
|
|
26
|
+
lists?: Partial<Record<HtmlListTag, HtmlListMapping>>;
|
|
27
|
+
wrapper?: HtmlContainerMapping;
|
|
28
|
+
marks?: {
|
|
29
|
+
bold?: HtmlMarkMapping;
|
|
30
|
+
link?: HtmlMarkMapping | ((context: { href: string }) => HtmlMarkMapping | null);
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
type InlineStyle = { bold?: boolean; href?: string };
|
|
34
|
+
type Run = InlineStyle & { text: string };
|
|
35
|
+
type HtmlTextBlock = { tag: HtmlBlockTag; runs: Run[] };
|
|
36
|
+
type HtmlListBlock = { tag: HtmlListTag; items: HtmlTextBlock[][] };
|
|
37
|
+
export type HtmlPasteBlock = HtmlTextBlock | HtmlListBlock;
|
|
38
|
+
|
|
39
|
+
/** Parse a detached document; never insert clipboard HTML into the editor DOM. */
|
|
40
|
+
export function parse_html_paste(html: string): HtmlPasteBlock[] {
|
|
41
|
+
const doc = new DOMParser().parseFromString(html, 'text/html');
|
|
42
|
+
const blocks: HtmlPasteBlock[] = [];
|
|
43
|
+
let current: HtmlTextBlock = { tag: 'p', runs: [] };
|
|
44
|
+
let current_list: HtmlListBlock | null = null;
|
|
45
|
+
let current_item: HtmlTextBlock[] | null = null;
|
|
46
|
+
const flush = () => {
|
|
47
|
+
// HTML whitespace collapses across inline elements, but explicit breaks survive.
|
|
48
|
+
while (current.runs.length && !current.runs[0].text.replace(/^ +/, '')) current.runs.shift();
|
|
49
|
+
if (current.runs.length) current.runs[0].text = current.runs[0].text.replace(/^ +/, '');
|
|
50
|
+
while (current.runs.length && !current.runs.at(-1)!.text.replace(/ +$/, '')) current.runs.pop();
|
|
51
|
+
if (current.runs.length)
|
|
52
|
+
current.runs.at(-1)!.text = current.runs.at(-1)!.text.replace(/ +$/, '');
|
|
53
|
+
// Drop empty paragraphs such as <p><br></p> or Word's <p> </p>.
|
|
54
|
+
if (current.runs.some((run) => run.text.trim())) (current_item || blocks).push(current);
|
|
55
|
+
current = { tag: 'p', runs: [] };
|
|
56
|
+
};
|
|
57
|
+
const walk = (node: Node, style: InlineStyle) => {
|
|
58
|
+
if (node.nodeType === 3) {
|
|
59
|
+
let text = (node.textContent || '').replace(/[\t\n\r\f ]+/g, ' ');
|
|
60
|
+
if (!current.runs.length || /[ \n]$/.test(current.runs.at(-1)!.text))
|
|
61
|
+
text = text.replace(/^ /, '');
|
|
62
|
+
if (text) current.runs.push({ ...style, text });
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (node.nodeType !== 1) return;
|
|
66
|
+
const element = node as Element;
|
|
67
|
+
const tag = element.localName;
|
|
68
|
+
if (
|
|
69
|
+
['script', 'style', 'template', 'noscript', 'iframe', 'object', 'svg', 'math'].includes(tag)
|
|
70
|
+
)
|
|
71
|
+
return;
|
|
72
|
+
if (tag === 'br') {
|
|
73
|
+
current.runs.push({ ...style, text: '\n' });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const next_style = { ...style };
|
|
77
|
+
if (tag === 'b' || tag === 'strong') next_style.bold = true;
|
|
78
|
+
// Inline font-weight wins over the tag: Google Docs wraps the whole clipboard in
|
|
79
|
+
// <b style="font-weight:normal">, and Docs and Word express bold as styled spans.
|
|
80
|
+
const font_weight = (element as HTMLElement).style?.fontWeight;
|
|
81
|
+
if (font_weight) {
|
|
82
|
+
next_style.bold = ['bold', 'bolder'].includes(font_weight) || Number(font_weight) >= 600;
|
|
83
|
+
}
|
|
84
|
+
if (tag === 'a') next_style.href = safe_html_href(element.getAttribute('href') || '');
|
|
85
|
+
if ((tag === 'ul' || tag === 'ol') && !current_list) {
|
|
86
|
+
flush();
|
|
87
|
+
const list: HtmlListBlock = { tag, items: [] };
|
|
88
|
+
current_list = list;
|
|
89
|
+
for (const child of element.childNodes) walk(child, next_style);
|
|
90
|
+
flush();
|
|
91
|
+
current_list = null;
|
|
92
|
+
list.items = list.items.filter((item) => item.length);
|
|
93
|
+
if (list.items.length) blocks.push(list);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
// Nested items join the outer list while retaining their own text blocks.
|
|
97
|
+
if (tag === 'li' && current_list) {
|
|
98
|
+
flush();
|
|
99
|
+
const parent_item = current_item;
|
|
100
|
+
current_item = [];
|
|
101
|
+
current_list.items.push(current_item);
|
|
102
|
+
for (const child of element.childNodes) walk(child, next_style);
|
|
103
|
+
flush();
|
|
104
|
+
current_item = parent_item;
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const is_block =
|
|
108
|
+
/^(p|h[1-6]|div|section|article|header|footer|main|aside|blockquote|pre|ul|ol|li|table|tr|td|th)$/.test(
|
|
109
|
+
tag
|
|
110
|
+
);
|
|
111
|
+
if (is_block) {
|
|
112
|
+
flush();
|
|
113
|
+
current.tag = /^h[1-6]$/.test(tag) ? (tag as HtmlBlockTag) : 'p';
|
|
114
|
+
}
|
|
115
|
+
for (const child of element.childNodes) walk(child, next_style);
|
|
116
|
+
if (is_block) flush();
|
|
117
|
+
};
|
|
118
|
+
for (const child of doc.body.childNodes) walk(child, {});
|
|
119
|
+
flush();
|
|
120
|
+
return blocks;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Map runs to exclusive Svedit marks. Supported links take precedence over bold. */
|
|
124
|
+
export function map_html_text(
|
|
125
|
+
blocks: HtmlPasteBlock[],
|
|
126
|
+
config: HtmlPasteConfig,
|
|
127
|
+
schema: DocumentSchema,
|
|
128
|
+
property: PropertyDefinition,
|
|
129
|
+
nodes: Record<string, DocumentNode>
|
|
130
|
+
): Text {
|
|
131
|
+
const result: Text = { content: '', marks: [], annotations: [] };
|
|
132
|
+
const allowed = property.type === 'text' ? property.mark_types || [] : [];
|
|
133
|
+
let previous_key = '';
|
|
134
|
+
const supported = (mapping: HtmlMarkMapping | null | undefined) =>
|
|
135
|
+
mapping && schema[mapping.type]?.kind === 'mark' && allowed.includes(mapping.type)
|
|
136
|
+
? mapping
|
|
137
|
+
: null;
|
|
138
|
+
const text_blocks = blocks.flatMap((block) => ('items' in block ? block.items.flat() : [block]));
|
|
139
|
+
for (const [index, block] of text_blocks.entries()) {
|
|
140
|
+
if (index) {
|
|
141
|
+
result.content += property.type === 'text' && property.allow_newlines ? '\n\n' : ' ';
|
|
142
|
+
previous_key = '';
|
|
143
|
+
}
|
|
144
|
+
for (const run of block.runs) {
|
|
145
|
+
let text =
|
|
146
|
+
property.type === 'text' && property.allow_newlines
|
|
147
|
+
? run.text
|
|
148
|
+
: run.text.replace(/[ \t\r\n\f]+/g, ' ');
|
|
149
|
+
if (property.type === 'text' && !property.allow_newlines && result.content.endsWith(' ')) {
|
|
150
|
+
text = text.replace(/^ +/, '');
|
|
151
|
+
}
|
|
152
|
+
const start_offset = result.content.length;
|
|
153
|
+
result.content += text;
|
|
154
|
+
const end_offset = result.content.length;
|
|
155
|
+
const link = config.marks?.link;
|
|
156
|
+
const mapping =
|
|
157
|
+
(run.href && supported(typeof link === 'function' ? link({ href: run.href }) : link)) ||
|
|
158
|
+
(run.bold && supported(config.marks?.bold));
|
|
159
|
+
const key = mapping ? JSON.stringify(mapping) : '';
|
|
160
|
+
if (mapping && end_offset > start_offset) {
|
|
161
|
+
const previous = result.marks.at(-1);
|
|
162
|
+
if (previous && previous_key === key && previous.end_offset === start_offset)
|
|
163
|
+
previous.end_offset = end_offset;
|
|
164
|
+
else {
|
|
165
|
+
let id_index = Object.keys(nodes).length;
|
|
166
|
+
while (nodes[`html_mark_${id_index}`]) id_index++;
|
|
167
|
+
const id = `html_mark_${id_index}`;
|
|
168
|
+
nodes[id] = { ...mapping.properties, id, type: mapping.type };
|
|
169
|
+
result.marks.push({ node_id: id, start_offset, end_offset });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
previous_key = key;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// Convert UTF-16 ranges only after the complete string is assembled: a grapheme
|
|
176
|
+
// (such as a combining accent or emoji) can span adjacent HTML elements.
|
|
177
|
+
const boundaries = [
|
|
178
|
+
...new Intl.Segmenter('en', { granularity: 'grapheme' }).segment(result.content)
|
|
179
|
+
].map((segment) => segment.index);
|
|
180
|
+
boundaries.push(result.content.length);
|
|
181
|
+
let offset = 0;
|
|
182
|
+
for (const mark of result.marks) {
|
|
183
|
+
while (boundaries[offset + 1] <= mark.start_offset) offset++;
|
|
184
|
+
mark.start_offset = offset;
|
|
185
|
+
while (boundaries[offset + 1] <= mark.end_offset) offset++;
|
|
186
|
+
mark.end_offset = offset;
|
|
187
|
+
}
|
|
188
|
+
result.marks = result.marks.filter((mark) => {
|
|
189
|
+
if (mark.end_offset > mark.start_offset) return true;
|
|
190
|
+
delete nodes[mark.node_id];
|
|
191
|
+
return false;
|
|
192
|
+
});
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function map_html_blocks(
|
|
197
|
+
blocks: HtmlPasteBlock[],
|
|
198
|
+
config: HtmlPasteConfig,
|
|
199
|
+
schema: DocumentSchema,
|
|
200
|
+
property: PropertyDefinition
|
|
201
|
+
) {
|
|
202
|
+
if (property.type !== 'node_array') return null;
|
|
203
|
+
const nodes: Record<string, DocumentNode> = {};
|
|
204
|
+
let node_index = 0;
|
|
205
|
+
const add_node = (type: string, properties: Record<string, unknown>) => {
|
|
206
|
+
const id = `html_block_${node_index++}`;
|
|
207
|
+
nodes[id] = { ...properties, id, type };
|
|
208
|
+
return id;
|
|
209
|
+
};
|
|
210
|
+
const add_text = (blocks: HtmlTextBlock[], mapping: HtmlBlockMapping) =>
|
|
211
|
+
add_node(mapping.type, {
|
|
212
|
+
...mapping.properties,
|
|
213
|
+
[mapping.text_property]: map_html_text(
|
|
214
|
+
blocks,
|
|
215
|
+
config,
|
|
216
|
+
schema,
|
|
217
|
+
schema[mapping.type].properties[mapping.text_property],
|
|
218
|
+
nodes
|
|
219
|
+
)
|
|
220
|
+
});
|
|
221
|
+
const add_container = (mapping: HtmlContainerMapping, children: string[]) =>
|
|
222
|
+
add_node(mapping.type, {
|
|
223
|
+
...mapping.properties,
|
|
224
|
+
[mapping.children_property]: { nodes: children, marks: [], annotations: [] }
|
|
225
|
+
});
|
|
226
|
+
const map_blocks = (blocks: HtmlPasteBlock[], target: PropertyDefinition): string[] | null => {
|
|
227
|
+
if (target.type !== 'node_array') return null;
|
|
228
|
+
const ids: string[] = [];
|
|
229
|
+
for (const block of blocks) {
|
|
230
|
+
if ('items' in block) {
|
|
231
|
+
const mapping = config.lists?.[block.tag];
|
|
232
|
+
const children = mapping && schema[mapping.type]?.properties[mapping.children_property];
|
|
233
|
+
if (
|
|
234
|
+
mapping &&
|
|
235
|
+
target.node_types.includes(mapping.type) &&
|
|
236
|
+
children?.type === 'node_array' &&
|
|
237
|
+
children.node_types.includes(mapping.item.type) &&
|
|
238
|
+
schema[mapping.item.type]?.kind === 'text' &&
|
|
239
|
+
schema[mapping.item.type].properties[mapping.item.text_property]?.type === 'text'
|
|
240
|
+
) {
|
|
241
|
+
ids.push(
|
|
242
|
+
add_container(
|
|
243
|
+
mapping,
|
|
244
|
+
block.items.map((item) => add_text(item, mapping.item))
|
|
245
|
+
)
|
|
246
|
+
);
|
|
247
|
+
} else {
|
|
248
|
+
const items = map_blocks(block.items.flat(), target);
|
|
249
|
+
if (!items) return null;
|
|
250
|
+
ids.push(...items);
|
|
251
|
+
}
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
const rule = config.blocks?.[block.tag];
|
|
255
|
+
let mapping = typeof rule === 'function' ? rule({ tag: block.tag }) : rule;
|
|
256
|
+
if (
|
|
257
|
+
!mapping ||
|
|
258
|
+
!target.node_types.includes(mapping.type) ||
|
|
259
|
+
schema[mapping.type]?.kind !== 'text' ||
|
|
260
|
+
schema[mapping.type]?.properties[mapping.text_property]?.type !== 'text'
|
|
261
|
+
) {
|
|
262
|
+
const type = get_default_text_node(target, schema);
|
|
263
|
+
const text_property = get_text_property_name(type, schema);
|
|
264
|
+
if (!type || !text_property || !target.node_types.includes(type)) return null;
|
|
265
|
+
mapping = { type, text_property };
|
|
266
|
+
}
|
|
267
|
+
ids.push(add_text([block], mapping));
|
|
268
|
+
}
|
|
269
|
+
return ids.length ? ids : null;
|
|
270
|
+
};
|
|
271
|
+
let main_nodes = map_blocks(blocks, property);
|
|
272
|
+
const wrapper = config.wrapper;
|
|
273
|
+
if (!main_nodes && wrapper && property.node_types.includes(wrapper.type)) {
|
|
274
|
+
const children = schema[wrapper.type]?.properties[wrapper.children_property];
|
|
275
|
+
const ids = children && map_blocks(blocks, children);
|
|
276
|
+
if (ids) main_nodes = [add_container(wrapper, ids)];
|
|
277
|
+
}
|
|
278
|
+
return main_nodes ? { nodes, main_nodes } : null;
|
|
279
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Escape text or a quoted HTML attribute value. */
|
|
2
|
+
export function escape_html(value: string): string {
|
|
3
|
+
return value
|
|
4
|
+
.replace(/&/g, '&')
|
|
5
|
+
.replace(/</g, '<')
|
|
6
|
+
.replace(/>/g, '>')
|
|
7
|
+
.replace(/"/g, '"')
|
|
8
|
+
.replace(/'/g, ''');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function safe_html_href(value: string): string | undefined {
|
|
12
|
+
const href = value.trim();
|
|
13
|
+
if (!href || [...href].some((char) => char.charCodeAt(0) <= 32 || char.charCodeAt(0) === 127))
|
|
14
|
+
return;
|
|
15
|
+
const scheme = href.match(/^([a-z][a-z\d+.-]*):/i)?.[1].toLowerCase();
|
|
16
|
+
return !scheme || ['http', 'https', 'mailto', 'tel'].includes(scheme) ? href : undefined;
|
|
17
|
+
}
|
package/src/lib/index.ts
CHANGED
|
@@ -54,6 +54,7 @@ export type {
|
|
|
54
54
|
TextPropertyProps,
|
|
55
55
|
CustomPropertyProps,
|
|
56
56
|
NodeArrayPropertyProps,
|
|
57
|
+
NodeGapToolsProps,
|
|
57
58
|
NodeProps,
|
|
58
59
|
NodeArrayAttachmentContext
|
|
59
60
|
} from './types.js';
|
|
@@ -84,3 +85,17 @@ export { KeyMapper, define_keymap } from './KeyMapper.svelte.js';
|
|
|
84
85
|
// Transforms and utilities
|
|
85
86
|
export * from './transforms.svelte.js';
|
|
86
87
|
export * from './utils.js';
|
|
88
|
+
|
|
89
|
+
export type {
|
|
90
|
+
HtmlPasteConfig,
|
|
91
|
+
HtmlBlockTag,
|
|
92
|
+
HtmlBlockMapping,
|
|
93
|
+
HtmlMarkMapping,
|
|
94
|
+
HtmlListTag,
|
|
95
|
+
HtmlListMapping,
|
|
96
|
+
HtmlContainerMapping
|
|
97
|
+
} from './html_paste.js';
|
|
98
|
+
|
|
99
|
+
export { export_text_html } from './html_export.js';
|
|
100
|
+
export type { MarkHtmlExporter } from './html_export.js';
|
|
101
|
+
export { escape_html, safe_html_href } from './html_utils.js';
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
@layer svedit-defaults {
|
|
2
2
|
:root {
|
|
3
|
-
|
|
4
|
-
--
|
|
5
|
-
|
|
6
|
-
--
|
|
7
|
-
|
|
3
|
+
/* Primary accent for active editing controls, carets, and selections. */
|
|
4
|
+
--editing: oklch(60% 0.22 283);
|
|
5
|
+
/* Low-opacity version of the editing accent for passive highlights. */
|
|
6
|
+
--editing-muted: oklch(from var(--editing) l c h / 0.1);
|
|
7
|
+
/* Low-emphasis strokes and outlines, such as inactive node gaps. */
|
|
8
|
+
--editing-stroke: color-mix(in oklch, currentColor 25%, transparent);
|
|
9
|
+
/* Foreground color for content displayed on the editing accent. */
|
|
10
|
+
--editing-foreground: oklch(100% 0 0);
|
|
8
11
|
}
|
|
9
12
|
}
|
package/src/lib/types.ts
CHANGED
|
@@ -21,7 +21,12 @@ export type NodeId = string;
|
|
|
21
21
|
|
|
22
22
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
23
23
|
export type DynamicRecord = Record<string, any>;
|
|
24
|
-
export type SessionConfig = DynamicRecord
|
|
24
|
+
export type SessionConfig = DynamicRecord & {
|
|
25
|
+
html_paste?: import('./html_paste.js').HtmlPasteConfig;
|
|
26
|
+
mark_html_exporters?: Record<string, import('./html_export.js').MarkHtmlExporter>;
|
|
27
|
+
view_classes?: boolean;
|
|
28
|
+
visibility_culling?: boolean;
|
|
29
|
+
};
|
|
25
30
|
export type CommandRegistry = DynamicRecord;
|
|
26
31
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
27
32
|
export type DocumentOperation = [string, ...any[]];
|
|
@@ -458,6 +463,24 @@ export type NodeArrayPropertyProps = {
|
|
|
458
463
|
[key: string]: unknown;
|
|
459
464
|
};
|
|
460
465
|
|
|
466
|
+
/**
|
|
467
|
+
* Props passed to the consumer's `system_components.node_gap_tools`
|
|
468
|
+
* component. It renders inside the active node gap marker (the one carrying
|
|
469
|
+
* the caret), so it inherits the `--row` orientation variable and
|
|
470
|
+
* appears/disappears together with the caret. The marker has
|
|
471
|
+
* pointer-events: none — interactive tools must set pointer-events: auto.
|
|
472
|
+
*/
|
|
473
|
+
export type NodeGapToolsProps = {
|
|
474
|
+
/** The full path to the node_array the gap belongs to */
|
|
475
|
+
path: DocumentPath;
|
|
476
|
+
/** The insertion offset of the gap (0..count) */
|
|
477
|
+
offset: number;
|
|
478
|
+
/** True for the gap before the first node */
|
|
479
|
+
is_first: boolean;
|
|
480
|
+
/** True for the gap after the last node */
|
|
481
|
+
is_last: boolean;
|
|
482
|
+
};
|
|
483
|
+
|
|
461
484
|
/**
|
|
462
485
|
* Props for the Node component
|
|
463
486
|
*/
|