diff --git a/lib/experimental/interactivity-api/class-wp-directive-processor.php b/lib/experimental/interactivity-api/class-wp-directive-processor.php index bb70068aa9482..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; } @@ -256,4 +268,43 @@ 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 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 new file mode 100644 index 0000000000000..15e57edfa4a6a --- /dev/null +++ b/lib/experimental/interactivity-api/class-wp-interactivity-initial-state.php @@ -0,0 +1,82 @@ +%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/directive-processing.php b/lib/experimental/interactivity-api/directive-processing.php index 075d31d577634..b49ee538390ff 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,16 +187,18 @@ 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', - 'data-wp-class' => 'gutenberg_interactivity_process_wp_class', - 'data-wp-style' => 'gutenberg_interactivity_process_wp_style', - 'data-wp-text' => 'gutenberg_interactivity_process_wp_text', + '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 ); @@ -207,9 +212,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() ) { @@ -270,7 +275,15 @@ function gutenberg_process_interactive_html( $html, $context, $inner_blocks = ar ); foreach ( $sorted_attrs as $attribute ) { - call_user_func( $directives[ $attribute ], $tags, $context ); + call_user_func_array( + $directives[ $attribute ], + array( + $tags, + $context, + end( $namespace_stack ), + &$namespace_stack, + ) + ); } } @@ -290,17 +303,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 $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( $path, array $context = array() ) { - $store = array_merge( - WP_Interactivity_Store::get_data(), - array( 'context' => $context ) +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(), ); /* @@ -329,7 +350,12 @@ function gutenberg_interactivity_evaluate_reference( $path, array $context = arr * E.g., "file" is an string and a "callable" (the "file" function exists). */ if ( $current instanceof Closure ) { - $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/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..b41b47c86c78c 100644 --- a/lib/experimental/interactivity-api/directives/wp-context.php +++ b/lib/experimental/interactivity-api/directives/wp-context.php @@ -10,19 +10,21 @@ * * @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 ) = is_string( $attr_value ) && ! empty( $attr_value ) + ? WP_Directive_Processor::parse_attribute_value( $attr_value, $ns ) + : array( $ns, null ); - $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-interactive.php b/lib/experimental/interactivity-api/directives/wp-interactive.php new file mode 100644 index 0000000000000..9f3471a8b4e6a --- /dev/null +++ b/lib/experimental/interactivity-api/directives/wp-interactive.php @@ -0,0 +1,44 @@ +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 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 ) ); } 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' ) ); + } } diff --git a/phpunit/experimental/interactivity-api/directives/wp-bind-test.php b/phpunit/experimental/interactivity-api/directives/wp-bind-test.php index bfb4c428cd946..8fe212bb8ed93 100644 --- a/phpunit/experimental/interactivity-api/directives/wp-bind-test.php +++ b/phpunit/experimental/interactivity-api/directives/wp-bind-test.php @@ -14,16 +14,17 @@ */ class Tests_Directives_WpBind extends WP_UnitTestCase { public function test_directive_sets_attribute() { - $markup = ''; + $markup = ''; $tags = new WP_HTML_Tag_Processor( $markup ); $tags->next_tag(); $context_before = new WP_Directive_Context( array( 'myblock' => array( 'imageSource' => './wordpress.png' ) ) ); $context = $context_before; - gutenberg_interactivity_process_wp_bind( $tags, $context ); + $directive_ns = 'myblock'; + gutenberg_interactivity_process_wp_bind( $tags, $context, $directive_ns ); $this->assertSame( - '', + '', $tags->get_updated_html() ); $this->assertSame( './wordpress.png', $tags->get_attribute( 'src' ) ); @@ -31,13 +32,14 @@ public function test_directive_sets_attribute() { } public function test_directive_ignores_empty_bound_attribute() { - $markup = ''; + $markup = ''; $tags = new WP_HTML_Tag_Processor( $markup ); $tags->next_tag(); $context_before = new WP_Directive_Context( array( 'myblock' => array( 'imageSource' => './wordpress.png' ) ) ); $context = $context_before; - gutenberg_interactivity_process_wp_bind( $tags, $context ); + $directive_ns = 'myblock'; + gutenberg_interactivity_process_wp_bind( $tags, $context, $directive_ns ); $this->assertSame( $markup, $tags->get_updated_html() ); $this->assertNull( $tags->get_attribute( 'src' ) ); diff --git a/phpunit/experimental/interactivity-api/directives/wp-class-test.php b/phpunit/experimental/interactivity-api/directives/wp-class-test.php index 419546c6d9ef8..f40486647ff8b 100644 --- a/phpunit/experimental/interactivity-api/directives/wp-class-test.php +++ b/phpunit/experimental/interactivity-api/directives/wp-class-test.php @@ -14,16 +14,17 @@ */ class Tests_Directives_WpClass extends WP_UnitTestCase { public function test_directive_adds_class() { - $markup = '
Test
'; + $markup = '
Test
'; $tags = new WP_HTML_Tag_Processor( $markup ); $tags->next_tag(); $context_before = new WP_Directive_Context( array( 'myblock' => array( 'isRed' => true ) ) ); $context = $context_before; - gutenberg_interactivity_process_wp_class( $tags, $context ); + $directive_ns = 'myblock'; + gutenberg_interactivity_process_wp_class( $tags, $context, $directive_ns ); $this->assertSame( - '
Test
', + '
Test
', $tags->get_updated_html() ); $this->assertStringContainsString( 'red', $tags->get_attribute( 'class' ) ); @@ -31,16 +32,17 @@ public function test_directive_adds_class() { } public function test_directive_removes_class() { - $markup = '
Test
'; + $markup = '
Test
'; $tags = new WP_HTML_Tag_Processor( $markup ); $tags->next_tag(); $context_before = new WP_Directive_Context( array( 'myblock' => array( 'isBlue' => false ) ) ); $context = $context_before; - gutenberg_interactivity_process_wp_class( $tags, $context ); + $directive_ns = 'myblock'; + gutenberg_interactivity_process_wp_class( $tags, $context, $directive_ns ); $this->assertSame( - '
Test
', + '
Test
', $tags->get_updated_html() ); $this->assertStringNotContainsString( 'blue', $tags->get_attribute( 'class' ) ); @@ -48,17 +50,18 @@ public function test_directive_removes_class() { } public function test_directive_removes_empty_class_attribute() { - $markup = '
Test
'; + $markup = '
Test
'; $tags = new WP_HTML_Tag_Processor( $markup ); $tags->next_tag(); $context_before = new WP_Directive_Context( array( 'myblock' => array( 'isBlue' => false ) ) ); $context = $context_before; - gutenberg_interactivity_process_wp_class( $tags, $context ); + $directive_ns = 'myblock'; + gutenberg_interactivity_process_wp_class( $tags, $context, $directive_ns ); $this->assertSame( // WP_HTML_Tag_Processor has a TODO note to prune whitespace after classname removal. - '
Test
', + '
Test
', $tags->get_updated_html() ); $this->assertNull( $tags->get_attribute( 'class' ) ); @@ -66,16 +69,17 @@ public function test_directive_removes_empty_class_attribute() { } public function test_directive_does_not_remove_non_existant_class() { - $markup = '
Test
'; + $markup = '
Test
'; $tags = new WP_HTML_Tag_Processor( $markup ); $tags->next_tag(); $context_before = new WP_Directive_Context( array( 'myblock' => array( 'isBlue' => false ) ) ); $context = $context_before; - gutenberg_interactivity_process_wp_class( $tags, $context ); + $directive_ns = 'myblock'; + gutenberg_interactivity_process_wp_class( $tags, $context, $directive_ns ); $this->assertSame( - '
Test
', + '
Test
', $tags->get_updated_html() ); $this->assertSame( 'green red', $tags->get_attribute( 'class' ) ); @@ -83,13 +87,14 @@ public function test_directive_does_not_remove_non_existant_class() { } public function test_directive_ignores_empty_class_name() { - $markup = '
Test
'; + $markup = '
Test
'; $tags = new WP_HTML_Tag_Processor( $markup ); $tags->next_tag(); $context_before = new WP_Directive_Context( array( 'myblock' => array( 'isRed' => true ) ) ); $context = $context_before; - gutenberg_interactivity_process_wp_class( $tags, $context ); + $directive_ns = 'myblock'; + gutenberg_interactivity_process_wp_class( $tags, $context, $directive_ns ); $this->assertSame( $markup, $tags->get_updated_html() ); $this->assertStringNotContainsString( 'red', $tags->get_attribute( 'class' ) ); diff --git a/phpunit/experimental/interactivity-api/directives/wp-context-test.php b/phpunit/experimental/interactivity-api/directives/wp-context-test.php index 1277b016848cc..788feec95fe7c 100644 --- a/phpunit/experimental/interactivity-api/directives/wp-context-test.php +++ b/phpunit/experimental/interactivity-api/directives/wp-context-test.php @@ -21,11 +21,12 @@ public function test_directive_merges_context_correctly_upon_wp_context_attribut ) ); - $markup = '
'; + $ns = 'myblock'; + $markup = '
'; $tags = new WP_HTML_Tag_Processor( $markup ); $tags->next_tag(); - gutenberg_interactivity_process_wp_context( $tags, $context ); + gutenberg_interactivity_process_wp_context( $tags, $context, $ns ); $this->assertSame( array( @@ -38,39 +39,39 @@ public function test_directive_merges_context_correctly_upon_wp_context_attribut public function test_directive_resets_context_correctly_upon_closing_tag() { $context = new WP_Directive_Context( - array( 'my-key' => 'original-value' ) + array( 'myblock' => array( 'my-key' => 'original-value' ) ) ); $context->set_context( - array( 'my-key' => 'new-value' ) + array( 'myblock' => array( 'my-key' => 'new-value' ) ) ); $markup = '
'; $tags = new WP_HTML_Tag_Processor( $markup ); $tags->next_tag( array( 'tag_closers' => 'visit' ) ); - gutenberg_interactivity_process_wp_context( $tags, $context ); + gutenberg_interactivity_process_wp_context( $tags, $context, 'myblock' ); $this->assertSame( array( 'my-key' => 'original-value' ), - $context->get_context() + $context->get_context()['myblock'] ); } public function test_directive_doesnt_throw_on_malformed_context_objects() { $context = new WP_Directive_Context( - array( 'my-key' => 'some-value' ) + array( 'myblock' => array( 'my-key' => 'some-value' ) ) ); $markup = '
'; $tags = new WP_HTML_Tag_Processor( $markup ); $tags->next_tag(); - gutenberg_interactivity_process_wp_context( $tags, $context ); + gutenberg_interactivity_process_wp_context( $tags, $context, 'myblock' ); $this->assertSame( array( 'my-key' => 'some-value' ), - $context->get_context() + $context->get_context()['myblock'] ); } @@ -87,36 +88,36 @@ public function test_directive_keeps_working_after_malformed_context_objects() { // Parent div. $tags->next_tag( array( 'tag_closers' => 'visit' ) ); - gutenberg_interactivity_process_wp_context( $tags, $context ); + gutenberg_interactivity_process_wp_context( $tags, $context, 'myblock' ); $this->assertSame( array( 'my-key' => 'some-value' ), - $context->get_context() + $context->get_context()['myblock'] ); // Children div. $tags->next_tag( array( 'tag_closers' => 'visit' ) ); - gutenberg_interactivity_process_wp_context( $tags, $context ); + gutenberg_interactivity_process_wp_context( $tags, $context, 'myblock' ); // Still the same context. $this->assertSame( array( 'my-key' => 'some-value' ), - $context->get_context() + $context->get_context()['myblock'] ); // Closing children div. $tags->next_tag( array( 'tag_closers' => 'visit' ) ); - gutenberg_interactivity_process_wp_context( $tags, $context ); + gutenberg_interactivity_process_wp_context( $tags, $context, 'myblock' ); // Still the same context. $this->assertSame( array( 'my-key' => 'some-value' ), - $context->get_context() + $context->get_context()['myblock'] ); // Closing parent div. $tags->next_tag( array( 'tag_closers' => 'visit' ) ); - gutenberg_interactivity_process_wp_context( $tags, $context ); + gutenberg_interactivity_process_wp_context( $tags, $context, 'myblock' ); // Now the context is empty. $this->assertSame( @@ -138,36 +139,36 @@ public function test_directive_keeps_working_with_a_directive_without_value() { // Parent div. $tags->next_tag( array( 'tag_closers' => 'visit' ) ); - gutenberg_interactivity_process_wp_context( $tags, $context ); + gutenberg_interactivity_process_wp_context( $tags, $context, 'myblock' ); $this->assertSame( array( 'my-key' => 'some-value' ), - $context->get_context() + $context->get_context()['myblock'] ); // Children div. $tags->next_tag( array( 'tag_closers' => 'visit' ) ); - gutenberg_interactivity_process_wp_context( $tags, $context ); + gutenberg_interactivity_process_wp_context( $tags, $context, 'myblock' ); // Still the same context. $this->assertSame( array( 'my-key' => 'some-value' ), - $context->get_context() + $context->get_context()['myblock'] ); // Closing children div. $tags->next_tag( array( 'tag_closers' => 'visit' ) ); - gutenberg_interactivity_process_wp_context( $tags, $context ); + gutenberg_interactivity_process_wp_context( $tags, $context, 'myblock' ); // Still the same context. $this->assertSame( array( 'my-key' => 'some-value' ), - $context->get_context() + $context->get_context()['myblock'] ); // Closing parent div. $tags->next_tag( array( 'tag_closers' => 'visit' ) ); - gutenberg_interactivity_process_wp_context( $tags, $context ); + gutenberg_interactivity_process_wp_context( $tags, $context, 'myblock' ); // Now the context is empty. $this->assertSame( @@ -189,36 +190,36 @@ public function test_directive_keeps_working_with_an_empty_directive() { // Parent div. $tags->next_tag( array( 'tag_closers' => 'visit' ) ); - gutenberg_interactivity_process_wp_context( $tags, $context ); + gutenberg_interactivity_process_wp_context( $tags, $context, 'myblock' ); $this->assertSame( array( 'my-key' => 'some-value' ), - $context->get_context() + $context->get_context()['myblock'] ); // Children div. $tags->next_tag( array( 'tag_closers' => 'visit' ) ); - gutenberg_interactivity_process_wp_context( $tags, $context ); + gutenberg_interactivity_process_wp_context( $tags, $context, 'myblock' ); // Still the same context. $this->assertSame( array( 'my-key' => 'some-value' ), - $context->get_context() + $context->get_context()['myblock'] ); // Closing children div. $tags->next_tag( array( 'tag_closers' => 'visit' ) ); - gutenberg_interactivity_process_wp_context( $tags, $context ); + gutenberg_interactivity_process_wp_context( $tags, $context, 'myblock' ); // Still the same context. $this->assertSame( array( 'my-key' => 'some-value' ), - $context->get_context() + $context->get_context()['myblock'] ); // Closing parent div. $tags->next_tag( array( 'tag_closers' => 'visit' ) ); - gutenberg_interactivity_process_wp_context( $tags, $context ); + gutenberg_interactivity_process_wp_context( $tags, $context, 'myblock' ); // Now the context is empty. $this->assertSame( diff --git a/phpunit/experimental/interactivity-api/directives/wp-style-test.php b/phpunit/experimental/interactivity-api/directives/wp-style-test.php index 51468bd8a2814..9625803ebca78 100644 --- a/phpunit/experimental/interactivity-api/directives/wp-style-test.php +++ b/phpunit/experimental/interactivity-api/directives/wp-style-test.php @@ -18,9 +18,9 @@ public function test_directive_adds_style() { $tags = new WP_HTML_Tag_Processor( $markup ); $tags->next_tag(); - $context_before = new WP_Directive_Context( array( 'color' => 'green' ) ); + $context_before = new WP_Directive_Context( array( 'myblock' => array( 'color' => 'green' ) ) ); $context = $context_before; - gutenberg_interactivity_process_wp_style( $tags, $context ); + gutenberg_interactivity_process_wp_style( $tags, $context, 'myblock' ); $this->assertSame( '
Test
', @@ -35,9 +35,9 @@ public function test_directive_ignores_empty_style() { $tags = new WP_HTML_Tag_Processor( $markup ); $tags->next_tag(); - $context_before = new WP_Directive_Context( array( 'color' => 'green' ) ); + $context_before = new WP_Directive_Context( array( 'myblock' => array( 'color' => 'green' ) ) ); $context = $context_before; - gutenberg_interactivity_process_wp_style( $tags, $context ); + gutenberg_interactivity_process_wp_style( $tags, $context, 'myblock' ); $this->assertSame( $markup, $tags->get_updated_html() ); $this->assertStringNotContainsString( 'color: green;', $tags->get_attribute( 'style' ) ); @@ -49,9 +49,9 @@ public function test_directive_works_without_style_attribute() { $tags = new WP_HTML_Tag_Processor( $markup ); $tags->next_tag(); - $context_before = new WP_Directive_Context( array( 'color' => 'green' ) ); + $context_before = new WP_Directive_Context( array( 'myblock' => array( 'color' => 'green' ) ) ); $context = $context_before; - gutenberg_interactivity_process_wp_style( $tags, $context ); + gutenberg_interactivity_process_wp_style( $tags, $context, 'myblock' ); $this->assertSame( '
Test
', diff --git a/phpunit/experimental/interactivity-api/directives/wp-text-test.php b/phpunit/experimental/interactivity-api/directives/wp-text-test.php index 81d2d0f370a64..9c889a3f0eb68 100644 --- a/phpunit/experimental/interactivity-api/directives/wp-text-test.php +++ b/phpunit/experimental/interactivity-api/directives/wp-text-test.php @@ -14,31 +14,31 @@ */ class Tests_Directives_WpText extends WP_UnitTestCase { public function test_directive_sets_inner_html_based_on_attribute_value_and_escapes_html() { - $markup = '
'; + $markup = '
'; $tags = new WP_Directive_Processor( $markup ); $tags->next_tag(); $context_before = new WP_Directive_Context( array( 'myblock' => array( 'someText' => 'The HTML tag
produces a line break.' ) ) ); $context = clone $context_before; - gutenberg_interactivity_process_wp_text( $tags, $context ); + gutenberg_interactivity_process_wp_text( $tags, $context, 'myblock' ); - $expected_markup = '
The HTML tag <br> produces a line break.
'; + $expected_markup = '
The HTML tag <br> produces a line break.
'; $this->assertSame( $expected_markup, $tags->get_updated_html() ); $this->assertSame( $context_before->get_context(), $context->get_context(), 'data-wp-text directive changed context' ); } public function test_directive_overwrites_inner_html_based_on_attribute_value() { - $markup = '
Lorem ipsum dolor sit.
'; + $markup = '
Lorem ipsum dolor sit.
'; $tags = new WP_Directive_Processor( $markup ); $tags->next_tag(); $context_before = new WP_Directive_Context( array( 'myblock' => array( 'someText' => 'Honi soit qui mal y pense.' ) ) ); $context = clone $context_before; - gutenberg_interactivity_process_wp_text( $tags, $context ); + gutenberg_interactivity_process_wp_text( $tags, $context, 'myblock' ); - $expected_markup = '
Honi soit qui mal y pense.
'; + $expected_markup = '
Honi soit qui mal y pense.
'; $this->assertSame( $expected_markup, $tags->get_updated_html() ); $this->assertSame( $context_before->get_context(), $context->get_context(), 'data-wp-text directive changed context' ); }