svedit 0.14.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 CHANGED
@@ -436,6 +436,138 @@ Defaults make it safe to add new defaultable properties, but they are not a repl
436
436
 
437
437
  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
438
 
439
+ ### Importing pasted HTML
440
+
441
+ Set `config.html_paste` to enable external HTML import. Without it, paste keeps its
442
+ existing plain-text behavior. Native Svedit clipboard data and media paste keep
443
+ their existing paths and bypass this importer.
444
+
445
+ ```ts
446
+ import type { HtmlPasteConfig } from 'svedit';
447
+
448
+ const html_paste: HtmlPasteConfig = {
449
+ blocks: {
450
+ p: { type: 'paragraph', text_property: 'content' },
451
+ h1: { type: 'heading1', text_property: 'content' },
452
+ h2: { type: 'heading2', text_property: 'content' }
453
+ },
454
+ marks: {
455
+ bold: { type: 'strong' },
456
+ link: ({ href }) => ({ type: 'link', properties: { href } })
457
+ }
458
+ };
459
+
460
+ const config = { /* other configuration */ html_paste };
461
+ ```
462
+
463
+ Use your app's schema names (the demo uses `heading_1`, for example). Blocks accept
464
+ `p` and `h1` through `h6`. Each mapping names a text node type and its text property.
465
+ A block mapping can also be a synchronous factory returning the same descriptor,
466
+ with extra node properties, or `null` to use the destination's default text node:
467
+
468
+ ```ts
469
+ h1: ({ tag }) => ({
470
+ type: 'heading',
471
+ text_property: 'content',
472
+ properties: { level: Number(tag.slice(1)) }
473
+ });
474
+ ```
475
+
476
+ `<strong>` and `<b>` use the `bold` mapping. Link factories receive the decoded,
477
+ trimmed `href`; they may return `null` to discard link formatting. The importer
478
+ accepts HTTP(S), mailto, tel, and relative links, and drops unsafe schemes.
479
+ Mappings describe data; they should not mutate the session. Svedit creates IDs
480
+ and fills schema defaults through its normal insertion transaction.
481
+
482
+ Map HTML lists separately because they contain an array of text nodes:
483
+
484
+ ```ts
485
+ html_paste.lists = {
486
+ ul: {
487
+ type: 'list',
488
+ children_property: 'list_items',
489
+ item: { type: 'list_item', text_property: 'content' },
490
+ properties: { layout: 'square' }
491
+ },
492
+ ol: {
493
+ type: 'list',
494
+ children_property: 'list_items',
495
+ item: { type: 'list_item', text_property: 'content' },
496
+ properties: { layout: 'decimal' }
497
+ }
498
+ };
499
+ ```
500
+
501
+ All type and property names come from your schema. Nested HTML lists flatten into
502
+ one list in item order, using the outer list's mapping. An item with multiple
503
+ paragraphs becomes one text node. Unmapped or disallowed lists fall back to the
504
+ destination's default text nodes; text fields receive flattened list text.
505
+
506
+ If a destination requires a containing block, configure an optional wrapper:
507
+
508
+ ```ts
509
+ html_paste.wrapper = { type: 'prose', children_property: 'body' };
510
+ ```
511
+
512
+ The wrapper is used only when blocks cannot be mapped directly to the destination
513
+ and the destination allows its type. Its `children_property` must be a node array;
514
+ blocks are mapped against that property's allowed node types. Wrapper mappings
515
+ also accept `properties` for schema-specific defaults.
516
+
517
+ Import respects the destination's allowed node and mark types. Unmapped or
518
+ disallowed headings fall back to its default text node; unsupported marks keep
519
+ their text. Marks are exclusive in Svedit, so a supported link takes precedence
520
+ over bold where they overlap. Existing active-mark insertion behavior still applies.
521
+
522
+ At a node caret, HTML blocks become nodes. A single text block pasted into text
523
+ retains the destination node type. A configured list follows the node insertion
524
+ path, even when it contains only one item. Multiple blocks pasted into a text node
525
+ follow the existing multi-paragraph behavior: insert nodes after that node without splitting
526
+ or replacing its text. In block fields such as titles and captions, blocks are
527
+ joined with blank lines, or spaces when `allow_newlines` is false.
528
+
529
+ The importer recognizes semantic tags, common block wrappers, entities and
530
+ `<br>`. Unsupported containers retain their text but do not import table
531
+ structure, images, or CSS formatting. Lists retain structure when a list mapping
532
+ is configured. Script/style content is ignored. Empty or
533
+ unusable HTML, and mapping errors, fall back to the existing plain-text path.
534
+
535
+ ### Exporting copied rich text
536
+
537
+ Copy and cut include both the embedded `data-svedit` payload and readable HTML.
538
+ Configure `mark_html_exporters` to map your schema's marks to HTML. Unknown marks
539
+ keep their text; data-only annotations remain in the native payload.
540
+
541
+ ```ts
542
+ import { export_text_html, escape_html, safe_html_href } from 'svedit';
543
+ import type { MarkHtmlExporter } from 'svedit';
544
+
545
+ const mark_html_exporters = {
546
+ strong: (_node, content) => `<strong>${content}</strong>`,
547
+ link: (node, content) => {
548
+ const href = safe_html_href(node.href || '');
549
+ return href ? `<a href="${escape_html(href)}">${content}</a>` : content;
550
+ }
551
+ } satisfies Record<string, MarkHtmlExporter>;
552
+
553
+ const config = {
554
+ mark_html_exporters,
555
+ html_exporters: {
556
+ paragraph: (node, session) => `<p>${export_text_html(node.content, session)}</p>`,
557
+ heading1: (node, session) => `<h1>${export_text_html(node.content, session)}</h1>`
558
+ }
559
+ };
560
+ ```
561
+
562
+ Text selections and the default node exporter use this automatically. Existing
563
+ custom `html_exporters` should call `export_text_html` for their text properties,
564
+ as above. The helper escapes text, preserves line breaks with `<br>`, and uses
565
+ Svedit's grapheme offsets. Mark exporters receive already escaped inline HTML;
566
+ only attribute values need escaping. Import and export mappings are separate
567
+ because import factories cannot be reliably reversed.
568
+
569
+ ### Custom handlers
570
+
439
571
  Two optional hooks are especially useful when integrating custom media workflows:
440
572
 
441
573
  - `handle_media_paste(session, pasted_media)`
@@ -1453,12 +1585,33 @@ The source code is compact and readable — less than 3000 LOC across a handful
1453
1585
 
1454
1586
  ## Developing Svedit
1455
1587
 
1588
+ Development requires Node.js 22.17 or newer.
1589
+
1456
1590
  Once you've cloned the Svedit repository and installed dependencies with `pnpm install`, start a development server:
1457
1591
 
1458
1592
  ```bash
1459
1593
  pnpm dev
1460
1594
  ```
1461
1595
 
1596
+ ### Running tests
1597
+
1598
+ Run the browser tests in Vitest's interactive watch mode:
1599
+
1600
+ ```bash
1601
+ pnpm test:unit
1602
+ ```
1603
+
1604
+ For a single run, use `pnpm test`.
1605
+
1606
+ The tests exercise real browser focus, selection, and keyboard behavior. Keep the
1607
+ browser runner tab and window focused and let each run finish uninterrupted.
1608
+ Do not switch tabs or applications, open DevTools, or click inside the runner
1609
+ while tests are running. Losing focus can cause selection and keyboard tests to
1610
+ fail even when the editor works correctly.
1611
+
1612
+ If a run was interrupted, return focus to the runner and rerun the tests. Failures
1613
+ that persist during a focused, uninterrupted run should be investigated.
1614
+
1462
1615
  ## Building
1463
1616
 
1464
1617
  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 class="selected-node-overlay" style="position-anchor: --{serialize_path(path)};"></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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"}
@@ -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
- // There's no parent path to insert into
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
- const node_offset = selection.path.at(-2) + 1;
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')
@@ -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.type === 'text') {
401
+ if (property_definition?.type === 'text') {
398
402
  const text_content = prop_value.content;
399
403
  if (text_content.trim()) {
400
- html += `<p>${text_content}</p>`;
404
+ html += `<p>${export_text_html(prop_value, session)}</p>`;
401
405
  }
402
- } else if (property_definition.type === 'node_array') {
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.content}</span>`;
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
- html = create_svedit_html_format(json_data, `<span>${value}</span>`);
521
- plain_text = String(value);
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
- let tr = session.tr;
615
- if (selection) {
616
- tr.set_selection(selection);
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
- const new_node_id = tr.build('the_node', {
646
- the_node: {
647
- id: 'the_node',
648
- type: default_text_node_type,
649
- [target_text_property_name]: text_content || {
650
- content: '',
651
- marks: [],
652
- annotations: []
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 plain_text,
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
- const html_content = event.clipboardData?.getData('text/html');
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":"AAyBA,OAAO,4BAA4B,CAAC;AACpC,OAAO,gCAAgC,CAAC;AAExC,OAAO,KAAK,EACV,cAAc,EACd,WAAW,EASX,MAAM,YAAY,CAAC;AAGrB,iBAAS,QAAQ,CAAC,CAAC,SAAS,cAAc;WA8lDb,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"}
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>): NodeId;
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>): this;
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,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,GAAG,MAAM;IA6CnE;;;;;;;;;;;;;;;;;;OAkBG;IACH,MAAM,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI;IAkBhC;;;;;;;;;;;;;;;;;;;OAmBG;IACH,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAkBxB;;;;;;OAMG;IACH,aAAa,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI;IAMzC;;;;OAIG;IACH,OAAO,CAAC,mBAAmB;IAI3B;;;;;;;;;;;;;;;;;;OAkBG;IACH,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,eAAe,CAAC,EAAE,aAAa,GAAG,IAAI;IAyErE;;;;;;;;;;;;;;;;;;OAkBG;IACH,iBAAiB,CAAC,eAAe,EAAE,MAAM,EAAE,qBAAqB,CAAC,EAAE,aAAa,GAAG,IAAI;IAiDvF;;;;;;;;;;;OAWG;IACH,gBAAgB,CAAC,SAAS,GAAE,UAAU,GAAG,SAAsB,GAAG,IAAI;IAqHtE;;;;;;;;;;;OAWG;IACH,YAAY,CACX,QAAQ,EAAE,MAAM,EAAE,EAClB,KAAK,GAAE,KAAK,CAAC,IAAI,CAAM,EACvB,WAAW,GAAE,KAAK,CAAC,UAAU,CAAM,EACnC,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAM,GACtC,IAAI;IA+EP;;;;;;;;;;;;;OAaG;IACH,WAAW,CACV,aAAa,EAAE,MAAM,EACrB,KAAK,GAAE,KAAK,CAAC,IAAI,CAAM,EACvB,WAAW,GAAE,KAAK,CAAC,UAAU,CAAM,EACnC,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAM,GACtC,IAAI;IAqGP;;;;;;;;OAQG;IACH,OAAO,CAAC,kCAAkC;CA8C1C"}
1
+ {"version":3,"file":"Transaction.svelte.d.ts","sourceRoot":"","sources":["../src/lib/Transaction.svelte.ts"],"names":[],"mappings":"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"}
@@ -233,14 +233,15 @@ export default class Transaction {
233
233
  return this;
234
234
  }
235
235
  // Takes a subgraph and constructs new nodes from it
236
- // NOTE: all ids will be mapped to new unique ids.
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 = {};
242
243
  for (const node of depth_first_nodes) {
243
- const new_id = this.generate_id();
244
+ const new_id = keep_ids ? node.id : this.generate_id();
244
245
  id_map[node.id] = new_id;
245
246
  let new_node = { ...node, id: new_id };
246
247
  const node_schema = this.schema[node.type];
@@ -638,7 +639,7 @@ export default class Transaction {
638
639
  * @param nodes - Node graph for restored marks and annotations
639
640
  * @returns This transaction instance for method chaining
640
641
  */
641
- insert_nodes(node_ids, marks = [], annotations = [], nodes = {}) {
642
+ insert_nodes(node_ids, marks = [], annotations = [], nodes = {}, { preserve_ids = false } = {}) {
642
643
  if (this.selection.type !== 'node')
643
644
  return this;
644
645
  // Unless caret is collapsed, delete the selected nodes as a first step
@@ -667,7 +668,7 @@ export default class Transaction {
667
668
  const mark_node = nodes[mark.node_id];
668
669
  if (!property_definition.mark_types?.includes(mark_node?.type))
669
670
  return null;
670
- const new_mark_node_id = this.build(mark.node_id, nodes);
671
+ const new_mark_node_id = this.build(mark.node_id, nodes, { preserve_ids });
671
672
  return {
672
673
  start_offset: start + mark.start_offset,
673
674
  end_offset: start + mark.end_offset,
@@ -693,7 +694,7 @@ export default class Transaction {
693
694
  const annotation_node = nodes[annotation.node_id];
694
695
  if (!property_definition.annotation_types?.includes(annotation_node?.type))
695
696
  return null;
696
- const new_annotation_node_id = this.build(annotation.node_id, nodes);
697
+ const new_annotation_node_id = this.build(annotation.node_id, nodes, { preserve_ids });
697
698
  return {
698
699
  start_offset: start + annotation.start_offset,
699
700
  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
+ }