From e92b4515261e6407b97517e8cce994d5c07913c8 Mon Sep 17 00:00:00 2001 From: David Arenas Date: Thu, 4 Jan 2024 11:01:18 +0100 Subject: [PATCH 01/20] Update directive processing to handle namespaces --- .../directive-processing.php | 43 ++++++++++++++----- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/lib/experimental/interactivity-api/directive-processing.php b/lib/experimental/interactivity-api/directive-processing.php index 075d31d577634..c604bffeb549f 100644 --- a/lib/experimental/interactivity-api/directive-processing.php +++ b/lib/experimental/interactivity-api/directive-processing.php @@ -43,12 +43,13 @@ function gutenberg_process_directives_in_root_blocks( $block_content, $block ) { $parsed_blocks = parse_blocks( $block_content ); $context = new WP_Directive_Context(); $processed_content = ''; + $namespace_stack = array(); foreach ( $parsed_blocks as $parsed_block ) { if ( 'core/interactivity-wrapper' === $parsed_block['blockName'] ) { - $processed_content .= gutenberg_process_interactive_block( $parsed_block, $context ); + $processed_content .= gutenberg_process_interactive_block( $parsed_block, $context, $namespace_stack ); } elseif ( 'core/non-interactivity-wrapper' === $parsed_block['blockName'] ) { - $processed_content .= gutenberg_process_non_interactive_block( $parsed_block, $context ); + $processed_content .= gutenberg_process_non_interactive_block( $parsed_block, $context, $namespace_stack ); } else { $processed_content .= $parsed_block['innerHTML']; } @@ -118,10 +119,11 @@ function gutenberg_mark_block_interactivity( $block_content, $block, $block_inst * * @param array $interactive_block The interactive block to process. * @param WP_Directive_Context $context The context to use when processing. + * @param array $namespace_stack Stack of namespackes passed by reference. * * @return string The processed HTML. */ -function gutenberg_process_interactive_block( $interactive_block, $context ) { +function gutenberg_process_interactive_block( $interactive_block, $context, &$namespace_stack ) { $block_index = 0; $content = ''; $interactive_inner_blocks = array(); @@ -137,7 +139,7 @@ function gutenberg_process_interactive_block( $interactive_block, $context ) { } } - return gutenberg_process_interactive_html( $content, $context, $interactive_inner_blocks ); + return gutenberg_process_interactive_html( $content, $context, $interactive_inner_blocks, $namespace_stack ); } /** @@ -147,10 +149,11 @@ function gutenberg_process_interactive_block( $interactive_block, $context ) { * * @param array $non_interactive_block The non-interactive block to process. * @param WP_Directive_Context $context The context to use when processing. + * @param array $namespace_stack Stack of namespackes passed by reference. * * @return string The processed HTML. */ -function gutenberg_process_non_interactive_block( $non_interactive_block, $context ) { +function gutenberg_process_non_interactive_block( $non_interactive_block, $context, &$namespace_stack ) { $block_index = 0; $content = ''; foreach ( $non_interactive_block['innerContent'] as $inner_content ) { @@ -164,9 +167,9 @@ function gutenberg_process_non_interactive_block( $non_interactive_block, $conte $inner_block = $non_interactive_block['innerBlocks'][ $block_index++ ]; if ( 'core/interactivity-wrapper' === $inner_block['blockName'] ) { - $content .= gutenberg_process_interactive_block( $inner_block, $context ); + $content .= gutenberg_process_interactive_block( $inner_block, $context, $namespace_stack ); } elseif ( 'core/non-interactivity-wrapper' === $inner_block['blockName'] ) { - $content .= gutenberg_process_non_interactive_block( $inner_block, $context ); + $content .= gutenberg_process_non_interactive_block( $inner_block, $context, $namespace_stack ); } } } @@ -184,10 +187,11 @@ function gutenberg_process_non_interactive_block( $non_interactive_block, $conte * @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, $inner_blocks = array() ) { +function gutenberg_process_interactive_html( $html, $context, $inner_blocks = array(), &$namespace_stack = array() ) { static $directives = array( 'data-wp-context' => 'gutenberg_interactivity_process_wp_context', 'data-wp-bind' => 'gutenberg_interactivity_process_wp_bind', @@ -207,9 +211,9 @@ function gutenberg_process_interactive_html( $html, $context, $inner_blocks = ar // Processes the inner blocks. if ( str_contains( $tag_name, 'WP-INNER-BLOCKS' ) && ! empty( $inner_blocks ) && ! $tags->is_tag_closer() ) { if ( 'core/interactivity-wrapper' === $inner_blocks[ $inner_blocks_index ]['blockName'] ) { - $inner_processed_blocks[ strtolower( $tag_name ) ] = gutenberg_process_interactive_block( $inner_blocks[ $inner_blocks_index++ ], $context ); + $inner_processed_blocks[ strtolower( $tag_name ) ] = gutenberg_process_interactive_block( $inner_blocks[ $inner_blocks_index++ ], $context, $namespace_stack ); } elseif ( 'core/non-interactivity-wrapper' === $inner_blocks[ $inner_blocks_index ]['blockName'] ) { - $inner_processed_blocks[ strtolower( $tag_name ) ] = gutenberg_process_non_interactive_block( $inner_blocks[ $inner_blocks_index++ ], $context ); + $inner_processed_blocks[ strtolower( $tag_name ) ] = gutenberg_process_non_interactive_block( $inner_blocks[ $inner_blocks_index++ ], $context, $namespace_stack ); } } if ( $tags->is_tag_closer() ) { @@ -269,8 +273,25 @@ function gutenberg_process_interactive_html( $html, $context, $inner_blocks = ar $attributes ); + // Push the new namespace if the current tag is an island. + $island = $tags->get_attribute( 'data-wp-interactive' ); + if ( isset( $island ) && ! $tags->is_tag_closer() ) { + $island_data = json_decode( $island, true ); + $namespace_stack[] = $island_data['namespace']; + } + foreach ( $sorted_attrs as $attribute ) { - call_user_func( $directives[ $attribute ], $tags, $context ); + call_user_func( + $directives[ $attribute ], + $tags, + $context, + end( $namespace_stack ) + ); + } + + // Pop the current namespace if this is the closing tag of an island. + if ( isset( $island ) && $tags->is_tag_closer() ) { + array_pop( $namespace_stack ); } } From 2d42cfa7a7cf839fb2a0f279acb5488e7e8f15b7 Mon Sep 17 00:00:00 2001 From: David Arenas Date: Thu, 4 Jan 2024 11:13:04 +0100 Subject: [PATCH 02/20] Add required namespace param to evaluate reference --- .../interactivity-api/directive-processing.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/experimental/interactivity-api/directive-processing.php b/lib/experimental/interactivity-api/directive-processing.php index c604bffeb549f..6b30a791d08b5 100644 --- a/lib/experimental/interactivity-api/directive-processing.php +++ b/lib/experimental/interactivity-api/directive-processing.php @@ -314,14 +314,15 @@ function gutenberg_process_interactive_html( $html, $context, $inner_blocks = ar * Resolves the reference using the store and the context from the provided * path. * + * @param string $ns Namespace. * @param string $path Path. * @param array $context Context data. * @return mixed */ -function gutenberg_interactivity_evaluate_reference( $path, array $context = array() ) { +function gutenberg_interactivity_evaluate_reference( $ns, $path, array $context = array() ) { $store = array_merge( - WP_Interactivity_Store::get_data(), - array( 'context' => $context ) + WP_Interactivity_Store::get_data()[ $ns ], + array( 'context' => $context[ $ns ] ) ); /* From 319e37656913fc9ecdf34dce4937fd3fa15f9b4d Mon Sep 17 00:00:00 2001 From: David Arenas Date: Thu, 4 Jan 2024 12:14:23 +0100 Subject: [PATCH 03/20] Handle references with namespaces --- .../class-wp-directive-processor.php | 18 +++++++++++++ .../directive-processing.php | 25 ++++++++++++------- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/lib/experimental/interactivity-api/class-wp-directive-processor.php b/lib/experimental/interactivity-api/class-wp-directive-processor.php index bb70068aa9482..a2a300b3538f8 100644 --- a/lib/experimental/interactivity-api/class-wp-directive-processor.php +++ b/lib/experimental/interactivity-api/class-wp-directive-processor.php @@ -256,4 +256,22 @@ public static function is_html_void_element( $tag_name ) { public static function parse_attribute_name( $name ) { return explode( '--', $name, 2 ); } + + /** + * Parse and extract the namespace and path from the given reference. + * + * @param string $reference Passed reference. + * @param string $ns Namespace fallback. + * @return array The resulting array + */ + public static function parse_reference( $reference, $ns_fallback = null ) { + $matches = array(); + $has_ns = preg_match( '/^([\w\-_\/]+)::(.+)$/', $reference, $matches ); + + if ( $has_ns ) { + return array_slice( $matches, 1 ); + } else { + return array( $ns_fallback, $reference ); + } + } } diff --git a/lib/experimental/interactivity-api/directive-processing.php b/lib/experimental/interactivity-api/directive-processing.php index 6b30a791d08b5..2a7d9d95a7c75 100644 --- a/lib/experimental/interactivity-api/directive-processing.php +++ b/lib/experimental/interactivity-api/directive-processing.php @@ -311,18 +311,25 @@ function gutenberg_process_interactive_html( $html, $context, $inner_blocks = ar } /** - * Resolves the reference using the store and the context from the provided - * path. + * Resolves the passed reference from the store and the context under the given + * namespace. * - * @param string $ns Namespace. - * @param string $path Path. + * 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 + * @return mixed Resolved value. */ -function gutenberg_interactivity_evaluate_reference( $ns, $path, array $context = array() ) { - $store = array_merge( - WP_Interactivity_Store::get_data()[ $ns ], - array( 'context' => $context[ $ns ] ) +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_reference( $reference, $ns ); + + $store = array( + 'state' => WP_Interactivity_Store::get_data()[ $ns ], + 'context' => $context[ $ns ] ?? array(), ); /* From 6e1ffa588d0f1afab0638cf1f1a10d9c90abeca3 Mon Sep 17 00:00:00 2001 From: David Arenas Date: Thu, 4 Jan 2024 13:15:29 +0100 Subject: [PATCH 04/20] Rename parse_reference to parse_attribute_value --- .../class-wp-directive-processor.php | 23 +++++++++++++------ .../directive-processing.php | 2 +- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/lib/experimental/interactivity-api/class-wp-directive-processor.php b/lib/experimental/interactivity-api/class-wp-directive-processor.php index a2a300b3538f8..22ba18defe55e 100644 --- a/lib/experimental/interactivity-api/class-wp-directive-processor.php +++ b/lib/experimental/interactivity-api/class-wp-directive-processor.php @@ -258,20 +258,29 @@ public static function parse_attribute_name( $name ) { } /** - * Parse and extract the namespace and path from the given reference. + * Parse and extract the namespace and path from the given value. * - * @param string $reference Passed reference. + * 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_reference( $reference, $ns_fallback = null ) { + public static function parse_attribute_value( $value, $ns = null ) { $matches = array(); - $has_ns = preg_match( '/^([\w\-_\/]+)::(.+)$/', $reference, $matches ); + $has_ns = preg_match( '/^([\w\-_\/]+)::(.+)$/', $value, $matches ); if ( $has_ns ) { - return array_slice( $matches, 1 ); - } else { - return array( $ns_fallback, $reference ); + list( , $ns, $value ) = $matches; + } + + // Try parsing the value. + $data = json_decode( $value, true ); + if ( null !== $data || 'null' === trim( $value ) ) { + $value = $data; } + + return array( $ns, $value ); } } diff --git a/lib/experimental/interactivity-api/directive-processing.php b/lib/experimental/interactivity-api/directive-processing.php index 2a7d9d95a7c75..12269eab99b20 100644 --- a/lib/experimental/interactivity-api/directive-processing.php +++ b/lib/experimental/interactivity-api/directive-processing.php @@ -325,7 +325,7 @@ function gutenberg_process_interactive_html( $html, $context, $inner_blocks = ar */ 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_reference( $reference, $ns ); + list( $ns, $path ) = WP_Directive_Processor::parse_attribute_value( $reference, $ns ); $store = array( 'state' => WP_Interactivity_Store::get_data()[ $ns ], From 9e5f801fcc3c9e9ab85fda7c13441a8ae3335ef5 Mon Sep 17 00:00:00 2001 From: David Arenas Date: Thu, 4 Jan 2024 13:21:43 +0100 Subject: [PATCH 05/20] Update directive implementations --- .../interactivity-api/directives/wp-bind.php | 7 ++++--- .../interactivity-api/directives/wp-class.php | 7 ++++--- .../interactivity-api/directives/wp-context.php | 14 +++++++------- .../interactivity-api/directives/wp-style.php | 7 ++++--- .../interactivity-api/directives/wp-text.php | 5 +++-- 5 files changed, 22 insertions(+), 18 deletions(-) diff --git a/lib/experimental/interactivity-api/directives/wp-bind.php b/lib/experimental/interactivity-api/directives/wp-bind.php index 54be4a9faeb7d..57d2e5deb23ab 100644 --- a/lib/experimental/interactivity-api/directives/wp-bind.php +++ b/lib/experimental/interactivity-api/directives/wp-bind.php @@ -11,8 +11,9 @@ * * @param WP_Directive_Processor $tags Tags. * @param WP_Directive_Context $context Directive context. + * @param string $ns Namespace. */ -function gutenberg_interactivity_process_wp_bind( $tags, $context ) { +function gutenberg_interactivity_process_wp_bind( $tags, $context, $ns ) { if ( $tags->is_tag_closer() ) { return; } @@ -25,8 +26,8 @@ function gutenberg_interactivity_process_wp_bind( $tags, $context ) { continue; } - $expr = $tags->get_attribute( $attr ); - $value = gutenberg_interactivity_evaluate_reference( $expr, $context->get_context() ); + $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 index 741cc75b42c60..ef91835be86fc 100644 --- a/lib/experimental/interactivity-api/directives/wp-class.php +++ b/lib/experimental/interactivity-api/directives/wp-class.php @@ -11,8 +11,9 @@ * * @param WP_Directive_Processor $tags Tags. * @param WP_Directive_Context $context Directive context. + * @param string $ns Namespace. */ -function gutenberg_interactivity_process_wp_class( $tags, $context ) { +function gutenberg_interactivity_process_wp_class( $tags, $context, $ns ) { if ( $tags->is_tag_closer() ) { return; } @@ -25,8 +26,8 @@ function gutenberg_interactivity_process_wp_class( $tags, $context ) { continue; } - $expr = $tags->get_attribute( $attr ); - $add_class = gutenberg_interactivity_evaluate_reference( $expr, $context->get_context() ); + $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 { diff --git a/lib/experimental/interactivity-api/directives/wp-context.php b/lib/experimental/interactivity-api/directives/wp-context.php index 7d92b0ac7b0c6..43bfcfa926d2f 100644 --- a/lib/experimental/interactivity-api/directives/wp-context.php +++ b/lib/experimental/interactivity-api/directives/wp-context.php @@ -10,19 +10,19 @@ * * @param WP_Directive_Processor $tags Tags. * @param WP_Directive_Context $context Directive context. + * @param string $ns Namespace. */ -function gutenberg_interactivity_process_wp_context( $tags, $context ) { +function gutenberg_interactivity_process_wp_context( $tags, $context, $ns ) { if ( $tags->is_tag_closer() ) { $context->rewind_context(); return; } - $value = $tags->get_attribute( 'data-wp-context' ); + $attr_value = $tags->get_attribute( 'data-wp-context' ); - $new_context = json_decode( - is_string( $value ) && ! empty( $value ) ? $value : '{}', - true - ); + //Separate namespace and value from the context directive attribute. + list( $ns, $data ) = WP_Directive_Processor::parse_attribute_value( $attr_value, $ns ); - $context->set_context( $new_context ?? array() ); + // 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-style.php b/lib/experimental/interactivity-api/directives/wp-style.php index e5d7b269ace7c..16432e5728260 100644 --- a/lib/experimental/interactivity-api/directives/wp-style.php +++ b/lib/experimental/interactivity-api/directives/wp-style.php @@ -11,8 +11,9 @@ * * @param WP_Directive_Processor $tags Tags. * @param WP_Directive_Context $context Directive context. + * @param string $ns Namespace. */ -function gutenberg_interactivity_process_wp_style( $tags, $context ) { +function gutenberg_interactivity_process_wp_style( $tags, $context, $ns ) { if ( $tags->is_tag_closer() ) { return; } @@ -25,8 +26,8 @@ function gutenberg_interactivity_process_wp_style( $tags, $context ) { continue; } - $expr = $tags->get_attribute( $attr ); - $style_value = gutenberg_interactivity_evaluate_reference( $expr, $context->get_context() ); + $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 ); diff --git a/lib/experimental/interactivity-api/directives/wp-text.php b/lib/experimental/interactivity-api/directives/wp-text.php index b0cfc98a74e70..c4c5bb27a31e1 100644 --- a/lib/experimental/interactivity-api/directives/wp-text.php +++ b/lib/experimental/interactivity-api/directives/wp-text.php @@ -11,8 +11,9 @@ * * @param WP_Directive_Processor $tags Tags. * @param WP_Directive_Context $context Directive context. + * @param string $ns Namespace. */ -function gutenberg_interactivity_process_wp_text( $tags, $context ) { +function gutenberg_interactivity_process_wp_text( $tags, $context, $ns ) { if ( $tags->is_tag_closer() ) { return; } @@ -22,6 +23,6 @@ function gutenberg_interactivity_process_wp_text( $tags, $context ) { return; } - $text = gutenberg_interactivity_evaluate_reference( $value, $context->get_context() ); + $text = gutenberg_interactivity_evaluate_reference( $value, $ns, $context->get_context() ); $tags->set_inner_html( esc_html( $text ) ); } From 262017414d8fc1e7cdd2123a177787c6d3607eaa Mon Sep 17 00:00:00 2001 From: David Arenas Date: Thu, 4 Jan 2024 16:54:13 +0100 Subject: [PATCH 06/20] Improve comments in `parse_attribute_value` --- .../class-wp-directive-processor.php | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/experimental/interactivity-api/class-wp-directive-processor.php b/lib/experimental/interactivity-api/class-wp-directive-processor.php index 22ba18defe55e..0b5d63e2a25aa 100644 --- a/lib/experimental/interactivity-api/class-wp-directive-processor.php +++ b/lib/experimental/interactivity-api/class-wp-directive-processor.php @@ -271,11 +271,23 @@ 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 parsing the value. + /* + * 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; From 80694be2a7e2dd989a72d2bd4fb0433866851c65 Mon Sep 17 00:00:00 2001 From: David Arenas Date: Wed, 29 Nov 2023 18:24:29 +0100 Subject: [PATCH 07/20] Restore changes from `store()` migration PR --- .../class-wp-interactivity-initial-state.php | 85 ++++++++ .../class-wp-interactivity-store.php | 69 ------- .../interactivity-api/initial-state.php | 29 +++ lib/experimental/interactivity-api/store.php | 28 --- lib/load.php | 4 +- ...ss-wp-interactivity-initial-state-test.php | 111 +++++++++++ .../class-wp-interactivity-store-test.php | 186 ------------------ 7 files changed, 227 insertions(+), 285 deletions(-) create mode 100644 lib/experimental/interactivity-api/class-wp-interactivity-initial-state.php delete mode 100644 lib/experimental/interactivity-api/class-wp-interactivity-store.php create mode 100644 lib/experimental/interactivity-api/initial-state.php delete mode 100644 lib/experimental/interactivity-api/store.php create mode 100644 phpunit/experimental/interactivity-api/class-wp-interactivity-initial-state-test.php delete mode 100644 phpunit/experimental/interactivity-api/class-wp-interactivity-store-test.php diff --git a/lib/experimental/interactivity-api/class-wp-interactivity-initial-state.php b/lib/experimental/interactivity-api/class-wp-interactivity-initial-state.php new file mode 100644 index 0000000000000..a1edbcff776fb --- /dev/null +++ b/lib/experimental/interactivity-api/class-wp-interactivity-initial-state.php @@ -0,0 +1,85 @@ +%s', + wp_json_encode( self::$initial_state, JSON_HEX_TAG | JSON_HEX_AMP ) + ); + } +} diff --git a/lib/experimental/interactivity-api/class-wp-interactivity-store.php b/lib/experimental/interactivity-api/class-wp-interactivity-store.php deleted file mode 100644 index c53701b14e8af..0000000000000 --- a/lib/experimental/interactivity-api/class-wp-interactivity-store.php +++ /dev/null @@ -1,69 +0,0 @@ -%s', - wp_json_encode( self::$store, JSON_HEX_TAG | JSON_HEX_AMP ) - ); - } -} diff --git a/lib/experimental/interactivity-api/initial-state.php b/lib/experimental/interactivity-api/initial-state.php new file mode 100644 index 0000000000000..a38d0da631f3c --- /dev/null +++ b/lib/experimental/interactivity-api/initial-state.php @@ -0,0 +1,29 @@ +assertEmpty( WP_Interactivity_Initial_State::get_data() ); + } + + public function test_initial_state_can_be_merged() { + $state = array( + 'a' => 1, + 'b' => 2, + 'nested' => array( + 'c' => 3, + ), + ); + WP_Interactivity_Initial_State::merge_state( 'core', $state ); + $this->assertSame( $state, WP_Interactivity_Initial_State::get_state( 'core' ) ); + } + + public function test_initial_state_can_be_extended() { + WP_Interactivity_Initial_State::merge_state( 'core', array( 'a' => 1 ) ); + WP_Interactivity_Initial_State::merge_state( 'core', array( 'b' => 2 ) ); + WP_Interactivity_Initial_State::merge_state( 'custom', array( 'c' => 3 ) ); + $this->assertSame( + array( + 'core' => array( + 'a' => 1, + 'b' => 2, + ), + 'custom' => array( + 'c' => 3, + ), + ), + WP_Interactivity_Initial_State::get_data() + ); + } + + public function test_initial_state_existing_props_should_be_overwritten() { + WP_Interactivity_Initial_State::merge_state( 'core', array( 'a' => 1 ) ); + WP_Interactivity_Initial_State::merge_state( 'core', array( 'a' => 'overwritten' ) ); + $this->assertSame( + array( + 'core' => array( + 'a' => 'overwritten', + ), + ), + WP_Interactivity_Initial_State::get_data() + ); + } + + public function test_initial_state_existing_indexed_arrays_should_be_replaced() { + WP_Interactivity_Initial_State::merge_state( 'core', array( 'a' => array( 1, 2 ) ) ); + WP_Interactivity_Initial_State::merge_state( 'core', array( 'a' => array( 3, 4 ) ) ); + $this->assertSame( + array( + 'core' => array( + 'a' => array( 3, 4 ), + ), + ), + WP_Interactivity_Initial_State::get_data() + ); + } + + public function test_initial_state_should_be_correctly_rendered() { + WP_Interactivity_Initial_State::merge_state( 'core', array( 'a' => 1 ) ); + WP_Interactivity_Initial_State::merge_state( 'core', array( 'b' => 2 ) ); + WP_Interactivity_Initial_State::merge_state( 'custom', array( 'c' => 3 ) ); + + ob_start(); + WP_Interactivity_Initial_State::render(); + $rendered = ob_get_clean(); + $this->assertSame( + '', + $rendered + ); + } + + public function test_initial_state_should_also_escape_tags_and_amps() { + WP_Interactivity_Initial_State::merge_state( + 'test', + array( + 'amps' => 'http://site.test/?foo=1&baz=2&bar=3', + 'tags' => 'Do not do this: + + + + + + '; + + $html = do_blocks( $post_content ); + $tags = new WP_HTML_Tag_Processor( $html ); + + $tags->next_tag( array( 'class_name' => 'bind-state' ) ); + $this->assertSame( 'state', $tags->get_attribute( 'data-value' ) ); + + $tags->next_tag( array( 'class_name' => 'bind-context' ) ); + $this->assertSame( 'context', $tags->get_attribute( 'data-value' ) ); + } + + public function test_namespace_should_be_inherited_from_same_element() { + /* + * This function call should be done inside block render functions. We + * run it here instead just for conveninence. + */ + wp_initial_state( 'test-2', array( 'text' => 'state-2' ) ); + + $post_content = ' + + + + + + + '; + + $html = do_blocks( $post_content ); + $tags = new WP_HTML_Tag_Processor( $html ); + + $tags->next_tag( array( 'class_name' => 'bind-state' ) ); + $this->assertSame( 'state-2', $tags->get_attribute( 'data-value' ) ); + + $tags->next_tag( array( 'class_name' => 'bind-context' ) ); + $this->assertSame( 'context-2', $tags->get_attribute( 'data-value' ) ); + } + + public function test_namespace_should_not_leak_from_descendant() { + /* + * This function call should be done inside block render functions. We + * run it here instead just for conveninence. + */ + wp_initial_state( 'test-1', array( 'text' => 'state-1' ) ); + wp_initial_state( 'test-2', array( 'text' => 'state-2' ) ); + + $post_content = ' + + + + + + '; + + $html = do_blocks( $post_content ); + $tags = new WP_HTML_Tag_Processor( $html ); + + $tags->next_tag( array( 'class_name' => 'target' ) ); + $this->assertSame( 'state-1', $tags->get_attribute( 'data-state' ) ); + $this->assertSame( 'context-1', $tags->get_attribute( 'data-context' ) ); + } + + public function test_namespace_should_not_leak_from_sibling() { + /* + * This function call should be done inside block render functions. We + * run it here instead just for conveninence. + */ + wp_initial_state( 'test-1', array( 'text' => 'state-1' ) ); + wp_initial_state( 'test-2', array( 'text' => 'state-2' ) ); + + $post_content = ' + + + + + + + '; + + $html = do_blocks( $post_content ); + $tags = new WP_HTML_Tag_Processor( $html ); + + $tags->next_tag( array( 'class_name' => 'target' ) ); + $this->assertSame( 'state-1', $tags->get_attribute( 'data-from-state' ) ); + $this->assertSame( 'context-1', $tags->get_attribute( 'data-from-context' ) ); + } + + public function test_namespace_can_be_overwritten_in_directives() { + /* + * This function call should be done inside block render functions. We + * run it here instead just for conveninence. + */ + wp_initial_state( 'test-1', array( 'text' => 'state-1' ) ); + wp_initial_state( 'test-2', array( 'text' => 'state-2' ) ); + + $post_content = ' + + + + + + '; + + $html = do_blocks( $post_content ); + $tags = new WP_HTML_Tag_Processor( $html ); + + $tags->next_tag( array( 'class_name' => 'inherited-ns' ) ); + $this->assertSame( 'state-1', $tags->get_attribute( 'data-value' ) ); + + $tags->next_tag( array( 'class_name' => 'custom-ns' ) ); + $this->assertSame( 'state-2', $tags->get_attribute( 'data-value' ) ); + + $tags->next_tag( array( 'class_name' => 'mixed-ns' ) ); + $this->assertSame( 'state-1', $tags->get_attribute( 'data-inherited-ns' ) ); + $this->assertSame( 'state-2', $tags->get_attribute( 'data-custom-ns' ) ); + } } From b4490496bde5c7bbaf921cce01461ef381b98776 Mon Sep 17 00:00:00 2001 From: David Arenas Date: Tue, 9 Jan 2024 16:02:08 +0100 Subject: [PATCH 16/20] Fix typo in comments --- .../interactivity-api/directives/wp-interactive.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/experimental/interactivity-api/directives/wp-interactive.php b/lib/experimental/interactivity-api/directives/wp-interactive.php index 72a0f70274562..02800982bd726 100644 --- a/lib/experimental/interactivity-api/directives/wp-interactive.php +++ b/lib/experimental/interactivity-api/directives/wp-interactive.php @@ -33,8 +33,8 @@ function gutenberg_interactivity_process_wp_interactive( $tags, $context, $ns, & * 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, independtly of whether - * the island definition was correct or contained a valid namespace. + * 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'] From dcfc2c094f6d7fdd5e16f9cf0009f07c46a146dd Mon Sep 17 00:00:00 2001 From: David Arenas Date: Tue, 9 Jan 2024 16:05:00 +0100 Subject: [PATCH 17/20] Handle empty attributes in wp-interactive --- .../interactivity-api/directives/wp-interactive.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/experimental/interactivity-api/directives/wp-interactive.php b/lib/experimental/interactivity-api/directives/wp-interactive.php index 02800982bd726..9f3471a8b4e6a 100644 --- a/lib/experimental/interactivity-api/directives/wp-interactive.php +++ b/lib/experimental/interactivity-api/directives/wp-interactive.php @@ -23,10 +23,12 @@ function gutenberg_interactivity_process_wp_interactive( $tags, $context, $ns, & /* * Decode the data-wp-interactive attribute. In the case it is not a valid - * JSON string, the NULL value is stored in `$island_data`. + * JSON string, NULL is stored in `$island_data`. */ $island = $tags->get_attribute( 'data-wp-interactive' ); - $island_data = json_decode( $island, true ); + $island_data = is_string( $island ) && ! empty( $island ) + ? json_decode( $island, true ) + : null; /* * Push the newly defined namespace, or the current one if the island From 4096915fb8c7cbbcadd88933dd9d31da0a534f07 Mon Sep 17 00:00:00 2001 From: David Arenas Date: Tue, 9 Jan 2024 16:08:17 +0100 Subject: [PATCH 18/20] Handle boolean attributes in wp-context --- lib/experimental/interactivity-api/directives/wp-context.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/experimental/interactivity-api/directives/wp-context.php b/lib/experimental/interactivity-api/directives/wp-context.php index 43bfcfa926d2f..b41b47c86c78c 100644 --- a/lib/experimental/interactivity-api/directives/wp-context.php +++ b/lib/experimental/interactivity-api/directives/wp-context.php @@ -21,7 +21,9 @@ function gutenberg_interactivity_process_wp_context( $tags, $context, $ns ) { $attr_value = $tags->get_attribute( 'data-wp-context' ); //Separate namespace and value from the context directive attribute. - list( $ns, $data ) = WP_Directive_Processor::parse_attribute_value( $attr_value, $ns ); + 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() ) ); From d8e650d724f55eb970ecc4afe057ddd1ab55b258 Mon Sep 17 00:00:00 2001 From: David Arenas Date: Tue, 9 Jan 2024 16:24:08 +0100 Subject: [PATCH 19/20] Do not support derived state yet --- .../interactivity-api/directive-processing.php | 9 ++++++--- .../interactivity-api/directive-processing-test.php | 2 ++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/experimental/interactivity-api/directive-processing.php b/lib/experimental/interactivity-api/directive-processing.php index 80d5ee71815da..b49ee538390ff 100644 --- a/lib/experimental/interactivity-api/directive-processing.php +++ b/lib/experimental/interactivity-api/directive-processing.php @@ -350,9 +350,12 @@ function gutenberg_interactivity_evaluate_reference( $reference, $ns, array $con * E.g., "file" is an string and a "callable" (the "file" function exists). */ if ( $current instanceof Closure ) { - // TODO: Do not pass the store as argument. Implement something similar - // to what we have in the JS runtime (`getContext()`, etc.). - $current = call_user_func( $current, $store ); + /* + * 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 (!). diff --git a/phpunit/experimental/interactivity-api/directive-processing-test.php b/phpunit/experimental/interactivity-api/directive-processing-test.php index 4b7ca71a64c90..d6b5bc3860a95 100644 --- a/phpunit/experimental/interactivity-api/directive-processing-test.php +++ b/phpunit/experimental/interactivity-api/directive-processing-test.php @@ -310,6 +310,8 @@ public function test_evaluate_function_should_return_null_for_unresolved_paths() } public function test_evaluate_function_should_execute_anonymous_functions() { + $this->markTestSkipped( 'Derived state was supported for `wp_store()` but not for `wp_initial_state()` yet.' ); + $context = new WP_Directive_Context( array( 'myblock' => array( 'count' => 2 ) ) ); wp_initial_state( From 3e61b1e224a2622aa4017a31e8fa6f99d448e868 Mon Sep 17 00:00:00 2001 From: Carlos Bravo Date: Tue, 9 Jan 2024 18:35:46 +0100 Subject: [PATCH 20/20] Add root blocks bail out quick by name --- .../class-wp-directive-processor.php | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/experimental/interactivity-api/class-wp-directive-processor.php b/lib/experimental/interactivity-api/class-wp-directive-processor.php index 0b5d63e2a25aa..3b8a38f973815 100644 --- a/lib/experimental/interactivity-api/class-wp-directive-processor.php +++ b/lib/experimental/interactivity-api/class-wp-directive-processor.php @@ -35,7 +35,11 @@ class WP_Directive_Processor extends Gutenberg_HTML_Tag_Processor_6_5 { * @param array $block The block to add. */ public static function mark_root_block( $block ) { - self::$root_block = md5( serialize( $block ) ); + if ( null !== $block['blockName'] ) { + self::$root_block = $block['blockName'] . md5( serialize( $block ) ); + } else { + self::$root_block = md5( serialize( $block ) ); + } } /** @@ -52,6 +56,14 @@ public static function unmark_root_block() { * @return bool True if block is a root block, false otherwise. */ public static function is_marked_as_root_block( $block ) { + // If self::$root_block is null, is impossible that any block has been marked as root. + if ( is_null( self::$root_block ) ) { + return false; + } + // Blocks whose blockName is null are specifically intended to convey - "this is a freeform HTML block." + if ( null !== $block['blockName'] ) { + return str_contains( self::$root_block, $block['blockName'] ) && $block['blockName'] . md5( serialize( $block ) ) === self::$root_block; + } return md5( serialize( $block ) ) === self::$root_block; }