From 335d6c8c00369e34501ea780cd7e3ea4bb995e54 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Fri, 25 Dec 2020 10:10:31 +0200 Subject: [PATCH] fix(compiler): incorrectly encapsulating selectors with escape sequences (#40264) CSS supports escaping in selectors, e.g. writing `.foo:bar` will match an element with the `foo` class and `bar` pseudo-class, but `.foo\:bar` will match the `foo:bar` class. Our shimmed shadow DOM encapsulation always assumes that `:` means a pseudo selector which breaks a selector like `.foo\:bar`. These changes add some extra logic so that escaped characters in selectors are preserved. Fixes #31844. PR Close #40264 --- packages/compiler/src/shadow_css.ts | 29 +++++++++++++++++------ packages/compiler/test/shadow_css_spec.ts | 11 ++++++++- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/packages/compiler/src/shadow_css.ts b/packages/compiler/src/shadow_css.ts index 72463ca8174..5fe6ecb8a87 100644 --- a/packages/compiler/src/shadow_css.ts +++ b/packages/compiler/src/shadow_css.ts @@ -485,12 +485,14 @@ class SafeSelector { constructor(selector: string) { // Replaces attribute selectors with placeholders. // The WS in [attr="va lue"] would otherwise be interpreted as a selector separator. - selector = selector.replace(/(\[[^\]]*\])/g, (_, keep) => { - const replaceBy = `__ph-${this.index}__`; - this.placeholders.push(keep); - this.index++; - return replaceBy; - }); + selector = this._escapeRegexMatches(selector, /(\[[^\]]*\])/g); + + // CSS allows for certain special characters to be used in selectors if they're escaped. + // E.g. `.foo:blue` won't match a class called `foo:blue`, because the colon denotes a + // pseudo-class, but writing `.foo\:blue` will match, because the colon was escaped. + // Replace all escape sequences (`\` followed by a character) with a placeholder so + // that our handling of pseudo-selectors doesn't mess with them. + selector = this._escapeRegexMatches(selector, /(\\.)/g); // Replaces the expression in `:nth-child(2n + 1)` with a placeholder. // WS and "+" would otherwise be interpreted as selector separators. @@ -503,12 +505,25 @@ class SafeSelector { } restore(content: string): string { - return content.replace(/__ph-(\d+)__/g, (ph, index) => this.placeholders[+index]); + return content.replace(/__ph-(\d+)__/g, (_ph, index) => this.placeholders[+index]); } content(): string { return this._content; } + + /** + * Replaces all of the substrings that match a regex within a + * special string (e.g. `__ph-0__`, `__ph-1__`, etc). + */ + private _escapeRegexMatches(content: string, pattern: RegExp): string { + return content.replace(pattern, (_, keep) => { + const replaceBy = `__ph-${this.index}__`; + this.placeholders.push(keep); + this.index++; + return replaceBy; + }); + } } const _cssContentNextSelectorRe = diff --git a/packages/compiler/test/shadow_css_spec.ts b/packages/compiler/test/shadow_css_spec.ts index ea9c7719721..5b9ba518813 100644 --- a/packages/compiler/test/shadow_css_spec.ts +++ b/packages/compiler/test/shadow_css_spec.ts @@ -10,7 +10,7 @@ import {CssRule, processRules, ShadowCss} from '@angular/compiler/src/shadow_css import {normalizeCSS} from '@angular/platform-browser/testing/src/browser_util'; { - describe('ShadowCss', function() { + describe('ShadowCss', () => { function s(css: string, contentAttr: string, hostAttr: string = '') { const shadowCss = new ShadowCss(); const shim = shadowCss.shimCssText(css, contentAttr, hostAttr); @@ -112,6 +112,15 @@ import {normalizeCSS} from '@angular/platform-browser/testing/src/browser_util'; expect(s('[is="one"] {}', 'contenta')).toEqual('[is="one"][contenta] {}'); }); + it('should handle escaped sequences in selectors', () => { + expect(s('one\\/two {}', 'contenta')).toEqual('one\\/two[contenta] {}'); + expect(s('one\\:two {}', 'contenta')).toEqual('one\\:two[contenta] {}'); + expect(s('one\\\\:two {}', 'contenta')).toEqual('one\\\\[contenta]:two {}'); + expect(s('.one\\:two {}', 'contenta')).toEqual('.one\\:two[contenta] {}'); + expect(s('.one\\:two .three\\:four {}', 'contenta')) + .toEqual('.one\\:two[contenta] .three\\:four[contenta] {}'); + }); + describe((':host'), () => { it('should handle no context', () => { expect(s(':host {}', 'contenta', 'a-host')).toEqual('[a-host] {}');