diff --git a/package-lock.json b/package-lock.json index ddfec3a5dddc6..fa81e2cadeed6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40551,6 +40551,12 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/parsel-js": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/parsel-js/-/parsel-js-1.1.2.tgz", + "integrity": "sha512-D66DG2nKx4Yoq66TMEyCUHlR2STGqO7vsBrX7tgyS9cfQyO6XD5JyzOiflwmWN6a4wbUAqpmHqmrxlTQVGZcbA==", + "license": "MIT" + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -41754,6 +41760,15 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true }, + "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": ">4 <9" + } + }, "node_modules/postcss-reduce-initial": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-6.0.0.tgz", @@ -52228,8 +52243,9 @@ "diff": "^4.0.2", "fast-deep-equal": "^3.1.3", "memize": "^2.1.0", + "parsel-js": "^1.1.2", "postcss": "^8.4.21", - "postcss-prefixwrap": "^1.51.0", + "postcss-prefix-selector": "^1.16.0", "postcss-urlrebase": "^1.4.0", "react-autosize-textarea": "^7.1.0", "react-easy-crop": "^5.0.6", @@ -52244,15 +52260,6 @@ "react-dom": "^18.0.0" } }, - "packages/block-editor/node_modules/postcss-prefixwrap": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/postcss-prefixwrap/-/postcss-prefixwrap-1.51.0.tgz", - "integrity": "sha512-PuP4md5zFSY921dUcLShwSLv2YyyDec0dK9/puXl/lu7ZNvJ1U59+ZEFRMS67xwfNg5nIIlPXnAycPJlhA/Isw==", - "license": "MIT", - "peerDependencies": { - "postcss": "*" - } - }, "packages/block-editor/node_modules/postcss-urlrebase": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/postcss-urlrebase/-/postcss-urlrebase-1.4.0.tgz", @@ -67255,19 +67262,15 @@ "diff": "^4.0.2", "fast-deep-equal": "^3.1.3", "memize": "^2.1.0", + "parsel-js": "^1.1.2", "postcss": "^8.4.21", - "postcss-prefixwrap": "^1.51.0", + "postcss-prefix-selector": "^1.16.0", "postcss-urlrebase": "^1.4.0", "react-autosize-textarea": "^7.1.0", "react-easy-crop": "^5.0.6", "remove-accents": "^0.5.0" }, "dependencies": { - "postcss-prefixwrap": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/postcss-prefixwrap/-/postcss-prefixwrap-1.51.0.tgz", - "integrity": "sha512-PuP4md5zFSY921dUcLShwSLv2YyyDec0dK9/puXl/lu7ZNvJ1U59+ZEFRMS67xwfNg5nIIlPXnAycPJlhA/Isw==" - }, "postcss-urlrebase": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/postcss-urlrebase/-/postcss-urlrebase-1.4.0.tgz", @@ -86950,6 +86953,11 @@ } } }, + "parsel-js": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/parsel-js/-/parsel-js-1.1.2.tgz", + "integrity": "sha512-D66DG2nKx4Yoq66TMEyCUHlR2STGqO7vsBrX7tgyS9cfQyO6XD5JyzOiflwmWN6a4wbUAqpmHqmrxlTQVGZcbA==" + }, "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -87826,6 +87834,11 @@ } } }, + "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", "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-6.0.0.tgz", diff --git a/packages/block-editor/README.md b/packages/block-editor/README.md index c798015804b3e..a474d1df7e56d 100644 --- a/packages/block-editor/README.md +++ b/packages/block-editor/README.md @@ -811,21 +811,12 @@ _Parameters_ - _styles_ `EditorStyle[]`: CSS rules. - _wrapperSelector_ `string`: Wrapper selector. +- _transformOptions_ `TransformOptions`: Additional options for style transformation. _Returns_ - `Array`: converted rules. -_Type Definition_ - -- _EditorStyle_ `Object` - -_Properties_ - -- _css_ `string`: the CSS block(s), as a single string. -- _baseURL_ `?string`: the base URL to be used as the reference when rewritting urls. -- _ignoredSelectors_ `?string[]`: the selectors not to wrap. - ### Typewriter Ensures that the text selection keeps the same vertical distance from the viewport during keyboard events within this component. The vertical distance can vary. It is the last clicked or scrolled to position. diff --git a/packages/block-editor/package.json b/packages/block-editor/package.json index 02765376e314b..7d2779c0395bc 100644 --- a/packages/block-editor/package.json +++ b/packages/block-editor/package.json @@ -72,8 +72,9 @@ "diff": "^4.0.2", "fast-deep-equal": "^3.1.3", "memize": "^2.1.0", + "parsel-js": "^1.1.2", "postcss": "^8.4.21", - "postcss-prefixwrap": "^1.51.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/components/block-canvas/index.js b/packages/block-editor/src/components/block-canvas/index.js index c6a47b7b5533c..9bad3ca22e95f 100644 --- a/packages/block-editor/src/components/block-canvas/index.js +++ b/packages/block-editor/src/components/block-canvas/index.js @@ -16,6 +16,13 @@ import { useMouseMoveTypingReset } from '../observe-typing'; import { useBlockSelectionClearer } from '../block-selection-clearer'; import { useBlockCommands } from '../use-block-commands'; +// EditorStyles is a memoized component, so avoid passing a new +// object reference on each render. +const EDITOR_STYLE_TRANSFORM_OPTIONS = { + // Don't transform selectors that already specify `.editor-styles-wrapper`. + ignoredSelectors: [ /\.editor-styles-wrapper/gi ], +}; + export function ExperimentalBlockCanvas( { shouldIframe = true, height = '300px', @@ -38,7 +45,8 @@ export function ExperimentalBlockCanvas( { > unlock( select( blockEditorStore ) ).getStyleOverrides(), [] @@ -88,14 +88,15 @@ function EditorStyles( { styles, scope } ) { return [ transformStyles( _styles.filter( ( style ) => style?.css ), - scope + scope, + transformOptions ), _styles .filter( ( style ) => style.__unstableType === 'svgs' ) .map( ( style ) => style.assets ) .join( '' ), ]; - }, [ styles, overrides, scope ] ); + }, [ styles, overrides, scope, transformOptions ] ); return ( <> diff --git a/packages/block-editor/src/hooks/duotone.js b/packages/block-editor/src/hooks/duotone.js index 0a1d630d00de5..51130ccb9ec2a 100644 --- a/packages/block-editor/src/hooks/duotone.js +++ b/packages/block-editor/src/hooks/duotone.js @@ -251,10 +251,6 @@ function useDuotoneStyles( { const selectors = duotoneSelector.split( ',' ); const selectorsScoped = selectors.map( ( selectorPart ) => { - // Extra .editor-styles-wrapper specificity is needed in the editor - // since we're not using inline styles to apply the filter. We need to - // override duotone applied by global styles and theme.json. - // Assuming the selector part is a subclass selector (not a tag name) // so we can prepend the filter id class. If we want to support elements // such as `img` or namespaces, we'll need to add a case for that here. diff --git a/packages/block-editor/src/hooks/style.js b/packages/block-editor/src/hooks/style.js index 4cd3335022454..2dcc19f424b2c 100644 --- a/packages/block-editor/src/hooks/style.js +++ b/packages/block-editor/src/hooks/style.js @@ -381,10 +381,7 @@ function useBlockProps( { name, style } ) { useBlockProps ) }`; - // The .editor-styles-wrapper selector is required on elements styles. As it is - // added to all other editor styles, not providing it causes reset and global - // styles to override element styles because of higher specificity. - const baseElementSelector = `.editor-styles-wrapper .${ blockElementsContainerIdentifier }`; + const baseElementSelector = `.${ blockElementsContainerIdentifier }`; const blockElementStyles = style?.elements; const styles = useMemo( () => { diff --git a/packages/block-editor/src/layouts/test/grid.js b/packages/block-editor/src/layouts/test/grid.js index 79257ec0200be..ab7b279aaae78 100644 --- a/packages/block-editor/src/layouts/test/grid.js +++ b/packages/block-editor/src/layouts/test/grid.js @@ -5,7 +5,7 @@ import grid from '../grid'; describe( 'getLayoutStyle', () => { it( 'should return only `grid-template-columns` and `container-type` properties if no non-default params are provided', () => { - const expected = `.editor-styles-wrapper .my-container { grid-template-columns: repeat(auto-fill, minmax(min(12rem, 100%), 1fr)); container-type: inline-size; }`; + const expected = `.my-container { grid-template-columns: repeat(auto-fill, minmax(min(12rem, 100%), 1fr)); container-type: inline-size; }`; const result = grid.getLayoutStyle( { selector: '.my-container', @@ -19,7 +19,7 @@ describe( 'getLayoutStyle', () => { expect( result ).toBe( expected ); } ); it( 'should return only `grid-template-columns` if columnCount property is provided', () => { - const expected = `.editor-styles-wrapper .my-container { grid-template-columns: repeat(3, minmax(0, 1fr)); }`; + const expected = `.my-container { grid-template-columns: repeat(3, minmax(0, 1fr)); }`; const result = grid.getLayoutStyle( { selector: '.my-container', diff --git a/packages/block-editor/src/layouts/test/utils.js b/packages/block-editor/src/layouts/test/utils.js index a2a3ac644d7ba..09cfef8f48f8f 100644 --- a/packages/block-editor/src/layouts/test/utils.js +++ b/packages/block-editor/src/layouts/test/utils.js @@ -37,7 +37,7 @@ const layoutDefinitions = { describe( 'getBlockGapCSS', () => { it( 'should output default blockGap rules', () => { const expected = - '.editor-styles-wrapper .my-container > * { margin-block-start: 0; margin-block-end: 0; }.editor-styles-wrapper .my-container > * + * { margin-block-start: 3em; margin-block-end: 0; }'; + '.my-container > * { margin-block-start: 0; margin-block-end: 0; }.my-container > * + * { margin-block-start: 3em; margin-block-end: 0; }'; const result = getBlockGapCSS( '.my-container', @@ -50,7 +50,7 @@ describe( 'getBlockGapCSS', () => { } ); it( 'should output flex blockGap rules', () => { - const expected = '.editor-styles-wrapper .my-container { gap: 3em; }'; + const expected = '.my-container { gap: 3em; }'; const result = getBlockGapCSS( '.my-container', @@ -97,7 +97,7 @@ describe( 'getBlockGapCSS', () => { } ); it( 'should treat a blockGap string containing 0 as a valid value', () => { - const expected = '.editor-styles-wrapper .my-container { gap: 0; }'; + const expected = '.my-container { gap: 0; }'; const result = getBlockGapCSS( '.my-container', @@ -113,21 +113,19 @@ describe( 'getBlockGapCSS', () => { describe( 'appendSelectors', () => { it( 'should append a subselector without an appended selector', () => { expect( appendSelectors( '.original-selector' ) ).toBe( - '.editor-styles-wrapper .original-selector' + '.original-selector' ); } ); it( 'should append a subselector to a single selector', () => { expect( appendSelectors( '.original-selector', '.appended' ) ).toBe( - '.editor-styles-wrapper .original-selector .appended' + '.original-selector .appended' ); } ); it( 'should append a subselector to multiple selectors', () => { expect( appendSelectors( '.first-selector,.second-selector', '.appended' ) - ).toBe( - '.editor-styles-wrapper .first-selector .appended,.editor-styles-wrapper .second-selector .appended' - ); + ).toBe( '.first-selector .appended,.second-selector .appended' ); } ); } ); diff --git a/packages/block-editor/src/layouts/utils.js b/packages/block-editor/src/layouts/utils.js index 30280b8906e7a..15a8d2cef8373 100644 --- a/packages/block-editor/src/layouts/utils.js +++ b/packages/block-editor/src/layouts/utils.js @@ -17,19 +17,11 @@ import { LAYOUT_DEFINITIONS } from './definitions'; * @return {string} - CSS selector. */ export function appendSelectors( selectors, append = '' ) { - // Ideally we shouldn't need the `.editor-styles-wrapper` increased specificity here - // The problem though is that we have a `.editor-styles-wrapper p { margin: reset; }` style - // it's used to reset the default margin added by wp-admin to paragraphs - // so we need this to be higher speficity otherwise, it won't be applied to paragraphs inside containers - // When the post editor is fully iframed, this extra classname could be removed. - return selectors .split( ',' ) .map( ( subselector ) => - `.editor-styles-wrapper ${ subselector }${ - append ? ` ${ append }` : '' - }` + `${ subselector }${ append ? ` ${ append }` : '' }` ) .join( ',' ); } 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 deleted file mode 100644 index 9da07667d0a3e..0000000000000 --- a/packages/block-editor/src/utils/test/__snapshots__/transform-styles.js.snap +++ /dev/null @@ -1,109 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`transformStyles URL rewrite should not replace absolute paths 1`] = ` -[ - "h1 { background: url(/images/test.png); }", -] -`; - -exports[`transformStyles URL rewrite should not replace remote paths 1`] = ` -[ - "h1 { background: url(http://wp.org/images/test.png); }", -] -`; - -exports[`transformStyles URL rewrite should replace complex relative paths 1`] = ` -[ - "h1 { background: url(http://wp-site.local/themes/gut/images/test.png); }", -] -`; - -exports[`transformStyles URL rewrite should rewrite relative paths 1`] = ` -[ - "h1 { background: url(http://wp-site.local/themes/gut/css/images/test.png); }", -] -`; - -exports[`transformStyles error handling should handle multiple instances of \`:root :where(body)\` 1`] = ` -[ - ".my-namespace { color: pink; } .my-namespace { color: orange; }", -] -`; - -exports[`transformStyles selector wrap should ignore font-face selectors 1`] = ` -[ - " - @font-face { - font-family: myFirstFont; - src: url(sansation_light.woff); - }", -] -`; - -exports[`transformStyles selector wrap should ignore keyframes 1`] = ` -[ - " - @keyframes edit-post__fade-in-animation { - from { - opacity: 0; - } - }", -] -`; - -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`] = ` -[ - " - .my-namespace { - --my-color: #ff0000; - }", -] -`; - -exports[`transformStyles selector wrap should replace root tags 1`] = ` -[ - ".my-namespace, .my-namespace h1 { color: red; }", -] -`; - -exports[`transformStyles selector wrap should wrap multiple selectors 1`] = ` -[ - ".my-namespace h1, .my-namespace h2 { color: red; }", -] -`; - -exports[`transformStyles selector wrap should wrap regular selectors 1`] = ` -[ - ".my-namespace h1 { color: red; }", -] -`; - -exports[`transformStyles selector wrap should wrap selectors inside container queries 1`] = ` -[ - " - @container (width > 400px) { - .my-namespace h1 { color: red; } - }", -] -`; - -exports[`transformStyles should not break with data urls 1`] = ` -[ - ".wp-block-group { - background-image: url("data:image/svg+xml,%3Csvg%3E.b%7Bclip-path:url(test);%7D%3C/svg%3E"); - color: red !important; - }", -] -`; diff --git a/packages/block-editor/src/utils/test/transform-styles.js b/packages/block-editor/src/utils/test/transform-styles.js index b0c6ca48deb35..3a68cc63c248e 100644 --- a/packages/block-editor/src/utils/test/transform-styles.js +++ b/packages/block-editor/src/utils/test/transform-styles.js @@ -62,14 +62,16 @@ describe( 'transformStyles', () => { ], '.my-namespace' ); + const expected = + ':root :where(body) .my-namespace { color: pink; } :root :where(body) .my-namespace { color: orange; }'; - expect( output ).toMatchSnapshot(); + expect( output ).toEqual( [ expected ] ); } ); } ); - describe( 'selector wrap', () => { - it( 'should wrap regular selectors', () => { - const input = `h1 { color: red; }`; + describe( 'root selectors', () => { + it( 'should append prefix after `:root :where(body)` selectors', () => { + const input = ':root :where(body) { color: red; }'; const output = transformStyles( [ { @@ -78,12 +80,13 @@ describe( 'transformStyles', () => { ], '.my-namespace' ); + const expected = ':root :where(body) .my-namespace { color: red; }'; - expect( output ).toMatchSnapshot(); + expect( output ).toEqual( [ expected ] ); } ); - it( 'should wrap multiple selectors', () => { - const input = `h1, h2 { color: red; }`; + it( 'should append prefix after `:where(body)` selectors', () => { + const input = ':where(body) { color: red; }'; const output = transformStyles( [ { @@ -92,27 +95,142 @@ describe( 'transformStyles', () => { ], '.my-namespace' ); + const expected = ':where(body) .my-namespace { color: red; }'; - expect( output ).toMatchSnapshot(); + expect( output ).toEqual( [ expected ] ); } ); - it( 'should ignore selectors', () => { - const input = `h1, body { color: red; }`; + it( 'should append prefix after body selectors', () => { + const input = `body { color: red; }`; + const output = transformStyles( + [ + { + css: input, + }, + ], + '.my-namespace' + ); + const expected = 'body .my-namespace { color: red; }'; + + expect( output ).toEqual( [ expected ] ); + } ); + + it( 'should append prefix after html selectors', () => { + const input = `html { color: red; }`; + const output = transformStyles( + [ + { + css: input, + }, + ], + '.my-namespace' + ); + const expected = 'html .my-namespace { color: red; }'; + + expect( output ).toEqual( [ expected ] ); + } ); + + it( 'should append prefix after html selectors, but before selectors that contain the word html', () => { + const input = `html [data-type="core/html"] .test { color: red; }`; + const output = transformStyles( + [ + { + css: input, + }, + ], + '.my-namespace' + ); + const expected = + 'html .my-namespace [data-type="core/html"] .test { color: red; }'; + + expect( output ).toEqual( [ expected ] ); + } ); + + it( 'should append prefix after :root selectors', () => { + const input = ':root { color: red; }'; + const output = transformStyles( + [ + { + css: input, + }, + ], + '.my-namespace' + ); + const expected = ':root .my-namespace { color: red; }'; + + expect( output ).toEqual( [ expected ] ); + } ); + + it( 'should append prefix after root selector when the selector contains a combinator without spaces around it', () => { + const input = ` + body> .some-style { color: red; } + body>.some-style { color: blue; } + body >.some-style { color: yellow; } + html body > .some-style { color: purple; } + html body.with-class+.some-style { color: silver; } + html body.with-class~.some-style { color: goldenrod; } + `; + const output = transformStyles( + [ + { + css: input, + }, + ], + '.my-namespace' + ); + const expected = ` + body .my-namespace>.some-style { color: red; } + body .my-namespace>.some-style { color: blue; } + body .my-namespace>.some-style { color: yellow; } + html body .my-namespace>.some-style { color: purple; } + html body.with-class .my-namespace+.some-style { color: silver; } + html body.with-class .my-namespace~.some-style { color: goldenrod; } + `; + + expect( output ).toEqual( [ expected ] ); + } ); + + it( 'appends after multiple root selectors', () => { + const input = ` + :root html[lang="th"] body { color: red; } + :root html[lang="th"] { color: orange; } + :root html[lang="th"] body .some-class { color: green; } + `; + const output = transformStyles( + [ + { + css: input, + }, + ], + '.my-namespace' + ); + const expected = ` + :root html[lang="th"] body .my-namespace { color: red; } + :root html[lang="th"] .my-namespace { color: orange; } + :root html[lang="th"] body .my-namespace .some-class { color: green; } + `; + + expect( output ).toEqual( [ expected ] ); + } ); + + it( 'should not double prefix a root selector', () => { + const input = 'body .my-namespace h1 { color: goldenrod; }'; const output = transformStyles( [ { css: input, - ignoredSelectors: [ 'body' ], }, ], '.my-namespace' ); - expect( output ).toMatchSnapshot(); + expect( output ).toEqual( [ input ] ); } ); + } ); - it( 'should replace root tags', () => { - const input = `body, h1 { color: red; }`; + describe( 'selector wrap', () => { + it( 'should wrap regular selectors', () => { + const input = `h1 { color: red; }`; const output = transformStyles( [ { @@ -121,23 +239,56 @@ describe( 'transformStyles', () => { ], '.my-namespace' ); + const expected = '.my-namespace h1 { color: red; }'; - expect( output ).toMatchSnapshot(); + expect( output ).toEqual( [ expected ] ); + } ); + + it( 'should wrap multiple selectors', () => { + const input = `h1, h2 { color: red; }`; + const output = transformStyles( + [ + { + css: input, + }, + ], + '.my-namespace' + ); + const expected = + '.my-namespace h1, .my-namespace h2 { color: red; }'; + + expect( output ).toEqual( [ expected ] ); + } ); + + it( 'should ignore ignored selectors', () => { + const input = `h1, body { color: red; }`; + const output = transformStyles( + [ + { + css: input, + ignoredSelectors: [ 'body' ], + }, + ], + '.my-namespace' + ); + const expected = '.my-namespace h1, body { color: red; }'; + + expect( output ).toEqual( [ expected ] ); } ); 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 input = '.has-body-text { color: red; }'; const output = transformStyles( [ { css: input, }, ], - prefix + '.my-namespace' ); + const expected = '.my-namespace .has-body-text { color: red; }'; - expect( output ).toEqual( [ `${ prefix } ${ input }` ] ); + expect( output ).toEqual( [ expected ] ); } ); it( 'should ignore keyframes', () => { @@ -156,7 +307,7 @@ describe( 'transformStyles', () => { '.my-namespace' ); - expect( output ).toMatchSnapshot(); + expect( output ).toEqual( [ input ] ); } ); it( 'should wrap selectors inside container queries', () => { @@ -172,8 +323,12 @@ describe( 'transformStyles', () => { ], '.my-namespace' ); + const expected = ` + @container (width > 400px) { + .my-namespace h1 { color: red; } + }`; - expect( output ).toMatchSnapshot(); + expect( output ).toEqual( [ expected ] ); } ); it( 'should ignore font-face selectors', () => { @@ -191,14 +346,11 @@ describe( 'transformStyles', () => { '.my-namespace' ); - expect( output ).toMatchSnapshot(); + expect( output ).toEqual( [ input ] ); } ); - it( 'should replace :root selectors', () => { - const input = ` - :root { - --my-color: #ff0000; - }`; + it( 'should not double wrap selectors', () => { + const input = ' .my-namespace h1, .red { color: red; }'; const output = transformStyles( [ { @@ -207,57 +359,111 @@ describe( 'transformStyles', () => { ], '.my-namespace' ); + const expected = ` .my-namespace h1, .my-namespace .red { color: red; }`; - expect( output ).toMatchSnapshot(); + expect( output ).toEqual( [ expected ] ); } ); - it( 'should not double wrap selectors', () => { - const input = ` .my-namespace h1, .red { color: red; }`; + it( 'should not double wrap selectors when the prefix can be misinterpreted as a regular expression', () => { + const input = + ' :where(.editor-styles-wrapper) h1, .red { color: red; }'; + const output = transformStyles( + [ + { + css: input, + }, + ], + ':where(.editor-styles-wrapper)' + ); + const expected = ` :where(.editor-styles-wrapper) h1, :where(.editor-styles-wrapper) .red { color: red; }`; + + expect( output ).toEqual( [ expected ] ); + } ); + it( 'should allow specification of ignoredSelectors per css input', () => { + const input = '.ignored { color: red; }'; const output = transformStyles( [ + { + css: input, + ignoredSelectors: [ '.ignored' ], + }, + { + css: input, + ignoredSelectors: [ /^\.ignored/ ], + }, { css: input, }, ], - '.my-namespace' + '.not' ); + const expected1 = input; + const expected2 = input; + const expected3 = '.not .ignored { color: red; }'; - expect( output ).toMatchSnapshot(); + expect( output ).toEqual( [ expected1, expected2, expected3 ] ); } ); - 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 }` ]; + it( 'allows specification of ignoredSelectors globally via the transformOptions param', () => { + const input1 = '.ignored { color: red; }'; + const input2 = '.modified { color: red; }'; + const input3 = '.regexed { color: red; }'; + const output = transformStyles( + [ + { + css: input1, + }, + { + css: input2, + }, + { + css: input3, + }, + ], + '.prefix', + { ignoredSelectors: [ '.ignored', /\.regexed/ ] } + ); + expect( output ).toEqual( [ + input1, + '.prefix .modified { color: red; }', + input3, + ] ); + } ); + + 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 output = transformStyles( [ { css: input, }, ], - prefix + '.my-namespace' ); + const expected = + '.my-namespace :where(.wp-element-button:active, .wp-block-button__link:active) { color: blue; }'; - expect( output ).toEqual( expected ); + expect( output ).toEqual( [ expected ] ); } ); it( 'should not try to prefix pseudo elements on `:where` selectors', () => { - const input = `:where(.wp-element-button, .wp-block-button__link)::before { color: blue; }`; - const prefix = '.my-namespace'; - const expected = [ `${ prefix } ${ input }` ]; - + const input = + ':where(.wp-element-button, .wp-block-button__link)::before { color: blue; }'; const output = transformStyles( [ { css: input, }, ], - prefix + '.my-namespace' ); + const expected = + '.my-namespace :where(.wp-element-button, .wp-block-button__link)::before { color: blue; }'; - expect( output ).toEqual( expected ); + expect( output ).toEqual( [ expected ] ); } ); } ); @@ -274,7 +480,7 @@ describe( 'transformStyles', () => { }, ] ); - expect( output ).toMatchSnapshot(); + expect( output ).toEqual( [ input ] ); } ); describe( 'URL rewrite', () => { @@ -286,8 +492,10 @@ describe( 'transformStyles', () => { baseURL: 'http://wp-site.local/themes/gut/css/', }, ] ); + const expected = + 'h1 { background: url(http://wp-site.local/themes/gut/css/images/test.png); }'; - expect( output ).toMatchSnapshot(); + expect( output ).toEqual( [ expected ] ); } ); it( 'should replace complex relative paths', () => { @@ -298,8 +506,10 @@ describe( 'transformStyles', () => { baseURL: 'http://wp-site.local/themes/gut/css/', }, ] ); + const expected = + 'h1 { background: url(http://wp-site.local/themes/gut/images/test.png); }'; - expect( output ).toMatchSnapshot(); + expect( output ).toEqual( [ expected ] ); } ); it( 'should not replace absolute paths', () => { @@ -310,8 +520,7 @@ describe( 'transformStyles', () => { baseURL: 'http://wp-site.local/themes/gut/css/', }, ] ); - - expect( output ).toMatchSnapshot(); + expect( output ).toEqual( [ input ] ); } ); it( 'should not replace remote paths', () => { @@ -323,7 +532,7 @@ describe( 'transformStyles', () => { }, ] ); - expect( output ).toMatchSnapshot(); + expect( output ).toEqual( [ input ] ); } ); } ); } ); diff --git a/packages/block-editor/src/utils/transform-styles/index.js b/packages/block-editor/src/utils/transform-styles/index.js index 9d57de3fa3833..936ebb016fa1b 100644 --- a/packages/block-editor/src/utils/transform-styles/index.js +++ b/packages/block-editor/src/utils/transform-styles/index.js @@ -1,39 +1,142 @@ /** * External dependencies */ +import * as parsel from 'parsel-js'; 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(); +const ROOT_SELECTOR_TOKENS = [ + { type: 'type', content: 'body' }, + { type: 'type', content: 'html' }, + { type: 'pseudo-class', content: ':root' }, + { type: 'pseudo-class', content: ':where(body)' }, + { type: 'pseudo-class', content: ':where(:root)' }, + { type: 'pseudo-class', content: ':where(html)' }, +]; + +/** + * Prefixes root selectors in a way that ensures consistent specificity. + * This requires special handling, since prefixing a classname before + * html, body, or :root will generally result in an invalid selector. + * + * Some libraries will simply replace the root selector with the prefix + * instead, but this results in inconsistent specificity. + * + * This function instead inserts the prefix after the root tags but before + * any other part of the selector. This results in consistent specificity: + * - If a `:where()` selector is used for the prefix, all selectors output + * by `transformStyles` will have no specificity increase. + * - If a classname, id, or something else is used as the prefix, all selectors + * will have the same specificity bump when transformed. + * + * @param {string} prefix The prefix. + * @param {string} selector The selector. + * + * @return {string} The prefixed root selector. + */ +function prefixRootSelector( prefix, selector ) { + // Use a tokenizer, since regular expressions are unreliable. + const tokenized = parsel.tokenize( selector ); + + // Find the last token that contains a root selector by walking back + // through the tokens. + const lastRootIndex = tokenized.findLastIndex( ( { content, type } ) => { + return ROOT_SELECTOR_TOKENS.some( + ( rootSelector ) => + content === rootSelector.content && type === rootSelector.type + ); + } ); + + // Walk forwards to find the combinator after the last root. + // This is where the root ends and the rest of the selector begins, + // and the index to insert before. + // Doing it this way takes into account that a root selector like + // 'body' may have additional id/class/pseudo-class/attribute-selector + // parts chained to it, which is difficult to quantify using a regex. + let insertionPoint = -1; + for ( let i = lastRootIndex + 1; i < tokenized.length; i++ ) { + if ( tokenized[ i ].type === 'combinator' ) { + insertionPoint = i; + break; + } + } + + // Tokenize and insert the prefix with a ' ' combinator before it. + const tokenizedPrefix = parsel.tokenize( prefix ); + tokenized.splice( + // Insert at the insertion point, or the end. + insertionPoint === -1 ? tokenized.length : insertionPoint, + 0, + { + type: 'combinator', + content: ' ', + }, + ...tokenizedPrefix + ); + + return parsel.stringify( tokenized ); +} + function transformStyle( { css, ignoredSelectors = [], baseURL }, - wrapperSelector = '' + wrapperSelector = '', + transformOptions ) { - // 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 { + const excludedSelectors = [ + ...ignoredSelectors, + ...( transformOptions?.ignoredSelectors ?? [] ), + wrapperSelector, + ]; + return postcss( [ wrapperSelector && - wrap( wrapperSelector, { - ignoredSelectors: [ - ...ignoredSelectors, - wrapperSelector, - ], + prefixSelector( { + prefix: wrapperSelector, + transform( prefix, selector, prefixedSelector ) { + // For backwards compatibility, don't use the `exclude` option + // of postcss-prefix-selector, instead handle it here to match + // the behavior of the old library (postcss-prefix-wrap) that + // `transformStyle` previously used. + if ( + excludedSelectors.some( ( excludedSelector ) => + excludedSelector instanceof RegExp + ? selector.match( excludedSelector ) + : selector.includes( excludedSelector ) + ) + ) { + return selector; + } + + const hasRootSelector = ROOT_SELECTOR_TOKENS.some( + ( rootSelector ) => + selector.startsWith( rootSelector.content ) + ); + + // Reorganize root selectors such that the root part comes before the prefix, + // but the prefix still comes before the remaining part of the selector. + if ( hasRootSelector ) { + return prefixRootSelector( prefix, selector ); + } + + return prefixedSelector; + }, } ), 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 @@ -54,18 +157,26 @@ function transformStyle( } /** - * Applies a series of CSS rule transforms to wrap selectors inside a given class and/or rewrite URLs depending on the parameters passed. - * * @typedef {Object} EditorStyle - * @property {string} css the CSS block(s), as a single string. - * @property {?string} baseURL the base URL to be used as the reference when rewritting urls. - * @property {?string[]} ignoredSelectors the selectors not to wrap. + * @property {string} css the CSS block(s), as a single string. + * @property {?string} baseURL the base URL to be used as the reference when rewritting urls. + * @property {?string[]} ignoredSelectors the selectors not to wrap. + */ + +/** + * @typedef {Object} TransformOptions + * @property {?string[]} ignoredSelectors the selectors not to wrap. + */ + +/** + * Applies a series of CSS rule transforms to wrap selectors inside a given class and/or rewrite URLs depending on the parameters passed. * - * @param {EditorStyle[]} styles CSS rules. - * @param {string} wrapperSelector Wrapper selector. + * @param {EditorStyle[]} styles CSS rules. + * @param {string} wrapperSelector Wrapper selector. + * @param {TransformOptions} transformOptions Additional options for style transformation. * @return {Array} converted rules. */ -const transformStyles = ( styles, wrapperSelector = '' ) => { +const transformStyles = ( styles, wrapperSelector = '', transformOptions ) => { let cache = cacheByWrapperSelector.get( wrapperSelector ); if ( ! cache ) { cache = new WeakMap(); @@ -74,7 +185,7 @@ const transformStyles = ( styles, wrapperSelector = '' ) => { return styles.map( ( style ) => { let css = cache.get( style ); if ( ! css ) { - css = transformStyle( style, wrapperSelector ); + css = transformStyle( style, wrapperSelector, transformOptions ); cache.set( style, css ); } return css; diff --git a/packages/block-library/src/button/editor.scss b/packages/block-library/src/button/editor.scss index 8c879c7e3b7c9..8531f810e59e2 100644 --- a/packages/block-library/src/button/editor.scss +++ b/packages/block-library/src/button/editor.scss @@ -32,8 +32,3 @@ div[data-type="core/button"] { display: table; } - -.editor-styles-wrapper .wp-block-button[style*="text-decoration"] .wp-block-button__link { - text-decoration: inherit; -} - diff --git a/packages/block-library/src/buttons/editor.scss b/packages/block-library/src/buttons/editor.scss index b97c915671ad9..1aa775fd1d648 100644 --- a/packages/block-library/src/buttons/editor.scss +++ b/packages/block-library/src/buttons/editor.scss @@ -52,12 +52,6 @@ $blocks-block__margin: 0.5em; margin-bottom: 0; } } - - .editor-styles-wrapper &.has-custom-font-size { - .wp-block-button__link { - font-size: inherit; - } - } } .wp-block[data-align="center"] > .wp-block-buttons { diff --git a/packages/block-library/src/comments-pagination/editor.scss b/packages/block-library/src/comments-pagination/editor.scss index dbf7e60ff65c0..a875c9e0ee21c 100644 --- a/packages/block-library/src/comments-pagination/editor.scss +++ b/packages/block-library/src/comments-pagination/editor.scss @@ -5,7 +5,7 @@ $pagination-margin: 0.5em; justify-content: center; } -.editor-styles-wrapper { +:where(.editor-styles-wrapper) { .wp-block-comments-pagination { max-width: 100%; &.block-editor-block-list__layout { diff --git a/packages/block-library/src/cover/editor.scss b/packages/block-library/src/cover/editor.scss index 189d658e955c9..b92c401311bee 100644 --- a/packages/block-library/src/cover/editor.scss +++ b/packages/block-library/src/cover/editor.scss @@ -1,9 +1,4 @@ .wp-block-cover { - /* Extra specificity needed because the reset.css applied in the editor context is overriding this rule. */ - .editor-styles-wrapper & { - box-sizing: border-box; - } - // Override default cover styles // because we're not ready yet to show the cover block. &.is-placeholder { diff --git a/packages/block-library/src/navigation/editor.scss b/packages/block-library/src/navigation/editor.scss index b279dc08cfe6e..fa09ebc538b97 100644 --- a/packages/block-library/src/navigation/editor.scss +++ b/packages/block-library/src/navigation/editor.scss @@ -5,6 +5,10 @@ // Undo default editor styles. // These need extra specificity. .editor-styles-wrapper .wp-block-navigation { + // Extra specificity is applied to do two things in classic themes: + // 1. Override the auto margin from alignment styles. This is needed as the `ul` element + // has a `wp-block` class applied to it, even though it's not a block. + // 2. Override list indentation that targets `ul,ol` elements. ul { margin-top: 0; margin-bottom: 0; @@ -478,7 +482,7 @@ $color-control-label-height: 20px; } } -// The iframe makes these rules a lot simpler. +// `body.editor-styles-wrapper` ensures this only applies to the iframed editor. body.editor-styles-wrapper .wp-block-navigation__responsive-container.is-menu-open { top: 0; right: 0; diff --git a/packages/block-library/src/query-pagination/editor.scss b/packages/block-library/src/query-pagination/editor.scss index 169f2b5ca72af..0b755d155091f 100644 --- a/packages/block-library/src/query-pagination/editor.scss +++ b/packages/block-library/src/query-pagination/editor.scss @@ -5,7 +5,7 @@ $pagination-margin: 0.5em; justify-content: center; } -.editor-styles-wrapper { +:where(.editor-styles-wrapper) { .wp-block-query-pagination { max-width: 100%; &.block-editor-block-list__layout { diff --git a/packages/block-library/src/site-title/editor.scss b/packages/block-library/src/site-title/editor.scss index ce83659170ff6..372a85db2f4b1 100644 --- a/packages/block-library/src/site-title/editor.scss +++ b/packages/block-library/src/site-title/editor.scss @@ -2,7 +2,3 @@ padding: 1em 0; border: 1px dashed; } - -.editor-styles-wrapper :where(.wp-block-site-title a) { - color: inherit; -} diff --git a/packages/edit-widgets/src/components/widget-areas-block-editor-content/index.js b/packages/edit-widgets/src/components/widget-areas-block-editor-content/index.js index a9024da1f0074..e1bd7a6b12f1e 100644 --- a/packages/edit-widgets/src/components/widget-areas-block-editor-content/index.js +++ b/packages/edit-widgets/src/components/widget-areas-block-editor-content/index.js @@ -45,7 +45,7 @@ export default function WidgetAreasBlockEditorContent( { diff --git a/test/unit/jest.config.js b/test/unit/jest.config.js index 4a3bb647a7879..30f9448539643 100644 --- a/test/unit/jest.config.js +++ b/test/unit/jest.config.js @@ -40,7 +40,7 @@ module.exports = { '^.+\\.[jt]sx?$': '/test/unit/scripts/babel-transformer.js', }, transformIgnorePatterns: [ - '/node_modules/(?!(docker-compose|yaml|preact|@preact)/)', + '/node_modules/(?!(docker-compose|yaml|preact|@preact|parsel-js)/)', '\\.pnp\\.[^\\/]+$', ], snapshotSerializers: [