svedit 0.12.0 → 0.14.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 (88) hide show
  1. package/README.md +35 -22
  2. package/dist/Command.svelte.d.ts +38 -30
  3. package/dist/Command.svelte.d.ts.map +1 -1
  4. package/dist/Command.svelte.js +271 -326
  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 +16 -13
  12. package/dist/Node.svelte.d.ts +3 -5
  13. package/dist/Node.svelte.d.ts.map +1 -1
  14. package/dist/NodeArrayProperty.svelte +39 -22
  15. package/dist/NodeArrayProperty.svelte.d.ts +3 -5
  16. package/dist/NodeArrayProperty.svelte.d.ts.map +1 -1
  17. package/dist/NodeCaret.svelte +2 -2
  18. package/dist/NodeCaret.svelte.d.ts +6 -14
  19. package/dist/NodeCaret.svelte.d.ts.map +1 -1
  20. package/dist/NodeGap.svelte +12 -6
  21. package/dist/NodeGap.svelte.d.ts +7 -14
  22. package/dist/NodeGap.svelte.d.ts.map +1 -1
  23. package/dist/NodeGapMarkers.svelte +36 -14
  24. package/dist/NodeGapMarkers.svelte.d.ts +6 -11
  25. package/dist/NodeGapMarkers.svelte.d.ts.map +1 -1
  26. package/dist/NodeSelectionMarkers.svelte +21 -7
  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 +81 -129
  30. package/dist/Session.svelte.d.ts.map +1 -1
  31. package/dist/Session.svelte.js +542 -648
  32. package/dist/Svedit.svelte +146 -140
  33. package/dist/Svedit.svelte.d.ts +30 -10
  34. package/dist/Svedit.svelte.d.ts.map +1 -1
  35. package/dist/TextProperty.svelte +37 -23
  36. package/dist/TextProperty.svelte.d.ts +3 -5
  37. package/dist/TextProperty.svelte.d.ts.map +1 -1
  38. package/dist/Transaction.svelte.d.ts +64 -127
  39. package/dist/Transaction.svelte.d.ts.map +1 -1
  40. package/dist/Transaction.svelte.js +839 -1001
  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 +67 -155
  45. package/dist/doc_utils.d.ts.map +1 -1
  46. package/dist/doc_utils.js +619 -863
  47. package/dist/index.d.ts +20 -17
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +1 -17
  50. package/dist/node_visibility.svelte.d.ts +110 -9
  51. package/dist/node_visibility.svelte.d.ts.map +1 -1
  52. package/dist/node_visibility.svelte.js +473 -544
  53. package/dist/paste_utils.d.ts +11 -44
  54. package/dist/paste_utils.d.ts.map +1 -1
  55. package/dist/paste_utils.js +111 -152
  56. package/dist/styles/svedit-colors.css +8 -5
  57. package/dist/transforms.svelte.d.ts +9 -7
  58. package/dist/transforms.svelte.d.ts.map +1 -1
  59. package/dist/transforms.svelte.js +125 -150
  60. package/dist/types.d.ts +294 -309
  61. package/dist/types.d.ts.map +1 -0
  62. package/dist/types.js +1 -0
  63. package/dist/utils.d.ts +40 -119
  64. package/dist/utils.d.ts.map +1 -1
  65. package/dist/utils.js +346 -473
  66. package/package.json +70 -70
  67. package/src/lib/{Command.svelte.js → Command.svelte.ts} +29 -17
  68. package/src/lib/CustomProperty.svelte +3 -5
  69. package/src/lib/{KeyMapper.svelte.js → KeyMapper.svelte.ts} +28 -11
  70. package/src/lib/Node.svelte +16 -13
  71. package/src/lib/NodeArrayProperty.svelte +39 -22
  72. package/src/lib/NodeCaret.svelte +2 -2
  73. package/src/lib/NodeGap.svelte +12 -6
  74. package/src/lib/NodeGapMarkers.svelte +36 -14
  75. package/src/lib/NodeSelectionMarkers.svelte +21 -7
  76. package/src/lib/{Session.svelte.js → Session.svelte.ts} +130 -127
  77. package/src/lib/Svedit.svelte +146 -140
  78. package/src/lib/TextProperty.svelte +37 -23
  79. package/src/lib/{Transaction.svelte.js → Transaction.svelte.ts} +139 -143
  80. package/src/lib/UnknownNode.svelte +4 -3
  81. package/src/lib/{doc_utils.js → doc_utils.ts} +247 -206
  82. package/src/lib/{index.js → index.ts} +48 -0
  83. package/src/lib/{node_visibility.svelte.js → node_visibility.svelte.ts} +68 -92
  84. package/src/lib/{paste_utils.js → paste_utils.ts} +47 -58
  85. package/src/lib/styles/svedit-colors.css +8 -5
  86. package/src/lib/{transforms.svelte.js → transforms.svelte.ts} +39 -21
  87. package/src/lib/{types.d.ts → types.ts} +161 -66
  88. package/src/lib/{utils.js → utils.ts} +110 -129
package/package.json CHANGED
@@ -1,71 +1,71 @@
1
1
  {
2
- "name": "svedit",
3
- "version": "0.12.0",
4
- "license": "MIT",
5
- "scripts": {
6
- "dev": "vite dev",
7
- "build": "vite build",
8
- "preview": "vite preview",
9
- "prepare": "svelte-kit sync || echo ''",
10
- "prepack": "svelte-kit sync && svelte-package && publint",
11
- "check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
12
- "check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
13
- "format": "prettier --write .",
14
- "lint": "prettier --check . && eslint .",
15
- "test:unit": "vitest",
16
- "test": "npm run test:unit -- --run"
17
- },
18
- "files": [
19
- "dist",
20
- "!dist/**/*.test.*",
21
- "!dist/**/*.spec.*",
22
- "src/lib",
23
- "!src/lib/**/*.test.*",
24
- "!src/lib/**/*.spec.*"
25
- ],
26
- "sideEffects": [
27
- "**/*.css"
28
- ],
29
- "svelte": "./dist/index.js",
30
- "types": "./dist/index.d.ts",
31
- "type": "module",
32
- "exports": {
33
- ".": {
34
- "types": "./dist/index.d.ts",
35
- "svelte": "./dist/index.js"
36
- }
37
- },
38
- "peerDependencies": {
39
- "svelte": "^5.0.0"
40
- },
41
- "devDependencies": {
42
- "@eslint/compat": "^2.0.3",
43
- "@eslint/js": "^10.0.1",
44
- "@sveltejs/adapter-auto": "^7.0.0",
45
- "@sveltejs/kit": "^2.55.0",
46
- "@sveltejs/package": "^2.5.7",
47
- "@sveltejs/vite-plugin-svelte": "^7.0.0",
48
- "@types/node": "^25.6.0",
49
- "@typescript-eslint/eslint-plugin": "^8.57.0",
50
- "@typescript-eslint/parser": "^8.57.0",
51
- "@vitest/browser": "^4.1.0",
52
- "@vitest/browser-preview": "^4.1.0",
53
- "eslint": "^10.0.3",
54
- "eslint-config-prettier": "^10.1.8",
55
- "eslint-plugin-svelte": "^3.15.1",
56
- "globals": "^17.4.0",
57
- "nanoid": "^5.1.6",
58
- "prettier": "^3.8.1",
59
- "prettier-plugin-svelte": "^3.5.1",
60
- "publint": "^0.3.18",
61
- "svelte": "^5.53.12",
62
- "svelte-check": "^4.4.5",
63
- "typescript": "^5.9.3",
64
- "vite": "^8.0.0",
65
- "vitest": "^4.1.0",
66
- "vitest-browser-svelte": "^2.1.0"
67
- },
68
- "allowScripts": {
69
- "fsevents@2.3.3": true
70
- }
71
- }
2
+ "name": "svedit",
3
+ "version": "0.14.0",
4
+ "homepage": "https://svedit.dev",
5
+ "license": "MIT",
6
+ "files": [
7
+ "dist",
8
+ "!dist/**/*.test.*",
9
+ "!dist/**/*.spec.*",
10
+ "src/lib",
11
+ "!src/lib/**/*.test.*",
12
+ "!src/lib/**/*.spec.*"
13
+ ],
14
+ "sideEffects": [
15
+ "**/*.css"
16
+ ],
17
+ "svelte": "./dist/index.js",
18
+ "types": "./dist/index.d.ts",
19
+ "type": "module",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.ts",
23
+ "svelte": "./dist/index.js"
24
+ }
25
+ },
26
+ "peerDependencies": {
27
+ "svelte": "^5.0.0"
28
+ },
29
+ "devDependencies": {
30
+ "@eslint/compat": "^2.0.3",
31
+ "@eslint/js": "^10.0.1",
32
+ "@sveltejs/adapter-auto": "^7.0.0",
33
+ "@sveltejs/kit": "^2.55.0",
34
+ "@sveltejs/package": "^2.5.7",
35
+ "@sveltejs/vite-plugin-svelte": "^7.0.0",
36
+ "@types/node": "^25.6.0",
37
+ "@typescript-eslint/eslint-plugin": "^8.57.0",
38
+ "@typescript-eslint/parser": "^8.57.0",
39
+ "@vitest/browser": "^4.1.0",
40
+ "@vitest/browser-preview": "^4.1.0",
41
+ "eslint": "^10.0.3",
42
+ "eslint-config-prettier": "^10.1.8",
43
+ "eslint-plugin-svelte": "^3.15.1",
44
+ "globals": "^17.4.0",
45
+ "nanoid": "^5.1.6",
46
+ "prettier": "^3.8.1",
47
+ "prettier-plugin-svelte": "^3.5.1",
48
+ "publint": "^0.3.18",
49
+ "svelte": "^5.53.12",
50
+ "svelte-check": "^4.4.5",
51
+ "typescript": "^5.9.3",
52
+ "vite": "^8.0.0",
53
+ "vitest": "^4.1.0",
54
+ "vitest-browser-svelte": "^2.1.0"
55
+ },
56
+ "allowScripts": {
57
+ "fsevents@2.3.3": true
58
+ },
59
+ "scripts": {
60
+ "dev": "vite dev",
61
+ "build": "vite build",
62
+ "preview": "vite preview",
63
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
64
+ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
65
+ "check:strict": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.strict.json",
66
+ "format": "prettier --write .",
67
+ "lint": "prettier --check . && eslint .",
68
+ "test:unit": "vitest",
69
+ "test": "pnpm test:unit --run"
70
+ }
71
+ }
@@ -1,6 +1,16 @@
1
1
  import { insert_default_node, break_text_node } from './transforms.svelte.js';
2
2
  import { can_switch_mark_type, get_selected_range_types } from './doc_utils.js';
3
3
  import { is_selection_collapsed, get_char_length, char_slice } from './utils.js';
4
+ import type Session from './Session.svelte.js';
5
+
6
+ // Commands operate on sessions with arbitrary application schemas.
7
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
8
+ type CommandSession = Session<any>;
9
+
10
+ export type CommandContext = {
11
+ session: CommandSession;
12
+ editable: boolean;
13
+ };
4
14
 
5
15
  /**
6
16
  * Base class for commands that can be executed in response to user actions
@@ -25,6 +35,8 @@ import { is_selection_collapsed, get_char_length, char_slice } from './utils.js'
25
35
  * ```
26
36
  */
27
37
  export default class Command {
38
+ context: CommandContext;
39
+
28
40
  /**
29
41
  * Derived state that indicates if the command is disabled.
30
42
  * Automatically computed from is_enabled().
@@ -34,19 +46,17 @@ export default class Command {
34
46
  /**
35
47
  * Creates a new Command instance.
36
48
  *
37
- * @param {any} context - The context object providing access to application state
49
+ * @param context - The context object providing access to application state
38
50
  */
39
- constructor(context) {
51
+ constructor(context: CommandContext) {
40
52
  this.context = context;
41
53
  }
42
54
 
43
55
  /**
44
56
  * Determines if the command can currently be executed.
45
57
  * Override this method to implement command-specific logic.
46
- *
47
- * @returns {boolean} true if the command can be executed, false otherwise
48
58
  */
49
- is_enabled() {
59
+ is_enabled(): boolean {
50
60
  return true;
51
61
  }
52
62
 
@@ -54,10 +64,8 @@ export default class Command {
54
64
  * Executes the command.
55
65
  * Override this method to implement the command's behavior.
56
66
  * Can be async for commands that need to perform asynchronous operations.
57
- *
58
- * @returns {void | Promise<void>}
59
67
  */
60
- execute() {
68
+ execute(): void | Promise<void> {
61
69
  throw new Error('Not implemented');
62
70
  }
63
71
  }
@@ -94,7 +102,7 @@ export class RedoCommand extends Command {
94
102
  */
95
103
  export class SelectParentCommand extends Command {
96
104
  is_enabled() {
97
- return (
105
+ return Boolean(
98
106
  this.context.editable &&
99
107
  this.context.session.selection &&
100
108
  this.context.session.selection.path.length > 3
@@ -114,7 +122,9 @@ export class SelectParentCommand extends Command {
114
122
  * touched types disable the command. Annotations never affect mark toggling.
115
123
  */
116
124
  export class ToggleMarkCommand extends Command {
117
- constructor(node_type, context) {
125
+ node_type: string;
126
+
127
+ constructor(node_type: string, context: CommandContext) {
118
128
  super(context);
119
129
  this.node_type = node_type;
120
130
  }
@@ -124,8 +134,7 @@ export class ToggleMarkCommand extends Command {
124
134
  is_active() {
125
135
  const selected_marks = this.context.session.selected_marks;
126
136
  return (
127
- selected_marks.length > 0 &&
128
- selected_marks.every(({ node }) => node?.type === this.node_type)
137
+ selected_marks.length > 0 && selected_marks.every(({ node }) => node?.type === this.node_type)
129
138
  );
130
139
  }
131
140
 
@@ -167,7 +176,9 @@ export class ToggleMarkCommand extends Command {
167
176
  * Marks and other annotation types never block the toggle.
168
177
  */
169
178
  export class ToggleAnnotationCommand extends Command {
170
- constructor(node_type, context) {
179
+ node_type: string;
180
+
181
+ constructor(node_type: string, context: CommandContext) {
171
182
  super(context);
172
183
  this.node_type = node_type;
173
184
  }
@@ -234,6 +245,7 @@ export class AddNewLineCommand extends Command {
234
245
  if (selection.anchor_offset !== selection.focus_offset) {
235
246
  tr.delete_selection();
236
247
  }
248
+ if (tr.selection.type !== 'text') return;
237
249
 
238
250
  const collapsed_offset = tr.selection.anchor_offset;
239
251
  const content = tr.get(tr.selection.path);
@@ -280,7 +292,7 @@ export class BreakTextNodeCommand extends Command {
280
292
  */
281
293
  export class SelectAllCommand extends Command {
282
294
  is_enabled() {
283
- return this.context.editable && this.context.session.selection;
295
+ return Boolean(this.context.editable && this.context.session.selection);
284
296
  }
285
297
 
286
298
  execute() {
@@ -318,7 +330,7 @@ export class SelectAllCommand extends Command {
318
330
  session.inspect(node_path.slice(0, -1))?.type === 'node_array';
319
331
 
320
332
  if (is_inside_node_array) {
321
- const node_index = parseInt(node_path.at(-1));
333
+ const node_index = node_path.at(-1) as number;
322
334
  session.selection = {
323
335
  type: 'node',
324
336
  path: node_path.slice(0, -1),
@@ -356,7 +368,7 @@ export class SelectAllCommand extends Command {
356
368
  session.inspect(parent_path.slice(0, -1))?.type === 'node_array';
357
369
 
358
370
  if (is_parent_node_array) {
359
- const parent_node_index = parseInt(parent_path.at(-1));
371
+ const parent_node_index = parent_path.at(-1) as number;
360
372
  session.selection = {
361
373
  type: 'node',
362
374
  path: parent_path.slice(0, -1),
@@ -376,7 +388,7 @@ export class SelectAllCommand extends Command {
376
388
  const is_inside_node_array = session.inspect(node_path.slice(0, -1))?.type === 'node_array';
377
389
 
378
390
  if (is_inside_node_array) {
379
- const node_index = parseInt(node_path.at(-1));
391
+ const node_index = node_path.at(-1) as number;
380
392
  session.selection = {
381
393
  type: 'node',
382
394
  path: node_path.slice(0, -1),
@@ -1,10 +1,8 @@
1
- <script>
1
+ <script lang="ts">
2
2
  import { serialize_path } from './utils.js';
3
+ import type { CustomPropertyProps } from './types.js';
3
4
 
4
- /** @import { CustomPropertyProps } from './types.d.ts'; */
5
-
6
- /** @type {CustomPropertyProps} */
7
- let { path, tag = 'div', class: css_class, children, style, ...rest } = $props();
5
+ let { path, tag = 'div', class: css_class, children, style, ...rest }: CustomPropertyProps = $props();
8
6
  let path_str = $derived(serialize_path(path));
9
7
 
10
8
  </script>
@@ -1,20 +1,30 @@
1
1
  import { is_virtual_keyboard_active } from './utils.js';
2
2
 
3
- const MODIFIER_KEYS = ['meta', 'ctrl', 'alt', 'shift'];
4
- const MODIFIER_EVENT_KEYS = {
3
+ const MODIFIER_KEYS: readonly string[] = ['meta', 'ctrl', 'alt', 'shift'];
4
+ const MODIFIER_EVENT_KEYS: Record<string, 'metaKey' | 'ctrlKey' | 'altKey' | 'shiftKey'> = {
5
5
  meta: 'metaKey',
6
6
  ctrl: 'ctrlKey',
7
7
  alt: 'altKey',
8
8
  shift: 'shiftKey'
9
9
  };
10
10
 
11
+ /**
12
+ * A command as required by the key mapper: anything with is_enabled/execute.
13
+ */
14
+ type KeymapCommand = { is_enabled: () => boolean; execute: () => unknown };
15
+
16
+ /**
17
+ * A keymap maps key combo strings to arrays of commands.
18
+ */
19
+ export type Keymap = Record<string, KeymapCommand[]>;
20
+
11
21
  /**
12
22
  * Validates and defines a keymap.
13
23
  * Throws an error if any key combo is invalid.
14
24
  * Valid formats: 'meta+e,ctrl+e' or 'meta+shift+a'
15
25
  * Invalid: 'meta+e+a' (only one non-modifier key allowed)
16
26
  */
17
- export function define_keymap(keymap) {
27
+ export function define_keymap<K extends Keymap>(keymap: K): K {
18
28
  for (const [key_combo] of Object.entries(keymap)) {
19
29
  const alternatives = key_combo.split(',');
20
30
 
@@ -36,7 +46,11 @@ export function define_keymap(keymap) {
36
46
  * Matches a keyboard event against a key combo string.
37
47
  * Example: 'meta+e,ctrl+e' matches either (metaKey && key==='e') OR (ctrlKey && key==='e')
38
48
  */
39
- function matches_key_combo(key_combo, event, virtual_keyboard_active = false) {
49
+ function matches_key_combo(
50
+ key_combo: string,
51
+ event: KeyboardEvent,
52
+ virtual_keyboard_active = false
53
+ ): boolean {
40
54
  const alternatives = key_combo.split(',');
41
55
 
42
56
  return alternatives.some((alternative) => {
@@ -57,9 +71,9 @@ function matches_key_combo(key_combo, event, virtual_keyboard_active = false) {
57
71
  const no_extra_modifiers = virtual_keyboard_active
58
72
  ? true
59
73
  : 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
- });
74
+ if (modifiers.includes(mod)) return true; // This modifier is expected
75
+ return !event[MODIFIER_EVENT_KEYS[mod]]; // This modifier should NOT be pressed
76
+ });
63
77
 
64
78
  // Check if the key matches
65
79
  const key_matches = event.key.toLowerCase() === non_modifier;
@@ -74,7 +88,7 @@ function matches_key_combo(key_combo, event, virtual_keyboard_active = false) {
74
88
  * Supports both sync and async commands. For async commands, errors are logged
75
89
  * but don't crash the application.
76
90
  */
77
- function handle_key_map(key_map, event) {
91
+ function handle_key_map(key_map: Keymap, event: KeyboardEvent): boolean {
78
92
  const virtual_keyboard_active = is_virtual_keyboard_active();
79
93
 
80
94
  for (const [key_combo, commands] of Object.entries(key_map)) {
@@ -114,6 +128,9 @@ function handle_key_map(key_map, event) {
114
128
  * mapper.pop_scope(); // When editor loses focus
115
129
  */
116
130
  export class KeyMapper {
131
+ scope_stack: Keymap[];
132
+ skip_onkeydown: boolean;
133
+
117
134
  constructor() {
118
135
  this.scope_stack = [];
119
136
  this.skip_onkeydown = false;
@@ -122,7 +139,7 @@ export class KeyMapper {
122
139
  /**
123
140
  * Push a new scope onto the stack (becomes highest priority)
124
141
  */
125
- push_scope(keymap) {
142
+ push_scope(keymap: Keymap): void {
126
143
  // console.log('pushed keymap', keymap);
127
144
  this.scope_stack.push(keymap);
128
145
  }
@@ -130,7 +147,7 @@ export class KeyMapper {
130
147
  /**
131
148
  * Pop the most recent scope from the stack
132
149
  */
133
- pop_scope() {
150
+ pop_scope(): Keymap | undefined {
134
151
  const keymap = this.scope_stack.pop();
135
152
  // console.log('popped keymap', keymap);
136
153
  return keymap;
@@ -139,7 +156,7 @@ export class KeyMapper {
139
156
  /**
140
157
  * Handle keyboard event by trying scopes from top to bottom
141
158
  */
142
- handle_keydown(event) {
159
+ handle_keydown(event: KeyboardEvent): void {
143
160
  // Key handling temporarily disabled (e.g. while character composition takes place)
144
161
  if (this.skip_onkeydown) return;
145
162
  // console.log('KeyMapper.handle_keydown', event);
@@ -1,19 +1,22 @@
1
- <script>
1
+ <script lang="ts">
2
2
  import { getContext } from 'svelte';
3
3
  import { serialize_path } from './utils.js';
4
+ import type {
5
+ NodeArrayAttachmentContext,
6
+ NodeArrayRenderContext,
7
+ NodeProps,
8
+ SveditRenderContext
9
+ } from './types.js';
4
10
 
5
- /** @import { NodeProps } from './types.d.ts'; */
11
+ const svedit = getContext<SveditRenderContext>('svedit');
6
12
 
7
- const svedit = getContext('svedit');
8
-
9
- /** @type {NodeProps} */
10
- let { path, children, tag = 'div', class: css_class, style = '', ...rest } = $props();
13
+ let { path, children, tag = 'div', class: css_class, style = '', ...rest }: NodeProps = $props();
11
14
 
12
15
  let node = $derived(svedit.session.get(path));
13
16
  let path_str = $derived(serialize_path(path));
14
17
 
15
- const node_array_meta = getContext('node_array_meta');
16
- let child_index = $derived(node_array_meta ? parseInt(String(path.at(-1)), 10) : -1);
18
+ const node_array_meta = getContext<NodeArrayRenderContext | undefined>('node_array_meta');
19
+ let child_index = $derived(node_array_meta ? (path.at(-1) as number) : -1);
17
20
  let is_first = $derived(node_array_meta && child_index === 0);
18
21
  let is_last = $derived(node_array_meta && child_index === node_array_meta.length - 1);
19
22
 
@@ -25,9 +28,9 @@
25
28
  // `annotations` props.
26
29
  let range_classes = $derived.by(() => {
27
30
  if (child_index < 0 || !node_array_meta?.annotations_for) return '';
28
- const classes = [];
31
+ const classes: string[] = [];
29
32
 
30
- const add_classes = (range, prefix) => {
33
+ const add_classes = (range: NodeArrayAttachmentContext | null | undefined, prefix: string) => {
31
34
  const range_type = range?.node?.type;
32
35
  if (!range_type) return;
33
36
  classes.push(`${prefix}-${range_type}`);
@@ -46,9 +49,9 @@
46
49
  <svelte:element
47
50
  this={tag}
48
51
  id={node.id}
49
- class="node-{node.type} {css_class}{range_classes
50
- ? ` ${range_classes}`
51
- : ''}{is_first ? ' first' : ''}{is_last ? ' last' : ''}"
52
+ class="node-{node.type} {css_class}{range_classes ? ` ${range_classes}` : ''}{is_first
53
+ ? ' first'
54
+ : ''}{is_last ? ' last' : ''}"
52
55
  data-node-id={node.id}
53
56
  data-path={path_str}
54
57
  data-type="node"
@@ -1,20 +1,33 @@
1
- <script>
1
+ <script lang="ts">
2
2
  import { getContext, setContext } from 'svelte';
3
3
  import UnknownNode from './UnknownNode.svelte';
4
4
  import { serialize_path, calculate_fragment_ranges } from './utils.js';
5
5
  import DefaultNodeGap from './NodeGap.svelte';
6
6
  import DefaultNodeGapMarkers from './NodeGapMarkers.svelte';
7
-
8
- /** @import { NodeArrayPropertyProps, NodeArrayAttachmentContext } from './types.d.ts'; */
9
-
10
- const svedit = getContext('svedit');
7
+ import type {
8
+ NodeArrayPropertyProps,
9
+ NodeArrayAttachmentContext,
10
+ NodeArrayFragment,
11
+ NodeId,
12
+ Attachment,
13
+ Mark,
14
+ NodeArrayRenderContext,
15
+ SveditRenderContext
16
+ } from './types.js';
17
+
18
+ const svedit = getContext<SveditRenderContext>('svedit');
11
19
  let NodeGap = $derived(svedit.session.config.system_components?.node_gap ?? DefaultNodeGap);
12
20
  let NodeGapMarkers = $derived(
13
21
  svedit.session.config.system_components?.node_gap_markers ?? DefaultNodeGapMarkers
14
22
  );
15
23
 
16
- /** @type {NodeArrayPropertyProps} */
17
- let { path, tag = 'div', class: css_class, style = '', ...rest } = $props();
24
+ let {
25
+ path,
26
+ tag = 'div',
27
+ class: css_class,
28
+ style = '',
29
+ ...rest
30
+ }: NodeArrayPropertyProps = $props();
18
31
 
19
32
  // Pre-joined once per path change; reused by data-path and by every
20
33
  // NodeGap's should_position_gap call to avoid N+1 joins per render.
@@ -27,9 +40,9 @@
27
40
 
28
41
  let fragments = $derived(get_fragments(node_ids, marks));
29
42
 
30
- function get_fragments(node_ids, marks) {
43
+ function get_fragments(node_ids: NodeId[], marks: Mark[]): NodeArrayFragment[] {
31
44
  const ranges = calculate_fragment_ranges(node_ids.length, marks);
32
- let fragments = [];
45
+ const fragments: NodeArrayFragment[] = [];
33
46
 
34
47
  for (const range of ranges) {
35
48
  const nodes_slice = node_ids.slice(range.start_offset, range.end_offset);
@@ -41,7 +54,7 @@
41
54
  node,
42
55
  nodes: nodes_slice,
43
56
  start_index: range.start_offset,
44
- mark_index: range.mark_index
57
+ mark_index: range.mark_index!
45
58
  });
46
59
  } else if (range.type === 'content') {
47
60
  fragments.push({
@@ -54,7 +67,7 @@
54
67
  return fragments;
55
68
  }
56
69
 
57
- function get_node_key(node_id, index) {
70
+ function get_node_key(node_id: NodeId, index: number): string {
58
71
  let occurrence = 0;
59
72
  for (let current_index = 0; current_index < index; current_index++) {
60
73
  if (node_ids[current_index] === node_id) occurrence += 1;
@@ -62,10 +75,11 @@
62
75
  return `${node_id}:${occurrence}`;
63
76
  }
64
77
 
65
- /**
66
- * @returns {NodeArrayAttachmentContext}
67
- */
68
- function get_range_context(range, index, node_index) {
78
+ function get_range_context(
79
+ range: Attachment,
80
+ index: number,
81
+ node_index: number
82
+ ): NodeArrayAttachmentContext {
69
83
  const node = svedit.session.get(range.node_id);
70
84
  const is_start = node_index === range.start_offset;
71
85
  const is_end = node_index === range.end_offset - 1;
@@ -80,18 +94,21 @@
80
94
  };
81
95
  }
82
96
 
83
- function get_covering_ranges(ranges, node_index) {
97
+ function get_covering_ranges(
98
+ ranges: Attachment[],
99
+ node_index: number
100
+ ): NodeArrayAttachmentContext[] {
84
101
  return ranges
85
102
  .map((range, index) => ({ range, index }))
86
103
  .filter(({ range }) => range.start_offset <= node_index && node_index < range.end_offset)
87
104
  .map(({ range, index }) => get_range_context(range, index, node_index));
88
105
  }
89
106
 
90
- function get_annotation_contexts(node_index) {
107
+ function get_annotation_contexts(node_index: number): NodeArrayAttachmentContext[] {
91
108
  return get_covering_ranges(annotations, node_index);
92
109
  }
93
110
 
94
- function get_mark(node_index) {
111
+ function get_mark(node_index: number): NodeArrayAttachmentContext | null {
95
112
  // The single mark wrapping this node, or null. Mark exclusivity
96
113
  // guarantees at most one. Annotations never wrap and are exposed via
97
114
  // `annotations` instead.
@@ -107,17 +124,17 @@
107
124
  serialize_path(svedit.session.selection.path) === path_str
108
125
  );
109
126
 
110
- setContext('node_array_meta', {
127
+ setContext<NodeArrayRenderContext>('node_array_meta', {
111
128
  get length() {
112
129
  return node_ids.length;
113
130
  },
114
131
  // Lets Node self-serve the marks and annotations covering a child, so
115
132
  // node wrappers can carry range classes without every node component
116
133
  // having to thread the `mark`/`annotations` props through.
117
- mark_for(node_index) {
134
+ mark_for(node_index: number) {
118
135
  return get_mark(node_index);
119
136
  },
120
- annotations_for(node_index) {
137
+ annotations_for(node_index: number) {
121
138
  return get_annotation_contexts(node_index);
122
139
  }
123
140
  });
@@ -145,7 +162,7 @@
145
162
  <NodeGap array_path={path} offset={0} count={0} empty />
146
163
  </div>
147
164
  {/if}
148
- {#snippet render_nodes(nodes_slice, start_index)}
165
+ {#snippet render_nodes(nodes_slice: NodeId[], start_index: number)}
149
166
  {#each nodes_slice as id, slice_index (get_node_key(id, start_index + slice_index))}
150
167
  {@const index = start_index + slice_index}
151
168
  {@const node = svedit.session.get(id)}
@@ -1,4 +1,4 @@
1
- <script>
1
+ <script lang="ts">
2
2
  /**
3
3
  * Visual insertion caret rendered inside the active insertion marker.
4
4
  *
@@ -30,7 +30,7 @@
30
30
  --_cp: var(--node-caret-row-inline-position, 50%);
31
31
  content: '';
32
32
  position: absolute;
33
- background: var(--node-caret-bg, var(--svedit-editing-stroke));
33
+ background: var(--node-caret-bg, var(--editing));
34
34
  /* Increase the visibility of the caret by contrasting it with a box shadow that's the inverted brightness of the current text color. */
35
35
  /* Component developers must set their background color and text color on the node itself, not a child element, for this to work. */
36
36
  box-shadow: var(--node-caret-shadow, 0 0 0 0.5px oklch(from currentColor calc(1 - l) c h));
@@ -1,6 +1,6 @@
1
- <script module>
2
- function cycle_z_index(/** @type {MouseEvent} */ e) {
3
- const el = /** @type {HTMLElement} */ (e.currentTarget);
1
+ <script module lang="ts">
2
+ function cycle_z_index(e: MouseEvent) {
3
+ const el = e.currentTarget as HTMLElement;
4
4
  if (el.hasAttribute('data-sent-to-back')) {
5
5
  document.querySelectorAll('.svedit-selectable[data-sent-to-back]').forEach((prev) => {
6
6
  prev.removeAttribute('data-sent-to-back');
@@ -10,10 +10,11 @@
10
10
  }
11
11
  </script>
12
12
 
13
- <script>
13
+ <script lang="ts">
14
14
  import { serialize_path } from './utils.js';
15
15
  import { getContext } from 'svelte';
16
16
  import { should_position_gap } from './node_visibility.svelte.js';
17
+ import type { DocumentPath, SveditRenderContext } from './types.js';
17
18
 
18
19
  /**
19
20
  * ┌─────────────────────────────────────────────────────────────────┐
@@ -58,8 +59,13 @@
58
59
  * sure your custom component also handles read-only mode by rendering a
59
60
  * plain `.node-gap` placeholder without editable internals.
60
61
  */
61
- const svedit = getContext('svedit');
62
- let { array_path, offset, count, empty = false } = $props();
62
+ const svedit = getContext<SveditRenderContext>('svedit');
63
+ let {
64
+ array_path,
65
+ offset,
66
+ count,
67
+ empty = false
68
+ }: { array_path: DocumentPath; offset: number; count: number; empty?: boolean } = $props();
63
69
  let is_editable = $derived(svedit.editable);
64
70
 
65
71
  let is_first = $derived(offset === 0);