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.
- package/README.md +62 -0
- package/package.json +34 -0
- package/src/css.rs +32 -0
- package/src/extractor.rs +898 -0
- package/src/hash.rs +103 -0
- package/src/index.ts +262 -0
- package/src/lib.rs +143 -0
- package/src/modifiers.rs +50 -0
- package/src/style_rule.rs +72 -0
- package/src/theme.rs +86 -0
- package/src/utilities.rs +381 -0
package/src/extractor.rs
ADDED
|
@@ -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
|
+
}
|