@@ -106,7 +106,8 @@ const visitors = {
106
106
selectors ,
107
107
/** @type {Compiler.Css.Rule } */ ( node . metadata . rule ) ,
108
108
context . state . element ,
109
- context . state . stylesheet
109
+ context . state . stylesheet ,
110
+ true
110
111
)
111
112
) {
112
113
mark ( inner , context . state . element ) ;
@@ -120,12 +121,18 @@ const visitors = {
120
121
} ;
121
122
122
123
/**
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
124
125
* @param {Compiler.Css.ComplexSelector } node
125
126
*/
126
127
function 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
+ ) ;
129
136
} ) ;
130
137
131
138
return node . children . slice ( 0 , i + 1 ) ;
@@ -136,9 +143,10 @@ function truncate(node) {
136
143
* @param {Compiler.Css.Rule } rule
137
144
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement } element
138
145
* @param {Compiler.Css.StyleSheet } stylesheet
146
+ * @param {boolean } check_has Whether or not to check the `:has(...)` selectors
139
147
* @returns {boolean }
140
148
*/
141
- function apply_selector ( relative_selectors , rule , element , stylesheet ) {
149
+ function apply_selector ( relative_selectors , rule , element , stylesheet , check_has ) {
142
150
const parent_selectors = relative_selectors . slice ( ) ;
143
151
const relative_selector = parent_selectors . pop ( ) ;
144
152
@@ -148,88 +156,121 @@ function apply_selector(relative_selectors, rule, element, stylesheet) {
148
156
relative_selector ,
149
157
rule ,
150
158
element ,
151
- stylesheet
159
+ stylesheet ,
160
+ check_has
152
161
) ;
153
162
154
163
if ( ! possible_match ) {
155
164
return false ;
156
165
}
157
166
158
167
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
+ }
165
178
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
+ }
168
185
169
- while ( parent ) {
170
- if ( parent . type === 'Component' || parent . type === 'SvelteComponent' ) {
171
- crossed_component_boundary = true ;
172
- }
186
+ return true ;
187
+ }
173
188
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
+ }
180
222
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 ) ;
182
228
}
183
229
184
- if ( name === '>' ) return parent_matched ;
230
+ parent_matched = true ;
185
231
}
186
232
187
- parent = /** @type { Compiler.TemplateNode | null } */ ( parent . parent ) ;
233
+ if ( name === '>' ) return parent_matched ;
188
234
}
189
235
190
- return parent_matched || parent_selectors . every ( ( selector ) => is_global ( selector , rule ) ) ;
236
+ parent = /** @type { Compiler.TemplateNode | null } */ ( parent . parent ) ;
191
237
}
192
238
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
+ }
196
241
197
- let sibling_matched = false ;
242
+ case '+' :
243
+ case '~' : {
244
+ const siblings = get_possible_element_siblings ( element , name === '+' ) ;
198
245
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 ) {
207
252
mark ( relative_selector , element ) ;
208
253
sibling_matched = true ;
209
254
}
255
+ } else if (
256
+ apply_selector ( parent_selectors , rule , possible_sibling , stylesheet , check_has )
257
+ ) {
258
+ mark ( relative_selector , element ) ;
259
+ sibling_matched = true ;
210
260
}
211
-
212
- return (
213
- sibling_matched ||
214
- ( get_element_parent ( element ) === null &&
215
- parent_selectors . every ( ( selector ) => is_global ( selector , rule ) ) )
216
- ) ;
217
261
}
218
262
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
+ ) ;
222
268
}
223
- }
224
269
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 ;
230
273
}
231
-
232
- return true ;
233
274
}
234
275
235
276
/**
@@ -295,10 +336,87 @@ const regex_backslash_and_following_character = /\\(.)/g;
295
336
* @param {Compiler.Css.Rule } rule
296
337
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement } element
297
338
* @param {Compiler.Css.StyleSheet } stylesheet
339
+ * @param {boolean } check_has Whether or not to check the `:has(...)` selectors
298
340
* @returns {boolean }
299
341
*/
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
+
301
353
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 ) {
302
420
if ( selector . type === 'Percentage' || selector . type === 'Nth' ) continue ;
303
421
304
422
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,
316
434
) {
317
435
const args = selector . args ;
318
436
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 ) ;
320
438
}
321
439
322
440
// 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,
326
444
let matched = false ;
327
445
328
446
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 ) ) {
330
448
complex_selector . metadata . used = true ;
331
449
matched = true ;
332
450
}
@@ -400,7 +518,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
400
518
const parent = /** @type {Compiler.Css.Rule } */ ( rule . metadata . parent_rule ) ;
401
519
402
520
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 ) ) {
404
522
complex_selector . metadata . used = true ;
405
523
matched = true ;
406
524
}
0 commit comments