mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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
This commit is contained in:
committed by
Andrew Kushnir
parent
64836215c6
commit
ecfb74d287
@@ -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.
|
||||
|
||||
@@ -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}(?![^(]*\\))`,
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
Reference in New Issue
Block a user