Skip to content

Commit 57b865a

Browse files
committed
Update
1 parent 4b805d7 commit 57b865a

File tree

2 files changed

+243
-137
lines changed

2 files changed

+243
-137
lines changed

libs/extractor/src/css_utils.rs

Lines changed: 199 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::collections::BTreeMap;
22

3+
use crate::utils::get_string_by_literal_expression;
34
use css::{
45
optimize_multi_css_value::{check_multi_css_optimize, optimize_mutli_css_value},
56
rm_css_comment::rm_css_comment,
@@ -76,126 +77,212 @@ pub fn css_to_style_literal<'a>(
7677
// Process each static style and check if it contains expression placeholders
7778
for style in static_styles {
7879
let value = style.value();
79-
let mut is_dynamic = false;
80-
let mut expr_idx = None;
8180

82-
// Check if this value contains a dynamic expression placeholder
81+
// Find all placeholders in this value
82+
let mut found_placeholders = Vec::new();
8383
for (placeholder, &idx) in expression_map.iter() {
8484
if value.contains(placeholder) {
85-
is_dynamic = true;
86-
expr_idx = Some(idx);
87-
break;
85+
found_placeholders.push((placeholder.clone(), idx));
8886
}
8987
}
9088

91-
if is_dynamic
92-
&& let Some(idx) = expr_idx
93-
&& idx < css.expressions.len()
94-
{
95-
// This is a dynamic style - the value comes from an expression
96-
let expr = &css.expressions[idx];
89+
if !found_placeholders.is_empty() {
90+
// Check if all expressions are literals that can be statically evaluated
9791

98-
// Check if expression is a function (arrow function or function expression)
99-
let is_function = matches!(
100-
expr,
101-
oxc_ast::ast::Expression::ArrowFunctionExpression(_)
102-
| oxc_ast::ast::Expression::FunctionExpression(_)
103-
);
92+
let mut all_literals = true;
93+
let mut literal_values = Vec::new();
10494

105-
let mut identifier = expression_to_code(expr);
106-
107-
// Normalize the code string
108-
// 1. Remove newlines and tabs, replace with spaces
109-
identifier = identifier.replace(['\n', '\t'], " ");
110-
// 2. Normalize multiple spaces to single space
111-
while identifier.contains(" ") {
112-
identifier = identifier.replace(" ", " ");
95+
for (_, idx) in &found_placeholders {
96+
if *idx < css.expressions.len() {
97+
if let Some(literal_value) =
98+
get_string_by_literal_expression(&css.expressions[*idx])
99+
{
100+
literal_values.push((*idx, literal_value));
101+
} else {
102+
all_literals = false;
103+
break;
104+
}
105+
} else {
106+
all_literals = false;
107+
break;
108+
}
113109
}
114-
// 3. Normalize arrow function whitespace
115-
identifier = identifier
116-
.replace(" => ", "=>")
117-
.replace(" =>", "=>")
118-
.replace("=> ", "=>");
119-
// 4. Normalize function expression formatting
120-
if is_function {
121-
// Normalize function() { } to function(){ }
122-
identifier = identifier.replace("function() {", "function(){");
123-
identifier = identifier.replace("function (", "function(");
124-
// Remove trailing semicolon and spaces before closing brace
125-
identifier = identifier.replace("; }", "}");
126-
identifier = identifier.replace(" }", "}");
127110

128-
// Wrap function in parentheses if not already wrapped
129-
// and add (rest) call
130-
let trimmed = identifier.trim();
131-
// Check if already wrapped in parentheses
132-
if !(trimmed.starts_with('(') && trimmed.ends_with(')')) {
133-
identifier = format!("({})", trimmed);
111+
if all_literals {
112+
// All expressions are literals - replace placeholders with literal values to create static style
113+
let mut static_value = value.to_string();
114+
for (placeholder, idx) in &found_placeholders {
115+
if let Some((_, literal_value)) = literal_values.iter().find(|(i, _)| i == idx)
116+
{
117+
static_value =
118+
static_value.replace(placeholder.as_str(), literal_value.as_str());
119+
}
134120
}
135-
// Add (rest) call
136-
identifier = format!("{}(rest)", identifier);
121+
// Create a new static style with the evaluated value
122+
styles.push(CssToStyleResult::Static(ExtractStaticStyle::new(
123+
style.property(),
124+
&static_value,
125+
style.level(),
126+
style.selector().cloned(),
127+
)));
128+
continue;
137129
}
138-
// 5. Normalize quotes
139-
if !is_function {
140-
// For non-function expressions, convert property access quotes
141-
// object["color"] -> object['color']
142-
identifier = identifier.replace("[\"", "['").replace("\"]", "']");
143-
} else {
144-
// For function expressions, convert string literals in ternary operators
145-
// This handles cases like: (props)=>props.b ? "a" : "b" -> (props)=>props.b ? 'a' : 'b'
146-
// Use simple pattern matching for ternary operator string literals
147-
// Pattern: ? "text" : "text" -> ? 'text' : 'text'
148-
// We'll replace " with ' but only in the context of ternary operators
149-
let mut result = String::new();
150-
let mut chars = identifier.chars().peekable();
151-
let mut in_ternary_string = false;
152130

153-
while let Some(ch) = chars.next() {
154-
if ch == '?' || ch == ':' {
155-
result.push(ch);
156-
// Skip whitespace
157-
while let Some(&' ') = chars.peek() {
158-
result.push(chars.next().unwrap());
159-
}
160-
// Check if next is a string literal
161-
if let Some(&'"') = chars.peek() {
162-
in_ternary_string = true;
163-
result.push('\'');
164-
chars.next(); // consume the "
165-
continue;
166-
}
167-
} else if in_ternary_string && ch == '"' {
168-
// Check if this is a closing quote by looking ahead
169-
let mut peeked = chars.clone();
170-
// Skip whitespace
171-
while let Some(&' ') = peeked.peek() {
172-
peeked.next();
173-
}
174-
// If next is : or ? or ) or } or end, it's a closing quote
175-
if peeked.peek().is_none()
176-
|| matches!(
177-
peeked.peek(),
178-
Some(&':') | Some(&'?') | Some(&')') | Some(&'}')
179-
)
180-
{
181-
result.push('\'');
182-
in_ternary_string = false;
183-
continue;
131+
// Not all expressions are literals - need to create dynamic style
132+
// Check if value is just a placeholder (no surrounding text)
133+
if found_placeholders.len() == 1 {
134+
let (placeholder, idx) = &found_placeholders[0];
135+
let trimmed_value = value.trim();
136+
if trimmed_value == placeholder.as_str() && *idx < css.expressions.len() {
137+
// Value is just the expression - use expression code directly
138+
let expr = &css.expressions[*idx];
139+
140+
// Check if expression is a function (arrow function or function expression)
141+
let is_function = matches!(
142+
expr,
143+
oxc_ast::ast::Expression::ArrowFunctionExpression(_)
144+
| oxc_ast::ast::Expression::FunctionExpression(_)
145+
);
146+
147+
let mut identifier = expression_to_code(expr);
148+
149+
// Normalize the code string
150+
// 1. Remove newlines and tabs, replace with spaces
151+
identifier = identifier.replace(['\n', '\t'], " ");
152+
// 2. Normalize multiple spaces to single space
153+
while identifier.contains(" ") {
154+
identifier = identifier.replace(" ", " ");
155+
}
156+
// 3. Normalize arrow function whitespace
157+
identifier = identifier
158+
.replace(" => ", "=>")
159+
.replace(" =>", "=>")
160+
.replace("=> ", "=>");
161+
// 4. Normalize function expression formatting
162+
if is_function {
163+
// Normalize function() { } to function(){ }
164+
identifier = identifier.replace("function() {", "function(){");
165+
identifier = identifier.replace("function (", "function(");
166+
// Remove trailing semicolon and spaces before closing brace
167+
identifier = identifier.replace("; }", "}");
168+
identifier = identifier.replace(" }", "}");
169+
170+
// Wrap function in parentheses if not already wrapped
171+
// and add (rest) call
172+
let trimmed = identifier.trim();
173+
// Check if already wrapped in parentheses
174+
if !(trimmed.starts_with('(') && trimmed.ends_with(')')) {
175+
identifier = format!("({})", trimmed);
184176
}
185-
// Not a closing quote, keep as is
186-
result.push(ch);
177+
// Add (rest) call
178+
identifier = format!("{}(rest)", identifier);
179+
}
180+
// 5. Normalize quotes
181+
if !is_function {
182+
// For non-function expressions, convert property access quotes
183+
// object["color"] -> object['color']
184+
identifier = identifier.replace("[\"", "['").replace("\"]", "']");
187185
} else {
188-
result.push(ch);
186+
// For function expressions, convert string literals in ternary operators
187+
// This handles cases like: (props)=>props.b ? "a" : "b" -> (props)=>props.b ? 'a' : 'b'
188+
// Use simple pattern matching for ternary operator string literals
189+
// Pattern: ? "text" : "text" -> ? 'text' : 'text'
190+
// We'll replace " with ' but only in the context of ternary operators
191+
let mut result = String::new();
192+
let mut chars = identifier.chars().peekable();
193+
let mut in_ternary_string = false;
194+
195+
while let Some(ch) = chars.next() {
196+
if ch == '?' || ch == ':' {
197+
result.push(ch);
198+
// Skip whitespace
199+
while let Some(&' ') = chars.peek() {
200+
result.push(chars.next().unwrap());
201+
}
202+
// Check if next is a string literal
203+
if let Some(&'"') = chars.peek() {
204+
in_ternary_string = true;
205+
result.push('\'');
206+
chars.next(); // consume the "
207+
continue;
208+
}
209+
} else if in_ternary_string && ch == '"' {
210+
// Check if this is a closing quote by looking ahead
211+
let mut peeked = chars.clone();
212+
// Skip whitespace
213+
while let Some(&' ') = peeked.peek() {
214+
peeked.next();
215+
}
216+
// If next is : or ? or ) or } or end, it's a closing quote
217+
if peeked.peek().is_none()
218+
|| matches!(
219+
peeked.peek(),
220+
Some(&':') | Some(&'?') | Some(&')') | Some(&'}')
221+
)
222+
{
223+
result.push('\'');
224+
in_ternary_string = false;
225+
continue;
226+
}
227+
// Not a closing quote, keep as is
228+
result.push(ch);
229+
} else {
230+
result.push(ch);
231+
}
232+
}
233+
identifier = result;
189234
}
235+
identifier = identifier.trim().to_string();
236+
237+
styles.push(CssToStyleResult::Dynamic(ExtractDynamicStyle::new(
238+
style.property(),
239+
style.level(),
240+
&identifier,
241+
style.selector().cloned(),
242+
)));
243+
continue;
190244
}
191-
identifier = result;
192245
}
193-
identifier = identifier.trim().to_string();
246+
247+
// Value has surrounding text - need to create template literal
248+
// Reconstruct the template literal by replacing placeholders with ${expr} syntax
249+
// The value contains placeholders like "__EXPR_0__px", we need to convert to `${expr}px`
250+
251+
let mut template_literal = value.to_string();
252+
253+
// Sort placeholders by their position in reverse order to avoid index shifting
254+
found_placeholders.sort_by(|(a_placeholder, _), (b_placeholder, _)| {
255+
template_literal
256+
.rfind(a_placeholder)
257+
.cmp(&template_literal.rfind(b_placeholder))
258+
});
259+
260+
// Replace each placeholder with the actual expression in template literal format
261+
for (placeholder, idx) in &found_placeholders {
262+
if *idx < css.expressions.len() {
263+
let expr = &css.expressions[*idx];
264+
let expr_code = expression_to_code(expr);
265+
// Normalize the expression code
266+
let mut normalized_code = expr_code.replace(['\n', '\t'], " ");
267+
while normalized_code.contains(" ") {
268+
normalized_code = normalized_code.replace(" ", " ");
269+
}
270+
normalized_code = normalized_code.trim().to_string();
271+
272+
// Replace placeholder with ${expr} syntax
273+
let expr_template = format!("${{{}}}", normalized_code);
274+
template_literal =
275+
template_literal.replace(placeholder.as_str(), &expr_template);
276+
}
277+
}
278+
279+
// Wrap in template literal backticks
280+
let final_identifier = format!("`{}`", template_literal);
194281

195282
styles.push(CssToStyleResult::Dynamic(ExtractDynamicStyle::new(
196283
style.property(),
197284
style.level(),
198-
&identifier,
285+
&final_identifier,
199286
style.selector().cloned(),
200287
)));
201288
continue;
@@ -238,9 +325,10 @@ pub fn css_to_style(
238325
.flat_map(|s| {
239326
let s = s.trim();
240327
if s.is_empty() {
241-
return None;
328+
None
329+
} else {
330+
Some(format!("@media{s}"))
242331
}
243-
Some(format!("@media{s}"))
244332
})
245333
.collect::<Vec<_>>();
246334
if media_inputs.len() > 1 {
@@ -772,6 +860,17 @@ mod tests {
772860
("transform", "rotate(90deg)", None),
773861
]
774862
)]
863+
#[case("`width: ${1}px;`", vec![("width", "1px", None)])]
864+
#[case("`width: ${\"1\"}px;`", vec![("width", "1px", None)])]
865+
#[case("`width: ${'1'}px;`", vec![("width", "1px", None)])]
866+
#[case("`width: ${`1`}px;`", vec![("width", "1px", None)])]
867+
#[case("`width: ${\"1px\"};`", vec![("width", "1px", None)])]
868+
#[case("`width: ${'1px'};`", vec![("width", "1px", None)])]
869+
#[case("`width: ${`1px`};`", vec![("width", "1px", None)])]
870+
#[case("`width: ${1 + 1}px;`", vec![("width", "`${1 + 1}px`", None)])]
871+
#[case("`width: ${func(1)}px;`", vec![("width", "`${func(1)}px`", None)])]
872+
#[case("`width: ${func(1)}${2}px;`", vec![("width", "`${func(1)}${2}px`", None)])]
873+
#[case("`width: ${1}${2}px;`", vec![("width", "12px", None)])]
775874
fn test_css_to_style_literal(
776875
#[case] input: &str,
777876
#[case] expected: Vec<(&str, &str, Option<StyleSelector>)>,

0 commit comments

Comments
 (0)