@@ -106,7 +106,8 @@ const visitors = {
106106 selectors ,
107107 /** @type {Compiler.Css.Rule } */ ( node . metadata . rule ) ,
108108 context . state . element ,
109- context . state . stylesheet
109+ context . state . stylesheet ,
110+ true
110111 )
111112 ) {
112113 mark ( inner , context . state . element ) ;
@@ -120,12 +121,18 @@ const visitors = {
120121} ;
121122
122123/**
123- * Discard trailing `:global(...)` selectors, these are unused for scoping purposes
124+ * Discard trailing `:global(...)` selectors without a `:has(...)` modifier , these are unused for scoping purposes
124125 * @param {Compiler.Css.ComplexSelector } node
125126 */
126127function truncate ( node ) {
127- const i = node . children . findLastIndex ( ( { metadata } ) => {
128- return ! metadata . is_global && ! metadata . is_global_like ;
128+ const i = node . children . findLastIndex ( ( { metadata, selectors } ) => {
129+ return (
130+ ! metadata . is_global_like &&
131+ ( ! metadata . is_global ||
132+ selectors . some (
133+ ( selector ) => selector . type === 'PseudoClassSelector' && selector . name === 'has'
134+ ) )
135+ ) ;
129136 } ) ;
130137
131138 return node . children . slice ( 0 , i + 1 ) ;
@@ -136,9 +143,10 @@ function truncate(node) {
136143 * @param {Compiler.Css.Rule } rule
137144 * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement } element
138145 * @param {Compiler.Css.StyleSheet } stylesheet
146+ * @param {boolean } check_has Whether or not to check the `:has(...)` selectors
139147 * @returns {boolean }
140148 */
141- function apply_selector ( relative_selectors , rule , element , stylesheet ) {
149+ function apply_selector ( relative_selectors , rule , element , stylesheet , check_has ) {
142150 const parent_selectors = relative_selectors . slice ( ) ;
143151 const relative_selector = parent_selectors . pop ( ) ;
144152
@@ -148,88 +156,121 @@ function apply_selector(relative_selectors, rule, element, stylesheet) {
148156 relative_selector ,
149157 rule ,
150158 element ,
151- stylesheet
159+ stylesheet ,
160+ check_has
152161 ) ;
153162
154163 if ( ! possible_match ) {
155164 return false ;
156165 }
157166
158167 if ( relative_selector . combinator ) {
159- const name = relative_selector . combinator . name ;
160-
161- switch ( name ) {
162- case ' ' :
163- case '>' : {
164- let parent = /** @type {Compiler.TemplateNode | null } */ ( element . parent ) ;
168+ return apply_combinator (
169+ relative_selector . combinator ,
170+ relative_selector ,
171+ parent_selectors ,
172+ rule ,
173+ element ,
174+ stylesheet ,
175+ check_has
176+ ) ;
177+ }
165178
166- let parent_matched = false ;
167- let crossed_component_boundary = false ;
179+ // if this is the left-most non-global selector, mark it — we want
180+ // `x y z {...}` to become `x.blah y z.blah {...}`
181+ const parent = parent_selectors [ parent_selectors . length - 1 ] ;
182+ if ( ! parent || is_global ( parent , rule ) ) {
183+ mark ( relative_selector , element ) ;
184+ }
168185
169- while ( parent ) {
170- if ( parent . type === 'Component' || parent . type === 'SvelteComponent' ) {
171- crossed_component_boundary = true ;
172- }
186+ return true ;
187+ }
173188
174- if ( parent . type === 'RegularElement' || parent . type === 'SvelteElement' ) {
175- if ( apply_selector ( parent_selectors , rule , parent , stylesheet ) ) {
176- // TODO the `name === ' '` causes false positives, but removing it causes false negatives...
177- if ( name === ' ' || crossed_component_boundary ) {
178- mark ( parent_selectors [ parent_selectors . length - 1 ] , parent ) ;
179- }
189+ /**
190+ * @param {Compiler.Css.Combinator } combinator
191+ * @param {Compiler.Css.RelativeSelector } relative_selector
192+ * @param {Compiler.Css.RelativeSelector[] } parent_selectors
193+ * @param {Compiler.Css.Rule } rule
194+ * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement } element
195+ * @param {Compiler.Css.StyleSheet } stylesheet
196+ * @param {boolean } check_has Whether or not to check the `:has(...)` selectors
197+ * @returns {boolean }
198+ */
199+ function apply_combinator (
200+ combinator ,
201+ relative_selector ,
202+ parent_selectors ,
203+ rule ,
204+ element ,
205+ stylesheet ,
206+ check_has
207+ ) {
208+ const name = combinator . name ;
209+
210+ switch ( name ) {
211+ case ' ' :
212+ case '>' : {
213+ let parent = /** @type {Compiler.TemplateNode | null } */ ( element . parent ) ;
214+
215+ let parent_matched = false ;
216+ let crossed_component_boundary = false ;
217+
218+ while ( parent ) {
219+ if ( parent . type === 'Component' || parent . type === 'SvelteComponent' ) {
220+ crossed_component_boundary = true ;
221+ }
180222
181- parent_matched = true ;
223+ if ( parent . type === 'RegularElement' || parent . type === 'SvelteElement' ) {
224+ if ( apply_selector ( parent_selectors , rule , parent , stylesheet , check_has ) ) {
225+ // TODO the `name === ' '` causes false positives, but removing it causes false negatives...
226+ if ( name === ' ' || crossed_component_boundary ) {
227+ mark ( parent_selectors [ parent_selectors . length - 1 ] , parent ) ;
182228 }
183229
184- if ( name === '>' ) return parent_matched ;
230+ parent_matched = true ;
185231 }
186232
187- parent = /** @type { Compiler.TemplateNode | null } */ ( parent . parent ) ;
233+ if ( name === '>' ) return parent_matched ;
188234 }
189235
190- return parent_matched || parent_selectors . every ( ( selector ) => is_global ( selector , rule ) ) ;
236+ parent = /** @type { Compiler.TemplateNode | null } */ ( parent . parent ) ;
191237 }
192238
193- case '+' :
194- case '~' : {
195- const siblings = get_possible_element_siblings ( element , name === '+' ) ;
239+ return parent_matched || parent_selectors . every ( ( selector ) => is_global ( selector , rule ) ) ;
240+ }
196241
197- let sibling_matched = false ;
242+ case '+' :
243+ case '~' : {
244+ const siblings = get_possible_element_siblings ( element , name === '+' ) ;
198245
199- for ( const possible_sibling of siblings . keys ( ) ) {
200- if ( possible_sibling . type === 'RenderTag' || possible_sibling . type === 'SlotElement' ) {
201- // `{@render foo()}<p>foo</p>` with `:global(.x) + p` is a match
202- if ( parent_selectors . length === 1 && parent_selectors [ 0 ] . metadata . is_global ) {
203- mark ( relative_selector , element ) ;
204- sibling_matched = true ;
205- }
206- } else if ( apply_selector ( parent_selectors , rule , possible_sibling , stylesheet ) ) {
246+ let sibling_matched = false ;
247+
248+ for ( const possible_sibling of siblings . keys ( ) ) {
249+ if ( possible_sibling . type === 'RenderTag' || possible_sibling . type === 'SlotElement' ) {
250+ // `{@render foo()}<p>foo</p>` with `:global(.x) + p` is a match
251+ if ( parent_selectors . length === 1 && parent_selectors [ 0 ] . metadata . is_global ) {
207252 mark ( relative_selector , element ) ;
208253 sibling_matched = true ;
209254 }
255+ } else if (
256+ apply_selector ( parent_selectors , rule , possible_sibling , stylesheet , check_has )
257+ ) {
258+ mark ( relative_selector , element ) ;
259+ sibling_matched = true ;
210260 }
211-
212- return (
213- sibling_matched ||
214- ( get_element_parent ( element ) === null &&
215- parent_selectors . every ( ( selector ) => is_global ( selector , rule ) ) )
216- ) ;
217261 }
218262
219- default :
220- // TODO other combinators
221- return true ;
263+ return (
264+ sibling_matched ||
265+ ( get_element_parent ( element ) === null &&
266+ parent_selectors . every ( ( selector ) => is_global ( selector , rule ) ) )
267+ ) ;
222268 }
223- }
224269
225- // if this is the left-most non-global selector, mark it — we want
226- // `x y z {...}` to become `x.blah y z.blah {...}`
227- const parent = parent_selectors [ parent_selectors . length - 1 ] ;
228- if ( ! parent || is_global ( parent , rule ) ) {
229- mark ( relative_selector , element ) ;
270+ default :
271+ // TODO other combinators
272+ return true ;
230273 }
231-
232- return true ;
233274}
234275
235276/**
@@ -295,10 +336,87 @@ const regex_backslash_and_following_character = /\\(.)/g;
295336 * @param {Compiler.Css.Rule } rule
296337 * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement } element
297338 * @param {Compiler.Css.StyleSheet } stylesheet
339+ * @param {boolean } check_has Whether or not to check the `:has(...)` selectors
298340 * @returns {boolean }
299341 */
300- function relative_selector_might_apply_to_node ( relative_selector , rule , element , stylesheet ) {
342+ function relative_selector_might_apply_to_node (
343+ relative_selector ,
344+ rule ,
345+ element ,
346+ stylesheet ,
347+ check_has
348+ ) {
349+ // Sort :has(...) selectors in one bucket and everything else into another
350+ const has_selectors = [ ] ;
351+ const other_selectors = [ ] ;
352+
301353 for ( const selector of relative_selector . selectors ) {
354+ if ( selector . type === 'PseudoClassSelector' && selector . name === 'has' && selector . args ) {
355+ has_selectors . push ( selector ) ;
356+ } else {
357+ other_selectors . push ( selector ) ;
358+ }
359+ }
360+
361+ // If we're called recursively from a :has(...) selector, we're on the way of checking if the other selectors match.
362+ // In that case ignore this check (because we just came from this) to avoid an infinite loop.
363+ if ( check_has && has_selectors . length > 0 ) {
364+ // :has(...) is special in that it means "look downwards in the CSS tree". Since our matching algorithm goes
365+ // upwards and back-to-front, we need to first check the selectors inside :has(...), then check the rest of the
366+ // selector in a way that is similar to ancestor matching. In a sense, we're treating `.x:has(.y)` as `.x .y`.
367+ for ( const has_selector of has_selectors ) {
368+ const complex_selectors = /** @type {Compiler.Css.SelectorList } */ ( has_selector . args )
369+ . children ;
370+ let matched = false ;
371+
372+ for ( const complex_selector of complex_selectors ) {
373+ const selectors = truncate ( complex_selector ) ;
374+ if (
375+ selectors . length === 0 /* is :global(...) */ ||
376+ apply_selector ( selectors , rule , element , stylesheet , check_has )
377+ ) {
378+ // Treat e.g. `.x:has(.y)` as `.x .y` with the .y part already being matched,
379+ // and now looking upwards for the .x part.
380+ if (
381+ apply_combinator (
382+ selectors [ 0 ] ?. combinator ?? descendant_combinator ,
383+ selectors [ 0 ] ?? [ ] ,
384+ [ relative_selector ] ,
385+ rule ,
386+ element ,
387+ stylesheet ,
388+ false
389+ )
390+ ) {
391+ complex_selector . metadata . used = true ;
392+ matched = true ;
393+ }
394+ }
395+ }
396+
397+ if ( ! matched ) {
398+ if ( relative_selector . metadata . is_global && ! relative_selector . metadata . is_global_like ) {
399+ // Edge case: `:global(.x):has(.y)` where `.x` is global but `.y` doesn't match.
400+ // Since `used` is set to `true` for `:global(.x)` in css-analyze beforehand, and
401+ // we have no way of knowing if it's safe to set it back to `false`, we'll mark
402+ // the inner selector as used and scoped to prevent it from being pruned, which could
403+ // result in a invalid CSS output (e.g. `.x:has(/* unused .y */)`). The result
404+ // can't match a real element, so the only drawback is the missing prune.
405+ // TODO clean this up some day
406+ complex_selectors [ 0 ] . metadata . used = true ;
407+ complex_selectors [ 0 ] . children . forEach ( ( selector ) => {
408+ selector . metadata . scoped = true ;
409+ } ) ;
410+ }
411+
412+ return false ;
413+ }
414+ }
415+
416+ return true ;
417+ }
418+
419+ for ( const selector of other_selectors ) {
302420 if ( selector . type === 'Percentage' || selector . type === 'Nth' ) continue ;
303421
304422 const name = selector . name . replace ( regex_backslash_and_following_character , '$1' ) ;
@@ -316,7 +434,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
316434 ) {
317435 const args = selector . args ;
318436 const complex_selector = args . children [ 0 ] ;
319- return apply_selector ( complex_selector . children , rule , element , stylesheet ) ;
437+ return apply_selector ( complex_selector . children , rule , element , stylesheet , check_has ) ;
320438 }
321439
322440 // We came across a :global, everything beyond it is global and therefore a potential match
@@ -326,7 +444,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
326444 let matched = false ;
327445
328446 for ( const complex_selector of selector . args . children ) {
329- if ( apply_selector ( truncate ( complex_selector ) , rule , element , stylesheet ) ) {
447+ if ( apply_selector ( truncate ( complex_selector ) , rule , element , stylesheet , check_has ) ) {
330448 complex_selector . metadata . used = true ;
331449 matched = true ;
332450 }
@@ -400,7 +518,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
400518 const parent = /** @type {Compiler.Css.Rule } */ ( rule . metadata . parent_rule ) ;
401519
402520 for ( const complex_selector of parent . prelude . children ) {
403- if ( apply_selector ( truncate ( complex_selector ) , parent , element , stylesheet ) ) {
521+ if ( apply_selector ( truncate ( complex_selector ) , parent , element , stylesheet , check_has ) ) {
404522 complex_selector . metadata . used = true ;
405523 matched = true ;
406524 }
0 commit comments