Skip to content

Commit

Permalink
feat(css): account for escaped ':' in css selectors (#3087)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
nemzes committed Nov 8, 2021
1 parent 1b8b7ec commit 6000681
Show file tree
Hide file tree
Showing 2 changed files with 18 additions and 9 deletions.
18 changes: 9 additions & 9 deletions src/utils/shadow-css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ const extractCommentsWithHash = (input: string): string[] => {

const _ruleRe = /(\s*)([^;\{\}]+?)(\s*)((?:{%BLOCK%}?\s*;?)|(?:\s*;))/g;
const _curlyRe = /([{}])/g;
const _selectorPartsRe = /(^.*?[^\\])??((:+)(.*)|$)/;
const OPEN_CURLY = '{';
const CLOSE_CURLY = '}';
const BLOCK_PLACEHOLDER = '%BLOCK%';
Expand Down Expand Up @@ -256,17 +257,19 @@ const selectorNeedsScoping = (selector: string, scopeSelector: string) => {
return !re.test(selector);
};

const injectScopingSelector = (selector: string, scopingSelector: string) => {
return selector.replace(_selectorPartsRe, (_: string, before = '', _colonGroup: string, colon = '', after = '') => {
return before + scopingSelector + colon + after;
});
};

const applySimpleSelectorScope = (selector: string, scopeSelector: string, hostSelector: string) => {
// In Android browser, the lastIndex is not reset when the regex is used in String.replace()
_polyfillHostRe.lastIndex = 0;
if (_polyfillHostRe.test(selector)) {
const replaceBy = `.${hostSelector}`;
return selector
.replace(_polyfillHostNoCombinatorRe, (_, selector) => {
return selector.replace(/([^:]*)(:*)(.*)/, (_: string, before: string, colon: string, after: string) => {
return before + replaceBy + colon + after;
});
})
.replace(_polyfillHostNoCombinatorRe, (_, selector) => injectScopingSelector(selector, replaceBy))
.replace(_polyfillHostRe, replaceBy + ' ');
}

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

Expand Down
9 changes: 9 additions & 0 deletions src/utils/test/scope-css.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,15 @@ describe('ShadowCss', function () {
expect(s('[is="one"] {}', 'a')).toEqual('[is="one"].a {}');
});

it('should handle escaped ":" in selector', () => {
expect(s('\\:one {}', 'a')).toEqual('\\:one.a {}');
expect(s('one\\:two {}', 'a')).toEqual('one\\:two.a {}');
expect(s('one\\:two:hover {}', 'a')).toEqual('one\\:two.a:hover {}');
expect(s('one\\:two::before {}', 'a')).toEqual('one\\:two.a::before {}');
expect(s('one\\:two::before:hover {}', 'a')).toEqual('one\\:two.a::before:hover {}');
expect(s('one\\:two:not(.three\\:four) {}', 'a')).toEqual('one\\:two.a:not(.three\\:four) {}');
});

describe(':host', () => {
it('should handle no context, commentOriginalSelector', () => {
expect(s(':host {}', 'a', true)).toEqual('/*!@:host*/.a-h {}');
Expand Down

0 comments on commit 6000681

Please sign in to comment.