Skip to content

Commit

Permalink
fix(compiler): handle :host-context with comma-separated child select…
Browse files Browse the repository at this point in the history
…or (#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
  • Loading branch information
crisbeto authored and AndrewKushnir committed Jan 21, 2025
1 parent 04c2447 commit 98f8207
Show file tree
Hide file tree
Showing 2 changed files with 59 additions and 4 deletions.
46 changes: 42 additions & 4 deletions packages/compiler/src/shadow_css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,41 @@ export class ShadowCss {
* .foo<scopeName> .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.

Expand Down Expand Up @@ -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}(?![^(]*\\))`,
Expand Down
17 changes: 17 additions & 0 deletions packages/compiler/test/shadow_css/host_and_host_context_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down

0 comments on commit 98f8207

Please sign in to comment.