Skip to content

Commit 6000681

Browse files
authored
feat(css): account for escaped ':' in css selectors (#3087)
Prior to this commit, when using `scoped: true` on a component, CSS class names with special characters are mangled due to incorrect escaping. This is particularly obvious when using a framework like Tailwind, which makes extensive use of special characters (:) in class names. With this commit `\:` remains escaped in CSS selectors when introducing scope class
1 parent 1b8b7ec commit 6000681

2 files changed

Lines changed: 18 additions & 9 deletions

File tree

src/utils/shadow-css.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ const extractCommentsWithHash = (input: string): string[] => {
9191

9292
const _ruleRe = /(\s*)([^;\{\}]+?)(\s*)((?:{%BLOCK%}?\s*;?)|(?:\s*;))/g;
9393
const _curlyRe = /([{}])/g;
94+
const _selectorPartsRe = /(^.*?[^\\])??((:+)(.*)|$)/;
9495
const OPEN_CURLY = '{';
9596
const CLOSE_CURLY = '}';
9697
const BLOCK_PLACEHOLDER = '%BLOCK%';
@@ -256,17 +257,19 @@ const selectorNeedsScoping = (selector: string, scopeSelector: string) => {
256257
return !re.test(selector);
257258
};
258259

260+
const injectScopingSelector = (selector: string, scopingSelector: string) => {
261+
return selector.replace(_selectorPartsRe, (_: string, before = '', _colonGroup: string, colon = '', after = '') => {
262+
return before + scopingSelector + colon + after;
263+
});
264+
};
265+
259266
const applySimpleSelectorScope = (selector: string, scopeSelector: string, hostSelector: string) => {
260267
// In Android browser, the lastIndex is not reset when the regex is used in String.replace()
261268
_polyfillHostRe.lastIndex = 0;
262269
if (_polyfillHostRe.test(selector)) {
263270
const replaceBy = `.${hostSelector}`;
264271
return selector
265-
.replace(_polyfillHostNoCombinatorRe, (_, selector) => {
266-
return selector.replace(/([^:]*)(:*)(.*)/, (_: string, before: string, colon: string, after: string) => {
267-
return before + replaceBy + colon + after;
268-
});
269-
})
272+
.replace(_polyfillHostNoCombinatorRe, (_, selector) => injectScopingSelector(selector, replaceBy))
270273
.replace(_polyfillHostRe, replaceBy + ' ');
271274
}
272275

@@ -292,10 +295,7 @@ const applyStrictSelectorScope = (selector: string, scopeSelector: string, hostS
292295
// remove :host since it should be unnecessary
293296
const t = p.replace(_polyfillHostRe, '');
294297
if (t.length > 0) {
295-
const matches = t.match(/([^:]*)(:*)(.*)/);
296-
if (matches) {
297-
scopedP = matches[1] + className + matches[2] + matches[3];
298-
}
298+
scopedP = injectScopingSelector(t, className);
299299
}
300300
}
301301

src/utils/test/scope-css.spec.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,15 @@ describe('ShadowCss', function () {
143143
expect(s('[is="one"] {}', 'a')).toEqual('[is="one"].a {}');
144144
});
145145

146+
it('should handle escaped ":" in selector', () => {
147+
expect(s('\\:one {}', 'a')).toEqual('\\:one.a {}');
148+
expect(s('one\\:two {}', 'a')).toEqual('one\\:two.a {}');
149+
expect(s('one\\:two:hover {}', 'a')).toEqual('one\\:two.a:hover {}');
150+
expect(s('one\\:two::before {}', 'a')).toEqual('one\\:two.a::before {}');
151+
expect(s('one\\:two::before:hover {}', 'a')).toEqual('one\\:two.a::before:hover {}');
152+
expect(s('one\\:two:not(.three\\:four) {}', 'a')).toEqual('one\\:two.a:not(.three\\:four) {}');
153+
});
154+
146155
describe(':host', () => {
147156
it('should handle no context, commentOriginalSelector', () => {
148157
expect(s(':host {}', 'a', true)).toEqual('/*!@:host*/.a-h {}');

0 commit comments

Comments
 (0)