@@ -122,9 +122,16 @@ impl<'a> Scanner<'a> {
122122 css. map ( |s| s[ content_start..self . pos ] . trim ( ) )
123123 }
124124
125- fn skip_at_rule ( & mut self ) {
125+ fn read_at_rule_name ( & mut self ) -> & ' a str {
126126 self . advance ( 1 ) ; // skip '@'
127- // Skip until ';' (statement) or matched '{...}' (block)
127+ let start = self . pos ;
128+ while !self . is_eof ( ) && is_ident_char ( self . bytes [ self . pos ] ) {
129+ self . advance ( 1 ) ;
130+ }
131+ std:: str:: from_utf8 ( & self . bytes [ start..self . pos ] ) . unwrap_or ( "" )
132+ }
133+
134+ fn skip_at_rule_prelude ( & mut self ) {
128135 while !self . is_eof ( ) {
129136 match self . bytes [ self . pos ] {
130137 b'"' | b'\'' => self . skip_string_literal ( ) ,
@@ -133,26 +140,29 @@ impl<'a> Scanner<'a> {
133140 self . advance ( 1 ) ;
134141 return ;
135142 }
143+ b'{' => return ,
144+ _ => self . advance ( 1 ) ,
145+ }
146+ }
147+ }
148+
149+ fn skip_at_rule_body ( & mut self ) {
150+ if self . is_eof ( ) || self . bytes [ self . pos ] != b'{' {
151+ return ;
152+ }
153+ self . advance ( 1 ) ;
154+ let mut depth = 1i32 ;
155+ while !self . is_eof ( ) && depth > 0 {
156+ match self . bytes [ self . pos ] {
157+ b'"' | b'\'' => self . skip_string_literal ( ) ,
158+ b'/' if self . peek_at ( 1 ) == Some ( b'*' ) => self . skip_comment ( ) ,
136159 b'{' => {
137- // Skip the block including nested braces
160+ depth += 1 ;
161+ self . advance ( 1 ) ;
162+ }
163+ b'}' => {
164+ depth -= 1 ;
138165 self . advance ( 1 ) ;
139- let mut depth = 1i32 ;
140- while !self . is_eof ( ) && depth > 0 {
141- match self . bytes [ self . pos ] {
142- b'"' | b'\'' => self . skip_string_literal ( ) ,
143- b'/' if self . peek_at ( 1 ) == Some ( b'*' ) => self . skip_comment ( ) ,
144- b'{' => {
145- depth += 1 ;
146- self . advance ( 1 ) ;
147- }
148- b'}' => {
149- depth -= 1 ;
150- self . advance ( 1 ) ;
151- }
152- _ => self . advance ( 1 ) ,
153- }
154- }
155- return ;
156166 }
157167 _ => self . advance ( 1 ) ,
158168 }
@@ -303,10 +313,13 @@ fn parse_impl(
303313 pending_ignores. push ( content) ;
304314 }
305315 }
306- // Skip @-rules at top level (e.g. @property, @import, @charset)
307- b'@' if brace_depth == initial_brace_depth => {
316+ b'@' => {
308317 pending_ignores. clear ( ) ;
309- s. skip_at_rule ( ) ;
318+ let name = s. read_at_rule_name ( ) ;
319+ s. skip_at_rule_prelude ( ) ;
320+ if !is_transparent_at_rule ( name) {
321+ s. skip_at_rule_body ( ) ;
322+ }
310323 }
311324 b'{' => {
312325 pending_ignores. clear ( ) ;
@@ -394,6 +407,18 @@ fn parse_impl(
394407 }
395408}
396409
410+ fn is_transparent_at_rule ( name : & str ) -> bool {
411+ const TRANSPARENT : & [ & str ] = & [
412+ "media" ,
413+ "supports" ,
414+ "container" ,
415+ "layer" ,
416+ "scope" ,
417+ "starting-style" ,
418+ ] ;
419+ TRANSPARENT . iter ( ) . any ( |t| name. eq_ignore_ascii_case ( t) )
420+ }
421+
397422fn is_ident_start ( b : u8 ) -> bool {
398423 b. is_ascii_alphabetic ( ) || b == b'-' || b == b'_' || b == b'\\' || b >= 0x80
399424}
@@ -829,6 +854,80 @@ mod tests {
829854 assert_eq ! ( result. properties[ 0 ] . ident. raw. as_str( ) , "color" ) ;
830855 }
831856
857+ #[ test]
858+ fn at_media_top_level_collects_inner_defs ( ) {
859+ let css = "@media (prefers-color-scheme: dark) {\n :root { --color: white; }\n }" ;
860+ let result = test_parse ( css) ;
861+ assert_eq ! ( result. properties. len( ) , 1 ) ;
862+ assert_eq ! ( result. properties[ 0 ] . ident. raw. as_str( ) , "--color" ) ;
863+ assert_eq ! ( result. properties[ 0 ] . value. raw. as_str( ) , "white" ) ;
864+ }
865+
866+ #[ test]
867+ fn at_media_with_bare_declarations ( ) {
868+ let css = "@media (max-width: 600px) {\n --pad: 8px;\n }" ;
869+ let result = test_parse ( css) ;
870+ assert_eq ! ( result. properties. len( ) , 1 ) ;
871+ assert_eq ! ( result. properties[ 0 ] . ident. raw. as_str( ) , "--pad" ) ;
872+ assert_eq ! ( result. properties[ 0 ] . value. raw. as_str( ) , "8px" ) ;
873+ }
874+
875+ #[ test]
876+ fn at_media_nested_in_supports ( ) {
877+ let css = "@supports (color: oklch(0 0 0)) {\n @media (prefers-color-scheme: dark) {\n :root { --x: 1; }\n }\n }" ;
878+ let result = test_parse ( css) ;
879+ assert_eq ! ( result. properties. len( ) , 1 ) ;
880+ assert_eq ! ( result. properties[ 0 ] . ident. raw. as_str( ) , "--x" ) ;
881+ assert_eq ! ( result. properties[ 0 ] . value. raw. as_str( ) , "1" ) ;
882+ }
883+
884+ #[ test]
885+ fn at_layer_block_form ( ) {
886+ let css = "@layer theme {\n :root { --x: 1; }\n }" ;
887+ let result = test_parse ( css) ;
888+ assert_eq ! ( result. properties. len( ) , 1 ) ;
889+ assert_eq ! ( result. properties[ 0 ] . ident. raw. as_str( ) , "--x" ) ;
890+ }
891+
892+ #[ test]
893+ fn at_layer_statement_form ( ) {
894+ let css = "@layer foo, bar;\n :root { --y: 2; }" ;
895+ let result = test_parse ( css) ;
896+ assert_eq ! ( result. properties. len( ) , 1 ) ;
897+ assert_eq ! ( result. properties[ 0 ] . ident. raw. as_str( ) , "--y" ) ;
898+ assert_eq ! ( result. properties[ 0 ] . value. raw. as_str( ) , "2" ) ;
899+ }
900+
901+ #[ test]
902+ fn at_media_inside_selector ( ) {
903+ let css = ":root {\n --base: 0;\n @media (prefers-color-scheme: dark) {\n --base: 1;\n }\n }" ;
904+ let result = test_parse ( css) ;
905+ let names: Vec < & str > = result
906+ . properties
907+ . iter ( )
908+ . map ( |p| p. ident . raw . as_str ( ) )
909+ . collect ( ) ;
910+ assert_eq ! ( names, vec![ "--base" , "--base" ] ) ;
911+ assert_eq ! ( result. properties[ 0 ] . value. raw. as_str( ) , "0" ) ;
912+ assert_eq ! ( result. properties[ 1 ] . value. raw. as_str( ) , "1" ) ;
913+ }
914+
915+ #[ test]
916+ fn at_media_uppercase ( ) {
917+ let css = "@MEDIA (prefers-color-scheme: dark) {\n :root { --x: 1; }\n }" ;
918+ let result = test_parse ( css) ;
919+ assert_eq ! ( result. properties. len( ) , 1 ) ;
920+ assert_eq ! ( result. properties[ 0 ] . ident. raw. as_str( ) , "--x" ) ;
921+ }
922+
923+ #[ test]
924+ fn at_media_with_string_in_prelude ( ) {
925+ let css = "@media (min-width: 100px) and (foo: \" with } brace\" ) {\n :root { --x: 1; }\n }" ;
926+ let result = test_parse ( css) ;
927+ assert_eq ! ( result. properties. len( ) , 1 ) ;
928+ assert_eq ! ( result. properties[ 0 ] . ident. raw. as_str( ) , "--x" ) ;
929+ }
930+
832931 #[ test]
833932 fn unterminated_comment_consumes_all_bytes ( ) {
834933 let str = OwnedStr :: from ( "/* {" ) ;
0 commit comments