svedit 0.11.1 → 0.13.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.
Files changed (93) hide show
  1. package/README.md +260 -82
  2. package/dist/Command.svelte.d.ts +54 -29
  3. package/dist/Command.svelte.d.ts.map +1 -1
  4. package/dist/Command.svelte.js +286 -283
  5. package/dist/CustomProperty.svelte +3 -5
  6. package/dist/CustomProperty.svelte.d.ts +3 -6
  7. package/dist/CustomProperty.svelte.d.ts.map +1 -1
  8. package/dist/KeyMapper.svelte.d.ts +19 -6
  9. package/dist/KeyMapper.svelte.d.ts.map +1 -1
  10. package/dist/KeyMapper.svelte.js +95 -112
  11. package/dist/Node.svelte +40 -9
  12. package/dist/Node.svelte.d.ts +3 -5
  13. package/dist/Node.svelte.d.ts.map +1 -1
  14. package/dist/NodeArrayProperty.svelte +142 -18
  15. package/dist/NodeArrayProperty.svelte.d.ts +3 -5
  16. package/dist/NodeArrayProperty.svelte.d.ts.map +1 -1
  17. package/dist/NodeCaret.svelte +1 -1
  18. package/dist/NodeCaret.svelte.d.ts +6 -14
  19. package/dist/NodeCaret.svelte.d.ts.map +1 -1
  20. package/dist/NodeGap.svelte +60 -12
  21. package/dist/NodeGap.svelte.d.ts +7 -14
  22. package/dist/NodeGap.svelte.d.ts.map +1 -1
  23. package/dist/NodeGapMarkers.svelte +19 -10
  24. package/dist/NodeGapMarkers.svelte.d.ts +6 -11
  25. package/dist/NodeGapMarkers.svelte.d.ts.map +1 -1
  26. package/dist/NodeSelectionMarkers.svelte +20 -6
  27. package/dist/NodeSelectionMarkers.svelte.d.ts +2 -5
  28. package/dist/NodeSelectionMarkers.svelte.d.ts.map +1 -1
  29. package/dist/Session.svelte.d.ts +85 -116
  30. package/dist/Session.svelte.d.ts.map +1 -1
  31. package/dist/Session.svelte.js +543 -571
  32. package/dist/Svedit.svelte +243 -216
  33. package/dist/Svedit.svelte.d.ts +30 -10
  34. package/dist/Svedit.svelte.d.ts.map +1 -1
  35. package/dist/{AnnotatedTextProperty.svelte → TextProperty.svelte} +71 -56
  36. package/dist/TextProperty.svelte.d.ts +5 -0
  37. package/dist/TextProperty.svelte.d.ts.map +1 -0
  38. package/dist/Transaction.svelte.d.ts +101 -126
  39. package/dist/Transaction.svelte.d.ts.map +1 -1
  40. package/dist/Transaction.svelte.js +843 -827
  41. package/dist/UnknownNode.svelte +4 -3
  42. package/dist/UnknownNode.svelte.d.ts +5 -9
  43. package/dist/UnknownNode.svelte.d.ts.map +1 -1
  44. package/dist/doc_utils.d.ts +109 -122
  45. package/dist/doc_utils.d.ts.map +1 -1
  46. package/dist/doc_utils.js +726 -698
  47. package/dist/index.d.ts +20 -17
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +2 -18
  50. package/dist/node_visibility.svelte.d.ts +132 -5
  51. package/dist/node_visibility.svelte.d.ts.map +1 -1
  52. package/dist/node_visibility.svelte.js +535 -642
  53. package/dist/paste_utils.d.ts +11 -43
  54. package/dist/paste_utils.d.ts.map +1 -1
  55. package/dist/paste_utils.js +111 -149
  56. package/dist/transforms.svelte.d.ts +9 -7
  57. package/dist/transforms.svelte.d.ts.map +1 -1
  58. package/dist/transforms.svelte.js +126 -152
  59. package/dist/types.d.ts +329 -291
  60. package/dist/types.d.ts.map +1 -0
  61. package/dist/types.js +1 -0
  62. package/dist/utils.d.ts +79 -95
  63. package/dist/utils.d.ts.map +1 -1
  64. package/dist/utils.js +392 -305
  65. package/package.json +69 -67
  66. package/src/lib/{Command.svelte.js → Command.svelte.ts} +111 -41
  67. package/src/lib/CustomProperty.svelte +3 -5
  68. package/src/lib/{KeyMapper.svelte.js → KeyMapper.svelte.ts} +28 -11
  69. package/src/lib/Node.svelte +40 -9
  70. package/src/lib/NodeArrayProperty.svelte +142 -18
  71. package/src/lib/NodeCaret.svelte +1 -1
  72. package/src/lib/NodeGap.svelte +60 -12
  73. package/src/lib/NodeGapMarkers.svelte +19 -10
  74. package/src/lib/NodeSelectionMarkers.svelte +20 -6
  75. package/src/lib/Session.svelte.ts +656 -0
  76. package/src/lib/Svedit.svelte +243 -216
  77. package/src/lib/{AnnotatedTextProperty.svelte → TextProperty.svelte} +71 -56
  78. package/src/lib/Transaction.svelte.ts +1020 -0
  79. package/src/lib/UnknownNode.svelte +4 -3
  80. package/src/lib/doc_utils.ts +1120 -0
  81. package/src/lib/{index.js → index.ts} +48 -1
  82. package/src/lib/node_visibility.svelte.ts +658 -0
  83. package/src/lib/{paste_utils.js → paste_utils.ts} +51 -59
  84. package/src/lib/{transforms.svelte.js → transforms.svelte.ts} +44 -27
  85. package/src/lib/{types.d.ts → types.ts} +250 -103
  86. package/src/lib/utils.ts +660 -0
  87. package/dist/AnnotatedTextProperty.svelte.d.ts +0 -7
  88. package/dist/AnnotatedTextProperty.svelte.d.ts.map +0 -1
  89. package/src/lib/Session.svelte.js +0 -575
  90. package/src/lib/Transaction.svelte.js +0 -846
  91. package/src/lib/doc_utils.js +0 -807
  92. package/src/lib/node_visibility.svelte.js +0 -718
  93. package/src/lib/utils.js +0 -465
@@ -1,19 +1,32 @@
1
- <script>
1
+ <script lang="ts">
2
2
  import { getContext } from 'svelte';
3
3
  import {
4
4
  char_slice,
5
5
  get_char_length,
6
6
  paths_equal,
7
7
  serialize_path,
8
- get_selection_range
8
+ get_selection_range,
9
+ calculate_fragment_ranges
9
10
  } from './utils.js';
10
11
 
11
- /** @import { AnnotatedTextPropertyProps, Annotation, Fragment, SelectionRange } from './types.d.ts'; */
12
-
13
- const svedit = getContext('svedit');
14
-
15
- /** @type {AnnotatedTextPropertyProps} */
16
- let { path, class: css_class, placeholder = '', tag = 'div', style = '', ...rest } = $props();
12
+ import type {
13
+ TextPropertyProps,
14
+ Mark,
15
+ Fragment,
16
+ SelectionRange,
17
+ SveditContext
18
+ } from './types.js';
19
+
20
+ const svedit = getContext<SveditContext>('svedit');
21
+
22
+ let {
23
+ path,
24
+ class: css_class,
25
+ placeholder = '',
26
+ tag = 'div',
27
+ style = '',
28
+ ...rest
29
+ }: TextPropertyProps = $props();
17
30
 
18
31
  let path_str = $derived(serialize_path(path));
19
32
 
@@ -23,16 +36,20 @@
23
36
  );
24
37
  });
25
38
 
26
- let plain_text = $derived(svedit.session.get(path).text);
27
- let is_empty = $derived(
28
- get_char_length(plain_text) === 0 && !(svedit.is_composing && is_focused)
29
- );
39
+ let plain_text = $derived(svedit.session.get(path).content);
40
+ // A string has zero grapheme clusters iff it has zero code units, so a
41
+ // plain length check avoids a full Intl.Segmenter pass (this derived
42
+ // re-runs for every text property on every document change).
43
+ let is_empty = $derived(plain_text.length === 0 && !(svedit.is_composing && is_focused));
30
44
 
31
- let is_collapsed = $derived(
32
- is_focused && svedit.session.selection?.anchor_offset == svedit.session.selection?.focus_offset
33
- );
45
+ let is_collapsed = $derived.by(() => {
46
+ const selection = svedit.session.selection;
47
+ return (
48
+ is_focused && selection?.type === 'text' && selection.anchor_offset === selection.focus_offset
49
+ );
50
+ });
34
51
 
35
- // Get selection highlight range if not inside an annotation
52
+ // Get selection highlight range if it does not touch marks.
36
53
  // Only render selection highlight when canvas is NOT focused.
37
54
  // This avoids DOM mutations (splitting text nodes for highlight spans)
38
55
  // while the user is actively selecting, which would cause selection
@@ -41,7 +58,7 @@
41
58
  if (svedit.canvas_focused) return null;
42
59
  if (is_collapsed) return null;
43
60
  if (!is_focused) return null;
44
- if (svedit.session.active_annotation()) return null;
61
+ if (svedit.session.selected_marks.length > 0) return null;
45
62
  const sel = svedit.session.selection;
46
63
  if (!sel || sel.type !== 'text') return null;
47
64
  return get_selection_range(sel);
@@ -49,61 +66,57 @@
49
66
 
50
67
  let fragments = $derived(
51
68
  get_fragments(
52
- svedit.session.get(path).text,
53
- svedit.session.get(path).annotations,
69
+ svedit.session.get(path).content,
70
+ svedit.session.get(path).marks,
54
71
  selection_highlight_range
55
72
  )
56
73
  );
57
74
 
58
75
  /**
59
- * Converts text with annotations into renderable fragments for display.
60
- * @param {string} text - The plain text content
61
- * @param {Array<Annotation>} annotations - Array of annotations
62
- * @param {SelectionRange} [selection_highlight_range] - Optional selection highlight range
63
- * @returns {Array<Fragment>} Array of fragments
76
+ * Converts text with marks into renderable fragments for display.
77
+ * Annotations never fragment the text; they are data-only.
64
78
  */
65
- function get_fragments(text, annotations, selection_highlight_range) {
66
- /** @type {Array<Fragment>} */
67
- let fragments = [];
68
- let last_index = 0;
79
+ function get_fragments(
80
+ text: string,
81
+ marks: Array<Mark>,
82
+ selection_highlight_range?: SelectionRange | null
83
+ ): Array<Fragment> {
84
+ // Fast path: no marks and no selection highlight means the whole text
85
+ // is a single content fragment. This runs for EVERY text property on
86
+ // EVERY document change, so skipping the two full Intl.Segmenter passes
87
+ // (get_char_length + char_slice over the whole text) matters at scale.
88
+ if (marks.length === 0 && !selection_highlight_range) {
89
+ return text.length > 0 ? [text] : [];
90
+ }
69
91
 
70
- // Merge annotations with selection highlight and sort by start offset
71
- const ranges = [
72
- ...annotations,
73
- ...(selection_highlight_range ? [selection_highlight_range] : [])
74
- ].sort((a, b) => a.start_offset - b.start_offset);
92
+ const ranges = calculate_fragment_ranges(
93
+ get_char_length(text),
94
+ marks,
95
+ selection_highlight_range
96
+ );
97
+ const fragments: Array<Fragment> = [];
75
98
 
76
99
  for (const range of ranges) {
77
- // Add text before this range
78
- if (range.start_offset > last_index) {
79
- fragments.push(char_slice(text, last_index, range.start_offset));
80
- }
81
-
82
100
  const content = char_slice(text, range.start_offset, range.end_offset);
83
101
 
84
- if ('node_id' in range) {
102
+ if (range.type === 'mark') {
85
103
  const node = svedit.session.get(range.node_id);
86
- if (!node) throw new Error(`Node not found for annotation ${range.node_id}`);
104
+ if (!node) throw new Error(`Node not found for mark ${range.node_id}`);
87
105
 
88
106
  fragments.push({
89
- type: 'annotation',
107
+ type: 'mark',
90
108
  node,
91
109
  content,
92
- annotation_index: annotations.indexOf(/** @type {Annotation} */ (range))
110
+ mark_index: range.mark_index!
93
111
  });
94
- } else {
112
+ } else if (range.type === 'selection_highlight') {
95
113
  fragments.push({
96
114
  type: 'selection_highlight',
97
115
  content
98
116
  });
117
+ } else {
118
+ fragments.push(content);
99
119
  }
100
-
101
- last_index = range.end_offset;
102
- }
103
-
104
- // Add any remaining text
105
- if (last_index < get_char_length(text)) {
106
- fragments.push(char_slice(text, last_index));
107
120
  }
108
121
 
109
122
  return fragments;
@@ -130,12 +143,14 @@
130
143
  {#if typeof fragment === 'string'}{fragment}{:else if fragment.type === 'selection_highlight'}<span
131
144
  class="selection-highlight"
132
145
  style="anchor-name: --selection-highlight;">{fragment.content}</span
133
- >{:else if fragment.type === 'annotation'}
134
- {@const AnnotationComponent = svedit.session.config.node_components[fragment.node.type]}
135
- <AnnotationComponent
136
- path={[...path, 'annotations', fragment.annotation_index, 'node_id']}
137
- content={fragment.content}
138
- />
146
+ >{:else if fragment.type === 'mark'}
147
+ {@const MarkComponent = svedit.session.config.node_components[fragment.node.type]}
148
+ {#if MarkComponent}
149
+ <MarkComponent
150
+ path={[...path, 'marks', fragment.mark_index, 'node_id']}
151
+ content={fragment.content}
152
+ />
153
+ {:else}<span class="mark-{fragment.node.type}">{fragment.content}</span>{/if}
139
154
  {/if}
140
155
  {/each}<!--
141
156
  -->{#if !is_focused || !is_empty}<br />{/if}