@colorye/react-native-css 0.3.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.
@@ -0,0 +1,490 @@
1
+ const calcRe = /calc\(([^)]+)\)/g;
2
+
3
+ const colorRe1 = /rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*(\d+)\s*)?\)/g;
4
+ const colorRe2 = /rgba?\(\s*(\d+)\s+(\d+)\s+(\d+)\s*\/\s*([\d.]+)\s*\)/g;
5
+
6
+ const itohex = (component) => {
7
+ const hex = Number(component).toString(16);
8
+ return hex.length === 1 ? `0${hex}` : hex;
9
+ };
10
+
11
+ function splitTopLevelCommas(str) {
12
+ const parts = [];
13
+ let current = "";
14
+ let depth = 0;
15
+ for (let i = 0; i < str.length; i++) {
16
+ const char = str[i];
17
+ if (char === "(") {
18
+ depth++;
19
+ current += char;
20
+ } else if (char === ")") {
21
+ depth--;
22
+ current += char;
23
+ } else if (char === "," && depth === 0) {
24
+ parts.push(current.trim());
25
+ current = "";
26
+ } else {
27
+ current += char;
28
+ }
29
+ }
30
+ if (current) {
31
+ parts.push(current.trim());
32
+ }
33
+ return parts;
34
+ }
35
+
36
+ function parseColorMixArg(arg) {
37
+ arg = arg.trim();
38
+ // Check if there is a percentage at the end
39
+ const percentEndMatch = arg.match(/\s+([\d.]+)%$/i);
40
+ if (percentEndMatch) {
41
+ const percentage = parseFloat(percentEndMatch[1]);
42
+ const colorStr = arg.substring(0, arg.length - percentEndMatch[0].length).trim();
43
+ return { colorStr, percentage };
44
+ }
45
+
46
+ // Check if there is a percentage at the beginning
47
+ const percentStartMatch = arg.match(/^([\d.]+)%\s+/i);
48
+ if (percentStartMatch) {
49
+ const percentage = parseFloat(percentStartMatch[1]);
50
+ const colorStr = arg.substring(percentStartMatch[0].length).trim();
51
+ return { colorStr, percentage };
52
+ }
53
+
54
+ return { colorStr: arg, percentage: null };
55
+ }
56
+
57
+ function hslToRgb(h, s, l) {
58
+ h = h / 360;
59
+ s = s / 100;
60
+ l = l / 100;
61
+ let r, g, b;
62
+ if (s === 0) {
63
+ r = g = b = l; // achromatic
64
+ } else {
65
+ const hue2rgb = (p, q, t) => {
66
+ if (t < 0) t += 1;
67
+ if (t > 1) t -= 1;
68
+ if (t < 1 / 6) return p + (q - p) * 6 * t;
69
+ if (t < 1 / 2) return q;
70
+ if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
71
+ return p;
72
+ };
73
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
74
+ const p = 2 * l - q;
75
+ r = hue2rgb(p, q, h + 1 / 3);
76
+ g = hue2rgb(p, q, h);
77
+ b = hue2rgb(p, q, h - 1 / 3);
78
+ }
79
+ return { r: Math.round(r * 255), g: Math.round(g * 255), b: Math.round(b * 255) };
80
+ }
81
+
82
+ function parseColor(colorStr) {
83
+ if (!colorStr) return null;
84
+ colorStr = colorStr.trim().toLowerCase();
85
+
86
+ if (colorStr === "transparent") {
87
+ return { r: 0, g: 0, b: 0, a: 0 };
88
+ }
89
+
90
+ // Hex
91
+ if (colorStr.startsWith("#")) {
92
+ const hex = colorStr.substring(1);
93
+ if (hex.length === 3 || hex.length === 4) {
94
+ const r = parseInt(hex[0] + hex[0], 16);
95
+ const g = parseInt(hex[1] + hex[1], 16);
96
+ const b = parseInt(hex[2] + hex[2], 16);
97
+ const a = hex.length === 4 ? parseInt(hex[3] + hex[3], 16) / 255 : 1;
98
+ return { r, g, b, a };
99
+ }
100
+ if (hex.length === 6 || hex.length === 8) {
101
+ const r = parseInt(hex.substring(0, 2), 16);
102
+ const g = parseInt(hex.substring(2, 4), 16);
103
+ const b = parseInt(hex.substring(4, 6), 16);
104
+ const a = hex.length === 8 ? parseInt(hex.substring(6, 8), 16) / 255 : 1;
105
+ return { r, g, b, a };
106
+ }
107
+ return null;
108
+ }
109
+
110
+ // rgb / rgba
111
+ const rgbMatch = colorStr.match(/rgba?\(\s*([\d.]+)(%?)\s*[\s,]\s*([\d.]+)(%?)\s*[\s,]\s*([\d.]+)(%?)(?:\s*[\s,/]\s*([\d.]+)(%?))?\s*\)/);
112
+ if (rgbMatch) {
113
+ let r = parseFloat(rgbMatch[1]);
114
+ if (rgbMatch[2] === "%") r = (r / 100) * 255;
115
+ let g = parseFloat(rgbMatch[3]);
116
+ if (rgbMatch[4] === "%") g = (g / 100) * 255;
117
+ let b = parseFloat(rgbMatch[5]);
118
+ if (rgbMatch[6] === "%") b = (b / 100) * 255;
119
+
120
+ let a = 1;
121
+ if (rgbMatch[7] !== undefined) {
122
+ a = parseFloat(rgbMatch[7]);
123
+ if (rgbMatch[8] === "%") a = a / 100;
124
+ }
125
+ return { r: Math.round(r), g: Math.round(g), b: Math.round(b), a };
126
+ }
127
+
128
+ // hsl / hsla
129
+ const hslMatch = colorStr.match(/hsla?\(\s*([\d.]+)(?:deg)?\s*[\s,]\s*([\d.]+)%\s*[\s,]\s*([\d.]+)%\s*(?:\s*[\s,/]\s*([\d.]+)(%?))?\s*\)/);
130
+ if (hslMatch) {
131
+ const h = parseFloat(hslMatch[1]);
132
+ const s = parseFloat(hslMatch[2]);
133
+ const l = parseFloat(hslMatch[3]);
134
+ let a = 1;
135
+ if (hslMatch[4] !== undefined) {
136
+ a = parseFloat(hslMatch[4]);
137
+ if (hslMatch[5] === "%") a = a / 100;
138
+ }
139
+ const rgb = hslToRgb(h, s, l);
140
+ return { ...rgb, a };
141
+ }
142
+
143
+ // oklch(L C H [/ A])
144
+ const oklchMatch = colorStr.match(/oklch\(\s*([\d.]+)(%?)\s+([\d.]+)\s+([\d.]+)(?:deg)?(?:\s*\/\s*([\d.]+)(%?))?\s*\)/);
145
+ if (oklchMatch) {
146
+ let L = parseFloat(oklchMatch[1]);
147
+ if (oklchMatch[2] === "%" || L > 1) L = L / 100;
148
+ const C = parseFloat(oklchMatch[3]);
149
+ const H = parseFloat(oklchMatch[4]);
150
+ let a = 1;
151
+ if (oklchMatch[5] !== undefined) {
152
+ a = parseFloat(oklchMatch[5]);
153
+ if (oklchMatch[6] === "%") a = a / 100;
154
+ }
155
+ const hRad = (H * Math.PI) / 180;
156
+ const okA = C * Math.cos(hRad);
157
+ const okB = C * Math.sin(hRad);
158
+ const l_ = L + 0.3963377774 * okA + 0.2158037573 * okB;
159
+ const m_ = L - 0.1055613458 * okA - 0.0638541728 * okB;
160
+ const s_ = L - 0.0894841775 * okA - 1.291485548 * okB;
161
+ const l = l_ * l_ * l_;
162
+ const m = m_ * m_ * m_;
163
+ const s = s_ * s_ * s_;
164
+ const r = +4.0767439362 * l - 3.3077115913 * m + 0.2309699292 * s;
165
+ const g = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s;
166
+ const bl = -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s;
167
+ const gamma = (c) => (c <= 0.0031308 ? 12.92 * c : 1.055 * Math.pow(Math.max(0, c), 1 / 2.4) - 0.055);
168
+ return {
169
+ r: Math.max(0, Math.min(255, Math.round(gamma(r) * 255))),
170
+ g: Math.max(0, Math.min(255, Math.round(gamma(g) * 255))),
171
+ b: Math.max(0, Math.min(255, Math.round(gamma(bl) * 255))),
172
+ a,
173
+ };
174
+ }
175
+
176
+ // lab(L a b [/ A])
177
+ const labMatch = colorStr.match(/lab\(\s*([\d.]+)(%?)\s+([+-]?[\d.]+)\s+([+-]?[\d.]+)(?:\s*\/\s*([\d.]+)(%?))?\s*\)/);
178
+ if (labMatch) {
179
+ let L = parseFloat(labMatch[1]);
180
+ const labA = parseFloat(labMatch[3]);
181
+ const labB = parseFloat(labMatch[4]);
182
+ let a = 1;
183
+ if (labMatch[5] !== undefined) {
184
+ a = parseFloat(labMatch[5]);
185
+ if (labMatch[6] === "%") a = a / 100;
186
+ }
187
+ let y = (L + 16) / 116;
188
+ let x = labA / 500 + y;
189
+ let z = y - labB / 200;
190
+ const fInv = (t) => (t > 6 / 29 ? t * t * t : 3 * (6 / 29) * (6 / 29) * (t - 4 / 29));
191
+ x = 0.95047 * fInv(x);
192
+ y = 1.0 * fInv(y);
193
+ z = 1.08883 * fInv(z);
194
+ let r = 3.2406 * x - 1.5372 * y - 0.4986 * z;
195
+ let g = -0.9689 * x + 1.8758 * y + 0.0415 * z;
196
+ let bl = 0.0557 * x - 0.204 * y + 1.057 * z;
197
+ const gamma = (c) => (c <= 0.0031308 ? 12.92 * c : 1.055 * Math.pow(Math.max(0, c), 1 / 2.4) - 0.055);
198
+ return {
199
+ r: Math.max(0, Math.min(255, Math.round(gamma(r) * 255))),
200
+ g: Math.max(0, Math.min(255, Math.round(gamma(g) * 255))),
201
+ b: Math.max(0, Math.min(255, Math.round(gamma(bl) * 255))),
202
+ a,
203
+ };
204
+ }
205
+
206
+ // Basic named colors fallback
207
+ const basicColors = {
208
+ white: { r: 255, g: 255, b: 255, a: 1 },
209
+ black: { r: 0, g: 0, b: 0, a: 1 },
210
+ red: { r: 255, g: 0, b: 0, a: 1 },
211
+ green: { r: 0, g: 128, b: 0, a: 1 },
212
+ blue: { r: 0, g: 0, b: 255, a: 1 },
213
+ yellow: { r: 255, g: 255, b: 0, a: 1 },
214
+ magenta: { r: 255, g: 0, b: 255, a: 1 },
215
+ cyan: { r: 0, g: 255, b: 255, a: 1 },
216
+ };
217
+
218
+ if (basicColors[colorStr]) {
219
+ return basicColors[colorStr];
220
+ }
221
+
222
+ return null;
223
+ }
224
+
225
+ function resolveColorMix(value) {
226
+ if (typeof value !== "string") return value;
227
+
228
+ let index;
229
+ while ((index = value.toLowerCase().indexOf("color-mix(")) !== -1) {
230
+ // Find matching closing parenthesis
231
+ let depth = 1;
232
+ let j = index + "color-mix(".length;
233
+ while (j < value.length && depth > 0) {
234
+ if (value[j] === "(") {
235
+ depth++;
236
+ } else if (value[j] === ")") {
237
+ depth--;
238
+ }
239
+ j++;
240
+ }
241
+
242
+ if (depth > 0) {
243
+ // Unmatched parenthesis, break to avoid infinite loop
244
+ break;
245
+ }
246
+
247
+ const innerContent = value.substring(index + "color-mix(".length, j - 1);
248
+
249
+ // Parse innerContent: "in <color-space>, <args>"
250
+ const commaIndex = innerContent.indexOf(",");
251
+ if (commaIndex === -1) {
252
+ // Invalid syntax, skip this one by replacing "color-mix(" with a placeholder temporarily
253
+ value = value.substring(0, index) + "COLOR_MIX_TEMP(" + value.substring(index + "color-mix(".length);
254
+ continue;
255
+ }
256
+
257
+ const argsPart = innerContent.substring(commaIndex + 1).trim();
258
+
259
+ // Check if argsPart contains another color-mix. If so, resolve the inner one first.
260
+ if (argsPart.toLowerCase().includes("color-mix(")) {
261
+ const resolvedArgsPart = resolveColorMix(argsPart);
262
+ value = value.substring(0, index + "color-mix(".length + commaIndex + 1) + " " + resolvedArgsPart + value.substring(j - 1);
263
+ continue;
264
+ }
265
+
266
+ // Now we can resolve this color-mix
267
+ const args = splitTopLevelCommas(argsPart);
268
+ if (args.length !== 2) {
269
+ // Invalid syntax
270
+ value = value.substring(0, index) + "COLOR_MIX_TEMP(" + value.substring(index + "color-mix(".length);
271
+ continue;
272
+ }
273
+
274
+ const arg1 = parseColorMixArg(args[0]);
275
+ const arg2 = parseColorMixArg(args[1]);
276
+
277
+ const c1 = parseColor(arg1.colorStr);
278
+ const c2 = parseColor(arg2.colorStr);
279
+
280
+ if (!c1 || !c2) {
281
+ // Parsing failed, skip
282
+ value = value.substring(0, index) + "COLOR_MIX_TEMP(" + value.substring(index + "color-mix(".length);
283
+ continue;
284
+ }
285
+
286
+ const p1 = arg1.percentage;
287
+ const p2 = arg2.percentage;
288
+
289
+ let w1, w2;
290
+ if (p1 !== null && p2 !== null) {
291
+ const sum = p1 + p2;
292
+ if (sum > 100) {
293
+ w1 = (p1 / sum) * 100;
294
+ w2 = (p2 / sum) * 100;
295
+ } else {
296
+ w1 = p1;
297
+ w2 = p2;
298
+ }
299
+ } else if (p1 !== null) {
300
+ w1 = p1;
301
+ w2 = 100 - p1;
302
+ } else if (p2 !== null) {
303
+ w2 = p2;
304
+ w1 = 100 - p2;
305
+ } else {
306
+ w1 = 50;
307
+ w2 = 50;
308
+ }
309
+
310
+ const a1 = c1.a;
311
+ const a2 = c2.a;
312
+
313
+ const totalWeight = w1 + w2;
314
+ let resolvedColor;
315
+ if (totalWeight === 0) {
316
+ resolvedColor = "transparent";
317
+ } else {
318
+ const f1 = w1 / totalWeight;
319
+ const f2 = w2 / totalWeight;
320
+
321
+ const mixedA = a1 * f1 + a2 * f2;
322
+
323
+ let mixedR, mixedG, mixedB;
324
+ if (mixedA === 0) {
325
+ mixedR = 0;
326
+ mixedG = 0;
327
+ mixedB = 0;
328
+ } else {
329
+ mixedR = Math.round((c1.r * a1 * f1 + c2.r * a2 * f2) / mixedA);
330
+ mixedG = Math.round((c1.g * a1 * f1 + c2.g * a2 * f2) / mixedA);
331
+ mixedB = Math.round((c1.b * a1 * f1 + c2.b * a2 * f2) / mixedA);
332
+ }
333
+
334
+ const finalA = totalWeight < 100 ? mixedA * (totalWeight / 100) : mixedA;
335
+ const roundedA = Math.round(finalA * 10000) / 10000;
336
+
337
+ resolvedColor = `rgba(${mixedR}, ${mixedG}, ${mixedB}, ${roundedA})`;
338
+ }
339
+
340
+ value = value.substring(0, index) + resolvedColor + value.substring(j);
341
+ }
342
+
343
+ // Restore placeholders
344
+ value = value.replace(/COLOR_MIX_TEMP\(/gi, "color-mix(");
345
+
346
+ return value;
347
+ }
348
+
349
+ function safeEvalMath(expr) {
350
+ if (!expr || typeof expr !== "string") return 0;
351
+
352
+ // Clean rem and px units if present
353
+ const sanitized = expr
354
+ .replace(/([\d.]+)rem/g, (_, n) => `${parseFloat(n) * 16}`)
355
+ .replace(/([\d.]+)px/g, "$1");
356
+
357
+ let pos = 0;
358
+ const len = sanitized.length;
359
+
360
+ function peek() {
361
+ while (pos < len && sanitized.charCodeAt(pos) <= 32) pos++;
362
+ return pos < len ? sanitized[pos] : "";
363
+ }
364
+
365
+ function get() {
366
+ const ch = peek();
367
+ pos++;
368
+ return ch;
369
+ }
370
+
371
+ function parseFactor() {
372
+ const ch = peek();
373
+ if (ch === "+") {
374
+ get();
375
+ return parseFactor();
376
+ }
377
+ if (ch === "-") {
378
+ get();
379
+ return -parseFactor();
380
+ }
381
+ if (ch === "(") {
382
+ get();
383
+ const val = parseExpression();
384
+ if (peek() === ")") get();
385
+ return val;
386
+ }
387
+
388
+ // Parse number
389
+ const start = pos;
390
+ let hasDot = false;
391
+ while (pos < len) {
392
+ const c = sanitized[pos];
393
+ if (c >= "0" && c <= "9") {
394
+ pos++;
395
+ } else if (c === "." && !hasDot) {
396
+ hasDot = true;
397
+ pos++;
398
+ } else {
399
+ break;
400
+ }
401
+ }
402
+
403
+ if (start === pos) return 0;
404
+ const numStr = sanitized.slice(start, pos);
405
+ const num = parseFloat(numStr);
406
+ return isNaN(num) ? 0 : num;
407
+ }
408
+
409
+ function parseTerm() {
410
+ let result = parseFactor();
411
+ while (true) {
412
+ const op = peek();
413
+ if (op === "*" || op === "/") {
414
+ get();
415
+ const factor = parseFactor();
416
+ result = op === "*" ? result * factor : (factor !== 0 ? result / factor : 0);
417
+ } else {
418
+ break;
419
+ }
420
+ }
421
+ return result;
422
+ }
423
+
424
+ function parseExpression() {
425
+ let result = parseTerm();
426
+ while (true) {
427
+ const op = peek();
428
+ if (op === "+" || op === "-") {
429
+ get();
430
+ const term = parseTerm();
431
+ result = op === "+" ? result + term : result - term;
432
+ } else {
433
+ break;
434
+ }
435
+ }
436
+ return result;
437
+ }
438
+
439
+ try {
440
+ const val = parseExpression();
441
+ return isNaN(val) ? 0 : val;
442
+ } catch {
443
+ return 0;
444
+ }
445
+ }
446
+
447
+ export default function CssCalc() {
448
+ this.calc = (value) => {
449
+ if (value === undefined || typeof value !== "string") return value;
450
+
451
+ return value.replace(calcRe, (_, calc) => {
452
+ return safeEvalMath(calc);
453
+ });
454
+ };
455
+
456
+ this.calcColor = (value) => {
457
+ if (value === undefined || typeof value !== "string") return value;
458
+
459
+ value = resolveColorMix(value);
460
+
461
+ // If the entire value is a single color (hex, rgb, hsl, oklch, lab), normalize to hex/rgba
462
+ const parsed = parseColor(value);
463
+ if (parsed) {
464
+ if (parsed.a === 1) {
465
+ return `#${itohex(parsed.r)}${itohex(parsed.g)}${itohex(parsed.b)}`;
466
+ }
467
+ const a = Math.round(parsed.a * 255);
468
+ return `#${itohex(parsed.r)}${itohex(parsed.g)}${itohex(parsed.b)}${itohex(a)}`;
469
+ }
470
+
471
+ value = value.replace(colorRe1, (_, r, g, b, a) => {
472
+ a = parseFloat(a);
473
+ if (isNaN(a) || a >= 1) return `#${itohex(r)}${itohex(g)}${itohex(b)}`;
474
+
475
+ a = Math.round(a * 255);
476
+ return `#${itohex(r)}${itohex(g)}${itohex(b)}${itohex(a)}`;
477
+ });
478
+ value = value.replace(colorRe2, (_, r, g, b, a) => {
479
+ a = parseFloat(a);
480
+ if (isNaN(a) || a >= 1) return `#${itohex(r)}${itohex(g)}${itohex(b)}`;
481
+
482
+ a = Math.round(a * 255);
483
+ return `#${itohex(r)}${itohex(g)}${itohex(b)}${itohex(a)}`;
484
+ });
485
+
486
+ return value;
487
+ };
488
+
489
+ return this;
490
+ }
@@ -0,0 +1,78 @@
1
+ import { camelize } from "../utils/helper";
2
+ import CssTransform from "./css-transform";
3
+
4
+ const SUPPORTED_MEDIA_TYPE = [
5
+ "min-width",
6
+ "min-height",
7
+ "max-width",
8
+ "max-height",
9
+ "prefers-color-scheme",
10
+ ];
11
+
12
+ export default function CssMedia() {
13
+ const transform = new CssTransform();
14
+
15
+ this.match = (media, { width, height, colorScheme } = {}) => {
16
+ const isMedia = media.startsWith("@media");
17
+ if (!isMedia) return [false];
18
+
19
+ let isValidMedia = true;
20
+ const widthRange = [Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY];
21
+ const heightRange = [Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY];
22
+
23
+ const mediaGroupsRe = /\([^)]*\)/g;
24
+ const mediaGroups = media.match(mediaGroupsRe)?.map((rawGroup) => {
25
+ const mediaGroupRe = /\(\s*([^:)\s]+)\s*:\s*([^:)\s]+)\s*\)/g;
26
+ let [, mediaType, mediaValue] = mediaGroupRe.exec(rawGroup) || [];
27
+
28
+ if (!SUPPORTED_MEDIA_TYPE.includes(mediaType)) {
29
+ isValidMedia = false;
30
+ return null;
31
+ }
32
+
33
+ mediaType = camelize(mediaType);
34
+
35
+ mediaValue = transform.transformUnsupportedUnit(mediaValue);
36
+ mediaValue = transform.transformViewportUnit(mediaValue, { width, height });
37
+ mediaValue = transform.removeUnit(mediaValue);
38
+
39
+ if (mediaType === "minWidth") {
40
+ widthRange[0] = mediaValue;
41
+ } else if (mediaType === "minHeight") {
42
+ heightRange[0] = mediaValue;
43
+ } else if (mediaType === "maxWidth") {
44
+ widthRange[1] = mediaValue;
45
+ } else if (mediaType === "maxHeight") {
46
+ heightRange[1] = mediaValue;
47
+ }
48
+
49
+ return [mediaType, mediaValue];
50
+ });
51
+ if (!Array.isArray(mediaGroups) || mediaGroups.length === 0) {
52
+ isValidMedia = false;
53
+ }
54
+ if (!isValidMedia) return [true, false];
55
+
56
+ if (mediaGroups[0][0] === "prefersColorScheme" && mediaGroups[0][1] === "dark") {
57
+ if (colorScheme === "dark") {
58
+ return [true, true];
59
+ } else {
60
+ return [true, false];
61
+ }
62
+ }
63
+
64
+ const isValidRange = widthRange[0] <= widthRange[1] && heightRange[0] <= heightRange[1];
65
+ if (!isValidRange) return [true, false];
66
+
67
+ const isMatchedMedia =
68
+ widthRange[0] <= width &&
69
+ width <= widthRange[1] &&
70
+ heightRange[0] <= height &&
71
+ height <= heightRange[1];
72
+ if (!isMatchedMedia) return [true, false];
73
+
74
+ return [true, true];
75
+ };
76
+
77
+ return this;
78
+ }