55 * Use of this source code is governed by an MIT-style license that can be
66 * found in the LICENSE file at https://angular.dev/license
77 */
8+ import * as chars from './chars' ;
89
910/**
1011 * The following set contains all keywords that can be used in the animation css shorthand
@@ -525,6 +526,40 @@ export class ShadowCss {
525526 } ) ;
526527 }
527528
529+ /**
530+ * Generator function that splits a string on top-level commas (commas that are not inside parentheses).
531+ * Yields each part of the string between top-level commas. Terminates if an extra closing paren is found.
532+ *
533+ * @param text The string to split
534+ */
535+ private * _splitOnTopLevelCommas ( text : string ) : Generator < string > {
536+ const length = text . length ;
537+ let parens = 0 ;
538+ let prev = 0 ;
539+
540+ for ( let i = 0 ; i < length ; i ++ ) {
541+ const charCode = text . charCodeAt ( i ) ;
542+
543+ if ( charCode === chars . $LPAREN ) {
544+ parens ++ ;
545+ } else if ( charCode === chars . $RPAREN ) {
546+ parens -- ;
547+ if ( parens < 0 ) {
548+ // Found an extra closing paren. Assume we want the list terminated here
549+ yield text . slice ( prev , i ) ;
550+ return ;
551+ }
552+ } else if ( charCode === chars . $COMMA && parens === 0 ) {
553+ // Found a top-level comma, yield the current chunk
554+ yield text . slice ( prev , i ) ;
555+ prev = i + 1 ;
556+ }
557+ }
558+
559+ // Yield the final chunk
560+ yield text . slice ( prev ) ;
561+ }
562+
528563 /*
529564 * convert a rule like :host-context(.foo) > .bar { }
530565 *
@@ -541,38 +576,14 @@ export class ShadowCss {
541576 * .foo<scopeName> .bar { ... }
542577 */
543578 private _convertColonHostContext ( cssText : string ) : string {
544- const length = cssText . length ;
545- let parens = 0 ;
546- let prev = 0 ;
547- let result = '' ;
548-
549579 // Splits up the selectors on their top-level commas, processes the :host-context in them
550580 // individually and stitches them back together. This ensures that individual selectors don't
551581 // affect each other.
552- for ( let i = 0 ; i < length ; i ++ ) {
553- const char = cssText [ i ] ;
554-
555- // If we hit a comma and there are no open parentheses, take the current chunk and process it.
556- if ( char === ',' && parens === 0 ) {
557- result += this . _convertColonHostContextInSelectorPart ( cssText . slice ( prev , i ) ) + ',' ;
558- prev = i + 1 ;
559- continue ;
560- }
561-
562- // We've hit the end. Take everything since the last comma.
563- if ( i === length - 1 ) {
564- result += this . _convertColonHostContextInSelectorPart ( cssText . slice ( prev ) ) ;
565- break ;
566- }
567-
568- if ( char === '(' ) {
569- parens ++ ;
570- } else if ( char === ')' ) {
571- parens -- ;
572- }
582+ const results : string [ ] = [ ] ;
583+ for ( const part of this . _splitOnTopLevelCommas ( cssText ) ) {
584+ results . push ( this . _convertColonHostContextInSelectorPart ( part ) ) ;
573585 }
574-
575- return result ;
586+ return results . join ( ',' ) ;
576587 }
577588
578589 private _convertColonHostContextInSelectorPart ( cssText : string ) : string {
@@ -587,18 +598,28 @@ export class ShadowCss {
587598
588599 // There may be more than `:host-context` in this selector so `selectorText` could look like:
589600 // `:host-context(.one):host-context(.two)`.
590- // Execute `_cssColonHostContextRe` over and over until we have extracted all the
591- // `:host-context` selectors from this selector.
592- let match : RegExpExecArray | null ;
593- while ( ( match = _cssColonHostContextRe . exec ( selectorText ) ) ) {
594- // `match` = [':host-context(<selectors>)<rest>', <selectors>, <rest>]
595-
596- // The `<selectors>` could actually be a comma separated list: `:host-context(.one, .two)`.
597- const newContextSelectors = ( match [ 1 ] ?? '' )
598- . trim ( )
599- . split ( ',' )
600- . map ( ( m ) => m . trim ( ) )
601- . filter ( ( m ) => m !== '' ) ;
601+ // Loop until every :host-context in the compound selector has been processed.
602+ let startIndex = selectorText . indexOf ( _polyfillHostContext ) ;
603+ while ( startIndex !== - 1 ) {
604+ const afterPrefix = selectorText . substring ( startIndex + _polyfillHostContext . length ) ;
605+
606+ if ( ! afterPrefix || afterPrefix [ 0 ] !== '(' ) {
607+ // Edge case of :host-context with no parens (e.g. `:host-context .inner`)
608+ selectorText = afterPrefix ;
609+ startIndex = selectorText . indexOf ( _polyfillHostContext ) ;
610+ continue ;
611+ }
612+
613+ // Extract comma-separated selectors between the parentheses
614+ const newContextSelectors : string [ ] = [ ] ;
615+ let endIndex = 0 ; // Index of the closing paren of the :host-context()
616+ for ( const selector of this . _splitOnTopLevelCommas ( afterPrefix . substring ( 1 ) ) ) {
617+ endIndex = endIndex + selector . length + 1 ;
618+ const trimmed = selector . trim ( ) ;
619+ if ( trimmed ) {
620+ newContextSelectors . push ( trimmed ) ;
621+ }
622+ }
602623
603624 // We must duplicate the current selector group for each of these new selectors.
604625 // For example if the current groups are:
@@ -627,7 +648,8 @@ export class ShadowCss {
627648 }
628649
629650 // Update the `selectorText` and see repeat to see if there are more `:host-context`s.
630- selectorText = match [ 2 ] ;
651+ selectorText = afterPrefix . substring ( endIndex + 1 ) ;
652+ startIndex = selectorText . indexOf ( _polyfillHostContext ) ;
631653 }
632654
633655 // The context selectors now must be combined with each other to capture all the possible
@@ -1054,7 +1076,6 @@ const _cssColonHostContextReGlobal = new RegExp(
10541076 `${ _cssScopedPseudoFunctionPrefix } (${ _hostContextPattern } )` ,
10551077 'gim' ,
10561078) ;
1057- const _cssColonHostContextRe = new RegExp ( _hostContextPattern , 'im' ) ;
10581079const _polyfillHostNoCombinator = _polyfillHost + '-no-combinator' ;
10591080const _polyfillHostNoCombinatorOutsidePseudoFunction = new RegExp (
10601081 `${ _polyfillHostNoCombinator } (?![^(]*\\))` ,
0 commit comments