fix(compiler): transform chained pseudo-selectors (#58681)

fix transformation logic for `:where` and `:is` pseudo-selectors
when these selectors were used in a chain. results were often broken,
the last letter of the selector was incorrectly trimmed.
see tests for examples

Fixes #58226

PR Close #58681
This commit is contained in:
Georgy Serga 2024-11-07 23:47:20 +00:00 committed by Pawel Kozlowski
parent 2be161d015
commit 80f56954ce
2 changed files with 83 additions and 32 deletions

View file

@ -800,30 +800,67 @@ export class ShadowCss {
const _pseudoFunctionAwareScopeSelectorPart = (selectorPart: string) => {
let scopedPart = '';
const cssPrefixWithPseudoSelectorFunctionMatch = selectorPart.match(
_cssPrefixWithPseudoSelectorFunction,
);
if (cssPrefixWithPseudoSelectorFunctionMatch) {
const [cssPseudoSelectorFunction] = cssPrefixWithPseudoSelectorFunctionMatch;
// Collect all outer `:where()` and `:is()` selectors,
// counting parenthesis to keep nested selectors intact.
const pseudoSelectorParts = [];
let pseudoSelectorMatch: RegExpExecArray | null;
while (
(pseudoSelectorMatch = _cssPrefixWithPseudoSelectorFunction.exec(selectorPart)) !== null
) {
let openedBrackets = 1;
let index = _cssPrefixWithPseudoSelectorFunction.lastIndex;
// Unwrap the pseudo selector to scope its contents.
// For example,
// - `:where(selectorToScope)` -> `selectorToScope`;
// - `:is(.foo, .bar)` -> `.foo, .bar`.
const selectorToScope = selectorPart.slice(cssPseudoSelectorFunction.length, -1);
if (selectorToScope.includes(_polyfillHostNoCombinator)) {
this._shouldScopeIndicator = true;
while (index < selectorPart.length) {
const currentSymbol = selectorPart[index];
index++;
if (currentSymbol === '(') {
openedBrackets++;
continue;
}
if (currentSymbol === ')') {
openedBrackets--;
if (openedBrackets === 0) {
break;
}
continue;
}
}
const scopedInnerPart = this._scopeSelector({
selector: selectorToScope,
scopeSelector,
hostSelector,
});
pseudoSelectorParts.push(
`${pseudoSelectorMatch[0]}${selectorPart.slice(_cssPrefixWithPseudoSelectorFunction.lastIndex, index)}`,
);
_cssPrefixWithPseudoSelectorFunction.lastIndex = index;
}
// Put the result back into the pseudo selector function.
scopedPart = `${cssPseudoSelectorFunction}${scopedInnerPart})`;
// If selector consists of only `:where()` and `:is()` on the outer level
// scope those pseudo-selectors individually, otherwise scope the whole
// selector.
if (pseudoSelectorParts.join('') === selectorPart) {
scopedPart = pseudoSelectorParts
.map((selectorPart) => {
const [cssPseudoSelectorFunction] =
selectorPart.match(_cssPrefixWithPseudoSelectorFunction) ?? [];
// Unwrap the pseudo selector to scope its contents.
// For example,
// - `:where(selectorToScope)` -> `selectorToScope`;
// - `:is(.foo, .bar)` -> `.foo, .bar`.
const selectorToScope = selectorPart.slice(cssPseudoSelectorFunction?.length, -1);
if (selectorToScope.includes(_polyfillHostNoCombinator)) {
this._shouldScopeIndicator = true;
}
const scopedInnerPart = this._scopeSelector({
selector: selectorToScope,
scopeSelector,
hostSelector,
});
// Put the result back into the pseudo selector function.
return `${cssPseudoSelectorFunction}${scopedInnerPart})`;
})
.join('');
} else {
this._shouldScopeIndicator =
this._shouldScopeIndicator || selectorPart.includes(_polyfillHostNoCombinator);
@ -962,7 +999,7 @@ class SafeSelector {
}
const _cssScopedPseudoFunctionPrefix = '(:(where|is)\\()?';
const _cssPrefixWithPseudoSelectorFunction = /^:(where|is)\(/i;
const _cssPrefixWithPseudoSelectorFunction = /:(where|is)\(/gi;
const _cssContentNextSelectorRe =
/polyfill-next-selector[^}]*content:[\s]*?(['"])(.*?)\1[;\s]*}([^{]*?){/gim;
const _cssContentRuleRe = /(polyfill-rule)[^}]*(content:[\s]*(['"])(.*?)\3)[;\s]*[^}]*}/gim;

View file

@ -93,9 +93,8 @@ describe('ShadowCss', () => {
'div[contenta]:where(.one) {}',
);
expect(shim('div:where() {}', 'contenta', 'hosta')).toEqualCss('div[contenta]:where() {}');
// See `xit('should parse concatenated pseudo selectors'`
expect(shim(':where(a):where(b) {}', 'contenta', 'hosta')).toEqualCss(
':where(a)[contenta]:where(b) {}',
':where(a[contenta]):where(b[contenta]) {}',
);
expect(shim('*:where(.one) {}', 'contenta', 'hosta')).toEqualCss('*[contenta]:where(.one) {}');
expect(shim('*:where(.one) ::ng-deep .foo {}', 'contenta', 'hosta')).toEqualCss(
@ -103,15 +102,6 @@ describe('ShadowCss', () => {
);
});
xit('should parse concatenated pseudo selectors', () => {
// Current logic leads to a result with an outer scope
// It could be changed, to not increase specificity
// Requires a more complex parsing
expect(shim(':where(a):where(b) {}', 'contenta', 'hosta')).toEqualCss(
':where(a[contenta]):where(b[contenta]) {}',
);
});
it('should handle pseudo functions correctly', () => {
// :where()
expect(shim(':where(.one) {}', 'contenta', 'hosta')).toEqualCss(':where(.one[contenta]) {}');
@ -200,6 +190,18 @@ describe('ShadowCss', () => {
).toEqualCss(
':where(:where(a[contenta]:has(.foo), b[contenta]) :is(.one[contenta], .two[contenta]:where(.foo > .bar))) {}',
);
expect(shim(':where(.two):first-child {}', 'contenta', 'hosta')).toEqualCss(
'[contenta]:where(.two):first-child {}',
);
expect(shim(':first-child:where(.two) {}', 'contenta', 'hosta')).toEqualCss(
'[contenta]:first-child:where(.two) {}',
);
expect(shim(':where(.two):nth-child(3) {}', 'contenta', 'hosta')).toEqualCss(
'[contenta]:where(.two):nth-child(3) {}',
);
expect(shim('table :where(td, th):hover { color: lime; }', 'contenta', 'hosta')).toEqualCss(
'table[contenta] [contenta]:where(td, th):hover { color:lime;}',
);
// complex selectors
expect(shim(':host:is([foo],[foo-2])>div.example-2 {}', 'contenta', 'a-host')).toEqualCss(
@ -272,6 +274,18 @@ describe('ShadowCss', () => {
expect(shim('.one :where(:host, .two) {}', 'contenta', 'hosta')).toEqualCss(
'.one :where([hosta], .two[contenta]) {}',
);
expect(shim(':is(.foo):is(:host):is(.two) {}', 'contenta', 'hosta')).toEqualCss(
':is(.foo):is([hosta]):is(.two[contenta]) {}',
);
expect(shim(':where(.one, :host .two):first-letter {}', 'contenta', 'hosta')).toEqualCss(
'[contenta]:where(.one, [hosta] .two):first-letter {}',
);
expect(shim(':first-child:where(.one, :host .two) {}', 'contenta', 'hosta')).toEqualCss(
'[contenta]:first-child:where(.one, [hosta] .two) {}',
);
expect(
shim(':where(.one, :host .two):nth-child(3):is(.foo, a:where(.bar)) {}', 'contenta', 'hosta'),
).toEqualCss('[contenta]:where(.one, [hosta] .two):nth-child(3):is(.foo, a:where(.bar)) {}');
});
it('should handle escaped selector with space (if followed by a hex char)', () => {