fix(compiler): support multiple :host-context() selectors (#40494)
				
					
				
			In `ViewEncapsulation.Emulated` mode, the compiler must generate additional combinations of selectors to handle the `:host-context()` pseudo-class function. Previously, when there is was more than one `:host-context()` selector in a rule, the compiler was generating invalid selectors. This commit generates all possible combinations of selectors needed to match the same elements as the native `:host-context()` selector. Fixes #19199 PR Close #40494
This commit is contained in:
		
							parent
							
								
									d6440c1808
								
							
						
					
					
						commit
						ba3f99d7cc
					
				| @ -260,7 +260,21 @@ export class ShadowCss { | ||||
|    * .foo<scopeName> > .bar | ||||
|    */ | ||||
|   private _convertColonHost(cssText: string): string { | ||||
|     return this._convertColonRule(cssText, _cssColonHostRe, this._colonHostPartReplacer); | ||||
|     return cssText.replace(_cssColonHostRe, (_, hostSelectors: string, otherSelectors: string) => { | ||||
|       if (hostSelectors) { | ||||
|         const convertedSelectors: string[] = []; | ||||
|         const hostSelectorArray = hostSelectors.split(',').map(p => p.trim()); | ||||
|         for (const hostSelector of hostSelectorArray) { | ||||
|           if (!hostSelector) break; | ||||
|           const convertedSelector = | ||||
|               _polyfillHostNoCombinator + hostSelector.replace(_polyfillHost, '') + otherSelectors; | ||||
|           convertedSelectors.push(convertedSelector); | ||||
|         } | ||||
|         return convertedSelectors.join(','); | ||||
|       } else { | ||||
|         return _polyfillHostNoCombinator + otherSelectors; | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   /* | ||||
| @ -268,7 +282,7 @@ export class ShadowCss { | ||||
|    * | ||||
|    * to | ||||
|    * | ||||
|    * .foo<scopeName> > .bar, .foo scopeName > .bar { } | ||||
|    * .foo<scopeName> > .bar, .foo <scopeName> > .bar { } | ||||
|    * | ||||
|    * and | ||||
|    * | ||||
| @ -279,40 +293,31 @@ export class ShadowCss { | ||||
|    * .foo<scopeName> .bar { ... } | ||||
|    */ | ||||
|   private _convertColonHostContext(cssText: string): string { | ||||
|     return this._convertColonRule( | ||||
|         cssText, _cssColonHostContextRe, this._colonHostContextPartReplacer); | ||||
|   } | ||||
|     return cssText.replace(_cssColonHostContextReGlobal, selectorText => { | ||||
|       // We have captured a selector that contains a `:host-context` rule.
 | ||||
|       // There may be more than one so `selectorText` could look like:
 | ||||
|       // `:host-context(.one):host-context(.two)`.
 | ||||
| 
 | ||||
|   private _convertColonRule(cssText: string, regExp: RegExp, partReplacer: Function): string { | ||||
|     // m[1] = :host(-context), m[2] = contents of (), m[3] rest of rule
 | ||||
|     return cssText.replace(regExp, function(...m: string[]) { | ||||
|       if (m[2]) { | ||||
|         const parts = m[2].split(','); | ||||
|         const r: string[] = []; | ||||
|         for (let i = 0; i < parts.length; i++) { | ||||
|           const p = parts[i].trim(); | ||||
|           if (!p) break; | ||||
|           r.push(partReplacer(_polyfillHostNoCombinator, p, m[3])); | ||||
|       const contextSelectors: string[] = []; | ||||
|       let match: RegExpMatchArray|null; | ||||
| 
 | ||||
|       // Execute `_cssColonHostContextRe` over and over until we have extracted all the
 | ||||
|       // `:host-context` selectors from this selector.
 | ||||
|       while (match = _cssColonHostContextRe.exec(selectorText)) { | ||||
|         // `match` = [':host-context(<selectors>)<rest>', <selectors>, <rest>]
 | ||||
|         const contextSelector = (match[1] ?? '').trim(); | ||||
|         if (contextSelector !== '') { | ||||
|           contextSelectors.push(contextSelector); | ||||
|         } | ||||
|         return r.join(','); | ||||
|       } else { | ||||
|         return _polyfillHostNoCombinator + m[3]; | ||||
|         selectorText = match[2]; | ||||
|       } | ||||
| 
 | ||||
|       // The context selectors now must be combined with each other to capture all the possible
 | ||||
|       // selectors that `:host-context` can match.
 | ||||
|       return combineHostContextSelectors(_polyfillHostNoCombinator, contextSelectors, selectorText); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   private _colonHostContextPartReplacer(host: string, part: string, suffix: string): string { | ||||
|     if (part.indexOf(_polyfillHost) > -1) { | ||||
|       return this._colonHostPartReplacer(host, part, suffix); | ||||
|     } else { | ||||
|       return host + part + suffix + ', ' + part + ' ' + host + suffix; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   private _colonHostPartReplacer(host: string, part: string, suffix: string): string { | ||||
|     return host + part.replace(_polyfillHost, '') + suffix; | ||||
|   } | ||||
| 
 | ||||
|   /* | ||||
|    * Convert combinators like ::shadow and pseudo-elements like ::content | ||||
|    * by replacing with space. | ||||
| @ -534,11 +539,12 @@ const _cssContentUnscopedRuleRe = | ||||
| const _polyfillHost = '-shadowcsshost'; | ||||
| // note: :host-context pre-processed to -shadowcsshostcontext.
 | ||||
| const _polyfillHostContext = '-shadowcsscontext'; | ||||
| const _parenSuffix = ')(?:\\((' + | ||||
| const _parenSuffix = '(?:\\((' + | ||||
|     '(?:\\([^)(]*\\)|[^)(]*)+?' + | ||||
|     ')\\))?([^,{]*)'; | ||||
| const _cssColonHostRe = new RegExp('(' + _polyfillHost + _parenSuffix, 'gim'); | ||||
| const _cssColonHostContextRe = new RegExp('(' + _polyfillHostContext + _parenSuffix, 'gim'); | ||||
| const _cssColonHostRe = new RegExp(_polyfillHost + _parenSuffix, 'gim'); | ||||
| const _cssColonHostContextReGlobal = new RegExp(_polyfillHostContext + _parenSuffix, 'gim'); | ||||
| const _cssColonHostContextRe = new RegExp(_polyfillHostContext + _parenSuffix, 'im'); | ||||
| const _polyfillHostNoCombinator = _polyfillHost + '-no-combinator'; | ||||
| const _polyfillHostNoCombinatorRe = /-shadowcsshost-no-combinator([^\s]*)/; | ||||
| const _shadowDOMSelectorsRe = [ | ||||
| @ -650,3 +656,56 @@ function escapeBlocks( | ||||
|   } | ||||
|   return new StringWithEscapedBlocks(resultParts.join(''), escapedBlocks); | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Combine the `contextSelectors` with the `hostMarker` and the `otherSelectors` | ||||
|  * to create a selector that matches the same as `:host-context()`. | ||||
|  * | ||||
|  * Given a single context selector `A` we need to output selectors that match on the host and as an | ||||
|  * ancestor of the host: | ||||
|  * | ||||
|  * ``` | ||||
|  * A <hostMarker>, A<hostMarker> {} | ||||
|  * ``` | ||||
|  * | ||||
|  * When there is more than one context selector we also have to create combinations of those | ||||
|  * selectors with each other. For example if there are `A` and `B` selectors the output is: | ||||
|  * | ||||
|  * ``` | ||||
|  * AB<hostMarker>, AB <hostMarker>, A B<hostMarker>, | ||||
|  * B A<hostMarker>, A B <hostMarker>, B A <hostMarker> {} | ||||
|  * ``` | ||||
|  * | ||||
|  * And so on... | ||||
|  * | ||||
|  * @param hostMarker the string that selects the host element. | ||||
|  * @param contextSelectors an array of context selectors that will be combined. | ||||
|  * @param otherSelectors the rest of the selectors that are not context selectors. | ||||
|  */ | ||||
| function combineHostContextSelectors( | ||||
|     hostMarker: string, contextSelectors: string[], otherSelectors: string): string { | ||||
|   // If there are no context selectors then just output a host marker
 | ||||
|   if (contextSelectors.length === 0) { | ||||
|     return hostMarker + otherSelectors; | ||||
|   } | ||||
| 
 | ||||
|   const combined: string[] = [contextSelectors.pop() || '']; | ||||
|   while (contextSelectors.length > 0) { | ||||
|     const length = combined.length; | ||||
|     const contextSelector = contextSelectors.pop(); | ||||
|     for (let i = 0; i < length; i++) { | ||||
|       const previousSelectors = combined[i]; | ||||
|       // Add the new selector as a descendant of the previous selectors
 | ||||
|       combined[length * 2 + i] = previousSelectors + ' ' + contextSelector; | ||||
|       // Add the new selector as an ancestor of the previous selectors
 | ||||
|       combined[length + i] = contextSelector + ' ' + previousSelectors; | ||||
|       // Add the new selector to act on the same element as the previous selectors
 | ||||
|       combined[i] = contextSelector + previousSelectors; | ||||
|     } | ||||
|   } | ||||
|   // Finally connect the selector to the `hostMarker`s: either acting directly on the host
 | ||||
|   // (A<hostMarker>) or as an ancestor (A <hostMarker>).
 | ||||
|   return combined | ||||
|       .map(s => `${s}${hostMarker}${otherSelectors}, ${s} ${hostMarker}${otherSelectors}`) | ||||
|       .join(','); | ||||
| } | ||||
|  | ||||
| @ -145,6 +145,10 @@ import {normalizeCSS} from '@angular/platform-browser/testing/src/browser_util'; | ||||
|             .toEqual('ul[a-host] > .z[contenta], li[a-host] > .z[contenta] {}'); | ||||
|       }); | ||||
| 
 | ||||
|       it('should handle compound class selectors', () => { | ||||
|         expect(s(':host(.a.b) {}', 'contenta', 'a-host')).toEqual('.a.b[a-host] {}'); | ||||
|       }); | ||||
| 
 | ||||
|       it('should handle multiple class selectors', () => { | ||||
|         expect(s(':host(.x,.y) {}', 'contenta', 'a-host')).toEqual('.x[a-host], .y[a-host] {}'); | ||||
|         expect(s(':host(.x,.y) > .z {}', 'contenta', 'a-host')) | ||||
| @ -208,6 +212,52 @@ import {normalizeCSS} from '@angular/platform-browser/testing/src/browser_util'; | ||||
|         expect(s(':host-context([a=b]) {}', 'contenta', 'a-host')) | ||||
|             .toEqual('[a=b][a-host], [a="b"] [a-host] {}'); | ||||
|       }); | ||||
| 
 | ||||
|       it('should handle multiple :host-context() selectors', () => { | ||||
|         expect(s(':host-context(.one):host-context(.two) {}', 'contenta', 'a-host')) | ||||
|             .toEqual( | ||||
|                 '.one.two[a-host], ' +    // `one` and `two` both on the host
 | ||||
|                 '.one.two [a-host], ' +   // `one` and `two` are both on the same ancestor
 | ||||
|                 '.one .two[a-host], ' +   // `one` is an ancestor and `two` is on the host
 | ||||
|                 '.one .two [a-host], ' +  // `one` and `two` are both ancestors (in that order)
 | ||||
|                 '.two .one[a-host], ' +   // `two` is an ancestor and `one` is on the host
 | ||||
|                 '.two .one [a-host]' +    // `two` and `one` are both ancestors (in that order)
 | ||||
|                 ' {}'); | ||||
| 
 | ||||
|         expect(s(':host-context(.X):host-context(.Y):host-context(.Z) {}', 'contenta', 'a-host') | ||||
|                    .replace(/ \{\}$/, '') | ||||
|                    .split(/\,\s+/)) | ||||
|             .toEqual([ | ||||
|               '.X.Y.Z[a-host]', | ||||
|               '.X.Y.Z [a-host]', | ||||
|               '.X.Y .Z[a-host]', | ||||
|               '.X.Y .Z [a-host]', | ||||
|               '.X.Z .Y[a-host]', | ||||
|               '.X.Z .Y [a-host]', | ||||
|               '.X .Y.Z[a-host]', | ||||
|               '.X .Y.Z [a-host]', | ||||
|               '.X .Y .Z[a-host]', | ||||
|               '.X .Y .Z [a-host]', | ||||
|               '.X .Z .Y[a-host]', | ||||
|               '.X .Z .Y [a-host]', | ||||
|               '.Y.Z .X[a-host]', | ||||
|               '.Y.Z .X [a-host]', | ||||
|               '.Y .Z .X[a-host]', | ||||
|               '.Y .Z .X [a-host]', | ||||
|               '.Z .Y .X[a-host]', | ||||
|               '.Z .Y .X [a-host]', | ||||
|             ]); | ||||
|       }); | ||||
| 
 | ||||
|       // This test is checking backward compatibility.
 | ||||
|       // It is not clear what the behaviour should be for a `:host-context` with no selectors.
 | ||||
|       // Arguably it is actually an error that should be reported.
 | ||||
|       it('should handle :host-context with no ancestor selectors', () => { | ||||
|         expect(s('.outer :host-context .inner {}', 'contenta', 'a-host')) | ||||
|             .toEqual('.outer [a-host] .inner[contenta] {}'); | ||||
|         expect(s('.outer :host-context() .inner {}', 'contenta', 'a-host')) | ||||
|             .toEqual('.outer [a-host] .inner[contenta] {}'); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     it('should support polyfill-next-selector', () => { | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user