@@ -10,6 +10,7 @@ import {
1010 type CssDeclarationAST ,
1111 type CssDocumentAST ,
1212 type CssFontFaceAST ,
13+ type CssGenericAtRuleAST ,
1314 type CssHostAST ,
1415 type CssImportAST ,
1516 type CssKeyframeAST ,
@@ -283,7 +284,8 @@ export const parse = (
283284 }
284285
285286 /**
286- * Parse declarations.
287+ * Parse declarations (without nesting support).
288+ * Used by @font-face, @page, keyframes.
287289 */
288290 function declarations ( ) {
289291 const decls : Array < CssDeclarationAST | CssCommentAST > = [ ] ;
@@ -307,6 +309,126 @@ export const parse = (
307309 return decls ;
308310 }
309311
312+ /**
313+ * Check if the current position looks like a nested rule
314+ * ('{' appears before ';' and '}' at the top level).
315+ */
316+ function looksLikeNestedRule ( ) : boolean {
317+ const bracePos = indexOfArrayWithBracketAndQuoteSupport ( css , [ '{' ] ) ;
318+ if ( bracePos === - 1 ) {
319+ return false ;
320+ }
321+ const semiPos = indexOfArrayWithBracketAndQuoteSupport ( css , [ ';' ] ) ;
322+ const closePos = indexOfArrayWithBracketAndQuoteSupport ( css , [ '}' ] ) ;
323+
324+ if ( semiPos !== - 1 && semiPos < bracePos ) {
325+ return false ;
326+ }
327+ if ( closePos !== - 1 && closePos < bracePos ) {
328+ return false ;
329+ }
330+ return true ;
331+ }
332+
333+ /**
334+ * Parse rule body with CSS nesting support.
335+ * Handles declarations, comments, nested rules, and nested at-rules.
336+ */
337+ function ruleBody ( ) :
338+ | Array < CssDeclarationAST | CssCommentAST | CssAtRuleAST >
339+ | undefined {
340+ const items : Array < CssDeclarationAST | CssCommentAST | CssAtRuleAST > = [ ] ;
341+
342+ if ( ! open ( ) ) {
343+ return error ( "missing '{'" ) ;
344+ }
345+ comments ( items ) ;
346+
347+ while ( css . length && css . charAt ( 0 ) !== '}' ) {
348+ // nested at-rule
349+ if ( css . charAt ( 0 ) === '@' ) {
350+ const ar = atRule ( ) ;
351+ if ( ar ) {
352+ items . push ( ar ) ;
353+ comments ( items ) ;
354+ continue ;
355+ }
356+ }
357+
358+ // nested rule ('{' comes before ';' and '}')
359+ if ( looksLikeNestedRule ( ) ) {
360+ const nestedR = rule ( ) ;
361+ if ( nestedR ) {
362+ items . push ( nestedR ) ;
363+ comments ( items ) ;
364+ continue ;
365+ }
366+ }
367+
368+ // declaration
369+ const decl = declaration ( ) ;
370+ if ( decl ) {
371+ items . push ( decl ) ;
372+ comments ( items ) ;
373+ continue ;
374+ }
375+
376+ // nothing matched
377+ break ;
378+ }
379+
380+ if ( ! close ( ) ) {
381+ return error ( "missing '}'" ) ;
382+ }
383+ return items ;
384+ }
385+
386+ /**
387+ * Parse rules, declarations, and nested rules.
388+ * Used by block at-rules (media, supports, etc.) to support
389+ * both top-level rules and declarations when nested inside a rule.
390+ */
391+ function rulesOrDeclarations ( ) {
392+ const items : Array <
393+ CssAtRuleAST | CssDeclarationAST | CssCommentAST
394+ > = [ ] ;
395+ whitespace ( ) ;
396+ comments ( items ) ;
397+ while ( css . length && css . charAt ( 0 ) !== '}' ) {
398+ // at-rule
399+ if ( css . charAt ( 0 ) === '@' ) {
400+ const ar = atRule ( ) ;
401+ if ( ar ) {
402+ items . push ( ar ) ;
403+ comments ( items ) ;
404+ continue ;
405+ }
406+ }
407+
408+ // nested rule ('{' comes before ';' and '}')
409+ if ( looksLikeNestedRule ( ) ) {
410+ const r = rule ( ) ;
411+ if ( r ) {
412+ items . push ( r ) ;
413+ comments ( items ) ;
414+ continue ;
415+ }
416+ }
417+
418+ // declaration
419+ const decl = declaration ( ) ;
420+ if ( decl ) {
421+ items . push ( decl ) ;
422+ comments ( items ) ;
423+ continue ;
424+ }
425+
426+ // nothing matched
427+ break ;
428+ }
429+ return items ;
430+ }
431+
310432 /**
311433 * Parse keyframe.
312434 */
@@ -397,7 +519,7 @@ export const parse = (
397519 return error ( "@supports missing '{'" ) ;
398520 }
399521
400- const style = comments < CssAtRuleAST > ( ) . concat ( rules ( ) ) ;
522+ const style = rulesOrDeclarations ( ) ;
401523
402524 if ( ! close ( ) ) {
403525 return error ( "@supports missing '}'" ) ;
@@ -426,7 +548,7 @@ export const parse = (
426548 return error ( "@host missing '{'" ) ;
427549 }
428550
429- const style = comments < CssAtRuleAST > ( ) . concat ( rules ( ) ) ;
551+ const style = rulesOrDeclarations ( ) ;
430552
431553 if ( ! close ( ) ) {
432554 return error ( "@host missing '}'" ) ;
@@ -454,7 +576,7 @@ export const parse = (
454576 return error ( "@container missing '{'" ) ;
455577 }
456578
457- const style = comments < CssAtRuleAST > ( ) . concat ( rules ( ) ) ;
579+ const style = rulesOrDeclarations ( ) ;
458580
459581 if ( ! close ( ) ) {
460582 return error ( "@container missing '}'" ) ;
@@ -490,7 +612,7 @@ export const parse = (
490612 } ) ;
491613 }
492614
493- const style = comments < CssAtRuleAST > ( ) . concat ( rules ( ) ) ;
615+ const style = rulesOrDeclarations ( ) ;
494616
495617 if ( ! close ( ) ) {
496618 return error ( "@layer missing '}'" ) ;
@@ -519,7 +641,7 @@ export const parse = (
519641 return error ( "@media missing '{'" ) ;
520642 }
521643
522- const style = comments < CssAtRuleAST > ( ) . concat ( rules ( ) ) ;
644+ const style = rulesOrDeclarations ( ) ;
523645
524646 if ( ! close ( ) ) {
525647 return error ( "@media missing '}'" ) ;
@@ -605,7 +727,7 @@ export const parse = (
605727 return error ( "@document missing '{'" ) ;
606728 }
607729
608- const style = comments < CssAtRuleAST > ( ) . concat ( rules ( ) ) ;
730+ const style = rulesOrDeclarations ( ) ;
609731
610732 if ( ! close ( ) ) {
611733 return error ( "@document missing '}'" ) ;
@@ -667,7 +789,7 @@ export const parse = (
667789 if ( ! open ( ) ) {
668790 return error ( "@starting-style missing '{'" ) ;
669791 }
670- const style = comments < CssAtRuleAST > ( ) . concat ( rules ( ) ) ;
792+ const style = rulesOrDeclarations ( ) ;
671793
672794 if ( ! close ( ) ) {
673795 return error ( "@starting-style missing '}'" ) ;
@@ -721,6 +843,56 @@ export const parse = (
721843 } ;
722844 }
723845
846+ /**
847+ * Parse generic/unknown at-rule (fallback for any unrecognized at-rule).
848+ * Handles both block at-rules (@scope { ... }) and statement at-rules (@foo ...;).
849+ */
850+ function atGeneric ( ) : CssGenericAtRuleAST | undefined {
851+ const pos = position ( ) ;
852+ const m = / ^ @ ( [ - \w ] + ) \s * / . exec ( css ) ;
853+ if ( ! m ) {
854+ return ;
855+ }
856+ const name = processMatch ( m ) [ 1 ] ;
857+
858+ // Capture prelude (everything between the name and '{' or ';')
859+ let prelude = '' ;
860+ const preludeEnd = indexOfArrayWithBracketAndQuoteSupport ( css , [ '{' , ';' ] ) ;
861+ if ( preludeEnd !== - 1 && preludeEnd > 0 ) {
862+ prelude = trim ( css . substring ( 0 , preludeEnd ) ) ;
863+ const fakeMatch = [ css . substring ( 0 , preludeEnd ) ] as unknown as RegExpExecArray ;
864+ processMatch ( fakeMatch ) ;
865+ }
866+
867+ // Block at-rule
868+ if ( open ( ) ) {
869+ const style = rulesOrDeclarations ( ) ;
870+
871+ if ( ! close ( ) ) {
872+ return error ( `@${ name } missing '}'` ) ;
873+ }
874+
875+ return pos < CssGenericAtRuleAST > ( {
876+ type : CssTypes . atRule ,
877+ name : name ,
878+ prelude : prelude ,
879+ rules : style ,
880+ } ) ;
881+ }
882+
883+ // Statement at-rule (ends with ';')
884+ const endMatch = / ^ [ ; \s ] * / . exec ( css ) ;
885+ if ( endMatch ) {
886+ processMatch ( endMatch ) ;
887+ }
888+
889+ return pos < CssGenericAtRuleAST > ( {
890+ type : CssTypes . atRule ,
891+ name : name ,
892+ prelude : prelude ,
893+ } ) ;
894+ }
895+
724896 /**
725897 * Parse at rule.
726898 */
@@ -743,7 +915,8 @@ export const parse = (
743915 atFontFace ( ) ||
744916 atContainer ( ) ||
745917 atStartingStyle ( ) ||
746- atLayer ( )
918+ atLayer ( ) ||
919+ atGeneric ( )
747920 ) ;
748921 }
749922
@@ -762,7 +935,7 @@ export const parse = (
762935 return pos < CssRuleAST > ( {
763936 type : CssTypes . rule ,
764937 selectors : sel ,
765- declarations : declarations ( ) || [ ] ,
938+ declarations : ruleBody ( ) || [ ] ,
766939 } ) ;
767940 }
768941
0 commit comments