@@ -340,7 +340,7 @@ export class ShadowCss {
340340 * captures how many (if any) leading whitespaces are present or a comma
341341 * - (?:(?:(['"])((?:\\\\|\\\2|(?!\2).)+)\2)|(-?[A-Za-z][\w\-]*))
342342 * captures two different possible keyframes, ones which are quoted or ones which are valid css
343- * idents (custom properties excluded)
343+ * indents (custom properties excluded)
344344 * - (?=[,\s;]|$)
345345 * simply matches the end of the possible keyframe, valid endings are: a comma, a space, a
346346 * semicolon or the end of the string
@@ -461,7 +461,7 @@ export class ShadowCss {
461461 */
462462 private _scopeCssText ( cssText : string , scopeSelector : string , hostSelector : string ) : string {
463463 const unscopedRules = this . _extractUnscopedRulesFromCssText ( cssText ) ;
464- // replace :host and :host-context -shadowcsshost and -shadowcsshost respectively
464+ // replace :host and :host-context with -shadowcsshost and -shadowcsshostcontext respectively
465465 cssText = this . _insertPolyfillHostInCssText ( cssText ) ;
466466 cssText = this . _convertColonHost ( cssText ) ;
467467 cssText = this . _convertColonHostContext ( cssText ) ;
@@ -618,7 +618,12 @@ export class ShadowCss {
618618 let selector = rule . selector ;
619619 let content = rule . content ;
620620 if ( rule . selector [ 0 ] !== '@' ) {
621- selector = this . _scopeSelector ( { selector, scopeSelector, hostSelector} ) ;
621+ selector = this . _scopeSelector ( {
622+ selector,
623+ scopeSelector,
624+ hostSelector,
625+ isParentSelector : true ,
626+ } ) ;
622627 } else if ( scopedAtRuleIdentifiers . some ( ( atRule ) => rule . selector . startsWith ( atRule ) ) ) {
623628 content = this . _scopeSelectors ( rule . content , scopeSelector , hostSelector ) ;
624629 } else if ( rule . selector . startsWith ( '@font-face' ) || rule . selector . startsWith ( '@page' ) ) {
@@ -658,20 +663,30 @@ export class ShadowCss {
658663 } ) ;
659664 }
660665
666+ private _safeSelector : SafeSelector | undefined ;
667+ private _shouldScopeIndicator : boolean | undefined ;
668+
669+ // `isParentSelector` is used to distinguish the selectors which are coming from
670+ // the initial selector string and any nested selectors, parsed recursively,
671+ // for example `selector = 'a:where(.one)'` could be the parent, while recursive call
672+ // would have `selector = '.one'`.
661673 private _scopeSelector ( {
662674 selector,
663675 scopeSelector,
664676 hostSelector,
665- shouldScope ,
677+ isParentSelector = false ,
666678 } : {
667679 selector : string ;
668680 scopeSelector : string ;
669681 hostSelector : string ;
670- shouldScope ?: boolean ;
682+ isParentSelector ?: boolean ;
671683 } ) : string {
672684 // Split the selector into independent parts by `,` (comma) unless
673685 // comma is within parenthesis, for example `:is(.one, two)`.
674- const selectorSplitRe = / ? , (? ! [ ^ \( ] * \) ) ? / ;
686+ // Negative lookup after comma allows not splitting inside nested parenthesis,
687+ // up to three levels (((,))).
688+ const selectorSplitRe =
689+ / ? , (? ! (?: [ ^ ) ( ] * (?: \( [ ^ ) ( ] * (?: \( [ ^ ) ( ] * (?: \( [ ^ ) ( ] * \) [ ^ ) ( ] * ) * \) [ ^ ) ( ] * ) * \) [ ^ ) ( ] * ) * \) ) ) ? / ;
675690
676691 return selector
677692 . split ( selectorSplitRe )
@@ -684,7 +699,7 @@ export class ShadowCss {
684699 selector : shallowPart ,
685700 scopeSelector,
686701 hostSelector,
687- shouldScope ,
702+ isParentSelector ,
688703 } ) ;
689704 } else {
690705 return shallowPart ;
@@ -718,9 +733,9 @@ export class ShadowCss {
718733 if ( _polyfillHostRe . test ( selector ) ) {
719734 const replaceBy = `[${ hostSelector } ]` ;
720735 return selector
721- . replace ( _polyfillHostNoCombinatorRe , ( hnc , selector ) => {
736+ . replace ( _polyfillHostNoCombinatorReGlobal , ( _hnc , selector ) => {
722737 return selector . replace (
723- / ( [ ^ : ] * ) ( : * ) ( .* ) / ,
738+ / ( [ ^ : \) ] * ) ( : * ) ( .* ) / ,
724739 ( _ : string , before : string , colon : string , after : string ) => {
725740 return before + replaceBy + colon + after ;
726741 } ,
@@ -738,12 +753,12 @@ export class ShadowCss {
738753 selector,
739754 scopeSelector,
740755 hostSelector,
741- shouldScope ,
756+ isParentSelector ,
742757 } : {
743758 selector : string ;
744759 scopeSelector : string ;
745760 hostSelector : string ;
746- shouldScope ?: boolean ;
761+ isParentSelector ?: boolean ;
747762 } ) : string {
748763 const isRe = / \[ i s = ( [ ^ \] ] * ) \] / g;
749764 scopeSelector = scopeSelector . replace ( isRe , ( _ : string , ...parts : string [ ] ) => parts [ 0 ] ) ;
@@ -759,6 +774,10 @@ export class ShadowCss {
759774
760775 if ( p . includes ( _polyfillHostNoCombinator ) ) {
761776 scopedP = this . _applySimpleSelectorScope ( p , scopeSelector , hostSelector ) ;
777+ if ( _polyfillHostNoCombinatorWithinPseudoFunction . test ( p ) ) {
778+ const [ _ , before , colon , after ] = scopedP . match ( / ( [ ^ : ] * ) ( : * ) ( .* ) / ) ! ;
779+ scopedP = before + attrName + colon + after ;
780+ }
762781 } else {
763782 // remove :host since it should be unnecessary
764783 const t = p . replace ( _polyfillHostRe , '' ) ;
@@ -775,44 +794,66 @@ export class ShadowCss {
775794
776795 // Wraps `_scopeSelectorPart()` to not use it directly on selectors with
777796 // pseudo selector functions like `:where()`. Selectors within pseudo selector
778- // functions are recursively sent to `_scopeSelector()` with the `shouldScope`
779- // argument, so the selectors get scoped correctly.
797+ // functions are recursively sent to `_scopeSelector()`.
780798 const _pseudoFunctionAwareScopeSelectorPart = ( selectorPart : string ) => {
781799 let scopedPart = '' ;
782800
783- const cssPseudoSelectorFunctionMatch = selectorPart . match ( _cssPseudoSelectorFunctionPrefix ) ;
784- if ( cssPseudoSelectorFunctionMatch ) {
785- const [ cssPseudoSelectorFunction ] = cssPseudoSelectorFunctionMatch ;
801+ const cssPrefixWithPseudoSelectorFunctionMatch = selectorPart . match (
802+ _cssPrefixWithPseudoSelectorFunction ,
803+ ) ;
804+ if ( cssPrefixWithPseudoSelectorFunctionMatch ) {
805+ const [ cssPseudoSelectorFunction , mainSelector , pseudoSelector ] =
806+ cssPrefixWithPseudoSelectorFunctionMatch ;
807+ const hasOuterHostNoCombinator = mainSelector . includes ( _polyfillHostNoCombinator ) ;
808+ const scopedMainSelector = mainSelector . replace (
809+ _polyfillHostNoCombinatorReGlobal ,
810+ `[${ hostSelector } ]` ,
811+ ) ;
812+
786813 // Unwrap the pseudo selector, to scope its contents.
787- // For example, `:where(selectorToScope)` -> `selectorToScope`.
814+ // For example,
815+ // - `:where(selectorToScope)` -> `selectorToScope`;
816+ // - `div:is(.foo, .bar)` -> `.foo, .bar`.
788817 const selectorToScope = selectorPart . slice ( cssPseudoSelectorFunction . length , - 1 ) ;
789818
819+ if ( selectorToScope . includes ( _polyfillHostNoCombinator ) ) {
820+ this . _shouldScopeIndicator = true ;
821+ }
822+
790823 const scopedInnerPart = this . _scopeSelector ( {
791824 selector : selectorToScope ,
792825 scopeSelector,
793826 hostSelector,
794- shouldScope : shouldScopeIndicator ,
795827 } ) ;
828+
796829 // Put the result back into the pseudo selector function.
797- scopedPart = `${ cssPseudoSelectorFunction } ${ scopedInnerPart } )` ;
830+ scopedPart = `${ scopedMainSelector } :${ pseudoSelector } (${ scopedInnerPart } )` ;
831+
832+ this . _shouldScopeIndicator = this . _shouldScopeIndicator || hasOuterHostNoCombinator ;
798833 } else {
799- shouldScopeIndicator =
800- shouldScopeIndicator || selectorPart . includes ( _polyfillHostNoCombinator ) ;
801- scopedPart = shouldScopeIndicator ? _scopeSelectorPart ( selectorPart ) : selectorPart ;
834+ this . _shouldScopeIndicator =
835+ this . _shouldScopeIndicator || selectorPart . includes ( _polyfillHostNoCombinator ) ;
836+ scopedPart = this . _shouldScopeIndicator ? _scopeSelectorPart ( selectorPart ) : selectorPart ;
802837 }
803838
804839 return scopedPart ;
805840 } ;
806841
807- const safeContent = new SafeSelector ( selector ) ;
808- selector = safeContent . content ( ) ;
842+ if ( isParentSelector ) {
843+ this . _safeSelector = new SafeSelector ( selector ) ;
844+ selector = this . _safeSelector . content ( ) ;
845+ }
809846
810847 let scopedSelector = '' ;
811848 let startIndex = 0 ;
812849 let res : RegExpExecArray | null ;
813850 // Combinators aren't used as a delimiter if they are within parenthesis,
814851 // for example `:where(.one .two)` stays intact.
815- const sep = / ( | > | \+ | ~ (? ! = ) ) (? ! [ ^ \( ] * \) ) \s * / g;
852+ // Similarly to selector separation by comma initially, negative lookahead
853+ // is used here to not break selectors within nested parenthesis up to three
854+ // nested layers.
855+ const sep =
856+ / ( | > | \+ | ~ (? ! = ) ) (? ! ( [ ^ ) ( ] * (?: \( [ ^ ) ( ] * (?: \( [ ^ ) ( ] * (?: \( [ ^ ) ( ] * \) [ ^ ) ( ] * ) * \) [ ^ ) ( ] * ) * \) [ ^ ) ( ] * ) * \) ) ) \s * / g;
816857
817858 // If a selector appears before :host it should not be shimmed as it
818859 // matches on ancestor elements and not on elements in the host's shadow
@@ -826,8 +867,13 @@ export class ShadowCss {
826867 // - `tag :host` -> `tag [h]` (`tag` is not scoped because it's considered part of a
827868 // `:host-context(tag)`)
828869 const hasHost = selector . includes ( _polyfillHostNoCombinator ) ;
829- // Only scope parts after the first `-shadowcsshost-no-combinator` when it is present
830- let shouldScopeIndicator = shouldScope ?? ! hasHost ;
870+ // Only scope parts after or on the same level as the first `-shadowcsshost-no-combinator`
871+ // when it is present. The selector has the same level when it is a part of a pseudo
872+ // selector, like `:where()`, for example `:where(:host, .foo)` would result in `.foo`
873+ // being scoped.
874+ if ( isParentSelector || this . _shouldScopeIndicator ) {
875+ this . _shouldScopeIndicator = ! hasHost ;
876+ }
831877
832878 while ( ( res = sep . exec ( selector ) ) !== null ) {
833879 const separator = res [ 1 ] ;
@@ -855,7 +901,8 @@ export class ShadowCss {
855901 scopedSelector += _pseudoFunctionAwareScopeSelectorPart ( part ) ;
856902
857903 // replace the placeholders with their original values
858- return safeContent . restore ( scopedSelector ) ;
904+ // using values stored inside the `safeSelector` instance.
905+ return this . _safeSelector ! . restore ( scopedSelector ) ;
859906 }
860907
861908 private _insertPolyfillHostInCssText ( selector : string ) : string {
@@ -920,7 +967,7 @@ class SafeSelector {
920967 }
921968}
922969
923- const _cssPseudoSelectorFunctionPrefix = / ^ : ( w h e r e | i s ) \( / gi ;
970+ const _cssPrefixWithPseudoSelectorFunction = / ^ ( [ ^ : ] * ) : ( w h e r e | i s ) \( / i ;
924971const _cssContentNextSelectorRe =
925972 / 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;
926973const _cssContentRuleRe = / ( p o l y f i l l - r u l e ) [ ^ } ] * ( c o n t e n t : [ \s ] * ( [ ' " ] ) ( .* ?) \3) [ ; \s ] * [ ^ } ] * } / gim;
@@ -934,7 +981,11 @@ const _cssColonHostRe = new RegExp(_polyfillHost + _parenSuffix, 'gim');
934981const _cssColonHostContextReGlobal = new RegExp ( _polyfillHostContext + _parenSuffix , 'gim' ) ;
935982const _cssColonHostContextRe = new RegExp ( _polyfillHostContext + _parenSuffix , 'im' ) ;
936983const _polyfillHostNoCombinator = _polyfillHost + '-no-combinator' ;
984+ const _polyfillHostNoCombinatorWithinPseudoFunction = new RegExp (
985+ `:.*(.*${ _polyfillHostNoCombinator } .*)` ,
986+ ) ;
937987const _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 ] * ) / ;
988+ const _polyfillHostNoCombinatorReGlobal = new RegExp ( _polyfillHostNoCombinatorRe , 'g' ) ;
938989const _shadowDOMSelectorsRe = [
939990 / : : s h a d o w / g,
940991 / : : c o n t e n t / g,
0 commit comments