From 1f9b753b325faaa94107e7e98a4b67132c91e620 Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+c4rl0sbr4v0@users.noreply.github.com> Date: Thu, 28 Dec 2023 13:24:51 +0100 Subject: [PATCH] Server directive processing: Stop processing non-interactive blocks (#56302) * Initial commit * It works, but is stripping comments * Added an extra return for debugging * Refactor to use string instead of arrays to compare * Use hidden textarea to save comments in production html * Use divs as delimiters, comments not working for interactive innner components * Back to array for references and comment delimiters * Something is working :-) * Refactor, process working * experiment replacing inner blocks * now working if there is only 1 interactive block * now working with 2 interactive blocks * Commented stopping at first custom element, after innerblocks is not being processed otherwise * Try using bookmarks, fix autoclosing tag not working * Not using bookmarks anymore, just processing inner inside processing function * Seems to be working, still needs a good test battery * Fix interactivity API directive processing * Remove tests that will be updated in next commits * Add markup tests * Use correct div group structure * Small refactor * remove not needed block name in interactive markers * Add more markup tests * refactor tests * Make inner blocks optional * Move directives declaration to declare them only if needed * Refactor more code, thanks to @darerodz * Remove only the first ocurrence * Improve comments and format * Update tests to use HTML API, fix non interactive blocks not being parsed * Unmark children of interactive blocks according to @DAreRodz comment * Add a p tag check test * Execute directives by priority * Remove gutenberg name from tests and use camelCase for context property * Fix empty style attribute error * Add test for directive ordering * Fix evaluate should only execute anonymous functions test * Fix wp-style tests * Improve tests * Test that we don't process non-interactive blocks * Stop unmarking children of interactive blocks * Move directives inside gutenberg_process_interactive_html --------- Co-authored-by: Luis Herranz Co-authored-by: David Arenas --- .../class-wp-directive-processor.php | 138 +++------ .../directive-processing.php | 282 ++++++++++++++++-- .../interactivity-api/directives/wp-style.php | 2 +- .../directive-processing-test.php | 274 +++++++++++------ .../directives/wp-style-test.php | 27 +- 5 files changed, 506 insertions(+), 217 deletions(-) diff --git a/lib/experimental/interactivity-api/class-wp-directive-processor.php b/lib/experimental/interactivity-api/class-wp-directive-processor.php index cf55a048bb9fa..bb70068aa9482 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 064fc8ea62cbb..075d31d577634 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 .= ''; + $interactive_inner_blocks[] = $interactive_block['innerBlocks'][ $block_index++ ]; + } + } + + return gutenberg_process_interactive_html( $content, $context, $interactive_inner_blocks ); +} + +/** + * Returns the HTML of a non-interactive block without processing the + * directives. For the inner blocks, it calls the corresponding function + * depending on the wrapper type. + * + * @param array $non_interactive_block The non-interactive block to process. + * @param WP_Directive_Context $context The context to use when processing. + * + * @return string The processed HTML. + */ +function gutenberg_process_non_interactive_block( $non_interactive_block, $context ) { + $block_index = 0; + $content = ''; + foreach ( $non_interactive_block['innerContent'] as $inner_content ) { + if ( is_string( $inner_content ) ) { + // This content belongs to a non interactive block and therefore it cannot + // contain directives. We add the HTML directly to the final output. + $content .= $inner_content; + } else { + // This is an inner block. It may be an interactive block or a + // non-interactive block. + $inner_block = $non_interactive_block['innerBlocks'][ $block_index++ ]; + + if ( 'core/interactivity-wrapper' === $inner_block['blockName'] ) { + $content .= gutenberg_process_interactive_block( $inner_block, $context ); + } elseif ( 'core/non-interactivity-wrapper' === $inner_block['blockName'] ) { + $content .= gutenberg_process_non_interactive_block( $inner_block, $context ); + } + } + } + return $content; +} + +/** + * Processes interactive HTML by applying directives to the HTML tags. + * + * It uses the WP_Directive_Processor class to parse the HTML and apply the + * directives. If a tag contains a 'WP-INNER-BLOCKS' string and there are inner + * blocks to process, the function processes these inner blocks and replaces the + * 'WP-INNER-BLOCKS' tag in the HTML with those blocks. + * + * @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. + * + * @return string The processed HTML. + */ +function gutenberg_process_interactive_html( $html, $context, $inner_blocks = 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', + ); + + $tags = new WP_Directive_Processor( $html ); + $prefix = 'data-wp-'; + $tag_stack = array(); + $inner_processed_blocks = array(); + $inner_blocks_index = 0; + while ( $tags->next_tag( array( 'tag_closers' => 'visit' ) ) ) { + $tag_name = $tags->get_tag(); + + // 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 ); + } 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 ); + } + } + 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 ) = $tags::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 ( + ! $tags::is_html_void_element( $tag_name ) && + ( 0 !== count( $attributes ) || 0 !== count( $tag_stack ) ) + ) { + $tag_stack[] = array( $tag_name, $attributes ); + } + } + + // Extract all directive names. They'll be used later on. + $directive_names = array_keys( $directives ); + $directive_names_rev = array_reverse( $directive_names ); + + /* + * Sort attributes by the order they appear in the `$directives` + * argument, considering it as the priority order in which + * directives should be processed. Note that the order is reversed + * for tag closers. + */ + $sorted_attrs = array_intersect( + $tags->is_tag_closer() + ? $directive_names_rev + : $directive_names, + $attributes + ); + + foreach ( $sorted_attrs as $attribute ) { + call_user_func( $directives[ $attribute ], $tags, $context ); + } + } + + $processed_html = $tags->get_updated_html(); + + // Replaces the inner block tags with the content of each inner block + // processed. + if ( ! empty( $inner_processed_blocks ) ) { + foreach ( $inner_processed_blocks as $inner_block_tag => $inner_block_content ) { + if ( str_contains( $processed_html, $inner_block_tag ) ) { + $processed_html = str_replace( '<' . $inner_block_tag . '>', $inner_block_content, $processed_html ); + } + } + } + return $processed_html; +} /** - * Resolve the reference using the store and the context from the provided path. + * Resolves the reference using the store and the context from the provided + * path. * * @param string $path Path. * @param array $context Context data. @@ -71,15 +304,14 @@ function gutenberg_interactivity_evaluate_reference( $path, array $context = arr ); /* - * Check first if the directive path is preceded by a negator operator (!), + * Checks first if the directive path is preceded by a negator operator (!), * indicating that the value obtained from the Interactivity Store (or the * passed context) using the subsequent path should be negated. */ $should_negate_value = '!' === $path[0]; - - $path = $should_negate_value ? substr( $path, 1 ) : $path; - $path_segments = explode( '.', $path ); - $current = $store; + $path = $should_negate_value ? substr( $path, 1 ) : $path; + $path_segments = explode( '.', $path ); + $current = $store; foreach ( $path_segments as $p ) { if ( isset( $current[ $p ] ) ) { $current = $current[ $p ]; @@ -89,7 +321,7 @@ function gutenberg_interactivity_evaluate_reference( $path, array $context = arr } /* - * Check if $current is an anonymous function or an arrow function, and if + * Checks if $current is an anonymous function or an arrow function, and if * so, call it passing the store. Other types of callables are ignored on * purpose, as arbitrary strings or arrays could be wrongly evaluated as * "callables". @@ -100,6 +332,6 @@ function gutenberg_interactivity_evaluate_reference( $path, array $context = arr $current = call_user_func( $current, $store ); } - // Return the opposite if it has a negator operator (!). + // Returns the opposite if it has a negator operator (!). return $should_negate_value ? ! $current : $current; } diff --git a/lib/experimental/interactivity-api/directives/wp-style.php b/lib/experimental/interactivity-api/directives/wp-style.php index 9c37f9082c2c0..e5d7b269ace7c 100644 --- a/lib/experimental/interactivity-api/directives/wp-style.php +++ b/lib/experimental/interactivity-api/directives/wp-style.php @@ -28,7 +28,7 @@ function gutenberg_interactivity_process_wp_style( $tags, $context ) { $expr = $tags->get_attribute( $attr ); $style_value = gutenberg_interactivity_evaluate_reference( $expr, $context->get_context() ); if ( $style_value ) { - $style_attr = $tags->get_attribute( 'style' ); + $style_attr = $tags->get_attribute( 'style' ) ?? ''; $style_attr = gutenberg_interactivity_set_style( $style_attr, $style_name, $style_value ); $tags->set_attribute( 'style', $style_attr ); } else { diff --git a/phpunit/experimental/interactivity-api/directive-processing-test.php b/phpunit/experimental/interactivity-api/directive-processing-test.php index 46ef0284df15d..99218f69a8fb7 100644 --- a/phpunit/experimental/interactivity-api/directive-processing-test.php +++ b/phpunit/experimental/interactivity-api/directive-processing-test.php @@ -4,81 +4,97 @@ * * @package Gutenberg * @subpackage Interactivity API - * - * @phpcs:disable Generic.Files.OneObjectStructurePerFile.MultipleFound */ +class Tests_Process_Directives extends WP_UnitTestCase { + public function set_up() { + parent::set_up(); -class Helper_Class { - // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable - public function process_foo_test( $tags, $context ) { - } - - public function increment( $store ) { - return $store['state']['count'] + $store['context']['count']; - } - - public static function static_increment( $store ) { - return $store['state']['count'] + $store['context']['count']; - } -} + register_block_type( + 'test/context-level-1', + array( + 'render_callback' => function ( $attributes, $content ) { + return '
' . $content . '
'; + }, + 'supports' => array( + 'interactivity' => true, + ), + ) + ); -function gutenberg_test_process_directives_helper_increment( $store ) { - return $store['state']['count'] + $store['context']['count']; -} + register_block_type( + 'test/context-level-2', + array( + 'render_callback' => function ( $attributes, $content ) { + return '
' . $content . '
'; + }, + 'supports' => array( + 'interactivity' => true, + ), + ) + ); -/** - * Tests for the gutenberg_interactivity_process_rendered_html function. - * - * @group interactivity-api - * @covers gutenberg_interactivity_process_rendered_html - */ -class Tests_Process_Directives extends WP_UnitTestCase { - public function test_correctly_call_attribute_directive_processor_on_closing_tag() { - - // PHPUnit cannot stub functions, only classes. - $test_helper = $this->createMock( Helper_Class::class ); - - $test_helper->expects( $this->exactly( 2 ) ) - ->method( 'process_foo_test' ) - ->with( - $this->callback( - function ( $p ) { - return 'DIV' === $p->get_tag() && ( - // Either this is a closing tag... - $p->is_tag_closer() || - // ...or it is an open tag, and has the directive attribute set. - ( ! $p->is_tag_closer() && 'abc' === $p->get_attribute( 'foo-test' ) ) - ); - } - ) - ); - - $directives = array( - 'foo-test' => array( $test_helper, 'process_foo_test' ), + register_block_type( + 'test/context-read-only', + array( + 'render_callback' => function () { + return '
'; + }, + 'supports' => array( + 'interactivity' => true, + ), + ) ); - $markup = '
Example:
This is a test>
Here is a nested div
'; - $tags = new WP_Directive_Processor( $markup ); - $tags->process_rendered_html( $tags, 'foo-', $directives ); - } + register_block_type( + 'test/non-interactive-with-directive', + array( + 'render_callback' => function () { + return ''; + }, + ) + ); - public function test_directives_with_double_hyphen_processed_correctly() { - $test_helper = $this->createMock( Helper_Class::class ); - $test_helper->expects( $this->atLeastOnce() ) - ->method( 'process_foo_test' ); + register_block_type( + 'test/context-level-with-manual-inner-block-rendering', + array( + 'render_callback' => function ( $attributes, $content, $block ) { + $inner_blocks_html = ''; + foreach ( $block->inner_blocks as $inner_block ) { + $inner_blocks_html .= $inner_block->render(); + } + return '
' . $inner_blocks_html . '
'; + }, + 'supports' => array( + 'interactivity' => true, + ), + ) + ); - $directives = array( - 'foo-test' => array( $test_helper, 'process_foo_test' ), + register_block_type( + 'test/directives-ordering', + array( + 'render_callback' => function () { + return ''; + }, + 'supports' => array( + 'interactivity' => true, + ), + ) ); + } - $markup = '
'; - $tags = new WP_Directive_Processor( $markup ); - $tags->process_rendered_html( $tags, 'foo-', $directives ); + public function tear_down() { + unregister_block_type( 'test/context-level-1' ); + unregister_block_type( 'test/context-level-2' ); + unregister_block_type( 'test/context-read-only' ); + unregister_block_type( 'test/non-interactive-with-directive' ); + unregister_block_type( 'test/context-level-with-manual-inner-block-rendering' ); + unregister_block_type( 'test/directives-ordering' ); + parent::tear_down(); } public function test_interactivity_process_directives_in_root_blocks() { - $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 8942559b2fe89..51468bd8a2814 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 = '
Test
'; + $markup = '
Test
'; $tags = new WP_HTML_Tag_Processor( $markup ); $tags->next_tag(); - $context_before = new WP_Directive_Context( array( 'myblock' => array( 'color' => 'green' ) ) ); + $context_before = new WP_Directive_Context( array( 'color' => 'green' ) ); $context = $context_before; gutenberg_interactivity_process_wp_style( $tags, $context ); $this->assertSame( - '
Test
', + '
Test
', $tags->get_updated_html() ); $this->assertStringContainsString( 'color: green;', $tags->get_attribute( 'style' ) ); @@ -31,11 +31,11 @@ public function test_directive_adds_style() { } public function test_directive_ignores_empty_style() { - $markup = '
Test
'; + $markup = '
Test
'; $tags = new WP_HTML_Tag_Processor( $markup ); $tags->next_tag(); - $context_before = new WP_Directive_Context( array( 'myblock' => array( 'color' => 'green' ) ) ); + $context_before = new WP_Directive_Context( array( 'color' => 'green' ) ); $context = $context_before; gutenberg_interactivity_process_wp_style( $tags, $context ); @@ -43,4 +43,21 @@ public function test_directive_ignores_empty_style() { $this->assertStringNotContainsString( 'color: green;', $tags->get_attribute( 'style' ) ); $this->assertSame( $context_before->get_context(), $context->get_context(), 'data-wp-style directive changed context' ); } + + public function test_directive_works_without_style_attribute() { + $markup = '
Test
'; + $tags = new WP_HTML_Tag_Processor( $markup ); + $tags->next_tag(); + + $context_before = new WP_Directive_Context( array( 'color' => 'green' ) ); + $context = $context_before; + gutenberg_interactivity_process_wp_style( $tags, $context ); + + $this->assertSame( + '
Test
', + $tags->get_updated_html() + ); + $this->assertSame( 'color: green;', $tags->get_attribute( 'style' ) ); + $this->assertSame( $context_before->get_context(), $context->get_context(), 'data-wp-style directive changed context' ); + } }