From ecfb74d287bec7bec37d0b476b321b047bef2c43 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Sat, 21 Dec 2024 16:09:39 +0200 Subject: [PATCH] fix(compiler): handle :host-context with comma-separated child selector (#59276) Both `:host` and `:host-context` work by looking for a specific character sequence that is terminated by `,` or `{` and replacing selectors inside of it with scoped versions. This is implemented as a regex which isn't aware of things like nested selectors. Normally this is fine for `:host`, because each `:host` produces one scoped selector which doesn't affect any child selectors, however it breaks down with `:host-context` which replaces each instance with two selectors. For example, if we have a selector in the form of `:host-context(.foo) a:not(.a, .b)`, the compiler ends up determining that `.a,` is the end selector and produces `.foo[a-host] a[contenta]:not(.a, .foo [a-host] a[contenta]:not(.a, .b) {}`. These changes resolve the issue by splitting the CSS alogn top-level commas, processing the `:host-context` in them individually, and stiching the CSS back together. PR Close #59276 --- packages/compiler/src/shadow_css.ts | 46 +++++++++++++++++-- .../shadow_css/host_and_host_context_spec.ts | 17 +++++++ 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/packages/compiler/src/shadow_css.ts b/packages/compiler/src/shadow_css.ts index f8548db07f1..a66aa6c551a 100644 --- a/packages/compiler/src/shadow_css.ts +++ b/packages/compiler/src/shadow_css.ts @@ -541,6 +541,41 @@ export class ShadowCss { * .foo .bar { ... } */ private _convertColonHostContext(cssText: string): string { + const length = cssText.length; + let parens = 0; + let prev = 0; + let result = ''; + + // Splits up the selectors on their top-level commas, processes the :host-context in them + // individually and stitches them back together. This ensures that individual selectors don't + // affect each other. + for (let i = 0; i < length; i++) { + const char = cssText[i]; + + // If we hit a comma and there are no open parentheses, take the current chunk and process it. + if (char === ',' && parens === 0) { + result += this._convertColonHostContextInSelectorPart(cssText.slice(prev, i)) + ','; + prev = i + 1; + continue; + } + + // We've hit the end. Take everything since the last comma. + if (i === length - 1) { + result += this._convertColonHostContextInSelectorPart(cssText.slice(prev)); + break; + } + + if (char === '(') { + parens++; + } else if (char === ')') { + parens--; + } + } + + return result; + } + + private _convertColonHostContextInSelectorPart(cssText: string): string { return cssText.replace(_cssColonHostContextReGlobal, (selectorText, pseudoPrefix) => { // We have captured a selector that contains a `:host-context` rule. @@ -1010,13 +1045,16 @@ const _cssContentUnscopedRuleRe = const _polyfillHost = '-shadowcsshost'; // note: :host-context pre-processed to -shadowcsshostcontext. const _polyfillHostContext = '-shadowcsscontext'; -const _parenSuffix = '(?:\\((' + '(?:\\([^)(]*\\)|[^)(]*)+?' + ')\\))?([^,{]*)'; -const _cssColonHostRe = new RegExp(_polyfillHost + _parenSuffix, 'gim'); +const _parenSuffix = '(?:\\((' + '(?:\\([^)(]*\\)|[^)(]*)+?' + ')\\))'; +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. +const _hostContextPattern = _polyfillHostContext + _parenSuffix + '?([^{]*)'; const _cssColonHostContextReGlobal = new RegExp( - _cssScopedPseudoFunctionPrefix + '(' + _polyfillHostContext + _parenSuffix + ')', + `${_cssScopedPseudoFunctionPrefix}(${_hostContextPattern})`, 'gim', ); -const _cssColonHostContextRe = new RegExp(_polyfillHostContext + _parenSuffix, 'im'); +const _cssColonHostContextRe = new RegExp(_hostContextPattern, 'im'); const _polyfillHostNoCombinator = _polyfillHost + '-no-combinator'; const _polyfillHostNoCombinatorOutsidePseudoFunction = new RegExp( `${_polyfillHostNoCombinator}(?![^(]*\\))`, 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 6bef826d196..00ebbe9ad50 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 @@ -257,6 +257,23 @@ describe('ShadowCss, :host and :host-context', () => { '{}', ); }); + + it('should handle :host-context with comma-separated child selector', () => { + expect(shim(':host-context(.foo) a:not(.a, .b) {}', 'contenta', 'a-host')).toEqualCss( + '.foo[a-host] a[contenta]:not(.a, .b), .foo [a-host] a[contenta]:not(.a, .b) {}', + ); + expect( + shim( + ':host-context(.foo) a:not([a], .b), .bar, :host-context(.baz) a:not([c], .d) {}', + 'contenta', + 'a-host', + ), + ).toEqualCss( + '.foo[a-host] a[contenta]:not([a], .b), .foo [a-host] a[contenta]:not([a], .b), ' + + '.bar[contenta], .baz[a-host] a[contenta]:not([c], .d), ' + + '.baz [a-host] a[contenta]:not([c], .d) {}', + ); + }); }); describe(':host-context and :host combination selector', () => {