svedit 0.14.0 → 0.15.1
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 -0
- package/dist/NodeSelectionMarkers.svelte +12 -1
- package/dist/Session.svelte.d.ts.map +1 -1
- package/dist/Session.svelte.js +17 -9
- package/dist/Svedit.svelte +99 -27
- package/dist/Svedit.svelte.d.ts.map +1 -1
- package/dist/Transaction.svelte.d.ts +6 -2
- package/dist/Transaction.svelte.d.ts.map +1 -1
- package/dist/Transaction.svelte.js +15 -7
- 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 +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/transforms.svelte.d.ts.map +1 -1
- package/dist/transforms.svelte.js +12 -0
- package/dist/types.d.ts +6 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +27 -26
- package/src/lib/NodeSelectionMarkers.svelte +12 -1
- package/src/lib/Session.svelte.ts +16 -11
- package/src/lib/Svedit.svelte +99 -27
- package/src/lib/Transaction.svelte.ts +20 -7
- 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 +14 -0
- package/src/lib/transforms.svelte.ts +14 -0
- package/src/lib/types.ts +6 -1
package/README.md
CHANGED
|
@@ -75,6 +75,8 @@ Each node has a `kind` that determines its behavior:
|
|
|
75
75
|
- Pressing Enter splits the node into two nodes of the same type
|
|
76
76
|
- Pressing Backspace at position 0 joins it with the previous node
|
|
77
77
|
|
|
78
|
+
When `break_text_node` splits through a mark or annotation, the right-hand occurrence receives an independent attachment node with the same properties. Editing one link or annotation therefore does not change the other. Ranges moved wholly to one side retain their ids. Subtree copying and pasting through `Transaction.build` also detach repeated attachment references in the copied data. These operations do not rewrite attachment references in existing stored documents.
|
|
79
|
+
|
|
78
80
|
If **any** of those assumptions don't hold, use `kind: 'block'`. Blocks can still have `text` properties with full editing support (typing, formatting, selection) — they just don't participate in split/join.
|
|
79
81
|
|
|
80
82
|
> **Common mistake:** A quote node with `content` + `author` properties might seem like `kind: 'text'` because both fields are editable text. But splitting a quote into two half-quotes doesn't make sense, and `join_text_node` hard-codes `node.content` — so Backspace in the `author` field would join the wrong property with the previous block and drop the author text. The correct kind is `'block'`.
|
|
@@ -436,6 +438,138 @@ Defaults make it safe to add new defaultable properties, but they are not a repl
|
|
|
436
438
|
|
|
437
439
|
Documents need a config object that tells Svedit how to render and manipulate your content. See the full example in [`src/routes/demo_config.ts`](src/routes/demo_config.ts).
|
|
438
440
|
|
|
441
|
+
### Importing pasted HTML
|
|
442
|
+
|
|
443
|
+
Set `config.html_paste` to enable external HTML import. Without it, paste keeps its
|
|
444
|
+
existing plain-text behavior. Native Svedit clipboard data and media paste keep
|
|
445
|
+
their existing paths and bypass this importer.
|
|
446
|
+
|
|
447
|
+
```ts
|
|
448
|
+
import type { HtmlPasteConfig } from 'svedit';
|
|
449
|
+
|
|
450
|
+
const html_paste: HtmlPasteConfig = {
|
|
451
|
+
blocks: {
|
|
452
|
+
p: { type: 'paragraph', text_property: 'content' },
|
|
453
|
+
h1: { type: 'heading1', text_property: 'content' },
|
|
454
|
+
h2: { type: 'heading2', text_property: 'content' }
|
|
455
|
+
},
|
|
456
|
+
marks: {
|
|
457
|
+
bold: { type: 'strong' },
|
|
458
|
+
link: ({ href }) => ({ type: 'link', properties: { href } })
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
const config = { /* other configuration */ html_paste };
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
Use your app's schema names (the demo uses `heading_1`, for example). Blocks accept
|
|
466
|
+
`p` and `h1` through `h6`. Each mapping names a text node type and its text property.
|
|
467
|
+
A block mapping can also be a synchronous factory returning the same descriptor,
|
|
468
|
+
with extra node properties, or `null` to use the destination's default text node:
|
|
469
|
+
|
|
470
|
+
```ts
|
|
471
|
+
h1: ({ tag }) => ({
|
|
472
|
+
type: 'heading',
|
|
473
|
+
text_property: 'content',
|
|
474
|
+
properties: { level: Number(tag.slice(1)) }
|
|
475
|
+
});
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
`<strong>` and `<b>` use the `bold` mapping. Link factories receive the decoded,
|
|
479
|
+
trimmed `href`; they may return `null` to discard link formatting. The importer
|
|
480
|
+
accepts HTTP(S), mailto, tel, and relative links, and drops unsafe schemes.
|
|
481
|
+
Mappings describe data; they should not mutate the session. Svedit creates IDs
|
|
482
|
+
and fills schema defaults through its normal insertion transaction.
|
|
483
|
+
|
|
484
|
+
Map HTML lists separately because they contain an array of text nodes:
|
|
485
|
+
|
|
486
|
+
```ts
|
|
487
|
+
html_paste.lists = {
|
|
488
|
+
ul: {
|
|
489
|
+
type: 'list',
|
|
490
|
+
children_property: 'list_items',
|
|
491
|
+
item: { type: 'list_item', text_property: 'content' },
|
|
492
|
+
properties: { layout: 'square' }
|
|
493
|
+
},
|
|
494
|
+
ol: {
|
|
495
|
+
type: 'list',
|
|
496
|
+
children_property: 'list_items',
|
|
497
|
+
item: { type: 'list_item', text_property: 'content' },
|
|
498
|
+
properties: { layout: 'decimal' }
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
All type and property names come from your schema. Nested HTML lists flatten into
|
|
504
|
+
one list in item order, using the outer list's mapping. An item with multiple
|
|
505
|
+
paragraphs becomes one text node. Unmapped or disallowed lists fall back to the
|
|
506
|
+
destination's default text nodes; text fields receive flattened list text.
|
|
507
|
+
|
|
508
|
+
If a destination requires a containing block, configure an optional wrapper:
|
|
509
|
+
|
|
510
|
+
```ts
|
|
511
|
+
html_paste.wrapper = { type: 'prose', children_property: 'body' };
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
The wrapper is used only when blocks cannot be mapped directly to the destination
|
|
515
|
+
and the destination allows its type. Its `children_property` must be a node array;
|
|
516
|
+
blocks are mapped against that property's allowed node types. Wrapper mappings
|
|
517
|
+
also accept `properties` for schema-specific defaults.
|
|
518
|
+
|
|
519
|
+
Import respects the destination's allowed node and mark types. Unmapped or
|
|
520
|
+
disallowed headings fall back to its default text node; unsupported marks keep
|
|
521
|
+
their text. Marks are exclusive in Svedit, so a supported link takes precedence
|
|
522
|
+
over bold where they overlap. Existing active-mark insertion behavior still applies.
|
|
523
|
+
|
|
524
|
+
At a node caret, HTML blocks become nodes. A single text block pasted into text
|
|
525
|
+
retains the destination node type. A configured list follows the node insertion
|
|
526
|
+
path, even when it contains only one item. Multiple blocks pasted into a text node
|
|
527
|
+
follow the existing multi-paragraph behavior: insert nodes after that node without splitting
|
|
528
|
+
or replacing its text. In block fields such as titles and captions, blocks are
|
|
529
|
+
joined with blank lines, or spaces when `allow_newlines` is false.
|
|
530
|
+
|
|
531
|
+
The importer recognizes semantic tags, common block wrappers, entities and
|
|
532
|
+
`<br>`. Unsupported containers retain their text but do not import table
|
|
533
|
+
structure, images, or CSS formatting. Lists retain structure when a list mapping
|
|
534
|
+
is configured. Script/style content is ignored. Empty or
|
|
535
|
+
unusable HTML, and mapping errors, fall back to the existing plain-text path.
|
|
536
|
+
|
|
537
|
+
### Exporting copied rich text
|
|
538
|
+
|
|
539
|
+
Copy and cut include both the embedded `data-svedit` payload and readable HTML.
|
|
540
|
+
Configure `mark_html_exporters` to map your schema's marks to HTML. Unknown marks
|
|
541
|
+
keep their text; data-only annotations remain in the native payload.
|
|
542
|
+
|
|
543
|
+
```ts
|
|
544
|
+
import { export_text_html, escape_html, safe_html_href } from 'svedit';
|
|
545
|
+
import type { MarkHtmlExporter } from 'svedit';
|
|
546
|
+
|
|
547
|
+
const mark_html_exporters = {
|
|
548
|
+
strong: (_node, content) => `<strong>${content}</strong>`,
|
|
549
|
+
link: (node, content) => {
|
|
550
|
+
const href = safe_html_href(node.href || '');
|
|
551
|
+
return href ? `<a href="${escape_html(href)}">${content}</a>` : content;
|
|
552
|
+
}
|
|
553
|
+
} satisfies Record<string, MarkHtmlExporter>;
|
|
554
|
+
|
|
555
|
+
const config = {
|
|
556
|
+
mark_html_exporters,
|
|
557
|
+
html_exporters: {
|
|
558
|
+
paragraph: (node, session) => `<p>${export_text_html(node.content, session)}</p>`,
|
|
559
|
+
heading1: (node, session) => `<h1>${export_text_html(node.content, session)}</h1>`
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
Text selections and the default node exporter use this automatically. Existing
|
|
565
|
+
custom `html_exporters` should call `export_text_html` for their text properties,
|
|
566
|
+
as above. The helper escapes text, preserves line breaks with `<br>`, and uses
|
|
567
|
+
Svedit's grapheme offsets. Mark exporters receive already escaped inline HTML;
|
|
568
|
+
only attribute values need escaping. Import and export mappings are separate
|
|
569
|
+
because import factories cannot be reliably reversed.
|
|
570
|
+
|
|
571
|
+
### Custom handlers
|
|
572
|
+
|
|
439
573
|
Two optional hooks are especially useful when integrating custom media workflows:
|
|
440
574
|
|
|
441
575
|
- `handle_media_paste(session, pasted_media)`
|
|
@@ -1453,12 +1587,33 @@ The source code is compact and readable — less than 3000 LOC across a handful
|
|
|
1453
1587
|
|
|
1454
1588
|
## Developing Svedit
|
|
1455
1589
|
|
|
1590
|
+
Development requires Node.js 22.17 or newer.
|
|
1591
|
+
|
|
1456
1592
|
Once you've cloned the Svedit repository and installed dependencies with `pnpm install`, start a development server:
|
|
1457
1593
|
|
|
1458
1594
|
```bash
|
|
1459
1595
|
pnpm dev
|
|
1460
1596
|
```
|
|
1461
1597
|
|
|
1598
|
+
### Running tests
|
|
1599
|
+
|
|
1600
|
+
Run the browser tests in Vitest's interactive watch mode:
|
|
1601
|
+
|
|
1602
|
+
```bash
|
|
1603
|
+
pnpm test:unit
|
|
1604
|
+
```
|
|
1605
|
+
|
|
1606
|
+
For a single run, use `pnpm test`.
|
|
1607
|
+
|
|
1608
|
+
The tests exercise real browser focus, selection, and keyboard behavior. Keep the
|
|
1609
|
+
browser runner tab and window focused and let each run finish uninterrupted.
|
|
1610
|
+
Do not switch tabs or applications, open DevTools, or click inside the runner
|
|
1611
|
+
while tests are running. Losing focus can cause selection and keyboard tests to
|
|
1612
|
+
fail even when the editor works correctly.
|
|
1613
|
+
|
|
1614
|
+
If a run was interrupted, return focus to the runner and rerun the tests. Failures
|
|
1615
|
+
that persist during a focused, uninterrupted run should be investigated.
|
|
1616
|
+
|
|
1462
1617
|
## Building
|
|
1463
1618
|
|
|
1464
1619
|
To create a production version of your app:
|
|
@@ -24,13 +24,18 @@
|
|
|
24
24
|
{#if svedit.session.selection?.type === 'property'}
|
|
25
25
|
<div
|
|
26
26
|
class="selected-property-overlay"
|
|
27
|
+
class:unfocused={!svedit.canvas_focused}
|
|
27
28
|
style="position-anchor: --{serialize_path(svedit.session.selection.path)};"
|
|
28
29
|
></div>
|
|
29
30
|
{/if}
|
|
30
31
|
|
|
31
32
|
{#if selected_node_paths}
|
|
32
33
|
{#each selected_node_paths as path (serialize_path(path))}
|
|
33
|
-
<div
|
|
34
|
+
<div
|
|
35
|
+
class="selected-node-overlay"
|
|
36
|
+
class:unfocused={!svedit.canvas_focused}
|
|
37
|
+
style="position-anchor: --{serialize_path(path)};"
|
|
38
|
+
></div>
|
|
34
39
|
{/each}
|
|
35
40
|
{/if}
|
|
36
41
|
|
|
@@ -61,4 +66,10 @@
|
|
|
61
66
|
pointer-events: none;
|
|
62
67
|
z-index: 12;
|
|
63
68
|
}
|
|
69
|
+
|
|
70
|
+
.selected-node-overlay.unfocused,
|
|
71
|
+
.selected-property-overlay.unfocused {
|
|
72
|
+
background: oklch(from var(--editing-muted) l 0 h / alpha);
|
|
73
|
+
outline-color: oklch(from var(--editing) l 0 h);
|
|
74
|
+
}
|
|
64
75
|
</style>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Session.svelte.d.ts","sourceRoot":"","sources":["../src/lib/Session.svelte.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"Session.svelte.d.ts","sourceRoot":"","sources":["../src/lib/Session.svelte.ts"],"names":[],"mappings":"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"}
|
package/dist/Session.svelte.js
CHANGED
|
@@ -361,17 +361,25 @@ export default class Session {
|
|
|
361
361
|
* @returns The next node insert caret selection, or null if none is available
|
|
362
362
|
*/
|
|
363
363
|
get_next_node_insert_caret(selection = this.selection) {
|
|
364
|
-
|
|
365
|
-
if (!selection || selection.path.length <= 2) {
|
|
364
|
+
if (!selection)
|
|
366
365
|
return null;
|
|
366
|
+
// Array indexes identify insertion positions. Single-node references can
|
|
367
|
+
// occur between arrays, so removing a fixed pair of segments is unsafe.
|
|
368
|
+
for (let index = selection.path.length - 1; index >= 1; index--) {
|
|
369
|
+
const node_index = selection.path[index];
|
|
370
|
+
if (typeof node_index !== 'number')
|
|
371
|
+
continue;
|
|
372
|
+
const parent_path = selection.path.slice(0, index);
|
|
373
|
+
if (this.inspect(parent_path).type !== 'node_array')
|
|
374
|
+
continue;
|
|
375
|
+
return {
|
|
376
|
+
type: 'node',
|
|
377
|
+
path: parent_path,
|
|
378
|
+
anchor_offset: node_index + 1,
|
|
379
|
+
focus_offset: node_index + 1
|
|
380
|
+
};
|
|
367
381
|
}
|
|
368
|
-
|
|
369
|
-
return {
|
|
370
|
-
type: 'node',
|
|
371
|
-
path: selection.path.slice(0, -2),
|
|
372
|
-
anchor_offset: node_offset,
|
|
373
|
-
focus_offset: node_offset
|
|
374
|
-
};
|
|
382
|
+
return null;
|
|
375
383
|
}
|
|
376
384
|
get_selected_text() {
|
|
377
385
|
if (this.selection?.type !== 'text')
|
package/dist/Svedit.svelte
CHANGED
|
@@ -19,6 +19,9 @@
|
|
|
19
19
|
get_default_text_node,
|
|
20
20
|
create_plain_text_nodes_payload
|
|
21
21
|
} from './paste_utils.js';
|
|
22
|
+
import { export_text_html } from './html_export.js';
|
|
23
|
+
import { escape_html } from './html_utils.js';
|
|
24
|
+
import { parse_html_paste, map_html_text, map_html_blocks } from './html_paste.js';
|
|
22
25
|
import { create_node_visibility } from './node_visibility.svelte.js';
|
|
23
26
|
import DefaultNodeSelectionMarkers from './NodeSelectionMarkers.svelte';
|
|
24
27
|
import './styles/svedit-colors.css';
|
|
@@ -31,6 +34,7 @@
|
|
|
31
34
|
NodeSelection,
|
|
32
35
|
TextSelection,
|
|
33
36
|
PropertySelection,
|
|
37
|
+
PropertyDefinition,
|
|
34
38
|
DocumentNode,
|
|
35
39
|
DocumentPath,
|
|
36
40
|
DynamicRecord,
|
|
@@ -394,12 +398,12 @@ ${fallback_html}`;
|
|
|
394
398
|
if (prop_name === 'id' || prop_name === 'type') continue;
|
|
395
399
|
const property_definition = node_schema.properties[prop_name];
|
|
396
400
|
// Check if this is a text property.
|
|
397
|
-
if (property_definition
|
|
401
|
+
if (property_definition?.type === 'text') {
|
|
398
402
|
const text_content = prop_value.content;
|
|
399
403
|
if (text_content.trim()) {
|
|
400
|
-
html += `<p>${
|
|
404
|
+
html += `<p>${export_text_html(prop_value, session)}</p>`;
|
|
401
405
|
}
|
|
402
|
-
} else if (property_definition
|
|
406
|
+
} else if (property_definition?.type === 'node_array') {
|
|
403
407
|
for (const child_id of prop_value.nodes) {
|
|
404
408
|
const child = session.get(child_id);
|
|
405
409
|
const child_exporter = html_exporters[child.type] || default_node_html_exporter;
|
|
@@ -480,7 +484,7 @@ ${fallback_html}`;
|
|
|
480
484
|
if (session.selection?.type === 'text') {
|
|
481
485
|
plain_text = session.get_selected_plain_text();
|
|
482
486
|
text = session.get_selected_text();
|
|
483
|
-
const fallback_html = `<span>${text.
|
|
487
|
+
const fallback_html = `<span>${export_text_html(text, session, text.nodes)}</span>`;
|
|
484
488
|
|
|
485
489
|
// console.log('Text copy:', {
|
|
486
490
|
// text,
|
|
@@ -517,8 +521,12 @@ ${fallback_html}`;
|
|
|
517
521
|
type: property_definition.type,
|
|
518
522
|
value
|
|
519
523
|
};
|
|
520
|
-
|
|
521
|
-
|
|
524
|
+
const fallback_html =
|
|
525
|
+
property_definition.type === 'text'
|
|
526
|
+
? export_text_html(value, session)
|
|
527
|
+
: escape_html(String(value));
|
|
528
|
+
html = create_svedit_html_format(json_data, `<span>${fallback_html}</span>`);
|
|
529
|
+
plain_text = property_definition.type === 'text' ? value.content : String(value);
|
|
522
530
|
}
|
|
523
531
|
|
|
524
532
|
// Write to clipboard using event.clipboardData
|
|
@@ -611,21 +619,22 @@ ${fallback_html}`;
|
|
|
611
619
|
const { nodes, main_nodes, marks = [], annotations = [] } = pasted_json || {};
|
|
612
620
|
if (!nodes || !main_nodes?.length) return false;
|
|
613
621
|
|
|
614
|
-
|
|
615
|
-
if (
|
|
616
|
-
|
|
617
|
-
}
|
|
618
|
-
if (tr.selection?.type !== 'node') return false;
|
|
619
|
-
|
|
620
|
-
const property_definition = session.inspect(tr.selection.path);
|
|
622
|
+
const target_selection = selection || session.selection;
|
|
623
|
+
if (target_selection?.type !== 'node') return false;
|
|
624
|
+
const property_definition = session.inspect(target_selection.path);
|
|
621
625
|
if (property_definition?.type !== 'node_array') return false;
|
|
622
626
|
|
|
627
|
+
const tr = session.tr;
|
|
628
|
+
if (selection) tr.set_selection(selection);
|
|
629
|
+
|
|
623
630
|
const default_text_node_type = get_default_text_node(property_definition, session.schema);
|
|
624
631
|
const target_text_property_name = get_text_property_name(
|
|
625
632
|
default_text_node_type,
|
|
626
633
|
session.schema
|
|
627
634
|
);
|
|
628
635
|
|
|
636
|
+
// Decide for the whole clipboard graph before creating any nodes or replacing the selection.
|
|
637
|
+
const preserve_ids = Object.values(nodes).every((node: DocumentNode) => !tr.doc.nodes[node.id]);
|
|
629
638
|
const nodes_to_insert = [];
|
|
630
639
|
let rejected = false;
|
|
631
640
|
for (const node_id of main_nodes) {
|
|
@@ -642,30 +651,36 @@ ${fallback_html}`;
|
|
|
642
651
|
default_text_node_type &&
|
|
643
652
|
target_text_property_name
|
|
644
653
|
) {
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
654
|
+
// Keep the clipboard graph so converted text retains its marks and annotations.
|
|
655
|
+
const new_node_id = tr.build(
|
|
656
|
+
node_id,
|
|
657
|
+
{
|
|
658
|
+
...nodes,
|
|
659
|
+
[node_id]: {
|
|
660
|
+
id: node_id,
|
|
661
|
+
type: default_text_node_type,
|
|
662
|
+
[target_text_property_name]: text_content || {
|
|
663
|
+
content: '',
|
|
664
|
+
marks: [],
|
|
665
|
+
annotations: []
|
|
666
|
+
}
|
|
653
667
|
}
|
|
654
|
-
}
|
|
655
|
-
|
|
668
|
+
},
|
|
669
|
+
{ preserve_ids }
|
|
670
|
+
);
|
|
656
671
|
nodes_to_insert.push(new_node_id);
|
|
657
672
|
} else {
|
|
658
673
|
rejected = true;
|
|
659
674
|
break;
|
|
660
675
|
}
|
|
661
676
|
} else {
|
|
662
|
-
const new_node_id = tr.build(node_id, nodes);
|
|
677
|
+
const new_node_id = tr.build(node_id, nodes, { preserve_ids });
|
|
663
678
|
nodes_to_insert.push(new_node_id);
|
|
664
679
|
}
|
|
665
680
|
}
|
|
666
681
|
|
|
667
682
|
if (!rejected) {
|
|
668
|
-
tr.insert_nodes(nodes_to_insert, marks, annotations, nodes);
|
|
683
|
+
tr.insert_nodes(nodes_to_insert, marks, annotations, nodes, { preserve_ids });
|
|
669
684
|
session.apply(tr);
|
|
670
685
|
return true;
|
|
671
686
|
}
|
|
@@ -679,13 +694,62 @@ ${fallback_html}`;
|
|
|
679
694
|
return false;
|
|
680
695
|
}
|
|
681
696
|
|
|
697
|
+
function try_html_paste(html: string): boolean {
|
|
698
|
+
const config = session.config.html_paste;
|
|
699
|
+
const selection = session.selection;
|
|
700
|
+
if (!config || !selection) return false;
|
|
701
|
+
const blocks = parse_html_paste(html);
|
|
702
|
+
if (!blocks.length) return false;
|
|
703
|
+
if (selection.type === 'text') {
|
|
704
|
+
const owner = session.get(selection.path.slice(0, -1));
|
|
705
|
+
const insert_as_nodes =
|
|
706
|
+
blocks.length > 1 ||
|
|
707
|
+
blocks.some(
|
|
708
|
+
(block) =>
|
|
709
|
+
'items' in block && (config.lists?.[block.tag] || block.items.flat().length > 1)
|
|
710
|
+
);
|
|
711
|
+
if (insert_as_nodes && owner && session.kind(owner) === 'text') {
|
|
712
|
+
const caret = get_node_insert_caret_after_text_selection(selection);
|
|
713
|
+
if (caret) {
|
|
714
|
+
const payload = map_html_blocks(
|
|
715
|
+
blocks,
|
|
716
|
+
config,
|
|
717
|
+
session.schema,
|
|
718
|
+
session.inspect(caret.path) as unknown as PropertyDefinition
|
|
719
|
+
);
|
|
720
|
+
if (payload && try_node_paste(payload, caret)) return true;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
const nodes: Record<string, DocumentNode> = {};
|
|
724
|
+
const text = map_html_text(
|
|
725
|
+
blocks,
|
|
726
|
+
config,
|
|
727
|
+
session.schema,
|
|
728
|
+
session.inspect(selection.path) as unknown as PropertyDefinition,
|
|
729
|
+
nodes
|
|
730
|
+
);
|
|
731
|
+
session.apply(session.tr.insert_text(text.content, text.marks, [], nodes));
|
|
732
|
+
return true;
|
|
733
|
+
}
|
|
734
|
+
const caret = get_target_node_insert_caret(selection);
|
|
735
|
+
if (!caret) return false;
|
|
736
|
+
const payload = map_html_blocks(
|
|
737
|
+
blocks,
|
|
738
|
+
config,
|
|
739
|
+
session.schema,
|
|
740
|
+
session.inspect(caret.path) as unknown as PropertyDefinition
|
|
741
|
+
);
|
|
742
|
+
return !!payload && try_node_paste(payload, caret);
|
|
743
|
+
}
|
|
744
|
+
|
|
682
745
|
async function onpaste(event: ClipboardEvent) {
|
|
683
746
|
// Only handle paste events if editable and focus is within the canvas
|
|
684
747
|
if (!editable) return;
|
|
685
748
|
if (!canvas_el?.contains(document.activeElement)) return;
|
|
686
749
|
event.preventDefault();
|
|
687
750
|
|
|
688
|
-
let
|
|
751
|
+
let html_content,
|
|
752
|
+
plain_text,
|
|
689
753
|
pasted_json,
|
|
690
754
|
pasted_media = [];
|
|
691
755
|
|
|
@@ -718,7 +782,7 @@ ${fallback_html}`;
|
|
|
718
782
|
} else {
|
|
719
783
|
// First try to extract svedit data from HTML format
|
|
720
784
|
try {
|
|
721
|
-
|
|
785
|
+
html_content = event.clipboardData?.getData('text/html');
|
|
722
786
|
if (html_content) {
|
|
723
787
|
pasted_json = extract_svedit_data_from_html(html_content);
|
|
724
788
|
}
|
|
@@ -733,6 +797,14 @@ ${fallback_html}`;
|
|
|
733
797
|
console.error('Failed to paste any content:', e);
|
|
734
798
|
}
|
|
735
799
|
|
|
800
|
+
if (!pasted_json && html_content && session.config.html_paste) {
|
|
801
|
+
try {
|
|
802
|
+
if (try_html_paste(html_content)) return;
|
|
803
|
+
} catch (error) {
|
|
804
|
+
console.warn('Failed to import pasted HTML; falling back to plain text:', error);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
736
808
|
// Try to construct a node payload from plain text when applicable
|
|
737
809
|
if (!pasted_json && typeof plain_text === 'string') {
|
|
738
810
|
plain_text = normalize_line_endings(plain_text);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Svedit.svelte.d.ts","sourceRoot":"","sources":["../src/lib/Svedit.svelte.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"Svedit.svelte.d.ts","sourceRoot":"","sources":["../src/lib/Svedit.svelte.ts"],"names":[],"mappings":"AA4BA,OAAO,4BAA4B,CAAC;AACpC,OAAO,gCAAgC,CAAC;AAExC,OAAO,KAAK,EACV,cAAc,EACd,WAAW,EAUX,MAAM,YAAY,CAAC;AAGrB,iBAAS,QAAQ,CAAC,CAAC,SAAS,cAAc;WAqqDb,WAAW,CAAC,CAAC,CAAC;aAAwB;QAAE,YAAY,aAAqB;KAAE;;;;EAAuE;AAC/K,cAAM,iBAAiB,CAAC,CAAC,SAAS,cAAc;IAC5C,KAAK,IAAI,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;IAChD,MAAM,IAAI,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;IAClD,KAAK,IAAI,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;IAChD,QAAQ;IACR,OAAO;;;CACV;AAED,UAAU,qBAAqB;IAC3B,KAAK,CAAC,SAAS,cAAc,EAAE,OAAO,EAAE,OAAO,QAAQ,EAAE,2BAA2B,CAAC,UAAU,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,QAAQ,EAAE,eAAe,CAAC,UAAU,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,EAAE,UAAU,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,UAAU,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG;QAAE,UAAU,CAAC,EAAE,UAAU,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAA;KAAE,GAAG,UAAU,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC;IACnZ,CAAC,CAAC,SAAS,cAAc,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,UAAU,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,GAAG,UAAU,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC;IAClJ,YAAY,CAAC,EAAE,UAAU,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC;CACjE;AACD,QAAA,MAAM,MAAM,EAAE,qBAAmC,CAAC;AAChC,KAAK,MAAM,CAAC,CAAC,SAAS,cAAc,IAAI,YAAY,CAAC,OAAO,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;AACzE,eAAe,MAAM,CAAC"}
|
|
@@ -109,7 +109,9 @@ export default class Transaction {
|
|
|
109
109
|
* ```
|
|
110
110
|
*/
|
|
111
111
|
set(path: DocumentPath, value: any): this;
|
|
112
|
-
build(node_id: NodeId, nodes: Record<string, DocumentNode
|
|
112
|
+
build(node_id: NodeId, nodes: Record<string, DocumentNode>, { preserve_ids }?: {
|
|
113
|
+
preserve_ids?: boolean;
|
|
114
|
+
}): NodeId;
|
|
113
115
|
/**
|
|
114
116
|
* Creates a new node in the document.
|
|
115
117
|
*
|
|
@@ -230,7 +232,9 @@ export default class Transaction {
|
|
|
230
232
|
* @param nodes - Node graph for restored marks and annotations
|
|
231
233
|
* @returns This transaction instance for method chaining
|
|
232
234
|
*/
|
|
233
|
-
insert_nodes(node_ids: NodeId[], marks?: Array<Mark>, annotations?: Array<Annotation>, nodes?: Record<NodeId, DocumentNode
|
|
235
|
+
insert_nodes(node_ids: NodeId[], marks?: Array<Mark>, annotations?: Array<Annotation>, nodes?: Record<NodeId, DocumentNode>, { preserve_ids }?: {
|
|
236
|
+
preserve_ids?: boolean;
|
|
237
|
+
}): this;
|
|
234
238
|
/**
|
|
235
239
|
* Inserts text at the current text selection position.
|
|
236
240
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Transaction.svelte.d.ts","sourceRoot":"","sources":["../src/lib/Transaction.svelte.ts"],"names":[],"mappings":"AA8BA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AACzD,OAAO,KAAK,EACX,MAAM,EACN,QAAQ,EACR,SAAS,EACT,QAAQ,EACR,cAAc,EACd,YAAY,EACZ,YAAY,EAEZ,IAAI,EACJ,UAAU,EACV,iBAAiB,EACjB,aAAa,EACb,UAAU,EACV,aAAa,EACb,MAAM,YAAY,CAAC;AAepB;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,OAAO,OAAO,WAAW;IAC/B,MAAM,EAAE,cAAc,CAAC;IACvB,GAAG,EAAE,QAAQ,CAAC;IAId,SAAS,EAAE,SAAS,CAAC;IACrB,MAAM,EAAE,aAAa,CAAC;IACtB,GAAG,EAAE,iBAAiB,EAAE,CAAC;IACzB,WAAW,EAAE,iBAAiB,EAAE,CAAC;IACjC,gBAAgB,EAAE,SAAS,GAAG,IAAI,CAAC;IACnC,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,kBAAkB,EAAE,OAAO,CAAC;IAE5B;;;;;;;OAOG;gBAEF,MAAM,EAAE,cAAc,EACtB,GAAG,EAAE,QAAQ,EACb,SAAS,EAAE,SAAS,GAAG,IAAI,EAC3B,MAAM,EAAE,aAAa;IAyBtB;;;;;OAKG;IAEH,GAAG,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM,GAAG,GAAG;IAIrC;;OAEG;IACH,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM;IAIrD;;OAEG;IACH,IAAI,CAAC,IAAI,EAAE,YAAY,GAAG,QAAQ;IAIlC;;OAEG;IACH,OAAO,CAAC,IAAI,EAAE,YAAY,GAAG,UAAU;IAIvC;;OAEG;IACH,WAAW,IAAI,MAAM;IAUrB;;;;OAIG;IACH,aAAa,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI;IAIvC;;OAEG;IACH,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE;IAI/C;;OAEG;IACH,IAAI,oBAAoB,IAAI,MAAM,EAAE,CAInC;IAED;;OAEG;IACH,IAAI,0BAA0B,IAAI,MAAM,EAAE,CAIzC;IAED;;OAEG;IACH,IAAI,cAAc,IAAI,kBAAkB,EAAE,CAEzC;IAED,IAAI,WAAW,IAAI,kBAAkB,GAAG,IAAI,CAE3C;IAED;;OAEG;IACH,IAAI,oBAAoB,IAAI,kBAAkB,EAAE,CAE/C;IAED,IAAI,iBAAiB,IAAI,kBAAkB,GAAG,IAAI,CAEjD;IAED;;OAEG;IACH,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,cAAc;IAItB;;;;;;;;;;;;;;;OAeG;IAEH,GAAG,CAAC,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,GAAG,GAAG,IAAI;IA2DzC,KAAK,
|
|
1
|
+
{"version":3,"file":"Transaction.svelte.d.ts","sourceRoot":"","sources":["../src/lib/Transaction.svelte.ts"],"names":[],"mappings":"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"}
|
|
@@ -233,14 +233,17 @@ export default class Transaction {
|
|
|
233
233
|
return this;
|
|
234
234
|
}
|
|
235
235
|
// Takes a subgraph and constructs new nodes from it
|
|
236
|
-
//
|
|
236
|
+
// IDs are regenerated by default; paste can preserve a collision-free subgraph.
|
|
237
237
|
// NOTE: Omitted properties will be populated with default values.
|
|
238
|
-
build(node_id, nodes) {
|
|
238
|
+
build(node_id, nodes, { preserve_ids = false } = {}) {
|
|
239
239
|
const depth_first_nodes = traverse(node_id, this.schema, nodes);
|
|
240
|
+
const keep_ids = preserve_ids && depth_first_nodes.every((node) => !this.doc.nodes[node.id]);
|
|
240
241
|
// This maps original ids to newly generated ids
|
|
241
242
|
const id_map = {};
|
|
243
|
+
// eslint-disable-next-line svelte/prefer-svelte-reactivity -- Transaction-local ownership lookup.
|
|
244
|
+
const used_attachment_ids = new Set();
|
|
242
245
|
for (const node of depth_first_nodes) {
|
|
243
|
-
const new_id = this.generate_id();
|
|
246
|
+
const new_id = keep_ids ? node.id : this.generate_id();
|
|
244
247
|
id_map[node.id] = new_id;
|
|
245
248
|
let new_node = { ...node, id: new_id };
|
|
246
249
|
const node_schema = this.schema[node.type];
|
|
@@ -249,7 +252,12 @@ export default class Transaction {
|
|
|
249
252
|
const prop_type = property_definition.type;
|
|
250
253
|
const value = new_node[property_name];
|
|
251
254
|
const remap_ranges = (ranges) => (ranges ?? []).map(({ start_offset, end_offset, node_id }) => {
|
|
252
|
-
|
|
255
|
+
// Clipboard subtrees may contain legacy shared attachments; detach each occurrence.
|
|
256
|
+
const next_id = used_attachment_ids.has(node_id)
|
|
257
|
+
? this.build(node_id, nodes)
|
|
258
|
+
: id_map[node_id] || node_id;
|
|
259
|
+
used_attachment_ids.add(node_id);
|
|
260
|
+
return { start_offset, end_offset, node_id: next_id };
|
|
253
261
|
});
|
|
254
262
|
if (prop_type === 'node_array' && value && typeof value === 'object') {
|
|
255
263
|
new_node[property_name] = {
|
|
@@ -638,7 +646,7 @@ export default class Transaction {
|
|
|
638
646
|
* @param nodes - Node graph for restored marks and annotations
|
|
639
647
|
* @returns This transaction instance for method chaining
|
|
640
648
|
*/
|
|
641
|
-
insert_nodes(node_ids, marks = [], annotations = [], nodes = {}) {
|
|
649
|
+
insert_nodes(node_ids, marks = [], annotations = [], nodes = {}, { preserve_ids = false } = {}) {
|
|
642
650
|
if (this.selection.type !== 'node')
|
|
643
651
|
return this;
|
|
644
652
|
// Unless caret is collapsed, delete the selected nodes as a first step
|
|
@@ -667,7 +675,7 @@ export default class Transaction {
|
|
|
667
675
|
const mark_node = nodes[mark.node_id];
|
|
668
676
|
if (!property_definition.mark_types?.includes(mark_node?.type))
|
|
669
677
|
return null;
|
|
670
|
-
const new_mark_node_id = this.build(mark.node_id, nodes);
|
|
678
|
+
const new_mark_node_id = this.build(mark.node_id, nodes, { preserve_ids });
|
|
671
679
|
return {
|
|
672
680
|
start_offset: start + mark.start_offset,
|
|
673
681
|
end_offset: start + mark.end_offset,
|
|
@@ -693,7 +701,7 @@ export default class Transaction {
|
|
|
693
701
|
const annotation_node = nodes[annotation.node_id];
|
|
694
702
|
if (!property_definition.annotation_types?.includes(annotation_node?.type))
|
|
695
703
|
return null;
|
|
696
|
-
const new_annotation_node_id = this.build(annotation.node_id, nodes);
|
|
704
|
+
const new_annotation_node_id = this.build(annotation.node_id, nodes, { preserve_ids });
|
|
697
705
|
return {
|
|
698
706
|
start_offset: start + annotation.start_offset,
|
|
699
707
|
end_offset: start + annotation.end_offset,
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { DocumentNode, SessionConfig, Text } from './types.js';
|
|
2
|
+
/** Receives already escaped inline HTML. Exporters must escape attribute values. */
|
|
3
|
+
export type MarkHtmlExporter = (node: DocumentNode, content: string) => string;
|
|
4
|
+
/** Export exclusive marks as inline HTML; data-only annotations remain in the native payload. */
|
|
5
|
+
export declare function export_text_html(text: Text, session: {
|
|
6
|
+
doc: {
|
|
7
|
+
nodes: Record<string, DocumentNode>;
|
|
8
|
+
};
|
|
9
|
+
config: SessionConfig;
|
|
10
|
+
}, nodes?: Record<string, DocumentNode>): string;
|
|
11
|
+
//# sourceMappingURL=html_export.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"html_export.d.ts","sourceRoot":"","sources":["../src/lib/html_export.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAGpE,oFAAoF;AACpF,MAAM,MAAM,gBAAgB,GAAG,CAAC,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;AAE/E,iGAAiG;AACjG,wBAAgB,gBAAgB,CAC/B,IAAI,EAAE,IAAI,EACV,OAAO,EAAE;IAAE,GAAG,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAA;KAAE,CAAC;IAAC,MAAM,EAAE,aAAa,CAAA;CAAE,EAChF,KAAK,+BAAoB,GACvB,MAAM,CAyBR"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { escape_html } from './html_utils.js';
|
|
2
|
+
/** Export exclusive marks as inline HTML; data-only annotations remain in the native payload. */
|
|
3
|
+
export function export_text_html(text, session, nodes = session.doc.nodes) {
|
|
4
|
+
const characters = [
|
|
5
|
+
...new Intl.Segmenter('en', { granularity: 'grapheme' }).segment(text.content)
|
|
6
|
+
].map((segment) => segment.segment);
|
|
7
|
+
const slice = (start, end) => escape_html(characters.slice(start, end).join('')).replace(/\r\n|\r|\n/g, '<br>');
|
|
8
|
+
let html = '';
|
|
9
|
+
let offset = 0;
|
|
10
|
+
for (const mark of [...text.marks].sort((a, b) => a.start_offset - b.start_offset)) {
|
|
11
|
+
if (!Number.isInteger(mark.start_offset) ||
|
|
12
|
+
!Number.isInteger(mark.end_offset) ||
|
|
13
|
+
mark.start_offset < offset ||
|
|
14
|
+
mark.end_offset > characters.length ||
|
|
15
|
+
mark.end_offset <= mark.start_offset)
|
|
16
|
+
continue;
|
|
17
|
+
html += slice(offset, mark.start_offset);
|
|
18
|
+
const content = slice(mark.start_offset, mark.end_offset);
|
|
19
|
+
const node = nodes[mark.node_id];
|
|
20
|
+
const exporter = node && session.config.mark_html_exporters?.[node.type];
|
|
21
|
+
html += exporter ? exporter(node, content) : content;
|
|
22
|
+
offset = mark.end_offset;
|
|
23
|
+
}
|
|
24
|
+
return html + slice(offset, characters.length);
|
|
25
|
+
}
|