|
1 | 1 | use std::collections::BTreeMap; |
2 | 2 |
|
| 3 | +use crate::utils::get_string_by_literal_expression; |
3 | 4 | use css::{ |
4 | 5 | optimize_multi_css_value::{check_multi_css_optimize, optimize_mutli_css_value}, |
5 | 6 | rm_css_comment::rm_css_comment, |
@@ -76,126 +77,212 @@ pub fn css_to_style_literal<'a>( |
76 | 77 | // Process each static style and check if it contains expression placeholders |
77 | 78 | for style in static_styles { |
78 | 79 | let value = style.value(); |
79 | | - let mut is_dynamic = false; |
80 | | - let mut expr_idx = None; |
81 | 80 |
|
82 | | - // Check if this value contains a dynamic expression placeholder |
| 81 | + // Find all placeholders in this value |
| 82 | + let mut found_placeholders = Vec::new(); |
83 | 83 | for (placeholder, &idx) in expression_map.iter() { |
84 | 84 | if value.contains(placeholder) { |
85 | | - is_dynamic = true; |
86 | | - expr_idx = Some(idx); |
87 | | - break; |
| 85 | + found_placeholders.push((placeholder.clone(), idx)); |
88 | 86 | } |
89 | 87 | } |
90 | 88 |
|
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 |
97 | 91 |
|
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(); |
104 | 94 |
|
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 | + } |
113 | 109 | } |
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(" }", "}"); |
127 | 110 |
|
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 | + } |
134 | 120 | } |
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; |
137 | 129 | } |
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; |
152 | 130 |
|
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); |
184 | 176 | } |
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("\"]", "']"); |
187 | 185 | } 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; |
189 | 234 | } |
| 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; |
190 | 244 | } |
191 | | - identifier = result; |
192 | 245 | } |
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); |
194 | 281 |
|
195 | 282 | styles.push(CssToStyleResult::Dynamic(ExtractDynamicStyle::new( |
196 | 283 | style.property(), |
197 | 284 | style.level(), |
198 | | - &identifier, |
| 285 | + &final_identifier, |
199 | 286 | style.selector().cloned(), |
200 | 287 | ))); |
201 | 288 | continue; |
@@ -238,9 +325,10 @@ pub fn css_to_style( |
238 | 325 | .flat_map(|s| { |
239 | 326 | let s = s.trim(); |
240 | 327 | if s.is_empty() { |
241 | | - return None; |
| 328 | + None |
| 329 | + } else { |
| 330 | + Some(format!("@media{s}")) |
242 | 331 | } |
243 | | - Some(format!("@media{s}")) |
244 | 332 | }) |
245 | 333 | .collect::<Vec<_>>(); |
246 | 334 | if media_inputs.len() > 1 { |
@@ -772,6 +860,17 @@ mod tests { |
772 | 860 | ("transform", "rotate(90deg)", None), |
773 | 861 | ] |
774 | 862 | )] |
| 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)])] |
775 | 874 | fn test_css_to_style_literal( |
776 | 875 | #[case] input: &str, |
777 | 876 | #[case] expected: Vec<(&str, &str, Option<StyleSelector>)>, |
|
0 commit comments