diff --git a/lib/experimental/interactivity-api/class-wp-directive-processor.php b/lib/experimental/interactivity-api/class-wp-directive-processor.php index 7d9ccdca453b8..e717b2e553943 100644 --- a/lib/experimental/interactivity-api/class-wp-directive-processor.php +++ b/lib/experimental/interactivity-api/class-wp-directive-processor.php @@ -27,17 +27,26 @@ class WP_Directive_Processor extends Gutenberg_HTML_Tag_Processor_6_4 { * * @var array */ - public static $root_blocks = array(); + public static $root_block = null; /** - * Add a root block to the list. + * Add a root block to the variable. * * @param array $block The block to add. * * @return void */ - public static function add_root_block( $block ) { - self::$root_blocks[] = md5( serialize( $block ) ); + public static function mark_root_block( $block ) { + self::$root_block = md5( serialize( $block ) ); + } + + /** + * Remove a root block to the variable. + * + * @return void + */ + public static function unmark_root_block() { + self::$root_block = null; } /** @@ -47,8 +56,17 @@ public static function add_root_block( $block ) { * * @return bool True if block is a root block, false otherwise. */ - public static function is_root_block( $block ) { - return in_array( md5( serialize( $block ) ), self::$root_blocks, true ); + public static function is_marked_as_root_block( $block ) { + return md5( serialize( $block ) ) === self::$root_block; + } + + /** + * Check if a root block has already been defined. + * + * @return bool True if block is a root block, false otherwise. + */ + public static function has_root_block() { + return isset( self::$root_block ); } @@ -92,6 +110,75 @@ public function next_balanced_closer() { return false; } + /** + * Traverses the HTML searching for Interactivity API directives and processing + * them. + * + * @param WP_Directive_Processor $tags An instance of the WP_Directive_Processor. + * @param string $prefix Attribute prefix. + * @param string[] $directives Directives. + * + * @return WP_Directive_Processor The modified instance of the + * WP_Directive_Processor. + */ + public function process_rendered_html( $tags, $prefix, $directives ) { + $context = new WP_Directive_Context(); + $tag_stack = array(); + + while ( $tags->next_tag( array( 'tag_closers' => 'visit' ) ) ) { + $tag_name = $tags->get_tag(); + + // Is this a tag that closes the latest opening 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 ) = WP_Directive_Processor::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 ( + ! WP_Directive_Processor::is_html_void_element( $tags->get_tag() ) && + ( 0 !== count( $attributes ) || 0 !== count( $tag_stack ) ) + ) { + $tag_stack[] = array( $tag_name, $attributes ); + } + } + + foreach ( $attributes as $attribute ) { + call_user_func( $directives[ $attribute ], $tags, $context ); + } + } + + return $tags; + } + /** * Return the content between two balanced tags. * diff --git a/lib/experimental/interactivity-api/directive-processing.php b/lib/experimental/interactivity-api/directive-processing.php index eae731e243891..064fc8ea62cbb 100644 --- a/lib/experimental/interactivity-api/directive-processing.php +++ b/lib/experimental/interactivity-api/directive-processing.php @@ -8,9 +8,9 @@ */ /** - * Process the Interactivity API directives using the root blocks of the - * outermost rendering, ignoring the root blocks of inner blocks like Patterns, - * Template Parts or Content. + * Mark if the block is a root block. Checks that there is already a root block + * in order not to mark template-parts or synced patterns as root blocks, where + * the parent is null. * * @param array $parsed_block The parsed block. * @param array $source_block The source block. @@ -18,121 +18,44 @@ * * @return array The parsed block. */ -function gutenberg_interactivity_process_directives( $parsed_block, $source_block, $parent_block ) { - static $is_inside_root_block = false; - static $process_directives_in_root_blocks = null; - - if ( ! isset( $process_directives_in_root_blocks ) ) { - /** - * Process directives in each root block. - * - * @param string $block_content The block content. - * @param array $block The full block. - * - * @return string Filtered block content. - */ - $process_directives_in_root_blocks = static function ( $block_content, $block ) use ( &$is_inside_root_block ) { - - if ( WP_Directive_Processor::is_root_block( $block ) ) { - - $directives = array( - 'data-wp-bind' => 'gutenberg_interactivity_process_wp_bind', - 'data-wp-context' => 'gutenberg_interactivity_process_wp_context', - '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( $block_content ); - $tags = gutenberg_interactivity_process_rendered_html( $tags, 'data-wp-', $directives ); - $is_inside_root_block = false; - return $tags->get_updated_html(); - - } - - return $block_content; - }; - add_filter( 'render_block', $process_directives_in_root_blocks, 10, 2 ); - } - - if ( ! isset( $parent_block ) && ! $is_inside_root_block ) { - WP_Directive_Processor::add_root_block( $parsed_block ); - $is_inside_root_block = true; +function gutenberg_interactivity_mark_root_blocks( $parsed_block, $source_block, $parent_block ) { + if ( ! isset( $parent_block ) && ! WP_Directive_Processor::has_root_block() ) { + WP_Directive_Processor::mark_root_block( $parsed_block ); } return $parsed_block; } -add_filter( 'render_block_data', 'gutenberg_interactivity_process_directives', 10, 3 ); - +add_filter( 'render_block_data', 'gutenberg_interactivity_mark_root_blocks', 10, 3 ); /** - * Traverses the HTML searching for Interactivity API directives and processing - * them. + * Process directives in each root block. * - * @param WP_Directive_Processor $tags An instance of the WP_Directive_Processor. - * @param string $prefix Attribute prefix. - * @param string[] $directives Directives. + * @param string $block_content The block content. + * @param array $block The full block. * - * @return WP_Directive_Processor The modified instance of the - * WP_Directive_Processor. + * @return string Filtered block content. */ -function gutenberg_interactivity_process_rendered_html( $tags, $prefix, $directives ) { - $context = new WP_Directive_Context(); - $tag_stack = array(); +function gutenberg_process_directives_in_root_blocks( $block_content, $block ) { + if ( WP_Directive_Processor::is_marked_as_root_block( $block ) ) { + WP_Directive_Processor::unmark_root_block(); + $directives = array( + 'data-wp-bind' => 'gutenberg_interactivity_process_wp_bind', + 'data-wp-context' => 'gutenberg_interactivity_process_wp_context', + '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( $block_content ); + $tags = $tags->process_rendered_html( $tags, 'data-wp-', $directives ); + return $tags->get_updated_html(); - while ( $tags->next_tag( array( 'tag_closers' => 'visit' ) ) ) { - $tag_name = $tags->get_tag(); - - // Is this a tag that closes the latest opening 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 ) = WP_Directive_Processor::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 ( - ! WP_Directive_Processor::is_html_void_element( $tags->get_tag() ) && - ( 0 !== count( $attributes ) || 0 !== count( $tag_stack ) ) - ) { - $tag_stack[] = array( $tag_name, $attributes ); - } - } - - foreach ( $attributes as $attribute ) { - call_user_func( $directives[ $attribute ], $tags, $context ); - } } - return $tags; + return $block_content; } +add_filter( 'render_block', 'gutenberg_process_directives_in_root_blocks', 10, 2 ); + /** * Resolve the reference using the store and the context from the provided path. diff --git a/packages/e2e-tests/plugins/interactive-blocks.php b/packages/e2e-tests/plugins/interactive-blocks.php index 956508a11361e..c551127548e80 100644 --- a/packages/e2e-tests/plugins/interactive-blocks.php +++ b/packages/e2e-tests/plugins/interactive-blocks.php @@ -40,7 +40,7 @@ function () { if ( 'true' === $_GET['disable_directives_ssr'] ) { remove_filter( 'render_block_data', - 'gutenberg_interactivity_process_directives' + 'gutenberg_interactivity_mark_root_blocks' ); } } diff --git a/phpunit/experimental/interactivity-api/directive-processing-test.php b/phpunit/experimental/interactivity-api/directive-processing-test.php index 97dddba3c263f..46ef0284df15d 100644 --- a/phpunit/experimental/interactivity-api/directive-processing-test.php +++ b/phpunit/experimental/interactivity-api/directive-processing-test.php @@ -24,7 +24,7 @@ public static function static_increment( $store ) { } function gutenberg_test_process_directives_helper_increment( $store ) { - return $store['state']['count'] + $store['context']['count']; + return $store['state']['count'] + $store['context']['count']; } /** @@ -59,8 +59,8 @@ function ( $p ) { ); $markup = '
Example:
This is a test>
Here is a nested div
'; - $tags = new WP_HTML_Tag_Processor( $markup ); - gutenberg_interactivity_process_rendered_html( $tags, 'foo-', $directives ); + $tags = new WP_Directive_Processor( $markup ); + $tags->process_rendered_html( $tags, 'foo-', $directives ); } public function test_directives_with_double_hyphen_processed_correctly() { @@ -73,73 +73,49 @@ public function test_directives_with_double_hyphen_processed_correctly() { ); $markup = '
'; - $tags = new WP_HTML_Tag_Processor( $markup ); - gutenberg_interactivity_process_rendered_html( $tags, 'foo-', $directives ); + $tags = new WP_Directive_Processor( $markup ); + $tags->process_rendered_html( $tags, 'foo-', $directives ); } public function test_interactivity_process_directives_in_root_blocks() { - $pattern_content = + + $block_content = '' . - '

Pattern Content Block 1

' . + '

Welcome to WordPress. This is your first post. Edit or delete it, then start writing!

' . '' . '' . - '

Pattern Content Block 2

' . + '

Welcome to WordPress.

' . ''; - register_block_pattern( - 'core/interactivity-pattern', - array( - 'title' => 'Interactivity Pattern', - 'content' => $pattern_content, - ) - ); - $providers = $this->data_only_root_blocks_are_processed(); - foreach ( $providers as $provider ) { - do_blocks( $provider['page_content'] ); - $this->assertSame( $provider['root_blocks'], count( WP_Directive_Processor::$root_blocks ) ); + $parsed_block = parse_blocks( $block_content )[0]; - } - } + $source_block = $parsed_block; - /** - * Data provider . - * - * @return array - **/ - public function data_only_root_blocks_are_processed() { + $rendered_content = render_block( $parsed_block ); - return array( - array( - 'root_blocks' => 2, - 'page_content' => - '' . - '
- ' . - '

The XYZ Doohickey Company was founded in 1971, and has been providing' . - 'quality doohickeys to the public ever since. Located in Gotham City, XYZ employs' . - 'over 2,000 people and does all kinds of awesome things for the Gotham community.

' . - ' -
' . - '' . - '' . - '
- ' . - '

The XYZ Doohickey Company was founded in 1971, and has been providing' . - 'quality doohickeys to the public ever since. Located in Gotham City, XYZ employs' . - 'over 2,000 people and does all kinds of awesome things for the Gotham community.

' . - ' -
' . - '', - ), - array( - 'root_blocks' => 2, - 'page_content' => - '' . - '

Welcome to WordPress. This is your first post. Edit or delete it, then start writing!

' . - '' . - '', - ), - ); + $parsed_block_second = parse_blocks( $block_content )[1]; + + $fake_parent_block = array(); + + // Test that root block is intially emtpy. + $this->assertEmpty( WP_Directive_Processor::$root_block ); + + // Test that root block is not added if there is a parent block. + gutenberg_interactivity_mark_root_blocks( $parsed_block, $source_block, $fake_parent_block ); + $this->assertEmpty( WP_Directive_Processor::$root_block ); + + // Test that root block is added if there is no parent block. + gutenberg_interactivity_mark_root_blocks( $parsed_block, $source_block, null ); + $current_root_block = WP_Directive_Processor::$root_block; + $this->assertNotEmpty( $current_root_block ); + + // Test that a root block is not added if there is already a root block defined. + gutenberg_interactivity_mark_root_blocks( $parsed_block_second, $source_block, null ); + $this->assertSame( $current_root_block, WP_Directive_Processor::$root_block ); + + // Test that root block is removed after processing. + gutenberg_process_directives_in_root_blocks( $rendered_content, $parsed_block ); + $this->assertEmpty( WP_Directive_Processor::$root_block ); } }