diff --git a/src/wp-includes/interactivity-api/class-wp-interactivity-api.php b/src/wp-includes/interactivity-api/class-wp-interactivity-api.php index 9e5b1be1fa5ed..7867cad1c9da7 100644 --- a/src/wp-includes/interactivity-api/class-wp-interactivity-api.php +++ b/src/wp-includes/interactivity-api/class-wp-interactivity-api.php @@ -290,34 +290,42 @@ private function process_directives_args( string $html, array &$context_stack, a } } /* - * If the matching opener tag didn't have any directives, it can skip the - * processing. - */ + * If the matching opener tag didn't have any directives, it can skip the + * processing. + */ if ( 0 === count( $directives_prefixes ) ) { continue; } - /* - * 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 + // Directive processing might be different depending on if it is entering the tag or exiting it. + $modes = array( + 'enter' => ! $p->is_tag_closer(), + 'exit' => $p->is_tag_closer() || ! $p->has_and_visits_its_closer_tag(), ); - // 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, &$tag_stack ) + foreach ( $modes as $mode => $should_run ) { + if ( ! $should_run ) { + continue; + } + + /* + * Sorts the attributes by the order of the `directives_processor` array + * and checks what directives are present in this element. + */ + $existing_directives_prefixes = array_intersect( + 'enter' === $mode ? $directive_processor_prefixes : $directive_processor_prefixes_reversed, + $directives_prefixes ); + foreach ( $existing_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, $mode, &$context_stack, &$namespace_stack, &$tag_stack ) + ); + } } } @@ -470,12 +478,13 @@ function ( $matches ) { * @since 6.5.0 * * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance. + * @param string $mode Whether the processing is entering or exiting the tag. * @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() ) { + private function data_wp_interactive_processor( WP_Interactivity_API_Directives_Processor $p, string $mode, array &$context_stack, array &$namespace_stack ) { + // When exiting tags, it removes the last namespace from the stack. + if ( 'exit' === $mode ) { array_pop( $namespace_stack ); return; } @@ -514,12 +523,13 @@ private function data_wp_interactive_processor( WP_Interactivity_API_Directives_ * @since 6.5.0 * * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance. + * @param string $mode Whether the processing is entering or exiting the tag. * @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() ) { + private function data_wp_context_processor( WP_Interactivity_API_Directives_Processor $p, string $mode, array &$context_stack, array &$namespace_stack ) { + // When exiting tags, it removes the last context from the stack. + if ( 'exit' === $mode ) { array_pop( $context_stack ); return; } @@ -560,11 +570,12 @@ private function data_wp_context_processor( WP_Interactivity_API_Directives_Proc * @since 6.5.0 * * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance. + * @param string $mode Whether the processing is entering or exiting the tag. * @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() ) { + private function data_wp_bind_processor( WP_Interactivity_API_Directives_Processor $p, string $mode, array &$context_stack, array &$namespace_stack ) { + if ( 'enter' === $mode ) { $all_bind_directives = $p->get_attribute_names_with_prefix( 'data-wp-bind--' ); foreach ( $all_bind_directives as $attribute_name ) { @@ -604,11 +615,12 @@ private function data_wp_bind_processor( WP_Interactivity_API_Directives_Process * @since 6.5.0 * * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance. + * @param string $mode Whether the processing is entering or exiting the tag. * @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() ) { + private function data_wp_class_processor( WP_Interactivity_API_Directives_Processor $p, string $mode, array &$context_stack, array &$namespace_stack ) { + if ( 'enter' === $mode ) { $all_class_directives = $p->get_attribute_names_with_prefix( 'data-wp-class--' ); foreach ( $all_class_directives as $attribute_name ) { @@ -638,11 +650,12 @@ private function data_wp_class_processor( WP_Interactivity_API_Directives_Proces * @since 6.5.0 * * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance. + * @param string $mode Whether the processing is entering or exiting the tag. * @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() ) { + private function data_wp_style_processor( WP_Interactivity_API_Directives_Processor $p, string $mode, array &$context_stack, array &$namespace_stack ) { + if ( 'enter' === $mode ) { $all_style_attributes = $p->get_attribute_names_with_prefix( 'data-wp-style--' ); foreach ( $all_style_attributes as $attribute_name ) { @@ -730,11 +743,12 @@ private function merge_style_property( string $style_attribute_value, string $st * @since 6.5.0 * * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance. + * @param string $mode Whether the processing is entering or exiting the tag. * @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() ) { + private function data_wp_text_processor( WP_Interactivity_API_Directives_Processor $p, string $mode, array &$context_stack, array &$namespace_stack ) { + if ( 'enter' === $mode ) { $attribute_value = $p->get_attribute( 'data-wp-text' ); $result = $this->evaluate( $attribute_value, end( $namespace_stack ), end( $context_stack ) ); @@ -827,10 +841,11 @@ class="screen-reader-text" * * @since 6.5.0 * - * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance. + * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance. + * @param string $mode Whether the processing is entering or exiting the tag. */ - private function data_wp_router_region_processor( WP_Interactivity_API_Directives_Processor $p ) { - if ( ! $p->is_tag_closer() && ! $this->has_processed_router_region ) { + private function data_wp_router_region_processor( WP_Interactivity_API_Directives_Processor $p, string $mode ) { + if ( 'enter' === $mode && ! $this->has_processed_router_region ) { $this->has_processed_router_region = true; // Initialize the `core/router` store. @@ -866,12 +881,13 @@ private function data_wp_router_region_processor( WP_Interactivity_API_Directive * @since 6.5.0 * * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance. + * @param string $mode Whether the processing is entering or exiting the tag. * @param array $context_stack The reference to the context stack. * @param array $namespace_stack The reference to the store namespace stack. * @param array $tag_stack The reference to the tag stack. */ - private function data_wp_each_processor( WP_Interactivity_API_Directives_Processor $p, array &$context_stack, array &$namespace_stack, array &$tag_stack ) { - if ( ! $p->is_tag_closer() && 'TEMPLATE' === $p->get_tag() ) { + private function data_wp_each_processor( WP_Interactivity_API_Directives_Processor $p, string $mode, array &$context_stack, array &$namespace_stack, array &$tag_stack ) { + if ( 'enter' === $mode && 'TEMPLATE' === $p->get_tag() ) { $attribute_name = $p->get_attribute_names_with_prefix( 'data-wp-each' )[0]; $extracted_suffix = $this->extract_prefix_and_suffix( $attribute_name ); $item_name = isset( $extracted_suffix[1] ) ? $this->kebab_to_camel_case( $extracted_suffix[1] ) : 'item'; diff --git a/tests/phpunit/tests/interactivity-api/wpInteractivityAPIFunctions.php b/tests/phpunit/tests/interactivity-api/wpInteractivityAPIFunctions.php index 3a08a2f1af576..5025cb8502a8f 100644 --- a/tests/phpunit/tests/interactivity-api/wpInteractivityAPIFunctions.php +++ b/tests/phpunit/tests/interactivity-api/wpInteractivityAPIFunctions.php @@ -417,4 +417,79 @@ public function test_process_directives_in_tags_that_dont_visit_closer_tag() { unregister_block_type( 'test/custom-directive-block' ); $this->assertEquals( '1', $processor->get_attribute( 'src' ) ); } + + /** + * Tests that context from void tags is not propagated to next tags. + * + * @ticket 60768 + * + * @covers wp_interactivity_process_directives_of_interactive_blocks + */ + public function test_process_context_directive_in_void_tags() { + register_block_type( + 'test/custom-directive-block', + array( + 'render_callback' => function () { + return '
'; + }, + 'supports' => array( + 'interactivity' => true, + ), + ) + ); + $post_content = ''; + $processed_content = do_blocks( $post_content ); + $processor = new WP_HTML_Tag_Processor( $processed_content ); + $processor->next_tag( + array( + 'tag_name' => 'input', + 'id' => 'first-input', + ) + ); + $first_input_value = $processor->get_attribute( 'value' ); + $processor->next_tag( + array( + 'tag_name' => 'input', + 'id' => 'second-input', + ) + ); + $second_input_value = $processor->get_attribute( 'value' ); + unregister_block_type( 'test/custom-directive-block' ); + $this->assertEquals( 'inner', $first_input_value ); + $this->assertEquals( 'outer', $second_input_value ); + } + + /** + * Tests that namespace from void tags is not propagated to next tags. + * + * @ticket 60768 + * + * @covers wp_interactivity_process_directives_of_interactive_blocks + */ + public function test_process_interactive_directive_in_void_tags() { + wp_interactivity_state( + 'void', + array( + 'text' => 'void', + ) + ); + register_block_type( + 'test/custom-directive-block', + array( + 'render_callback' => function () { + return '
'; + }, + 'supports' => array( + 'interactivity' => true, + ), + ) + ); + $post_content = ''; + $processed_content = do_blocks( $post_content ); + $processor = new WP_HTML_Tag_Processor( $processed_content ); + $processor->next_tag( array( 'tag_name' => 'input' ) ); + $input_value = $processor->get_attribute( 'value' ); + unregister_block_type( 'test/custom-directive-block' ); + $this->assertNull( $input_value ); + } }