@@ -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
11214pub fn css_to_style (
12215 css : & str ,
@@ -182,8 +385,59 @@ pub fn optimize_css_block(css: &str) -> String {
182385mod 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{
0 commit comments