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) {
|
||||
// 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 =
|
||||
|
|
|
@ -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] {}');
|
||||
|
|
Loading…
Reference in New Issue