diff --git a/docs/reference-guides/data/data-core-block-editor.md b/docs/reference-guides/data/data-core-block-editor.md
index 7b0bd386daaf48..b03e905d166bc0 100644
--- a/docs/reference-guides/data/data-core-block-editor.md
+++ b/docs/reference-guides/data/data-core-block-editor.md
@@ -409,6 +409,19 @@ _Returns_
- `WPBlock[]`: Block objects.
+### getBlocksByName
+
+Returns all blocks that match a blockName. Results include nested blocks.
+
+_Parameters_
+
+- _state_ `Object`: Global application state.
+- _blockName_ `?string`: Optional block name, if not specified, returns an empty array.
+
+_Returns_
+
+- `Array`: Array of clientIds of blocks with name equal to blockName.
+
### getBlockSelectionEnd
Returns the current block selection end. This value may be null, and it may represent either a singular block selection or multi-selection end. A selection is singular if its start and end match.
diff --git a/docs/reference-guides/data/data-core-notices.md b/docs/reference-guides/data/data-core-notices.md
index e11e6f226169f9..d36098429811dd 100644
--- a/docs/reference-guides/data/data-core-notices.md
+++ b/docs/reference-guides/data/data-core-notices.md
@@ -277,7 +277,7 @@ export const ExampleComponent = () => {
const notices = useSelect( ( select ) =>
select( noticesStore ).getNotices()
);
- const { removeNotices } = useDispatch( noticesStore );
+ const { removeAllNotices } = useDispatch( noticesStore );
return (
<>
diff --git a/lib/block-supports/shadow.php b/lib/block-supports/shadow.php
index 4a28c98b79325d..87258930faf10e 100644
--- a/lib/block-supports/shadow.php
+++ b/lib/block-supports/shadow.php
@@ -53,9 +53,8 @@ function gutenberg_apply_shadow_support( $block_type, $block_attributes ) {
$shadow_block_styles = array();
- $preset_shadow = array_key_exists( 'shadow', $block_attributes ) ? "var:preset|shadow|{$block_attributes['shadow']}" : null;
- $custom_shadow = isset( $block_attributes['style']['shadow'] ) ? $block_attributes['style']['shadow'] : null;
- $shadow_block_styles['shadow'] = $preset_shadow ? $preset_shadow : $custom_shadow;
+ $custom_shadow = $block_attributes['style']['shadow'] ?? null;
+ $shadow_block_styles['shadow'] = $custom_shadow;
$attributes = array();
$styles = gutenberg_style_engine_get_styles( $shadow_block_styles );
diff --git a/lib/blocks.php b/lib/blocks.php
index d5283afeb7f999..e1d4622a0f23da 100644
--- a/lib/blocks.php
+++ b/lib/blocks.php
@@ -440,27 +440,6 @@ function gutenberg_legacy_wp_block_post_meta( $value, $object_id, $meta_key, $si
add_filter( 'default_post_metadata', 'gutenberg_legacy_wp_block_post_meta', 10, 4 );
-/**
- * Registers the metadata block attribute for all block types.
- *
- * @param array $args Array of arguments for registering a block type.
- * @return array $args
- */
-function gutenberg_register_metadata_attribute( $args ) {
- // Setup attributes if needed.
- if ( ! isset( $args['attributes'] ) || ! is_array( $args['attributes'] ) ) {
- $args['attributes'] = array();
- }
-
- if ( ! array_key_exists( 'metadata', $args['attributes'] ) ) {
- $args['attributes']['metadata'] = array(
- 'type' => 'object',
- );
- }
-
- return $args;
-}
-add_filter( 'register_block_type_args', 'gutenberg_register_metadata_attribute' );
/**
* Strips all HTML from the content of footnotes, and sanitizes the ID.
diff --git a/lib/experimental/block-bindings/block-bindings.php b/lib/compat/wordpress-6.5/block-bindings/block-bindings.php
similarity index 100%
rename from lib/experimental/block-bindings/block-bindings.php
rename to lib/compat/wordpress-6.5/block-bindings/block-bindings.php
diff --git a/lib/experimental/block-bindings/class-wp-block-bindings.php b/lib/compat/wordpress-6.5/block-bindings/class-wp-block-bindings.php
similarity index 98%
rename from lib/experimental/block-bindings/class-wp-block-bindings.php
rename to lib/compat/wordpress-6.5/block-bindings/class-wp-block-bindings.php
index 7eb443dd367a6f..fedc652688a249 100644
--- a/lib/experimental/block-bindings/class-wp-block-bindings.php
+++ b/lib/compat/wordpress-6.5/block-bindings/class-wp-block-bindings.php
@@ -101,7 +101,7 @@ public function replace_html( $block_content, $block_name, $block_attr, $source_
foreach ( $selector_attribute_names as $name ) {
$selector_attrs[ $name ] = $block_reader->get_attribute( $name );
}
- $selector_markup = "<$selector>" . esc_html( $source_value ) . "$selector>";
+ $selector_markup = "<$selector>" . wp_kses_post( $source_value ) . "$selector>";
$amended_content = new WP_HTML_Tag_Processor( $selector_markup );
$amended_content->next_tag();
foreach ( $selector_attrs as $attribute_key => $attribute_value ) {
diff --git a/lib/compat/wordpress-6.5/block-bindings/sources/pattern.php b/lib/compat/wordpress-6.5/block-bindings/sources/pattern.php
new file mode 100644
index 00000000000000..740d1983d6fe80
--- /dev/null
+++ b/lib/compat/wordpress-6.5/block-bindings/sources/pattern.php
@@ -0,0 +1,35 @@
+attributes, array( 'metadata', 'id' ), false ) ) {
+ return null;
+ }
+ $block_id = $block_instance->attributes['metadata']['id'];
+ $attribute_override = _wp_array_get( $block_instance->context, array( 'pattern/overrides', $block_id, $attribute_name ), null );
+ if ( null === $attribute_override ) {
+ return null;
+ }
+ switch ( $attribute_override[0] ) {
+ case 0: // remove
+ /**
+ * TODO: This currently doesn't remove the attribute, but only set it to an empty string.
+ * It's a temporary solution until the block binding API supports different operations.
+ */
+ return '';
+ case 1: // replace
+ return $attribute_override[1];
+ default:
+ return null;
+ }
+ };
+ wp_block_bindings_register_source(
+ 'pattern_attributes',
+ __( 'Pattern Attributes', 'gutenberg' ),
+ $pattern_source_callback
+ );
+}
diff --git a/lib/experimental/block-bindings/sources/post-meta.php b/lib/compat/wordpress-6.5/block-bindings/sources/post-meta.php
similarity index 100%
rename from lib/experimental/block-bindings/sources/post-meta.php
rename to lib/compat/wordpress-6.5/block-bindings/sources/post-meta.php
diff --git a/lib/compat/wordpress-6.5/blocks.php b/lib/compat/wordpress-6.5/blocks.php
index b6890c14dc1f97..dccaeba5cc3352 100644
--- a/lib/compat/wordpress-6.5/blocks.php
+++ b/lib/compat/wordpress-6.5/blocks.php
@@ -22,3 +22,105 @@ function gutenberg_register_block_type_args_shim( $args ) {
if ( ! method_exists( 'WP_Block_Type', 'get_variations' ) ) {
add_filter( 'register_block_type_args', 'gutenberg_register_block_type_args_shim' );
}
+
+
+/**
+ * Registers the metadata block attribute for all block types.
+ *
+ * @param array $args Array of arguments for registering a block type.
+ * @return array $args
+ */
+function gutenberg_register_metadata_attribute( $args ) {
+ // Setup attributes if needed.
+ if ( ! isset( $args['attributes'] ) || ! is_array( $args['attributes'] ) ) {
+ $args['attributes'] = array();
+ }
+
+ if ( ! array_key_exists( 'metadata', $args['attributes'] ) ) {
+ $args['attributes']['metadata'] = array(
+ 'type' => 'object',
+ );
+ }
+
+ return $args;
+}
+add_filter( 'register_block_type_args', 'gutenberg_register_metadata_attribute' );
+
+
+if ( ! function_exists( 'gutenberg_process_block_bindings' ) ) {
+ /**
+ * Process the block bindings attribute.
+ *
+ * @param string $block_content Block Content.
+ * @param array $block Block attributes.
+ * @param WP_Block $block_instance The block instance.
+ */
+ function gutenberg_process_block_bindings( $block_content, $block, $block_instance ) {
+
+ // Allowed blocks that support block bindings.
+ // TODO: Look for a mechanism to opt-in for this. Maybe adding a property to block attributes?
+ $allowed_blocks = array(
+ 'core/paragraph' => array( 'content' ),
+ 'core/heading' => array( 'content' ),
+ 'core/image' => array( 'url', 'title', 'alt' ),
+ 'core/button' => array( 'url', 'text', 'linkTarget' ),
+ );
+
+ // If the block doesn't have the bindings property or isn't one of the allowed block types, return.
+ if ( ! isset( $block['attrs']['metadata']['bindings'] ) || ! isset( $allowed_blocks[ $block_instance->name ] ) ) {
+ return $block_content;
+ }
+
+ /*
+ * Assuming the following format for the bindings property of the "metadata" attribute:
+ *
+ * "bindings": {
+ * "title": {
+ * "source": {
+ * "name": "post_meta",
+ * "attributes": { "value": "text_custom_field" }
+ * }
+ * },
+ * "url": {
+ * "source": {
+ * "name": "post_meta",
+ * "attributes": { "value": "text_custom_field" }
+ * }
+ * }
+ * }
+ */
+
+ $block_bindings_sources = wp_block_bindings_get_sources();
+ $modified_block_content = $block_content;
+ foreach ( $block['attrs']['metadata']['bindings'] as $binding_attribute => $binding_source ) {
+
+ // If the attribute is not in the list, process next attribute.
+ if ( ! in_array( $binding_attribute, $allowed_blocks[ $block_instance->name ], true ) ) {
+ continue;
+ }
+ // If no source is provided, or that source is not registered, process next attribute.
+ if ( ! isset( $binding_source['source'] ) || ! isset( $binding_source['source']['name'] ) || ! isset( $block_bindings_sources[ $binding_source['source']['name'] ] ) ) {
+ continue;
+ }
+
+ $source_callback = $block_bindings_sources[ $binding_source['source']['name'] ]['apply'];
+ // Get the value based on the source.
+ if ( ! isset( $binding_source['source']['attributes'] ) ) {
+ $source_args = array();
+ } else {
+ $source_args = $binding_source['source']['attributes'];
+ }
+ $source_value = $source_callback( $source_args, $block_instance, $binding_attribute );
+ // If the value is null, process next attribute.
+ if ( is_null( $source_value ) ) {
+ continue;
+ }
+
+ // Process the HTML based on the block and the attribute.
+ $modified_block_content = wp_block_bindings_replace_html( $modified_block_content, $block_instance->name, $binding_attribute, $source_value );
+ }
+ return $modified_block_content;
+ }
+}
+
+add_filter( 'render_block', 'gutenberg_process_block_bindings', 20, 3 );
diff --git a/lib/compat/wordpress-6.5/class-wp-navigation-block-renderer.php b/lib/compat/wordpress-6.5/class-wp-navigation-block-renderer.php
deleted file mode 100644
index 9c270f59fa220e..00000000000000
--- a/lib/compat/wordpress-6.5/class-wp-navigation-block-renderer.php
+++ /dev/null
@@ -1,664 +0,0 @@
-.
- *
- * @var array
- */
- private static $nav_blocks_wrapped_in_list_item = array(
- 'core/navigation-link',
- 'core/home-link',
- 'core/site-title',
- 'core/site-logo',
- 'core/navigation-submenu',
- );
-
- /**
- * Used to determine which blocks need an
wrapper.
- *
- * @var array
- */
- private static $needs_list_item_wrapper = array(
- 'core/site-title',
- 'core/site-logo',
- );
-
- /**
- * Keeps track of all the navigation names that have been seen.
- *
- * @var array
- */
- private static $seen_menu_names = array();
-
- /**
- * Returns whether or not this is responsive navigation.
- *
- * @param array $attributes The block attributes.
- * @return bool Returns whether or not this is responsive navigation.
- */
- private static function is_responsive( $attributes ) {
- /**
- * This is for backwards compatibility after the `isResponsive` attribute was been removed.
- */
-
- $has_old_responsive_attribute = ! empty( $attributes['isResponsive'] ) && $attributes['isResponsive'];
- return isset( $attributes['overlayMenu'] ) && 'never' !== $attributes['overlayMenu'] || $has_old_responsive_attribute;
- }
-
- /**
- * Returns whether or not a navigation has a submenu.
- *
- * @param WP_Block_List $inner_blocks The list of inner blocks.
- * @return bool Returns whether or not a navigation has a submenu.
- */
- private static function has_submenus( $inner_blocks ) {
- foreach ( $inner_blocks as $inner_block ) {
- $inner_block_content = $inner_block->render();
- $p = new WP_HTML_Tag_Processor( $inner_block_content );
- if ( $p->next_tag(
- array(
- 'name' => 'LI',
- 'class_name' => 'has-child',
- )
- ) ) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * Determine whether to load the view script.
- *
- * @param array $attributes The block attributes.
- * @param WP_Block_List $inner_blocks The list of inner blocks.
- * @return bool Returns whether or not to load the view script.
- */
- private static function should_load_view_script( $attributes, $inner_blocks ) {
- $has_submenus = static::has_submenus( $inner_blocks );
- $is_responsive_menu = static::is_responsive( $attributes );
- return ( $has_submenus && ( $attributes['openSubmenusOnClick'] || $attributes['showSubmenuIcon'] ) ) || $is_responsive_menu;
- }
-
- /**
- * Returns whether or not a block needs a list item wrapper.
- *
- * @param WP_Block $block The block.
- * @return bool Returns whether or not a block needs a list item wrapper.
- */
- private static function does_block_need_a_list_item_wrapper( $block ) {
- return in_array( $block->name, static::$needs_list_item_wrapper, true );
- }
-
- /**
- * Returns the markup for a single inner block.
- *
- * @param WP_Block $inner_block The inner block.
- * @return string Returns the markup for a single inner block.
- */
- private static function get_markup_for_inner_block( $inner_block ) {
- $inner_block_content = $inner_block->render();
- if ( ! empty( $inner_block_content ) ) {
- if ( static::does_block_need_a_list_item_wrapper( $inner_block ) ) {
- return '
' . $inner_block_content . '
';
- }
-
- return $inner_block_content;
- }
- }
-
- /**
- * Returns the html for the inner blocks of the navigation block.
- *
- * @param array $attributes The block attributes.
- * @param WP_Block_List $inner_blocks The list of inner blocks.
- * @return string Returns the html for the inner blocks of the navigation block.
- */
- private static function get_inner_blocks_html( $attributes, $inner_blocks ) {
- $has_submenus = static::has_submenus( $inner_blocks );
- $should_load_view_script = static::should_load_view_script( $attributes, $inner_blocks );
-
- $style = static::get_styles( $attributes );
- $class = static::get_classes( $attributes );
- $container_attributes = get_block_wrapper_attributes(
- array(
- 'class' => 'wp-block-navigation__container ' . $class,
- 'style' => $style,
- )
- );
-
- $inner_blocks_html = '';
- $is_list_open = false;
-
- foreach ( $inner_blocks as $inner_block ) {
- $is_list_item = in_array( $inner_block->name, static::$nav_blocks_wrapped_in_list_item, true );
-
- if ( $is_list_item && ! $is_list_open ) {
- $is_list_open = true;
- $inner_blocks_html .= sprintf(
- '
';
- }
-
- // Add directives to the submenu if needed.
- if ( $has_submenus && $should_load_view_script ) {
- $tags = new WP_HTML_Tag_Processor( $inner_blocks_html );
- $inner_blocks_html = gutenberg_block_core_navigation_add_directives_to_submenu( $tags, $attributes );
- }
-
- return $inner_blocks_html;
- }
-
- /**
- * Gets the inner blocks for the navigation block from the navigation post.
- *
- * @param array $attributes The block attributes.
- * @return WP_Block_List Returns the inner blocks for the navigation block.
- */
- private static function get_inner_blocks_from_navigation_post( $attributes ) {
- $navigation_post = get_post( $attributes['ref'] );
- if ( ! isset( $navigation_post ) ) {
- return new WP_Block_List( array(), $attributes );
- }
-
- // Only published posts are valid. If this is changed then a corresponding change
- // must also be implemented in `use-navigation-menu.js`.
- if ( 'publish' === $navigation_post->post_status ) {
- $parsed_blocks = parse_blocks( $navigation_post->post_content );
-
- // 'parse_blocks' includes a null block with '\n\n' as the content when
- // it encounters whitespace. This code strips it.
- $compacted_blocks = gutenberg_block_core_navigation_filter_out_empty_blocks( $parsed_blocks );
-
- // TODO - this uses the full navigation block attributes for the
- // context which could be refined.
- return new WP_Block_List( $compacted_blocks, $attributes );
- }
- }
-
- /**
- * Gets the inner blocks for the navigation block from the fallback.
- *
- * @param array $attributes The block attributes.
- * @return WP_Block_List Returns the inner blocks for the navigation block.
- */
- private static function get_inner_blocks_from_fallback( $attributes ) {
- $fallback_blocks = gutenberg_block_core_navigation_get_fallback_blocks();
-
- // Fallback my have been filtered so do basic test for validity.
- if ( empty( $fallback_blocks ) || ! is_array( $fallback_blocks ) ) {
- return new WP_Block_List( array(), $attributes );
- }
-
- return new WP_Block_List( $fallback_blocks, $attributes );
- }
-
- /**
- * Gets the inner blocks for the navigation block.
- *
- * @param array $attributes The block attributes.
- * @param WP_Block $block The parsed block.
- * @return WP_Block_List Returns the inner blocks for the navigation block.
- */
- private static function get_inner_blocks( $attributes, $block ) {
- $inner_blocks = $block->inner_blocks;
-
- // Ensure that blocks saved with the legacy ref attribute name (navigationMenuId) continue to render.
- if ( array_key_exists( 'navigationMenuId', $attributes ) ) {
- $attributes['ref'] = $attributes['navigationMenuId'];
- }
-
- // If:
- // - the gutenberg plugin is active
- // - `__unstableLocation` is defined
- // - we have menu items at the defined location
- // - we don't have a relationship to a `wp_navigation` Post (via `ref`).
- // ...then create inner blocks from the classic menu assigned to that location.
- if (
- defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN &&
- array_key_exists( '__unstableLocation', $attributes ) &&
- ! array_key_exists( 'ref', $attributes ) &&
- ! empty( gutenberg_block_core_navigation_get_menu_items_at_location( $attributes['__unstableLocation'] ) )
- ) {
- $inner_blocks = gutenberg_block_core_navigation_get_inner_blocks_from_unstable_location( $attributes );
- }
-
- // Load inner blocks from the navigation post.
- if ( array_key_exists( 'ref', $attributes ) ) {
- $inner_blocks = static::get_inner_blocks_from_navigation_post( $attributes );
- }
-
- // If there are no inner blocks then fallback to rendering an appropriate fallback.
- if ( empty( $inner_blocks ) ) {
- $inner_blocks = static::get_inner_blocks_from_fallback( $attributes );
- }
-
- /**
- * Filter navigation block $inner_blocks.
- * Allows modification of a navigation block menu items.
- *
- * @since 6.1.0
- *
- * @param \WP_Block_List $inner_blocks
- */
- $inner_blocks = apply_filters( 'block_core_navigation_render_inner_blocks', $inner_blocks );
-
- $post_ids = gutenberg_block_core_navigation_get_post_ids( $inner_blocks );
- if ( $post_ids ) {
- _prime_post_caches( $post_ids, false, false );
- }
-
- return $inner_blocks;
- }
-
- /**
- * Gets the name of the current navigation, if it has one.
- *
- * @param array $attributes The block attributes.
- * @return string Returns the name of the navigation.
- */
- private static function get_navigation_name( $attributes ) {
-
- $navigation_name = $attributes['ariaLabel'] ?? '';
-
- // Load the navigation post.
- if ( array_key_exists( 'ref', $attributes ) ) {
- $navigation_post = get_post( $attributes['ref'] );
- if ( ! isset( $navigation_post ) ) {
- return $navigation_name;
- }
-
- // Only published posts are valid. If this is changed then a corresponding change
- // must also be implemented in `use-navigation-menu.js`.
- if ( 'publish' === $navigation_post->post_status ) {
- $navigation_name = $navigation_post->post_title;
-
- // This is used to count the number of times a navigation name has been seen,
- // so that we can ensure every navigation has a unique id.
- if ( isset( static::$seen_menu_names[ $navigation_name ] ) ) {
- ++static::$seen_menu_names[ $navigation_name ];
- } else {
- static::$seen_menu_names[ $navigation_name ] = 1;
- }
- }
- }
-
- return $navigation_name;
- }
-
- /**
- * Returns the layout class for the navigation block.
- *
- * @param array $attributes The block attributes.
- * @return string Returns the layout class for the navigation block.
- */
- private static function get_layout_class( $attributes ) {
- $layout_justification = array(
- 'left' => 'items-justified-left',
- 'right' => 'items-justified-right',
- 'center' => 'items-justified-center',
- 'space-between' => 'items-justified-space-between',
- );
-
- $layout_class = '';
- if (
- isset( $attributes['layout']['justifyContent'] ) &&
- isset( $layout_justification[ $attributes['layout']['justifyContent'] ] )
- ) {
- $layout_class .= $layout_justification[ $attributes['layout']['justifyContent'] ];
- }
- if ( isset( $attributes['layout']['orientation'] ) && 'vertical' === $attributes['layout']['orientation'] ) {
- $layout_class .= ' is-vertical';
- }
-
- if ( isset( $attributes['layout']['flexWrap'] ) && 'nowrap' === $attributes['layout']['flexWrap'] ) {
- $layout_class .= ' no-wrap';
- }
- return $layout_class;
- }
-
- /**
- * Return classes for the navigation block.
- *
- * @param array $attributes The block attributes.
- * @return string Returns the classes for the navigation block.
- */
- private static function get_classes( $attributes ) {
- // Restore legacy classnames for submenu positioning.
- $layout_class = static::get_layout_class( $attributes );
- $colors = gutenberg_block_core_navigation_build_css_colors( $attributes );
- $font_sizes = gutenberg_block_core_navigation_build_css_font_sizes( $attributes );
- $is_responsive_menu = static::is_responsive( $attributes );
-
- // Manually add block support text decoration as CSS class.
- $text_decoration = $attributes['style']['typography']['textDecoration'] ?? null;
- $text_decoration_class = sprintf( 'has-text-decoration-%s', $text_decoration );
-
- // Sets the is-collapsed class when the navigation is set to always use the overlay.
- // This saves us from needing to do this check in the view.js file (see the collapseNav function).
- $is_collapsed_class = static::is_always_overlay( $attributes ) ? array( 'is-collapsed' ) : array();
-
- $classes = array_merge(
- $colors['css_classes'],
- $font_sizes['css_classes'],
- $is_responsive_menu ? array( 'is-responsive' ) : array(),
- $layout_class ? array( $layout_class ) : array(),
- $text_decoration ? array( $text_decoration_class ) : array(),
- $is_collapsed_class
- );
- return implode( ' ', $classes );
- }
-
- private static function is_always_overlay( $attributes ) {
- return isset( $attributes['overlayMenu'] ) && 'always' === $attributes['overlayMenu'];
- }
-
- /**
- * Get styles for the navigation block.
- *
- * @param array $attributes The block attributes.
- * @return string Returns the styles for the navigation block.
- */
- private static function get_styles( $attributes ) {
- $colors = gutenberg_block_core_navigation_build_css_colors( $attributes );
- $font_sizes = gutenberg_block_core_navigation_build_css_font_sizes( $attributes );
- $block_styles = isset( $attributes['styles'] ) ? $attributes['styles'] : '';
- return $block_styles . $colors['inline_styles'] . $font_sizes['inline_styles'];
- }
-
- /**
- * Get the responsive container markup
- *
- * @param array $attributes The block attributes.
- * @param WP_Block_List $inner_blocks The list of inner blocks.
- * @param string $inner_blocks_html The markup for the inner blocks.
- * @return string Returns the container markup.
- */
- private static function get_responsive_container_markup( $attributes, $inner_blocks, $inner_blocks_html ) {
- $should_load_view_script = static::should_load_view_script( $attributes, $inner_blocks );
- $colors = gutenberg_block_core_navigation_build_css_colors( $attributes );
- $modal_unique_id = wp_unique_id( 'modal-' );
-
- $responsive_container_classes = array(
- 'wp-block-navigation__responsive-container',
- implode( ' ', $colors['overlay_css_classes'] ),
- );
- $open_button_classes = array(
- 'wp-block-navigation__responsive-container-open',
- );
-
- $should_display_icon_label = isset( $attributes['hasIcon'] ) && true === $attributes['hasIcon'];
- $toggle_button_icon = '';
- if ( isset( $attributes['icon'] ) ) {
- if ( 'menu' === $attributes['icon'] ) {
- $toggle_button_icon = '';
- }
- }
- $toggle_button_content = $should_display_icon_label ? $toggle_button_icon : __( 'Menu' );
- $toggle_close_button_icon = '';
- $toggle_close_button_content = $should_display_icon_label ? $toggle_close_button_icon : __( 'Close' );
- $toggle_aria_label_open = $should_display_icon_label ? 'aria-label="' . __( 'Open menu' ) . '"' : ''; // Open button label.
- $toggle_aria_label_close = $should_display_icon_label ? 'aria-label="' . __( 'Close menu' ) . '"' : ''; // Close button label.
-
- // Add Interactivity API directives to the markup if needed.
- $open_button_directives = '';
- $responsive_container_directives = '';
- $responsive_dialog_directives = '';
- $close_button_directives = '';
- if ( $should_load_view_script ) {
- $open_button_directives = '
- data-wp-on--click="actions.openMenuOnClick"
- data-wp-on--keydown="actions.handleMenuKeydown"
- ';
- $responsive_container_directives = '
- data-wp-class--has-modal-open="state.isMenuOpen"
- data-wp-class--is-menu-open="state.isMenuOpen"
- data-wp-watch="callbacks.initMenu"
- data-wp-on--keydown="actions.handleMenuKeydown"
- data-wp-on--focusout="actions.handleMenuFocusout"
- tabindex="-1"
- ';
- $responsive_dialog_directives = '
- data-wp-bind--aria-modal="state.ariaModal"
- data-wp-bind--aria-label="state.ariaLabel"
- data-wp-bind--role="state.roleAttribute"
- ';
- $close_button_directives = '
- data-wp-on--click="actions.closeMenuOnClick"
- ';
- $responsive_container_content_directives = '
- data-wp-watch="callbacks.focusFirstElement"
- ';
- }
-
- return sprintf(
- '
-
-
-
-
-
- %2$s
-
-
-
-
',
- esc_attr( $modal_unique_id ),
- $inner_blocks_html,
- $toggle_aria_label_open,
- $toggle_aria_label_close,
- esc_attr( implode( ' ', $responsive_container_classes ) ),
- esc_attr( implode( ' ', $open_button_classes ) ),
- esc_attr( safecss_filter_attr( $colors['overlay_inline_styles'] ) ),
- $toggle_button_content,
- $toggle_close_button_content,
- $open_button_directives,
- $responsive_container_directives,
- $responsive_dialog_directives,
- $close_button_directives,
- $responsive_container_content_directives
- );
- }
-
- /**
- * Get the wrapper attributes
- *
- * @param array $attributes The block attributes.
- * @param WP_Block_List $inner_blocks A list of inner blocks.
- * @return string Returns the navigation block markup.
- */
- private static function get_nav_wrapper_attributes( $attributes, $inner_blocks ) {
- $nav_menu_name = static::get_unique_navigation_name( $attributes );
- $should_load_view_script = static::should_load_view_script( $attributes, $inner_blocks );
- $is_responsive_menu = static::is_responsive( $attributes );
- $style = static::get_styles( $attributes );
- $class = static::get_classes( $attributes );
- $wrapper_attributes = get_block_wrapper_attributes(
- array(
- 'class' => $class,
- 'style' => $style,
- 'aria-label' => $nav_menu_name,
- )
- );
-
- if ( $is_responsive_menu ) {
- $nav_element_directives = static::get_nav_element_directives( $should_load_view_script, $attributes );
- $wrapper_attributes .= ' ' . $nav_element_directives;
- }
-
- return $wrapper_attributes;
- }
-
- /**
- * Get the nav element directives
- *
- * @param bool $should_load_view_script Whether or not the view script should be loaded.
- * @return string the directives for the navigation element.
- */
- private static function get_nav_element_directives( $should_load_view_script, $attributes ) {
- if ( ! $should_load_view_script ) {
- return '';
- }
- // When adding to this array be mindful of security concerns.
- $nav_element_context = wp_json_encode(
- array(
- 'overlayOpenedBy' => array(),
- 'type' => 'overlay',
- 'roleAttribute' => '',
- 'ariaLabel' => __( 'Menu' ),
- ),
- JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP
- );
- $nav_element_directives = '
- data-wp-interactive=\'{"namespace":"core/navigation"}\'
- data-wp-context=\'' . $nav_element_context . '\'
- ';
-
- // When the navigation overlayMenu attribute is set to "always"
- // we don't need to use JavaScript to collapse the menu as we set the class manually.
- if ( ! static::is_always_overlay( $attributes ) ) {
- $nav_element_directives .= 'data-wp-init="callbacks.initNav"';
- $nav_element_directives .= ' '; // space separator
- $nav_element_directives .= 'data-wp-class--is-collapsed="context.isCollapsed"';
- }
-
- return $nav_element_directives;
- }
-
- /**
- * Handle view script loading.
- *
- * @param array $attributes The block attributes.
- * @param WP_Block $block The parsed block.
- * @param WP_Block_List $inner_blocks The list of inner blocks.
- */
- private static function handle_view_script_loading( $attributes, $block, $inner_blocks ) {
- $should_load_view_script = static::should_load_view_script( $attributes, $inner_blocks );
- $is_gutenberg_plugin = defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN;
- $view_js_file = 'wp-block-navigation-view';
- $script_handles = $block->block_type->view_script_handles;
-
- if ( $is_gutenberg_plugin ) {
- if ( $should_load_view_script ) {
- gutenberg_enqueue_module( '@wordpress/block-library/navigation-block' );
- }
- // Remove the view script because we are using the module.
- $block->block_type->view_script_handles = array_diff( $script_handles, array( $view_js_file ) );
- } else {
- // If the script already exists, there is no point in removing it from viewScript.
- if ( ! wp_script_is( $view_js_file ) ) {
-
- // If the script is not needed, and it is still in the `view_script_handles`, remove it.
- if ( ! $should_load_view_script && in_array( $view_js_file, $script_handles, true ) ) {
- $block->block_type->view_script_handles = array_diff( $script_handles, array( $view_js_file ) );
- }
- // If the script is needed, but it was previously removed, add it again.
- if ( $should_load_view_script && ! in_array( $view_js_file, $script_handles, true ) ) {
- $block->block_type->view_script_handles = array_merge( $script_handles, array( $view_js_file ) );
- }
- }
- }
- }
-
- /**
- * Returns the markup for the navigation block.
- *
- * @param array $attributes The block attributes.
- * @param WP_Block_List $inner_blocks The list of inner blocks.
- * @return string Returns the navigation wrapper markup.
- */
- private static function get_wrapper_markup( $attributes, $inner_blocks ) {
- $inner_blocks_html = static::get_inner_blocks_html( $attributes, $inner_blocks );
- if ( static::is_responsive( $attributes ) ) {
- return static::get_responsive_container_markup( $attributes, $inner_blocks, $inner_blocks_html );
- }
- return $inner_blocks_html;
- }
-
- /**
- * Returns a unique name for the navigation.
- *
- * @param array $attributes The block attributes.
- * @return string Returns a unique name for the navigation.
- */
- private static function get_unique_navigation_name( $attributes ) {
- $nav_menu_name = static::get_navigation_name( $attributes );
-
- // If the menu name has been used previously then append an ID
- // to the name to ensure uniqueness across a given post.
- if ( isset( static::$seen_menu_names[ $nav_menu_name ] ) && static::$seen_menu_names[ $nav_menu_name ] > 1 ) {
- $count = static::$seen_menu_names[ $nav_menu_name ];
- $nav_menu_name = $nav_menu_name . ' ' . ( $count );
- }
-
- return $nav_menu_name;
- }
-
- /**
- * Renders the navigation block.
- *
- * @param array $attributes The block attributes.
- * @param string $content The saved content.
- * @param WP_Block $block The parsed block.
- * @return string Returns the navigation block markup.
- */
- public static function render( $attributes, $content, $block ) {
- /**
- * Deprecated:
- * The rgbTextColor and rgbBackgroundColor attributes
- * have been deprecated in favor of
- * customTextColor and customBackgroundColor ones.
- * Move the values from old attrs to the new ones.
- */
- if ( isset( $attributes['rgbTextColor'] ) && empty( $attributes['textColor'] ) ) {
- $attributes['customTextColor'] = $attributes['rgbTextColor'];
- }
-
- if ( isset( $attributes['rgbBackgroundColor'] ) && empty( $attributes['backgroundColor'] ) ) {
- $attributes['customBackgroundColor'] = $attributes['rgbBackgroundColor'];
- }
-
- unset( $attributes['rgbTextColor'], $attributes['rgbBackgroundColor'] );
-
- $inner_blocks = static::get_inner_blocks( $attributes, $block );
- // Prevent navigation blocks referencing themselves from rendering.
- if ( gutenberg_block_core_navigation_block_contains_core_navigation( $inner_blocks ) ) {
- return '';
- }
-
- static::handle_view_script_loading( $attributes, $block, $inner_blocks );
-
- return sprintf(
- '',
- static::get_nav_wrapper_attributes( $attributes, $inner_blocks ),
- static::get_wrapper_markup( $attributes, $inner_blocks )
- );
- }
-}
diff --git a/lib/compat/wordpress-6.5/class-wp-script-modules.php b/lib/compat/wordpress-6.5/class-wp-script-modules.php
new file mode 100644
index 00000000000000..f6a2a348f92ef3
--- /dev/null
+++ b/lib/compat/wordpress-6.5/class-wp-script-modules.php
@@ -0,0 +1,323 @@
+
+ */
+ private $enqueued_before_registered = array();
+
+ /**
+ * Registers the script module if no script module with that script module
+ * identifier has already been registered.
+ *
+ * @since 6.5.0
+ *
+ * @param string $id The identifier of the script module. Should be unique. It will be used in the
+ * final import map.
+ * @param string $src Optional. Full URL of the script module, or path of the script module relative
+ * to the WordPress root directory. If it is provided and the script module has
+ * not been registered yet, it will be registered.
+ * @param array $deps {
+ * Optional. List of dependencies.
+ *
+ * @type string|array $0... {
+ * An array of script module identifiers of the dependencies of this script
+ * module. The dependencies can be strings or arrays. If they are arrays,
+ * they need an `id` key with the script module identifier, and can contain
+ * an `import` key with either `static` or `dynamic`. By default,
+ * dependencies that don't contain an `import` key are considered static.
+ *
+ * @type string $id The script module identifier.
+ * @type string $import Optional. Import type. May be either `static` or
+ * `dynamic`. Defaults to `static`.
+ * }
+ * }
+ * @param string|false|null $version Optional. String specifying the script module version number. Defaults to false.
+ * It is added to the URL as a query string for cache busting purposes. If $version
+ * is set to false, the version number is the currently installed WordPress version.
+ * If $version is set to null, no version is added.
+ */
+ public function register( string $id, string $src, array $deps = array(), $version = false ) {
+ if ( ! isset( $this->registered[ $id ] ) ) {
+ $dependencies = array();
+ foreach ( $deps as $dependency ) {
+ if ( is_array( $dependency ) ) {
+ if ( ! isset( $dependency['id'] ) ) {
+ _doing_it_wrong( __METHOD__, __( 'Missing required id key in entry among dependencies array.' ), '6.5.0' );
+ continue;
+ }
+ $dependencies[] = array(
+ 'id' => $dependency['id'],
+ 'import' => isset( $dependency['import'] ) && 'dynamic' === $dependency['import'] ? 'dynamic' : 'static',
+ );
+ } elseif ( is_string( $dependency ) ) {
+ $dependencies[] = array(
+ 'id' => $dependency,
+ 'import' => 'static',
+ );
+ } else {
+ _doing_it_wrong( __METHOD__, __( 'Entries in dependencies array must be either strings or arrays with an id key.' ), '6.5.0' );
+ }
+ }
+
+ $this->registered[ $id ] = array(
+ 'src' => $src,
+ 'version' => $version,
+ 'enqueue' => isset( $this->enqueued_before_registered[ $id ] ),
+ 'dependencies' => $dependencies,
+ );
+ }
+ }
+
+ /**
+ * Marks the script module to be enqueued in the page.
+ *
+ * If a src is provided and the script module has not been registered yet, it
+ * will be registered.
+ *
+ * @since 6.5.0
+ *
+ * @param string $id The identifier of the script module. Should be unique. It will be used in the
+ * final import map.
+ * @param string $src Optional. Full URL of the script module, or path of the script module relative
+ * to the WordPress root directory. If it is provided and the script module has
+ * not been registered yet, it will be registered.
+ * @param array $deps {
+ * Optional. List of dependencies.
+ *
+ * @type string|array $0... {
+ * An array of script module identifiers of the dependencies of this script
+ * module. The dependencies can be strings or arrays. If they are arrays,
+ * they need an `id` key with the script module identifier, and can contain
+ * an `import` key with either `static` or `dynamic`. By default,
+ * dependencies that don't contain an `import` key are considered static.
+ *
+ * @type string $id The script module identifier.
+ * @type string $import Optional. Import type. May be either `static` or
+ * `dynamic`. Defaults to `static`.
+ * }
+ * }
+ * @param string|false|null $version Optional. String specifying the script module version number. Defaults to false.
+ * It is added to the URL as a query string for cache busting purposes. If $version
+ * is set to false, the version number is the currently installed WordPress version.
+ * If $version is set to null, no version is added.
+ */
+ public function enqueue( string $id, string $src = '', array $deps = array(), $version = false ) {
+ if ( isset( $this->registered[ $id ] ) ) {
+ $this->registered[ $id ]['enqueue'] = true;
+ } elseif ( $src ) {
+ $this->register( $id, $src, $deps, $version );
+ $this->registered[ $id ]['enqueue'] = true;
+ } else {
+ $this->enqueued_before_registered[ $id ] = true;
+ }
+ }
+
+ /**
+ * Unmarks the script module so it will no longer be enqueued in the page.
+ *
+ * @since 6.5.0
+ *
+ * @param string $id The identifier of the script module.
+ */
+ public function dequeue( string $id ) {
+ if ( isset( $this->registered[ $id ] ) ) {
+ $this->registered[ $id ]['enqueue'] = false;
+ }
+ unset( $this->enqueued_before_registered[ $id ] );
+ }
+
+ /**
+ * Adds the hooks to print the import map, enqueued script modules and script
+ * module preloads.
+ *
+ * In classic themes, the script modules used by the blocks are not yet known
+ * when the `wp_head` actions is fired, so it needs to print everything in the
+ * footer.
+ *
+ * @since 6.5.0
+ */
+ public function add_hooks() {
+ $position = wp_is_block_theme() ? 'wp_head' : 'wp_footer';
+ add_action( $position, array( $this, 'print_import_map' ) );
+ add_action( $position, array( $this, 'print_enqueued_script_modules' ) );
+ add_action( $position, array( $this, 'print_script_module_preloads' ) );
+ }
+
+ /**
+ * Prints the enqueued script modules using script tags with type="module"
+ * attributes.
+ *
+ * @since 6.5.0
+ */
+ public function print_enqueued_script_modules() {
+ foreach ( $this->get_marked_for_enqueue() as $id => $script_module ) {
+ wp_print_script_tag(
+ array(
+ 'type' => 'module',
+ 'src' => $this->get_versioned_src( $script_module ),
+ 'id' => $id . '-js-module',
+ )
+ );
+ }
+ }
+
+ /**
+ * Prints the the static dependencies of the enqueued script modules using
+ * link tags with rel="modulepreload" attributes.
+ *
+ * If a script module is marked for enqueue, it will not be preloaded.
+ *
+ * @since 6.5.0
+ */
+ public function print_script_module_preloads() {
+ foreach ( $this->get_dependencies( array_keys( $this->get_marked_for_enqueue() ), array( 'static' ) ) as $id => $script_module ) {
+ // Don't preload if it's marked for enqueue.
+ if ( true !== $script_module['enqueue'] ) {
+ echo sprintf(
+ '',
+ esc_url( $this->get_versioned_src( $script_module ) ),
+ esc_attr( $id . '-js-modulepreload' )
+ );
+ }
+ }
+ }
+
+ /**
+ * Prints the import map using a script tag with a type="importmap" attribute.
+ *
+ * @since 6.5.0
+ */
+ public function print_import_map() {
+ $import_map = $this->get_import_map();
+ if ( ! empty( $import_map['imports'] ) ) {
+ wp_print_inline_script_tag(
+ wp_json_encode( $import_map, JSON_HEX_TAG | JSON_HEX_AMP ),
+ array(
+ 'type' => 'importmap',
+ 'id' => 'wp-importmap',
+ )
+ );
+ }
+ }
+
+ /**
+ * Returns the import map array.
+ *
+ * @since 6.5.0
+ *
+ * @return array Array with an `imports` key mapping to an array of script module identifiers and their respective
+ * URLs, including the version query.
+ */
+ private function get_import_map(): array {
+ $imports = array();
+ foreach ( $this->get_dependencies( array_keys( $this->get_marked_for_enqueue() ) ) as $id => $script_module ) {
+ $imports[ $id ] = $this->get_versioned_src( $script_module );
+ }
+ return array( 'imports' => $imports );
+ }
+
+ /**
+ * Retrieves the list of script modules marked for enqueue.
+ *
+ * @since 6.5.0
+ *
+ * @return array Script modules marked for enqueue, keyed by script module identifier.
+ */
+ private function get_marked_for_enqueue(): array {
+ $enqueued = array();
+ foreach ( $this->registered as $id => $script_module ) {
+ if ( true === $script_module['enqueue'] ) {
+ $enqueued[ $id ] = $script_module;
+ }
+ }
+ return $enqueued;
+ }
+
+ /**
+ * Retrieves all the dependencies for the given script module identifiers,
+ * filtered by import types.
+ *
+ * It will consolidate an array containing a set of unique dependencies based
+ * on the requested import types: 'static', 'dynamic', or both. This method is
+ * recursive and also retrieves dependencies of the dependencies.
+ *
+ * @since 6.5.0
+ *
+
+ * @param string[] $ids The identifiers of the script modules for which to gather dependencies.
+ * @param array $import_types Optional. Import types of dependencies to retrieve: 'static', 'dynamic', or both.
+ * Default is both.
+ * @return array List of dependencies, keyed by script module identifier.
+ */
+ private function get_dependencies( array $ids, array $import_types = array( 'static', 'dynamic' ) ) {
+ return array_reduce(
+ $ids,
+ function ( $dependency_script_modules, $id ) use ( $import_types ) {
+ $dependencies = array();
+ foreach ( $this->registered[ $id ]['dependencies'] as $dependency ) {
+ if (
+ in_array( $dependency['import'], $import_types, true ) &&
+ isset( $this->registered[ $dependency['id'] ] ) &&
+ ! isset( $dependency_script_modules[ $dependency['id'] ] )
+ ) {
+ $dependencies[ $dependency['id'] ] = $this->registered[ $dependency['id'] ];
+ }
+ }
+ return array_merge( $dependency_script_modules, $dependencies, $this->get_dependencies( array_keys( $dependencies ), $import_types ) );
+ },
+ array()
+ );
+ }
+
+ /**
+ * Gets the versioned URL for a script module src.
+ *
+ * If $version is set to false, the version number is the currently installed
+ * WordPress version. If $version is set to null, no version is added.
+ * Otherwise, the string passed in $version is used.
+ *
+ * @since 6.5.0
+ *
+ * @param array $script_module The script module.
+ * @return string The script module src with a version if relevant.
+ */
+ private function get_versioned_src( array $script_module ): string {
+ $args = array();
+ if ( false === $script_module['version'] ) {
+ $args['ver'] = get_bloginfo( 'version' );
+ } elseif ( null !== $script_module['version'] ) {
+ $args['ver'] = $script_module['version'];
+ }
+ if ( $args ) {
+ return add_query_arg( $args, $script_module['src'] );
+ }
+ return $script_module['src'];
+ }
+ }
+}
diff --git a/lib/compat/wordpress-6.5/interactivity-api/class-wp-interactivity-api-directives-processor.php b/lib/compat/wordpress-6.5/interactivity-api/class-wp-interactivity-api-directives-processor.php
new file mode 100644
index 00000000000000..b437bcefa67568
--- /dev/null
+++ b/lib/compat/wordpress-6.5/interactivity-api/class-wp-interactivity-api-directives-processor.php
@@ -0,0 +1,143 @@
+get_balanced_tag_bookmarks();
+ if ( ! $bookmarks ) {
+ return null;
+ }
+ list( $start_name, $end_name ) = $bookmarks;
+
+ $start = $this->bookmarks[ $start_name ]->start + $this->bookmarks[ $start_name ]->length + 1;
+ $end = $this->bookmarks[ $end_name ]->start;
+
+ $this->seek( $start_name );
+ $this->release_bookmark( $start_name );
+ $this->release_bookmark( $end_name );
+
+ return substr( $this->html, $start, $end - $start );
+ }
+
+ /**
+ * Sets the content between two balanced tags.
+ *
+ * @access private
+ *
+ * @param string $new_content The string to replace the content between the matching tags.
+ * @return bool Whether the content was successfully replaced.
+ */
+ public function set_content_between_balanced_tags( string $new_content ): bool {
+ $this->get_updated_html();
+
+ $bookmarks = $this->get_balanced_tag_bookmarks();
+ if ( ! $bookmarks ) {
+ return false;
+ }
+ list( $start_name, $end_name ) = $bookmarks;
+
+ $start = $this->bookmarks[ $start_name ]->start + $this->bookmarks[ $start_name ]->length + 1;
+ $end = $this->bookmarks[ $end_name ]->start;
+
+ $this->seek( $start_name );
+ $this->release_bookmark( $start_name );
+ $this->release_bookmark( $end_name );
+
+ $this->lexical_updates[] = new Gutenberg_HTML_Text_Replacement_6_5( $start, $end - $start, esc_html( $new_content ) );
+ return true;
+ }
+
+ /**
+ * Returns a pair of bookmarks for the current opening tag and the matching
+ * closing tag.
+ *
+ * @return array|null A pair of bookmarks, or null if there's no matching closing tag.
+ */
+ private function get_balanced_tag_bookmarks() {
+ static $i = 0;
+ $start_name = 'start_of_balanced_tag_' . ++$i;
+
+ $this->set_bookmark( $start_name );
+ if ( ! $this->next_balanced_closer() ) {
+ $this->release_bookmark( $start_name );
+ return null;
+ }
+
+ $end_name = 'end_of_balanced_tag_' . ++$i;
+ $this->set_bookmark( $end_name );
+
+ return array( $start_name, $end_name );
+ }
+
+ /**
+ * Finds the matching closing tag for an opening tag.
+ *
+ * When called while the processor is on an open tag, it traverses the HTML
+ * until it finds the matching closing tag, respecting any in-between content,
+ * including nested tags of the same name. Returns false when called on a
+ * closing or void tag, or if no matching closing tag was found.
+ *
+ * @return bool Whether a matching closing tag was found.
+ */
+ private function next_balanced_closer(): bool {
+ $depth = 0;
+ $tag_name = $this->get_tag();
+
+ if ( $this->is_void() ) {
+ return false;
+ }
+
+ while ( $this->next_tag(
+ array(
+ 'tag_name' => $tag_name,
+ 'tag_closers' => 'visit',
+ )
+ ) ) {
+ if ( ! $this->is_tag_closer() ) {
+ ++$depth;
+ continue;
+ }
+
+ if ( 0 === $depth ) {
+ return true;
+ }
+
+ --$depth;
+ }
+
+ return false;
+ }
+
+ /**
+ * Checks whether the current tag is void.
+ *
+ * @access private
+ *
+ * @return bool Whether the current tag is void or not.
+ */
+ public function is_void(): bool {
+ $tag_name = $this->get_tag();
+ return Gutenberg_HTML_Processor_6_5::is_void( null !== $tag_name ? $tag_name : '' );
+ }
+ }
+}
diff --git a/lib/compat/wordpress-6.5/interactivity-api/class-wp-interactivity-api.php b/lib/compat/wordpress-6.5/interactivity-api/class-wp-interactivity-api.php
new file mode 100644
index 00000000000000..de1d8b2a9e7890
--- /dev/null
+++ b/lib/compat/wordpress-6.5/interactivity-api/class-wp-interactivity-api.php
@@ -0,0 +1,671 @@
+ 'data_wp_interactive_processor',
+ 'data-wp-context' => 'data_wp_context_processor',
+ 'data-wp-bind' => 'data_wp_bind_processor',
+ 'data-wp-class' => 'data_wp_class_processor',
+ 'data-wp-style' => 'data_wp_style_processor',
+ 'data-wp-text' => 'data_wp_text_processor',
+ );
+
+ /**
+ * Holds the initial state of the different Interactivity API stores.
+ *
+ * This state is used during the server directive processing. Then, it is
+ * serialized and sent to the client as part of the interactivity data to be
+ * recovered during the hydration of the client interactivity stores.
+ *
+ * @since 6.5.0
+ * @var array
+ */
+ private $state_data = array();
+
+ /**
+ * Holds the configuration required by the different Interactivity API stores.
+ *
+ * This configuration is serialized and sent to the client as part of the
+ * interactivity data and can be accessed by the client interactivity stores.
+ *
+ * @since 6.5.0
+ * @var array
+ */
+ private $config_data = array();
+
+ /**
+ * Gets and/or sets the initial state of an Interactivity API store for a
+ * given namespace.
+ *
+ * If state for that store namespace already exists, it merges the new
+ * provided state with the existing one.
+ *
+ * @since 6.5.0
+ *
+ * @param string $store_namespace The unique store namespace identifier.
+ * @param array $state Optional. The array that will be merged with the existing state for the specified
+ * store namespace.
+ * @return array The current state for the specified store namespace.
+ */
+ public function state( string $store_namespace, array $state = null ): array {
+ if ( ! isset( $this->state_data[ $store_namespace ] ) ) {
+ $this->state_data[ $store_namespace ] = array();
+ }
+ if ( is_array( $state ) ) {
+ $this->state_data[ $store_namespace ] = array_replace_recursive(
+ $this->state_data[ $store_namespace ],
+ $state
+ );
+ }
+ return $this->state_data[ $store_namespace ];
+ }
+
+ /**
+ * Gets and/or sets the configuration of the Interactivity API for a given
+ * store namespace.
+ *
+ * If configuration for that store namespace exists, it merges the new
+ * provided configuration with the existing one.
+ *
+ * @since 6.5.0
+ *
+ * @param string $store_namespace The unique store namespace identifier.
+ * @param array $config Optional. The array that will be merged with the existing configuration for the
+ * specified store namespace.
+ * @return array The current configuration for the specified store namespace.
+ */
+ public function config( string $store_namespace, array $config = null ): array {
+ if ( ! isset( $this->config_data[ $store_namespace ] ) ) {
+ $this->config_data[ $store_namespace ] = array();
+ }
+ if ( is_array( $config ) ) {
+ $this->config_data[ $store_namespace ] = array_replace_recursive(
+ $this->config_data[ $store_namespace ],
+ $config
+ );
+ }
+ return $this->config_data[ $store_namespace ];
+ }
+
+ /**
+ * Prints the serialized client-side interactivity data.
+ *
+ * Encodes the config and initial state into JSON and prints them inside a
+ * script tag of type "application/json". Once in the browser, the state will
+ * be parsed and used to hydrate the client-side interactivity stores and the
+ * configuration will be available using a `getConfig` utility.
+ *
+ * @since 6.5.0
+ */
+ public function print_client_interactivity_data() {
+ $store = array();
+ $has_state = ! empty( $this->state_data );
+ $has_config = ! empty( $this->config_data );
+
+ if ( $has_state || $has_config ) {
+ if ( $has_config ) {
+ $store['config'] = $this->config_data;
+ }
+ if ( $has_state ) {
+ $store['state'] = $this->state_data;
+ }
+ wp_print_inline_script_tag(
+ wp_json_encode(
+ $store,
+ JSON_HEX_TAG | JSON_HEX_AMP
+ ),
+ array(
+ 'type' => 'application/json',
+ 'id' => 'wp-interactivity-data',
+ )
+ );
+ }
+ }
+
+ /**
+ * Registers the `@wordpress/interactivity` script modules.
+ *
+ * @since 6.5.0
+ */
+ public function register_script_modules() {
+ wp_register_script_module(
+ '@wordpress/interactivity',
+ gutenberg_url( '/build/interactivity/index.min.js' ),
+ array(),
+ defined( 'GUTENBERG_VERSION' ) ? GUTENBERG_VERSION : get_bloginfo( 'version' )
+ );
+ }
+
+ /**
+ * Adds the necessary hooks for the Interactivity API.
+ *
+ * @since 6.5.0
+ */
+ public function add_hooks() {
+ add_action( 'wp_footer', array( $this, 'print_client_interactivity_data' ) );
+ }
+
+ /**
+ * Processes the interactivity directives contained within the HTML content
+ * and updates the markup accordingly.
+ *
+ * @since 6.5.0
+ *
+ * @param string $html The HTML content to process.
+ * @return string The processed HTML content. It returns the original content when the HTML contains unbalanced tags.
+ */
+ public function process_directives( string $html ): string {
+ $p = new WP_Interactivity_API_Directives_Processor( $html );
+ $tag_stack = array();
+ $namespace_stack = array();
+ $context_stack = array();
+ $unbalanced = false;
+
+ $directive_processor_prefixes = array_keys( self::$directive_processors );
+ $directive_processor_prefixes_reversed = array_reverse( $directive_processor_prefixes );
+
+ while ( $p->next_tag( array( 'tag_closers' => 'visit' ) ) && false === $unbalanced ) {
+ $tag_name = $p->get_tag();
+
+ if ( $p->is_tag_closer() ) {
+ list( $opening_tag_name, $directives_prefixes ) = end( $tag_stack );
+
+ if ( 0 === count( $tag_stack ) || $opening_tag_name !== $tag_name ) {
+
+ /*
+ * If the tag stack is empty or the matching opening tag is not the
+ * same than the closing tag, it means the HTML is unbalanced and it
+ * stops processing it.
+ */
+ $unbalanced = true;
+ continue;
+ } else {
+
+ /*
+ * It removes the last tag from the stack.
+ */
+ array_pop( $tag_stack );
+
+ /*
+ * If the matching opening tag didn't have any directives, it can skip
+ * the processing.
+ */
+ if ( 0 === count( $directives_prefixes ) ) {
+ continue;
+ }
+ }
+ } else {
+ $directives_prefixes = array();
+
+ foreach ( $p->get_attribute_names_with_prefix( 'data-wp-' ) as $attribute_name ) {
+
+ /*
+ * Extracts the directive prefix to see if there is a server directive
+ * processor registered for that directive.
+ */
+ list( $directive_prefix ) = $this->extract_prefix_and_suffix( $attribute_name );
+ if ( array_key_exists( $directive_prefix, self::$directive_processors ) ) {
+ $directives_prefixes[] = $directive_prefix;
+ }
+ }
+
+ /*
+ * If this is not a void element, it adds it to the tag stack so it can
+ * process its closing tag and check for unbalanced tags.
+ */
+ if ( ! $p->is_void() ) {
+ $tag_stack[] = array( $tag_name, $directives_prefixes );
+ }
+ }
+
+ /*
+ * Sorts the attributes by the order of the `directives_processor` array
+ * and checks what directives are present in this element. The processing
+ * order is reversed for tag closers.
+ */
+ $directives_prefixes = array_intersect(
+ $p->is_tag_closer()
+ ? $directive_processor_prefixes_reversed
+ : $directive_processor_prefixes,
+ $directives_prefixes
+ );
+
+ // Executes the directive processors present in this element.
+ foreach ( $directives_prefixes as $directive_prefix ) {
+ $func = is_array( self::$directive_processors[ $directive_prefix ] )
+ ? self::$directive_processors[ $directive_prefix ]
+ : array( $this, self::$directive_processors[ $directive_prefix ] );
+ call_user_func_array(
+ $func,
+ array( $p, &$context_stack, &$namespace_stack )
+ );
+ }
+ }
+
+ /*
+ * It returns the original content if the HTML is unbalanced because
+ * unbalanced HTML is not safe to process. In that case, the Interactivity
+ * API runtime will update the HTML on the client side during the hydration.
+ */
+ return $unbalanced || 0 < count( $tag_stack ) ? $html : $p->get_updated_html();
+ }
+
+ /**
+ * Evaluates the reference path passed to a directive based on the current
+ * store namespace, state and context.
+ *
+ * @since 6.5.0
+ *
+ * @param string|true $directive_value The directive attribute value string or `true` when it's a boolean attribute.
+ * @param string $default_namespace The default namespace to use if none is explicitly defined in the directive
+ * value.
+ * @param array|false $context The current context for evaluating the directive or false if there is no
+ * context.
+ * @return mixed|null The result of the evaluation. Null if the reference path doesn't exist.
+ */
+ private function evaluate( $directive_value, string $default_namespace, $context = false ) {
+ list( $ns, $path ) = $this->extract_directive_value( $directive_value, $default_namespace );
+ if ( empty( $path ) ) {
+ return null;
+ }
+
+ $store = array(
+ 'state' => isset( $this->state_data[ $ns ] ) ? $this->state_data[ $ns ] : array(),
+ 'context' => isset( $context[ $ns ] ) ? $context[ $ns ] : array(),
+ );
+
+ // Checks if the reference path is preceded by a negator operator (!).
+ $should_negate_value = '!' === $path[0];
+ $path = $should_negate_value ? substr( $path, 1 ) : $path;
+
+ // Extracts the value from the store using the reference path.
+ $path_segments = explode( '.', $path );
+ $current = $store;
+ foreach ( $path_segments as $path_segment ) {
+ if ( isset( $current[ $path_segment ] ) ) {
+ $current = $current[ $path_segment ];
+ } else {
+ return null;
+ }
+ }
+
+ // Returns the opposite if it contains a negator operator (!).
+ return $should_negate_value ? ! $current : $current;
+ }
+
+ /**
+ * Extracts the directive attribute name to separate and return the directive
+ * prefix and an optional suffix.
+ *
+ * The suffix is the string after the first double hyphen and the prefix is
+ * everything that comes before the suffix.
+ *
+ * Example:
+ *
+ * extract_prefix_and_suffix( 'data-wp-interactive' ) => array( 'data-wp-interactive', null )
+ * extract_prefix_and_suffix( 'data-wp-bind--src' ) => array( 'data-wp-bind', 'src' )
+ * extract_prefix_and_suffix( 'data-wp-foo--and--bar' ) => array( 'data-wp-foo', 'and--bar' )
+ *
+ * @since 6.5.0
+ *
+ * @param string $directive_name The directive attribute name.
+ * @return array An array containing the directive prefix and optional suffix.
+ */
+ private function extract_prefix_and_suffix( string $directive_name ): array {
+ return explode( '--', $directive_name, 2 );
+ }
+
+ /**
+ * Parses and extracts the namespace and reference path from the given
+ * directive attribute value.
+ *
+ * If the value doesn't contain an explicit namespace, it returns the
+ * default one. If the value contains a JSON object instead of a reference
+ * path, the function tries to parse it and return the resulting array. If
+ * the value contains strings that reprenset booleans ("true" and "false"),
+ * numbers ("1" and "1.2") or "null", the function also transform them to
+ * regular booleans, numbers and `null`.
+ *
+ * Example:
+ *
+ * extract_directive_value( 'actions.foo', 'myPlugin' ) => array( 'myPlugin', 'actions.foo' )
+ * extract_directive_value( 'otherPlugin::actions.foo', 'myPlugin' ) => array( 'otherPlugin', 'actions.foo' )
+ * extract_directive_value( '{ "isOpen": false }', 'myPlugin' ) => array( 'myPlugin', array( 'isOpen' => false ) )
+ * extract_directive_value( 'otherPlugin::{ "isOpen": false }', 'myPlugin' ) => array( 'otherPlugin', array( 'isOpen' => false ) )
+ *
+ * @since 6.5.0
+ *
+ * @param string|true $directive_value The directive attribute value. It can be `true` when it's a boolean
+ * attribute.
+ * @param string|null $default_namespace Optional. The default namespace if none is explicitly defined.
+ * @return array An array containing the namespace in the first item and the JSON, the reference path, or null on the
+ * second item.
+ */
+ private function extract_directive_value( $directive_value, $default_namespace = null ): array {
+ if ( empty( $directive_value ) || is_bool( $directive_value ) ) {
+ return array( $default_namespace, null );
+ }
+
+ // Replaces the value and namespace if there is a namespace in the value.
+ if ( 1 === preg_match( '/^([\w\-_\/]+)::./', $directive_value ) ) {
+ list($default_namespace, $directive_value) = explode( '::', $directive_value, 2 );
+ }
+
+ /*
+ * Tries to decode the value as a JSON object. If it fails and the value
+ * isn't `null`, it returns the value as it is. Otherwise, it returns the
+ * decoded JSON or null for the string `null`.
+ */
+ $decoded_json = json_decode( $directive_value, true );
+ if ( null !== $decoded_json || 'null' === $directive_value ) {
+ $directive_value = $decoded_json;
+ }
+
+ return array( $default_namespace, $directive_value );
+ }
+
+
+ /**
+ * Processes the `data-wp-interactive` directive.
+ *
+ * It adds the default store namespace defined in the directive value to the
+ * stack so it's available for the nested interactivity elements.
+ *
+ * @since 6.5.0
+ *
+ * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance.
+ * @param array $context_stack The reference to the context stack.
+ * @param array $namespace_stack The reference to the store namespace stack.
+ */
+ private function data_wp_interactive_processor( WP_Interactivity_API_Directives_Processor $p, array &$context_stack, array &$namespace_stack ) {
+ // In closing tags, it removes the last namespace from the stack.
+ if ( $p->is_tag_closer() ) {
+ return array_pop( $namespace_stack );
+ }
+
+ // Tries to decode the `data-wp-interactive` attribute value.
+ $attribute_value = $p->get_attribute( 'data-wp-interactive' );
+ $decoded_json = is_string( $attribute_value ) && ! empty( $attribute_value )
+ ? json_decode( $attribute_value, true )
+ : null;
+
+ /*
+ * Pushes the newly defined namespace or the current one if the
+ * `data-wp-interactive` definition was invalid or does not contain a
+ * namespace. It does so because the function pops out the current namespace
+ * from the stack whenever it finds a `data-wp-interactive`'s closing tag,
+ * independently of whether the previous `data-wp-interactive` definition
+ * contained a valid namespace.
+ */
+ $namespace_stack[] = isset( $decoded_json['namespace'] )
+ ? $decoded_json['namespace']
+ : end( $namespace_stack );
+ }
+
+ /**
+ * Processes the `data-wp-context` directive.
+ *
+ * It adds the context defined in the directive value to the stack so it's
+ * available for the nested interactivity elements.
+ *
+ * @since 6.5.0
+ *
+ * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance.
+ * @param array $context_stack The reference to the context stack.
+ * @param array $namespace_stack The reference to the store namespace stack.
+ */
+ private function data_wp_context_processor( WP_Interactivity_API_Directives_Processor $p, array &$context_stack, array &$namespace_stack ) {
+ // In closing tags, it removes the last context from the stack.
+ if ( $p->is_tag_closer() ) {
+ return array_pop( $context_stack );
+ }
+
+ $attribute_value = $p->get_attribute( 'data-wp-context' );
+ $namespace_value = end( $namespace_stack );
+
+ // Separates the namespace from the context JSON object.
+ list( $namespace_value, $decoded_json ) = is_string( $attribute_value ) && ! empty( $attribute_value )
+ ? $this->extract_directive_value( $attribute_value, $namespace_value )
+ : array( $namespace_value, null );
+
+ /*
+ * If there is a namespace, it adds a new context to the stack merging the
+ * previous context with the new one.
+ */
+ if ( is_string( $namespace_value ) ) {
+ array_push(
+ $context_stack,
+ array_replace_recursive(
+ end( $context_stack ) !== false ? end( $context_stack ) : array(),
+ array( $namespace_value => is_array( $decoded_json ) ? $decoded_json : array() )
+ )
+ );
+ } else {
+ /*
+ * If there is no namespace, it pushes the current context to the stack.
+ * It needs to do so because the function pops out the current context
+ * from the stack whenever it finds a `data-wp-context`'s closing tag.
+ */
+ array_push( $context_stack, end( $context_stack ) );
+ }
+ }
+
+ /**
+ * Processes the `data-wp-bind` directive.
+ *
+ * It updates or removes the bound attributes based on the evaluation of its
+ * associated reference.
+ *
+ * @since 6.5.0
+ *
+ * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance.
+ * @param array $context_stack The reference to the context stack.
+ * @param array $namespace_stack The reference to the store namespace stack.
+ */
+ private function data_wp_bind_processor( WP_Interactivity_API_Directives_Processor $p, array &$context_stack, array &$namespace_stack ) {
+ if ( ! $p->is_tag_closer() ) {
+ $all_bind_directives = $p->get_attribute_names_with_prefix( 'data-wp-bind--' );
+
+ foreach ( $all_bind_directives as $attribute_name ) {
+ list( , $bound_attribute ) = $this->extract_prefix_and_suffix( $attribute_name );
+ if ( empty( $bound_attribute ) ) {
+ return;
+ }
+
+ $attribute_value = $p->get_attribute( $attribute_name );
+ $result = $this->evaluate( $attribute_value, end( $namespace_stack ), end( $context_stack ) );
+
+ if ( null !== $result && ( false !== $result || '-' === $bound_attribute[4] ) ) {
+ /*
+ * If the result of the evaluation is a boolean and the attribute is
+ * `aria-` or `data-, convert it to a string "true" or "false". It
+ * follows the exact same logic as Preact because it needs to
+ * replicate what Preact will later do in the client:
+ * https://github.com/preactjs/preact/blob/ea49f7a0f9d1ff2c98c0bdd66aa0cbc583055246/src/diff/props.js#L131C24-L136
+ */
+ if ( is_bool( $result ) && '-' === $bound_attribute[4] ) {
+ $result = $result ? 'true' : 'false';
+ }
+ $p->set_attribute( $bound_attribute, $result );
+ } else {
+ $p->remove_attribute( $bound_attribute );
+ }
+ }
+ }
+ }
+
+
+ /**
+ * Processes the `data-wp-class` directive.
+ *
+ * It adds or removes CSS classes in the current HTML element based on the
+ * evaluation of its associated references.
+ *
+ * @since 6.5.0
+ *
+ * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance.
+ * @param array $context_stack The reference to the context stack.
+ * @param array $namespace_stack The reference to the store namespace stack.
+ */
+ private function data_wp_class_processor( WP_Interactivity_API_Directives_Processor $p, array &$context_stack, array &$namespace_stack ) {
+ if ( ! $p->is_tag_closer() ) {
+ $all_class_directives = $p->get_attribute_names_with_prefix( 'data-wp-class--' );
+
+ foreach ( $all_class_directives as $attribute_name ) {
+ list( , $class_name ) = $this->extract_prefix_and_suffix( $attribute_name );
+ if ( empty( $class_name ) ) {
+ return;
+ }
+
+ $attribute_value = $p->get_attribute( $attribute_name );
+ $result = $this->evaluate( $attribute_value, end( $namespace_stack ), end( $context_stack ) );
+
+ if ( $result ) {
+ $p->add_class( $class_name );
+ } else {
+ $p->remove_class( $class_name );
+ }
+ }
+ }
+ }
+
+ /**
+ * Processes the `data-wp-style` directive.
+ *
+ * It updates the style attribute value of the current HTML element based on
+ * the evaluation of its associated references.
+ *
+ * @since 6.5.0
+ *
+ * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance.
+ * @param array $context_stack The reference to the context stack.
+ * @param array $namespace_stack The reference to the store namespace stack.
+ */
+ private function data_wp_style_processor( WP_Interactivity_API_Directives_Processor $p, array &$context_stack, array &$namespace_stack ) {
+ if ( ! $p->is_tag_closer() ) {
+ $all_style_attributes = $p->get_attribute_names_with_prefix( 'data-wp-style--' );
+
+ foreach ( $all_style_attributes as $attribute_name ) {
+ list( , $style_property ) = $this->extract_prefix_and_suffix( $attribute_name );
+ if ( empty( $style_property ) ) {
+ continue;
+ }
+
+ $directive_attribute_value = $p->get_attribute( $attribute_name );
+ $style_property_value = $this->evaluate( $directive_attribute_value, end( $namespace_stack ), end( $context_stack ) );
+ $style_attribute_value = $p->get_attribute( 'style' );
+ $style_attribute_value = ( $style_attribute_value && ! is_bool( $style_attribute_value ) ) ? $style_attribute_value : '';
+
+ /*
+ * Checks first if the style property is not falsy and the style
+ * attribute value is not empty because if it is, it doesn't need to
+ * update the attribute value.
+ */
+ if ( $style_property_value || ( ! $style_property_value && $style_attribute_value ) ) {
+ $style_attribute_value = $this->set_style_property( $style_attribute_value, $style_property, $style_property_value );
+ /*
+ * If the style attribute value is not empty, it sets it. Otherwise,
+ * it removes it.
+ */
+ if ( ! empty( $style_attribute_value ) ) {
+ $p->set_attribute( 'style', $style_attribute_value );
+ } else {
+ $p->remove_attribute( 'style' );
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Sets an individual style property in the `style` attribute of an HTML
+ * element, updating or removing the property when necessary.
+ *
+ * If a property is modified, it is added at the end of the list to make sure
+ * that it overrides the previous ones.
+ *
+ * @since 6.5.0
+ *
+ * Example:
+ *
+ * set_style_property( 'color:green;', 'color', 'red' ) => 'color:red;'
+ * set_style_property( 'background:green;', 'color', 'red' ) => 'background:green;color:red;'
+ * set_style_property( 'color:green;', 'color', null ) => ''
+ *
+ * @param string $style_attribute_value The current style attribute value.
+ * @param string $style_property_name The style property name to set.
+ * @param string|false|null $style_property_value The value to set for the style property. With false, null or an
+ * empty string, it removes the style property.
+ * @return string The new style attribute value after the specified property has been added, updated or removed.
+ */
+ private function set_style_property( string $style_attribute_value, string $style_property_name, $style_property_value ): string {
+ $style_assignments = explode( ';', $style_attribute_value );
+ $result = array();
+ $style_property_value = ! empty( $style_property_value ) ? rtrim( trim( $style_property_value ), ';' ) : null;
+ $new_style_property = $style_property_value ? $style_property_name . ':' . $style_property_value . ';' : '';
+
+ // Generate an array with all the properties but the modified one.
+ foreach ( $style_assignments as $style_assignment ) {
+ if ( empty( trim( $style_assignment ) ) ) {
+ continue;
+ }
+ list( $name, $value ) = explode( ':', $style_assignment );
+ if ( trim( $name ) !== $style_property_name ) {
+ $result[] = trim( $name ) . ':' . trim( $value ) . ';';
+ }
+ }
+
+ // Add the new/modified property at the end of the list.
+ array_push( $result, $new_style_property );
+
+ return implode( '', $result );
+ }
+
+ /**
+ * Processes the `data-wp-text` directive.
+ *
+ * It updates the inner content of the current HTML element based on the
+ * evaluation of its associated reference.
+ *
+ * @since 6.5.0
+ *
+ * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance.
+ * @param array $context_stack The reference to the context stack.
+ * @param array $namespace_stack The reference to the store namespace stack.
+ */
+ private function data_wp_text_processor( WP_Interactivity_API_Directives_Processor $p, array &$context_stack, array &$namespace_stack ) {
+ if ( ! $p->is_tag_closer() ) {
+ $attribute_value = $p->get_attribute( 'data-wp-text' );
+ $result = $this->evaluate( $attribute_value, end( $namespace_stack ), end( $context_stack ) );
+
+ /*
+ * Follows the same logic as Preact in the client and only changes the
+ * content if the value is a string or a number. Otherwise, it removes the
+ * content.
+ */
+ if ( is_string( $result ) || is_numeric( $result ) ) {
+ $p->set_content_between_balanced_tags( esc_html( $result ) );
+ } else {
+ $p->set_content_between_balanced_tags( '' );
+ }
+ }
+ }
+ }
+
+}
diff --git a/lib/compat/wordpress-6.5/interactivity-api/interactivity-api.php b/lib/compat/wordpress-6.5/interactivity-api/interactivity-api.php
new file mode 100644
index 00000000000000..cd7ca7fb902870
--- /dev/null
+++ b/lib/compat/wordpress-6.5/interactivity-api/interactivity-api.php
@@ -0,0 +1,144 @@
+get_registered( $block_name );
+
+ if ( isset( $block_name ) && isset( $block_type->supports['interactivity'] ) && $block_type->supports['interactivity'] ) {
+ // Annotates the root interactive block for processing.
+ $root_interactive_block = array( $block_name, md5( serialize( $parsed_block ) ) );
+
+ /*
+ * Adds a filter to process the root interactive block once it has
+ * finished rendering.
+ */
+ $process_interactive_blocks = static function ( $content, $parsed_block ) use ( &$root_interactive_block, &$process_interactive_blocks ) {
+ // Checks whether the current block is the root interactive block.
+ list($root_block_name, $root_block_md5) = $root_interactive_block;
+ if ( $root_block_name === $parsed_block['blockName'] && md5( serialize( $parsed_block ) ) === $root_block_md5 ) {
+ // The root interactive blocks has finished rendering, process it.
+ $content = wp_interactivity_process_directives( $content );
+ // Removes the filter and reset the root interactive block.
+ remove_filter( 'render_block', $process_interactive_blocks );
+ $root_interactive_block = null;
+ }
+ return $content;
+ };
+
+ /*
+ * Uses a priority of 20 to ensure that other filters can add additional
+ * directives before the processing starts.
+ */
+ add_filter( 'render_block', $process_interactive_blocks, 20, 2 );
+ }
+ }
+
+ return $parsed_block;
+ }
+ add_filter( 'render_block_data', 'wp_interactivity_process_directives_of_interactive_blocks', 10, 1 );
+}
+
+if ( ! function_exists( 'wp_interactivity' ) ) {
+ /**
+ * Retrieves the main WP_Interactivity_API instance.
+ *
+ * It provides access to the WP_Interactivity_API instance, creating one if it
+ * doesn't exist yet. It also registers the hooks and necessary script
+ * modules.
+ *
+ * @since 6.5.0
+ *
+ * @return WP_Interactivity_API The main WP_Interactivity_API instance.
+ */
+ function wp_interactivity() {
+ static $instance = null;
+ if ( is_null( $instance ) ) {
+ $instance = new WP_Interactivity_API();
+ $instance->add_hooks();
+ $instance->register_script_modules();
+ }
+ return $instance;
+ }
+}
+
+if ( ! function_exists( 'wp_interactivity_process_directives' ) ) {
+ /**
+ * Processes the interactivity directives contained within the HTML content
+ * and updates the markup accordingly.
+ *
+ * @since 6.5.0
+ *
+ * @param string $html The HTML content to process.
+ * @return string The processed HTML content. It returns the original content when the HTML contains unbalanced tags.
+ */
+ function wp_interactivity_process_directives( $html ) {
+ return wp_interactivity()->process_directives( $html );
+ }
+}
+
+if ( ! function_exists( 'wp_interactivity_state' ) ) {
+ /**
+ * Gets and/or sets the initial state of an Interactivity API store for a
+ * given namespace.
+ *
+ * If state for that store namespace already exists, it merges the new
+ * provided state with the existing one.
+ *
+ * @since 6.5.0
+ *
+ * @param string $store_namespace The unique store namespace identifier.
+ * @param array $state Optional. The array that will be merged with the existing state for the specified
+ * store namespace.
+ * @return array The current state for the specified store namespace.
+ */
+ function wp_interactivity_state( $store_namespace, $state = null ) {
+ return wp_interactivity()->state( $store_namespace, $state );
+ }
+}
+
+if ( ! function_exists( 'wp_interactivity_config' ) ) {
+ /**
+ * Gets and/or sets the configuration of the Interactivity API for a given
+ * store namespace.
+ *
+ * If configuration for that store namespace exists, it merges the new
+ * provided configuration with the existing one.
+ *
+ * @since 6.5.0
+ *
+ * @param string $store_namespace The unique store namespace identifier.
+ * @param array $config Optional. The array that will be merged with the existing configuration for the
+ * specified store namespace.
+ * @return array The current configuration for the specified store namespace.
+ */
+ function wp_interactivity_config( $store_namespace, $initial_state = null ) {
+ return wp_interactivity()->config( $store_namespace, $initial_state );
+ }
+}
diff --git a/lib/compat/wordpress-6.5/scripts-modules.php b/lib/compat/wordpress-6.5/scripts-modules.php
new file mode 100644
index 00000000000000..ba329b255b1965
--- /dev/null
+++ b/lib/compat/wordpress-6.5/scripts-modules.php
@@ -0,0 +1,119 @@
+add_hooks();
+ }
+ return $instance;
+ }
+}
+
+if ( ! function_exists( 'wp_register_script_module' ) ) {
+ /**
+ * Registers the script module if no script module with that script module
+ * identifier has already been registered.
+ *
+ * @since 6.5.0
+ *
+ * @param string $id The identifier of the script module. Should be unique. It will be used in the
+ * final import map.
+ * @param string $src Optional. Full URL of the script module, or path of the script module relative
+ * to the WordPress root directory. If it is provided and the script module has
+ * not been registered yet, it will be registered.
+ * @param array $deps {
+ * Optional. List of dependencies.
+ *
+ * @type string|array $0... {
+ * An array of script module identifiers of the dependencies of this script
+ * module. The dependencies can be strings or arrays. If they are arrays,
+ * they need an `id` key with the script module identifier, and can contain
+ * an `import` key with either `static` or `dynamic`. By default,
+ * dependencies that don't contain an `import` key are considered static.
+ *
+ * @type string $id The script module identifier.
+ * @type string $import Optional. Import type. May be either `static` or
+ * `dynamic`. Defaults to `static`.
+ * }
+ * }
+ * @param string|false|null $version Optional. String specifying the script module version number. Defaults to false.
+ * It is added to the URL as a query string for cache busting purposes. If $version
+ * is set to false, the version number is the currently installed WordPress version.
+ * If $version is set to null, no version is added.
+ */
+ function wp_register_script_module( string $id, string $src, array $deps = array(), $version = false ) {
+ wp_script_modules()->register( $id, $src, $deps, $version );
+ }
+}
+
+if ( ! function_exists( 'wp_enqueue_script_module' ) ) {
+ /**
+ * Marks the script module to be enqueued in the page.
+ *
+ * If a src is provided and the script module has not been registered yet, it
+ * will be registered.
+ *
+ * @since 6.5.0
+ *
+ * @param string $id The identifier of the script module. Should be unique. It will be used in the
+ * final import map.
+ * @param string $src Optional. Full URL of the script module, or path of the script module relative
+ * to the WordPress root directory. If it is provided and the script module has
+ * not been registered yet, it will be registered.
+ * @param array $deps {
+ * Optional. List of dependencies.
+ *
+ * @type string|array $0... {
+ * An array of script module identifiers of the dependencies of this script
+ * module. The dependencies can be strings or arrays. If they are arrays,
+ * they need an `id` key with the script module identifier, and can contain
+ * an `import` key with either `static` or `dynamic`. By default,
+ * dependencies that don't contain an `import` key are considered static.
+ *
+ * @type string $id The script module identifier.
+ * @type string $import Optional. Import type. May be either `static` or
+ * `dynamic`. Defaults to `static`.
+ * }
+ * }
+ * @param string|false|null $version Optional. String specifying the script module version number. Defaults to false.
+ * It is added to the URL as a query string for cache busting purposes. If $version
+ * is set to false, the version number is the currently installed WordPress version.
+ * If $version is set to null, no version is added.
+ */
+ function wp_enqueue_script_module( string $id, string $src = '', array $deps = array(), $version = false ) {
+ wp_script_modules()->enqueue( $id, $src, $deps, $version );
+ }
+}
+
+if ( ! function_exists( 'wp_dequeue_script_module' ) ) {
+ /**
+ * Unmarks the script module so it is no longer enqueued in the page.
+ *
+ * @since 6.5.0
+ *
+ * @param string $id The identifier of the script module.
+ */
+ function wp_dequeue_script_module( string $id ) {
+ wp_script_modules()->dequeue( $id );
+ }
+}
diff --git a/lib/experimental/block-bindings/index.php b/lib/experimental/block-bindings/index.php
deleted file mode 100644
index dc9a6c9b96957b..00000000000000
--- a/lib/experimental/block-bindings/index.php
+++ /dev/null
@@ -1,19 +0,0 @@
-attributes, array( 'metadata', 'id' ), false ) ) {
- return null;
- }
- $block_id = $block_instance->attributes['metadata']['id'];
- return _wp_array_get( $block_instance->context, array( 'pattern/overrides', $block_id, $attribute_name ), null );
- };
- wp_block_bindings_register_source(
- 'pattern_attributes',
- __( 'Pattern Attributes', 'gutenberg' ),
- $pattern_source_callback
- );
-}
diff --git a/lib/experimental/blocks.php b/lib/experimental/blocks.php
index 93b65f95fc61ae..fc67f2c9d43770 100644
--- a/lib/experimental/blocks.php
+++ b/lib/experimental/blocks.php
@@ -77,92 +77,3 @@ function wp_enqueue_block_view_script( $block_name, $args ) {
add_filter( 'render_block', $callback, 10, 2 );
}
}
-
-
-
-
-$gutenberg_experiments = get_option( 'gutenberg-experiments' );
-if ( $gutenberg_experiments && (
- array_key_exists( 'gutenberg-block-bindings', $gutenberg_experiments ) ||
- array_key_exists( 'gutenberg-pattern-partial-syncing', $gutenberg_experiments )
-) ) {
-
- require_once __DIR__ . '/block-bindings/index.php';
-
- if ( ! function_exists( 'gutenberg_process_block_bindings' ) ) {
- /**
- * Process the block bindings attribute.
- *
- * @param string $block_content Block Content.
- * @param array $block Block attributes.
- * @param WP_Block $block_instance The block instance.
- */
- function gutenberg_process_block_bindings( $block_content, $block, $block_instance ) {
-
- // Allowed blocks that support block bindings.
- // TODO: Look for a mechanism to opt-in for this. Maybe adding a property to block attributes?
- $allowed_blocks = array(
- 'core/paragraph' => array( 'content' ),
- 'core/heading' => array( 'content' ),
- 'core/image' => array( 'url', 'title', 'alt' ),
- 'core/button' => array( 'url', 'text' ),
- );
-
- // If the block doesn't have the bindings property or isn't one of the allowed block types, return.
- if ( ! isset( $block['attrs']['metadata']['bindings'] ) || ! isset( $allowed_blocks[ $block_instance->name ] ) ) {
- return $block_content;
- }
-
- // Assuming the following format for the bindings property of the "metadata" attribute:
- //
- // "bindings": {
- // "title": {
- // "source": {
- // "name": "post_meta",
- // "attributes": { "value": "text_custom_field" }
- // }
- // },
- // "url": {
- // "source": {
- // "name": "post_meta",
- // "attributes": { "value": "text_custom_field" }
- // }
- // }
- // }
- //
-
- $block_bindings_sources = wp_block_bindings_get_sources();
- $modified_block_content = $block_content;
- foreach ( $block['attrs']['metadata']['bindings'] as $binding_attribute => $binding_source ) {
-
- // If the attribute is not in the list, process next attribute.
- if ( ! in_array( $binding_attribute, $allowed_blocks[ $block_instance->name ], true ) ) {
- continue;
- }
- // If no source is provided, or that source is not registered, process next attribute.
- if ( ! isset( $binding_source['source'] ) || ! isset( $binding_source['source']['name'] ) || ! isset( $block_bindings_sources[ $binding_source['source']['name'] ] ) ) {
- continue;
- }
-
- $source_callback = $block_bindings_sources[ $binding_source['source']['name'] ]['apply'];
- // Get the value based on the source.
- if ( ! isset( $binding_source['source']['attributes'] ) ) {
- $source_args = array();
- } else {
- $source_args = $binding_source['source']['attributes'];
- }
- $source_value = $source_callback( $source_args, $block_instance, $binding_attribute );
- // If the value is null, process next attribute.
- if ( is_null( $source_value ) ) {
- continue;
- }
-
- // Process the HTML based on the block and the attribute.
- $modified_block_content = wp_block_bindings_replace_html( $modified_block_content, $block_instance->name, $binding_attribute, $source_value );
- }
- return $modified_block_content;
- }
- }
-
- add_filter( 'render_block', 'gutenberg_process_block_bindings', 20, 3 );
-}
diff --git a/lib/experimental/editor-settings.php b/lib/experimental/editor-settings.php
index 729376cf030dd9..37774e07b27691 100644
--- a/lib/experimental/editor-settings.php
+++ b/lib/experimental/editor-settings.php
@@ -25,18 +25,9 @@ function gutenberg_enable_experiments() {
if ( $gutenberg_experiments && array_key_exists( 'gutenberg-group-grid-variation', $gutenberg_experiments ) ) {
wp_add_inline_script( 'wp-block-editor', 'window.__experimentalEnableGroupGridVariation = true', 'before' );
}
-
- if ( $gutenberg_experiments && array_key_exists( 'gutenberg-block-bindings', $gutenberg_experiments ) ) {
- wp_add_inline_script( 'wp-block-editor', 'window.__experimentalBlockBindings = true', 'before' );
- }
-
if ( gutenberg_is_experiment_enabled( 'gutenberg-no-tinymce' ) ) {
wp_add_inline_script( 'wp-block-library', 'window.__experimentalDisableTinymce = true', 'before' );
}
-
- if ( $gutenberg_experiments && array_key_exists( 'gutenberg-pattern-partial-syncing', $gutenberg_experiments ) ) {
- wp_add_inline_script( 'wp-block-editor', 'window.__experimentalPatternPartialSyncing = true', 'before' );
- }
}
add_action( 'admin_init', 'gutenberg_enable_experiments' );
diff --git a/lib/experimental/fonts/font-library/class-wp-font-collection.php b/lib/experimental/fonts/font-library/class-wp-font-collection.php
index 6189da5fa984b1..1ff96b1343b453 100644
--- a/lib/experimental/fonts/font-library/class-wp-font-collection.php
+++ b/lib/experimental/fonts/font-library/class-wp-font-collection.php
@@ -21,41 +21,128 @@
class WP_Font_Collection {
/**
- * Font collection configuration.
+ * The unique slug for the font collection.
+ *
+ * @since 6.5.0
+ *
+ * @var string
+ */
+ private $slug;
+
+ /**
+ * The name of the font collection.
+ *
+ * @since 6.5.0
+ *
+ * @var string
+ */
+ private $name;
+
+ /**
+ * Description of the font collection.
+ *
+ * @since 6.5.0
+ *
+ * @var string
+ */
+ private $description;
+
+ /**
+ * Source of the font collection.
+ *
+ * @since 6.5.0
+ *
+ * @var string
+ */
+ private $src;
+
+ /**
+ * Array of font families in the collection.
*
* @since 6.5.0
*
* @var array
*/
- private $config;
+ private $font_families;
+
+ /**
+ * Categories associated with the font collection.
+ *
+ * @since 6.5.0
+ *
+ * @var array
+ */
+ private $categories;
+
/**
* WP_Font_Collection constructor.
*
* @since 6.5.0
*
- * @param array $config Font collection config options.
- * See {@see wp_register_font_collection()} for the supported fields.
- * @throws Exception If the required parameters are missing.
+ * @param array $config Font collection config options. {
+ * @type string $slug The font collection's unique slug.
+ * @type string $name The font collection's name.
+ * @type string $description The font collection's description.
+ * @type string $src The font collection's source.
+ * @type array $font_families An array of font families in the font collection.
+ * @type array $categories The font collection's categories.
+ * }
*/
public function __construct( $config ) {
- if ( empty( $config ) || ! is_array( $config ) ) {
- throw new Exception( 'Font Collection config options is required as a non-empty array.' );
- }
+ $this->is_config_valid( $config );
+
+ $this->slug = isset( $config['slug'] ) ? $config['slug'] : '';
+ $this->name = isset( $config['name'] ) ? $config['name'] : '';
+ $this->description = isset( $config['description'] ) ? $config['description'] : '';
+ $this->src = isset( $config['src'] ) ? $config['src'] : '';
+ $this->font_families = isset( $config['font_families'] ) ? $config['font_families'] : array();
+ $this->categories = isset( $config['categories'] ) ? $config['categories'] : array();
+ }
- if ( empty( $config['slug'] ) || ! is_string( $config['slug'] ) ) {
- throw new Exception( 'Font Collection config slug is required as a non-empty string.' );
+ /**
+ * Checks if the font collection config is valid.
+ *
+ * @since 6.5.0
+ *
+ * @param array $config Font collection config options. {
+ * @type string $slug The font collection's unique slug.
+ * @type string $name The font collection's name.
+ * @type string $description The font collection's description.
+ * @type string $src The font collection's source.
+ * @type array $font_families An array of font families in the font collection.
+ * @type array $categories The font collection's categories.
+ * }
+ * @return bool True if the font collection config is valid and false otherwise.
+ */
+ public static function is_config_valid( $config ) {
+ if ( empty( $config ) || ! is_array( $config ) ) {
+ _doing_it_wrong( __METHOD__, __( 'Font Collection config options are required as a non-empty array.', 'gutenberg' ), '6.5.0' );
+ return false;
}
- if ( empty( $config['name'] ) || ! is_string( $config['name'] ) ) {
- throw new Exception( 'Font Collection config name is required as a non-empty string.' );
+ $required_keys = array( 'slug', 'name' );
+ foreach ( $required_keys as $key ) {
+ if ( empty( $config[ $key ] ) ) {
+ _doing_it_wrong(
+ __METHOD__,
+ // translators: %s: Font collection config key.
+ sprintf( __( 'Font Collection config %s is required as a non-empty string.', 'gutenberg' ), $key ),
+ '6.5.0'
+ );
+ return false;
+ }
}
- if ( ( empty( $config['src'] ) || ! is_string( $config['src'] ) ) && ( empty( $config['data'] ) ) ) {
- throw new Exception( 'Font Collection config "src" option OR "data" option is required.' );
+ if (
+ ( empty( $config['src'] ) && empty( $config['font_families'] ) ) ||
+ ( ! empty( $config['src'] ) && ! empty( $config['font_families'] ) )
+ ) {
+ _doing_it_wrong( __METHOD__, __( 'Font Collection config "src" option OR "font_families" option are required.', 'gutenberg' ), '6.5.0' );
+ return false;
}
- $this->config = $config;
+ return true;
}
/**
@@ -73,56 +160,59 @@ public function __construct( $config ) {
*/
public function get_config() {
return array(
- 'slug' => $this->config['slug'],
- 'name' => $this->config['name'],
- 'description' => $this->config['description'] ?? '',
+ 'slug' => $this->slug,
+ 'name' => $this->name,
+ 'description' => $this->description,
);
}
/**
- * Gets the font collection config and data.
+ * Gets the font collection content.
*
- * This function returns an array containing the font collection's unique ID,
- * name, and its data as a PHP array.
+ * Load the font collection data from the src if it is not already loaded.
*
* @since 6.5.0
*
- * @return array {
- * An array of font collection config and data.
+ * @return array|WP_Error {
+ * An array of font collection contents.
*
- * @type string $slug The font collection's unique ID.
- * @type string $name The font collection's name.
- * @type string $description The font collection's description.
- * @type array $data The font collection's data as a PHP array.
+ * @type array $font_families The font collection's font families.
+ * @type string $categories The font collection's categories.
* }
+ *
+ * A WP_Error object if there was an error loading the font collection data.
*/
- public function get_config_and_data() {
- $config_and_data = $this->get_config();
- $config_and_data['data'] = $this->load_data();
- return $config_and_data;
+ public function get_content() {
+ // If the font families are not loaded, and the src is not empty, load the data from the src.
+ if ( empty( $this->font_families ) && ! empty( $this->src ) ) {
+ $data = $this->load_contents_from_src();
+ if ( is_wp_error( $data ) ) {
+ return $data;
+ }
+ }
+
+ return array(
+ 'font_families' => $this->font_families,
+ 'categories' => $this->categories,
+ );
}
/**
- * Loads the font collection data.
+ * Loads the font collection data from the src.
*
* @since 6.5.0
*
* @return array|WP_Error An array containing the list of font families in font-collection.json format on success,
* else an instance of WP_Error on failure.
*/
- public function load_data() {
-
- if ( ! empty( $this->config['data'] ) ) {
- return $this->config['data'];
- }
-
+ private function load_contents_from_src() {
// If the src is a URL, fetch the data from the URL.
- if ( str_contains( $this->config['src'], 'http' ) && str_contains( $this->config['src'], '://' ) ) {
- if ( ! wp_http_validate_url( $this->config['src'] ) ) {
+ if ( preg_match( '#^https?://#', $this->src ) ) {
+ if ( ! wp_http_validate_url( $this->src ) ) {
return new WP_Error( 'font_collection_read_error', __( 'Invalid URL for Font Collection data.', 'gutenberg' ) );
}
- $response = wp_remote_get( $this->config['src'] );
+ $response = wp_remote_get( $this->src );
if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
return new WP_Error( 'font_collection_read_error', __( 'Error fetching the Font Collection data from a URL.', 'gutenberg' ) );
}
@@ -133,15 +223,22 @@ public function load_data() {
}
// If the src is a file path, read the data from the file.
} else {
- if ( ! file_exists( $this->config['src'] ) ) {
+ if ( ! file_exists( $this->src ) ) {
return new WP_Error( 'font_collection_read_error', __( 'Font Collection data JSON file does not exist.', 'gutenberg' ) );
}
- $data = wp_json_file_decode( $this->config['src'], array( 'associative' => true ) );
+ $data = wp_json_file_decode( $this->src, array( 'associative' => true ) );
if ( empty( $data ) ) {
return new WP_Error( 'font_collection_read_error', __( 'Error reading the Font Collection data JSON file contents.', 'gutenberg' ) );
}
}
+ if ( empty( $data['font_families'] ) ) {
+ return new WP_Error( 'font_collection_contents_error', __( 'Font Collection data JSON file does not contain font families.', 'gutenberg' ) );
+ }
+
+ $this->font_families = $data['font_families'];
+ $this->categories = $data['categories'] ?? array();
+
return $data;
}
}
diff --git a/lib/experimental/fonts/font-library/class-wp-font-family-utils.php b/lib/experimental/fonts/font-library/class-wp-font-family-utils.php
index 35e6856e50aad8..b291a8f1ee348d 100644
--- a/lib/experimental/fonts/font-library/class-wp-font-family-utils.php
+++ b/lib/experimental/fonts/font-library/class-wp-font-family-utils.php
@@ -91,7 +91,7 @@ public static function format_font_family( $font_family ) {
function ( $family ) {
$trimmed = trim( $family );
if ( ! empty( $trimmed ) && strpos( $trimmed, ' ' ) !== false && strpos( $trimmed, "'" ) === false && strpos( $trimmed, '"' ) === false ) {
- return "'" . $trimmed . "'";
+ return '"' . $trimmed . '"';
}
return $trimmed;
},
@@ -107,4 +107,84 @@ function ( $family ) {
return $font_family;
}
+
+ /**
+ * Generates a slug from font face properties, e.g. `open sans;normal;400;100%;U+0-10FFFF`
+ *
+ * Used for comparison with other font faces in the same family, to prevent duplicates
+ * that would both match according the CSS font matching spec. Uses only simple case-insensitive
+ * matching for fontFamily and unicodeRange, so does not handle overlapping font-family lists or
+ * unicode ranges.
+ *
+ * @since 6.5.0
+ *
+ * @link https://drafts.csswg.org/css-fonts/#font-style-matching
+ *
+ * @param array $settings {
+ * Font face settings.
+ *
+ * @type string $fontFamily Font family name.
+ * @type string $fontStyle Optional font style, defaults to 'normal'.
+ * @type string $fontWeight Optional font weight, defaults to 400.
+ * @type string $fontStretch Optional font stretch, defaults to '100%'.
+ * @type string $unicodeRange Optional unicode range, defaults to 'U+0-10FFFF'.
+ * }
+ * @return string Font face slug.
+ */
+ public static function get_font_face_slug( $settings ) {
+ $settings = wp_parse_args(
+ $settings,
+ array(
+ 'fontFamily' => '',
+ 'fontStyle' => 'normal',
+ 'fontWeight' => '400',
+ 'fontStretch' => '100%',
+ 'unicodeRange' => 'U+0-10FFFF',
+ )
+ );
+
+ // Convert all values to lowercase for comparison.
+ // Font family names may use multibyte characters.
+ $font_family = mb_strtolower( $settings['fontFamily'] );
+ $font_style = strtolower( $settings['fontStyle'] );
+ $font_weight = strtolower( $settings['fontWeight'] );
+ $font_stretch = strtolower( $settings['fontStretch'] );
+ $unicode_range = strtoupper( $settings['unicodeRange'] );
+
+ // Convert weight keywords to numeric strings.
+ $font_weight = str_replace( 'normal', '400', $font_weight );
+ $font_weight = str_replace( 'bold', '700', $font_weight );
+
+ // Convert stretch keywords to numeric strings.
+ $font_stretch_map = array(
+ 'ultra-condensed' => '50%',
+ 'extra-condensed' => '62.5%',
+ 'condensed' => '75%',
+ 'semi-condensed' => '87.5%',
+ 'normal' => '100%',
+ 'semi-expanded' => '112.5%',
+ 'expanded' => '125%',
+ 'extra-expanded' => '150%',
+ 'ultra-expanded' => '200%',
+ );
+ $font_stretch = str_replace( array_keys( $font_stretch_map ), array_values( $font_stretch_map ), $font_stretch );
+
+ $slug_elements = array( $font_family, $font_style, $font_weight, $font_stretch, $unicode_range );
+
+ $slug_elements = array_map(
+ function ( $elem ) {
+ // Remove quotes to normalize font-family names, and ';' to use as a separator.
+ $elem = trim( str_replace( array( '"', "'", ';' ), '', $elem ) );
+
+ // Normalize comma separated lists by removing whitespace in between items,
+ // but keep whitespace within items (e.g. "Open Sans" and "OpenSans" are different fonts).
+ // CSS spec for whitespace includes: U+000A LINE FEED, U+0009 CHARACTER TABULATION, or U+0020 SPACE,
+ // which by default are all matched by \s in PHP.
+ return preg_replace( '/,\s+/', ',', $elem );
+ },
+ $slug_elements
+ );
+
+ return join( ';', $slug_elements );
+ }
}
diff --git a/lib/experimental/fonts/font-library/class-wp-font-family.php b/lib/experimental/fonts/font-library/class-wp-font-family.php
deleted file mode 100644
index f64aebc0c8efa7..00000000000000
--- a/lib/experimental/fonts/font-library/class-wp-font-family.php
+++ /dev/null
@@ -1,621 +0,0 @@
-data = $font_data;
- }
-
- /**
- * Gets the font family data.
- *
- * @since 6.5.0
- *
- * @return array An array in fontFamily theme.json format.
- */
- public function get_data() {
- return $this->data;
- }
-
- /**
- * Gets the font family data.
- *
- * @since 6.5.0
- *
- * @return string fontFamily in theme.json format as stringified JSON.
- */
- public function get_data_as_json() {
- return wp_json_encode( $this->get_data() );
- }
-
- /**
- * Checks whether the font family has font faces defined.
- *
- * @since 6.5.0
- *
- * @return bool True if the font family has font faces defined, false otherwise.
- */
- public function has_font_faces() {
- return ! empty( $this->data['fontFace'] ) && is_array( $this->data['fontFace'] );
- }
-
- /**
- * Removes font family assets.
- *
- * @since 6.5.0
- *
- * @return bool True if assets were removed, false otherwise.
- */
- private function remove_font_family_assets() {
- if ( $this->has_font_faces() ) {
- foreach ( $this->data['fontFace'] as $font_face ) {
- $were_assets_removed = $this->delete_font_face_assets( $font_face );
- if ( false === $were_assets_removed ) {
- return false;
- }
- }
- }
- return true;
- }
-
- /**
- * Removes a font family from the database and deletes its assets.
- *
- * @since 6.5.0
- *
- * @return bool|WP_Error True if the font family was uninstalled, WP_Error otherwise.
- */
- public function uninstall() {
- $post = $this->get_data_from_post();
- if ( null === $post ) {
- return new WP_Error(
- 'font_family_not_found',
- __( 'The font family could not be found.', 'gutenberg' )
- );
- }
-
- if (
- ! $this->remove_font_family_assets() ||
- ! wp_delete_post( $post->ID, true )
- ) {
- return new WP_Error(
- 'font_family_not_deleted',
- __( 'The font family could not be deleted.', 'gutenberg' )
- );
- }
-
- return true;
- }
-
- /**
- * Deletes a specified font asset file from the fonts directory.
- *
- * @since 6.5.0
- *
- * @param string $src The path of the font asset file to delete.
- * @return bool Whether the file was deleted.
- */
- private static function delete_asset( $src ) {
- $filename = basename( $src );
- $file_path = path_join( wp_get_font_dir()['path'], $filename );
-
- wp_delete_file( $file_path );
-
- return ! file_exists( $file_path );
- }
-
- /**
- * Deletes all font face asset files associated with a given font face.
- *
- * @since 6.5.0
- *
- * @param array $font_face The font face array containing the 'src' attribute
- * with the file path(s) to be deleted.
- * @return bool True if delete was successful, otherwise false.
- */
- private static function delete_font_face_assets( $font_face ) {
- $sources = (array) $font_face['src'];
- foreach ( $sources as $src ) {
- $was_asset_removed = self::delete_asset( $src );
- if ( ! $was_asset_removed ) {
- // Bail if any of the assets could not be removed.
- return false;
- }
- }
- return true;
- }
-
- /**
- * Gets the overrides for the 'wp_handle_upload' function.
- *
- * @since 6.5.0
- *
- * @param string $filename The filename to be used for the uploaded file.
- * @return array The overrides for the 'wp_handle_upload' function.
- */
- private function get_upload_overrides( $filename ) {
- return array(
- // Arbitrary string to avoid the is_uploaded_file() check applied
- // when using 'wp_handle_upload'.
- 'action' => 'wp_handle_font_upload',
- // Not testing a form submission.
- 'test_form' => false,
- // Seems mime type for files that are not images cannot be tested.
- // See wp_check_filetype_and_ext().
- 'test_type' => true,
- 'mimes' => WP_Font_Library::get_expected_font_mime_types_per_php_version(),
- 'unique_filename_callback' => static function () use ( $filename ) {
- // Keep the original filename.
- return $filename;
- },
- );
- }
-
- /**
- * Downloads a font asset from a specified source URL and saves it to
- * the font directory.
- *
- * @since 6.5.0
- *
- * @param string $url The source URL of the font asset to be downloaded.
- * @param string $filename The filename to save the downloaded font asset as.
- * @return string|bool The relative path to the downloaded font asset.
- * False if the download failed.
- */
- private function download_asset( $url, $filename ) {
- // Include file with download_url() if function doesn't exist.
- if ( ! function_exists( 'download_url' ) ) {
- require_once ABSPATH . 'wp-admin/includes/file.php';
- }
-
- // Downloads the font asset or returns false.
- $temp_file = download_url( $url );
- if ( is_wp_error( $temp_file ) ) {
- return false;
- }
-
- $overrides = $this->get_upload_overrides( $filename );
-
- $file = array(
- 'tmp_name' => $temp_file,
- 'name' => $filename,
- );
-
- $handled_file = wp_handle_upload( $file, $overrides );
-
- // Cleans the temp file.
- @unlink( $temp_file );
-
- if ( ! isset( $handled_file['url'] ) ) {
- return false;
- }
-
- // Returns the relative path to the downloaded font asset to be used as
- // font face src.
- return $handled_file['url'];
- }
-
- /**
- * Moves an uploaded font face asset from temp folder to the fonts directory.
- *
- * This is used when uploading local fonts.
- *
- * @since 6.5.0
- *
- * @param array $font_face Font face to download.
- * @param array $file Uploaded file.
- * @return array New font face with all assets downloaded and referenced in
- * the font face definition.
- */
- private function move_font_face_asset( $font_face, $file ) {
- $new_font_face = $font_face;
- $filename = WP_Font_Family_Utils::get_filename_from_font_face(
- $this->data['slug'],
- $font_face,
- $file['name']
- );
-
- // Remove the uploaded font asset reference from the font face definition
- // because it is no longer needed.
- unset( $new_font_face['uploadedFile'] );
-
- // Move the uploaded font asset from the temp folder to the fonts directory.
- if ( ! function_exists( 'wp_handle_upload' ) ) {
- require_once ABSPATH . 'wp-admin/includes/file.php';
- }
-
- $overrides = $this->get_upload_overrides( $filename );
-
- $handled_file = wp_handle_upload( $file, $overrides );
-
- if ( isset( $handled_file['url'] ) ) {
- // If the file was successfully moved, update the font face definition
- // to reference the new file location.
- $new_font_face['src'] = $handled_file['url'];
- }
-
- return $new_font_face;
- }
-
- /**
- * Sanitizes the font family data using WP_Theme_JSON.
- *
- * @since 6.5.0
- *
- * @return array A sanitized font family definition.
- */
- private function sanitize() {
- // Creates the structure of theme.json array with the new fonts.
- $fonts_json = array(
- 'version' => '2',
- 'settings' => array(
- 'typography' => array(
- 'fontFamilies' => array(
- 'custom' => array(
- $this->data,
- ),
- ),
- ),
- ),
- );
-
- // Creates a new WP_Theme_JSON object with the new fonts to
- // leverage sanitization and validation.
- $fonts_json = WP_Theme_JSON_Gutenberg::remove_insecure_properties( $fonts_json );
- $theme_json = new WP_Theme_JSON_Gutenberg( $fonts_json );
- $theme_data = $theme_json->get_data();
- $sanitized_font = ! empty( $theme_data['settings']['typography']['fontFamilies'] )
- ? $theme_data['settings']['typography']['fontFamilies'][0]
- : array();
-
- $sanitized_font['slug'] = _wp_to_kebab_case( $sanitized_font['slug'] );
- $sanitized_font['fontFamily'] = WP_Font_Family_Utils::format_font_family( $sanitized_font['fontFamily'] );
- $this->data = $sanitized_font;
- return $this->data;
- }
-
- /**
- * Downloads font face assets.
- *
- * Downloads the font face asset(s) associated with a font face. It works with
- * both single source URLs and arrays of multiple source URLs.
- *
- * @since 6.5.0
- *
- * @param array $font_face The font face array containing the 'src' attribute
- * with the source URL(s) of the assets.
- * @return array The modified font face array with the new source URL(s) to
- * the downloaded assets.
- */
- private function download_font_face_assets( $font_face ) {
- $new_font_face = $font_face;
- $sources = (array) $font_face['downloadFromUrl'];
- $new_font_face['src'] = array();
- $index = 0;
-
- foreach ( $sources as $src ) {
- $suffix = $index++ > 0 ? $index : '';
- $filename = WP_Font_Family_Utils::get_filename_from_font_face(
- $this->data['slug'],
- $font_face,
- $src,
- $suffix
- );
- $new_src = $this->download_asset( $src, $filename );
- if ( $new_src ) {
- $new_font_face['src'][] = $new_src;
- }
- }
-
- if ( count( $new_font_face['src'] ) === 1 ) {
- $new_font_face['src'] = $new_font_face['src'][0];
- }
-
- // Remove the download url reference from the font face definition
- // because it is no longer needed.
- unset( $new_font_face['downloadFromUrl'] );
-
- return $new_font_face;
- }
-
-
- /**
- * Downloads font face assets if the font family is a Google font,
- * or moves them if it is a local font.
- *
- * @since 6.5.0
- *
- * @param array $files An array of files to be installed.
- * @return bool True if the font faces were downloaded or moved successfully, false otherwise.
- */
- private function download_or_move_font_faces( $files ) {
- if ( ! $this->has_font_faces() ) {
- return true;
- }
-
- $new_font_faces = array();
- foreach ( $this->data['fontFace'] as $font_face ) {
- // If the fonts are not meant to be downloaded or uploaded
- // (for example to install fonts that use a remote url).
- $new_font_face = $font_face;
-
- $font_face_is_repeated = false;
-
- // If the font face has the same fontStyle and fontWeight as an existing, continue.
- foreach ( $new_font_faces as $font_to_compare ) {
- if ( $new_font_face['fontStyle'] === $font_to_compare['fontStyle'] &&
- $new_font_face['fontWeight'] === $font_to_compare['fontWeight'] ) {
- $font_face_is_repeated = true;
- }
- }
-
- if ( $font_face_is_repeated ) {
- continue;
- }
-
- // If the font face requires the use of the filesystem, create the fonts dir if it doesn't exist.
- if ( ! empty( $font_face['downloadFromUrl'] ) && ! empty( $font_face['uploadedFile'] ) ) {
- wp_mkdir_p( wp_get_font_dir()['path'] );
- }
-
- // If installing google fonts, download the font face assets.
- if ( ! empty( $font_face['downloadFromUrl'] ) ) {
- $new_font_face = $this->download_font_face_assets( $new_font_face );
- }
-
- // If installing local fonts, move the font face assets from
- // the temp folder to the wp fonts directory.
- if ( ! empty( $font_face['uploadedFile'] ) && ! empty( $files ) ) {
- $new_font_face = $this->move_font_face_asset(
- $new_font_face,
- $files[ $new_font_face['uploadedFile'] ]
- );
- }
-
- /*
- * If the font face assets were successfully downloaded, add the font face
- * to the new font. Font faces with failed downloads are not added to the
- * new font.
- */
- if ( ! empty( $new_font_face['src'] ) ) {
- $new_font_faces[] = $new_font_face;
- }
- }
-
- if ( ! empty( $new_font_faces ) ) {
- $this->data['fontFace'] = $new_font_faces;
- return true;
- }
-
- return false;
- }
-
- /**
- * Gets the post for a font family.
- *
- * @since 6.5.0
- *
- * @return WP_Post|null The post for this font family object or
- * null if the post does not exist.
- */
- public function get_font_post() {
- $args = array(
- 'post_type' => 'wp_font_family',
- 'post_name' => $this->data['slug'],
- 'name' => $this->data['slug'],
- 'posts_per_page' => 1,
- );
-
- $posts_query = new WP_Query( $args );
-
- if ( $posts_query->have_posts() ) {
- return $posts_query->posts[0];
- }
-
- return null;
- }
-
- /**
- * Gets the data for this object from the database and
- * sets it to the data property.
- *
- * @since 6.5.0
- *
- * @return WP_Post|null The post for this font family object or
- * null if the post does not exist.
- */
- private function get_data_from_post() {
- $post = $this->get_font_post();
- if ( $post ) {
- $this->data = json_decode( $post->post_content, true );
- return $post;
- }
-
- return null;
- }
-
- /**
- * Creates a post for a font family.
- *
- * @since 6.5.0
- *
- * @return int|WP_Error Post ID if the post was created, WP_Error otherwise.
- */
- private function create_font_post() {
- $post = array(
- 'post_title' => $this->data['name'],
- 'post_name' => $this->data['slug'],
- 'post_type' => 'wp_font_family',
- 'post_content' => $this->get_data_as_json(),
- 'post_status' => 'publish',
- );
-
- $post_id = wp_insert_post( $post );
- if ( 0 === $post_id || is_wp_error( $post_id ) ) {
- return new WP_Error(
- 'font_post_creation_failed',
- __( 'Font post creation failed', 'gutenberg' )
- );
- }
-
- return $post_id;
- }
-
- /**
- * Gets the font faces that are in both the existing and incoming font families.
- *
- * @since 6.5.0
- *
- * @param array $existing The existing font faces.
- * @param array $incoming The incoming font faces.
- * @return array The font faces that are in both the existing and incoming font families.
- */
- private function get_intersecting_font_faces( $existing, $incoming ) {
- $intersecting = array();
- foreach ( $existing as $existing_face ) {
- foreach ( $incoming as $incoming_face ) {
- if ( $incoming_face['fontStyle'] === $existing_face['fontStyle'] &&
- $incoming_face['fontWeight'] === $existing_face['fontWeight'] &&
- $incoming_face['src'] !== $existing_face['src'] ) {
- $intersecting[] = $existing_face;
- }
- }
- }
- return $intersecting;
- }
-
- /**
- * Updates a post for a font family.
- *
- * @since 6.5.0
- *
- * @param WP_Post $post The post to update.
- * @return int|WP_Error Post ID if the update was successful, WP_Error otherwise.
- */
- private function update_font_post( $post ) {
- $post_font_data = json_decode( $post->post_content, true );
- $new_data = WP_Font_Family_Utils::merge_fonts_data( $post_font_data, $this->data );
- if ( isset( $post_font_data['fontFace'] ) && ! empty( $post_font_data['fontFace'] ) ) {
- $intersecting = $this->get_intersecting_font_faces( $post_font_data['fontFace'], $new_data['fontFace'] );
- }
-
- if ( isset( $intersecting ) && ! empty( $intersecting ) ) {
- $serialized_font_faces = array_map( 'serialize', $new_data['fontFace'] );
- $serialized_intersecting = array_map( 'serialize', $intersecting );
-
- $diff = array_diff( $serialized_font_faces, $serialized_intersecting );
-
- $new_data['fontFace'] = array_values( array_map( 'unserialize', $diff ) );
-
- foreach ( $intersecting as $intersect ) {
- $this->delete_font_face_assets( $intersect );
- }
- }
- $this->data = $new_data;
-
- $post = array(
- 'ID' => $post->ID,
- 'post_content' => $this->get_data_as_json(),
- );
-
- $post_id = wp_update_post( $post );
-
- if ( 0 === $post_id || is_wp_error( $post_id ) ) {
- return new WP_Error(
- 'font_post_update_failed',
- __( 'Font post update failed', 'gutenberg' )
- );
- }
-
- return $post_id;
- }
-
- /**
- * Creates a post for a font in the Font Library if it doesn't exist,
- * or updates it if it does.
- *
- * @since 6.5.0
- *
- * @return int|WP_Error Post id if the post was created or updated successfully,
- * WP_Error otherwise.
- */
- private function create_or_update_font_post() {
- $this->sanitize();
-
- $post = $this->get_font_post();
- if ( $post ) {
- return $this->update_font_post( $post );
- }
-
- return $this->create_font_post();
- }
-
- /**
- * Installs the font family into the library.
- *
- * @since 6.5.0
- *
- * @param array $files Optional. An array of files to be installed. Default null.
- * @return array|WP_Error An array of font family data on success, WP_Error otherwise.
- */
- public function install( $files = null ) {
- add_filter( 'upload_mimes', array( 'WP_Font_Library', 'set_allowed_mime_types' ) );
- add_filter( 'upload_dir', 'wp_get_font_dir' );
- $were_assets_written = $this->download_or_move_font_faces( $files );
- remove_filter( 'upload_dir', 'wp_get_font_dir' );
- remove_filter( 'upload_mimes', array( 'WP_Font_Library', 'set_allowed_mime_types' ) );
-
- if ( ! $were_assets_written ) {
- return new WP_Error(
- 'font_face_download_failed',
- __( 'The font face assets could not be written.', 'gutenberg' )
- );
- }
-
- $post_id = $this->create_or_update_font_post();
-
- if ( is_wp_error( $post_id ) ) {
- return $post_id;
- }
-
- return $this->get_data();
- }
-}
diff --git a/lib/experimental/fonts/font-library/class-wp-font-library.php b/lib/experimental/fonts/font-library/class-wp-font-library.php
index fd36f6ba073c4f..51a84b957ea117 100644
--- a/lib/experimental/fonts/font-library/class-wp-font-library.php
+++ b/lib/experimental/fonts/font-library/class-wp-font-library.php
@@ -62,11 +62,17 @@ public static function get_expected_font_mime_types_per_php_version( $php_versio
* @return WP_Font_Collection|WP_Error A font collection is it was registered successfully and a WP_Error otherwise.
*/
public static function register_font_collection( $config ) {
+ if ( ! WP_Font_Collection::is_config_valid( $config ) ) {
+ $error_message = __( 'Font collection config is invalid.', 'gutenberg' );
+ return new WP_Error( 'font_collection_registration_error', $error_message );
+ }
+
$new_collection = new WP_Font_Collection( $config );
- if ( self::is_collection_registered( $config['slug'] ) ) {
+
+ if ( self::is_collection_registered( $new_collection->get_config()['slug'] ) ) {
$error_message = sprintf(
/* translators: %s: Font collection slug. */
- __( 'Font collection with slug: "%s" is already registered.', 'default' ),
+ __( 'Font collection with slug: "%s" is already registered.', 'gutenberg' ),
$config['slug']
);
_doing_it_wrong(
@@ -76,7 +82,7 @@ public static function register_font_collection( $config ) {
);
return new WP_Error( 'font_collection_registration_error', $error_message );
}
- self::$collections[ $config['slug'] ] = $new_collection;
+ self::$collections[ $new_collection->get_config()['slug'] ] = $new_collection;
return $new_collection;
}
diff --git a/lib/experimental/fonts/font-library/class-wp-rest-autosave-font-families-controller.php b/lib/experimental/fonts/font-library/class-wp-rest-autosave-font-families-controller.php
deleted file mode 100644
index 0e31bd4004b40f..00000000000000
--- a/lib/experimental/fonts/font-library/class-wp-rest-autosave-font-families-controller.php
+++ /dev/null
@@ -1,25 +0,0 @@
- WP_REST_Server::READABLE,
- 'callback' => array( $this, 'get_font_collections' ),
- 'permission_callback' => array( $this, 'update_font_library_permissions_check' ),
+ 'callback' => array( $this, 'get_items' ),
+ 'permission_callback' => array( $this, 'get_items_permissions_check' ),
),
)
);
@@ -54,13 +54,29 @@ public function register_routes() {
array(
array(
'methods' => WP_REST_Server::READABLE,
- 'callback' => array( $this, 'get_font_collection' ),
- 'permission_callback' => array( $this, 'update_font_library_permissions_check' ),
+ 'callback' => array( $this, 'get_item' ),
+ 'permission_callback' => array( $this, 'get_items_permissions_check' ),
),
)
);
}
+ /**
+ * Gets the font collections available.
+ *
+ * @since 6.5.0
+ *
+ * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
+ */
+ public function get_items( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
+ $collections = array();
+ foreach ( WP_Font_Library::get_font_collections() as $collection ) {
+ $collections[] = $collection->get_config();
+ }
+
+ return rest_ensure_response( $collections, 200 );
+ }
+
/**
* Gets a font collection.
*
@@ -69,54 +85,42 @@ public function register_routes() {
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
- public function get_font_collection( $request ) {
+ public function get_item( $request ) {
$slug = $request->get_param( 'slug' );
$collection = WP_Font_Library::get_font_collection( $slug );
+
// If the collection doesn't exist returns a 404.
if ( is_wp_error( $collection ) ) {
$collection->add_data( array( 'status' => 404 ) );
return $collection;
}
- $config_and_data = $collection->get_config_and_data();
- $collection_data = $config_and_data['data'];
- // If there was an error getting the collection data, return the error.
- if ( is_wp_error( $collection_data ) ) {
- $collection_data->add_data( array( 'status' => 500 ) );
- return $collection_data;
- }
-
- return new WP_REST_Response( $config_and_data );
- }
+ $config = $collection->get_config();
+ $contents = $collection->get_content();
- /**
- * Gets the font collections available.
- *
- * @since 6.5.0
- *
- * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
- */
- public function get_font_collections() {
- $collections = array();
- foreach ( WP_Font_Library::get_font_collections() as $collection ) {
- $collections[] = $collection->get_config_and_data();
+ // If there was an error getting the collection data, return the error.
+ if ( is_wp_error( $contents ) ) {
+ $contents->add_data( array( 'status' => 500 ) );
+ return $contents;
}
- return new WP_REST_Response( $collections, 200 );
+ $collection_data = array_merge( $config, $contents );
+ return rest_ensure_response( $collection_data );
}
/**
- * Checks whether the user has permissions to update the Font Library.
+ * Checks whether the user has permissions to use the Fonts Collections.
*
* @since 6.5.0
*
* @return true|WP_Error True if the request has write access for the item, WP_Error object otherwise.
*/
- public function update_font_library_permissions_check() {
+ public function get_items_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
+
if ( ! current_user_can( 'edit_theme_options' ) ) {
return new WP_Error(
- 'rest_cannot_update_font_library',
- __( 'Sorry, you are not allowed to update the Font Library on this site.', 'gutenberg' ),
+ 'rest_cannot_read',
+ __( 'Sorry, you are not allowed to use the Font Library on this site.', 'gutenberg' ),
array(
'status' => rest_authorization_required_code(),
)
diff --git a/lib/experimental/fonts/font-library/class-wp-rest-font-faces-controller.php b/lib/experimental/fonts/font-library/class-wp-rest-font-faces-controller.php
new file mode 100644
index 00000000000000..fac32362325f49
--- /dev/null
+++ b/lib/experimental/fonts/font-library/class-wp-rest-font-faces-controller.php
@@ -0,0 +1,836 @@
+namespace,
+ '/' . $this->rest_base,
+ array(
+ 'args' => array(
+ 'font_family_id' => array(
+ 'description' => __( 'The ID for the parent font family of the font face.', 'gutenberg' ),
+ 'type' => 'integer',
+ 'required' => true,
+ ),
+ ),
+ array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => array( $this, 'get_items' ),
+ 'permission_callback' => array( $this, 'get_items_permissions_check' ),
+ 'args' => $this->get_collection_params(),
+ ),
+ array(
+ 'methods' => WP_REST_Server::CREATABLE,
+ 'callback' => array( $this, 'create_item' ),
+ 'permission_callback' => array( $this, 'create_item_permissions_check' ),
+ 'args' => $this->get_create_params(),
+ ),
+ 'schema' => array( $this, 'get_public_item_schema' ),
+ )
+ );
+
+ register_rest_route(
+ $this->namespace,
+ '/' . $this->rest_base . '/(?P[\d]+)',
+ array(
+ 'args' => array(
+ 'font_family_id' => array(
+ 'description' => __( 'The ID for the parent font family of the font face.', 'gutenberg' ),
+ 'type' => 'integer',
+ 'required' => true,
+ ),
+ 'id' => array(
+ 'description' => __( 'Unique identifier for the font face.', 'gutenberg' ),
+ 'type' => 'integer',
+ 'required' => true,
+ ),
+ ),
+ array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => array( $this, 'get_item' ),
+ 'permission_callback' => array( $this, 'get_item_permissions_check' ),
+ 'args' => array(
+ 'context' => $this->get_context_param( array( 'default' => 'edit' ) ),
+ ),
+ ),
+ array(
+ 'methods' => WP_REST_Server::DELETABLE,
+ 'callback' => array( $this, 'delete_item' ),
+ 'permission_callback' => array( $this, 'delete_item_permissions_check' ),
+ 'args' => array(
+ 'force' => array(
+ 'type' => 'boolean',
+ 'default' => false,
+ 'description' => __( 'Whether to bypass Trash and force deletion.', 'default' ),
+ ),
+ ),
+ ),
+ 'schema' => array( $this, 'get_public_item_schema' ),
+ )
+ );
+ }
+
+ /**
+ * Checks if a given request has access to font faces.
+ *
+ * @since 6.5.0
+ *
+ * @param WP_REST_Request $request Full details about the request.
+ * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
+ */
+ public function get_items_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- required by parent class
+ $post_type = get_post_type_object( $this->post_type );
+
+ if ( ! current_user_can( $post_type->cap->read ) ) {
+ return new WP_Error(
+ 'rest_cannot_read',
+ __( 'Sorry, you are not allowed to access font faces.', 'gutenberg' ),
+ array( 'status' => rest_authorization_required_code() )
+ );
+ }
+
+ return true;
+ }
+
+ /**
+ * Checks if a given request has access to a font face.
+ *
+ * @since 6.5.0
+ *
+ * @param WP_REST_Request $request Full details about the request.
+ * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
+ */
+ public function get_item_permissions_check( $request ) {
+ return $this->get_items_permissions_check( $request );
+ }
+
+ /**
+ * Validates settings when creating a font face.
+ *
+ * @since 6.5.0
+ *
+ * @param string $value Encoded JSON string of font face settings.
+ * @param WP_REST_Request $request Request object.
+ * @return false|WP_Error True if the settings are valid, otherwise a WP_Error object.
+ */
+ public function validate_create_font_face_settings( $value, $request ) {
+ $settings = json_decode( $value, true );
+
+ // Check settings string is valid JSON.
+ if ( null === $settings ) {
+ return new WP_Error(
+ 'rest_invalid_param',
+ __( 'font_face_settings parameter must be a valid JSON string.', 'gutenberg' ),
+ array( 'status' => 400 )
+ );
+ }
+
+ // Check that the font face settings match the theme.json schema.
+ $schema = $this->get_item_schema()['properties']['font_face_settings'];
+ $has_valid_settings = rest_validate_value_from_schema( $settings, $schema, 'font_face_settings' );
+
+ if ( is_wp_error( $has_valid_settings ) ) {
+ $has_valid_settings->add_data( array( 'status' => 400 ) );
+ return $has_valid_settings;
+ }
+
+ // Check that none of the required settings are empty values.
+ $required = $schema['required'];
+ foreach ( $required as $key ) {
+ if ( isset( $settings[ $key ] ) && ! $settings[ $key ] ) {
+ return new WP_Error(
+ 'rest_invalid_param',
+ /* translators: %s: Font family setting key. */
+ sprintf( __( 'font_face_setting[%s] cannot be empty.', 'gutenberg' ), $key ),
+ array( 'status' => 400 )
+ );
+ }
+ }
+
+ $srcs = is_array( $settings['src'] ) ? $settings['src'] : array( $settings['src'] );
+
+ // Check that srcs are non-empty strings.
+ $filtered_src = array_filter( array_filter( $srcs, 'is_string' ) );
+ if ( empty( $filtered_src ) ) {
+ return new WP_Error(
+ 'rest_invalid_param',
+ __( 'font_face_settings[src] values must be non-empty strings.', 'gutenberg' ),
+ array( 'status' => 400 )
+ );
+ }
+
+ // Check that each file in the request references a src in the settings.
+ $files = $request->get_file_params();
+ foreach ( array_keys( $files ) as $file ) {
+ if ( ! in_array( $file, $srcs, true ) ) {
+ return new WP_Error(
+ 'rest_invalid_param',
+ // translators: %s: File key (e.g. `file-0`) in the request data.
+ sprintf( __( 'File %1$s must be used in font_face_settings[src].', 'gutenberg' ), $file ),
+ array( 'status' => 400 )
+ );
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Sanitizes the font face settings when creating a font face.
+ *
+ * @since 6.5.0
+ *
+ * @param string $value Encoded JSON string of font face settings.
+ * @param WP_REST_Request $request Request object.
+ * @return array Decoded array of font face settings.
+ */
+ public function sanitize_font_face_settings( $value ) {
+ // Settings arrive as stringified JSON, since this is a multipart/form-data request.
+ $settings = json_decode( $value, true );
+
+ if ( isset( $settings['fontFamily'] ) ) {
+ $settings['fontFamily'] = WP_Font_Family_Utils::format_font_family( $settings['fontFamily'] );
+ }
+
+ return $settings;
+ }
+
+ /**
+ * Retrieves a collection of font faces within the parent font family.
+ *
+ * @since 6.5.0
+ *
+ * @param WP_REST_Request $request Full details about the request.
+ * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
+ */
+ public function get_items( $request ) {
+ $font_family = $this->get_parent_font_family_post( $request['font_family_id'] );
+ if ( is_wp_error( $font_family ) ) {
+ return $font_family;
+ }
+
+ return parent::get_items( $request );
+ }
+
+ /**
+ * Retrieves a single font face within the parent font family.
+ *
+ * @since 6.5.0
+ *
+ * @param WP_REST_Request $request Full details about the request.
+ * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
+ */
+ public function get_item( $request ) {
+ $post = $this->get_post( $request['id'] );
+ if ( is_wp_error( $post ) ) {
+ return $post;
+ }
+
+ // Check that the font face has a valid parent font family.
+ $font_family = $this->get_parent_font_family_post( $request['font_family_id'] );
+ if ( is_wp_error( $font_family ) ) {
+ return $font_family;
+ }
+
+ if ( (int) $font_family->ID !== (int) $post->post_parent ) {
+ return new WP_Error(
+ 'rest_font_face_parent_id_mismatch',
+ /* translators: %d: A post id. */
+ sprintf( __( 'The font face does not belong to the specified font family with id of "%d"', 'gutenberg' ), $font_family->ID ),
+ array( 'status' => 404 )
+ );
+ }
+
+ return parent::get_item( $request );
+ }
+
+ /**
+ * Creates a font face for the parent font family.
+ *
+ * @since 6.5.0
+ *
+ * @param WP_REST_Request $request Full details about the request.
+ * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
+ */
+ public function create_item( $request ) {
+ $font_family = $this->get_parent_font_family_post( $request['font_family_id'] );
+ if ( is_wp_error( $font_family ) ) {
+ return $font_family;
+ }
+
+ // Settings have already been decoded by ::sanitize_font_face_settings().
+ $settings = $request->get_param( 'font_face_settings' );
+ $file_params = $request->get_file_params();
+
+ // Check that the necessary font face properties are unique.
+ $query = new WP_Query(
+ array(
+ 'post_type' => $this->post_type,
+ 'posts_per_page' => 1,
+ 'title' => WP_Font_Family_Utils::get_font_face_slug( $settings ),
+ 'update_post_meta_cache' => false,
+ 'update_post_term_cache' => false,
+ )
+ );
+ if ( ! empty( $query->get_posts() ) ) {
+ return new WP_Error(
+ 'rest_duplicate_font_face',
+ __( 'A font face matching those settings already exists.', 'gutenberg' ),
+ array( 'status' => 400 )
+ );
+ }
+
+ // Move the uploaded font asset from the temp folder to the fonts directory.
+ if ( ! function_exists( 'wp_handle_upload' ) ) {
+ require_once ABSPATH . 'wp-admin/includes/file.php';
+ }
+
+ $srcs = is_string( $settings['src'] ) ? array( $settings['src'] ) : $settings['src'];
+ $processed_srcs = array();
+ $font_file_meta = array();
+
+ foreach ( $srcs as $src ) {
+ // If src not a file reference, use it as is.
+ if ( ! isset( $file_params[ $src ] ) ) {
+ $processed_srcs[] = $src;
+ continue;
+ }
+
+ $file = $file_params[ $src ];
+ $font_file = $this->handle_font_file_upload( $file );
+ if ( is_wp_error( $font_file ) ) {
+ return $font_file;
+ }
+
+ $processed_srcs[] = $font_file['url'];
+ $font_file_meta[] = $this->relative_fonts_path( $font_file['file'] );
+ }
+
+ // Store the updated settings for prepare_item_for_database to use.
+ $settings['src'] = count( $processed_srcs ) === 1 ? $processed_srcs[0] : $processed_srcs;
+ $request->set_param( 'font_face_settings', $settings );
+
+ // Ensure that $settings data is slashed, so values with quotes are escaped.
+ // WP_REST_Posts_Controller::create_item uses wp_slash() on the post_content.
+ $font_face_post = parent::create_item( $request );
+
+ if ( is_wp_error( $font_face_post ) ) {
+ return $font_face_post;
+ }
+
+ $font_face_id = $font_face_post->data['id'];
+
+ foreach ( $font_file_meta as $font_file_path ) {
+ add_post_meta( $font_face_id, '_wp_font_face_file', $font_file_path );
+ }
+
+ return $font_face_post;
+ }
+
+ /**
+ * Deletes a single font face.
+ *
+ * @since 6.5.0
+ *
+ * @param WP_REST_Request $request Full details about the request.
+ * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
+ */
+ public function delete_item( $request ) {
+ $post = $this->get_post( $request['id'] );
+ if ( is_wp_error( $post ) ) {
+ return $post;
+ }
+
+ $font_family = $this->get_parent_font_family_post( $request['font_family_id'] );
+ if ( is_wp_error( $font_family ) ) {
+ return $font_family;
+ }
+
+ if ( (int) $font_family->ID !== (int) $post->post_parent ) {
+ return new WP_Error(
+ 'rest_font_face_parent_id_mismatch',
+ /* translators: %d: A post id. */
+ sprintf( __( 'The font face does not belong to the specified font family with id of "%d"', 'gutenberg' ), $font_family->ID ),
+ array( 'status' => 404 )
+ );
+ }
+
+ $force = isset( $request['force'] ) ? (bool) $request['force'] : false;
+
+ // We don't support trashing for font faces.
+ if ( ! $force ) {
+ return new WP_Error(
+ 'rest_trash_not_supported',
+ /* translators: %s: force=true */
+ sprintf( __( "Font faces do not support trashing. Set '%s' to delete.", 'gutenberg' ), 'force=true' ),
+ array( 'status' => 501 )
+ );
+ }
+
+ return parent::delete_item( $request );
+ }
+
+ /**
+ * Prepares a single font face output for response.
+ *
+ * @since 6.5.0
+ *
+ * @param WP_Post $item Post object.
+ * @param WP_REST_Request $request Request object.
+ * @return WP_REST_Response Response object.
+ */
+ public function prepare_item_for_response( $item, $request ) {
+ $fields = $this->get_fields_for_response( $request );
+ $data = array();
+
+ if ( rest_is_field_included( 'id', $fields ) ) {
+ $data['id'] = $item->ID;
+ }
+ if ( rest_is_field_included( 'theme_json_version', $fields ) ) {
+ $data['theme_json_version'] = 2;
+ }
+
+ if ( rest_is_field_included( 'parent', $fields ) ) {
+ $data['parent'] = $item->post_parent;
+ }
+
+ if ( rest_is_field_included( 'font_face_settings', $fields ) ) {
+ $data['font_face_settings'] = $this->get_settings_from_post( $item );
+ }
+
+ $context = ! empty( $request['context'] ) ? $request['context'] : 'edit';
+ $data = $this->add_additional_fields_to_object( $data, $request );
+ $data = $this->filter_response_by_context( $data, $context );
+
+ $response = rest_ensure_response( $data );
+
+ if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
+ $links = $this->prepare_links( $item );
+ $response->add_links( $links );
+ }
+
+ /**
+ * Filters the font face data for a REST API response.
+ *
+ * @since 6.5.0
+ *
+ * @param WP_REST_Response $response The response object.
+ * @param WP_Post $post Font face post object.
+ * @param WP_REST_Request $request Request object.
+ */
+ return apply_filters( 'rest_prepare_wp_font_face', $response, $item, $request );
+ }
+
+ /**
+ * Retrieves the post's schema, conforming to JSON Schema.
+ *
+ * @since 6.5.0
+ *
+ * @return array Item schema data.
+ */
+ public function get_item_schema() {
+ if ( $this->schema ) {
+ return $this->add_additional_fields_schema( $this->schema );
+ }
+
+ $schema = array(
+ '$schema' => 'http://json-schema.org/draft-04/schema#',
+ 'title' => $this->post_type,
+ 'type' => 'object',
+ // Base properties for every Post.
+ 'properties' => array(
+ 'id' => array(
+ 'description' => __( 'Unique identifier for the post.', 'default' ),
+ 'type' => 'integer',
+ 'context' => array( 'edit', 'embed' ),
+ 'readonly' => true,
+ ),
+ 'theme_json_version' => array(
+ 'description' => __( 'Version of the theme.json schema used for the typography settings.', 'gutenberg' ),
+ 'type' => 'integer',
+ 'default' => 2,
+ 'minimum' => 2,
+ 'maximum' => 2,
+ 'context' => array( 'edit', 'embed' ),
+ ),
+ 'parent' => array(
+ 'description' => __( 'The ID for the parent font family of the font face.', 'gutenberg' ),
+ 'type' => 'integer',
+ 'context' => array( 'edit', 'embed' ),
+ ),
+ // Font face settings come directly from theme.json schema
+ // See https://schemas.wp.org/trunk/theme.json
+ 'font_face_settings' => array(
+ 'description' => __( 'font-face declaration in theme.json format.', 'gutenberg' ),
+ 'type' => 'object',
+ 'context' => array( 'edit', 'embed' ),
+ 'properties' => array(
+ 'fontFamily' => array(
+ 'description' => __( 'CSS font-family value.', 'gutenberg' ),
+ 'type' => 'string',
+ 'default' => '',
+ ),
+ 'fontStyle' => array(
+ 'description' => __( 'CSS font-style value.', 'gutenberg' ),
+ 'type' => 'string',
+ 'default' => 'normal',
+ ),
+ 'fontWeight' => array(
+ 'description' => __( 'List of available font weights, separated by a space.', 'gutenberg' ),
+ 'default' => '400',
+ // Changed from `oneOf` to avoid errors from loose type checking.
+ // e.g. a fontWeight of "400" validates as both a string and an integer due to is_numeric check.
+ 'type' => array( 'string', 'integer' ),
+ ),
+ 'fontDisplay' => array(
+ 'description' => __( 'CSS font-display value.', 'gutenberg' ),
+ 'type' => 'string',
+ 'default' => 'fallback',
+ 'enum' => array(
+ 'auto',
+ 'block',
+ 'fallback',
+ 'swap',
+ 'optional',
+ ),
+ ),
+ 'src' => array(
+ 'description' => __( 'Paths or URLs to the font files.', 'gutenberg' ),
+ // Changed from `oneOf` to `anyOf` due to rest_sanitize_array converting a string into an array.
+ 'anyOf' => array(
+ array(
+ 'type' => 'string',
+ ),
+ array(
+ 'type' => 'array',
+ 'items' => array(
+ 'type' => 'string',
+ ),
+ ),
+ ),
+ 'default' => array(),
+ ),
+ 'fontStretch' => array(
+ 'description' => __( 'CSS font-stretch value.', 'gutenberg' ),
+ 'type' => 'string',
+ ),
+ 'ascentOverride' => array(
+ 'description' => __( 'CSS ascent-override value.', 'gutenberg' ),
+ 'type' => 'string',
+ ),
+ 'descentOverride' => array(
+ 'description' => __( 'CSS descent-override value.', 'gutenberg' ),
+ 'type' => 'string',
+ ),
+ 'fontVariant' => array(
+ 'description' => __( 'CSS font-variant value.', 'gutenberg' ),
+ 'type' => 'string',
+ ),
+ 'fontFeatureSettings' => array(
+ 'description' => __( 'CSS font-feature-settings value.', 'gutenberg' ),
+ 'type' => 'string',
+ ),
+ 'fontVariationSettings' => array(
+ 'description' => __( 'CSS font-variation-settings value.', 'gutenberg' ),
+ 'type' => 'string',
+ ),
+ 'lineGapOverride' => array(
+ 'description' => __( 'CSS line-gap-override value.', 'gutenberg' ),
+ 'type' => 'string',
+ ),
+ 'sizeAdjust' => array(
+ 'description' => __( 'CSS size-adjust value.', 'gutenberg' ),
+ 'type' => 'string',
+ ),
+ 'unicodeRange' => array(
+ 'description' => __( 'CSS unicode-range value.', 'gutenberg' ),
+ 'type' => 'string',
+ ),
+ 'preview' => array(
+ 'description' => __( 'URL to a preview image of the font face.', 'gutenberg' ),
+ 'type' => 'string',
+ ),
+ ),
+ 'required' => array( 'fontFamily', 'src' ),
+ 'additionalProperties' => false,
+ ),
+ ),
+ );
+
+ $this->schema = $schema;
+
+ return $this->add_additional_fields_schema( $this->schema );
+ }
+
+ /**
+ * Retrieves the query params for the font face collection.
+ *
+ * @since 6.5.0
+ *
+ * @return array Collection parameters.
+ */
+ public function get_collection_params() {
+ $query_params = parent::get_collection_params();
+
+ $query_params['context']['default'] = 'edit';
+
+ // Remove unneeded params.
+ unset( $query_params['after'] );
+ unset( $query_params['modified_after'] );
+ unset( $query_params['before'] );
+ unset( $query_params['modified_before'] );
+ unset( $query_params['search'] );
+ unset( $query_params['search_columns'] );
+ unset( $query_params['slug'] );
+ unset( $query_params['status'] );
+
+ $query_params['orderby']['default'] = 'id';
+ $query_params['orderby']['enum'] = array( 'id', 'include' );
+
+ /**
+ * Filters collection parameters for the font face controller.
+ *
+ * @since 6.5.0
+ *
+ * @param array $query_params JSON Schema-formatted collection parameters.
+ */
+ return apply_filters( 'rest_wp_font_face_collection_params', $query_params );
+ }
+
+ /**
+ * Get the params used when creating a new font face.
+ *
+ * @since 6.5.0
+ *
+ * @return array Font face create arguments.
+ */
+ public function get_create_params() {
+ $properties = $this->get_item_schema()['properties'];
+ return array(
+ 'theme_json_version' => $properties['theme_json_version'],
+ // When creating, font_face_settings is stringified JSON, to work with multipart/form-data used
+ // when uploading font files.
+ 'font_face_settings' => array(
+ 'description' => __( 'font-face declaration in theme.json format, encoded as a string.', 'gutenberg' ),
+ 'type' => 'string',
+ 'required' => true,
+ 'validate_callback' => array( $this, 'validate_create_font_face_settings' ),
+ 'sanitize_callback' => array( $this, 'sanitize_font_face_settings' ),
+ ),
+ );
+ }
+
+ /**
+ * Get the parent font family, if the ID is valid.
+ *
+ * @since 6.5.0
+ *
+ * @param int $font_family_id Supplied ID.
+ * @return WP_Post|WP_Error Post object if ID is valid, WP_Error otherwise.
+ */
+ protected function get_parent_font_family_post( $font_family_id ) {
+ $error = new WP_Error(
+ 'rest_post_invalid_parent',
+ __( 'Invalid post parent ID.', 'default' ),
+ array( 'status' => 404 )
+ );
+
+ if ( (int) $font_family_id <= 0 ) {
+ return $error;
+ }
+
+ $font_family_post = get_post( (int) $font_family_id );
+
+ if ( empty( $font_family_post ) || empty( $font_family_post->ID )
+ || 'wp_font_family' !== $font_family_post->post_type
+ ) {
+ return $error;
+ }
+
+ return $font_family_post;
+ }
+
+ /**
+ * Prepares links for the request.
+ *
+ * @since 6.5.0
+ *
+ * @param WP_Post $post Post object.
+ * @return array Links for the given post.
+ */
+ protected function prepare_links( $post ) {
+ // Entity meta.
+ $links = array(
+ 'self' => array(
+ 'href' => rest_url( $this->namespace . '/font-families/' . $post->post_parent . '/font-faces/' . $post->ID ),
+ ),
+ 'collection' => array(
+ 'href' => rest_url( $this->namespace . '/font-families/' . $post->post_parent . '/font-faces' ),
+ ),
+ 'parent' => array(
+ 'href' => rest_url( $this->namespace . '/font-families/' . $post->post_parent ),
+ ),
+ );
+
+ return $links;
+ }
+
+ /**
+ * Prepares a single font face post for creation.
+ *
+ * @since 6.5.0
+ *
+ * @param WP_REST_Request $request Request object.
+ * @return stdClass|WP_Error Post object or WP_Error.
+ */
+ protected function prepare_item_for_database( $request ) {
+ $prepared_post = new stdClass();
+
+ // Settings have already been decoded by ::sanitize_font_face_settings().
+ $settings = $request->get_param( 'font_face_settings' );
+
+ // Store this "slug" as the post_title rather than post_name, since it uses the fontFamily setting,
+ // which may contain multibyte characters.
+ $title = WP_Font_Family_Utils::get_font_face_slug( $settings );
+
+ $prepared_post->post_type = $this->post_type;
+ $prepared_post->post_parent = $request['font_family_id'];
+ $prepared_post->post_status = 'publish';
+ $prepared_post->post_title = $title;
+ $prepared_post->post_name = sanitize_title( $title );
+ $prepared_post->post_content = wp_json_encode( $settings );
+
+ return $prepared_post;
+ }
+
+ /**
+ * Handles the upload of a font file using wp_handle_upload().
+ *
+ * @since 6.5.0
+ *
+ * @param array $file Single file item from $_FILES.
+ * @return array Array containing uploaded file attributes on success, or error on failure.
+ */
+ protected function handle_font_file_upload( $file ) {
+ add_filter( 'upload_mimes', array( 'WP_Font_Library', 'set_allowed_mime_types' ) );
+ add_filter( 'upload_dir', 'wp_get_font_dir' );
+
+ $overrides = array(
+ 'upload_error_handler' => array( $this, 'handle_font_file_upload_error' ),
+ // Arbitrary string to avoid the is_uploaded_file() check applied
+ // when using 'wp_handle_upload'.
+ 'action' => 'wp_handle_font_upload',
+ // Not testing a form submission.
+ 'test_form' => false,
+ // Seems mime type for files that are not images cannot be tested.
+ // See wp_check_filetype_and_ext().
+ 'test_type' => true,
+ // Only allow uploading font files for this request.
+ 'mimes' => WP_Font_Library::get_expected_font_mime_types_per_php_version(),
+ );
+
+ $uploaded_file = wp_handle_upload( $file, $overrides );
+
+ remove_filter( 'upload_dir', 'wp_get_font_dir' );
+ remove_filter( 'upload_mimes', array( 'WP_Font_Library', 'set_allowed_mime_types' ) );
+
+ return $uploaded_file;
+ }
+
+ /**
+ * Handles file upload error.
+ *
+ * @since 6.5.0
+ *
+ * @param array $file File upload data.
+ * @param string $message Error message from wp_handle_upload().
+ * @return WP_Error WP_Error object.
+ */
+ public function handle_font_file_upload_error( $file, $message ) {
+ $status = 500;
+ $code = 'rest_font_upload_unknown_error';
+
+ if ( __( 'Sorry, you are not allowed to upload this file type.', 'default' ) === $message ) {
+ $status = 400;
+ $code = 'rest_font_upload_invalid_file_type';
+ }
+
+ return new WP_Error( $code, $message, array( 'status' => $status ) );
+ }
+
+ /**
+ * Returns relative path to an uploaded font file.
+ *
+ * The path is relative to the current fonts dir.
+ *
+ * @since 6.5.0
+ * @access private
+ *
+ * @param string $path Full path to the file.
+ * @return string Relative path on success, unchanged path on failure.
+ */
+ protected function relative_fonts_path( $path ) {
+ $new_path = $path;
+
+ $fonts_dir = wp_get_font_dir();
+ if ( str_starts_with( $new_path, $fonts_dir['path'] ) ) {
+ $new_path = str_replace( $fonts_dir, '', $new_path );
+ $new_path = ltrim( $new_path, '/' );
+ }
+
+ return $new_path;
+ }
+
+ /**
+ * Gets the font face's settings from the post.
+ *
+ * @since 6.5.0
+ *
+ * @param WP_Post $post Font face post object.
+ * @return array Font face settings array.
+ */
+ protected function get_settings_from_post( $post ) {
+ $settings = json_decode( $post->post_content, true );
+ $properties = $this->get_item_schema()['properties']['font_face_settings']['properties'];
+
+ // Provide required, empty settings if needed.
+ if ( null === $settings ) {
+ $settings = array(
+ 'fontFamily' => '',
+ 'src' => array(),
+ );
+ }
+
+ // Only return the properties defined in the schema.
+ return array_intersect_key( $settings, $properties );
+ }
+}
diff --git a/lib/experimental/fonts/font-library/class-wp-rest-font-families-controller.php b/lib/experimental/fonts/font-library/class-wp-rest-font-families-controller.php
index ede8762c88c6dc..887a8a5250cc32 100644
--- a/lib/experimental/fonts/font-library/class-wp-rest-font-families-controller.php
+++ b/lib/experimental/fonts/font-library/class-wp-rest-font-families-controller.php
@@ -1,12 +1,10 @@
rest_base = 'font-families';
- $this->namespace = 'wp/v2';
- $this->post_type = 'wp_font_family';
- }
+ protected $allow_batch = false;
/**
- * Registers the routes for the objects of the controller.
+ * Checks if a given request has access to font families.
*
* @since 6.5.0
+ *
+ * @param WP_REST_Request $request Full details about the request.
+ * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
*/
- public function register_routes() {
- register_rest_route(
- $this->namespace,
- '/' . $this->rest_base,
- array(
- array(
- 'methods' => WP_REST_Server::READABLE,
- 'callback' => array( $this, 'get_items' ),
- 'permission_callback' => array( $this, 'update_font_library_permissions_check' ),
- ),
- )
- );
+ public function get_items_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- required by parent class
+ $post_type = get_post_type_object( $this->post_type );
- register_rest_route(
- $this->namespace,
- '/' . $this->rest_base,
- array(
- array(
- 'methods' => WP_REST_Server::EDITABLE,
- 'callback' => array( $this, 'install_fonts' ),
- 'permission_callback' => array( $this, 'update_font_library_permissions_check' ),
- 'args' => array(
- 'font_family_settings' => array(
- 'required' => true,
- 'type' => 'string',
- 'validate_callback' => array( $this, 'validate_install_font_families' ),
- ),
- ),
- ),
- )
- );
+ if ( ! current_user_can( $post_type->cap->read ) ) {
+ return new WP_Error(
+ 'rest_cannot_read',
+ __( 'Sorry, you are not allowed to access font families.', 'gutenberg' ),
+ array( 'status' => rest_authorization_required_code() )
+ );
+ }
- register_rest_route(
- $this->namespace,
- '/' . $this->rest_base,
- array(
- array(
- 'methods' => WP_REST_Server::DELETABLE,
- 'callback' => array( $this, 'uninstall_fonts' ),
- 'permission_callback' => array( $this, 'update_font_library_permissions_check' ),
- 'args' => $this->uninstall_schema(),
- ),
- )
- );
+ return true;
}
/**
- * Returns validation errors in font families data for installation.
+ * Checks if a given request has access to a font family.
*
* @since 6.5.0
*
- * @param array[] $font_families Font families to install.
- * @param array $files Files to install.
- * @return array $error_messages Array of error messages.
+ * @param WP_REST_Request $request Full details about the request.
+ * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
*/
- private function get_validation_errors( $font_family_settings, $files ) {
- $error_messages = array();
+ public function get_item_permissions_check( $request ) {
+ return $this->get_items_permissions_check( $request );
+ }
- if ( ! is_array( $font_family_settings ) ) {
- $error_messages[] = __( 'font_family_settings should be a font family definition.', 'gutenberg' );
- return $error_messages;
+ /**
+ * Validates settings when creating or updating a font family.
+ *
+ * @since 6.5.0
+ *
+ * @param string $value Encoded JSON string of font family settings.
+ * @param WP_REST_Request $request Request object.
+ * @return false|WP_Error True if the settings are valid, otherwise a WP_Error object.
+ */
+ public function validate_font_family_settings( $value, $request ) {
+ $settings = json_decode( $value, true );
+
+ // Check settings string is valid JSON.
+ if ( null === $settings ) {
+ return new WP_Error(
+ 'rest_invalid_param',
+ __( 'font_family_settings parameter must be a valid JSON string.', 'gutenberg' ),
+ array( 'status' => 400 )
+ );
}
- if (
- ! isset( $font_family_settings['slug'] ) ||
- ! isset( $font_family_settings['name'] ) ||
- ! isset( $font_family_settings['fontFamily'] )
- ) {
- $error_messages[] = __( 'Font family should have slug, name and fontFamily properties defined.', 'gutenberg' );
+ $schema = $this->get_item_schema()['properties']['font_family_settings'];
+ $required = $schema['required'];
- return $error_messages;
- }
+ if ( isset( $request['id'] ) ) {
+ // Allow sending individual properties if we are updating an existing font family.
+ unset( $schema['required'] );
- if ( isset( $font_family_settings['fontFace'] ) ) {
- if ( ! is_array( $font_family_settings['fontFace'] ) ) {
- $error_messages[] = __( 'Font family should have fontFace property defined as an array.', 'gutenberg' );
+ // But don't allow updating the slug, since it is used as a unique identifier.
+ if ( isset( $settings['slug'] ) ) {
+ return new WP_Error(
+ 'rest_invalid_param',
+ __( 'font_family_settings[slug] cannot be updated.', 'gutenberg' ),
+ array( 'status' => 400 )
+ );
}
+ }
- if ( count( $font_family_settings['fontFace'] ) < 1 ) {
- $error_messages[] = __( 'Font family should have at least one font face definition.', 'gutenberg' );
- }
+ // Check that the font face settings match the theme.json schema.
+ $has_valid_settings = rest_validate_value_from_schema( $settings, $schema, 'font_family_settings' );
- if ( ! empty( $font_family_settings['fontFace'] ) ) {
- for ( $face_index = 0; $face_index < count( $font_family_settings['fontFace'] ); $face_index++ ) {
-
- $font_face = $font_family_settings['fontFace'][ $face_index ];
- if ( ! isset( $font_face['fontWeight'] ) || ! isset( $font_face['fontStyle'] ) ) {
- $error_messages[] = sprintf(
- // translators: font face index.
- __( 'Font family Font face [%1$s] should have fontWeight and fontStyle properties defined.', 'gutenberg' ),
- $face_index
- );
- }
-
- if ( isset( $font_face['downloadFromUrl'] ) && isset( $font_face['uploadedFile'] ) ) {
- $error_messages[] = sprintf(
- // translators: font face index.
- __( 'Font family Font face [%1$s] should have only one of the downloadFromUrl or uploadedFile properties defined and not both.', 'gutenberg' ),
- $face_index
- );
- }
-
- if ( isset( $font_face['uploadedFile'] ) ) {
- if ( ! isset( $files[ $font_face['uploadedFile'] ] ) ) {
- $error_messages[] = sprintf(
- // translators: font face index.
- __( 'Font family Font face [%1$s] file is not defined in the request files.', 'gutenberg' ),
- $face_index
- );
- }
- }
- }
+ if ( is_wp_error( $has_valid_settings ) ) {
+ $has_valid_settings->add_data( array( 'status' => 400 ) );
+ return $has_valid_settings;
+ }
+
+ // Check that none of the required settings are empty values.
+ foreach ( $required as $key ) {
+ if ( isset( $settings[ $key ] ) && ! $settings[ $key ] ) {
+ return new WP_Error(
+ 'rest_invalid_param',
+ /* translators: %s: Font family setting key. */
+ sprintf( __( 'font_family_settings[%s] cannot be empty.', 'gutenberg' ), $key ),
+ array( 'status' => 400 )
+ );
}
}
- return $error_messages;
+ return true;
}
/**
- * Validate input for the install endpoint.
+ * Sanitizes the font family settings when creating or updating a font family.
*
- * @since 6.5.0
+ * @since 6.5.0
*
- * @param string $param The font families to install.
- * @param WP_REST_Request $request The request object.
- * @return true|WP_Error True if the parameter is valid, WP_Error otherwise.
+ * @param string $value Encoded JSON string of font family settings.
+ * @param WP_REST_Request $request Request object.
+ * @return array Decoded array font family settings.
*/
- public function validate_install_font_families( $param, $request ) {
- $font_family_settings = json_decode( $param, true );
- $files = $request->get_file_params();
- $error_messages = $this->get_validation_errors( $font_family_settings, $files );
+ public function sanitize_font_family_settings( $value ) {
+ $settings = json_decode( $value, true );
- if ( empty( $error_messages ) ) {
- return true;
+ if ( isset( $settings['fontFamily'] ) ) {
+ $settings['fontFamily'] = WP_Font_Family_Utils::format_font_family( $settings['fontFamily'] );
}
- return new WP_Error( 'rest_invalid_param', implode( ', ', $error_messages ), array( 'status' => 400 ) );
+ // Provide default for preview, if not provided.
+ if ( ! isset( $settings['preview'] ) ) {
+ $settings['preview'] = '';
+ }
+
+ return $settings;
}
/**
- * Gets the schema for the uninstall endpoint.
+ * Creates a single font family.
*
* @since 6.5.0
*
- * @return array Schema array.
+ * @param WP_REST_Request $request Full details about the request.
+ * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
- public function uninstall_schema() {
- return array(
- 'font_families' => array(
- 'type' => 'array',
- 'description' => __( 'The font families to install.', 'gutenberg' ),
- 'required' => true,
- 'minItems' => 1,
- 'items' => array(
- 'required' => true,
- 'type' => 'object',
- 'properties' => array(
- 'slug' => array(
- 'type' => 'string',
- 'description' => __( 'The font family slug.', 'gutenberg' ),
- 'required' => true,
- ),
- ),
- ),
- ),
+ public function create_item( $request ) {
+ $settings = $request->get_param( 'font_family_settings' );
+
+ // Check that the font family slug is unique.
+ $query = new WP_Query(
+ array(
+ 'post_type' => $this->post_type,
+ 'posts_per_page' => 1,
+ 'name' => $settings['slug'],
+ 'update_post_meta_cache' => false,
+ 'update_post_term_cache' => false,
+ )
);
+ if ( ! empty( $query->get_posts() ) ) {
+ return new WP_Error(
+ 'rest_duplicate_font_family',
+ /* translators: %s: Font family slug. */
+ sprintf( __( 'A font family with slug "%s" already exists.', 'gutenberg' ), $settings['slug'] ),
+ array( 'status' => 400 )
+ );
+ }
+
+ return parent::create_item( $request );
}
/**
- * Removes font families from the Font Library and all their assets.
+ * Deletes a single font family.
*
* @since 6.5.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
- public function uninstall_fonts( $request ) {
- $fonts_to_uninstall = $request->get_param( 'font_families' );
+ public function delete_item( $request ) {
+ $force = isset( $request['force'] ) ? (bool) $request['force'] : false;
- $errors = array();
- $successes = array();
-
- if ( empty( $fonts_to_uninstall ) ) {
- $errors[] = new WP_Error(
- 'no_fonts_to_install',
- __( 'No fonts to uninstall', 'gutenberg' )
- );
- $data = array(
- 'successes' => $successes,
- 'errors' => $errors,
+ // We don't support trashing for font families.
+ if ( ! $force ) {
+ return new WP_Error(
+ 'rest_trash_not_supported',
+ /* translators: %s: force=true */
+ sprintf( __( "Font faces do not support trashing. Set '%s' to delete.", 'gutenberg' ), 'force=true' ),
+ array( 'status' => 501 )
);
- $response = rest_ensure_response( $data );
- $response->set_status( 400 );
- return $response;
}
- foreach ( $fonts_to_uninstall as $font_data ) {
- $font = new WP_Font_Family( $font_data );
- $result = $font->uninstall();
- if ( is_wp_error( $result ) ) {
- $errors[] = $result;
- } else {
- $successes[] = $result;
- }
- }
- $data = array(
- 'successes' => $successes,
- 'errors' => $errors,
- );
- return rest_ensure_response( $data );
+ return parent::delete_item( $request );
}
/**
- * Checks whether the user has permissions to update the Font Library.
+ * Prepares a single font family output for response.
*
* @since 6.5.0
*
- * @return true|WP_Error True if the request has write access for the item, WP_Error object otherwise.
+ * @param WP_Post $item Post object.
+ * @param WP_REST_Request $request Request object.
+ * @return WP_REST_Response Response object.
*/
- public function update_font_library_permissions_check() {
- if ( ! current_user_can( 'edit_theme_options' ) ) {
- return new WP_Error(
- 'rest_cannot_update_font_library',
- __( 'Sorry, you are not allowed to update the Font Library on this site.', 'gutenberg' ),
- array(
- 'status' => rest_authorization_required_code(),
- )
- );
+ public function prepare_item_for_response( $item, $request ) {
+ $fields = $this->get_fields_for_response( $request );
+ $data = array();
+
+ if ( rest_is_field_included( 'id', $fields ) ) {
+ $data['id'] = $item->ID;
}
- return true;
+
+ if ( rest_is_field_included( 'theme_json_version', $fields ) ) {
+ $data['theme_json_version'] = 2;
+ }
+
+ if ( rest_is_field_included( 'font_faces', $fields ) ) {
+ $data['font_faces'] = $this->get_font_face_ids( $item->ID );
+ }
+
+ if ( rest_is_field_included( 'font_family_settings', $fields ) ) {
+ $data['font_family_settings'] = $this->get_settings_from_post( $item );
+ }
+
+ $context = ! empty( $request['context'] ) ? $request['context'] : 'edit';
+ $data = $this->add_additional_fields_to_object( $data, $request );
+ $data = $this->filter_response_by_context( $data, $context );
+
+ $response = rest_ensure_response( $data );
+
+ if ( rest_is_field_included( '_links', $fields ) ) {
+ $links = $this->prepare_links( $item );
+ $response->add_links( $links );
+ }
+
+ /**
+ * Filters the font family data for a REST API response.
+ *
+ * @since 6.5.0
+ *
+ * @param WP_REST_Response $response The response object.
+ * @param WP_Post $post Font family post object.
+ * @param WP_REST_Request $request Request object.
+ */
+ return apply_filters( 'rest_prepare_wp_font_family', $response, $item, $request );
+ }
+
+ /**
+ * Retrieves the post's schema, conforming to JSON Schema.
+ *
+ * @since 6.5.0
+ *
+ * @return array Item schema data.
+ */
+ public function get_item_schema() {
+ if ( $this->schema ) {
+ return $this->add_additional_fields_schema( $this->schema );
+ }
+
+ $schema = array(
+ '$schema' => 'http://json-schema.org/draft-04/schema#',
+ 'title' => $this->post_type,
+ 'type' => 'object',
+ // Base properties for every Post.
+ 'properties' => array(
+ 'id' => array(
+ 'description' => __( 'Unique identifier for the post.', 'default' ),
+ 'type' => 'integer',
+ 'context' => array( 'edit' ),
+ 'readonly' => true,
+ ),
+ 'theme_json_version' => array(
+ 'description' => __( 'Version of the theme.json schema used for the typography settings.', 'gutenberg' ),
+ 'type' => 'integer',
+ 'default' => 2,
+ 'minimum' => 2,
+ 'maximum' => 2,
+ 'context' => array( 'edit' ),
+ ),
+ 'font_faces' => array(
+ 'description' => __( 'The IDs of the child font faces in the font family.', 'gutenberg' ),
+ 'type' => 'array',
+ 'context' => array( 'edit' ),
+ 'items' => array(
+ 'type' => 'integer',
+ ),
+ ),
+ // Font family settings come directly from theme.json schema
+ // See https://schemas.wp.org/trunk/theme.json
+ 'font_family_settings' => array(
+ 'description' => __( 'font-face declaration in theme.json format.', 'gutenberg' ),
+ 'type' => 'object',
+ 'context' => array( 'edit' ),
+ 'properties' => array(
+ 'name' => array(
+ 'description' => 'Name of the font family preset, translatable.',
+ 'type' => 'string',
+ ),
+ 'slug' => array(
+ 'description' => 'Kebab-case unique identifier for the font family preset.',
+ 'type' => 'string',
+ ),
+ 'fontFamily' => array(
+ 'description' => 'CSS font-family value.',
+ 'type' => 'string',
+ ),
+ 'preview' => array(
+ 'description' => 'URL to a preview image of the font family.',
+ 'type' => 'string',
+ ),
+ ),
+ 'required' => array( 'name', 'slug', 'fontFamily' ),
+ 'additionalProperties' => false,
+ ),
+ ),
+ );
+
+ $this->schema = $schema;
+
+ return $this->add_additional_fields_schema( $this->schema );
}
/**
- * Checks whether the font directory exists or not.
+ * Retrieves the query params for the font family collection.
*
* @since 6.5.0
*
- * @return bool Whether the font directory exists.
+ * @return array Collection parameters.
*/
- private function has_upload_directory() {
- $upload_dir = wp_get_font_dir()['path'];
- return is_dir( $upload_dir );
+ public function get_collection_params() {
+ $query_params = parent::get_collection_params();
+
+ $query_params['context']['default'] = 'edit';
+
+ // Remove unneeded params.
+ unset( $query_params['after'] );
+ unset( $query_params['modified_after'] );
+ unset( $query_params['before'] );
+ unset( $query_params['modified_before'] );
+ unset( $query_params['search'] );
+ unset( $query_params['search_columns'] );
+ unset( $query_params['status'] );
+
+ $query_params['orderby']['default'] = 'id';
+ $query_params['orderby']['enum'] = array( 'id', 'include' );
+
+ /**
+ * Filters collection parameters for the font family controller.
+ *
+ * @since 6.5.0
+ *
+ * @param array $query_params JSON Schema-formatted collection parameters.
+ */
+ return apply_filters( 'rest_wp_font_family_collection_params', $query_params );
}
/**
- * Checks whether the user has write permissions to the temp and fonts directories.
+ * Retrieves the query params for the font family collection, defaulting to the 'edit' context.
*
* @since 6.5.0
*
- * @return true|WP_Error True if the user has write permissions, WP_Error object otherwise.
+ * @param array $args Optional. Additional arguments for context parameter. Default empty array.
+ * @return array Context parameter details.
*/
- private function has_write_permission() {
- // The update endpoints requires write access to the temp and the fonts directories.
- $temp_dir = get_temp_dir();
- $upload_dir = wp_get_font_dir()['path'];
- if ( ! is_writable( $temp_dir ) || ! wp_is_writable( $upload_dir ) ) {
- return false;
+ public function get_context_param( $args = array() ) {
+ if ( isset( $args['default'] ) ) {
+ $args['default'] = 'edit';
}
- return true;
+ return parent::get_context_param( $args );
}
/**
- * Checks whether the request needs write permissions.
+ * Get the arguments used when creating or updating a font family.
*
* @since 6.5.0
*
- * @param array[] $font_family_settings Font family definition.
- * @return bool Whether the request needs write permissions.
+ * @return array Font family create/edit arguments.
*/
- private function needs_write_permission( $font_family_settings ) {
- if ( isset( $font_family_settings['fontFace'] ) ) {
- foreach ( $font_family_settings['fontFace'] as $face ) {
- // If the font is being downloaded from a URL or uploaded, it needs write permissions.
- if ( isset( $face['downloadFromUrl'] ) || isset( $face['uploadedFile'] ) ) {
- return true;
- }
- }
+ public function get_endpoint_args_for_item_schema( $method = WP_REST_Server::CREATABLE ) {
+ if ( WP_REST_Server::CREATABLE === $method || WP_REST_Server::EDITABLE === $method ) {
+ $properties = $this->get_item_schema()['properties'];
+ return array(
+ 'theme_json_version' => $properties['theme_json_version'],
+ // When creating or updating, font_family_settings is stringified JSON, to work with multipart/form-data.
+ // Font families don't currently support file uploads, but may accept preview files in the future.
+ 'font_family_settings' => array(
+ 'description' => __( 'font-family declaration in theme.json format, encoded as a string.', 'gutenberg' ),
+ 'type' => 'string',
+ 'required' => true,
+ 'validate_callback' => array( $this, 'validate_font_family_settings' ),
+ 'sanitize_callback' => array( $this, 'sanitize_font_family_settings' ),
+ ),
+ );
}
- return false;
+
+ return parent::get_endpoint_args_for_item_schema( $method );
}
/**
- * Installs new fonts.
+ * Get the child font face post IDs.
*
- * Takes a request containing new fonts to install, downloads their assets, and adds them
- * to the Font Library.
+ * @since 6.5.0
+ *
+ * @param int $font_family_id Font family post ID.
+ * @return int[] Array of child font face post IDs.
+ * .
+ */
+ protected function get_font_face_ids( $font_family_id ) {
+ $query = new WP_Query(
+ array(
+ 'fields' => 'ids',
+ 'post_parent' => $font_family_id,
+ 'post_type' => 'wp_font_face',
+ 'posts_per_page' => 99,
+ 'order' => 'ASC',
+ 'orderby' => 'id',
+ 'update_post_meta_cache' => false,
+ 'update_post_term_cache' => false,
+ )
+ );
+
+ return $query->get_posts();
+ }
+
+ /**
+ * Prepares font family links for the request.
*
* @since 6.5.0
*
- * @param WP_REST_Request $request The request object containing the new fonts to install
- * in the request parameters.
- * @return WP_REST_Response|WP_Error The updated Font Library post content.
+ * @param WP_Post $post Post object.
+ * @return array Links for the given post.
*/
- public function install_fonts( $request ) {
- // Get new fonts to install.
- $font_family_settings = $request->get_param( 'font_family_settings' );
-
- /*
- * As this is receiving form data, the font families are encoded as a string.
- * The form data is used because local fonts need to use that format to
- * attach the files in the request.
- */
- $font_family_settings = json_decode( $font_family_settings, true );
+ protected function prepare_links( $post ) {
+ // Entity meta.
+ $links = parent::prepare_links( $post );
- $successes = array();
- $errors = array();
- $response_status = 200;
+ return array(
+ 'self' => $links['self'],
+ 'collection' => $links['collection'],
+ 'font_faces' => $this->prepare_font_face_links( $post->ID ),
+ );
+ }
- if ( empty( $font_family_settings ) ) {
- $errors[] = new WP_Error(
- 'no_fonts_to_install',
- __( 'No fonts to install', 'gutenberg' )
+ /**
+ * Prepares child font face links for the request.
+ *
+ * @param int $font_family_id Font family post ID.
+ * @return array Links for the child font face posts.
+ */
+ protected function prepare_font_face_links( $font_family_id ) {
+ $font_face_ids = $this->get_font_face_ids( $font_family_id );
+ $links = array();
+ foreach ( $font_face_ids as $font_face_id ) {
+ $links[] = array(
+ 'embeddable' => true,
+ 'href' => rest_url( $this->namespace . '/' . $this->rest_base . '/' . $font_family_id . '/font-faces/' . $font_face_id ),
);
- $response_status = 400;
}
+ return $links;
+ }
- if ( $this->needs_write_permission( $font_family_settings ) ) {
- $upload_dir = wp_get_font_dir()['path'];
- if ( ! $this->has_upload_directory() ) {
- if ( ! wp_mkdir_p( $upload_dir ) ) {
- $errors[] = new WP_Error(
- 'cannot_create_fonts_folder',
- sprintf(
- /* translators: %s: Directory path. */
- __( 'Error: Unable to create directory %s.', 'gutenberg' ),
- esc_html( $upload_dir )
- )
- );
- $response_status = 500;
- }
+ /**
+ * Prepares a single font family post for create or update.
+ *
+ * @since 6.5.0
+ *
+ * @param WP_REST_Request $request Request object.
+ * @return stdClass|WP_Error Post object or WP_Error.
+ */
+ protected function prepare_item_for_database( $request ) {
+ $prepared_post = new stdClass();
+ // Settings have already been decoded by ::sanitize_font_family_settings().
+ $settings = $request->get_param( 'font_family_settings' );
+
+ // This is an update and we merge with the existing font family.
+ if ( isset( $request['id'] ) ) {
+ $existing_post = $this->get_post( $request['id'] );
+ if ( is_wp_error( $existing_post ) ) {
+ return $existing_post;
}
- if ( $this->has_upload_directory() && ! $this->has_write_permission() ) {
- $errors[] = new WP_Error(
- 'cannot_write_fonts_folder',
- __( 'Error: WordPress does not have permission to write the fonts folder on your server.', 'gutenberg' )
- );
- $response_status = 500;
- }
+ $prepared_post->ID = $existing_post->ID;
+ $existing_settings = $this->get_settings_from_post( $existing_post );
+ $settings = array_merge( $existing_settings, $settings );
}
- if ( ! empty( $errors ) ) {
- $data = array(
- 'successes' => $successes,
- 'errors' => $errors,
- );
- $response = rest_ensure_response( $data );
- $response->set_status( $response_status );
- return $response;
- }
+ $prepared_post->post_type = $this->post_type;
+ $prepared_post->post_status = 'publish';
+ $prepared_post->post_title = $settings['name'];
+ $prepared_post->post_name = sanitize_title( $settings['slug'] );
- // Get uploaded files (used when installing local fonts).
- $files = $request->get_file_params();
- $font = new WP_Font_Family( $font_family_settings );
- $result = $font->install( $files );
- if ( is_wp_error( $result ) ) {
- $errors[] = $result;
- } else {
- $successes[] = $result;
- }
+ // Remove duplicate information from settings.
+ unset( $settings['name'] );
+ unset( $settings['slug'] );
+
+ $prepared_post->post_content = wp_json_encode( $settings );
- $data = array(
- 'successes' => $successes,
- 'errors' => $errors,
+ return $prepared_post;
+ }
+
+ /**
+ * Gets the font family's settings from the post.
+ *
+ * @since 6.5.0
+ *
+ * @param WP_Post $post Font family post object.
+ * @return array Font family settings array.
+ */
+ protected function get_settings_from_post( $post ) {
+ $settings_json = json_decode( $post->post_content, true );
+
+ // Default to empty strings if the settings are missing.
+ return array(
+ 'name' => isset( $post->post_title ) && $post->post_title ? $post->post_title : '',
+ 'slug' => isset( $post->post_name ) && $post->post_name ? $post->post_name : '',
+ 'fontFamily' => isset( $settings_json['fontFamily'] ) && $settings_json['fontFamily'] ? $settings_json['fontFamily'] : '',
+ 'preview' => isset( $settings_json['preview'] ) && $settings_json['preview'] ? $settings_json['preview'] : '',
);
- return rest_ensure_response( $data );
}
}
diff --git a/lib/experimental/fonts/font-library/font-library.php b/lib/experimental/fonts/font-library/font-library.php
index d1ad8e1447ad9c..e9744da5958f4c 100644
--- a/lib/experimental/fonts/font-library/font-library.php
+++ b/lib/experimental/fonts/font-library/font-library.php
@@ -22,16 +22,73 @@
function gutenberg_init_font_library_routes() {
// @core-merge: This code will go into Core's `create_initial_post_types()`.
$args = array(
+ 'labels' => array(
+ 'name' => __( 'Font Families', 'gutenberg' ),
+ 'singular_name' => __( 'Font Family', 'gutenberg' ),
+ ),
'public' => false,
- '_builtin' => true, /* internal use only. don't use this when registering your own post type. */
- 'label' => 'Font Family',
+ '_builtin' => true, /* internal use only. don't use this when registering your own post type. */
+ 'hierarchical' => false,
+ 'capabilities' => array(
+ 'read' => 'edit_theme_options',
+ 'read_post' => 'edit_theme_options',
+ 'read_private_posts' => 'edit_theme_options',
+ 'create_posts' => 'edit_theme_options',
+ 'publish_posts' => 'edit_theme_options',
+ 'edit_post' => 'edit_theme_options',
+ 'edit_posts' => 'edit_theme_options',
+ 'edit_others_posts' => 'edit_theme_options',
+ 'edit_published_posts' => 'edit_theme_options',
+ 'delete_post' => 'edit_theme_options',
+ 'delete_posts' => 'edit_theme_options',
+ 'delete_others_posts' => 'edit_theme_options',
+ 'delete_published_posts' => 'edit_theme_options',
+ ),
+ 'map_meta_cap' => false,
+ 'query_var' => false,
'show_in_rest' => true,
'rest_base' => 'font-families',
'rest_controller_class' => 'WP_REST_Font_Families_Controller',
- 'autosave_rest_controller_class' => 'WP_REST_Autosave_Font_Families_Controller',
+ // Disable autosave endpoints for font families.
+ 'autosave_rest_controller_class' => 'stdClass',
);
register_post_type( 'wp_font_family', $args );
+ register_post_type(
+ 'wp_font_face',
+ array(
+ 'labels' => array(
+ 'name' => __( 'Font Faces', 'gutenberg' ),
+ 'singular_name' => __( 'Font Face', 'gutenberg' ),
+ ),
+ 'public' => false,
+ '_builtin' => true, /* internal use only. don't use this when registering your own post type. */
+ 'hierarchical' => false,
+ 'capabilities' => array(
+ 'read' => 'edit_theme_options',
+ 'read_post' => 'edit_theme_options',
+ 'read_private_posts' => 'edit_theme_options',
+ 'create_posts' => 'edit_theme_options',
+ 'publish_posts' => 'edit_theme_options',
+ 'edit_post' => 'edit_theme_options',
+ 'edit_posts' => 'edit_theme_options',
+ 'edit_others_posts' => 'edit_theme_options',
+ 'edit_published_posts' => 'edit_theme_options',
+ 'delete_post' => 'edit_theme_options',
+ 'delete_posts' => 'edit_theme_options',
+ 'delete_others_posts' => 'edit_theme_options',
+ 'delete_published_posts' => 'edit_theme_options',
+ ),
+ 'map_meta_cap' => false,
+ 'query_var' => false,
+ 'show_in_rest' => true,
+ 'rest_base' => 'font-families/(?P[\d]+)/font-faces',
+ 'rest_controller_class' => 'WP_REST_Font_Faces_Controller',
+ // Disable autosave endpoints for font faces.
+ 'autosave_rest_controller_class' => 'stdClass',
+ )
+ );
+
// @core-merge: This code will go into Core's `create_initial_rest_routes()`.
$font_collections_controller = new WP_REST_Font_Collections_Controller();
$font_collections_controller->register_routes();
@@ -79,7 +136,7 @@ function wp_unregister_font_collection( $collection_id ) {
'slug' => 'default-font-collection',
'name' => 'Google Fonts',
'description' => __( 'Add from Google Fonts. Fonts are copied to and served from your site.', 'gutenberg' ),
- 'src' => 'https://s.w.org/images/fonts/16.7/collections/google-fonts-with-preview.json',
+ 'src' => 'https://s.w.org/images/fonts/17.6/collections/google-fonts-with-preview.json',
);
wp_register_font_collection( $default_font_collection );
@@ -132,3 +189,141 @@ function wp_get_font_dir( $defaults = array() ) {
return apply_filters( 'font_dir', $defaults );
}
}
+
+// @core-merge: Filters should go in `src/wp-includes/default-filters.php`,
+// functions in a general file for font library.
+if ( ! function_exists( '_wp_after_delete_font_family' ) ) {
+ /**
+ * Deletes child font faces when a font family is deleted.
+ *
+ * @access private
+ * @since 6.5.0
+ *
+ * @param int $post_id Post ID.
+ * @param WP_Post $post Post object.
+ * @return void
+ */
+ function _wp_after_delete_font_family( $post_id, $post ) {
+ if ( 'wp_font_family' !== $post->post_type ) {
+ return;
+ }
+
+ $font_faces = get_children(
+ array(
+ 'post_parent' => $post_id,
+ 'post_type' => 'wp_font_face',
+ )
+ );
+
+ foreach ( $font_faces as $font_face ) {
+ wp_delete_post( $font_face->ID, true );
+ }
+ }
+ add_action( 'deleted_post', '_wp_after_delete_font_family', 10, 2 );
+}
+
+if ( ! function_exists( '_wp_before_delete_font_face' ) ) {
+ /**
+ * Deletes associated font files when a font face is deleted.
+ *
+ * @access private
+ * @since 6.5.0
+ *
+ * @param int $post_id Post ID.
+ * @param WP_Post $post Post object.
+ * @return void
+ */
+ function _wp_before_delete_font_face( $post_id, $post ) {
+ if ( 'wp_font_face' !== $post->post_type ) {
+ return;
+ }
+
+ $font_files = get_post_meta( $post_id, '_wp_font_face_file', false );
+
+ foreach ( $font_files as $font_file ) {
+ wp_delete_file( wp_get_font_dir()['path'] . '/' . $font_file );
+ }
+ }
+ add_action( 'before_delete_post', '_wp_before_delete_font_face', 10, 2 );
+}
+
+// @core-merge: Do not merge this back compat function, it is for supporting a legacy font family format only in Gutenberg.
+/**
+ * Convert legacy font family posts to the new format.
+ *
+ * @return void
+ */
+function gutenberg_convert_legacy_font_family_format() {
+ if ( get_option( 'gutenberg_font_family_format_converted' ) ) {
+ return;
+ }
+
+ $font_families = new WP_Query(
+ array(
+ 'post_type' => 'wp_font_family',
+ // Set a maximum, but in reality there will be far less than this.
+ 'posts_per_page' => 999,
+ 'update_post_meta_cache' => false,
+ 'update_post_term_cache' => false,
+ )
+ );
+
+ foreach ( $font_families->get_posts() as $font_family ) {
+ $already_converted = get_post_meta( $font_family->ID, '_gutenberg_legacy_font_family', true );
+ if ( $already_converted ) {
+ continue;
+ }
+
+ // Stash the old font family content in a meta field just in case we need it.
+ update_post_meta( $font_family->ID, '_gutenberg_legacy_font_family', $font_family->post_content );
+
+ $font_family_json = json_decode( $font_family->post_content, true );
+ if ( ! $font_family_json ) {
+ continue;
+ }
+
+ $font_faces = $font_family_json['fontFace'] ?? array();
+ unset( $font_family_json['fontFace'] );
+
+ // Save wp_font_face posts within the family.
+ foreach ( $font_faces as $font_face ) {
+ $args = array();
+ $args['post_type'] = 'wp_font_face';
+ $args['post_title'] = WP_Font_Family_Utils::get_font_face_slug( $font_face );
+ $args['post_name'] = sanitize_title( $args['post_title'] );
+ $args['post_status'] = 'publish';
+ $args['post_parent'] = $font_family->ID;
+ $args['post_content'] = wp_json_encode( $font_face );
+
+ $font_face_id = wp_insert_post( wp_slash( $args ) );
+
+ $file_urls = (array) $font_face['src'] ?? array();
+
+ foreach ( $file_urls as $file_url ) {
+ // continue if the file is not local.
+ if ( false === strpos( $file_url, site_url() ) ) {
+ continue;
+ }
+
+ $relative_path = basename( $file_url );
+ update_post_meta( $font_face_id, '_wp_font_face_file', $relative_path );
+ }
+ }
+
+ // Update the font family post to remove the font face data.
+ $args = array();
+ $args['ID'] = $font_family->ID;
+ $args['post_title'] = $font_family_json['name'] ?? '';
+ $args['post_name'] = sanitize_title( $font_family_json['slug'] );
+
+ unset( $font_family_json['name'] );
+ unset( $font_family_json['slug'] );
+
+ $args['post_content'] = wp_json_encode( $font_family_json );
+
+ wp_update_post( wp_slash( $args ) );
+ }
+
+ update_option( 'gutenberg_font_family_format_converted', true );
+}
+add_action( 'init', 'gutenberg_convert_legacy_font_family_format' );
diff --git a/lib/experimental/interactivity-api/class-wp-directive-context.php b/lib/experimental/interactivity-api/class-wp-directive-context.php
deleted file mode 100644
index 4276eddca20acb..00000000000000
--- a/lib/experimental/interactivity-api/class-wp-directive-context.php
+++ /dev/null
@@ -1,82 +0,0 @@
-
- *
- *
- *
- *
- *
- *
- */
-class WP_Directive_Context {
- /**
- * The stack used to store contexts internally.
- *
- * @var array An array of contexts.
- */
- protected $stack = array( array() );
-
- /**
- * Constructor.
- *
- * Accepts a context as an argument to initialize this with.
- *
- * @param array $context A context.
- */
- public function __construct( $context = array() ) {
- $this->set_context( $context );
- }
-
- /**
- * Return the current context.
- *
- * @return array The current context.
- */
- public function get_context() {
- return end( $this->stack );
- }
-
- /**
- * Set the current context.
- *
- * @param array $context The context to be set.
- *
- * @return void
- */
- public function set_context( $context ) {
- array_push(
- $this->stack,
- array_replace_recursive( $this->get_context(), $context )
- );
- }
-
- /**
- * Reset the context to its previous state.
- *
- * @return void
- */
- public function rewind_context() {
- array_pop( $this->stack );
- }
-}
diff --git a/lib/experimental/interactivity-api/class-wp-directive-processor.php b/lib/experimental/interactivity-api/class-wp-directive-processor.php
deleted file mode 100644
index 723b36026ce2af..00000000000000
--- a/lib/experimental/interactivity-api/class-wp-directive-processor.php
+++ /dev/null
@@ -1,283 +0,0 @@
-get_tag();
-
- if ( self::is_html_void_element( $tag_name ) ) {
- return false;
- }
-
- while ( $this->next_tag(
- array(
- 'tag_name' => $tag_name,
- 'tag_closers' => 'visit',
- )
- ) ) {
- if ( ! $this->is_tag_closer() ) {
- ++$depth;
- continue;
- }
-
- if ( 0 === $depth ) {
- return true;
- }
-
- --$depth;
- }
-
- return false;
- }
-
- /**
- * Returns the content between two balanced tags.
- *
- * When called on an opening tag, return the HTML content found between that
- * opening tag and its matching closing tag.
- *
- * @return string The content between the current opening and its matching
- * closing tag.
- */
- public function get_inner_html() {
- $bookmarks = $this->get_balanced_tag_bookmarks();
- if ( ! $bookmarks ) {
- return false;
- }
- list( $start_name, $end_name ) = $bookmarks;
-
- $start = $this->bookmarks[ $start_name ]->start + $this->bookmarks[ $start_name ]->length + 1;
- $end = $this->bookmarks[ $end_name ]->start;
-
- $this->seek( $start_name ); // Return to original position.
- $this->release_bookmark( $start_name );
- $this->release_bookmark( $end_name );
-
- return substr( $this->html, $start, $end - $start );
- }
-
- /**
- * Sets the content between two balanced tags.
- *
- * When called on an opening tag, set the HTML content found between that
- * opening tag and its matching closing tag.
- *
- * @param string $new_html The string to replace the content between the
- * matching tags with.
- * @return bool Whether the content was successfully replaced.
- */
- public function set_inner_html( $new_html ) {
- $this->get_updated_html(); // Apply potential previous updates.
-
- $bookmarks = $this->get_balanced_tag_bookmarks();
- if ( ! $bookmarks ) {
- return false;
- }
- list( $start_name, $end_name ) = $bookmarks;
-
- $start = $this->bookmarks[ $start_name ]->start + $this->bookmarks[ $start_name ]->length + 1;
- $end = $this->bookmarks[ $end_name ]->start;
-
- $this->seek( $start_name ); // Return to original position.
- $this->release_bookmark( $start_name );
- $this->release_bookmark( $end_name );
-
- $this->lexical_updates[] = new Gutenberg_HTML_Text_Replacement_6_5( $start, $end - $start, $new_html );
- return true;
- }
-
- /**
- * Returns a pair of bookmarks for the current opening tag and the matching
- * closing tag.
- *
- * @return array|false A pair of bookmarks, or false if there's no matching
- * closing tag.
- */
- public function get_balanced_tag_bookmarks() {
- $i = 0;
- while ( array_key_exists( 'start' . $i, $this->bookmarks ) ) {
- ++$i;
- }
- $start_name = 'start' . $i;
-
- $this->set_bookmark( $start_name );
- if ( ! $this->next_balanced_closer() ) {
- $this->release_bookmark( $start_name );
- return false;
- }
-
- $i = 0;
- while ( array_key_exists( 'end' . $i, $this->bookmarks ) ) {
- ++$i;
- }
- $end_name = 'end' . $i;
- $this->set_bookmark( $end_name );
-
- return array( $start_name, $end_name );
- }
-
- /**
- * Checks whether a given HTML element is void (e.g. ).
- *
- * @see https://html.spec.whatwg.org/#elements-2
- *
- * @param string $tag_name The element in question.
- * @return bool True if the element is void.
- */
- public static function is_html_void_element( $tag_name ) {
- switch ( $tag_name ) {
- case 'AREA':
- case 'BASE':
- case 'BR':
- case 'COL':
- case 'EMBED':
- case 'HR':
- case 'IMG':
- case 'INPUT':
- case 'LINK':
- case 'META':
- case 'SOURCE':
- case 'TRACK':
- case 'WBR':
- return true;
-
- default:
- return false;
- }
- }
-
- /**
- * Extracts and return the directive type and the the part after the double
- * hyphen from an attribute name (if present), in an array format.
- *
- * Examples:
- *
- * 'wp-island' => array( 'wp-island', null )
- * 'wp-bind--src' => array( 'wp-bind', 'src' )
- * 'wp-thing--and--thang' => array( 'wp-thing', 'and--thang' )
- *
- * @param string $name The attribute name.
- * @return array The resulting array.
- */
- public static function parse_attribute_name( $name ) {
- return explode( '--', $name, 2 );
- }
-
- /**
- * Parse and extract the namespace and path from the given value.
- *
- * If the value contains a JSON instead of a path, the function parses it
- * and returns the resulting array.
- *
- * @param string $value Passed value.
- * @param string $ns Namespace fallback.
- * @return array The resulting array
- */
- public static function parse_attribute_value( $value, $ns = null ) {
- $matches = array();
- $has_ns = preg_match( '/^([\w\-_\/]+)::(.+)$/', $value, $matches );
-
- /*
- * Overwrite both `$ns` and `$value` variables if `$value` explicitly
- * contains a namespace.
- */
- if ( $has_ns ) {
- list( , $ns, $value ) = $matches;
- }
-
- /*
- * Try to decode `$value` as a JSON object. If it works, `$value` is
- * replaced with the resulting array. The original string is preserved
- * otherwise.
- *
- * Note that `json_decode` returns `null` both for an invalid JSON or
- * the `'null'` string (a valid JSON). In the latter case, `$value` is
- * replaced with `null`.
- */
- $data = json_decode( $value, true );
- if ( null !== $data || 'null' === trim( $value ) ) {
- $value = $data;
- }
-
- return array( $ns, $value );
- }
-}
diff --git a/lib/experimental/interactivity-api/class-wp-interactivity-initial-state.php b/lib/experimental/interactivity-api/class-wp-interactivity-initial-state.php
deleted file mode 100644
index 15e57edfa4a6a2..00000000000000
--- a/lib/experimental/interactivity-api/class-wp-interactivity-initial-state.php
+++ /dev/null
@@ -1,82 +0,0 @@
-%s',
- wp_json_encode( self::$initial_state, JSON_HEX_TAG | JSON_HEX_AMP )
- );
- }
-}
diff --git a/lib/experimental/interactivity-api/directive-processing.php b/lib/experimental/interactivity-api/directive-processing.php
deleted file mode 100644
index 5a97166d6d22bf..00000000000000
--- a/lib/experimental/interactivity-api/directive-processing.php
+++ /dev/null
@@ -1,214 +0,0 @@
-get_registered( $parsed_block['blockName'] );
- $is_interactive = isset( $block_type->supports['interactivity'] ) && $block_type->supports['interactivity'];
- if ( $is_interactive ) {
- WP_Directive_Processor::mark_interactive_root_block( $parsed_block );
- }
- }
-
- return $parsed_block;
-}
-add_filter( 'render_block_data', 'gutenberg_interactivity_mark_root_interactive_blocks', 10, 1 );
-
-/**
- * Processes the directives in the root blocks.
- *
- * @param string $block_content The block content.
- * @param array $block The full block.
- *
- * @return string Filtered block content.
- */
-function gutenberg_process_directives_in_root_blocks( $block_content, $block ) {
- if ( WP_Directive_Processor::is_marked_as_interactive_root_block( $block ) ) {
- WP_Directive_Processor::unmark_interactive_root_block();
- $context = new WP_Directive_Context();
- $namespace_stack = array();
- return gutenberg_process_interactive_html( $block_content, $context, $namespace_stack );
- }
-
- return $block_content;
-}
-add_filter( 'render_block', 'gutenberg_process_directives_in_root_blocks', 10, 2 );
-
-/**
- * Processes interactive HTML by applying directives to the HTML tags.
- *
- * It uses the WP_Directive_Processor class to parse the HTML and apply the
- * directives. If a tag contains a 'WP-INNER-BLOCKS' string and there are inner
- * blocks to process, the function processes these inner blocks and replaces the
- * 'WP-INNER-BLOCKS' tag in the HTML with those blocks.
- *
- * @param string $html The HTML to process.
- * @param mixed $context The context to use when processing.
- * @param array $inner_blocks The inner blocks to process.
- * @param array $namespace_stack Stack of namespackes passed by reference.
- *
- * @return string The processed HTML.
- */
-function gutenberg_process_interactive_html( $html, $context, &$namespace_stack = array() ) {
- static $directives = array(
- 'data-wp-interactive' => 'gutenberg_interactivity_process_wp_interactive',
- 'data-wp-context' => 'gutenberg_interactivity_process_wp_context',
- 'data-wp-bind' => 'gutenberg_interactivity_process_wp_bind',
- 'data-wp-class' => 'gutenberg_interactivity_process_wp_class',
- 'data-wp-style' => 'gutenberg_interactivity_process_wp_style',
- 'data-wp-text' => 'gutenberg_interactivity_process_wp_text',
- );
-
- $tags = new WP_Directive_Processor( $html );
- $prefix = 'data-wp-';
- $tag_stack = array();
- while ( $tags->next_tag( array( 'tag_closers' => 'visit' ) ) ) {
- $tag_name = $tags->get_tag();
-
- if ( $tags->is_tag_closer() ) {
- if ( 0 === count( $tag_stack ) ) {
- continue;
- }
- list( $latest_opening_tag_name, $attributes ) = end( $tag_stack );
- if ( $latest_opening_tag_name === $tag_name ) {
- array_pop( $tag_stack );
- // If the matching opening tag didn't have any directives, we move on.
- if ( 0 === count( $attributes ) ) {
- continue;
- }
- }
- } else {
- $attributes = array();
- foreach ( $tags->get_attribute_names_with_prefix( $prefix ) as $name ) {
- /*
- * Removes the part after the double hyphen before looking for
- * the directive processor inside `$directives`, e.g., "wp-bind"
- * from "wp-bind--src" and "wp-context" from "wp-context" etc...
- */
- list( $type ) = $tags::parse_attribute_name( $name );
- if ( array_key_exists( $type, $directives ) ) {
- $attributes[] = $type;
- }
- }
-
- /*
- * If this is an open tag, and if it either has directives, or if
- * we're inside a tag that does, take note of this tag and its
- * directives so we can call its directive processor once we
- * encounter the matching closing tag.
- */
- if (
- ! $tags::is_html_void_element( $tag_name ) &&
- ( 0 !== count( $attributes ) || 0 !== count( $tag_stack ) )
- ) {
- $tag_stack[] = array( $tag_name, $attributes );
- }
- }
-
- // Extract all directive names. They'll be used later on.
- $directive_names = array_keys( $directives );
- $directive_names_rev = array_reverse( $directive_names );
-
- /*
- * Sort attributes by the order they appear in the `$directives`
- * argument, considering it as the priority order in which
- * directives should be processed. Note that the order is reversed
- * for tag closers.
- */
- $sorted_attrs = array_intersect(
- $tags->is_tag_closer()
- ? $directive_names_rev
- : $directive_names,
- $attributes
- );
-
- foreach ( $sorted_attrs as $attribute ) {
- call_user_func_array(
- $directives[ $attribute ],
- array(
- $tags,
- $context,
- end( $namespace_stack ),
- &$namespace_stack,
- )
- );
- }
- }
-
- return $tags->get_updated_html();
-}
-
-/**
- * Resolves the passed reference from the store and the context under the given
- * namespace.
- *
- * A reference could be either a single path or a namespace followed by a path,
- * separated by two colons, i.e, `namespace::path.to.prop`. If the reference
- * contains a namespace, that namespace overrides the one passed as argument.
- *
- * @param string $reference Reference value.
- * @param string $ns Inherited namespace.
- * @param array $context Context data.
- * @return mixed Resolved value.
- */
-function gutenberg_interactivity_evaluate_reference( $reference, $ns, array $context = array() ) {
- // Extract the namespace from the reference (if present).
- list( $ns, $path ) = WP_Directive_Processor::parse_attribute_value( $reference, $ns );
-
- $store = array(
- 'state' => WP_Interactivity_Initial_State::get_state( $ns ),
- 'context' => $context[ $ns ] ?? array(),
- );
-
- /*
- * Checks first if the directive path is preceded by a negator operator (!),
- * indicating that the value obtained from the Interactivity Store (or the
- * passed context) using the subsequent path should be negated.
- */
- $should_negate_value = '!' === $path[0];
- $path = $should_negate_value ? substr( $path, 1 ) : $path;
- $path_segments = explode( '.', $path );
- $current = $store;
- foreach ( $path_segments as $p ) {
- if ( isset( $current[ $p ] ) ) {
- $current = $current[ $p ];
- } else {
- return null;
- }
- }
-
- /*
- * Checks if $current is an anonymous function or an arrow function, and if
- * so, call it passing the store. Other types of callables are ignored on
- * purpose, as arbitrary strings or arrays could be wrongly evaluated as
- * "callables".
- *
- * E.g., "file" is an string and a "callable" (the "file" function exists).
- */
- if ( $current instanceof Closure ) {
- /*
- * TODO: Figure out a way to implement derived state without having to
- * pass the store as argument:
- *
- * $current = call_user_func( $current );
- */
- }
-
- // Returns the opposite if it has a negator operator (!).
- return $should_negate_value ? ! $current : $current;
-}
diff --git a/lib/experimental/interactivity-api/directives/wp-bind.php b/lib/experimental/interactivity-api/directives/wp-bind.php
deleted file mode 100644
index 57d2e5deb23ab4..00000000000000
--- a/lib/experimental/interactivity-api/directives/wp-bind.php
+++ /dev/null
@@ -1,33 +0,0 @@
-is_tag_closer() ) {
- return;
- }
-
- $prefixed_attributes = $tags->get_attribute_names_with_prefix( 'data-wp-bind--' );
-
- foreach ( $prefixed_attributes as $attr ) {
- list( , $bound_attr ) = WP_Directive_Processor::parse_attribute_name( $attr );
- if ( empty( $bound_attr ) ) {
- continue;
- }
-
- $reference = $tags->get_attribute( $attr );
- $value = gutenberg_interactivity_evaluate_reference( $reference, $ns, $context->get_context() );
- $tags->set_attribute( $bound_attr, $value );
- }
-}
diff --git a/lib/experimental/interactivity-api/directives/wp-class.php b/lib/experimental/interactivity-api/directives/wp-class.php
deleted file mode 100644
index ef91835be86fc1..00000000000000
--- a/lib/experimental/interactivity-api/directives/wp-class.php
+++ /dev/null
@@ -1,37 +0,0 @@
-is_tag_closer() ) {
- return;
- }
-
- $prefixed_attributes = $tags->get_attribute_names_with_prefix( 'data-wp-class--' );
-
- foreach ( $prefixed_attributes as $attr ) {
- list( , $class_name ) = WP_Directive_Processor::parse_attribute_name( $attr );
- if ( empty( $class_name ) ) {
- continue;
- }
-
- $reference = $tags->get_attribute( $attr );
- $add_class = gutenberg_interactivity_evaluate_reference( $reference, $ns, $context->get_context() );
- if ( $add_class ) {
- $tags->add_class( $class_name );
- } else {
- $tags->remove_class( $class_name );
- }
- }
-}
diff --git a/lib/experimental/interactivity-api/directives/wp-context.php b/lib/experimental/interactivity-api/directives/wp-context.php
deleted file mode 100644
index b41b47c86c78c3..00000000000000
--- a/lib/experimental/interactivity-api/directives/wp-context.php
+++ /dev/null
@@ -1,30 +0,0 @@
-is_tag_closer() ) {
- $context->rewind_context();
- return;
- }
-
- $attr_value = $tags->get_attribute( 'data-wp-context' );
-
- //Separate namespace and value from the context directive attribute.
- list( $ns, $data ) = is_string( $attr_value ) && ! empty( $attr_value )
- ? WP_Directive_Processor::parse_attribute_value( $attr_value, $ns )
- : array( $ns, null );
-
- // Add parsed data to the context under the corresponding namespace.
- $context->set_context( array( $ns => is_array( $data ) ? $data : array() ) );
-}
diff --git a/lib/experimental/interactivity-api/directives/wp-interactive.php b/lib/experimental/interactivity-api/directives/wp-interactive.php
deleted file mode 100644
index 9f3471a8b4e6a9..00000000000000
--- a/lib/experimental/interactivity-api/directives/wp-interactive.php
+++ /dev/null
@@ -1,44 +0,0 @@
-is_tag_closer() ) {
- array_pop( $ns_stack );
- return;
- }
-
- /*
- * Decode the data-wp-interactive attribute. In the case it is not a valid
- * JSON string, NULL is stored in `$island_data`.
- */
- $island = $tags->get_attribute( 'data-wp-interactive' );
- $island_data = is_string( $island ) && ! empty( $island )
- ? json_decode( $island, true )
- : null;
-
- /*
- * Push the newly defined namespace, or the current one if the island
- * definition was invalid or does not contain a namespace.
- *
- * This is done because the function pops out the current namespace from the
- * stack whenever it finds an island's closing tag, independently of whether
- * the island definition was correct or it contained a valid namespace.
- */
- $ns_stack[] = isset( $island_data ) && $island_data['namespace']
- ? $island_data['namespace']
- : $ns;
-}
diff --git a/lib/experimental/interactivity-api/directives/wp-style.php b/lib/experimental/interactivity-api/directives/wp-style.php
deleted file mode 100644
index 16432e57282606..00000000000000
--- a/lib/experimental/interactivity-api/directives/wp-style.php
+++ /dev/null
@@ -1,73 +0,0 @@
-is_tag_closer() ) {
- return;
- }
-
- $prefixed_attributes = $tags->get_attribute_names_with_prefix( 'data-wp-style--' );
-
- foreach ( $prefixed_attributes as $attr ) {
- list( , $style_name ) = WP_Directive_Processor::parse_attribute_name( $attr );
- if ( empty( $style_name ) ) {
- continue;
- }
-
- $reference = $tags->get_attribute( $attr );
- $style_value = gutenberg_interactivity_evaluate_reference( $reference, $ns, $context->get_context() );
- if ( $style_value ) {
- $style_attr = $tags->get_attribute( 'style' ) ?? '';
- $style_attr = gutenberg_interactivity_set_style( $style_attr, $style_name, $style_value );
- $tags->set_attribute( 'style', $style_attr );
- } else {
- // TODO: Do we want to unset styles if they're null?
- }
- }
-}
-
-/**
- * Set style.
- *
- * @param string $style Existing style to amend.
- * @param string $name Style property name.
- * @param string $value Style property value.
- * @return string Amended styles.
- */
-function gutenberg_interactivity_set_style( $style, $name, $value ) {
- $style_assignments = explode( ';', $style );
- $modified = false;
- foreach ( $style_assignments as $style_assignment ) {
- list( $style_name ) = explode( ':', $style_assignment );
- if ( trim( $style_name ) === $name ) {
- // TODO: Retain surrounding whitespace from $style_value, if any.
- $style_assignment = $style_name . ': ' . $value;
- $modified = true;
- break;
- }
- }
-
- if ( ! $modified ) {
- $new_style_assignment = $name . ': ' . $value;
- // If the last element is empty or whitespace-only, we insert
- // the new "key: value" pair before it.
- if ( empty( trim( end( $style_assignments ) ) ) ) {
- array_splice( $style_assignments, - 1, 0, $new_style_assignment );
- } else {
- array_push( $style_assignments, $new_style_assignment );
- }
- }
- return implode( ';', $style_assignments );
-}
diff --git a/lib/experimental/interactivity-api/directives/wp-text.php b/lib/experimental/interactivity-api/directives/wp-text.php
deleted file mode 100644
index c4c5bb27a31e10..00000000000000
--- a/lib/experimental/interactivity-api/directives/wp-text.php
+++ /dev/null
@@ -1,28 +0,0 @@
-is_tag_closer() ) {
- return;
- }
-
- $value = $tags->get_attribute( 'data-wp-text' );
- if ( null === $value ) {
- return;
- }
-
- $text = gutenberg_interactivity_evaluate_reference( $value, $ns, $context->get_context() );
- $tags->set_inner_html( esc_html( $text ) );
-}
diff --git a/lib/experimental/interactivity-api/initial-state.php b/lib/experimental/interactivity-api/initial-state.php
deleted file mode 100644
index a38d0da631f3c4..00000000000000
--- a/lib/experimental/interactivity-api/initial-state.php
+++ /dev/null
@@ -1,29 +0,0 @@
- $dependency['id'],
- 'type' => isset( $dependency['type'] ) && 'dynamic' === $dependency['type'] ? 'dynamic' : 'static',
- );
- } elseif ( is_string( $dependency ) ) {
- $deps[] = array(
- 'id' => $dependency,
- 'type' => 'static',
- );
- }
- }
-
- self::$registered[ $module_identifier ] = array(
- 'src' => $src,
- 'version' => $version,
- 'enqueued' => in_array( $module_identifier, self::$enqueued_modules_before_register, true ),
- 'dependencies' => $deps,
- );
- }
- }
-
- /**
- * Marks the module to be enqueued in the page.
- *
- * @param string $module_identifier The identifier of the module.
- */
- public static function enqueue( $module_identifier ) {
- if ( isset( self::$registered[ $module_identifier ] ) ) {
- self::$registered[ $module_identifier ]['enqueued'] = true;
- } elseif ( ! in_array( $module_identifier, self::$enqueued_modules_before_register, true ) ) {
- self::$enqueued_modules_before_register[] = $module_identifier;
- }
- }
-
- /**
- * Unmarks the module so it is no longer enqueued in the page.
- *
- * @param string $module_identifier The identifier of the module.
- */
- public static function dequeue( $module_identifier ) {
- if ( isset( self::$registered[ $module_identifier ] ) ) {
- self::$registered[ $module_identifier ]['enqueued'] = false;
- }
- $key = array_search( $module_identifier, self::$enqueued_modules_before_register, true );
- if ( false !== $key ) {
- array_splice( self::$enqueued_modules_before_register, $key, 1 );
- }
- }
-
- /**
- * Returns the import map array.
- *
- * @return array Array with an 'imports' key mapping to an array of module identifiers and their respective source URLs, including the version query.
- */
- public static function get_import_map() {
- $imports = array();
- foreach ( self::get_dependencies( array_keys( self::get_enqueued() ) ) as $module_identifier => $module ) {
- $imports[ $module_identifier ] = $module['src'] . self::get_version_query_string( $module['version'] );
- }
- return array( 'imports' => $imports );
- }
-
- /**
- * Prints the import map using a script tag with an type="importmap" attribute.
- */
- public static function print_import_map() {
- $import_map = self::get_import_map();
- if ( ! empty( $import_map['imports'] ) ) {
- echo '';
- }
- }
-
- /**
- * Prints all the enqueued modules using '
- );
- }
-
- /**
- * Gets the version of a module.
- *
- * If SCRIPT_DEBUG is true, the version is the current timestamp. If $version
- * is set to false, the version number is the currently installed WordPress
- * version. If $version is set to null, no version is added.
- *
- * @param array $version The version of the module.
- * @return string A string presenting the version.
- */
- private static function get_version_query_string( $version ) {
- if ( defined( 'SCRIPT_DEBUG ' ) && SCRIPT_DEBUG ) {
- return '?ver=' . time();
- } elseif ( false === $version ) {
- return '?ver=' . get_bloginfo( 'version' );
- } elseif ( null !== $version ) {
- return '?ver=' . $version;
- }
- return '';
- }
-
- /**
- * Retrieves an array of enqueued modules.
- *
- * @return array Array of modules keyed by module identifier.
- */
- private static function get_enqueued() {
- $enqueued = array();
- foreach ( self::$registered as $module_identifier => $module ) {
- if ( true === $module['enqueued'] ) {
- $enqueued[ $module_identifier ] = $module;
- }
- }
- return $enqueued;
- }
-
- /**
- * Retrieves all the dependencies for given modules depending on type.
- *
- * This method is recursive to also retrieve dependencies of the dependencies.
- * It will consolidate an array containing unique dependencies based on the
- * requested types ('static' or 'dynamic').
- *
- * @param array $module_identifiers The identifiers of the modules for which to gather dependencies.
- * @param array $types Optional. Types of dependencies to retrieve: 'static', 'dynamic', or both. Default is both.
- * @return array Array of modules keyed by module identifier.
- */
- private static function get_dependencies( $module_identifiers, $types = array( 'static', 'dynamic' ) ) {
- return array_reduce(
- $module_identifiers,
- function ( $dependency_modules, $module_identifier ) use ( $types ) {
- $dependencies = array();
- foreach ( self::$registered[ $module_identifier ]['dependencies'] as $dependency ) {
- if (
- in_array( $dependency['type'], $types, true ) &&
- isset( self::$registered[ $dependency['id'] ] ) &&
- ! isset( $dependency_modules[ $dependency['id'] ] )
- ) {
- $dependencies[ $dependency['id'] ] = self::$registered[ $dependency['id'] ];
- }
- }
- return array_merge( $dependency_modules, $dependencies, self::get_dependencies( array_keys( $dependencies ), $types ) );
- },
- array()
- );
- }
-}
-
-/**
- * Registers the module if no module with that module identifier has already
- * been registered.
- *
- * @param string $module_identifier The identifier of the module. Should be unique. It will be used in the final import map.
- * @param string $src Full URL of the module, or path of the script relative to the WordPress root directory.
- * @param array $dependencies Optional. An array of module identifiers of the dependencies of this module. The dependencies can be strings or arrays. If they are arrays, they need an `id` key with the module identifier, and can contain a `type` key with either `static` or `dynamic`. By default, dependencies that don't contain a type are considered static.
- * @param string|false|null $version Optional. String specifying module version number. Defaults to false. It is added to the URL as a query string for cache busting purposes. If SCRIPT_DEBUG is true, the version is the current timestamp. If $version is set to false, the version number is the currently installed WordPress version. If $version is set to null, no version is added.
- */
-function gutenberg_register_module( $module_identifier, $src, $dependencies = array(), $version = false ) {
- Gutenberg_Modules::register( $module_identifier, $src, $dependencies, $version );
-}
-
-/**
- * Marks the module to be enqueued in the page.
- *
- * @param string $module_identifier The identifier of the module.
- */
-function gutenberg_enqueue_module( $module_identifier ) {
- Gutenberg_Modules::enqueue( $module_identifier );
-}
-
-/**
- * Unmarks the module so it is not longer enqueued in the page.
- *
- * @param string $module_identifier The identifier of the module.
- */
-function gutenberg_dequeue_module( $module_identifier ) {
- Gutenberg_Modules::dequeue( $module_identifier );
-}
-
-$modules_position = wp_is_block_theme() ? 'wp_head' : 'wp_footer';
-// Prints the import map in the head tag in block themes. Otherwise in the footer.
-add_action( $modules_position, array( 'Gutenberg_Modules', 'print_import_map' ) );
-
-// Prints the enqueued modules in the head tag in block themes. Otherwise in the footer.
-add_action( $modules_position, array( 'Gutenberg_Modules', 'print_enqueued_modules' ) );
-
-// Prints the preloaded modules in the head tag in block themes. Otherwise in the footer.
-add_action( $modules_position, array( 'Gutenberg_Modules', 'print_module_preloads' ) );
-
-// Prints the script that loads the import map polyfill in the footer.
-add_action( 'wp_head', array( 'Gutenberg_Modules', 'print_import_map_polyfill' ), 11 );
-
-/**
- * Add module fields from block metadata to WP_Block_Type settings.
- *
- * This filter allows us to register modules from block metadata and attach additional fields to
- * WP_Block_Type instances.
- *
- * @param array $settings Array of determined settings for registering a block type.
- * @param array $metadata Metadata provided for registering a block type.
- */
-function gutenberg_filter_block_type_metadata_settings_register_modules( $settings, $metadata = null ) {
- $module_fields = array(
- 'viewModule' => 'view_module_ids',
- );
- foreach ( $module_fields as $metadata_field_name => $settings_field_name ) {
- if ( ! empty( $settings[ $metadata_field_name ] ) ) {
- $metadata[ $metadata_field_name ] = $settings[ $metadata_field_name ];
- }
- if ( ! empty( $metadata[ $metadata_field_name ] ) ) {
- $modules = $metadata[ $metadata_field_name ];
- $processed_modules = array();
- if ( is_array( $modules ) ) {
- for ( $index = 0; $index < count( $modules ); $index++ ) {
- $processed_modules[] = gutenberg_register_block_module_id(
- $metadata,
- $metadata_field_name,
- $index
- );
- }
- } else {
- $processed_modules[] = gutenberg_register_block_module_id(
- $metadata,
- $metadata_field_name
- );
- }
- $settings[ $settings_field_name ] = $processed_modules;
- }
- }
-
- return $settings;
-}
-
-add_filter( 'block_type_metadata_settings', 'gutenberg_filter_block_type_metadata_settings_register_modules', 10, 2 );
-
-/**
- * Enqueue modules associated with the block.
- *
- * @param string $block_content The block content.
- * @param array $block The full block, including name and attributes.
- * @param WP_Block $instance The block instance.
- */
-function gutenberg_filter_render_block_enqueue_view_modules( $block_content, $parsed_block, $block_instance ) {
- $block_type = $block_instance->block_type;
-
- if ( ! empty( $block_type->view_module_ids ) ) {
- foreach ( $block_type->view_module_ids as $module_id ) {
- gutenberg_enqueue_module( $module_id );
- }
- }
-
- return $block_content;
-}
-
-add_filter( 'render_block', 'gutenberg_filter_render_block_enqueue_view_modules', 10, 3 );
-
-/**
- * Finds a module ID for the selected block metadata field. It detects
- * when a path to file was provided and finds a corresponding asset file
- * with details necessary to register the module under an automatically
- * generated module ID.
- *
- * This is analogous to the `register_block_script_handle` in WordPress Core.
- *
- * @param array $metadata Block metadata.
- * @param string $field_name Field name to pick from metadata.
- * @param int $index Optional. Index of the script to register when multiple items passed.
- * Default 0.
- * @return string Module ID.
- */
-function gutenberg_register_block_module_id( $metadata, $field_name, $index = 0 ) {
- if ( empty( $metadata[ $field_name ] ) ) {
- return false;
- }
-
- $module_id = $metadata[ $field_name ];
- if ( is_array( $module_id ) ) {
- if ( empty( $module_id[ $index ] ) ) {
- return false;
- }
- $module_id = $module_id[ $index ];
- }
-
- $module_path = remove_block_asset_path_prefix( $module_id );
- if ( $module_id === $module_path ) {
- return $module_id;
- }
-
- $path = dirname( $metadata['file'] );
- $module_asset_raw_path = $path . '/' . substr_replace( $module_path, '.asset.php', - strlen( '.js' ) );
- $module_id = gutenberg_generate_block_asset_module_id( $metadata['name'], $field_name, $index );
- $module_asset_path = wp_normalize_path( realpath( $module_asset_raw_path ) );
-
- if ( empty( $module_asset_path ) ) {
- _doing_it_wrong(
- __FUNCTION__,
- sprintf(
- // This string is from WordPress Core. See `register_block_script_handle`.
- // Translators: This is a translation from WordPress Core (default). No need to translate.
- __( 'The asset file (%1$s) for the "%2$s" defined in "%3$s" block definition is missing.', 'default' ),
- $module_asset_raw_path,
- $field_name,
- $metadata['name']
- ),
- '6.5.0'
- );
- return false;
- }
-
- $module_path_norm = wp_normalize_path( realpath( $path . '/' . $module_path ) );
- $module_uri = get_block_asset_url( $module_path_norm );
- $module_asset = require $module_asset_path;
- $module_dependencies = isset( $module_asset['dependencies'] ) ? $module_asset['dependencies'] : array();
-
- gutenberg_register_module(
- $module_id,
- $module_uri,
- $module_dependencies,
- isset( $module_asset['version'] ) ? $module_asset['version'] : false
- );
-
- return $module_id;
-}
-
-/**
- * Generates the module ID for an asset based on the name of the block
- * and the field name provided.
- *
- * This is analogous to the `generate_block_asset_handle` in WordPress Core.
- *
- * @param string $block_name Name of the block.
- * @param string $field_name Name of the metadata field.
- * @param int $index Optional. Index of the asset when multiple items passed.
- * Default 0.
- * @return string Generated module ID for the block's field.
- */
-function gutenberg_generate_block_asset_module_id( $block_name, $field_name, $index = 0 ) {
- if ( str_starts_with( $block_name, 'core/' ) ) {
- $asset_handle = str_replace( 'core/', 'wp-block-', $block_name );
- if ( str_starts_with( $field_name, 'editor' ) ) {
- $asset_handle .= '-editor';
- }
- if ( str_starts_with( $field_name, 'view' ) ) {
- $asset_handle .= '-view';
- }
- if ( $index > 0 ) {
- $asset_handle .= '-' . ( $index + 1 );
- }
- return $asset_handle;
- }
-
- $field_mappings = array(
- 'viewModule' => 'view-module',
- );
- $asset_handle = str_replace( '/', '-', $block_name ) .
- '-' . $field_mappings[ $field_name ];
- if ( $index > 0 ) {
- $asset_handle .= '-' . ( $index + 1 );
- }
- return $asset_handle;
-}
-
-function gutenberg_register_view_module_ids_rest_field() {
- register_rest_field(
- 'block-type',
- 'view_module_ids',
- array(
- 'get_callback' => function ( $item ) {
- $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $item['name'] );
- if ( isset( $block_type->view_module_ids ) ) {
- return $block_type->view_module_ids;
- }
- return array();
- },
- )
- );
-}
-
-add_action( 'rest_api_init', 'gutenberg_register_view_module_ids_rest_field' );
diff --git a/lib/experimental/script-modules.php b/lib/experimental/script-modules.php
new file mode 100644
index 00000000000000..1f481a8098b20c
--- /dev/null
+++ b/lib/experimental/script-modules.php
@@ -0,0 +1,234 @@
+ 'view_module_ids',
+ );
+ foreach ( $module_fields as $metadata_field_name => $settings_field_name ) {
+ if ( ! empty( $settings[ $metadata_field_name ] ) ) {
+ $metadata[ $metadata_field_name ] = $settings[ $metadata_field_name ];
+ }
+ if ( ! empty( $metadata[ $metadata_field_name ] ) ) {
+ $modules = $metadata[ $metadata_field_name ];
+ $processed_modules = array();
+ if ( is_array( $modules ) ) {
+ for ( $index = 0; $index < count( $modules ); $index++ ) {
+ $processed_modules[] = gutenberg_register_block_module_id(
+ $metadata,
+ $metadata_field_name,
+ $index
+ );
+ }
+ } else {
+ $processed_modules[] = gutenberg_register_block_module_id(
+ $metadata,
+ $metadata_field_name
+ );
+ }
+ $settings[ $settings_field_name ] = $processed_modules;
+ }
+ }
+
+ return $settings;
+}
+
+add_filter( 'block_type_metadata_settings', 'gutenberg_filter_block_type_metadata_settings_register_modules', 10, 2 );
+
+/**
+ * Enqueue modules associated with the block.
+ *
+ * @param string $block_content The block content.
+ * @param array $parsed_block The full block, including name and attributes.
+ * @param WP_Block $block_instance The block instance.
+ */
+function gutenberg_filter_render_block_enqueue_view_modules( $block_content, $parsed_block, $block_instance ) {
+ $block_type = $block_instance->block_type;
+
+ if ( ! empty( $block_type->view_module_ids ) ) {
+ foreach ( $block_type->view_module_ids as $module_id ) {
+ wp_enqueue_script_module( $module_id );
+ }
+ }
+
+ return $block_content;
+}
+
+add_filter( 'render_block', 'gutenberg_filter_render_block_enqueue_view_modules', 10, 3 );
+
+/**
+ * Finds a module ID for the selected block metadata field. It detects
+ * when a path to file was provided and finds a corresponding asset file
+ * with details necessary to register the module under an automatically
+ * generated module ID.
+ *
+ * This is analogous to the `register_block_script_handle` in WordPress Core.
+ *
+ * @param array $metadata Block metadata.
+ * @param string $field_name Field name to pick from metadata.
+ * @param int $index Optional. Index of the script to register when multiple items passed.
+ * Default 0.
+ * @return string Module ID.
+ */
+function gutenberg_register_block_module_id( $metadata, $field_name, $index = 0 ) {
+ if ( empty( $metadata[ $field_name ] ) ) {
+ return false;
+ }
+
+ $module_id = $metadata[ $field_name ];
+ if ( is_array( $module_id ) ) {
+ if ( empty( $module_id[ $index ] ) ) {
+ return false;
+ }
+ $module_id = $module_id[ $index ];
+ }
+
+ $module_path = remove_block_asset_path_prefix( $module_id );
+ if ( $module_id === $module_path ) {
+ return $module_id;
+ }
+
+ $path = dirname( $metadata['file'] );
+ $module_asset_raw_path = $path . '/' . substr_replace( $module_path, '.asset.php', - strlen( '.js' ) );
+ $module_id = gutenberg_generate_block_asset_module_id( $metadata['name'], $field_name, $index );
+ $module_asset_path = wp_normalize_path( realpath( $module_asset_raw_path ) );
+
+ if ( empty( $module_asset_path ) ) {
+ _doing_it_wrong(
+ __FUNCTION__,
+ sprintf(
+ // This string is from WordPress Core. See `register_block_script_handle`.
+ // Translators: This is a translation from WordPress Core (default). No need to translate.
+ __( 'The asset file (%1$s) for the "%2$s" defined in "%3$s" block definition is missing.', 'default' ),
+ $module_asset_raw_path,
+ $field_name,
+ $metadata['name']
+ ),
+ '6.5.0'
+ );
+ return false;
+ }
+
+ $module_path_norm = wp_normalize_path( realpath( $path . '/' . $module_path ) );
+ $module_uri = get_block_asset_url( $module_path_norm );
+ $module_asset = require $module_asset_path;
+ $module_dependencies = isset( $module_asset['dependencies'] ) ? $module_asset['dependencies'] : array();
+
+ wp_register_script_module(
+ $module_id,
+ $module_uri,
+ $module_dependencies,
+ isset( $module_asset['version'] ) ? $module_asset['version'] : false
+ );
+
+ return $module_id;
+}
+
+/**
+ * Generates the module ID for an asset based on the name of the block
+ * and the field name provided.
+ *
+ * This is analogous to the `generate_block_asset_handle` in WordPress Core.
+ *
+ * @param string $block_name Name of the block.
+ * @param string $field_name Name of the metadata field.
+ * @param int $index Optional. Index of the asset when multiple items passed.
+ * Default 0.
+ * @return string Generated module ID for the block's field.
+ */
+function gutenberg_generate_block_asset_module_id( $block_name, $field_name, $index = 0 ) {
+ if ( str_starts_with( $block_name, 'core/' ) ) {
+ $asset_handle = str_replace( 'core/', 'wp-block-', $block_name );
+ if ( str_starts_with( $field_name, 'editor' ) ) {
+ $asset_handle .= '-editor';
+ }
+ if ( str_starts_with( $field_name, 'view' ) ) {
+ $asset_handle .= '-view';
+ }
+ if ( $index > 0 ) {
+ $asset_handle .= '-' . ( $index + 1 );
+ }
+ return $asset_handle;
+ }
+
+ $field_mappings = array(
+ 'viewModule' => 'view-module',
+ );
+ $asset_handle = str_replace( '/', '-', $block_name ) .
+ '-' . $field_mappings[ $field_name ];
+ if ( $index > 0 ) {
+ $asset_handle .= '-' . ( $index + 1 );
+ }
+ return $asset_handle;
+}
+
+/**
+ * Registers a REST field for block types to provide view module IDs.
+ *
+ * Adds the `view_module_ids` field to block type objects in the REST API, which
+ * lists the script module IDs for any script modules associated with the
+ * block's viewModule(s) key.
+ *
+ * @since 6.5.0
+ */
+function gutenberg_register_view_module_ids_rest_field() {
+ register_rest_field(
+ 'block-type',
+ 'view_module_ids',
+ array(
+ 'get_callback' => function ( $item ) {
+ $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $item['name'] );
+ if ( isset( $block_type->view_module_ids ) ) {
+ return $block_type->view_module_ids;
+ }
+ return array();
+ },
+ )
+ );
+}
+
+add_action( 'rest_api_init', 'gutenberg_register_view_module_ids_rest_field' );
+
+/**
+ * Registers the module if no module with that module identifier has already
+ * been registered.
+ *
+ * @param string $module_identifier The identifier of the module. Should be unique. It will be used in the final import map.
+ * @param string $src Full URL of the module, or path of the script relative to the WordPress root directory.
+ * @param array $dependencies Optional. An array of module identifiers of the dependencies of this module. The dependencies can be strings or arrays. If they are arrays, they need an `id` key with the module identifier, and can contain an `import` key with either `static` or `dynamic`. By default, dependencies that don't contain an import are considered static.
+ * @param string|false|null $version Optional. String specifying module version number. Defaults to false. It is added to the URL as a query string for cache busting purposes. If $version is set to false, the version number is the currently installed WordPress version. If $version is set to null, no version is added.
+ * @deprecated 17.6.0 gutenberg_register_module is deprecated. Please use wp_register_script_module instead.
+ */
+function gutenberg_register_module( $module_identifier, $src = '', $dependencies = array(), $version = false ) {
+ _deprecated_function( __FUNCTION__, 'Gutenberg 17.6.0', 'wp_register_script_module' );
+ wp_script_modules()->register( $module_identifier, $src, $dependencies, $version );
+}
+
+/**
+ * Marks the module to be enqueued in the page.
+ *
+ * @param string $module_identifier The identifier of the module.
+ * @deprecated 17.6.0 gutenberg_enqueue_module is deprecated. Please use wp_enqueue_script_module instead.
+ */
+function gutenberg_enqueue_module( $module_identifier ) {
+ _deprecated_function( __FUNCTION__, 'Gutenberg 17.6.0', 'wp_enqueue_script_module' );
+ wp_script_modules()->enqueue( $module_identifier );
+}
+
+/**
+ * Unmarks the module so it is not longer enqueued in the page.
+ *
+ * @param string $module_identifier The identifier of the module.
+ * @deprecated 17.6.0 gutenberg_dequeue_module is deprecated. Please use wp_dequeue_script_module instead.
+ */
+function gutenberg_dequeue_module( $module_identifier ) {
+ _deprecated_function( __FUNCTION__, 'Gutenberg 17.6.0', 'wp_dequeue_script_module' );
+ wp_script_modules()->dequeue( $module_identifier );
+}
diff --git a/lib/experiments-page.php b/lib/experiments-page.php
index 8af1eb82c6bed0..bccbed2195958b 100644
--- a/lib/experiments-page.php
+++ b/lib/experiments-page.php
@@ -126,30 +126,6 @@ function gutenberg_initialize_experiments_settings() {
)
);
- add_settings_field(
- 'gutenberg-custom-fields',
- __( 'Block Bindings & Custom Fields', 'gutenberg' ),
- 'gutenberg_display_experiment_field',
- 'gutenberg-experiments',
- 'gutenberg_experiments_section',
- array(
- 'label' => __( 'Test connecting block attributes to different sources like custom fields', 'gutenberg' ),
- 'id' => 'gutenberg-block-bindings',
- )
- );
-
- add_settings_field(
- 'gutenberg-pattern-partial-syncing',
- __( 'Pattern overrides', 'gutenberg' ),
- 'gutenberg_display_experiment_field',
- 'gutenberg-experiments',
- 'gutenberg_experiments_section',
- array(
- 'label' => __( 'Test overrides in synced patterns', 'gutenberg' ),
- 'id' => 'gutenberg-pattern-partial-syncing',
- )
- );
-
register_setting(
'gutenberg-experiments',
'gutenberg-experiments'
diff --git a/lib/load.php b/lib/load.php
index 8c9c8532d573ca..4b2b4d5d8b0db8 100644
--- a/lib/load.php
+++ b/lib/load.php
@@ -106,8 +106,17 @@ function gutenberg_is_experiment_enabled( $name ) {
// WordPress 6.5 compat.
require __DIR__ . '/compat/wordpress-6.5/blocks.php';
require __DIR__ . '/compat/wordpress-6.5/block-patterns.php';
-require __DIR__ . '/compat/wordpress-6.5/class-wp-navigation-block-renderer.php';
require __DIR__ . '/compat/wordpress-6.5/kses.php';
+require __DIR__ . '/compat/wordpress-6.5/interactivity-api/class-wp-interactivity-api.php';
+require __DIR__ . '/compat/wordpress-6.5/interactivity-api/class-wp-interactivity-api-directives-processor.php';
+require __DIR__ . '/compat/wordpress-6.5/interactivity-api/interactivity-api.php';
+require __DIR__ . '/compat/wordpress-6.5/class-wp-script-modules.php';
+require __DIR__ . '/compat/wordpress-6.5/scripts-modules.php';
+require __DIR__ . '/compat/wordpress-6.5/block-bindings/class-wp-block-bindings.php';
+require __DIR__ . '/compat/wordpress-6.5/block-bindings/block-bindings.php';
+require __DIR__ . '/compat/wordpress-6.5/block-bindings/sources/post-meta.php';
+require __DIR__ . '/compat/wordpress-6.5/block-bindings/sources/pattern.php';
+
// Experimental features.
require __DIR__ . '/experimental/block-editor-settings-mobile.php';
@@ -116,26 +125,12 @@ function gutenberg_is_experiment_enabled( $name ) {
require __DIR__ . '/experimental/kses.php';
require __DIR__ . '/experimental/l10n.php';
require __DIR__ . '/experimental/synchronization.php';
+require __DIR__ . '/experimental/script-modules.php';
if ( gutenberg_is_experiment_enabled( 'gutenberg-no-tinymce' ) ) {
require __DIR__ . '/experimental/disable-tinymce.php';
}
-require __DIR__ . '/experimental/interactivity-api/class-wp-interactivity-initial-state.php';
-require __DIR__ . '/experimental/interactivity-api/initial-state.php';
-require __DIR__ . '/experimental/interactivity-api/modules.php';
-require __DIR__ . '/experimental/interactivity-api/class-wp-directive-processor.php';
-require __DIR__ . '/experimental/interactivity-api/class-wp-directive-context.php';
-require __DIR__ . '/experimental/interactivity-api/directive-processing.php';
-require __DIR__ . '/experimental/interactivity-api/directives/wp-bind.php';
-require __DIR__ . '/experimental/interactivity-api/directives/wp-context.php';
-require __DIR__ . '/experimental/interactivity-api/directives/wp-class.php';
-require __DIR__ . '/experimental/interactivity-api/directives/wp-style.php';
-require __DIR__ . '/experimental/interactivity-api/directives/wp-text.php';
-require __DIR__ . '/experimental/interactivity-api/directives/wp-interactive.php';
-
-require __DIR__ . '/experimental/modules/class-gutenberg-modules.php';
-
// Fonts API / Font Face.
remove_action( 'plugins_loaded', '_wp_theme_json_webfonts_handler' ); // Turns off WordPress 6.0's stopgap handler.
@@ -143,10 +138,9 @@ function gutenberg_is_experiment_enabled( $name ) {
require __DIR__ . '/experimental/fonts/font-library/class-wp-font-collection.php';
require __DIR__ . '/experimental/fonts/font-library/class-wp-font-library.php';
require __DIR__ . '/experimental/fonts/font-library/class-wp-font-family-utils.php';
-require __DIR__ . '/experimental/fonts/font-library/class-wp-font-family.php';
require __DIR__ . '/experimental/fonts/font-library/class-wp-rest-font-families-controller.php';
+require __DIR__ . '/experimental/fonts/font-library/class-wp-rest-font-faces-controller.php';
require __DIR__ . '/experimental/fonts/font-library/class-wp-rest-font-collections-controller.php';
-require __DIR__ . '/experimental/fonts/font-library/class-wp-rest-autosave-font-families-controller.php';
require __DIR__ . '/experimental/fonts/font-library/font-library.php';
// Load the Font Face and Font Face Resolver, if not already loaded by WordPress Core.
diff --git a/package-lock.json b/package-lock.json
index 059b710d89811c..48abf7ff587d0a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -165,14 +165,14 @@
"babel-plugin-react-native-platform-specific-extensions": "1.1.1",
"babel-plugin-transform-remove-console": "6.9.4",
"benchmark": "2.1.4",
- "browserslist": "4.21.10",
+ "browserslist": "4.22.2",
"caniuse-lite": "1.0.30001579",
"chalk": "4.1.1",
"change-case": "4.1.2",
"commander": "9.2.0",
"concurrently": "3.5.0",
"copy-webpack-plugin": "10.2.0",
- "core-js-builder": "3.31.0",
+ "core-js-builder": "3.35.1",
"cross-env": "3.2.4",
"css-loader": "6.2.0",
"cssnano": "6.0.1",
@@ -19623,28 +19623,22 @@
"dev": true
},
"node_modules/assert": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz",
- "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==",
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.1.tgz",
+ "integrity": "sha512-zzw1uCAgLbsKwBfFc8CX78DDg+xZeBksSO3vwVIDDN5i94eOrPsSSyiVhmsSABFDM/OcpE2aagCat9dnWQLG1A==",
"dev": true,
"dependencies": {
- "object-assign": "^4.1.1",
- "util": "0.10.3"
+ "object.assign": "^4.1.4",
+ "util": "^0.10.4"
}
},
- "node_modules/assert/node_modules/inherits": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz",
- "integrity": "sha512-8nWq2nLTAwd02jTqJExUYFSD/fKq6VH9Y/oG2accc/kdI0V98Bag8d5a4gi3XHz73rDWa2PvTtvcWYquKqSENA==",
- "dev": true
- },
"node_modules/assert/node_modules/util": {
- "version": "0.10.3",
- "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
- "integrity": "sha512-5KiHfsmkqacuKjkRkdV7SsfDJ2EGiPsK92s2MhNSY0craxjTdKTtqKsJaCWp4LW33ZZ0OPUv1WO/TFvNQRiQxQ==",
+ "version": "0.10.4",
+ "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz",
+ "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==",
"dev": true,
"dependencies": {
- "inherits": "2.0.1"
+ "inherits": "2.0.3"
}
},
"node_modules/assign-symbols": {
@@ -19686,10 +19680,16 @@
"integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ=="
},
"node_modules/async-each": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz",
- "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==",
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.6.tgz",
+ "integrity": "sha512-c646jH1avxr+aVpndVMeAfYw7wAa6idufrlN3LPA4PmKS0QEGp6PIC9nwz0WQkkvBGAMEki3pFdtxaF39J9vvg==",
"dev": true,
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://paulmillr.com/funding/"
+ }
+ ],
"optional": true
},
"node_modules/async-limiter": {
@@ -20801,20 +20801,23 @@
}
},
"node_modules/browserify-sign": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.1.tgz",
- "integrity": "sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==",
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.2.tgz",
+ "integrity": "sha512-1rudGyeYY42Dk6texmv7c4VcQ0EsvVbLwZkA+AQB7SxvXxmcD93jcHie8bzecJ+ChDlmAm2Qyu0+Ccg5uhZXCg==",
"dev": true,
"dependencies": {
- "bn.js": "^5.1.1",
- "browserify-rsa": "^4.0.1",
+ "bn.js": "^5.2.1",
+ "browserify-rsa": "^4.1.0",
"create-hash": "^1.2.0",
"create-hmac": "^1.1.7",
- "elliptic": "^6.5.3",
+ "elliptic": "^6.5.4",
"inherits": "^2.0.4",
- "parse-asn1": "^5.1.5",
- "readable-stream": "^3.6.0",
- "safe-buffer": "^5.2.0"
+ "parse-asn1": "^5.1.6",
+ "readable-stream": "^3.6.2",
+ "safe-buffer": "^5.2.1"
+ },
+ "engines": {
+ "node": ">= 4"
}
},
"node_modules/browserify-sign/node_modules/inherits": {
@@ -20867,9 +20870,9 @@
}
},
"node_modules/browserslist": {
- "version": "4.21.10",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz",
- "integrity": "sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==",
+ "version": "4.22.2",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz",
+ "integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==",
"funding": [
{
"type": "opencollective",
@@ -20885,10 +20888,10 @@
}
],
"dependencies": {
- "caniuse-lite": "^1.0.30001517",
- "electron-to-chromium": "^1.4.477",
- "node-releases": "^2.0.13",
- "update-browserslist-db": "^1.0.11"
+ "caniuse-lite": "^1.0.30001565",
+ "electron-to-chromium": "^1.4.601",
+ "node-releases": "^2.0.14",
+ "update-browserslist-db": "^1.0.13"
},
"bin": {
"browserslist": "cli.js"
@@ -23114,9 +23117,9 @@
}
},
"node_modules/core-js": {
- "version": "3.31.0",
- "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.31.0.tgz",
- "integrity": "sha512-NIp2TQSGfR6ba5aalZD+ZQ1fSxGhDo/s1w0nx3RYzf2pnJxt7YynxFlFScP6eV7+GZsKO95NSjGxyJsU3DZgeQ==",
+ "version": "3.35.1",
+ "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.35.1.tgz",
+ "integrity": "sha512-IgdsbxNyMskrTFxa9lWHyMwAJU5gXOPP+1yO+K59d50VLVAIDAbs7gIv705KzALModfK3ZrSZTPNpC0PQgIZuw==",
"hasInstallScript": true,
"funding": {
"type": "opencollective",
@@ -23124,15 +23127,15 @@
}
},
"node_modules/core-js-builder": {
- "version": "3.31.0",
- "resolved": "https://registry.npmjs.org/core-js-builder/-/core-js-builder-3.31.0.tgz",
- "integrity": "sha512-qx8vgRM3U4+IjlMqNQl7Vj53ectTm1FpzJ+nJSQuT865StCXvusxCO+HuASWIKlkoc+96AfnFa2MEdqvGep9nA==",
+ "version": "3.35.1",
+ "resolved": "https://registry.npmjs.org/core-js-builder/-/core-js-builder-3.35.1.tgz",
+ "integrity": "sha512-CfIvg+khWyS7ElbRXhuQH9QVUQxRDFl8AUwP08BxubJmYtRZuqwjwvc/DVnwRXQ6sqg8ghfNRgnUDWzwNhU/Rw==",
"dev": true,
"dependencies": {
- "core-js": "3.31.0",
- "core-js-compat": "3.31.0",
+ "core-js": "3.35.1",
+ "core-js-compat": "3.35.1",
"mkdirp": ">=0.5.5 <1",
- "webpack": ">=4.46.0 <5"
+ "webpack": ">=4.47.0 <5"
},
"engines": {
"node": ">=8.9.0"
@@ -23327,9 +23330,9 @@
}
},
"node_modules/core-js-builder/node_modules/webpack": {
- "version": "4.46.0",
- "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.46.0.tgz",
- "integrity": "sha512-6jJuJjg8znb/xRItk7bkT0+Q7AHCYjjFnvKIWQPkNIOyRqoCGvkOs0ipeQzrqz4l5FtN5ZI/ukEHroeX/o1/5Q==",
+ "version": "4.47.0",
+ "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.47.0.tgz",
+ "integrity": "sha512-td7fYwgLSrky3fI1EuU5cneU4+pbH6GgOfuKNS1tNPcfdGinGELAqsb/BP4nnvZyKSG2i/xFGU7+n2PvZA8HJQ==",
"dev": true,
"dependencies": {
"@webassemblyjs/ast": "1.9.0",
@@ -23386,11 +23389,11 @@
}
},
"node_modules/core-js-compat": {
- "version": "3.31.0",
- "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.31.0.tgz",
- "integrity": "sha512-hM7YCu1cU6Opx7MXNu0NuumM0ezNeAeRKadixyiQELWY3vT3De9S4J5ZBMraWV2vZnrE1Cirl0GtFtDtMUXzPw==",
+ "version": "3.35.1",
+ "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.35.1.tgz",
+ "integrity": "sha512-sftHa5qUJY3rs9Zht1WEnmkvXputCyDBczPnr7QDgL8n3qrF3CMXY4VPSYtOLLiOUJcah2WNXREd48iOl6mQIw==",
"dependencies": {
- "browserslist": "^4.21.5"
+ "browserslist": "^4.22.2"
},
"funding": {
"type": "opencollective",
@@ -23983,9 +23986,9 @@
}
},
"node_modules/cyclist": {
- "version": "0.2.2",
- "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-0.2.2.tgz",
- "integrity": "sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=",
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.2.tgz",
+ "integrity": "sha512-0sVXIohTfLqVIW3kb/0n6IiWF3Ifj5nm2XaSrLq2DI6fKIGa2fYAZdk917rUneaeLVpYfFcyXE2ft0fe3remsA==",
"dev": true
},
"node_modules/damerau-levenshtein": {
@@ -24891,9 +24894,9 @@
}
},
"node_modules/des.js": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz",
- "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz",
+ "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==",
"dev": true,
"dependencies": {
"inherits": "^2.0.1",
@@ -25559,9 +25562,9 @@
}
},
"node_modules/electron-to-chromium": {
- "version": "1.4.525",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.525.tgz",
- "integrity": "sha512-GIZ620hDK4YmIqAWkscG4W6RwY6gOx1y5J6f4JUQwctiJrqH2oxZYU4mXHi35oV32tr630UcepBzSBGJ/WYcZA=="
+ "version": "1.4.643",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.643.tgz",
+ "integrity": "sha512-QHscvvS7gt155PtoRC0dR2ilhL8E9LHhfTQEq1uD5AL0524rBLAwpAREFH06f87/e45B9XkR6Ki5dbhbCsVEIg=="
},
"node_modules/elegant-spinner": {
"version": "1.0.1",
@@ -27691,9 +27694,10 @@
"dev": true
},
"node_modules/figgy-pudding": {
- "version": "3.5.1",
- "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz",
- "integrity": "sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w==",
+ "version": "3.5.2",
+ "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz",
+ "integrity": "sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==",
+ "deprecated": "This module is no longer supported.",
"dev": true
},
"node_modules/figures": {
@@ -28116,13 +28120,13 @@
}
},
"node_modules/flush-write-stream": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.0.3.tgz",
- "integrity": "sha512-calZMC10u0FMUqoiunI2AiGIIUtUIvifNwkHhNupZH4cbNnW1Itkoh/Nf5HFYmDrwWPjrUxpkZT0KhuCq0jmGw==",
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz",
+ "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==",
"dev": true,
"dependencies": {
- "inherits": "^2.0.1",
- "readable-stream": "^2.0.4"
+ "inherits": "^2.0.3",
+ "readable-stream": "^2.3.6"
}
},
"node_modules/fn.name": {
@@ -39413,9 +39417,9 @@
"dev": true
},
"node_modules/nan": {
- "version": "2.14.2",
- "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz",
- "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==",
+ "version": "2.18.0",
+ "resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz",
+ "integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==",
"dev": true,
"optional": true
},
@@ -39736,9 +39740,9 @@
"dev": true
},
"node_modules/node-releases": {
- "version": "2.0.13",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz",
- "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ=="
+ "version": "2.0.14",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
+ "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw=="
},
"node_modules/node-stream-zip": {
"version": "1.15.0",
@@ -42114,12 +42118,12 @@
"dev": true
},
"node_modules/parallel-transform": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.1.0.tgz",
- "integrity": "sha1-1BDwZbBdojCB/NEPKIVMKb2jOwY=",
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz",
+ "integrity": "sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==",
"dev": true,
"dependencies": {
- "cyclist": "~0.2.2",
+ "cyclist": "^1.0.1",
"inherits": "^2.0.3",
"readable-stream": "^2.1.5"
}
@@ -48096,9 +48100,9 @@
}
},
"node_modules/source-list-map": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.0.tgz",
- "integrity": "sha512-I2UmuJSRr/T8jisiROLU3A3ltr+swpniSmNPI4Ml3ZCX6tVnDsuZzK7F2hl5jTqbZBWCEKlj5HRQiPExXLgE8A==",
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz",
+ "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==",
"dev": true
},
"node_modules/source-map": {
@@ -48527,9 +48531,9 @@
}
},
"node_modules/stream-each": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.2.tgz",
- "integrity": "sha512-mc1dbFhGBxvTM3bIWmAAINbqiuAk9TATcfIQC8P+/+HJefgaiTlMn2dHvkX8qlI12KeYKSQ1Ua9RrIqrn1VPoA==",
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz",
+ "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==",
"dev": true,
"dependencies": {
"end-of-stream": "^1.1.0",
@@ -50950,9 +50954,9 @@
}
},
"node_modules/update-browserslist-db": {
- "version": "1.0.11",
- "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz",
- "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==",
+ "version": "1.0.13",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
+ "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==",
"funding": [
{
"type": "opencollective",
@@ -55702,7 +55706,7 @@
},
"packages/react-native-aztec": {
"name": "@wordpress/react-native-aztec",
- "version": "1.111.0",
+ "version": "1.111.1",
"license": "GPL-2.0-or-later",
"dependencies": {
"@wordpress/element": "file:../element",
@@ -55715,7 +55719,7 @@
},
"packages/react-native-bridge": {
"name": "@wordpress/react-native-bridge",
- "version": "1.111.0",
+ "version": "1.111.1",
"license": "GPL-2.0-or-later",
"dependencies": {
"@wordpress/react-native-aztec": "file:../react-native-aztec"
@@ -55726,7 +55730,7 @@
},
"packages/react-native-editor": {
"name": "@wordpress/react-native-editor",
- "version": "1.111.0",
+ "version": "1.111.1",
"hasInstallScript": true,
"license": "GPL-2.0-or-later",
"dependencies": {
@@ -71933,28 +71937,22 @@
}
},
"assert": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz",
- "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==",
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.1.tgz",
+ "integrity": "sha512-zzw1uCAgLbsKwBfFc8CX78DDg+xZeBksSO3vwVIDDN5i94eOrPsSSyiVhmsSABFDM/OcpE2aagCat9dnWQLG1A==",
"dev": true,
"requires": {
- "object-assign": "^4.1.1",
- "util": "0.10.3"
+ "object.assign": "^4.1.4",
+ "util": "^0.10.4"
},
"dependencies": {
- "inherits": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz",
- "integrity": "sha512-8nWq2nLTAwd02jTqJExUYFSD/fKq6VH9Y/oG2accc/kdI0V98Bag8d5a4gi3XHz73rDWa2PvTtvcWYquKqSENA==",
- "dev": true
- },
"util": {
- "version": "0.10.3",
- "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
- "integrity": "sha512-5KiHfsmkqacuKjkRkdV7SsfDJ2EGiPsK92s2MhNSY0craxjTdKTtqKsJaCWp4LW33ZZ0OPUv1WO/TFvNQRiQxQ==",
+ "version": "0.10.4",
+ "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz",
+ "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==",
"dev": true,
"requires": {
- "inherits": "2.0.1"
+ "inherits": "2.0.3"
}
}
}
@@ -71989,9 +71987,9 @@
"integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ=="
},
"async-each": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz",
- "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==",
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.6.tgz",
+ "integrity": "sha512-c646jH1avxr+aVpndVMeAfYw7wAa6idufrlN3LPA4PmKS0QEGp6PIC9nwz0WQkkvBGAMEki3pFdtxaF39J9vvg==",
"dev": true,
"optional": true
},
@@ -72876,20 +72874,20 @@
}
},
"browserify-sign": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.1.tgz",
- "integrity": "sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==",
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.2.tgz",
+ "integrity": "sha512-1rudGyeYY42Dk6texmv7c4VcQ0EsvVbLwZkA+AQB7SxvXxmcD93jcHie8bzecJ+ChDlmAm2Qyu0+Ccg5uhZXCg==",
"dev": true,
"requires": {
- "bn.js": "^5.1.1",
- "browserify-rsa": "^4.0.1",
+ "bn.js": "^5.2.1",
+ "browserify-rsa": "^4.1.0",
"create-hash": "^1.2.0",
"create-hmac": "^1.1.7",
- "elliptic": "^6.5.3",
+ "elliptic": "^6.5.4",
"inherits": "^2.0.4",
- "parse-asn1": "^5.1.5",
- "readable-stream": "^3.6.0",
- "safe-buffer": "^5.2.0"
+ "parse-asn1": "^5.1.6",
+ "readable-stream": "^3.6.2",
+ "safe-buffer": "^5.2.1"
},
"dependencies": {
"inherits": {
@@ -72927,14 +72925,14 @@
}
},
"browserslist": {
- "version": "4.21.10",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz",
- "integrity": "sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==",
+ "version": "4.22.2",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz",
+ "integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==",
"requires": {
- "caniuse-lite": "^1.0.30001517",
- "electron-to-chromium": "^1.4.477",
- "node-releases": "^2.0.13",
- "update-browserslist-db": "^1.0.11"
+ "caniuse-lite": "^1.0.30001565",
+ "electron-to-chromium": "^1.4.601",
+ "node-releases": "^2.0.14",
+ "update-browserslist-db": "^1.0.13"
}
},
"bser": {
@@ -74672,20 +74670,20 @@
}
},
"core-js": {
- "version": "3.31.0",
- "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.31.0.tgz",
- "integrity": "sha512-NIp2TQSGfR6ba5aalZD+ZQ1fSxGhDo/s1w0nx3RYzf2pnJxt7YynxFlFScP6eV7+GZsKO95NSjGxyJsU3DZgeQ=="
+ "version": "3.35.1",
+ "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.35.1.tgz",
+ "integrity": "sha512-IgdsbxNyMskrTFxa9lWHyMwAJU5gXOPP+1yO+K59d50VLVAIDAbs7gIv705KzALModfK3ZrSZTPNpC0PQgIZuw=="
},
"core-js-builder": {
- "version": "3.31.0",
- "resolved": "https://registry.npmjs.org/core-js-builder/-/core-js-builder-3.31.0.tgz",
- "integrity": "sha512-qx8vgRM3U4+IjlMqNQl7Vj53ectTm1FpzJ+nJSQuT865StCXvusxCO+HuASWIKlkoc+96AfnFa2MEdqvGep9nA==",
+ "version": "3.35.1",
+ "resolved": "https://registry.npmjs.org/core-js-builder/-/core-js-builder-3.35.1.tgz",
+ "integrity": "sha512-CfIvg+khWyS7ElbRXhuQH9QVUQxRDFl8AUwP08BxubJmYtRZuqwjwvc/DVnwRXQ6sqg8ghfNRgnUDWzwNhU/Rw==",
"dev": true,
"requires": {
- "core-js": "3.31.0",
- "core-js-compat": "3.31.0",
+ "core-js": "3.35.1",
+ "core-js-compat": "3.35.1",
"mkdirp": ">=0.5.5 <1",
- "webpack": ">=4.46.0 <5"
+ "webpack": ">=4.47.0 <5"
},
"dependencies": {
"ajv": {
@@ -74837,9 +74835,9 @@
}
},
"webpack": {
- "version": "4.46.0",
- "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.46.0.tgz",
- "integrity": "sha512-6jJuJjg8znb/xRItk7bkT0+Q7AHCYjjFnvKIWQPkNIOyRqoCGvkOs0ipeQzrqz4l5FtN5ZI/ukEHroeX/o1/5Q==",
+ "version": "4.47.0",
+ "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.47.0.tgz",
+ "integrity": "sha512-td7fYwgLSrky3fI1EuU5cneU4+pbH6GgOfuKNS1tNPcfdGinGELAqsb/BP4nnvZyKSG2i/xFGU7+n2PvZA8HJQ==",
"dev": true,
"requires": {
"@webassemblyjs/ast": "1.9.0",
@@ -74880,11 +74878,11 @@
}
},
"core-js-compat": {
- "version": "3.31.0",
- "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.31.0.tgz",
- "integrity": "sha512-hM7YCu1cU6Opx7MXNu0NuumM0ezNeAeRKadixyiQELWY3vT3De9S4J5ZBMraWV2vZnrE1Cirl0GtFtDtMUXzPw==",
+ "version": "3.35.1",
+ "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.35.1.tgz",
+ "integrity": "sha512-sftHa5qUJY3rs9Zht1WEnmkvXputCyDBczPnr7QDgL8n3qrF3CMXY4VPSYtOLLiOUJcah2WNXREd48iOl6mQIw==",
"requires": {
- "browserslist": "^4.21.5"
+ "browserslist": "^4.22.2"
}
},
"core-js-pure": {
@@ -75341,9 +75339,9 @@
}
},
"cyclist": {
- "version": "0.2.2",
- "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-0.2.2.tgz",
- "integrity": "sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=",
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.2.tgz",
+ "integrity": "sha512-0sVXIohTfLqVIW3kb/0n6IiWF3Ifj5nm2XaSrLq2DI6fKIGa2fYAZdk917rUneaeLVpYfFcyXE2ft0fe3remsA==",
"dev": true
},
"damerau-levenshtein": {
@@ -76002,9 +76000,9 @@
"dev": true
},
"des.js": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz",
- "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz",
+ "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==",
"dev": true,
"requires": {
"inherits": "^2.0.1",
@@ -76515,9 +76513,9 @@
}
},
"electron-to-chromium": {
- "version": "1.4.525",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.525.tgz",
- "integrity": "sha512-GIZ620hDK4YmIqAWkscG4W6RwY6gOx1y5J6f4JUQwctiJrqH2oxZYU4mXHi35oV32tr630UcepBzSBGJ/WYcZA=="
+ "version": "1.4.643",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.643.tgz",
+ "integrity": "sha512-QHscvvS7gt155PtoRC0dR2ilhL8E9LHhfTQEq1uD5AL0524rBLAwpAREFH06f87/e45B9XkR6Ki5dbhbCsVEIg=="
},
"elegant-spinner": {
"version": "1.0.1",
@@ -78157,9 +78155,9 @@
"dev": true
},
"figgy-pudding": {
- "version": "3.5.1",
- "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz",
- "integrity": "sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w==",
+ "version": "3.5.2",
+ "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz",
+ "integrity": "sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==",
"dev": true
},
"figures": {
@@ -78496,13 +78494,13 @@
"integrity": "sha512-2hJ5ACYeJCzNtiVULov6pljKOLygy0zddoqSI1fFetM+XRPpRshFdGEijtqlamA1XwyZ+7rhryI6FQFzvtLWUQ=="
},
"flush-write-stream": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.0.3.tgz",
- "integrity": "sha512-calZMC10u0FMUqoiunI2AiGIIUtUIvifNwkHhNupZH4cbNnW1Itkoh/Nf5HFYmDrwWPjrUxpkZT0KhuCq0jmGw==",
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz",
+ "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==",
"dev": true,
"requires": {
- "inherits": "^2.0.1",
- "readable-stream": "^2.0.4"
+ "inherits": "^2.0.3",
+ "readable-stream": "^2.3.6"
}
},
"fn.name": {
@@ -87175,9 +87173,9 @@
"dev": true
},
"nan": {
- "version": "2.14.2",
- "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz",
- "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==",
+ "version": "2.18.0",
+ "resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz",
+ "integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==",
"dev": true,
"optional": true
},
@@ -87424,9 +87422,9 @@
"dev": true
},
"node-releases": {
- "version": "2.0.13",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz",
- "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ=="
+ "version": "2.0.14",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
+ "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw=="
},
"node-stream-zip": {
"version": "1.15.0",
@@ -89220,12 +89218,12 @@
"dev": true
},
"parallel-transform": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.1.0.tgz",
- "integrity": "sha1-1BDwZbBdojCB/NEPKIVMKb2jOwY=",
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz",
+ "integrity": "sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==",
"dev": true,
"requires": {
- "cyclist": "~0.2.2",
+ "cyclist": "^1.0.1",
"inherits": "^2.0.3",
"readable-stream": "^2.1.5"
}
@@ -93765,9 +93763,9 @@
}
},
"source-list-map": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.0.tgz",
- "integrity": "sha512-I2UmuJSRr/T8jisiROLU3A3ltr+swpniSmNPI4Ml3ZCX6tVnDsuZzK7F2hl5jTqbZBWCEKlj5HRQiPExXLgE8A==",
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz",
+ "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==",
"dev": true
},
"source-map": {
@@ -94105,9 +94103,9 @@
}
},
"stream-each": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.2.tgz",
- "integrity": "sha512-mc1dbFhGBxvTM3bIWmAAINbqiuAk9TATcfIQC8P+/+HJefgaiTlMn2dHvkX8qlI12KeYKSQ1Ua9RrIqrn1VPoA==",
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz",
+ "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==",
"dev": true,
"requires": {
"end-of-stream": "^1.1.0",
@@ -95969,9 +95967,9 @@
"optional": true
},
"update-browserslist-db": {
- "version": "1.0.11",
- "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz",
- "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==",
+ "version": "1.0.13",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
+ "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==",
"requires": {
"escalade": "^3.1.1",
"picocolors": "^1.0.0"
diff --git a/package.json b/package.json
index 7d44eda369d687..8550c6ff3f9603 100644
--- a/package.json
+++ b/package.json
@@ -177,14 +177,14 @@
"babel-plugin-react-native-platform-specific-extensions": "1.1.1",
"babel-plugin-transform-remove-console": "6.9.4",
"benchmark": "2.1.4",
- "browserslist": "4.21.10",
+ "browserslist": "4.22.2",
"caniuse-lite": "1.0.30001579",
"chalk": "4.1.1",
"change-case": "4.1.2",
"commander": "9.2.0",
"concurrently": "3.5.0",
"copy-webpack-plugin": "10.2.0",
- "core-js-builder": "3.31.0",
+ "core-js-builder": "3.35.1",
"cross-env": "3.2.4",
"css-loader": "6.2.0",
"cssnano": "6.0.1",
diff --git a/packages/babel-preset-default/bin/index.js b/packages/babel-preset-default/bin/index.js
index a2d86233565138..54c35564d43d74 100755
--- a/packages/babel-preset-default/bin/index.js
+++ b/packages/babel-preset-default/bin/index.js
@@ -8,17 +8,13 @@ const { minify } = require( 'uglify-js' );
const { writeFile } = require( 'fs' ).promises;
builder( {
- modules: [ 'es', 'web' ],
+ modules: [ 'es.', 'web.' ],
exclude: [
- // core-js is extremely conservative in which polyfills to include.
- // Since we don't care about the tiny browser implementation bugs behind its decision
- // to polyfill these features, we forcefully prevent them from being included.
- // @see https://github.com/WordPress/gutenberg/pull/31279
- 'es.promise',
// This is an IE-only feature which we don't use, and don't want to polyfill.
// @see https://github.com/WordPress/gutenberg/pull/49234
'web.immediate',
],
+ summary: { console: { size: true, modules: true } },
targets: require( '@wordpress/browserslist-config' ),
filename: './build/polyfill.js',
} )
diff --git a/packages/block-editor/CHANGELOG.md b/packages/block-editor/CHANGELOG.md
index a66143dd30ac92..ec4793ec970ee7 100644
--- a/packages/block-editor/CHANGELOG.md
+++ b/packages/block-editor/CHANGELOG.md
@@ -2,6 +2,8 @@
## Unreleased
+- Deprecated `__experimentalRecursionProvider` and `__experimentalUseHasRecursion` in favor of their new stable counterparts `RecursionProvider` and `useHasRecursion`.
+
## 12.17.0 (2024-01-10)
## 12.16.0 (2023-12-13)
diff --git a/packages/block-editor/README.md b/packages/block-editor/README.md
index 5917ac235505cb..6fc51d84512122 100644
--- a/packages/block-editor/README.md
+++ b/packages/block-editor/README.md
@@ -707,6 +707,23 @@ _Related_
Private @wordpress/block-editor APIs.
+### RecursionProvider
+
+A React context provider for use with the `useHasRecursion` hook to prevent recursive renders.
+
+Wrap block content with this provider and provide the same `uniqueId` prop as used with `useHasRecursion`.
+
+_Parameters_
+
+- _props_ `Object`:
+- _props.uniqueId_ `*`: Any value that acts as a unique identifier for a block instance.
+- _props.blockName_ `string`: Optional block name.
+- _props.children_ `JSX.Element`: React children.
+
+_Returns_
+
+- `JSX.Element`: A React element.
+
### ReusableBlocksRenameHint
Undocumented declaration.
@@ -941,6 +958,21 @@ _Returns_
- `any`: value
+### useHasRecursion
+
+A React hook for keeping track of blocks previously rendered up in the block tree. Blocks susceptible to recursion can use this hook in their `Edit` function to prevent said recursion.
+
+Use this with the `RecursionProvider` component, using the same `uniqueId` value for both the hook and the provider.
+
+_Parameters_
+
+- _uniqueId_ `*`: Any value that acts as a unique identifier for a block instance.
+- _blockName_ `string`: Optional block name.
+
+_Returns_
+
+- `boolean`: A boolean describing whether the provided id has already been rendered.
+
### useInnerBlocksProps
This hook is used to lightly mark an element as an inner blocks wrapper element. Call this hook and pass the returned props to the element to mark as an inner blocks wrapper, automatically rendering inner blocks as children. If you define a ref for the element, it is important to pass the ref to this hook, which the hook in turn will pass to the component through the props it returns. Optionally, you can also pass any other props through this hook, and they will be merged and returned.
diff --git a/packages/block-editor/src/components/block-inspector/index.js b/packages/block-editor/src/components/block-inspector/index.js
index 9cb3b89a7bc252..a52b8b9a01507f 100644
--- a/packages/block-editor/src/components/block-inspector/index.js
+++ b/packages/block-editor/src/components/block-inspector/index.js
@@ -93,8 +93,7 @@ const BlockInspector = ( { showNoBlockSelectedMessage = true } ) => {
topLevelLockedBlock:
__unstableGetContentLockingParent( _selectedBlockClientId ) ||
( getTemplateLock( _selectedBlockClientId ) === 'contentOnly' ||
- ( _selectedBlockName === 'core/block' &&
- window.__experimentalPatternPartialSyncing )
+ _selectedBlockName === 'core/block'
? _selectedBlockClientId
: undefined ),
};
@@ -307,6 +306,10 @@ const BlockInspectorSingleBlock = ( { clientId, blockName } ) => {
label={ __( 'Background' ) }
/>
+
diff --git a/packages/block-editor/src/components/block-list/content.scss b/packages/block-editor/src/components/block-list/content.scss
index 2b778c0892cfa5..a650710bfb119a 100644
--- a/packages/block-editor/src/components/block-list/content.scss
+++ b/packages/block-editor/src/components/block-list/content.scss
@@ -199,9 +199,6 @@ _::-webkit-full-page-media, _:future, :root .has-multi-selection .block-editor-b
width: 100%;
.components-notice {
- margin-left: 0;
- margin-right: 0;
-
.components-notice__content {
font-size: $default-font-size;
}
diff --git a/packages/block-editor/src/components/block-list/use-in-between-inserter.js b/packages/block-editor/src/components/block-list/use-in-between-inserter.js
index bd323ed057d733..044e5b185a2244 100644
--- a/packages/block-editor/src/components/block-list/use-in-between-inserter.js
+++ b/packages/block-editor/src/components/block-list/use-in-between-inserter.js
@@ -77,8 +77,7 @@ export function useInBetweenInserter() {
if (
getTemplateLock( rootClientId ) ||
getBlockEditingMode( rootClientId ) === 'disabled' ||
- ( getBlockName( rootClientId ) === 'core/block' &&
- window.__experimentalPatternPartialSyncing )
+ getBlockName( rootClientId ) === 'core/block'
) {
return;
}
diff --git a/packages/block-editor/src/components/block-popover/inbetween.js b/packages/block-editor/src/components/block-popover/inbetween.js
index a0175c4d4ae584..afc983b1a4196e 100644
--- a/packages/block-editor/src/components/block-popover/inbetween.js
+++ b/packages/block-editor/src/components/block-popover/inbetween.js
@@ -33,6 +33,8 @@ function BlockPopoverInbetween( {
children,
__unstablePopoverSlot,
__unstableContentRef,
+ operation = 'insert',
+ nearestSide = 'right',
...props
} ) {
// This is a temporary hack to get the inbetween inserter to recompute properly.
@@ -81,7 +83,10 @@ function BlockPopoverInbetween( {
return undefined;
}
- const contextElement = previousElement || nextElement;
+ const contextElement =
+ operation === 'group'
+ ? nextElement || previousElement
+ : previousElement || nextElement;
return {
contextElement,
@@ -98,7 +103,20 @@ function BlockPopoverInbetween( {
let width = 0;
let height = 0;
- if ( isVertical ) {
+ if ( operation === 'group' ) {
+ const targetRect = nextRect || previousRect;
+ top = targetRect.top;
+ // No spacing is likely around blocks in this operation.
+ // So width of the inserter containing rect is set to 0.
+ width = 0;
+ height = targetRect.bottom - targetRect.top;
+ // Popover calculates its distance from mid-block so some
+ // adjustments are needed to make it appear in the right place.
+ left =
+ nearestSide === 'left'
+ ? targetRect.left - 2
+ : targetRect.right - 2;
+ } else if ( isVertical ) {
// vertical
top = previousRect ? previousRect.bottom : nextRect.top;
width = previousRect ? previousRect.width : nextRect.width;
@@ -141,6 +159,8 @@ function BlockPopoverInbetween( {
popoverRecomputeCounter,
isVertical,
isVisible,
+ operation,
+ nearestSide,
] );
const popoverScrollRef = usePopoverScroll( __unstableContentRef );
diff --git a/packages/block-editor/src/components/block-removal-warning-modal/index.js b/packages/block-editor/src/components/block-removal-warning-modal/index.js
index a6de602bcdda81..846883e48e9cd3 100644
--- a/packages/block-editor/src/components/block-removal-warning-modal/index.js
+++ b/packages/block-editor/src/components/block-removal-warning-modal/index.js
@@ -50,11 +50,12 @@ export function BlockRemovalWarningModal( { rules } ) {
{ _n(
- 'Post or page content will not be displayed if you delete this block.',
- 'Post or page content will not be displayed if you delete these blocks.',
+ 'Deleting this block will stop your post or page content from displaying on this template. It is not recommended.',
+ 'Deleting these blocks will stop your post or page content from displaying on this template. It is not recommended.',
blockNamesForPrompt.length
) }
', $tags->get_inner_html() );
- }
-
- public function test_set_inner_html_on_void_element_has_no_effect() {
- $tags = new WP_Directive_Processor( self::HTML );
-
- $tags->next_tag( 'img' );
- $content = $tags->set_inner_html( 'This is the new img content' );
- $this->assertFalse( $content );
- $this->assertSame( self::HTML, $tags->get_updated_html() );
- }
-
- public function test_set_inner_html_sets_content_correctly() {
- $tags = new WP_Directive_Processor( self::HTML );
-
- $tags->next_tag( 'section' );
- $tags->set_inner_html( 'This is the new section content.' );
- $this->assertSame( '
outside
This is the new section content.', $tags->get_updated_html() );
- }
-
- public function test_set_inner_html_updates_bookmarks_correctly() {
- $tags = new WP_Directive_Processor( self::HTML );
-
- $tags->next_tag( 'div' );
- $tags->set_bookmark( 'start' );
- $tags->next_tag( 'img' );
- $this->assertSame( 'IMG', $tags->get_tag() );
- $tags->set_bookmark( 'after' );
- $tags->seek( 'start' );
-
- $tags->set_inner_html( 'This is the new div content.' );
- $this->assertSame( '
This is the new div content.
inside
', $tags->get_updated_html() );
- $tags->seek( 'after' );
- $this->assertSame( 'IMG', $tags->get_tag() );
- }
-
- public function test_set_inner_html_subsequent_updates_on_the_same_tag_work() {
- $tags = new WP_Directive_Processor( self::HTML );
-
- $tags->next_tag( 'section' );
- $tags->set_inner_html( 'This is the new section content.' );
- $tags->set_inner_html( 'This is the even newer section content.' );
- $this->assertSame( '
outside
This is the even newer section content.', $tags->get_updated_html() );
- }
-
- public function test_set_inner_html_followed_by_set_attribute_works() {
- $tags = new WP_Directive_Processor( self::HTML );
-
- $tags->next_tag( 'section' );
- $tags->set_inner_html( 'This is the new section content.' );
- $tags->set_attribute( 'id', 'thesection' );
- $this->assertSame( '
outside
This is the new section content.', $tags->get_updated_html() );
- }
-
- public function test_set_inner_html_preceded_by_set_attribute_works() {
- $tags = new WP_Directive_Processor( self::HTML );
-
- $tags->next_tag( 'section' );
- $tags->set_attribute( 'id', 'thesection' );
- $tags->set_inner_html( 'This is the new section content.' );
- $this->assertSame( '
outside
This is the new section content.', $tags->get_updated_html() );
- }
-
- /**
- * TODO: Review this, how that the code is in Gutenberg.
- */
- public function test_set_inner_html_invalidates_bookmarks_that_point_to_replaced_content() {
- $this->markTestSkipped( "This requires on bookmark invalidation, which is only in GB's WP 6.3 compat layer." );
-
- $tags = new WP_Directive_Processor( self::HTML );
-
- $tags->next_tag( 'section' );
- $tags->set_bookmark( 'start' );
- $tags->next_tag( 'img' );
- $tags->set_bookmark( 'replaced' );
- $tags->seek( 'start' );
-
- $tags->set_inner_html( 'This is the new section content.' );
- $this->assertSame( '