typewritingclass-compiler 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.
@@ -0,0 +1,898 @@
1
+ use std::collections::HashMap;
2
+
3
+ use oxc_allocator::Allocator;
4
+ use oxc_ast::ast::*;
5
+ use oxc_parser::Parser;
6
+ use oxc_span::SourceType;
7
+
8
+ use crate::css;
9
+ use crate::hash;
10
+ use crate::modifiers;
11
+ use crate::style_rule::StyleRule;
12
+ use crate::theme::ThemeData;
13
+ use crate::utilities::{self, Value};
14
+
15
+ /// What a local name is bound to after import resolution
16
+ #[derive(Debug, Clone)]
17
+ enum Binding {
18
+ /// A utility function (e.g., bg, p, rounded)
19
+ Utility(String),
20
+ /// A modifier (e.g., hover, focus, dark, sm)
21
+ Modifier(String),
22
+ /// cx() core function
23
+ Cx,
24
+ /// when() core function
25
+ When,
26
+ /// css() escape hatch
27
+ Css,
28
+ /// dynamic() wrapper
29
+ Dynamic,
30
+ /// A color scale object (e.g., blue, red) — value is the color name
31
+ ColorScale(String),
32
+ /// A resolved string value (e.g., theme token like border radius, shadow, size, weight)
33
+ ResolvedValue(String),
34
+ /// A text size token — stores (font_size, line_height)
35
+ TextSize(String, String),
36
+ /// A namespace import (e.g., `import * as shadows from '...'`)
37
+ Namespace(String),
38
+ }
39
+
40
+ pub struct DiagnosticInfo {
41
+ pub message: String,
42
+ pub line: u32,
43
+ pub column: u32,
44
+ pub severity: String,
45
+ }
46
+
47
+ /// Result of a single file transform
48
+ pub struct TransformResult {
49
+ pub code: String,
50
+ pub css_rules: Vec<(String, String, u32)>, // (class_name, css_text, layer)
51
+ pub next_layer: u32,
52
+ pub has_dynamic: bool,
53
+ pub diagnostics: Vec<DiagnosticInfo>,
54
+ }
55
+
56
+ /// All known utility function names exported from typewritingclass
57
+ const UTILITY_NAMES: &[&str] = &[
58
+ "p", "px", "py", "pt", "pr", "pb", "pl", "m", "mx", "my", "mt", "mr", "mb", "ml", "gap",
59
+ "gapX", "gapY", "bg", "textColor", "borderColor", "text", "font", "tracking", "leading",
60
+ "textAlign", "flex", "flexCol", "flexRow", "flexWrap", "inlineFlex", "grid", "gridCols",
61
+ "gridRows", "w", "h", "size", "minW", "minH", "maxW", "maxH", "display", "items", "justify",
62
+ "self", "overflow", "overflowX", "overflowY", "relative", "absolute", "fixed", "sticky",
63
+ "top", "right", "bottom", "left", "inset", "z", "rounded", "roundedT", "roundedB", "roundedL",
64
+ "roundedR", "border", "borderT", "borderR", "borderB", "borderL", "ring", "shadow", "opacity",
65
+ "backdrop", "cursor", "select", "pointerEvents",
66
+ ];
67
+
68
+ /// Counter for dynamic variable IDs (per-file)
69
+ struct DynCounter {
70
+ next: u32,
71
+ }
72
+
73
+ impl DynCounter {
74
+ fn new() -> Self { Self { next: 0 } }
75
+ fn next_id(&mut self) -> String {
76
+ let id = format!("--twc-d{}", self.next);
77
+ self.next += 1;
78
+ id
79
+ }
80
+ }
81
+
82
+ pub fn transform(source: &str, filename: &str, layer_offset: u32, theme: &ThemeData, strict: bool) -> TransformResult {
83
+ let allocator = Allocator::default();
84
+ let source_type = SourceType::from_path(filename).unwrap_or_default();
85
+ let ret = Parser::new(&allocator, source, source_type).parse();
86
+
87
+ if ret.panicked || !ret.errors.is_empty() {
88
+ return TransformResult {
89
+ code: source.to_string(),
90
+ css_rules: vec![],
91
+ next_layer: layer_offset,
92
+ has_dynamic: false,
93
+ diagnostics: vec![],
94
+ };
95
+ }
96
+
97
+ let program = &ret.program;
98
+
99
+ // Phase 1: Collect import bindings
100
+ let bindings = collect_imports(program, theme);
101
+
102
+ if bindings.is_empty() {
103
+ return TransformResult {
104
+ code: source.to_string(),
105
+ css_rules: vec![],
106
+ next_layer: layer_offset,
107
+ has_dynamic: false,
108
+ diagnostics: vec![],
109
+ };
110
+ }
111
+
112
+ // Check if there's a cx binding
113
+ let has_cx = bindings.values().any(|b| matches!(b, Binding::Cx));
114
+ let has_dynamic_import = bindings.values().any(|b| matches!(b, Binding::Dynamic));
115
+
116
+ if !has_cx {
117
+ // No cx() usage, just prepend inject import
118
+ let code = prepend_inject(source);
119
+ return TransformResult {
120
+ code,
121
+ css_rules: vec![],
122
+ next_layer: layer_offset,
123
+ has_dynamic: false,
124
+ diagnostics: vec![],
125
+ };
126
+ }
127
+
128
+ // Phase 2: Find cx() calls and try to extract them
129
+ let mut layer = layer_offset;
130
+ let mut css_rules = vec![];
131
+ let mut replacements: Vec<(u32, u32, String)> = vec![];
132
+ let mut has_dynamic = false;
133
+ let mut diagnostics: Vec<DiagnosticInfo> = vec![];
134
+ let mut dyn_counter = DynCounter::new();
135
+
136
+ visit_expressions(program, &mut |expr| {
137
+ if let Expression::CallExpression(call) = expr {
138
+ if is_cx_call(call, &bindings) {
139
+ match try_extract_cx(call, &bindings, &mut layer, theme, &mut dyn_counter) {
140
+ Some(ExtractedCx::Static(class_str, rules)) => {
141
+ let span = call.span;
142
+ replacements.push((span.start, span.end, format!("'{}'", class_str)));
143
+ css_rules.extend(rules);
144
+ }
145
+ Some(ExtractedCx::Dynamic(class_str, rules, dyn_bindings)) => {
146
+ has_dynamic = true;
147
+ let span = call.span;
148
+ // Build the bindings object literal
149
+ let bindings_obj = format_bindings_object(&dyn_bindings);
150
+ replacements.push((
151
+ span.start,
152
+ span.end,
153
+ format!("__twcDynamic('{}', {})", class_str, bindings_obj),
154
+ ));
155
+ css_rules.extend(rules);
156
+ }
157
+ None => {
158
+ // Could not statically extract
159
+ if strict && !has_dynamic_import {
160
+ let span = call.span;
161
+ diagnostics.push(DiagnosticInfo {
162
+ message: format!(
163
+ "cx() call could not be statically evaluated. Wrap runtime values with dynamic() or disable strict mode."
164
+ ),
165
+ line: span.start,
166
+ column: 0,
167
+ severity: "error".to_string(),
168
+ });
169
+ }
170
+ }
171
+ }
172
+ }
173
+ }
174
+ });
175
+
176
+ // Phase 3: Apply replacements (reverse order to preserve spans)
177
+ let mut code = source.to_string();
178
+ replacements.sort_by(|a, b| b.0.cmp(&a.0));
179
+ for (start, end, replacement) in &replacements {
180
+ code.replace_range(*start as usize..*end as usize, replacement);
181
+ }
182
+
183
+ // Phase 4: Inject appropriate imports
184
+ if has_dynamic {
185
+ // Import runtime helper for dynamic values
186
+ if !code.contains("typewritingclass/runtime") {
187
+ code = format!("import {{ __twcDynamic }} from 'typewritingclass/runtime';\n{}", code);
188
+ }
189
+ }
190
+
191
+ if !has_dynamic && replacements.len() == count_cx_calls(program, &bindings) {
192
+ // All cx() calls were statically extracted and no dynamic — no runtime needed
193
+ // Just ensure virtual CSS import is there (handled by plugin)
194
+ } else {
195
+ // Some cx() calls need runtime fallback
196
+ code = prepend_inject(&code);
197
+ }
198
+
199
+ TransformResult {
200
+ code,
201
+ css_rules,
202
+ next_layer: layer,
203
+ has_dynamic,
204
+ diagnostics,
205
+ }
206
+ }
207
+
208
+ fn format_bindings_object(bindings: &[(String, String)]) -> String {
209
+ let pairs: Vec<String> = bindings
210
+ .iter()
211
+ .map(|(key, expr)| format!("'{}': {}", key, expr))
212
+ .collect();
213
+ format!("{{ {} }}", pairs.join(", "))
214
+ }
215
+
216
+ fn count_cx_calls(program: &Program, bindings: &HashMap<String, Binding>) -> usize {
217
+ let mut count = 0;
218
+ visit_expressions(program, &mut |expr| {
219
+ if let Expression::CallExpression(call) = expr {
220
+ if is_cx_call(call, bindings) {
221
+ count += 1;
222
+ }
223
+ }
224
+ });
225
+ count
226
+ }
227
+
228
+ fn prepend_inject(source: &str) -> String {
229
+ if source.contains("typewritingclass/inject") {
230
+ source.to_string()
231
+ } else {
232
+ format!("import 'typewritingclass/inject';\n{}", source)
233
+ }
234
+ }
235
+
236
+ /// Collect all imports from typewritingclass modules and resolve them to Bindings
237
+ fn collect_imports(program: &Program, theme: &ThemeData) -> HashMap<String, Binding> {
238
+ let mut bindings = HashMap::new();
239
+
240
+ for stmt in &program.body {
241
+ let Statement::ImportDeclaration(import) = stmt else {
242
+ continue;
243
+ };
244
+ let source_value = import.source.value.as_str();
245
+
246
+ // Only process typewritingclass imports
247
+ if !source_value.starts_with("typewritingclass") {
248
+ continue;
249
+ }
250
+
251
+ let Some(specifiers) = &import.specifiers else {
252
+ continue;
253
+ };
254
+
255
+ for spec in specifiers {
256
+ match spec {
257
+ ImportDeclarationSpecifier::ImportSpecifier(named) => {
258
+ let imported_name = match &named.imported {
259
+ ModuleExportName::IdentifierName(id) => id.name.as_str(),
260
+ ModuleExportName::IdentifierReference(id) => id.name.as_str(),
261
+ ModuleExportName::StringLiteral(s) => s.value.as_str(),
262
+ };
263
+ let local_name = named.local.name.as_str().to_string();
264
+
265
+ let binding = resolve_import(source_value, imported_name, theme);
266
+ if let Some(b) = binding {
267
+ bindings.insert(local_name, b);
268
+ }
269
+ }
270
+ ImportDeclarationSpecifier::ImportNamespaceSpecifier(ns) => {
271
+ let local_name = ns.local.name.as_str().to_string();
272
+ bindings.insert(local_name, Binding::Namespace(source_value.to_string()));
273
+ }
274
+ ImportDeclarationSpecifier::ImportDefaultSpecifier(_) => {
275
+ // Default imports from typewritingclass modules aren't typical
276
+ }
277
+ }
278
+ }
279
+ }
280
+
281
+ bindings
282
+ }
283
+
284
+ /// Given an import source and exported name, determine the Binding
285
+ fn resolve_import(source: &str, name: &str, theme: &ThemeData) -> Option<Binding> {
286
+ match source {
287
+ "typewritingclass" => {
288
+ // Core API
289
+ if name == "cx" {
290
+ return Some(Binding::Cx);
291
+ }
292
+ if name == "when" {
293
+ return Some(Binding::When);
294
+ }
295
+ if name == "css" {
296
+ return Some(Binding::Css);
297
+ }
298
+ if name == "dynamic" {
299
+ return Some(Binding::Dynamic);
300
+ }
301
+ // Modifiers
302
+ if modifiers::is_modifier(name) {
303
+ return Some(Binding::Modifier(name.to_string()));
304
+ }
305
+ // Utilities
306
+ if UTILITY_NAMES.contains(&name) {
307
+ return Some(Binding::Utility(name.to_string()));
308
+ }
309
+ None
310
+ }
311
+ "typewritingclass/theme/colors" => {
312
+ // Named color constants
313
+ if let Some(val) = theme.resolve_named_color(name) {
314
+ return Some(Binding::ResolvedValue(val.to_string()));
315
+ }
316
+ // Color scales
317
+ if theme.colors.contains_key(name) {
318
+ return Some(Binding::ColorScale(name.to_string()));
319
+ }
320
+ None
321
+ }
322
+ "typewritingclass/theme/typography" => {
323
+ // Text size tokens
324
+ if let Some((fs, lh)) = theme.resolve_text_size(name) {
325
+ return Some(Binding::TextSize(fs.to_string(), lh.to_string()));
326
+ }
327
+ // Font weight tokens
328
+ if let Some(val) = theme.resolve_font_weight(name) {
329
+ return Some(Binding::ResolvedValue(val.to_string()));
330
+ }
331
+ None
332
+ }
333
+ "typewritingclass/theme/borders" => {
334
+ if let Some(val) = theme.resolve_radius(name) {
335
+ return Some(Binding::ResolvedValue(val.to_string()));
336
+ }
337
+ None
338
+ }
339
+ "typewritingclass/theme/shadows" => {
340
+ if let Some(val) = theme.resolve_shadow(name) {
341
+ return Some(Binding::ResolvedValue(val.to_string()));
342
+ }
343
+ None
344
+ }
345
+ "typewritingclass/theme/sizes" => {
346
+ if let Some(val) = theme.resolve_size(name) {
347
+ return Some(Binding::ResolvedValue(val.to_string()));
348
+ }
349
+ None
350
+ }
351
+ _ if source.starts_with("typewritingclass/theme") => {
352
+ // Generic theme sub-path
353
+ None
354
+ }
355
+ _ => None,
356
+ }
357
+ }
358
+
359
+ /// Check if a call expression is a cx() call
360
+ fn is_cx_call(call: &CallExpression, bindings: &HashMap<String, Binding>) -> bool {
361
+ if let Expression::Identifier(id) = &call.callee {
362
+ if let Some(Binding::Cx) = bindings.get(id.name.as_str()) {
363
+ return true;
364
+ }
365
+ }
366
+ false
367
+ }
368
+
369
+ enum ExtractedCx {
370
+ /// All arguments were static — just class names
371
+ Static(String, Vec<(String, String, u32)>),
372
+ /// Some arguments had dynamic() — class names + dynamic bindings
373
+ Dynamic(String, Vec<(String, String, u32)>, Vec<(String, String)>),
374
+ }
375
+
376
+ /// Try to statically extract a cx() call.
377
+ /// Returns None if any argument can't be evaluated (falls back to runtime).
378
+ fn try_extract_cx(
379
+ call: &CallExpression,
380
+ bindings: &HashMap<String, Binding>,
381
+ layer: &mut u32,
382
+ theme: &ThemeData,
383
+ dyn_counter: &mut DynCounter,
384
+ ) -> Option<ExtractedCx> {
385
+ let mut class_names = vec![];
386
+ let mut rules = vec![];
387
+ let mut all_dynamic_bindings: Vec<(String, String)> = vec![];
388
+
389
+ for arg in &call.arguments {
390
+ let expr = arg.as_expression()?;
391
+ match evaluate_cx_arg(expr, bindings, theme, dyn_counter)? {
392
+ CxArg::Rule(rule) => {
393
+ let l = *layer;
394
+ *layer += 1;
395
+ // Collect dynamic bindings before generating hash/css
396
+ for (var_name, expr_text) in &rule.dynamic_bindings {
397
+ all_dynamic_bindings.push((var_name.clone(), expr_text.clone()));
398
+ }
399
+ let class_name = hash::generate_hash(&rule, l);
400
+ let css_text = css::render_rule(&class_name, &rule);
401
+ class_names.push(class_name.clone());
402
+ rules.push((class_name, css_text, l));
403
+ }
404
+ CxArg::ClassName(s) => {
405
+ class_names.push(s);
406
+ }
407
+ }
408
+ }
409
+
410
+ let class_str = class_names.join(" ");
411
+
412
+ if all_dynamic_bindings.is_empty() {
413
+ Some(ExtractedCx::Static(class_str, rules))
414
+ } else {
415
+ Some(ExtractedCx::Dynamic(class_str, rules, all_dynamic_bindings))
416
+ }
417
+ }
418
+
419
+ enum CxArg {
420
+ Rule(StyleRule),
421
+ ClassName(String),
422
+ }
423
+
424
+ /// Evaluate a single argument to cx()
425
+ fn evaluate_cx_arg(
426
+ expr: &Expression,
427
+ bindings: &HashMap<String, Binding>,
428
+ theme: &ThemeData,
429
+ dyn_counter: &mut DynCounter,
430
+ ) -> Option<CxArg> {
431
+ match expr {
432
+ Expression::StringLiteral(s) => Some(CxArg::ClassName(s.value.to_string())),
433
+
434
+ Expression::CallExpression(call) => {
435
+ // Could be: utility call, when(mod)(rules), css({...})
436
+ evaluate_call_as_cx_arg(call, bindings, theme, dyn_counter)
437
+ }
438
+
439
+ _ => None, // Can't evaluate — bail
440
+ }
441
+ }
442
+
443
+ /// Evaluate a call expression that appears as a cx() argument
444
+ fn evaluate_call_as_cx_arg(
445
+ call: &CallExpression,
446
+ bindings: &HashMap<String, Binding>,
447
+ theme: &ThemeData,
448
+ dyn_counter: &mut DynCounter,
449
+ ) -> Option<CxArg> {
450
+ match &call.callee {
451
+ Expression::Identifier(id) => {
452
+ let binding = bindings.get(id.name.as_str())?;
453
+ match binding {
454
+ Binding::Utility(name) => {
455
+ let args = evaluate_call_args(&call.arguments, bindings, theme, dyn_counter)?;
456
+ // Special handling for text() with TextSize tokens
457
+ if name == "text" && call.arguments.len() == 1 {
458
+ if let Some(expr) = call.arguments[0].as_expression() {
459
+ if let Expression::Identifier(arg_id) = expr {
460
+ if let Some(Binding::TextSize(fs, lh)) =
461
+ bindings.get(arg_id.name.as_str())
462
+ {
463
+ let rule = utilities::evaluate_text_with_size(fs, lh);
464
+ return Some(CxArg::Rule(rule));
465
+ }
466
+ }
467
+ }
468
+ }
469
+ let rule = utilities::evaluate(name, &args, theme)?;
470
+ Some(CxArg::Rule(rule))
471
+ }
472
+ Binding::Css => {
473
+ // css({ key: value, ... })
474
+ let rule = evaluate_css_call(call, bindings)?;
475
+ Some(CxArg::Rule(rule))
476
+ }
477
+ _ => None,
478
+ }
479
+ }
480
+ // when(modifier)(rules) pattern: callee is a CallExpression
481
+ Expression::CallExpression(inner_call) => {
482
+ if let Expression::Identifier(id) = &inner_call.callee {
483
+ if let Some(Binding::When) = bindings.get(id.name.as_str()) {
484
+ return evaluate_when_call(inner_call, call, bindings, theme, dyn_counter);
485
+ }
486
+ }
487
+ None
488
+ }
489
+ _ => None,
490
+ }
491
+ }
492
+
493
+ /// Evaluate a when(modifier1, modifier2)(rule1, rule2) call
494
+ fn evaluate_when_call(
495
+ when_call: &CallExpression, // when(modifier, ...)
496
+ outer_call: &CallExpression, // (rule, ...)
497
+ bindings: &HashMap<String, Binding>,
498
+ theme: &ThemeData,
499
+ dyn_counter: &mut DynCounter,
500
+ ) -> Option<CxArg> {
501
+ // Collect modifier names
502
+ let mut modifier_names = vec![];
503
+ for arg in &when_call.arguments {
504
+ let expr = arg.as_expression()?;
505
+ if let Expression::Identifier(id) = expr {
506
+ if let Some(Binding::Modifier(name)) = bindings.get(id.name.as_str()) {
507
+ modifier_names.push(name.clone());
508
+ } else {
509
+ return None;
510
+ }
511
+ } else {
512
+ return None;
513
+ }
514
+ }
515
+
516
+ // Evaluate the rules (outer call arguments)
517
+ let mut style_rules = vec![];
518
+ for arg in &outer_call.arguments {
519
+ let expr = arg.as_expression()?;
520
+ match evaluate_cx_arg(expr, bindings, theme, dyn_counter)? {
521
+ CxArg::Rule(rule) => style_rules.push(rule),
522
+ _ => return None,
523
+ }
524
+ }
525
+
526
+ // Combine rules
527
+ let mut combined = StyleRule::merge(&style_rules);
528
+
529
+ // Apply modifiers in reverse order (matching TS reduceRight)
530
+ for name in modifier_names.iter().rev() {
531
+ combined = modifiers::apply(name, combined)?;
532
+ }
533
+
534
+ Some(CxArg::Rule(combined))
535
+ }
536
+
537
+ /// Evaluate a css({ key: 'value', ... }) call
538
+ fn evaluate_css_call(
539
+ call: &CallExpression,
540
+ _bindings: &HashMap<String, Binding>,
541
+ ) -> Option<StyleRule> {
542
+ if call.arguments.len() != 1 {
543
+ return None;
544
+ }
545
+ let expr = call.arguments[0].as_expression()?;
546
+ if let Expression::ObjectExpression(obj) = expr {
547
+ let mut declarations = vec![];
548
+ for prop in &obj.properties {
549
+ if let ObjectPropertyKind::ObjectProperty(p) = prop {
550
+ let key = match &p.key {
551
+ PropertyKey::StaticIdentifier(id) => id.name.as_str().to_string(),
552
+ PropertyKey::StringLiteral(s) => s.value.to_string(),
553
+ _ => return None,
554
+ };
555
+ let value = match &p.value {
556
+ Expression::StringLiteral(s) => s.value.to_string(),
557
+ _ => return None,
558
+ };
559
+ declarations.push((key, value));
560
+ } else {
561
+ return None; // SpreadProperty etc
562
+ }
563
+ }
564
+ Some(StyleRule {
565
+ declarations,
566
+ selectors: vec![],
567
+ media_queries: vec![],
568
+ dynamic_bindings: vec![],
569
+ })
570
+ } else {
571
+ None
572
+ }
573
+ }
574
+
575
+ /// Resolve call arguments to Values
576
+ fn evaluate_call_args(
577
+ args: &oxc_allocator::Vec<Argument>,
578
+ bindings: &HashMap<String, Binding>,
579
+ theme: &ThemeData,
580
+ dyn_counter: &mut DynCounter,
581
+ ) -> Option<Vec<Value>> {
582
+ let mut values = vec![];
583
+ for arg in args {
584
+ let expr = arg.as_expression()?;
585
+ values.push(evaluate_value(expr, bindings, theme, dyn_counter)?);
586
+ }
587
+ Some(values)
588
+ }
589
+
590
+ /// Evaluate an expression to a Value (string, number, or dynamic)
591
+ fn evaluate_value(
592
+ expr: &Expression,
593
+ bindings: &HashMap<String, Binding>,
594
+ theme: &ThemeData,
595
+ dyn_counter: &mut DynCounter,
596
+ ) -> Option<Value> {
597
+ match expr {
598
+ Expression::StringLiteral(s) => Some(Value::Str(s.value.to_string())),
599
+ Expression::NumericLiteral(n) => Some(Value::Num(n.value)),
600
+
601
+ Expression::Identifier(id) => {
602
+ match bindings.get(id.name.as_str())? {
603
+ Binding::ResolvedValue(val) => Some(Value::Str(val.clone())),
604
+ Binding::TextSize(fs, _lh) => {
605
+ // When a text size token is used as a plain value (not with text()),
606
+ // just return the fontSize as a string
607
+ Some(Value::Str(fs.clone()))
608
+ }
609
+ _ => None,
610
+ }
611
+ }
612
+
613
+ // dynamic(expr) pattern
614
+ Expression::CallExpression(call) => {
615
+ if let Expression::Identifier(callee_id) = &call.callee {
616
+ if let Some(Binding::Dynamic) = bindings.get(callee_id.name.as_str()) {
617
+ // dynamic() call — generate a CSS custom property ID
618
+ if call.arguments.len() == 1 {
619
+ let inner_expr = call.arguments[0].as_expression()?;
620
+ let id = dyn_counter.next_id();
621
+ // Get the source text of the inner expression
622
+ let expr_text = extract_source_text(inner_expr);
623
+ return Some(Value::Dynamic(id, expr_text));
624
+ }
625
+ }
626
+ }
627
+ None
628
+ }
629
+
630
+ // blue[500] pattern — computed member expression
631
+ Expression::ComputedMemberExpression(computed) => {
632
+ if let Expression::Identifier(obj_id) = &computed.object {
633
+ if let Some(Binding::ColorScale(color_name)) =
634
+ bindings.get(obj_id.name.as_str())
635
+ {
636
+ // The property should be a numeric literal (shade)
637
+ if let Expression::NumericLiteral(n) = &computed.expression {
638
+ let shade = format!("{}", n.value as u32);
639
+ if let Some(hex) = theme.resolve_color(color_name, &shade) {
640
+ return Some(Value::Str(hex.to_string()));
641
+ }
642
+ }
643
+ }
644
+ }
645
+ None
646
+ }
647
+
648
+ // namespace.member pattern — e.g., typography.bold, shadows.lg
649
+ Expression::StaticMemberExpression(member) => {
650
+ if let Expression::Identifier(obj_id) = &member.object {
651
+ let prop_name = member.property.name.as_str();
652
+ if let Some(Binding::Namespace(source)) = bindings.get(obj_id.name.as_str()) {
653
+ return resolve_namespace_member(source, prop_name, theme);
654
+ }
655
+ }
656
+ None
657
+ }
658
+
659
+ _ => None,
660
+ }
661
+ }
662
+
663
+ /// Extract the source text representation of an expression for use in generated code
664
+ fn extract_source_text(expr: &Expression) -> String {
665
+ match expr {
666
+ Expression::Identifier(id) => id.name.to_string(),
667
+ Expression::StringLiteral(s) => format!("'{}'", s.value),
668
+ Expression::NumericLiteral(n) => format!("{}", n.value),
669
+ Expression::StaticMemberExpression(member) => {
670
+ let obj = extract_source_text(&member.object);
671
+ format!("{}.{}", obj, member.property.name)
672
+ }
673
+ Expression::ComputedMemberExpression(computed) => {
674
+ let obj = extract_source_text(&computed.object);
675
+ let prop = extract_source_text(&computed.expression);
676
+ format!("{}[{}]", obj, prop)
677
+ }
678
+ Expression::CallExpression(call) => {
679
+ let callee = extract_source_text(&call.callee);
680
+ let args: Vec<String> = call.arguments.iter().filter_map(|a| {
681
+ a.as_expression().map(extract_source_text)
682
+ }).collect();
683
+ format!("{}({})", callee, args.join(", "))
684
+ }
685
+ Expression::TemplateLiteral(tmpl) => {
686
+ // Simple case: reconstruct template literal
687
+ let mut result = String::from("`");
688
+ for (i, quasi) in tmpl.quasis.iter().enumerate() {
689
+ result.push_str(quasi.value.raw.as_str());
690
+ if i < tmpl.expressions.len() {
691
+ result.push_str("${");
692
+ result.push_str(&extract_source_text(&tmpl.expressions[i]));
693
+ result.push('}');
694
+ }
695
+ }
696
+ result.push('`');
697
+ result
698
+ }
699
+ Expression::BinaryExpression(bin) => {
700
+ let left = extract_source_text(&bin.left);
701
+ let right = extract_source_text(&bin.right);
702
+ let op = match bin.operator {
703
+ BinaryOperator::Addition => "+",
704
+ BinaryOperator::Subtraction => "-",
705
+ BinaryOperator::Multiplication => "*",
706
+ BinaryOperator::Division => "/",
707
+ _ => "?",
708
+ };
709
+ format!("{} {} {}", left, op, right)
710
+ }
711
+ _ => "undefined".to_string(),
712
+ }
713
+ }
714
+
715
+ /// Resolve a namespace.member access (e.g., typography.bold, shadows.md)
716
+ fn resolve_namespace_member(source: &str, member: &str, theme: &ThemeData) -> Option<Value> {
717
+ match source {
718
+ "typewritingclass/theme/typography" => {
719
+ if let Some((fs, _lh)) = theme.resolve_text_size(member) {
720
+ // For font weights, return as resolved value
721
+ return Some(Value::Str(fs.to_string()));
722
+ }
723
+ if let Some(val) = theme.resolve_font_weight(member) {
724
+ return Some(Value::Str(val.to_string()));
725
+ }
726
+ None
727
+ }
728
+ "typewritingclass/theme/shadows" => {
729
+ if let Some(val) = theme.resolve_shadow(member) {
730
+ return Some(Value::Str(val.to_string()));
731
+ }
732
+ None
733
+ }
734
+ "typewritingclass/theme/borders" => {
735
+ if let Some(val) = theme.resolve_radius(member) {
736
+ return Some(Value::Str(val.to_string()));
737
+ }
738
+ None
739
+ }
740
+ "typewritingclass/theme/sizes" => {
741
+ if let Some(val) = theme.resolve_size(member) {
742
+ return Some(Value::Str(val.to_string()));
743
+ }
744
+ None
745
+ }
746
+ "typewritingclass/theme/colors" => {
747
+ if let Some(val) = theme.resolve_named_color(member) {
748
+ return Some(Value::Str(val.to_string()));
749
+ }
750
+ None
751
+ }
752
+ _ => None,
753
+ }
754
+ }
755
+
756
+ /// Visit all expressions in a program (simple recursive walker)
757
+ fn visit_expressions<'a>(program: &'a Program, visitor: &mut dyn FnMut(&'a Expression<'a>)) {
758
+ for stmt in &program.body {
759
+ visit_statement(stmt, visitor);
760
+ }
761
+ }
762
+
763
+ fn visit_statement<'a>(stmt: &'a Statement<'a>, visitor: &mut dyn FnMut(&'a Expression<'a>)) {
764
+ match stmt {
765
+ Statement::ExpressionStatement(expr_stmt) => {
766
+ visit_expr(&expr_stmt.expression, visitor);
767
+ }
768
+ Statement::VariableDeclaration(var_decl) => {
769
+ for decl in &var_decl.declarations {
770
+ if let Some(init) = &decl.init {
771
+ visit_expr(init, visitor);
772
+ }
773
+ }
774
+ }
775
+ Statement::ReturnStatement(ret) => {
776
+ if let Some(arg) = &ret.argument {
777
+ visit_expr(arg, visitor);
778
+ }
779
+ }
780
+ Statement::IfStatement(if_stmt) => {
781
+ visit_expr(&if_stmt.test, visitor);
782
+ visit_statement(&if_stmt.consequent, visitor);
783
+ if let Some(alt) = &if_stmt.alternate {
784
+ visit_statement(alt, visitor);
785
+ }
786
+ }
787
+ Statement::BlockStatement(block) => {
788
+ for s in &block.body {
789
+ visit_statement(s, visitor);
790
+ }
791
+ }
792
+ Statement::ForStatement(f) => {
793
+ if let Some(body) = Some(&f.body) {
794
+ visit_statement(body, visitor);
795
+ }
796
+ }
797
+ Statement::ForInStatement(f) => {
798
+ visit_statement(&f.body, visitor);
799
+ }
800
+ Statement::ForOfStatement(f) => {
801
+ visit_statement(&f.body, visitor);
802
+ }
803
+ Statement::WhileStatement(w) => {
804
+ visit_expr(&w.test, visitor);
805
+ visit_statement(&w.body, visitor);
806
+ }
807
+ Statement::ExportDefaultDeclaration(def) => {
808
+ if let Some(expr) = def.declaration.as_expression() {
809
+ visit_expr(expr, visitor);
810
+ }
811
+ }
812
+ Statement::ExportNamedDeclaration(named) => {
813
+ if let Some(Declaration::VariableDeclaration(var_decl)) = &named.declaration {
814
+ for decl in &var_decl.declarations {
815
+ if let Some(init) = &decl.init {
816
+ visit_expr(init, visitor);
817
+ }
818
+ }
819
+ }
820
+ }
821
+ Statement::FunctionDeclaration(func) => {
822
+ if let Some(body) = &func.body {
823
+ for s in &body.statements {
824
+ visit_statement(s, visitor);
825
+ }
826
+ }
827
+ }
828
+ _ => {}
829
+ }
830
+ }
831
+
832
+ fn visit_expr<'a>(expr: &'a Expression<'a>, visitor: &mut dyn FnMut(&'a Expression<'a>)) {
833
+ visitor(expr);
834
+
835
+ match expr {
836
+ Expression::CallExpression(call) => {
837
+ visit_expr(&call.callee, visitor);
838
+ for arg in &call.arguments {
839
+ if let Some(e) = arg.as_expression() {
840
+ visit_expr(e, visitor);
841
+ }
842
+ }
843
+ }
844
+ Expression::AssignmentExpression(assign) => {
845
+ visit_expr(&assign.right, visitor);
846
+ }
847
+ Expression::BinaryExpression(bin) => {
848
+ visit_expr(&bin.left, visitor);
849
+ visit_expr(&bin.right, visitor);
850
+ }
851
+ Expression::ConditionalExpression(cond) => {
852
+ visit_expr(&cond.test, visitor);
853
+ visit_expr(&cond.consequent, visitor);
854
+ visit_expr(&cond.alternate, visitor);
855
+ }
856
+ Expression::SequenceExpression(seq) => {
857
+ for e in &seq.expressions {
858
+ visit_expr(e, visitor);
859
+ }
860
+ }
861
+ Expression::TemplateLiteral(tmpl) => {
862
+ for e in &tmpl.expressions {
863
+ visit_expr(e, visitor);
864
+ }
865
+ }
866
+ Expression::ArrayExpression(arr) => {
867
+ for elem in &arr.elements {
868
+ if let Some(e) = elem.as_expression() {
869
+ visit_expr(e, visitor);
870
+ }
871
+ }
872
+ }
873
+ Expression::ObjectExpression(obj) => {
874
+ for prop in &obj.properties {
875
+ if let ObjectPropertyKind::ObjectProperty(p) = prop {
876
+ visit_expr(&p.value, visitor);
877
+ }
878
+ }
879
+ }
880
+ Expression::ArrowFunctionExpression(arrow) => {
881
+ if arrow.expression {
882
+ if let Some(stmt) = arrow.body.statements.first() {
883
+ if let Statement::ExpressionStatement(es) = stmt {
884
+ visit_expr(&es.expression, visitor);
885
+ }
886
+ }
887
+ } else {
888
+ for s in &arrow.body.statements {
889
+ visit_statement(s, visitor);
890
+ }
891
+ }
892
+ }
893
+ Expression::ParenthesizedExpression(paren) => {
894
+ visit_expr(&paren.expression, visitor);
895
+ }
896
+ _ => {}
897
+ }
898
+ }