@@ -84,7 +84,7 @@ class CSS2PHPConverter:
8484 print (f"Warning: Failed to fetch { source } : { e } " )
8585 return None
8686
87- def _parse_css (self , css_content : str ) -> tuple [dict , ProcessingStats ]:
87+ def _parse_css (self , css_content : str , split_selectors : bool = False ) -> tuple [dict , ProcessingStats ]: # split_selectors parameter added
8888 """Parse CSS content and return class map with statistics"""
8989 start_time = time .time ()
9090
@@ -96,8 +96,8 @@ class_map = defaultdict(lambda: {'default': {}, 'media': {}})
9696 output_size = 0 ,
9797 parsing_time = 0 ,
9898 class_count = 0 ,
99- media_queries = 0 ,
100- pseudo_classes = 0 ,
99+ media_queries = 0 ,
100+ pseudo_classes = 0 ,
101101 file_path = "" ,
102102 syntax_valid = False # Initialized as False, will be updated after validation
103103 )
@@ -107,27 +107,54 @@ class_count=0,
107107 # Handle normal style rules
108108 selectors = [s .strip () for s in rule .selectorText .split (',' )]
109109 for selector in selectors :
110- if selector .startswith ('.' ):
111- class_name = selector .strip ('.' )
112- if ':' in class_name :
113- base_class , pseudo = class_name .split (':' , 1 )
114- class_name = f"{ base_class } :{ pseudo } "
115- stats .pseudo_classes += 1
116-
117- properties = {
118- prop .name : prop .value
119- for prop in rule .style
120- if prop .name and prop .value
121- }
122-
123- if media_query :
124- if class_name not in class_map :
125- class_map [class_name ] = {'default' : {}, 'media' : {}} # Ensure class entry exists
126- class_map [class_name ]['media' ][media_query ] = properties
127- stats .media_queries += 1
128- else :
129- class_map [class_name ]['default' ].update (properties )
130- stats .class_count += 1
110+ if split_selectors : # Split selectors logic
111+ individual_selectors = [s .strip () for s in selector .split (' ' )] # Split by space
112+ for individual_selector in individual_selectors :
113+ if individual_selector .startswith ('.' ):
114+ class_name = individual_selector .strip ('.' )
115+ if ':' in class_name :
116+ base_class , pseudo = class_name .split (':' , 1 )
117+ class_name = f"{ base_class } :{ pseudo } "
118+ stats .pseudo_classes += 1
119+
120+ properties = {
121+ prop .name : prop .value
122+ for prop in rule .style
123+ if prop .name and prop .value
124+ }
125+
126+ if media_query :
127+ if class_name not in class_map :
128+ class_map [class_name ] = {'default' : {}, 'media' : {}} # Ensure class entry exists
129+ class_map [class_name ]['media' ][media_query ] = properties
130+ stats .media_queries += 1
131+ else :
132+ class_map [class_name ]['default' ].update (properties )
133+ stats .class_count += 1
134+
135+ else : # Original logic - keep combined selectors
136+ if selector .startswith ('.' ):
137+ class_name = selector .strip ('.' )
138+ if ':' in class_name :
139+ base_class , pseudo = class_name .split (':' , 1 )
140+ class_name = f"{ base_class } :{ pseudo } "
141+ stats .pseudo_classes += 1
142+
143+ properties = {
144+ prop .name : prop .value
145+ for prop in rule .style
146+ if prop .name and prop .value
147+ }
148+
149+ if media_query :
150+ if class_name not in class_map :
151+ class_map [class_name ] = {'default' : {}, 'media' : {}} # Ensure class entry exists
152+ class_map [class_name ]['media' ][media_query ] = properties
153+ stats .media_queries += 1
154+ else :
155+ class_map [class_name ]['default' ].update (properties )
156+ stats .class_count += 1
157+
131158
132159 elif isinstance (rule , cssutils .css .CSSMediaRule ):
133160 media_query_text = rule .media .mediaText
@@ -141,7 +168,7 @@ class_map[class_name]['default'].update(properties)
141168 stats .parsing_time = time .time ()
142169 return dict (class_map ), stats
143170
144- def _generate_php (self , class_map : dict , source : str , stats : ProcessingStats ) -> str :
171+ def _generate_php (self , class_map : dict , source : str , stats : ProcessingStats , split_selectors : bool = False ) -> str :
145172 """Generate PHP array code from class map with comprehensive header"""
146173 # Calculate additional stats
147174 compression_ratio = (stats .output_size / stats .input_size * 100 ) if stats .input_size > 0 else 0
@@ -177,13 +204,16 @@ class_map[class_name]['default'].update(properties)
177204 * Skip errors: { 'Yes' if self .skip_errors else 'No' }
178205 * Timeout: { self .timeout } s
179206 * Overwrite enabled: { 'Yes' if self .overwrite else 'No' }
207+ * Split Selectors: { 'Yes' if split_selectors else 'No' }
180208 *
181209 * File Structure:
182210 * ------------------------------------------
183- * - Array keys are CSS class names
184- * - Each class has 'default' and optional 'media' properties
185- * - Media queries are nested under 'media' key
186- * - All properties are sorted alphabetically
211+ * - Array keys are CSS class selectors.
212+ * - { 'Combined selectors are kept as single keys' if not split_selectors else 'Combined selectors are split into individual keys.' }
213+ * - Each key represents a CSS selector and its associated styles.
214+ * - Each class selector has 'default' and optional 'media' properties.
215+ * - Media queries are nested under 'media' key.
216+ * - All properties are sorted alphabetically.
187217 * ==========================================
188218 */
189219
@@ -198,7 +228,7 @@ class_map[class_name]['default'].update(properties)
198228 clean_class = re .sub (r'\\([\.:/\-])' , r'\1' , clean_class )
199229 # Only escape single quotes for PHP
200230 escaped_class = clean_class .replace ("'" , "\\ '" )
201-
231+
202232 php_code += f" '{ escaped_class } ' => [\n "
203233
204234 if rules ['default' ]:
@@ -348,7 +378,7 @@ class_map[class_name]['default'].update(properties)
348378 ]
349379 return "\n " .join (report )
350380
351- def convert (self , source : str , output_path : str = None , name_prefix : str = '' ) -> Optional [ProcessingStats ]:
381+ def convert (self , source : str , output_path : str = None , name_prefix : str = '' , split_selectors : bool = False ) -> Optional [ProcessingStats ]: # split_selectors parameter added
352382 """Convert CSS source to PHP array"""
353383 try :
354384 start_time = time .time ()
@@ -372,8 +402,8 @@ class_map[class_name]['default'].update(properties)
372402 output_path .parent .mkdir (parents = True , exist_ok = True )
373403
374404 # Process CSS and generate PHP
375- class_map , stats = self ._parse_css (css_content )
376- php_code = self ._generate_php (class_map , source , stats )
405+ class_map , stats = self ._parse_css (css_content , split_selectors = split_selectors ) # Pass split_selectors to _parse_css
406+ php_code = self ._generate_php (class_map , source , stats , split_selectors = split_selectors )
377407
378408 # Validate and try to fix PHP syntax
379409 is_valid , error_msg , fixed_code = self ._validate_php_syntax (php_code )
@@ -468,7 +498,7 @@ class_map, stats = self._parse_css(css_content)
468498 print (f"📊 Summary:" )
469499 print (f" • Files processed: { len (processed_files )} " )
470500 print (f" • Unique classes: { len (merged_arrays )} " )
471- print (f" • Duplicates skipped : { len (duplicates )} " ) # Still report duplicates (though now merged)
501+ print (f" • Duplicates found : { len (duplicates )} " ) # Still report duplicates (though now merged)
472502 print (f" • Output file: { output_file } " )
473503 print (f" • PHP Syntax: { '✅ Valid' if is_valid else '❌ Invalid' } " )
474504
@@ -499,6 +529,16 @@ class_map, stats = self._parse_css(css_content)
499529 * Duplicate Information (Properties Merged):
500530 * ------------------------------------------
501531{ format_duplicate_info (duplicates )}
532+ * ==========================================
533+ *
534+ * File Structure:
535+ * ------------------------------------------
536+ * - Array keys are CSS class selectors.
537+ * - Combined selectors are kept as single keys.
538+ * - Each key represents a CSS selector and its associated styles.
539+ * - Each class selector has 'default' and optional 'media' properties.
540+ * - Media queries are nested under 'media' key.
541+ * - All properties are sorted alphabetically.
502542 * ==========================================
503543 */
504544
@@ -608,32 +648,32 @@ class_map, stats = self._parse_css(css_content)
608648 lines = content .split ('\n ' )
609649 indent_level = 0
610650 formatted_lines = []
611-
651+
612652 for line in lines :
613653 line = line .strip ()
614654 if not line :
615655 continue
616-
656+
617657 # Adjust indentation based on brackets
618658 indent_level += line .count ('[' ) - line .count (']' )
619659 spaces = ' ' * indent_level
620660 formatted_lines .append (spaces + line )
621-
661+
622662 if ']' in line :
623663 indent_level = max (0 , indent_level - line .count (']' ))
624-
664+
625665 return '\n ' .join (formatted_lines )
626666
627667def format_duplicate_info (duplicates : Dict [str , List [str ]]) -> str :
628668 """Format duplicate information for header comment"""
629669 if not duplicates :
630670 return " * No duplicates found"
631-
671+
632672 lines = []
633673 for key , files in sorted (duplicates .items ()):
634674 files_str = ', ' .join (files )
635675 lines .append (f" * '{ key } ' found in: { files_str } " )
636-
676+
637677 return '\n ' .join (lines )
638678
639679def main ():
@@ -655,7 +695,7 @@ class_map, stats = self._parse_css(css_content)
655695""" ,
656696 formatter_class = argparse .RawDescriptionHelpFormatter
657697 )
658-
698+
659699 # Keep existing arguments
660700 parser .add_argument ('sources' , nargs = '*' , help = "CSS file paths or URLs" )
661701 parser .add_argument ('-o' , '--output' , help = "Output directory path" )
@@ -671,6 +711,7 @@ class_map, stats = self._parse_css(css_content)
671711 parser .add_argument ('-mn' , '--merge-name' , default = 'main' , help = "Name prefix for merged file (default: main)" )
672712 parser .add_argument ('-v' , '--version' , action = 'version' , version = f'%(prog)s { __version__ } \n Created by { __author__ } \n { __repo__ } ' )
673713 parser .add_argument ('-i' , '--info' , action = 'store_true' , help = "Show detailed info" )
714+ parser .add_argument ('--split-selectors' , action = 'store_true' , help = "Split combined CSS selectors into individual keys in PHP array (may lose specificity)" ) # Added split-selectors argument
674715 args = parser .parse_args ()
675716
676717 # Handle merge operation if requested
@@ -687,7 +728,7 @@ class_map, stats = self._parse_css(css_content)
687728 if not args .sources :
688729 parser .print_help ()
689730 return
690-
731+
691732 converter = CSS2PHPConverter (
692733 timeout = args .timeout ,
693734 skip_errors = args .skip_errors ,
@@ -703,7 +744,7 @@ class_map, stats = self._parse_css(css_content)
703744 for source in args .sources :
704745 print (f"\n Processing: { source } " )
705746 try :
706- stats = converter .convert (source , args .output , args .name_prefix )
747+ stats = converter .convert (source , args .output , args .name_prefix , split_selectors = args . split_selectors ) # Pass split_selectors
707748 if stats :
708749 results .append (stats )
709750 if stats .syntax_valid :
@@ -724,7 +765,8 @@ class_map, stats = self._parse_css(css_content)
724765 'Processing time' : f"{ stats .parsing_time :.3f} s" ,
725766 'Pseudo-classes' : stats .pseudo_classes ,
726767 'Syntax validation' : '✅ Valid' if stats .syntax_valid else '❌ Invalid' ,
727- 'Syntax error details' : stats .syntax_error_message # Show error details in info
768+ 'Syntax error details' : stats .syntax_error_message , # Show error details in info
769+ 'Split Selectors' : 'Yes' if args .split_selectors else 'No' # Show Split Selectors info in stats
728770 }
729771 for key , value in sorted (info .items ()):
730772 print (f" • { key } : { value } " )
@@ -754,7 +796,8 @@ class_map, stats = self._parse_css(css_content)
754796 'Total media queries' : sum (s .media_queries for s in results ),
755797 'Total output size' : f"{ total_output :,} bytes" ,
756798 'Total processing time' : f"{ total_time :.2f} s" ,
757- 'Total syntax errors' : sum (1 for s in results if not s .syntax_valid ) # Count total syntax errors
799+ 'Total syntax errors' : sum (1 for s in results if not s .syntax_valid ), # Count total syntax errors
800+ 'Split Selectors' : 'Yes' if args .split_selectors else 'No' # Show Split Selectors info in summary
758801 }
759802
760803 for key , value in sorted (summary .items ()):
0 commit comments