svedit 0.10.0 → 0.11.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.
@@ -0,0 +1,167 @@
1
+ /**
2
+ * @import { DocumentSchema } from './types.d.ts'
3
+ */
4
+
5
+ export function normalize_line_endings(text) {
6
+ return text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
7
+ }
8
+
9
+ export function dedent_plain_text(plain_text) {
10
+ const lines = normalize_line_endings(plain_text).split('\n');
11
+ if (lines.length < 2) return plain_text;
12
+
13
+ const non_empty_lines = lines.filter((line) => line.trim().length > 0);
14
+ if (non_empty_lines.length === 0) return plain_text;
15
+
16
+ const indented_non_empty_lines = non_empty_lines.filter((line) => /^[\t ]+/.test(line));
17
+ const indented_line_ratio = indented_non_empty_lines.length / non_empty_lines.length;
18
+ if (indented_line_ratio < 0.8) return plain_text;
19
+
20
+ const leading_whitespace_lengths = indented_non_empty_lines
21
+ .map((line) => line.match(/^[\t ]+/)?.[0].length || 0)
22
+ .filter(Boolean);
23
+ if (leading_whitespace_lengths.length === 0) return plain_text;
24
+
25
+ const dedent_size = Math.min(...leading_whitespace_lengths);
26
+ if (dedent_size <= 0) return plain_text;
27
+
28
+ return lines
29
+ .map((line) => {
30
+ if (line.trim().length === 0) return line;
31
+ let removable_count = 0;
32
+ while (removable_count < dedent_size && removable_count < line.length) {
33
+ const char = line[removable_count];
34
+ if (char === ' ' || char === '\t') {
35
+ removable_count += 1;
36
+ } else {
37
+ break;
38
+ }
39
+ }
40
+ return line.slice(removable_count);
41
+ })
42
+ .join('\n');
43
+ }
44
+
45
+ export function split_plain_text_paragraphs(plain_text) {
46
+ return normalize_line_endings(plain_text)
47
+ .split(/\n{2,}/)
48
+ .map((fragment) => fragment.trim())
49
+ .filter(Boolean);
50
+ }
51
+
52
+ export function normalize_plain_text_for_single_line_property(plain_text) {
53
+ return normalize_line_endings(plain_text).replace(/\s*\n+\s*/g, ' ');
54
+ }
55
+
56
+ /**
57
+ * @param {string | null | undefined} node_type
58
+ * @param {DocumentSchema} schema
59
+ * @returns {string | null}
60
+ */
61
+ export function get_text_property_name(node_type, schema) {
62
+ if (!node_type) return null;
63
+ const node_schema = schema[node_type];
64
+ if (!node_schema || node_schema.kind !== 'text') return null;
65
+ if (node_schema.properties?.content?.type === 'annotated_text') return 'content';
66
+ return (
67
+ Object.entries(node_schema.properties).find(([, property_definition]) => {
68
+ return property_definition.type === 'annotated_text';
69
+ })?.[0] || null
70
+ );
71
+ }
72
+
73
+ /**
74
+ * @param {any} node
75
+ * @param {DocumentSchema} schema
76
+ * @returns {{ text: string, annotations: any[] } | null}
77
+ */
78
+ export function get_text_content(node, schema) {
79
+ if (!node || typeof node !== 'object') return null;
80
+
81
+ const text_property_name = get_text_property_name(node.type, schema);
82
+ if (
83
+ text_property_name &&
84
+ node[text_property_name] &&
85
+ typeof node[text_property_name].text === 'string' &&
86
+ Array.isArray(node[text_property_name].annotations)
87
+ ) {
88
+ return node[text_property_name];
89
+ }
90
+
91
+ if (node.content && typeof node.content.text === 'string' && Array.isArray(node.content.annotations)) {
92
+ return node.content;
93
+ }
94
+
95
+ return null;
96
+ }
97
+
98
+ /**
99
+ * @param {any} node
100
+ * @param {DocumentSchema} schema
101
+ * @returns {boolean}
102
+ */
103
+ export function is_text_like_node_payload(node, schema) {
104
+ if (!node || typeof node !== 'object') return false;
105
+ if (schema[node.type]?.kind === 'text') return true;
106
+ return !!get_text_content(node, schema);
107
+ }
108
+
109
+ /**
110
+ * @param {any} node_array_property_definition
111
+ * @param {DocumentSchema} schema
112
+ * @returns {string | null}
113
+ */
114
+ export function get_default_text_node(node_array_property_definition, schema) {
115
+ if (
116
+ node_array_property_definition?.type !== 'node_array' ||
117
+ !Array.isArray(node_array_property_definition.node_types)
118
+ ) {
119
+ return null;
120
+ }
121
+
122
+ const default_node_type = node_array_property_definition.default_node_type;
123
+ if (default_node_type && schema[default_node_type]?.kind === 'text') {
124
+ return default_node_type;
125
+ }
126
+
127
+ return (
128
+ node_array_property_definition.node_types.find((node_type) => schema[node_type]?.kind === 'text') ||
129
+ null
130
+ );
131
+ }
132
+
133
+ /**
134
+ * @param {string[]} paragraph_fragments
135
+ * @param {string | null | undefined} node_type
136
+ * @param {DocumentSchema} schema
137
+ * @returns {{ main_nodes: string[], nodes: Record<string, any> } | null}
138
+ */
139
+ export function create_plain_text_nodes_payload(paragraph_fragments, node_type, schema) {
140
+ if (!Array.isArray(paragraph_fragments) || paragraph_fragments.length === 0 || !node_type) {
141
+ return null;
142
+ }
143
+
144
+ const text_property_name = get_text_property_name(node_type, schema);
145
+ if (!text_property_name) return null;
146
+
147
+ const payload = {
148
+ main_nodes: [],
149
+ nodes: {}
150
+ };
151
+
152
+ for (let i = 0; i < paragraph_fragments.length; i++) {
153
+ const fragment = paragraph_fragments[i];
154
+ const node_id = 'fragment_' + i;
155
+ payload.nodes[node_id] = {
156
+ id: node_id,
157
+ type: node_type,
158
+ [text_property_name]: {
159
+ text: fragment,
160
+ annotations: []
161
+ }
162
+ };
163
+ payload.main_nodes.push(node_id);
164
+ }
165
+
166
+ return payload;
167
+ }
package/dist/utils.d.ts CHANGED
@@ -1,5 +1,18 @@
1
1
  /**
2
- * Detect if the current browser is on a mobile device
2
+ * Detect if the virtual keyboard is likely visible.
3
+ *
4
+ * This is a heuristic based on the visual viewport becoming smaller than the
5
+ * layout viewport.
6
+ *
7
+ * @returns {boolean} true if the virtual keyboard is likely active, false otherwise
8
+ */
9
+ export function is_virtual_keyboard_active(): boolean;
10
+ /**
11
+ * Detect if the current browser is likely on a mobile device.
12
+ *
13
+ * This uses the user agent only so touch-capable laptops are not treated as
14
+ * mobile browsers.
15
+ *
3
16
  * @returns {boolean} true if mobile browser, false otherwise
4
17
  */
5
18
  export function is_mobile_browser(): boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/lib/utils.js"],"names":[],"mappings":"AAMA;;;GAGG;AACH,qCAFa,OAAO,CAanB;AAGD;;;GAGG;AACH,6CAFa,OAAO,CAUnB;AAED;;;;;;;;;;;;;GAaG;AACH,qCARW,MAAM,GACJ,MAAM,CASlB;AAGD;;;;;;;;;;;;;;GAcG;AACH,iCATW,MAAM,SACN,MAAM,GACJ,MAAM,CAUlB;AAED;;;;;;;;;;;;;;GAcG;AACH,gCATW,MAAM,SACN,MAAM,QACN,MAAM,GACJ,MAAM,CAYlB;AAED;;;;;;;;;;;;;GAaG;AACH,0CARW,MAAM,gBACN,MAAM,GACJ,MAAM,CAmBlB;AAED;;;;;;;;;;;;;GAaG;AACH,0CARW,MAAM,eACN,MAAM,GACJ,MAAM,CAelB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,4DAZW,aAAa,eACb,MAAM,GACJ,CAAC,aAAa,EAAE,aAAa,CAAC,CA6C1C;AAED;;;;;;;;;;;;;GAaG;AACH,gDARW,aAAa,eACb,aAAa,GACX,aAAa,CA4CzB;AAED;;;;;;;;GAQG;AACH,qCANW,MAAM,GACJ,MAAM,CAUlB;AAMD;;;;;;;;;GASG;AACH,sDAHW,MAAM,GACJ,OAAO,CAQnB;AAED;;;;;;GAMG;AACH,oDAJW,MAAM,UACN,MAAM,GACJ,IAAI,CAQhB;AAED;;;;;;;;;;;;GAYG;AACH,qCANW,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,GACpB,MAAM,CAmBlB;AAED;;;;;;;;GAQG;AACH,kDANW,MAAM,GACJ,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,CAuBlC;AAED;;;;;;GAMG;AACH,+BAJW,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,KACtB,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,GACpB,OAAO,CAKnB;AAED,uEAgCC;AAED;;;;;GAKG;AACH,gDAHW,SAAS,GACP,cAAc,GAAG,IAAI,CAWjC;AAED,gEAMC;AApKD,6BAA8B,IAAI,CAAC;mCAtRsC,cAAc;+BAAd,cAAc;oCAAd,cAAc"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/lib/utils.js"],"names":[],"mappings":"AAMA;;;;;;;GAOG;AACH,8CAFa,OAAO,CAanB;AAED;;;;;;;GAOG;AACH,qCAFa,OAAO,CASnB;AAGD;;;GAGG;AACH,6CAFa,OAAO,CAUnB;AAED;;;;;;;;;;;;;GAaG;AACH,qCARW,MAAM,GACJ,MAAM,CASlB;AAGD;;;;;;;;;;;;;;GAcG;AACH,iCATW,MAAM,SACN,MAAM,GACJ,MAAM,CAUlB;AAED;;;;;;;;;;;;;;GAcG;AACH,gCATW,MAAM,SACN,MAAM,QACN,MAAM,GACJ,MAAM,CAYlB;AAED;;;;;;;;;;;;;GAaG;AACH,0CARW,MAAM,gBACN,MAAM,GACJ,MAAM,CAmBlB;AAED;;;;;;;;;;;;;GAaG;AACH,0CARW,MAAM,eACN,MAAM,GACJ,MAAM,CAelB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,4DAZW,aAAa,eACb,MAAM,GACJ,CAAC,aAAa,EAAE,aAAa,CAAC,CA6C1C;AAED;;;;;;;;;;;;;GAaG;AACH,gDARW,aAAa,eACb,aAAa,GACX,aAAa,CA4CzB;AAED;;;;;;;;GAQG;AACH,qCANW,MAAM,GACJ,MAAM,CAUlB;AAMD;;;;;;;;;GASG;AACH,sDAHW,MAAM,GACJ,OAAO,CAQnB;AAED;;;;;;GAMG;AACH,oDAJW,MAAM,UACN,MAAM,GACJ,IAAI,CAQhB;AAED;;;;;;;;;;;;GAYG;AACH,qCANW,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,GACpB,MAAM,CAmBlB;AAED;;;;;;;;GAQG;AACH,kDANW,MAAM,GACJ,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,CAuBlC;AAED;;;;;;GAMG;AACH,+BAJW,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,KACtB,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,GACpB,OAAO,CAKnB;AAED,uEAgCC;AAED;;;;;GAKG;AACH,gDAHW,SAAS,GACP,cAAc,GAAG,IAAI,CAWjC;AAED,gEAMC;AApKD,6BAA8B,IAAI,CAAC;mCA3SsC,cAAc;+BAAd,cAAc;oCAAd,cAAc"}
package/dist/utils.js CHANGED
@@ -5,20 +5,41 @@
5
5
  const SEGMENTER = new Intl.Segmenter('en', { granularity: 'grapheme' });
6
6
 
7
7
  /**
8
- * Detect if the current browser is on a mobile device
8
+ * Detect if the virtual keyboard is likely visible.
9
+ *
10
+ * This is a heuristic based on the visual viewport becoming smaller than the
11
+ * layout viewport.
12
+ *
13
+ * @returns {boolean} true if the virtual keyboard is likely active, false otherwise
14
+ */
15
+ export function is_virtual_keyboard_active() {
16
+ if (typeof window === 'undefined' || typeof document === 'undefined') {
17
+ return false;
18
+ }
19
+
20
+ const visual_viewport = window.visualViewport;
21
+ if (!visual_viewport) {
22
+ return false;
23
+ }
24
+
25
+ return visual_viewport.height < document.documentElement.clientHeight;
26
+ }
27
+
28
+ /**
29
+ * Detect if the current browser is likely on a mobile device.
30
+ *
31
+ * This uses the user agent only so touch-capable laptops are not treated as
32
+ * mobile browsers.
33
+ *
9
34
  * @returns {boolean} true if mobile browser, false otherwise
10
35
  */
11
36
  export function is_mobile_browser() {
12
- if (typeof window === 'undefined' || typeof navigator === 'undefined') {
37
+ if (typeof navigator === 'undefined') {
13
38
  return false;
14
39
  }
15
40
 
16
41
  const user_agent = navigator.userAgent;
17
- return (
18
- /iPhone|iPad|iPod|Android|Mobile/i.test(user_agent) ||
19
- 'ontouchstart' in window ||
20
- navigator.maxTouchPoints > 0
21
- );
42
+ return /iPhone|iPad|iPod|Android|Mobile/i.test(user_agent);
22
43
  }
23
44
 
24
45
  // ‼️‼️‼️‼️‼️‼️ UNUSED UTILITY BELOW ‼️‼️‼️‼️‼️‼️
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svedit",
3
- "version": "0.10.0",
3
+ "version": "0.11.1",
4
4
  "license": "MIT",
5
5
  "scripts": {
6
6
  "dev": "vite dev",
@@ -5,7 +5,6 @@
5
5
  get_char_length,
6
6
  paths_equal,
7
7
  serialize_path,
8
- snake_to_pascal,
9
8
  get_selection_range
10
9
  } from './utils.js';
11
10
 
@@ -109,7 +108,6 @@
109
108
 
110
109
  return fragments;
111
110
  }
112
-
113
111
  </script>
114
112
 
115
113
  <!-- ATTENTION: The comments are needed to prevent unwanted text nodes with whitespace. -->
@@ -133,8 +131,7 @@
133
131
  class="selection-highlight"
134
132
  style="anchor-name: --selection-highlight;">{fragment.content}</span
135
133
  >{:else if fragment.type === 'annotation'}
136
- {@const AnnotationComponent =
137
- svedit.session.config.node_components[snake_to_pascal(fragment.node.type)]}
134
+ {@const AnnotationComponent = svedit.session.config.node_components[fragment.node.type]}
138
135
  <AnnotationComponent
139
136
  path={[...path, 'annotations', fragment.annotation_index, 'node_id']}
140
137
  content={fragment.content}
@@ -167,7 +164,7 @@
167
164
  caret-color: transparent !important;
168
165
  }
169
166
  :global(.svedit.editable) [placeholder].empty.focused::before {
170
- content: "";
167
+ content: '';
171
168
  /* we limit width & height to avoid layout shifts in case the text has a lower natural height */
172
169
  width: 0px;
173
170
  height: 1cap;
@@ -1,6 +1,6 @@
1
1
  import { insert_default_node, break_text_node } from './transforms.svelte.js';
2
2
  import { can_switch_annotation_type } from './doc_utils.js';
3
- import { is_selection_collapsed, is_mobile_browser, get_char_length } from './utils.js';
3
+ import { is_selection_collapsed, get_char_length, char_slice } from './utils.js';
4
4
 
5
5
  /**
6
6
  * Base class for commands that can be executed in response to user actions
@@ -153,7 +153,6 @@ export class ToggleAnnotationCommand extends Command {
153
153
  /**
154
154
  * Command that adds a new line character at the current caret position.
155
155
  * Only works in text selections where newlines are allowed.
156
- * Disabled on mobile browsers where Shift+Enter has different behavior.
157
156
  */
158
157
  export class AddNewLineCommand extends Command {
159
158
  is_enabled() {
@@ -162,14 +161,32 @@ export class AddNewLineCommand extends Command {
162
161
 
163
162
  return (
164
163
  this.context.editable &&
165
- !is_mobile_browser() &&
166
164
  selection?.type === 'text' &&
167
165
  session.inspect(selection.path).allow_newlines
168
166
  );
169
167
  }
170
168
 
171
169
  execute() {
172
- this.context.session.apply(this.context.session.tr.insert_text('\n'));
170
+ const session = this.context.session;
171
+ const selection = session.selection;
172
+ if (!selection || selection.type !== 'text') return;
173
+ if (!session.inspect(selection.path).allow_newlines) return;
174
+
175
+ const tr = session.tr;
176
+ if (selection.anchor_offset !== selection.focus_offset) {
177
+ tr.delete_selection();
178
+ }
179
+
180
+ const collapsed_offset = tr.selection.anchor_offset;
181
+ const content = tr.get(tr.selection.path);
182
+ const text_before_caret = char_slice(content.text, 0, collapsed_offset);
183
+ const line_start_index = text_before_caret.lastIndexOf('\n') + 1;
184
+ const current_line_prefix = text_before_caret.slice(line_start_index);
185
+ const indentation_match = current_line_prefix.match(/^[\t ]*/);
186
+ const indentation = indentation_match ? indentation_match[0] : '';
187
+
188
+ tr.insert_text(`\n${indentation}`);
189
+ session.apply(tr);
173
190
  }
174
191
  }
175
192
 
@@ -180,7 +197,15 @@ export class AddNewLineCommand extends Command {
180
197
  */
181
198
  export class BreakTextNodeCommand extends Command {
182
199
  is_enabled() {
183
- return this.context.editable && this.context.session.selection?.type === 'text';
200
+ const session = this.context.session;
201
+ const selection = session.selection;
202
+ if (!this.context.editable || selection?.type !== 'text') return false;
203
+
204
+ const owner_node = session.get(selection.path.slice(0, -1));
205
+ const owner_node_schema = owner_node ? session.schema[owner_node.type] : null;
206
+ if (!owner_node_schema || owner_node_schema.kind !== 'text') return false;
207
+
208
+ return session.inspect(selection.path.slice(0, -2))?.type === 'node_array';
184
209
  }
185
210
 
186
211
  execute() {
@@ -1,3 +1,5 @@
1
+ import { is_virtual_keyboard_active } from './utils.js';
2
+
1
3
  const MODIFIER_KEYS = ['meta', 'ctrl', 'alt', 'shift'];
2
4
  const MODIFIER_EVENT_KEYS = {
3
5
  meta: 'metaKey',
@@ -34,7 +36,7 @@ export function define_keymap(keymap) {
34
36
  * Matches a keyboard event against a key combo string.
35
37
  * Example: 'meta+e,ctrl+e' matches either (metaKey && key==='e') OR (ctrlKey && key==='e')
36
38
  */
37
- function matches_key_combo(key_combo, event) {
39
+ function matches_key_combo(key_combo, event, virtual_keyboard_active = false) {
38
40
  const alternatives = key_combo.split(',');
39
41
 
40
42
  return alternatives.some((alternative) => {
@@ -42,14 +44,22 @@ function matches_key_combo(key_combo, event) {
42
44
  const modifiers = parts.filter((part) => MODIFIER_KEYS.includes(part));
43
45
  const non_modifier = parts.find((part) => !MODIFIER_KEYS.includes(part));
44
46
 
47
+ if (virtual_keyboard_active && modifiers.length > 0) {
48
+ return false;
49
+ }
50
+
45
51
  // Check if all specified modifiers are pressed
46
52
  const modifiers_match = modifiers.every((mod) => event[MODIFIER_EVENT_KEYS[mod]]);
47
53
 
48
54
  // Check if no unspecified modifiers are pressed
49
- const no_extra_modifiers = MODIFIER_KEYS.every((mod) => {
50
- if (modifiers.includes(mod)) return true; // This modifier is expected
51
- return !event[MODIFIER_EVENT_KEYS[mod]]; // This modifier should NOT be pressed
52
- });
55
+ // NOTE: On virtual keyboards (iOS / Anroid) modifier keys are just ignored, because
56
+ // they must be assumed accidental (e.g. Shift activated as a result of auto-capitalization)
57
+ const no_extra_modifiers = virtual_keyboard_active
58
+ ? true
59
+ : MODIFIER_KEYS.every((mod) => {
60
+ if (modifiers.includes(mod)) return true; // This modifier is expected
61
+ return !event[MODIFIER_EVENT_KEYS[mod]]; // This modifier should NOT be pressed
62
+ });
53
63
 
54
64
  // Check if the key matches
55
65
  const key_matches = event.key.toLowerCase() === non_modifier;
@@ -65,8 +75,10 @@ function matches_key_combo(key_combo, event) {
65
75
  * but don't crash the application.
66
76
  */
67
77
  function handle_key_map(key_map, event) {
78
+ const virtual_keyboard_active = is_virtual_keyboard_active();
79
+
68
80
  for (const [key_combo, commands] of Object.entries(key_map)) {
69
- if (matches_key_combo(key_combo, event)) {
81
+ if (matches_key_combo(key_combo, event, virtual_keyboard_active)) {
70
82
  // Find the first enabled command and execute it
71
83
  const enabled_command = commands.find((cmd) => cmd.is_enabled());
72
84
  if (enabled_command) {
@@ -1,15 +1,17 @@
1
1
  <script>
2
2
  import { getContext, setContext } from 'svelte';
3
3
  import UnknownNode from './UnknownNode.svelte';
4
- import { serialize_path, snake_to_pascal } from './utils.js';
4
+ import { serialize_path } from './utils.js';
5
5
  import DefaultNodeGap from './NodeGap.svelte';
6
6
  import DefaultNodeGapMarkers from './NodeGapMarkers.svelte';
7
7
 
8
8
  /** @import { NodeArrayPropertyProps } from './types.d.ts'; */
9
9
 
10
10
  const svedit = getContext('svedit');
11
- let NodeGap = $derived(svedit.session.config.system_components?.NodeGap ?? DefaultNodeGap);
12
- let NodeGapMarkers = $derived(svedit.session.config.system_components?.NodeGapMarkers ?? DefaultNodeGapMarkers);
11
+ let NodeGap = $derived(svedit.session.config.system_components?.node_gap ?? DefaultNodeGap);
12
+ let NodeGapMarkers = $derived(
13
+ svedit.session.config.system_components?.node_gap_markers ?? DefaultNodeGapMarkers
14
+ );
13
15
 
14
16
  /** @type {NodeArrayPropertyProps} */
15
17
  let { path, tag = 'div', class: css_class, style = '', ...rest } = $props();
@@ -26,8 +28,8 @@
26
28
  // NodeGap while the caret is in the placeholder, so its <br> isn't a
27
29
  // second arrow-key stop (issue #260).
28
30
  let is_focused = $derived(
29
- svedit.session.selection?.type === 'node'
30
- && serialize_path(svedit.session.selection.path) === path_str
31
+ svedit.session.selection?.type === 'node' &&
32
+ serialize_path(svedit.session.selection.path) === path_str
31
33
  );
32
34
 
33
35
  setContext('node_array_meta', {
@@ -35,7 +37,6 @@
35
37
  return node_ids.length;
36
38
  }
37
39
  });
38
-
39
40
  </script>
40
41
 
41
42
  <!-- we use the anchor of node_array in Overlays.svelte to position the last insertion point in a horizontal layout based on the right edge of the container -->
@@ -55,21 +56,12 @@
55
56
  data-type="node"
56
57
  style="anchor-name: --{serialize_path([...path, 0])};"
57
58
  >
58
- <NodeGap
59
- array_path={path}
60
- offset={0}
61
- count={0}
62
- empty
63
- />
59
+ <NodeGap array_path={path} offset={0} count={0} empty />
64
60
  </div>
65
61
  {/if}
66
62
  {#each nodes as node, index (index)}
67
- <NodeGap
68
- array_path={path}
69
- offset={index}
70
- count={nodes.length}
71
- />
72
- {@const Component = svedit.session.config.node_components[snake_to_pascal(node.type)]}
63
+ <NodeGap array_path={path} offset={index} count={nodes.length} />
64
+ {@const Component = svedit.session.config.node_components[node.type]}
73
65
  {#if Component}
74
66
  <Component path={[...path, index]} />
75
67
  {:else}
@@ -77,11 +69,7 @@
77
69
  {/if}
78
70
  {/each}
79
71
  {#if node_ids.length > 0}
80
- <NodeGap
81
- array_path={path}
82
- offset={node_ids.length}
83
- count={node_ids.length}
84
- />
72
+ <NodeGap array_path={path} offset={node_ids.length} count={node_ids.length} />
85
73
  {/if}
86
74
  {#if svedit.editable && NodeGapMarkers}
87
75
  <NodeGapMarkers {path} count={node_ids.length} />
@@ -89,7 +77,7 @@
89
77
  </svelte:element>
90
78
 
91
79
  <style>
92
- /* position: relative makes this the containing block for the gap's
80
+ /* position: relative makes this the containing block for the gap's
93
81
  .svedit-selectable, which fills it via inset:0. You may override to
94
82
  position: absolute (e.g. so an empty array doesn't occupy flow space)
95
83
  — still a containing block; then also set position: relative on the
@@ -102,4 +90,4 @@
102
90
  min-width: 24px;
103
91
  cursor: pointer;
104
92
  }
105
- </style>
93
+ </style>