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:
parent
8ebac24b48
commit
335d6c8c00
@ -485,12 +485,14 @@ class SafeSelector {
|
|||||||
constructor(selector: string) {
|
constructor(selector: string) {
|
||||||
// Replaces attribute selectors with placeholders.
|
// Replaces attribute selectors with placeholders.
|
||||||
// The WS in [attr="va lue"] would otherwise be interpreted as a selector separator.
|
// The WS in [attr="va lue"] would otherwise be interpreted as a selector separator.
|
||||||
selector = selector.replace(/(\[[^\]]*\])/g, (_, keep) => {
|
selector = this._escapeRegexMatches(selector, /(\[[^\]]*\])/g);
|
||||||
const replaceBy = `__ph-${this.index}__`;
|
|
||||||
this.placeholders.push(keep);
|
// CSS allows for certain special characters to be used in selectors if they're escaped.
|
||||||
this.index++;
|
// E.g. `.foo:blue` won't match a class called `foo:blue`, because the colon denotes a
|
||||||
return replaceBy;
|
// 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.
|
// Replaces the expression in `:nth-child(2n + 1)` with a placeholder.
|
||||||
// WS and "+" would otherwise be interpreted as selector separators.
|
// WS and "+" would otherwise be interpreted as selector separators.
|
||||||
@ -503,12 +505,25 @@ class SafeSelector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
restore(content: string): string {
|
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 {
|
content(): string {
|
||||||
return this._content;
|
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 =
|
const _cssContentNextSelectorRe =
|
||||||
|
@ -10,7 +10,7 @@ import {CssRule, processRules, ShadowCss} from '@angular/compiler/src/shadow_css
|
|||||||
import {normalizeCSS} from '@angular/platform-browser/testing/src/browser_util';
|
import {normalizeCSS} from '@angular/platform-browser/testing/src/browser_util';
|
||||||
|
|
||||||
{
|
{
|
||||||
describe('ShadowCss', function() {
|
describe('ShadowCss', () => {
|
||||||
function s(css: string, contentAttr: string, hostAttr: string = '') {
|
function s(css: string, contentAttr: string, hostAttr: string = '') {
|
||||||
const shadowCss = new ShadowCss();
|
const shadowCss = new ShadowCss();
|
||||||
const shim = shadowCss.shimCssText(css, contentAttr, hostAttr);
|
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] {}');
|
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'), () => {
|
describe((':host'), () => {
|
||||||
it('should handle no context', () => {
|
it('should handle no context', () => {
|
||||||
expect(s(':host {}', 'contenta', 'a-host')).toEqual('[a-host] {}');
|
expect(s(':host {}', 'contenta', 'a-host')).toEqual('[a-host] {}');
|
||||||
|
Loading…
x
Reference in New Issue
Block a user