ripple 0.2.166 → 0.2.167
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/compiler/phases/1-parse/index.js +30 -1
- package/src/compiler/phases/1-parse/style.js +36 -1
- package/src/compiler/phases/2-analyze/css-analyze.js +145 -0
- package/src/compiler/phases/2-analyze/index.js +7 -0
- package/src/compiler/phases/2-analyze/prune.js +165 -11
- package/src/compiler/phases/2-analyze/validation.js +156 -0
- package/src/compiler/phases/3-transform/stylesheet.js +102 -3
- package/tests/client/css/global-additional-cases.test.ripple +702 -0
- package/tests/client/css/global-advanced-selectors.test.ripple +229 -0
- package/tests/client/css/global-at-rules.test.ripple +126 -0
- package/tests/client/css/global-basic.test.ripple +165 -0
- package/tests/client/css/global-classes-ids.test.ripple +179 -0
- package/tests/client/css/global-combinators.test.ripple +124 -0
- package/tests/client/css/global-complex-nesting.test.ripple +221 -0
- package/tests/client/css/global-edge-cases.test.ripple +200 -0
- package/tests/client/css/global-keyframes.test.ripple +101 -0
- package/tests/client/css/global-nested.test.ripple +150 -0
- package/tests/client/css/global-pseudo.test.ripple +155 -0
- package/tests/client/css/global-scoping.test.ripple +229 -0
- package/tests/client/dynamic-elements.test.ripple +0 -1
- package/tests/server/streaming-ssr.test.ripple +9 -6
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "Ripple is an elegant TypeScript UI framework",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Dominic Gannaway",
|
|
6
|
-
"version": "0.2.
|
|
6
|
+
"version": "0.2.167",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"module": "src/runtime/index-client.js",
|
|
9
9
|
"main": "src/runtime/index-client.js",
|
|
@@ -81,6 +81,6 @@
|
|
|
81
81
|
"typescript": "^5.9.2"
|
|
82
82
|
},
|
|
83
83
|
"peerDependencies": {
|
|
84
|
-
"ripple": "0.2.
|
|
84
|
+
"ripple": "0.2.167"
|
|
85
85
|
}
|
|
86
86
|
}
|
|
@@ -1382,7 +1382,36 @@ function RipplePlugin(config) {
|
|
|
1382
1382
|
element.loc.start = position;
|
|
1383
1383
|
element.metadata = {};
|
|
1384
1384
|
element.children = [];
|
|
1385
|
-
|
|
1385
|
+
|
|
1386
|
+
// Check if this is a <script> or <style> tag
|
|
1387
|
+
const tagName = this.value;
|
|
1388
|
+
const isScriptOrStyle = tagName === 'script' || tagName === 'style';
|
|
1389
|
+
|
|
1390
|
+
let open;
|
|
1391
|
+
if (isScriptOrStyle) {
|
|
1392
|
+
// Manually parse opening tag to avoid jsx_parseOpeningElementAt consuming content
|
|
1393
|
+
const tagStart = this.start;
|
|
1394
|
+
const tagEndPos = this.input.indexOf('>', tagStart);
|
|
1395
|
+
|
|
1396
|
+
open = {
|
|
1397
|
+
type: 'JSXOpeningElement',
|
|
1398
|
+
name: { type: 'JSXIdentifier', name: tagName },
|
|
1399
|
+
attributes: [],
|
|
1400
|
+
selfClosing: false,
|
|
1401
|
+
end: tagEndPos + 1,
|
|
1402
|
+
loc: {
|
|
1403
|
+
end: {
|
|
1404
|
+
line: this.curLine,
|
|
1405
|
+
column: tagEndPos + 1,
|
|
1406
|
+
},
|
|
1407
|
+
},
|
|
1408
|
+
};
|
|
1409
|
+
|
|
1410
|
+
// Position after the '>'
|
|
1411
|
+
this.pos = tagEndPos + 1;
|
|
1412
|
+
} else {
|
|
1413
|
+
open = this.jsx_parseOpeningElementAt();
|
|
1414
|
+
}
|
|
1386
1415
|
|
|
1387
1416
|
// Check if this is a namespaced element (tsx:react)
|
|
1388
1417
|
const is_tsx_compat = open.name.type === 'JSXNamespacedName';
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { hash } from '../../utils.js';
|
|
2
2
|
|
|
3
|
+
const REGEX_MATCHER = /^[~^$*|]?=/;
|
|
4
|
+
const REGEX_ATTRIBUTE_FLAGS = /^[a-zA-Z]+/;
|
|
3
5
|
const REGEX_COMMENT_CLOSE = /\*\//;
|
|
4
6
|
const REGEX_HTML_COMMENT_CLOSE = /-->/;
|
|
5
7
|
const REGEX_PERCENTAGE = /^\d+(\.\d+)?%/;
|
|
@@ -155,7 +157,7 @@ function read_at_rule(parser) {
|
|
|
155
157
|
end: parser.index,
|
|
156
158
|
name,
|
|
157
159
|
prelude,
|
|
158
|
-
block
|
|
160
|
+
block,
|
|
159
161
|
};
|
|
160
162
|
}
|
|
161
163
|
|
|
@@ -553,6 +555,39 @@ function read_selector(parser, inside_pseudo_class = false) {
|
|
|
553
555
|
throw new Error('Unexpected end of input');
|
|
554
556
|
}
|
|
555
557
|
|
|
558
|
+
/**
|
|
559
|
+
* Read a property that may or may not be quoted, e.g.
|
|
560
|
+
* `foo` or `'foo bar'` or `"foo bar"`
|
|
561
|
+
* @param {Parser} parser
|
|
562
|
+
*/
|
|
563
|
+
function read_attribute_value(parser) {
|
|
564
|
+
let value = '';
|
|
565
|
+
let escaped = false;
|
|
566
|
+
const quote_mark = parser.eat('"') ? '"' : parser.eat("'") ? "'" : null;
|
|
567
|
+
|
|
568
|
+
while (parser.index < parser.template.length) {
|
|
569
|
+
const char = parser.template[parser.index];
|
|
570
|
+
if (escaped) {
|
|
571
|
+
value += '\\' + char;
|
|
572
|
+
escaped = false;
|
|
573
|
+
} else if (char === '\\') {
|
|
574
|
+
escaped = true;
|
|
575
|
+
} else if (quote_mark ? char === quote_mark : /[\s\]]/.test(char)) {
|
|
576
|
+
if (quote_mark) {
|
|
577
|
+
parser.eat(quote_mark, true);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
return value.trim();
|
|
581
|
+
} else {
|
|
582
|
+
value += char;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
parser.index++;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
throw new Error('Unexpected end of input');
|
|
589
|
+
}
|
|
590
|
+
|
|
556
591
|
function read_identifier(parser) {
|
|
557
592
|
const start = parser.index;
|
|
558
593
|
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { walk } from 'zimmerframe';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* True if is `:global` without arguments
|
|
5
|
+
* @param {any} simple_selector
|
|
6
|
+
*/
|
|
7
|
+
function is_global_block_selector(simple_selector) {
|
|
8
|
+
return (
|
|
9
|
+
simple_selector.type === 'PseudoClassSelector' &&
|
|
10
|
+
simple_selector.name === 'global' &&
|
|
11
|
+
simple_selector.args === null
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* True if is `:global(...)` or `:global` and no pseudo class that is scoped.
|
|
17
|
+
* @param {any} relative_selector
|
|
18
|
+
*/
|
|
19
|
+
function is_global(relative_selector) {
|
|
20
|
+
const first = relative_selector.selectors[0];
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
first?.type === 'PseudoClassSelector' &&
|
|
24
|
+
first.name === 'global' &&
|
|
25
|
+
(first.args === null ||
|
|
26
|
+
// Only these two selector types keep the whole selector global, because e.g.
|
|
27
|
+
// :global(button).x means that the selector is still scoped because of the .x
|
|
28
|
+
relative_selector.selectors.every(
|
|
29
|
+
(selector) =>
|
|
30
|
+
selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector',
|
|
31
|
+
))
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Analyze CSS and set metadata for global selectors
|
|
37
|
+
* @param {any} css - The CSS AST
|
|
38
|
+
*/
|
|
39
|
+
export function analyze_css(css) {
|
|
40
|
+
walk(css, { rule: null }, {
|
|
41
|
+
Rule(node, context) {
|
|
42
|
+
node.metadata.parent_rule = context.state.rule;
|
|
43
|
+
|
|
44
|
+
// Check for :global blocks
|
|
45
|
+
// A global block is when the selector starts with :global and has no local selectors before it
|
|
46
|
+
for (const complex_selector of node.prelude.children) {
|
|
47
|
+
let is_global_block = false;
|
|
48
|
+
|
|
49
|
+
for (
|
|
50
|
+
let selector_idx = 0;
|
|
51
|
+
selector_idx < complex_selector.children.length;
|
|
52
|
+
selector_idx++
|
|
53
|
+
) {
|
|
54
|
+
const child = complex_selector.children[selector_idx];
|
|
55
|
+
const idx = child.selectors.findIndex(is_global_block_selector);
|
|
56
|
+
|
|
57
|
+
if (is_global_block) {
|
|
58
|
+
// All selectors after :global are unscoped
|
|
59
|
+
child.metadata.is_global_like = true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Only set is_global_block if this is the FIRST RelativeSelector and it starts with :global
|
|
63
|
+
if (selector_idx === 0 && idx === 0) {
|
|
64
|
+
// `child` starts with `:global` and is the first selector in the chain
|
|
65
|
+
is_global_block = true;
|
|
66
|
+
node.metadata.is_global_block = is_global_block;
|
|
67
|
+
} else if (idx === 0) {
|
|
68
|
+
// :global appears later in the selector chain (e.g., `div :global p`)
|
|
69
|
+
// Set is_global_block for marking subsequent selectors as global-like
|
|
70
|
+
is_global_block = true;
|
|
71
|
+
} else if (idx !== -1) {
|
|
72
|
+
// `:global` is not at the start - this is invalid but we'll let it through for now
|
|
73
|
+
// The transform phase will handle removal
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Pass the current rule as state to nested nodes
|
|
79
|
+
const state = { rule: node };
|
|
80
|
+
context.visit(node.prelude, state);
|
|
81
|
+
context.visit(node.block, state);
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
ComplexSelector(node, context) {
|
|
85
|
+
// Set the rule metadata before analyzing children
|
|
86
|
+
node.metadata.rule = context.state.rule;
|
|
87
|
+
|
|
88
|
+
context.next(); // analyse relevant selectors first
|
|
89
|
+
|
|
90
|
+
{
|
|
91
|
+
const global = node.children.find(is_global);
|
|
92
|
+
|
|
93
|
+
if (global) {
|
|
94
|
+
const idx = node.children.indexOf(global);
|
|
95
|
+
if (global.selectors[0].args !== null && idx !== 0 && idx !== node.children.length - 1) {
|
|
96
|
+
// ensure `:global(...)` is not used in the middle of a selector (but multiple `global(...)` in sequence are ok)
|
|
97
|
+
for (let i = idx + 1; i < node.children.length; i++) {
|
|
98
|
+
if (!is_global(node.children[i])) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
`:global(...) can be at the start or end of a selector sequence, but not in the middle`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Set is_global metadata
|
|
109
|
+
node.metadata.is_global = node.children.every(
|
|
110
|
+
({ metadata }) => metadata.is_global || metadata.is_global_like,
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
node.metadata.used ||= node.metadata.is_global;
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
PseudoClassSelector(node, context) {
|
|
117
|
+
// Walk into :is(), :where(), :has(), and :not() to initialize metadata for nested selectors
|
|
118
|
+
if (
|
|
119
|
+
(node.name === 'is' || node.name === 'where' || node.name === 'has' || node.name === 'not') &&
|
|
120
|
+
node.args
|
|
121
|
+
) {
|
|
122
|
+
context.next();
|
|
123
|
+
}
|
|
124
|
+
}, RelativeSelector(node, context) {
|
|
125
|
+
// Check if this selector is a :global selector
|
|
126
|
+
node.metadata.is_global = node.selectors.length >= 1 && is_global(node);
|
|
127
|
+
|
|
128
|
+
// Check for :root and other global-like selectors
|
|
129
|
+
if (
|
|
130
|
+
node.selectors.length >= 1 &&
|
|
131
|
+
node.selectors.every(
|
|
132
|
+
(selector) =>
|
|
133
|
+
selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector',
|
|
134
|
+
)
|
|
135
|
+
) {
|
|
136
|
+
const first = node.selectors[0];
|
|
137
|
+
node.metadata.is_global_like ||=
|
|
138
|
+
(first.type === 'PseudoClassSelector' && first.name === 'host') ||
|
|
139
|
+
(first.type === 'PseudoClassSelector' && first.name === 'root');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
context.next();
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
}
|
|
@@ -13,8 +13,10 @@ import {
|
|
|
13
13
|
import { extract_paths } from '../../../utils/ast.js';
|
|
14
14
|
import is_reference from 'is-reference';
|
|
15
15
|
import { prune_css } from './prune.js';
|
|
16
|
+
import { analyze_css } from './css-analyze.js';
|
|
16
17
|
import { error } from '../../errors.js';
|
|
17
18
|
import { is_event_attribute } from '../../../utils/events.js';
|
|
19
|
+
import { validate_nesting } from './validation.js';
|
|
18
20
|
|
|
19
21
|
const valid_in_head = new Set(['title', 'base', 'link', 'meta', 'style', 'script', 'noscript']);
|
|
20
22
|
|
|
@@ -349,6 +351,9 @@ const visitors = {
|
|
|
349
351
|
const css = node.css;
|
|
350
352
|
|
|
351
353
|
if (css !== null) {
|
|
354
|
+
// Analyze CSS to set global selector metadata
|
|
355
|
+
analyze_css(css);
|
|
356
|
+
|
|
352
357
|
for (const node of elements) {
|
|
353
358
|
prune_css(css, node);
|
|
354
359
|
}
|
|
@@ -642,6 +647,8 @@ const visitors = {
|
|
|
642
647
|
|
|
643
648
|
mark_control_flow_has_template(path);
|
|
644
649
|
|
|
650
|
+
validate_nesting(node, state, context);
|
|
651
|
+
|
|
645
652
|
// Store capitalized name for dynamic components/elements
|
|
646
653
|
if (node.id.tracked) {
|
|
647
654
|
const original_name = node.id.name;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { walk } from 'zimmerframe';
|
|
2
|
+
import { is_element_dom_element } from '../../utils.js';
|
|
2
3
|
|
|
3
4
|
const seen = new Set();
|
|
4
5
|
const regex_backslash_and_following_character = /\\(.)/g;
|
|
@@ -216,6 +217,41 @@ function get_descendant_elements(node, adjacent_only) {
|
|
|
216
217
|
return descendants;
|
|
217
218
|
}
|
|
218
219
|
|
|
220
|
+
/**
|
|
221
|
+
* Check if an element can render dynamic content that might affect CSS matching
|
|
222
|
+
* @param {any} element
|
|
223
|
+
* @param {boolean} check_classes - Whether to check for dynamic class attributes
|
|
224
|
+
* @returns {boolean}
|
|
225
|
+
*/
|
|
226
|
+
function can_render_dynamic_content(element, check_classes = false) {
|
|
227
|
+
if (!is_element_dom_element(element)) {
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Either a dynamic element or component (only can tell at runtime)
|
|
232
|
+
// But dynamic elements should return false ideally
|
|
233
|
+
if (element.id?.tracked) {
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Check for dynamic class attributes if requested (for class-based selectors)
|
|
238
|
+
if (check_classes && element.attributes) {
|
|
239
|
+
for (const attr of element.attributes) {
|
|
240
|
+
if (attr.type === 'Attribute' && attr.name?.name === 'class') {
|
|
241
|
+
// Check if class value is an expression (not a static string)
|
|
242
|
+
if (attr.value && typeof attr.value === 'object') {
|
|
243
|
+
// If it's a CallExpression or other dynamic value, it's dynamic
|
|
244
|
+
if (attr.value.type !== 'Literal' && attr.value.type !== 'Text') {
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
|
|
219
255
|
function get_possible_element_siblings(node, direction, adjacent_only) {
|
|
220
256
|
const siblings = new Map();
|
|
221
257
|
const parent = get_element_parent(node);
|
|
@@ -248,7 +284,12 @@ function get_possible_element_siblings(node, direction, adjacent_only) {
|
|
|
248
284
|
|
|
249
285
|
if (sibling.type === 'Element' || sibling.type === 'Component') {
|
|
250
286
|
siblings.set(sibling, true);
|
|
251
|
-
|
|
287
|
+
// Don't break for dynamic elements (children, Components, dynamic components)
|
|
288
|
+
// as they can render dynamic content or might render nothing
|
|
289
|
+
const isDynamic = can_render_dynamic_content(sibling, false);
|
|
290
|
+
if (adjacent_only && !isDynamic) {
|
|
291
|
+
break; // Only immediate sibling for '+' combinator
|
|
292
|
+
}
|
|
252
293
|
}
|
|
253
294
|
// Stop at non-whitespace text nodes for adjacent selectors
|
|
254
295
|
else if (adjacent_only && sibling.type === 'Text' && sibling.value?.trim()) {
|
|
@@ -295,11 +336,64 @@ function apply_combinator(relative_selector, rest_selectors, rule, node, directi
|
|
|
295
336
|
let sibling_matched = false;
|
|
296
337
|
|
|
297
338
|
for (const possible_sibling of siblings.keys()) {
|
|
298
|
-
if
|
|
299
|
-
|
|
339
|
+
// Check if this sibling can render dynamic content
|
|
340
|
+
// For class selectors, also check if element has dynamic classes
|
|
341
|
+
const has_class_selector = rest_selectors.some((sel) =>
|
|
342
|
+
sel.selectors?.some((s) => s.type === 'ClassSelector'),
|
|
343
|
+
);
|
|
344
|
+
const is_dynamic = can_render_dynamic_content(possible_sibling, has_class_selector);
|
|
345
|
+
|
|
346
|
+
if (is_dynamic) {
|
|
347
|
+
if (rest_selectors.length > 0) {
|
|
348
|
+
// Check if the first selector in the rest is global
|
|
349
|
+
const first_rest_selector = rest_selectors[0];
|
|
350
|
+
if (is_global(first_rest_selector, rule)) {
|
|
351
|
+
// Global selector followed by possibly more selectors
|
|
352
|
+
// Check if remaining selectors could match elements after this component
|
|
353
|
+
const remaining = rest_selectors.slice(1);
|
|
354
|
+
if (remaining.length === 0) {
|
|
355
|
+
// Just a global selector, mark as matched
|
|
356
|
+
sibling_matched = true;
|
|
357
|
+
} else {
|
|
358
|
+
// Check if there are any elements after this component that could match the remaining selectors
|
|
359
|
+
const parent = get_element_parent(node);
|
|
360
|
+
if (parent) {
|
|
361
|
+
const container = parent.children || parent.body || [];
|
|
362
|
+
const component_index = container.indexOf(possible_sibling);
|
|
363
|
+
|
|
364
|
+
// For adjacent combinator, only check immediate next element
|
|
365
|
+
// For general sibling, check all following elements
|
|
366
|
+
const search_start = component_index + 1;
|
|
367
|
+
const search_end = combinator.name === '+' ? search_start + 1 : container.length;
|
|
368
|
+
|
|
369
|
+
for (let i = search_start; i < search_end; i++) {
|
|
370
|
+
const subsequent = container[i];
|
|
371
|
+
if (subsequent.type === 'Element') {
|
|
372
|
+
if (apply_selector(remaining, rule, subsequent, direction)) {
|
|
373
|
+
sibling_matched = true;
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
if (combinator.name === '+') break; // For adjacent, only check first element
|
|
377
|
+
} else if (subsequent.type === 'Component') {
|
|
378
|
+
// Skip components when looking for the target element
|
|
379
|
+
if (combinator.name === '+') {
|
|
380
|
+
// For adjacent, continue looking
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
} else if (rest_selectors.length === 1 && rest_selectors[0].metadata.is_global) {
|
|
389
|
+
// Single global selector always matches
|
|
300
390
|
sibling_matched = true;
|
|
301
391
|
}
|
|
302
|
-
|
|
392
|
+
// Don't apply_selector for dynamic elements - they won't match regular element selectors
|
|
393
|
+
} else if (
|
|
394
|
+
possible_sibling.type === 'Element' &&
|
|
395
|
+
apply_selector(rest_selectors, rule, possible_sibling, direction)
|
|
396
|
+
) {
|
|
303
397
|
sibling_matched = true;
|
|
304
398
|
}
|
|
305
399
|
}
|
|
@@ -338,19 +432,67 @@ function get_element_parent(node) {
|
|
|
338
432
|
return null;
|
|
339
433
|
}
|
|
340
434
|
|
|
435
|
+
/**
|
|
436
|
+
* `true` if is a pseudo class that cannot be or is not scoped
|
|
437
|
+
* @param {Compiler.AST.CSS.SimpleSelector} selector
|
|
438
|
+
*/
|
|
439
|
+
function is_unscoped_pseudo_class(selector) {
|
|
440
|
+
return (
|
|
441
|
+
selector.type === 'PseudoClassSelector' &&
|
|
442
|
+
// These make the selector scoped
|
|
443
|
+
((selector.name !== 'has' &&
|
|
444
|
+
selector.name !== 'is' &&
|
|
445
|
+
selector.name !== 'where' &&
|
|
446
|
+
// :not is special because we want to scope as specific as possible, but because :not
|
|
447
|
+
// inverses the result, we want to leave the unscoped, too. The exception is more than
|
|
448
|
+
// one selector in the :not (.e.g :not(.x .y)), then .x and .y should be scoped
|
|
449
|
+
(selector.name !== 'not' ||
|
|
450
|
+
selector.args === null ||
|
|
451
|
+
selector.args.children.every((c) => c.children.length === 1))) ||
|
|
452
|
+
// selectors with has/is/where/not can also be global if all their children are global
|
|
453
|
+
selector.args === null ||
|
|
454
|
+
selector.args.children.every((c) => c.children.every((r) => is_global_simple(r))))
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* True if is `:global(...)` or `:global` and no pseudo class that is scoped.
|
|
460
|
+
* @param {Compiler.AST.CSS.RelativeSelector} relative_selector
|
|
461
|
+
*/
|
|
462
|
+
function is_global_simple(relative_selector) {
|
|
463
|
+
const first = relative_selector.selectors[0];
|
|
464
|
+
|
|
465
|
+
return (
|
|
466
|
+
first.type === 'PseudoClassSelector' &&
|
|
467
|
+
first.name === 'global' &&
|
|
468
|
+
(first.args === null ||
|
|
469
|
+
// Only these two selector types keep the whole selector global, because e.g.
|
|
470
|
+
// :global(button).x means that the selector is still scoped because of the .x
|
|
471
|
+
relative_selector.selectors.every(
|
|
472
|
+
(selector) =>
|
|
473
|
+
is_unscoped_pseudo_class(selector) || selector.type === 'PseudoElementSelector',
|
|
474
|
+
))
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
|
|
341
478
|
function is_global(selector, rule) {
|
|
342
479
|
if (selector.metadata.is_global || selector.metadata.is_global_like) {
|
|
343
480
|
return true;
|
|
344
481
|
}
|
|
345
482
|
|
|
483
|
+
let explicitly_global = false;
|
|
484
|
+
|
|
346
485
|
for (const s of selector.selectors) {
|
|
347
486
|
/** @type {Compiler.AST.CSS.SelectorList | null} */
|
|
348
487
|
let selector_list = null;
|
|
488
|
+
let can_be_global = false;
|
|
349
489
|
let owner = rule;
|
|
350
490
|
|
|
351
491
|
if (s.type === 'PseudoClassSelector') {
|
|
352
492
|
if ((s.name === 'is' || s.name === 'where') && s.args) {
|
|
353
493
|
selector_list = s.args;
|
|
494
|
+
} else {
|
|
495
|
+
can_be_global = is_unscoped_pseudo_class(s);
|
|
354
496
|
}
|
|
355
497
|
}
|
|
356
498
|
|
|
@@ -359,18 +501,19 @@ function is_global(selector, rule) {
|
|
|
359
501
|
selector_list = owner.prelude;
|
|
360
502
|
}
|
|
361
503
|
|
|
362
|
-
const has_global_selectors = selector_list?.children.some((complex_selector) => {
|
|
504
|
+
const has_global_selectors = !!selector_list?.children.some((complex_selector) => {
|
|
363
505
|
return complex_selector.children.every((relative_selector) =>
|
|
364
506
|
is_global(relative_selector, owner),
|
|
365
507
|
);
|
|
366
508
|
});
|
|
509
|
+
explicitly_global ||= has_global_selectors;
|
|
367
510
|
|
|
368
|
-
if (!has_global_selectors) {
|
|
511
|
+
if (!has_global_selectors && !can_be_global) {
|
|
369
512
|
return false;
|
|
370
513
|
}
|
|
371
514
|
}
|
|
372
515
|
|
|
373
|
-
return
|
|
516
|
+
return explicitly_global || selector.selectors.length === 0;
|
|
374
517
|
}
|
|
375
518
|
|
|
376
519
|
function is_text_attribute(attribute) {
|
|
@@ -425,6 +568,7 @@ function is_outer_global(relative_selector) {
|
|
|
425
568
|
const first = relative_selector.selectors[0];
|
|
426
569
|
|
|
427
570
|
return (
|
|
571
|
+
first &&
|
|
428
572
|
first.type === 'PseudoClassSelector' &&
|
|
429
573
|
first.name === 'global' &&
|
|
430
574
|
(first.args === null ||
|
|
@@ -710,7 +854,7 @@ export function prune_css(css, element) {
|
|
|
710
854
|
context.next();
|
|
711
855
|
}
|
|
712
856
|
},
|
|
713
|
-
ComplexSelector(node) {
|
|
857
|
+
ComplexSelector(node, context) {
|
|
714
858
|
const selectors = get_relative_selectors(node);
|
|
715
859
|
|
|
716
860
|
seen.clear();
|
|
@@ -726,9 +870,19 @@ export function prune_css(css, element) {
|
|
|
726
870
|
node.metadata.used = true;
|
|
727
871
|
}
|
|
728
872
|
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
873
|
+
context.next();
|
|
874
|
+
},
|
|
875
|
+
PseudoClassSelector(node, context) {
|
|
876
|
+
// Visit nested selectors inside :has(), :is(), :where(), and :not()
|
|
877
|
+
if (
|
|
878
|
+
(node.name === 'has' ||
|
|
879
|
+
node.name === 'is' ||
|
|
880
|
+
node.name === 'where' ||
|
|
881
|
+
node.name === 'not') &&
|
|
882
|
+
node.args
|
|
883
|
+
) {
|
|
884
|
+
context.next();
|
|
885
|
+
}
|
|
732
886
|
},
|
|
733
887
|
});
|
|
734
888
|
}
|