@@ -65,26 +65,41 @@ pub fn shim_css_text(css: &str, content_attr: &str, host_attr: &str) -> String {
6565
6666 let mut result = result;
6767
68- // Step 1: Handle :host-context() - must be done before :host
68+ // Step 1: Process polyfill directives (polyfill-next-selector, polyfill-rule)
69+ // These convert ShadowDOM rules to work with the CSS shim
70+ result = insert_polyfill_directives ( & result) ;
71+ result = insert_polyfill_rules ( & result) ;
72+
73+ // Step 2: Extract unscoped rules (polyfill-unscoped-rule)
74+ // These rules are added at the end WITHOUT scoping
75+ let unscoped_rules = extract_unscoped_rules ( & result) ;
76+ result = remove_unscoped_rules ( & result) ;
77+
78+ // Step 3: Handle :host-context() - must be done before :host
6979 result = convert_colon_host_context ( & result, content_attr, host_attr) ;
7080
71- // Step 2 : Handle :host selectors
81+ // Step 4 : Handle :host selectors
7282 result = convert_colon_host ( & result, host_attr) ;
7383
74- // Step 3 : Handle ::ng-deep and other shadow DOM selectors
84+ // Step 5 : Handle ::ng-deep and other shadow DOM selectors
7585 result = convert_shadow_dom_selectors ( & result) ;
7686
77- // Step 4 : Scope keyframes and animation properties
87+ // Step 6 : Scope keyframes and animation properties
7888 if !content_attr. is_empty ( ) {
7989 result = scope_keyframes_related_css ( & result, content_attr) ;
8090 }
8191
82- // Step 5 : Scope all selectors with the content attribute
92+ // Step 7 : Scope all selectors with the content attribute
8393 if !content_attr. is_empty ( ) {
8494 result = scope_selectors ( & result, content_attr, host_attr) ;
8595 }
8696
87- // Step 6: Restore comments
97+ // Step 8: Append unscoped rules (without scoping)
98+ if !unscoped_rules. is_empty ( ) {
99+ result = result + "\n " + unscoped_rules. trim ( ) ;
100+ }
101+
102+ // Step 9: Restore comments
88103 restore_comments ( & result, & comments)
89104}
90105
@@ -430,11 +445,16 @@ fn convert_colon_host_context(css: &str, content_attr: &str, host_attr: &str) ->
430445/// `:host` becomes `[host_attr]`
431446/// `:host(.active)` becomes `.active[host_attr]`
432447/// `:host[attr]` becomes `[attr][host_attr]`
448+ /// `:host.class` becomes `.class[host_attr]`
449+ /// `:host#id` becomes `#id[host_attr]`
433450fn convert_colon_host ( css : & str , host_attr : & str ) -> String {
434- // Pattern: :host followed by optional (selector)
451+ // Pattern: :host followed by (selector) in parentheses
435452 let host_with_selector_re: & Regex = regex ! ( r":host\(([^)]+)\)" ) ;
436453 // Pattern: :host followed by attribute selector directly (no space)
437454 let host_with_attr_re: & Regex = regex ! ( r":host(\[[^\]]+\])" ) ;
455+ // Pattern: :host followed by class, id, or pseudo selector directly attached
456+ // Matches: :host.class, :host#id, :host:hover, :host.class:before, etc.
457+ let host_with_class_re: & Regex = regex ! ( r":host([.#:][^{\s,]+)" ) ;
438458 let host_plain_re: & Regex = regex ! ( r":host\b" ) ;
439459
440460 let attr = if host_attr. is_empty ( ) { String :: new ( ) } else { format ! ( "[{}]" , host_attr) } ;
@@ -455,7 +475,15 @@ fn convert_colon_host(css: &str, host_attr: &str) -> String {
455475 } )
456476 . to_string ( ) ;
457477
458- // Then handle plain :host
478+ // Then handle :host.class, :host#id, :host:pseudo directly attached
479+ let result = host_with_class_re
480+ . replace_all ( & result, |caps : & regex:: Captures | {
481+ let attached_selector = caps. get ( 1 ) . map_or ( "" , |m| m. as_str ( ) ) ;
482+ format ! ( "{}{}" , attached_selector, attr)
483+ } )
484+ . to_string ( ) ;
485+
486+ // Finally handle plain :host
459487 host_plain_re. replace_all ( & result, attr. as_str ( ) ) . to_string ( )
460488}
461489
@@ -985,6 +1013,130 @@ fn is_keyframe_selector(selector: &str) -> bool {
9851013 false
9861014}
9871015
1016+ // ============================================================================
1017+ // Polyfill Directive Processing
1018+ // ============================================================================
1019+
1020+ /// Process styles to convert native ShadowDOM rules that will trip up the CSS parser.
1021+ ///
1022+ /// Converts polyfill-next-selector rules like:
1023+ /// ```css
1024+ /// polyfill-next-selector { content: ':host menu-item'; }
1025+ /// ::content menu-item { ... }
1026+ /// ```
1027+ ///
1028+ /// to:
1029+ /// ```css
1030+ /// :host menu-item { ... }
1031+ /// ```
1032+ fn insert_polyfill_directives ( css : & str ) -> String {
1033+ // Handle both single and double quotes separately since Rust regex doesn't support backreferences
1034+ let re_single: & Regex =
1035+ regex ! ( r#"(?im)polyfill-next-selector[^}]*content:[\s]*?'([^']*)'[;\s]*\}([^{]*?)\{"# ) ;
1036+ let re_double: & Regex =
1037+ regex ! ( r#"(?im)polyfill-next-selector[^}]*content:[\s]*?"([^"]*)"[;\s]*\}([^{]*?)\{"# ) ;
1038+
1039+ let result = re_single
1040+ . replace_all ( css, |caps : & regex:: Captures | {
1041+ // Replace with: selector {
1042+ format ! ( "{} {{" , & caps[ 1 ] )
1043+ } )
1044+ . into_owned ( ) ;
1045+
1046+ re_double
1047+ . replace_all ( & result, |caps : & regex:: Captures | format ! ( "{} {{" , & caps[ 1 ] ) )
1048+ . into_owned ( )
1049+ }
1050+
1051+ /// Process styles to add rules which will only apply under the polyfill.
1052+ ///
1053+ /// Converts polyfill-rule rules like:
1054+ /// ```css
1055+ /// polyfill-rule {
1056+ /// content: ':host menu-item';
1057+ /// background: blue;
1058+ /// }
1059+ /// ```
1060+ ///
1061+ /// to:
1062+ /// ```css
1063+ /// :host menu-item { background: blue; }
1064+ /// ```
1065+ fn insert_polyfill_rules ( css : & str ) -> String {
1066+ // Handle both single and double quotes separately
1067+ let re_single: & Regex =
1068+ regex ! ( r#"(?im)polyfill-rule[^}]*content:[\s]*'([^']*)'[;\s]*([^}]*)\}"# ) ;
1069+ let re_double: & Regex =
1070+ regex ! ( r#"(?im)polyfill-rule[^}]*content:[\s]*"([^"]*)"[;\s]*([^}]*)\}"# ) ;
1071+
1072+ let replace_fn = |caps : & regex:: Captures | {
1073+ let selector = & caps[ 1 ] ;
1074+ let rule_body = & caps[ 2 ] ;
1075+ let cleaned_body = rule_body. trim ( ) . trim_start_matches ( ';' ) . trim ( ) ;
1076+ format ! ( "{} {{ {} }}" , selector, cleaned_body)
1077+ } ;
1078+
1079+ let result = re_single. replace_all ( css, replace_fn) . into_owned ( ) ;
1080+ re_double. replace_all ( & result, replace_fn) . into_owned ( )
1081+ }
1082+
1083+ /// Extract unscoped rules from CSS text.
1084+ ///
1085+ /// These are rules that should apply under the polyfill but NOT be scoped.
1086+ ///
1087+ /// Converts polyfill-unscoped-rule rules like:
1088+ /// ```css
1089+ /// @polyfill-unscoped-rule {
1090+ /// content: 'menu-item';
1091+ /// background: blue;
1092+ /// }
1093+ /// ```
1094+ ///
1095+ /// to:
1096+ /// ```css
1097+ /// menu-item { background: blue; }
1098+ /// ```
1099+ ///
1100+ /// Returns the extracted unscoped rules as a string.
1101+ fn extract_unscoped_rules ( css : & str ) -> String {
1102+ // Handle both single and double quotes separately
1103+ let re_single: & Regex =
1104+ regex ! ( r#"(?im)polyfill-unscoped-rule[^}]*content:[\s]*'([^']*)'[;\s]*([^}]*)\}"# ) ;
1105+ let re_double: & Regex =
1106+ regex ! ( r#"(?im)polyfill-unscoped-rule[^}]*content:[\s]*"([^"]*)"[;\s]*([^}]*)\}"# ) ;
1107+
1108+ let mut result = String :: new ( ) ;
1109+
1110+ for caps in re_single. captures_iter ( css) {
1111+ let selector = & caps[ 1 ] ;
1112+ let rule_body = & caps[ 2 ] ;
1113+ let cleaned_body = rule_body. trim ( ) . trim_start_matches ( ';' ) . trim ( ) ;
1114+ result. push_str ( & format ! ( "{} {{ {} }}\n \n " , selector, cleaned_body) ) ;
1115+ }
1116+
1117+ for caps in re_double. captures_iter ( css) {
1118+ let selector = & caps[ 1 ] ;
1119+ let rule_body = & caps[ 2 ] ;
1120+ let cleaned_body = rule_body. trim ( ) . trim_start_matches ( ';' ) . trim ( ) ;
1121+ result. push_str ( & format ! ( "{} {{ {} }}\n \n " , selector, cleaned_body) ) ;
1122+ }
1123+
1124+ result
1125+ }
1126+
1127+ /// Remove polyfill-unscoped-rule blocks from CSS text.
1128+ ///
1129+ /// This is called after extracting the unscoped rules.
1130+ fn remove_unscoped_rules ( css : & str ) -> String {
1131+ let re_single: & Regex =
1132+ regex ! ( r#"(?im)polyfill-unscoped-rule[^}]*content:[\s]*'[^']*'[;\s]*[^}]*\}"# ) ;
1133+ let re_double: & Regex =
1134+ regex ! ( r#"(?im)polyfill-unscoped-rule[^}]*content:[\s]*"[^"]*"[;\s]*[^}]*\}"# ) ;
1135+
1136+ let result = re_single. replace_all ( css, "" ) . into_owned ( ) ;
1137+ re_double. replace_all ( & result, "" ) . into_owned ( )
1138+ }
1139+
9881140#[ cfg( test) ]
9891141mod tests {
9901142 use super :: * ;
@@ -1050,4 +1202,110 @@ mod tests {
10501202 assert ! ( result. contains( "@media" ) , "Got: {}" , result) ;
10511203 assert ! ( result. contains( ".button[contenta]" ) , "Got: {}" , result) ;
10521204 }
1205+
1206+ // ============================================================================
1207+ // Polyfill Directive Tests
1208+ // ============================================================================
1209+
1210+ #[ test]
1211+ fn test_polyfill_next_selector_single_quotes ( ) {
1212+ let result = shim_css_text ( "polyfill-next-selector {content: 'x > y'} z {}" , "contenta" , "" ) ;
1213+ assert ! ( result. contains( "x[contenta]" ) , "Got: {}" , result) ;
1214+ assert ! ( result. contains( "y[contenta]" ) , "Got: {}" , result) ;
1215+ assert ! ( !result. contains( "polyfill-next-selector" ) , "Got: {}" , result) ;
1216+ assert ! ( !result. contains( "z" ) , "Got: {}" , result) ;
1217+ }
1218+
1219+ #[ test]
1220+ fn test_polyfill_next_selector_double_quotes ( ) {
1221+ let result =
1222+ shim_css_text ( "polyfill-next-selector {content: \" x > y\" } z {}" , "contenta" , "" ) ;
1223+ assert ! ( result. contains( "x[contenta]" ) , "Got: {}" , result) ;
1224+ assert ! ( result. contains( "y[contenta]" ) , "Got: {}" , result) ;
1225+ }
1226+
1227+ #[ test]
1228+ fn test_polyfill_next_selector_with_attribute ( ) {
1229+ let result = shim_css_text (
1230+ "polyfill-next-selector {content: 'button[priority=\" 1\" ]'} z {}" ,
1231+ "contenta" ,
1232+ "" ,
1233+ ) ;
1234+ assert ! (
1235+ result. contains( "button[priority=\" 1\" ][contenta]" ) ,
1236+ "Got: {}" ,
1237+ result
1238+ ) ;
1239+ }
1240+
1241+ #[ test]
1242+ fn test_polyfill_unscoped_rule_single_quotes ( ) {
1243+ let result = shim_css_text (
1244+ "polyfill-unscoped-rule {content: '#menu > .bar';color: blue;}" ,
1245+ "contenta" ,
1246+ "" ,
1247+ ) ;
1248+ // Unscoped rules should NOT have the content attribute
1249+ assert ! ( result. contains( "#menu > .bar" ) , "Got: {}" , result) ;
1250+ assert ! ( result. contains( "color: blue" ) , "Got: {}" , result) ;
1251+ assert ! ( !result. contains( "[contenta]" ) || result. contains( "#menu > .bar {" ) , "Unscoped rule should not have content attribute. Got: {}" , result) ;
1252+ }
1253+
1254+ #[ test]
1255+ fn test_polyfill_unscoped_rule_double_quotes ( ) {
1256+ let result = shim_css_text (
1257+ "polyfill-unscoped-rule {content: \" #menu > .bar\" ;color: blue;}" ,
1258+ "contenta" ,
1259+ "" ,
1260+ ) ;
1261+ assert ! ( result. contains( "#menu > .bar" ) , "Got: {}" , result) ;
1262+ assert ! ( result. contains( "color: blue" ) , "Got: {}" , result) ;
1263+ }
1264+
1265+ #[ test]
1266+ fn test_polyfill_unscoped_rule_multiple ( ) {
1267+ let result = shim_css_text (
1268+ "polyfill-unscoped-rule {content: 'foo';color: blue;}polyfill-unscoped-rule {content: 'bar';color: red;}" ,
1269+ "contenta" ,
1270+ "" ,
1271+ ) ;
1272+ assert ! ( result. contains( "foo {" ) , "Got: {}" , result) ;
1273+ assert ! ( result. contains( "bar {" ) , "Got: {}" , result) ;
1274+ assert ! ( result. contains( "color: blue" ) , "Got: {}" , result) ;
1275+ assert ! ( result. contains( "color: red" ) , "Got: {}" , result) ;
1276+ }
1277+
1278+ #[ test]
1279+ fn test_polyfill_rule_single_quotes ( ) {
1280+ let result = shim_css_text (
1281+ "polyfill-rule {content: ':host.foo .bar';color: blue;}" ,
1282+ "contenta" ,
1283+ "a-host" ,
1284+ ) ;
1285+ // polyfill-rule content gets scoped like a regular rule
1286+ assert ! ( result. contains( ".foo[a-host]" ) , "Got: {}" , result) ;
1287+ assert ! ( result. contains( ".bar[contenta]" ) , "Got: {}" , result) ;
1288+ assert ! ( result. contains( "color: blue" ) , "Got: {}" , result) ;
1289+ }
1290+
1291+ #[ test]
1292+ fn test_polyfill_rule_double_quotes ( ) {
1293+ let result = shim_css_text (
1294+ "polyfill-rule {content: \" :host.foo .bar\" ;color: blue;}" ,
1295+ "contenta" ,
1296+ "a-host" ,
1297+ ) ;
1298+ assert ! ( result. contains( ".foo[a-host]" ) , "Got: {}" , result) ;
1299+ assert ! ( result. contains( ".bar[contenta]" ) , "Got: {}" , result) ;
1300+ }
1301+
1302+ #[ test]
1303+ fn test_polyfill_rule_with_attribute ( ) {
1304+ let result = shim_css_text (
1305+ "polyfill-rule {content: 'button[priority=\" 1\" ]'}" ,
1306+ "contenta" ,
1307+ "a-host" ,
1308+ ) ;
1309+ assert ! ( result. contains( "button[priority=\" 1\" ][contenta]" ) , "Got: {}" , result) ;
1310+ }
10531311}
0 commit comments