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
This commit is contained in:
Kristiyan Kostadinov 2020-12-25 10:10:31 +02:00 committed by Joey Perrott
parent 8ebac24b48
commit 335d6c8c00
2 changed files with 32 additions and 8 deletions

View File

@ -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 =

View File

@ -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] {}');