@symbiote-native/engine 0.5.0 → 1.0.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 (91) hide show
  1. package/README.md +39 -14
  2. package/android/CMakeLists.txt +51 -0
  3. package/android/build.gradle +90 -0
  4. package/android/src/main/AndroidManifest.xml +1 -0
  5. package/android/src/main/cpp/SymbioteEngineJni.cpp +72 -0
  6. package/android/src/main/java/dev/symbiotenative/engine/SymbioteEngineModule.kt +43 -0
  7. package/android/src/main/java/dev/symbiotenative/engine/SymbioteEnginePackage.kt +35 -0
  8. package/build/accessibility-info/shared.js +1 -1
  9. package/build/accessibility-props.d.ts +1 -8
  10. package/build/accessibility-props.js +13 -16
  11. package/build/animated/animations/composition.d.ts +1 -1
  12. package/build/animated/animations/composition.js +18 -4
  13. package/build/animated/easing.d.ts +3 -2
  14. package/build/animated/easing.js +17 -88
  15. package/build/animated/event.js +6 -1
  16. package/build/animated/host-binding.d.ts +1 -1
  17. package/build/animated/host-binding.js +19 -4
  18. package/build/animated/index.d.ts +1 -1
  19. package/build/animated/mock.d.ts +1 -19
  20. package/build/animated/props.js +1 -1
  21. package/build/animated/rgba.js +16 -50
  22. package/build/events/index.js +88 -40
  23. package/build/fabric-props.d.ts +1 -1
  24. package/build/fabric-props.js +116 -184
  25. package/build/fabric.d.ts +9 -0
  26. package/build/fabric.js +32 -0
  27. package/build/host-access.d.ts +125 -0
  28. package/build/host-access.js +280 -0
  29. package/build/host-behavior.d.ts +84 -21
  30. package/build/host-behavior.js +196 -30
  31. package/build/image-source-write.d.ts +16 -0
  32. package/build/image-source-write.js +65 -0
  33. package/build/imperative.d.ts +49 -0
  34. package/build/imperative.js +258 -0
  35. package/build/index.d.ts +14 -7
  36. package/build/index.js +53 -10
  37. package/build/mutation-buffer.d.ts +222 -0
  38. package/build/mutation-buffer.js +491 -0
  39. package/build/native-engine.d.ts +182 -0
  40. package/build/native-engine.js +178 -0
  41. package/build/native-tree-host.d.ts +25 -0
  42. package/build/native-tree-host.js +66 -0
  43. package/build/node.d.ts +172 -57
  44. package/build/node.js +839 -383
  45. package/build/pan-responder/index.js +27 -52
  46. package/build/platform-color/index.d.ts +1 -1
  47. package/build/platform-color/index.js +11 -4
  48. package/build/process-background-image/index.js +30 -566
  49. package/build/process-background-longhands.d.ts +4 -0
  50. package/build/process-background-longhands.js +44 -0
  51. package/build/process-box-shadow/index.js +23 -187
  52. package/build/process-filter.js +27 -300
  53. package/build/process-transform/index.d.ts +1 -1
  54. package/build/process-transform/index.js +25 -107
  55. package/build/process-transform-origin/index.d.ts +1 -1
  56. package/build/process-transform-origin/index.js +29 -102
  57. package/build/registry.d.ts +36 -0
  58. package/build/registry.js +73 -0
  59. package/build/sound-manager/index.d.ts +3 -0
  60. package/build/sound-manager/index.js +36 -0
  61. package/build/structured-style.d.ts +10 -0
  62. package/build/structured-style.js +180 -0
  63. package/build/style-registry/index.d.ts +14 -0
  64. package/build/style-registry/index.js +60 -11
  65. package/build/surface.d.ts +31 -2
  66. package/build/surface.js +138 -56
  67. package/build/text-input-state.d.ts +1 -0
  68. package/build/text-input-state.js +17 -3
  69. package/build/tree-host.d.ts +307 -0
  70. package/build/tree-host.js +211 -0
  71. package/build/view-config.js +4 -4
  72. package/codegen-specs/NativeSymbioteEngine.ts +27 -0
  73. package/cpp/SymbioteDebug.cpp +51 -0
  74. package/cpp/SymbioteDebug.h +54 -0
  75. package/cpp/SymbioteEngineBindings.cpp +232 -0
  76. package/cpp/SymbioteEngineBindings.h +59 -0
  77. package/cpp/SymbioteFabricProps.cpp +2619 -0
  78. package/cpp/SymbioteFabricProps.h +223 -0
  79. package/cpp/SymbioteTree.cpp +2478 -0
  80. package/cpp/SymbioteTree.h +257 -0
  81. package/ios/SymbioteEngineModule.h +25 -0
  82. package/ios/SymbioteEngineModule.mm +44 -0
  83. package/package.json +31 -3
  84. package/react-native.config.cjs +23 -0
  85. package/symbiote-engine.podspec +42 -0
  86. package/build/animated/bezier.d.ts +0 -1
  87. package/build/animated/bezier.js +0 -102
  88. package/build/commit.d.ts +0 -49
  89. package/build/commit.js +0 -1058
  90. package/build/tags.d.ts +0 -2
  91. package/build/tags.js +0 -40
@@ -1,571 +1,35 @@
1
- // JS-side port of RN's processBackgroundImage (Libraries/StyleSheet/processBackgroundImage.js).
2
- // Same root cause as boxShadow/filter/transform: `experimental_backgroundImage` registers with
3
- // enableNativeCSSParsing(), which DEFAULTS TO FALSE, so RN's stock path parses the CSS gradient
4
- // string / structured array in JS and sends only the processed array to native - Fabric's C++
5
- // never sees the raw string. This restores that missing JS parse.
6
- import { isOpaqueColorValue, processColor } from '../platform-color/index.js';
1
+ // RN's own processBackgroundImage, imported rather than ported.
2
+ //
3
+ // Why it is needed at all: RN registers `experimental_backgroundImage` with
4
+ // `nativeCSSParsing ? true : {process: processBackgroundImage}`, and enableNativeCSSParsing()
5
+ // defaults to false - so the stock path parses the CSS string / structured array in JS and native
6
+ // never sees the raw string.
7
+ //
8
+ // Why the try/catch: upstream has no `throw` statement, but it reads `bgImage.colorStops.length`
9
+ // unguarded (processBackgroundImage.js:205), so a gradient object without colorStops raises a
10
+ // TypeError - and a commit path must never throw. Reachable rather than ceremonial: the input is
11
+ // a plain record from app code, and nothing upstream of here checks that field exists.
12
+ //
13
+ // What changed by importing, beyond deleting 600 lines:
14
+ //
15
+ // - Colour stops go through RN's own `processColor` (upstream imports it directly) instead of
16
+ // our injected `setColorProcessor` seam. On a real host those are the same function.
17
+ // - A half-specified radial position is now FORWARDED rather than replaced by centre. Our port
18
+ // required one vertical and one horizontal edge and fell back to centre otherwise; upstream
19
+ // takes any non-null position as-is (`:178`). Both type systems forbid the half-specified
20
+ // shape - ours in IRadialGradientPosition, Flow in RadialGradientPosition - so this is only
21
+ // reachable from untyped JS, and inheriting upstream's answer is the point of the exercise.
22
+ // The `dlog` our guard carried goes with it.
7
23
  import { dlog } from '../debug.js';
8
- import { isRecord } from '../type-guards.js';
9
- // RN processBackgroundImage.js: pre-compiled patterns.
10
- const NEWLINE_REGEX = /\n/g;
11
- const GRADIENT_REGEX = /^(linear|radial)-gradient\(((?:\([^)]*\)|[^()])*)\)/;
12
- const COMMA_SPLIT_REGEX = /,(?![^(]*\))/;
13
- const WHITESPACE_SPLIT_REGEX = /\s+/;
14
- const COLOR_STOP_PARTS_REGEX = /\S+\([^)]*\)|\S+/g;
15
- const WHITESPACE_NORMALIZE_REGEX = /\s+/g;
16
- const LINEAR_GRADIENT_DIRECTION_REGEX = /^to\s+(?:top|bottom|left|right)(?:\s+(?:top|bottom|left|right))?/i;
17
- const LINEAR_GRADIENT_ANGLE_UNIT_REGEX = /^([+-]?\d*\.?\d+)(deg|grad|rad|turn)$/i;
18
- const LINEAR_GRADIENT_DEFAULT_DIRECTION = {
19
- type: 'angle',
20
- value: 180,
21
- };
22
- const DEFAULT_RADIAL_SHAPE = 'ellipse';
23
- const DEFAULT_RADIAL_SIZE = 'farthest-corner';
24
- const DEFAULT_RADIAL_POSITION = {
25
- top: '50%',
26
- left: '50%',
27
- };
28
- function isStringOrNumber(value) {
29
- return typeof value === 'string' || typeof value === 'number';
30
- }
31
- // A color-stop position is a plain px number or a percentage string; anything else is invalid.
32
- function isPositionValue(value) {
33
- return (typeof value === 'number' ||
34
- (typeof value === 'string' && value.endsWith('%')));
35
- }
36
- // A gradient color may already be a platform int (array form) or a CSS string/PlatformColor
37
- // object needing processColor; mirrors process-box-shadow's processShadowColor.
38
- function processStopColor(color) {
39
- if (typeof color === 'number')
40
- return color;
41
- if (typeof color === 'string' || isOpaqueColorValue(color))
42
- return processColor(color);
43
- return null;
44
- }
45
- // RN processBackgroundImage.js's `getPositionFromCSSValue`: `px` resolves to a plain number,
46
- // `%` stays a string, anything else is unresolvable (returns undefined, like RN's fallthrough).
47
- function getPositionFromCSSValue(position) {
48
- if (position.endsWith('px'))
49
- return parseFloat(position);
50
- if (position.endsWith('%'))
51
- return position;
52
- return undefined;
53
- }
54
- function getAngleInDegrees(angle) {
55
- const match = angle.match(LINEAR_GRADIENT_ANGLE_UNIT_REGEX);
56
- if (!match || match[1] == null)
57
- return null;
58
- const value = parseFloat(match[1]);
59
- switch (match[2]) {
60
- case 'deg':
61
- return value;
62
- case 'grad':
63
- return value * 0.9;
64
- case 'rad':
65
- return (value * 180) / Math.PI;
66
- case 'turn':
67
- return value * 360;
68
- default:
69
- return null;
70
- }
71
- }
72
- function getDirectionForKeyword(direction) {
73
- const normalized = direction
74
- .replace(WHITESPACE_NORMALIZE_REGEX, ' ')
75
- .toLowerCase();
76
- switch (normalized) {
77
- case 'to top':
78
- return { type: 'angle', value: 0 };
79
- case 'to right':
80
- return { type: 'angle', value: 90 };
81
- case 'to bottom':
82
- return { type: 'angle', value: 180 };
83
- case 'to left':
84
- return { type: 'angle', value: 270 };
85
- case 'to top right':
86
- case 'to right top':
87
- return { type: 'keyword', value: 'to top right' };
88
- case 'to bottom right':
89
- case 'to right bottom':
90
- return { type: 'keyword', value: 'to bottom right' };
91
- case 'to top left':
92
- case 'to left top':
93
- return { type: 'keyword', value: 'to top left' };
94
- case 'to bottom left':
95
- case 'to left bottom':
96
- return { type: 'keyword', value: 'to bottom left' };
97
- default:
98
- return null;
99
- }
100
- }
101
- //#region array form (structured objects, e.g. the animated / sticky-header hot path)
102
- function resolveRadialShape(value) {
103
- if (value == null)
104
- return DEFAULT_RADIAL_SHAPE;
105
- if (value === 'circle' || value === 'ellipse')
106
- return value;
107
- return null;
108
- }
109
- function resolveRadialSize(value) {
110
- if (value == null)
111
- return DEFAULT_RADIAL_SIZE;
112
- if (value === 'closest-side' ||
113
- value === 'closest-corner' ||
114
- value === 'farthest-side' ||
115
- value === 'farthest-corner') {
116
- return value;
117
- }
118
- if (isRecord(value) &&
119
- isStringOrNumber(value.x) &&
120
- isStringOrNumber(value.y)) {
121
- return { x: value.x, y: value.y };
122
- }
123
- return null;
124
- }
125
- function isRadialPositionValue(value) {
126
- if (!isRecord(value))
127
- return false;
128
- const hasVertical = isStringOrNumber(value.top) || isStringOrNumber(value.bottom);
129
- const hasHorizontal = isStringOrNumber(value.left) || isStringOrNumber(value.right);
130
- return hasVertical && hasHorizontal;
131
- }
132
- // RN processBackgroundImage.js's `processColorStops`. Returns `null` on any invalid stop (web
133
- // semantics: an invalid gradient applies none of it, same as processBoxShadow/processFilter).
134
- function processColorStopsArray(rawColorStops) {
135
- if (!Array.isArray(rawColorStops))
136
- return null;
137
- const processed = [];
138
- for (const rawStop of rawColorStops) {
139
- if (!isRecord(rawStop))
140
- return null;
141
- const positions = rawStop.positions;
142
- // Color transition hint syntax (`red, 20%, blue`): a position with no color of its own.
143
- if (rawStop.color == null &&
144
- Array.isArray(positions) &&
145
- positions.length === 1) {
146
- const position = positions[0];
147
- if (!isPositionValue(position))
148
- return null;
149
- processed.push({ color: null, position });
150
- continue;
151
- }
152
- const processedColor = processStopColor(rawStop.color);
153
- if (processedColor == null)
154
- return null;
155
- // Two+ positions on one color-stop object is CSS's shorthand for repeating that color at
156
- // each position (e.g. `red 0% 50%` == two adjacent stops both colored red).
157
- if (Array.isArray(positions) && positions.length > 0) {
158
- for (const position of positions) {
159
- if (!isPositionValue(position))
160
- return null;
161
- processed.push({ color: processedColor, position });
162
- }
163
- }
164
- else {
165
- processed.push({ color: processedColor, position: null });
166
- }
167
- }
168
- return processed;
169
- }
170
- function processBackgroundImageArray(rawList) {
171
- const result = [];
172
- for (const rawBgImage of rawList) {
173
- const colorStops = processColorStopsArray(rawBgImage.colorStops);
174
- if (colorStops == null) {
175
- dlog('processBackgroundImage reject: invalid color stop');
176
- return [];
177
- }
178
- if (rawBgImage.type === 'linear-gradient') {
179
- let direction = LINEAR_GRADIENT_DEFAULT_DIRECTION;
180
- const rawDirection = rawBgImage.direction;
181
- const bgDirection = typeof rawDirection === 'string' ? rawDirection.toLowerCase() : null;
182
- if (bgDirection != null) {
183
- if (LINEAR_GRADIENT_ANGLE_UNIT_REGEX.test(bgDirection)) {
184
- const parsedAngle = getAngleInDegrees(bgDirection);
185
- if (parsedAngle == null) {
186
- dlog(`processBackgroundImage reject: invalid linear-gradient angle "${bgDirection}"`);
187
- return [];
188
- }
189
- direction = { type: 'angle', value: parsedAngle };
190
- }
191
- else if (LINEAR_GRADIENT_DIRECTION_REGEX.test(bgDirection)) {
192
- const parsedDirection = getDirectionForKeyword(bgDirection);
193
- if (parsedDirection == null) {
194
- dlog(`processBackgroundImage reject: invalid linear-gradient direction "${bgDirection}"`);
195
- return [];
196
- }
197
- direction = parsedDirection;
198
- }
199
- else {
200
- dlog(`processBackgroundImage reject: invalid linear-gradient direction "${bgDirection}"`);
201
- return [];
202
- }
203
- }
204
- result.push({ type: 'linear-gradient', direction, colorStops });
205
- continue;
206
- }
207
- if (rawBgImage.type === 'radial-gradient') {
208
- const shape = resolveRadialShape(rawBgImage.shape);
209
- if (shape == null) {
210
- dlog('processBackgroundImage reject: invalid radial-gradient shape');
211
- return [];
212
- }
213
- const size = resolveRadialSize(rawBgImage.size);
214
- if (size == null) {
215
- dlog('processBackgroundImage reject: invalid radial-gradient size');
216
- return [];
217
- }
218
- const position = isRadialPositionValue(rawBgImage.position)
219
- ? rawBgImage.position
220
- : DEFAULT_RADIAL_POSITION;
221
- result.push({
222
- type: 'radial-gradient',
223
- shape,
224
- size,
225
- position,
226
- colorStops,
227
- });
228
- }
229
- }
230
- return result;
231
- }
232
- //#endregion array form
233
- //#region CSS string form
234
- // Split a `background-image` value on its TOP-LEVEL commas only (multiple gradients, or a
235
- // color function's internal commas like `rgba(0, 0, 0, .3)`, must not be confused for one
236
- // another) - depth-tracking instead of a lookahead regex because a gradient's own arg list can
237
- // itself contain nested parens (`linear-gradient(to right, rgba(0,0,0,.5), blue)`).
238
- function splitGradients(input) {
239
- const result = [];
240
- let current = '';
241
- let depth = 0;
242
- for (const char of input) {
243
- if (char === '(') {
244
- depth++;
245
- }
246
- else if (char === ')') {
247
- depth--;
248
- }
249
- else if (char === ',' && depth === 0) {
250
- result.push(current.trim());
251
- current = '';
252
- continue;
253
- }
254
- current += char;
255
- }
256
- if (current.trim() !== '')
257
- result.push(current.trim());
258
- return result;
259
- }
260
- function parseColorStopsCSSString(parts) {
261
- const stops = parts.join(',').split(COMMA_SPLIT_REGEX);
262
- const colorStops = [];
263
- let prevStopParts = null;
264
- for (let i = 0; i < stops.length; i++) {
265
- const trimmedStop = stops[i].trim().toLowerCase();
266
- const colorStopParts = trimmedStop.match(COLOR_STOP_PARTS_REGEX);
267
- if (colorStopParts == null)
268
- return null;
269
- if (colorStopParts.length === 3) {
270
- const position1 = getPositionFromCSSValue(colorStopParts[1]);
271
- const position2 = getPositionFromCSSValue(colorStopParts[2]);
272
- const processedColor = processStopColor(colorStopParts[0]);
273
- if (processedColor == null || position1 == null || position2 == null)
274
- return null;
275
- colorStops.push({ color: processedColor, position: position1 });
276
- colorStops.push({ color: processedColor, position: position2 });
277
- }
278
- else if (colorStopParts.length === 2) {
279
- const position = getPositionFromCSSValue(colorStopParts[1]);
280
- const processedColor = processStopColor(colorStopParts[0]);
281
- if (processedColor == null || position == null)
282
- return null;
283
- colorStops.push({ color: processedColor, position });
284
- }
285
- else if (colorStopParts.length === 1) {
286
- const position = getPositionFromCSSValue(colorStopParts[0]);
287
- if (position != null) {
288
- // Transition-hint syntax must have a color-bearing stop on both sides.
289
- const prevWasPositionOnly = prevStopParts != null &&
290
- prevStopParts.length === 1 &&
291
- getPositionFromCSSValue(prevStopParts[0]) != null;
292
- if (prevWasPositionOnly || i === stops.length - 1 || i === 0)
293
- return null;
294
- colorStops.push({ color: null, position });
295
- }
296
- else {
297
- const processedColor = processStopColor(colorStopParts[0]);
298
- if (processedColor == null)
299
- return null;
300
- colorStops.push({ color: processedColor, position: null });
301
- }
302
- }
303
- else {
304
- return null;
305
- }
306
- prevStopParts = colorStopParts;
307
- }
308
- return colorStops;
309
- }
310
- function parseLinearGradientCSSString(gradientContent) {
311
- const parts = gradientContent.split(',');
312
- let direction = LINEAR_GRADIENT_DEFAULT_DIRECTION;
313
- const trimmedDirection = (parts[0] ?? '').trim().toLowerCase();
314
- if (LINEAR_GRADIENT_ANGLE_UNIT_REGEX.test(trimmedDirection)) {
315
- const parsedAngle = getAngleInDegrees(trimmedDirection);
316
- if (parsedAngle == null)
317
- return null;
318
- direction = { type: 'angle', value: parsedAngle };
319
- parts.shift();
320
- }
321
- else if (LINEAR_GRADIENT_DIRECTION_REGEX.test(trimmedDirection)) {
322
- const parsedDirection = getDirectionForKeyword(trimmedDirection);
323
- if (parsedDirection == null)
324
- return null;
325
- direction = parsedDirection;
326
- parts.shift();
327
- }
328
- const colorStops = parseColorStopsCSSString(parts);
329
- if (colorStops == null)
330
- return null;
331
- return { type: 'linear-gradient', direction, colorStops };
332
- }
333
- // The `at <position>` clause of `radial-gradient(... at <position>, <color-stops>)`. Drains
334
- // `tokens` (the SAME queue the caller is walking) via `shift()`, mirroring RN's in-place mutation
335
- // of `firstPartTokens` - not a copy, so the caller sees it emptied after this returns.
336
- function parseRadialGradientPositionTokens(tokens) {
337
- if (tokens.length === 0)
338
- return null;
339
- let top;
340
- let left;
341
- let right;
342
- let bottom;
343
- // 1. [ left | center | right | top | bottom | <length-percentage> ]
344
- if (tokens.length === 1) {
345
- const token = tokens.shift();
346
- if (token == null)
347
- return null;
348
- const tokenTrimmed = token.toLowerCase().trim();
349
- if (tokenTrimmed === 'left') {
350
- left = '0%';
351
- top = '50%';
352
- }
353
- else if (tokenTrimmed === 'center') {
354
- left = '50%';
355
- top = '50%';
356
- }
357
- else if (tokenTrimmed === 'right') {
358
- left = '100%';
359
- top = '50%';
360
- }
361
- else if (tokenTrimmed === 'top') {
362
- left = '50%';
363
- top = '0%';
364
- }
365
- else if (tokenTrimmed === 'bottom') {
366
- left = '50%';
367
- top = '100%';
368
- }
369
- else if (tokenTrimmed.endsWith('px') || tokenTrimmed.endsWith('%')) {
370
- const value = getPositionFromCSSValue(tokenTrimmed);
371
- if (value == null)
372
- return null;
373
- left = value;
374
- top = '50%';
375
- }
376
- }
377
- // 2. [ left | center | right ] && [ top | center | bottom ], or two length-percentages
378
- if (tokens.length === 2) {
379
- const t1 = tokens.shift();
380
- const t2 = tokens.shift();
381
- if (t1 == null || t2 == null)
382
- return null;
383
- const token1 = t1.toLowerCase().trim();
384
- const token2 = t2.toLowerCase().trim();
385
- const horizontalPositions = ['left', 'center', 'right'];
386
- const verticalPositions = ['top', 'center', 'bottom'];
387
- if (horizontalPositions.includes(token1) &&
388
- verticalPositions.includes(token2)) {
389
- left = token1 === 'left' ? '0%' : token1 === 'center' ? '50%' : '100%';
390
- top = token2 === 'top' ? '0%' : token2 === 'center' ? '50%' : '100%';
391
- }
392
- else if (verticalPositions.includes(token1) &&
393
- horizontalPositions.includes(token2)) {
394
- left = token2 === 'left' ? '0%' : token2 === 'center' ? '50%' : '100%';
395
- top = token1 === 'top' ? '0%' : token1 === 'center' ? '50%' : '100%';
396
- }
397
- else {
398
- if (token1 === 'left')
399
- left = '0%';
400
- else if (token1 === 'center')
401
- left = '50%';
402
- else if (token1 === 'right')
403
- left = '100%';
404
- else if (token1.endsWith('px') || token1.endsWith('%')) {
405
- const value = getPositionFromCSSValue(token1);
406
- if (value == null)
407
- return null;
408
- left = value;
409
- }
410
- else
411
- return null;
412
- if (token2 === 'top')
413
- top = '0%';
414
- else if (token2 === 'center')
415
- top = '50%';
416
- else if (token2 === 'bottom')
417
- top = '100%';
418
- else if (token2.endsWith('px') || token2.endsWith('%')) {
419
- const value = getPositionFromCSSValue(token2);
420
- if (value == null)
421
- return null;
422
- top = value;
423
- }
424
- else
425
- return null;
426
- }
427
- }
428
- // 3. [ [ left | right ] <length-percentage> ] && [ [ top | bottom ] <length-percentage> ]
429
- if (tokens.length === 4) {
430
- const [t1, t2, t3, t4] = tokens.splice(0, 4);
431
- if (t1 == null || t2 == null || t3 == null || t4 == null)
432
- return null;
433
- const keyword1 = t1.toLowerCase().trim();
434
- const value1 = getPositionFromCSSValue(t2.toLowerCase().trim());
435
- const keyword2 = t3.toLowerCase().trim();
436
- const value2 = getPositionFromCSSValue(t4.toLowerCase().trim());
437
- if (value1 == null || value2 == null)
438
- return null;
439
- if (keyword1 === 'left')
440
- left = value1;
441
- else if (keyword1 === 'right')
442
- right = value1;
443
- else if (keyword1 === 'top')
444
- top = value1;
445
- else if (keyword1 === 'bottom')
446
- bottom = value1;
447
- else
448
- return null;
449
- if (keyword2 === 'left')
450
- left = value2;
451
- else if (keyword2 === 'right')
452
- right = value2;
453
- else if (keyword2 === 'top')
454
- top = value2;
455
- else if (keyword2 === 'bottom')
456
- bottom = value2;
457
- else
458
- return null;
459
- }
460
- if (top != null && left != null)
461
- return { top, left };
462
- if (bottom != null && right != null)
463
- return { bottom, right };
464
- if (top != null && right != null)
465
- return { top, right };
466
- if (bottom != null && left != null)
467
- return { bottom, left };
468
- return null;
469
- }
470
- function parseRadialGradientCSSString(gradientContent) {
471
- let shape = DEFAULT_RADIAL_SHAPE;
472
- let size = DEFAULT_RADIAL_SIZE;
473
- let position = { ...DEFAULT_RADIAL_POSITION };
474
- const parts = gradientContent.split(COMMA_SPLIT_REGEX);
475
- const firstPartStr = (parts[0] ?? '').trim();
476
- const remainingParts = [...parts];
477
- let hasShapeSizeOrPositionString = false;
478
- let hasExplicitSingleSize = false;
479
- let hasExplicitShape = false;
480
- const firstPartTokens = firstPartStr.split(WHITESPACE_SPLIT_REGEX);
481
- while (firstPartTokens.length > 0) {
482
- let token = firstPartTokens.shift();
483
- if (token == null)
484
- continue;
485
- let tokenTrimmed = token.toLowerCase().trim();
486
- if (tokenTrimmed === 'circle' || tokenTrimmed === 'ellipse') {
487
- shape = tokenTrimmed;
488
- hasShapeSizeOrPositionString = true;
489
- hasExplicitShape = true;
490
- }
491
- else if (tokenTrimmed === 'closest-corner' ||
492
- tokenTrimmed === 'farthest-corner' ||
493
- tokenTrimmed === 'closest-side' ||
494
- tokenTrimmed === 'farthest-side') {
495
- size = tokenTrimmed;
496
- hasShapeSizeOrPositionString = true;
497
- }
498
- else if (tokenTrimmed.endsWith('px') || tokenTrimmed.endsWith('%')) {
499
- const sizeX = getPositionFromCSSValue(tokenTrimmed);
500
- if (sizeX == null || (typeof sizeX === 'number' && sizeX < 0))
501
- return null;
502
- hasShapeSizeOrPositionString = true;
503
- size = { x: sizeX, y: sizeX };
504
- token = firstPartTokens.shift();
505
- if (token == null) {
506
- hasExplicitSingleSize = true;
507
- continue;
508
- }
509
- tokenTrimmed = token.toLowerCase().trim();
510
- if (tokenTrimmed.endsWith('px') || tokenTrimmed.endsWith('%')) {
511
- const sizeY = getPositionFromCSSValue(tokenTrimmed);
512
- if (sizeY == null || (typeof sizeY === 'number' && sizeY < 0))
513
- return null;
514
- size = { x: sizeX, y: sizeY };
515
- }
516
- else {
517
- hasExplicitSingleSize = true;
518
- }
519
- }
520
- else if (tokenTrimmed === 'at') {
521
- hasShapeSizeOrPositionString = true;
522
- const parsedPosition = parseRadialGradientPositionTokens(firstPartTokens);
523
- if (parsedPosition == null)
524
- return null;
525
- position = parsedPosition;
526
- break;
527
- }
528
- // No shape/size/position token found in this iteration - the rest of the first part is a
529
- // color stop, not gradient config.
530
- if (!hasShapeSizeOrPositionString)
531
- break;
532
- }
533
- if (hasShapeSizeOrPositionString) {
534
- remainingParts.shift();
535
- if (!hasExplicitShape && hasExplicitSingleSize)
536
- shape = 'circle';
537
- if (hasExplicitSingleSize && hasExplicitShape && shape === 'ellipse')
538
- return null;
539
- }
540
- const colorStops = parseColorStopsCSSString(remainingParts);
541
- if (colorStops == null)
542
- return null;
543
- return { type: 'radial-gradient', shape, size, position, colorStops };
544
- }
545
- function parseBackgroundImageCSSString(cssString) {
546
- const gradients = [];
547
- for (const bgImageString of splitGradients(cssString)) {
548
- const bgImage = bgImageString.toLowerCase();
549
- const match = GRADIENT_REGEX.exec(bgImage);
550
- if (!match || match[1] == null || match[2] == null)
551
- continue;
552
- const isRadial = match[1].toLowerCase() === 'radial';
553
- const gradient = isRadial
554
- ? parseRadialGradientCSSString(match[2])
555
- : parseLinearGradientCSSString(match[2]);
556
- if (gradient != null)
557
- gradients.push(gradient);
558
- }
559
- return gradients;
560
- }
561
- //#endregion CSS string form
562
- // RN processBackgroundImage.js's default export. Returns [] on any invalid gradient - web
563
- // semantics: an invalid `background-image` paints none of it rather than a partial gradient.
24
+ // @ts-expect-error - untyped Flow source. Metro compiles it; vitest.config.ts strips the types.
25
+ import processBackgroundImageUpstream from 'react-native/Libraries/StyleSheet/processBackgroundImage';
564
26
  export function processBackgroundImage(backgroundImage) {
565
- if (backgroundImage == null)
27
+ try {
28
+ const parsed = processBackgroundImageUpstream(backgroundImage);
29
+ return Array.isArray(parsed) ? parsed : [];
30
+ }
31
+ catch (error) {
32
+ dlog(`processBackgroundImage: dropped an invalid background image - ${error instanceof Error ? error.message : String(error)}`);
566
33
  return [];
567
- if (typeof backgroundImage === 'string') {
568
- return parseBackgroundImageCSSString(backgroundImage.replace(NEWLINE_REGEX, ' '));
569
34
  }
570
- return processBackgroundImageArray(backgroundImage);
571
35
  }
@@ -0,0 +1,4 @@
1
+ export type IBackgroundLonghandInput = string | ReadonlyArray<unknown> | undefined;
2
+ export declare function processBackgroundSize(value: IBackgroundLonghandInput): ReadonlyArray<unknown> | undefined;
3
+ export declare function processBackgroundPosition(value: IBackgroundLonghandInput): ReadonlyArray<unknown> | undefined;
4
+ export declare function processBackgroundRepeat(value: IBackgroundLonghandInput): ReadonlyArray<unknown> | undefined;
@@ -0,0 +1,44 @@
1
+ // RN's own processBackgroundSize / processBackgroundPosition / processBackgroundRepeat, imported
2
+ // rather than ported.
3
+ //
4
+ // Why they are needed at all: ReactNativeStyleAttributes registers all three with
5
+ // `nativeCSSParsing ? true : {process: processX}` (:50, :54, :58), and enableNativeCSSParsing()
6
+ // defaults to false - so RN's stock path parses the CSS string into the per-axis structure
7
+ // Fabric's C++ expects. We registered `experimental_backgroundImage` and not its three siblings,
8
+ // so a string for any of them reached native unparsed: dropped on iOS, cast to the wrong
9
+ // ReadableMap type on Android. Same shape as the process-transform incident.
10
+ //
11
+ // Why importing is free here even by this repo's strictest reading: all three upstream files
12
+ // contain ONLY `import type` lines. Their value-import closure is empty - no processColor, no
13
+ // Platform, no TurboModule floor, no renderer. There is nothing to weigh.
14
+ //
15
+ // Why no try/catch, unlike the other upstream wrappers: these three do not validate through
16
+ // `invariant`. They answer `[]` for anything they cannot parse, which upstream's own comment calls
17
+ // "do not apply any background" - so refusal is a RETURN here, not a throw.
18
+ import { dlog } from './debug.js';
19
+ // @ts-expect-error - untyped Flow source. Metro compiles it; vitest.config.ts strips the types.
20
+ import processBackgroundSizeUpstream from 'react-native/Libraries/StyleSheet/processBackgroundSize';
21
+ // @ts-expect-error - untyped Flow source. Metro compiles it; vitest.config.ts strips the types.
22
+ import processBackgroundPositionUpstream from 'react-native/Libraries/StyleSheet/processBackgroundPosition';
23
+ // @ts-expect-error - untyped Flow source. Metro compiles it; vitest.config.ts strips the types.
24
+ import processBackgroundRepeatUpstream from 'react-native/Libraries/StyleSheet/processBackgroundRepeat';
25
+ // An empty result is upstream's way of saying it parsed nothing usable. The payload builder's
26
+ // rule is that a refused processor leaves the key ABSENT, so it must not become `[]` - an empty
27
+ // array is a real value, and committing one asks native to apply "no background size" rather than
28
+ // leaving the declaration unset.
29
+ function orAbsent(parsed, key, value) {
30
+ if (!Array.isArray(parsed) || parsed.length === 0) {
31
+ dlog(`${key}: RN parsed nothing usable from ${JSON.stringify(value)}`);
32
+ return undefined;
33
+ }
34
+ return parsed;
35
+ }
36
+ export function processBackgroundSize(value) {
37
+ return orAbsent(processBackgroundSizeUpstream(value), 'experimental_backgroundSize', value);
38
+ }
39
+ export function processBackgroundPosition(value) {
40
+ return orAbsent(processBackgroundPositionUpstream(value), 'experimental_backgroundPosition', value);
41
+ }
42
+ export function processBackgroundRepeat(value) {
43
+ return orAbsent(processBackgroundRepeatUpstream(value), 'experimental_backgroundRepeat', value);
44
+ }