ripple 0.1.1 → 0.2.0
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/LICENSE +21 -0
- package/package.json +56 -24
- package/src/ai.js +292 -0
- package/src/compiler/errors.js +26 -0
- package/src/compiler/index.js +26 -0
- package/src/compiler/phases/1-parse/index.js +543 -0
- package/src/compiler/phases/1-parse/style.js +566 -0
- package/src/compiler/phases/2-analyze/index.js +509 -0
- package/src/compiler/phases/2-analyze/prune.js +572 -0
- package/src/compiler/phases/3-transform/index.js +1572 -0
- package/src/compiler/phases/3-transform/segments.js +91 -0
- package/src/compiler/phases/3-transform/stylesheet.js +372 -0
- package/src/compiler/scope.js +421 -0
- package/src/compiler/utils.js +552 -0
- package/src/constants.js +4 -0
- package/src/jsx-runtime.d.ts +94 -0
- package/src/jsx-runtime.js +46 -0
- package/src/runtime/array.js +215 -0
- package/src/runtime/index.js +39 -0
- package/src/runtime/internal/client/blocks.js +247 -0
- package/src/runtime/internal/client/constants.js +23 -0
- package/src/runtime/internal/client/events.js +223 -0
- package/src/runtime/internal/client/for.js +388 -0
- package/src/runtime/internal/client/if.js +35 -0
- package/src/runtime/internal/client/index.js +53 -0
- package/src/runtime/internal/client/operations.js +72 -0
- package/src/runtime/internal/client/portal.js +33 -0
- package/src/runtime/internal/client/render.js +156 -0
- package/src/runtime/internal/client/runtime.js +909 -0
- package/src/runtime/internal/client/template.js +51 -0
- package/src/runtime/internal/client/try.js +139 -0
- package/src/runtime/internal/client/utils.js +16 -0
- package/src/utils/ast.js +214 -0
- package/src/utils/builders.js +733 -0
- package/src/utils/patterns.js +23 -0
- package/src/utils/sanitize_template_string.js +7 -0
- package/test-mappings.js +0 -0
- package/types/index.d.ts +2 -0
- package/.npmignore +0 -2
- package/History.md +0 -3
- package/Readme.md +0 -151
- package/lib/exec/index.js +0 -60
- package/ripple.js +0 -645
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
import { walk } from 'zimmerframe';
|
|
2
|
+
|
|
3
|
+
const seen = new Set();
|
|
4
|
+
const regex_backslash_and_following_character = /\\(.)/g;
|
|
5
|
+
|
|
6
|
+
function get_relative_selectors(node) {
|
|
7
|
+
const selectors = truncate(node);
|
|
8
|
+
|
|
9
|
+
if (node.metadata.rule?.metadata.parent_rule && selectors.length > 0) {
|
|
10
|
+
let has_explicit_nesting_selector = false;
|
|
11
|
+
|
|
12
|
+
// nesting could be inside pseudo classes like :is, :has or :where
|
|
13
|
+
for (let selector of selectors) {
|
|
14
|
+
walk(selector, null, {
|
|
15
|
+
// @ts-ignore
|
|
16
|
+
NestingSelector() {
|
|
17
|
+
has_explicit_nesting_selector = true;
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// if we found one we can break from the others
|
|
22
|
+
if (has_explicit_nesting_selector) break;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (!has_explicit_nesting_selector) {
|
|
26
|
+
if (selectors[0].combinator === null) {
|
|
27
|
+
selectors[0] = {
|
|
28
|
+
...selectors[0],
|
|
29
|
+
combinator: descendant_combinator
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
selectors.unshift(nesting_selector);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return selectors;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function truncate(node) {
|
|
41
|
+
const i = node.children.findLastIndex(({ metadata, selectors }) => {
|
|
42
|
+
const first = selectors[0];
|
|
43
|
+
return (
|
|
44
|
+
// not after a :global selector
|
|
45
|
+
!metadata.is_global_like &&
|
|
46
|
+
!(first.type === 'PseudoClassSelector' && first.name === 'global' && first.args === null) &&
|
|
47
|
+
// not a :global(...) without a :has/is/where(...) modifier that is scoped
|
|
48
|
+
!metadata.is_global
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return node.children.slice(0, i + 1).map((child) => {
|
|
53
|
+
// In case of `:root.y:has(...)`, `y` is unscoped, but everything in `:has(...)` should be scoped (if not global).
|
|
54
|
+
// To properly accomplish that, we gotta filter out all selector types except `:has`.
|
|
55
|
+
const root = child.selectors.find((s) => s.type === 'PseudoClassSelector' && s.name === 'root');
|
|
56
|
+
if (!root || child.metadata.is_global_like) return child;
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
...child,
|
|
60
|
+
selectors: child.selectors.filter((s) => s.type === 'PseudoClassSelector' && s.name === 'has')
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function apply_selector(relative_selectors, rule, element) {
|
|
66
|
+
const parent_selectors = relative_selectors.slice();
|
|
67
|
+
const relative_selector = parent_selectors.pop();
|
|
68
|
+
|
|
69
|
+
const matched =
|
|
70
|
+
!!relative_selector &&
|
|
71
|
+
relative_selector_might_apply_to_node(relative_selector, rule, element) &&
|
|
72
|
+
apply_combinator(relative_selector, parent_selectors, rule, element);
|
|
73
|
+
|
|
74
|
+
if (matched) {
|
|
75
|
+
if (!is_outer_global(relative_selector)) {
|
|
76
|
+
relative_selector.metadata.scoped = true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
element.metadata.scoped = true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return matched;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function apply_combinator(relative_selector, parent_selectors, rule, node) {
|
|
86
|
+
if (!relative_selector.combinator) return true;
|
|
87
|
+
|
|
88
|
+
const name = relative_selector.combinator.name;
|
|
89
|
+
|
|
90
|
+
switch (name) {
|
|
91
|
+
case ' ':
|
|
92
|
+
case '>': {
|
|
93
|
+
let parent_matched = false;
|
|
94
|
+
|
|
95
|
+
const path = node.metadata.path;
|
|
96
|
+
let i = path.length;
|
|
97
|
+
|
|
98
|
+
while (i--) {
|
|
99
|
+
const parent = path[i];
|
|
100
|
+
|
|
101
|
+
if (parent.type === 'Element') {
|
|
102
|
+
if (apply_selector(parent_selectors, rule, parent)) {
|
|
103
|
+
parent_matched = true;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (name === '>') return parent_matched;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return parent_matched || parent_selectors.every((selector) => is_global(selector, rule));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
case '+':
|
|
114
|
+
case '~': {
|
|
115
|
+
const siblings = get_possible_element_siblings(node, name === '+');
|
|
116
|
+
|
|
117
|
+
let sibling_matched = false;
|
|
118
|
+
|
|
119
|
+
for (const possible_sibling of siblings.keys()) {
|
|
120
|
+
if (possible_sibling.type === 'RenderTag' || possible_sibling.type === 'SlotElement') {
|
|
121
|
+
// `{@render foo()}<p>foo</p>` with `:global(.x) + p` is a match
|
|
122
|
+
if (parent_selectors.length === 1 && parent_selectors[0].metadata.is_global) {
|
|
123
|
+
sibling_matched = true;
|
|
124
|
+
}
|
|
125
|
+
} else if (apply_selector(parent_selectors, rule, possible_sibling)) {
|
|
126
|
+
sibling_matched = true;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
sibling_matched ||
|
|
132
|
+
(get_element_parent(node) === null &&
|
|
133
|
+
parent_selectors.every((selector) => is_global(selector, rule)))
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
default:
|
|
138
|
+
// TODO other combinators
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function get_element_parent(node) {
|
|
144
|
+
let path = node.metadata.path;
|
|
145
|
+
let i = path.length;
|
|
146
|
+
|
|
147
|
+
while (i--) {
|
|
148
|
+
const parent = path[i];
|
|
149
|
+
|
|
150
|
+
if (parent.type === 'RegularElement' || parent.type === 'SvelteElement') {
|
|
151
|
+
return parent;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function is_global(selector, rule) {
|
|
159
|
+
if (selector.metadata.is_global || selector.metadata.is_global_like) {
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
for (const s of selector.selectors) {
|
|
164
|
+
/** @type {Compiler.AST.CSS.SelectorList | null} */
|
|
165
|
+
let selector_list = null;
|
|
166
|
+
let owner = rule;
|
|
167
|
+
|
|
168
|
+
if (s.type === 'PseudoClassSelector') {
|
|
169
|
+
if ((s.name === 'is' || s.name === 'where') && s.args) {
|
|
170
|
+
selector_list = s.args;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (s.type === 'NestingSelector') {
|
|
175
|
+
owner = /** @type {Compiler.AST.CSS.Rule} */ (rule.metadata.parent_rule);
|
|
176
|
+
selector_list = owner.prelude;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const has_global_selectors = selector_list?.children.some((complex_selector) => {
|
|
180
|
+
return complex_selector.children.every((relative_selector) =>
|
|
181
|
+
is_global(relative_selector, owner)
|
|
182
|
+
);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
if (!has_global_selectors) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function is_text_attribute(attribute) {
|
|
194
|
+
return attribute.value.type === 'Literal';
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function test_attribute(operator, expected_value, case_insensitive, value) {
|
|
198
|
+
if (case_insensitive) {
|
|
199
|
+
expected_value = expected_value.toLowerCase();
|
|
200
|
+
value = value.toLowerCase();
|
|
201
|
+
}
|
|
202
|
+
switch (operator) {
|
|
203
|
+
case '=':
|
|
204
|
+
return value === expected_value;
|
|
205
|
+
case '~=':
|
|
206
|
+
return value.split(/\s/).includes(expected_value);
|
|
207
|
+
case '|=':
|
|
208
|
+
return `${value}-`.startsWith(`${expected_value}-`);
|
|
209
|
+
case '^=':
|
|
210
|
+
return value.startsWith(expected_value);
|
|
211
|
+
case '$=':
|
|
212
|
+
return value.endsWith(expected_value);
|
|
213
|
+
case '*=':
|
|
214
|
+
return value.includes(expected_value);
|
|
215
|
+
default:
|
|
216
|
+
throw new Error("this shouldn't happen");
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function attribute_matches(node, name, expected_value, operator, case_insensitive) {
|
|
221
|
+
for (const attribute of node.attributes) {
|
|
222
|
+
if (attribute.type === 'SpreadAttribute') return true;
|
|
223
|
+
|
|
224
|
+
if (attribute.type !== 'Attribute') continue;
|
|
225
|
+
if (attribute.name.name.toLowerCase() !== name.toLowerCase()) continue;
|
|
226
|
+
|
|
227
|
+
if (expected_value === null) return true;
|
|
228
|
+
|
|
229
|
+
if (is_text_attribute(attribute)) {
|
|
230
|
+
return test_attribute(operator, expected_value, case_insensitive, attribute.value.value);
|
|
231
|
+
} else {
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function is_outer_global(relative_selector) {
|
|
240
|
+
const first = relative_selector.selectors[0];
|
|
241
|
+
|
|
242
|
+
return (
|
|
243
|
+
first.type === 'PseudoClassSelector' &&
|
|
244
|
+
first.name === 'global' &&
|
|
245
|
+
(first.args === null ||
|
|
246
|
+
// Only these two selector types can keep the whole selector global, because e.g.
|
|
247
|
+
// :global(button).x means that the selector is still scoped because of the .x
|
|
248
|
+
relative_selector.selectors.every(
|
|
249
|
+
(selector) =>
|
|
250
|
+
selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector'
|
|
251
|
+
))
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function relative_selector_might_apply_to_node(relative_selector, rule, element) {
|
|
256
|
+
// Sort :has(...) selectors in one bucket and everything else into another
|
|
257
|
+
const has_selectors = [];
|
|
258
|
+
const other_selectors = [];
|
|
259
|
+
|
|
260
|
+
for (const selector of relative_selector.selectors) {
|
|
261
|
+
if (selector.type === 'PseudoClassSelector' && selector.name === 'has' && selector.args) {
|
|
262
|
+
has_selectors.push(selector);
|
|
263
|
+
} else {
|
|
264
|
+
other_selectors.push(selector);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// If we're called recursively from a :has(...) selector, we're on the way of checking if the other selectors match.
|
|
269
|
+
// In that case ignore this check (because we just came from this) to avoid an infinite loop.
|
|
270
|
+
if (has_selectors.length > 0) {
|
|
271
|
+
/** @type {Array<Compiler.AST.RegularElement | Compiler.AST.SvelteElement>} */
|
|
272
|
+
const child_elements = [];
|
|
273
|
+
/** @type {Array<Compiler.AST.RegularElement | Compiler.AST.SvelteElement>} */
|
|
274
|
+
const descendant_elements = [];
|
|
275
|
+
/** @type {Array<Compiler.AST.RegularElement | Compiler.AST.SvelteElement>} */
|
|
276
|
+
let sibling_elements; // do them lazy because it's rarely used and expensive to calculate
|
|
277
|
+
|
|
278
|
+
// If this is a :has inside a global selector, we gotta include the element itself, too,
|
|
279
|
+
// because the global selector might be for an element that's outside the component,
|
|
280
|
+
// e.g. :root:has(.scoped), :global(.foo):has(.scoped), or :root { &:has(.scoped) {} }
|
|
281
|
+
const rules = get_parent_rules(rule);
|
|
282
|
+
const include_self =
|
|
283
|
+
rules.some((r) => r.prelude.children.some((c) => c.children.some((s) => is_global(s, r)))) ||
|
|
284
|
+
rules[rules.length - 1].prelude.children.some((c) =>
|
|
285
|
+
c.children.some((r) =>
|
|
286
|
+
r.selectors.some(
|
|
287
|
+
(s) =>
|
|
288
|
+
s.type === 'PseudoClassSelector' &&
|
|
289
|
+
(s.name === 'root' || (s.name === 'global' && s.args))
|
|
290
|
+
)
|
|
291
|
+
)
|
|
292
|
+
);
|
|
293
|
+
if (include_self) {
|
|
294
|
+
child_elements.push(element);
|
|
295
|
+
descendant_elements.push(element);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* @param {Compiler.AST.SvelteNode} node
|
|
300
|
+
* @param {{ is_child: boolean }} state
|
|
301
|
+
*/
|
|
302
|
+
function walk_children(node, state) {
|
|
303
|
+
walk(node, state, {
|
|
304
|
+
_(node, context) {
|
|
305
|
+
if (node.type === 'Element') {
|
|
306
|
+
descendant_elements.push(node);
|
|
307
|
+
|
|
308
|
+
if (context.state.is_child) {
|
|
309
|
+
child_elements.push(node);
|
|
310
|
+
context.state.is_child = false;
|
|
311
|
+
context.next();
|
|
312
|
+
context.state.is_child = true;
|
|
313
|
+
} else {
|
|
314
|
+
context.next();
|
|
315
|
+
}
|
|
316
|
+
} else {
|
|
317
|
+
context.next();
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
walk_children(element.fragment, { is_child: true });
|
|
324
|
+
|
|
325
|
+
// :has(...) is special in that it means "look downwards in the CSS tree". Since our matching algorithm goes
|
|
326
|
+
// upwards and back-to-front, we need to first check the selectors inside :has(...), then check the rest of the
|
|
327
|
+
// selector in a way that is similar to ancestor matching. In a sense, we're treating `.x:has(.y)` as `.x .y`.
|
|
328
|
+
for (const has_selector of has_selectors) {
|
|
329
|
+
const complex_selectors = /** @type {Compiler.AST.CSS.SelectorList} */ (has_selector.args)
|
|
330
|
+
.children;
|
|
331
|
+
let matched = false;
|
|
332
|
+
|
|
333
|
+
for (const complex_selector of complex_selectors) {
|
|
334
|
+
const selectors = truncate(complex_selector);
|
|
335
|
+
const left_most_combinator = selectors[0]?.combinator ?? descendant_combinator;
|
|
336
|
+
// In .x:has(> y), we want to search for y, ignoring the left-most combinator
|
|
337
|
+
// (else it would try to walk further up and fail because there are no selectors left)
|
|
338
|
+
if (selectors.length > 0) {
|
|
339
|
+
selectors[0] = {
|
|
340
|
+
...selectors[0],
|
|
341
|
+
combinator: null
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const descendants =
|
|
346
|
+
left_most_combinator.name === '+' || left_most_combinator.name === '~'
|
|
347
|
+
? (sibling_elements ??= get_following_sibling_elements(element, include_self))
|
|
348
|
+
: left_most_combinator.name === '>'
|
|
349
|
+
? child_elements
|
|
350
|
+
: descendant_elements;
|
|
351
|
+
|
|
352
|
+
let selector_matched = false;
|
|
353
|
+
|
|
354
|
+
// Iterate over all descendant elements and check if the selector inside :has matches
|
|
355
|
+
for (const element of descendants) {
|
|
356
|
+
if (
|
|
357
|
+
selectors.length === 0 /* is :global(...) */ ||
|
|
358
|
+
(element.metadata.scoped && selector_matched) ||
|
|
359
|
+
apply_selector(selectors, rule, element)
|
|
360
|
+
) {
|
|
361
|
+
complex_selector.metadata.used = true;
|
|
362
|
+
selector_matched = matched = true;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (!matched) {
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
for (const selector of other_selectors) {
|
|
374
|
+
if (selector.type === 'Percentage' || selector.type === 'Nth') continue;
|
|
375
|
+
|
|
376
|
+
const name = selector.name.replace(regex_backslash_and_following_character, '$1');
|
|
377
|
+
|
|
378
|
+
switch (selector.type) {
|
|
379
|
+
case 'PseudoClassSelector': {
|
|
380
|
+
if (name === 'host' || name === 'root') return false;
|
|
381
|
+
|
|
382
|
+
if (
|
|
383
|
+
name === 'global' &&
|
|
384
|
+
selector.args !== null &&
|
|
385
|
+
relative_selector.selectors.length === 1
|
|
386
|
+
) {
|
|
387
|
+
const args = selector.args;
|
|
388
|
+
const complex_selector = args.children[0];
|
|
389
|
+
return apply_selector(complex_selector.children, rule, element);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// We came across a :global, everything beyond it is global and therefore a potential match
|
|
393
|
+
if (name === 'global' && selector.args === null) return true;
|
|
394
|
+
|
|
395
|
+
// :not(...) contents should stay unscoped. Scoping them would achieve the opposite of what we want,
|
|
396
|
+
// because they are then _more_ likely to bleed out of the component. The exception is complex selectors
|
|
397
|
+
// with descendants, in which case we scope them all.
|
|
398
|
+
if (name === 'not' && selector.args) {
|
|
399
|
+
for (const complex_selector of selector.args.children) {
|
|
400
|
+
walk(complex_selector, null, {
|
|
401
|
+
ComplexSelector(node, context) {
|
|
402
|
+
node.metadata.used = true;
|
|
403
|
+
context.next();
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
const relative = truncate(complex_selector);
|
|
407
|
+
|
|
408
|
+
if (complex_selector.children.length > 1) {
|
|
409
|
+
// 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).
|
|
410
|
+
// We can't fully check if that actually matches with our current algorithm, so we just assume it does.
|
|
411
|
+
// The result may not match a real element, so the only drawback is the missing prune.
|
|
412
|
+
for (const selector of relative) {
|
|
413
|
+
selector.metadata.scoped = true;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** @type {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | null} */
|
|
417
|
+
let el = element;
|
|
418
|
+
while (el) {
|
|
419
|
+
el.metadata.scoped = true;
|
|
420
|
+
el = get_element_parent(el);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
break;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if ((name === 'is' || name === 'where') && selector.args) {
|
|
429
|
+
let matched = false;
|
|
430
|
+
|
|
431
|
+
for (const complex_selector of selector.args.children) {
|
|
432
|
+
const relative = truncate(complex_selector);
|
|
433
|
+
const is_global = relative.length === 0;
|
|
434
|
+
|
|
435
|
+
if (is_global) {
|
|
436
|
+
complex_selector.metadata.used = true;
|
|
437
|
+
matched = true;
|
|
438
|
+
} else if (apply_selector(relative, rule, element)) {
|
|
439
|
+
complex_selector.metadata.used = true;
|
|
440
|
+
matched = true;
|
|
441
|
+
} else if (complex_selector.children.length > 1 && (name == 'is' || name == 'where')) {
|
|
442
|
+
// foo :is(bar baz) can also mean that bar is an ancestor of foo, and baz a descendant.
|
|
443
|
+
// We can't fully check if that actually matches with our current algorithm, so we just assume it does.
|
|
444
|
+
// The result may not match a real element, so the only drawback is the missing prune.
|
|
445
|
+
complex_selector.metadata.used = true;
|
|
446
|
+
matched = true;
|
|
447
|
+
for (const selector of relative) {
|
|
448
|
+
selector.metadata.scoped = true;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (!matched) {
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
break;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
case 'PseudoElementSelector': {
|
|
462
|
+
break;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
case 'AttributeSelector': {
|
|
466
|
+
const whitelisted = whitelist_attribute_selector.get(element.id.name.toLowerCase());
|
|
467
|
+
if (
|
|
468
|
+
!whitelisted?.includes(selector.id.name.toLowerCase()) &&
|
|
469
|
+
!attribute_matches(
|
|
470
|
+
element,
|
|
471
|
+
selector.name,
|
|
472
|
+
selector.value && unquote(selector.value),
|
|
473
|
+
selector.matcher,
|
|
474
|
+
selector.flags?.includes('i') ?? false
|
|
475
|
+
)
|
|
476
|
+
) {
|
|
477
|
+
return false;
|
|
478
|
+
}
|
|
479
|
+
break;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
case 'ClassSelector': {
|
|
483
|
+
if (
|
|
484
|
+
!attribute_matches(element, 'class', name, '~=', false) &&
|
|
485
|
+
!element.attributes.some(
|
|
486
|
+
(attribute) => attribute.type === 'ClassDirective' && attribute.name === name
|
|
487
|
+
)
|
|
488
|
+
) {
|
|
489
|
+
return false;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
break;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
case 'IdSelector': {
|
|
496
|
+
if (!attribute_matches(element, 'id', name, '=', false)) {
|
|
497
|
+
return false;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
break;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
case 'TypeSelector': {
|
|
504
|
+
if (
|
|
505
|
+
element.id.name.toLowerCase() !== name.toLowerCase() &&
|
|
506
|
+
name !== '*' &&
|
|
507
|
+
element.id.name[0].toLowerCase() === element.id.name[0]
|
|
508
|
+
) {
|
|
509
|
+
return false;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
break;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
case 'NestingSelector': {
|
|
516
|
+
let matched = false;
|
|
517
|
+
|
|
518
|
+
const parent = /** @type {Compiler.AST.CSS.Rule} */ (rule.metadata.parent_rule);
|
|
519
|
+
|
|
520
|
+
for (const complex_selector of parent.prelude.children) {
|
|
521
|
+
if (
|
|
522
|
+
apply_selector(get_relative_selectors(complex_selector), parent, element) ||
|
|
523
|
+
complex_selector.children.every((s) => is_global(s, parent))
|
|
524
|
+
) {
|
|
525
|
+
complex_selector.metadata.used = true;
|
|
526
|
+
matched = true;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (!matched) {
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
break;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// possible match
|
|
540
|
+
return true;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
export function prune_css(css, element) {
|
|
544
|
+
walk(css, null, {
|
|
545
|
+
Rule(node, context) {
|
|
546
|
+
if (node.metadata.is_global_block) {
|
|
547
|
+
context.visit(node.prelude);
|
|
548
|
+
} else {
|
|
549
|
+
context.next();
|
|
550
|
+
}
|
|
551
|
+
},
|
|
552
|
+
ComplexSelector(node) {
|
|
553
|
+
const selectors = get_relative_selectors(node);
|
|
554
|
+
|
|
555
|
+
seen.clear();
|
|
556
|
+
|
|
557
|
+
if (
|
|
558
|
+
apply_selector(
|
|
559
|
+
selectors,
|
|
560
|
+
/** @type {Compiler.AST.CSS.Rule} */ (node.metadata.rule),
|
|
561
|
+
element
|
|
562
|
+
)
|
|
563
|
+
) {
|
|
564
|
+
node.metadata.used = true;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// note: we don't call context.next() here, we only recurse into
|
|
568
|
+
// selectors that don't belong to rules (i.e. inside `:is(...)` etc)
|
|
569
|
+
// when we encounter them below
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
}
|