elit 3.5.6 → 3.5.8

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 (128) hide show
  1. package/Cargo.toml +1 -1
  2. package/README.md +1 -1
  3. package/desktop/build.rs +83 -0
  4. package/desktop/icon.rs +106 -0
  5. package/desktop/lib.rs +2 -0
  6. package/desktop/main.rs +235 -0
  7. package/desktop/native_main.rs +128 -0
  8. package/desktop/native_renderer/action_widgets.rs +184 -0
  9. package/desktop/native_renderer/app_models.rs +171 -0
  10. package/desktop/native_renderer/app_runtime.rs +140 -0
  11. package/desktop/native_renderer/container_rendering.rs +610 -0
  12. package/desktop/native_renderer/content_widgets.rs +634 -0
  13. package/desktop/native_renderer/css_models.rs +371 -0
  14. package/desktop/native_renderer/embedded_surfaces.rs +414 -0
  15. package/desktop/native_renderer/form_controls.rs +516 -0
  16. package/desktop/native_renderer/interaction_dispatch.rs +89 -0
  17. package/desktop/native_renderer/runtime_support.rs +135 -0
  18. package/desktop/native_renderer/utilities.rs +495 -0
  19. package/desktop/native_renderer/vector_drawing.rs +491 -0
  20. package/desktop/native_renderer.rs +4122 -0
  21. package/desktop/runtime/external.rs +422 -0
  22. package/desktop/runtime/mod.rs +67 -0
  23. package/desktop/runtime/quickjs.rs +106 -0
  24. package/desktop/window.rs +383 -0
  25. package/dist/build.d.ts +1 -1
  26. package/dist/cli.cjs +16 -2
  27. package/dist/cli.mjs +16 -2
  28. package/dist/config.d.ts +1 -1
  29. package/dist/coverage.d.ts +1 -1
  30. package/dist/desktop-auto-render.cjs +2370 -0
  31. package/dist/desktop-auto-render.d.ts +13 -0
  32. package/dist/desktop-auto-render.js +2341 -0
  33. package/dist/desktop-auto-render.mjs +2344 -0
  34. package/dist/render-context.cjs +118 -0
  35. package/dist/render-context.d.ts +39 -0
  36. package/dist/render-context.js +77 -0
  37. package/dist/render-context.mjs +87 -0
  38. package/dist/{server-CNgDUgSZ.d.ts → server-FCdUqabc.d.ts} +1 -1
  39. package/dist/server.d.ts +1 -1
  40. package/package.json +26 -3
  41. package/dist/build.d.mts +0 -20
  42. package/dist/chokidar.d.mts +0 -134
  43. package/dist/cli.d.mts +0 -81
  44. package/dist/config.d.mts +0 -254
  45. package/dist/coverage.d.mts +0 -85
  46. package/dist/database.d.mts +0 -52
  47. package/dist/desktop.d.mts +0 -68
  48. package/dist/dom.d.mts +0 -87
  49. package/dist/el.d.mts +0 -208
  50. package/dist/fs.d.mts +0 -255
  51. package/dist/hmr.d.mts +0 -38
  52. package/dist/http.d.mts +0 -169
  53. package/dist/https.d.mts +0 -108
  54. package/dist/index.d.mts +0 -13
  55. package/dist/mime-types.d.mts +0 -48
  56. package/dist/native.d.mts +0 -136
  57. package/dist/path.d.mts +0 -163
  58. package/dist/router.d.mts +0 -49
  59. package/dist/runtime.d.mts +0 -97
  60. package/dist/server-D0Dp4R5z.d.mts +0 -449
  61. package/dist/server.d.mts +0 -7
  62. package/dist/state.d.mts +0 -117
  63. package/dist/style.d.mts +0 -232
  64. package/dist/test-reporter.d.mts +0 -77
  65. package/dist/test-runtime.d.mts +0 -122
  66. package/dist/test.d.mts +0 -39
  67. package/dist/types.d.mts +0 -586
  68. package/dist/universal.d.mts +0 -21
  69. package/dist/ws.d.mts +0 -200
  70. package/dist/wss.d.mts +0 -108
  71. package/src/build.ts +0 -362
  72. package/src/chokidar.ts +0 -427
  73. package/src/cli.ts +0 -1162
  74. package/src/config.ts +0 -509
  75. package/src/coverage.ts +0 -1479
  76. package/src/database.ts +0 -1410
  77. package/src/desktop-auto-render.ts +0 -317
  78. package/src/desktop-cli.ts +0 -1533
  79. package/src/desktop.ts +0 -99
  80. package/src/dev-build.ts +0 -340
  81. package/src/dom.ts +0 -901
  82. package/src/el.ts +0 -183
  83. package/src/fs.ts +0 -609
  84. package/src/hmr.ts +0 -149
  85. package/src/http.ts +0 -856
  86. package/src/https.ts +0 -411
  87. package/src/index.ts +0 -16
  88. package/src/mime-types.ts +0 -222
  89. package/src/mobile-cli.ts +0 -2313
  90. package/src/native-background.ts +0 -444
  91. package/src/native-border.ts +0 -343
  92. package/src/native-canvas.ts +0 -260
  93. package/src/native-cli.ts +0 -414
  94. package/src/native-color.ts +0 -904
  95. package/src/native-estimation.ts +0 -194
  96. package/src/native-grid.ts +0 -590
  97. package/src/native-interaction.ts +0 -1289
  98. package/src/native-layout.ts +0 -568
  99. package/src/native-link.ts +0 -76
  100. package/src/native-render-support.ts +0 -361
  101. package/src/native-spacing.ts +0 -231
  102. package/src/native-state.ts +0 -318
  103. package/src/native-strings.ts +0 -46
  104. package/src/native-transform.ts +0 -120
  105. package/src/native-types.ts +0 -439
  106. package/src/native-typography.ts +0 -254
  107. package/src/native-units.ts +0 -441
  108. package/src/native-vector.ts +0 -910
  109. package/src/native.ts +0 -5606
  110. package/src/path.ts +0 -493
  111. package/src/pm-cli.ts +0 -2498
  112. package/src/preview-build.ts +0 -294
  113. package/src/render-context.ts +0 -138
  114. package/src/router.ts +0 -260
  115. package/src/runtime.ts +0 -97
  116. package/src/server.ts +0 -2294
  117. package/src/state.ts +0 -556
  118. package/src/style.ts +0 -1790
  119. package/src/test-globals.d.ts +0 -184
  120. package/src/test-reporter.ts +0 -609
  121. package/src/test-runtime.ts +0 -1359
  122. package/src/test.ts +0 -368
  123. package/src/types.ts +0 -381
  124. package/src/universal.ts +0 -81
  125. package/src/wapk-cli.ts +0 -3213
  126. package/src/workspace-package.ts +0 -102
  127. package/src/ws.ts +0 -648
  128. package/src/wss.ts +0 -241
@@ -1,1289 +0,0 @@
1
- import { type NativeStyleResolveOptions } from './style';
2
- import { getNativeStyleResolveOptions, parsePlainNumericValue } from './native-units';
3
- import { parseCssColor, toComposeColorLiteral } from './native-color';
4
- import { isExternalDestination, resolveNativeLinkHint } from './native-link';
5
- import { flattenTextContent, quoteKotlinString, quoteSwiftString } from './native-strings';
6
- import { prependComposeModifierCall } from './native-canvas';
7
- import { buildComposeTextStyleLiteralFromStyle } from './native-typography';
8
- import type {
9
- NativeBindingReference,
10
- NativeControlEventExpressionOptions,
11
- NativeElementNode,
12
- NativeNode,
13
- NativePickerOption,
14
- NativePropValue,
15
- } from './native-types';
16
-
17
- const NATIVE_PATTERN_MAX_LENGTH = 500;
18
- const REDOS_NESTED_QUANTIFIER = /\([^()]*[+*][^()]*\)[+*]/;
19
- const IMAGE_FALLBACK_STOP_WORDS = new Set([
20
- 'image',
21
- 'icon',
22
- 'public',
23
- 'assets',
24
- 'asset',
25
- 'favicon',
26
- 'svg',
27
- 'png',
28
- 'jpg',
29
- 'jpeg',
30
- 'webp',
31
- ]);
32
-
33
- function resolveNativeInputTypeValue(sourceTag: string, props: Record<string, NativePropValue>): string | undefined {
34
- if (sourceTag === 'textarea') {
35
- return 'textarea';
36
- }
37
-
38
- if (sourceTag !== 'input') {
39
- return undefined;
40
- }
41
-
42
- return typeof props.type === 'string' && props.type.trim()
43
- ? props.type.trim().toLowerCase()
44
- : 'text';
45
- }
46
-
47
- export function isCheckboxInput(sourceTag: string, props: Record<string, NativePropValue>): boolean {
48
- return resolveNativeInputTypeValue(sourceTag, props) === 'checkbox';
49
- }
50
-
51
- export function isRangeInput(sourceTag: string, props: Record<string, NativePropValue>): boolean {
52
- return resolveNativeInputTypeValue(sourceTag, props) === 'range';
53
- }
54
-
55
- export function toNativeBoolean(value: NativePropValue | undefined): boolean {
56
- if (typeof value === 'boolean') return value;
57
- if (typeof value === 'number') return value !== 0;
58
- if (typeof value === 'string') {
59
- const normalized = value.trim().toLowerCase();
60
- return normalized === 'true' || normalized === '1' || normalized === 'on' || normalized === 'yes';
61
- }
62
- return false;
63
- }
64
-
65
- export function buildComposeButtonModifier(
66
- modifier: string,
67
- onClickExpression?: string,
68
- enabled = true,
69
- interactionSourceName?: string,
70
- ): string {
71
- if (!onClickExpression || !enabled) {
72
- return modifier;
73
- }
74
-
75
- return prependComposeModifierCall(
76
- modifier,
77
- interactionSourceName
78
- ? `clickable(interactionSource = ${interactionSourceName}, indication = LocalIndication.current) { ${onClickExpression} }`
79
- : `clickable { ${onClickExpression} }`,
80
- );
81
- }
82
-
83
- export function buildComposeTextInputArgsFromStyle(
84
- node: NativeElementNode,
85
- style: Record<string, NativePropValue> | undefined,
86
- submitActionExpression?: string,
87
- styleResolveOptions: NativeStyleResolveOptions = getNativeStyleResolveOptions('generic'),
88
- ): string[] {
89
- const args: string[] = [];
90
- if (style) {
91
- const textStyle = buildComposeTextStyleLiteralFromStyle(style, styleResolveOptions);
92
- if (textStyle) {
93
- args.push(`textStyle = ${textStyle}`);
94
- }
95
-
96
- const color = parseCssColor(style.color);
97
- if (color) {
98
- args.push(`cursorBrush = SolidColor(${toComposeColorLiteral(color)})`);
99
- }
100
- }
101
-
102
- if (isNativeDisabled(node)) {
103
- args.push('enabled = false');
104
- }
105
-
106
- if (isNativeReadOnly(node)) {
107
- args.push('readOnly = true');
108
- }
109
-
110
- const keyboardType = resolveComposeKeyboardType(node);
111
- if (keyboardType || submitActionExpression) {
112
- const keyboardArgs: string[] = [];
113
- if (keyboardType) {
114
- keyboardArgs.push(`keyboardType = ${keyboardType}`);
115
- }
116
- if (submitActionExpression) {
117
- keyboardArgs.push('imeAction = androidx.compose.ui.text.input.ImeAction.Done');
118
- }
119
- args.push(`keyboardOptions = androidx.compose.foundation.text.KeyboardOptions(${keyboardArgs.join(', ')})`);
120
- }
121
-
122
- if (submitActionExpression) {
123
- args.push(`keyboardActions = androidx.compose.foundation.text.KeyboardActions(onDone = { ${submitActionExpression} })`);
124
- }
125
-
126
- if (resolveNativeTextInputType(node) === 'password') {
127
- args.push('visualTransformation = androidx.compose.ui.text.input.PasswordVisualTransformation()');
128
- }
129
-
130
- args.push(`singleLine = ${node.sourceTag === 'textarea' ? 'false' : 'true'}`);
131
- if (node.sourceTag === 'textarea') {
132
- args.push('minLines = 4');
133
- }
134
-
135
- return args;
136
- }
137
-
138
- export function buildSwiftUIButtonModifiersFromStyle(
139
- node: NativeElementNode,
140
- modifiers: string[],
141
- style: Record<string, NativePropValue> | undefined,
142
- ): string[] {
143
- const interactiveModifiers = [
144
- ...(node.sourceTag === 'button' && isNativeDisabled(node) ? ['.disabled(true)'] : []),
145
- ...modifiers,
146
- ];
147
- return style ? ['.buttonStyle(.plain)', ...interactiveModifiers] : interactiveModifiers;
148
- }
149
-
150
- export function isNativeDisabled(node: NativeElementNode): boolean {
151
- return toNativeBoolean(node.props.disabled) || toNativeBoolean(node.props['aria-disabled']);
152
- }
153
-
154
- export function isNativeFormControl(node: NativeElementNode): boolean {
155
- return node.component === 'TextInput' || node.component === 'Toggle' || node.component === 'Picker' || node.component === 'Slider';
156
- }
157
-
158
- export function isNativeEnabled(node: NativeElementNode): boolean {
159
- return (node.component === 'Button' || isNativeFormControl(node)) && !isNativeDisabled(node);
160
- }
161
-
162
- export function isNativeChecked(node: NativeElementNode): boolean {
163
- return toNativeBoolean(node.props.checked) || toNativeBoolean(node.props['aria-checked']);
164
- }
165
-
166
- export function isNativeSelected(node: NativeElementNode): boolean {
167
- return toNativeBoolean(node.props.selected)
168
- || toNativeBoolean(node.props['aria-selected'])
169
- || typeof node.props['aria-current'] === 'string';
170
- }
171
-
172
- export function hasNativePressedAccessibilityState(node: NativeElementNode): boolean {
173
- return node.props['aria-pressed'] !== undefined;
174
- }
175
-
176
- export function isNativePressed(node: NativeElementNode): boolean {
177
- return toNativeBoolean(node.props['aria-pressed'])
178
- || toNativeBoolean(node.props.pressed)
179
- || toNativeBoolean(node.props.active);
180
- }
181
-
182
- export function isNativeActive(node: NativeElementNode): boolean {
183
- return !isNativeDisabled(node) && isNativePressed(node);
184
- }
185
-
186
- export function isNativeRequired(node: NativeElementNode): boolean {
187
- return toNativeBoolean(node.props.required) || toNativeBoolean(node.props['aria-required']);
188
- }
189
-
190
- export function isNativeMultiple(node: NativeElementNode): boolean {
191
- return node.component === 'Picker' && toNativeBoolean(node.props.multiple);
192
- }
193
-
194
- function canNativeParticipateInValidation(node: NativeElementNode): boolean {
195
- return isNativeFormControl(node) && !isNativeDisabled(node);
196
- }
197
-
198
- export function resolveNativeTextInputValue(node: NativeElementNode): string {
199
- return typeof node.props.value === 'string' || typeof node.props.value === 'number'
200
- ? String(node.props.value)
201
- : '';
202
- }
203
-
204
- function parseNativeNonNegativeIntegerConstraint(value: NativePropValue | undefined): number | undefined {
205
- const parsed = parsePlainNumericValue(value);
206
- return parsed !== undefined && Number.isInteger(parsed) && parsed >= 0
207
- ? parsed
208
- : undefined;
209
- }
210
-
211
- function resolveNativeTextInputMinLength(node: NativeElementNode): number | undefined {
212
- return parseNativeNonNegativeIntegerConstraint(node.props.minLength ?? node.props.minlength);
213
- }
214
-
215
- function resolveNativeTextInputMaxLength(node: NativeElementNode): number | undefined {
216
- return parseNativeNonNegativeIntegerConstraint(node.props.maxLength ?? node.props.maxlength);
217
- }
218
-
219
- function resolveNativePatternExpression(node: NativeElementNode): RegExp | undefined {
220
- if (node.component !== 'TextInput' || typeof node.props.pattern !== 'string' || !node.props.pattern.trim()) {
221
- return undefined;
222
- }
223
-
224
- const pattern = node.props.pattern.trim();
225
- if (pattern.length > NATIVE_PATTERN_MAX_LENGTH || REDOS_NESTED_QUANTIFIER.test(pattern)) {
226
- return undefined;
227
- }
228
-
229
- try {
230
- return new RegExp(`^(?:${pattern})$`);
231
- } catch {
232
- return undefined;
233
- }
234
- }
235
-
236
- function resolveNativeNumericConstraint(value: NativePropValue | undefined): number | undefined {
237
- return parsePlainNumericValue(value);
238
- }
239
-
240
- export function resolveNativeStepConstraint(node: NativeElementNode): number | undefined {
241
- if (node.props.step === undefined) {
242
- return undefined;
243
- }
244
-
245
- if (typeof node.props.step === 'string' && node.props.step.trim().toLowerCase() === 'any') {
246
- return undefined;
247
- }
248
-
249
- const parsed = resolveNativeNumericConstraint(node.props.step);
250
- return parsed !== undefined && parsed > 0 ? parsed : undefined;
251
- }
252
-
253
- export function resolveNativeTextInputType(node: NativeElementNode): 'text' | 'password' | 'email' | 'number' | 'tel' | 'url' | 'search' | 'textarea' {
254
- const inputType = resolveNativeInputTypeValue(node.sourceTag, node.props);
255
-
256
- switch (inputType) {
257
- case 'password':
258
- case 'email':
259
- case 'number':
260
- case 'tel':
261
- case 'url':
262
- case 'search':
263
- return inputType;
264
- default:
265
- return 'text';
266
- }
267
- }
268
-
269
- function supportsNativePatternValidation(node: NativeElementNode): boolean {
270
- switch (resolveNativeTextInputType(node)) {
271
- case 'text':
272
- case 'password':
273
- case 'email':
274
- case 'tel':
275
- case 'url':
276
- case 'search':
277
- return true;
278
- default:
279
- return false;
280
- }
281
- }
282
-
283
- function isNativeEmailValue(value: string): boolean {
284
- return /^[^\s@]+@[^\s@.]+(?:\.[^\s@.]+)+$/.test(value);
285
- }
286
-
287
- function isNativeUrlValue(value: string): boolean {
288
- try {
289
- const parsed = new URL(value);
290
- return Boolean(parsed.protocol && parsed.hostname);
291
- } catch {
292
- return false;
293
- }
294
- }
295
-
296
- export function hasNativeValidationConstraint(node: NativeElementNode): boolean {
297
- if (node.props['aria-invalid'] !== undefined) {
298
- return true;
299
- }
300
-
301
- if (node.component === 'TextInput') {
302
- return isNativeRequired(node)
303
- || resolveNativeTextInputMinLength(node) !== undefined
304
- || resolveNativeTextInputMaxLength(node) !== undefined
305
- || resolveNativePatternExpression(node) !== undefined
306
- || resolveNativeNumericConstraint(node.props.min) !== undefined
307
- || resolveNativeNumericConstraint(node.props.max) !== undefined
308
- || resolveNativeStepConstraint(node) !== undefined
309
- || resolveNativeTextInputType(node) === 'email'
310
- || resolveNativeTextInputType(node) === 'url'
311
- || resolveNativeTextInputType(node) === 'number';
312
- }
313
-
314
- return isNativeRequired(node);
315
- }
316
-
317
- function isNativeTextInputConstraintInvalid(node: NativeElementNode): boolean {
318
- const value = resolveNativeTextInputValue(node);
319
- const trimmedValue = value.trim();
320
-
321
- if (isNativeRequired(node) && trimmedValue.length === 0) {
322
- return true;
323
- }
324
-
325
- if (trimmedValue.length === 0) {
326
- return false;
327
- }
328
-
329
- const inputType = resolveNativeTextInputType(node);
330
- if (inputType === 'email' && !isNativeEmailValue(trimmedValue)) {
331
- return true;
332
- }
333
-
334
- if (inputType === 'url' && !isNativeUrlValue(trimmedValue)) {
335
- return true;
336
- }
337
-
338
- const minLength = resolveNativeTextInputMinLength(node);
339
- if (minLength !== undefined && value.length < minLength) {
340
- return true;
341
- }
342
-
343
- const maxLength = resolveNativeTextInputMaxLength(node);
344
- if (maxLength !== undefined && value.length > maxLength) {
345
- return true;
346
- }
347
-
348
- const patternExpression = supportsNativePatternValidation(node)
349
- ? resolveNativePatternExpression(node)
350
- : undefined;
351
- if (patternExpression && !patternExpression.test(value)) {
352
- return true;
353
- }
354
-
355
- if (inputType === 'number') {
356
- const numericValue = Number(trimmedValue);
357
- if (!Number.isFinite(numericValue)) {
358
- return true;
359
- }
360
-
361
- const min = resolveNativeNumericConstraint(node.props.min);
362
- if (min !== undefined && numericValue < min) {
363
- return true;
364
- }
365
-
366
- const max = resolveNativeNumericConstraint(node.props.max);
367
- if (max !== undefined && numericValue > max) {
368
- return true;
369
- }
370
-
371
- const step = resolveNativeStepConstraint(node);
372
- if (step !== undefined) {
373
- const stepBase = resolveNativeNumericConstraint(node.props.min) ?? 0;
374
- const steps = (numericValue - stepBase) / step;
375
- if (Math.abs(steps - Math.round(steps)) > 1e-7) {
376
- return true;
377
- }
378
- }
379
- }
380
-
381
- return false;
382
- }
383
-
384
- export function isNativePlaceholderShown(node: NativeElementNode): boolean {
385
- return node.component === 'TextInput'
386
- && typeof node.props.placeholder === 'string'
387
- && node.props.placeholder.length > 0
388
- && resolveNativeTextInputValue(node).length === 0;
389
- }
390
-
391
- export function isNativeReadOnlyState(node: NativeElementNode): boolean {
392
- return node.component === 'TextInput' && (isNativeReadOnly(node) || isNativeDisabled(node));
393
- }
394
-
395
- export function isNativeReadWrite(node: NativeElementNode): boolean {
396
- return node.component === 'TextInput' && !isNativeReadOnlyState(node);
397
- }
398
-
399
- export function isNativeElementEmpty(node: NativeElementNode): boolean {
400
- return node.children.every((child) => child.kind === 'text' && child.value.length === 0);
401
- }
402
-
403
- export function isNativeFocusWithin(node: NativeElementNode): boolean {
404
- if (isNativePseudoFocused(node)) {
405
- return true;
406
- }
407
-
408
- return node.children.some((child) => child.kind === 'element' && isNativeFocusWithin(child));
409
- }
410
-
411
- function isNativeAriaInvalid(node: NativeElementNode): boolean {
412
- const value = node.props['aria-invalid'];
413
- if (value === undefined || value === null || value === false) {
414
- return false;
415
- }
416
-
417
- if (typeof value === 'string') {
418
- const normalized = value.trim().toLowerCase();
419
- return normalized.length > 0 && normalized !== 'false';
420
- }
421
-
422
- return toNativeBoolean(value);
423
- }
424
-
425
- export function isNativeInvalid(node: NativeElementNode): boolean {
426
- if (isNativeAriaInvalid(node)) {
427
- return true;
428
- }
429
-
430
- if (!isNativeFormControl(node)) {
431
- return false;
432
- }
433
-
434
- if (!canNativeParticipateInValidation(node)) {
435
- return false;
436
- }
437
-
438
- if (node.component === 'TextInput') {
439
- return isNativeTextInputConstraintInvalid(node);
440
- }
441
-
442
- if (!isNativeRequired(node)) {
443
- return false;
444
- }
445
-
446
- if (node.component === 'Toggle') {
447
- return !isNativeChecked(node);
448
- }
449
-
450
- if (node.component === 'Picker') {
451
- const options = resolveNativePickerOptions(node);
452
- return isNativeMultiple(node)
453
- ? resolveNativePickerInitialSelections(node, options).length === 0
454
- : resolveNativePickerInitialSelection(node, options).trim().length === 0;
455
- }
456
-
457
- return false;
458
- }
459
-
460
- export function isNativeValid(node: NativeElementNode): boolean {
461
- return canNativeParticipateInValidation(node) && !isNativeInvalid(node);
462
- }
463
-
464
- export function isNativeOptional(node: NativeElementNode): boolean {
465
- return isNativeFormControl(node) && !isNativeRequired(node);
466
- }
467
-
468
- export function isNativeReadOnly(node: NativeElementNode): boolean {
469
- return toNativeBoolean(node.props.readOnly) || toNativeBoolean(node.props.readonly);
470
- }
471
-
472
- function parseNativeTabIndex(node: NativeElementNode): number | undefined {
473
- const rawValue = node.props.tabIndex ?? node.props.tabindex;
474
- if (typeof rawValue === 'number' && Number.isInteger(rawValue)) {
475
- return rawValue;
476
- }
477
-
478
- if (typeof rawValue === 'string' && /^-?\d+$/.test(rawValue.trim())) {
479
- return Number(rawValue.trim());
480
- }
481
-
482
- return undefined;
483
- }
484
-
485
- function hasNativeExplicitFocusSignal(node: NativeElementNode): boolean {
486
- return toNativeBoolean(node.props.autoFocus)
487
- || toNativeBoolean(node.props.autofocus)
488
- || toNativeBoolean(node.props.focused)
489
- || toNativeBoolean(node.props['aria-focused']);
490
- }
491
-
492
- function isNativeFocusableRole(node: NativeElementNode): boolean {
493
- const role = typeof node.props.role === 'string'
494
- ? node.props.role.trim().toLowerCase()
495
- : undefined;
496
-
497
- return role === 'button'
498
- || role === 'link'
499
- || role === 'checkbox'
500
- || role === 'switch'
501
- || role === 'tab'
502
- || role === 'textbox'
503
- || role === 'combobox';
504
- }
505
-
506
- function isNativeFocusableElement(node: NativeElementNode): boolean {
507
- if (isNativeDisabled(node)) {
508
- return false;
509
- }
510
-
511
- const tabIndex = parseNativeTabIndex(node);
512
- if (tabIndex !== undefined) {
513
- return tabIndex >= 0;
514
- }
515
-
516
- if (toNativeBoolean(node.props.contentEditable) || toNativeBoolean(node.props.contenteditable)) {
517
- return true;
518
- }
519
-
520
- if (node.component === 'TextInput' || node.component === 'Button' || node.component === 'Link' || node.component === 'Toggle' || node.component === 'Picker' || node.component === 'Slider') {
521
- return true;
522
- }
523
-
524
- return isNativeFocusableRole(node);
525
- }
526
-
527
- export function isNativePseudoFocused(node: NativeElementNode): boolean {
528
- return hasNativeExplicitFocusSignal(node) && isNativeFocusableElement(node);
529
- }
530
-
531
- export function shouldNativeAutoFocus(node: NativeElementNode): boolean {
532
- return node.component === 'TextInput' && hasNativeExplicitFocusSignal(node) && !isNativeDisabled(node);
533
- }
534
-
535
- export function isNativeMuted(node: NativeElementNode): boolean {
536
- return toNativeBoolean(node.props.muted);
537
- }
538
-
539
- export function shouldNativeShowVideoControls(node: NativeElementNode): boolean {
540
- return node.sourceTag === 'video' && toNativeBoolean(node.props.controls);
541
- }
542
-
543
- export function resolveNativeVideoPoster(node: NativeElementNode): string | undefined {
544
- return node.sourceTag === 'video' && typeof node.props.poster === 'string' && node.props.poster.trim()
545
- ? node.props.poster.trim()
546
- : undefined;
547
- }
548
-
549
- export function shouldNativePlayInline(node: NativeElementNode): boolean {
550
- return node.sourceTag === 'video' && (
551
- toNativeBoolean(node.props.playsInline)
552
- || toNativeBoolean(node.props.playsinline)
553
- );
554
- }
555
-
556
- export function resolveNativeExplicitAccessibilityLabel(node: NativeElementNode): string | undefined {
557
- const explicitLabel = typeof node.props['aria-label'] === 'string' && node.props['aria-label'].trim()
558
- ? node.props['aria-label'].trim()
559
- : typeof node.props.title === 'string' && node.props.title.trim()
560
- ? node.props.title.trim()
561
- : undefined;
562
-
563
- if (explicitLabel) {
564
- return explicitLabel;
565
- }
566
-
567
- if (typeof node.props.alt === 'string' && node.props.alt.trim()) {
568
- return node.props.alt.trim();
569
- }
570
-
571
- return undefined;
572
- }
573
-
574
- export function resolveNativeMediaLabel(node: NativeElementNode): string {
575
- const explicitLabel = typeof node.props['aria-label'] === 'string' && node.props['aria-label'].trim()
576
- ? node.props['aria-label'].trim()
577
- : typeof node.props.title === 'string' && node.props.title.trim()
578
- ? node.props.title.trim()
579
- : undefined;
580
-
581
- if (explicitLabel) {
582
- return explicitLabel;
583
- }
584
-
585
- const textContent = flattenTextContent(node.children).trim();
586
- if (textContent) {
587
- return textContent;
588
- }
589
-
590
- return node.sourceTag === 'audio' ? 'Audio' : 'Video';
591
- }
592
-
593
- function tokenizeImageFallbackWords(value: string): string[] {
594
- return value
595
- .split(/[^a-zA-Z0-9]+/)
596
- .map((token) => token.trim())
597
- .filter((token) => token.length > 0)
598
- .filter((token) => !/^\d+$/.test(token))
599
- .filter((token) => !IMAGE_FALLBACK_STOP_WORDS.has(token.toLowerCase()));
600
- }
601
-
602
- export function resolveImageFallbackLabel(source: string, alt?: string): string {
603
- const altTokens = alt ? tokenizeImageFallbackWords(alt) : [];
604
- const sourceTokens = tokenizeImageFallbackWords(source.replace(/\.[a-z0-9]+$/i, ''));
605
- const tokens = altTokens.length > 0 ? altTokens : sourceTokens;
606
-
607
- if (tokens.length === 0) {
608
- return 'IMG';
609
- }
610
-
611
- if (tokens.length === 1) {
612
- return tokens[0]!.slice(0, 2).toUpperCase();
613
- }
614
-
615
- const initials = tokens
616
- .slice(0, 2)
617
- .map((token) => token[0]!.toUpperCase())
618
- .join('');
619
-
620
- return initials || 'IMG';
621
- }
622
-
623
- export function resolveNativeAccessibilityLabel(node: NativeElementNode): string | undefined {
624
- const explicitLabel = resolveNativeExplicitAccessibilityLabel(node);
625
-
626
- if (explicitLabel) {
627
- return explicitLabel;
628
- }
629
-
630
- if (node.component === 'Picker') {
631
- if (typeof node.props.placeholder === 'string' && node.props.placeholder.trim()) {
632
- return node.props.placeholder.trim();
633
- }
634
-
635
- return isNativeMultiple(node) ? 'Selection list' : 'Select';
636
- }
637
-
638
- const textContent = flattenTextContent(node.children).trim();
639
- if (textContent) {
640
- return textContent;
641
- }
642
-
643
- if (typeof node.props.placeholder === 'string' && node.props.placeholder.trim()) {
644
- return node.props.placeholder.trim();
645
- }
646
-
647
- if (node.component === 'Media') {
648
- return resolveNativeMediaLabel(node);
649
- }
650
-
651
- if (node.component === 'WebView') {
652
- return 'Web content';
653
- }
654
-
655
- return undefined;
656
- }
657
-
658
- export function resolveNativeAccessibilityHint(node: NativeElementNode): string | undefined {
659
- const parts: string[] = [];
660
-
661
- if (typeof node.props['aria-description'] === 'string' && node.props['aria-description'].trim()) {
662
- parts.push(node.props['aria-description'].trim());
663
- }
664
-
665
- const linkHint = resolveNativeLinkHint(node);
666
- if (linkHint) {
667
- parts.push(linkHint);
668
- }
669
-
670
- return parts.length > 0 ? parts.join(', ') : undefined;
671
- }
672
-
673
- export function resolveNativeAccessibilityRole(node: NativeElementNode): 'button' | 'link' | 'checkbox' | 'switch' | 'tab' | 'image' | 'heading' | undefined {
674
- const explicitRole = typeof node.props.role === 'string'
675
- ? node.props.role.trim().toLowerCase()
676
- : undefined;
677
-
678
- switch (explicitRole) {
679
- case 'button':
680
- case 'link':
681
- case 'checkbox':
682
- case 'switch':
683
- case 'tab':
684
- case 'image':
685
- case 'heading':
686
- return explicitRole;
687
- case 'img':
688
- return 'image';
689
- default:
690
- break;
691
- }
692
-
693
- return undefined;
694
- }
695
-
696
- export function hasExplicitNativeAccessibilitySignal(node: NativeElementNode): boolean {
697
- return Boolean(
698
- resolveNativeExplicitAccessibilityLabel(node)
699
- || resolveNativeAccessibilityHint(node)
700
- || resolveNativeLinkHint(node)
701
- || (typeof node.props.role === 'string' && node.props.role.trim())
702
- || node.props['aria-selected'] !== undefined
703
- || node.props['aria-checked'] !== undefined
704
- || node.props['aria-pressed'] !== undefined
705
- || node.props['aria-disabled'] !== undefined
706
- || node.props['aria-expanded'] !== undefined
707
- || node.props['aria-invalid'] !== undefined
708
- || node.props['aria-current'] !== undefined
709
- || node.props['aria-valuetext'] !== undefined
710
- || node.props['aria-required'] !== undefined
711
- || toNativeBoolean(node.props.required)
712
- || isNativeMultiple(node)
713
- );
714
- }
715
-
716
- export function shouldEmitNativeAccessibilityLabel(node: NativeElementNode): boolean {
717
- return hasExplicitNativeAccessibilitySignal(node);
718
- }
719
-
720
- export function resolveNativeAccessibilityStateParts(node: NativeElementNode): string[] {
721
- const parts: string[] = [];
722
- const role = resolveNativeAccessibilityRole(node);
723
- const hasSelectedState = node.props['aria-selected'] !== undefined || typeof node.props['aria-current'] === 'string' || role === 'tab';
724
- const hasCheckedState = node.component === 'Toggle' || node.props['aria-checked'] !== undefined || role === 'checkbox' || role === 'switch';
725
- const hasPressedState = hasNativePressedAccessibilityState(node);
726
-
727
- if (isNativeRequired(node)) {
728
- parts.push('Required');
729
- }
730
-
731
- if (isNativeInvalid(node)) {
732
- parts.push('Invalid');
733
- }
734
-
735
- if (!isNativeInvalid(node) && hasNativeValidationConstraint(node) && isNativeValid(node)) {
736
- parts.push('Valid');
737
- }
738
-
739
- if (isNativeDisabled(node)) {
740
- parts.push('Disabled');
741
- }
742
-
743
- if (hasSelectedState) {
744
- parts.push(isNativeSelected(node) ? 'Selected' : 'Not selected');
745
- }
746
-
747
- if (hasCheckedState) {
748
- parts.push(isNativeChecked(node) ? 'Checked' : 'Unchecked');
749
- }
750
-
751
- if (hasPressedState) {
752
- parts.push(isNativePressed(node) ? 'Pressed' : 'Not pressed');
753
- }
754
-
755
- if (node.props['aria-expanded'] !== undefined) {
756
- parts.push(toNativeBoolean(node.props['aria-expanded']) ? 'Expanded' : 'Collapsed');
757
- }
758
-
759
- if (typeof node.props['aria-valuetext'] === 'string' && node.props['aria-valuetext'].trim()) {
760
- parts.push(node.props['aria-valuetext'].trim());
761
- }
762
-
763
- return [...new Set(parts)];
764
- }
765
-
766
- function resolveComposeAccessibilityRoleExpression(node: NativeElementNode): string | undefined {
767
- switch (resolveNativeAccessibilityRole(node)) {
768
- case 'button':
769
- case 'link':
770
- return 'Role.Button';
771
- case 'checkbox':
772
- return 'Role.Checkbox';
773
- case 'switch':
774
- return 'Role.Switch';
775
- case 'tab':
776
- return 'Role.Tab';
777
- case 'image':
778
- return 'Role.Image';
779
- default:
780
- return undefined;
781
- }
782
- }
783
-
784
- export function buildComposeAccessibilityModifier(node: NativeElementNode): string | undefined {
785
- if (!hasExplicitNativeAccessibilitySignal(node)) {
786
- return undefined;
787
- }
788
-
789
- const statements: string[] = [];
790
- const label = shouldEmitNativeAccessibilityLabel(node) ? resolveNativeAccessibilityLabel(node) : undefined;
791
- const hint = resolveNativeAccessibilityHint(node);
792
- const stateParts = resolveNativeAccessibilityStateParts(node);
793
- const stateDescription = [hint, ...stateParts].filter((value): value is string => Boolean(value)).join(', ');
794
- const roleExpression = resolveComposeAccessibilityRoleExpression(node);
795
- const role = resolveNativeAccessibilityRole(node);
796
-
797
- if (label) {
798
- statements.push(`contentDescription = ${quoteKotlinString(label)}`);
799
- }
800
-
801
- if (roleExpression) {
802
- statements.push(`role = ${roleExpression}`);
803
- }
804
-
805
- if ((node.props['aria-selected'] !== undefined || typeof node.props['aria-current'] === 'string' || role === 'tab') && isNativeSelected(node)) {
806
- statements.push('selected = true');
807
- }
808
-
809
- if (stateDescription) {
810
- statements.push(`stateDescription = ${quoteKotlinString(stateDescription)}`);
811
- }
812
-
813
- if (role === 'heading') {
814
- statements.push('heading()');
815
- }
816
-
817
- if (node.props['aria-disabled'] !== undefined) {
818
- statements.push('disabled()');
819
- }
820
-
821
- return statements.length > 0
822
- ? `semantics(mergeDescendants = true) { ${statements.join('; ')} }`
823
- : undefined;
824
- }
825
-
826
- export function buildSwiftAccessibilityModifiers(node: NativeElementNode): string[] {
827
- if (!hasExplicitNativeAccessibilitySignal(node)) {
828
- return [];
829
- }
830
-
831
- const modifiers: string[] = [];
832
- const label = shouldEmitNativeAccessibilityLabel(node) ? resolveNativeAccessibilityLabel(node) : undefined;
833
- const hint = resolveNativeAccessibilityHint(node);
834
- const value = resolveNativeAccessibilityStateParts(node).join(', ');
835
- const role = resolveNativeAccessibilityRole(node);
836
-
837
- if (label) {
838
- modifiers.push(`.accessibilityLabel(${quoteSwiftString(label)})`);
839
- }
840
-
841
- if (hint) {
842
- modifiers.push(`.accessibilityHint(${quoteSwiftString(hint)})`);
843
- }
844
-
845
- if (value) {
846
- modifiers.push(`.accessibilityValue(${quoteSwiftString(value)})`);
847
- }
848
-
849
- if (role === 'button') {
850
- modifiers.push('.accessibilityAddTraits(.isButton)');
851
- } else if (role === 'link') {
852
- modifiers.push('.accessibilityAddTraits(.isLink)');
853
- } else if (role === 'image') {
854
- modifiers.push('.accessibilityAddTraits(.isImage)');
855
- } else if (role === 'heading') {
856
- modifiers.push('.accessibilityAddTraits(.isHeader)');
857
- }
858
-
859
- if ((node.props['aria-selected'] !== undefined || typeof node.props['aria-current'] === 'string' || role === 'tab') && isNativeSelected(node)) {
860
- modifiers.push('.accessibilityAddTraits(.isSelected)');
861
- }
862
-
863
- return modifiers;
864
- }
865
-
866
- export function resolveNativeRangeMin(node: NativeElementNode): number {
867
- return resolveNativeNumericConstraint(node.props.min) ?? 0;
868
- }
869
-
870
- export function resolveNativeRangeMax(node: NativeElementNode): number {
871
- const min = resolveNativeRangeMin(node);
872
- const max = resolveNativeNumericConstraint(node.props.max);
873
- return max !== undefined && max > min ? max : min + 100;
874
- }
875
-
876
- export function resolveNativeRangeInitialValue(node: NativeElementNode): number {
877
- const min = resolveNativeRangeMin(node);
878
- const max = resolveNativeRangeMax(node);
879
- const value = resolveNativeNumericConstraint(node.props.value);
880
- const candidate = value !== undefined ? value : min;
881
- return Math.min(max, Math.max(min, candidate));
882
- }
883
-
884
- export function resolveComposeSliderSteps(node: NativeElementNode): number | undefined {
885
- const step = resolveNativeStepConstraint(node);
886
- if (step === undefined) {
887
- return undefined;
888
- }
889
-
890
- const intervals = (resolveNativeRangeMax(node) - resolveNativeRangeMin(node)) / step;
891
- if (!Number.isFinite(intervals)) {
892
- return undefined;
893
- }
894
-
895
- const roundedIntervals = Math.round(intervals);
896
- if (roundedIntervals < 1 || Math.abs(intervals - roundedIntervals) > 1e-7) {
897
- return undefined;
898
- }
899
-
900
- return Math.max(0, roundedIntervals - 1);
901
- }
902
-
903
- export function resolveComposeKeyboardType(node: NativeElementNode): string | undefined {
904
- switch (resolveNativeTextInputType(node)) {
905
- case 'email':
906
- return 'androidx.compose.ui.text.input.KeyboardType.Email';
907
- case 'number':
908
- return 'androidx.compose.ui.text.input.KeyboardType.Decimal';
909
- case 'password':
910
- return 'androidx.compose.ui.text.input.KeyboardType.Password';
911
- case 'tel':
912
- return 'androidx.compose.ui.text.input.KeyboardType.Phone';
913
- case 'url':
914
- return 'androidx.compose.ui.text.input.KeyboardType.Uri';
915
- case 'search':
916
- return 'androidx.compose.ui.text.input.KeyboardType.Text';
917
- default:
918
- return undefined;
919
- }
920
- }
921
-
922
- export function resolveSwiftKeyboardTypeModifier(node: NativeElementNode): string | undefined {
923
- switch (resolveNativeTextInputType(node)) {
924
- case 'email':
925
- return '.keyboardType(.emailAddress)';
926
- case 'number':
927
- return '.keyboardType(.decimalPad)';
928
- case 'tel':
929
- return '.keyboardType(.phonePad)';
930
- case 'url':
931
- return '.keyboardType(.URL)';
932
- case 'search':
933
- return '.keyboardType(.webSearch)';
934
- default:
935
- return undefined;
936
- }
937
- }
938
-
939
- export function shouldDisableNativeTextCapitalization(node: NativeElementNode): boolean {
940
- const inputType = resolveNativeTextInputType(node);
941
- return inputType === 'email' || inputType === 'password' || inputType === 'url';
942
- }
943
-
944
- export function serializeNativePayload(value: NativePropValue | undefined): string | undefined {
945
- if (value === undefined) return undefined;
946
- if (typeof value === 'string') return value;
947
- return JSON.stringify(value);
948
- }
949
-
950
- export function resolveNativeAction(node: NativeElementNode): string | undefined {
951
- return typeof node.props.nativeAction === 'string' && node.props.nativeAction.trim()
952
- ? node.props.nativeAction
953
- : undefined;
954
- }
955
-
956
- export function resolveNativeRoute(node: NativeElementNode): string | undefined {
957
- if (typeof node.props.nativeRoute === 'string' && node.props.nativeRoute.trim()) {
958
- return node.props.nativeRoute;
959
- }
960
-
961
- const destination = typeof node.props.destination === 'string' ? node.props.destination : undefined;
962
- if (destination && !isExternalDestination(destination)) {
963
- return destination;
964
- }
965
-
966
- return undefined;
967
- }
968
-
969
- export function buildComposeBridgeInvocation(action?: string, route?: string, payloadJson?: string): string | undefined {
970
- const args: string[] = [];
971
-
972
- if (action) args.push(`action = ${quoteKotlinString(action)}`);
973
- if (route) args.push(`route = ${quoteKotlinString(route)}`);
974
- if (payloadJson) args.push(`payloadJson = ${quoteKotlinString(payloadJson)}`);
975
-
976
- return args.length > 0 ? `ElitNativeBridge.dispatch(${args.join(', ')})` : undefined;
977
- }
978
-
979
- export function buildSwiftBridgeInvocation(action?: string, route?: string, payloadJson?: string): string | undefined {
980
- const args: string[] = [];
981
-
982
- if (action) args.push(`action: ${quoteSwiftString(action)}`);
983
- if (route) args.push(`route: ${quoteSwiftString(route)}`);
984
- if (payloadJson) args.push(`payloadJson: ${quoteSwiftString(payloadJson)}`);
985
-
986
- return args.length > 0 ? `ElitNativeBridge.dispatch(${args.join(', ')})` : undefined;
987
- }
988
-
989
- function resolveNativeControlEventInputType(node: NativeElementNode): string | undefined {
990
- if (node.component === 'Picker') {
991
- return isNativeMultiple(node) ? 'select-multiple' : 'select-one';
992
- }
993
-
994
- if (node.component === 'Toggle') {
995
- return typeof node.props.type === 'string' && node.props.type.trim()
996
- ? node.props.type.trim().toLowerCase()
997
- : 'checkbox';
998
- }
999
-
1000
- if (node.component === 'Slider') {
1001
- return 'range';
1002
- }
1003
-
1004
- return resolveNativeInputTypeValue(node.sourceTag, node.props);
1005
- }
1006
-
1007
- function shouldDispatchNativeControlEvent(node: NativeElementNode, eventName: 'input' | 'change' | 'submit'): boolean {
1008
- if (!node.events.includes(eventName)) {
1009
- return false;
1010
- }
1011
-
1012
- return !(eventName === 'input' && getNativeBindingReference(node) && node.events.every((candidate) => candidate === 'input'));
1013
- }
1014
-
1015
- function resolveNativeControlEventAction(node: NativeElementNode, eventName: 'input' | 'change' | 'submit'): string {
1016
- return resolveNativeAction(node) ?? `elit.event.${eventName}`;
1017
- }
1018
-
1019
- function buildComposeControlEventPayloadInvocation(
1020
- node: NativeElementNode,
1021
- eventName: 'input' | 'change' | 'submit',
1022
- options: NativeControlEventExpressionOptions = {},
1023
- ): string {
1024
- const args = [
1025
- `event = ${quoteKotlinString(eventName)}`,
1026
- `sourceTag = ${quoteKotlinString(node.sourceTag)}`,
1027
- ];
1028
- const inputType = resolveNativeControlEventInputType(node);
1029
- const detailJson = serializeNativePayload(node.props.nativePayload);
1030
-
1031
- if (inputType) {
1032
- args.push(`inputType = ${quoteKotlinString(inputType)}`);
1033
- }
1034
- if (options.valueExpression) {
1035
- args.push(`value = ${options.valueExpression}`);
1036
- }
1037
- if (options.valuesExpression) {
1038
- args.push(`values = ${options.valuesExpression}`);
1039
- }
1040
- if (options.checkedExpression) {
1041
- args.push(`checked = ${options.checkedExpression}`);
1042
- }
1043
- if (detailJson) {
1044
- args.push(`detailJson = ${quoteKotlinString(detailJson)}`);
1045
- }
1046
-
1047
- return `ElitNativeBridge.controlEventPayload(${args.join(', ')})`;
1048
- }
1049
-
1050
- export function buildComposeControlEventDispatchInvocation(
1051
- node: NativeElementNode,
1052
- eventName: 'input' | 'change' | 'submit',
1053
- options: NativeControlEventExpressionOptions = {},
1054
- ): string {
1055
- return `ElitNativeBridge.dispatch(action = ${quoteKotlinString(resolveNativeControlEventAction(node, eventName))}, payloadJson = ${buildComposeControlEventPayloadInvocation(node, eventName, options)})`;
1056
- }
1057
-
1058
- export function buildComposeControlEventDispatchStatements(
1059
- node: NativeElementNode,
1060
- options: NativeControlEventExpressionOptions = {},
1061
- ): string[] {
1062
- const statements: string[] = [];
1063
- if (shouldDispatchNativeControlEvent(node, 'input')) {
1064
- statements.push(buildComposeControlEventDispatchInvocation(node, 'input', options));
1065
- }
1066
- if (shouldDispatchNativeControlEvent(node, 'change')) {
1067
- statements.push(buildComposeControlEventDispatchInvocation(node, 'change', options));
1068
- }
1069
- return statements;
1070
- }
1071
-
1072
- function buildSwiftControlEventPayloadInvocation(
1073
- node: NativeElementNode,
1074
- eventName: 'input' | 'change' | 'submit',
1075
- options: NativeControlEventExpressionOptions = {},
1076
- ): string {
1077
- const args = [
1078
- `event: ${quoteSwiftString(eventName)}`,
1079
- `sourceTag: ${quoteSwiftString(node.sourceTag)}`,
1080
- ];
1081
- const inputType = resolveNativeControlEventInputType(node);
1082
- const detailJson = serializeNativePayload(node.props.nativePayload);
1083
-
1084
- if (inputType) {
1085
- args.push(`inputType: ${quoteSwiftString(inputType)}`);
1086
- }
1087
- if (options.valueExpression) {
1088
- args.push(`value: ${options.valueExpression}`);
1089
- }
1090
- if (options.valuesExpression) {
1091
- args.push(`values: ${options.valuesExpression}`);
1092
- }
1093
- if (options.checkedExpression) {
1094
- args.push(`checked: ${options.checkedExpression}`);
1095
- }
1096
- if (detailJson) {
1097
- args.push(`detailJson: ${quoteSwiftString(detailJson)}`);
1098
- }
1099
-
1100
- return `ElitNativeBridge.controlEventPayload(${args.join(', ')})`;
1101
- }
1102
-
1103
- export function buildSwiftControlEventDispatchInvocation(
1104
- node: NativeElementNode,
1105
- eventName: 'input' | 'change' | 'submit',
1106
- options: NativeControlEventExpressionOptions = {},
1107
- ): string {
1108
- return `ElitNativeBridge.dispatch(action: ${quoteSwiftString(resolveNativeControlEventAction(node, eventName))}, payloadJson: ${buildSwiftControlEventPayloadInvocation(node, eventName, options)})`;
1109
- }
1110
-
1111
- export function buildSwiftControlEventDispatchStatements(
1112
- node: NativeElementNode,
1113
- options: NativeControlEventExpressionOptions = {},
1114
- ): string[] {
1115
- const statements: string[] = [];
1116
- if (shouldDispatchNativeControlEvent(node, 'input')) {
1117
- statements.push(buildSwiftControlEventDispatchInvocation(node, 'input', options));
1118
- }
1119
- if (shouldDispatchNativeControlEvent(node, 'change')) {
1120
- statements.push(buildSwiftControlEventDispatchInvocation(node, 'change', options));
1121
- }
1122
- return statements;
1123
- }
1124
-
1125
- export function getNativeBindingReference(node: NativeElementNode): NativeBindingReference | undefined {
1126
- const binding = node.props.nativeBinding;
1127
- if (!binding || typeof binding !== 'object' || Array.isArray(binding)) {
1128
- return undefined;
1129
- }
1130
-
1131
- const id = typeof binding.id === 'string' ? binding.id : undefined;
1132
- const kind = binding.kind === 'value' || binding.kind === 'checked' ? binding.kind : undefined;
1133
- const valueType = binding.valueType === 'boolean' || binding.valueType === 'number' || binding.valueType === 'string' || binding.valueType === 'string-array'
1134
- ? binding.valueType
1135
- : undefined;
1136
-
1137
- if (!id || !kind || !valueType) {
1138
- return undefined;
1139
- }
1140
-
1141
- return { id, kind, valueType };
1142
- }
1143
-
1144
- function collectNativePickerOptionNodes(nodes: NativeNode[]): NativeElementNode[] {
1145
- const options: NativeElementNode[] = [];
1146
-
1147
- for (const node of nodes) {
1148
- if (node.kind !== 'element') {
1149
- continue;
1150
- }
1151
-
1152
- if (node.component === 'Option') {
1153
- options.push(node);
1154
- continue;
1155
- }
1156
-
1157
- if (node.sourceTag === 'optgroup') {
1158
- options.push(...collectNativePickerOptionNodes(node.children));
1159
- }
1160
- }
1161
-
1162
- return options;
1163
- }
1164
-
1165
- export function resolveNativePickerOptionLabel(node: NativeElementNode): string {
1166
- if (typeof node.props.label === 'string' && node.props.label.trim()) {
1167
- return node.props.label;
1168
- }
1169
-
1170
- const textContent = flattenTextContent(node.children).trim();
1171
- if (textContent) {
1172
- return textContent;
1173
- }
1174
-
1175
- if (typeof node.props.value === 'string' || typeof node.props.value === 'number' || typeof node.props.value === 'boolean') {
1176
- return String(node.props.value);
1177
- }
1178
-
1179
- return 'Option';
1180
- }
1181
-
1182
- function resolveNativePickerOptionValue(node: NativeElementNode): string {
1183
- if (typeof node.props.value === 'string' || typeof node.props.value === 'number' || typeof node.props.value === 'boolean') {
1184
- return String(node.props.value);
1185
- }
1186
-
1187
- return resolveNativePickerOptionLabel(node);
1188
- }
1189
-
1190
- export function resolveNativePickerOptions(node: NativeElementNode): NativePickerOption[] {
1191
- return collectNativePickerOptionNodes(node.children).map((optionNode) => ({
1192
- label: resolveNativePickerOptionLabel(optionNode),
1193
- value: resolveNativePickerOptionValue(optionNode),
1194
- selected: isNativeSelected(optionNode),
1195
- disabled: isNativeDisabled(optionNode),
1196
- }));
1197
- }
1198
-
1199
- export function resolveNativePickerInitialSelection(node: NativeElementNode, options: NativePickerOption[]): string {
1200
- if (isNativeMultiple(node)) {
1201
- return resolveNativePickerInitialSelections(node, options)[0] ?? '';
1202
- }
1203
-
1204
- const explicitValue = typeof node.props.value === 'string' || typeof node.props.value === 'number' || typeof node.props.value === 'boolean'
1205
- ? String(node.props.value)
1206
- : undefined;
1207
-
1208
- if (explicitValue && options.some((option) => option.value === explicitValue)) {
1209
- return explicitValue;
1210
- }
1211
-
1212
- const selectedOption = options.find((option) => option.selected);
1213
-
1214
- if (selectedOption) {
1215
- return selectedOption.value;
1216
- }
1217
-
1218
- if (isNativeRequired(node)) {
1219
- return '';
1220
- }
1221
-
1222
- return options[0]?.value ?? '';
1223
- }
1224
-
1225
- export function resolveNativePickerInitialSelections(node: NativeElementNode, options: NativePickerOption[]): string[] {
1226
- if (Array.isArray(node.props.value)) {
1227
- const explicitValues = node.props.value
1228
- .filter((value): value is string | number | boolean => typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean')
1229
- .map((value) => String(value));
1230
-
1231
- return explicitValues.filter((value, index) => explicitValues.indexOf(value) === index && options.some((option) => option.value === value));
1232
- }
1233
-
1234
- const explicitValue = typeof node.props.value === 'string' || typeof node.props.value === 'number' || typeof node.props.value === 'boolean'
1235
- ? String(node.props.value)
1236
- : undefined;
1237
-
1238
- if (explicitValue && options.some((option) => option.value === explicitValue)) {
1239
- return [explicitValue];
1240
- }
1241
-
1242
- return options
1243
- .filter((option) => option.selected)
1244
- .map((option) => option.value);
1245
- }
1246
-
1247
- export function resolveNativePickerDisplayLabel(value: string, options: NativePickerOption[]): string {
1248
- return options.find((option) => option.value === value)?.label ?? value;
1249
- }
1250
-
1251
- export function buildComposePickerLabelExpression(selectionExpression: string, options: NativePickerOption[], placeholder?: string): string {
1252
- const fallbackLabel = placeholder ? quoteKotlinString(placeholder) : undefined;
1253
-
1254
- if (options.length === 0 || options.every((option) => option.value === option.label)) {
1255
- return fallbackLabel
1256
- ? `if (${selectionExpression}.isEmpty()) ${fallbackLabel} else ${selectionExpression}`
1257
- : selectionExpression;
1258
- }
1259
-
1260
- const branches = options.map((option) => `${quoteKotlinString(option.value)} -> ${quoteKotlinString(option.label)}`).join('; ');
1261
- return fallbackLabel
1262
- ? `when (${selectionExpression}) { "" -> ${fallbackLabel}; ${branches}; else -> ${selectionExpression} }`
1263
- : `when (${selectionExpression}) { ${branches}; else -> ${selectionExpression} }`;
1264
- }
1265
-
1266
- export function resolveNativeProgressFraction(props: Record<string, NativePropValue>): number | undefined {
1267
- const value = parsePlainNumericValue(props.value);
1268
- if (value === undefined) {
1269
- return undefined;
1270
- }
1271
-
1272
- const max = parsePlainNumericValue(props.max);
1273
- const denominator = max !== undefined && max > 0 ? max : 1;
1274
- return Math.max(0, Math.min(1, value / denominator));
1275
- }
1276
-
1277
- export function resolveNativeSurfaceSource(node: NativeElementNode): string | undefined {
1278
- const source = typeof node.props.source === 'string' && node.props.source.trim()
1279
- ? node.props.source.trim()
1280
- : typeof node.props.src === 'string' && node.props.src.trim()
1281
- ? node.props.src.trim()
1282
- : typeof node.props.data === 'string' && node.props.data.trim()
1283
- ? node.props.data.trim()
1284
- : typeof node.props.destination === 'string' && node.props.destination.trim()
1285
- ? node.props.destination.trim()
1286
- : undefined;
1287
-
1288
- return source && source.length > 0 ? source : undefined;
1289
- }