@ccheever/exact-renderer 0.1.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.
- package/package.json +118 -0
- package/src/__tests__/adapter-window-state.test.tsx +190 -0
- package/src/__tests__/attrs.test.ts +157 -0
- package/src/__tests__/classname.test.ts +332 -0
- package/src/__tests__/color.test.ts +169 -0
- package/src/__tests__/dom-mirror.test.ts +682 -0
- package/src/__tests__/dom-shim.test.ts +274 -0
- package/src/__tests__/fixtures/SvelteCounter.svelte +7 -0
- package/src/__tests__/fixtures/SvelteInput.svelte +8 -0
- package/src/__tests__/host-config.test.ts +51 -0
- package/src/__tests__/host-ops.test.ts +2234 -0
- package/src/__tests__/image-source.test.ts +135 -0
- package/src/__tests__/liquid-glass.test.ts +72 -0
- package/src/__tests__/multi-root.test.ts +118 -0
- package/src/__tests__/native-view-events.test.ts +102 -0
- package/src/__tests__/nodes.test.ts +399 -0
- package/src/__tests__/normalize.test.ts +576 -0
- package/src/__tests__/paragraph-lowering.test.tsx +144 -0
- package/src/__tests__/props.test.ts +518 -0
- package/src/__tests__/protocol-encoder.test.ts +732 -0
- package/src/__tests__/protocol-fixture-bytes.test.ts +41 -0
- package/src/__tests__/reconciler.test.tsx +241 -0
- package/src/__tests__/svelte-adapter.test.ts +166 -0
- package/src/__tests__/svg-source.test.ts +71 -0
- package/src/__tests__/tags.test.ts +354 -0
- package/src/__tests__/toggle.test.ts +441 -0
- package/src/__tests__/transitions.test.ts +106 -0
- package/src/__tests__/web-primitives.test.tsx +454 -0
- package/src/__tests__/window-hooks.test.tsx +447 -0
- package/src/adapter-contract.ts +68 -0
- package/src/attrs.ts +596 -0
- package/src/classname-contract.ts +87 -0
- package/src/classname-resolve.ts +553 -0
- package/src/classname-runtime.ts +29 -0
- package/src/components.ts +214 -0
- package/src/css-variable-context.ts +83 -0
- package/src/dom-hydration.ts +160 -0
- package/src/dom-mirror.ts +1459 -0
- package/src/dom-shim.ts +1736 -0
- package/src/group-context.ts +69 -0
- package/src/host-config.ts +431 -0
- package/src/host-ops.ts +3167 -0
- package/src/image-source.native.ts +703 -0
- package/src/image-source.ts +554 -0
- package/src/index.ts +278 -0
- package/src/inspector-runtime.ts +244 -0
- package/src/inspector.ts +3570 -0
- package/src/jsx-augmentations.ts +54 -0
- package/src/keyboard-avoidance.ts +217 -0
- package/src/native-primitives.ts +43 -0
- package/src/native-view-events.ts +322 -0
- package/src/native-view.ts +60 -0
- package/src/nodes/index.ts +41 -0
- package/src/nodes/node.ts +531 -0
- package/src/peer-context.ts +100 -0
- package/src/primitives.native.ts +8 -0
- package/src/primitives.ts +8 -0
- package/src/props/index.ts +14 -0
- package/src/props/normalize.ts +816 -0
- package/src/protocol/encoder.ts +940 -0
- package/src/protocol/index.ts +33 -0
- package/src/reconciler.ts +581 -0
- package/src/runtime.ts +11 -0
- package/src/safe-area.ts +543 -0
- package/src/solid.ts +490 -0
- package/src/style/color.js +1 -0
- package/src/style/color.ts +15 -0
- package/src/style/index.js +1 -0
- package/src/style/index.ts +22 -0
- package/src/style/normalize.js +1 -0
- package/src/style/normalize.ts +1426 -0
- package/src/svelte.ts +349 -0
- package/src/svg-source.ts +222 -0
- package/src/tags/index.ts +21 -0
- package/src/tags/tag-map.ts +289 -0
- package/src/text/paragraph-lowering.ts +310 -0
- package/src/types.ts +1175 -0
- package/src/vue.ts +535 -0
- package/src/web-host.ts +19 -0
- package/src/web-primitives.ts +1654 -0
|
@@ -0,0 +1,1426 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Style Normalization
|
|
3
|
+
*
|
|
4
|
+
* This module converts user-provided style objects into the canonical
|
|
5
|
+
* internal format used by the protocol encoder.
|
|
6
|
+
*
|
|
7
|
+
* Design Principles:
|
|
8
|
+
* - Input validation and clamping for safety
|
|
9
|
+
* - Consistent handling of shorthand properties
|
|
10
|
+
* - Clear precedence rules (specific > general)
|
|
11
|
+
* - Web defaults with RN compatibility mode
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type {
|
|
15
|
+
ViewStyle,
|
|
16
|
+
FullStyle,
|
|
17
|
+
CanonicalStyle,
|
|
18
|
+
TransitionConfig,
|
|
19
|
+
StyleTransition,
|
|
20
|
+
TransitionEasing,
|
|
21
|
+
TransitionEasingName,
|
|
22
|
+
CubicBezierEasing,
|
|
23
|
+
CanonicalTransitionConfig,
|
|
24
|
+
CanonicalTransitionMap,
|
|
25
|
+
CanonicalTransitionProperty,
|
|
26
|
+
DimensionValue,
|
|
27
|
+
DimensionInput,
|
|
28
|
+
FlexDirection,
|
|
29
|
+
Direction,
|
|
30
|
+
ResolvedDirection,
|
|
31
|
+
WritingMode,
|
|
32
|
+
} from '../types.js';
|
|
33
|
+
import { resolveFontFamilyId } from '@exact/core/assets-fonts-state';
|
|
34
|
+
import { parseColor } from './color.js';
|
|
35
|
+
|
|
36
|
+
const TIMING_DURATION_DEFAULT_MS = 250;
|
|
37
|
+
const TIMING_DELAY_DEFAULT_MS = 0;
|
|
38
|
+
const TIMING_EASING_DEFAULT: TransitionEasingName = 'easeInOut';
|
|
39
|
+
|
|
40
|
+
const SPRING_DELAY_DEFAULT_MS = 0;
|
|
41
|
+
const SPRING_DAMPING_DEFAULT = 16;
|
|
42
|
+
const SPRING_STIFFNESS_DEFAULT = 220;
|
|
43
|
+
const SPRING_MASS_DEFAULT = 1;
|
|
44
|
+
const SPRING_VELOCITY_DEFAULT = 0;
|
|
45
|
+
|
|
46
|
+
const FONT_VARIANT_NUMERIC_TABULAR_NUMS = 1 << 0;
|
|
47
|
+
const FONT_VARIANT_NUMERIC_PROPORTIONAL_NUMS = 1 << 1;
|
|
48
|
+
const FONT_VARIANT_NUMERIC_LINING_NUMS = 1 << 2;
|
|
49
|
+
const FONT_VARIANT_NUMERIC_OLDSTYLE_NUMS = 1 << 3;
|
|
50
|
+
const FONT_VARIANT_NUMERIC_SLASHED_ZERO = 1 << 4;
|
|
51
|
+
|
|
52
|
+
const FONT_WEIGHT_MAP: Readonly<Record<string, number>> = {
|
|
53
|
+
normal: 400,
|
|
54
|
+
bold: 700,
|
|
55
|
+
'100': 100,
|
|
56
|
+
'200': 200,
|
|
57
|
+
'300': 300,
|
|
58
|
+
'400': 400,
|
|
59
|
+
'500': 500,
|
|
60
|
+
'600': 600,
|
|
61
|
+
'700': 700,
|
|
62
|
+
'800': 800,
|
|
63
|
+
'900': 900,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const TRANSITION_PROPERTY_ALIASES: Record<string, CanonicalTransitionProperty> = {
|
|
67
|
+
opacity: 'opacity',
|
|
68
|
+
transform: 'transform',
|
|
69
|
+
'background-color': 'backgroundColor',
|
|
70
|
+
backgroundcolor: 'backgroundColor',
|
|
71
|
+
backgroundColor: 'backgroundColor',
|
|
72
|
+
'border-radius': 'borderRadius',
|
|
73
|
+
borderradius: 'borderRadius',
|
|
74
|
+
borderRadius: 'borderRadius',
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const TRANSITION_EASING_ALIASES: Record<string, TransitionEasingName> = {
|
|
78
|
+
linear: 'linear',
|
|
79
|
+
ease: 'easeInOut',
|
|
80
|
+
'ease-in': 'easeIn',
|
|
81
|
+
easeIn: 'easeIn',
|
|
82
|
+
'ease-out': 'easeOut',
|
|
83
|
+
easeOut: 'easeOut',
|
|
84
|
+
'ease-in-out': 'easeInOut',
|
|
85
|
+
easeInOut: 'easeInOut',
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export interface NormalizeStyleOptions {
|
|
89
|
+
resolvedDirection?: ResolvedDirection;
|
|
90
|
+
resolvedWritingMode?: WritingMode;
|
|
91
|
+
includeInheritedValues?: boolean;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
type CanonicalDimensionKey = Extract<keyof CanonicalStyle,
|
|
95
|
+
| 'width'
|
|
96
|
+
| 'height'
|
|
97
|
+
| 'minWidth'
|
|
98
|
+
| 'minHeight'
|
|
99
|
+
| 'maxWidth'
|
|
100
|
+
| 'maxHeight'
|
|
101
|
+
| 'paddingTop'
|
|
102
|
+
| 'paddingRight'
|
|
103
|
+
| 'paddingBottom'
|
|
104
|
+
| 'paddingLeft'
|
|
105
|
+
| 'marginTop'
|
|
106
|
+
| 'marginRight'
|
|
107
|
+
| 'marginBottom'
|
|
108
|
+
| 'marginLeft'
|
|
109
|
+
| 'flexBasis'
|
|
110
|
+
| 'top'
|
|
111
|
+
| 'right'
|
|
112
|
+
| 'bottom'
|
|
113
|
+
| 'left'
|
|
114
|
+
>;
|
|
115
|
+
|
|
116
|
+
const BOX_DIMENSION_KEYS = [
|
|
117
|
+
'width',
|
|
118
|
+
'height',
|
|
119
|
+
'minWidth',
|
|
120
|
+
'minHeight',
|
|
121
|
+
'maxWidth',
|
|
122
|
+
'maxHeight',
|
|
123
|
+
] as const satisfies readonly CanonicalDimensionKey[];
|
|
124
|
+
|
|
125
|
+
const PADDING_KEYS = {
|
|
126
|
+
top: 'paddingTop',
|
|
127
|
+
right: 'paddingRight',
|
|
128
|
+
bottom: 'paddingBottom',
|
|
129
|
+
left: 'paddingLeft',
|
|
130
|
+
} as const satisfies Record<PhysicalEdge, CanonicalDimensionKey>;
|
|
131
|
+
|
|
132
|
+
const MARGIN_KEYS = {
|
|
133
|
+
top: 'marginTop',
|
|
134
|
+
right: 'marginRight',
|
|
135
|
+
bottom: 'marginBottom',
|
|
136
|
+
left: 'marginLeft',
|
|
137
|
+
} as const satisfies Record<PhysicalEdge, CanonicalDimensionKey>;
|
|
138
|
+
|
|
139
|
+
const INSET_KEYS = {
|
|
140
|
+
top: 'top',
|
|
141
|
+
right: 'right',
|
|
142
|
+
bottom: 'bottom',
|
|
143
|
+
left: 'left',
|
|
144
|
+
} as const satisfies Record<PhysicalEdge, CanonicalDimensionKey>;
|
|
145
|
+
|
|
146
|
+
const PADDING_EDGE_KEYS = [
|
|
147
|
+
'paddingTop',
|
|
148
|
+
'paddingRight',
|
|
149
|
+
'paddingBottom',
|
|
150
|
+
'paddingLeft',
|
|
151
|
+
] as const satisfies readonly CanonicalDimensionKey[];
|
|
152
|
+
|
|
153
|
+
const MARGIN_EDGE_KEYS = [
|
|
154
|
+
'marginTop',
|
|
155
|
+
'marginRight',
|
|
156
|
+
'marginBottom',
|
|
157
|
+
'marginLeft',
|
|
158
|
+
] as const satisfies readonly CanonicalDimensionKey[];
|
|
159
|
+
|
|
160
|
+
const POSITION_TRAILING_EDGE_KEYS = [
|
|
161
|
+
'right',
|
|
162
|
+
'bottom',
|
|
163
|
+
'left',
|
|
164
|
+
] as const satisfies readonly CanonicalDimensionKey[];
|
|
165
|
+
|
|
166
|
+
type DirectStyleAssignment = readonly [keyof CanonicalStyle, keyof FullStyle];
|
|
167
|
+
type NumberStyleKind = 'finite' | 'positive' | 'nonNegative' | 'unit' | 'floorPositive';
|
|
168
|
+
type NumberStyleAssignment = readonly [keyof CanonicalStyle, keyof FullStyle, NumberStyleKind];
|
|
169
|
+
|
|
170
|
+
const FLEX_DIRECT_ASSIGNMENTS: readonly DirectStyleAssignment[] = [
|
|
171
|
+
['flexWrap', 'flexWrap'],
|
|
172
|
+
['justifyContent', 'justifyContent'],
|
|
173
|
+
['alignItems', 'alignItems'],
|
|
174
|
+
['alignSelf', 'alignSelf'],
|
|
175
|
+
];
|
|
176
|
+
|
|
177
|
+
const FLEX_NUMBER_ASSIGNMENTS: readonly NumberStyleAssignment[] = [
|
|
178
|
+
['flexGrow', 'flexGrow', 'finite'],
|
|
179
|
+
['flexShrink', 'flexShrink', 'finite'],
|
|
180
|
+
];
|
|
181
|
+
|
|
182
|
+
const GAP_NUMBER_ASSIGNMENTS: readonly NumberStyleAssignment[] = [
|
|
183
|
+
['rowGap', 'rowGap', 'finite'],
|
|
184
|
+
['columnGap', 'columnGap', 'finite'],
|
|
185
|
+
];
|
|
186
|
+
|
|
187
|
+
const APPEARANCE_NUMBER_ASSIGNMENTS: readonly NumberStyleAssignment[] = [
|
|
188
|
+
['opacity', 'opacity', 'unit'],
|
|
189
|
+
['borderRadius', 'borderRadius', 'nonNegative'],
|
|
190
|
+
['borderTopLeftRadius', 'borderTopLeftRadius', 'nonNegative'],
|
|
191
|
+
['borderTopRightRadius', 'borderTopRightRadius', 'nonNegative'],
|
|
192
|
+
['borderBottomLeftRadius', 'borderBottomLeftRadius', 'nonNegative'],
|
|
193
|
+
['borderBottomRightRadius', 'borderBottomRightRadius', 'nonNegative'],
|
|
194
|
+
['borderWidth', 'borderWidth', 'nonNegative'],
|
|
195
|
+
['borderTopWidth', 'borderTopWidth', 'nonNegative'],
|
|
196
|
+
['borderRightWidth', 'borderRightWidth', 'nonNegative'],
|
|
197
|
+
['borderBottomWidth', 'borderBottomWidth', 'nonNegative'],
|
|
198
|
+
['borderLeftWidth', 'borderLeftWidth', 'nonNegative'],
|
|
199
|
+
['backdropBlur', 'backdropBlur', 'nonNegative'],
|
|
200
|
+
['aspectRatio', 'aspectRatio', 'positive'],
|
|
201
|
+
];
|
|
202
|
+
|
|
203
|
+
const SHADOW_NUMBER_ASSIGNMENTS: readonly NumberStyleAssignment[] = [
|
|
204
|
+
['shadowRadius', 'shadowRadius', 'nonNegative'],
|
|
205
|
+
['shadowOpacity', 'shadowOpacity', 'unit'],
|
|
206
|
+
];
|
|
207
|
+
|
|
208
|
+
const TYPOGRAPHY_NUMBER_ASSIGNMENTS: readonly NumberStyleAssignment[] = [
|
|
209
|
+
['fontSize', 'fontSize', 'positive'],
|
|
210
|
+
['lineHeight', 'lineHeight', 'finite'],
|
|
211
|
+
['letterSpacing', 'letterSpacing', 'finite'],
|
|
212
|
+
['numberOfLines', 'numberOfLines', 'floorPositive'],
|
|
213
|
+
];
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Parse a dimension input into a canonical dimension value.
|
|
217
|
+
*
|
|
218
|
+
* @param input - The dimension input (number, string, or explicit)
|
|
219
|
+
* @returns The canonical dimension value, or undefined if invalid
|
|
220
|
+
*/
|
|
221
|
+
export function parseDimension(input: DimensionInput | undefined): DimensionValue | undefined {
|
|
222
|
+
if (input === undefined || input === null) {
|
|
223
|
+
return undefined;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Number is interpreted as points
|
|
227
|
+
if (typeof input === 'number') {
|
|
228
|
+
if (!isFinite(input)) {
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
return { type: 'points', value: input };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// String parsing
|
|
235
|
+
if (typeof input === 'string') {
|
|
236
|
+
const trimmed = input.trim();
|
|
237
|
+
|
|
238
|
+
if (trimmed === 'auto') {
|
|
239
|
+
return { type: 'auto' };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Check for percentage
|
|
243
|
+
if (trimmed.endsWith('%')) {
|
|
244
|
+
const value = parseFloat(trimmed.slice(0, -1));
|
|
245
|
+
if (!isFinite(value)) {
|
|
246
|
+
return undefined;
|
|
247
|
+
}
|
|
248
|
+
return { type: 'percent', value };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Check for px suffix
|
|
252
|
+
if (trimmed.endsWith('px')) {
|
|
253
|
+
const value = parseFloat(trimmed.slice(0, -2));
|
|
254
|
+
if (!isFinite(value)) {
|
|
255
|
+
return undefined;
|
|
256
|
+
}
|
|
257
|
+
return { type: 'points', value };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Try parsing as a number
|
|
261
|
+
const value = parseFloat(trimmed);
|
|
262
|
+
if (!isFinite(value)) {
|
|
263
|
+
return undefined;
|
|
264
|
+
}
|
|
265
|
+
return { type: 'points', value };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Already a dimension value object
|
|
269
|
+
if (typeof input === 'object' && 'type' in input) {
|
|
270
|
+
if (input.type === 'auto') {
|
|
271
|
+
return { type: 'auto' };
|
|
272
|
+
}
|
|
273
|
+
if ((input.type === 'points' || input.type === 'percent') && typeof input.value === 'number') {
|
|
274
|
+
if (!isFinite(input.value)) {
|
|
275
|
+
return undefined;
|
|
276
|
+
}
|
|
277
|
+
return input as DimensionValue;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return undefined;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function setDimension(
|
|
285
|
+
canonical: CanonicalStyle,
|
|
286
|
+
key: CanonicalDimensionKey,
|
|
287
|
+
input: DimensionInput | undefined,
|
|
288
|
+
): void {
|
|
289
|
+
const value = parseDimension(input);
|
|
290
|
+
if (value) {
|
|
291
|
+
canonical[key] = value;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function setDimensionValue(
|
|
296
|
+
canonical: CanonicalStyle,
|
|
297
|
+
key: CanonicalDimensionKey,
|
|
298
|
+
value: DimensionValue,
|
|
299
|
+
): void {
|
|
300
|
+
canonical[key] = value;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function setDimensionEdges(
|
|
304
|
+
canonical: CanonicalStyle,
|
|
305
|
+
keys: readonly CanonicalDimensionKey[],
|
|
306
|
+
value: DimensionValue,
|
|
307
|
+
): void {
|
|
308
|
+
for (const key of keys) {
|
|
309
|
+
canonical[key] = value;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function normalizeNumberStyleValue(value: unknown, kind: NumberStyleKind): number | undefined {
|
|
314
|
+
if (typeof value !== 'number' || !isFinite(value)) {
|
|
315
|
+
return undefined;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
switch (kind) {
|
|
319
|
+
case 'positive':
|
|
320
|
+
return value > 0 ? value : undefined;
|
|
321
|
+
case 'nonNegative':
|
|
322
|
+
return Math.max(0, value);
|
|
323
|
+
case 'unit':
|
|
324
|
+
return Math.max(0, Math.min(1, value));
|
|
325
|
+
case 'floorPositive':
|
|
326
|
+
return value > 0 ? Math.floor(value) : undefined;
|
|
327
|
+
case 'finite':
|
|
328
|
+
default:
|
|
329
|
+
return value;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function assignNumberStyles(
|
|
334
|
+
canonical: CanonicalStyle,
|
|
335
|
+
style: FullStyle,
|
|
336
|
+
assignments: readonly NumberStyleAssignment[],
|
|
337
|
+
): void {
|
|
338
|
+
const target = canonical as Record<string, unknown>;
|
|
339
|
+
const source = style as Record<string, unknown>;
|
|
340
|
+
for (const [targetKey, sourceKey, kind] of assignments) {
|
|
341
|
+
const value = normalizeNumberStyleValue(source[sourceKey], kind);
|
|
342
|
+
if (value !== undefined) {
|
|
343
|
+
target[targetKey] = value;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function assignDefinedStyles(
|
|
349
|
+
canonical: CanonicalStyle,
|
|
350
|
+
style: FullStyle,
|
|
351
|
+
assignments: readonly DirectStyleAssignment[],
|
|
352
|
+
): void {
|
|
353
|
+
const target = canonical as Record<string, unknown>;
|
|
354
|
+
const source = style as Record<string, unknown>;
|
|
355
|
+
for (const [targetKey, sourceKey] of assignments) {
|
|
356
|
+
const value = source[sourceKey];
|
|
357
|
+
if (value !== undefined) {
|
|
358
|
+
target[targetKey] = value;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function parseExplicitDirection(value: unknown): Direction | undefined {
|
|
364
|
+
return value === 'ltr' || value === 'rtl' || value === 'auto'
|
|
365
|
+
? value
|
|
366
|
+
: undefined;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function parseWritingMode(value: unknown): WritingMode | undefined {
|
|
370
|
+
return value === 'horizontal-tb' || value === 'vertical-rl' || value === 'vertical-lr'
|
|
371
|
+
? value
|
|
372
|
+
: undefined;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
type PhysicalEdge = 'top' | 'right' | 'bottom' | 'left';
|
|
376
|
+
|
|
377
|
+
function inlineStartEdge(
|
|
378
|
+
direction: ResolvedDirection,
|
|
379
|
+
writingMode: WritingMode,
|
|
380
|
+
): PhysicalEdge {
|
|
381
|
+
switch (writingMode) {
|
|
382
|
+
case 'vertical-rl':
|
|
383
|
+
case 'vertical-lr':
|
|
384
|
+
return direction === 'rtl' ? 'bottom' : 'top';
|
|
385
|
+
case 'horizontal-tb':
|
|
386
|
+
default:
|
|
387
|
+
return direction === 'rtl' ? 'right' : 'left';
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function inlineEndEdge(
|
|
392
|
+
direction: ResolvedDirection,
|
|
393
|
+
writingMode: WritingMode,
|
|
394
|
+
): PhysicalEdge {
|
|
395
|
+
switch (writingMode) {
|
|
396
|
+
case 'vertical-rl':
|
|
397
|
+
case 'vertical-lr':
|
|
398
|
+
return direction === 'rtl' ? 'top' : 'bottom';
|
|
399
|
+
case 'horizontal-tb':
|
|
400
|
+
default:
|
|
401
|
+
return direction === 'rtl' ? 'left' : 'right';
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function assignPaddingEdge(
|
|
406
|
+
canonical: CanonicalStyle,
|
|
407
|
+
edge: PhysicalEdge,
|
|
408
|
+
value: DimensionValue,
|
|
409
|
+
): void {
|
|
410
|
+
setDimensionValue(canonical, PADDING_KEYS[edge], value);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function assignMarginEdge(
|
|
414
|
+
canonical: CanonicalStyle,
|
|
415
|
+
edge: PhysicalEdge,
|
|
416
|
+
value: DimensionValue,
|
|
417
|
+
): void {
|
|
418
|
+
setDimensionValue(canonical, MARGIN_KEYS[edge], value);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function assignInsetEdge(
|
|
422
|
+
canonical: CanonicalStyle,
|
|
423
|
+
edge: PhysicalEdge,
|
|
424
|
+
value: DimensionValue,
|
|
425
|
+
): void {
|
|
426
|
+
setDimensionValue(canonical, INSET_KEYS[edge], value);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Parse a font weight value to a numeric weight (100-900).
|
|
431
|
+
*
|
|
432
|
+
* @param weight - The font weight input
|
|
433
|
+
* @returns The numeric weight, or undefined if invalid
|
|
434
|
+
*/
|
|
435
|
+
export function parseFontWeight(weight: string | number | undefined): number | undefined {
|
|
436
|
+
if (weight === undefined || weight === null) {
|
|
437
|
+
return undefined;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Already a number
|
|
441
|
+
if (typeof weight === 'number') {
|
|
442
|
+
if (weight >= 100 && weight <= 900) {
|
|
443
|
+
return weight;
|
|
444
|
+
}
|
|
445
|
+
return undefined;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return FONT_WEIGHT_MAP[weight];
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Parse a CSS font-variant-numeric string into the protocol bitmask.
|
|
453
|
+
*
|
|
454
|
+
* Mutually exclusive categories use last-token-wins semantics to match CSS.
|
|
455
|
+
*/
|
|
456
|
+
export function parseFontVariantNumeric(value: string | undefined): number {
|
|
457
|
+
if (typeof value !== 'string') {
|
|
458
|
+
return 0;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
let mask = 0;
|
|
462
|
+
|
|
463
|
+
for (const token of value.split(/\s+/)) {
|
|
464
|
+
switch (token) {
|
|
465
|
+
case 'tabular-nums':
|
|
466
|
+
mask = (mask & ~FONT_VARIANT_NUMERIC_PROPORTIONAL_NUMS) | FONT_VARIANT_NUMERIC_TABULAR_NUMS;
|
|
467
|
+
break;
|
|
468
|
+
case 'proportional-nums':
|
|
469
|
+
mask = (mask & ~FONT_VARIANT_NUMERIC_TABULAR_NUMS) | FONT_VARIANT_NUMERIC_PROPORTIONAL_NUMS;
|
|
470
|
+
break;
|
|
471
|
+
case 'lining-nums':
|
|
472
|
+
mask = (mask & ~FONT_VARIANT_NUMERIC_OLDSTYLE_NUMS) | FONT_VARIANT_NUMERIC_LINING_NUMS;
|
|
473
|
+
break;
|
|
474
|
+
case 'oldstyle-nums':
|
|
475
|
+
mask = (mask & ~FONT_VARIANT_NUMERIC_LINING_NUMS) | FONT_VARIANT_NUMERIC_OLDSTYLE_NUMS;
|
|
476
|
+
break;
|
|
477
|
+
case 'slashed-zero':
|
|
478
|
+
mask |= FONT_VARIANT_NUMERIC_SLASHED_ZERO;
|
|
479
|
+
break;
|
|
480
|
+
case 'normal':
|
|
481
|
+
mask = 0;
|
|
482
|
+
break;
|
|
483
|
+
default:
|
|
484
|
+
break;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return mask;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Extract transform values from the transform array.
|
|
493
|
+
*
|
|
494
|
+
* @param transform - The transform array from styles
|
|
495
|
+
* @returns Object with extracted transform values
|
|
496
|
+
*/
|
|
497
|
+
export function extractTransforms(
|
|
498
|
+
transform: ViewStyle['transform']
|
|
499
|
+
): { x?: number; y?: number; scale?: number; rotate?: number } {
|
|
500
|
+
if (!transform || !Array.isArray(transform)) {
|
|
501
|
+
return {};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const result: { x?: number; y?: number; scale?: number; rotate?: number } = {};
|
|
505
|
+
|
|
506
|
+
for (const t of transform) {
|
|
507
|
+
if ('translateX' in t && typeof t.translateX === 'number') {
|
|
508
|
+
result.x = t.translateX;
|
|
509
|
+
} else if ('translateY' in t && typeof t.translateY === 'number') {
|
|
510
|
+
result.y = t.translateY;
|
|
511
|
+
} else if ('scale' in t && typeof t.scale === 'number') {
|
|
512
|
+
result.scale = t.scale;
|
|
513
|
+
} else if ('rotate' in t && typeof t.rotate === 'string') {
|
|
514
|
+
// Parse rotation - support deg and rad
|
|
515
|
+
const rotateStr = t.rotate.trim();
|
|
516
|
+
if (rotateStr.endsWith('deg')) {
|
|
517
|
+
const degrees = parseFloat(rotateStr.slice(0, -3));
|
|
518
|
+
if (isFinite(degrees)) {
|
|
519
|
+
result.rotate = (degrees * Math.PI) / 180;
|
|
520
|
+
}
|
|
521
|
+
} else if (rotateStr.endsWith('rad')) {
|
|
522
|
+
const radians = parseFloat(rotateStr.slice(0, -3));
|
|
523
|
+
if (isFinite(radians)) {
|
|
524
|
+
result.rotate = radians;
|
|
525
|
+
}
|
|
526
|
+
} else {
|
|
527
|
+
// Assume degrees if no unit
|
|
528
|
+
const degrees = parseFloat(rotateStr);
|
|
529
|
+
if (isFinite(degrees)) {
|
|
530
|
+
result.rotate = (degrees * Math.PI) / 180;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return result;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function parseLengthToken(token: string): number | undefined {
|
|
540
|
+
const trimmed = token.trim();
|
|
541
|
+
if (trimmed.length === 0) {
|
|
542
|
+
return undefined;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if (trimmed.endsWith('px')) {
|
|
546
|
+
const value = parseFloat(trimmed.slice(0, -2));
|
|
547
|
+
return isFinite(value) ? value : undefined;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const value = parseFloat(trimmed);
|
|
551
|
+
return isFinite(value) ? value : undefined;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function splitTopLevelTokens(input: string, separator: 'comma' | 'space'): string[] {
|
|
555
|
+
const tokens: string[] = [];
|
|
556
|
+
let current = '';
|
|
557
|
+
let depth = 0;
|
|
558
|
+
|
|
559
|
+
for (const char of input.trim()) {
|
|
560
|
+
if (char === '(') {
|
|
561
|
+
depth += 1;
|
|
562
|
+
current += char;
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (char === ')') {
|
|
567
|
+
depth = Math.max(0, depth - 1);
|
|
568
|
+
current += char;
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const shouldSplit =
|
|
573
|
+
depth === 0 &&
|
|
574
|
+
(separator === 'comma' ? char === ',' : /\s/.test(char));
|
|
575
|
+
if (shouldSplit) {
|
|
576
|
+
const token = current.trim();
|
|
577
|
+
if (token.length > 0) {
|
|
578
|
+
tokens.push(token);
|
|
579
|
+
}
|
|
580
|
+
current = '';
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
current += char;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const token = current.trim();
|
|
588
|
+
if (token.length > 0) {
|
|
589
|
+
tokens.push(token);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
return tokens;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function firstBoxShadowLayer(input: string): string {
|
|
596
|
+
let depth = 0;
|
|
597
|
+
|
|
598
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
599
|
+
const char = input[index]!;
|
|
600
|
+
if (char === '(') {
|
|
601
|
+
depth += 1;
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
if (char === ')') {
|
|
605
|
+
depth = Math.max(0, depth - 1);
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
if (char === ',' && depth === 0) {
|
|
609
|
+
return input.slice(0, index).trim();
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
return input.trim();
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function splitShadowColor(
|
|
617
|
+
color: ReturnType<typeof parseColor> | undefined,
|
|
618
|
+
): { color?: ReturnType<typeof parseColor>; opacity?: number } {
|
|
619
|
+
if (!color) {
|
|
620
|
+
return {};
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
return {
|
|
624
|
+
color: { ...color, a: 255 },
|
|
625
|
+
opacity: color.a / 255,
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function parseBoxShadow(value: string | undefined): {
|
|
630
|
+
offsetX: number;
|
|
631
|
+
offsetY: number;
|
|
632
|
+
radius: number;
|
|
633
|
+
color?: ReturnType<typeof parseColor>;
|
|
634
|
+
opacity?: number;
|
|
635
|
+
} | undefined {
|
|
636
|
+
if (typeof value !== 'string') {
|
|
637
|
+
return undefined;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const layer = firstBoxShadowLayer(value);
|
|
641
|
+
if (layer.length === 0 || layer === 'none') {
|
|
642
|
+
return undefined;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const tokens = splitTopLevelTokens(layer, 'space').filter((token) => token !== 'inset');
|
|
646
|
+
if (tokens.length < 2) {
|
|
647
|
+
return undefined;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
let colorTokenIndex = -1;
|
|
651
|
+
for (let index = tokens.length - 1; index >= 0; index -= 1) {
|
|
652
|
+
if (parseColor(tokens[index]!)) {
|
|
653
|
+
colorTokenIndex = index;
|
|
654
|
+
break;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const color =
|
|
659
|
+
colorTokenIndex >= 0 ? parseColor(tokens[colorTokenIndex]!) ?? undefined : undefined;
|
|
660
|
+
const lengthTokens = tokens.filter((_, index) => index !== colorTokenIndex);
|
|
661
|
+
const lengths = lengthTokens
|
|
662
|
+
.map((token) => parseLengthToken(token))
|
|
663
|
+
.filter((token): token is number => typeof token === 'number');
|
|
664
|
+
|
|
665
|
+
if (lengths.length < 2) {
|
|
666
|
+
return undefined;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const normalizedColor = splitShadowColor(color);
|
|
670
|
+
return {
|
|
671
|
+
offsetX: lengths[0]!,
|
|
672
|
+
offsetY: lengths[1]!,
|
|
673
|
+
radius: Math.max(0, lengths[2] ?? 0),
|
|
674
|
+
color: normalizedColor.color,
|
|
675
|
+
opacity: normalizedColor.opacity,
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function normalizeTransitionPropertyName(input: string): CanonicalTransitionProperty | undefined {
|
|
680
|
+
return TRANSITION_PROPERTY_ALIASES[input.trim()];
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function normalizePositiveMs(value: unknown, fallback: number): number {
|
|
684
|
+
if (typeof value !== 'number' || !isFinite(value)) {
|
|
685
|
+
return fallback;
|
|
686
|
+
}
|
|
687
|
+
return Math.max(0, Math.round(value));
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function normalizeSpringDuration(value: unknown): number | undefined {
|
|
691
|
+
if (value === undefined || value === null) {
|
|
692
|
+
return undefined;
|
|
693
|
+
}
|
|
694
|
+
return normalizePositiveMs(value, 0);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function normalizeBoolean(value: unknown, fallback: boolean): boolean {
|
|
698
|
+
if (typeof value !== 'boolean') {
|
|
699
|
+
return fallback;
|
|
700
|
+
}
|
|
701
|
+
return value;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function normalizeCubicBezierEasing(value: unknown): CubicBezierEasing | undefined {
|
|
705
|
+
if (!Array.isArray(value) || value.length !== 4) {
|
|
706
|
+
return undefined;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
const [x1, y1, x2, y2] = value;
|
|
710
|
+
if (
|
|
711
|
+
typeof x1 !== 'number' || !isFinite(x1) ||
|
|
712
|
+
typeof y1 !== 'number' || !isFinite(y1) ||
|
|
713
|
+
typeof x2 !== 'number' || !isFinite(x2) ||
|
|
714
|
+
typeof y2 !== 'number' || !isFinite(y2)
|
|
715
|
+
) {
|
|
716
|
+
return undefined;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
return [x1, y1, x2, y2];
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function normalizeTransitionEasing(easing: TransitionEasing | undefined): TransitionEasing {
|
|
723
|
+
if (typeof easing === 'string') {
|
|
724
|
+
return TRANSITION_EASING_ALIASES[easing] ?? TIMING_EASING_DEFAULT;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
const cubicBezier = normalizeCubicBezierEasing(easing);
|
|
728
|
+
return cubicBezier ?? TIMING_EASING_DEFAULT;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function normalizeTransitionConfig(config: TransitionConfig | undefined): CanonicalTransitionConfig | undefined {
|
|
732
|
+
if (!config || config.type === 'none') {
|
|
733
|
+
return undefined;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
if (config.type === 'timing') {
|
|
737
|
+
return {
|
|
738
|
+
type: 'timing',
|
|
739
|
+
duration: normalizePositiveMs(config.duration, TIMING_DURATION_DEFAULT_MS),
|
|
740
|
+
delay: normalizePositiveMs(config.delay, TIMING_DELAY_DEFAULT_MS),
|
|
741
|
+
easing: normalizeTransitionEasing(config.easing),
|
|
742
|
+
respectsReducedMotion: normalizeBoolean(config.respectsReducedMotion, true),
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
return {
|
|
747
|
+
type: 'spring',
|
|
748
|
+
duration: normalizeSpringDuration(config.duration),
|
|
749
|
+
delay: normalizePositiveMs(config.delay, SPRING_DELAY_DEFAULT_MS),
|
|
750
|
+
damping: typeof config.damping === 'number' && isFinite(config.damping) ? config.damping : SPRING_DAMPING_DEFAULT,
|
|
751
|
+
stiffness: typeof config.stiffness === 'number' && isFinite(config.stiffness) ? config.stiffness : SPRING_STIFFNESS_DEFAULT,
|
|
752
|
+
mass: typeof config.mass === 'number' && isFinite(config.mass) ? config.mass : SPRING_MASS_DEFAULT,
|
|
753
|
+
velocity: typeof config.velocity === 'number' && isFinite(config.velocity) ? config.velocity : SPRING_VELOCITY_DEFAULT,
|
|
754
|
+
respectsReducedMotion: normalizeBoolean(config.respectsReducedMotion, true),
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function parseTimeToken(token: string): number | undefined {
|
|
759
|
+
const trimmed = token.trim();
|
|
760
|
+
|
|
761
|
+
if (trimmed.endsWith('ms')) {
|
|
762
|
+
const value = parseFloat(trimmed.slice(0, -2));
|
|
763
|
+
return isFinite(value) ? Math.max(0, Math.round(value)) : undefined;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
if (trimmed.endsWith('s')) {
|
|
767
|
+
const value = parseFloat(trimmed.slice(0, -1));
|
|
768
|
+
return isFinite(value) ? Math.max(0, Math.round(value * 1000)) : undefined;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
return undefined;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function parseCubicBezierFunction(token: string): CubicBezierEasing | undefined {
|
|
775
|
+
const match = /^cubic-bezier\((.+)\)$/i.exec(token.trim());
|
|
776
|
+
if (!match) {
|
|
777
|
+
return undefined;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
const values = match[1]
|
|
781
|
+
.split(',')
|
|
782
|
+
.map((value) => parseFloat(value.trim()));
|
|
783
|
+
|
|
784
|
+
return normalizeCubicBezierEasing(values);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
type ParsedSpringConfig = {
|
|
788
|
+
damping?: number;
|
|
789
|
+
stiffness?: number;
|
|
790
|
+
mass?: number;
|
|
791
|
+
velocity?: number;
|
|
792
|
+
};
|
|
793
|
+
|
|
794
|
+
function parseSpringFunction(token: string): ParsedSpringConfig | undefined {
|
|
795
|
+
const match = /^spring\((.*)\)$/i.exec(token.trim());
|
|
796
|
+
if (!match) {
|
|
797
|
+
return undefined;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
const params: Record<string, number> = {};
|
|
801
|
+
for (const entry of match[1].split(',')) {
|
|
802
|
+
const [rawKey, rawValue] = entry.split(':');
|
|
803
|
+
if (!rawKey || !rawValue) {
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
const key = rawKey.trim();
|
|
807
|
+
const value = parseFloat(rawValue.trim());
|
|
808
|
+
if (isFinite(value)) {
|
|
809
|
+
params[key] = value;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
return {
|
|
814
|
+
damping: params.damping,
|
|
815
|
+
stiffness: params.stiffness,
|
|
816
|
+
mass: params.mass,
|
|
817
|
+
velocity: params.velocity,
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function splitTransitionEntries(input: string): string[] {
|
|
822
|
+
return splitTopLevelTokens(input, 'comma');
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function tokenizeTransitionEntry(entry: string): string[] {
|
|
826
|
+
return splitTopLevelTokens(entry, 'space');
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
function parseTransitionStringEntry(entry: string): [CanonicalTransitionProperty, CanonicalTransitionConfig] | undefined {
|
|
830
|
+
const tokens = tokenizeTransitionEntry(entry);
|
|
831
|
+
if (tokens.length < 2) {
|
|
832
|
+
return undefined;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
const property = normalizeTransitionPropertyName(tokens[0]);
|
|
836
|
+
if (!property) {
|
|
837
|
+
return undefined;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
const initialDuration = parseTimeToken(tokens[1]);
|
|
841
|
+
const initialSpring = parseSpringFunction(tokens[1]);
|
|
842
|
+
if (initialDuration === undefined && !initialSpring) {
|
|
843
|
+
return undefined;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
let duration = initialDuration;
|
|
847
|
+
let delay = 0;
|
|
848
|
+
let easing: TransitionEasing = TIMING_EASING_DEFAULT;
|
|
849
|
+
let config: TransitionConfig = initialSpring
|
|
850
|
+
? {
|
|
851
|
+
type: 'spring',
|
|
852
|
+
duration,
|
|
853
|
+
damping: initialSpring.damping,
|
|
854
|
+
stiffness: initialSpring.stiffness,
|
|
855
|
+
mass: initialSpring.mass,
|
|
856
|
+
velocity: initialSpring.velocity,
|
|
857
|
+
}
|
|
858
|
+
: {
|
|
859
|
+
type: 'timing',
|
|
860
|
+
duration: duration ?? TIMING_DURATION_DEFAULT_MS,
|
|
861
|
+
easing,
|
|
862
|
+
};
|
|
863
|
+
|
|
864
|
+
for (const token of tokens.slice(2)) {
|
|
865
|
+
const spring = parseSpringFunction(token);
|
|
866
|
+
if (spring) {
|
|
867
|
+
config = {
|
|
868
|
+
type: 'spring',
|
|
869
|
+
duration,
|
|
870
|
+
delay,
|
|
871
|
+
damping: typeof spring.damping === 'number' ? spring.damping : undefined,
|
|
872
|
+
stiffness: typeof spring.stiffness === 'number' ? spring.stiffness : undefined,
|
|
873
|
+
mass: typeof spring.mass === 'number' ? spring.mass : undefined,
|
|
874
|
+
velocity: typeof spring.velocity === 'number' ? spring.velocity : undefined,
|
|
875
|
+
};
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
const cubicBezier = parseCubicBezierFunction(token);
|
|
880
|
+
if (cubicBezier) {
|
|
881
|
+
easing = cubicBezier;
|
|
882
|
+
if (config.type === 'timing') {
|
|
883
|
+
config = { ...config, easing };
|
|
884
|
+
}
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
const time = parseTimeToken(token);
|
|
889
|
+
if (time !== undefined) {
|
|
890
|
+
if (duration === undefined) {
|
|
891
|
+
duration = time;
|
|
892
|
+
if (config.type === 'timing') {
|
|
893
|
+
config = { ...config, duration };
|
|
894
|
+
} else {
|
|
895
|
+
config = { ...config, duration };
|
|
896
|
+
}
|
|
897
|
+
} else {
|
|
898
|
+
delay = time;
|
|
899
|
+
config = { ...config, delay };
|
|
900
|
+
}
|
|
901
|
+
continue;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
const easingName = TRANSITION_EASING_ALIASES[token];
|
|
905
|
+
if (easingName) {
|
|
906
|
+
easing = easingName;
|
|
907
|
+
if (config.type === 'timing') {
|
|
908
|
+
config = { ...config, easing };
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
const normalized = normalizeTransitionConfig(config);
|
|
914
|
+
return normalized ? [property, normalized] : undefined;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
/**
|
|
918
|
+
* Normalize transition metadata from the style object.
|
|
919
|
+
*
|
|
920
|
+
* The normalized transition map intentionally lives outside CanonicalStyle so
|
|
921
|
+
* style diffing and protocol emission can treat transition metadata separately.
|
|
922
|
+
*/
|
|
923
|
+
export function normalizeTransition(transition: StyleTransition | undefined): CanonicalTransitionMap {
|
|
924
|
+
if (!transition) {
|
|
925
|
+
return {};
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
if (typeof transition === 'string') {
|
|
929
|
+
const trimmed = transition.trim();
|
|
930
|
+
if (trimmed.length === 0 || trimmed === 'none') {
|
|
931
|
+
return {};
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
const normalized: CanonicalTransitionMap = {};
|
|
935
|
+
for (const entry of splitTransitionEntries(trimmed)) {
|
|
936
|
+
const parsed = parseTransitionStringEntry(entry);
|
|
937
|
+
if (!parsed) {
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
940
|
+
const [property, config] = parsed;
|
|
941
|
+
normalized[property] = config;
|
|
942
|
+
}
|
|
943
|
+
return normalized;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
const normalized: CanonicalTransitionMap = {};
|
|
947
|
+
for (const [propertyName, config] of Object.entries(transition)) {
|
|
948
|
+
const property = normalizeTransitionPropertyName(propertyName);
|
|
949
|
+
if (!property) {
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
const normalizedConfig = normalizeTransitionConfig(config);
|
|
954
|
+
if (normalizedConfig) {
|
|
955
|
+
normalized[property] = normalizedConfig;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
return normalized;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
/**
|
|
963
|
+
* Compare two normalized transition maps.
|
|
964
|
+
*/
|
|
965
|
+
export function transitionsEqual(a: CanonicalTransitionMap, b: CanonicalTransitionMap): boolean {
|
|
966
|
+
if (a === b) {
|
|
967
|
+
return true;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
const keysA = Object.keys(a) as CanonicalTransitionProperty[];
|
|
971
|
+
const keysB = Object.keys(b) as CanonicalTransitionProperty[];
|
|
972
|
+
|
|
973
|
+
if (keysA.length !== keysB.length) {
|
|
974
|
+
return false;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
for (const key of keysA) {
|
|
978
|
+
const valA = a[key];
|
|
979
|
+
const valB = b[key];
|
|
980
|
+
|
|
981
|
+
if (!valA || !valB || valA.type !== valB.type) {
|
|
982
|
+
return false;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
if (valA.delay !== valB.delay || valA.respectsReducedMotion !== valB.respectsReducedMotion) {
|
|
986
|
+
return false;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
if (valA.type === 'timing' && valB.type === 'timing') {
|
|
990
|
+
if (valA.duration !== valB.duration) {
|
|
991
|
+
return false;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
const easingA = valA.easing;
|
|
995
|
+
const easingB = valB.easing;
|
|
996
|
+
if (typeof easingA === 'string' || typeof easingB === 'string') {
|
|
997
|
+
if (easingA !== easingB) {
|
|
998
|
+
return false;
|
|
999
|
+
}
|
|
1000
|
+
} else if (
|
|
1001
|
+
easingA[0] !== easingB[0] ||
|
|
1002
|
+
easingA[1] !== easingB[1] ||
|
|
1003
|
+
easingA[2] !== easingB[2] ||
|
|
1004
|
+
easingA[3] !== easingB[3]
|
|
1005
|
+
) {
|
|
1006
|
+
return false;
|
|
1007
|
+
}
|
|
1008
|
+
continue;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
if (valA.type === 'spring' && valB.type === 'spring') {
|
|
1012
|
+
if (
|
|
1013
|
+
valA.duration !== valB.duration ||
|
|
1014
|
+
valA.damping !== valB.damping ||
|
|
1015
|
+
valA.stiffness !== valB.stiffness ||
|
|
1016
|
+
valA.mass !== valB.mass ||
|
|
1017
|
+
valA.velocity !== valB.velocity
|
|
1018
|
+
) {
|
|
1019
|
+
return false;
|
|
1020
|
+
}
|
|
1021
|
+
continue;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
return false;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
return true;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
/**
|
|
1031
|
+
* Normalize a style object into canonical internal format.
|
|
1032
|
+
*
|
|
1033
|
+
* @param style - The user-provided style object
|
|
1034
|
+
* @param defaultFlexDirection - The default flex direction for this element type
|
|
1035
|
+
* @returns The normalized canonical style
|
|
1036
|
+
*/
|
|
1037
|
+
export function normalizeStyle(
|
|
1038
|
+
style: FullStyle | undefined,
|
|
1039
|
+
defaultFlexDirection: FlexDirection = 'row',
|
|
1040
|
+
options: NormalizeStyleOptions = {},
|
|
1041
|
+
): CanonicalStyle {
|
|
1042
|
+
if (!style) {
|
|
1043
|
+
return {};
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
const canonical: CanonicalStyle = {};
|
|
1047
|
+
const styleRecord = style as Record<string, unknown>;
|
|
1048
|
+
const explicitDirection = parseExplicitDirection(style.direction);
|
|
1049
|
+
const resolvedDirection: ResolvedDirection =
|
|
1050
|
+
explicitDirection === 'ltr' || explicitDirection === 'rtl'
|
|
1051
|
+
? explicitDirection
|
|
1052
|
+
: options.resolvedDirection ?? 'ltr';
|
|
1053
|
+
const explicitWritingMode = parseWritingMode(style.writingMode);
|
|
1054
|
+
const resolvedWritingMode = explicitWritingMode ?? options.resolvedWritingMode ?? 'horizontal-tb';
|
|
1055
|
+
|
|
1056
|
+
if (options.includeInheritedValues === true || explicitDirection === 'ltr' || explicitDirection === 'rtl') {
|
|
1057
|
+
canonical.direction = resolvedDirection;
|
|
1058
|
+
}
|
|
1059
|
+
if (options.includeInheritedValues === true || explicitWritingMode !== undefined) {
|
|
1060
|
+
canonical.writingMode = resolvedWritingMode;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// ==========================================================================
|
|
1064
|
+
// Dimensions
|
|
1065
|
+
// ==========================================================================
|
|
1066
|
+
|
|
1067
|
+
for (const key of BOX_DIMENSION_KEYS) {
|
|
1068
|
+
setDimension(canonical, key, style[key]);
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
// ==========================================================================
|
|
1072
|
+
// Padding (shorthand first, then specific overrides)
|
|
1073
|
+
// ==========================================================================
|
|
1074
|
+
|
|
1075
|
+
// Handle padding shorthand
|
|
1076
|
+
const padding = parseDimension(style.padding);
|
|
1077
|
+
if (padding) {
|
|
1078
|
+
setDimensionEdges(canonical, PADDING_EDGE_KEYS, padding);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// Handle paddingVertical/paddingHorizontal
|
|
1082
|
+
const paddingVertical = parseDimension(style.paddingVertical);
|
|
1083
|
+
if (paddingVertical) {
|
|
1084
|
+
setDimensionValue(canonical, PADDING_KEYS.top, paddingVertical);
|
|
1085
|
+
setDimensionValue(canonical, PADDING_KEYS.bottom, paddingVertical);
|
|
1086
|
+
}
|
|
1087
|
+
const paddingHorizontal = parseDimension(style.paddingHorizontal);
|
|
1088
|
+
if (paddingHorizontal) {
|
|
1089
|
+
setDimensionValue(canonical, PADDING_KEYS.right, paddingHorizontal);
|
|
1090
|
+
setDimensionValue(canonical, PADDING_KEYS.left, paddingHorizontal);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
const paddingStart = parseDimension(
|
|
1094
|
+
(style.paddingStart ?? style.paddingInlineStart ?? styleRecord['padding-inline-start']) as
|
|
1095
|
+
| DimensionInput
|
|
1096
|
+
| undefined,
|
|
1097
|
+
);
|
|
1098
|
+
if (paddingStart) {
|
|
1099
|
+
assignPaddingEdge(canonical, inlineStartEdge(resolvedDirection, resolvedWritingMode), paddingStart);
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
const paddingEnd = parseDimension(
|
|
1103
|
+
(style.paddingEnd ?? style.paddingInlineEnd ?? styleRecord['padding-inline-end']) as
|
|
1104
|
+
| DimensionInput
|
|
1105
|
+
| undefined,
|
|
1106
|
+
);
|
|
1107
|
+
if (paddingEnd) {
|
|
1108
|
+
assignPaddingEdge(canonical, inlineEndEdge(resolvedDirection, resolvedWritingMode), paddingEnd);
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
// Handle specific padding (highest precedence)
|
|
1112
|
+
for (const key of PADDING_EDGE_KEYS) {
|
|
1113
|
+
setDimension(canonical, key, style[key]);
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// ==========================================================================
|
|
1117
|
+
// Margin (shorthand first, then specific overrides)
|
|
1118
|
+
// ==========================================================================
|
|
1119
|
+
|
|
1120
|
+
// Handle margin shorthand
|
|
1121
|
+
const margin = parseDimension(style.margin);
|
|
1122
|
+
if (margin) {
|
|
1123
|
+
setDimensionEdges(canonical, MARGIN_EDGE_KEYS, margin);
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
// Handle marginVertical/marginHorizontal
|
|
1127
|
+
const marginVertical = parseDimension(style.marginVertical);
|
|
1128
|
+
if (marginVertical) {
|
|
1129
|
+
setDimensionValue(canonical, MARGIN_KEYS.top, marginVertical);
|
|
1130
|
+
setDimensionValue(canonical, MARGIN_KEYS.bottom, marginVertical);
|
|
1131
|
+
}
|
|
1132
|
+
const marginHorizontal = parseDimension(style.marginHorizontal);
|
|
1133
|
+
if (marginHorizontal) {
|
|
1134
|
+
setDimensionValue(canonical, MARGIN_KEYS.right, marginHorizontal);
|
|
1135
|
+
setDimensionValue(canonical, MARGIN_KEYS.left, marginHorizontal);
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
const marginStart = parseDimension(
|
|
1139
|
+
(style.marginStart ?? style.marginInlineStart ?? styleRecord['margin-inline-start']) as
|
|
1140
|
+
| DimensionInput
|
|
1141
|
+
| undefined,
|
|
1142
|
+
);
|
|
1143
|
+
if (marginStart) {
|
|
1144
|
+
assignMarginEdge(canonical, inlineStartEdge(resolvedDirection, resolvedWritingMode), marginStart);
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
const marginEnd = parseDimension(
|
|
1148
|
+
(style.marginEnd ?? style.marginInlineEnd ?? styleRecord['margin-inline-end']) as
|
|
1149
|
+
| DimensionInput
|
|
1150
|
+
| undefined,
|
|
1151
|
+
);
|
|
1152
|
+
if (marginEnd) {
|
|
1153
|
+
assignMarginEdge(canonical, inlineEndEdge(resolvedDirection, resolvedWritingMode), marginEnd);
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
// Handle specific margin (highest precedence)
|
|
1157
|
+
for (const key of MARGIN_EDGE_KEYS) {
|
|
1158
|
+
setDimension(canonical, key, style[key]);
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
// ==========================================================================
|
|
1162
|
+
// Flexbox
|
|
1163
|
+
// ==========================================================================
|
|
1164
|
+
|
|
1165
|
+
// Handle 'flex' shorthand
|
|
1166
|
+
if (typeof style.flex === 'number' && isFinite(style.flex)) {
|
|
1167
|
+
// CSS spec: flex: N means flexGrow: N, flexShrink: 1, flexBasis: 0
|
|
1168
|
+
canonical.flexGrow = style.flex;
|
|
1169
|
+
canonical.flexShrink = 1;
|
|
1170
|
+
canonical.flexBasis = { type: 'points', value: 0 };
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
// Specific flex properties override shorthand
|
|
1174
|
+
assignNumberStyles(canonical, style, FLEX_NUMBER_ASSIGNMENTS);
|
|
1175
|
+
|
|
1176
|
+
setDimension(canonical, 'flexBasis', style.flexBasis);
|
|
1177
|
+
|
|
1178
|
+
// Flex direction - apply default if not specified
|
|
1179
|
+
if (style.flexDirection !== undefined) {
|
|
1180
|
+
canonical.flexDirection = style.flexDirection;
|
|
1181
|
+
} else if (defaultFlexDirection !== 'row') {
|
|
1182
|
+
// Only include if non-web default
|
|
1183
|
+
canonical.flexDirection = defaultFlexDirection;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
assignDefinedStyles(canonical, style, FLEX_DIRECT_ASSIGNMENTS);
|
|
1187
|
+
|
|
1188
|
+
// Gap
|
|
1189
|
+
if (typeof style.gap === 'number' && isFinite(style.gap)) {
|
|
1190
|
+
canonical.rowGap = style.gap;
|
|
1191
|
+
canonical.columnGap = style.gap;
|
|
1192
|
+
}
|
|
1193
|
+
assignNumberStyles(canonical, style, GAP_NUMBER_ASSIGNMENTS);
|
|
1194
|
+
|
|
1195
|
+
// ==========================================================================
|
|
1196
|
+
// Position
|
|
1197
|
+
// ==========================================================================
|
|
1198
|
+
|
|
1199
|
+
if (style.position !== undefined) {
|
|
1200
|
+
canonical.positionType = style.position;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
setDimension(canonical, 'top', style.top);
|
|
1204
|
+
|
|
1205
|
+
const start = parseDimension(
|
|
1206
|
+
(style.start ?? style.insetInlineStart ?? styleRecord['inset-inline-start']) as
|
|
1207
|
+
| DimensionInput
|
|
1208
|
+
| undefined,
|
|
1209
|
+
);
|
|
1210
|
+
if (start) {
|
|
1211
|
+
assignInsetEdge(canonical, inlineStartEdge(resolvedDirection, resolvedWritingMode), start);
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
const end = parseDimension(
|
|
1215
|
+
(style.end ?? style.insetInlineEnd ?? styleRecord['inset-inline-end']) as
|
|
1216
|
+
| DimensionInput
|
|
1217
|
+
| undefined,
|
|
1218
|
+
);
|
|
1219
|
+
if (end) {
|
|
1220
|
+
assignInsetEdge(canonical, inlineEndEdge(resolvedDirection, resolvedWritingMode), end);
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
for (const key of POSITION_TRAILING_EDGE_KEYS) {
|
|
1224
|
+
setDimension(canonical, key, style[key]);
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
if (typeof style.zIndex === 'number' && isFinite(style.zIndex)) {
|
|
1228
|
+
canonical.zIndex = style.zIndex;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
// ==========================================================================
|
|
1232
|
+
// Transform
|
|
1233
|
+
// ==========================================================================
|
|
1234
|
+
|
|
1235
|
+
if (style.transform) {
|
|
1236
|
+
const transforms = extractTransforms(style.transform);
|
|
1237
|
+
if (transforms.x !== undefined) canonical.transformX = transforms.x;
|
|
1238
|
+
if (transforms.y !== undefined) canonical.transformY = transforms.y;
|
|
1239
|
+
if (transforms.scale !== undefined) canonical.transformScale = transforms.scale;
|
|
1240
|
+
if (transforms.rotate !== undefined) canonical.transformRotate = transforms.rotate;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
// ==========================================================================
|
|
1244
|
+
// Appearance
|
|
1245
|
+
// ==========================================================================
|
|
1246
|
+
|
|
1247
|
+
if (style.display === 'flex' || style.display === 'grid' || style.display === 'none') {
|
|
1248
|
+
canonical.display = style.display;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
if (style.backgroundColor) {
|
|
1252
|
+
const color = parseColor(style.backgroundColor);
|
|
1253
|
+
if (color) canonical.backgroundColor = color;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
assignNumberStyles(canonical, style, APPEARANCE_NUMBER_ASSIGNMENTS);
|
|
1257
|
+
|
|
1258
|
+
if (style.borderColor) {
|
|
1259
|
+
const color = parseColor(style.borderColor);
|
|
1260
|
+
if (color) canonical.borderColor = color;
|
|
1261
|
+
}
|
|
1262
|
+
// Per-side border colors (ENG-22085).
|
|
1263
|
+
if (style.borderTopColor) {
|
|
1264
|
+
const color = parseColor(style.borderTopColor);
|
|
1265
|
+
if (color) canonical.borderTopColor = color;
|
|
1266
|
+
}
|
|
1267
|
+
if (style.borderRightColor) {
|
|
1268
|
+
const color = parseColor(style.borderRightColor);
|
|
1269
|
+
if (color) canonical.borderRightColor = color;
|
|
1270
|
+
}
|
|
1271
|
+
if (style.borderBottomColor) {
|
|
1272
|
+
const color = parseColor(style.borderBottomColor);
|
|
1273
|
+
if (color) canonical.borderBottomColor = color;
|
|
1274
|
+
}
|
|
1275
|
+
if (style.borderLeftColor) {
|
|
1276
|
+
const color = parseColor(style.borderLeftColor);
|
|
1277
|
+
if (color) canonical.borderLeftColor = color;
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
if (style.boxShadow) {
|
|
1281
|
+
const boxShadow = parseBoxShadow(style.boxShadow);
|
|
1282
|
+
if (boxShadow?.color) {
|
|
1283
|
+
canonical.shadowColor = boxShadow.color;
|
|
1284
|
+
}
|
|
1285
|
+
if (boxShadow?.opacity !== undefined) {
|
|
1286
|
+
canonical.shadowOpacity = boxShadow.opacity;
|
|
1287
|
+
}
|
|
1288
|
+
if (boxShadow) {
|
|
1289
|
+
canonical.shadowOffsetX = boxShadow.offsetX;
|
|
1290
|
+
canonical.shadowOffsetY = boxShadow.offsetY;
|
|
1291
|
+
canonical.shadowRadius = boxShadow.radius;
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
if (style.shadowColor) {
|
|
1296
|
+
const normalizedShadow = splitShadowColor(parseColor(style.shadowColor));
|
|
1297
|
+
if (normalizedShadow.color) {
|
|
1298
|
+
canonical.shadowColor = normalizedShadow.color;
|
|
1299
|
+
}
|
|
1300
|
+
if (normalizedShadow.opacity !== undefined) {
|
|
1301
|
+
canonical.shadowOpacity = normalizedShadow.opacity;
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
if (
|
|
1306
|
+
typeof style.shadowOffset === 'object' &&
|
|
1307
|
+
style.shadowOffset !== null &&
|
|
1308
|
+
typeof style.shadowOffset.width === 'number' &&
|
|
1309
|
+
isFinite(style.shadowOffset.width) &&
|
|
1310
|
+
typeof style.shadowOffset.height === 'number' &&
|
|
1311
|
+
isFinite(style.shadowOffset.height)
|
|
1312
|
+
) {
|
|
1313
|
+
canonical.shadowOffsetX = style.shadowOffset.width;
|
|
1314
|
+
canonical.shadowOffsetY = style.shadowOffset.height;
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
assignNumberStyles(canonical, style, SHADOW_NUMBER_ASSIGNMENTS);
|
|
1318
|
+
|
|
1319
|
+
if (style.overflow !== undefined) {
|
|
1320
|
+
canonical.overflow = style.overflow;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
// ==========================================================================
|
|
1324
|
+
// Typography
|
|
1325
|
+
// ==========================================================================
|
|
1326
|
+
|
|
1327
|
+
if (style.fontFamily !== undefined) {
|
|
1328
|
+
const fontFamily = resolveFontFamilyId(style.fontFamily);
|
|
1329
|
+
if (fontFamily !== undefined) {
|
|
1330
|
+
canonical.fontFamily = fontFamily;
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
assignNumberStyles(canonical, style, TYPOGRAPHY_NUMBER_ASSIGNMENTS);
|
|
1335
|
+
|
|
1336
|
+
if (style.color) {
|
|
1337
|
+
const color = parseColor(style.color);
|
|
1338
|
+
if (color) canonical.textColor = color;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
const fontWeight = parseFontWeight(style.fontWeight);
|
|
1342
|
+
if (fontWeight !== undefined) canonical.fontWeight = fontWeight;
|
|
1343
|
+
|
|
1344
|
+
if (style.fontStyle === 'normal' || style.fontStyle === 'italic') {
|
|
1345
|
+
canonical.fontStyle = style.fontStyle;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
if (typeof style.fontVariantNumeric === 'string') {
|
|
1349
|
+
const fontVariantNumeric = parseFontVariantNumeric(style.fontVariantNumeric);
|
|
1350
|
+
if (fontVariantNumeric !== 0) {
|
|
1351
|
+
canonical.fontVariantNumeric = fontVariantNumeric;
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
if (style.textAlign !== undefined) {
|
|
1356
|
+
canonical.textAlign = style.textAlign;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
if (
|
|
1360
|
+
style.textDecorationLine === 'none' ||
|
|
1361
|
+
style.textDecorationLine === 'underline' ||
|
|
1362
|
+
style.textDecorationLine === 'line-through' ||
|
|
1363
|
+
style.textDecorationLine === 'underline line-through'
|
|
1364
|
+
) {
|
|
1365
|
+
canonical.textDecorationLine = style.textDecorationLine;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
if (style.ellipsizeMode !== undefined) {
|
|
1369
|
+
canonical.ellipsizeMode = style.ellipsizeMode;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
return canonical;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
/**
|
|
1376
|
+
* Compare two canonical styles and return true if they are equal.
|
|
1377
|
+
*
|
|
1378
|
+
* @param a - First style
|
|
1379
|
+
* @param b - Second style
|
|
1380
|
+
* @returns True if styles are equal
|
|
1381
|
+
*/
|
|
1382
|
+
export function stylesEqual(a: CanonicalStyle, b: CanonicalStyle): boolean {
|
|
1383
|
+
// Quick reference check
|
|
1384
|
+
if (a === b) return true;
|
|
1385
|
+
|
|
1386
|
+
// Compare all keys
|
|
1387
|
+
const keysA = Object.keys(a) as (keyof CanonicalStyle)[];
|
|
1388
|
+
const keysB = Object.keys(b) as (keyof CanonicalStyle)[];
|
|
1389
|
+
|
|
1390
|
+
if (keysA.length !== keysB.length) return false;
|
|
1391
|
+
|
|
1392
|
+
for (const key of keysA) {
|
|
1393
|
+
const valA = a[key];
|
|
1394
|
+
const valB = b[key];
|
|
1395
|
+
|
|
1396
|
+
if (valA === valB) continue;
|
|
1397
|
+
|
|
1398
|
+
// Deep compare for objects
|
|
1399
|
+
if (typeof valA === 'object' && typeof valB === 'object' && valA !== null && valB !== null) {
|
|
1400
|
+
// Compare dimension values
|
|
1401
|
+
if ('type' in valA && 'type' in valB) {
|
|
1402
|
+
if (valA.type !== valB.type) return false;
|
|
1403
|
+
if ('value' in valA && 'value' in valB) {
|
|
1404
|
+
if (valA.value !== valB.value) return false;
|
|
1405
|
+
}
|
|
1406
|
+
continue;
|
|
1407
|
+
}
|
|
1408
|
+
// Compare RGBA colors
|
|
1409
|
+
if ('r' in valA && 'r' in valB) {
|
|
1410
|
+
if (
|
|
1411
|
+
valA.r !== valB.r ||
|
|
1412
|
+
valA.g !== valB.g ||
|
|
1413
|
+
valA.b !== valB.b ||
|
|
1414
|
+
valA.a !== valB.a
|
|
1415
|
+
) {
|
|
1416
|
+
return false;
|
|
1417
|
+
}
|
|
1418
|
+
continue;
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
return false;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
return true;
|
|
1426
|
+
}
|