@@ -338,7 +338,7 @@ export class ShadowCss {
338338 * captures how many (if any) leading whitespaces are present or a comma
339339 * - (?:(?:(['"])((?:\\\\|\\\2|(?!\2).)+)\2)|(-?[A-Za-z][\w\-]*))
340340 * captures two different possible keyframes, ones which are quoted or ones which are valid css
341- * idents (custom properties excluded)
341+ * indents (custom properties excluded)
342342 * - (?=[,\s;]|$)
343343 * simply matches the end of the possible keyframe, valid endings are: a comma, a space, a
344344 * semicolon or the end of the string
@@ -459,7 +459,7 @@ export class ShadowCss {
459459 */
460460 private _scopeCssText ( cssText : string , scopeSelector : string , hostSelector : string ) : string {
461461 const unscopedRules = this . _extractUnscopedRulesFromCssText ( cssText ) ;
462- // replace :host and :host-context -shadowcsshost and -shadowcsshost respectively
462+ // replace :host and :host-context with -shadowcsshost and -shadowcsshostcontext respectively
463463 cssText = this . _insertPolyfillHostInCssText ( cssText ) ;
464464 cssText = this . _convertColonHost ( cssText ) ;
465465 cssText = this . _convertColonHostContext ( cssText ) ;
@@ -616,7 +616,12 @@ export class ShadowCss {
616616 let selector = rule . selector ;
617617 let content = rule . content ;
618618 if ( rule . selector [ 0 ] !== '@' ) {
619- selector = this . _scopeSelector ( { selector, scopeSelector, hostSelector} ) ;
619+ selector = this . _scopeSelector ( {
620+ selector,
621+ scopeSelector,
622+ hostSelector,
623+ isParentSelector : true ,
624+ } ) ;
620625 } else if ( scopedAtRuleIdentifiers . some ( ( atRule ) => rule . selector . startsWith ( atRule ) ) ) {
621626 content = this . _scopeSelectors ( rule . content , scopeSelector , hostSelector ) ;
622627 } else if ( rule . selector . startsWith ( '@font-face' ) || rule . selector . startsWith ( '@page' ) ) {
@@ -656,20 +661,30 @@ export class ShadowCss {
656661 } ) ;
657662 }
658663
664+ private _safeSelector : SafeSelector | undefined ;
665+ private _shouldScopeIndicator : boolean | undefined ;
666+
667+ // `isParentSelector` is used to distinguish the selectors which are coming from
668+ // the initial selector string and any nested selectors, parsed recursively,
669+ // for example `selector = 'a:where(.one)'` could be the parent, while recursive call
670+ // would have `selector = '.one'`.
659671 private _scopeSelector ( {
660672 selector,
661673 scopeSelector,
662674 hostSelector,
663- shouldScope ,
675+ isParentSelector = false ,
664676 } : {
665677 selector : string ;
666678 scopeSelector : string ;
667679 hostSelector : string ;
668- shouldScope ?: boolean ;
680+ isParentSelector ?: boolean ;
669681 } ) : string {
670682 // Split the selector into independent parts by `,` (comma) unless
671683 // comma is within parenthesis, for example `:is(.one, two)`.
672- const selectorSplitRe = / ? , (? ! [ ^ \( ] * \) ) ? / ;
684+ // Negative lookup after comma allows not splitting inside nested parenthesis,
685+ // up to three levels (((,))).
686+ const selectorSplitRe =
687+ / ? , (? ! (?: [ ^ ) ( ] * (?: \( [ ^ ) ( ] * (?: \( [ ^ ) ( ] * (?: \( [ ^ ) ( ] * \) [ ^ ) ( ] * ) * \) [ ^ ) ( ] * ) * \) [ ^ ) ( ] * ) * \) ) ) ? / ;
673688
674689 return selector
675690 . split ( selectorSplitRe )
@@ -682,7 +697,7 @@ export class ShadowCss {
682697 selector : shallowPart ,
683698 scopeSelector,
684699 hostSelector,
685- shouldScope ,
700+ isParentSelector ,
686701 } ) ;
687702 } else {
688703 return shallowPart ;
@@ -716,9 +731,9 @@ export class ShadowCss {
716731 if ( _polyfillHostRe . test ( selector ) ) {
717732 const replaceBy = `[${ hostSelector } ]` ;
718733 return selector
719- . replace ( _polyfillHostNoCombinatorRe , ( hnc , selector ) => {
734+ . replace ( _polyfillHostNoCombinatorReGlobal , ( _hnc , selector ) => {
720735 return selector . replace (
721- / ( [ ^ : ] * ) ( : * ) ( .* ) / ,
736+ / ( [ ^ : \) ] * ) ( : * ) ( .* ) / ,
722737 ( _ : string , before : string , colon : string , after : string ) => {
723738 return before + replaceBy + colon + after ;
724739 } ,
@@ -736,12 +751,12 @@ export class ShadowCss {
736751 selector,
737752 scopeSelector,
738753 hostSelector,
739- shouldScope ,
754+ isParentSelector ,
740755 } : {
741756 selector : string ;
742757 scopeSelector : string ;
743758 hostSelector : string ;
744- shouldScope ?: boolean ;
759+ isParentSelector ?: boolean ;
745760 } ) : string {
746761 const isRe = / \[ i s = ( [ ^ \] ] * ) \] / g;
747762 scopeSelector = scopeSelector . replace ( isRe , ( _ : string , ...parts : string [ ] ) => parts [ 0 ] ) ;
@@ -757,6 +772,10 @@ export class ShadowCss {
757772
758773 if ( p . includes ( _polyfillHostNoCombinator ) ) {
759774 scopedP = this . _applySimpleSelectorScope ( p , scopeSelector , hostSelector ) ;
775+ if ( _polyfillHostNoCombinatorWithinPseudoFunction . test ( p ) ) {
776+ const [ _ , before , colon , after ] = scopedP . match ( / ( [ ^ : ] * ) ( : * ) ( .* ) / ) ! ;
777+ scopedP = before + attrName + colon + after ;
778+ }
760779 } else {
761780 // remove :host since it should be unnecessary
762781 const t = p . replace ( _polyfillHostRe , '' ) ;
@@ -773,44 +792,66 @@ export class ShadowCss {
773792
774793 // Wraps `_scopeSelectorPart()` to not use it directly on selectors with
775794 // pseudo selector functions like `:where()`. Selectors within pseudo selector
776- // functions are recursively sent to `_scopeSelector()` with the `shouldScope`
777- // argument, so the selectors get scoped correctly.
795+ // functions are recursively sent to `_scopeSelector()`.
778796 const _pseudoFunctionAwareScopeSelectorPart = ( selectorPart : string ) => {
779797 let scopedPart = '' ;
780798
781- const cssPseudoSelectorFunctionMatch = selectorPart . match ( _cssPseudoSelectorFunctionPrefix ) ;
782- if ( cssPseudoSelectorFunctionMatch ) {
783- const [ cssPseudoSelectorFunction ] = cssPseudoSelectorFunctionMatch ;
799+ const cssPrefixWithPseudoSelectorFunctionMatch = selectorPart . match (
800+ _cssPrefixWithPseudoSelectorFunction ,
801+ ) ;
802+ if ( cssPrefixWithPseudoSelectorFunctionMatch ) {
803+ const [ cssPseudoSelectorFunction , mainSelector , pseudoSelector ] =
804+ cssPrefixWithPseudoSelectorFunctionMatch ;
805+ const hasOuterHostNoCombinator = mainSelector . includes ( _polyfillHostNoCombinator ) ;
806+ const scopedMainSelector = mainSelector . replace (
807+ _polyfillHostNoCombinatorReGlobal ,
808+ `[${ hostSelector } ]` ,
809+ ) ;
810+
784811 // Unwrap the pseudo selector, to scope its contents.
785- // For example, `:where(selectorToScope)` -> `selectorToScope`.
812+ // For example,
813+ // - `:where(selectorToScope)` -> `selectorToScope`;
814+ // - `div:is(.foo, .bar)` -> `.foo, .bar`.
786815 const selectorToScope = selectorPart . slice ( cssPseudoSelectorFunction . length , - 1 ) ;
787816
817+ if ( selectorToScope . includes ( _polyfillHostNoCombinator ) ) {
818+ this . _shouldScopeIndicator = true ;
819+ }
820+
788821 const scopedInnerPart = this . _scopeSelector ( {
789822 selector : selectorToScope ,
790823 scopeSelector,
791824 hostSelector,
792- shouldScope : shouldScopeIndicator ,
793825 } ) ;
826+
794827 // Put the result back into the pseudo selector function.
795- scopedPart = `${ cssPseudoSelectorFunction } ${ scopedInnerPart } )` ;
828+ scopedPart = `${ scopedMainSelector } :${ pseudoSelector } (${ scopedInnerPart } )` ;
829+
830+ this . _shouldScopeIndicator = this . _shouldScopeIndicator || hasOuterHostNoCombinator ;
796831 } else {
797- shouldScopeIndicator =
798- shouldScopeIndicator || selectorPart . includes ( _polyfillHostNoCombinator ) ;
799- scopedPart = shouldScopeIndicator ? _scopeSelectorPart ( selectorPart ) : selectorPart ;
832+ this . _shouldScopeIndicator =
833+ this . _shouldScopeIndicator || selectorPart . includes ( _polyfillHostNoCombinator ) ;
834+ scopedPart = this . _shouldScopeIndicator ? _scopeSelectorPart ( selectorPart ) : selectorPart ;
800835 }
801836
802837 return scopedPart ;
803838 } ;
804839
805- const safeContent = new SafeSelector ( selector ) ;
806- selector = safeContent . content ( ) ;
840+ if ( isParentSelector ) {
841+ this . _safeSelector = new SafeSelector ( selector ) ;
842+ selector = this . _safeSelector . content ( ) ;
843+ }
807844
808845 let scopedSelector = '' ;
809846 let startIndex = 0 ;
810847 let res : RegExpExecArray | null ;
811848 // Combinators aren't used as a delimiter if they are within parenthesis,
812849 // for example `:where(.one .two)` stays intact.
813- const sep = / ( | > | \+ | ~ (? ! = ) ) (? ! [ ^ \( ] * \) ) \s * / g;
850+ // Similarly to selector separation by comma initially, negative lookahead
851+ // is used here to not break selectors within nested parenthesis up to three
852+ // nested layers.
853+ const sep =
854+ / ( | > | \+ | ~ (? ! = ) ) (? ! ( [ ^ ) ( ] * (?: \( [ ^ ) ( ] * (?: \( [ ^ ) ( ] * (?: \( [ ^ ) ( ] * \) [ ^ ) ( ] * ) * \) [ ^ ) ( ] * ) * \) [ ^ ) ( ] * ) * \) ) ) \s * / g;
814855
815856 // If a selector appears before :host it should not be shimmed as it
816857 // matches on ancestor elements and not on elements in the host's shadow
@@ -824,8 +865,13 @@ export class ShadowCss {
824865 // - `tag :host` -> `tag [h]` (`tag` is not scoped because it's considered part of a
825866 // `:host-context(tag)`)
826867 const hasHost = selector . includes ( _polyfillHostNoCombinator ) ;
827- // Only scope parts after the first `-shadowcsshost-no-combinator` when it is present
828- let shouldScopeIndicator = shouldScope ?? ! hasHost ;
868+ // Only scope parts after or on the same level as the first `-shadowcsshost-no-combinator`
869+ // when it is present. The selector has the same level when it is a part of a pseudo
870+ // selector, like `:where()`, for example `:where(:host, .foo)` would result in `.foo`
871+ // being scoped.
872+ if ( isParentSelector || this . _shouldScopeIndicator ) {
873+ this . _shouldScopeIndicator = ! hasHost ;
874+ }
829875
830876 while ( ( res = sep . exec ( selector ) ) !== null ) {
831877 const separator = res [ 1 ] ;
@@ -853,7 +899,8 @@ export class ShadowCss {
853899 scopedSelector += _pseudoFunctionAwareScopeSelectorPart ( part ) ;
854900
855901 // replace the placeholders with their original values
856- return safeContent . restore ( scopedSelector ) ;
902+ // using values stored inside the `safeSelector` instance.
903+ return this . _safeSelector ! . restore ( scopedSelector ) ;
857904 }
858905
859906 private _insertPolyfillHostInCssText ( selector : string ) : string {
@@ -918,7 +965,7 @@ class SafeSelector {
918965 }
919966}
920967
921- const _cssPseudoSelectorFunctionPrefix = / ^ : ( w h e r e | i s ) \( / gi ;
968+ const _cssPrefixWithPseudoSelectorFunction = / ^ ( [ ^ : ] * ) : ( w h e r e | i s ) \( / i ;
922969const _cssContentNextSelectorRe =
923970 / p o l y f i l l - n e x t - s e l e c t o r [ ^ } ] * c o n t e n t : [ \s ] * ?( [ ' " ] ) ( .* ?) \1[ ; \s ] * } ( [ ^ { ] * ?) { / gim;
924971const _cssContentRuleRe = / ( p o l y f i l l - r u l e ) [ ^ } ] * ( c o n t e n t : [ \s ] * ( [ ' " ] ) ( .* ?) \3) [ ; \s ] * [ ^ } ] * } / gim;
@@ -932,7 +979,11 @@ const _cssColonHostRe = new RegExp(_polyfillHost + _parenSuffix, 'gim');
932979const _cssColonHostContextReGlobal = new RegExp ( _polyfillHostContext + _parenSuffix , 'gim' ) ;
933980const _cssColonHostContextRe = new RegExp ( _polyfillHostContext + _parenSuffix , 'im' ) ;
934981const _polyfillHostNoCombinator = _polyfillHost + '-no-combinator' ;
982+ const _polyfillHostNoCombinatorWithinPseudoFunction = new RegExp (
983+ `:.*(.*${ _polyfillHostNoCombinator } .*)` ,
984+ ) ;
935985const _polyfillHostNoCombinatorRe = / - s h a d o w c s s h o s t - n o - c o m b i n a t o r ( [ ^ \s ] * ) / ;
986+ const _polyfillHostNoCombinatorReGlobal = new RegExp ( _polyfillHostNoCombinatorRe , 'g' ) ;
936987const _shadowDOMSelectorsRe = [
937988 / : : s h a d o w / g,
938989 / : : c o n t e n t / g,
0 commit comments