diff --git a/lib/experimental/interactivity-api/class-wp-directive-processor.php b/lib/experimental/interactivity-api/class-wp-directive-processor.php
index cf55a048bb9fa5..bb70068aa9482b 100644
--- a/lib/experimental/interactivity-api/class-wp-directive-processor.php
+++ b/lib/experimental/interactivity-api/class-wp-directive-processor.php
@@ -13,47 +13,42 @@
/**
* This processor is built on top of the HTML Tag Processor and augments its
* capabilities to process the Interactivity API directives.
- *
- * IMPORTANT DISCLAIMER: This code is highly experimental and its only purpose
- * is to provide a way to test the server-side rendering of the Interactivity
- * API. Most of this code will be discarded once the HTML Processor is
- * available. Please restrain from investing unnecessary time and effort trying
- * to improve this code.
*/
class WP_Directive_Processor extends Gutenberg_HTML_Tag_Processor_6_5 {
+ /**
+ * String containing the current root block.
+ *
+ * @var string
+ */
+ public static $root_block = null;
/**
- * An array of root blocks.
+ * Array containing the direct children of interactive blocks.
*
* @var array
*/
- public static $root_block = null;
+ public static $children_of_interactive_block = array();
/**
- * Add a root block to the variable.
+ * Sets the current root block.
*
* @param array $block The block to add.
- *
- * @return void
*/
public static function mark_root_block( $block ) {
self::$root_block = md5( serialize( $block ) );
}
/**
- * Remove a root block to the variable.
- *
- * @return void
+ * Resets the root block.
*/
public static function unmark_root_block() {
self::$root_block = null;
}
/**
- * Check if block is a root block.
+ * Checks if block is a root block.
*
* @param array $block The block to check.
- *
* @return bool True if block is a root block, false otherwise.
*/
public static function is_marked_as_root_block( $block ) {
@@ -61,17 +56,36 @@ public static function is_marked_as_root_block( $block ) {
}
/**
- * Check if a root block has already been defined.
+ * Checks if a root block has already been defined.
*
- * @return bool True if block is a root block, false otherwise.
+ * @return bool True if there is a root block, false otherwise.
*/
public static function has_root_block() {
return isset( self::$root_block );
}
+ /**
+ * Stores a reference to a direct children of an interactive block to be able
+ * to identify it later.
+ *
+ * @param array $block The block to add.
+ */
+ public static function mark_children_of_interactive_block( $block ) {
+ self::$children_of_interactive_block[] = md5( serialize( $block ) );
+ }
/**
- * Find the matching closing tag for an opening tag.
+ * Checks if block is marked as children of an interactive block.
+ *
+ * @param array $block The block to check.
+ * @return bool True if block is a children of an interactive block, false otherwise.
+ */
+ public static function is_marked_as_children_of_interactive_block( $block ) {
+ return in_array( md5( serialize( $block ) ), self::$children_of_interactive_block, true );
+ }
+
+ /**
+ * Finds the matching closing tag for an opening tag.
*
* When called while on an open tag, traverse the HTML until we find the
* matching closing tag, respecting any in-between content, including nested
@@ -111,76 +125,7 @@ public function next_balanced_closer() {
}
/**
- * 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.
+ * 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.
@@ -206,14 +151,13 @@ public function get_inner_html() {
}
/**
- * Set the content between two balanced tags.
+ * 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 ) {
@@ -237,7 +181,7 @@ public function set_inner_html( $new_html ) {
}
/**
- * Return a pair of bookmarks for the current opening tag and the matching
+ * 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
@@ -267,12 +211,12 @@ public function get_balanced_tag_bookmarks() {
}
/**
- * Whether a given HTML element is void (e.g.
).
+ * 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.
- *
- * @see https://html.spec.whatwg.org/#elements-2
*/
public static function is_html_void_element( $tag_name ) {
switch ( $tag_name ) {
@@ -297,7 +241,7 @@ public static function is_html_void_element( $tag_name ) {
}
/**
- * Extract and return the directive type and the the part after the double
+ * 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:
@@ -307,7 +251,7 @@ public static function is_html_void_element( $tag_name ) {
* 'wp-thing--and--thang' => array( 'wp-thing', 'and--thang' )
*
* @param string $name The attribute name.
- * @return array The resulting array
+ * @return array The resulting array.
*/
public static function parse_attribute_name( $name ) {
return explode( '--', $name, 2 );
diff --git a/lib/experimental/interactivity-api/directive-processing.php b/lib/experimental/interactivity-api/directive-processing.php
index 064fc8ea62cbb2..075d31d577634c 100644
--- a/lib/experimental/interactivity-api/directive-processing.php
+++ b/lib/experimental/interactivity-api/directive-processing.php
@@ -1,16 +1,16 @@
'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();
+ // Parse our own block delimiters for interactive and non-interactive blocks.
+ $parsed_blocks = parse_blocks( $block_content );
+ $context = new WP_Directive_Context();
+ $processed_content = '';
+
+ foreach ( $parsed_blocks as $parsed_block ) {
+ if ( 'core/interactivity-wrapper' === $parsed_block['blockName'] ) {
+ $processed_content .= gutenberg_process_interactive_block( $parsed_block, $context );
+ } elseif ( 'core/non-interactivity-wrapper' === $parsed_block['blockName'] ) {
+ $processed_content .= gutenberg_process_non_interactive_block( $parsed_block, $context );
+ } else {
+ $processed_content .= $parsed_block['innerHTML'];
+ }
+ }
+ return $processed_content;
+ }
+
+ return $block_content;
+}
+add_filter( 'render_block', 'gutenberg_process_directives_in_root_blocks', 20, 2 );
+/**
+ * Marks the block as a children of an interactive block.
+ *
+ * @param array $parsed_block The parsed block.
+ * @param array $source_block The source block.
+ * @param WP_Block $parent_block The parent block.
+ */
+function gutenberg_mark_chidren_of_interactive_block( $parsed_block, $source_block, $parent_block ) {
+ if (
+ isset( $parent_block ) &&
+ isset( $parent_block->block_type->supports['interactivity'] ) &&
+ $parent_block->block_type->supports['interactivity']
+ ) {
+ WP_Directive_Processor::mark_children_of_interactive_block( $source_block );
}
+ return $parsed_block;
+}
+add_filter( 'render_block_data', 'gutenberg_mark_chidren_of_interactive_block', 100, 3 );
+/**
+ * Adds a comment delimiter to mark if the block is interactive or not.
+ *
+ * @param string $block_content The block content.
+ * @param array $block The full block, including name and attributes.
+ * @param WP_Block $block_instance The block instance.
+ */
+function gutenberg_mark_block_interactivity( $block_content, $block, $block_instance ) {
+ if (
+ isset( $block_instance->block_type->supports['interactivity'] ) &&
+ $block_instance->block_type->supports['interactivity']
+ ) {
+ // Wraps the interactive block with a comment delimiter to be able to
+ // process it later.
+ return get_comment_delimited_block_content(
+ 'core/interactivity-wrapper',
+ array(),
+ $block_content
+ );
+ } elseif ( WP_Directive_Processor::is_marked_as_children_of_interactive_block( $block ) ) {
+ // Wraps the non-interactive block with a comment delimiter to be able to
+ // skip it later.
+ return get_comment_delimited_block_content(
+ 'core/non-interactivity-wrapper',
+ array(),
+ $block_content
+ );
+ }
return $block_content;
}
-add_filter( 'render_block', 'gutenberg_process_directives_in_root_blocks', 10, 2 );
+add_filter( 'render_block', 'gutenberg_mark_block_interactivity', 10, 3 );
+
+/**
+ * Traverses the HTML of an interactive block, searching for Interactivity API
+ * directives and processing them. For the inner blocks, it calls the
+ * corresponding function depending on the wrapper type.
+ *
+ * @param array $interactive_block The interactive block to process.
+ * @param WP_Directive_Context $context The context to use when processing.
+ *
+ * @return string The processed HTML.
+ */
+function gutenberg_process_interactive_block( $interactive_block, $context ) {
+ $block_index = 0;
+ $content = '';
+ $interactive_inner_blocks = array();
+
+ foreach ( $interactive_block['innerContent'] as $inner_content ) {
+ if ( is_string( $inner_content ) ) {
+ $content .= $inner_content;
+ } else {
+ // This is an inner block. It may be an interactive block or a
+ // non-interactive block.
+ $content .= '
Welcome to WordPress. This is your first post. Edit or delete it, then start writing!
' . @@ -87,15 +103,11 @@ public function test_interactivity_process_directives_in_root_blocks() { 'Welcome to WordPress.
' . ''; - $parsed_block = parse_blocks( $block_content )[0]; - - $source_block = $parsed_block; - - $rendered_content = render_block( $parsed_block ); - + $parsed_block = parse_blocks( $block_content )[0]; + $source_block = $parsed_block; + $rendered_content = render_block( $parsed_block ); $parsed_block_second = parse_blocks( $block_content )[1]; - - $fake_parent_block = array(); + $fake_parent_block = array(); // Test that root block is intially emtpy. $this->assertEmpty( WP_Directive_Processor::$root_block ); @@ -117,16 +129,103 @@ public function test_interactivity_process_directives_in_root_blocks() { gutenberg_process_directives_in_root_blocks( $rendered_content, $parsed_block ); $this->assertEmpty( WP_Directive_Processor::$root_block ); } -} + public function test_directive_processing_of_interactive_block() { + $post_content = ''; + $rendered_blocks = do_blocks( $post_content ); + $p = new WP_HTML_Tag_Processor( $rendered_blocks ); + $p->next_tag( array( 'class_name' => 'level-1-input-1' ) ); + $value = $p->get_attribute( 'value' ); + $this->assertSame( 'level-1', $value ); + $p->next_tag( array( 'class_name' => 'level-1-input-2' ) ); + $value = $p->get_attribute( 'value' ); + $this->assertSame( 'level-1', $value ); + } + + public function test_directive_processing_two_interactive_blocks_at_same_level() { + $post_content = ''; + $rendered_blocks = do_blocks( $post_content ); + $p = new WP_HTML_Tag_Processor( $rendered_blocks ); + $p->next_tag( array( 'class_name' => 'level-1-input-1' ) ); + $value = $p->get_attribute( 'value' ); + $this->assertSame( 'level-1', $value ); + $p->next_tag( array( 'class_name' => 'level-1-input-2' ) ); + $value = $p->get_attribute( 'value' ); + $this->assertSame( 'level-1', $value ); + $p->next_tag( array( 'class_name' => 'level-2-input-1' ) ); + $value = $p->get_attribute( 'value' ); + $this->assertSame( 'level-2', $value ); + } + + public function test_directives_are_processed_at_tag_end() { + $post_content = ''; + $rendered_blocks = do_blocks( $post_content ); + $p = new WP_HTML_Tag_Processor( $rendered_blocks ); + $p->next_tag( array( 'class_name' => 'level-1-input-1' ) ); + $value = $p->get_attribute( 'value' ); + $this->assertSame( 'level-1', $value ); + $p->next_tag( array( 'class_name' => 'level-2-input-1' ) ); + $value = $p->get_attribute( 'value' ); + $this->assertSame( 'level-2', $value ); + $p->next_tag( array( 'class_name' => 'read-only-input-1' ) ); + $value = $p->get_attribute( 'value' ); + $this->assertSame( 'level-1', $value ); + $p->next_tag( array( 'class_name' => 'level-1-input-2' ) ); + $value = $p->get_attribute( 'value' ); + $this->assertSame( 'level-1', $value ); + } + + public function test_non_interactive_children_of_interactive_is_rendered() { + $post_content = 'Welcome
'; + $rendered_blocks = do_blocks( $post_content ); + $p = new WP_HTML_Tag_Processor( $rendered_blocks ); + $p->next_tag( array( 'class_name' => 'level-1-input-1' ) ); + $value = $p->get_attribute( 'value' ); + $this->assertSame( 'level-1', $value ); + $p->next_tag( array( 'class_name' => 'read-only-input-1' ) ); + $value = $p->get_attribute( 'value' ); + $this->assertSame( 'level-1', $value ); + $p->next_tag(); + $this->assertSame( 'P', $p->get_tag() ); + $p->next_tag( array( 'class_name' => 'level-1-input-2' ) ); + $value = $p->get_attribute( 'value' ); + $this->assertSame( 'level-1', $value ); + } + + public function test_non_interactive_blocks_are_not_processed() { + $post_content = ''; + $rendered_blocks = do_blocks( $post_content ); + $p = new WP_HTML_Tag_Processor( $rendered_blocks ); + $p->next_tag( array( 'class_name' => 'non-interactive-with-directive' ) ); + $value = $p->get_attribute( 'value' ); + $this->assertSame( null, $value ); + } + + public function test_non_interactive_blocks_with_manual_inner_block_rendering_are_not_processed() { + $post_content = ''; + $rendered_blocks = do_blocks( $post_content ); + $p = new WP_HTML_Tag_Processor( $rendered_blocks ); + $p->next_tag( array( 'class_name' => 'non-interactive-with-directive' ) ); + $value = $p->get_attribute( 'value' ); + $this->assertSame( null, $value ); + } + + public function test_directives_ordering() { + $post_content = ''; + $rendered_blocks = do_blocks( $post_content ); + $p = new WP_HTML_Tag_Processor( $rendered_blocks ); + $p->next_tag(); + + $value = $p->get_attribute( 'class' ); + $this->assertSame( 'other-class some-class', $value ); + + $value = $p->get_attribute( 'value' ); + $this->assertSame( 'some-value', $value ); + + $value = $p->get_attribute( 'style' ); + $this->assertSame( 'display: none;', $value ); + } -/** - * Tests for the gutenberg_interactivity_evaluate_reference function. - * - * @group interactivity-api - * @covers gutenberg_interactivity_evaluate_reference - */ -class Tests_Utils_Evaluate extends WP_UnitTestCase { public function test_evaluate_function_should_access_state() { // Init a simple store. wp_store( @@ -142,6 +241,7 @@ public function test_evaluate_function_should_access_state() { ), ) ); + $this->assertSame( 1, gutenberg_interactivity_evaluate_reference( 'state.core.number' ) ); $this->assertTrue( gutenberg_interactivity_evaluate_reference( 'state.core.bool' ) ); $this->assertSame( 'hi', gutenberg_interactivity_evaluate_reference( 'state.core.nested.string' ) ); @@ -158,10 +258,12 @@ public function test_evaluate_function_should_access_passed_context() { ), ), ); + $this->assertSame( 2, gutenberg_interactivity_evaluate_reference( 'context.local.number', $context ) ); $this->assertFalse( gutenberg_interactivity_evaluate_reference( 'context.local.bool', $context ) ); $this->assertTrue( gutenberg_interactivity_evaluate_reference( '!context.local.bool', $context ) ); $this->assertSame( 'bye', gutenberg_interactivity_evaluate_reference( 'context.local.nested.string', $context ) ); + // Previously defined state is also accessible. $this->assertSame( 1, gutenberg_interactivity_evaluate_reference( 'state.core.number' ) ); $this->assertTrue( gutenberg_interactivity_evaluate_reference( 'state.core.bool' ) ); @@ -174,7 +276,6 @@ public function test_evaluate_function_should_return_null_for_unresolved_paths() public function test_evaluate_function_should_execute_anonymous_functions() { $context = new WP_Directive_Context( array( 'count' => 2 ) ); - $helper = new Helper_Class(); wp_store( array( @@ -182,14 +283,13 @@ public function test_evaluate_function_should_execute_anonymous_functions() { 'count' => 3, ), 'selectors' => array( - 'anonymous_function' => function ( $store ) { + 'anonymous_function' => function ( $store ) { return $store['state']['count'] + $store['context']['count']; }, // Other types of callables should not be executed. - 'function_name' => 'gutenberg_test_process_directives_helper_increment', - 'class_method' => array( $helper, 'increment' ), - 'class_static_method' => 'Helper_Class::static_increment', - 'class_static_method_as_array' => array( 'Helper_Class', 'static_increment' ), + 'function_name' => 'gutenberg_test_process_directives_helper_increment', + 'class_method' => array( $this, 'increment' ), + 'class_static_method' => array( 'Tests_Process_Directives', 'static_increment' ), ), ) ); @@ -200,16 +300,12 @@ public function test_evaluate_function_should_execute_anonymous_functions() { gutenberg_interactivity_evaluate_reference( 'selectors.function_name', $context->get_context() ) ); $this->assertSame( - array( $helper, 'increment' ), + array( $this, 'increment' ), gutenberg_interactivity_evaluate_reference( 'selectors.class_method', $context->get_context() ) ); $this->assertSame( - 'Helper_Class::static_increment', + array( 'Tests_Process_Directives', 'static_increment' ), gutenberg_interactivity_evaluate_reference( 'selectors.class_static_method', $context->get_context() ) ); - $this->assertSame( - array( 'Helper_Class', 'static_increment' ), - gutenberg_interactivity_evaluate_reference( 'selectors.class_static_method_as_array', $context->get_context() ) - ); } } diff --git a/phpunit/experimental/interactivity-api/directives/wp-style-test.php b/phpunit/experimental/interactivity-api/directives/wp-style-test.php index 8942559b2fe89f..51468bd8a28141 100644 --- a/phpunit/experimental/interactivity-api/directives/wp-style-test.php +++ b/phpunit/experimental/interactivity-api/directives/wp-style-test.php @@ -14,16 +14,16 @@ */ class Tests_Directives_WpStyle extends WP_UnitTestCase { public function test_directive_adds_style() { - $markup = '