From e078397e56d56ed1fe21ce73941b9b3952fb15fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 10 Oct 2025 19:28:29 +0200 Subject: [PATCH] [Draft] Distinguish between block attributes and attribute fields in BlockMarkupProcessor --- .../class-blockmarkupprocessor.php | 233 +++++++++++++++--- .../class-blockmarkupurlprocessor.php | 4 +- .../Tests/BlockMarkupProcessorTest.php | 86 +++---- 3 files changed, 241 insertions(+), 82 deletions(-) diff --git a/components/DataLiberation/BlockMarkup/class-blockmarkupprocessor.php b/components/DataLiberation/BlockMarkup/class-blockmarkupprocessor.php index 9e0f92672..5f1f39e9f 100644 --- a/components/DataLiberation/BlockMarkup/class-blockmarkupprocessor.php +++ b/components/DataLiberation/BlockMarkup/class-blockmarkupprocessor.php @@ -73,16 +73,25 @@ class BlockMarkupProcessor extends WP_HTML_Tag_Processor { private $last_block_error; /** - * A flattened list of paths (arrays of keys) to every attribute found in - * $block_attributes. This is used by next_block_attribute() to traverse - * attributes without relying on PHP iterator classes. + * A list of names for every top-level attribute found in $block_attributes. + * This is used by next_block_attribute() to traverse attributes without relying + * on PHP iterator classes. + * + * @var array|null + */ + private $block_attribute_names = null; + + /** + * A flattened list of paths (arrays of keys) to every attribute field found in + * $block_attributes. Attribute fields are the nested values of attributes that hold + * arrays. * * @var array>|null */ - private $block_attribute_paths = null; + private $block_attribute_field_paths = null; /** - * The index of the current attribute inside $block_attribute_paths. + * The index of the current attribute inside $block_attribute_names. * Starts at -1 so that the first call to next_block_attribute() positions * the cursor at index 0. * @@ -90,6 +99,15 @@ class BlockMarkupProcessor extends WP_HTML_Tag_Processor { */ private $block_attribute_index = -1; + /** + * The index of the current attribute field inside $block_attribute_field_paths. + * Starts at -1 so that the first call to next_block_attribute_field() positions + * the cursor at index 0. + * + * @var int + */ + private $block_attribute_field_index = -1; + /** * Gets the type of the current token, adding a special '#block-comment' type * for WordPress block delimiters. @@ -254,6 +272,7 @@ public function set_block_attributes( $attributes ) { return false; } $this->block_attributes = $attributes; + $this->reset_block_attribute_iterators(); $this->block_attributes_updated = true; return true; @@ -294,10 +313,9 @@ public function next_token(): bool { } $this->get_updated_html(); - $this->block_name = null; - $this->block_attributes = null; - $this->block_attribute_paths = null; - $this->block_attribute_index = -1; + $this->block_name = null; + $this->block_attributes = null; + $this->reset_block_attribute_iterators(); $this->block_closer = false; $this->self_closing_flag = false; $this->block_attributes_updated = false; @@ -502,6 +520,51 @@ private function block_attribute_updates_to_modifiable_text_updates() { return true; } + /** + * Clears cached attribute traversal state so it can be rebuilt on demand. + * + * @return void + */ + private function reset_block_attribute_iterators() { + $this->block_attribute_names = null; + $this->block_attribute_field_paths = null; + $this->block_attribute_index = -1; + $this->block_attribute_field_index = -1; + } + + /** + * Builds the cached attribute traversal state when required. + * + * @return void + */ + private function ensure_block_attribute_iterators_built() { + if ( null !== $this->block_attribute_names ) { + return; + } + + $this->block_attribute_index = -1; + $this->block_attribute_field_index = -1; + + $block_attributes = $this->get_block_attributes(); + if ( ! is_array( $block_attributes ) ) { + $this->block_attribute_names = array(); + $this->block_attribute_field_paths = array(); + + return; + } + + $all_paths = $this->build_block_attribute_paths( $block_attributes ); + + $this->block_attribute_names = array_keys( $block_attributes ); + $this->block_attribute_field_paths = array(); + + foreach ( $all_paths as $path ) { + if ( count( $path ) > 1 ) { + $this->block_attribute_field_paths[] = $path; + } + } + } + /** * Advances to the next block attribute when a block is matched. * @@ -512,27 +575,21 @@ public function next_block_attribute() { return false; } - if ( null === $this->block_attribute_paths ) { - $block_attributes = $this->get_block_attributes(); - if ( ! is_array( $block_attributes ) ) { - return false; - } - - $this->block_attribute_paths = $this->build_block_attribute_paths( $block_attributes ); - $this->block_attribute_index = -1; - } + $this->ensure_block_attribute_iterators_built(); ++$this->block_attribute_index; - return isset( $this->block_attribute_paths[ $this->block_attribute_index ] ); + return isset( $this->block_attribute_names[ $this->block_attribute_index ] ); } - protected function get_block_attribute_path() { - if ( null === $this->block_attribute_paths || ! isset( $this->block_attribute_paths[ $this->block_attribute_index ] ) ) { + private function get_block_attribute_path() { + $this->ensure_block_attribute_iterators_built(); + + if ( ! isset( $this->block_attribute_names[ $this->block_attribute_index ] ) ) { return false; } - return $this->block_attribute_paths[ $this->block_attribute_index ]; + return array( $this->block_attribute_names[ $this->block_attribute_index ] ); } /** @@ -541,12 +598,12 @@ protected function get_block_attribute_path() { * @return string|false The attribute key or false if no attribute was matched */ public function get_block_attribute_key() { - if ( null === $this->block_attribute_paths || ! isset( $this->block_attribute_paths[ $this->block_attribute_index ] ) ) { + $path = $this->get_block_attribute_path(); + + if ( false === $path ) { return false; } - $path = $this->block_attribute_paths[ $this->block_attribute_index ]; - return $path[ count( $path ) - 1 ]; } @@ -556,11 +613,110 @@ public function get_block_attribute_key() { * @return mixed|false The attribute value or false if no attribute was matched */ public function get_block_attribute_value() { - if ( null === $this->block_attribute_paths || ! isset( $this->block_attribute_paths[ $this->block_attribute_index ] ) ) { + $path = $this->get_block_attribute_path(); + + if ( false === $path ) { + return false; + } + + return $this->get_value_for_block_attribute_path( $path ); + } + + /** + * Sets the value of the currently matched block attribute. + * + * @param mixed $new_value The new value to set + * + * @return bool Whether the value was successfully set + */ + public function set_block_attribute_value( $new_value ) { + $path = $this->get_block_attribute_path(); + + if ( false === $path ) { + return false; + } + + if ( 1 !== count( $path ) ) { + return false; + } + + return $this->set_value_for_block_attribute_path( $path, $new_value ); + } + + /** + * Advances to the next block attribute field when a block is matched. + * + * @return bool Whether we successfully advanced to the next attribute field. + */ + public function next_block_attribute_field() { + if ( '#block-comment' !== $this->get_token_type() ) { + return false; + } + + $this->ensure_block_attribute_iterators_built(); + + ++$this->block_attribute_field_index; + + return isset( $this->block_attribute_field_paths[ $this->block_attribute_field_index ] ); + } + + public function get_block_attribute_field_path() { + $this->ensure_block_attribute_iterators_built(); + + if ( ! isset( $this->block_attribute_field_paths[ $this->block_attribute_field_index ] ) ) { + return false; + } + + return $this->block_attribute_field_paths[ $this->block_attribute_field_index ]; + } + + /** + * Gets the value of the currently matched block attribute field. + * + * @return mixed|false The field value or false if no field was matched + */ + public function get_block_attribute_field_value() { + $path = $this->get_block_attribute_field_path(); + + if ( false === $path ) { + return false; + } + + return $this->get_value_for_block_attribute_path( $path ); + } + + /** + * Sets the value of the currently matched block attribute field. + * + * @param mixed $new_value The new value to set. + * + * @return bool Whether the value was successfully set. + */ + public function set_block_attribute_field_value( $new_value ) { + $path = $this->get_block_attribute_field_path(); + + if ( false === $path ) { + return false; + } + + if ( count( $path ) < 2 ) { + return false; + } + + return $this->set_value_for_block_attribute_path( $path, $new_value ); + } + + /** + * Retrieves a value from the current block attributes using the provided path. + * + * @param array $path The path to resolve. + * @return mixed|false The resolved value or false when the path is invalid. + */ + private function get_value_for_block_attribute_path( $path ) { + if ( empty( $path ) || ! is_array( $this->block_attributes ) ) { return false; } - $path = $this->block_attribute_paths[ $this->block_attribute_index ]; $value = $this->block_attributes; foreach ( $path as $segment ) { @@ -574,30 +730,33 @@ public function get_block_attribute_value() { } /** - * Sets the value of the currently matched block attribute. + * Updates a value within the current block attributes using the provided path. * - * @param mixed $new_value The new value to set - * - * @return bool Whether the value was successfully set + * @param array $path The path to update. + * @param mixed $new_value The new value to assign. + * @return bool True if the value was updated, false otherwise. */ - public function set_block_attribute_value( $new_value ) { - if ( null === $this->block_attribute_paths || ! isset( $this->block_attribute_paths[ $this->block_attribute_index ] ) ) { + private function set_value_for_block_attribute_path( $path, $new_value ) { + if ( empty( $path ) || ! is_array( $this->block_attributes ) ) { return false; } - $path = $this->block_attribute_paths[ $this->block_attribute_index ]; - $ref =& $this->block_attributes; $depth = count( $path ); + for ( $i = 0; $i < $depth - 1; $i++ ) { $segment = $path[ $i ]; if ( ! is_array( $ref ) || ! array_key_exists( $segment, $ref ) ) { - return false; // Path is invalid. + return false; } $ref =& $ref[ $segment ]; } - $last_key = $path[ $depth - 1 ]; + $last_key = $path[ $depth - 1 ]; + if ( ! is_array( $ref ) ) { + return false; + } + $ref[ $last_key ] = $new_value; $this->block_attributes_updated = true; diff --git a/components/DataLiberation/BlockMarkup/class-blockmarkupurlprocessor.php b/components/DataLiberation/BlockMarkup/class-blockmarkupurlprocessor.php index 4d3aa5e58..4e421ef0d 100644 --- a/components/DataLiberation/BlockMarkup/class-blockmarkupurlprocessor.php +++ b/components/DataLiberation/BlockMarkup/class-blockmarkupurlprocessor.php @@ -185,9 +185,7 @@ private function next_url_attribute() { private function next_url_block_attribute() { while ( $this->next_block_attribute() ) { $url_maybe = $this->get_block_attribute_value(); - if ( ! is_string( $url_maybe ) || - count( $this->get_block_attribute_path() ) > 1 - ) { + if ( ! is_string( $url_maybe ) ) { // @TODO: support arrays, objects, and other non-string data structures. continue; } diff --git a/components/DataLiberation/Tests/BlockMarkupProcessorTest.php b/components/DataLiberation/Tests/BlockMarkupProcessorTest.php index 69c070055..fb4d4c572 100644 --- a/components/DataLiberation/Tests/BlockMarkupProcessorTest.php +++ b/components/DataLiberation/Tests/BlockMarkupProcessorTest.php @@ -246,54 +246,59 @@ public function test_next_block_attribute_finds_the_second_attribute() { $this->assertEquals( 'New York City', $p->get_block_attribute_value(), 'Failed to find the block attribute value' ); } - public function test_next_block_attribute_finds_nested_attributes() { + public function test_next_block_attribute_field_finds_nested_fields() { $p = new BlockMarkupProcessor( '' ); $this->assertTrue( $p->next_token(), 'Failed to find the block opener' ); - $this->assertTrue( $p->next_block_attribute(), 'Failed to find the first block attribute' ); - $this->assertTrue( $p->next_block_attribute(), 'Failed to find the second block attribute' ); - $this->assertTrue( $p->next_block_attribute(), 'Failed to find the third block attribute' ); + $this->assertTrue( $p->next_block_attribute_field(), 'Failed to find the first block attribute field' ); - $this->assertEquals( 'lowres', $p->get_block_attribute_key(), 'Failed to find the block attribute name' ); - $this->assertEquals( 'small.png', $p->get_block_attribute_value(), 'Failed to find the block attribute value' ); + $this->assertEquals( array( 'sources', 'lowres' ), $p->get_block_attribute_field_path(), 'Failed to find the block attribute field path' ); + $this->assertEquals( 'small.png', $p->get_block_attribute_field_value(), 'Failed to find the block attribute field value' ); - $this->assertTrue( $p->next_block_attribute(), 'Failed to find the second block attribute' ); + $this->assertTrue( $p->next_block_attribute_field(), 'Failed to find the second block attribute field' ); - $this->assertEquals( 'hires', $p->get_block_attribute_key(), 'Failed to find the block attribute name' ); - $this->assertEquals( 'large.png', $p->get_block_attribute_value(), 'Failed to find the block attribute value' ); + $this->assertEquals( array( 'sources', 'hires' ), $p->get_block_attribute_field_path(), 'Failed to find the block attribute field path' ); + $this->assertEquals( 'large.png', $p->get_block_attribute_field_value(), 'Failed to find the block attribute field value' ); + $this->assertFalse( $p->next_block_attribute_field(), 'Returned true even though there was no next block attribute field' ); } - public function test_next_block_attribute_loops_over_lists() { + public function test_next_block_attribute_field_loops_over_lists() { $p = new BlockMarkupProcessor( '' ); $this->assertTrue( $p->next_token(), 'Failed to find the block opener' ); - $this->assertTrue( $p->next_block_attribute(), 'Failed to find the first block attribute' ); - $this->assertTrue( $p->next_block_attribute(), 'Failed to find the second block attribute' ); - $this->assertTrue( $p->next_block_attribute(), 'Failed to find the third block attribute' ); + $this->assertTrue( $p->next_block_attribute_field(), 'Failed to find the first block attribute field' ); - $this->assertEquals( 0, $p->get_block_attribute_key(), 'Failed to find the block attribute name' ); - $this->assertEquals( 'small.png', $p->get_block_attribute_value(), 'Failed to find the block attribute value' ); + $this->assertEquals( array( 'sources', 0 ), $p->get_block_attribute_field_path(), 'Failed to find the block attribute field path' ); + $this->assertEquals( 'small.png', $p->get_block_attribute_field_value(), 'Failed to find the block attribute field value' ); - $this->assertTrue( $p->next_block_attribute(), 'Failed to find the second block attribute' ); + $this->assertTrue( $p->next_block_attribute_field(), 'Failed to find the second block attribute field' ); - $this->assertEquals( 1, $p->get_block_attribute_key(), 'Failed to find the block attribute name' ); - $this->assertEquals( 'large.png', $p->get_block_attribute_value(), 'Failed to find the block attribute value' ); + $this->assertEquals( array( 'sources', 1 ), $p->get_block_attribute_field_path(), 'Failed to find the block attribute field path' ); + $this->assertEquals( 'large.png', $p->get_block_attribute_field_value(), 'Failed to find the block attribute field value' ); + $this->assertFalse( $p->next_block_attribute_field(), 'Returned true even though there was no next block attribute field' ); } - public function test_next_block_attribute_finds_top_level_attributes_after_nesting() { + public function test_next_block_attribute_finds_top_level_attributes_after_nested_fields() { $p = new BlockMarkupProcessor( '' ); $this->assertTrue( $p->next_token(), 'Failed to find the block opener' ); $this->assertTrue( $p->next_block_attribute(), 'Failed to find the first block attribute' ); + $this->assertEquals( 'sources', $p->get_block_attribute_key(), 'Failed to find the first block attribute name' ); $this->assertTrue( $p->next_block_attribute(), 'Failed to find the second block attribute' ); - $this->assertTrue( $p->next_block_attribute(), 'Failed to find the third block attribute' ); - $this->assertTrue( $p->next_block_attribute(), 'Failed to find the fourth block attribute' ); - $this->assertEquals( 'class', $p->get_block_attribute_key(), 'Failed to find the block attribute name' ); $this->assertEquals( 'wp-bold', $p->get_block_attribute_value(), 'Failed to find the block attribute value' ); + $this->assertFalse( $p->next_block_attribute(), 'Should not find more top level block attributes' ); + } + + public function test_next_block_attribute_field_returns_false_when_no_fields_exist() { + $p = new BlockMarkupProcessor( + '' + ); + $this->assertTrue( $p->next_token(), 'Failed to find the block opener' ); + $this->assertFalse( $p->next_block_attribute_field(), 'Returned true even though no block attribute fields exist' ); } public function test_set_block_attribute_value_updates_a_simple_attribute() { @@ -322,17 +327,16 @@ public function test_set_block_attribute_value_updates_affects_get_block_attribu $this->assertEquals( 'wp-italics', $p->get_block_attribute_value(), 'Failed to find the block attribute value' ); } - public function test_set_block_attribute_value_updates_a_nested_attribute() { + public function test_set_block_attribute_field_value_updates_a_nested_field() { $p = new BlockMarkupProcessor( '' ); $this->assertTrue( $p->next_token(), 'Failed to find the block opener' ); - $this->assertTrue( $p->next_block_attribute(), 'Failed to find the first block attribute' ); - $this->assertTrue( $p->next_block_attribute(), 'Failed to find the second block attribute' ); - $this->assertTrue( $p->next_block_attribute(), 'Failed to find the third block attribute' ); + $this->assertTrue( $p->next_block_attribute_field(), 'Failed to find the first block attribute field' ); + $this->assertTrue( $p->next_block_attribute_field(), 'Failed to find the second block attribute field' ); - $p->set_block_attribute_value( 'medium.png' ); - $this->assertEquals( 'medium.png', $p->get_block_attribute_value(), 'Failed to find the block attribute value' ); + $p->set_block_attribute_field_value( 'medium.png' ); + $this->assertEquals( 'medium.png', $p->get_block_attribute_field_value(), 'Failed to find the block attribute field value' ); $this->assertEquals( '', $p->get_updated_html(), @@ -340,17 +344,16 @@ public function test_set_block_attribute_value_updates_a_nested_attribute() { ); } - public function test_set_block_attribute_value_updates_a_list_value() { + public function test_set_block_attribute_field_value_updates_a_list_value() { $p = new BlockMarkupProcessor( '' ); $this->assertTrue( $p->next_token(), 'Failed to find the block opener' ); - $this->assertTrue( $p->next_block_attribute(), 'Failed to find the first block attribute' ); - $this->assertTrue( $p->next_block_attribute(), 'Failed to find the second block attribute' ); - $this->assertTrue( $p->next_block_attribute(), 'Failed to find the third block attribute' ); + $this->assertTrue( $p->next_block_attribute_field(), 'Failed to find the first block attribute field' ); + $this->assertTrue( $p->next_block_attribute_field(), 'Failed to find the second block attribute field' ); - $p->set_block_attribute_value( 'medium.png' ); - $this->assertEquals( 'medium.png', $p->get_block_attribute_value(), 'Failed to find the block attribute value' ); + $p->set_block_attribute_field_value( 'medium.png' ); + $this->assertEquals( 'medium.png', $p->get_block_attribute_field_value(), 'Failed to find the block attribute field value' ); $this->assertEquals( '', $p->get_updated_html(), @@ -358,21 +361,20 @@ public function test_set_block_attribute_value_updates_a_list_value() { ); } - public function test_set_block_attribute_can_be_called_multiple_times() { + public function test_set_block_attribute_field_can_be_called_multiple_times() { $p = new BlockMarkupProcessor( '' ); $this->assertTrue( $p->next_token(), 'Failed to find the block opener' ); - $this->assertTrue( $p->next_block_attribute(), 'Failed to find the first block attribute' ); - $this->assertTrue( $p->next_block_attribute(), 'Failed to find the second block attribute' ); - $this->assertTrue( $p->next_block_attribute(), 'Failed to find the third block attribute' ); + $this->assertTrue( $p->next_block_attribute_field(), 'Failed to find the first block attribute field' ); + $this->assertTrue( $p->next_block_attribute_field(), 'Failed to find the second block attribute field' ); - $p->set_block_attribute_value( 'medium.png' ); - $p->set_block_attribute_value( 'oh-completely-different-image.png' ); + $p->set_block_attribute_field_value( 'medium.png' ); + $p->set_block_attribute_field_value( 'oh-completely-different-image.png' ); $this->assertEquals( 'oh-completely-different-image.png', - $p->get_block_attribute_value(), - 'Failed to find the block attribute value' + $p->get_block_attribute_field_value(), + 'Failed to find the block attribute field value' ); $this->assertEquals( '',