From e89e279e0328eca38cd9cc4baff36e042afe3373 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Greg=20Zi=C3=83=C2=B3=C3=85=E2=80=9Akowski?= Date: Mon, 2 Dec 2024 08:08:34 +0000 Subject: [PATCH] Interactivity API: Support length property on strings and arrays on the server The Interactivity API tries to align client and server rendering so that the behavior is the same. Adds missing handling for `.length` to directives processing on the server on strings and numeric arrays which is inherently supported through JavaScript language on the client. Props jonsurrell, gziolo, luisherranz. Fixes #62582. git-svn-id: https://develop.svn.wordpress.org/trunk@59477 602fd350-edb4-49c9-b593-d223f7449a82 --- .../class-wp-interactivity-api.php | 30 +++++++++++++ .../interactivity-api/wpInteractivityAPI.php | 44 +++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/src/wp-includes/interactivity-api/class-wp-interactivity-api.php b/src/wp-includes/interactivity-api/class-wp-interactivity-api.php index f6764736f9e8a..8283349868d64 100644 --- a/src/wp-includes/interactivity-api/class-wp-interactivity-api.php +++ b/src/wp-includes/interactivity-api/class-wp-interactivity-api.php @@ -579,6 +579,36 @@ private function evaluate( $directive_value ) { $path_segments = explode( '.', $path ); $current = $store; foreach ( $path_segments as $path_segment ) { + /* + * Special case for numeric arrays and strings. Add length + * property mimicking JavaScript behavior. + * + * @since 6.8.0 + */ + if ( 'length' === $path_segment ) { + if ( is_array( $current ) && array_is_list( $current ) ) { + $current = count( $current ); + break; + } + + if ( is_string( $current ) ) { + /* + * Differences in encoding between PHP strings and + * JavaScript mean that it's complicated to calculate + * the string length JavaScript would see from PHP. + * `strlen` is a reasonable approximation. + * + * Users that desire a more precise length likely have + * more precise needs than "bytelength" and should + * implement their own length calculation in derived + * state taking into account encoding and their desired + * output (codepoints, graphemes, bytes, etc.). + */ + $current = strlen( $current ); + break; + } + } + if ( ( is_array( $current ) || $current instanceof ArrayAccess ) && isset( $current[ $path_segment ] ) ) { $current = $current[ $path_segment ]; } elseif ( is_object( $current ) && isset( $current->$path_segment ) ) { diff --git a/tests/phpunit/tests/interactivity-api/wpInteractivityAPI.php b/tests/phpunit/tests/interactivity-api/wpInteractivityAPI.php index c63c64c6888e6..bcc4ba6bdf149 100644 --- a/tests/phpunit/tests/interactivity-api/wpInteractivityAPI.php +++ b/tests/phpunit/tests/interactivity-api/wpInteractivityAPI.php @@ -1541,4 +1541,48 @@ public function test_get_element_outside_of_directive_processing() { $element = $this->interactivity->get_element(); $this->assertNull( $element ); } + + /** + * Verify behavior of .length directive access. + * + * @ticket 62582 + * + * @covers ::process_directives + * + * @dataProvider data_length_directives + * + * @param mixed $value The property value. + * @param string $expected The expected property length as a string, + * or "" if no length is expected. + */ + public function test_process_directives_string_array_length( $value, string $expected ) { + $this->interactivity->state( + 'myPlugin', + array( 'prop' => $value ) + ); + $html = '
'; + $processed_html = $this->interactivity->process_directives( $html ); + $processor = new WP_HTML_Tag_Processor( $processed_html ); + $processor->next_tag( 'DIV' ); + $processor->next_token(); + $this->assertSame( $expected, $processor->get_modifiable_text() ); + } + + /** + * Data provider. + * + * @return array + */ + public static function data_length_directives(): array { + return array( + 'numeric array' => array( array( 'a', 'b', 'c' ), '3' ), + 'empty array' => array( array(), '0' ), + 'string' => array( 'abc', '3' ), + 'empty string' => array( '', '0' ), + + // Failure cases resulting in empty string. + 'non-numeric array' => array( array( 'a' => 'a' ), '' ), + 'object' => array( new stdClass(), '' ), + ); + } }