@symbiote-native/css-parser 0.2.3 → 0.4.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/README.md +46 -26
- package/build/generate-dts/index.js +3 -8
- package/build/generate-dts-cli.js +5 -1
- package/build/golden-corpus/fixtures/ScopedGlobalDemo.svelte +25 -0
- package/build/golden-corpus/fixtures/ScopedGlobalDemo.svelte.d.ts +3 -0
- package/build/index.d.ts +8 -7
- package/build/index.js +4 -4
- package/build/lightning/declarations.d.ts +46 -0
- package/build/lightning/declarations.js +885 -0
- package/build/lightning/rules.d.ts +52 -0
- package/build/lightning/rules.js +157 -0
- package/build/lightning/selectors.d.ts +21 -0
- package/build/lightning/selectors.js +372 -0
- package/build/metro-css-module/index.d.ts +30 -2
- package/build/metro-css-module/index.js +142 -36
- package/build/metro-transformer/index.js +14 -22
- package/build/preprocessors/index.js +4 -3
- package/build/properties.d.ts +5 -6
- package/build/properties.js +29 -17
- package/build/scoped-classes.d.ts +23 -0
- package/build/scoped-classes.js +35 -0
- package/build/values.d.ts +3 -7
- package/build/values.js +3 -60
- package/package.json +2 -3
- package/typescript-plugin.cjs +32 -25
- package/build/global-selectors.d.ts +0 -1
- package/build/global-selectors.js +0 -22
- package/build/parser/index.d.ts +0 -22
- package/build/parser/index.js +0 -221
|
@@ -0,0 +1,885 @@
|
|
|
1
|
+
// Declaration half of the lightningcss-typed CSS pipeline: one lightningcss `Declaration` object
|
|
2
|
+
// (from a visitor, or out of `rule.value.declarations.declarations`) → zero or more React Native
|
|
3
|
+
// style entries.
|
|
4
|
+
//
|
|
5
|
+
// WHY it maps off the typed AST: the retired text mapper re-parsed the declaration TEXT, so it
|
|
6
|
+
// re-derived structure lightningcss already hands over, and it lost what it never looked at.
|
|
7
|
+
// Three shipped bugs came straight from that (traps 4, 5 and the `var()` one in
|
|
8
|
+
// `.claude/rules/style-registry-collisions.md`):
|
|
9
|
+
//
|
|
10
|
+
// padding: 8px 16px text pass → { padding: 8 } the 16 is silently gone
|
|
11
|
+
// width: calc(100% - 24px) text pass → { width: 100 } paints ~100pt wide
|
|
12
|
+
// color: var(--from-other) text pass → { color: 'var(--from-other)' } literal ships to Fabric
|
|
13
|
+
//
|
|
14
|
+
// lightningcss hands all three back typed: a shorthand arrives PRE-EXPANDED as a Rect, a `calc()`
|
|
15
|
+
// arrives as a sum tree with per-operand units, and a `var()` arrives as an `unparsed` declaration
|
|
16
|
+
// carrying real var tokens. So each bug becomes a decision rather than an accident — expand the
|
|
17
|
+
// Rect, evaluate the sum only when every operand reduces to px, and drop an unresolvable `var()`
|
|
18
|
+
// with a warning instead of emitting its text.
|
|
19
|
+
//
|
|
20
|
+
// The property NAME table (`../properties.ts`) outlived the text pass and is still the one place
|
|
21
|
+
// that says what RN calls a property and how the "unsupported property dropped" warning is
|
|
22
|
+
// worded. Only value conversion lives here, against the typed AST.
|
|
23
|
+
//
|
|
24
|
+
// `!important` needs nothing special: lightningcss files those under
|
|
25
|
+
// `declarations.importantDeclarations`, and the caller feeds them in like any other declaration.
|
|
26
|
+
import { transform } from 'lightningcss';
|
|
27
|
+
import { PROPERTY_TABLE, mapCSSProperty } from "../properties.js";
|
|
28
|
+
import { REM_TO_PX, warnOnce } from "../values.js";
|
|
29
|
+
//#region warnings
|
|
30
|
+
// Warn-once bookkeeping is keyed by FILE because the public entry point takes one declaration at a
|
|
31
|
+
// time — there is no per-run object to hang a `Set` off the way `compileCssToRules` does per call.
|
|
32
|
+
// A Metro watch process therefore warns once per (file, property) for the life of the process,
|
|
33
|
+
// which is the intended granularity: the same bad property in the same file is one authoring
|
|
34
|
+
// mistake, however many times it is recompiled.
|
|
35
|
+
const warnedByFile = new Map();
|
|
36
|
+
function warnedKeysFor(filename) {
|
|
37
|
+
const existing = warnedByFile.get(filename);
|
|
38
|
+
if (existing)
|
|
39
|
+
return existing;
|
|
40
|
+
const created = new Set();
|
|
41
|
+
warnedByFile.set(filename, created);
|
|
42
|
+
return created;
|
|
43
|
+
}
|
|
44
|
+
function warnDrop(context, key, reason) {
|
|
45
|
+
warnOnce(warnedKeysFor(context.filename), key, `[@symbiote-native/css-parser] ${reason}, dropped`);
|
|
46
|
+
}
|
|
47
|
+
//#endregion warnings
|
|
48
|
+
//#region unknown-value readers
|
|
49
|
+
function isRecord(value) {
|
|
50
|
+
return typeof value === 'object' && value !== null;
|
|
51
|
+
}
|
|
52
|
+
function readNumber(value) {
|
|
53
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
54
|
+
}
|
|
55
|
+
function readString(value) {
|
|
56
|
+
return typeof value === 'string' ? value : null;
|
|
57
|
+
}
|
|
58
|
+
function readType(value) {
|
|
59
|
+
return isRecord(value) ? readString(value.type) : null;
|
|
60
|
+
}
|
|
61
|
+
// lightningcss stores lengths, alphas and ratios as f32, so `1.4` comes back as
|
|
62
|
+
// 1.399999976158142. Seven significant digits is the round-trip precision of f32 — it restores the
|
|
63
|
+
// authored decimal without inventing one.
|
|
64
|
+
function roundFloat(value) {
|
|
65
|
+
return Number(value.toPrecision(7));
|
|
66
|
+
}
|
|
67
|
+
//#endregion unknown-value readers
|
|
68
|
+
//#region lengths
|
|
69
|
+
const ABSOLUTE_UNIT_TO_PX = {
|
|
70
|
+
px: 1,
|
|
71
|
+
in: 96,
|
|
72
|
+
cm: 96 / 2.54,
|
|
73
|
+
mm: 96 / 25.4,
|
|
74
|
+
q: 96 / 101.6,
|
|
75
|
+
pt: 96 / 72,
|
|
76
|
+
pc: 16,
|
|
77
|
+
};
|
|
78
|
+
// RN's `borderWidth` takes a number, so the CSS keywords have to become the browser-default points.
|
|
79
|
+
const BORDER_WIDTH_KEYWORD_PX = {
|
|
80
|
+
thin: 1,
|
|
81
|
+
medium: 3,
|
|
82
|
+
thick: 5,
|
|
83
|
+
};
|
|
84
|
+
function unitToPx(unit, amount, context) {
|
|
85
|
+
// `em` is scaled by the root size exactly as the old parser does: this package resolves styles
|
|
86
|
+
// without a box tree, so there is no parent font size to scale against.
|
|
87
|
+
if (unit === 'rem' || unit === 'em') {
|
|
88
|
+
return amount * (context.remToPx ?? REM_TO_PX);
|
|
89
|
+
}
|
|
90
|
+
const factor = ABSOLUTE_UNIT_TO_PX[unit];
|
|
91
|
+
return factor === undefined ? null : amount * factor;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Reduce any of lightningcss's length-ish value wrappers to px / percent / auto, or `null` when RN
|
|
95
|
+
* cannot express it (a viewport unit, an unevaluable `calc()`, `normal`).
|
|
96
|
+
*
|
|
97
|
+
* The wrappers nest differently per property — `font-size` is `{length:{dimension:{unit,value}}}`,
|
|
98
|
+
* `letter-spacing` is `{length:{value:{unit,value}}}`, `padding-top` is
|
|
99
|
+
* `{length-percentage:{dimension:{unit,value}}}` — so this walks the wrappers instead of encoding
|
|
100
|
+
* one shape per property.
|
|
101
|
+
*/
|
|
102
|
+
function readLength(value, context) {
|
|
103
|
+
if (!isRecord(value))
|
|
104
|
+
return null;
|
|
105
|
+
// A bare `LengthValue` (`{unit, value}`) — the leaf every wrapper bottoms out at.
|
|
106
|
+
const unit = readString(value.unit);
|
|
107
|
+
const amount = readNumber(value.value);
|
|
108
|
+
if (unit !== null && amount !== null) {
|
|
109
|
+
const px = unitToPx(unit, amount, context);
|
|
110
|
+
return px === null ? null : { kind: 'px', value: px };
|
|
111
|
+
}
|
|
112
|
+
switch (value.type) {
|
|
113
|
+
case 'auto':
|
|
114
|
+
return { kind: 'auto', value: 0 };
|
|
115
|
+
case 'percentage': {
|
|
116
|
+
const fraction = readNumber(value.value);
|
|
117
|
+
// lightningcss reports a percentage as a FRACTION: `100%` arrives as 1.
|
|
118
|
+
return fraction === null
|
|
119
|
+
? null
|
|
120
|
+
: { kind: 'percent', value: fraction * 100 };
|
|
121
|
+
}
|
|
122
|
+
// RN's `lineHeight` has no multiplier form, so a unitless `line-height: 1.4` becomes 1.4
|
|
123
|
+
// points — wrong, but it is what the retired text pass already shipped and not this file's fix.
|
|
124
|
+
case 'number':
|
|
125
|
+
case 'integer': {
|
|
126
|
+
const plain = readNumber(value.value);
|
|
127
|
+
return plain === null ? null : { kind: 'px', value: plain };
|
|
128
|
+
}
|
|
129
|
+
case 'calc':
|
|
130
|
+
return evaluateCalc(value.value, context);
|
|
131
|
+
case 'length':
|
|
132
|
+
case 'length-percentage':
|
|
133
|
+
case 'dimension':
|
|
134
|
+
case 'value':
|
|
135
|
+
return readLength(value.value, context);
|
|
136
|
+
default: {
|
|
137
|
+
const keyword = readString(value.type);
|
|
138
|
+
const px = keyword === null ? undefined : BORDER_WIDTH_KEYWORD_PX[keyword];
|
|
139
|
+
return px === undefined ? null : { kind: 'px', value: px };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function lengthToStyleValue(length) {
|
|
144
|
+
switch (length.kind) {
|
|
145
|
+
case 'auto':
|
|
146
|
+
return 'auto';
|
|
147
|
+
case 'percent':
|
|
148
|
+
return `${roundFloat(length.value)}%`;
|
|
149
|
+
case 'px':
|
|
150
|
+
return roundFloat(length.value);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
//#endregion lengths
|
|
154
|
+
//#region calc()
|
|
155
|
+
/**
|
|
156
|
+
* Evaluate a `calc()` sum when EVERY operand reduces to px; return `null` otherwise.
|
|
157
|
+
*
|
|
158
|
+
* lightningcss already folds a same-unit expression before we see it (`calc(10px + 4px)` arrives
|
|
159
|
+
* as `14px`, `calc(2rem * 2)` as `4rem`), so what reaches here is a mixed expression: either mixed
|
|
160
|
+
* LENGTHS (`calc(1rem + 2px)` — evaluable once rem is in px) or a length mixed with a percentage /
|
|
161
|
+
* viewport unit (`calc(100% - 24px)` — RN has no expression form, so the caller drops it).
|
|
162
|
+
*/
|
|
163
|
+
function evaluateCalc(node, context) {
|
|
164
|
+
if (!isRecord(node))
|
|
165
|
+
return null;
|
|
166
|
+
switch (node.type) {
|
|
167
|
+
case 'function':
|
|
168
|
+
case 'calc':
|
|
169
|
+
return evaluateCalc(node.value, context);
|
|
170
|
+
case 'sum': {
|
|
171
|
+
if (!Array.isArray(node.value))
|
|
172
|
+
return null;
|
|
173
|
+
let total = 0;
|
|
174
|
+
for (const operand of node.value) {
|
|
175
|
+
const term = evaluateCalc(operand, context);
|
|
176
|
+
if (term === null || term.kind !== 'px')
|
|
177
|
+
return null;
|
|
178
|
+
total += term.value;
|
|
179
|
+
}
|
|
180
|
+
return { kind: 'px', value: total };
|
|
181
|
+
}
|
|
182
|
+
case 'value':
|
|
183
|
+
return readLength(node.value, context);
|
|
184
|
+
// `min()` / `max()` / `clamp()` / `product` — no RN equivalent, and guessing one is how the
|
|
185
|
+
// old evaluator ended up emitting 100 for `calc(100% - 24px)`.
|
|
186
|
+
default:
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
//#endregion calc()
|
|
191
|
+
//#region colors
|
|
192
|
+
function toHexChannel(channel) {
|
|
193
|
+
const clamped = Math.min(255, Math.max(0, Math.round(channel)));
|
|
194
|
+
return clamped.toString(16).padStart(2, '0');
|
|
195
|
+
}
|
|
196
|
+
// `rgba(0, 0, 0, .3)` survives lightningcss as alpha 0.3019607961177826 — the f32 of 77/255, since
|
|
197
|
+
// alpha is stored 8-bit. Print the shortest decimal that quantizes back to the same byte, so the
|
|
198
|
+
// output reads like the authored value (`0.3`) rather than its rounding noise.
|
|
199
|
+
function shortestAlpha(alpha) {
|
|
200
|
+
const byte = Math.round(alpha * 255);
|
|
201
|
+
for (let digits = 1; digits <= 3; digits++) {
|
|
202
|
+
const candidate = Number(alpha.toFixed(digits));
|
|
203
|
+
if (Math.round(candidate * 255) === byte)
|
|
204
|
+
return candidate;
|
|
205
|
+
}
|
|
206
|
+
return roundFloat(alpha);
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* A lightningcss `CssColor` → the string shape Fabric is already shipped: a hex when opaque
|
|
210
|
+
* (`#000`, `#0f1e30`), `rgba(r, g, b, a)` when translucent.
|
|
211
|
+
*
|
|
212
|
+
* lightningcss normalizes every legacy notation (`red`, `#000`, `hsl(...)`) to `rgb`, so a keyword
|
|
213
|
+
* comes out as its hex — RN's `processColor` reads both. A wide-gamut color (`oklch`, `lab`) has no
|
|
214
|
+
* RN equivalent at all and returns `null` so the caller drops it with a warning.
|
|
215
|
+
*/
|
|
216
|
+
function colorToCss(value) {
|
|
217
|
+
if (!isRecord(value))
|
|
218
|
+
return null;
|
|
219
|
+
if (value.type === 'currentcolor')
|
|
220
|
+
return 'currentColor';
|
|
221
|
+
if (value.type !== 'rgb')
|
|
222
|
+
return null;
|
|
223
|
+
const r = readNumber(value.r);
|
|
224
|
+
const g = readNumber(value.g);
|
|
225
|
+
const b = readNumber(value.b);
|
|
226
|
+
const alpha = readNumber(value.alpha);
|
|
227
|
+
if (r === null || g === null || b === null || alpha === null)
|
|
228
|
+
return null;
|
|
229
|
+
if (alpha < 1) {
|
|
230
|
+
return `rgba(${Math.round(r)}, ${Math.round(g)}, ${Math.round(b)}, ${shortestAlpha(alpha)})`;
|
|
231
|
+
}
|
|
232
|
+
const hex = `${toHexChannel(r)}${toHexChannel(g)}${toHexChannel(b)}`;
|
|
233
|
+
const isShortenable = hex[0] === hex[1] && hex[2] === hex[3] && hex[4] === hex[5];
|
|
234
|
+
return isShortenable ? `#${hex[0]}${hex[2]}${hex[4]}` : `#${hex}`;
|
|
235
|
+
}
|
|
236
|
+
//#endregion colors
|
|
237
|
+
//#region serializing a declaration back to CSS text
|
|
238
|
+
const SERIALIZER_TEMPLATE = new TextEncoder().encode('.s{color:red}');
|
|
239
|
+
const SERIALIZER_DECODER = new TextDecoder();
|
|
240
|
+
/**
|
|
241
|
+
* The CSS text of one declaration's VALUE, produced by lightningcss's own printer.
|
|
242
|
+
*
|
|
243
|
+
* Needed for the `raw` property kind — `transform`, `filter`, `background-image`,
|
|
244
|
+
* `box-shadow`, `transform-origin` are passed to the engine as unparsed CSS text on purpose (see
|
|
245
|
+
* the PROPERTY_TABLE comments: RN's own JS processors parse that exact syntax at commit time). We
|
|
246
|
+
* hold their typed AST and need text back, and re-printing a gradient / transform-list / filter
|
|
247
|
+
* chain by hand would be a second, narrower serializer that drifts from lightningcss's.
|
|
248
|
+
*
|
|
249
|
+
* The trick is to hand lightningcss a one-rule stylesheet and swap that rule's declarations for
|
|
250
|
+
* ours, then read the value out of the printed output.
|
|
251
|
+
*/
|
|
252
|
+
function serializeValue(declaration) {
|
|
253
|
+
try {
|
|
254
|
+
const result = transform({
|
|
255
|
+
filename: 'symbiote-declaration.css',
|
|
256
|
+
code: SERIALIZER_TEMPLATE,
|
|
257
|
+
visitor: {
|
|
258
|
+
Rule(rule) {
|
|
259
|
+
if (rule.type !== 'style')
|
|
260
|
+
return;
|
|
261
|
+
rule.value.declarations = {
|
|
262
|
+
declarations: [declaration],
|
|
263
|
+
importantDeclarations: [],
|
|
264
|
+
};
|
|
265
|
+
return rule;
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
const css = SERIALIZER_DECODER.decode(result.code);
|
|
270
|
+
const open = css.indexOf('{');
|
|
271
|
+
const close = css.lastIndexOf('}');
|
|
272
|
+
if (open === -1 || close <= open)
|
|
273
|
+
return null;
|
|
274
|
+
const body = css
|
|
275
|
+
.slice(open + 1, close)
|
|
276
|
+
.trim()
|
|
277
|
+
.replace(/;$/, '');
|
|
278
|
+
const colon = body.indexOf(':');
|
|
279
|
+
return colon === -1 ? null : body.slice(colon + 1).trim();
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
//#endregion serializing a declaration back to CSS text
|
|
286
|
+
//#region var() substitution
|
|
287
|
+
const TOKEN_TEXT = {
|
|
288
|
+
colon: ':',
|
|
289
|
+
semicolon: ';',
|
|
290
|
+
comma: ',',
|
|
291
|
+
'parenthesis-block': '(',
|
|
292
|
+
'square-bracket-block': '[',
|
|
293
|
+
'curly-bracket-block': '{',
|
|
294
|
+
'close-parenthesis': ')',
|
|
295
|
+
'close-square-bracket': ']',
|
|
296
|
+
'close-curly-bracket': '}',
|
|
297
|
+
comment: '',
|
|
298
|
+
};
|
|
299
|
+
function rawTokenToText(token) {
|
|
300
|
+
if (!isRecord(token))
|
|
301
|
+
return null;
|
|
302
|
+
const type = readString(token.type);
|
|
303
|
+
if (type === null)
|
|
304
|
+
return null;
|
|
305
|
+
const fixed = TOKEN_TEXT[type];
|
|
306
|
+
if (fixed !== undefined)
|
|
307
|
+
return fixed;
|
|
308
|
+
const text = readString(token.value);
|
|
309
|
+
const amount = readNumber(token.value);
|
|
310
|
+
switch (type) {
|
|
311
|
+
case 'ident':
|
|
312
|
+
case 'delim':
|
|
313
|
+
case 'white-space':
|
|
314
|
+
return text;
|
|
315
|
+
case 'at-keyword':
|
|
316
|
+
return text === null ? null : `@${text}`;
|
|
317
|
+
case 'hash':
|
|
318
|
+
case 'id-hash':
|
|
319
|
+
return text === null ? null : `#${text}`;
|
|
320
|
+
case 'string':
|
|
321
|
+
return text === null ? null : JSON.stringify(text);
|
|
322
|
+
case 'unquoted-url':
|
|
323
|
+
return text === null ? null : `url(${text})`;
|
|
324
|
+
case 'function':
|
|
325
|
+
return text === null ? null : `${text}(`;
|
|
326
|
+
case 'number':
|
|
327
|
+
return amount === null ? null : String(roundFloat(amount));
|
|
328
|
+
case 'percentage':
|
|
329
|
+
return amount === null ? null : `${roundFloat(amount * 100)}%`;
|
|
330
|
+
case 'dimension': {
|
|
331
|
+
const unit = readString(token.unit);
|
|
332
|
+
return amount === null || unit === null
|
|
333
|
+
? null
|
|
334
|
+
: `${roundFloat(amount)}${unit}`;
|
|
335
|
+
}
|
|
336
|
+
default:
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Print a token list (an `unparsed` declaration's value, or a custom property's) back to CSS text.
|
|
342
|
+
*
|
|
343
|
+
* In `substitute` mode every `var(--x)` is replaced from the printer's variable map, falling back
|
|
344
|
+
* to the `var()` fallback argument when the name is undeclared; a name that resolves to nothing is
|
|
345
|
+
* pushed onto `missing` so the caller can drop the declaration and say WHICH custom property was
|
|
346
|
+
* never declared. In `keep` mode the `var()` is printed verbatim.
|
|
347
|
+
*/
|
|
348
|
+
function tokensToText(tokens, printer) {
|
|
349
|
+
if (!Array.isArray(tokens))
|
|
350
|
+
return null;
|
|
351
|
+
let text = '';
|
|
352
|
+
for (const token of tokens) {
|
|
353
|
+
const piece = tokenOrValueToText(token, printer);
|
|
354
|
+
if (piece === null)
|
|
355
|
+
return null;
|
|
356
|
+
text += piece;
|
|
357
|
+
}
|
|
358
|
+
return text;
|
|
359
|
+
}
|
|
360
|
+
function tokenOrValueToText(node, printer) {
|
|
361
|
+
if (!isRecord(node))
|
|
362
|
+
return null;
|
|
363
|
+
switch (node.type) {
|
|
364
|
+
case 'token':
|
|
365
|
+
return rawTokenToText(node.value);
|
|
366
|
+
case 'color':
|
|
367
|
+
return colorToCss(node.value);
|
|
368
|
+
case 'dashed-ident':
|
|
369
|
+
return readString(node.value);
|
|
370
|
+
case 'length':
|
|
371
|
+
case 'angle':
|
|
372
|
+
case 'time':
|
|
373
|
+
case 'resolution': {
|
|
374
|
+
const dimension = isRecord(node.value) ? node.value : null;
|
|
375
|
+
if (dimension === null)
|
|
376
|
+
return null;
|
|
377
|
+
const unit = readString(dimension.unit) ?? readString(dimension.type);
|
|
378
|
+
const amount = readNumber(dimension.value);
|
|
379
|
+
return unit === null || amount === null
|
|
380
|
+
? null
|
|
381
|
+
: `${roundFloat(amount)}${unit}`;
|
|
382
|
+
}
|
|
383
|
+
case 'function': {
|
|
384
|
+
if (!isRecord(node.value))
|
|
385
|
+
return null;
|
|
386
|
+
const name = readString(node.value.name);
|
|
387
|
+
const args = tokensToText(node.value.arguments, printer);
|
|
388
|
+
return name === null || args === null ? null : `${name}(${args})`;
|
|
389
|
+
}
|
|
390
|
+
case 'var':
|
|
391
|
+
return variableToText(node.value, printer);
|
|
392
|
+
// `url()` / `env()` / an animation name have no place in an RN style value.
|
|
393
|
+
default:
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
function variableToText(variable, printer) {
|
|
398
|
+
if (!isRecord(variable))
|
|
399
|
+
return null;
|
|
400
|
+
const name = isRecord(variable.name) ? readString(variable.name.ident) : null;
|
|
401
|
+
if (name === null)
|
|
402
|
+
return null;
|
|
403
|
+
const hasFallback = variable.fallback !== undefined && variable.fallback !== null;
|
|
404
|
+
if (printer.varMode === 'keep') {
|
|
405
|
+
if (!hasFallback)
|
|
406
|
+
return `var(${name})`;
|
|
407
|
+
const fallback = tokensToText(variable.fallback, printer);
|
|
408
|
+
return fallback === null ? null : `var(${name}, ${fallback})`;
|
|
409
|
+
}
|
|
410
|
+
const declared = printer.variables.get(name);
|
|
411
|
+
if (declared !== undefined)
|
|
412
|
+
return declared;
|
|
413
|
+
if (hasFallback)
|
|
414
|
+
return tokensToText(variable.fallback, printer);
|
|
415
|
+
// Declared in ANOTHER file — the documented trap this pipeline exists to stop shipping. The
|
|
416
|
+
// registry is per-file, so there is nothing to resolve against and no value worth emitting.
|
|
417
|
+
printer.missing.push(name);
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
// Every one of these arrives from lightningcss PRE-EXPANDED into a Rect, which is what makes the
|
|
421
|
+
// old parser's `padding: 8px 16px -> { padding: 8 }` fixable at all.
|
|
422
|
+
const RECT_PROPERTIES = {
|
|
423
|
+
padding: {
|
|
424
|
+
shorthand: 'padding',
|
|
425
|
+
sides: ['paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft'],
|
|
426
|
+
read: 'length',
|
|
427
|
+
},
|
|
428
|
+
margin: {
|
|
429
|
+
shorthand: 'margin',
|
|
430
|
+
sides: ['marginTop', 'marginRight', 'marginBottom', 'marginLeft'],
|
|
431
|
+
read: 'length',
|
|
432
|
+
},
|
|
433
|
+
'border-width': {
|
|
434
|
+
shorthand: 'borderWidth',
|
|
435
|
+
sides: [
|
|
436
|
+
'borderTopWidth',
|
|
437
|
+
'borderRightWidth',
|
|
438
|
+
'borderBottomWidth',
|
|
439
|
+
'borderLeftWidth',
|
|
440
|
+
],
|
|
441
|
+
read: 'length',
|
|
442
|
+
},
|
|
443
|
+
'border-color': {
|
|
444
|
+
shorthand: 'borderColor',
|
|
445
|
+
sides: [
|
|
446
|
+
'borderTopColor',
|
|
447
|
+
'borderRightColor',
|
|
448
|
+
'borderBottomColor',
|
|
449
|
+
'borderLeftColor',
|
|
450
|
+
],
|
|
451
|
+
read: 'color',
|
|
452
|
+
},
|
|
453
|
+
// RN has `borderStyle` and no per-side style prop, so a four-value `border-style` is
|
|
454
|
+
// inexpressible rather than expandable.
|
|
455
|
+
'border-style': { shorthand: 'borderStyle', sides: null, read: 'keyword' },
|
|
456
|
+
// Always expanded: `top`/`right`/`bottom`/`left` are the props every RN version has, whereas the
|
|
457
|
+
// `inset` shorthand is newer — expanding costs three keys and can never be wrong.
|
|
458
|
+
inset: {
|
|
459
|
+
shorthand: null,
|
|
460
|
+
sides: ['top', 'right', 'bottom', 'left'],
|
|
461
|
+
read: 'length',
|
|
462
|
+
},
|
|
463
|
+
};
|
|
464
|
+
const RECT_KEYS = ['top', 'right', 'bottom', 'left'];
|
|
465
|
+
const CORNER_KEYS = [
|
|
466
|
+
'topLeft',
|
|
467
|
+
'topRight',
|
|
468
|
+
'bottomRight',
|
|
469
|
+
'bottomLeft',
|
|
470
|
+
];
|
|
471
|
+
const CORNER_PROPS = [
|
|
472
|
+
'borderTopLeftRadius',
|
|
473
|
+
'borderTopRightRadius',
|
|
474
|
+
'borderBottomRightRadius',
|
|
475
|
+
'borderBottomLeftRadius',
|
|
476
|
+
];
|
|
477
|
+
// A corner LONGHAND never arrives as a bare length: lightningcss gives a Size2D pair, so
|
|
478
|
+
// `border-top-left-radius: 20px` is `[20px, 20px]` and `4px 8px` is `[4px, 8px]`. Reading it as
|
|
479
|
+
// one length made every corner longhand drop with "cannot express" on a plain `20px` — found in
|
|
480
|
+
// `examples/solid/App.css`'s `.sheet`, the only place in the corpus that uses them.
|
|
481
|
+
const CORNER_RADIUS_PROPERTIES = new Set([
|
|
482
|
+
'border-top-left-radius',
|
|
483
|
+
'border-top-right-radius',
|
|
484
|
+
'border-bottom-right-radius',
|
|
485
|
+
'border-bottom-left-radius',
|
|
486
|
+
]);
|
|
487
|
+
function readRectSide(side, mapping, context) {
|
|
488
|
+
switch (mapping.read) {
|
|
489
|
+
case 'length': {
|
|
490
|
+
const length = readLength(side, context);
|
|
491
|
+
return length === null ? null : lengthToStyleValue(length);
|
|
492
|
+
}
|
|
493
|
+
case 'color':
|
|
494
|
+
return colorToCss(side);
|
|
495
|
+
case 'keyword':
|
|
496
|
+
return readString(side);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
function rectToStyle(cssProperty, value, mapping, context) {
|
|
500
|
+
if (!isRecord(value))
|
|
501
|
+
return {};
|
|
502
|
+
const resolved = [];
|
|
503
|
+
for (const key of RECT_KEYS) {
|
|
504
|
+
const side = readRectSide(value[key], mapping, context);
|
|
505
|
+
if (side === null) {
|
|
506
|
+
warnDrop(context, cssProperty, `"${cssProperty}" has a value React Native cannot express`);
|
|
507
|
+
return {};
|
|
508
|
+
}
|
|
509
|
+
resolved.push(side);
|
|
510
|
+
}
|
|
511
|
+
const uniform = resolved.every(side => side === resolved[0]);
|
|
512
|
+
if (uniform && mapping.shorthand !== null) {
|
|
513
|
+
return { [mapping.shorthand]: resolved[0] };
|
|
514
|
+
}
|
|
515
|
+
if (mapping.sides === null) {
|
|
516
|
+
warnDrop(context, cssProperty, `"${cssProperty}" differs per side and React Native has only the "${mapping.shorthand}" shorthand`);
|
|
517
|
+
return {};
|
|
518
|
+
}
|
|
519
|
+
const style = {};
|
|
520
|
+
mapping.sides.forEach((prop, index) => {
|
|
521
|
+
style[prop] = resolved[index];
|
|
522
|
+
});
|
|
523
|
+
return style;
|
|
524
|
+
}
|
|
525
|
+
function borderRadiusToStyle(value, context) {
|
|
526
|
+
if (!isRecord(value))
|
|
527
|
+
return {};
|
|
528
|
+
const resolved = [];
|
|
529
|
+
for (const key of CORNER_KEYS) {
|
|
530
|
+
const corner = value[key];
|
|
531
|
+
// Each corner is an [x, y] pair; RN has no elliptical radius, so only x survives.
|
|
532
|
+
const radius = Array.isArray(corner) ? corner[0] : corner;
|
|
533
|
+
const length = readLength(radius, context);
|
|
534
|
+
if (length === null) {
|
|
535
|
+
warnDrop(context, 'border-radius', '"border-radius" has a value React Native cannot express');
|
|
536
|
+
return {};
|
|
537
|
+
}
|
|
538
|
+
resolved.push(lengthToStyleValue(length));
|
|
539
|
+
}
|
|
540
|
+
if (resolved.every(corner => corner === resolved[0])) {
|
|
541
|
+
return { borderRadius: resolved[0] };
|
|
542
|
+
}
|
|
543
|
+
const style = {};
|
|
544
|
+
CORNER_PROPS.forEach((prop, index) => {
|
|
545
|
+
style[prop] = resolved[index];
|
|
546
|
+
});
|
|
547
|
+
return style;
|
|
548
|
+
}
|
|
549
|
+
function gapToStyle(value, context) {
|
|
550
|
+
if (!isRecord(value))
|
|
551
|
+
return {};
|
|
552
|
+
// `gap: normal` IS the initial value; dropping it changes nothing, so it stays silent.
|
|
553
|
+
const row = readLength(value.row, context);
|
|
554
|
+
const column = readLength(value.column, context);
|
|
555
|
+
if (row === null || column === null)
|
|
556
|
+
return {};
|
|
557
|
+
const rowValue = lengthToStyleValue(row);
|
|
558
|
+
const columnValue = lengthToStyleValue(column);
|
|
559
|
+
return rowValue === columnValue
|
|
560
|
+
? { gap: rowValue }
|
|
561
|
+
: { rowGap: rowValue, columnGap: columnValue };
|
|
562
|
+
}
|
|
563
|
+
function flexToStyle(value, context) {
|
|
564
|
+
if (!isRecord(value))
|
|
565
|
+
return {};
|
|
566
|
+
const grow = readNumber(value.grow);
|
|
567
|
+
const shrink = readNumber(value.shrink);
|
|
568
|
+
if (grow === null || shrink === null)
|
|
569
|
+
return {};
|
|
570
|
+
const basis = readLength(value.basis, context);
|
|
571
|
+
const isRnFlexShorthand = shrink === 1 &&
|
|
572
|
+
basis !== null &&
|
|
573
|
+
basis.kind === 'percent' &&
|
|
574
|
+
basis.value === 0;
|
|
575
|
+
// RN's `flex: n` means exactly `n 1 0` — the CSS `flex: <number>` expansion — so the common case
|
|
576
|
+
// stays the one key the old parser emitted.
|
|
577
|
+
if (isRnFlexShorthand)
|
|
578
|
+
return { flex: roundFloat(grow) };
|
|
579
|
+
const style = {
|
|
580
|
+
flexGrow: roundFloat(grow),
|
|
581
|
+
flexShrink: roundFloat(shrink),
|
|
582
|
+
};
|
|
583
|
+
if (basis !== null)
|
|
584
|
+
style.flexBasis = lengthToStyleValue(basis);
|
|
585
|
+
return style;
|
|
586
|
+
}
|
|
587
|
+
// `text-shadow` is the one property with no engine-side processor to defer to: RN never got a
|
|
588
|
+
// unified CSS-string `textShadow` prop, only the three decomposed legacy props. So this package
|
|
589
|
+
// is the only place that CAN decompose it. The output shape (three keys, a `{width, height}`
|
|
590
|
+
// offset object, `#000000` when the author wrote no color) is the one the registry has always
|
|
591
|
+
// been fed, carried over from the retired text pass this replaced.
|
|
592
|
+
const TEXT_SHADOW_DEFAULT_COLOR = '#000000';
|
|
593
|
+
function shadowLengthPx(value, context) {
|
|
594
|
+
const length = readLength(value, context);
|
|
595
|
+
return length === null || length.kind !== 'px'
|
|
596
|
+
? null
|
|
597
|
+
: roundFloat(length.value);
|
|
598
|
+
}
|
|
599
|
+
function textShadowToStyle(value, context) {
|
|
600
|
+
if (!Array.isArray(value))
|
|
601
|
+
return {};
|
|
602
|
+
if (value.length > 1) {
|
|
603
|
+
// Same key and wording as ../values.ts, so a stylesheet compiled by either pipeline reports
|
|
604
|
+
// the limitation identically.
|
|
605
|
+
warnOnce(warnedKeysFor(context.filename), 'text-shadow:multiple', '[@symbiote-native/css-parser] multiple text-shadow layers are not supported, only the first is applied');
|
|
606
|
+
}
|
|
607
|
+
const layer = value[0];
|
|
608
|
+
if (!isRecord(layer))
|
|
609
|
+
return {};
|
|
610
|
+
const width = shadowLengthPx(layer.xOffset, context);
|
|
611
|
+
const height = shadowLengthPx(layer.yOffset, context);
|
|
612
|
+
if (width === null || height === null) {
|
|
613
|
+
warnDrop(context, 'text-shadow', '"text-shadow" has offsets React Native cannot express');
|
|
614
|
+
return {};
|
|
615
|
+
}
|
|
616
|
+
// CSS defaults an omitted shadow color to `currentColor`, which RN has no concept of; the text
|
|
617
|
+
// parser lands on black for the same reason, so both pipelines agree.
|
|
618
|
+
const color = colorToCss(layer.color);
|
|
619
|
+
const isUsableColor = color !== null && color !== 'currentColor';
|
|
620
|
+
return {
|
|
621
|
+
textShadowColor: isUsableColor ? color : TEXT_SHADOW_DEFAULT_COLOR,
|
|
622
|
+
textShadowOffset: { width, height },
|
|
623
|
+
textShadowRadius: shadowLengthPx(layer.blur, context) ?? 0,
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
//#endregion shorthand (Rect / corner / axis) expansion
|
|
627
|
+
//#region entry point
|
|
628
|
+
// A `var()` may resolve to text holding another `var()`; each round re-parses, so cap the chain
|
|
629
|
+
// rather than trusting authored CSS to terminate it.
|
|
630
|
+
const MAX_SUBSTITUTION_DEPTH = 4;
|
|
631
|
+
const INITIAL_VALUE_KEYWORDS = new Set(['normal', 'none']);
|
|
632
|
+
// Single-color properties. `border-color` is absent on purpose — it arrives as a Rect and is
|
|
633
|
+
// handled by {@link RECT_PROPERTIES}, which reads each side with the same {@link colorToCss}.
|
|
634
|
+
const COLOR_PROPERTIES = new Set([
|
|
635
|
+
'color',
|
|
636
|
+
'background-color',
|
|
637
|
+
'border-top-color',
|
|
638
|
+
'border-right-color',
|
|
639
|
+
'border-bottom-color',
|
|
640
|
+
'border-left-color',
|
|
641
|
+
]);
|
|
642
|
+
function isDeclaration(value) {
|
|
643
|
+
return isRecord(value) && typeof value.property === 'string';
|
|
644
|
+
}
|
|
645
|
+
/** Keep only the scalar entries `mapCSSProperty` produced — {@link IStyleObject} carries no objects. */
|
|
646
|
+
function toStyleObject(mapped) {
|
|
647
|
+
const style = {};
|
|
648
|
+
if (mapped === null)
|
|
649
|
+
return style;
|
|
650
|
+
for (const [prop, value] of Object.entries(mapped)) {
|
|
651
|
+
if (typeof value === 'string' || typeof value === 'number') {
|
|
652
|
+
style[prop] = value;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
return style;
|
|
656
|
+
}
|
|
657
|
+
/** Re-parse one substituted declaration back through lightningcss. */
|
|
658
|
+
function parseDeclarationText(cssProperty, valueText) {
|
|
659
|
+
const parsed = [];
|
|
660
|
+
try {
|
|
661
|
+
transform({
|
|
662
|
+
filename: 'symbiote-substituted.css',
|
|
663
|
+
code: new TextEncoder().encode(`.s{${cssProperty}:${valueText}}`),
|
|
664
|
+
visitor: {
|
|
665
|
+
Rule(rule) {
|
|
666
|
+
if (rule.type !== 'style')
|
|
667
|
+
return;
|
|
668
|
+
parsed.push(...rule.value.declarations.declarations, ...rule.value.declarations.importantDeclarations);
|
|
669
|
+
},
|
|
670
|
+
},
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
catch {
|
|
674
|
+
return [];
|
|
675
|
+
}
|
|
676
|
+
return parsed;
|
|
677
|
+
}
|
|
678
|
+
function unparsedToStyle(value, context, depth) {
|
|
679
|
+
const cssProperty = value.propertyId.property;
|
|
680
|
+
// An unparsed CUSTOM property is still just a `--x` declaration — the caller's pre-pass owns it.
|
|
681
|
+
if (cssProperty === 'custom')
|
|
682
|
+
return {};
|
|
683
|
+
const missing = [];
|
|
684
|
+
const valueText = tokensToText(value.value, {
|
|
685
|
+
variables: context.variables,
|
|
686
|
+
varMode: 'substitute',
|
|
687
|
+
missing,
|
|
688
|
+
});
|
|
689
|
+
if (missing.length > 0) {
|
|
690
|
+
warnDrop(context, `${cssProperty}:var`, `custom ${missing.length === 1 ? 'property' : 'properties'} ${missing.map(name => `"${name}"`).join(', ')} used by "${cssProperty}" ${missing.length === 1 ? 'is' : 'are'} not declared in this file`);
|
|
691
|
+
return {};
|
|
692
|
+
}
|
|
693
|
+
if (valueText === null || depth >= MAX_SUBSTITUTION_DEPTH) {
|
|
694
|
+
warnDrop(context, cssProperty, `"${cssProperty}" has a value React Native cannot express`);
|
|
695
|
+
return {};
|
|
696
|
+
}
|
|
697
|
+
const style = {};
|
|
698
|
+
for (const parsed of parseDeclarationText(cssProperty, valueText)) {
|
|
699
|
+
Object.assign(style, declarationToStyleAt(parsed, context, depth + 1));
|
|
700
|
+
}
|
|
701
|
+
return style;
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* One lightningcss `Declaration` (the object handed to a visitor / found in
|
|
705
|
+
* `rule.value.declarations.declarations`) mapped to zero or more RN style entries.
|
|
706
|
+
*/
|
|
707
|
+
export function declarationToStyle(declaration, context) {
|
|
708
|
+
return declarationToStyleAt(declaration, context, 0);
|
|
709
|
+
}
|
|
710
|
+
function declarationToStyleAt(declaration, context, depth) {
|
|
711
|
+
if (!isDeclaration(declaration))
|
|
712
|
+
return {};
|
|
713
|
+
if (declaration.property === 'custom') {
|
|
714
|
+
const name = declaration.value.name;
|
|
715
|
+
// A `--x` declaration: the caller's pre-pass collected it into `context.variables`.
|
|
716
|
+
if (name.startsWith('--'))
|
|
717
|
+
return {};
|
|
718
|
+
// lightningcss files a property IT does not know under `custom` too, so this is where a
|
|
719
|
+
// genuine typo (`colr: red`) surfaces. `mapCSSProperty` owns the drop-warning wording — the
|
|
720
|
+
// value argument is unread on that path, it warns and returns null on the table miss.
|
|
721
|
+
mapCSSProperty(name, '', warnedKeysFor(context.filename));
|
|
722
|
+
return {};
|
|
723
|
+
}
|
|
724
|
+
if (declaration.property === 'unparsed') {
|
|
725
|
+
return unparsedToStyle(declaration.value, context, depth);
|
|
726
|
+
}
|
|
727
|
+
const cssProperty = declaration.property;
|
|
728
|
+
const value = declaration.value;
|
|
729
|
+
const rect = RECT_PROPERTIES[cssProperty];
|
|
730
|
+
if (rect)
|
|
731
|
+
return rectToStyle(cssProperty, value, rect, context);
|
|
732
|
+
switch (cssProperty) {
|
|
733
|
+
case 'border-radius':
|
|
734
|
+
return borderRadiusToStyle(value, context);
|
|
735
|
+
case 'gap':
|
|
736
|
+
return gapToStyle(value, context);
|
|
737
|
+
case 'flex':
|
|
738
|
+
return flexToStyle(value, context);
|
|
739
|
+
case 'overflow': {
|
|
740
|
+
// RN has one `overflow`; a differing x/y is inexpressible.
|
|
741
|
+
const x = isRecord(value) ? readString(value.x) : null;
|
|
742
|
+
const y = isRecord(value) ? readString(value.y) : null;
|
|
743
|
+
if (x === null || x !== y) {
|
|
744
|
+
warnDrop(context, cssProperty, '"overflow" differs per axis and React Native has one overflow prop');
|
|
745
|
+
return {};
|
|
746
|
+
}
|
|
747
|
+
return { overflow: x };
|
|
748
|
+
}
|
|
749
|
+
case 'aspect-ratio': {
|
|
750
|
+
const ratio = isRecord(value) ? value.ratio : null;
|
|
751
|
+
const width = Array.isArray(ratio) ? readNumber(ratio[0]) : null;
|
|
752
|
+
const height = Array.isArray(ratio) ? readNumber(ratio[1]) : null;
|
|
753
|
+
if (width === null || height === null || height === 0)
|
|
754
|
+
return {};
|
|
755
|
+
return { aspectRatio: roundFloat(width / height) };
|
|
756
|
+
}
|
|
757
|
+
case 'text-shadow':
|
|
758
|
+
return textShadowToStyle(value, context);
|
|
759
|
+
// A CSS-Modules DIRECTIVE, not a style property — and lightningcss has ALREADY acted on it:
|
|
760
|
+
// the composed names come back through `exports[...].composes`, which
|
|
761
|
+
// `metro-css-module/index.ts` walks to flatten a chain. Measured 2026-08-20: it arrives as a
|
|
762
|
+
// first-class `property: 'composes'`, NOT under `custom` where an unknown name goes, so it fell
|
|
763
|
+
// through to the PROPERTY_TABLE miss and every author of a WORKING `.module.*` file was told
|
|
764
|
+
// "unsupported CSS property" on every build. The drop warnings are the only signal that a real
|
|
765
|
+
// rule died; a channel that cries wolf on working code is the one nobody reads when it matters.
|
|
766
|
+
case 'composes':
|
|
767
|
+
return {};
|
|
768
|
+
default:
|
|
769
|
+
break;
|
|
770
|
+
}
|
|
771
|
+
const mapping = PROPERTY_TABLE[cssProperty];
|
|
772
|
+
if (!mapping) {
|
|
773
|
+
mapCSSProperty(cssProperty, '', warnedKeysFor(context.filename));
|
|
774
|
+
return {};
|
|
775
|
+
}
|
|
776
|
+
// Colors are `raw` in the table but must NOT go through the printer below: lightningcss prints
|
|
777
|
+
// a translucent color as `#0000004d`, and the shape this pipeline ships is `rgba(0, 0, 0, 0.3)`.
|
|
778
|
+
if (COLOR_PROPERTIES.has(cssProperty)) {
|
|
779
|
+
const color = colorToCss(value);
|
|
780
|
+
if (color === null) {
|
|
781
|
+
warnDrop(context, cssProperty, `"${cssProperty}" uses a color space React Native cannot read`);
|
|
782
|
+
return {};
|
|
783
|
+
}
|
|
784
|
+
return { [mapping.rnProperty]: color };
|
|
785
|
+
}
|
|
786
|
+
// Only the horizontal half survives — RN has no elliptical radius, the same rule
|
|
787
|
+
// `borderRadiusToStyle` already applies to each corner of the shorthand.
|
|
788
|
+
if (CORNER_RADIUS_PROPERTIES.has(cssProperty)) {
|
|
789
|
+
const radius = readLength(Array.isArray(value) ? value[0] : value, context);
|
|
790
|
+
if (radius === null) {
|
|
791
|
+
warnDrop(context, cssProperty, `"${cssProperty}" has a value React Native cannot express`);
|
|
792
|
+
return {};
|
|
793
|
+
}
|
|
794
|
+
return { [mapping.rnProperty]: lengthToStyleValue(radius) };
|
|
795
|
+
}
|
|
796
|
+
if (mapping.kind === 'raw') {
|
|
797
|
+
const text = serializeValue(declaration);
|
|
798
|
+
if (text === null) {
|
|
799
|
+
warnDrop(context, cssProperty, `"${cssProperty}" has a value React Native cannot express`);
|
|
800
|
+
return {};
|
|
801
|
+
}
|
|
802
|
+
return toStyleObject(mapCSSProperty(cssProperty, text, warnedKeysFor(context.filename)));
|
|
803
|
+
}
|
|
804
|
+
if (mapping.kind === 'number') {
|
|
805
|
+
const plain = readNumber(value) ?? (isRecord(value) ? readNumber(value.value) : null);
|
|
806
|
+
if (plain === null)
|
|
807
|
+
return {};
|
|
808
|
+
return { [mapping.rnProperty]: roundFloat(plain) };
|
|
809
|
+
}
|
|
810
|
+
const length = readLength(value, context);
|
|
811
|
+
if (length === null) {
|
|
812
|
+
// `normal` / `none` ARE the CSS initial values (`letter-spacing`, `line-height`) — dropping
|
|
813
|
+
// one changes nothing on screen, so it never earns a warning.
|
|
814
|
+
if (INITIAL_VALUE_KEYWORDS.has(readType(value) ?? ''))
|
|
815
|
+
return {};
|
|
816
|
+
// The `calc()` that survived lightningcss's own folding mixes a percentage or a viewport unit
|
|
817
|
+
// with a length — RN has no expression form, and the old parser's silent `100` for
|
|
818
|
+
// `calc(100% - 24px)` is exactly the bug this drop replaces.
|
|
819
|
+
const reason = readType(isRecord(value) ? value.value : null) === 'calc'
|
|
820
|
+
? `"${cssProperty}" uses a calc() mixing units React Native cannot resolve`
|
|
821
|
+
: `"${cssProperty}" has a value React Native cannot express`;
|
|
822
|
+
warnDrop(context, cssProperty, reason);
|
|
823
|
+
return {};
|
|
824
|
+
}
|
|
825
|
+
return { [mapping.rnProperty]: lengthToStyleValue(length) };
|
|
826
|
+
}
|
|
827
|
+
//#endregion entry point
|
|
828
|
+
//#region file-wide custom-property pre-pass
|
|
829
|
+
/**
|
|
830
|
+
* Every custom property (`--x`) declared anywhere in the file, name -> its raw value text, in the
|
|
831
|
+
* shape {@link IDeclarationContext.variables} expects.
|
|
832
|
+
*
|
|
833
|
+
* A pass of its OWN, ahead of mapping declarations, because nothing orders `:root` first — a
|
|
834
|
+
* component stylesheet may declare a token in a class rule, inside `@media`, or below its first
|
|
835
|
+
* use, and a single forward walk would miss it. Later declaration of the same name wins, which is
|
|
836
|
+
* what the cascade does within one file.
|
|
837
|
+
*
|
|
838
|
+
* The value is stored VERBATIM: a token list that is itself a `var()` chain is printed back as
|
|
839
|
+
* `var(--other)`, and resolution stays in {@link declarationToStyle}, which is the only place that
|
|
840
|
+
* knows whether the chain terminates. Serialization goes through the same token printer the
|
|
841
|
+
* `unparsed` path uses — this package keeps ONE printer.
|
|
842
|
+
*/
|
|
843
|
+
export function variablesIn(css, filename) {
|
|
844
|
+
const variables = new Map();
|
|
845
|
+
if (!css)
|
|
846
|
+
return variables;
|
|
847
|
+
const printer = {
|
|
848
|
+
variables,
|
|
849
|
+
varMode: 'keep',
|
|
850
|
+
missing: [],
|
|
851
|
+
};
|
|
852
|
+
try {
|
|
853
|
+
transform({
|
|
854
|
+
filename,
|
|
855
|
+
code: new TextEncoder().encode(css),
|
|
856
|
+
// A malformed rule elsewhere in the file must not cost us the tokens that ARE declared.
|
|
857
|
+
errorRecovery: true,
|
|
858
|
+
visitor: {
|
|
859
|
+
Rule(rule) {
|
|
860
|
+
if (rule.type !== 'style')
|
|
861
|
+
return;
|
|
862
|
+
const block = rule.value.declarations;
|
|
863
|
+
for (const declaration of [
|
|
864
|
+
...block.declarations,
|
|
865
|
+
...block.importantDeclarations,
|
|
866
|
+
]) {
|
|
867
|
+
if (declaration.property !== 'custom')
|
|
868
|
+
continue;
|
|
869
|
+
const name = declaration.value.name;
|
|
870
|
+
if (!name.startsWith('--'))
|
|
871
|
+
continue;
|
|
872
|
+
const text = tokensToText(declaration.value.value, printer);
|
|
873
|
+
if (text !== null)
|
|
874
|
+
variables.set(name, text.trim());
|
|
875
|
+
}
|
|
876
|
+
},
|
|
877
|
+
},
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
catch {
|
|
881
|
+
return variables;
|
|
882
|
+
}
|
|
883
|
+
return variables;
|
|
884
|
+
}
|
|
885
|
+
//#endregion file-wide custom-property pre-pass
|