From aad6ced0ef5e535d1a6eae7c79df4e03ea73b7f2 Mon Sep 17 00:00:00 2001 From: Matthew Berry Date: Wed, 5 Nov 2025 23:37:55 +0000 Subject: [PATCH] fix(compiler): support one additional level of nesting in :host() Previously we supported one level of nested parentheses inside of a `:host()` selector, e.g. `:host(:not(p))`. This caused a breakage in g3 when I migrated a selector from `:host:not(:has(p))` to `:host(:not(:has(p)))`. This change adds support for just one more level of nesting. It'd be nice to move everything to a real CSS parser (or even update it to count parentheses like I did with :host-context()), but I wasn't able to get that to work in ~20 minutes and I'm focusing on other things at the moment. This change punts the problem until somebody tries to use just one more level of nesting in a selector. Fixes #64830 (cherry picked from commit 444143758e0526fe31f790793ebeb41b73fbff3b) --- packages/compiler/src/shadow_css.ts | 8 +++++++- .../test/shadow_css/host_and_host_context_spec.ts | 3 +++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/compiler/src/shadow_css.ts b/packages/compiler/src/shadow_css.ts index 9fded1d8ad5..6b24cefbc5c 100644 --- a/packages/compiler/src/shadow_css.ts +++ b/packages/compiler/src/shadow_css.ts @@ -1067,7 +1067,13 @@ const _cssContentUnscopedRuleRe = const _polyfillHost = '-shadowcsshost'; // note: :host-context pre-processed to -shadowcsshostcontext. const _polyfillHostContext = '-shadowcsscontext'; -const _parenSuffix = '(?:\\((' + '(?:\\([^)(]*\\)|[^)(]*)+?' + ')\\))'; +// Matches text content with no parentheses, e.g., "foo" +const _noParens = '[^)(]*'; +// Matches content with at most ONE level of nesting, e.g., "a(b)c" +const _level1Parens = String.raw`(?:\(${_noParens}\)|${_noParens})+?`; +// Matches content with at most TWO levels of nesting, e.g., "a(b(c)d)e" +const _level2Parens = String.raw`(?:\(${_level1Parens}\)|${_noParens})+?`; +const _parenSuffix = String.raw`(?:\((${_level2Parens})\))`; const _cssColonHostRe = new RegExp(_polyfillHost + _parenSuffix + '?([^,{]*)', 'gim'); // note: :host-context patterns are terminated with `{`, as opposed to :host which // is both `{` and `,` because :host-context handles top-level commas differently. diff --git a/packages/compiler/test/shadow_css/host_and_host_context_spec.ts b/packages/compiler/test/shadow_css/host_and_host_context_spec.ts index e934a9ccd2c..20999e3f4c8 100644 --- a/packages/compiler/test/shadow_css/host_and_host_context_spec.ts +++ b/packages/compiler/test/shadow_css/host_and_host_context_spec.ts @@ -83,6 +83,9 @@ describe('ShadowCss, :host and :host-context', () => { expect(shim(':host(:not(p)):before {}', 'contenta', 'a-host')).toEqualCss( '[a-host]:not(p):before {}', ); + expect(shim(':host(:not(:has(p))) {}', 'contenta', 'a-host')).toEqualCss( + '[a-host]:not(:has(p)) {}', + ); expect(shim(':host:not(:host.foo) {}', 'contenta', 'a-host')).toEqualCss( '[a-host]:not([a-host].foo) {}', );