ripple 0.2.166 → 0.2.168
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/src/runtime/internal/client/blocks.js +7 -5
- package/src/runtime/internal/client/index.js +2 -1
- package/src/runtime/internal/client/runtime.js +9 -5
- 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
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { error } from '../../errors.js';
|
|
2
|
+
|
|
3
|
+
const invalid_nestings = {
|
|
4
|
+
// <p> cannot contain block-level elements
|
|
5
|
+
p: new Set([
|
|
6
|
+
'address',
|
|
7
|
+
'article',
|
|
8
|
+
'aside',
|
|
9
|
+
'blockquote',
|
|
10
|
+
'details',
|
|
11
|
+
'div',
|
|
12
|
+
'dl',
|
|
13
|
+
'fieldset',
|
|
14
|
+
'figcaption',
|
|
15
|
+
'figure',
|
|
16
|
+
'footer',
|
|
17
|
+
'form',
|
|
18
|
+
'h1',
|
|
19
|
+
'h2',
|
|
20
|
+
'h3',
|
|
21
|
+
'h4',
|
|
22
|
+
'h5',
|
|
23
|
+
'h6',
|
|
24
|
+
'header',
|
|
25
|
+
'hgroup',
|
|
26
|
+
'hr',
|
|
27
|
+
'main',
|
|
28
|
+
'menu',
|
|
29
|
+
'nav',
|
|
30
|
+
'ol',
|
|
31
|
+
'p',
|
|
32
|
+
'pre',
|
|
33
|
+
'section',
|
|
34
|
+
'table',
|
|
35
|
+
'ul',
|
|
36
|
+
]),
|
|
37
|
+
// <span> cannot contain block-level elements
|
|
38
|
+
span: new Set([
|
|
39
|
+
'address',
|
|
40
|
+
'article',
|
|
41
|
+
'aside',
|
|
42
|
+
'blockquote',
|
|
43
|
+
'details',
|
|
44
|
+
'div',
|
|
45
|
+
'dl',
|
|
46
|
+
'fieldset',
|
|
47
|
+
'figcaption',
|
|
48
|
+
'figure',
|
|
49
|
+
'footer',
|
|
50
|
+
'form',
|
|
51
|
+
'h1',
|
|
52
|
+
'h2',
|
|
53
|
+
'h3',
|
|
54
|
+
'h4',
|
|
55
|
+
'h5',
|
|
56
|
+
'h6',
|
|
57
|
+
'header',
|
|
58
|
+
'hgroup',
|
|
59
|
+
'hr',
|
|
60
|
+
'main',
|
|
61
|
+
'menu',
|
|
62
|
+
'nav',
|
|
63
|
+
'ol',
|
|
64
|
+
'p',
|
|
65
|
+
'pre',
|
|
66
|
+
'section',
|
|
67
|
+
'table',
|
|
68
|
+
'ul',
|
|
69
|
+
]),
|
|
70
|
+
// Interactive elements cannot be nested
|
|
71
|
+
a: new Set(['a', 'button']),
|
|
72
|
+
button: new Set(['a', 'button']),
|
|
73
|
+
// Form elements
|
|
74
|
+
label: new Set(['label']),
|
|
75
|
+
form: new Set(['form']),
|
|
76
|
+
// Headings cannot be nested within each other
|
|
77
|
+
h1: new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6']),
|
|
78
|
+
h2: new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6']),
|
|
79
|
+
h3: new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6']),
|
|
80
|
+
h4: new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6']),
|
|
81
|
+
h5: new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6']),
|
|
82
|
+
h6: new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6']),
|
|
83
|
+
// Table structure
|
|
84
|
+
table: new Set(['table', 'tr', 'td', 'th']), // Can only contain caption, colgroup, thead, tbody, tfoot
|
|
85
|
+
thead: new Set(['caption', 'colgroup', 'thead', 'tbody', 'tfoot', 'td', 'th']), // Can only contain tr
|
|
86
|
+
tbody: new Set(['caption', 'colgroup', 'thead', 'tbody', 'tfoot', 'td', 'th']), // Can only contain tr
|
|
87
|
+
tfoot: new Set(['caption', 'colgroup', 'thead', 'tbody', 'tfoot', 'td', 'th']), // Can only contain tr
|
|
88
|
+
tr: new Set(['caption', 'colgroup', 'thead', 'tbody', 'tfoot', 'tr']), // Can only contain td and th
|
|
89
|
+
td: new Set(['td', 'th']), // Cannot nest td/th elements
|
|
90
|
+
th: new Set(['td', 'th']), // Cannot nest td/th elements
|
|
91
|
+
// Media elements
|
|
92
|
+
picture: new Set(['picture']),
|
|
93
|
+
// Main landmark - only one per document, cannot be nested
|
|
94
|
+
main: new Set(['main']),
|
|
95
|
+
// Other semantic restrictions
|
|
96
|
+
figcaption: new Set(['figcaption']),
|
|
97
|
+
dt: new Set([
|
|
98
|
+
'header',
|
|
99
|
+
'footer',
|
|
100
|
+
'article',
|
|
101
|
+
'aside',
|
|
102
|
+
'nav',
|
|
103
|
+
'section',
|
|
104
|
+
'h1',
|
|
105
|
+
'h2',
|
|
106
|
+
'h3',
|
|
107
|
+
'h4',
|
|
108
|
+
'h5',
|
|
109
|
+
'h6',
|
|
110
|
+
]),
|
|
111
|
+
// No interactive content inside summary
|
|
112
|
+
summary: new Set(['summary']),
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* @param {any} element
|
|
117
|
+
* @returns {string | null}
|
|
118
|
+
*/
|
|
119
|
+
function get_element_tag(element) {
|
|
120
|
+
return element.id.type === 'Identifier' ? element.id.name : null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @param {any} element
|
|
125
|
+
* @param {any} state
|
|
126
|
+
* @param {any} context
|
|
127
|
+
*/
|
|
128
|
+
export function validate_nesting(element, state, context) {
|
|
129
|
+
const tag = get_element_tag(element);
|
|
130
|
+
|
|
131
|
+
if (tag === null) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
for (let i = context.path.length - 1; i >= 0; i--) {
|
|
136
|
+
const parent = context.path[i];
|
|
137
|
+
if (parent.type === 'Element') {
|
|
138
|
+
const parent_tag = get_element_tag(parent);
|
|
139
|
+
if (parent_tag === null) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (parent_tag in invalid_nestings) {
|
|
144
|
+
const validation_set =
|
|
145
|
+
invalid_nestings[/** @type {keyof typeof invalid_nestings} */ (parent_tag)];
|
|
146
|
+
if (validation_set.has(tag)) {
|
|
147
|
+
error(
|
|
148
|
+
`Invalid HTML nesting: <${tag}> cannot be a descendant of <${parent_tag}>.`,
|
|
149
|
+
state.analysis.module.filename,
|
|
150
|
+
context,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -24,6 +24,65 @@ function is_in_global_block(path) {
|
|
|
24
24
|
return path.some((node) => node.type === 'Rule' && node.metadata.is_global_block);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Check if we're inside a pseudo-class selector that's INSIDE a :global() wrapper
|
|
29
|
+
* or adjacent to a :global modifier
|
|
30
|
+
* @param {any[]} path
|
|
31
|
+
*/
|
|
32
|
+
function is_in_global_pseudo(path) {
|
|
33
|
+
// Walk up the path to find if we're inside a :global() pseudo-class selector with args
|
|
34
|
+
// or if we're in a pseudo-class that's in the same RelativeSelector as a :global modifier
|
|
35
|
+
for (let i = path.length - 1; i >= 0; i--) {
|
|
36
|
+
const node = path[i];
|
|
37
|
+
|
|
38
|
+
// Case 1: :global(...) with args - we're inside it
|
|
39
|
+
if (node.type === 'PseudoClassSelector' && node.name === 'global' && node.args !== null) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Case 2: We're in a PseudoClassSelector (like :is, :where, :has, :not)
|
|
44
|
+
// Check if there's a :global modifier in the same RelativeSelector
|
|
45
|
+
if (
|
|
46
|
+
node.type === 'PseudoClassSelector' &&
|
|
47
|
+
(node.name === 'is' || node.name === 'where' || node.name === 'has' || node.name === 'not')
|
|
48
|
+
) {
|
|
49
|
+
// Look for the parent RelativeSelector
|
|
50
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
51
|
+
const ancestor = path[j];
|
|
52
|
+
if (ancestor.type === 'RelativeSelector') {
|
|
53
|
+
// Check if this RelativeSelector has a :global modifier (no args)
|
|
54
|
+
const hasGlobalModifier = ancestor.selectors.some(
|
|
55
|
+
(s) => s.type === 'PseudoClassSelector' && s.name === 'global' && s.args === null,
|
|
56
|
+
);
|
|
57
|
+
if (hasGlobalModifier) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Check if a rule has :global in the middle (like `div :global p`)
|
|
70
|
+
* These rules should treat nested selectors as global
|
|
71
|
+
* @param {AST.CSS.Rule} rule
|
|
72
|
+
*/
|
|
73
|
+
function has_global_in_middle(rule) {
|
|
74
|
+
for (const complex_selector of rule.prelude.children) {
|
|
75
|
+
for (let i = 0; i < complex_selector.children.length; i++) {
|
|
76
|
+
const child = complex_selector.children[i];
|
|
77
|
+
// Check if this is a :global selector that's not at the start
|
|
78
|
+
if (i > 0 && child.metadata.is_global) {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
27
86
|
function remove_global_pseudo_class(selector, combinator, state) {
|
|
28
87
|
if (selector.args === null) {
|
|
29
88
|
let start = selector.start;
|
|
@@ -75,13 +134,19 @@ function is_empty(rule, is_in_global_block) {
|
|
|
75
134
|
return rule.block.children.length === 0;
|
|
76
135
|
}
|
|
77
136
|
|
|
137
|
+
// Rules with :global in the middle (like `div :global p`) should treat nested rules as global
|
|
138
|
+
const has_mid_global = has_global_in_middle(rule);
|
|
139
|
+
|
|
78
140
|
for (const child of rule.block.children) {
|
|
79
141
|
if (child.type === 'Declaration') {
|
|
80
142
|
return false;
|
|
81
143
|
}
|
|
82
144
|
|
|
83
145
|
if (child.type === 'Rule') {
|
|
84
|
-
if (
|
|
146
|
+
if (
|
|
147
|
+
(is_used(child) || is_in_global_block || has_mid_global) &&
|
|
148
|
+
!is_empty(child, is_in_global_block || has_mid_global)
|
|
149
|
+
) {
|
|
85
150
|
return false;
|
|
86
151
|
}
|
|
87
152
|
}
|
|
@@ -196,8 +261,10 @@ const visitors = {
|
|
|
196
261
|
},
|
|
197
262
|
SelectorList(node, { state, next, path }) {
|
|
198
263
|
// Only add comments if we're not inside a complex selector that itself is unused or a global block
|
|
264
|
+
// or inside a pseudo-class that's part of a global selector
|
|
199
265
|
if (
|
|
200
266
|
!is_in_global_block(path) &&
|
|
267
|
+
!is_in_global_pseudo(path) &&
|
|
201
268
|
!path.find((n) => n.type === 'ComplexSelector' && !n.metadata.used)
|
|
202
269
|
) {
|
|
203
270
|
const children = node.children;
|
|
@@ -280,8 +347,39 @@ const visitors = {
|
|
|
280
347
|
ComplexSelector(node, context) {
|
|
281
348
|
const before_bumped = context.state.specificity.bumped;
|
|
282
349
|
|
|
350
|
+
// Check if we're inside a :has/:is/:where/:not pseudo-class that's part of a global selector
|
|
351
|
+
// In that case, we should still scope the contents even though the parent is global
|
|
352
|
+
const parentPath = context.path;
|
|
353
|
+
let insideScopingPseudo = false;
|
|
354
|
+
|
|
355
|
+
// Walk up the path to find if we're inside args of :has/:is/:where/:not
|
|
356
|
+
for (let i = parentPath.length - 1; i >= 0; i--) {
|
|
357
|
+
const pathNode = parentPath[i];
|
|
358
|
+
|
|
359
|
+
// Check if we're inside a SelectorList that belongs to a scoping pseudo-class
|
|
360
|
+
if (pathNode.type === 'SelectorList' && i > 0) {
|
|
361
|
+
const parent = parentPath[i - 1];
|
|
362
|
+
if (
|
|
363
|
+
parent.type === 'PseudoClassSelector' &&
|
|
364
|
+
(parent.name === 'has' ||
|
|
365
|
+
parent.name === 'is' ||
|
|
366
|
+
parent.name === 'where' ||
|
|
367
|
+
parent.name === 'not')
|
|
368
|
+
) {
|
|
369
|
+
// Now check if this pseudo-class is part of a global RelativeSelector
|
|
370
|
+
for (let j = i - 2; j >= 0; j--) {
|
|
371
|
+
if (parentPath[j].type === 'RelativeSelector' && parentPath[j].metadata?.is_global) {
|
|
372
|
+
insideScopingPseudo = true;
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
283
381
|
for (const relative_selector of node.children) {
|
|
284
|
-
if (relative_selector.metadata.is_global) {
|
|
382
|
+
if (relative_selector.metadata.is_global && !insideScopingPseudo) {
|
|
285
383
|
const global = /** @type {AST.CSS.PseudoClassSelector} */ (relative_selector.selectors[0]);
|
|
286
384
|
remove_global_pseudo_class(global, relative_selector.combinator, context.state);
|
|
287
385
|
|
|
@@ -303,7 +401,8 @@ const visitors = {
|
|
|
303
401
|
}
|
|
304
402
|
}
|
|
305
403
|
|
|
306
|
-
if
|
|
404
|
+
// Skip scoping if we're inside a global block
|
|
405
|
+
if (relative_selector.metadata.scoped && !is_in_global_block(context.path)) {
|
|
307
406
|
if (relative_selector.selectors.length === 1) {
|
|
308
407
|
// skip standalone :is/:where/& selectors
|
|
309
408
|
const selector = relative_selector.selectors[0];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** @import { Block, Derived, CompatOptions } from '#client' */
|
|
1
|
+
/** @import { Block, Derived, CompatOptions, Component } from '#client' */
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
BLOCK_HAS_RUN,
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
active_block,
|
|
20
20
|
active_component,
|
|
21
21
|
active_reaction,
|
|
22
|
+
create_component_ctx,
|
|
22
23
|
is_block_dirty,
|
|
23
24
|
run_block,
|
|
24
25
|
run_teardown,
|
|
@@ -151,7 +152,7 @@ export function root(fn, compat) {
|
|
|
151
152
|
};
|
|
152
153
|
}
|
|
153
154
|
|
|
154
|
-
return block(ROOT_BLOCK,
|
|
155
|
+
return block(ROOT_BLOCK, target_fn, { compat }, create_component_ctx());
|
|
155
156
|
}
|
|
156
157
|
|
|
157
158
|
/**
|
|
@@ -181,13 +182,14 @@ function push_block(block, parent_block) {
|
|
|
181
182
|
/**
|
|
182
183
|
* @param {number} flags
|
|
183
184
|
* @param {Function} fn
|
|
184
|
-
* @param {any} state
|
|
185
|
+
* @param {any} [state]
|
|
186
|
+
* @param {Component} [co]
|
|
185
187
|
* @returns {Block}
|
|
186
188
|
*/
|
|
187
|
-
export function block(flags, fn, state = null) {
|
|
189
|
+
export function block(flags, fn, state = null, co) {
|
|
188
190
|
/** @type {Block} */
|
|
189
191
|
var block = {
|
|
190
|
-
co: active_component,
|
|
192
|
+
co: co || active_component,
|
|
191
193
|
d: null,
|
|
192
194
|
first: null,
|
|
193
195
|
f: flags,
|
|
@@ -4,6 +4,7 @@ export {
|
|
|
4
4
|
next_sibling as sibling,
|
|
5
5
|
document,
|
|
6
6
|
create_text,
|
|
7
|
+
init_operations,
|
|
7
8
|
} from './operations.js';
|
|
8
9
|
|
|
9
10
|
export {
|
|
@@ -16,7 +17,7 @@ export {
|
|
|
16
17
|
set_selected,
|
|
17
18
|
} from './render.js';
|
|
18
19
|
|
|
19
|
-
export { render, render_spread, async, ref, branch, destroy_block } from './blocks.js';
|
|
20
|
+
export { render, render_spread, async, ref, branch, destroy_block, root } from './blocks.js';
|
|
20
21
|
|
|
21
22
|
export { event, delegate } from './events.js';
|
|
22
23
|
|
|
@@ -1197,16 +1197,20 @@ export function safe_scope(err = 'Cannot access outside of a component context')
|
|
|
1197
1197
|
return /** @type {Block} */ (active_scope);
|
|
1198
1198
|
}
|
|
1199
1199
|
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
*/
|
|
1203
|
-
export function push_component() {
|
|
1204
|
-
var component = {
|
|
1200
|
+
export function create_component_ctx() {
|
|
1201
|
+
return {
|
|
1205
1202
|
c: null,
|
|
1206
1203
|
e: null,
|
|
1207
1204
|
m: false,
|
|
1208
1205
|
p: active_component,
|
|
1209
1206
|
};
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
/**
|
|
1210
|
+
* @returns {void}
|
|
1211
|
+
*/
|
|
1212
|
+
export function push_component() {
|
|
1213
|
+
var component = create_component_ctx();
|
|
1210
1214
|
active_component = component;
|
|
1211
1215
|
}
|
|
1212
1216
|
|