diff --git a/package-lock.json b/package-lock.json index 6680025d768be..f89c7f6aa04a1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41778,12 +41778,13 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true }, - "node_modules/postcss-prefixwrap": { - "version": "1.41.0", - "resolved": "https://registry.npmjs.org/postcss-prefixwrap/-/postcss-prefixwrap-1.41.0.tgz", - "integrity": "sha512-gmwwAEE+ci3/ZKjUZppTETINlh1QwihY8gCstInuS7ohk353KYItU4d64hvnUvO2GUy29hBGPHz4Ce+qJRi90A==", + "node_modules/postcss-prefix-selector": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/postcss-prefix-selector/-/postcss-prefix-selector-1.16.1.tgz", + "integrity": "sha512-Umxu+FvKMwlY6TyDzGFoSUnzW+NOfMBLyC1tAkIjgX+Z/qGspJeRjVC903D7mx7TuBpJlwti2ibXtWuA7fKMeQ==", + "license": "MIT", "peerDependencies": { - "postcss": "*" + "postcss": ">4 <9" } }, "node_modules/postcss-reduce-initial": { @@ -52261,7 +52262,7 @@ "fast-deep-equal": "^3.1.3", "memize": "^2.1.0", "postcss": "^8.4.21", - "postcss-prefixwrap": "^1.41.0", + "postcss-prefix-selector": "^1.16.0", "postcss-urlrebase": "^1.4.0", "react-autosize-textarea": "^7.1.0", "react-easy-crop": "^5.0.6", @@ -67281,7 +67282,7 @@ "fast-deep-equal": "^3.1.3", "memize": "^2.1.0", "postcss": "^8.4.21", - "postcss-prefixwrap": "^1.41.0", + "postcss-prefix-selector": "^1.16.0", "postcss-urlrebase": "^1.4.0", "react-autosize-textarea": "^7.1.0", "react-easy-crop": "^5.0.6", @@ -87827,10 +87828,10 @@ } } }, - "postcss-prefixwrap": { - "version": "1.41.0", - "resolved": "https://registry.npmjs.org/postcss-prefixwrap/-/postcss-prefixwrap-1.41.0.tgz", - "integrity": "sha512-gmwwAEE+ci3/ZKjUZppTETINlh1QwihY8gCstInuS7ohk353KYItU4d64hvnUvO2GUy29hBGPHz4Ce+qJRi90A==" + "postcss-prefix-selector": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/postcss-prefix-selector/-/postcss-prefix-selector-1.16.1.tgz", + "integrity": "sha512-Umxu+FvKMwlY6TyDzGFoSUnzW+NOfMBLyC1tAkIjgX+Z/qGspJeRjVC903D7mx7TuBpJlwti2ibXtWuA7fKMeQ==" }, "postcss-reduce-initial": { "version": "6.0.0", diff --git a/packages/block-editor/package.json b/packages/block-editor/package.json index aadf192f3016e..75c7b1d310700 100644 --- a/packages/block-editor/package.json +++ b/packages/block-editor/package.json @@ -73,7 +73,7 @@ "fast-deep-equal": "^3.1.3", "memize": "^2.1.0", "postcss": "^8.4.21", - "postcss-prefixwrap": "^1.41.0", + "postcss-prefix-selector": "^1.16.0", "postcss-urlrebase": "^1.4.0", "react-autosize-textarea": "^7.1.0", "react-easy-crop": "^5.0.6", diff --git a/packages/block-editor/src/utils/test/__snapshots__/transform-styles.js.snap b/packages/block-editor/src/utils/test/__snapshots__/transform-styles.js.snap index 9da07667d0a3e..30e63e0e96006 100644 --- a/packages/block-editor/src/utils/test/__snapshots__/transform-styles.js.snap +++ b/packages/block-editor/src/utils/test/__snapshots__/transform-styles.js.snap @@ -40,6 +40,12 @@ exports[`transformStyles selector wrap should ignore font-face selectors 1`] = ` ] `; +exports[`transformStyles selector wrap should ignore ignored selectors 1`] = ` +[ + ".my-namespace h1, body { color: red; }", +] +`; + exports[`transformStyles selector wrap should ignore keyframes 1`] = ` [ " @@ -51,18 +57,6 @@ exports[`transformStyles selector wrap should ignore keyframes 1`] = ` ] `; -exports[`transformStyles selector wrap should ignore selectors 1`] = ` -[ - ".my-namespace h1, body { color: red; }", -] -`; - -exports[`transformStyles selector wrap should not double wrap selectors 1`] = ` -[ - " .my-namespace h1, .my-namespace .red { color: red; }", -] -`; - exports[`transformStyles selector wrap should replace :root selectors 1`] = ` [ " @@ -72,7 +66,7 @@ exports[`transformStyles selector wrap should replace :root selectors 1`] = ` ] `; -exports[`transformStyles selector wrap should replace root tags 1`] = ` +exports[`transformStyles selector wrap should replace root selectors 1`] = ` [ ".my-namespace, .my-namespace h1 { color: red; }", ] diff --git a/packages/block-editor/src/utils/test/transform-styles.js b/packages/block-editor/src/utils/test/transform-styles.js index 8245ce6283107..f0d89711af341 100644 --- a/packages/block-editor/src/utils/test/transform-styles.js +++ b/packages/block-editor/src/utils/test/transform-styles.js @@ -96,7 +96,7 @@ describe( 'transformStyles', () => { expect( output ).toMatchSnapshot(); } ); - it( 'should ignore selectors', () => { + it( 'should ignore ignored selectors', () => { const input = `h1, body { color: red; }`; const output = transformStyles( [ @@ -111,7 +111,7 @@ describe( 'transformStyles', () => { expect( output ).toMatchSnapshot(); } ); - it( 'should replace root tags', () => { + it( 'should replace root selectors', () => { const input = `body, h1 { color: red; }`; const output = transformStyles( [ @@ -125,6 +125,21 @@ describe( 'transformStyles', () => { expect( output ).toMatchSnapshot(); } ); + it( `should not try to replace 'body' in the middle of a classname`, () => { + const prefix = '.my-namespace'; + const input = `.has-body-text { color: red; }`; + const output = transformStyles( + [ + { + css: input, + }, + ], + prefix + ); + + expect( output ).toEqual( [ `${ prefix } ${ input }` ] ); + } ); + it( 'should ignore keyframes', () => { const input = ` @keyframes edit-post__fade-in-animation { @@ -197,7 +212,7 @@ describe( 'transformStyles', () => { } ); it( 'should not double wrap selectors', () => { - const input = ` .my-namespace h1, .red { color: red; }`; + const input = ` .my-namespace h1, .my-namespace .red { color: red; }`; const output = transformStyles( [ @@ -208,7 +223,41 @@ describe( 'transformStyles', () => { '.my-namespace' ); - expect( output ).toMatchSnapshot(); + expect( output ).toEqual( [ input ] ); + } ); + + it( 'should not double prefix a root selector', () => { + const input = 'body .my-namespace h1 { color: goldenrod; }'; + + const output = transformStyles( + [ + { + css: input, + }, + ], + '.my-namespace' + ); + + expect( output ).toEqual( [ + '.my-namespace h1 { color: goldenrod; }', + ] ); + } ); + + it( 'should not try to wrap items within `:where` selectors', () => { + const input = `:where(.wp-element-button:active, .wp-block-button__link:active) { color: blue; }`; + const prefix = '.my-namespace'; + const expected = [ `${ prefix } ${ input }` ]; + + const output = transformStyles( + [ + { + css: input, + }, + ], + prefix + ); + + expect( output ).toEqual( expected ); } ); } ); diff --git a/packages/block-editor/src/utils/transform-styles/index.js b/packages/block-editor/src/utils/transform-styles/index.js index 9d57de3fa3833..9e5a6259abbc5 100644 --- a/packages/block-editor/src/utils/transform-styles/index.js +++ b/packages/block-editor/src/utils/transform-styles/index.js @@ -2,38 +2,67 @@ * External dependencies */ import postcss, { CssSyntaxError } from 'postcss'; -import wrap from 'postcss-prefixwrap'; +import prefixSelector from 'postcss-prefix-selector'; import rebaseUrl from 'postcss-urlrebase'; const cacheByWrapperSelector = new Map(); +// Ordering is important since `:root` would also match `:root :where(body)`. +const ROOT_SELECTOR_REGEX = + /^(:root :where\(body\)|:where\(body\)|:root|html|body)/; + +function replaceDoublePrefix( selector, prefix ) { + // Avoid prefixing an already prefixed selector. + const doublePrefix = `${ prefix } ${ prefix }`; + if ( selector.startsWith( doublePrefix ) ) { + return selector.replace( doublePrefix, prefix ); + } + return selector; +} + function transformStyle( { css, ignoredSelectors = [], baseURL }, wrapperSelector = '' ) { - // When there is no wrapper selector or base URL, there is no need + // When there is no wrapper selector and no base URL, there is no need // to transform the CSS. This is most cases because in the default // iframed editor, no wrapping is needed, and not many styles // provide a base URL. if ( ! wrapperSelector && ! baseURL ) { return css; } - const postcssFriendlyCSS = css - .replace( /:root :where\(body\)/g, 'body' ) - .replace( /:where\(body\)/g, 'body' ); try { return postcss( [ wrapperSelector && - wrap( wrapperSelector, { - ignoredSelectors: [ - ...ignoredSelectors, - wrapperSelector, - ], + prefixSelector( { + prefix: wrapperSelector, + exclude: [ ...ignoredSelectors, wrapperSelector ], + transform( prefix, selector, prefixedSelector ) { + // `html`, `body` and `:root` need some special handling since they + // generally cannot be prefixed with a class name and produce a valid + // selector. Instead we replace the whole root part of the selector. + if ( ROOT_SELECTOR_REGEX.test( selector ) ) { + const updatedRootSelector = selector.replace( + ROOT_SELECTOR_REGEX, + prefix + ); + + return replaceDoublePrefix( + updatedRootSelector, + prefix + ); + } + + return replaceDoublePrefix( + prefixedSelector, + prefix + ); + }, } ), baseURL && rebaseUrl( { rootUrl: baseURL } ), ].filter( Boolean ) - ).process( postcssFriendlyCSS, {} ).css; // use sync PostCSS API + ).process( css, {} ).css; // use sync PostCSS API } catch ( error ) { if ( error instanceof CssSyntaxError ) { // eslint-disable-next-line no-console