Skip to content

Commit 9419ea3

Browse files
mattrbeckAndrewKushnir
authored andcommitted
fix(compiler): support complex selectors in :nth-child()
:nth-child() (and its siblings) support complex expressions, e.g. `:nth-child(2n of :is(.foo, .bar))`. Previously we'd choke because of the `:is()`. Now, we reuse the `_parenSuffix` subexpression to match nested parentheses the same way we do for :host() and :host-context(). Note that we only support 3 levels of nesting, so a selector like `:nth-child(n of :is(:has(:not(.foo))))` will still break. I'll say yet again that we really should add a proper parser so we stop getting bug reports like this :) Fixes #64913 (cherry picked from commit 24cfd5a)
1 parent f00fb46 commit 9419ea3

File tree

3 files changed

+20
-2
lines changed

3 files changed

+20
-2
lines changed

packages/compiler/src/shadow_css.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1029,9 +1029,9 @@ class SafeSelector {
10291029

10301030
// Replaces the expression in `:nth-child(2n + 1)` with a placeholder.
10311031
// WS and "+" would otherwise be interpreted as selector separators.
1032-
this._content = selector.replace(/(:nth-[-\w]+)(\([^)]+\))/g, (_, pseudo, exp) => {
1032+
this._content = selector.replace(nthRegex, (_, pseudo, exp) => {
10331033
const replaceBy = `__ph-${this.index}__`;
1034-
this.placeholders.push(exp);
1034+
this.placeholders.push(`(${exp})`);
10351035
this.index++;
10361036
return pseudo + replaceBy;
10371037
});
@@ -1076,6 +1076,7 @@ const _level1Parens = String.raw`(?:\(${_noParens}\)|${_noParens})+?`;
10761076
// Matches content with at most TWO levels of nesting, e.g., "a(b(c)d)e"
10771077
const _level2Parens = String.raw`(?:\(${_level1Parens}\)|${_noParens})+?`;
10781078
const _parenSuffix = String.raw`(?:\((${_level2Parens})\))`;
1079+
const nthRegex = new RegExp(String.raw`(:nth-[-\w]+)` + _parenSuffix, 'g');
10791080
const _cssColonHostRe = new RegExp(_polyfillHost + _parenSuffix + '?([^,{]*)', 'gim');
10801081
// note: :host-context patterns are terminated with `{`, as opposed to :host which
10811082
// is both `{` and `,` because :host-context handles top-level commas differently.

packages/compiler/test/shadow_css/host_and_host_context_spec.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ describe('ShadowCss, :host and :host-context', () => {
7171
expect(shim(':host:nth-child(8n+1) {}', 'contenta', 'a-host')).toEqualCss(
7272
'[a-host]:nth-child(8n+1) {}',
7373
);
74+
expect(shim(':host(:nth-child(3n of :not(p, a))) {}', 'contenta', 'a-host')).toEqualCss(
75+
'[a-host]:nth-child(3n of :not(p, a)) {}',
76+
);
7477
expect(shim(':host:nth-of-type(8n+1) {}', 'contenta', 'a-host')).toEqualCss(
7578
'[a-host]:nth-of-type(8n+1) {}',
7679
);

packages/compiler/test/shadow_css/shadow_css_spec.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,20 @@ describe('ShadowCss', () => {
214214
'table[contenta] [contenta]:where(td, th):hover { color:lime;}',
215215
);
216216

217+
// :nth
218+
expect(shim(':nth-child(3n of :not(p, a), :is(.foo)) {}', 'contenta', 'hosta')).toEqualCss(
219+
'[contenta]:nth-child(3n of :not(p, a), :is(.foo)) {}',
220+
);
221+
expect(shim('li:nth-last-child(-n + 3) {}', 'contenta', 'a-host')).toEqualCss(
222+
'li[contenta]:nth-last-child(-n + 3) {}',
223+
);
224+
expect(shim('dd:nth-last-of-type(3n) {}', 'contenta', 'a-host')).toEqualCss(
225+
'dd[contenta]:nth-last-of-type(3n) {}',
226+
);
227+
expect(shim('dd:nth-of-type(even) {}', 'contenta', 'a-host')).toEqualCss(
228+
'dd[contenta]:nth-of-type(even) {}',
229+
);
230+
217231
// complex selectors
218232
expect(shim(':host:is([foo],[foo-2])>div.example-2 {}', 'contenta', 'a-host')).toEqualCss(
219233
'[a-host]:is([foo],[foo-2]) > div.example-2[contenta] {}',

0 commit comments

Comments
 (0)