ripple 0.3.13 → 0.3.14
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/CHANGELOG.md +21 -0
- package/package.json +5 -30
- package/src/runtime/array.js +38 -38
- package/src/runtime/create-subscriber.js +2 -2
- package/src/runtime/internal/client/bindings.js +4 -6
- package/src/runtime/internal/client/events.js +8 -3
- package/src/runtime/internal/client/hmr.js +5 -17
- package/src/runtime/internal/client/runtime.js +1 -0
- package/src/runtime/internal/server/blocks.js +7 -9
- package/src/runtime/internal/server/index.js +14 -22
- package/src/runtime/media-query.js +34 -33
- package/src/runtime/object.js +7 -10
- package/src/runtime/proxy.js +2 -3
- package/src/runtime/reactive-value.js +23 -21
- package/src/utils/ast.js +1 -1
- package/src/utils/attributes.js +43 -0
- package/src/utils/builders.js +2 -2
- package/tests/client/basic/basic.errors.test.rsrx +1 -1
- package/tests/client/basic/basic.styling.test.rsrx +1 -1
- package/tests/client/compiler/compiler.assignments.test.rsrx +1 -1
- package/tests/client/compiler/compiler.attributes.test.rsrx +1 -1
- package/tests/client/compiler/compiler.basic.test.rsrx +13 -13
- package/tests/client/compiler/compiler.tracked-access.test.rsrx +1 -1
- package/tests/client/compiler/compiler.try-in-function.test.rsrx +1 -1
- package/tests/client/compiler/compiler.typescript.test.rsrx +1 -1
- package/tests/client/css/global-additional-cases.test.rsrx +1 -1
- package/tests/client/css/global-advanced-selectors.test.rsrx +1 -1
- package/tests/client/css/global-at-rules.test.rsrx +1 -1
- package/tests/client/css/global-basic.test.rsrx +1 -1
- package/tests/client/css/global-classes-ids.test.rsrx +1 -1
- package/tests/client/css/global-combinators.test.rsrx +1 -1
- package/tests/client/css/global-complex-nesting.test.rsrx +1 -1
- package/tests/client/css/global-edge-cases.test.rsrx +1 -1
- package/tests/client/css/global-keyframes.test.rsrx +1 -1
- package/tests/client/css/global-nested.test.rsrx +1 -1
- package/tests/client/css/global-pseudo.test.rsrx +1 -1
- package/tests/client/css/global-scoping.test.rsrx +1 -1
- package/tests/client/css/style-identifier.test.rsrx +1 -1
- package/tests/client/return.test.rsrx +1 -1
- package/tests/hydration/build-components.js +1 -1
- package/tests/server/style-identifier.test.rsrx +1 -1
- package/tests/setup-server.js +1 -1
- package/tests/utils/compiler-compat-config.test.js +1 -1
- package/src/compiler/comment-utils.js +0 -91
- package/src/compiler/errors.js +0 -77
- package/src/compiler/identifier-utils.js +0 -80
- package/src/compiler/index.d.ts +0 -127
- package/src/compiler/index.js +0 -89
- package/src/compiler/phases/1-parse/index.js +0 -3007
- package/src/compiler/phases/1-parse/style.js +0 -704
- package/src/compiler/phases/2-analyze/css-analyze.js +0 -160
- package/src/compiler/phases/2-analyze/index.js +0 -2208
- package/src/compiler/phases/2-analyze/prune.js +0 -1131
- package/src/compiler/phases/2-analyze/validation.js +0 -168
- package/src/compiler/phases/3-transform/client/index.js +0 -5264
- package/src/compiler/phases/3-transform/segments.js +0 -2125
- package/src/compiler/phases/3-transform/server/index.js +0 -1749
- package/src/compiler/phases/3-transform/stylesheet.js +0 -545
- package/src/compiler/scope.js +0 -476
- package/src/compiler/source-map-utils.js +0 -358
- package/src/compiler/types/acorn.d.ts +0 -11
- package/src/compiler/types/estree-jsx.d.ts +0 -11
- package/src/compiler/types/estree.d.ts +0 -11
- package/src/compiler/types/index.d.ts +0 -1411
- package/src/compiler/types/parse.d.ts +0 -1723
- package/src/compiler/utils.js +0 -1258
|
@@ -1,1131 +0,0 @@
|
|
|
1
|
-
/** @import * as AST from 'estree' */
|
|
2
|
-
/** @import { Visitors, TopScopedClasses, StyleClasses } from '#compiler' */
|
|
3
|
-
/** @typedef {0 | 1} Direction */
|
|
4
|
-
|
|
5
|
-
import { walk } from 'zimmerframe';
|
|
6
|
-
import { is_element_dom_element, is_element_dynamic } from '../../utils.js';
|
|
7
|
-
|
|
8
|
-
const regex_backslash_and_following_character = /\\(.)/g;
|
|
9
|
-
/** @type {Direction} */
|
|
10
|
-
const FORWARD = 0;
|
|
11
|
-
/** @type {Direction} */
|
|
12
|
-
const BACKWARD = 1;
|
|
13
|
-
|
|
14
|
-
// this will be set for every prune_css call
|
|
15
|
-
// since the code is synchronous, this is safe
|
|
16
|
-
/** @type {string} */
|
|
17
|
-
let css_hash;
|
|
18
|
-
/** @type {StyleClasses} */
|
|
19
|
-
let style_identifier_classes;
|
|
20
|
-
/** @type {TopScopedClasses} */
|
|
21
|
-
let top_scoped_classes;
|
|
22
|
-
|
|
23
|
-
// CSS selector constants
|
|
24
|
-
/**
|
|
25
|
-
* @param {number} start
|
|
26
|
-
* @param {number} end
|
|
27
|
-
* @returns {AST.CSS.Combinator}
|
|
28
|
-
*/
|
|
29
|
-
function create_descendant_combinator(start, end) {
|
|
30
|
-
return { name: ' ', type: 'Combinator', start, end };
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* @param {AST.CSS.RelativeSelector} relative_selector
|
|
35
|
-
* @param {AST.CSS.ClassSelector} selector
|
|
36
|
-
* @returns {boolean}
|
|
37
|
-
*/
|
|
38
|
-
function is_standalone_class_selector(relative_selector, selector) {
|
|
39
|
-
return relative_selector.selectors.length === 1 && relative_selector.selectors[0] === selector;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**`
|
|
43
|
-
* @param {number} start
|
|
44
|
-
* @param {number} end
|
|
45
|
-
* @returns {AST.CSS.RelativeSelector}
|
|
46
|
-
*/
|
|
47
|
-
function create_nesting_selector(start, end) {
|
|
48
|
-
return {
|
|
49
|
-
type: 'RelativeSelector',
|
|
50
|
-
selectors: [{ type: 'NestingSelector', name: '&', start, end }],
|
|
51
|
-
combinator: null,
|
|
52
|
-
metadata: { is_global: false, is_global_like: false, scoped: false },
|
|
53
|
-
start,
|
|
54
|
-
end,
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* @param {number} start
|
|
60
|
-
* @param {number} end
|
|
61
|
-
* @returns {AST.CSS.RelativeSelector}
|
|
62
|
-
*/
|
|
63
|
-
function create_any_selector(start, end) {
|
|
64
|
-
return {
|
|
65
|
-
type: 'RelativeSelector',
|
|
66
|
-
selectors: [{ type: 'TypeSelector', name: '*', start, end }],
|
|
67
|
-
combinator: null,
|
|
68
|
-
metadata: { is_global: false, is_global_like: false, scoped: false },
|
|
69
|
-
start,
|
|
70
|
-
end,
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Whitelist for attribute selectors on specific elements
|
|
75
|
-
const whitelist_attribute_selector = new Map([
|
|
76
|
-
['details', ['open']],
|
|
77
|
-
['dialog', ['open']],
|
|
78
|
-
['form', ['novalidate']],
|
|
79
|
-
['iframe', ['allow', 'allowfullscreen', 'allowpaymentrequest', 'loading', 'referrerpolicy']],
|
|
80
|
-
['img', ['loading']],
|
|
81
|
-
[
|
|
82
|
-
'input',
|
|
83
|
-
[
|
|
84
|
-
'accept',
|
|
85
|
-
'autocomplete',
|
|
86
|
-
'capture',
|
|
87
|
-
'checked',
|
|
88
|
-
'disabled',
|
|
89
|
-
'max',
|
|
90
|
-
'maxlength',
|
|
91
|
-
'min',
|
|
92
|
-
'minlength',
|
|
93
|
-
'multiple',
|
|
94
|
-
'pattern',
|
|
95
|
-
'placeholder',
|
|
96
|
-
'readonly',
|
|
97
|
-
'required',
|
|
98
|
-
'size',
|
|
99
|
-
'step',
|
|
100
|
-
],
|
|
101
|
-
],
|
|
102
|
-
['object', ['typemustmatch']],
|
|
103
|
-
['ol', ['reversed', 'start', 'type']],
|
|
104
|
-
['optgroup', ['disabled']],
|
|
105
|
-
['option', ['disabled', 'selected']],
|
|
106
|
-
['script', ['async', 'defer', 'nomodule', 'type']],
|
|
107
|
-
['select', ['disabled', 'multiple', 'required', 'size']],
|
|
108
|
-
[
|
|
109
|
-
'textarea',
|
|
110
|
-
[
|
|
111
|
-
'autocomplete',
|
|
112
|
-
'disabled',
|
|
113
|
-
'maxlength',
|
|
114
|
-
'minlength',
|
|
115
|
-
'placeholder',
|
|
116
|
-
'readonly',
|
|
117
|
-
'required',
|
|
118
|
-
'rows',
|
|
119
|
-
'wrap',
|
|
120
|
-
],
|
|
121
|
-
],
|
|
122
|
-
['video', ['autoplay', 'controls', 'loop', 'muted', 'playsinline']],
|
|
123
|
-
]);
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* @param {AST.CSS.ComplexSelector} node
|
|
127
|
-
*/
|
|
128
|
-
function get_relative_selectors(node) {
|
|
129
|
-
const selectors = truncate(node);
|
|
130
|
-
|
|
131
|
-
if (node.metadata.rule?.metadata.parent_rule && selectors.length > 0) {
|
|
132
|
-
let has_explicit_nesting_selector = false;
|
|
133
|
-
|
|
134
|
-
// nesting could be inside pseudo classes like :is, :has or :where
|
|
135
|
-
for (let selector of selectors) {
|
|
136
|
-
walk(
|
|
137
|
-
selector,
|
|
138
|
-
null,
|
|
139
|
-
/** @type {Visitors<AST.CSS.Node, null>} */ ({
|
|
140
|
-
NestingSelector() {
|
|
141
|
-
has_explicit_nesting_selector = true;
|
|
142
|
-
},
|
|
143
|
-
}),
|
|
144
|
-
);
|
|
145
|
-
|
|
146
|
-
// if we found one we can break from the others
|
|
147
|
-
if (has_explicit_nesting_selector) break;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (!has_explicit_nesting_selector) {
|
|
151
|
-
if (selectors[0].combinator === null) {
|
|
152
|
-
selectors[0] = {
|
|
153
|
-
...selectors[0],
|
|
154
|
-
combinator: create_descendant_combinator(selectors[0].start, selectors[0].end),
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
selectors.unshift(create_nesting_selector(selectors[0].start, selectors[0].end));
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return selectors;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
*
|
|
167
|
-
* @param {AST.CSS.ComplexSelector} node
|
|
168
|
-
* @returns {AST.CSS.RelativeSelector[]}
|
|
169
|
-
*/
|
|
170
|
-
function truncate(node) {
|
|
171
|
-
const i = node.children.findLastIndex(({ metadata, selectors }) => {
|
|
172
|
-
const first = selectors[0];
|
|
173
|
-
return (
|
|
174
|
-
// not after a :global selector
|
|
175
|
-
!metadata.is_global_like &&
|
|
176
|
-
!(first.type === 'PseudoClassSelector' && first.name === 'global' && first.args === null) &&
|
|
177
|
-
// not a :global(...) without a :has/is/where(...) modifier that is scoped
|
|
178
|
-
!metadata.is_global
|
|
179
|
-
);
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
return node.children.slice(0, i + 1).map((child) => {
|
|
183
|
-
// In case of `:root.y:has(...)`, `y` is unscoped, but everything in `:has(...)` should be scoped (if not global).
|
|
184
|
-
// To properly accomplish that, we gotta filter out all selector types except `:has`.
|
|
185
|
-
const root = child.selectors.find((s) => s.type === 'PseudoClassSelector' && s.name === 'root');
|
|
186
|
-
if (!root || child.metadata.is_global_like) return child;
|
|
187
|
-
|
|
188
|
-
return {
|
|
189
|
-
...child,
|
|
190
|
-
selectors: child.selectors.filter(
|
|
191
|
-
(s) => s.type === 'PseudoClassSelector' && s.name === 'has',
|
|
192
|
-
),
|
|
193
|
-
};
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* @param {AST.CSS.RelativeSelector[]} relative_selectors
|
|
199
|
-
* @param {AST.CSS.Rule} rule
|
|
200
|
-
* @param {AST.Element} element
|
|
201
|
-
* @param {Direction} direction
|
|
202
|
-
* @returns {boolean}
|
|
203
|
-
*/
|
|
204
|
-
function apply_selector(relative_selectors, rule, element, direction) {
|
|
205
|
-
const rest_selectors = relative_selectors.slice();
|
|
206
|
-
const relative_selector = direction === FORWARD ? rest_selectors.shift() : rest_selectors.pop();
|
|
207
|
-
|
|
208
|
-
const matched =
|
|
209
|
-
!!relative_selector &&
|
|
210
|
-
relative_selector_might_apply_to_node(relative_selector, rule, element, direction) &&
|
|
211
|
-
apply_combinator(relative_selector, rest_selectors, rule, element, direction);
|
|
212
|
-
|
|
213
|
-
if (matched) {
|
|
214
|
-
if (!is_outer_global(relative_selector)) {
|
|
215
|
-
relative_selector.metadata.scoped = true;
|
|
216
|
-
|
|
217
|
-
// Store scoped class information on element for language server features
|
|
218
|
-
if (!relative_selector.metadata.is_global && !relative_selector.metadata.is_global_like) {
|
|
219
|
-
// Extract class selectors from the relative selector
|
|
220
|
-
for (const selector of relative_selector.selectors) {
|
|
221
|
-
if (selector.type === 'ClassSelector') {
|
|
222
|
-
const name = selector.name.replace(regex_backslash_and_following_character, '$1');
|
|
223
|
-
|
|
224
|
-
if (!element.metadata.css) {
|
|
225
|
-
element.metadata.css = {
|
|
226
|
-
scopedClasses: new Map(),
|
|
227
|
-
hash: css_hash,
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// Store class name → CSS location in scopedClasses
|
|
232
|
-
if (!element.metadata.css.scopedClasses.has(name)) {
|
|
233
|
-
element.metadata.css.scopedClasses.set(name, {
|
|
234
|
-
start: selector.start,
|
|
235
|
-
end: selector.end,
|
|
236
|
-
selector: selector,
|
|
237
|
-
});
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
element.metadata.scoped = true;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
return matched;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/**
|
|
251
|
-
* @param {AST.Element} node
|
|
252
|
-
* @param {boolean} adjacent_only
|
|
253
|
-
* @returns {AST.Element[]}
|
|
254
|
-
*/
|
|
255
|
-
function get_ancestor_elements(node, adjacent_only) {
|
|
256
|
-
/** @type {AST.Element[]} */
|
|
257
|
-
const ancestors = [];
|
|
258
|
-
|
|
259
|
-
const path = node.metadata.path;
|
|
260
|
-
let i = path.length;
|
|
261
|
-
|
|
262
|
-
while (i--) {
|
|
263
|
-
const parent = path[i];
|
|
264
|
-
|
|
265
|
-
if (parent.type === 'Element') {
|
|
266
|
-
ancestors.push(parent);
|
|
267
|
-
if (adjacent_only) {
|
|
268
|
-
break;
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
return ancestors;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
/**
|
|
277
|
-
* @param {AST.Element} node
|
|
278
|
-
* @param {boolean} adjacent_only
|
|
279
|
-
* @returns {AST.Element[]}
|
|
280
|
-
*/
|
|
281
|
-
function get_descendant_elements(node, adjacent_only) {
|
|
282
|
-
/** @type {AST.Element[]} */
|
|
283
|
-
const descendants = [];
|
|
284
|
-
|
|
285
|
-
/**
|
|
286
|
-
* @param {AST.Node} current_node
|
|
287
|
-
* @param {number} depth
|
|
288
|
-
* @returns {void}
|
|
289
|
-
*/
|
|
290
|
-
function visit(current_node, depth = 0) {
|
|
291
|
-
if (current_node.type === 'Element' && current_node !== node) {
|
|
292
|
-
descendants.push(current_node);
|
|
293
|
-
if (adjacent_only) return; // Only direct children for '>' combinator
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
// Visit children based on Ripple's AST structure
|
|
297
|
-
if (/** @type {AST.Element} */ (current_node).children) {
|
|
298
|
-
for (const child of /** @type {AST.Element} */ (current_node).children) {
|
|
299
|
-
visit(child, depth + 1);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
if (/** @type {AST.Component} */ (current_node).body) {
|
|
304
|
-
for (const child of /** @type {AST.Component} */ (current_node).body) {
|
|
305
|
-
visit(child, depth + 1);
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// For template nodes and interpolation expressions
|
|
310
|
-
if (
|
|
311
|
-
(current_node.type === 'RippleExpression' ||
|
|
312
|
-
current_node.type === 'Text' ||
|
|
313
|
-
current_node.type === 'Html') &&
|
|
314
|
-
/** @type {AST.RippleExpression | AST.Html | AST.TextNode} */ (current_node).expression &&
|
|
315
|
-
typeof (
|
|
316
|
-
/** @type {AST.RippleExpression | AST.Html | AST.TextNode} */ (current_node).expression
|
|
317
|
-
) === 'object'
|
|
318
|
-
) {
|
|
319
|
-
visit(
|
|
320
|
-
/** @type {AST.RippleExpression | AST.Html | AST.TextNode} */ (current_node).expression,
|
|
321
|
-
depth + 1,
|
|
322
|
-
);
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// Start from node's children
|
|
327
|
-
if (node.children) {
|
|
328
|
-
for (const child of node.children) {
|
|
329
|
-
visit(child);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
return descendants;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
/**
|
|
337
|
-
* Check if an element can render dynamic content that might affect CSS matching
|
|
338
|
-
* @param {AST.Node} element
|
|
339
|
-
* @param {boolean} check_classes - Whether to check for dynamic class attributes
|
|
340
|
-
* @returns {boolean}
|
|
341
|
-
*/
|
|
342
|
-
function can_render_dynamic_content(element, check_classes = false) {
|
|
343
|
-
if (!is_element_dom_element(element)) {
|
|
344
|
-
return true;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// Either a dynamic element or component (only can tell at runtime)
|
|
348
|
-
// But dynamic elements should return false ideally
|
|
349
|
-
if (is_element_dynamic(/** @type {AST.Element} */ (element))) {
|
|
350
|
-
return true;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// Check for dynamic class attributes if requested (for class-based selectors)
|
|
354
|
-
if (check_classes && /** @type {AST.Element} */ (element).attributes) {
|
|
355
|
-
for (const attr of /** @type {AST.Element} */ (element).attributes) {
|
|
356
|
-
if (attr.type === 'Attribute' && attr.name.name === 'class') {
|
|
357
|
-
// Check if class value is an expression (not a static string)
|
|
358
|
-
if (attr.value && typeof attr.value === 'object') {
|
|
359
|
-
// If it's a CallExpression or other dynamic value, it's dynamic
|
|
360
|
-
if (attr.value.type !== 'Literal' && attr.value.type !== 'Text') {
|
|
361
|
-
return true;
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
return false;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
/**
|
|
372
|
-
* @param {AST.Node} node
|
|
373
|
-
* @param {Direction} direction
|
|
374
|
-
* @param {boolean} adjacent_only
|
|
375
|
-
* @returns {Map<AST.Element, boolean>}
|
|
376
|
-
*/
|
|
377
|
-
function get_possible_element_siblings(node, direction, adjacent_only) {
|
|
378
|
-
const siblings = new Map();
|
|
379
|
-
// Parent has to be an Element not a Component
|
|
380
|
-
const parent = get_element_parent(node);
|
|
381
|
-
|
|
382
|
-
if (!parent) {
|
|
383
|
-
return siblings;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
// Get the container that holds the siblings
|
|
387
|
-
const container = parent.children || [];
|
|
388
|
-
const node_index = container.indexOf(node);
|
|
389
|
-
|
|
390
|
-
if (node_index === -1) return siblings;
|
|
391
|
-
|
|
392
|
-
// Determine which siblings to check based on direction
|
|
393
|
-
let start, end, step;
|
|
394
|
-
if (direction === FORWARD) {
|
|
395
|
-
start = node_index + 1;
|
|
396
|
-
end = container.length;
|
|
397
|
-
step = 1;
|
|
398
|
-
} else {
|
|
399
|
-
start = node_index - 1;
|
|
400
|
-
end = -1;
|
|
401
|
-
step = -1;
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// Collect siblings
|
|
405
|
-
for (let i = start; i !== end; i += step) {
|
|
406
|
-
const sibling = container[i];
|
|
407
|
-
|
|
408
|
-
if (sibling.type === 'Element' || sibling.type === 'Component') {
|
|
409
|
-
siblings.set(sibling, true);
|
|
410
|
-
// Don't break for dynamic elements (children, Components, dynamic components)
|
|
411
|
-
// as they can render dynamic content or might render nothing
|
|
412
|
-
const isDynamic = can_render_dynamic_content(sibling, false);
|
|
413
|
-
if (adjacent_only && !isDynamic) {
|
|
414
|
-
break; // Only immediate sibling for '+' combinator
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
// Stop at non-whitespace text nodes for adjacent selectors
|
|
418
|
-
else if (
|
|
419
|
-
adjacent_only &&
|
|
420
|
-
(sibling.type === 'RippleExpression' || sibling.type === 'Text') &&
|
|
421
|
-
sibling.expression.type === 'Literal' &&
|
|
422
|
-
typeof sibling.expression.value === 'string' &&
|
|
423
|
-
sibling.expression.value.trim()
|
|
424
|
-
) {
|
|
425
|
-
break;
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
return siblings;
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
/**
|
|
433
|
-
* @param {AST.CSS.RelativeSelector} relative_selector
|
|
434
|
-
* @param {AST.CSS.RelativeSelector[]} rest_selectors
|
|
435
|
-
* @param {AST.CSS.Rule} rule
|
|
436
|
-
* @param {AST.Element} node
|
|
437
|
-
* @param {Direction} direction
|
|
438
|
-
* @returns {boolean}
|
|
439
|
-
*/
|
|
440
|
-
function apply_combinator(relative_selector, rest_selectors, rule, node, direction) {
|
|
441
|
-
const combinator =
|
|
442
|
-
direction == FORWARD ? rest_selectors[0]?.combinator : relative_selector.combinator;
|
|
443
|
-
if (!combinator) return true;
|
|
444
|
-
|
|
445
|
-
switch (combinator.name) {
|
|
446
|
-
case ' ':
|
|
447
|
-
case '>': {
|
|
448
|
-
const is_adjacent = combinator.name === '>';
|
|
449
|
-
const parents =
|
|
450
|
-
direction === FORWARD
|
|
451
|
-
? get_descendant_elements(node, is_adjacent)
|
|
452
|
-
: get_ancestor_elements(node, is_adjacent);
|
|
453
|
-
let parent_matched = false;
|
|
454
|
-
|
|
455
|
-
for (const parent of parents) {
|
|
456
|
-
if (apply_selector(rest_selectors, rule, parent, direction)) {
|
|
457
|
-
parent_matched = true;
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
return (
|
|
462
|
-
parent_matched ||
|
|
463
|
-
(direction === BACKWARD &&
|
|
464
|
-
(!is_adjacent || parents.length === 0) &&
|
|
465
|
-
rest_selectors.every((selector) => is_global(selector, rule)))
|
|
466
|
-
);
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
case '+':
|
|
470
|
-
case '~': {
|
|
471
|
-
const siblings = get_possible_element_siblings(node, direction, combinator.name === '+');
|
|
472
|
-
|
|
473
|
-
let sibling_matched = false;
|
|
474
|
-
|
|
475
|
-
for (const possible_sibling of siblings.keys()) {
|
|
476
|
-
// Check if this sibling can render dynamic content
|
|
477
|
-
// For class selectors, also check if element has dynamic classes
|
|
478
|
-
const has_class_selector = rest_selectors.some((sel) =>
|
|
479
|
-
sel.selectors?.some((s) => s.type === 'ClassSelector'),
|
|
480
|
-
);
|
|
481
|
-
const is_dynamic = can_render_dynamic_content(possible_sibling, has_class_selector);
|
|
482
|
-
|
|
483
|
-
if (is_dynamic) {
|
|
484
|
-
if (rest_selectors.length > 0) {
|
|
485
|
-
// Check if the first selector in the rest is global
|
|
486
|
-
const first_rest_selector = rest_selectors[0];
|
|
487
|
-
if (is_global(first_rest_selector, rule)) {
|
|
488
|
-
// Global selector followed by possibly more selectors
|
|
489
|
-
// Check if remaining selectors could match elements after this component
|
|
490
|
-
const remaining = rest_selectors.slice(1);
|
|
491
|
-
if (remaining.length === 0) {
|
|
492
|
-
// Just a global selector, mark as matched
|
|
493
|
-
sibling_matched = true;
|
|
494
|
-
} else {
|
|
495
|
-
// Check if there are any elements after this component that could match the remaining selectors
|
|
496
|
-
const parent = get_element_parent(node);
|
|
497
|
-
if (parent) {
|
|
498
|
-
const container = parent.children || [];
|
|
499
|
-
const component_index = container.indexOf(possible_sibling);
|
|
500
|
-
|
|
501
|
-
// For adjacent combinator, only check immediate next element
|
|
502
|
-
// For general sibling, check all following elements
|
|
503
|
-
const search_start = component_index + 1;
|
|
504
|
-
const search_end = combinator.name === '+' ? search_start + 1 : container.length;
|
|
505
|
-
|
|
506
|
-
for (let i = search_start; i < search_end; i++) {
|
|
507
|
-
const subsequent = container[i];
|
|
508
|
-
if (subsequent.type === 'Element') {
|
|
509
|
-
if (apply_selector(remaining, rule, subsequent, direction)) {
|
|
510
|
-
sibling_matched = true;
|
|
511
|
-
break;
|
|
512
|
-
}
|
|
513
|
-
if (combinator.name === '+') break; // For adjacent, only check first element
|
|
514
|
-
} else if (subsequent.type === 'Component') {
|
|
515
|
-
// Skip components when looking for the target element
|
|
516
|
-
if (combinator.name === '+') {
|
|
517
|
-
// For adjacent, continue looking
|
|
518
|
-
continue;
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
} else if (rest_selectors.length === 1 && rest_selectors[0].metadata.is_global) {
|
|
526
|
-
// Single global selector always matches
|
|
527
|
-
sibling_matched = true;
|
|
528
|
-
}
|
|
529
|
-
// Don't apply_selector for dynamic elements - they won't match regular element selectors
|
|
530
|
-
} else if (
|
|
531
|
-
possible_sibling.type === 'Element' &&
|
|
532
|
-
apply_selector(rest_selectors, rule, possible_sibling, direction)
|
|
533
|
-
) {
|
|
534
|
-
sibling_matched = true;
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
return (
|
|
539
|
-
sibling_matched ||
|
|
540
|
-
(direction === BACKWARD &&
|
|
541
|
-
get_element_parent(node) === null &&
|
|
542
|
-
rest_selectors.every((selector) => is_global(selector, rule)))
|
|
543
|
-
);
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
default:
|
|
547
|
-
// TODO other combinators
|
|
548
|
-
return true;
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
/**
|
|
552
|
-
* @param {AST.Node} node
|
|
553
|
-
* @returns {AST.Element | null}
|
|
554
|
-
*/
|
|
555
|
-
function get_element_parent(node) {
|
|
556
|
-
// Check if metadata and path exist
|
|
557
|
-
if (!node.metadata || !node.metadata.path) {
|
|
558
|
-
return null;
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
let path = node.metadata.path;
|
|
562
|
-
let i = path.length;
|
|
563
|
-
|
|
564
|
-
while (i--) {
|
|
565
|
-
const parent = path[i];
|
|
566
|
-
|
|
567
|
-
if (parent.type === 'Element') {
|
|
568
|
-
return parent;
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
return null;
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
/**
|
|
576
|
-
* `true` if is a pseudo class that cannot be or is not scoped
|
|
577
|
-
* @param {AST.CSS.SimpleSelector} selector
|
|
578
|
-
* @returns {boolean}
|
|
579
|
-
*/
|
|
580
|
-
function is_unscoped_pseudo_class(selector) {
|
|
581
|
-
return (
|
|
582
|
-
selector.type === 'PseudoClassSelector' &&
|
|
583
|
-
// These make the selector scoped
|
|
584
|
-
((selector.name !== 'has' &&
|
|
585
|
-
selector.name !== 'is' &&
|
|
586
|
-
selector.name !== 'where' &&
|
|
587
|
-
// :not is special because we want to scope as specific as possible, but because :not
|
|
588
|
-
// inverses the result, we want to leave the unscoped, too. The exception is more than
|
|
589
|
-
// one selector in the :not (.e.g :not(.x .y)), then .x and .y should be scoped
|
|
590
|
-
(selector.name !== 'not' ||
|
|
591
|
-
selector.args === null ||
|
|
592
|
-
selector.args.children.every((c) => c.children.length === 1))) ||
|
|
593
|
-
// selectors with has/is/where/not can also be global if all their children are global
|
|
594
|
-
selector.args === null ||
|
|
595
|
-
selector.args.children.every((c) => c.children.every((r) => is_global_simple(r))))
|
|
596
|
-
);
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
/**
|
|
600
|
-
* True if is `:global(...)` or `:global` and no pseudo class that is scoped.
|
|
601
|
-
* @param {AST.CSS.RelativeSelector} relative_selector
|
|
602
|
-
*/
|
|
603
|
-
function is_global_simple(relative_selector) {
|
|
604
|
-
const first = relative_selector.selectors[0];
|
|
605
|
-
|
|
606
|
-
return (
|
|
607
|
-
first.type === 'PseudoClassSelector' &&
|
|
608
|
-
first.name === 'global' &&
|
|
609
|
-
(first.args === null ||
|
|
610
|
-
// Only these two selector types keep the whole selector global, because e.g.
|
|
611
|
-
// :global(button).x means that the selector is still scoped because of the .x
|
|
612
|
-
relative_selector.selectors.every(
|
|
613
|
-
(selector) =>
|
|
614
|
-
is_unscoped_pseudo_class(selector) || selector.type === 'PseudoElementSelector',
|
|
615
|
-
))
|
|
616
|
-
);
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
/**
|
|
620
|
-
* @param {AST.CSS.RelativeSelector} selector
|
|
621
|
-
* @param {AST.CSS.Rule} rule
|
|
622
|
-
* @return {boolean}
|
|
623
|
-
*/
|
|
624
|
-
function is_global(selector, rule) {
|
|
625
|
-
if (selector.metadata.is_global || selector.metadata.is_global_like) {
|
|
626
|
-
return true;
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
let explicitly_global = false;
|
|
630
|
-
|
|
631
|
-
for (const s of selector.selectors) {
|
|
632
|
-
/** @type {AST.CSS.SelectorList | null} */
|
|
633
|
-
let selector_list = null;
|
|
634
|
-
let can_be_global = false;
|
|
635
|
-
let owner = rule;
|
|
636
|
-
|
|
637
|
-
if (s.type === 'PseudoClassSelector') {
|
|
638
|
-
if ((s.name === 'is' || s.name === 'where') && s.args) {
|
|
639
|
-
selector_list = s.args;
|
|
640
|
-
} else {
|
|
641
|
-
can_be_global = is_unscoped_pseudo_class(s);
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
if (s.type === 'NestingSelector') {
|
|
646
|
-
owner = /** @type {AST.CSS.Rule} */ (rule.metadata.parent_rule);
|
|
647
|
-
selector_list = owner.prelude;
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
const has_global_selectors = !!selector_list?.children.some((complex_selector) => {
|
|
651
|
-
return complex_selector.children.every((relative_selector) =>
|
|
652
|
-
is_global(relative_selector, owner),
|
|
653
|
-
);
|
|
654
|
-
});
|
|
655
|
-
explicitly_global ||= has_global_selectors;
|
|
656
|
-
|
|
657
|
-
if (!has_global_selectors && !can_be_global) {
|
|
658
|
-
return false;
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
return explicitly_global || selector.selectors.length === 0;
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
/**
|
|
666
|
-
* @param {AST.Attribute} attribute
|
|
667
|
-
* @returns {attribute is AST.Attribute & { value: AST.Literal & { value: string } }}
|
|
668
|
-
*/
|
|
669
|
-
function is_text_attribute(attribute) {
|
|
670
|
-
return attribute.value?.type === 'Literal' && typeof attribute.value.value === 'string';
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
/**
|
|
674
|
-
* @param {string | null} operator
|
|
675
|
-
* @param {string} expected_value
|
|
676
|
-
* @param {boolean} case_insensitive
|
|
677
|
-
* @param {string} value
|
|
678
|
-
* @returns {boolean}
|
|
679
|
-
*/
|
|
680
|
-
function test_attribute(operator, expected_value, case_insensitive, value) {
|
|
681
|
-
if (case_insensitive) {
|
|
682
|
-
expected_value = expected_value.toLowerCase();
|
|
683
|
-
value = value.toLowerCase();
|
|
684
|
-
}
|
|
685
|
-
switch (operator) {
|
|
686
|
-
case '=':
|
|
687
|
-
return value === expected_value;
|
|
688
|
-
case '~=':
|
|
689
|
-
return value.split(/\s/).includes(expected_value);
|
|
690
|
-
case '|=':
|
|
691
|
-
return `${value}-`.startsWith(`${expected_value}-`);
|
|
692
|
-
case '^=':
|
|
693
|
-
return value.startsWith(expected_value);
|
|
694
|
-
case '$=':
|
|
695
|
-
return value.endsWith(expected_value);
|
|
696
|
-
case '*=':
|
|
697
|
-
return value.includes(expected_value);
|
|
698
|
-
default:
|
|
699
|
-
throw new Error("this shouldn't happen");
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
/**
|
|
704
|
-
* @param {AST.Element} node
|
|
705
|
-
* @param {string} name
|
|
706
|
-
* @param {string | null} expected_value
|
|
707
|
-
* @param {string | null} operator
|
|
708
|
-
* @param {boolean} case_insensitive
|
|
709
|
-
* @returns {boolean}
|
|
710
|
-
*/
|
|
711
|
-
function attribute_matches(node, name, expected_value, operator, case_insensitive) {
|
|
712
|
-
for (const attribute of node.attributes) {
|
|
713
|
-
if (attribute.type === 'SpreadAttribute') return true;
|
|
714
|
-
|
|
715
|
-
if (attribute.type !== 'Attribute') continue;
|
|
716
|
-
|
|
717
|
-
const lowerCaseName = name.toLowerCase();
|
|
718
|
-
if (![lowerCaseName, `$${lowerCaseName}`].includes(attribute.name.name.toLowerCase())) continue;
|
|
719
|
-
|
|
720
|
-
if (expected_value === null) return true;
|
|
721
|
-
|
|
722
|
-
if (is_text_attribute(attribute)) {
|
|
723
|
-
return test_attribute(operator, expected_value, case_insensitive, attribute.value.value);
|
|
724
|
-
} else {
|
|
725
|
-
return true;
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
return false;
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
/**
|
|
733
|
-
* @param {AST.CSS.RelativeSelector} relative_selector
|
|
734
|
-
* @returns {boolean}
|
|
735
|
-
*/
|
|
736
|
-
function is_outer_global(relative_selector) {
|
|
737
|
-
const first = relative_selector.selectors[0];
|
|
738
|
-
|
|
739
|
-
return (
|
|
740
|
-
first &&
|
|
741
|
-
first.type === 'PseudoClassSelector' &&
|
|
742
|
-
first.name === 'global' &&
|
|
743
|
-
(first.args === null ||
|
|
744
|
-
// Only these two selector types can keep the whole selector global, because e.g.
|
|
745
|
-
// :global(button).x means that the selector is still scoped because of the .x
|
|
746
|
-
relative_selector.selectors.every(
|
|
747
|
-
(selector) =>
|
|
748
|
-
selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector',
|
|
749
|
-
))
|
|
750
|
-
);
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
/**
|
|
754
|
-
* @param {AST.CSS.RelativeSelector} relative_selector
|
|
755
|
-
* @param {AST.CSS.Rule} rule
|
|
756
|
-
* @param {AST.Element} element
|
|
757
|
-
* @param {Direction} direction
|
|
758
|
-
* @return {boolean}
|
|
759
|
-
*/
|
|
760
|
-
function relative_selector_might_apply_to_node(relative_selector, rule, element, direction) {
|
|
761
|
-
// Sort :has(...) selectors in one bucket and everything else into another
|
|
762
|
-
const has_selectors = [];
|
|
763
|
-
const other_selectors = [];
|
|
764
|
-
|
|
765
|
-
for (const selector of relative_selector.selectors) {
|
|
766
|
-
if (selector.type === 'PseudoClassSelector' && selector.name === 'has' && selector.args) {
|
|
767
|
-
has_selectors.push(selector);
|
|
768
|
-
} else {
|
|
769
|
-
other_selectors.push(selector);
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
// If we're called recursively from a :has(...) selector, we're on the way of checking if the other selectors match.
|
|
774
|
-
// In that case ignore this check (because we just came from this) to avoid an infinite loop.
|
|
775
|
-
if (has_selectors.length > 0) {
|
|
776
|
-
// If this is a :has inside a global selector, we gotta include the element itself, too,
|
|
777
|
-
// because the global selector might be for an element that's outside the component,
|
|
778
|
-
// e.g. :root:has(.scoped), :global(.foo):has(.scoped), or :root { &:has(.scoped) {} }
|
|
779
|
-
const rules = get_parent_rules(rule);
|
|
780
|
-
const include_self =
|
|
781
|
-
rules.some((r) => r.prelude.children.some((c) => c.children.some((s) => is_global(s, r)))) ||
|
|
782
|
-
rules[rules.length - 1].prelude.children.some((c) =>
|
|
783
|
-
c.children.some((r) =>
|
|
784
|
-
r.selectors.some(
|
|
785
|
-
(s) =>
|
|
786
|
-
s.type === 'PseudoClassSelector' &&
|
|
787
|
-
(s.name === 'root' || (s.name === 'global' && s.args)),
|
|
788
|
-
),
|
|
789
|
-
),
|
|
790
|
-
);
|
|
791
|
-
|
|
792
|
-
// :has(...) is special in that it means "look downwards in the CSS tree". Since our matching algorithm goes
|
|
793
|
-
// upwards and back-to-front, we need to first check the selectors inside :has(...), then check the rest of the
|
|
794
|
-
// selector in a way that is similar to ancestor matching. In a sense, we're treating `.x:has(.y)` as `.x .y`.
|
|
795
|
-
for (const has_selector of has_selectors) {
|
|
796
|
-
const complex_selectors = /** @type {AST.CSS.SelectorList} */ (has_selector.args).children;
|
|
797
|
-
let matched = false;
|
|
798
|
-
|
|
799
|
-
for (const complex_selector of complex_selectors) {
|
|
800
|
-
const [first, ...rest] = truncate(complex_selector);
|
|
801
|
-
// if it was just a :global(...)
|
|
802
|
-
if (!first) {
|
|
803
|
-
complex_selector.metadata.used = true;
|
|
804
|
-
matched = true;
|
|
805
|
-
continue;
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
if (include_self) {
|
|
809
|
-
const selector_including_self = [
|
|
810
|
-
first.combinator ? { ...first, combinator: null } : first,
|
|
811
|
-
...rest,
|
|
812
|
-
];
|
|
813
|
-
if (apply_selector(selector_including_self, rule, element, FORWARD)) {
|
|
814
|
-
complex_selector.metadata.used = true;
|
|
815
|
-
matched = true;
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
const selector_excluding_self = [
|
|
820
|
-
create_any_selector(first.start, first.end),
|
|
821
|
-
first.combinator
|
|
822
|
-
? first
|
|
823
|
-
: { ...first, combinator: create_descendant_combinator(first.start, first.end) },
|
|
824
|
-
...rest,
|
|
825
|
-
];
|
|
826
|
-
if (apply_selector(selector_excluding_self, rule, element, FORWARD)) {
|
|
827
|
-
complex_selector.metadata.used = true;
|
|
828
|
-
matched = true;
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
if (!matched) {
|
|
833
|
-
return false;
|
|
834
|
-
}
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
for (const selector of other_selectors) {
|
|
839
|
-
if (selector.type === 'Percentage' || selector.type === 'Nth') continue;
|
|
840
|
-
|
|
841
|
-
const name = selector.name.replace(regex_backslash_and_following_character, '$1');
|
|
842
|
-
|
|
843
|
-
switch (selector.type) {
|
|
844
|
-
case 'PseudoClassSelector': {
|
|
845
|
-
if (name === 'host' || name === 'root') return false;
|
|
846
|
-
|
|
847
|
-
if (
|
|
848
|
-
name === 'global' &&
|
|
849
|
-
selector.args !== null &&
|
|
850
|
-
relative_selector.selectors.length === 1
|
|
851
|
-
) {
|
|
852
|
-
const args = selector.args;
|
|
853
|
-
const complex_selector = args.children[0];
|
|
854
|
-
return apply_selector(complex_selector.children, rule, element, BACKWARD);
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
// We came across a :global, everything beyond it is global and therefore a potential match
|
|
858
|
-
if (name === 'global' && selector.args === null) return true;
|
|
859
|
-
|
|
860
|
-
// :not(...) contents should stay unscoped. Scoping them would achieve the opposite of what we want,
|
|
861
|
-
// because they are then _more_ likely to bleed out of the component. The exception is complex selectors
|
|
862
|
-
// with descendants, in which case we scope them all.
|
|
863
|
-
if (name === 'not' && selector.args) {
|
|
864
|
-
for (const complex_selector of selector.args.children) {
|
|
865
|
-
walk(complex_selector, null, {
|
|
866
|
-
ComplexSelector(node, context) {
|
|
867
|
-
node.metadata.used = true;
|
|
868
|
-
context.next();
|
|
869
|
-
},
|
|
870
|
-
});
|
|
871
|
-
const relative = truncate(complex_selector);
|
|
872
|
-
|
|
873
|
-
if (complex_selector.children.length > 1) {
|
|
874
|
-
// foo:not(bar foo) means that bar is an ancestor of foo (side note: ending with foo is the only way the selector make sense).
|
|
875
|
-
// We can't fully check if that actually matches with our current algorithm, so we just assume it does.
|
|
876
|
-
// The result may not match a real element, so the only drawback is the missing prune.
|
|
877
|
-
for (const selector of relative) {
|
|
878
|
-
selector.metadata.scoped = true;
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
/** @type {AST.Element | null} */
|
|
882
|
-
let el = element;
|
|
883
|
-
while (el) {
|
|
884
|
-
el.metadata.scoped = true;
|
|
885
|
-
el = get_element_parent(el);
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
break;
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
if ((name === 'is' || name === 'where') && selector.args) {
|
|
894
|
-
let matched = false;
|
|
895
|
-
|
|
896
|
-
for (const complex_selector of selector.args.children) {
|
|
897
|
-
const relative = truncate(complex_selector);
|
|
898
|
-
const is_global = relative.length === 0;
|
|
899
|
-
|
|
900
|
-
if (is_global) {
|
|
901
|
-
complex_selector.metadata.used = true;
|
|
902
|
-
matched = true;
|
|
903
|
-
} else if (apply_selector(relative, rule, element, BACKWARD)) {
|
|
904
|
-
complex_selector.metadata.used = true;
|
|
905
|
-
matched = true;
|
|
906
|
-
} else if (complex_selector.children.length > 1 && (name == 'is' || name == 'where')) {
|
|
907
|
-
// foo :is(bar baz) can also mean that bar is an ancestor of foo, and baz a descendant.
|
|
908
|
-
// We can't fully check if that actually matches with our current algorithm, so we just assume it does.
|
|
909
|
-
// The result may not match a real element, so the only drawback is the missing prune.
|
|
910
|
-
complex_selector.metadata.used = true;
|
|
911
|
-
matched = true;
|
|
912
|
-
for (const selector of relative) {
|
|
913
|
-
selector.metadata.scoped = true;
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
if (!matched) {
|
|
919
|
-
return false;
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
break;
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
case 'PseudoElementSelector': {
|
|
927
|
-
break;
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
case 'AttributeSelector': {
|
|
931
|
-
const whitelisted = whitelist_attribute_selector.get(
|
|
932
|
-
/** @type {AST.Identifier} */ (element.id).name.toLowerCase(),
|
|
933
|
-
);
|
|
934
|
-
if (
|
|
935
|
-
!whitelisted?.includes(selector.name.toLowerCase()) &&
|
|
936
|
-
!attribute_matches(
|
|
937
|
-
element,
|
|
938
|
-
selector.name,
|
|
939
|
-
selector.value && unquote(selector.value),
|
|
940
|
-
selector.matcher,
|
|
941
|
-
selector.flags?.includes('i') ?? false,
|
|
942
|
-
)
|
|
943
|
-
) {
|
|
944
|
-
return false;
|
|
945
|
-
}
|
|
946
|
-
break;
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
case 'ClassSelector': {
|
|
950
|
-
if (
|
|
951
|
-
!attribute_matches(element, 'class', name, '~=', false) &&
|
|
952
|
-
(!style_identifier_classes.has(name) ||
|
|
953
|
-
!is_standalone_class_selector(relative_selector, selector))
|
|
954
|
-
) {
|
|
955
|
-
return false;
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
break;
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
case 'IdSelector': {
|
|
962
|
-
if (!attribute_matches(element, 'id', name, '=', false)) {
|
|
963
|
-
return false;
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
break;
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
case 'TypeSelector': {
|
|
970
|
-
if (
|
|
971
|
-
element.id.type === 'Identifier' &&
|
|
972
|
-
element.id.name.toLowerCase() !== name.toLowerCase() &&
|
|
973
|
-
name !== '*'
|
|
974
|
-
) {
|
|
975
|
-
return false;
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
break;
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
case 'NestingSelector': {
|
|
982
|
-
let matched = false;
|
|
983
|
-
|
|
984
|
-
const parent = /** @type {AST.CSS.Rule} */ (rule.metadata.parent_rule);
|
|
985
|
-
|
|
986
|
-
for (const complex_selector of parent.prelude.children) {
|
|
987
|
-
if (
|
|
988
|
-
apply_selector(get_relative_selectors(complex_selector), parent, element, direction) ||
|
|
989
|
-
complex_selector.children.every((s) => is_global(s, parent))
|
|
990
|
-
) {
|
|
991
|
-
complex_selector.metadata.used = true;
|
|
992
|
-
matched = true;
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
if (!matched) {
|
|
997
|
-
return false;
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
break;
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
// possible match
|
|
1006
|
-
return true;
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
/**
|
|
1010
|
-
* @param {string} str
|
|
1011
|
-
* @returns {string}
|
|
1012
|
-
*/
|
|
1013
|
-
function unquote(str) {
|
|
1014
|
-
if (
|
|
1015
|
-
(str[0] === '"' && str[str.length - 1] === '"') ||
|
|
1016
|
-
(str[0] === "'" && str[str.length - 1] === "'")
|
|
1017
|
-
) {
|
|
1018
|
-
return str.slice(1, -1);
|
|
1019
|
-
}
|
|
1020
|
-
return str;
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
/**
|
|
1024
|
-
* @param {AST.CSS.Rule} rule
|
|
1025
|
-
* @returns {AST.CSS.Rule[]}
|
|
1026
|
-
*/
|
|
1027
|
-
function get_parent_rules(rule) {
|
|
1028
|
-
const rules = [rule];
|
|
1029
|
-
let current = rule;
|
|
1030
|
-
|
|
1031
|
-
while (current.metadata.parent_rule) {
|
|
1032
|
-
current = current.metadata.parent_rule;
|
|
1033
|
-
rules.unshift(current);
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
return rules;
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
|
-
/**
|
|
1040
|
-
* Check if a CSS rule contains animation or animation-name properties
|
|
1041
|
-
* @param {AST.CSS.Rule} rule
|
|
1042
|
-
* @returns {boolean}
|
|
1043
|
-
*/
|
|
1044
|
-
function rule_has_animation(rule) {
|
|
1045
|
-
if (!rule.block) return false;
|
|
1046
|
-
|
|
1047
|
-
for (const child of rule.block.children) {
|
|
1048
|
-
if (child.type === 'Declaration') {
|
|
1049
|
-
const prop = child.property?.toLowerCase();
|
|
1050
|
-
if (prop === 'animation' || prop === 'animation-name') {
|
|
1051
|
-
return true;
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
return false;
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
/**
|
|
1060
|
-
* @param {AST.CSS.StyleSheet} css
|
|
1061
|
-
* @param {AST.Element} element
|
|
1062
|
-
* @param {StyleClasses} styleClasses
|
|
1063
|
-
* @param {TopScopedClasses} topScopedClasses
|
|
1064
|
-
* @return {void}
|
|
1065
|
-
*/
|
|
1066
|
-
export function prune_css(css, element, styleClasses, topScopedClasses) {
|
|
1067
|
-
css_hash = css.hash;
|
|
1068
|
-
style_identifier_classes = styleClasses;
|
|
1069
|
-
top_scoped_classes = topScopedClasses;
|
|
1070
|
-
|
|
1071
|
-
/** @type {Visitors<AST.CSS.Node, null>} */
|
|
1072
|
-
const visitors = {
|
|
1073
|
-
Rule(node, context) {
|
|
1074
|
-
if (node.metadata.is_global_block) {
|
|
1075
|
-
context.visit(node.prelude);
|
|
1076
|
-
} else {
|
|
1077
|
-
context.next();
|
|
1078
|
-
}
|
|
1079
|
-
},
|
|
1080
|
-
ComplexSelector(node, context) {
|
|
1081
|
-
const selectors = get_relative_selectors(node);
|
|
1082
|
-
|
|
1083
|
-
const rule = /** @type {AST.CSS.Rule} */ (node.metadata.rule);
|
|
1084
|
-
|
|
1085
|
-
if (apply_selector(selectors, rule, element, BACKWARD) || rule_has_animation(rule)) {
|
|
1086
|
-
node.metadata.used = true;
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
// Populate top_scoped_classes for truly standalone class selectors (for #style support).
|
|
1090
|
-
// A class is standalone only when the entire effective selector chain (after resolving
|
|
1091
|
-
// nesting and stripping :global) is a single RelativeSelector with a single ClassSelector.
|
|
1092
|
-
// This prevents classes from compound selectors like `.wrapper .nested` or selectors
|
|
1093
|
-
// inside :global() from being treated as valid #style targets.
|
|
1094
|
-
if (selectors.length === 1) {
|
|
1095
|
-
const sole_selector = selectors[0];
|
|
1096
|
-
if (
|
|
1097
|
-
!sole_selector.metadata.is_global &&
|
|
1098
|
-
!sole_selector.metadata.is_global_like &&
|
|
1099
|
-
sole_selector.selectors.length === 1 &&
|
|
1100
|
-
sole_selector.selectors[0].type === 'ClassSelector'
|
|
1101
|
-
) {
|
|
1102
|
-
const class_selector = sole_selector.selectors[0];
|
|
1103
|
-
const name = class_selector.name.replace(regex_backslash_and_following_character, '$1');
|
|
1104
|
-
if (!top_scoped_classes.has(name)) {
|
|
1105
|
-
top_scoped_classes.set(name, {
|
|
1106
|
-
start: class_selector.start,
|
|
1107
|
-
end: class_selector.end,
|
|
1108
|
-
selector: class_selector,
|
|
1109
|
-
});
|
|
1110
|
-
}
|
|
1111
|
-
}
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
context.next();
|
|
1115
|
-
},
|
|
1116
|
-
PseudoClassSelector(node, context) {
|
|
1117
|
-
// Visit nested selectors inside :has(), :is(), :where(), and :not()
|
|
1118
|
-
if (
|
|
1119
|
-
(node.name === 'has' ||
|
|
1120
|
-
node.name === 'is' ||
|
|
1121
|
-
node.name === 'where' ||
|
|
1122
|
-
node.name === 'not') &&
|
|
1123
|
-
node.args
|
|
1124
|
-
) {
|
|
1125
|
-
context.next();
|
|
1126
|
-
}
|
|
1127
|
-
},
|
|
1128
|
-
};
|
|
1129
|
-
|
|
1130
|
-
walk(css, null, visitors);
|
|
1131
|
-
}
|