From 127f7d4cf24a0e3664d80ed1a1f9ffafdf3b20ef Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 20 Oct 2023 10:09:56 +0200 Subject: [PATCH 01/14] Fix typo in FontLibraryModal (#55439) --- .../global-styles/font-library-modal/font-collection.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js b/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js index 61de80838f6372..08ff1a95d6e41a 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js @@ -244,7 +244,7 @@ function FontCollection( { id } ) { ! fonts.length && ( { __( - 'No fonts found. Try with a different seach term' + 'No fonts found. Try with a different search term' ) } ) } From 6a2c92b70b17d6a3361de92b39884e7d309ab546 Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+c4rl0sbr4v0@users.noreply.github.com> Date: Fri, 20 Oct 2023 11:01:00 +0200 Subject: [PATCH 02/14] Fix: - Update the title when using enhanced pagination. (#55446) * Add title update when using enhanced pagination * Simplify code * Use innertext to prevent character encoding * Add changelog * Add e2e test * Fix code review recommendations --- packages/interactivity/CHANGELOG.md | 4 ++++ packages/interactivity/src/router.js | 7 +++++-- .../interactivity/fixtures/interactivity-utils.ts | 1 + .../e2e/specs/interactivity/router-regions.spec.ts | 14 ++++++++++++++ 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/packages/interactivity/CHANGELOG.md b/packages/interactivity/CHANGELOG.md index 2e1a6b8683c7fa..a7f3180b2172ab 100644 --- a/packages/interactivity/CHANGELOG.md +++ b/packages/interactivity/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Bug Fix + +- Update the title when using enhanced pagination. ([#55446](https://github.com/WordPress/gutenberg/pull/55446)) + ## 2.5.0 (2023-10-18) ## 2.4.0 (2023-10-05) diff --git a/packages/interactivity/src/router.js b/packages/interactivity/src/router.js index ee9755126994e3..68d1bc677addf3 100644 --- a/packages/interactivity/src/router.js +++ b/packages/interactivity/src/router.js @@ -55,8 +55,8 @@ const regionsToVdom = ( dom ) => { const id = region.getAttribute( attrName ); regions[ id ] = toVdom( region ); } ); - - return { regions }; + const title = dom.querySelector( 'title' )?.innerText; + return { regions, title }; }; // Prefetch a page. We store the promise to avoid triggering a second fetch for @@ -76,6 +76,9 @@ const renderRegions = ( page ) => { const fragment = getRegionRootFragment( region ); render( page.regions[ id ], fragment ); } ); + if ( page.title ) { + document.title = page.title; + } }; // Variable to store the current navigation. diff --git a/test/e2e/specs/interactivity/fixtures/interactivity-utils.ts b/test/e2e/specs/interactivity/fixtures/interactivity-utils.ts index fc0dc4b30d664e..44507ad34813e8 100644 --- a/test/e2e/specs/interactivity/fixtures/interactivity-utils.ts +++ b/test/e2e/specs/interactivity/fixtures/interactivity-utils.ts @@ -47,6 +47,7 @@ export default class InteractivityUtils { content: ``, status: 'publish' as 'publish', date_gmt: '2023-01-01T00:00:00', + title: alias, }; const { link } = await this.requestUtils.createPost( payload ); diff --git a/test/e2e/specs/interactivity/router-regions.spec.ts b/test/e2e/specs/interactivity/router-regions.spec.ts index cbe66b7bd1b217..f1ea308d6c2563 100644 --- a/test/e2e/specs/interactivity/router-regions.spec.ts +++ b/test/e2e/specs/interactivity/router-regions.spec.ts @@ -97,4 +97,18 @@ test.describe( 'Router regions', () => { await page.getByTestId( 'back' ).click(); await expect( nestedRegionSsr ).toHaveText( 'content from page 1' ); } ); + + test( 'Page title is updated 2', async ( { page } ) => { + await expect( page ).toHaveTitle( + 'router regions – page 1 – gutenberg' + ); + await page.getByTestId( 'next' ).click(); + await expect( page ).toHaveTitle( + 'router regions – page 2 – gutenberg' + ); + await page.getByTestId( 'back' ).click(); + await expect( page ).toHaveTitle( + 'router regions – page 1 – gutenberg' + ); + } ); } ); From 5b10d27ec406cbdec84e14408cd350ba9e8c544a Mon Sep 17 00:00:00 2001 From: Glen Davies Date: Fri, 20 Oct 2023 22:11:15 +1300 Subject: [PATCH 03/14] Patterns: fix capabilities settings for pattern categories (#55379) Co-authored-by: Daniel Richards --- lib/compat/wordpress-6.4/block-patterns.php | 23 ++-- lib/compat/wordpress-6.4/blocks.php | 24 ++++ ...s-gutenberg-rest-blocks-controller-6-4.php | 75 +++++++++++ ...erg-rest-pattern-categories-controller.php | 45 +++++++ lib/load.php | 2 + packages/core-data/src/resolvers.js | 1 + packages/editor/src/components/index.js | 1 + .../pattern-categories-selector.js | 119 ++++++++++++++++++ .../src/components/category-editor.js | 97 ++++++++++++++ .../src/components/category-selector.js | 89 ++++++------- .../src/components/create-pattern-modal.js | 75 ++++++----- .../rename-pattern-category-modal.js | 2 +- packages/patterns/src/components/style.scss | 23 ++++ packages/patterns/src/private-apis.js | 3 + 14 files changed, 483 insertions(+), 96 deletions(-) create mode 100644 lib/compat/wordpress-6.4/class-gutenberg-rest-blocks-controller-6-4.php create mode 100644 lib/compat/wordpress-6.4/class-gutenberg-rest-pattern-categories-controller.php create mode 100644 packages/editor/src/components/post-taxonomies/pattern-categories-selector.js create mode 100644 packages/patterns/src/components/category-editor.js diff --git a/lib/compat/wordpress-6.4/block-patterns.php b/lib/compat/wordpress-6.4/block-patterns.php index 922dea910b47a0..bbb910ff400d01 100644 --- a/lib/compat/wordpress-6.4/block-patterns.php +++ b/lib/compat/wordpress-6.4/block-patterns.php @@ -16,20 +16,21 @@ */ function gutenberg_register_taxonomy_patterns() { $args = array( - 'public' => true, - 'publicly_queryable' => false, - 'hierarchical' => false, - 'labels' => array( + 'public' => true, + 'publicly_queryable' => false, + 'hierarchical' => false, + 'labels' => array( 'name' => _x( 'Pattern Categories', 'taxonomy general name' ), 'singular_name' => _x( 'Pattern Category', 'taxonomy singular name' ), ), - 'query_var' => false, - 'rewrite' => false, - 'show_ui' => true, - '_builtin' => true, - 'show_in_nav_menus' => false, - 'show_in_rest' => true, - 'show_admin_column' => true, + 'query_var' => false, + 'rewrite' => false, + 'show_ui' => true, + '_builtin' => true, + 'show_in_nav_menus' => false, + 'show_in_rest' => true, + 'show_admin_column' => true, + 'rest_controller_class' => 'Gutenberg_REST_Pattern_Categories_Controller', ); register_taxonomy( 'wp_pattern_category', array( 'wp_block' ), $args ); } diff --git a/lib/compat/wordpress-6.4/blocks.php b/lib/compat/wordpress-6.4/blocks.php index 74fa9253e45d50..073302dbab65f8 100644 --- a/lib/compat/wordpress-6.4/blocks.php +++ b/lib/compat/wordpress-6.4/blocks.php @@ -21,3 +21,27 @@ function gutenberg_add_custom_capabilities_to_wp_block( $args ) { return $args; } add_filter( 'register_wp_block_post_type_args', 'gutenberg_add_custom_capabilities_to_wp_block', 10, 1 ); + +/** + * Updates the wp_block REST enpoint in order to modify the wp_pattern_category action + * links that are returned because as although the taxonomy is flat Author level users + * are only allowed to assign categories. + * + * Note: This should be removed when the minimum required WP version is >= 6.4. + * + * @see https://github.com/WordPress/gutenberg/pull/55379 + * + * @param array $args Register post type args. + * @param string $post_type The post type string. + * + * @return array Register post type args. + */ +function gutenberg_update_patterns_block_rest_controller_class( $args, $post_type ) { + if ( 'wp_block' === $post_type ) { + $args['rest_controller_class'] = 'Gutenberg_REST_Blocks_Controller_6_4'; + } + + return $args; +} + +add_filter( 'register_post_type_args', 'gutenberg_update_patterns_block_rest_controller_class', 11, 2 ); diff --git a/lib/compat/wordpress-6.4/class-gutenberg-rest-blocks-controller-6-4.php b/lib/compat/wordpress-6.4/class-gutenberg-rest-blocks-controller-6-4.php new file mode 100644 index 00000000000000..bc91492e269791 --- /dev/null +++ b/lib/compat/wordpress-6.4/class-gutenberg-rest-blocks-controller-6-4.php @@ -0,0 +1,75 @@ +post_type ); + + if ( 'attachment' !== $this->post_type && current_user_can( $post_type->cap->publish_posts ) ) { + $rels[] = 'https://api.w.org/action-publish'; + } + + if ( current_user_can( 'unfiltered_html' ) ) { + $rels[] = 'https://api.w.org/action-unfiltered-html'; + } + + if ( 'post' === $post_type->name ) { + if ( current_user_can( $post_type->cap->edit_others_posts ) && current_user_can( $post_type->cap->publish_posts ) ) { + $rels[] = 'https://api.w.org/action-sticky'; + } + } + + if ( post_type_supports( $post_type->name, 'author' ) ) { + if ( current_user_can( $post_type->cap->edit_others_posts ) ) { + $rels[] = 'https://api.w.org/action-assign-author'; + } + } + + $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) ); + + foreach ( $taxonomies as $tax ) { + $tax_base = ! empty( $tax->rest_base ) ? $tax->rest_base : $tax->name; + + if ( current_user_can( $tax->cap->edit_terms ) ) { + $rels[] = 'https://api.w.org/action-create-' . $tax_base; + } + + if ( current_user_can( $tax->cap->assign_terms ) ) { + $rels[] = 'https://api.w.org/action-assign-' . $tax_base; + } + } + + return $rels; + } +} diff --git a/lib/compat/wordpress-6.4/class-gutenberg-rest-pattern-categories-controller.php b/lib/compat/wordpress-6.4/class-gutenberg-rest-pattern-categories-controller.php new file mode 100644 index 00000000000000..e249d67e8acaa4 --- /dev/null +++ b/lib/compat/wordpress-6.4/class-gutenberg-rest-pattern-categories-controller.php @@ -0,0 +1,45 @@ +check_is_taxonomy_allowed( $this->taxonomy ) ) { + return false; + } + + $taxonomy_obj = get_taxonomy( $this->taxonomy ); + + // Patterns categories are a flat hierarchy (like tags), but work more like post categories in terms of permissions. + if ( ! current_user_can( $taxonomy_obj->cap->edit_terms ) ) { + return new WP_Error( + 'rest_cannot_create', + __( 'Sorry, you are not allowed to create terms in this taxonomy.' ), + array( 'status' => rest_authorization_required_code() ) + ); + } + + return true; + } +} diff --git a/lib/load.php b/lib/load.php index 2b178af5fb9bfe..381248e0f44bf0 100644 --- a/lib/load.php +++ b/lib/load.php @@ -53,6 +53,8 @@ function gutenberg_is_experiment_enabled( $name ) { // WordPress 6.4 compat. require_once __DIR__ . '/compat/wordpress-6.4/class-gutenberg-rest-global-styles-revisions-controller-6-4.php'; require_once __DIR__ . '/compat/wordpress-6.4/class-gutenberg-rest-block-patterns-controller.php'; + require_once __DIR__ . '/compat/wordpress-6.4/class-gutenberg-rest-blocks-controller-6-4.php'; + require_once __DIR__ . '/compat/wordpress-6.4/class-gutenberg-rest-pattern-categories-controller.php'; require_once __DIR__ . '/compat/wordpress-6.4/rest-api.php'; require_once __DIR__ . '/compat/wordpress-6.4/theme-previews.php'; diff --git a/packages/core-data/src/resolvers.js b/packages/core-data/src/resolvers.js index 07e9cd98cb5ec3..5fc7cd14f35c0b 100644 --- a/packages/core-data/src/resolvers.js +++ b/packages/core-data/src/resolvers.js @@ -651,6 +651,7 @@ export const getUserPatternCategories = { per_page: -1, _fields: 'id,name,description,slug', + context: 'view', } ); diff --git a/packages/editor/src/components/index.js b/packages/editor/src/components/index.js index 39b562806c109a..99de8c83c6fbe1 100644 --- a/packages/editor/src/components/index.js +++ b/packages/editor/src/components/index.js @@ -62,6 +62,7 @@ export { export { default as PostTaxonomies } from './post-taxonomies'; export { FlatTermSelector as PostTaxonomiesFlatTermSelector } from './post-taxonomies/flat-term-selector'; export { HierarchicalTermSelector as PostTaxonomiesHierarchicalTermSelector } from './post-taxonomies/hierarchical-term-selector'; +export { PatternCategoriesSelector as PostPatternCategoriesSelector } from './post-taxonomies/pattern-categories-selector'; export { default as PostTaxonomiesCheck } from './post-taxonomies/check'; export { default as PostTextEditor } from './post-text-editor'; export { default as PostTitle } from './post-title'; diff --git a/packages/editor/src/components/post-taxonomies/pattern-categories-selector.js b/packages/editor/src/components/post-taxonomies/pattern-categories-selector.js new file mode 100644 index 00000000000000..ac6a60aa009349 --- /dev/null +++ b/packages/editor/src/components/post-taxonomies/pattern-categories-selector.js @@ -0,0 +1,119 @@ +/** + * WordPress dependencies + */ +import { useSelect, useDispatch } from '@wordpress/data'; +import { store as coreStore } from '@wordpress/core-data'; +import { addFilter } from '@wordpress/hooks'; +import { privateApis as patternsPrivateApis } from '@wordpress/patterns'; + +/** + * Internal dependencies + */ +import { unlock } from '../../lock-unlock'; +import { store as editorStore } from '../../store'; + +const { CategorySelector } = unlock( patternsPrivateApis ); + +const EMPTY_ARRAY = []; + +const DEFAULT_QUERY = { + per_page: -1, + orderby: 'name', + order: 'asc', + _fields: 'id,name,parent', + context: 'view', +}; + +/* + * Pattern categories are a flat taxonomy but do not allow Author users and below to create + * new categories, so this selector overrides the default flat taxonomy selector for + * wp_block post types and users without 'create' capability for wp_pattern_category. + */ +export function PatternCategoriesSelector( { slug } ) { + const { hasAssignAction, terms, availableTerms, taxonomy, loading } = + useSelect( + ( select ) => { + const { getCurrentPost, getEditedPostAttribute } = + select( editorStore ); + const { getTaxonomy, getEntityRecords, isResolving } = + select( coreStore ); + const _taxonomy = getTaxonomy( slug ); + const post = getCurrentPost(); + + return { + hasAssignAction: _taxonomy + ? post._links?.[ + 'wp:action-assign-' + _taxonomy.rest_base + ] ?? false + : false, + terms: _taxonomy + ? getEditedPostAttribute( _taxonomy.rest_base ) + : EMPTY_ARRAY, + loading: isResolving( 'getEntityRecords', [ + 'taxonomy', + slug, + DEFAULT_QUERY, + ] ), + availableTerms: + getEntityRecords( 'taxonomy', slug, DEFAULT_QUERY ) || + EMPTY_ARRAY, + taxonomy: _taxonomy, + }; + }, + [ slug ] + ); + + const { editPost } = useDispatch( editorStore ); + + if ( ! hasAssignAction || loading || availableTerms.length === 0 ) { + return null; + } + + const onUpdateTerms = ( termIds ) => { + editPost( { [ taxonomy.rest_base ]: termIds } ); + }; + + const onChange = ( term ) => { + const hasTerm = terms.includes( term.id ); + const newTerms = hasTerm + ? terms.filter( ( id ) => id !== term.id ) + : [ ...terms, term.id ]; + onUpdateTerms( newTerms ); + }; + + const isCategorySelected = ( term ) => terms.includes( term.id ); + + const categoryOptions = availableTerms.map( ( term ) => ( { + ...term, + label: term.name, + } ) ); + + return ( + + ); +} + +export default function patternCategorySelector( OriginalComponent ) { + return function ( props ) { + const canAddCategories = useSelect( ( select ) => { + const { canUser } = select( coreStore ); + return canUser( 'create', 'wp_pattern_category' ); + } ); + if ( props.slug === 'wp_pattern_category' && ! canAddCategories ) { + return ; + } + + return ; + }; +} + +addFilter( + 'editor.PostTaxonomyType', + 'core/pattern-category-selector', + patternCategorySelector +); diff --git a/packages/patterns/src/components/category-editor.js b/packages/patterns/src/components/category-editor.js new file mode 100644 index 00000000000000..a394013af333b2 --- /dev/null +++ b/packages/patterns/src/components/category-editor.js @@ -0,0 +1,97 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { useMemo, useState } from '@wordpress/element'; +import { FormTokenField } from '@wordpress/components'; +import { useDebounce } from '@wordpress/compose'; +import { decodeEntities } from '@wordpress/html-entities'; + +/** + * Internal dependencies + */ +import CategorySelector from './category-selector'; + +const unescapeString = ( arg ) => decodeEntities( arg ); + +export const CATEGORY_SLUG = 'wp_pattern_category'; + +export default function CategoryEditor( { + categoryTerms, + onChange, + categoryMap, + canAddCategories, +} ) { + const categoryOptions = Array.from( categoryMap.values() ); + const [ search, setSearch ] = useState( '' ); + const debouncedSearch = useDebounce( setSearch, 500 ); + + const suggestions = useMemo( () => { + return Array.from( categoryMap.values() ) + .map( ( category ) => unescapeString( category.label ) ) + .filter( ( category ) => { + if ( search !== '' ) { + return category + .toLowerCase() + .includes( search.toLowerCase() ); + } + return true; + } ) + .sort( ( a, b ) => a.localeCompare( b ) ); + }, [ search, categoryMap ] ); + + function handleChange( termNames ) { + const uniqueTerms = termNames.reduce( ( terms, newTerm ) => { + if ( + ! terms.some( + ( term ) => term.toLowerCase() === newTerm.toLowerCase() + ) + ) { + terms.push( newTerm ); + } + return terms; + }, [] ); + + onChange( uniqueTerms ); + } + const isCategorySelected = ( selectedCategory ) => + categoryTerms.includes( selectedCategory.label ); + + const onCategorySelectChange = ( selectedCategory ) => { + if ( categoryTerms.includes( selectedCategory.label ) ) { + onChange( + categoryTerms.filter( + ( categoryTerm ) => categoryTerm !== selectedCategory.label + ) + ); + } else { + onChange( [ ...categoryTerms, selectedCategory.label ] ); + } + }; + + return ( + <> + { canAddCategories && ( + + ) } + { ! canAddCategories && categoryOptions.length > 0 && ( + + ) } + + ); +} diff --git a/packages/patterns/src/components/category-selector.js b/packages/patterns/src/components/category-selector.js index 7f00350e278ecf..84dae97e232f89 100644 --- a/packages/patterns/src/components/category-selector.js +++ b/packages/patterns/src/components/category-selector.js @@ -2,65 +2,48 @@ * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { useMemo, useState } from '@wordpress/element'; -import { FormTokenField } from '@wordpress/components'; -import { useDebounce } from '@wordpress/compose'; +import { CheckboxControl, BaseControl } from '@wordpress/components'; import { decodeEntities } from '@wordpress/html-entities'; -const unescapeString = ( arg ) => { - return decodeEntities( arg ); -}; - -export const CATEGORY_SLUG = 'wp_pattern_category'; - export default function CategorySelector( { - categoryTerms, onChange, - categoryMap, + isCategorySelected, + categoryOptions, + showLabel = true, } ) { - const [ search, setSearch ] = useState( '' ); - const debouncedSearch = useDebounce( setSearch, 500 ); - - const suggestions = useMemo( () => { - return Array.from( categoryMap.values() ) - .map( ( category ) => unescapeString( category.label ) ) - .filter( ( category ) => { - if ( search !== '' ) { - return category - .toLowerCase() - .includes( search.toLowerCase() ); - } - return true; - } ) - .sort( ( a, b ) => a.localeCompare( b ) ); - }, [ search, categoryMap ] ); - - function handleChange( termNames ) { - const uniqueTerms = termNames.reduce( ( terms, newTerm ) => { - if ( - ! terms.some( - ( term ) => term.toLowerCase() === newTerm.toLowerCase() - ) - ) { - terms.push( newTerm ); - } - return terms; - }, [] ); - - onChange( uniqueTerms ); - } + const renderTerms = ( renderedTerms ) => { + return renderedTerms.map( ( category ) => { + return ( +
+ onChange( category ) } + label={ decodeEntities( category.label ) } + /> +
+ ); + } ); + }; return ( - + + { showLabel && ( + + { __( 'Categories' ) } + + ) } +
+ { renderTerms( categoryOptions ) } +
+
); } diff --git a/packages/patterns/src/components/create-pattern-modal.js b/packages/patterns/src/components/create-pattern-modal.js index 22d20fd0372657..67953ecdee6685 100644 --- a/packages/patterns/src/components/create-pattern-modal.js +++ b/packages/patterns/src/components/create-pattern-modal.js @@ -24,7 +24,7 @@ import { PATTERN_DEFAULT_CATEGORY, PATTERN_SYNC_TYPES } from '../constants'; * Internal dependencies */ import { store as patternsStore } from '../store'; -import CategorySelector, { CATEGORY_SLUG } from './category-selector'; +import CategoryEditor, { CATEGORY_SLUG } from './category-editor'; import { unlock } from '../lock-unlock'; export default function CreatePatternModal( { @@ -48,41 +48,46 @@ export default function CreatePatternModal( { const { saveEntityRecord, invalidateResolution } = useDispatch( coreStore ); const { createErrorNotice } = useDispatch( noticesStore ); - const { corePatternCategories, userPatternCategories } = useSelect( - ( select ) => { - const { getUserPatternCategories, getBlockPatternCategories } = - select( coreStore ); + const { corePatternCategories, userPatternCategories, canAddCategories } = + useSelect( ( select ) => { + const { + getUserPatternCategories, + getBlockPatternCategories, + canUser, + } = select( coreStore ); return { corePatternCategories: getBlockPatternCategories(), userPatternCategories: getUserPatternCategories(), + canAddCategories: canUser( 'create', 'wp_pattern_category' ), }; - } - ); + } ); const categoryMap = useMemo( () => { // Merge the user and core pattern categories and remove any duplicates. const uniqueCategories = new Map(); - [ ...userPatternCategories, ...corePatternCategories ].forEach( - ( category ) => { - if ( - ! uniqueCategories.has( category.label ) && - // There are two core categories with `Post` label so explicitly remove the one with - // the `query` slug to avoid any confusion. - category.name !== 'query' - ) { - // We need to store the name separately as this is used as the slug in the - // taxonomy and may vary from the label. - uniqueCategories.set( category.label, { - label: category.label, - value: category.label, - name: category.name, - } ); - } + [ + ...userPatternCategories, + ...( canAddCategories ? corePatternCategories : [] ), + ].forEach( ( category ) => { + if ( + ! uniqueCategories.has( category.label ) && + // There are two core categories with `Post` label so explicitly remove the one with + // the `query` slug to avoid any confusion. + category.name !== 'query' + ) { + // We need to store the name separately as this is used as the slug in the + // taxonomy and may vary from the label. + uniqueCategories.set( category.label, { + label: category.label, + value: category.label, + name: category.name, + id: category.id, + } ); } - ); + } ); return uniqueCategories; - }, [ userPatternCategories, corePatternCategories ] ); + }, [ userPatternCategories, corePatternCategories, canAddCategories ] ); async function onCreate( patternTitle, sync ) { if ( ! title || isSaving ) { @@ -91,11 +96,18 @@ export default function CreatePatternModal( { try { setIsSaving( true ); - const categories = await Promise.all( - categoryTerms.map( ( termName ) => - findOrCreateTerm( termName ) - ) - ); + let categories; + if ( canAddCategories ) { + categories = await Promise.all( + categoryTerms.map( ( termName ) => + findOrCreateTerm( termName ) + ) + ); + } else { + categories = categoryTerms.map( + ( term ) => categoryMap.get( term ).id + ); + } const newPattern = await createPattern( patternTitle, @@ -173,10 +185,11 @@ export default function CreatePatternModal( { placeholder={ __( 'My pattern' ) } className="patterns-create-modal__name-input" /> - Date: Fri, 20 Oct 2023 11:59:29 +0200 Subject: [PATCH 04/14] [RNMobile] Classic block: Add option to convert to blocks (#55461) * Add option to convert Classic block to blocks in Missing block * Add integration tests for Classic block * Update `react-native-editor` changelog --- .../test/__snapshots__/index.native.js.snap | 7 +++ .../src/freeform/test/index.native.js | 57 +++++++++++++++++++ .../block-library/src/missing/edit.native.js | 49 ++++++++++++++-- packages/react-native-editor/CHANGELOG.md | 1 + 4 files changed, 108 insertions(+), 6 deletions(-) create mode 100644 packages/block-library/src/freeform/test/__snapshots__/index.native.js.snap create mode 100644 packages/block-library/src/freeform/test/index.native.js diff --git a/packages/block-library/src/freeform/test/__snapshots__/index.native.js.snap b/packages/block-library/src/freeform/test/__snapshots__/index.native.js.snap new file mode 100644 index 00000000000000..aad87487b93bec --- /dev/null +++ b/packages/block-library/src/freeform/test/__snapshots__/index.native.js.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Classic block converts content into blocks 1`] = ` +" +

I'm classic!

+" +`; diff --git a/packages/block-library/src/freeform/test/index.native.js b/packages/block-library/src/freeform/test/index.native.js new file mode 100644 index 00000000000000..08ba635d4cd777 --- /dev/null +++ b/packages/block-library/src/freeform/test/index.native.js @@ -0,0 +1,57 @@ +/** + * External dependencies + */ +import { + fireEvent, + getBlock, + getEditorHtml, + initializeEditor, + screen, + setupCoreBlocks, + within, +} from 'test/helpers'; + +const CLASSIC_BLOCK_HTML = `

I'm classic!

`; +const DEFAULT_EDITOR_CAPABILITIES = { + unsupportedBlockEditor: true, + canEnableUnsupportedBlockEditor: true, +}; + +setupCoreBlocks(); + +describe( 'Classic block', () => { + it( 'displays option to edit using web editor', async () => { + await initializeEditor( { + initialHtml: CLASSIC_BLOCK_HTML, + capabilities: DEFAULT_EDITOR_CAPABILITIES, + } ); + + const block = getBlock( screen, 'Classic' ); + fireEvent.press( block ); + + // Tap the block to open the unsupported block details + fireEvent.press( within( block ).getByText( 'Unsupported' ) ); + + const actionButton = screen.getByText( 'Edit using web editor' ); + expect( actionButton ).toBeVisible(); + } ); + + it( 'converts content into blocks', async () => { + await initializeEditor( { + initialHtml: CLASSIC_BLOCK_HTML, + capabilities: DEFAULT_EDITOR_CAPABILITIES, + } ); + + const block = getBlock( screen, 'Classic' ); + fireEvent.press( block ); + + // Tap the block to open the unsupported block details + fireEvent.press( within( block ).getByText( 'Unsupported' ) ); + + const actionButton = screen.getByText( 'Convert to blocks' ); + expect( actionButton ).toBeVisible(); + + fireEvent.press( actionButton ); + expect( getEditorHtml() ).toMatchSnapshot(); + } ); +} ); diff --git a/packages/block-library/src/missing/edit.native.js b/packages/block-library/src/missing/edit.native.js index 8aa4738aeea85d..a6164f590ca21d 100644 --- a/packages/block-library/src/missing/edit.native.js +++ b/packages/block-library/src/missing/edit.native.js @@ -14,7 +14,7 @@ import { import { Icon } from '@wordpress/components'; import { compose, withPreferredColorScheme } from '@wordpress/compose'; import { coreBlocks } from '@wordpress/block-library'; -import { normalizeIconObject } from '@wordpress/blocks'; +import { normalizeIconObject, rawHandler, serialize } from '@wordpress/blocks'; import { Component } from '@wordpress/element'; import { __, _x, sprintf } from '@wordpress/i18n'; import { help, plugins } from '@wordpress/icons'; @@ -24,6 +24,7 @@ import { UnsupportedBlockDetails, store as blockEditorStore, } from '@wordpress/block-editor'; +import { store as noticesStore } from '@wordpress/notices'; /** * Internal dependencies @@ -34,6 +35,8 @@ import styles from './style.scss'; const UBE_INCOMPATIBLE_BLOCKS = [ 'core/block' ]; const I18N_BLOCK_SCHEMA_TITLE = 'block title'; +const EMPTY_ARRAY = []; + export class UnsupportedBlockEdit extends Component { constructor( props ) { super( props ); @@ -119,16 +122,39 @@ export class UnsupportedBlockEdit extends Component { } renderSheet( blockTitle, blockName ) { - const { clientId } = this.props; + const { block, clientId, createSuccessNotice, replaceBlocks } = + this.props; const { showHelp } = this.state; + /* translators: Missing block alert title. %s: The localized block name */ const titleFormat = __( "'%s' is not fully-supported" ); const title = sprintf( titleFormat, blockTitle ); - const description = applyFilters( + let description = applyFilters( 'native.missing_block_detail', __( 'We are working hard to add more blocks with each release.' ), blockName ); + let customActions = EMPTY_ARRAY; + + // For Classic blocks, we offer the alternative to convert the content to blocks. + if ( blockName === 'core/freeform' ) { + description += + ' ' + + __( 'Alternatively, you can convert the content to blocks.' ); + /* translators: displayed right after the classic block is converted to blocks. %s: The localized classic block name */ + const successNotice = __( "'%s' block converted to blocks" ); + customActions = [ + { + label: __( 'Convert to blocks' ), + onPress: () => { + createSuccessNotice( + sprintf( successNotice, blockTitle ) + ); + replaceBlocks( block ); + }, + }, + ]; + } return ( ); } @@ -202,8 +229,9 @@ export class UnsupportedBlockEdit extends Component { } export default compose( [ - withSelect( ( select, { attributes } ) => { - const { capabilities } = select( blockEditorStore ).getSettings(); + withSelect( ( select, { attributes, clientId } ) => { + const { getBlock, getSettings } = select( blockEditorStore ); + const { capabilities } = getSettings(); return { isUnsupportedBlockEditorSupported: capabilities?.unsupportedBlockEditor === true, @@ -211,14 +239,23 @@ export default compose( [ capabilities?.canEnableUnsupportedBlockEditor === true, isEditableInUnsupportedBlockEditor: ! UBE_INCOMPATIBLE_BLOCKS.includes( attributes.originalName ), + block: getBlock( clientId ), }; } ), withDispatch( ( dispatch, ownProps ) => { - const { selectBlock } = dispatch( blockEditorStore ); + const { selectBlock, replaceBlocks } = dispatch( blockEditorStore ); + const { createSuccessNotice } = dispatch( noticesStore ); return { selectBlock() { selectBlock( ownProps.clientId ); }, + replaceBlocks( block ) { + replaceBlocks( + ownProps.clientId, + rawHandler( { HTML: serialize( block ) } ) + ); + }, + createSuccessNotice, }; } ), withPreferredColorScheme, diff --git a/packages/react-native-editor/CHANGELOG.md b/packages/react-native-editor/CHANGELOG.md index 9e69c51a6b73ba..3c5fe5766720b0 100644 --- a/packages/react-native-editor/CHANGELOG.md +++ b/packages/react-native-editor/CHANGELOG.md @@ -10,6 +10,7 @@ For each user feature we should also add a importance categorization label to i --> ## Unreleased +- [*] Classic block: Add option to convert to blocks [#55461] ## 1.106.0 - [*] Exit Preformatted and Verse blocks by triple pressing the Return key [#53354] From fb7c9c5bbe243f0508ffa3c7a31662b74939b743 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= <583546+oandregal@users.noreply.github.com> Date: Fri, 20 Oct 2023 13:18:56 +0200 Subject: [PATCH 05/14] DataViews: document `data` and `filters` (#55504) --- .../src/components/dataviews/README.md | 179 ++++++++++++++---- 1 file changed, 137 insertions(+), 42 deletions(-) diff --git a/packages/edit-site/src/components/dataviews/README.md b/packages/edit-site/src/components/dataviews/README.md index 9128c71e64874d..460235c8df3df4 100644 --- a/packages/edit-site/src/components/dataviews/README.md +++ b/packages/edit-site/src/components/dataviews/README.md @@ -1,16 +1,44 @@ # DataView -This file aims to document the main APIs related to the DataView component. +This file documents the DataViews UI component, which provides an API to render datasets using different view types (table, grid, etc.). + +```js + +``` + +## Data + +The dataset to work with, represented as a one-dimensional array. + +Example: + +```js +[ + { id: 1, title: "Title", ... }, + { ... } +] +``` ## View -The view is responsible for configuring how the dataset is visible to the user. For example: +The view object configures how the dataset is visible to the user. + +Example: ```js { type: 'list', - page: 1, perPage: 5, + page: 1, sort: { field: 'date', direction: 'desc', @@ -26,44 +54,77 @@ The view is responsible for configuring how the dataset is visible to the user. } ``` -- `type`: one of `list` or `grid`. -- `page`: the current page. -- `perPage`: number of records per page. -- `sort.field`: field used for sorting. -- `sort.direction`: one of `asc` or `desc`. -- `filters`: the filters applied to the dataset. +- `type`: view type, one of `list` or `grid`. +- `perPage`: number of records to show per page. +- `page`: the page that is visible. +- `sort.field`: field used for sorting the dataset. +- `sort.direction`: the direction to use for sorting, one of `asc` or `desc`. +- `filters`: the filters applied to the dataset. See filters section. - `visibleFilters`: the `id` of the filters that are visible in the UI. - `hiddenFields`: the `id` of the fields that are hidden in the UI. - `layout`: ... -The view configuration is used to retrieve the corresponding entity that holds the dataset: +Note that it's the consumer's responsibility to provide the data and make sure the dataset corresponds to the view's config (sort, pagination, filters, etc.). + +Example: ```js -const { - records: pages, - isLoading: isLoadingPages, - totalItems, - totalPages -} = useEntityRecords( 'postType', 'page', { - per_page: view.perPage, - page: view.page, - order: view.sort?.direction, - orderby: view.sort?.field - ...view.filters -} ); +function MyCustomPageList() { + const [ view, setView ] = useState( { + type: 'list', + page: 1, + "...": "..." + } ); + + const queryArgs = useMemo( + () => ( { + per_page: view.perPage, + page: view.page, + order: view.sort?.direction, + orderby: view.sort?.field + ...view.filters + } ), + [ view ] + ); + + const { + records + } = useEntityRecords( 'postType', 'page', queryArgs ); + + return ( + + ); +} ``` ## Fields -The fields describe the dataset. For example: +The fields describe the visible items for each record in the dataset. + +Example: ```js [ + { + id: 'date', + header: __( 'Date' ), + getValue: ( { item } ) => item.date, + render: ( { item } ) => { + return ( + + ); + } + }, { id: 'author', header: __( 'Author' ), getValue: ( { item } ) => item.author, - render: ( {item} ) => { + render: ( { item } ) => { return ( { item.author } ); @@ -73,37 +134,71 @@ The fields describe the dataset. For example: { value: 2, label: 'User' } ] filters: [ - 'enumeration', + 'enumeration' { id: 'author_search', type: 'search', name: __( 'Search by author' ) } ], - }, + } ] ``` - `id`: identifier for the field. Unique. -- `header`: the field name for the UI. +- `header`: the field's name to be shown in the UI. - `getValue`: function that returns the value of the field. - `render`: function that renders the field. -- `elements`: a set of valid values for the field. -- `filters`: what filters are available for the user to use. A filter contains the following properties: - - `id`: unique identifier for the filter. Matches the entity query param. If not provided, the field's `id` is used. - - `name`: nice looking name for the filter. If not provided, the field's `header` is used. - - `type`: the type of filter. One of `search` or `enumeration`. - - `resetLabel`: the label for the reset option of the filter. If none provided, `All` is used. - - `resetValue`: the value for the reset option of the filter. If none provedid, `''` is used. +- `elements`: the set of valid values for the field's value. +- `filters`: what filters are available for the user to use. See filters section. + +## Filters -## DataViews +Filters describe the conditions a record should match to be listed as part of the dataset. -The UI component responsible for rendering the dataset. +Filters can be provided globally, as a property of the `DataViews` component, or per field, should they be considered part of a fields' description. ```js +const field = [ + { + id: 'author', + filters: [ + 'enumeration' + { id: 'author_search', type: 'search', name: __( 'Search by author' ) } + ], + } +]; + ``` + +A filter is an object that may contain the following properties: + +- `id`: unique identifier for the filter. Matches the entity query param. Field filters may omit it, in which case the field's `id` will be used. +- `name`: nice looking name for the filter. Field filters may omit it, in which case the field's `header` will be used. +- `type`: the type of filter. One of `search` or `enumeration`. +- `elements`: for filters of type `enumeration`, the list of options to show. A one-dimensional array of object with value/label keys, as in `[ { value: 1, label: "Value name" } ]`. + - `value`: what's serialized into the view's filters. + - `label`: nice-looking name for users. +- `resetValue`: for filters of type `enumeration`, this is the value for the reset option. If none is provided, `''` will be used. +- `resetLabel`: for filters of type `enumeration`, this is the label for the reset option. If none is provided, `All` will be used. + +As a convenience, field's filter can provide abbreviated versions for the filter. All of following examples result in the same filter: + +```js +const field = [ + { + id: 'author', + header: __( 'Author' ), + elements: authors, + filters: [ + 'enumeration', + { type: 'enumeration' }, + { id: 'author', type: 'enumeration' }, + { id: 'author', type: 'enumeration', name: __( 'Author' ) }, + { id: 'author', type: 'enumeration', name: __( 'Author' ), elements: authors }, + ], + } +]; +``` From 4afae151a0974dbba4f60dfe6664a39d5e9aa25d Mon Sep 17 00:00:00 2001 From: Siobhan Bamber Date: Fri, 20 Oct 2023 14:02:03 +0100 Subject: [PATCH 06/14] [RNMobile] Synced Patterns: Ensure title is always visible (#55399) Ensure the title section of Synced Patterns is visible within the editor when a block-based theme is used with a background that contrasts the device's dark/light mode. --- .../src/block/edit-title.native.js | 29 ++++++++++--------- packages/react-native-editor/CHANGELOG.md | 1 + 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/packages/block-library/src/block/edit-title.native.js b/packages/block-library/src/block/edit-title.native.js index 0a574f2f0cfa83..d0c7d981202d99 100644 --- a/packages/block-library/src/block/edit-title.native.js +++ b/packages/block-library/src/block/edit-title.native.js @@ -6,7 +6,7 @@ import { Text, View } from 'react-native'; /** * WordPress dependencies */ -import { Icon } from '@wordpress/components'; +import { Icon, useGlobalStyles } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { withPreferredColorScheme } from '@wordpress/compose'; import { help, lock } from '@wordpress/icons'; @@ -17,18 +17,21 @@ import { help, lock } from '@wordpress/icons'; import styles from './editor.scss'; function EditTitle( { getStylesFromColorScheme, title } ) { - const lockIconStyle = getStylesFromColorScheme( - styles.lockIcon, - styles.lockIconDark - ); - const titleStyle = getStylesFromColorScheme( - styles.title, - styles.titleDark - ); - const infoIconStyle = getStylesFromColorScheme( - styles.infoIcon, - styles.infoIconDark - ); + const globalStyles = useGlobalStyles(); + const baseColors = globalStyles?.baseColors?.color; + + const lockIconStyle = [ + getStylesFromColorScheme( styles.lockIcon, styles.lockIconDark ), + baseColors && { color: baseColors.text }, + ]; + const titleStyle = [ + getStylesFromColorScheme( styles.title, styles.titleDark ), + baseColors && { color: baseColors.text }, + ]; + const infoIconStyle = [ + getStylesFromColorScheme( styles.infoIcon, styles.infoIconDark ), + baseColors && { color: baseColors.text }, + ]; const separatorStyle = getStylesFromColorScheme( styles.separator, styles.separatorDark diff --git a/packages/react-native-editor/CHANGELOG.md b/packages/react-native-editor/CHANGELOG.md index 3c5fe5766720b0..9077664ca60be2 100644 --- a/packages/react-native-editor/CHANGELOG.md +++ b/packages/react-native-editor/CHANGELOG.md @@ -10,6 +10,7 @@ For each user feature we should also add a importance categorization label to i --> ## Unreleased +- [*] Synced Patterns: Fix visibility of heading section when used with block based themes in dark mode [#55399] - [*] Classic block: Add option to convert to blocks [#55461] ## 1.106.0 From 145ee70286ba660664e38a8e506a4f5c34dd5947 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= <583546+oandregal@users.noreply.github.com> Date: Fri, 20 Oct 2023 16:51:13 +0200 Subject: [PATCH 07/14] Remove string from pages sidebar (#55510) --- packages/edit-site/src/components/dataviews/sidebar-content.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/edit-site/src/components/dataviews/sidebar-content.js b/packages/edit-site/src/components/dataviews/sidebar-content.js index c10565b38de37f..5015e73bc4a635 100644 --- a/packages/edit-site/src/components/dataviews/sidebar-content.js +++ b/packages/edit-site/src/components/dataviews/sidebar-content.js @@ -1,3 +1,4 @@ export default function DataViewsSidebarContent() { - return

Add views ui here

; + // TODO: add views UI. + return null; } From fd8bdfe9007795045f01991505034ebc1258e861 Mon Sep 17 00:00:00 2001 From: Artemio Morales Date: Fri, 20 Oct 2023 11:02:35 -0500 Subject: [PATCH 08/14] Image: Improve focus management in lightbox (#55428) * Improve focus management This commit removes the logic to set focus differently based on event.pointerType and instead sets focus on the dialog itself when the lightbox opens, and on the lightbox trigger when the lightbox closes. * Add delay before focusing when closing lightbox * Put focus back on close button when opening lightbox It turns out that placing focus on the modal was causing inconsistent behavior in Safari, so I've put the focus back on the close button when the lightbox opens, which performs predictably. I've also added a tabindex to the image, which prevents the focus ring from erroneously showing when opening the lightbox with a mouse in Chrome and Firefox. * Move focus to the dialog when opening the lightbox. * Fix SVG markup. * Consistent indentation with spaces. * Remove unnecessary tabindex --------- Co-authored-by: Andrea Fercia --- packages/block-library/src/image/index.php | 6 ++-- packages/block-library/src/image/view.js | 32 +++++++++------------- 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/packages/block-library/src/image/index.php b/packages/block-library/src/image/index.php index f29202fe63d195..c465677a986e05 100644 --- a/packages/block-library/src/image/index.php +++ b/packages/block-library/src/image/index.php @@ -235,6 +235,7 @@ function block_core_image_render_lightbox( $block_content, $block ) { $button = $img[0] . ''; @@ -319,12 +320,13 @@ function block_core_image_render_lightbox( $block_content, $block ) { data-wp-on--touchmove="actions.core.image.handleTouchMove" data-wp-on--touchend="actions.core.image.handleTouchEnd" data-wp-on--click="actions.core.image.hideLightbox" + tabindex="-1" > - + HTML; diff --git a/packages/block-library/src/image/view.js b/packages/block-library/src/image/view.js index 30d1259637e3d9..331c0e79c731fc 100644 --- a/packages/block-library/src/image/view.js +++ b/packages/block-library/src/image/view.js @@ -135,7 +135,7 @@ store( false ); }, - hideLightbox: async ( { context, event } ) => { + hideLightbox: async ( { context } ) => { context.core.image.hideAnimationEnabled = true; if ( context.core.image.lightboxEnabled ) { // We want to wait until the close animation is completed @@ -149,19 +149,15 @@ store( 'scroll', scrollCallback ); + // If we don't delay before changing the focus, + // the focus ring will appear on Firefox before + // the image has finished animating, which looks broken. + context.core.image.lightboxTriggerRef.focus( { + preventScroll: true, + } ); }, 450 ); context.core.image.lightboxEnabled = false; - - // We want to avoid drawing attention to the button - // after the lightbox closes for mouse and touch users. - // Note that the `event.pointerType` property returns - // as an empty string if a keyboard fired the event. - if ( event.pointerType === '' ) { - context.core.image.lastFocusedElement.focus( { - preventScroll: true, - } ); - } } }, handleKeydown: ( { context, actions, event } ) => { @@ -266,6 +262,10 @@ store( image: { initOriginImage: ( { context, ref } ) => { context.core.image.imageRef = ref; + context.core.image.lightboxTriggerRef = + ref.parentElement.querySelector( + '.lightbox-trigger' + ); if ( ref.complete ) { context.core.image.imageLoaded = true; context.core.image.imageCurrentSrc = ref.currentSrc; @@ -282,14 +282,8 @@ store( focusableElements.length - 1 ]; - // We want to avoid drawing unnecessary attention to the close - // button for mouse and touch users. Note that even if opening - // the lightbox via keyboard, the event fired is of type - // `pointerEvent`, so we need to rely on the `event.pointerType` - // property, which returns an empty string for keyboard events. - if ( context.core.image.pointerType === '' ) { - ref.querySelector( '.close-button' ).focus(); - } + // Move focus to the dialog when opening it. + ref.focus(); } }, setButtonStyles: ( { context, ref } ) => { From 33fabf5ceba1be77dcf8e453bde59b64b7d862e0 Mon Sep 17 00:00:00 2001 From: strarsis Date: Fri, 20 Oct 2023 19:57:13 +0200 Subject: [PATCH 09/14] Use PostCSS + PostCSS plugins for style transformation (#49521) * Use PostCSS + PostCSS plugins for style transformation. * Remove the now replaced CSS parsing code. * Use synchronous PostCSS API. * Update package-lock.json * Add basic wrapping test * Use correct postcss-prefixwrap package * Update test snapshots * Add more tests and fix type signature * Ensure that data URLs do not break the CSS transformer * Do not optional chain something that is not optional * Filter out non-CSS styles before passing the array to transformStyles --------- Co-authored-by: Luis Felipe Zaguini --- package-lock.json | 120 ++- packages/block-editor/README.md | 14 +- packages/block-editor/package.json | 7 +- .../__snapshots__/transform-styles.js.snap | 103 +++ .../src/utils/test/transform-styles.js | 217 ++++++ .../src/utils/transform-styles/ast/index.js | 5 - .../src/utils/transform-styles/ast/parse.js | 732 ------------------ .../ast/stringify/compiler.js | 50 -- .../ast/stringify/compress.js | 238 ------ .../ast/stringify/identity.js | 286 ------- .../transform-styles/ast/stringify/index.js | 32 - .../src/utils/transform-styles/index.js | 50 +- .../test/__snapshots__/traverse.js.snap | 7 - .../utils/transform-styles/test/traverse.js | 24 - .../test/__snapshots__/url-rewrite.js.snap | 25 - .../test/__snapshots__/wrap.js.snap | 64 -- .../transforms/test/url-rewrite.js | 39 - .../transform-styles/transforms/test/wrap.js | 95 --- .../transforms/url-rewrite.js | 139 ---- .../utils/transform-styles/transforms/wrap.js | 56 -- .../src/utils/transform-styles/traverse.js | 32 - packages/block-library/src/html/preview.js | 13 +- 22 files changed, 474 insertions(+), 1874 deletions(-) create mode 100644 packages/block-editor/src/utils/test/__snapshots__/transform-styles.js.snap create mode 100644 packages/block-editor/src/utils/test/transform-styles.js delete mode 100644 packages/block-editor/src/utils/transform-styles/ast/index.js delete mode 100644 packages/block-editor/src/utils/transform-styles/ast/parse.js delete mode 100644 packages/block-editor/src/utils/transform-styles/ast/stringify/compiler.js delete mode 100644 packages/block-editor/src/utils/transform-styles/ast/stringify/compress.js delete mode 100644 packages/block-editor/src/utils/transform-styles/ast/stringify/identity.js delete mode 100644 packages/block-editor/src/utils/transform-styles/ast/stringify/index.js delete mode 100644 packages/block-editor/src/utils/transform-styles/test/__snapshots__/traverse.js.snap delete mode 100644 packages/block-editor/src/utils/transform-styles/test/traverse.js delete mode 100644 packages/block-editor/src/utils/transform-styles/transforms/test/__snapshots__/url-rewrite.js.snap delete mode 100644 packages/block-editor/src/utils/transform-styles/transforms/test/__snapshots__/wrap.js.snap delete mode 100644 packages/block-editor/src/utils/transform-styles/transforms/test/url-rewrite.js delete mode 100644 packages/block-editor/src/utils/transform-styles/transforms/test/wrap.js delete mode 100644 packages/block-editor/src/utils/transform-styles/transforms/url-rewrite.js delete mode 100644 packages/block-editor/src/utils/transform-styles/transforms/wrap.js delete mode 100644 packages/block-editor/src/utils/transform-styles/traverse.js diff --git a/package-lock.json b/package-lock.json index ed33a0a19a48eb..7603e03b936bff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46332,6 +46332,14 @@ "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==", + "peerDependencies": { + "postcss": "*" + } + }, "node_modules/postcss-reduce-initial": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-6.0.0.tgz", @@ -46441,6 +46449,22 @@ "postcss": "^8.2.15" } }, + "node_modules/postcss-urlrebase": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/postcss-urlrebase/-/postcss-urlrebase-1.3.0.tgz", + "integrity": "sha512-LOFN43n1IewKriXiypMNNinXeptttSyGGRLPbBMdQzuTvvCEo5mz/gG06y/HqrkN7p3ayHQf2R2bTBv639FOaQ==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "peerDependencies": { + "postcss": "^8.3.0" + } + }, + "node_modules/postcss-urlrebase/node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, "node_modules/postcss-value-parser": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", @@ -52850,11 +52874,6 @@ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "dev": true }, - "node_modules/traverse": { - "version": "0.6.6", - "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.6.tgz", - "integrity": "sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc=" - }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -56066,13 +56085,14 @@ "diff": "^4.0.2", "dom-scroll-into-view": "^1.2.1", "fast-deep-equal": "^3.1.3", - "inherits": "^2.0.3", "memize": "^2.1.0", + "postcss": "^8.4.21", + "postcss-prefixwrap": "^1.41.0", + "postcss-urlrebase": "^1.0.0", "react-autosize-textarea": "^7.1.0", "react-easy-crop": "^4.5.1", "rememo": "^4.0.2", - "remove-accents": "^0.5.0", - "traverse": "^0.6.6" + "remove-accents": "^0.5.0" }, "engines": { "node": ">=12" @@ -56082,6 +56102,41 @@ "react-dom": "^18.0.0" } }, + "packages/block-editor/node_modules/postcss": { + "version": "8.4.30", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.30.tgz", + "integrity": "sha512-7ZEao1g4kd68l97aWG/etQKPKq07us0ieSZ2TnFDk11i0ZfDW2AwKHYU8qv4MZKqN2fdBfg+7q0ES06UA73C1g==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "packages/block-editor/node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "engines": { + "node": ">=0.10.0" + } + }, "packages/block-library": { "name": "@wordpress/block-library", "version": "8.21.0", @@ -69479,13 +69534,31 @@ "diff": "^4.0.2", "dom-scroll-into-view": "^1.2.1", "fast-deep-equal": "^3.1.3", - "inherits": "^2.0.3", "memize": "^2.1.0", + "postcss": "^8.4.21", + "postcss-prefixwrap": "^1.41.0", + "postcss-urlrebase": "^1.0.0", "react-autosize-textarea": "^7.1.0", "react-easy-crop": "^4.5.1", "rememo": "^4.0.2", - "remove-accents": "^0.5.0", - "traverse": "^0.6.6" + "remove-accents": "^0.5.0" + }, + "dependencies": { + "postcss": { + "version": "8.4.30", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.30.tgz", + "integrity": "sha512-7ZEao1g4kd68l97aWG/etQKPKq07us0ieSZ2TnFDk11i0ZfDW2AwKHYU8qv4MZKqN2fdBfg+7q0ES06UA73C1g==", + "requires": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + } + }, + "source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==" + } } }, "@wordpress/block-library": { @@ -95672,6 +95745,11 @@ } } }, + "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-reduce-initial": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-6.0.0.tgz", @@ -95748,6 +95826,21 @@ "postcss-selector-parser": "^6.0.5" } }, + "postcss-urlrebase": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/postcss-urlrebase/-/postcss-urlrebase-1.3.0.tgz", + "integrity": "sha512-LOFN43n1IewKriXiypMNNinXeptttSyGGRLPbBMdQzuTvvCEo5mz/gG06y/HqrkN7p3ayHQf2R2bTBv639FOaQ==", + "requires": { + "postcss-value-parser": "^4.2.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + } + } + }, "postcss-value-parser": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", @@ -100640,11 +100733,6 @@ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "dev": true }, - "traverse": { - "version": "0.6.6", - "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.6.tgz", - "integrity": "sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc=" - }, "tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", diff --git a/packages/block-editor/README.md b/packages/block-editor/README.md index 02671d5dca0e3c..9c7a72f0897143 100644 --- a/packages/block-editor/README.md +++ b/packages/block-editor/README.md @@ -789,13 +789,23 @@ Applies a series of CSS rule transforms to wrap selectors inside a given class a _Parameters_ -- _styles_ `Object|Array`: CSS rules. -- _wrapperClassName_ `string`: Wrapper Class Name. +- _styles_ `EditorStyle[]`: CSS rules. +- _wrapperSelector_ `string`: Wrapper selector. _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 225d9c987638af..5abf843b85f51a 100644 --- a/packages/block-editor/package.json +++ b/packages/block-editor/package.json @@ -71,13 +71,14 @@ "diff": "^4.0.2", "dom-scroll-into-view": "^1.2.1", "fast-deep-equal": "^3.1.3", - "inherits": "^2.0.3", "memize": "^2.1.0", + "postcss": "^8.4.21", + "postcss-prefixwrap": "^1.41.0", + "postcss-urlrebase": "^1.0.0", "react-autosize-textarea": "^7.1.0", "react-easy-crop": "^4.5.1", "rememo": "^4.0.2", - "remove-accents": "^0.5.0", - "traverse": "^0.6.6" + "remove-accents": "^0.5.0" }, "peerDependencies": { "react": "^18.0.0", 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 new file mode 100644 index 00000000000000..28c4202f414a9e --- /dev/null +++ b/packages/block-editor/src/utils/test/__snapshots__/transform-styles.js.snap @@ -0,0 +1,103 @@ +// 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 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 new file mode 100644 index 00000000000000..f162a0b2f6048c --- /dev/null +++ b/packages/block-editor/src/utils/test/transform-styles.js @@ -0,0 +1,217 @@ +/** + * Internal dependencies + */ +import transformStyles from '../transform-styles'; + +describe( 'transformStyles', () => { + describe( 'selector wrap', () => { + it( 'should wrap regular selectors', () => { + const input = `h1 { color: red; }`; + const output = transformStyles( + [ + { + css: input, + }, + ], + '.my-namespace' + ); + + expect( output ).toMatchSnapshot(); + } ); + + it( 'should wrap multiple selectors', () => { + const input = `h1, h2 { color: red; }`; + const output = transformStyles( + [ + { + css: input, + }, + ], + '.my-namespace' + ); + + expect( output ).toMatchSnapshot(); + } ); + + it( 'should ignore selectors', () => { + const input = `h1, body { color: red; }`; + const output = transformStyles( + [ + { + css: input, + ignoredSelectors: [ 'body' ], + }, + ], + '.my-namespace' + ); + + expect( output ).toMatchSnapshot(); + } ); + + it( 'should replace root tags', () => { + const input = `body, h1 { color: red; }`; + const output = transformStyles( + [ + { + css: input, + }, + ], + '.my-namespace' + ); + + expect( output ).toMatchSnapshot(); + } ); + + it( 'should ignore keyframes', () => { + const input = ` + @keyframes edit-post__fade-in-animation { + from { + opacity: 0; + } + }`; + const output = transformStyles( + [ + { + css: input, + }, + ], + '.my-namespace' + ); + + expect( output ).toMatchSnapshot(); + } ); + + it( 'should wrap selectors inside container queries', () => { + const input = ` + @container (width > 400px) { + h1 { color: red; } + }`; + const output = transformStyles( + [ + { + css: input, + }, + ], + '.my-namespace' + ); + + expect( output ).toMatchSnapshot(); + } ); + + it( 'should ignore font-face selectors', () => { + const input = ` + @font-face { + font-family: myFirstFont; + src: url(sansation_light.woff); + }`; + const output = transformStyles( + [ + { + css: input, + }, + ], + '.my-namespace' + ); + + expect( output ).toMatchSnapshot(); + } ); + + it( 'should replace :root selectors', () => { + const input = ` + :root { + --my-color: #ff0000; + }`; + const output = transformStyles( + [ + { + css: input, + }, + ], + '.my-namespace' + ); + + expect( output ).toMatchSnapshot(); + } ); + + it( 'should not double wrap selectors', () => { + const input = ` .my-namespace h1, .red { color: red; }`; + + const output = transformStyles( + [ + { + css: input, + }, + ], + '.my-namespace' + ); + + expect( output ).toMatchSnapshot(); + } ); + } ); + + it( 'should not break with data urls', () => { + const input = `.wp-block-group { + background-image: url("data:image/svg+xml,%3Csvg%3E.b%7Bclip-path:url(test);%7D%3C/svg%3E"); + color: red !important; + }`; + + const output = transformStyles( [ + { + css: input, + baseURL: 'http://wp-site.local/themes/gut/css/', + }, + ] ); + + expect( output ).toMatchSnapshot(); + } ); + + describe( 'URL rewrite', () => { + it( 'should rewrite relative paths', () => { + const input = `h1 { background: url(images/test.png); }`; + const output = transformStyles( [ + { + css: input, + baseURL: 'http://wp-site.local/themes/gut/css/', + }, + ] ); + + expect( output ).toMatchSnapshot(); + } ); + + it( 'should replace complex relative paths', () => { + const input = `h1 { background: url(../images/test.png); }`; + const output = transformStyles( [ + { + css: input, + baseURL: 'http://wp-site.local/themes/gut/css/', + }, + ] ); + + expect( output ).toMatchSnapshot(); + } ); + + it( 'should not replace absolute paths', () => { + const input = `h1 { background: url(/images/test.png); }`; + const output = transformStyles( [ + { + css: input, + baseURL: 'http://wp-site.local/themes/gut/css/', + }, + ] ); + + expect( output ).toMatchSnapshot(); + } ); + + it( 'should not replace remote paths', () => { + const input = `h1 { background: url(http://wp.org/images/test.png); }`; + const output = transformStyles( [ + { + css: input, + baseURL: 'http://wp-site.local/themes/gut/css/', + }, + ] ); + + expect( output ).toMatchSnapshot(); + } ); + } ); +} ); diff --git a/packages/block-editor/src/utils/transform-styles/ast/index.js b/packages/block-editor/src/utils/transform-styles/ast/index.js deleted file mode 100644 index b4dc1de499f474..00000000000000 --- a/packages/block-editor/src/utils/transform-styles/ast/index.js +++ /dev/null @@ -1,5 +0,0 @@ -// Adapted from https://github.com/reworkcss/css -// because we needed to remove source map support. - -export { default as parse } from './parse'; -export { default as stringify } from './stringify'; diff --git a/packages/block-editor/src/utils/transform-styles/ast/parse.js b/packages/block-editor/src/utils/transform-styles/ast/parse.js deleted file mode 100644 index 8f7d227d61442d..00000000000000 --- a/packages/block-editor/src/utils/transform-styles/ast/parse.js +++ /dev/null @@ -1,732 +0,0 @@ -/* eslint-disable @wordpress/no-unused-vars-before-return */ - -// Adapted from https://github.com/reworkcss/css -// because we needed to remove source map support. - -// http://www.w3.org/TR/CSS21/grammar.htm -// https://github.com/visionmedia/css-parse/pull/49#issuecomment-30088027 -const commentre = /\/\*[^*]*\*+([^/*][^*]*\*+)*\//g; - -export default function ( css, options ) { - options = options || {}; - - /** - * Positional. - */ - - let lineno = 1; - let column = 1; - - /** - * Update lineno and column based on `str`. - */ - - function updatePosition( str ) { - const lines = str.match( /\n/g ); - if ( lines ) { - lineno += lines.length; - } - const i = str.lastIndexOf( '\n' ); - // eslint-disable-next-line no-bitwise - column = ~i ? str.length - i : column + str.length; - } - - /** - * Mark position and patch `node.position`. - */ - - function position() { - const start = { line: lineno, column }; - return function ( node ) { - node.position = new Position( start ); - whitespace(); - return node; - }; - } - - /** - * Store position information for a node - */ - - function Position( start ) { - this.start = start; - this.end = { line: lineno, column }; - this.source = options.source; - } - - /** - * Non-enumerable source string - */ - - Position.prototype.content = css; - - /** - * Error `msg`. - */ - - const errorsList = []; - - function error( msg ) { - const err = new Error( - options.source + ':' + lineno + ':' + column + ': ' + msg - ); - err.reason = msg; - err.filename = options.source; - err.line = lineno; - err.column = column; - err.source = css; - - if ( options.silent ) { - errorsList.push( err ); - } else { - throw err; - } - } - - /** - * Parse stylesheet. - */ - - function stylesheet() { - const rulesList = rules(); - - return { - type: 'stylesheet', - stylesheet: { - source: options.source, - rules: rulesList, - parsingErrors: errorsList, - }, - }; - } - - /** - * Opening brace. - */ - - function open() { - return match( /^{\s*/ ); - } - - /** - * Closing brace. - */ - - function close() { - return match( /^}/ ); - } - - /** - * Parse ruleset. - */ - - function rules() { - let node; - const accumulator = []; - whitespace(); - comments( accumulator ); - while ( - css.length && - css.charAt( 0 ) !== '}' && - ( node = atrule() || rule() ) - ) { - if ( node !== false ) { - accumulator.push( node ); - comments( accumulator ); - } - } - return accumulator; - } - - /** - * Match `re` and return captures. - */ - - function match( re ) { - const m = re.exec( css ); - if ( ! m ) { - return; - } - const str = m[ 0 ]; - updatePosition( str ); - css = css.slice( str.length ); - return m; - } - - /** - * Parse whitespace. - */ - - function whitespace() { - match( /^\s*/ ); - } - - /** - * Parse comments; - */ - - function comments( accumulator ) { - let c; - accumulator = accumulator || []; - // eslint-disable-next-line no-cond-assign - while ( ( c = comment() ) ) { - if ( c !== false ) { - accumulator.push( c ); - } - } - return accumulator; - } - - /** - * Parse comment. - */ - - function comment() { - const pos = position(); - if ( '/' !== css.charAt( 0 ) || '*' !== css.charAt( 1 ) ) { - return; - } - - let i = 2; - while ( - '' !== css.charAt( i ) && - ( '*' !== css.charAt( i ) || '/' !== css.charAt( i + 1 ) ) - ) { - ++i; - } - i += 2; - - if ( '' === css.charAt( i - 1 ) ) { - return error( 'End of comment missing' ); - } - - const str = css.slice( 2, i - 2 ); - column += 2; - updatePosition( str ); - css = css.slice( i ); - column += 2; - - return pos( { - type: 'comment', - comment: str, - } ); - } - - /** - * Parse selector. - */ - - function selector() { - const m = match( /^([^{]+)/ ); - if ( ! m ) { - return; - } - // FIXME: Remove all comments from selectors http://ostermiller.org/findcomment.html - return trim( m[ 0 ] ) - .replace( /\/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*\/+/g, '' ) - .replace( /"(?:\\"|[^"])*"|'(?:\\'|[^'])*'/g, function ( matched ) { - return matched.replace( /,/g, '\u200C' ); - } ) - .split( /\s*(?![^(]*\)),\s*/ ) - .map( function ( s ) { - return s.replace( /\u200C/g, ',' ); - } ); - } - - /** - * Parse declaration. - */ - - function declaration() { - const pos = position(); - - // prop. - let prop = match( /^(\*?[-#\/\*\\\w]+(\[[0-9a-z_-]+\])?)\s*/ ); - if ( ! prop ) { - return; - } - prop = trim( prop[ 0 ] ); - - // : - if ( ! match( /^:\s*/ ) ) { - return error( "property missing ':'" ); - } - - // val. - const val = match( - /^((?:'(?:\\'|.)*?'|"(?:\\"|.)*?"|\([^\)]*?\)|[^};])+)/ - ); - - const ret = pos( { - type: 'declaration', - property: prop.replace( commentre, '' ), - value: val ? trim( val[ 0 ] ).replace( commentre, '' ) : '', - } ); - - // ; - match( /^[;\s]*/ ); - - return ret; - } - - /** - * Parse declarations. - */ - - function declarations() { - const decls = []; - - if ( ! open() ) { - return error( "missing '{'" ); - } - comments( decls ); - - // declarations. - let decl; - // eslint-disable-next-line no-cond-assign - while ( ( decl = declaration() ) ) { - if ( decl !== false ) { - decls.push( decl ); - comments( decls ); - } - } - - if ( ! close() ) { - return error( "missing '}'" ); - } - return decls; - } - - /** - * Parse keyframe. - */ - - function keyframe() { - let m; - const vals = []; - const pos = position(); - - // eslint-disable-next-line no-cond-assign - while ( ( m = match( /^((\d+\.\d+|\.\d+|\d+)%?|[a-z]+)\s*/ ) ) ) { - vals.push( m[ 1 ] ); - match( /^,\s*/ ); - } - - if ( ! vals.length ) { - return; - } - - return pos( { - type: 'keyframe', - values: vals, - declarations: declarations(), - } ); - } - - /** - * Parse keyframes. - */ - - function atkeyframes() { - const pos = position(); - let m = match( /^@([-\w]+)?keyframes\s*/ ); - - if ( ! m ) { - return; - } - const vendor = m[ 1 ]; - - // identifier - m = match( /^([-\w]+)\s*/ ); - if ( ! m ) { - return error( '@keyframes missing name' ); - } - const name = m[ 1 ]; - - if ( ! open() ) { - return error( "@keyframes missing '{'" ); - } - - let frame; - let frames = comments(); - // eslint-disable-next-line no-cond-assign - while ( ( frame = keyframe() ) ) { - frames.push( frame ); - frames = frames.concat( comments() ); - } - - if ( ! close() ) { - return error( "@keyframes missing '}'" ); - } - - return pos( { - type: 'keyframes', - name, - vendor, - keyframes: frames, - } ); - } - - /** - * Parse supports. - */ - - function atsupports() { - const pos = position(); - const m = match( /^@supports *([^{]+)/ ); - - if ( ! m ) { - return; - } - const supports = trim( m[ 1 ] ); - - if ( ! open() ) { - return error( "@supports missing '{'" ); - } - - const style = comments().concat( rules() ); - - if ( ! close() ) { - return error( "@supports missing '}'" ); - } - - return pos( { - type: 'supports', - supports, - rules: style, - } ); - } - - /** - * Parse host. - */ - - function athost() { - const pos = position(); - const m = match( /^@host\s*/ ); - - if ( ! m ) { - return; - } - - if ( ! open() ) { - return error( "@host missing '{'" ); - } - - const style = comments().concat( rules() ); - - if ( ! close() ) { - return error( "@host missing '}'" ); - } - - return pos( { - type: 'host', - rules: style, - } ); - } - - /** - * Parse media. - */ - - function atmedia() { - const pos = position(); - const m = match( /^@media *([^{]+)/ ); - - if ( ! m ) { - return; - } - const media = trim( m[ 1 ] ); - - if ( ! open() ) { - return error( "@media missing '{'" ); - } - - const style = comments().concat( rules() ); - - if ( ! close() ) { - return error( "@media missing '}'" ); - } - - return pos( { - type: 'media', - media, - rules: style, - } ); - } - - /** - * Parse container. - */ - - function atcontainer() { - const pos = position(); - const m = match( /^@container *([^{]+)/ ); - - if ( ! m ) { - return; - } - const container = trim( m[ 1 ] ); - - if ( ! open() ) { - return error( "@container missing '{'" ); - } - - const style = comments().concat( rules() ); - - if ( ! close() ) { - return error( "@container missing '}'" ); - } - - return pos( { - type: 'container', - container, - rules: style, - } ); - } - - /** - * Parse custom-media. - */ - - function atcustommedia() { - const pos = position(); - const m = match( /^@custom-media\s+(--[^\s]+)\s*([^{;]+);/ ); - if ( ! m ) { - return; - } - - return pos( { - type: 'custom-media', - name: trim( m[ 1 ] ), - media: trim( m[ 2 ] ), - } ); - } - - /** - * Parse paged media. - */ - - function atpage() { - const pos = position(); - const m = match( /^@page */ ); - if ( ! m ) { - return; - } - - const sel = selector() || []; - - if ( ! open() ) { - return error( "@page missing '{'" ); - } - let decls = comments(); - - // declarations. - let decl; - // eslint-disable-next-line no-cond-assign - while ( ( decl = declaration() ) ) { - decls.push( decl ); - decls = decls.concat( comments() ); - } - - if ( ! close() ) { - return error( "@page missing '}'" ); - } - - return pos( { - type: 'page', - selectors: sel, - declarations: decls, - } ); - } - - /** - * Parse document. - */ - - function atdocument() { - const pos = position(); - const m = match( /^@([-\w]+)?document *([^{]+)/ ); - if ( ! m ) { - return; - } - - const vendor = trim( m[ 1 ] ); - const doc = trim( m[ 2 ] ); - - if ( ! open() ) { - return error( "@document missing '{'" ); - } - - const style = comments().concat( rules() ); - - if ( ! close() ) { - return error( "@document missing '}'" ); - } - - return pos( { - type: 'document', - document: doc, - vendor, - rules: style, - } ); - } - - /** - * Parse font-face. - */ - - function atfontface() { - const pos = position(); - const m = match( /^@font-face\s*/ ); - if ( ! m ) { - return; - } - - if ( ! open() ) { - return error( "@font-face missing '{'" ); - } - let decls = comments(); - - // declarations. - let decl; - // eslint-disable-next-line no-cond-assign - while ( ( decl = declaration() ) ) { - decls.push( decl ); - decls = decls.concat( comments() ); - } - - if ( ! close() ) { - return error( "@font-face missing '}'" ); - } - - return pos( { - type: 'font-face', - declarations: decls, - } ); - } - - /** - * Parse import - */ - - const atimport = _compileAtrule( 'import' ); - - /** - * Parse charset - */ - - const atcharset = _compileAtrule( 'charset' ); - - /** - * Parse namespace - */ - - const atnamespace = _compileAtrule( 'namespace' ); - - /** - * Parse non-block at-rules - */ - - function _compileAtrule( name ) { - const re = new RegExp( '^@' + name + '\\s*([^;]+);' ); - return function () { - const pos = position(); - const m = match( re ); - if ( ! m ) { - return; - } - const ret = { type: name }; - ret[ name ] = m[ 1 ].trim(); - return pos( ret ); - }; - } - - /** - * Parse at rule. - */ - - function atrule() { - if ( css[ 0 ] !== '@' ) { - return; - } - - return ( - atkeyframes() || - atmedia() || - atcontainer() || - atcustommedia() || - atsupports() || - atimport() || - atcharset() || - atnamespace() || - atdocument() || - atpage() || - athost() || - atfontface() - ); - } - - /** - * Parse rule. - */ - - function rule() { - const pos = position(); - const sel = selector(); - - if ( ! sel ) { - return error( 'selector missing' ); - } - comments(); - - return pos( { - type: 'rule', - selectors: sel, - declarations: declarations(), - } ); - } - - return addParent( stylesheet() ); -} - -/** - * Trim `str`. - */ - -function trim( str ) { - return str ? str.replace( /^\s+|\s+$/g, '' ) : ''; -} - -/** - * Adds non-enumerable parent node reference to each node. - */ - -function addParent( obj, parent ) { - const isNode = obj && typeof obj.type === 'string'; - const childParent = isNode ? obj : parent; - - for ( const k in obj ) { - const value = obj[ k ]; - if ( Array.isArray( value ) ) { - value.forEach( function ( v ) { - addParent( v, childParent ); - } ); - } else if ( value && typeof value === 'object' ) { - addParent( value, childParent ); - } - } - - if ( isNode ) { - Object.defineProperty( obj, 'parent', { - configurable: true, - writable: true, - enumerable: false, - value: parent || null, - } ); - } - - return obj; -} - -/* eslint-enable @wordpress/no-unused-vars-before-return */ diff --git a/packages/block-editor/src/utils/transform-styles/ast/stringify/compiler.js b/packages/block-editor/src/utils/transform-styles/ast/stringify/compiler.js deleted file mode 100644 index d2500b730424f7..00000000000000 --- a/packages/block-editor/src/utils/transform-styles/ast/stringify/compiler.js +++ /dev/null @@ -1,50 +0,0 @@ -// Adapted from https://github.com/reworkcss/css -// because we needed to remove source map support. - -/** - * Expose `Compiler`. - */ - -export default Compiler; - -/** - * Initialize a compiler. - */ - -function Compiler( opts ) { - this.options = opts || {}; -} - -/** - * Emit `str` - */ - -Compiler.prototype.emit = function ( str ) { - return str; -}; - -/** - * Visit `node`. - */ - -Compiler.prototype.visit = function ( node ) { - return this[ node.type ]( node ); -}; - -/** - * Map visit over array of `nodes`, optionally using a `delim` - */ - -Compiler.prototype.mapVisit = function ( nodes, delim ) { - let buf = ''; - delim = delim || ''; - - for ( let i = 0, length = nodes.length; i < length; i++ ) { - buf += this.visit( nodes[ i ] ); - if ( delim && i < length - 1 ) { - buf += this.emit( delim ); - } - } - - return buf; -}; diff --git a/packages/block-editor/src/utils/transform-styles/ast/stringify/compress.js b/packages/block-editor/src/utils/transform-styles/ast/stringify/compress.js deleted file mode 100644 index 6a2a3af3769be0..00000000000000 --- a/packages/block-editor/src/utils/transform-styles/ast/stringify/compress.js +++ /dev/null @@ -1,238 +0,0 @@ -// Adapted from https://github.com/reworkcss/css -// because we needed to remove source map support. - -/** - * External dependencies - */ -import inherits from 'inherits'; - -/** - * Internal dependencies - */ -import Base from './compiler'; - -/** - * Expose compiler. - */ - -export default Compiler; - -/** - * Initialize a new `Compiler`. - */ - -function Compiler( options ) { - Base.call( this, options ); -} - -/** - * Inherit from `Base.prototype`. - */ - -inherits( Compiler, Base ); - -/** - * Compile `node`. - */ - -Compiler.prototype.compile = function ( node ) { - return node.stylesheet.rules.map( this.visit, this ).join( '' ); -}; - -/** - * Visit comment node. - */ - -Compiler.prototype.comment = function ( node ) { - return this.emit( '', node.position ); -}; - -/** - * Visit import node. - */ - -Compiler.prototype.import = function ( node ) { - return this.emit( '@import ' + node.import + ';', node.position ); -}; - -/** - * Visit media node. - */ - -Compiler.prototype.media = function ( node ) { - return ( - this.emit( '@media ' + node.media, node.position ) + - this.emit( '{' ) + - this.mapVisit( node.rules ) + - this.emit( '}' ) - ); -}; - -/** - * Visit container node. - */ - -Compiler.prototype.container = function ( node ) { - return ( - this.emit( '@container ' + node.container, node.position ) + - this.emit( '{' ) + - this.mapVisit( node.rules ) + - this.emit( '}' ) - ); -}; - -/** - * Visit document node. - */ - -Compiler.prototype.document = function ( node ) { - const doc = '@' + ( node.vendor || '' ) + 'document ' + node.document; - - return ( - this.emit( doc, node.position ) + - this.emit( '{' ) + - this.mapVisit( node.rules ) + - this.emit( '}' ) - ); -}; - -/** - * Visit charset node. - */ - -Compiler.prototype.charset = function ( node ) { - return this.emit( '@charset ' + node.charset + ';', node.position ); -}; - -/** - * Visit namespace node. - */ - -Compiler.prototype.namespace = function ( node ) { - return this.emit( '@namespace ' + node.namespace + ';', node.position ); -}; - -/** - * Visit supports node. - */ - -Compiler.prototype.supports = function ( node ) { - return ( - this.emit( '@supports ' + node.supports, node.position ) + - this.emit( '{' ) + - this.mapVisit( node.rules ) + - this.emit( '}' ) - ); -}; - -/** - * Visit keyframes node. - */ - -Compiler.prototype.keyframes = function ( node ) { - return ( - this.emit( - '@' + ( node.vendor || '' ) + 'keyframes ' + node.name, - node.position - ) + - this.emit( '{' ) + - this.mapVisit( node.keyframes ) + - this.emit( '}' ) - ); -}; - -/** - * Visit keyframe node. - */ - -Compiler.prototype.keyframe = function ( node ) { - const decls = node.declarations; - - return ( - this.emit( node.values.join( ',' ), node.position ) + - this.emit( '{' ) + - this.mapVisit( decls ) + - this.emit( '}' ) - ); -}; - -/** - * Visit page node. - */ - -Compiler.prototype.page = function ( node ) { - const sel = node.selectors.length ? node.selectors.join( ', ' ) : ''; - - return ( - this.emit( '@page ' + sel, node.position ) + - this.emit( '{' ) + - this.mapVisit( node.declarations ) + - this.emit( '}' ) - ); -}; - -/** - * Visit font-face node. - */ - -Compiler.prototype[ 'font-face' ] = function ( node ) { - return ( - this.emit( '@font-face', node.position ) + - this.emit( '{' ) + - this.mapVisit( node.declarations ) + - this.emit( '}' ) - ); -}; - -/** - * Visit host node. - */ - -Compiler.prototype.host = function ( node ) { - return ( - this.emit( '@host', node.position ) + - this.emit( '{' ) + - this.mapVisit( node.rules ) + - this.emit( '}' ) - ); -}; - -/** - * Visit custom-media node. - */ - -Compiler.prototype[ 'custom-media' ] = function ( node ) { - return this.emit( - '@custom-media ' + node.name + ' ' + node.media + ';', - node.position - ); -}; - -/** - * Visit rule node. - */ - -Compiler.prototype.rule = function ( node ) { - const decls = node.declarations; - if ( ! decls.length ) { - return ''; - } - - return ( - this.emit( node.selectors.join( ',' ), node.position ) + - this.emit( '{' ) + - this.mapVisit( decls ) + - this.emit( '}' ) - ); -}; - -/** - * Visit declaration node. - */ - -Compiler.prototype.declaration = function ( node ) { - return ( - this.emit( node.property + ':' + node.value, node.position ) + - this.emit( ';' ) - ); -}; diff --git a/packages/block-editor/src/utils/transform-styles/ast/stringify/identity.js b/packages/block-editor/src/utils/transform-styles/ast/stringify/identity.js deleted file mode 100644 index 760ca4044631ee..00000000000000 --- a/packages/block-editor/src/utils/transform-styles/ast/stringify/identity.js +++ /dev/null @@ -1,286 +0,0 @@ -/* eslint-disable @wordpress/no-unused-vars-before-return */ - -// Adapted from https://github.com/reworkcss/css -// because we needed to remove source map support. - -/** - * External dependencies - */ -import inherits from 'inherits'; - -/** - * Internal dependencies - */ -import Base from './compiler'; - -/** - * Expose compiler. - */ - -export default Compiler; - -/** - * Initialize a new `Compiler`. - */ - -function Compiler( options ) { - options = options || {}; - Base.call( this, options ); - this.indentation = options.indent; -} - -/** - * Inherit from `Base.prototype`. - */ - -inherits( Compiler, Base ); - -/** - * Compile `node`. - */ - -Compiler.prototype.compile = function ( node ) { - return this.stylesheet( node ); -}; - -/** - * Visit stylesheet node. - */ - -Compiler.prototype.stylesheet = function ( node ) { - return this.mapVisit( node.stylesheet.rules, '\n\n' ); -}; - -/** - * Visit comment node. - */ - -Compiler.prototype.comment = function ( node ) { - return this.emit( - this.indent() + '/*' + node.comment + '*/', - node.position - ); -}; - -/** - * Visit import node. - */ - -Compiler.prototype.import = function ( node ) { - return this.emit( '@import ' + node.import + ';', node.position ); -}; - -/** - * Visit media node. - */ - -Compiler.prototype.media = function ( node ) { - return ( - this.emit( '@media ' + node.media, node.position ) + - this.emit( ' {\n' + this.indent( 1 ) ) + - this.mapVisit( node.rules, '\n\n' ) + - this.emit( this.indent( -1 ) + '\n}' ) - ); -}; - -/** - * Visit container node. - */ - -Compiler.prototype.container = function ( node ) { - return ( - this.emit( '@container ' + node.container, node.position ) + - this.emit( ' {\n' + this.indent( 1 ) ) + - this.mapVisit( node.rules, '\n\n' ) + - this.emit( this.indent( -1 ) + '\n}' ) - ); -}; - -/** - * Visit document node. - */ - -Compiler.prototype.document = function ( node ) { - const doc = '@' + ( node.vendor || '' ) + 'document ' + node.document; - - return ( - this.emit( doc, node.position ) + - this.emit( ' ' + ' {\n' + this.indent( 1 ) ) + - this.mapVisit( node.rules, '\n\n' ) + - this.emit( this.indent( -1 ) + '\n}' ) - ); -}; - -/** - * Visit charset node. - */ - -Compiler.prototype.charset = function ( node ) { - return this.emit( '@charset ' + node.charset + ';', node.position ); -}; - -/** - * Visit namespace node. - */ - -Compiler.prototype.namespace = function ( node ) { - return this.emit( '@namespace ' + node.namespace + ';', node.position ); -}; - -/** - * Visit supports node. - */ - -Compiler.prototype.supports = function ( node ) { - return ( - this.emit( '@supports ' + node.supports, node.position ) + - this.emit( ' {\n' + this.indent( 1 ) ) + - this.mapVisit( node.rules, '\n\n' ) + - this.emit( this.indent( -1 ) + '\n}' ) - ); -}; - -/** - * Visit keyframes node. - */ - -Compiler.prototype.keyframes = function ( node ) { - return ( - this.emit( - '@' + ( node.vendor || '' ) + 'keyframes ' + node.name, - node.position - ) + - this.emit( ' {\n' + this.indent( 1 ) ) + - this.mapVisit( node.keyframes, '\n' ) + - this.emit( this.indent( -1 ) + '}' ) - ); -}; - -/** - * Visit keyframe node. - */ - -Compiler.prototype.keyframe = function ( node ) { - const decls = node.declarations; - - return ( - this.emit( this.indent() ) + - this.emit( node.values.join( ', ' ), node.position ) + - this.emit( ' {\n' + this.indent( 1 ) ) + - this.mapVisit( decls, '\n' ) + - this.emit( this.indent( -1 ) + '\n' + this.indent() + '}\n' ) - ); -}; - -/** - * Visit page node. - */ - -Compiler.prototype.page = function ( node ) { - const sel = node.selectors.length ? node.selectors.join( ', ' ) + ' ' : ''; - - return ( - this.emit( '@page ' + sel, node.position ) + - this.emit( '{\n' ) + - this.emit( this.indent( 1 ) ) + - this.mapVisit( node.declarations, '\n' ) + - this.emit( this.indent( -1 ) ) + - this.emit( '\n}' ) - ); -}; - -/** - * Visit font-face node. - */ - -Compiler.prototype[ 'font-face' ] = function ( node ) { - return ( - this.emit( '@font-face ', node.position ) + - this.emit( '{\n' ) + - this.emit( this.indent( 1 ) ) + - this.mapVisit( node.declarations, '\n' ) + - this.emit( this.indent( -1 ) ) + - this.emit( '\n}' ) - ); -}; - -/** - * Visit host node. - */ - -Compiler.prototype.host = function ( node ) { - return ( - this.emit( '@host', node.position ) + - this.emit( ' {\n' + this.indent( 1 ) ) + - this.mapVisit( node.rules, '\n\n' ) + - this.emit( this.indent( -1 ) + '\n}' ) - ); -}; - -/** - * Visit custom-media node. - */ - -Compiler.prototype[ 'custom-media' ] = function ( node ) { - return this.emit( - '@custom-media ' + node.name + ' ' + node.media + ';', - node.position - ); -}; - -/** - * Visit rule node. - */ - -Compiler.prototype.rule = function ( node ) { - const indent = this.indent(); - const decls = node.declarations; - if ( ! decls.length ) { - return ''; - } - - return ( - this.emit( - node.selectors - .map( function ( s ) { - return indent + s; - } ) - .join( ',\n' ), - node.position - ) + - this.emit( ' {\n' ) + - this.emit( this.indent( 1 ) ) + - this.mapVisit( decls, '\n' ) + - this.emit( this.indent( -1 ) ) + - this.emit( '\n' + this.indent() + '}' ) - ); -}; - -/** - * Visit declaration node. - */ - -Compiler.prototype.declaration = function ( node ) { - return ( - this.emit( this.indent() ) + - this.emit( node.property + ': ' + node.value, node.position ) + - this.emit( ';' ) - ); -}; - -/** - * Increase, decrease or return current indentation. - */ - -Compiler.prototype.indent = function ( level ) { - this.level = this.level || 1; - - if ( null !== level ) { - this.level += level; - return ''; - } - - return Array( this.level ).join( this.indentation || ' ' ); -}; - -/* eslint-enable @wordpress/no-unused-vars-before-return */ diff --git a/packages/block-editor/src/utils/transform-styles/ast/stringify/index.js b/packages/block-editor/src/utils/transform-styles/ast/stringify/index.js deleted file mode 100644 index 2f332cdb52bec9..00000000000000 --- a/packages/block-editor/src/utils/transform-styles/ast/stringify/index.js +++ /dev/null @@ -1,32 +0,0 @@ -// Adapted from https://github.com/reworkcss/css -// because we needed to remove source map support. - -/** - * Internal dependencies - */ -import Compressed from './compress'; -import Identity from './identity'; - -/** - * Stringfy the given AST `node`. - * - * Options: - * - * - `compress` space-optimized output - * - `sourcemap` return an object with `.code` and `.map` - * - * @param {Object} node - * @param {Object} [options] - * @return {string} - */ - -export default function ( node, options ) { - options = options || {}; - - const compiler = options.compress - ? new Compressed( options ) - : new Identity( options ); - - const code = compiler.compile( node ); - return code; -} diff --git a/packages/block-editor/src/utils/transform-styles/index.js b/packages/block-editor/src/utils/transform-styles/index.js index c43e816d401e9c..8f5e1702307a45 100644 --- a/packages/block-editor/src/utils/transform-styles/index.js +++ b/packages/block-editor/src/utils/transform-styles/index.js @@ -1,36 +1,36 @@ /** - * WordPress dependencies + * External dependencies */ -import { compose } from '@wordpress/compose'; - -/** - * Internal dependencies - */ -import traverse from './traverse'; -import urlRewrite from './transforms/url-rewrite'; -import wrap from './transforms/wrap'; +import postcss from 'postcss'; +import wrap from 'postcss-prefixwrap'; +import rebaseUrl from 'postcss-urlrebase'; /** * Applies a series of CSS rule transforms to wrap selectors inside a given class and/or rewrite URLs depending on the parameters passed. * - * @param {Object|Array} styles CSS rules. - * @param {string} wrapperClassName Wrapper Class Name. + * @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. + * + * @param {EditorStyle[]} styles CSS rules. + * @param {string} wrapperSelector Wrapper selector. * @return {Array} converted rules. */ -const transformStyles = ( styles, wrapperClassName = '' ) => { - return Object.values( styles ?? [] ).map( ( { css, baseURL } ) => { - const transforms = []; - if ( wrapperClassName ) { - transforms.push( wrap( wrapperClassName ) ); - } - if ( baseURL ) { - transforms.push( urlRewrite( baseURL ) ); - } - if ( transforms.length ) { - return traverse( css, compose( transforms ) ); - } - - return css; +const transformStyles = ( styles, wrapperSelector = '' ) => { + return styles.map( ( { css, ignoredSelectors = [], baseURL } ) => { + return postcss( + [ + wrapperSelector && + wrap( wrapperSelector, { + ignoredSelectors: [ + ...ignoredSelectors, + wrapperSelector, + ], + } ), + baseURL && rebaseUrl( { rootUrl: baseURL } ), + ].filter( Boolean ) + ).process( css, {} ).css; // use sync PostCSS API } ); }; diff --git a/packages/block-editor/src/utils/transform-styles/test/__snapshots__/traverse.js.snap b/packages/block-editor/src/utils/transform-styles/test/__snapshots__/traverse.js.snap deleted file mode 100644 index 1ff3cab7d63365..00000000000000 --- a/packages/block-editor/src/utils/transform-styles/test/__snapshots__/traverse.js.snap +++ /dev/null @@ -1,7 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`CSS traverse Should traverse the CSS 1`] = ` -"namespace h1 { -color: red; -}" -`; diff --git a/packages/block-editor/src/utils/transform-styles/test/traverse.js b/packages/block-editor/src/utils/transform-styles/test/traverse.js deleted file mode 100644 index bb1be2635fe535..00000000000000 --- a/packages/block-editor/src/utils/transform-styles/test/traverse.js +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Internal dependencies - */ -import traverse from '../traverse'; - -describe( 'CSS traverse', () => { - it( 'Should traverse the CSS', () => { - const input = `h1 { color: red; }`; - const output = traverse( input, ( node ) => { - if ( node.type === 'rule' ) { - return { - ...node, - selectors: node.selectors.map( - ( selector ) => 'namespace ' + selector - ), - }; - } - - return node; - } ); - - expect( output ).toMatchSnapshot(); - } ); -} ); diff --git a/packages/block-editor/src/utils/transform-styles/transforms/test/__snapshots__/url-rewrite.js.snap b/packages/block-editor/src/utils/transform-styles/transforms/test/__snapshots__/url-rewrite.js.snap deleted file mode 100644 index 48aaf43221e7d5..00000000000000 --- a/packages/block-editor/src/utils/transform-styles/transforms/test/__snapshots__/url-rewrite.js.snap +++ /dev/null @@ -1,25 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`URL rewrite should not replace absolute paths 1`] = ` -"h1 { -background: url(/images/test.png); -}" -`; - -exports[`URL rewrite should not replace remote paths 1`] = ` -"h1 { -background: url(http://wp.org/images/test.png); -}" -`; - -exports[`URL rewrite should replace complex relative paths 1`] = ` -"h1 { -background: url(http://wp-site.local/themes/gut/images/test.png); -}" -`; - -exports[`URL rewrite should replace relative paths 1`] = ` -"h1 { -background: url(http://wp-site.local/themes/gut/css/images/test.png); -}" -`; diff --git a/packages/block-editor/src/utils/transform-styles/transforms/test/__snapshots__/wrap.js.snap b/packages/block-editor/src/utils/transform-styles/transforms/test/__snapshots__/wrap.js.snap deleted file mode 100644 index b9815cdc700b38..00000000000000 --- a/packages/block-editor/src/utils/transform-styles/transforms/test/__snapshots__/wrap.js.snap +++ /dev/null @@ -1,64 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`CSS selector wrap should ignore font-face selectors 1`] = ` -"@font-face { -font-family: myFirstFont; -src: url(sansation_light.woff); -}" -`; - -exports[`CSS selector wrap should ignore keyframes 1`] = ` -"@keyframes edit-post__fade-in-animation { -from { -opacity: 0; -} -}" -`; - -exports[`CSS selector wrap should ignore selectors 1`] = ` -".my-namespace h1, -body { -color: red; -}" -`; - -exports[`CSS selector wrap should not double wrap selectors 1`] = ` -".my-namespace h1, -.my-namespace .red { -color: red; -}" -`; - -exports[`CSS selector wrap should replace :root selectors 1`] = ` -".my-namespace { ---my-color: #ff0000; -}" -`; - -exports[`CSS selector wrap should replace root tags 1`] = ` -".my-namespace, -.my-namespace h1 { -color: red; -}" -`; - -exports[`CSS selector wrap should wrap multiple selectors 1`] = ` -".my-namespace h1, -.my-namespace h2 { -color: red; -}" -`; - -exports[`CSS selector wrap should wrap regular selectors 1`] = ` -".my-namespace h1 { -color: red; -}" -`; - -exports[`CSS selector wrap should wrap selectors inside container queries 1`] = ` -"@container (width > 400px) { -.my-namespace h1 { -color: red; -} -}" -`; diff --git a/packages/block-editor/src/utils/transform-styles/transforms/test/url-rewrite.js b/packages/block-editor/src/utils/transform-styles/transforms/test/url-rewrite.js deleted file mode 100644 index abbbf0754187e3..00000000000000 --- a/packages/block-editor/src/utils/transform-styles/transforms/test/url-rewrite.js +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Internal dependencies - */ -import traverse from '../../traverse'; -import rewrite from '../url-rewrite'; - -describe( 'URL rewrite', () => { - it( 'should replace relative paths', () => { - const callback = rewrite( 'http://wp-site.local/themes/gut/css/' ); - const input = `h1 { background: url(images/test.png); }`; - const output = traverse( input, callback ); - - expect( output ).toMatchSnapshot(); - } ); - - it( 'should replace complex relative paths', () => { - const callback = rewrite( 'http://wp-site.local/themes/gut/css/' ); - const input = `h1 { background: url(../images/test.png); }`; - const output = traverse( input, callback ); - - expect( output ).toMatchSnapshot(); - } ); - - it( 'should not replace absolute paths', () => { - const callback = rewrite( 'http://wp-site.local/themes/gut/css/' ); - const input = `h1 { background: url(/images/test.png); }`; - const output = traverse( input, callback ); - - expect( output ).toMatchSnapshot(); - } ); - - it( 'should not replace remote paths', () => { - const callback = rewrite( 'http://wp-site.local/themes/gut/css/' ); - const input = `h1 { background: url(http://wp.org/images/test.png); }`; - const output = traverse( input, callback ); - - expect( output ).toMatchSnapshot(); - } ); -} ); diff --git a/packages/block-editor/src/utils/transform-styles/transforms/test/wrap.js b/packages/block-editor/src/utils/transform-styles/transforms/test/wrap.js deleted file mode 100644 index a1f4f141d21c9b..00000000000000 --- a/packages/block-editor/src/utils/transform-styles/transforms/test/wrap.js +++ /dev/null @@ -1,95 +0,0 @@ -/** - * Internal dependencies - */ -import traverse from '../../traverse'; -import wrap from '../wrap'; - -describe( 'CSS selector wrap', () => { - it( 'should wrap regular selectors', () => { - const callback = wrap( '.my-namespace' ); - const input = `h1 { color: red; }`; - const output = traverse( input, callback ); - - expect( output ).toMatchSnapshot(); - } ); - - it( 'should wrap multiple selectors', () => { - const callback = wrap( '.my-namespace' ); - const input = `h1, h2 { color: red; }`; - const output = traverse( input, callback ); - - expect( output ).toMatchSnapshot(); - } ); - - it( 'should ignore selectors', () => { - const callback = wrap( '.my-namespace', [ 'body' ] ); - const input = `h1, body { color: red; }`; - const output = traverse( input, callback ); - - expect( output ).toMatchSnapshot(); - } ); - - it( 'should replace root tags', () => { - const callback = wrap( '.my-namespace' ); - const input = `body, h1 { color: red; }`; - const output = traverse( input, callback ); - - expect( output ).toMatchSnapshot(); - } ); - - it( 'should ignore keyframes', () => { - const callback = wrap( '.my-namespace' ); - const input = ` - @keyframes edit-post__fade-in-animation { - from { - opacity: 0; - } - }`; - const output = traverse( input, callback ); - - expect( output ).toMatchSnapshot(); - } ); - - it( 'should wrap selectors inside container queries', () => { - const callback = wrap( '.my-namespace' ); - const input = ` - @container (width > 400px) { - h1 { color: red; } - }`; - const output = traverse( input, callback ); - - expect( output ).toMatchSnapshot(); - } ); - - it( 'should ignore font-face selectors', () => { - const callback = wrap( '.my-namespace' ); - const input = ` - @font-face { - font-family: myFirstFont; - src: url(sansation_light.woff); - }`; - const output = traverse( input, callback ); - - expect( output ).toMatchSnapshot(); - } ); - - it( 'should replace :root selectors', () => { - const callback = wrap( '.my-namespace' ); - const input = ` - :root { - --my-color: #ff0000; - }`; - const output = traverse( input, callback ); - - expect( output ).toMatchSnapshot(); - } ); - - it( 'should not double wrap selectors', () => { - const callback = wrap( '.my-namespace' ); - const input = ` .my-namespace h1, .red { color: red; }`; - - const output = traverse( input, callback ); - - expect( output ).toMatchSnapshot(); - } ); -} ); diff --git a/packages/block-editor/src/utils/transform-styles/transforms/url-rewrite.js b/packages/block-editor/src/utils/transform-styles/transforms/url-rewrite.js deleted file mode 100644 index e3461cb1088d78..00000000000000 --- a/packages/block-editor/src/utils/transform-styles/transforms/url-rewrite.js +++ /dev/null @@ -1,139 +0,0 @@ -/** - * Return `true` if the given path is http/https. - * - * @param {string} filePath path - * - * @return {boolean} is remote path. - */ -function isRemotePath( filePath ) { - return /^(?:https?:)?\/\//.test( filePath ); -} - -/** - * Return `true` if the given filePath is an absolute url. - * - * @param {string} filePath path - * - * @return {boolean} is absolute path. - */ -function isAbsolutePath( filePath ) { - return /^\/(?!\/)/.test( filePath ); -} - -/** - * Whether or not the url should be inluded. - * - * @param {Object} meta url meta info - * - * @return {boolean} is valid. - */ -function isValidURL( meta ) { - // Ignore hashes or data uris. - if ( - meta.value.indexOf( 'data:' ) === 0 || - meta.value.indexOf( '#' ) === 0 - ) { - return false; - } - - if ( isAbsolutePath( meta.value ) ) { - return false; - } - - // Do not handle the http/https urls if `includeRemote` is false. - if ( isRemotePath( meta.value ) ) { - return false; - } - - return true; -} - -/** - * Get the absolute path of the url, relative to the basePath - * - * @param {string} str the url - * @param {string} baseURL base URL - * - * @return {string} the full path to the file - */ -function getResourcePath( str, baseURL ) { - return new URL( str, baseURL ).toString(); -} - -/** - * Process the single `url()` pattern - * - * @param {string} baseURL the base URL for relative URLs. - * - * @return {Promise} the Promise. - */ -function processURL( baseURL ) { - return ( meta ) => ( { - ...meta, - newUrl: - 'url(' + - meta.before + - meta.quote + - getResourcePath( meta.value, baseURL ) + - meta.quote + - meta.after + - ')', - } ); -} - -/** - * Get all `url()`s, and return the meta info - * - * @param {string} value decl.value. - * - * @return {Array} the urls. - */ -function getURLs( value ) { - const reg = /url\((\s*)(['"]?)(.+?)\2(\s*)\)/g; - let match; - const URLs = []; - - while ( ( match = reg.exec( value ) ) !== null ) { - const meta = { - source: match[ 0 ], - before: match[ 1 ], - quote: match[ 2 ], - value: match[ 3 ], - after: match[ 4 ], - }; - if ( isValidURL( meta ) ) { - URLs.push( meta ); - } - } - return URLs; -} - -/** - * Replace the raw value's `url()` segment to the new value - * - * @param {string} raw the raw value. - * @param {Array} URLs the URLs to replace. - * - * @return {string} the new value. - */ -function replaceURLs( raw, URLs ) { - URLs.forEach( ( item ) => { - raw = raw.replace( item.source, item.newUrl ); - } ); - - return raw; -} - -const rewrite = ( rootURL ) => ( node ) => { - if ( node.type === 'declaration' ) { - const updatedURLs = getURLs( node.value ).map( processURL( rootURL ) ); - return { - ...node, - value: replaceURLs( node.value, updatedURLs ), - }; - } - - return node; -}; - -export default rewrite; diff --git a/packages/block-editor/src/utils/transform-styles/transforms/wrap.js b/packages/block-editor/src/utils/transform-styles/transforms/wrap.js deleted file mode 100644 index 74b940f80352b9..00000000000000 --- a/packages/block-editor/src/utils/transform-styles/transforms/wrap.js +++ /dev/null @@ -1,56 +0,0 @@ -/** - * @constant string IS_ROOT_TAG Regex to check if the selector is a root tag selector. - */ -const IS_ROOT_TAG = /^(body|html|:root).*$/; - -/** - * Creates a callback to modify selectors so they only apply within a certain - * namespace. - * - * @param {string} namespace Namespace to prefix selectors with. - * @param {string[]} ignore Selectors to not prefix. - * - * @return {(node: Object) => Object} Callback to wrap selectors. - */ -const wrap = - ( namespace, ignore = [] ) => - ( node ) => { - /** - * Updates selector if necessary. - * - * @param {string} selector Selector to modify. - * - * @return {string} Updated selector. - */ - const updateSelector = ( selector ) => { - if ( ignore.includes( selector.trim() ) ) { - return selector; - } - - // Skip the update when a selector already has a namespace + space (" "). - if ( selector.trim().startsWith( `${ namespace } ` ) ) { - return selector; - } - - // Anything other than a root tag is always prefixed. - { - if ( ! selector.match( IS_ROOT_TAG ) ) { - return namespace + ' ' + selector; - } - } - - // HTML and Body elements cannot be contained within our container so lets extract their styles. - return selector.replace( /^(body|html|:root)/, namespace ); - }; - - if ( node.type === 'rule' ) { - return { - ...node, - selectors: node.selectors.map( updateSelector ), - }; - } - - return node; - }; - -export default wrap; diff --git a/packages/block-editor/src/utils/transform-styles/traverse.js b/packages/block-editor/src/utils/transform-styles/traverse.js deleted file mode 100644 index 28ad59b4ea7996..00000000000000 --- a/packages/block-editor/src/utils/transform-styles/traverse.js +++ /dev/null @@ -1,32 +0,0 @@ -/** - * External dependencies - */ -import traverse from 'traverse'; - -/** - * Internal dependencies - */ -import { parse, stringify } from './ast'; - -function traverseCSS( css, callback ) { - try { - const parsed = parse( css ); - - const updated = traverse.map( parsed, function ( node ) { - if ( ! node ) { - return node; - } - const updatedNode = callback( node ); - return this.update( updatedNode ); - } ); - - return stringify( updated ); - } catch ( err ) { - // eslint-disable-next-line no-console - console.warn( 'Error while traversing the CSS: ' + err ); - - return null; - } -} - -export default traverseCSS; diff --git a/packages/block-library/src/html/preview.js b/packages/block-library/src/html/preview.js index 4d3fe5f41915a6..d515c0119aa8db 100644 --- a/packages/block-library/src/html/preview.js +++ b/packages/block-library/src/html/preview.js @@ -22,12 +22,17 @@ const DEFAULT_STYLES = ` `; export default function HTMLEditPreview( { content, isSelected } ) { - const settingStyles = useSelect( ( select ) => { - return select( blockEditorStore ).getSettings()?.styles; - }, [] ); + const settingStyles = useSelect( + ( select ) => select( blockEditorStore ).getSettings().styles + ); const styles = useMemo( - () => [ DEFAULT_STYLES, ...transformStyles( settingStyles ) ], + () => [ + DEFAULT_STYLES, + ...transformStyles( + settingStyles.filter( ( style ) => style.css ) + ), + ], [ settingStyles ] ); From 2a8d5b5a8165fc5cb3ee6658ff81a9384ae01cdd Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sat, 21 Oct 2023 20:52:14 +0200 Subject: [PATCH 10/14] Scripts: Fix typo in readme (#55531) --- packages/scripts/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/scripts/README.md b/packages/scripts/README.md index 8e80d806b17e7d..89ae3f1fff05c5 100644 --- a/packages/scripts/README.md +++ b/packages/scripts/README.md @@ -491,7 +491,7 @@ Should there be any situation where you want to provide your own Jest config, yo - there is a file called `jest-unit.config.js`, `jest-unit.config.json`, `jest.config.js`, or `jest.config.json` in the top-level directory of your package (at the same level than your `package.json`). - a `jest` object can be provided in the `package.json` file with the test configuration. -### `test-plyawright` +### `test-playwright` Launches the Playwright End-To-End (E2E) test runner. Similar to Puppeteer, it provides a high-level API to control a headless browser. From 2788a9cf8b8149be3ee52dd15ce91fa55815f36a Mon Sep 17 00:00:00 2001 From: Kevin Batdorf Date: Sun, 22 Oct 2023 17:13:49 -0400 Subject: [PATCH 11/14] [create-block] Add ABSPATH check (#55533) * Add ABSPATH check * User perf plugin syntax --- .../plugin-templates/$slug.php.mustache | 4 ++++ .../plugin-templates/$slug.php.mustache | 4 ++++ packages/create-block/lib/templates/es5/$slug.php.mustache | 4 ++++ packages/create-block/lib/templates/plugin/$slug.php.mustache | 4 ++++ 4 files changed, 16 insertions(+) diff --git a/packages/create-block-interactive-template/plugin-templates/$slug.php.mustache b/packages/create-block-interactive-template/plugin-templates/$slug.php.mustache index 2322881ab0d710..52c9c4966646fa 100644 --- a/packages/create-block-interactive-template/plugin-templates/$slug.php.mustache +++ b/packages/create-block-interactive-template/plugin-templates/$slug.php.mustache @@ -30,6 +30,10 @@ * @package {{namespace}} */ +if ( ! defined( 'ABSPATH' ) ) { + exit; // Exit if accessed directly. +} + /** * Registers the block using the metadata loaded from the `block.json` file. * Behind the scenes, it registers also all assets so they can be enqueued diff --git a/packages/create-block-tutorial-template/plugin-templates/$slug.php.mustache b/packages/create-block-tutorial-template/plugin-templates/$slug.php.mustache index 2322881ab0d710..52c9c4966646fa 100644 --- a/packages/create-block-tutorial-template/plugin-templates/$slug.php.mustache +++ b/packages/create-block-tutorial-template/plugin-templates/$slug.php.mustache @@ -30,6 +30,10 @@ * @package {{namespace}} */ +if ( ! defined( 'ABSPATH' ) ) { + exit; // Exit if accessed directly. +} + /** * Registers the block using the metadata loaded from the `block.json` file. * Behind the scenes, it registers also all assets so they can be enqueued diff --git a/packages/create-block/lib/templates/es5/$slug.php.mustache b/packages/create-block/lib/templates/es5/$slug.php.mustache index a04553af1e7d8b..0f64471e434a7f 100644 --- a/packages/create-block/lib/templates/es5/$slug.php.mustache +++ b/packages/create-block/lib/templates/es5/$slug.php.mustache @@ -30,6 +30,10 @@ * @package {{namespace}} */ +if ( ! defined( 'ABSPATH' ) ) { + exit; // Exit if accessed directly. +} + /** * Registers all block assets so that they can be enqueued through the block editor * in the corresponding context. diff --git a/packages/create-block/lib/templates/plugin/$slug.php.mustache b/packages/create-block/lib/templates/plugin/$slug.php.mustache index 2ed0354314cc47..90f293f1472f4e 100644 --- a/packages/create-block/lib/templates/plugin/$slug.php.mustache +++ b/packages/create-block/lib/templates/plugin/$slug.php.mustache @@ -30,6 +30,10 @@ * @package {{namespace}} */ +if ( ! defined( 'ABSPATH' ) ) { + exit; // Exit if accessed directly. +} + /** * Registers the block using the metadata loaded from the `block.json` file. * Behind the scenes, it registers also all assets so they can be enqueued From ade7e648250795e0ea4b89a6a3b905b5c61bce97 Mon Sep 17 00:00:00 2001 From: Bart Kalisz Date: Mon, 23 Oct 2023 13:38:48 +0000 Subject: [PATCH 12/14] Performance tests: Fix canvas locator timeout (#55441) --- test/performance/fixtures/perf-utils.ts | 53 ++++++++++------------ test/performance/playwright.config.ts | 1 + test/performance/specs/post-editor.spec.js | 4 +- test/performance/specs/site-editor.spec.js | 6 +-- 4 files changed, 28 insertions(+), 36 deletions(-) diff --git a/test/performance/fixtures/perf-utils.ts b/test/performance/fixtures/perf-utils.ts index a61af86684e6bb..dcd9579364e10b 100644 --- a/test/performance/fixtures/perf-utils.ts +++ b/test/performance/fixtures/perf-utils.ts @@ -33,26 +33,19 @@ export class PerfUtils { * @return Locator for the editor canvas element. */ async getCanvas() { - return await Promise.any( [ - ( async () => { - const legacyCanvasLocator = this.page.locator( - '.wp-block-post-content' - ); - await legacyCanvasLocator.waitFor( { - timeout: 120_000, - } ); - return legacyCanvasLocator; - } )(), - ( async () => { - const iframedCanvasLocator = this.page.frameLocator( - '[name=editor-canvas]' - ); - await iframedCanvasLocator - .locator( 'body' ) - .waitFor( { timeout: 120_000 } ); - return iframedCanvasLocator; - } )(), - ] ); + const canvasLocator = this.page.locator( + '.wp-block-post-content, iframe[name=editor-canvas]' + ); + + const isFramed = await canvasLocator.evaluate( + ( node ) => node.tagName === 'IFRAME' + ); + + if ( isFramed ) { + return canvasLocator.frameLocator( ':scope' ); + } + + return canvasLocator; } /** @@ -61,9 +54,7 @@ export class PerfUtils { * @return URL of the saved draft. */ async saveDraft() { - await this.page - .getByRole( 'button', { name: 'Save draft' } ) - .click( { timeout: 60_000 } ); + await this.page.getByRole( 'button', { name: 'Save draft' } ).click(); await expect( this.page.getByRole( 'button', { name: 'Saved' } ) ).toBeDisabled(); @@ -75,6 +66,8 @@ export class PerfUtils { * Disables the editor autosave function. */ async disableAutosave() { + await this.page.waitForFunction( () => window?.wp?.data ); + await this.page.evaluate( () => { return window.wp.data .dispatch( 'core/editor' ) @@ -83,12 +76,6 @@ export class PerfUtils { localAutosaveInterval: 100000000000, } ); } ); - - const { autosaveInterval } = await this.page.evaluate( () => { - return window.wp.data.select( 'core/editor' ).getEditorSettings(); - } ); - - expect( autosaveInterval ).toBe( 100000000000 ); } /** @@ -139,6 +126,10 @@ export class PerfUtils { throw new Error( `File not found: ${ filepath }` ); } + await this.page.waitForFunction( + () => window?.wp?.blocks && window?.wp?.data + ); + return await this.page.evaluate( ( html: string ) => { const { parse } = window.wp.blocks; const { dispatch } = window.wp.data; @@ -159,6 +150,10 @@ export class PerfUtils { * Generates and loads a 1000 empty paragraphs into the editor canvas. */ async load1000Paragraphs() { + await this.page.waitForFunction( + () => window?.wp?.blocks && window?.wp?.data + ); + await this.page.evaluate( () => { const { createBlock } = window.wp.blocks; const { dispatch } = window.wp.data; diff --git a/test/performance/playwright.config.ts b/test/performance/playwright.config.ts index a8208342ac2d81..ed221b1dc7bfbe 100644 --- a/test/performance/playwright.config.ts +++ b/test/performance/playwright.config.ts @@ -27,6 +27,7 @@ const config = defineConfig( { ), use: { ...baseConfig.use, + actionTimeout: 120_000, // 2 minutes. video: 'off', }, } ); diff --git a/test/performance/specs/post-editor.spec.js b/test/performance/specs/post-editor.spec.js index 7f590330465278..da20e3c3e667b5 100644 --- a/test/performance/specs/post-editor.spec.js +++ b/test/performance/specs/post-editor.spec.js @@ -70,9 +70,7 @@ test.describe( 'Post Editor Performance', () => { const canvas = await perfUtils.getCanvas(); // Wait for the first block. - await canvas.locator( '.wp-block' ).first().waitFor( { - timeout: 120_000, - } ); + await canvas.locator( '.wp-block' ).first().waitFor(); // Get the durations. const loadingDurations = await metrics.getLoadingDurations(); diff --git a/test/performance/specs/site-editor.spec.js b/test/performance/specs/site-editor.spec.js index f2f211dd52e6e0..28a1cbb0ecde29 100644 --- a/test/performance/specs/site-editor.spec.js +++ b/test/performance/specs/site-editor.spec.js @@ -86,9 +86,7 @@ test.describe( 'Site Editor Performance', () => { const canvas = await perfUtils.getCanvas(); // Wait for the first block. - await canvas.locator( '.wp-block' ).first().waitFor( { - timeout: 120_000, - } ); + await canvas.locator( '.wp-block' ).first().waitFor(); // Get the durations. const loadingDurations = await metrics.getLoadingDurations(); @@ -142,7 +140,7 @@ test.describe( 'Site Editor Performance', () => { // Spinner was used instead of the progress bar in an earlier version of the site editor. '.edit-site-canvas-loader, .edit-site-canvas-spinner' ) - .waitFor( { state: 'hidden', timeout: 120_000 } ); + .waitFor( { state: 'hidden' } ); const canvas = await perfUtils.getCanvas(); From b1c52f3ea1c4579e009b80954e90c3ef36b270cf Mon Sep 17 00:00:00 2001 From: Glen Davies Date: Tue, 24 Oct 2023 04:48:00 +1300 Subject: [PATCH 13/14] Revert "Patterns: fix capabilities settings for pattern categories (#55379)" (#55532) This reverts commit 5b10d27ec406cbdec84e14408cd350ba9e8c544a. --- lib/compat/wordpress-6.4/block-patterns.php | 23 ++-- lib/compat/wordpress-6.4/blocks.php | 24 ---- ...s-gutenberg-rest-blocks-controller-6-4.php | 75 ----------- ...erg-rest-pattern-categories-controller.php | 45 ------- lib/load.php | 2 - packages/core-data/src/resolvers.js | 1 - packages/editor/src/components/index.js | 1 - .../pattern-categories-selector.js | 119 ------------------ .../src/components/category-editor.js | 97 -------------- .../src/components/category-selector.js | 89 +++++++------ .../src/components/create-pattern-modal.js | 75 +++++------ .../rename-pattern-category-modal.js | 2 +- packages/patterns/src/components/style.scss | 23 ---- packages/patterns/src/private-apis.js | 3 - 14 files changed, 96 insertions(+), 483 deletions(-) delete mode 100644 lib/compat/wordpress-6.4/class-gutenberg-rest-blocks-controller-6-4.php delete mode 100644 lib/compat/wordpress-6.4/class-gutenberg-rest-pattern-categories-controller.php delete mode 100644 packages/editor/src/components/post-taxonomies/pattern-categories-selector.js delete mode 100644 packages/patterns/src/components/category-editor.js diff --git a/lib/compat/wordpress-6.4/block-patterns.php b/lib/compat/wordpress-6.4/block-patterns.php index bbb910ff400d01..922dea910b47a0 100644 --- a/lib/compat/wordpress-6.4/block-patterns.php +++ b/lib/compat/wordpress-6.4/block-patterns.php @@ -16,21 +16,20 @@ */ function gutenberg_register_taxonomy_patterns() { $args = array( - 'public' => true, - 'publicly_queryable' => false, - 'hierarchical' => false, - 'labels' => array( + 'public' => true, + 'publicly_queryable' => false, + 'hierarchical' => false, + 'labels' => array( 'name' => _x( 'Pattern Categories', 'taxonomy general name' ), 'singular_name' => _x( 'Pattern Category', 'taxonomy singular name' ), ), - 'query_var' => false, - 'rewrite' => false, - 'show_ui' => true, - '_builtin' => true, - 'show_in_nav_menus' => false, - 'show_in_rest' => true, - 'show_admin_column' => true, - 'rest_controller_class' => 'Gutenberg_REST_Pattern_Categories_Controller', + 'query_var' => false, + 'rewrite' => false, + 'show_ui' => true, + '_builtin' => true, + 'show_in_nav_menus' => false, + 'show_in_rest' => true, + 'show_admin_column' => true, ); register_taxonomy( 'wp_pattern_category', array( 'wp_block' ), $args ); } diff --git a/lib/compat/wordpress-6.4/blocks.php b/lib/compat/wordpress-6.4/blocks.php index 073302dbab65f8..74fa9253e45d50 100644 --- a/lib/compat/wordpress-6.4/blocks.php +++ b/lib/compat/wordpress-6.4/blocks.php @@ -21,27 +21,3 @@ function gutenberg_add_custom_capabilities_to_wp_block( $args ) { return $args; } add_filter( 'register_wp_block_post_type_args', 'gutenberg_add_custom_capabilities_to_wp_block', 10, 1 ); - -/** - * Updates the wp_block REST enpoint in order to modify the wp_pattern_category action - * links that are returned because as although the taxonomy is flat Author level users - * are only allowed to assign categories. - * - * Note: This should be removed when the minimum required WP version is >= 6.4. - * - * @see https://github.com/WordPress/gutenberg/pull/55379 - * - * @param array $args Register post type args. - * @param string $post_type The post type string. - * - * @return array Register post type args. - */ -function gutenberg_update_patterns_block_rest_controller_class( $args, $post_type ) { - if ( 'wp_block' === $post_type ) { - $args['rest_controller_class'] = 'Gutenberg_REST_Blocks_Controller_6_4'; - } - - return $args; -} - -add_filter( 'register_post_type_args', 'gutenberg_update_patterns_block_rest_controller_class', 11, 2 ); diff --git a/lib/compat/wordpress-6.4/class-gutenberg-rest-blocks-controller-6-4.php b/lib/compat/wordpress-6.4/class-gutenberg-rest-blocks-controller-6-4.php deleted file mode 100644 index bc91492e269791..00000000000000 --- a/lib/compat/wordpress-6.4/class-gutenberg-rest-blocks-controller-6-4.php +++ /dev/null @@ -1,75 +0,0 @@ -post_type ); - - if ( 'attachment' !== $this->post_type && current_user_can( $post_type->cap->publish_posts ) ) { - $rels[] = 'https://api.w.org/action-publish'; - } - - if ( current_user_can( 'unfiltered_html' ) ) { - $rels[] = 'https://api.w.org/action-unfiltered-html'; - } - - if ( 'post' === $post_type->name ) { - if ( current_user_can( $post_type->cap->edit_others_posts ) && current_user_can( $post_type->cap->publish_posts ) ) { - $rels[] = 'https://api.w.org/action-sticky'; - } - } - - if ( post_type_supports( $post_type->name, 'author' ) ) { - if ( current_user_can( $post_type->cap->edit_others_posts ) ) { - $rels[] = 'https://api.w.org/action-assign-author'; - } - } - - $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) ); - - foreach ( $taxonomies as $tax ) { - $tax_base = ! empty( $tax->rest_base ) ? $tax->rest_base : $tax->name; - - if ( current_user_can( $tax->cap->edit_terms ) ) { - $rels[] = 'https://api.w.org/action-create-' . $tax_base; - } - - if ( current_user_can( $tax->cap->assign_terms ) ) { - $rels[] = 'https://api.w.org/action-assign-' . $tax_base; - } - } - - return $rels; - } -} diff --git a/lib/compat/wordpress-6.4/class-gutenberg-rest-pattern-categories-controller.php b/lib/compat/wordpress-6.4/class-gutenberg-rest-pattern-categories-controller.php deleted file mode 100644 index e249d67e8acaa4..00000000000000 --- a/lib/compat/wordpress-6.4/class-gutenberg-rest-pattern-categories-controller.php +++ /dev/null @@ -1,45 +0,0 @@ -check_is_taxonomy_allowed( $this->taxonomy ) ) { - return false; - } - - $taxonomy_obj = get_taxonomy( $this->taxonomy ); - - // Patterns categories are a flat hierarchy (like tags), but work more like post categories in terms of permissions. - if ( ! current_user_can( $taxonomy_obj->cap->edit_terms ) ) { - return new WP_Error( - 'rest_cannot_create', - __( 'Sorry, you are not allowed to create terms in this taxonomy.' ), - array( 'status' => rest_authorization_required_code() ) - ); - } - - return true; - } -} diff --git a/lib/load.php b/lib/load.php index 381248e0f44bf0..2b178af5fb9bfe 100644 --- a/lib/load.php +++ b/lib/load.php @@ -53,8 +53,6 @@ function gutenberg_is_experiment_enabled( $name ) { // WordPress 6.4 compat. require_once __DIR__ . '/compat/wordpress-6.4/class-gutenberg-rest-global-styles-revisions-controller-6-4.php'; require_once __DIR__ . '/compat/wordpress-6.4/class-gutenberg-rest-block-patterns-controller.php'; - require_once __DIR__ . '/compat/wordpress-6.4/class-gutenberg-rest-blocks-controller-6-4.php'; - require_once __DIR__ . '/compat/wordpress-6.4/class-gutenberg-rest-pattern-categories-controller.php'; require_once __DIR__ . '/compat/wordpress-6.4/rest-api.php'; require_once __DIR__ . '/compat/wordpress-6.4/theme-previews.php'; diff --git a/packages/core-data/src/resolvers.js b/packages/core-data/src/resolvers.js index 5fc7cd14f35c0b..07e9cd98cb5ec3 100644 --- a/packages/core-data/src/resolvers.js +++ b/packages/core-data/src/resolvers.js @@ -651,7 +651,6 @@ export const getUserPatternCategories = { per_page: -1, _fields: 'id,name,description,slug', - context: 'view', } ); diff --git a/packages/editor/src/components/index.js b/packages/editor/src/components/index.js index 99de8c83c6fbe1..39b562806c109a 100644 --- a/packages/editor/src/components/index.js +++ b/packages/editor/src/components/index.js @@ -62,7 +62,6 @@ export { export { default as PostTaxonomies } from './post-taxonomies'; export { FlatTermSelector as PostTaxonomiesFlatTermSelector } from './post-taxonomies/flat-term-selector'; export { HierarchicalTermSelector as PostTaxonomiesHierarchicalTermSelector } from './post-taxonomies/hierarchical-term-selector'; -export { PatternCategoriesSelector as PostPatternCategoriesSelector } from './post-taxonomies/pattern-categories-selector'; export { default as PostTaxonomiesCheck } from './post-taxonomies/check'; export { default as PostTextEditor } from './post-text-editor'; export { default as PostTitle } from './post-title'; diff --git a/packages/editor/src/components/post-taxonomies/pattern-categories-selector.js b/packages/editor/src/components/post-taxonomies/pattern-categories-selector.js deleted file mode 100644 index ac6a60aa009349..00000000000000 --- a/packages/editor/src/components/post-taxonomies/pattern-categories-selector.js +++ /dev/null @@ -1,119 +0,0 @@ -/** - * WordPress dependencies - */ -import { useSelect, useDispatch } from '@wordpress/data'; -import { store as coreStore } from '@wordpress/core-data'; -import { addFilter } from '@wordpress/hooks'; -import { privateApis as patternsPrivateApis } from '@wordpress/patterns'; - -/** - * Internal dependencies - */ -import { unlock } from '../../lock-unlock'; -import { store as editorStore } from '../../store'; - -const { CategorySelector } = unlock( patternsPrivateApis ); - -const EMPTY_ARRAY = []; - -const DEFAULT_QUERY = { - per_page: -1, - orderby: 'name', - order: 'asc', - _fields: 'id,name,parent', - context: 'view', -}; - -/* - * Pattern categories are a flat taxonomy but do not allow Author users and below to create - * new categories, so this selector overrides the default flat taxonomy selector for - * wp_block post types and users without 'create' capability for wp_pattern_category. - */ -export function PatternCategoriesSelector( { slug } ) { - const { hasAssignAction, terms, availableTerms, taxonomy, loading } = - useSelect( - ( select ) => { - const { getCurrentPost, getEditedPostAttribute } = - select( editorStore ); - const { getTaxonomy, getEntityRecords, isResolving } = - select( coreStore ); - const _taxonomy = getTaxonomy( slug ); - const post = getCurrentPost(); - - return { - hasAssignAction: _taxonomy - ? post._links?.[ - 'wp:action-assign-' + _taxonomy.rest_base - ] ?? false - : false, - terms: _taxonomy - ? getEditedPostAttribute( _taxonomy.rest_base ) - : EMPTY_ARRAY, - loading: isResolving( 'getEntityRecords', [ - 'taxonomy', - slug, - DEFAULT_QUERY, - ] ), - availableTerms: - getEntityRecords( 'taxonomy', slug, DEFAULT_QUERY ) || - EMPTY_ARRAY, - taxonomy: _taxonomy, - }; - }, - [ slug ] - ); - - const { editPost } = useDispatch( editorStore ); - - if ( ! hasAssignAction || loading || availableTerms.length === 0 ) { - return null; - } - - const onUpdateTerms = ( termIds ) => { - editPost( { [ taxonomy.rest_base ]: termIds } ); - }; - - const onChange = ( term ) => { - const hasTerm = terms.includes( term.id ); - const newTerms = hasTerm - ? terms.filter( ( id ) => id !== term.id ) - : [ ...terms, term.id ]; - onUpdateTerms( newTerms ); - }; - - const isCategorySelected = ( term ) => terms.includes( term.id ); - - const categoryOptions = availableTerms.map( ( term ) => ( { - ...term, - label: term.name, - } ) ); - - return ( - - ); -} - -export default function patternCategorySelector( OriginalComponent ) { - return function ( props ) { - const canAddCategories = useSelect( ( select ) => { - const { canUser } = select( coreStore ); - return canUser( 'create', 'wp_pattern_category' ); - } ); - if ( props.slug === 'wp_pattern_category' && ! canAddCategories ) { - return ; - } - - return ; - }; -} - -addFilter( - 'editor.PostTaxonomyType', - 'core/pattern-category-selector', - patternCategorySelector -); diff --git a/packages/patterns/src/components/category-editor.js b/packages/patterns/src/components/category-editor.js deleted file mode 100644 index a394013af333b2..00000000000000 --- a/packages/patterns/src/components/category-editor.js +++ /dev/null @@ -1,97 +0,0 @@ -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; -import { useMemo, useState } from '@wordpress/element'; -import { FormTokenField } from '@wordpress/components'; -import { useDebounce } from '@wordpress/compose'; -import { decodeEntities } from '@wordpress/html-entities'; - -/** - * Internal dependencies - */ -import CategorySelector from './category-selector'; - -const unescapeString = ( arg ) => decodeEntities( arg ); - -export const CATEGORY_SLUG = 'wp_pattern_category'; - -export default function CategoryEditor( { - categoryTerms, - onChange, - categoryMap, - canAddCategories, -} ) { - const categoryOptions = Array.from( categoryMap.values() ); - const [ search, setSearch ] = useState( '' ); - const debouncedSearch = useDebounce( setSearch, 500 ); - - const suggestions = useMemo( () => { - return Array.from( categoryMap.values() ) - .map( ( category ) => unescapeString( category.label ) ) - .filter( ( category ) => { - if ( search !== '' ) { - return category - .toLowerCase() - .includes( search.toLowerCase() ); - } - return true; - } ) - .sort( ( a, b ) => a.localeCompare( b ) ); - }, [ search, categoryMap ] ); - - function handleChange( termNames ) { - const uniqueTerms = termNames.reduce( ( terms, newTerm ) => { - if ( - ! terms.some( - ( term ) => term.toLowerCase() === newTerm.toLowerCase() - ) - ) { - terms.push( newTerm ); - } - return terms; - }, [] ); - - onChange( uniqueTerms ); - } - const isCategorySelected = ( selectedCategory ) => - categoryTerms.includes( selectedCategory.label ); - - const onCategorySelectChange = ( selectedCategory ) => { - if ( categoryTerms.includes( selectedCategory.label ) ) { - onChange( - categoryTerms.filter( - ( categoryTerm ) => categoryTerm !== selectedCategory.label - ) - ); - } else { - onChange( [ ...categoryTerms, selectedCategory.label ] ); - } - }; - - return ( - <> - { canAddCategories && ( - - ) } - { ! canAddCategories && categoryOptions.length > 0 && ( - - ) } - - ); -} diff --git a/packages/patterns/src/components/category-selector.js b/packages/patterns/src/components/category-selector.js index 84dae97e232f89..7f00350e278ecf 100644 --- a/packages/patterns/src/components/category-selector.js +++ b/packages/patterns/src/components/category-selector.js @@ -2,48 +2,65 @@ * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { CheckboxControl, BaseControl } from '@wordpress/components'; +import { useMemo, useState } from '@wordpress/element'; +import { FormTokenField } from '@wordpress/components'; +import { useDebounce } from '@wordpress/compose'; import { decodeEntities } from '@wordpress/html-entities'; +const unescapeString = ( arg ) => { + return decodeEntities( arg ); +}; + +export const CATEGORY_SLUG = 'wp_pattern_category'; + export default function CategorySelector( { + categoryTerms, onChange, - isCategorySelected, - categoryOptions, - showLabel = true, + categoryMap, } ) { - const renderTerms = ( renderedTerms ) => { - return renderedTerms.map( ( category ) => { - return ( -
- onChange( category ) } - label={ decodeEntities( category.label ) } - /> -
- ); - } ); - }; + const [ search, setSearch ] = useState( '' ); + const debouncedSearch = useDebounce( setSearch, 500 ); + + const suggestions = useMemo( () => { + return Array.from( categoryMap.values() ) + .map( ( category ) => unescapeString( category.label ) ) + .filter( ( category ) => { + if ( search !== '' ) { + return category + .toLowerCase() + .includes( search.toLowerCase() ); + } + return true; + } ) + .sort( ( a, b ) => a.localeCompare( b ) ); + }, [ search, categoryMap ] ); + + function handleChange( termNames ) { + const uniqueTerms = termNames.reduce( ( terms, newTerm ) => { + if ( + ! terms.some( + ( term ) => term.toLowerCase() === newTerm.toLowerCase() + ) + ) { + terms.push( newTerm ); + } + return terms; + }, [] ); + + onChange( uniqueTerms ); + } return ( - - { showLabel && ( - - { __( 'Categories' ) } - - ) } -
- { renderTerms( categoryOptions ) } -
-
+ ); } diff --git a/packages/patterns/src/components/create-pattern-modal.js b/packages/patterns/src/components/create-pattern-modal.js index 67953ecdee6685..22d20fd0372657 100644 --- a/packages/patterns/src/components/create-pattern-modal.js +++ b/packages/patterns/src/components/create-pattern-modal.js @@ -24,7 +24,7 @@ import { PATTERN_DEFAULT_CATEGORY, PATTERN_SYNC_TYPES } from '../constants'; * Internal dependencies */ import { store as patternsStore } from '../store'; -import CategoryEditor, { CATEGORY_SLUG } from './category-editor'; +import CategorySelector, { CATEGORY_SLUG } from './category-selector'; import { unlock } from '../lock-unlock'; export default function CreatePatternModal( { @@ -48,46 +48,41 @@ export default function CreatePatternModal( { const { saveEntityRecord, invalidateResolution } = useDispatch( coreStore ); const { createErrorNotice } = useDispatch( noticesStore ); - const { corePatternCategories, userPatternCategories, canAddCategories } = - useSelect( ( select ) => { - const { - getUserPatternCategories, - getBlockPatternCategories, - canUser, - } = select( coreStore ); + const { corePatternCategories, userPatternCategories } = useSelect( + ( select ) => { + const { getUserPatternCategories, getBlockPatternCategories } = + select( coreStore ); return { corePatternCategories: getBlockPatternCategories(), userPatternCategories: getUserPatternCategories(), - canAddCategories: canUser( 'create', 'wp_pattern_category' ), }; - } ); + } + ); const categoryMap = useMemo( () => { // Merge the user and core pattern categories and remove any duplicates. const uniqueCategories = new Map(); - [ - ...userPatternCategories, - ...( canAddCategories ? corePatternCategories : [] ), - ].forEach( ( category ) => { - if ( - ! uniqueCategories.has( category.label ) && - // There are two core categories with `Post` label so explicitly remove the one with - // the `query` slug to avoid any confusion. - category.name !== 'query' - ) { - // We need to store the name separately as this is used as the slug in the - // taxonomy and may vary from the label. - uniqueCategories.set( category.label, { - label: category.label, - value: category.label, - name: category.name, - id: category.id, - } ); + [ ...userPatternCategories, ...corePatternCategories ].forEach( + ( category ) => { + if ( + ! uniqueCategories.has( category.label ) && + // There are two core categories with `Post` label so explicitly remove the one with + // the `query` slug to avoid any confusion. + category.name !== 'query' + ) { + // We need to store the name separately as this is used as the slug in the + // taxonomy and may vary from the label. + uniqueCategories.set( category.label, { + label: category.label, + value: category.label, + name: category.name, + } ); + } } - } ); + ); return uniqueCategories; - }, [ userPatternCategories, corePatternCategories, canAddCategories ] ); + }, [ userPatternCategories, corePatternCategories ] ); async function onCreate( patternTitle, sync ) { if ( ! title || isSaving ) { @@ -96,18 +91,11 @@ export default function CreatePatternModal( { try { setIsSaving( true ); - let categories; - if ( canAddCategories ) { - categories = await Promise.all( - categoryTerms.map( ( termName ) => - findOrCreateTerm( termName ) - ) - ); - } else { - categories = categoryTerms.map( - ( term ) => categoryMap.get( term ).id - ); - } + const categories = await Promise.all( + categoryTerms.map( ( termName ) => + findOrCreateTerm( termName ) + ) + ); const newPattern = await createPattern( patternTitle, @@ -185,11 +173,10 @@ export default function CreatePatternModal( { placeholder={ __( 'My pattern' ) } className="patterns-create-modal__name-input" /> - Date: Tue, 24 Oct 2023 10:39:12 +0200 Subject: [PATCH 14/14] Improve toolbar button visual focus (#55523) * Update block-toolbar-button-style__focus mixin * Use block-toolbar-button-style__focus on block styles --- packages/base-styles/_mixins.scss | 2 +- packages/block-editor/src/components/block-styles/style.scss | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/base-styles/_mixins.scss b/packages/base-styles/_mixins.scss index b621c34551da53..b988c0499f1fb8 100644 --- a/packages/base-styles/_mixins.scss +++ b/packages/base-styles/_mixins.scss @@ -64,7 +64,7 @@ */ @mixin block-toolbar-button-style__focus() { - box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color), inset 0 0 0 4px $white; + box-shadow: inset 0 0 0 $border-width var(--wp-components-color-background, $white), 0 0 0 var(--wp-admin-border-width-focus) var(--wp-components-color-accent, var(--wp-admin-theme-color, #3858e9)); // Windows High Contrast mode will show this outline, but not the box-shadow. outline: 2px solid transparent; diff --git a/packages/block-editor/src/components/block-styles/style.scss b/packages/block-editor/src/components/block-styles/style.scss index 9e80e93b0cd641..ab2a4b0c9ac982 100644 --- a/packages/block-editor/src/components/block-styles/style.scss +++ b/packages/block-editor/src/components/block-styles/style.scss @@ -58,7 +58,7 @@ &:focus, &.is-active:focus { - box-shadow: inset 0 0 0 $border-width var(--wp-components-color-background, #fff), 0 0 0 var(--wp-admin-border-width-focus) var(--wp-components-color-accent, var(--wp-admin-theme-color, #3858e9)); + @include block-toolbar-button-style__focus(); } }