Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 80f5695

Browse files
GeorgySergapkozlowski-opensource
authored andcommitted
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
1 parent 2be161d commit 80f5695

File tree

2 files changed

+84
-33
lines changed

2 files changed

+84
-33
lines changed

‎packages/compiler/src/shadow_css.ts

Lines changed: 59 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -800,30 +800,67 @@ export class ShadowCss {
800800
const _pseudoFunctionAwareScopeSelectorPart = (selectorPart: string) => {
801801
let scopedPart = '';
802802

803-
const cssPrefixWithPseudoSelectorFunctionMatch = selectorPart.match(
804-
_cssPrefixWithPseudoSelectorFunction,
805-
);
806-
if (cssPrefixWithPseudoSelectorFunctionMatch) {
807-
const [cssPseudoSelectorFunction] = cssPrefixWithPseudoSelectorFunctionMatch;
808-
809-
// Unwrap the pseudo selector to scope its contents.
810-
// For example,
811-
// - `:where(selectorToScope)` -> `selectorToScope`;
812-
// - `:is(.foo, .bar)` -> `.foo, .bar`.
813-
const selectorToScope = selectorPart.slice(cssPseudoSelectorFunction.length, -1);
814-
815-
if (selectorToScope.includes(_polyfillHostNoCombinator)) {
816-
this._shouldScopeIndicator = true;
803+
// Collect all outer `:where()` and `:is()` selectors,
804+
// counting parenthesis to keep nested selectors intact.
805+
const pseudoSelectorParts = [];
806+
let pseudoSelectorMatch: RegExpExecArray | null;
807+
while (
808+
(pseudoSelectorMatch = _cssPrefixWithPseudoSelectorFunction.exec(selectorPart)) !== null
809+
) {
810+
let openedBrackets = 1;
811+
let index = _cssPrefixWithPseudoSelectorFunction.lastIndex;
812+
813+
while (index < selectorPart.length) {
814+
const currentSymbol = selectorPart[index];
815+
index++;
816+
if (currentSymbol === '(') {
817+
openedBrackets++;
818+
continue;
819+
}
820+
if (currentSymbol === ')') {
821+
openedBrackets--;
822+
if (openedBrackets === 0) {
823+
break;
824+
}
825+
continue;
826+
}
817827
}
818828

819-
const scopedInnerPart = this._scopeSelector({
820-
selector: selectorToScope,
821-
scopeSelector,
822-
hostSelector,
823-
});
829+
pseudoSelectorParts.push(
830+
`${pseudoSelectorMatch[0]}${selectorPart.slice(_cssPrefixWithPseudoSelectorFunction.lastIndex, index)}`,
831+
);
832+
_cssPrefixWithPseudoSelectorFunction.lastIndex = index;
833+
}
834+
835+
// If selector consists of only `:where()` and `:is()` on the outer level
836+
// scope those pseudo-selectors individually, otherwise scope the whole
837+
// selector.
838+
if (pseudoSelectorParts.join('') === selectorPart) {
839+
scopedPart = pseudoSelectorParts
840+
.map((selectorPart) => {
841+
const [cssPseudoSelectorFunction] =
842+
selectorPart.match(_cssPrefixWithPseudoSelectorFunction) ?? [];
843+
844+
// Unwrap the pseudo selector to scope its contents.
845+
// For example,
846+
// - `:where(selectorToScope)` -> `selectorToScope`;
847+
// - `:is(.foo, .bar)` -> `.foo, .bar`.
848+
const selectorToScope = selectorPart.slice(cssPseudoSelectorFunction?.length, -1);
849+
850+
if (selectorToScope.includes(_polyfillHostNoCombinator)) {
851+
this._shouldScopeIndicator = true;
852+
}
853+
854+
const scopedInnerPart = this._scopeSelector({
855+
selector: selectorToScope,
856+
scopeSelector,
857+
hostSelector,
858+
});
824859

825-
// Put the result back into the pseudo selector function.
826-
scopedPart = `${cssPseudoSelectorFunction}${scopedInnerPart})`;
860+
// Put the result back into the pseudo selector function.
861+
return `${cssPseudoSelectorFunction}${scopedInnerPart})`;
862+
})
863+
.join('');
827864
} else {
828865
this._shouldScopeIndicator =
829866
this._shouldScopeIndicator || selectorPart.includes(_polyfillHostNoCombinator);
@@ -962,7 +999,7 @@ class SafeSelector {
962999
}
9631000

9641001
const _cssScopedPseudoFunctionPrefix = '(:(where|is)\\()?';
965-
const _cssPrefixWithPseudoSelectorFunction = /^:(where|is)\(/i;
1002+
const _cssPrefixWithPseudoSelectorFunction = /:(where|is)\(/gi;
9661003
const _cssContentNextSelectorRe =
9671004
/polyfill-next-selector[^}]*content:[\s]*?(['"])(.*?)\1[;\s]*}([^{]*?){/gim;
9681005
const _cssContentRuleRe = /(polyfill-rule)[^}]*(content:[\s]*(['"])(.*?)\3)[;\s]*[^}]*}/gim;

‎packages/compiler/test/shadow_css/shadow_css_spec.ts

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -93,25 +93,15 @@ describe('ShadowCss', () => {
9393
'div[contenta]:where(.one) {}',
9494
);
9595
expect(shim('div:where() {}', 'contenta', 'hosta')).toEqualCss('div[contenta]:where() {}');
96-
// See `xit('should parse concatenated pseudo selectors'`
9796
expect(shim(':where(a):where(b) {}', 'contenta', 'hosta')).toEqualCss(
98-
':where(a)[contenta]:where(b) {}',
97+
':where(a[contenta]):where(b[contenta]) {}',
9998
);
10099
expect(shim('*:where(.one) {}', 'contenta', 'hosta')).toEqualCss('*[contenta]:where(.one) {}');
101100
expect(shim('*:where(.one) ::ng-deep .foo {}', 'contenta', 'hosta')).toEqualCss(
102101
'*[contenta]:where(.one) .foo {}',
103102
);
104103
});
105104

106-
xit('should parse concatenated pseudo selectors', () => {
107-
// Current logic leads to a result with an outer scope
108-
// It could be changed, to not increase specificity
109-
// Requires a more complex parsing
110-
expect(shim(':where(a):where(b) {}', 'contenta', 'hosta')).toEqualCss(
111-
':where(a[contenta]):where(b[contenta]) {}',
112-
);
113-
});
114-
115105
it('should handle pseudo functions correctly', () => {
116106
// :where()
117107
expect(shim(':where(.one) {}', 'contenta', 'hosta')).toEqualCss(':where(.one[contenta]) {}');
@@ -200,6 +190,18 @@ describe('ShadowCss', () => {
200190
).toEqualCss(
201191
':where(:where(a[contenta]:has(.foo), b[contenta]) :is(.one[contenta], .two[contenta]:where(.foo > .bar))) {}',
202192
);
193+
expect(shim(':where(.two):first-child {}', 'contenta', 'hosta')).toEqualCss(
194+
'[contenta]:where(.two):first-child {}',
195+
);
196+
expect(shim(':first-child:where(.two) {}', 'contenta', 'hosta')).toEqualCss(
197+
'[contenta]:first-child:where(.two) {}',
198+
);
199+
expect(shim(':where(.two):nth-child(3) {}', 'contenta', 'hosta')).toEqualCss(
200+
'[contenta]:where(.two):nth-child(3) {}',
201+
);
202+
expect(shim('table :where(td, th):hover { color: lime; }', 'contenta', 'hosta')).toEqualCss(
203+
'table[contenta] [contenta]:where(td, th):hover { color:lime;}',
204+
);
203205

204206
// complex selectors
205207
expect(shim(':host:is([foo],[foo-2])>div.example-2 {}', 'contenta', 'a-host')).toEqualCss(
@@ -272,6 +274,18 @@ describe('ShadowCss', () => {
272274
expect(shim('.one :where(:host, .two) {}', 'contenta', 'hosta')).toEqualCss(
273275
'.one :where([hosta], .two[contenta]) {}',
274276
);
277+
expect(shim(':is(.foo):is(:host):is(.two) {}', 'contenta', 'hosta')).toEqualCss(
278+
':is(.foo):is([hosta]):is(.two[contenta]) {}',
279+
);
280+
expect(shim(':where(.one, :host .two):first-letter {}', 'contenta', 'hosta')).toEqualCss(
281+
'[contenta]:where(.one, [hosta] .two):first-letter {}',
282+
);
283+
expect(shim(':first-child:where(.one, :host .two) {}', 'contenta', 'hosta')).toEqualCss(
284+
'[contenta]:first-child:where(.one, [hosta] .two) {}',
285+
);
286+
expect(
287+
shim(':where(.one, :host .two):nth-child(3):is(.foo, a:where(.bar)) {}', 'contenta', 'hosta'),
288+
).toEqualCss('[contenta]:where(.one, [hosta] .two):nth-child(3):is(.foo, a:where(.bar)) {}');
275289
});
276290

277291
it('should handle escaped selector with space (if followed by a hex char)', () => {

0 commit comments

Comments
 (0)