@@ -11,6 +11,17 @@ struct Scanner<'a> {
1111 col : u32 ,
1212}
1313
14+ struct AtPropertyParts {
15+ name_start : usize ,
16+ name_end : usize ,
17+ name_line : u32 ,
18+ name_col : u32 ,
19+ value_start : usize ,
20+ value_end : usize ,
21+ value_line : u32 ,
22+ value_col : u32 ,
23+ }
24+
1425impl < ' a > Scanner < ' a > {
1526 fn new_with_offset ( css : & ' a OwnedStr , line_offset : u32 , column_offset : u32 ) -> Self {
1627 Self {
@@ -169,6 +180,110 @@ impl<'a> Scanner<'a> {
169180 }
170181 }
171182
183+ fn skip_inline_and_newlines ( & mut self ) {
184+ while !self . is_eof ( ) && matches ! ( self . bytes[ self . pos] , b' ' | b'\t' | b'\n' | b'\r' ) {
185+ self . advance ( 1 ) ;
186+ }
187+ }
188+
189+ /// Scans an `@property --name { … initial-value: V; … }` rule.
190+ /// The leading `@property` token has already been consumed.
191+ /// Returns prelude name + `initial-value` spans (the value span is empty
192+ /// when no `initial-value` declaration is present). The closing `}` is
193+ /// consumed. Returns `None` only when the prelude lacks an identifier or
194+ /// the body never opens.
195+ fn scan_at_property_rule ( & mut self ) -> Option < AtPropertyParts > {
196+ self . skip_inline_and_newlines ( ) ;
197+
198+ let name_start = self . pos ;
199+ let name_line = self . line ;
200+ let name_col = self . col ;
201+ while !self . is_eof ( ) {
202+ if self . bytes [ self . pos ] == b'\\' {
203+ self . skip_escape ( ) ;
204+ } else if is_ident_char ( self . bytes [ self . pos ] ) {
205+ self . advance ( 1 ) ;
206+ } else {
207+ break ;
208+ }
209+ }
210+ let name_end = self . pos ;
211+ if name_start == name_end {
212+ self . skip_at_rule_prelude ( ) ;
213+ self . skip_at_rule_body ( ) ;
214+ return None ;
215+ }
216+
217+ self . skip_at_rule_prelude ( ) ;
218+ if self . is_eof ( ) || self . bytes [ self . pos ] != b'{' {
219+ return None ;
220+ }
221+ self . advance ( 1 ) ; // consume '{'
222+
223+ let mut value_start = self . pos ;
224+ let mut value_end = self . pos ;
225+ let mut value_line = self . line ;
226+ let mut value_col = self . col ;
227+
228+ let mut depth = 1i32 ;
229+ while !self . is_eof ( ) && depth > 0 {
230+ match self . bytes [ self . pos ] {
231+ b'"' | b'\'' => self . skip_string_literal ( ) ,
232+ b'/' if self . peek_at ( 1 ) == Some ( b'*' ) => self . skip_comment ( ) ,
233+ b'{' => {
234+ depth += 1 ;
235+ self . advance ( 1 ) ;
236+ }
237+ b'}' => {
238+ depth -= 1 ;
239+ self . advance ( 1 ) ;
240+ }
241+ _ if depth == 1 && is_ident_start ( self . bytes [ self . pos ] ) => {
242+ let prop_name_start = self . pos ;
243+ while !self . is_eof ( ) {
244+ if self . bytes [ self . pos ] == b'\\' {
245+ self . skip_escape ( ) ;
246+ } else if is_ident_char ( self . bytes [ self . pos ] ) {
247+ self . advance ( 1 ) ;
248+ } else {
249+ break ;
250+ }
251+ }
252+ let prop_name_end = self . pos ;
253+ self . skip_whitespace ( ) ;
254+ if !self . is_eof ( ) && self . bytes [ self . pos ] == b':' {
255+ self . advance ( 1 ) ;
256+ self . skip_whitespace ( ) ;
257+ let line = self . line ;
258+ let col = self . col ;
259+ let start = self . pos ;
260+ let end = self . scan_value_end ( ) ;
261+ let prop_name = & self . bytes [ prop_name_start..prop_name_end] ;
262+ if prop_name. eq_ignore_ascii_case ( b"initial-value" ) {
263+ // Last `initial-value` wins, matching CSS cascade.
264+ value_start = start;
265+ value_end = end;
266+ value_line = line;
267+ value_col = col;
268+ }
269+ }
270+ }
271+ _ => self . advance ( 1 ) ,
272+ }
273+ }
274+
275+ Some ( AtPropertyParts {
276+ name_start,
277+ name_end,
278+ name_line,
279+ name_col,
280+ value_start,
281+ value_end,
282+ value_line,
283+ value_col,
284+ } )
285+ }
286+
172287 fn scan_value_end ( & mut self ) -> usize {
173288 let mut paren_depth = 0i32 ;
174289
@@ -314,11 +429,36 @@ fn parse_impl(
314429 }
315430 }
316431 b'@' => {
317- pending_ignores . clear ( ) ;
432+ let ignore_comments = std :: mem :: take ( & mut pending_ignores ) ;
318433 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 ( ) ;
434+ if name. eq_ignore_ascii_case ( "property" ) {
435+ if let Some ( parts) = s. scan_at_property_rule ( ) {
436+ let raw_name = css. slice ( parts. name_start ..parts. name_end ) ;
437+ let raw_value = css. map ( |s| s[ parts. value_start ..parts. value_end ] . trim ( ) ) ;
438+ properties. push ( Property {
439+ file_path : file_path. clone ( ) ,
440+ source : source. clone ( ) ,
441+ ident : PropertyIdent :: new (
442+ raw_name,
443+ parts. name_start + byte_offset,
444+ parts. name_line ,
445+ parts. name_col ,
446+ ) ,
447+ value : PropertyValue {
448+ raw : raw_value,
449+ offset : parts. value_start + byte_offset,
450+ line : parts. value_line ,
451+ column : parts. value_col ,
452+ } ,
453+ ignore_comments,
454+ token_list : OnceCell :: new ( ) ,
455+ } ) ;
456+ }
457+ } else {
458+ s. skip_at_rule_prelude ( ) ;
459+ if !is_transparent_at_rule ( name) {
460+ s. skip_at_rule_body ( ) ;
461+ }
322462 }
323463 }
324464 b'{' => {
@@ -829,21 +969,116 @@ mod tests {
829969 }
830970
831971 #[ test]
832- fn at_property_rule_skipped ( ) {
972+ fn at_property_rule_collects_definition ( ) {
833973 let css = "@property --my-color {\n syntax: \" <color>\" ;\n inherits: false;\n initial-value: red;\n }\n .a { color: var(--my-color); }" ;
834974 let result = test_parse ( css) ;
835- assert_eq ! ( result. properties. len( ) , 1 ) ;
836- assert_eq ! ( result. properties[ 0 ] . ident. raw. as_str( ) , "color" ) ;
837- assert_eq ! ( result. properties[ 0 ] . value. raw. as_str( ) , "var(--my-color)" ) ;
975+ assert_eq ! ( result. properties. len( ) , 2 ) ;
976+ assert_eq ! ( result. properties[ 0 ] . ident. raw. as_str( ) , "--my-color" ) ;
977+ assert_eq ! ( result. properties[ 0 ] . value. raw. as_str( ) , "red" ) ;
978+ assert_eq ! ( result. properties[ 1 ] . ident. raw. as_str( ) , "color" ) ;
979+ assert_eq ! ( result. properties[ 1 ] . value. raw. as_str( ) , "var(--my-color)" ) ;
838980 }
839981
840982 #[ test]
841- fn at_property_between_selectors ( ) {
983+ fn at_property_between_selectors_collects_definition ( ) {
842984 let css = ".before { margin: 0; }\n @property --x {\n syntax: \" *\" ;\n inherits: true;\n }\n .after { padding: 0; }" ;
843985 let result = test_parse ( css) ;
844- assert_eq ! ( result. properties. len( ) , 2 ) ;
986+ assert_eq ! ( result. properties. len( ) , 3 ) ;
845987 assert_eq ! ( result. properties[ 0 ] . ident. raw. as_str( ) , "margin" ) ;
846- assert_eq ! ( result. properties[ 1 ] . ident. raw. as_str( ) , "padding" ) ;
988+ assert_eq ! ( result. properties[ 1 ] . ident. raw. as_str( ) , "--x" ) ;
989+ assert_eq ! ( result. properties[ 1 ] . value. raw. as_str( ) , "" ) ;
990+ assert_eq ! ( result. properties[ 2 ] . ident. raw. as_str( ) , "padding" ) ;
991+ }
992+
993+ #[ test]
994+ fn at_property_extracts_initial_value ( ) {
995+ let css = "@property --logo-color {\n syntax: \" <color>\" ;\n inherits: false;\n initial-value: #c0ffee;\n }" ;
996+ let result = test_parse ( css) ;
997+ assert_eq ! ( result. properties. len( ) , 1 ) ;
998+ let p = & result. properties [ 0 ] ;
999+ assert_eq ! ( p. ident. raw. as_str( ) , "--logo-color" ) ;
1000+ assert_eq ! ( p. value. raw. as_str( ) , "#c0ffee" ) ;
1001+ // name on line 0, after "@property " (10 cols)
1002+ assert_eq ! ( p. ident. line, 0 ) ;
1003+ assert_eq ! ( p. ident. column, 10 ) ;
1004+ // value on line 3, after " initial-value: " (17 cols)
1005+ assert_eq ! ( p. value. line, 3 ) ;
1006+ assert_eq ! ( p. value. column, 17 ) ;
1007+ }
1008+
1009+ #[ test]
1010+ fn at_property_without_initial_value ( ) {
1011+ let css = "@property --x {\n syntax: \" *\" ;\n inherits: true;\n }" ;
1012+ let result = test_parse ( css) ;
1013+ assert_eq ! ( result. properties. len( ) , 1 ) ;
1014+ assert_eq ! ( result. properties[ 0 ] . ident. raw. as_str( ) , "--x" ) ;
1015+ assert_eq ! ( result. properties[ 0 ] . value. raw. as_str( ) , "" ) ;
1016+ }
1017+
1018+ #[ test]
1019+ fn at_property_initial_value_with_var ( ) {
1020+ let css = "@property --x {\n syntax: \" <color>\" ;\n initial-value: var(--base, red);\n }" ;
1021+ let result = test_parse ( css) ;
1022+ assert_eq ! ( result. properties. len( ) , 1 ) ;
1023+ assert_eq ! ( result. properties[ 0 ] . ident. raw. as_str( ) , "--x" ) ;
1024+ assert_eq ! ( result. properties[ 0 ] . value. raw. as_str( ) , "var(--base, red)" ) ;
1025+ }
1026+
1027+ #[ test]
1028+ fn at_property_with_string_in_syntax ( ) {
1029+ let css =
1030+ "@property --x {\n syntax: \" <length> | <percentage>\" ;\n initial-value: 8px;\n }" ;
1031+ let result = test_parse ( css) ;
1032+ assert_eq ! ( result. properties. len( ) , 1 ) ;
1033+ assert_eq ! ( result. properties[ 0 ] . ident. raw. as_str( ) , "--x" ) ;
1034+ assert_eq ! ( result. properties[ 0 ] . value. raw. as_str( ) , "8px" ) ;
1035+ }
1036+
1037+ #[ test]
1038+ fn at_property_invalid_name ( ) {
1039+ let css = "@property foo {\n initial-value: red;\n }\n .a { color: red; }" ;
1040+ let result = test_parse ( css) ;
1041+ assert_eq ! ( result. properties. len( ) , 2 ) ;
1042+ assert_eq ! ( result. properties[ 0 ] . ident. raw. as_str( ) , "foo" ) ;
1043+ assert_eq ! ( result. properties[ 0 ] . value. raw. as_str( ) , "red" ) ;
1044+ assert_eq ! ( result. properties[ 1 ] . ident. raw. as_str( ) , "color" ) ;
1045+ }
1046+
1047+ #[ test]
1048+ fn at_property_last_initial_value_wins ( ) {
1049+ let css = "@property --x {\n initial-value: red;\n initial-value: blue;\n }" ;
1050+ let result = test_parse ( css) ;
1051+ assert_eq ! ( result. properties. len( ) , 1 ) ;
1052+ assert_eq ! ( result. properties[ 0 ] . value. raw. as_str( ) , "blue" ) ;
1053+ }
1054+
1055+ #[ test]
1056+ fn at_property_cvk_ignore_propagates ( ) {
1057+ let css = "/* cvk-ignore */\n @property --x {\n initial-value: red;\n }" ;
1058+ let result = test_parse ( css) ;
1059+ assert_eq ! ( result. properties. len( ) , 1 ) ;
1060+ assert_eq ! (
1061+ result. properties[ 0 ] . ignore_comments,
1062+ map_owned_str( vec![ "cvk-ignore" ] )
1063+ ) ;
1064+ }
1065+
1066+ #[ test]
1067+ fn at_property_inside_media ( ) {
1068+ let css = "@media (prefers-color-scheme: dark) {\n @property --x {\n initial-value: white;\n }\n }" ;
1069+ let result = test_parse ( css) ;
1070+ assert_eq ! ( result. properties. len( ) , 1 ) ;
1071+ assert_eq ! ( result. properties[ 0 ] . ident. raw. as_str( ) , "--x" ) ;
1072+ assert_eq ! ( result. properties[ 0 ] . value. raw. as_str( ) , "white" ) ;
1073+ }
1074+
1075+ #[ test]
1076+ fn at_property_uppercase ( ) {
1077+ let css = "@PROPERTY --x {\n initial-value: red;\n }" ;
1078+ let result = test_parse ( css) ;
1079+ assert_eq ! ( result. properties. len( ) , 1 ) ;
1080+ assert_eq ! ( result. properties[ 0 ] . ident. raw. as_str( ) , "--x" ) ;
1081+ assert_eq ! ( result. properties[ 0 ] . value. raw. as_str( ) , "red" ) ;
8471082 }
8481083
8491084 #[ test]
0 commit comments