Skip to content

Commit 09fb141

Browse files
committed
Implement
1 parent 2b8d1c8 commit 09fb141

File tree

3 files changed

+346
-63
lines changed

3 files changed

+346
-63
lines changed

libs/extractor/src/css_utils.rs

Lines changed: 255 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,211 @@ use css::{
55
rm_css_comment::rm_css_comment,
66
style_selector::StyleSelector,
77
};
8+
use oxc_ast::ast::TemplateLiteral;
89

9-
use crate::extract_style::extract_static_style::ExtractStaticStyle;
10+
use crate::extract_style::{
11+
extract_dynamic_style::ExtractDynamicStyle, extract_static_style::ExtractStaticStyle,
12+
};
13+
14+
pub enum CssToStyleResult {
15+
Static(ExtractStaticStyle),
16+
Dynamic(ExtractDynamicStyle),
17+
}
18+
19+
pub fn css_to_style_literal<'a>(css: &TemplateLiteral<'a>) -> Vec<CssToStyleResult> {
20+
use crate::utils::expression_to_code;
21+
22+
let mut styles = vec![];
23+
24+
// If there are no expressions, just process quasis as static CSS
25+
if css.expressions.is_empty() {
26+
for quasi in css.quasis.iter() {
27+
styles.extend(
28+
css_to_style(&quasi.value.raw, 0, &None)
29+
.into_iter()
30+
.map(|ex| CssToStyleResult::Static(ex)),
31+
);
32+
}
33+
return styles;
34+
}
35+
36+
// Process template literal with expressions
37+
// Template literal format: `text ${expr1} text ${expr2} text`
38+
// We need to parse CSS and identify where expressions are used
39+
40+
// Build a combined CSS string with unique placeholders for expressions
41+
// Use a format that won't conflict with actual CSS values
42+
let mut css_parts = Vec::new();
43+
let mut expression_map = std::collections::HashMap::new();
44+
45+
for (i, quasi) in css.quasis.iter().enumerate() {
46+
css_parts.push(quasi.value.raw.to_string());
47+
48+
// Add expression placeholder if not the last quasi
49+
if i < css.expressions.len() {
50+
// Use a unique placeholder format that CSS parser won't modify
51+
let placeholder = format!("__EXPR_{}__", i);
52+
expression_map.insert(placeholder.clone(), i);
53+
css_parts.push(placeholder);
54+
}
55+
}
56+
57+
let combined_css = css_parts.join("");
58+
59+
// Parse CSS to extract static styles
60+
let static_styles = css_to_style(&combined_css, 0, &None);
61+
62+
// Process each static style and check if it contains expression placeholders
63+
for style in static_styles {
64+
let value = style.value();
65+
let mut is_dynamic = false;
66+
let mut expr_idx = None;
67+
68+
// Check if this value contains a dynamic expression placeholder
69+
for (placeholder, &idx) in expression_map.iter() {
70+
if value.contains(placeholder) {
71+
is_dynamic = true;
72+
expr_idx = Some(idx);
73+
break;
74+
}
75+
}
76+
77+
if is_dynamic {
78+
if let Some(idx) = expr_idx {
79+
if idx < css.expressions.len() {
80+
// This is a dynamic style - the value comes from an expression
81+
let expr = &css.expressions[idx];
82+
83+
// Check if expression is a function (arrow function or function expression)
84+
let is_function = matches!(
85+
expr,
86+
oxc_ast::ast::Expression::ArrowFunctionExpression(_)
87+
| oxc_ast::ast::Expression::FunctionExpression(_)
88+
);
89+
90+
let mut identifier = expression_to_code(expr);
91+
92+
// Normalize the code string
93+
// 1. Remove newlines and tabs, replace with spaces
94+
identifier = identifier.replace('\n', " ").replace('\t', " ");
95+
// 2. Normalize multiple spaces to single space
96+
while identifier.contains(" ") {
97+
identifier = identifier.replace(" ", " ");
98+
}
99+
// 3. Normalize arrow function whitespace
100+
identifier = identifier
101+
.replace(" => ", "=>")
102+
.replace(" =>", "=>")
103+
.replace("=> ", "=>");
104+
// 4. Normalize function expression formatting
105+
if is_function {
106+
// Normalize function() { } to function(){ }
107+
identifier = identifier.replace("function() {", "function(){");
108+
identifier = identifier.replace("function (", "function(");
109+
// Remove trailing semicolon and spaces before closing brace
110+
identifier = identifier.replace("; }", "}");
111+
identifier = identifier.replace(" }", "}");
112+
113+
// Wrap function in parentheses if not already wrapped
114+
// and add (rest) call
115+
let trimmed = identifier.trim();
116+
// Check if already wrapped in parentheses
117+
if !(trimmed.starts_with('(') && trimmed.ends_with(')')) {
118+
identifier = format!("({})", trimmed);
119+
}
120+
// Add (rest) call
121+
identifier = format!("{}(rest)", identifier);
122+
}
123+
// 5. Normalize quotes
124+
if !is_function {
125+
// For non-function expressions, convert property access quotes
126+
// object["color"] -> object['color']
127+
identifier = identifier.replace("[\"", "['").replace("\"]", "']");
128+
} else {
129+
// For function expressions, convert string literals in ternary operators
130+
// This handles cases like: (props)=>props.b ? "a" : "b" -> (props)=>props.b ? 'a' : 'b'
131+
// Use simple pattern matching for ternary operator string literals
132+
// Pattern: ? "text" : "text" -> ? 'text' : 'text'
133+
// We'll replace " with ' but only in the context of ternary operators
134+
let mut result = String::new();
135+
let mut chars = identifier.chars().peekable();
136+
let mut in_ternary_string = false;
137+
138+
while let Some(ch) = chars.next() {
139+
if ch == '?' || ch == ':' {
140+
result.push(ch);
141+
// Skip whitespace
142+
while let Some(&' ') = chars.peek() {
143+
result.push(chars.next().unwrap());
144+
}
145+
// Check if next is a string literal
146+
if let Some(&'"') = chars.peek() {
147+
in_ternary_string = true;
148+
result.push('\'');
149+
chars.next(); // consume the "
150+
continue;
151+
}
152+
} else if in_ternary_string && ch == '"' {
153+
// Check if this is a closing quote by looking ahead
154+
let mut peeked = chars.clone();
155+
// Skip whitespace
156+
while let Some(&' ') = peeked.peek() {
157+
peeked.next();
158+
}
159+
// If next is : or ? or ) or } or end, it's a closing quote
160+
if peeked.peek().is_none()
161+
|| matches!(
162+
peeked.peek(),
163+
Some(&':') | Some(&'?') | Some(&')') | Some(&'}')
164+
)
165+
{
166+
result.push('\'');
167+
in_ternary_string = false;
168+
continue;
169+
}
170+
// Not a closing quote, keep as is
171+
result.push(ch);
172+
} else {
173+
result.push(ch);
174+
}
175+
}
176+
identifier = result;
177+
}
178+
identifier = identifier.trim().to_string();
179+
180+
styles.push(CssToStyleResult::Dynamic(ExtractDynamicStyle::new(
181+
style.property(),
182+
style.level(),
183+
&identifier,
184+
style.selector().cloned(),
185+
)));
186+
continue;
187+
}
188+
}
189+
}
190+
191+
// Check if property name contains a dynamic expression placeholder
192+
let property = style.property();
193+
let mut prop_is_dynamic = false;
194+
195+
for placeholder in expression_map.keys() {
196+
if property.contains(placeholder) {
197+
prop_is_dynamic = true;
198+
break;
199+
}
200+
}
201+
202+
if prop_is_dynamic {
203+
// Property name is dynamic - skip for now as it's more complex
204+
continue;
205+
}
206+
207+
// Static style
208+
styles.push(CssToStyleResult::Static(style));
209+
}
210+
211+
styles
212+
}
10213

11214
pub fn css_to_style(
12215
css: &str,
@@ -182,8 +385,59 @@ pub fn optimize_css_block(css: &str) -> String {
182385
mod tests {
183386
use super::*;
184387

388+
use oxc_allocator::Allocator;
389+
use oxc_ast::ast::{Expression, Statement};
390+
use oxc_parser::Parser;
391+
use oxc_span::SourceType;
185392
use rstest::rstest;
186393

394+
#[rstest]
395+
#[case("`background-color: red;`", vec![("background-color", "red", None)])]
396+
#[case("`background-color: ${color};`", vec![("background-color", "color", None)])]
397+
#[case("`background-color: ${color}`", vec![("background-color", "color", None)])]
398+
#[case("`background-color: ${color};color: blue;`", vec![("background-color", "color", None), ("color", "blue", None)])]
399+
#[case("`background-color: ${()=>\"arrow dynamic\"}`", vec![("background-color", "(()=>\"arrow dynamic\")(rest)", None)])]
400+
#[case("`background-color: ${()=>\"arrow dynamic\"};color: blue;`", vec![("background-color", "(()=>\"arrow dynamic\")(rest)", None), ("color", "blue", None)])]
401+
#[case("`color: blue;background-color: ${()=>\"arrow dynamic\"};`", vec![("color", "blue", None),("background-color", "(()=>\"arrow dynamic\")(rest)", None)])]
402+
#[case("`background-color: ${function(){ return \"arrow dynamic\"}}`", vec![("background-color", "(function(){ return \"arrow dynamic\"})(rest)", None)])]
403+
#[case("`background-color: ${object.color}`", vec![("background-color", "object.color", None)])]
404+
#[case("`background-color: ${object['color']}`", vec![("background-color", "object['color']", None)])]
405+
#[case("`background-color: ${func()}`", vec![("background-color", "func()", None)])]
406+
#[case("`background-color: ${(props)=>props.b ? 'a' : 'b'}`", vec![("background-color", "((props)=>props.b ? 'a' : 'b')(rest)", None)])]
407+
#[case("`background-color: ${(props)=>props.b ? null : undefined}`", vec![("background-color", "((props)=>props.b ? null : undefined)(rest)", None)])]
408+
fn test_css_to_style_literal(
409+
#[case] input: &str,
410+
#[case] expected: Vec<(&str, &str, Option<StyleSelector>)>,
411+
) {
412+
// parse template literal code
413+
let allocator = Allocator::default();
414+
let css = Parser::new(&allocator, input, SourceType::ts()).parse();
415+
if let Statement::ExpressionStatement(expr) = &css.program.body[0]
416+
&& let Expression::TemplateLiteral(tmp) = &expr.expression
417+
{
418+
let styles = css_to_style_literal(tmp);
419+
let mut result: Vec<(&str, &str, Option<StyleSelector>)> = styles
420+
.iter()
421+
.map(|prop| match prop {
422+
CssToStyleResult::Static(style) => {
423+
(style.property(), style.value(), style.selector().cloned())
424+
}
425+
CssToStyleResult::Dynamic(dynamic) => (
426+
dynamic.property(),
427+
dynamic.identifier(),
428+
dynamic.selector().cloned(),
429+
),
430+
})
431+
.collect();
432+
result.sort();
433+
let mut expected_sorted = expected.clone();
434+
expected_sorted.sort();
435+
assert_eq!(result, expected_sorted);
436+
} else {
437+
panic!("not a template literal");
438+
}
439+
}
440+
187441
#[rstest]
188442
#[case(
189443
"div{

libs/extractor/src/extractor/extract_style_from_styled.rs

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,6 @@ pub fn extract_style_from_styled<'a>(
7777
split_filename: Option<&str>,
7878
imports: &HashMap<String, ExportVariableKind>,
7979
) -> (ExtractResult<'a>, Expression<'a>) {
80-
println!("-----------");
8180
let (result, new_expr) = match expression {
8281
// Case 1: styled.div`css` or styled("div")`css`
8382
Expression::TaggedTemplateExpression(tag) => {
@@ -101,11 +100,8 @@ pub fn extract_style_from_styled<'a>(
101100
.collect();
102101

103102
if let Some(default_class_name) = default_class_name {
104-
props_styles.extend(
105-
default_class_name
106-
.into_iter()
107-
.map(ExtractStyleProp::Static),
108-
);
103+
props_styles
104+
.extend(default_class_name.into_iter().map(ExtractStyleProp::Static));
109105
}
110106

111107
let class_name =
@@ -135,11 +131,6 @@ pub fn extract_style_from_styled<'a>(
135131
let (tag_name, default_class_name) =
136132
extract_base_tag_and_class_name(&call.callee, styled_name, imports);
137133

138-
println!(
139-
"tag_name: {:?}, default_class_name: {:?}",
140-
tag_name, default_class_name
141-
);
142-
143134
if let Some(tag_name) = tag_name
144135
&& call.arguments.len() == 1
145136
{
@@ -162,11 +153,7 @@ pub fn extract_style_from_styled<'a>(
162153
&None,
163154
);
164155
if let Some(default_class_name) = default_class_name {
165-
styles.extend(
166-
default_class_name
167-
.into_iter()
168-
.map(ExtractStyleProp::Static),
169-
);
156+
styles.extend(default_class_name.into_iter().map(ExtractStyleProp::Static));
170157
}
171158

172159
let class_name =

0 commit comments

Comments
 (0)