From 600068168c86dba9ea610b5e8a0dbba00ff4d1f4 Mon Sep 17 00:00:00 2001 From: Nelson Menezes Date: Mon, 8 Nov 2021 14:54:23 +0100 Subject: [PATCH] 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 --- src/utils/shadow-css.ts | 18 +++++++++--------- src/utils/test/scope-css.spec.ts | 9 +++++++++ 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/utils/shadow-css.ts b/src/utils/shadow-css.ts index 6f547ffe398..4a18ac6270a 100644 --- a/src/utils/shadow-css.ts +++ b/src/utils/shadow-css.ts @@ -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%'; @@ -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 + ' '); } @@ -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); } } diff --git a/src/utils/test/scope-css.spec.ts b/src/utils/test/scope-css.spec.ts index 11c17360be5..5f155735e00 100644 --- a/src/utils/test/scope-css.spec.ts +++ b/src/utils/test/scope-css.spec.ts @@ -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 {}');