rbx-css 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/dist/lib.js ADDED
@@ -0,0 +1,2812 @@
1
+ import { createRequire } from "node:module";
2
+ var __create = Object.create;
3
+ var __getProtoOf = Object.getPrototypeOf;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ function __accessProp(key) {
8
+ return this[key];
9
+ }
10
+ var __toESMCache_node;
11
+ var __toESMCache_esm;
12
+ var __toESM = (mod, isNodeMode, target) => {
13
+ var canCache = mod != null && typeof mod === "object";
14
+ if (canCache) {
15
+ var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
16
+ var cached = cache.get(mod);
17
+ if (cached)
18
+ return cached;
19
+ }
20
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
21
+ const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
22
+ for (let key of __getOwnPropNames(mod))
23
+ if (!__hasOwnProp.call(to, key))
24
+ __defProp(to, key, {
25
+ get: __accessProp.bind(mod, key),
26
+ enumerable: true
27
+ });
28
+ if (canCache)
29
+ cache.set(mod, to);
30
+ return to;
31
+ };
32
+ var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
33
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
34
+
35
+ // src/parser/css-parser.ts
36
+ import { transform } from "lightningcss";
37
+ function resolveNestingSelector(parentSelectors, childSelectors) {
38
+ const resolved = [];
39
+ for (const childSel of childSelectors) {
40
+ const nestingIndex = childSel.findIndex((comp) => comp.type === "nesting");
41
+ if (nestingIndex === -1) {
42
+ for (const parentSel of parentSelectors) {
43
+ resolved.push([
44
+ ...parentSel,
45
+ { type: "combinator", value: "descendant" },
46
+ ...childSel
47
+ ]);
48
+ }
49
+ } else {
50
+ for (const parentSel of parentSelectors) {
51
+ const newSel = [
52
+ ...childSel.slice(0, nestingIndex),
53
+ ...parentSel,
54
+ ...childSel.slice(nestingIndex + 1)
55
+ ];
56
+ resolved.push(newSel);
57
+ }
58
+ }
59
+ }
60
+ return resolved;
61
+ }
62
+ function resolveFullSelectorStack(selectorStack) {
63
+ if (selectorStack.length === 0)
64
+ return [];
65
+ if (selectorStack.length === 1)
66
+ return selectorStack[0];
67
+ let resolved = selectorStack[0];
68
+ for (let i = 1;i < selectorStack.length; i++) {
69
+ resolved = resolveNestingSelector(resolved, selectorStack[i]);
70
+ }
71
+ return resolved;
72
+ }
73
+ function parseCSS(source, filename) {
74
+ const collectedRules = [];
75
+ const collectedMedia = [];
76
+ const mediaStack = [];
77
+ const selectorStack = [];
78
+ transform({
79
+ filename,
80
+ code: Buffer.from(source),
81
+ visitor: {
82
+ Rule(rule) {
83
+ if (rule.type === "media") {
84
+ const value = rule.value;
85
+ const query = JSON.parse(JSON.stringify(value.query));
86
+ const entry = { query, rules: [] };
87
+ collectedMedia.push(entry);
88
+ mediaStack.push({ query, entry });
89
+ } else if (rule.type === "style") {
90
+ const value = rule.value;
91
+ const selectors = value.selectors;
92
+ const decls = value.declarations;
93
+ selectorStack.push(selectors);
94
+ const parsed = {
95
+ selectors: JSON.parse(JSON.stringify(selectors)),
96
+ declarations: JSON.parse(JSON.stringify(decls.declarations))
97
+ };
98
+ if (parsed.declarations.length > 0) {
99
+ if (mediaStack.length > 0) {
100
+ mediaStack[mediaStack.length - 1].entry.rules.push(parsed);
101
+ } else {
102
+ collectedRules.push(parsed);
103
+ }
104
+ }
105
+ } else if (rule.type === "nested-declarations") {
106
+ const value = rule.value;
107
+ const decls = value.declarations;
108
+ const declarations = JSON.parse(JSON.stringify(decls.declarations));
109
+ if (declarations.length > 0 && selectorStack.length > 0) {
110
+ const resolvedSelectors = resolveFullSelectorStack(selectorStack.map((s) => JSON.parse(JSON.stringify(s))));
111
+ const parsed = {
112
+ selectors: resolvedSelectors,
113
+ declarations
114
+ };
115
+ if (mediaStack.length > 0) {
116
+ mediaStack[mediaStack.length - 1].entry.rules.push(parsed);
117
+ } else {
118
+ collectedRules.push(parsed);
119
+ }
120
+ }
121
+ }
122
+ },
123
+ RuleExit(rule) {
124
+ if (rule.type === "media") {
125
+ mediaStack.pop();
126
+ } else if (rule.type === "style") {
127
+ selectorStack.pop();
128
+ }
129
+ }
130
+ }
131
+ });
132
+ return { rules: collectedRules, mediaRules: collectedMedia };
133
+ }
134
+
135
+ // src/mappers/colors.ts
136
+ function convertCssColor(cssColor) {
137
+ if (!cssColor || typeof cssColor !== "object")
138
+ return null;
139
+ const c = cssColor;
140
+ if (c.type === "rgb") {
141
+ const r = Math.round(c.r);
142
+ const g = Math.round(c.g);
143
+ const b = Math.round(c.b);
144
+ const alpha = c.alpha;
145
+ return {
146
+ color: [r, g, b],
147
+ transparency: alpha < 1 ? Math.round((1 - alpha) * 100) / 100 : undefined
148
+ };
149
+ }
150
+ if (c.type === "oklch") {
151
+ const rgb = oklchToRgb(c.l, c.c, c.h);
152
+ const alpha = c.alpha;
153
+ return {
154
+ color: rgb,
155
+ transparency: alpha < 1 ? Math.round((1 - alpha) * 100) / 100 : undefined
156
+ };
157
+ }
158
+ if (c.type === "oklab") {
159
+ const rgb = oklabToRgb(c.l, c.a, c.b);
160
+ const alpha = c.alpha;
161
+ return {
162
+ color: rgb,
163
+ transparency: alpha < 1 ? Math.round((1 - alpha) * 100) / 100 : undefined
164
+ };
165
+ }
166
+ if (c.type === "lab") {
167
+ const rgb = labToRgb(c.l, c.a, c.b);
168
+ const alpha = c.alpha;
169
+ return {
170
+ color: rgb,
171
+ transparency: alpha < 1 ? Math.round((1 - alpha) * 100) / 100 : undefined
172
+ };
173
+ }
174
+ if (c.type === "lch") {
175
+ const hRad = c.h * Math.PI / 180;
176
+ const a = c.c * Math.cos(hRad);
177
+ const b = c.c * Math.sin(hRad);
178
+ const rgb = labToRgb(c.l, a, b);
179
+ const alpha = c.alpha;
180
+ return {
181
+ color: rgb,
182
+ transparency: alpha < 1 ? Math.round((1 - alpha) * 100) / 100 : undefined
183
+ };
184
+ }
185
+ if (c.type === "currentcolor") {
186
+ return null;
187
+ }
188
+ return null;
189
+ }
190
+ function gammaCorrect(x) {
191
+ return x <= 0.0031308 ? x * 12.92 : 1.055 * Math.pow(x, 1 / 2.4) - 0.055;
192
+ }
193
+ function clampRgb(r, g, b) {
194
+ return [
195
+ Math.round(Math.max(0, Math.min(1, r)) * 255),
196
+ Math.round(Math.max(0, Math.min(1, g)) * 255),
197
+ Math.round(Math.max(0, Math.min(1, b)) * 255)
198
+ ];
199
+ }
200
+ function oklabToRgb(L, a, b) {
201
+ const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
202
+ const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
203
+ const s_ = L - 0.0894841775 * a - 1.291485548 * b;
204
+ const l = l_ * l_ * l_;
205
+ const m = m_ * m_ * m_;
206
+ const s = s_ * s_ * s_;
207
+ const rLin = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s;
208
+ const gLin = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s;
209
+ const bLin = -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s;
210
+ return clampRgb(gammaCorrect(rLin), gammaCorrect(gLin), gammaCorrect(bLin));
211
+ }
212
+ function oklchToRgb(L, C, H) {
213
+ const hRad = H * Math.PI / 180;
214
+ return oklabToRgb(L, C * Math.cos(hRad), C * Math.sin(hRad));
215
+ }
216
+ function labToRgb(L, a, b) {
217
+ const fy = (L + 16) / 116;
218
+ const fx = a / 500 + fy;
219
+ const fz = fy - b / 200;
220
+ const delta = 6 / 29;
221
+ const invF = (t) => t > delta ? t * t * t : 3 * delta * delta * (t - 4 / 29);
222
+ const X = 0.95047 * invF(fx);
223
+ const Y = 1 * invF(fy);
224
+ const Z = 1.08883 * invF(fz);
225
+ const rLin = 3.2404542 * X - 1.5371385 * Y - 0.4985314 * Z;
226
+ const gLin = -0.969266 * X + 1.8760108 * Y + 0.041556 * Z;
227
+ const bLin = 0.0556434 * X - 0.2040259 * Y + 1.0572252 * Z;
228
+ return clampRgb(gammaCorrect(rLin), gammaCorrect(gLin), gammaCorrect(bLin));
229
+ }
230
+ function rgbToHex(rgb) {
231
+ return "#" + rgb.map((c) => Math.round(c).toString(16).padStart(2, "0")).join("");
232
+ }
233
+
234
+ // src/ir/tokens.ts
235
+ function extractTokens(rules, warnings) {
236
+ const tokens = new Map;
237
+ const nonRootRules = [];
238
+ for (const rule of rules) {
239
+ if (isRootSelector(rule.selectors)) {
240
+ for (const decl of rule.declarations) {
241
+ const d = decl;
242
+ if (d.property === "custom") {
243
+ const val = d.value;
244
+ const name = val.name.replace(/^--/, "");
245
+ const tokenValue = inferTokenValue(val.value, name, warnings);
246
+ if (tokenValue) {
247
+ tokens.set(name, tokenValue);
248
+ }
249
+ }
250
+ }
251
+ } else {
252
+ nonRootRules.push(rule);
253
+ }
254
+ }
255
+ return { tokens, nonRootRules };
256
+ }
257
+ function isRootSelector(selectors) {
258
+ return selectors.some((sel) => {
259
+ if (sel.length !== 1)
260
+ return false;
261
+ const comp = sel[0];
262
+ return comp.type === "pseudo-class" && comp.kind === "root";
263
+ });
264
+ }
265
+ function inferTokenValue(tokens, _varName, _warnings) {
266
+ if (!tokens || tokens.length === 0)
267
+ return null;
268
+ const first = tokens[0];
269
+ if (first.type === "color") {
270
+ const colorResult = convertCssColor(first.value);
271
+ if (colorResult) {
272
+ return { type: "Color3", value: colorResult.color };
273
+ }
274
+ }
275
+ if (first.type === "length") {
276
+ const val = first.value;
277
+ if (val.unit === "px") {
278
+ return { type: "UDim", value: [0, val.value] };
279
+ }
280
+ if (val.unit === "%") {
281
+ return { type: "UDim", value: [val.value / 100, 0] };
282
+ }
283
+ }
284
+ if (first.type === "dimension") {
285
+ const val = first.value;
286
+ if (val.unit === "px") {
287
+ return { type: "UDim", value: [0, val.value] };
288
+ }
289
+ }
290
+ if (first.type === "token") {
291
+ const tok = first.value;
292
+ if (tok.type === "string") {
293
+ return { type: "string", value: tok.value };
294
+ }
295
+ if (tok.type === "number") {
296
+ return { type: "number", value: tok.value };
297
+ }
298
+ if (tok.type === "dimension") {
299
+ const dim = tok.value;
300
+ return {
301
+ type: "UDim",
302
+ value: [0, dim.value]
303
+ };
304
+ }
305
+ if (tok.type === "ident") {
306
+ return { type: "string", value: tok.value };
307
+ }
308
+ }
309
+ if (typeof first === "string") {
310
+ return { type: "string", value: first };
311
+ }
312
+ return null;
313
+ }
314
+
315
+ // src/ir/themes.ts
316
+ function extractThemes(parsed, warnings) {
317
+ const themes = new Map;
318
+ for (const rule of parsed.rules) {
319
+ const themeName = extractDataThemeAttribute(rule.selectors);
320
+ if (themeName) {
321
+ const themeTokens = themes.get(themeName) ?? new Map;
322
+ extractCustomProperties(rule.declarations, themeTokens, warnings);
323
+ themes.set(themeName, themeTokens);
324
+ }
325
+ }
326
+ for (const media of parsed.mediaRules) {
327
+ const scheme = extractColorScheme(media.query);
328
+ if (scheme) {
329
+ const themeTokens = themes.get(scheme) ?? new Map;
330
+ for (const rule of media.rules) {
331
+ if (isRootSelector2(rule.selectors)) {
332
+ extractCustomProperties(rule.declarations, themeTokens, warnings);
333
+ }
334
+ }
335
+ themes.set(scheme, themeTokens);
336
+ }
337
+ }
338
+ return themes;
339
+ }
340
+ function extractDataThemeAttribute(selectors) {
341
+ for (const sel of selectors) {
342
+ for (const comp of sel) {
343
+ const c = comp;
344
+ if (c.type === "attribute") {
345
+ if (c.name === "data-theme") {
346
+ const op = c.operation;
347
+ if (op?.operator === "equal") {
348
+ return op.value;
349
+ }
350
+ }
351
+ }
352
+ }
353
+ }
354
+ return null;
355
+ }
356
+ function extractColorScheme(query) {
357
+ return findColorScheme(query);
358
+ }
359
+ function findColorScheme(obj) {
360
+ if (!obj || typeof obj !== "object")
361
+ return null;
362
+ const o = obj;
363
+ if (o.type === "feature") {
364
+ const value = o.value;
365
+ if (value?.type === "plain") {
366
+ const name = value.name;
367
+ if (name === "prefers-color-scheme") {
368
+ const featureValue = value.value;
369
+ if (featureValue?.type === "ident") {
370
+ return featureValue.value;
371
+ }
372
+ }
373
+ }
374
+ }
375
+ if (Array.isArray(obj)) {
376
+ for (const item of obj) {
377
+ const found = findColorScheme(item);
378
+ if (found)
379
+ return found;
380
+ }
381
+ } else {
382
+ for (const val of Object.values(o)) {
383
+ const found = findColorScheme(val);
384
+ if (found)
385
+ return found;
386
+ }
387
+ }
388
+ return null;
389
+ }
390
+ function isRootSelector2(selectors) {
391
+ return selectors.some((sel) => {
392
+ if (sel.length !== 1)
393
+ return false;
394
+ const comp = sel[0];
395
+ return comp.type === "pseudo-class" && comp.kind === "root";
396
+ });
397
+ }
398
+ function extractCustomProperties(declarations, tokens, warnings) {
399
+ for (const decl of declarations) {
400
+ const d = decl;
401
+ if (d.property === "custom") {
402
+ const val = d.value;
403
+ const name = val.name.replace(/^--/, "");
404
+ const tokenValue = inferTokenValue2(val.value);
405
+ if (tokenValue) {
406
+ tokens.set(name, tokenValue);
407
+ }
408
+ }
409
+ }
410
+ }
411
+ function inferTokenValue2(tokens) {
412
+ if (!tokens || tokens.length === 0)
413
+ return null;
414
+ const first = tokens[0];
415
+ if (first.type === "color") {
416
+ const colorResult = convertCssColor(first.value);
417
+ if (colorResult) {
418
+ return { type: "Color3", value: colorResult.color };
419
+ }
420
+ }
421
+ if (first.type === "length") {
422
+ const val = first.value;
423
+ if (val.unit === "px") {
424
+ return { type: "UDim", value: [0, val.value] };
425
+ }
426
+ }
427
+ if (first.type === "token") {
428
+ const tok = first.value;
429
+ if (tok.type === "string") {
430
+ return { type: "string", value: tok.value };
431
+ }
432
+ if (tok.type === "number") {
433
+ return { type: "number", value: tok.value };
434
+ }
435
+ }
436
+ return null;
437
+ }
438
+
439
+ // src/mappings/elements.ts
440
+ var HTML_TO_ROBLOX = {
441
+ div: "Frame",
442
+ span: "TextLabel",
443
+ p: "TextLabel",
444
+ h1: "TextLabel",
445
+ h2: "TextLabel",
446
+ h3: "TextLabel",
447
+ h4: "TextLabel",
448
+ h5: "TextLabel",
449
+ h6: "TextLabel",
450
+ button: "TextButton",
451
+ input: "TextBox",
452
+ img: "ImageLabel",
453
+ a: "TextButton",
454
+ canvas: "ViewportFrame",
455
+ label: "TextLabel",
456
+ textarea: "TextBox",
457
+ video: "VideoFrame",
458
+ scroll: "ScrollingFrame",
459
+ nav: "Frame",
460
+ header: "Frame",
461
+ footer: "Frame",
462
+ main: "Frame",
463
+ section: "Frame",
464
+ article: "Frame",
465
+ aside: "Frame",
466
+ form: "Frame",
467
+ ul: "Frame",
468
+ ol: "Frame",
469
+ li: "Frame",
470
+ table: "Frame",
471
+ thead: "Frame",
472
+ tbody: "Frame",
473
+ tfoot: "Frame",
474
+ tr: "Frame",
475
+ td: "TextLabel",
476
+ th: "TextLabel",
477
+ dialog: "Frame",
478
+ details: "Frame",
479
+ summary: "TextButton",
480
+ select: "Frame",
481
+ option: "TextButton",
482
+ optgroup: "Frame"
483
+ };
484
+
485
+ // src/mappings/roblox-classes.ts
486
+ var ROBLOX_GUI_CLASSES = new Set([
487
+ "Frame",
488
+ "ScrollingFrame",
489
+ "TextLabel",
490
+ "TextButton",
491
+ "TextBox",
492
+ "ImageLabel",
493
+ "ImageButton",
494
+ "ViewportFrame",
495
+ "VideoFrame",
496
+ "CanvasGroup",
497
+ "BillboardGui",
498
+ "SurfaceGui",
499
+ "ScreenGui",
500
+ "GuiButton",
501
+ "GuiObject",
502
+ "LayerCollector"
503
+ ]);
504
+
505
+ // src/mappings/pseudo-classes.ts
506
+ var CSS_TO_ROBLOX_PSEUDO = {
507
+ hover: "Hover",
508
+ active: "Press",
509
+ focus: "NonDefault",
510
+ disabled: "NonDefault"
511
+ };
512
+
513
+ // src/mappers/selector.ts
514
+ var IGNORED_HTML_ELEMENTS = new Set([
515
+ "html",
516
+ "body",
517
+ "head",
518
+ "meta",
519
+ "link",
520
+ "style",
521
+ "script",
522
+ "noscript",
523
+ "hr",
524
+ "br",
525
+ "b",
526
+ "i",
527
+ "em",
528
+ "strong",
529
+ "u",
530
+ "s",
531
+ "mark",
532
+ "small",
533
+ "sub",
534
+ "sup",
535
+ "code",
536
+ "kbd",
537
+ "samp",
538
+ "pre",
539
+ "blockquote",
540
+ "cite",
541
+ "q",
542
+ "abbr",
543
+ "address",
544
+ "dl",
545
+ "dt",
546
+ "dd",
547
+ "menu",
548
+ "caption",
549
+ "colgroup",
550
+ "col",
551
+ "fieldset",
552
+ "legend",
553
+ "figure",
554
+ "figcaption",
555
+ "audio",
556
+ "source",
557
+ "track",
558
+ "embed",
559
+ "object",
560
+ "iframe",
561
+ "svg",
562
+ "picture",
563
+ "progress",
564
+ "meter",
565
+ "output",
566
+ "map",
567
+ "area",
568
+ "ruby",
569
+ "rt",
570
+ "rp",
571
+ "wbr",
572
+ "data",
573
+ "time",
574
+ "var",
575
+ "dfn",
576
+ "ins",
577
+ "del"
578
+ ]);
579
+ var IGNORED_PSEUDO_CLASSES = new Set([
580
+ "host",
581
+ "where",
582
+ "is",
583
+ "has",
584
+ "not",
585
+ "first-child",
586
+ "last-child",
587
+ "nth-child",
588
+ "nth-last-child",
589
+ "first-of-type",
590
+ "last-of-type",
591
+ "only-child",
592
+ "only-of-type",
593
+ "empty",
594
+ "checked",
595
+ "indeterminate",
596
+ "default",
597
+ "valid",
598
+ "invalid",
599
+ "required",
600
+ "optional",
601
+ "read-only",
602
+ "read-write",
603
+ "placeholder-shown",
604
+ "autofill",
605
+ "enabled",
606
+ "link",
607
+ "visited",
608
+ "any-link",
609
+ "target",
610
+ "scope",
611
+ "defined",
612
+ "before",
613
+ "after",
614
+ "placeholder",
615
+ "selection",
616
+ "marker",
617
+ "backdrop",
618
+ "custom"
619
+ ]);
620
+ function escapeSelectorIdent(name) {
621
+ return name.replace(/([^a-zA-Z0-9_-])/g, "\\$1");
622
+ }
623
+ function mapSelector(components, warnings) {
624
+ const parts = [];
625
+ for (const comp of components) {
626
+ switch (comp.type) {
627
+ case "class":
628
+ parts.push(`.${escapeSelectorIdent(comp.name)}`);
629
+ break;
630
+ case "id":
631
+ parts.push(`#${escapeSelectorIdent(comp.name)}`);
632
+ break;
633
+ case "type": {
634
+ const name = comp.name;
635
+ if (ROBLOX_GUI_CLASSES.has(name)) {
636
+ parts.push(name);
637
+ } else if (HTML_TO_ROBLOX[name]) {
638
+ parts.push(HTML_TO_ROBLOX[name]);
639
+ } else if (IGNORED_HTML_ELEMENTS.has(name)) {
640
+ return null;
641
+ } else {
642
+ parts.push(name);
643
+ warnings.warn({
644
+ code: "unsupported-selector",
645
+ message: `Unknown element '${name}', passing through as-is`
646
+ });
647
+ }
648
+ break;
649
+ }
650
+ case "pseudo-class": {
651
+ const kind = comp.kind;
652
+ const mapped = CSS_TO_ROBLOX_PSEUDO[kind];
653
+ if (mapped) {
654
+ parts.push(`:${mapped}`);
655
+ } else if (kind === "root") {
656
+ return null;
657
+ } else if (IGNORED_PSEUDO_CLASSES.has(kind)) {
658
+ return null;
659
+ } else {
660
+ warnings.warn({
661
+ code: "unsupported-selector",
662
+ message: `Pseudo-class ':${kind}' has no Roblox equivalent`
663
+ });
664
+ }
665
+ break;
666
+ }
667
+ case "combinator":
668
+ if (comp.value === "child") {
669
+ parts.push(" > ");
670
+ } else if (comp.value === "descendant") {
671
+ parts.push(" ");
672
+ } else {
673
+ warnings.warn({
674
+ code: "unsupported-selector",
675
+ message: `Combinator '${comp.value}' not supported in Roblox`
676
+ });
677
+ return null;
678
+ }
679
+ break;
680
+ case "universal":
681
+ return null;
682
+ case "attribute":
683
+ return null;
684
+ default:
685
+ break;
686
+ }
687
+ }
688
+ return parts.join("") || null;
689
+ }
690
+ function isDataThemeSelector(selectors) {
691
+ for (const sel of selectors) {
692
+ for (const comp of sel) {
693
+ if (comp.type === "attribute") {
694
+ const c = comp;
695
+ if (c.name === "data-theme") {
696
+ const op = c.operation;
697
+ if (op?.operator === "equal") {
698
+ return op.value;
699
+ }
700
+ }
701
+ }
702
+ }
703
+ }
704
+ return null;
705
+ }
706
+
707
+ // src/mappers/units.ts
708
+ function convertLengthDimension(dim, warnings) {
709
+ if (!dim || typeof dim !== "object")
710
+ return null;
711
+ const d = dim;
712
+ if (d.type === "dimension") {
713
+ const val = d.value;
714
+ const unit = val.unit;
715
+ const value = val.value;
716
+ switch (unit) {
717
+ case "px":
718
+ return { scale: 0, offset: value };
719
+ case "rem":
720
+ return { scale: 0, offset: value * 16 };
721
+ case "em":
722
+ return { scale: 0, offset: value * 16 };
723
+ case "vw":
724
+ case "vh":
725
+ return { scale: value / 100, offset: 0 };
726
+ default:
727
+ warnings.warn({
728
+ code: "unsupported-unit",
729
+ message: `'${unit}' not supported, consider using 'px' - skipped`
730
+ });
731
+ return null;
732
+ }
733
+ }
734
+ if (d.type === "percentage") {
735
+ const value = d.value;
736
+ return { scale: value, offset: 0 };
737
+ }
738
+ if (d.type === "calc") {
739
+ warnings.warn({
740
+ code: "unsupported-unit",
741
+ message: "calc() expressions are not supported - skipped"
742
+ });
743
+ return null;
744
+ }
745
+ return null;
746
+ }
747
+ function toUDim(result) {
748
+ return { type: "UDim", value: [result.scale, result.offset] };
749
+ }
750
+
751
+ // src/mappers/fonts.ts
752
+ var FONT_FAMILY_MAP = {
753
+ gothamssm: "GothamSSm",
754
+ gotham: "GothamSSm",
755
+ "builder sans": "BuilderSans",
756
+ "source sans pro": "SourceSansPro",
757
+ roboto: "Roboto",
758
+ montserrat: "Montserrat",
759
+ monospace: "RobotoMono",
760
+ "sans-serif": "GothamSSm",
761
+ serif: "Merriweather"
762
+ };
763
+ function mapFontFamily(families, warnings) {
764
+ for (const raw of families) {
765
+ const normalized = raw.replace(/['"]/g, "").trim().toLowerCase();
766
+ const mapped = FONT_FAMILY_MAP[normalized];
767
+ if (mapped)
768
+ return mapped;
769
+ }
770
+ const name = families[0].replace(/['"]/g, "").trim();
771
+ warnings.warn({
772
+ code: "partial-mapping",
773
+ message: `Unknown font '${name}', assuming rbxasset://fonts/families/${name}.json`
774
+ });
775
+ return name;
776
+ }
777
+ var WEIGHT_MAP = {
778
+ "100": "Thin",
779
+ thin: "Thin",
780
+ "200": "ExtraLight",
781
+ "extra-light": "ExtraLight",
782
+ "300": "Light",
783
+ light: "Light",
784
+ "400": "Regular",
785
+ normal: "Regular",
786
+ "500": "Medium",
787
+ medium: "Medium",
788
+ "600": "SemiBold",
789
+ "semi-bold": "SemiBold",
790
+ "700": "Bold",
791
+ bold: "Bold",
792
+ "800": "ExtraBold",
793
+ "extra-bold": "ExtraBold",
794
+ "900": "Heavy",
795
+ black: "Heavy"
796
+ };
797
+ function mapFontWeight(weight) {
798
+ return WEIGHT_MAP[String(weight).toLowerCase()] ?? "Regular";
799
+ }
800
+ function mapFontStyle(style) {
801
+ return style === "italic" ? "Italic" : "Normal";
802
+ }
803
+
804
+ // src/mappers/properties.ts
805
+ var IGNORED_CSS_PROPERTIES = new Set([
806
+ "box-sizing",
807
+ "text-decoration",
808
+ "text-decoration-line",
809
+ "text-decoration-style",
810
+ "text-decoration-color",
811
+ "text-decoration-thickness",
812
+ "text-size-adjust",
813
+ "tab-size",
814
+ "text-indent",
815
+ "text-transform",
816
+ "text-rendering",
817
+ "text-shadow",
818
+ "white-space",
819
+ "word-spacing",
820
+ "letter-spacing",
821
+ "list-style",
822
+ "list-style-type",
823
+ "list-style-position",
824
+ "list-style-image",
825
+ "resize",
826
+ "appearance",
827
+ "bottom",
828
+ "right",
829
+ "float",
830
+ "clear",
831
+ "content",
832
+ "counter-increment",
833
+ "counter-reset",
834
+ "quotes",
835
+ "border-collapse",
836
+ "border-spacing",
837
+ "table-layout",
838
+ "caption-side",
839
+ "empty-cells",
840
+ "border-top-width",
841
+ "border-right-width",
842
+ "border-bottom-width",
843
+ "border-left-width",
844
+ "border-top-style",
845
+ "border-right-style",
846
+ "border-bottom-style",
847
+ "border-left-style",
848
+ "border-top-color",
849
+ "border-right-color",
850
+ "border-bottom-color",
851
+ "border-left-color",
852
+ "border-image",
853
+ "border-image-source",
854
+ "border-image-slice",
855
+ "border-image-width",
856
+ "border-image-outset",
857
+ "border-image-repeat",
858
+ "outline-offset",
859
+ "background-position",
860
+ "background-position-x",
861
+ "background-position-y",
862
+ "background-repeat",
863
+ "background-size",
864
+ "background-origin",
865
+ "background-clip",
866
+ "background-attachment",
867
+ "transition",
868
+ "transition-property",
869
+ "transition-duration",
870
+ "transition-timing-function",
871
+ "transition-delay",
872
+ "transition-behavior",
873
+ "animation",
874
+ "animation-name",
875
+ "animation-duration",
876
+ "animation-timing-function",
877
+ "animation-delay",
878
+ "animation-iteration-count",
879
+ "animation-direction",
880
+ "animation-fill-mode",
881
+ "animation-play-state",
882
+ "pointer-events",
883
+ "user-select",
884
+ "touch-action",
885
+ "will-change",
886
+ "contain",
887
+ "isolation",
888
+ "mix-blend-mode",
889
+ "filter",
890
+ "backdrop-filter",
891
+ "clip-path",
892
+ "mask",
893
+ "mask-image",
894
+ "object-position",
895
+ "scroll-behavior",
896
+ "scroll-margin",
897
+ "scroll-padding",
898
+ "overscroll-behavior",
899
+ "hyphens",
900
+ "writing-mode",
901
+ "direction",
902
+ "unicode-bidi",
903
+ "columns",
904
+ "column-count",
905
+ "column-gap",
906
+ "column-rule",
907
+ "column-span",
908
+ "column-width",
909
+ "break-before",
910
+ "break-after",
911
+ "break-inside",
912
+ "page-break-before",
913
+ "page-break-after",
914
+ "page-break-inside",
915
+ "orphans",
916
+ "widows",
917
+ "accent-color",
918
+ "caret-color",
919
+ "color-scheme",
920
+ "forced-color-adjust",
921
+ "print-color-adjust"
922
+ ]);
923
+ function mapDeclarations(declarations, warnings) {
924
+ const props = new Map;
925
+ const acc = { hasFlex: false, hasGrid: false };
926
+ for (const decl of declarations) {
927
+ mapSingleDeclaration(decl, props, acc, warnings);
928
+ }
929
+ const pseudoInstances = finalizeAccumulator(acc, props, warnings);
930
+ return {
931
+ properties: props,
932
+ pseudoInstances,
933
+ overflowScroll: acc.overflowScroll ?? false
934
+ };
935
+ }
936
+ function mapSingleDeclaration(decl, props, acc, warnings) {
937
+ const property = decl.property;
938
+ const value = decl.value;
939
+ if (IGNORED_CSS_PROPERTIES.has(property))
940
+ return;
941
+ if (property === "unparsed") {
942
+ handleUnparsed(value, props, acc, warnings);
943
+ return;
944
+ }
945
+ if (property === "custom") {
946
+ handleCustomProperty(value, props, acc, warnings);
947
+ return;
948
+ }
949
+ switch (property) {
950
+ case "background-color": {
951
+ const result = convertCssColor(value);
952
+ if (result) {
953
+ props.set("BackgroundColor3", {
954
+ type: "Color3",
955
+ value: result.color
956
+ });
957
+ if (result.transparency !== undefined) {
958
+ props.set("BackgroundTransparency", {
959
+ type: "number",
960
+ value: result.transparency
961
+ });
962
+ }
963
+ }
964
+ break;
965
+ }
966
+ case "background": {
967
+ handleBackground(value, props, acc, warnings);
968
+ break;
969
+ }
970
+ case "color": {
971
+ const result = convertCssColor(value);
972
+ if (result) {
973
+ props.set("TextColor3", { type: "Color3", value: result.color });
974
+ if (result.transparency !== undefined) {
975
+ props.set("TextTransparency", {
976
+ type: "number",
977
+ value: result.transparency
978
+ });
979
+ }
980
+ }
981
+ break;
982
+ }
983
+ case "opacity": {
984
+ const opacity = value;
985
+ if (opacity !== 1) {
986
+ props.set("BackgroundTransparency", {
987
+ type: "number",
988
+ value: 1 - opacity
989
+ });
990
+ }
991
+ break;
992
+ }
993
+ case "width": {
994
+ acc.widthX = handleSize(value, warnings);
995
+ break;
996
+ }
997
+ case "height": {
998
+ acc.heightY = handleSize(value, warnings);
999
+ break;
1000
+ }
1001
+ case "left": {
1002
+ const v = value;
1003
+ if (v.type === "length-percentage") {
1004
+ const result = convertLengthDimension(v.value, warnings);
1005
+ if (result && result !== "auto")
1006
+ acc.leftX = result;
1007
+ }
1008
+ break;
1009
+ }
1010
+ case "top": {
1011
+ const v = value;
1012
+ if (v.type === "length-percentage") {
1013
+ const result = convertLengthDimension(v.value, warnings);
1014
+ if (result && result !== "auto")
1015
+ acc.topY = result;
1016
+ }
1017
+ break;
1018
+ }
1019
+ case "display": {
1020
+ handleDisplay(value, props, acc);
1021
+ break;
1022
+ }
1023
+ case "flex-direction": {
1024
+ acc.flexDirection = value;
1025
+ break;
1026
+ }
1027
+ case "justify-content": {
1028
+ const v = value;
1029
+ acc.justifyContent = extractContentAlignment(v);
1030
+ break;
1031
+ }
1032
+ case "align-items": {
1033
+ const v = value;
1034
+ acc.alignItems = extractItemsAlignment(v);
1035
+ break;
1036
+ }
1037
+ case "gap": {
1038
+ const v = value;
1039
+ const row = v.row;
1040
+ const column = v.column;
1041
+ if (row?.type === "length-percentage") {
1042
+ const result = convertLengthDimension(row.value, warnings);
1043
+ if (result && result !== "auto") {
1044
+ acc.gap = toUDim(result);
1045
+ acc.gridGapY = toUDim(result);
1046
+ }
1047
+ }
1048
+ if (column?.type === "length-percentage") {
1049
+ const result = convertLengthDimension(column.value, warnings);
1050
+ if (result && result !== "auto") {
1051
+ acc.gridGapX = toUDim(result);
1052
+ }
1053
+ } else if (acc.gridGapY) {
1054
+ acc.gridGapX = acc.gridGapY;
1055
+ }
1056
+ break;
1057
+ }
1058
+ case "flex-wrap": {
1059
+ const v = value;
1060
+ acc.flexWrap = v === "wrap" || v === "wrap-reverse";
1061
+ break;
1062
+ }
1063
+ case "flex-grow": {
1064
+ acc.flexGrow = value;
1065
+ break;
1066
+ }
1067
+ case "flex-shrink": {
1068
+ acc.flexShrink = value;
1069
+ break;
1070
+ }
1071
+ case "padding": {
1072
+ handlePadding(value, acc, warnings);
1073
+ break;
1074
+ }
1075
+ case "padding-top":
1076
+ case "padding-bottom":
1077
+ case "padding-left":
1078
+ case "padding-right": {
1079
+ handlePaddingSide(property, value, acc, warnings);
1080
+ break;
1081
+ }
1082
+ case "padding-inline": {
1083
+ const pi = value;
1084
+ handlePaddingSide("padding-left", pi.inlineStart, acc, warnings);
1085
+ handlePaddingSide("padding-right", pi.inlineEnd, acc, warnings);
1086
+ break;
1087
+ }
1088
+ case "padding-block": {
1089
+ const pb = value;
1090
+ handlePaddingSide("padding-top", pb.blockStart, acc, warnings);
1091
+ handlePaddingSide("padding-bottom", pb.blockEnd, acc, warnings);
1092
+ break;
1093
+ }
1094
+ case "padding-inline-start":
1095
+ case "padding-inline-end": {
1096
+ const physicalProp = property === "padding-inline-start" ? "padding-left" : "padding-right";
1097
+ handlePaddingSide(physicalProp, value, acc, warnings);
1098
+ break;
1099
+ }
1100
+ case "padding-block-start":
1101
+ case "padding-block-end": {
1102
+ const physicalProp = property === "padding-block-start" ? "padding-top" : "padding-bottom";
1103
+ handlePaddingSide(physicalProp, value, acc, warnings);
1104
+ break;
1105
+ }
1106
+ case "border": {
1107
+ handleBorder(value, acc, warnings);
1108
+ break;
1109
+ }
1110
+ case "border-top":
1111
+ case "border-right":
1112
+ case "border-bottom":
1113
+ case "border-left": {
1114
+ handleBorder(value, acc, warnings);
1115
+ break;
1116
+ }
1117
+ case "border-radius": {
1118
+ handleBorderRadius(value, acc, warnings);
1119
+ break;
1120
+ }
1121
+ case "border-color": {
1122
+ const v = value;
1123
+ const colorVal = v.top ?? value;
1124
+ const result = convertCssColor(colorVal);
1125
+ if (result)
1126
+ acc.borderColor = result.color;
1127
+ break;
1128
+ }
1129
+ case "border-width": {
1130
+ const v = value;
1131
+ const top = v.top;
1132
+ if (top?.type === "length") {
1133
+ const inner = top.value;
1134
+ if (inner?.type === "value") {
1135
+ const dim = inner.value;
1136
+ acc.borderWidth = dim.value;
1137
+ }
1138
+ }
1139
+ break;
1140
+ }
1141
+ case "border-style": {
1142
+ const v = value;
1143
+ acc.borderStyle = v.top ?? "solid";
1144
+ break;
1145
+ }
1146
+ case "font-size": {
1147
+ const v = value;
1148
+ if (v.type === "length") {
1149
+ const inner = v.value;
1150
+ if (inner.type === "dimension") {
1151
+ const dim = inner.value;
1152
+ if (dim.unit === "px") {
1153
+ props.set("TextSize", {
1154
+ type: "number",
1155
+ value: dim.value
1156
+ });
1157
+ } else if (dim.unit === "rem" || dim.unit === "em") {
1158
+ props.set("TextSize", {
1159
+ type: "number",
1160
+ value: dim.value * 16
1161
+ });
1162
+ }
1163
+ }
1164
+ }
1165
+ break;
1166
+ }
1167
+ case "font-family": {
1168
+ const families = value;
1169
+ acc.fontFamily = mapFontFamily(families, warnings);
1170
+ break;
1171
+ }
1172
+ case "font-weight": {
1173
+ const v = value;
1174
+ if (v.type === "absolute") {
1175
+ const abs = v.value;
1176
+ acc.fontWeight = mapFontWeight(abs.value);
1177
+ } else if (v.type === "bolder" || v.type === "lighter") {
1178
+ acc.fontWeight = mapFontWeight(v.type);
1179
+ }
1180
+ break;
1181
+ }
1182
+ case "font-style": {
1183
+ if (typeof value === "string") {
1184
+ acc.fontStyle = mapFontStyle(value);
1185
+ } else {
1186
+ const v = value;
1187
+ acc.fontStyle = mapFontStyle(v.type);
1188
+ }
1189
+ break;
1190
+ }
1191
+ case "text-align": {
1192
+ const v = value;
1193
+ props.set("TextXAlignment", mapTextXAlignment(v));
1194
+ break;
1195
+ }
1196
+ case "vertical-align": {
1197
+ const v = value;
1198
+ const align = typeof v === "string" ? v : v.type === "keyword" ? v.value : "top";
1199
+ props.set("TextYAlignment", mapTextYAlignment(align));
1200
+ break;
1201
+ }
1202
+ case "z-index": {
1203
+ const v = value;
1204
+ if (v.type === "integer") {
1205
+ props.set("ZIndex", { type: "number", value: v.value });
1206
+ }
1207
+ break;
1208
+ }
1209
+ case "overflow": {
1210
+ const v = value;
1211
+ const x = v.x;
1212
+ const y = v.y;
1213
+ if (x === "hidden" || y === "hidden") {
1214
+ props.set("ClipsDescendants", { type: "boolean", value: true });
1215
+ }
1216
+ if (x === "scroll" || y === "scroll") {
1217
+ acc.overflowScroll = true;
1218
+ warnings.warn({
1219
+ code: "partial-mapping",
1220
+ message: "overflow: scroll detected — will trigger ScrollingFrame upgrade via manifest"
1221
+ });
1222
+ }
1223
+ break;
1224
+ }
1225
+ case "visibility": {
1226
+ if (value === "hidden") {
1227
+ props.set("Visible", { type: "boolean", value: false });
1228
+ }
1229
+ break;
1230
+ }
1231
+ case "background-image": {
1232
+ const imgs = value;
1233
+ if (imgs?.length > 0) {
1234
+ const img = imgs[0];
1235
+ if (img.type === "url") {
1236
+ const urlVal = img.value;
1237
+ props.set("Image", {
1238
+ type: "string",
1239
+ value: urlVal.url
1240
+ });
1241
+ }
1242
+ }
1243
+ break;
1244
+ }
1245
+ case "object-fit": {
1246
+ handleObjectFit(value, props);
1247
+ break;
1248
+ }
1249
+ case "aspect-ratio": {
1250
+ const v = value;
1251
+ const ratio = v.ratio;
1252
+ if (ratio) {
1253
+ acc.aspectRatio = ratio[0] / ratio[1];
1254
+ }
1255
+ break;
1256
+ }
1257
+ case "line-height": {
1258
+ const v = value;
1259
+ if (v.type === "number") {
1260
+ props.set("LineHeight", {
1261
+ type: "number",
1262
+ value: v.value
1263
+ });
1264
+ }
1265
+ break;
1266
+ }
1267
+ case "word-wrap":
1268
+ case "overflow-wrap": {
1269
+ if (value === "break-word") {
1270
+ props.set("TextWrapped", { type: "boolean", value: true });
1271
+ }
1272
+ break;
1273
+ }
1274
+ case "text-overflow": {
1275
+ if (value === "ellipsis") {
1276
+ props.set("TextTruncate", {
1277
+ type: "Enum",
1278
+ enum: "TextTruncate",
1279
+ value: "AtEnd"
1280
+ });
1281
+ }
1282
+ break;
1283
+ }
1284
+ case "min-width": {
1285
+ const px = extractPxFromLengthPercentage(value, warnings);
1286
+ if (px !== null)
1287
+ acc.minWidth = px;
1288
+ break;
1289
+ }
1290
+ case "max-width": {
1291
+ const px = extractPxFromLengthPercentage(value, warnings);
1292
+ if (px !== null)
1293
+ acc.maxWidth = px;
1294
+ break;
1295
+ }
1296
+ case "min-height": {
1297
+ const px = extractPxFromLengthPercentage(value, warnings);
1298
+ if (px !== null)
1299
+ acc.minHeight = px;
1300
+ break;
1301
+ }
1302
+ case "max-height": {
1303
+ const px = extractPxFromLengthPercentage(value, warnings);
1304
+ if (px !== null)
1305
+ acc.maxHeight = px;
1306
+ break;
1307
+ }
1308
+ case "transform-origin": {
1309
+ handleTransformOrigin(value, props);
1310
+ break;
1311
+ }
1312
+ case "position":
1313
+ case "cursor":
1314
+ break;
1315
+ case "outline": {
1316
+ handleOutline(value, acc, warnings);
1317
+ break;
1318
+ }
1319
+ case "transform": {
1320
+ handleTransform(value, acc, warnings);
1321
+ break;
1322
+ }
1323
+ case "order": {
1324
+ const v = value;
1325
+ if (typeof v === "number") {
1326
+ props.set("LayoutOrder", { type: "number", value: v });
1327
+ }
1328
+ break;
1329
+ }
1330
+ case "align-self": {
1331
+ const v = value;
1332
+ acc.alignSelf = extractItemsAlignment(v);
1333
+ break;
1334
+ }
1335
+ case "flex-basis": {
1336
+ const v = value;
1337
+ if (v.type === "length-percentage") {
1338
+ const result = convertLengthDimension(v.value, warnings);
1339
+ if (result && result !== "auto") {
1340
+ acc.flexBasis = result;
1341
+ }
1342
+ }
1343
+ break;
1344
+ }
1345
+ case "grid-template-columns": {
1346
+ acc.hasGrid = true;
1347
+ handleGridTemplateColumns(value, acc, warnings);
1348
+ break;
1349
+ }
1350
+ case "grid-template-rows": {
1351
+ acc.hasGrid = true;
1352
+ handleGridTemplateRows(value, acc, warnings);
1353
+ break;
1354
+ }
1355
+ case "margin":
1356
+ case "margin-top":
1357
+ case "margin-right":
1358
+ case "margin-bottom":
1359
+ case "margin-left":
1360
+ case "margin-block":
1361
+ case "margin-inline":
1362
+ warnings.warn({
1363
+ code: "unsupported-property",
1364
+ message: `'${property}' has no Roblox equivalent (use 'gap' on parent flex container instead) - skipped`
1365
+ });
1366
+ break;
1367
+ default:
1368
+ warnings.warn({
1369
+ code: "unsupported-property",
1370
+ message: `'${property}' has no Roblox equivalent - skipped`
1371
+ });
1372
+ }
1373
+ }
1374
+ function handleDisplay(value, props, acc) {
1375
+ const v = value;
1376
+ if (v.type === "keyword" && v.value === "none") {
1377
+ props.set("Visible", { type: "boolean", value: false });
1378
+ return;
1379
+ }
1380
+ if (v.type === "pair") {
1381
+ const inside = v.inside;
1382
+ if (inside?.type === "flex") {
1383
+ acc.hasFlex = true;
1384
+ } else if (inside?.type === "grid") {
1385
+ acc.hasGrid = true;
1386
+ }
1387
+ }
1388
+ }
1389
+ function handleSize(value, warnings) {
1390
+ const v = value;
1391
+ if (v.type === "auto")
1392
+ return "auto";
1393
+ if (v.type === "length-percentage") {
1394
+ const result = convertLengthDimension(v.value, warnings);
1395
+ if (result)
1396
+ return result;
1397
+ }
1398
+ return;
1399
+ }
1400
+ function handlePadding(value, acc, warnings) {
1401
+ for (const [side, key] of [
1402
+ ["top", "paddingTop"],
1403
+ ["right", "paddingRight"],
1404
+ ["bottom", "paddingBottom"],
1405
+ ["left", "paddingLeft"]
1406
+ ]) {
1407
+ const sideVal = value[side];
1408
+ if (sideVal?.type === "length-percentage") {
1409
+ const result = convertLengthDimension(sideVal.value, warnings);
1410
+ if (result && result !== "auto") {
1411
+ acc[key] = toUDim(result);
1412
+ }
1413
+ }
1414
+ }
1415
+ }
1416
+ function handlePaddingSide(property, value, acc, warnings) {
1417
+ const v = value;
1418
+ if (v.type === "length-percentage") {
1419
+ const result = convertLengthDimension(v.value, warnings);
1420
+ if (result && result !== "auto") {
1421
+ const udim = toUDim(result);
1422
+ switch (property) {
1423
+ case "padding-top":
1424
+ acc.paddingTop = udim;
1425
+ break;
1426
+ case "padding-bottom":
1427
+ acc.paddingBottom = udim;
1428
+ break;
1429
+ case "padding-left":
1430
+ acc.paddingLeft = udim;
1431
+ break;
1432
+ case "padding-right":
1433
+ acc.paddingRight = udim;
1434
+ break;
1435
+ }
1436
+ }
1437
+ }
1438
+ }
1439
+ function handleBorder(value, acc, warnings) {
1440
+ const width = value.width;
1441
+ if (width?.type === "length") {
1442
+ const inner = width.value;
1443
+ if (inner?.type === "value") {
1444
+ const dim = inner.value;
1445
+ acc.borderWidth = dim.value;
1446
+ }
1447
+ }
1448
+ const style = value.style;
1449
+ acc.borderStyle = style ?? "solid";
1450
+ if (style === "dashed" || style === "dotted") {
1451
+ warnings.warn({
1452
+ code: "partial-mapping",
1453
+ message: `border-style '${style}' not supported in Roblox, using solid`
1454
+ });
1455
+ }
1456
+ const color = convertCssColor(value.color);
1457
+ if (color) {
1458
+ acc.borderColor = color.color;
1459
+ if (color.transparency !== undefined) {
1460
+ acc.borderTransparency = color.transparency;
1461
+ }
1462
+ }
1463
+ }
1464
+ function handleBorderRadius(value, acc, warnings) {
1465
+ const topLeft = value.topLeft;
1466
+ if (!topLeft?.[0])
1467
+ return;
1468
+ const first = topLeft[0];
1469
+ const allSame = ["topLeft", "topRight", "bottomRight", "bottomLeft"].every((corner) => {
1470
+ const c = value[corner];
1471
+ if (!c?.[0])
1472
+ return false;
1473
+ const dim = c[0];
1474
+ return dim.type === first.type && JSON.stringify(dim.value) === JSON.stringify(first.value);
1475
+ });
1476
+ if (!allSame) {
1477
+ warnings.warn({
1478
+ code: "partial-mapping",
1479
+ message: "Per-corner border-radius not supported in Roblox, using first value"
1480
+ });
1481
+ }
1482
+ if (first.type === "dimension") {
1483
+ const dim = first.value;
1484
+ if (dim.unit === "px") {
1485
+ acc.borderRadius = {
1486
+ type: "UDim",
1487
+ value: [0, dim.value]
1488
+ };
1489
+ } else if (dim.unit === "rem" || dim.unit === "em") {
1490
+ acc.borderRadius = {
1491
+ type: "UDim",
1492
+ value: [0, dim.value * 16]
1493
+ };
1494
+ }
1495
+ } else if (first.type === "percentage") {
1496
+ acc.borderRadius = {
1497
+ type: "UDim",
1498
+ value: [first.value, 0]
1499
+ };
1500
+ }
1501
+ }
1502
+ function handleBackground(value, props, acc, warnings) {
1503
+ const layers = value;
1504
+ if (!Array.isArray(layers) || layers.length === 0)
1505
+ return;
1506
+ const layer = layers[0];
1507
+ const image = layer.image;
1508
+ if (image?.type === "gradient") {
1509
+ const gradient = image.value;
1510
+ if (gradient.type === "linear") {
1511
+ handleLinearGradient(gradient, acc, warnings);
1512
+ } else {
1513
+ warnings.warn({
1514
+ code: "unsupported-property",
1515
+ message: `'${gradient.type}-gradient' not supported, only linear-gradient`
1516
+ });
1517
+ }
1518
+ return;
1519
+ }
1520
+ const color = layer.color;
1521
+ if (color) {
1522
+ const result = convertCssColor(color);
1523
+ if (result && result.color[0] === 0 && result.color[1] === 0 && result.color[2] === 0 && result.transparency === 1) {
1524
+ props.set("BackgroundTransparency", { type: "number", value: 1 });
1525
+ } else if (result) {
1526
+ props.set("BackgroundColor3", { type: "Color3", value: result.color });
1527
+ if (result.transparency !== undefined) {
1528
+ props.set("BackgroundTransparency", {
1529
+ type: "number",
1530
+ value: result.transparency
1531
+ });
1532
+ }
1533
+ }
1534
+ }
1535
+ }
1536
+ function handleLinearGradient(gradient, acc, _warnings) {
1537
+ const direction = gradient.direction;
1538
+ if (direction?.type === "angle") {
1539
+ const angle = direction.value;
1540
+ acc.gradientRotation = angle.value;
1541
+ }
1542
+ const items = gradient.items;
1543
+ if (items) {
1544
+ acc.gradientStops = [];
1545
+ let autoIndex = 0;
1546
+ const colorStops = items.filter((i) => i.type === "color-stop");
1547
+ const stopCount = colorStops.length;
1548
+ for (const item of colorStops) {
1549
+ if (item.type === "color-stop") {
1550
+ const colorResult = convertCssColor(item.color);
1551
+ if (colorResult) {
1552
+ const position = item.position !== null ? extractGradientPosition(item.position) : autoIndex / Math.max(stopCount - 1, 1);
1553
+ acc.gradientStops.push({
1554
+ position,
1555
+ color: colorResult.color
1556
+ });
1557
+ }
1558
+ autoIndex++;
1559
+ }
1560
+ }
1561
+ }
1562
+ }
1563
+ function extractGradientPosition(pos) {
1564
+ if (pos.type === "percentage")
1565
+ return pos.value;
1566
+ return 0;
1567
+ }
1568
+ function handleObjectFit(value, props) {
1569
+ switch (value) {
1570
+ case "cover":
1571
+ props.set("ScaleType", {
1572
+ type: "Enum",
1573
+ enum: "ScaleType",
1574
+ value: "Crop"
1575
+ });
1576
+ break;
1577
+ case "contain":
1578
+ props.set("ScaleType", {
1579
+ type: "Enum",
1580
+ enum: "ScaleType",
1581
+ value: "Fit"
1582
+ });
1583
+ break;
1584
+ case "fill":
1585
+ props.set("ScaleType", {
1586
+ type: "Enum",
1587
+ enum: "ScaleType",
1588
+ value: "Stretch"
1589
+ });
1590
+ break;
1591
+ }
1592
+ }
1593
+ function handleTransformOrigin(value, props) {
1594
+ const x = resolveAnchorAxis(value.x);
1595
+ const y = resolveAnchorAxis(value.y);
1596
+ if (x !== null && y !== null) {
1597
+ props.set("AnchorPoint", { type: "Vector2", value: [x, y] });
1598
+ }
1599
+ }
1600
+ function resolveAnchorAxis(axis) {
1601
+ if (!axis)
1602
+ return null;
1603
+ if (axis.type === "center")
1604
+ return 0.5;
1605
+ if (axis.type === "left" || axis.type === "top")
1606
+ return 0;
1607
+ if (axis.type === "right" || axis.type === "bottom")
1608
+ return 1;
1609
+ if (axis.type === "side") {
1610
+ const side = axis.side;
1611
+ if (side === "left" || side === "top")
1612
+ return 0;
1613
+ if (side === "right" || side === "bottom")
1614
+ return 1;
1615
+ }
1616
+ if (axis.type === "length") {
1617
+ const v = axis.value;
1618
+ if (v.type === "percentage")
1619
+ return v.value / 100;
1620
+ }
1621
+ return null;
1622
+ }
1623
+ function handleOutline(value, acc, warnings) {
1624
+ handleBorder(value, acc, warnings);
1625
+ acc.borderStyle = "outline";
1626
+ }
1627
+ function handleCustomProperty(value, props, acc, warnings) {
1628
+ const name = value.name;
1629
+ const tokens = value.value;
1630
+ if (name === "object-fit" && tokens?.[0]) {
1631
+ const tok = tokens[0];
1632
+ if (tok.type === "token") {
1633
+ const tokenVal = tok.value;
1634
+ if (tokenVal.type === "ident") {
1635
+ handleObjectFit(tokenVal.value, props);
1636
+ }
1637
+ }
1638
+ return;
1639
+ }
1640
+ }
1641
+ function handleUnparsed(unparsed, props, acc, _warnings) {
1642
+ const propertyId = unparsed.propertyId;
1643
+ const propName = propertyId.property;
1644
+ const tokens = unparsed.value;
1645
+ if (IGNORED_CSS_PROPERTIES.has(propName))
1646
+ return;
1647
+ const varRef = extractVarReference(tokens);
1648
+ if (varRef) {
1649
+ mapTokenReference(propName, varRef, props, acc);
1650
+ }
1651
+ }
1652
+ function extractVarReference(tokens) {
1653
+ for (const tok of tokens) {
1654
+ const t = tok;
1655
+ if (t.type === "var") {
1656
+ const val = t.value;
1657
+ const name = val.name;
1658
+ const ident = name.ident;
1659
+ return ident.replace(/^--/, "");
1660
+ }
1661
+ }
1662
+ return null;
1663
+ }
1664
+ function mapTokenReference(cssProperty, tokenName, props, acc) {
1665
+ const tokenRef = { type: "token", name: tokenName };
1666
+ switch (cssProperty) {
1667
+ case "background-color":
1668
+ props.set("BackgroundColor3", tokenRef);
1669
+ props.set("BackgroundTransparency", { type: "number", value: 0 });
1670
+ break;
1671
+ case "color":
1672
+ props.set("TextColor3", tokenRef);
1673
+ break;
1674
+ case "border-radius":
1675
+ acc.borderRadius = tokenRef;
1676
+ break;
1677
+ case "border-color":
1678
+ break;
1679
+ case "padding":
1680
+ acc.paddingTop = tokenRef;
1681
+ acc.paddingBottom = tokenRef;
1682
+ acc.paddingLeft = tokenRef;
1683
+ acc.paddingRight = tokenRef;
1684
+ break;
1685
+ case "padding-top":
1686
+ acc.paddingTop = tokenRef;
1687
+ break;
1688
+ case "padding-bottom":
1689
+ acc.paddingBottom = tokenRef;
1690
+ break;
1691
+ case "padding-left":
1692
+ acc.paddingLeft = tokenRef;
1693
+ break;
1694
+ case "padding-right":
1695
+ acc.paddingRight = tokenRef;
1696
+ break;
1697
+ case "gap":
1698
+ acc.gap = tokenRef;
1699
+ break;
1700
+ case "width":
1701
+ acc.widthX = { scale: 0, offset: 0 };
1702
+ props.set("Size", tokenRef);
1703
+ break;
1704
+ case "height":
1705
+ acc.heightY = { scale: 0, offset: 0 };
1706
+ break;
1707
+ case "font-size":
1708
+ props.set("TextSize", tokenRef);
1709
+ break;
1710
+ case "font-family":
1711
+ props.set("FontFace", tokenRef);
1712
+ break;
1713
+ case "opacity":
1714
+ props.set("BackgroundTransparency", tokenRef);
1715
+ break;
1716
+ default:
1717
+ break;
1718
+ }
1719
+ }
1720
+ function mapTextXAlignment(value) {
1721
+ switch (value) {
1722
+ case "left":
1723
+ return { type: "Enum", enum: "TextXAlignment", value: "Left" };
1724
+ case "center":
1725
+ return { type: "Enum", enum: "TextXAlignment", value: "Center" };
1726
+ case "right":
1727
+ return { type: "Enum", enum: "TextXAlignment", value: "Right" };
1728
+ default:
1729
+ return { type: "Enum", enum: "TextXAlignment", value: "Left" };
1730
+ }
1731
+ }
1732
+ function mapTextYAlignment(value) {
1733
+ switch (value) {
1734
+ case "top":
1735
+ return { type: "Enum", enum: "TextYAlignment", value: "Top" };
1736
+ case "center":
1737
+ case "middle":
1738
+ return { type: "Enum", enum: "TextYAlignment", value: "Center" };
1739
+ case "bottom":
1740
+ return { type: "Enum", enum: "TextYAlignment", value: "Bottom" };
1741
+ default:
1742
+ return { type: "Enum", enum: "TextYAlignment", value: "Top" };
1743
+ }
1744
+ }
1745
+ function extractPxFromLengthPercentage(value, warnings) {
1746
+ const v = value;
1747
+ if (v.type === "length-percentage") {
1748
+ const inner = v.value;
1749
+ if (inner.type === "dimension") {
1750
+ const dim = inner.value;
1751
+ if (dim.unit === "px")
1752
+ return dim.value;
1753
+ if (dim.unit === "rem" || dim.unit === "em")
1754
+ return dim.value * 16;
1755
+ }
1756
+ }
1757
+ return null;
1758
+ }
1759
+ function extractContentAlignment(v) {
1760
+ if (typeof v === "string")
1761
+ return v;
1762
+ if (v.type === "normal")
1763
+ return "flex-start";
1764
+ if (v.type === "content-distribution")
1765
+ return v.value;
1766
+ if (v.type === "content-position") {
1767
+ return v.value;
1768
+ }
1769
+ return "flex-start";
1770
+ }
1771
+ function extractItemsAlignment(v) {
1772
+ if (typeof v === "string")
1773
+ return v;
1774
+ if (v.type === "normal")
1775
+ return "stretch";
1776
+ if (v.type === "self-position")
1777
+ return v.value;
1778
+ if (v.type === "keyword")
1779
+ return v.value;
1780
+ return "stretch";
1781
+ }
1782
+ function handleTransform(transforms, acc, warnings) {
1783
+ if (!Array.isArray(transforms))
1784
+ return;
1785
+ for (const t of transforms) {
1786
+ const tf = t;
1787
+ switch (tf.type) {
1788
+ case "scale": {
1789
+ const vals = tf.value;
1790
+ if (vals && vals.length >= 1) {
1791
+ const x = vals[0].value;
1792
+ const y = vals.length >= 2 ? vals[1].value : x;
1793
+ acc.scale = x === y ? x : (x + y) / 2;
1794
+ }
1795
+ break;
1796
+ }
1797
+ case "scaleX":
1798
+ case "scaleY": {
1799
+ const v = tf.value;
1800
+ acc.scale = v.value;
1801
+ break;
1802
+ }
1803
+ case "rotate": {
1804
+ const v = tf.value;
1805
+ if (v.type === "deg") {
1806
+ acc.rotation = v.value;
1807
+ } else if (v.type === "rad") {
1808
+ acc.rotation = v.value * (180 / Math.PI);
1809
+ } else if (v.type === "turn") {
1810
+ acc.rotation = v.value * 360;
1811
+ }
1812
+ break;
1813
+ }
1814
+ default:
1815
+ warnings.warn({
1816
+ code: "unsupported-property",
1817
+ message: `transform function '${tf.type}' has no Roblox equivalent - skipped`
1818
+ });
1819
+ }
1820
+ }
1821
+ }
1822
+ function extractTrackSize(trackBreadth, warnings) {
1823
+ if (trackBreadth.type === "length") {
1824
+ const dim = trackBreadth.value;
1825
+ if (dim.type === "dimension") {
1826
+ const val = dim.value;
1827
+ const unit = val.unit;
1828
+ const value = val.value;
1829
+ switch (unit) {
1830
+ case "px":
1831
+ return { scale: 0, offset: value };
1832
+ case "rem":
1833
+ return { scale: 0, offset: value * 16 };
1834
+ default:
1835
+ return null;
1836
+ }
1837
+ }
1838
+ }
1839
+ return null;
1840
+ }
1841
+ function handleGridTemplateColumns(value, acc, warnings) {
1842
+ const v = value;
1843
+ if (v.type !== "track-list")
1844
+ return;
1845
+ const items = v.items;
1846
+ if (!Array.isArray(items))
1847
+ return;
1848
+ for (const item of items) {
1849
+ const t = item;
1850
+ if (t.type === "track-repeat") {
1851
+ const repeat = t.value;
1852
+ const count = repeat?.count;
1853
+ if (count?.type === "number") {
1854
+ acc.gridMaxCellsPerRow = count.value;
1855
+ }
1856
+ const trackSizes = repeat?.trackSizes;
1857
+ if (trackSizes && trackSizes.length > 0) {
1858
+ const first = trackSizes[0];
1859
+ if (first.type === "track-breadth") {
1860
+ const size = extractTrackSize(first.value, warnings);
1861
+ if (size)
1862
+ acc.gridCellWidth = size;
1863
+ }
1864
+ }
1865
+ } else if (t.type === "track-size") {
1866
+ if (!acc.gridMaxCellsPerRow)
1867
+ acc.gridMaxCellsPerRow = 0;
1868
+ acc.gridMaxCellsPerRow++;
1869
+ if (acc.gridMaxCellsPerRow === 1) {
1870
+ const breadth = t.value;
1871
+ if (breadth.type === "track-breadth") {
1872
+ const size = extractTrackSize(breadth.value, warnings);
1873
+ if (size)
1874
+ acc.gridCellWidth = size;
1875
+ }
1876
+ }
1877
+ }
1878
+ }
1879
+ }
1880
+ function handleGridTemplateRows(value, acc, warnings) {
1881
+ const v = value;
1882
+ if (v.type !== "track-list")
1883
+ return;
1884
+ const items = v.items;
1885
+ if (!Array.isArray(items))
1886
+ return;
1887
+ for (const item of items) {
1888
+ const t = item;
1889
+ if (t.type === "track-repeat") {
1890
+ const repeat = t.value;
1891
+ const trackSizes = repeat?.trackSizes;
1892
+ if (trackSizes && trackSizes.length > 0) {
1893
+ const first = trackSizes[0];
1894
+ if (first.type === "track-breadth") {
1895
+ const size = extractTrackSize(first.value, warnings);
1896
+ if (size)
1897
+ acc.gridCellHeight = size;
1898
+ }
1899
+ }
1900
+ } else if (t.type === "track-size") {
1901
+ const breadth = t.value;
1902
+ if (breadth.type === "track-breadth") {
1903
+ const size = extractTrackSize(breadth.value, warnings);
1904
+ if (size) {
1905
+ acc.gridCellHeight = size;
1906
+ break;
1907
+ }
1908
+ }
1909
+ }
1910
+ }
1911
+ }
1912
+ function mapFillDirection(direction) {
1913
+ if (direction === "row" || direction === "row-reverse") {
1914
+ return { type: "Enum", enum: "FillDirection", value: "Horizontal" };
1915
+ }
1916
+ return { type: "Enum", enum: "FillDirection", value: "Vertical" };
1917
+ }
1918
+ function mapJustifyContent(justifyContent, flexDirection) {
1919
+ const isHorizontal = !flexDirection || flexDirection === "row" || flexDirection === "row-reverse";
1920
+ const prop = isHorizontal ? "HorizontalAlignment" : "VerticalAlignment";
1921
+ const enumName = isHorizontal ? "HorizontalAlignment" : "VerticalAlignment";
1922
+ switch (justifyContent) {
1923
+ case "flex-start":
1924
+ case "start":
1925
+ return {
1926
+ prop,
1927
+ value: {
1928
+ type: "Enum",
1929
+ enum: enumName,
1930
+ value: isHorizontal ? "Left" : "Top"
1931
+ }
1932
+ };
1933
+ case "center":
1934
+ return {
1935
+ prop,
1936
+ value: { type: "Enum", enum: enumName, value: "Center" }
1937
+ };
1938
+ case "flex-end":
1939
+ case "end":
1940
+ return {
1941
+ prop,
1942
+ value: {
1943
+ type: "Enum",
1944
+ enum: enumName,
1945
+ value: isHorizontal ? "Right" : "Bottom"
1946
+ }
1947
+ };
1948
+ case "space-between":
1949
+ return {
1950
+ prop: isHorizontal ? "HorizontalFlex" : "VerticalFlex",
1951
+ value: { type: "Enum", enum: "UIFlexAlignment", value: "SpaceBetween" }
1952
+ };
1953
+ case "space-around":
1954
+ return {
1955
+ prop: isHorizontal ? "HorizontalFlex" : "VerticalFlex",
1956
+ value: { type: "Enum", enum: "UIFlexAlignment", value: "SpaceAround" }
1957
+ };
1958
+ case "space-evenly":
1959
+ return {
1960
+ prop: isHorizontal ? "HorizontalFlex" : "VerticalFlex",
1961
+ value: { type: "Enum", enum: "UIFlexAlignment", value: "SpaceEvenly" }
1962
+ };
1963
+ default:
1964
+ return null;
1965
+ }
1966
+ }
1967
+ function mapAlignItems(alignItems, flexDirection) {
1968
+ const isHorizontal = !flexDirection || flexDirection === "row" || flexDirection === "row-reverse";
1969
+ const prop = isHorizontal ? "VerticalAlignment" : "HorizontalAlignment";
1970
+ const enumName = isHorizontal ? "VerticalAlignment" : "HorizontalAlignment";
1971
+ switch (alignItems) {
1972
+ case "flex-start":
1973
+ case "start":
1974
+ return {
1975
+ prop,
1976
+ value: {
1977
+ type: "Enum",
1978
+ enum: enumName,
1979
+ value: isHorizontal ? "Top" : "Left"
1980
+ }
1981
+ };
1982
+ case "center":
1983
+ return {
1984
+ prop,
1985
+ value: { type: "Enum", enum: enumName, value: "Center" }
1986
+ };
1987
+ case "flex-end":
1988
+ case "end":
1989
+ return {
1990
+ prop,
1991
+ value: {
1992
+ type: "Enum",
1993
+ enum: enumName,
1994
+ value: isHorizontal ? "Bottom" : "Right"
1995
+ }
1996
+ };
1997
+ case "stretch":
1998
+ return {
1999
+ prop: "ItemLineAlignment",
2000
+ value: { type: "Enum", enum: "ItemLineAlignment", value: "Stretch" }
2001
+ };
2002
+ default:
2003
+ return null;
2004
+ }
2005
+ }
2006
+ function mapAlignSelf(alignSelf) {
2007
+ switch (alignSelf) {
2008
+ case "flex-start":
2009
+ case "start":
2010
+ return { type: "Enum", enum: "ItemLineAlignment", value: "Start" };
2011
+ case "center":
2012
+ return { type: "Enum", enum: "ItemLineAlignment", value: "Center" };
2013
+ case "flex-end":
2014
+ case "end":
2015
+ return { type: "Enum", enum: "ItemLineAlignment", value: "End" };
2016
+ case "stretch":
2017
+ return { type: "Enum", enum: "ItemLineAlignment", value: "Stretch" };
2018
+ default:
2019
+ return null;
2020
+ }
2021
+ }
2022
+ function finalizeAccumulator(acc, props, warnings) {
2023
+ const pseudos = [];
2024
+ if (acc.flexBasis) {
2025
+ const isRow = !acc.flexDirection || acc.flexDirection === "row" || acc.flexDirection === "row-reverse";
2026
+ if (isRow && acc.widthX === undefined) {
2027
+ acc.widthX = acc.flexBasis;
2028
+ } else if (!isRow && acc.heightY === undefined) {
2029
+ acc.heightY = acc.flexBasis;
2030
+ }
2031
+ }
2032
+ if (acc.widthX !== undefined || acc.heightY !== undefined) {
2033
+ const xAuto = acc.widthX === "auto";
2034
+ const yAuto = acc.heightY === "auto";
2035
+ if (xAuto && yAuto) {
2036
+ props.set("AutomaticSize", {
2037
+ type: "Enum",
2038
+ enum: "AutomaticSize",
2039
+ value: "XY"
2040
+ });
2041
+ } else if (xAuto) {
2042
+ props.set("AutomaticSize", {
2043
+ type: "Enum",
2044
+ enum: "AutomaticSize",
2045
+ value: "X"
2046
+ });
2047
+ if (acc.heightY && acc.heightY !== "auto") {
2048
+ props.set("Size", {
2049
+ type: "UDim2",
2050
+ value: [0, 0, acc.heightY.scale, acc.heightY.offset]
2051
+ });
2052
+ }
2053
+ } else if (yAuto) {
2054
+ props.set("AutomaticSize", {
2055
+ type: "Enum",
2056
+ enum: "AutomaticSize",
2057
+ value: "Y"
2058
+ });
2059
+ if (acc.widthX && acc.widthX !== "auto") {
2060
+ props.set("Size", {
2061
+ type: "UDim2",
2062
+ value: [acc.widthX.scale, acc.widthX.offset, 0, 0]
2063
+ });
2064
+ }
2065
+ } else {
2066
+ const x = acc.widthX ?? {
2067
+ scale: 0,
2068
+ offset: 0
2069
+ };
2070
+ const y = acc.heightY ?? {
2071
+ scale: 0,
2072
+ offset: 0
2073
+ };
2074
+ props.set("Size", {
2075
+ type: "UDim2",
2076
+ value: [x.scale, x.offset, y.scale, y.offset]
2077
+ });
2078
+ }
2079
+ }
2080
+ if (acc.leftX !== undefined || acc.topY !== undefined) {
2081
+ const x = acc.leftX ?? { scale: 0, offset: 0 };
2082
+ const y = acc.topY ?? { scale: 0, offset: 0 };
2083
+ props.set("Position", {
2084
+ type: "UDim2",
2085
+ value: [x.scale, x.offset, y.scale, y.offset]
2086
+ });
2087
+ }
2088
+ if (acc.fontFamily) {
2089
+ props.set("FontFace", {
2090
+ type: "Font",
2091
+ family: acc.fontFamily,
2092
+ weight: acc.fontWeight,
2093
+ style: acc.fontStyle
2094
+ });
2095
+ }
2096
+ if (acc.paddingTop || acc.paddingBottom || acc.paddingLeft || acc.paddingRight) {
2097
+ const paddingProps = new Map;
2098
+ if (acc.paddingTop)
2099
+ paddingProps.set("PaddingTop", acc.paddingTop);
2100
+ if (acc.paddingBottom)
2101
+ paddingProps.set("PaddingBottom", acc.paddingBottom);
2102
+ if (acc.paddingLeft)
2103
+ paddingProps.set("PaddingLeft", acc.paddingLeft);
2104
+ if (acc.paddingRight)
2105
+ paddingProps.set("PaddingRight", acc.paddingRight);
2106
+ pseudos.push({ type: "UIPadding", properties: paddingProps });
2107
+ }
2108
+ if (acc.borderRadius) {
2109
+ const cornerProps = new Map;
2110
+ cornerProps.set("CornerRadius", acc.borderRadius);
2111
+ pseudos.push({ type: "UICorner", properties: cornerProps });
2112
+ }
2113
+ if (acc.borderWidth && acc.borderStyle !== "none") {
2114
+ const strokeProps = new Map;
2115
+ strokeProps.set("Thickness", {
2116
+ type: "number",
2117
+ value: acc.borderWidth
2118
+ });
2119
+ if (acc.borderColor) {
2120
+ strokeProps.set("Color", {
2121
+ type: "Color3",
2122
+ value: acc.borderColor
2123
+ });
2124
+ }
2125
+ if (acc.borderTransparency !== undefined) {
2126
+ strokeProps.set("Transparency", {
2127
+ type: "number",
2128
+ value: acc.borderTransparency
2129
+ });
2130
+ }
2131
+ strokeProps.set("ApplyStrokeMode", {
2132
+ type: "Enum",
2133
+ enum: "ApplyStrokeMode",
2134
+ value: acc.borderStyle === "outline" ? "Contextual" : "Border"
2135
+ });
2136
+ pseudos.push({ type: "UIStroke", properties: strokeProps });
2137
+ }
2138
+ const hasFlexProperties = acc.justifyContent !== undefined || acc.alignItems !== undefined || acc.flexDirection !== undefined || acc.flexWrap !== undefined || acc.gap !== undefined;
2139
+ if (acc.hasFlex || hasFlexProperties) {
2140
+ const layoutProps = new Map;
2141
+ if (acc.hasFlex || acc.flexDirection) {
2142
+ layoutProps.set("FillDirection", mapFillDirection(acc.flexDirection ?? "row"));
2143
+ }
2144
+ if (acc.justifyContent) {
2145
+ const alignment = mapJustifyContent(acc.justifyContent, acc.flexDirection);
2146
+ if (alignment)
2147
+ layoutProps.set(alignment.prop, alignment.value);
2148
+ }
2149
+ if (acc.alignItems) {
2150
+ const alignment = mapAlignItems(acc.alignItems, acc.flexDirection);
2151
+ if (alignment)
2152
+ layoutProps.set(alignment.prop, alignment.value);
2153
+ }
2154
+ if (acc.gap)
2155
+ layoutProps.set("Padding", acc.gap);
2156
+ if (acc.flexWrap) {
2157
+ layoutProps.set("Wraps", { type: "boolean", value: true });
2158
+ }
2159
+ pseudos.push({ type: "UIListLayout", properties: layoutProps });
2160
+ }
2161
+ if (acc.flexGrow !== undefined || acc.flexShrink !== undefined || acc.alignSelf !== undefined) {
2162
+ const flexProps = new Map;
2163
+ if (acc.flexGrow !== undefined || acc.flexShrink !== undefined) {
2164
+ flexProps.set("FlexMode", {
2165
+ type: "Enum",
2166
+ enum: "UIFlexMode",
2167
+ value: "Custom"
2168
+ });
2169
+ if (acc.flexGrow !== undefined) {
2170
+ flexProps.set("GrowRatio", {
2171
+ type: "number",
2172
+ value: acc.flexGrow
2173
+ });
2174
+ }
2175
+ if (acc.flexShrink !== undefined) {
2176
+ flexProps.set("ShrinkRatio", {
2177
+ type: "number",
2178
+ value: acc.flexShrink
2179
+ });
2180
+ }
2181
+ }
2182
+ if (acc.alignSelf) {
2183
+ const alignment = mapAlignSelf(acc.alignSelf);
2184
+ if (alignment)
2185
+ flexProps.set("ItemLineAlignment", alignment);
2186
+ }
2187
+ pseudos.push({ type: "UIFlexItem", properties: flexProps });
2188
+ }
2189
+ if (acc.gradientStops && acc.gradientStops.length > 0) {
2190
+ const gradientProps = new Map;
2191
+ gradientProps.set("Color", {
2192
+ type: "ColorSequence",
2193
+ stops: acc.gradientStops
2194
+ });
2195
+ if (acc.gradientRotation !== undefined) {
2196
+ gradientProps.set("Rotation", {
2197
+ type: "number",
2198
+ value: acc.gradientRotation
2199
+ });
2200
+ }
2201
+ pseudos.push({ type: "UIGradient", properties: gradientProps });
2202
+ }
2203
+ if (acc.aspectRatio !== undefined) {
2204
+ const aspectProps = new Map;
2205
+ aspectProps.set("AspectRatio", {
2206
+ type: "number",
2207
+ value: acc.aspectRatio
2208
+ });
2209
+ pseudos.push({ type: "UIAspectRatioConstraint", properties: aspectProps });
2210
+ }
2211
+ if (acc.minWidth !== undefined || acc.maxWidth !== undefined || acc.minHeight !== undefined || acc.maxHeight !== undefined) {
2212
+ const constraintProps = new Map;
2213
+ if (acc.minWidth !== undefined || acc.minHeight !== undefined) {
2214
+ constraintProps.set("MinSize", {
2215
+ type: "Vector2",
2216
+ value: [acc.minWidth ?? 0, acc.minHeight ?? 0]
2217
+ });
2218
+ }
2219
+ if (acc.maxWidth !== undefined || acc.maxHeight !== undefined) {
2220
+ constraintProps.set("MaxSize", {
2221
+ type: "Vector2",
2222
+ value: [acc.maxWidth ?? Infinity, acc.maxHeight ?? Infinity]
2223
+ });
2224
+ }
2225
+ pseudos.push({ type: "UISizeConstraint", properties: constraintProps });
2226
+ }
2227
+ if (acc.scale !== undefined) {
2228
+ const scaleProps = new Map;
2229
+ scaleProps.set("Scale", { type: "number", value: acc.scale });
2230
+ pseudos.push({ type: "UIScale", properties: scaleProps });
2231
+ }
2232
+ if (acc.rotation !== undefined) {
2233
+ props.set("Rotation", { type: "number", value: acc.rotation });
2234
+ }
2235
+ if (acc.hasGrid) {
2236
+ const gridProps = new Map;
2237
+ if (acc.gridCellWidth || acc.gridCellHeight) {
2238
+ const cellW = acc.gridCellWidth ?? { scale: 0, offset: 100 };
2239
+ const cellH = acc.gridCellHeight ?? { scale: 0, offset: 100 };
2240
+ gridProps.set("CellSize", {
2241
+ type: "UDim2",
2242
+ value: [cellW.scale, cellW.offset, cellH.scale, cellH.offset]
2243
+ });
2244
+ }
2245
+ if (acc.gridGapX || acc.gridGapY) {
2246
+ const gapX = acc.gridGapX ?? {
2247
+ type: "UDim",
2248
+ value: [0, 0]
2249
+ };
2250
+ const gapY = acc.gridGapY ?? {
2251
+ type: "UDim",
2252
+ value: [0, 0]
2253
+ };
2254
+ const xVal = gapX.value;
2255
+ const yVal = gapY.value;
2256
+ gridProps.set("CellPadding", {
2257
+ type: "UDim2",
2258
+ value: [xVal[0], xVal[1], yVal[0], yVal[1]]
2259
+ });
2260
+ }
2261
+ if (acc.gridMaxCellsPerRow !== undefined) {
2262
+ gridProps.set("FillDirectionMaxCells", {
2263
+ type: "number",
2264
+ value: acc.gridMaxCellsPerRow
2265
+ });
2266
+ }
2267
+ pseudos.push({ type: "UIGridLayout", properties: gridProps });
2268
+ }
2269
+ return pseudos;
2270
+ }
2271
+
2272
+ // src/warnings.ts
2273
+ class WarningCollector {
2274
+ warnings = [];
2275
+ level;
2276
+ strict;
2277
+ constructor(level = "all", strict = false) {
2278
+ this.level = level;
2279
+ this.strict = strict;
2280
+ }
2281
+ warn(warning) {
2282
+ if (this.shouldEmit(warning.code)) {
2283
+ this.warnings.push(warning);
2284
+ }
2285
+ }
2286
+ getWarnings() {
2287
+ return this.warnings;
2288
+ }
2289
+ hasErrors() {
2290
+ return this.strict && this.warnings.length > 0;
2291
+ }
2292
+ format() {
2293
+ return this.warnings.map((w) => {
2294
+ const loc = w.file ? `${w.file}:${w.line ?? 0}:${w.column ?? 0} ` : "";
2295
+ return `warning: ${loc}[${w.code}] ${w.message}`;
2296
+ }).join(`
2297
+ `);
2298
+ }
2299
+ shouldEmit(code) {
2300
+ if (this.level === "none")
2301
+ return false;
2302
+ if (this.level === "unsupported") {
2303
+ return code.startsWith("unsupported");
2304
+ }
2305
+ return true;
2306
+ }
2307
+ }
2308
+
2309
+ // src/compiler.ts
2310
+ function compile(sources, options) {
2311
+ const warnings = new WarningCollector(options.warnLevel, options.strict);
2312
+ const allParsed = sources.map((s) => parseCSS(s.content, s.filename));
2313
+ const allRules = allParsed.flatMap((p) => p.rules);
2314
+ const allMediaRules = allParsed.flatMap((p) => p.mediaRules);
2315
+ const { tokens, nonRootRules } = extractTokens(allRules, warnings);
2316
+ const themes = extractThemes({ rules: allRules, mediaRules: allMediaRules }, warnings);
2317
+ for (const mediaRule of allMediaRules) {
2318
+ const query = mediaRule.query;
2319
+ if (isUniversalMediaQuery(query)) {
2320
+ for (const rule of mediaRule.rules) {
2321
+ nonRootRules.push(rule);
2322
+ }
2323
+ }
2324
+ }
2325
+ const styleRules = nonRootRules.filter((rule) => {
2326
+ return !isDataThemeSelector(rule.selectors);
2327
+ });
2328
+ const NON_INHERITABLE_TEXT_PROPS = new Set(["TextXAlignment", "TextYAlignment"]);
2329
+ const TEXT_CAPABLE_CLASSES = new Set(["TextLabel", "TextButton", "TextBox"]);
2330
+ const irRules = [];
2331
+ const overflowScrollClasses = new Map;
2332
+ for (const rule of styleRules) {
2333
+ for (const selectorComponents of rule.selectors) {
2334
+ const mappedSelector = mapSelector(selectorComponents, warnings);
2335
+ if (!mappedSelector)
2336
+ continue;
2337
+ const { properties, pseudoInstances, overflowScroll } = mapDeclarations(rule.declarations, warnings);
2338
+ if (/^[A-Z]\w+$/.test(mappedSelector) && !TEXT_CAPABLE_CLASSES.has(mappedSelector)) {
2339
+ for (const prop of NON_INHERITABLE_TEXT_PROPS) {
2340
+ properties.delete(prop);
2341
+ }
2342
+ }
2343
+ const classMatches = mappedSelector.match(/\.([a-zA-Z0-9_-]+)/g);
2344
+ if (classMatches) {
2345
+ for (const match of classMatches) {
2346
+ const className = match.slice(1);
2347
+ if (overflowScroll) {
2348
+ overflowScrollClasses.set(className, true);
2349
+ } else if (!overflowScrollClasses.has(className)) {
2350
+ overflowScrollClasses.set(className, false);
2351
+ }
2352
+ }
2353
+ }
2354
+ if (properties.size > 0) {
2355
+ irRules.push({
2356
+ selector: mappedSelector,
2357
+ properties,
2358
+ pseudoInstances: []
2359
+ });
2360
+ }
2361
+ for (const pseudo of pseudoInstances) {
2362
+ irRules.push({
2363
+ selector: `${mappedSelector}::${pseudo.type}`,
2364
+ properties: pseudo.properties,
2365
+ pseudoInstances: []
2366
+ });
2367
+ }
2368
+ }
2369
+ }
2370
+ if (options.includeBaseRules !== false) {
2371
+ generateBaseElementRules(irRules);
2372
+ }
2373
+ generateSizeCompoundRules(irRules);
2374
+ const themeMap = new Map;
2375
+ for (const [themeName, themeTokens] of themes) {
2376
+ themeMap.set(themeName, {
2377
+ name: `${options.name}_${themeName}`,
2378
+ tokens: themeTokens,
2379
+ rules: []
2380
+ });
2381
+ }
2382
+ const ir = {
2383
+ name: options.name,
2384
+ tokens,
2385
+ rules: irRules,
2386
+ themes: themeMap.size > 0 ? themeMap : undefined
2387
+ };
2388
+ return { ir, warnings, overflowScrollClasses };
2389
+ }
2390
+ function generateBaseElementRules(irRules) {
2391
+ const autoSizeXY = {
2392
+ type: "Enum",
2393
+ enum: "AutomaticSize",
2394
+ value: "XY"
2395
+ };
2396
+ const transparentBg = { type: "number", value: 1 };
2397
+ const textLabelProps = new Map;
2398
+ textLabelProps.set("AutomaticSize", autoSizeXY);
2399
+ textLabelProps.set("BackgroundTransparency", transparentBg);
2400
+ irRules.unshift({
2401
+ selector: "TextLabel",
2402
+ properties: textLabelProps,
2403
+ pseudoInstances: []
2404
+ });
2405
+ const textButtonProps = new Map;
2406
+ textButtonProps.set("AutomaticSize", autoSizeXY);
2407
+ textButtonProps.set("BackgroundTransparency", transparentBg);
2408
+ irRules.unshift({
2409
+ selector: "TextButton",
2410
+ properties: textButtonProps,
2411
+ pseudoInstances: []
2412
+ });
2413
+ const textBoxProps = new Map;
2414
+ textBoxProps.set("AutomaticSize", autoSizeXY);
2415
+ irRules.unshift({
2416
+ selector: "TextBox",
2417
+ properties: textBoxProps,
2418
+ pseudoInstances: []
2419
+ });
2420
+ const frameProps = new Map;
2421
+ frameProps.set("BackgroundTransparency", transparentBg);
2422
+ irRules.unshift({
2423
+ selector: "Frame",
2424
+ properties: frameProps,
2425
+ pseudoInstances: []
2426
+ });
2427
+ const scrollingFrameProps = new Map;
2428
+ scrollingFrameProps.set("BackgroundTransparency", transparentBg);
2429
+ irRules.unshift({
2430
+ selector: "ScrollingFrame",
2431
+ properties: scrollingFrameProps,
2432
+ pseudoInstances: []
2433
+ });
2434
+ }
2435
+ function generateSizeCompoundRules(irRules) {
2436
+ const widthRules = [];
2437
+ const heightRules = [];
2438
+ for (const rule of irRules) {
2439
+ if (!rule.selector.match(/^\.[a-zA-Z0-9_-]+$/) && !rule.selector.match(/^\.[a-zA-Z0-9_-]+\[/))
2440
+ continue;
2441
+ const sizeVal = rule.properties.get("Size");
2442
+ const autoVal = rule.properties.get("AutomaticSize");
2443
+ if (!sizeVal || sizeVal.type !== "UDim2")
2444
+ continue;
2445
+ const [xScale, xOffset, yScale, yOffset] = sizeVal.value;
2446
+ const autoStr = autoVal && autoVal.type === "Enum" ? autoVal.value : null;
2447
+ const xIsSet = xScale !== 0 || xOffset !== 0;
2448
+ const yIsSet = yScale !== 0 || yOffset !== 0;
2449
+ if (xIsSet && !yIsSet) {
2450
+ widthRules.push({
2451
+ selector: rule.selector,
2452
+ udim2: sizeVal.value,
2453
+ hasAuto: autoStr
2454
+ });
2455
+ } else if (yIsSet && !xIsSet) {
2456
+ heightRules.push({
2457
+ selector: rule.selector,
2458
+ udim2: sizeVal.value,
2459
+ hasAuto: autoStr
2460
+ });
2461
+ }
2462
+ }
2463
+ for (const rule of irRules) {
2464
+ if (!rule.selector.match(/^\.[a-zA-Z0-9_-]+$/))
2465
+ continue;
2466
+ const autoVal = rule.properties.get("AutomaticSize");
2467
+ const sizeVal = rule.properties.get("Size");
2468
+ if (!autoVal || autoVal.type !== "Enum" || sizeVal)
2469
+ continue;
2470
+ if (autoVal.value === "X") {
2471
+ widthRules.push({
2472
+ selector: rule.selector,
2473
+ udim2: [0, 0, 0, 0],
2474
+ hasAuto: "X"
2475
+ });
2476
+ } else if (autoVal.value === "Y") {
2477
+ heightRules.push({
2478
+ selector: rule.selector,
2479
+ udim2: [0, 0, 0, 0],
2480
+ hasAuto: "Y"
2481
+ });
2482
+ }
2483
+ }
2484
+ if (widthRules.length === 0 || heightRules.length === 0)
2485
+ return;
2486
+ for (const w of widthRules) {
2487
+ for (const h of heightRules) {
2488
+ const compoundSelector = `${w.selector}${h.selector}`;
2489
+ const props = new Map;
2490
+ const xAuto = w.hasAuto === "X";
2491
+ const yAuto = h.hasAuto === "Y";
2492
+ if (xAuto && yAuto) {
2493
+ props.set("AutomaticSize", {
2494
+ type: "Enum",
2495
+ enum: "AutomaticSize",
2496
+ value: "XY"
2497
+ });
2498
+ } else if (xAuto) {
2499
+ props.set("AutomaticSize", {
2500
+ type: "Enum",
2501
+ enum: "AutomaticSize",
2502
+ value: "X"
2503
+ });
2504
+ if (h.udim2[2] !== 0 || h.udim2[3] !== 0) {
2505
+ props.set("Size", {
2506
+ type: "UDim2",
2507
+ value: [0, 0, h.udim2[2], h.udim2[3]]
2508
+ });
2509
+ }
2510
+ } else if (yAuto) {
2511
+ props.set("AutomaticSize", {
2512
+ type: "Enum",
2513
+ enum: "AutomaticSize",
2514
+ value: "Y"
2515
+ });
2516
+ if (w.udim2[0] !== 0 || w.udim2[1] !== 0) {
2517
+ props.set("Size", {
2518
+ type: "UDim2",
2519
+ value: [w.udim2[0], w.udim2[1], 0, 0]
2520
+ });
2521
+ }
2522
+ } else {
2523
+ props.set("Size", {
2524
+ type: "UDim2",
2525
+ value: [w.udim2[0], w.udim2[1], h.udim2[2], h.udim2[3]]
2526
+ });
2527
+ }
2528
+ if (props.size > 0) {
2529
+ irRules.push({
2530
+ selector: compoundSelector,
2531
+ properties: props,
2532
+ pseudoInstances: []
2533
+ });
2534
+ }
2535
+ }
2536
+ }
2537
+ }
2538
+ function isUniversalMediaQuery(query) {
2539
+ const queries = query.mediaQueries;
2540
+ if (!queries || queries.length !== 1)
2541
+ return false;
2542
+ const q = queries[0];
2543
+ if (q.condition) {
2544
+ const cond = q.condition;
2545
+ if (cond.type === "feature") {
2546
+ const feature = cond.value;
2547
+ if (feature.type === "plain") {
2548
+ if ((feature.name === "hover" || feature.name === "pointer") && feature.value !== undefined) {
2549
+ return true;
2550
+ }
2551
+ }
2552
+ }
2553
+ }
2554
+ return false;
2555
+ }
2556
+ // src/codegen/luau.ts
2557
+ function generateLuau(ir, options) {
2558
+ const lines = [];
2559
+ const t = options.minify ? "" : " ";
2560
+ if (!options.minify) {
2561
+ lines.push("-- Auto-generated by rbx-css");
2562
+ if (options.sourceFile) {
2563
+ lines.push(`-- Source: ${options.sourceFile}`);
2564
+ }
2565
+ lines.push("");
2566
+ }
2567
+ lines.push("local function createStyleSheet()");
2568
+ lines.push(`${t}local sheet = Instance.new("StyleSheet")`);
2569
+ lines.push(`${t}sheet.Name = ${luauStr(ir.name)}`);
2570
+ lines.push("");
2571
+ if (ir.tokens.size > 0) {
2572
+ lines.push(`${t}-- Tokens from :root`);
2573
+ for (const [name, value] of ir.tokens) {
2574
+ lines.push(`${t}sheet:SetAttribute(${luauStr(name)}, ${serializeTokenValue(value)})`);
2575
+ }
2576
+ lines.push("");
2577
+ }
2578
+ for (const rule of ir.rules) {
2579
+ emitRule(rule, lines, t);
2580
+ }
2581
+ if (ir.themes && ir.themes.size > 0) {
2582
+ emitThemes(ir, lines, t);
2583
+ }
2584
+ lines.push(`${t}return sheet`);
2585
+ lines.push("end");
2586
+ lines.push("");
2587
+ lines.push("return createStyleSheet");
2588
+ lines.push("");
2589
+ return lines.join(`
2590
+ `);
2591
+ }
2592
+ function emitRule(rule, lines, t) {
2593
+ const t2 = t + t;
2594
+ lines.push(`${t}-- Rule: ${rule.selector}`);
2595
+ lines.push(`${t}do`);
2596
+ lines.push(`${t2}local rule = Instance.new("StyleRule")`);
2597
+ lines.push(`${t2}rule.Selector = ${luauStr(rule.selector)}`);
2598
+ lines.push(`${t2}rule.Parent = sheet`);
2599
+ if (rule.properties.size === 1) {
2600
+ const [propName, propValue] = [...rule.properties.entries()][0];
2601
+ lines.push(`${t2}rule:SetProperty(${luauStr(propName)}, ${serializeRobloxValue(propValue)})`);
2602
+ } else if (rule.properties.size > 1) {
2603
+ lines.push(`${t2}rule:SetProperties({`);
2604
+ for (const [propName, propValue] of rule.properties) {
2605
+ lines.push(`${t2}${t}${propName} = ${serializeRobloxValue(propValue)},`);
2606
+ }
2607
+ lines.push(`${t2}})`);
2608
+ }
2609
+ lines.push(`${t}end`);
2610
+ lines.push("");
2611
+ }
2612
+ function emitThemes(ir, lines, t) {
2613
+ if (!ir.themes)
2614
+ return;
2615
+ const t2 = t + t;
2616
+ lines.push(`${t}-- Themes`);
2617
+ lines.push(`${t}local themes = {}`);
2618
+ lines.push("");
2619
+ for (const [themeName, themeIR] of ir.themes) {
2620
+ lines.push(`${t}do`);
2621
+ lines.push(`${t2}local theme = Instance.new("StyleSheet")`);
2622
+ lines.push(`${t2}theme.Name = ${luauStr(themeIR.name)}`);
2623
+ for (const [tokenName, tokenValue] of themeIR.tokens) {
2624
+ lines.push(`${t2}theme:SetAttribute(${luauStr(tokenName)}, ${serializeTokenValue(tokenValue)})`);
2625
+ }
2626
+ lines.push(`${t2}themes[${luauStr(themeName)}] = theme`);
2627
+ lines.push(`${t}end`);
2628
+ lines.push("");
2629
+ }
2630
+ lines.push(`${t}-- Theme derive`);
2631
+ lines.push(`${t}local themeDerive = Instance.new("StyleDerive")`);
2632
+ lines.push(`${t}themeDerive.Parent = sheet`);
2633
+ lines.push("");
2634
+ lines.push(`${t}-- Theme switcher`);
2635
+ lines.push(`${t}local function setTheme(themeName: string)`);
2636
+ lines.push(`${t2}themeDerive.StyleSheet = themes[themeName]`);
2637
+ lines.push(`${t}end`);
2638
+ lines.push("");
2639
+ }
2640
+ function serializeRobloxValue(value) {
2641
+ switch (value.type) {
2642
+ case "Color3":
2643
+ return `Color3.fromRGB(${value.value[0]}, ${value.value[1]}, ${value.value[2]})`;
2644
+ case "UDim2":
2645
+ return `UDim2.new(${value.value.join(", ")})`;
2646
+ case "UDim":
2647
+ return `UDim.new(${value.value.join(", ")})`;
2648
+ case "number":
2649
+ return formatNumber(value.value);
2650
+ case "boolean":
2651
+ return String(value.value);
2652
+ case "Enum":
2653
+ return `Enum.${value.enum}.${value.value}`;
2654
+ case "token":
2655
+ return `"$${value.name}"`;
2656
+ case "Font":
2657
+ return serializeFont(value);
2658
+ case "Vector2":
2659
+ return `Vector2.new(${value.value.map(formatNumber).join(", ")})`;
2660
+ case "string":
2661
+ return luauStr(value.value);
2662
+ case "ColorSequence":
2663
+ return serializeColorSequence(value.stops);
2664
+ }
2665
+ }
2666
+ function serializeFont(font) {
2667
+ const path = `rbxasset://fonts/families/${font.family}.json`;
2668
+ const args = [luauStr(path)];
2669
+ if (font.weight && font.weight !== "Regular") {
2670
+ args.push(`Enum.FontWeight.${font.weight}`);
2671
+ }
2672
+ if (font.style && font.style !== "Normal") {
2673
+ if (args.length === 1) {
2674
+ args.push("Enum.FontWeight.Regular");
2675
+ }
2676
+ args.push(`Enum.FontStyle.${font.style}`);
2677
+ }
2678
+ return `Font.new(${args.join(", ")})`;
2679
+ }
2680
+ function serializeColorSequence(stops) {
2681
+ if (stops.length === 2 && stops[0].position === 0 && stops[1].position === 1) {
2682
+ return `ColorSequence.new(Color3.fromRGB(${stops[0].color.join(", ")}), Color3.fromRGB(${stops[1].color.join(", ")}))`;
2683
+ }
2684
+ const keypoints = stops.map((s) => `ColorSequenceKeypoint.new(${formatNumber(s.position)}, Color3.fromRGB(${s.color.join(", ")}))`).join(", ");
2685
+ return `ColorSequence.new({${keypoints}})`;
2686
+ }
2687
+ function serializeTokenValue(value) {
2688
+ switch (value.type) {
2689
+ case "Color3":
2690
+ return `Color3.fromHex(${luauStr(rgbToHex(value.value))})`;
2691
+ case "UDim":
2692
+ return `UDim.new(${value.value.join(", ")})`;
2693
+ case "number":
2694
+ return formatNumber(value.value);
2695
+ case "Font":
2696
+ return serializeFont(value);
2697
+ case "string":
2698
+ return luauStr(value.value);
2699
+ }
2700
+ }
2701
+ function luauStr(s) {
2702
+ return `"${s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
2703
+ }
2704
+ function formatNumber(n) {
2705
+ if (n === Infinity)
2706
+ return "math.huge";
2707
+ if (n === -Infinity)
2708
+ return "-math.huge";
2709
+ if (Number.isInteger(n))
2710
+ return String(n);
2711
+ const s = n.toPrecision(10);
2712
+ return parseFloat(s).toString();
2713
+ }
2714
+ // src/codegen/rbxmx.ts
2715
+ function generateRBXMX(ir) {
2716
+ let referentCounter = 0;
2717
+ const nextReferent = () => `RBX${String(++referentCounter).padStart(4, "0")}`;
2718
+ const lines = [];
2719
+ lines.push('<roblox version="4">');
2720
+ const sheetRef = nextReferent();
2721
+ lines.push(` <Item class="StyleSheet" referent="${sheetRef}">`);
2722
+ lines.push(" <Properties>");
2723
+ lines.push(` <string name="Name">${escapeXml(ir.name)}</string>`);
2724
+ lines.push(" </Properties>");
2725
+ if (ir.tokens.size > 0) {
2726
+ lines.push(` <!-- Tokens -->`);
2727
+ for (const [name, value] of ir.tokens) {
2728
+ lines.push(` <!-- ${escapeXml(name)}: ${serializeTokenComment(value)} -->`);
2729
+ }
2730
+ }
2731
+ for (const rule of ir.rules) {
2732
+ const ruleRef = nextReferent();
2733
+ lines.push(` <Item class="StyleRule" referent="${ruleRef}">`, " <Properties>", ` <string name="Selector">${escapeXml(rule.selector)}</string>`, " </Properties>");
2734
+ if (rule.properties.size > 0) {
2735
+ lines.push(" <!-- Properties:");
2736
+ for (const [propName, propValue] of rule.properties) {
2737
+ lines.push(` ${escapeXml(propName)} = ${serializeValueComment(propValue)}`);
2738
+ }
2739
+ lines.push(" -->");
2740
+ }
2741
+ lines.push(" </Item>");
2742
+ }
2743
+ lines.push(" </Item>");
2744
+ if (ir.themes) {
2745
+ for (const [themeName, themeIR] of ir.themes) {
2746
+ const themeRef = nextReferent();
2747
+ lines.push(` <Item class="StyleSheet" referent="${themeRef}">`, " <Properties>", ` <string name="Name">${escapeXml(themeIR.name)}</string>`, " </Properties>", " </Item>");
2748
+ }
2749
+ }
2750
+ lines.push("</roblox>", "");
2751
+ return lines.join(`
2752
+ `);
2753
+ }
2754
+ function escapeXml(s) {
2755
+ return s.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
2756
+ }
2757
+ function serializeTokenComment(value) {
2758
+ switch (value.type) {
2759
+ case "Color3":
2760
+ return rgbToHex(value.value);
2761
+ case "UDim":
2762
+ return `UDim(${value.value.join(", ")})`;
2763
+ case "number":
2764
+ return String(value.value);
2765
+ case "Font":
2766
+ return value.family;
2767
+ case "string":
2768
+ return value.value;
2769
+ }
2770
+ }
2771
+ function serializeValueComment(value) {
2772
+ switch (value.type) {
2773
+ case "Color3":
2774
+ return `Color3(${value.value.join(", ")})`;
2775
+ case "UDim2":
2776
+ return `UDim2(${value.value.join(", ")})`;
2777
+ case "UDim":
2778
+ return `UDim(${value.value.join(", ")})`;
2779
+ case "number":
2780
+ return String(value.value);
2781
+ case "boolean":
2782
+ return String(value.value);
2783
+ case "Enum":
2784
+ return `Enum.${value.enum}.${value.value}`;
2785
+ case "token":
2786
+ return `$${value.name}`;
2787
+ case "Font":
2788
+ return value.family;
2789
+ case "Vector2":
2790
+ return `Vector2(${value.value.join(", ")})`;
2791
+ case "string":
2792
+ return value.value;
2793
+ case "ColorSequence":
2794
+ return "ColorSequence(...)";
2795
+ }
2796
+ }
2797
+ // src/manifest.ts
2798
+ function generateManifest(overflowScrollClasses) {
2799
+ const classes = {};
2800
+ for (const [className, overflowScroll] of overflowScrollClasses) {
2801
+ classes[className] = { overflowScroll };
2802
+ }
2803
+ const elementMap = { ...HTML_TO_ROBLOX };
2804
+ return { classes, elementMap };
2805
+ }
2806
+ export {
2807
+ generateRBXMX,
2808
+ generateManifest,
2809
+ generateLuau,
2810
+ compile,
2811
+ WarningCollector
2812
+ };