From d6ee74a06d8bcb10ec15bc88bbecfa2cbdd4aff4 Mon Sep 17 00:00:00 2001 From: Georgy Serga Date: Thu, 7 Nov 2024 20:46:51 +0000 Subject: [PATCH 1/4] fix(compiler): fix `:host` parsing in pseudo-selectors fix several use-cases where `:host` was used in or around pseudo-selectors - `:host` followed by a comma inside pseudo-selectors - `:host` outside of pseudo-selectors when another `:host` is present within see tests for examples --- packages/compiler/src/shadow_css.ts | 13 +++++++------ .../shadow_css/host_and_host_context_spec.ts | 15 +++++++++++++++ .../test/shadow_css/shadow_css_spec.ts | 18 ++++++++++++++++++ 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/packages/compiler/src/shadow_css.ts b/packages/compiler/src/shadow_css.ts index 1d7988568fbf..fe1bb4acc618 100644 --- a/packages/compiler/src/shadow_css.ts +++ b/packages/compiler/src/shadow_css.ts @@ -743,7 +743,7 @@ export class ShadowCss { }, ); }) - .replace(_polyfillHostRe, replaceBy + ' '); + .replace(_polyfillHostRe, replaceBy); } return scopeSelector + ' ' + selector; @@ -765,7 +765,7 @@ export class ShadowCss { const isRe = /\[is=([^\]]*)\]/g; scopeSelector = scopeSelector.replace(isRe, (_: string, ...parts: string[]) => parts[0]); - const attrName = '[' + scopeSelector + ']'; + const attrName = `[${scopeSelector}]`; const _scopeSelectorPart = (p: string) => { let scopedP = p.trim(); @@ -776,7 +776,7 @@ export class ShadowCss { if (p.includes(_polyfillHostNoCombinator)) { scopedP = this._applySimpleSelectorScope(p, scopeSelector, hostSelector); - if (_polyfillHostNoCombinatorWithinPseudoFunction.test(p)) { + if (!p.match(_polyfillHostNoCombinatorOutsidePseudoFunction)) { const [_, before, colon, after] = scopedP.match(/([^:]*)(:*)(.*)/)!; scopedP = before + attrName + colon + after; } @@ -979,10 +979,11 @@ const _cssColonHostContextReGlobal = new RegExp( ); const _cssColonHostContextRe = new RegExp(_polyfillHostContext + _parenSuffix, 'im'); const _polyfillHostNoCombinator = _polyfillHost + '-no-combinator'; -const _polyfillHostNoCombinatorWithinPseudoFunction = new RegExp( - `:.*\\(.*${_polyfillHostNoCombinator}.*\\)`, +const _polyfillHostNoCombinatorOutsidePseudoFunction = new RegExp( + `${_polyfillHostNoCombinator}(?![^(]*\\))`, + 'g', ); -const _polyfillHostNoCombinatorRe = /-shadowcsshost-no-combinator([^\s]*)/; +const _polyfillHostNoCombinatorRe = /-shadowcsshost-no-combinator([^\s,]*)/; const _polyfillHostNoCombinatorReGlobal = new RegExp(_polyfillHostNoCombinatorRe, 'g'); const _shadowDOMSelectorsRe = [ /::shadow/g, 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 5b5689feb2be..b0cc38e42fee 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 @@ -83,6 +83,21 @@ describe('ShadowCss, :host and :host-context', () => { expect(shim(':host(:not(p)):before {}', 'contenta', 'a-host')).toEqualCss( '[a-host]:not(p):before {}', ); + expect(shim(':host:not(:host.foo) {}', 'contenta', 'a-host')).toEqualCss( + '[a-host]:not([a-host].foo) {}', + ); + expect(shim(':host:not(.foo:host) {}', 'contenta', 'a-host')).toEqualCss( + '[a-host]:not(.foo[a-host]) {}', + ); + expect(shim(':host:not(:host.foo, :host.bar) {}', 'contenta', 'a-host')).toEqualCss( + '[a-host]:not([a-host].foo, .bar[a-host]) {}', + ); + expect(shim(':host:not(:host.foo, .bar :host) {}', 'contenta', 'a-host')).toEqualCss( + '[a-host]:not([a-host].foo, .bar [a-host]) {}', + ); + expect(shim(':host:not(.foo, .bar) {}', 'contenta', 'a-host')).toEqualCss( + '[a-host]:not(.foo, .bar) {}', + ); }); // see b/63672152 diff --git a/packages/compiler/test/shadow_css/shadow_css_spec.ts b/packages/compiler/test/shadow_css/shadow_css_spec.ts index 77a0a361a319..8eb90a4c0efd 100644 --- a/packages/compiler/test/shadow_css/shadow_css_spec.ts +++ b/packages/compiler/test/shadow_css/shadow_css_spec.ts @@ -229,6 +229,24 @@ describe('ShadowCss', () => { expect(shim(':has(a) :has(b) {}', 'contenta', 'hosta')).toEqualCss( '[contenta]:has(a) [contenta]:has(b) {}', ); + expect(shim(':has(a, b) {}', 'contenta', 'hosta')).toEqualCss('[contenta]:has(a, b) {}'); + expect(shim(':has(a, b:where(.foo), :is(.bar)) {}', 'contenta', 'hosta')).toEqualCss( + '[contenta]:has(a, b:where(.foo), :is(.bar)) {}', + ); + expect( + shim(':has(a, b:where(.foo), :is(.bar):first-child):first-letter {}', 'contenta', 'hosta'), + ).toEqualCss('[contenta]:has(a, b:where(.foo), :is(.bar):first-child):first-letter {}'); + expect( + shim(':where(a, b:where(.foo), :has(.bar):first-child) {}', 'contenta', 'hosta'), + ).toEqualCss( + ':where(a[contenta], b[contenta]:where(.foo), [contenta]:has(.bar):first-child) {}', + ); + expect(shim(':has(.one :host, .two) {}', 'contenta', 'hosta')).toEqualCss( + '[contenta]:has(.one [hosta], .two) {}', + ); + expect(shim(':has(.one, :host) {}', 'contenta', 'hosta')).toEqualCss( + '[contenta]:has(.one, [hosta]) {}', + ); }); it('should handle :host inclusions inside pseudo-selectors selectors', () => { From 617eef0b54250e7d2db1596f40e272b5af7209ef Mon Sep 17 00:00:00 2001 From: Georgy Serga Date: Thu, 7 Nov 2024 23:47:20 +0000 Subject: [PATCH 2/4] fix(compiler): transform chained pseudo-selectors 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 --- packages/compiler/src/shadow_css.ts | 81 ++++++++++++++----- .../test/shadow_css/shadow_css_spec.ts | 36 ++++++--- 2 files changed, 84 insertions(+), 33 deletions(-) diff --git a/packages/compiler/src/shadow_css.ts b/packages/compiler/src/shadow_css.ts index fe1bb4acc618..24304a767d5e 100644 --- a/packages/compiler/src/shadow_css.ts +++ b/packages/compiler/src/shadow_css.ts @@ -800,30 +800,67 @@ export class ShadowCss { const _pseudoFunctionAwareScopeSelectorPart = (selectorPart: string) => { let scopedPart = ''; - const cssPrefixWithPseudoSelectorFunctionMatch = selectorPart.match( - _cssPrefixWithPseudoSelectorFunction, - ); - if (cssPrefixWithPseudoSelectorFunctionMatch) { - const [cssPseudoSelectorFunction] = cssPrefixWithPseudoSelectorFunctionMatch; - - // 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; + // 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; + + 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; + } + + // 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. - scopedPart = `${cssPseudoSelectorFunction}${scopedInnerPart})`; + // 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; diff --git a/packages/compiler/test/shadow_css/shadow_css_spec.ts b/packages/compiler/test/shadow_css/shadow_css_spec.ts index 8eb90a4c0efd..7e869cccc67a 100644 --- a/packages/compiler/test/shadow_css/shadow_css_spec.ts +++ b/packages/compiler/test/shadow_css/shadow_css_spec.ts @@ -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)', () => { From 78b6a8209857058ffe29b48f5a7815ab31e409cf Mon Sep 17 00:00:00 2001 From: Georgy Serga Date: Fri, 8 Nov 2024 19:51:31 +0000 Subject: [PATCH 3/4] fix(compiler): resolve `:host:host-context(.foo)` fix results which had to parse several `-shadowcsshost-no-combinator` occurrences in a single selector --- packages/compiler/src/shadow_css.ts | 11 ++++++----- .../test/shadow_css/host_and_host_context_spec.ts | 12 ++++++++++++ 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/packages/compiler/src/shadow_css.ts b/packages/compiler/src/shadow_css.ts index 24304a767d5e..fb8ded564cbe 100644 --- a/packages/compiler/src/shadow_css.ts +++ b/packages/compiler/src/shadow_css.ts @@ -734,16 +734,18 @@ export class ShadowCss { _polyfillHostRe.lastIndex = 0; if (_polyfillHostRe.test(selector)) { const replaceBy = `[${hostSelector}]`; - return selector - .replace(_polyfillHostNoCombinatorReGlobal, (_hnc, selector) => { + let result = selector; + while (result.match(_polyfillHostNoCombinatorRe)) { + result = result.replace(_polyfillHostNoCombinatorRe, (_hnc, selector) => { return selector.replace( /([^:\)]*)(:*)(.*)/, (_: string, before: string, colon: string, after: string) => { return before + replaceBy + colon + after; }, ); - }) - .replace(_polyfillHostRe, replaceBy); + }); + } + return result.replace(_polyfillHostRe, replaceBy); } return scopeSelector + ' ' + selector; @@ -1021,7 +1023,6 @@ const _polyfillHostNoCombinatorOutsidePseudoFunction = new RegExp( 'g', ); const _polyfillHostNoCombinatorRe = /-shadowcsshost-no-combinator([^\s,]*)/; -const _polyfillHostNoCombinatorReGlobal = new RegExp(_polyfillHostNoCombinatorRe, 'g'); const _shadowDOMSelectorsRe = [ /::shadow/g, /::content/g, 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 b0cc38e42fee..a47f882b198f 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 @@ -255,6 +255,18 @@ describe('ShadowCss, :host and :host-context', () => { ); }); + it('should handle no selector :host', () => { + // The second selector below should have a `[a-host]` attribute selector + // attached to `.one`, current `:host-context` unwrapping logic doesn't + // work correctly on prefixed selectors, see #58345. + expect(shim(':host:host-context(.one) {}', 'contenta', 'a-host')).toEqualCss( + '.one[a-host][a-host], .one [a-host] {}', + ); + expect(shim(':host-context(.one) :host {}', 'contenta', 'a-host')).toEqualCss( + '.one [a-host] {}', + ); + }); + it('should handle selectors on different elements', () => { expect(shim(':host-context(div) :host(.x) > .y {}', 'contenta', 'a-host')).toEqualCss( 'div .x[a-host] > .y[contenta] {}', From c292cb4c5225323d1c3a3b2986a445780ef669c6 Mon Sep 17 00:00:00 2001 From: Georgy Serga Date: Fri, 8 Nov 2024 21:24:59 +0000 Subject: [PATCH 4/4] fix(compiler): fix multiline selectors multiline selectors where not correctly recognized by the regexp, fix it to fetch newlines as well Fixes #58399 --- packages/compiler/src/shadow_css.ts | 4 ++-- .../test/shadow_css/host_and_host_context_spec.ts | 11 +++++++++++ packages/compiler/test/shadow_css/shadow_css_spec.ts | 11 +++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/packages/compiler/src/shadow_css.ts b/packages/compiler/src/shadow_css.ts index fb8ded564cbe..0a40a35f67f0 100644 --- a/packages/compiler/src/shadow_css.ts +++ b/packages/compiler/src/shadow_css.ts @@ -779,14 +779,14 @@ export class ShadowCss { if (p.includes(_polyfillHostNoCombinator)) { scopedP = this._applySimpleSelectorScope(p, scopeSelector, hostSelector); if (!p.match(_polyfillHostNoCombinatorOutsidePseudoFunction)) { - const [_, before, colon, after] = scopedP.match(/([^:]*)(:*)(.*)/)!; + const [_, before, colon, after] = scopedP.match(/([^:]*)(:*)([\s\S]*)/)!; scopedP = before + attrName + colon + after; } } else { // remove :host since it should be unnecessary const t = p.replace(_polyfillHostRe, ''); if (t.length > 0) { - const matches = t.match(/([^:]*)(:*)(.*)/); + const matches = t.match(/([^:]*)(:*)([\s\S]*)/); if (matches) { scopedP = matches[1] + attrName + matches[2] + matches[3]; } 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 a47f882b198f..6bef826d196b 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 @@ -119,6 +119,17 @@ describe('ShadowCss, :host and :host-context', () => { 'cmp [a-host] child {}', ); }); + + it('should support newlines in the same selector and content ', () => { + const selector = `.foo:not( + :host) { + background-color: + green; + }`; + expect(shim(selector, 'contenta', 'a-host')).toEqualCss( + '.foo[contenta]:not( [a-host]) { background-color:green;}', + ); + }); }); describe(':host-context', () => { diff --git a/packages/compiler/test/shadow_css/shadow_css_spec.ts b/packages/compiler/test/shadow_css/shadow_css_spec.ts index 7e869cccc67a..8705f65e5e6a 100644 --- a/packages/compiler/test/shadow_css/shadow_css_spec.ts +++ b/packages/compiler/test/shadow_css/shadow_css_spec.ts @@ -47,6 +47,17 @@ describe('ShadowCss', () => { expect(shim(css, 'contenta')).toEqualCss(expected); }); + it('should support newlines in the same selector and content ', () => { + const selector = `.foo:not( + .bar) { + background-color: + green; + }`; + expect(shim(selector, 'contenta', 'a-host')).toEqualCss( + '.foo[contenta]:not( .bar) { background-color:green;}', + ); + }); + it('should handle complicated selectors', () => { expect(shim('one::before {}', 'contenta')).toEqualCss('one[contenta]::before {}'); expect(shim('one two {}', 'contenta')).toEqualCss('one[contenta] two[contenta] {}');