@@ -38,100 +38,38 @@ function getIgnoreComment(node) {
3838 }
3939}
4040
41- // Parse @scope at-rule params into scope-start / scope-end clauses.
42- // Grammar: <scope-start>? (to <scope-end>)? where each clause is "(...)".
43- // Tracks paren depth, string literals, CSS comments, and identifier escapes
44- // so the "to" keyword is detected at the structural position rather than
45- // matched as a substring. Returns null on unparseable input. Fixes #90.
41+ // Parse `@scope (start)? (to (end))?` params (#90). Uses postcss-value-parser
42+ // to tokenize parens, strings, escapes, and comments correctly. Quirk: `to(...)`
43+ // with no whitespace parses as one function — we split it back so all spacings
44+ // reduce to the same three grammar shapes.
4645function parseScopeParams ( params ) {
47- const len = params . length ;
48- let i = 0 ;
49-
50- const skipWs = ( ) => {
51- while ( i < len ) {
52- if ( / \s / . test ( params [ i ] ) ) {
53- i ++ ;
54- } else if ( params [ i ] === "/" && params [ i + 1 ] === "*" ) {
55- const close = params . indexOf ( "*/" , i + 2 ) ;
56- if ( close === - 1 ) return false ;
57- i = close + 2 ;
58- } else {
59- return true ;
60- }
61- }
62- return true ;
63- } ;
64-
65- // Read inner contents of `(...)` at position i; advance i past `)`.
66- // Tracks paren depth, string literals (with backslash escapes), CSS
67- // comments, and CSS ident escapes (`\(`, `\)`, etc.).
68- const readParens = ( ) => {
69- if ( params [ i ] !== "(" ) return null ;
70- let depth = 1 ;
71- let j = i + 1 ;
72- let inStr = null ;
73- while ( j < len && depth > 0 ) {
74- const c = params [ j ] ;
75- if ( inStr ) {
76- if ( c === "\\" ) {
77- j += 2 ; // skip the escaped char (incl. closing quote, newline, etc.)
78- continue ;
79- }
80- if ( c === inStr ) inStr = null ;
81- j ++ ;
82- } else if ( c === "\\" ) {
83- j += 2 ; // CSS ident escape — the next char is literal, ignore parens
84- } else if ( c === "/" && params [ j + 1 ] === "*" ) {
85- const close = params . indexOf ( "*/" , j + 2 ) ;
86- if ( close === - 1 ) return null ;
87- j = close + 2 ;
88- } else if ( c === '"' || c === "'" ) {
89- inStr = c ;
90- j ++ ;
91- } else if ( c === "(" ) {
92- depth ++ ;
93- j ++ ;
94- } else if ( c === ")" ) {
95- depth -- ;
96- j ++ ;
97- } else {
98- j ++ ;
99- }
100- }
101- if ( depth !== 0 ) return null ;
102- const inner = params . slice ( i + 1 , j - 1 ) ;
103- i = j ;
104- return inner ;
105- } ;
106-
107- if ( ! skipWs ( ) ) return null ;
108- let start = null ;
109- let end = null ;
110-
111- if ( params [ i ] === "(" ) {
112- start = readParens ( ) ;
113- if ( start === null ) return null ;
114- if ( ! skipWs ( ) ) return null ;
115- }
46+ const nodes = valueParser ( params )
47+ . nodes . filter ( ( n ) => n . type !== "space" )
48+ . flatMap ( ( n ) =>
49+ n . type === "function" && n . value . toLowerCase ( ) === "to"
50+ ? [
51+ { type : "word" , value : "to" } ,
52+ { ...n , value : "" } ,
53+ ]
54+ : [ n ]
55+ ) ;
11656
117- if ( i < len ) {
118- // Expect "to" keyword (case-insensitive per CSS) followed by `(scope-end)`.
119- if ( params . slice ( i , i + 2 ) . toLowerCase ( ) !== "to" ) return null ;
120- // Boundary check: the char after "to" must be whitespace, "(", or
121- // start-of-comment. Empty fallback is safe: if "to" is at end-of-input
122- // with no trailing context, the subsequent paren check rejects it.
123- const next = i + 2 < len ? params [ i + 2 ] : "" ;
124- if ( next !== "" && ! / \s | \( / . test ( next ) && next !== "/" ) return null ;
125- i += 2 ;
126- if ( ! skipWs ( ) ) return null ;
127- if ( params [ i ] !== "(" ) return null ;
128- end = readParens ( ) ;
129- if ( end === null ) return null ;
130- if ( ! skipWs ( ) ) return null ;
131- if ( i < len ) return null ; // trailing garbage
132- }
57+ const isParen = ( n ) => n && n . type === "function" && n . value === "" ;
58+ const isTo = ( n ) => n && n . type === "word" && n . value . toLowerCase ( ) === "to" ;
59+ const inner = ( n ) => valueParser . stringify ( n . nodes ) ;
13360
134- return { start, end } ;
61+ if ( nodes . length === 1 && isParen ( nodes [ 0 ] ) )
62+ return { start : inner ( nodes [ 0 ] ) , end : null } ;
63+ if ( nodes . length === 2 && isTo ( nodes [ 0 ] ) && isParen ( nodes [ 1 ] ) )
64+ return { start : null , end : inner ( nodes [ 1 ] ) } ;
65+ if (
66+ nodes . length === 3 &&
67+ isParen ( nodes [ 0 ] ) &&
68+ isTo ( nodes [ 1 ] ) &&
69+ isParen ( nodes [ 2 ] )
70+ )
71+ return { start : inner ( nodes [ 0 ] ) , end : inner ( nodes [ 2 ] ) } ;
72+ return null ;
13573}
13674
13775function normalizeNodeArray ( nodes ) {
@@ -709,88 +647,60 @@ module.exports = (options = {}) => {
709647 global : globalKeyframes ,
710648 } ) ;
711649 } ) ;
712- } else if ( / s c o p e $ / i. test ( atRule . name ) ) {
713- if ( atRule . params ) {
714- const ignoreComment = pureMode
715- ? getIgnoreComment ( atRule )
716- : undefined ;
717-
718- if ( ignoreComment ) {
719- ignoreComment . remove ( ) ;
720- }
650+ return ;
651+ }
721652
722- const parsed = parseScopeParams ( atRule . params ) ;
723- if ( ! parsed ) {
724- atRule . warn (
725- result ,
726- "Could not parse @scope params; selectors will not be " +
727- "localized for this rule. Params: " +
728- JSON . stringify ( atRule . params )
653+ if ( / s c o p e $ / i. test ( atRule . name ) && atRule . params ) {
654+ const ignoreComment = pureMode && getIgnoreComment ( atRule ) ;
655+ if ( ignoreComment ) ignoreComment . remove ( ) ;
656+
657+ const parsed = parseScopeParams ( atRule . params ) ;
658+ if ( ! parsed ) {
659+ atRule . warn (
660+ result ,
661+ `Could not parse @scope params; selectors will not be localized. Params: ${ JSON . stringify (
662+ atRule . params
663+ ) } `
664+ ) ;
665+ } else {
666+ const localize = ( selector ) => {
667+ const context = localizeNode (
668+ selector . trim ( ) ,
669+ options . mode ,
670+ localAliasMap
729671 ) ;
730- }
731- if ( parsed ) {
732- const localizeSelector = ( selector ) => {
733- const context = localizeNode (
734- selector ,
735- options . mode ,
736- localAliasMap
672+ if (
673+ enforcePureMode &&
674+ context . hasPureGlobals &&
675+ ! ignoreComment
676+ ) {
677+ throw atRule . error (
678+ 'Selector in at-rule"' +
679+ selector +
680+ '" is not pure ' +
681+ "(pure selectors must contain at least one local class or id)"
737682 ) ;
738- context . options = options ;
739- context . localAliasMap = localAliasMap ;
740- if (
741- enforcePureMode &&
742- context . hasPureGlobals &&
743- ! ignoreComment
744- ) {
745- throw atRule . error (
746- 'Selector in at-rule"' +
747- selector +
748- '" is not pure ' +
749- "(pure selectors must contain at least one local class or id)"
750- ) ;
751- }
752- return context . selector ;
753- } ;
754-
755- const start =
756- parsed . start !== null
757- ? localizeSelector ( parsed . start . trim ( ) )
758- : null ;
759- const end =
760- parsed . end !== null
761- ? localizeSelector ( parsed . end . trim ( ) )
762- : null ;
763-
764- if ( start !== null && end !== null ) {
765- atRule . params = `(${ start } ) to (${ end } )` ;
766- } else if ( start !== null ) {
767- atRule . params = `(${ start } )` ;
768- } else if ( end !== null ) {
769- atRule . params = `to (${ end } )` ;
770683 }
771- }
684+ return context . selector ;
685+ } ;
686+ atRule . params = [
687+ parsed . start !== null && `(${ localize ( parsed . start ) } )` ,
688+ parsed . end !== null && `to (${ localize ( parsed . end ) } )` ,
689+ ]
690+ . filter ( Boolean )
691+ . join ( " " ) ;
772692 }
693+ }
773694
774- // Guard matches the non-scope branch below — body-less @scope
775- // at-rules (or postcss-misparsed inputs) have undefined .nodes;
776- // unconditional forEach crashes.
777- if ( atRule . nodes ) {
778- atRule . nodes . forEach ( ( declaration ) => {
779- if ( declaration . type === "decl" ) {
780- localizeDeclaration ( declaration , {
781- localAliasMap,
782- options : options ,
783- global : globalMode ,
784- } ) ;
785- }
786- } ) ;
787- }
788- } else if ( atRule . nodes ) {
695+ // Localize decls in the at-rule body. Shallow on purpose — nested
696+ // rules are picked up by walkRules below. Body-less at-rules
697+ // (e.g. `@scope (.foo);`) have undefined .nodes.
698+ if ( atRule . nodes ) {
789699 atRule . nodes . forEach ( ( declaration ) => {
790700 if ( declaration . type === "decl" ) {
791701 localizeDeclaration ( declaration , {
792702 localAliasMap,
793- options : options ,
703+ options,
794704 global : globalMode ,
795705 } ) ;
796706 }
0 commit comments