diff -r 48c4eec2b7e6 -r 8c2e4d02f4ef wp/wp-includes/interactivity-api/class-wp-interactivity-api.php --- a/wp/wp-includes/interactivity-api/class-wp-interactivity-api.php Fri Sep 05 18:40:08 2025 +0200 +++ b/wp/wp-includes/interactivity-api/class-wp-interactivity-api.php Fri Sep 05 18:52:52 2025 +0200 @@ -96,6 +96,16 @@ private $context_stack = null; /** + * Representation in array format of the element currently being processed. + * + * This is only available during directive processing, otherwise it is `null`. + * + * @since 6.7.0 + * @var array{attributes: array}|null + */ + private $current_element = null; + + /** * Gets and/or sets the initial state of an Interactivity API store for a * given namespace. * @@ -192,13 +202,48 @@ * configuration will be available using a `getConfig` utility. * * @since 6.5.0 + * + * @deprecated 6.7.0 Client data passing is handled by the {@see "script_module_data_{$module_id}"} filter. */ public function print_client_interactivity_data() { - if ( empty( $this->state_data ) && empty( $this->config_data ) ) { - return; + _deprecated_function( __METHOD__, '6.7.0' ); + } + + /** + * Set client-side interactivity-router data. + * + * Once in the browser, the state will be parsed and used to hydrate the client-side + * interactivity stores and the configuration will be available using a `getConfig` utility. + * + * @since 6.7.0 + * + * @param array $data Data to filter. + * @return array Data for the Interactivity Router script module. + */ + public function filter_script_module_interactivity_router_data( array $data ): array { + if ( ! isset( $data['i18n'] ) ) { + $data['i18n'] = array(); } + $data['i18n']['loading'] = __( 'Loading page, please wait.' ); + $data['i18n']['loaded'] = __( 'Page Loaded.' ); + return $data; + } - $interactivity_data = array(); + /** + * Set client-side interactivity data. + * + * Once in the browser, the state will be parsed and used to hydrate the client-side + * interactivity stores and the configuration will be available using a `getConfig` utility. + * + * @since 6.7.0 + * + * @param array $data Data to filter. + * @return array Data for the Interactivity API script module. + */ + public function filter_script_module_interactivity_data( array $data ): array { + if ( empty( $this->state_data ) && empty( $this->config_data ) ) { + return $data; + } $config = array(); foreach ( $this->config_data as $key => $value ) { @@ -207,7 +252,7 @@ } } if ( ! empty( $config ) ) { - $interactivity_data['config'] = $config; + $data['config'] = $config; } $state = array(); @@ -217,52 +262,10 @@ } } if ( ! empty( $state ) ) { - $interactivity_data['state'] = $state; + $data['state'] = $state; } - if ( ! empty( $interactivity_data ) ) { - /* - * This data will be printed as JSON inside a script tag like this: - * - * - * A script tag must be closed by a sequence beginning with `` will be printed as `\u003C/script\u00E3`. - * - * - JSON_HEX_TAG: All < and > are converted to \u003C and \u003E. - * - JSON_UNESCAPED_SLASHES: Don't escape /. - * - * If the page will use UTF-8 encoding, it's safe to print unescaped unicode: - * - * - JSON_UNESCAPED_UNICODE: Encode multibyte Unicode characters literally (instead of as `\uXXXX`). - * - JSON_UNESCAPED_LINE_TERMINATORS: The line terminators are kept unescaped when - * JSON_UNESCAPED_UNICODE is supplied. It uses the same behaviour as it was - * before PHP 7.1 without this constant. Available as of PHP 7.1.0. - * - * The JSON specification requires encoding in UTF-8, so if the generated HTML page - * is not encoded in UTF-8 then it's not safe to include those literals. They must - * be escaped to avoid encoding issues. - * - * @see https://www.rfc-editor.org/rfc/rfc8259.html for details on encoding requirements. - * @see https://www.php.net/manual/en/json.constants.php for details on these constants. - * @see https://html.spec.whatwg.org/#script-data-state for details on script tag parsing. - */ - $json_encode_flags = JSON_HEX_TAG | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_LINE_TERMINATORS; - if ( ! is_utf8_charset() ) { - $json_encode_flags = JSON_HEX_TAG | JSON_UNESCAPED_SLASHES; - } - - wp_print_inline_script_tag( - wp_json_encode( - $interactivity_data, - $json_encode_flags - ), - array( - 'type' => 'application/json', - 'id' => 'wp-interactivity-data', - ) - ); - } + return $data; } /** @@ -306,23 +309,35 @@ } /** + * Returns an array representation of the current element being processed. + * + * The returned array contains a copy of the element attributes. + * + * @since 6.7.0 + * + * @return array{attributes: array}|null Current element. + */ + public function get_element(): ?array { + if ( null === $this->current_element ) { + _doing_it_wrong( + __METHOD__, + __( 'The element can only be read during directive processing.' ), + '6.7.0' + ); + } + + return $this->current_element; + } + + /** * Registers the `@wordpress/interactivity` script modules. * + * @deprecated 6.7.0 Script Modules registration is handled by {@see wp_default_script_modules()}. + * * @since 6.5.0 */ public function register_script_modules() { - $suffix = wp_scripts_get_suffix(); - - wp_register_script_module( - '@wordpress/interactivity', - includes_url( "js/dist/interactivity$suffix.js" ) - ); - - wp_register_script_module( - '@wordpress/interactivity-router', - includes_url( "js/dist/interactivity-router$suffix.js" ), - array( '@wordpress/interactivity' ) - ); + _deprecated_function( __METHOD__, '6.7.0', 'wp_default_script_modules' ); } /** @@ -331,11 +346,8 @@ * @since 6.5.0 */ public function add_hooks() { - add_action( 'wp_enqueue_scripts', array( $this, 'register_script_modules' ) ); - add_action( 'wp_footer', array( $this, 'print_client_interactivity_data' ) ); - - add_action( 'admin_enqueue_scripts', array( $this, 'register_script_modules' ) ); - add_action( 'admin_print_footer_scripts', array( $this, 'print_client_interactivity_data' ) ); + add_filter( 'script_module_data_@wordpress/interactivity', array( $this, 'filter_script_module_interactivity_data' ) ); + add_filter( 'script_module_data_@wordpress/interactivity-router', array( $this, 'filter_script_module_interactivity_router_data' ) ); } /** @@ -439,6 +451,26 @@ // Checks if there is a server directive processor registered for each directive. foreach ( $p->get_attribute_names_with_prefix( 'data-wp-' ) as $attribute_name ) { + if ( ! preg_match( + /* + * This must align with the client-side regex used by the interactivity API. + * @see https://github.com/WordPress/gutenberg/blob/ca616014255efbb61f34c10917d52a2d86c1c660/packages/interactivity/src/vdom.ts#L20-L32 + */ + '/' . + '^data-wp-' . + // Match alphanumeric characters including hyphen-separated + // segments. It excludes underscore intentionally to prevent confusion. + // E.g., "custom-directive". + '([a-z0-9]+(?:-[a-z0-9]+)*)' . + // (Optional) Match '--' followed by any alphanumeric charachters. It + // excludes underscore intentionally to prevent confusion, but it can + // contain multiple hyphens. E.g., "--custom-prefix--with-more-info". + '(?:--([a-z0-9_-]+))?$' . + '/i', + $attribute_name + ) ) { + continue; + } list( $directive_prefix ) = $this->extract_prefix_and_suffix( $attribute_name ); if ( array_key_exists( $directive_prefix, self::$directive_processors ) ) { $directives_prefixes[] = $directive_prefix; @@ -468,6 +500,19 @@ 'exit' => $p->is_tag_closer() || ! $p->has_and_visits_its_closer_tag(), ); + // Get the element attributes to include them in the element representation. + $element_attrs = array(); + $attr_names = $p->get_attribute_names_with_prefix( '' ) ?? array(); + + foreach ( $attr_names as $name ) { + $element_attrs[ $name ] = $p->get_attribute( $name ); + } + + // Assign the current element right before running its directive processors. + $this->current_element = array( + 'attributes' => $element_attrs, + ); + foreach ( $modes as $mode => $should_run ) { if ( ! $should_run ) { continue; @@ -489,6 +534,9 @@ call_user_func_array( $func, array( $p, $mode, &$tag_stack ) ); } } + + // Clear the current element. + $this->current_element = null; } if ( $unbalanced ) { @@ -551,12 +599,43 @@ $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 ) ) { $current = $current->$path_segment; } else { - return null; + $current = null; + break; } if ( $current instanceof Closure ) { @@ -1013,16 +1092,27 @@ } /** - * Outputs the markup for the top loading indicator and the screen reader - * notifications during client-side navigations. + * Deprecated. + * + * @since 6.5.0 + * @deprecated 6.7.0 Use {@see WP_Interactivity_API::print_router_markup} instead. + */ + public function print_router_loading_and_screen_reader_markup() { + _deprecated_function( __METHOD__, '6.7.0', 'WP_Interactivity_API::print_router_markup' ); + + // Call the new method. + $this->print_router_markup(); + } + + /** + * Outputs markup for the @wordpress/interactivity-router script module. * * This method prints a div element representing a loading bar visible during - * navigation, as well as an aria-live region that can be read by screen - * readers to announce navigation status. + * navigation. * - * @since 6.5.0 + * @since 6.7.0 */ - public function print_router_loading_and_screen_reader_markup() { + public function print_router_markup() { echo << -
HTML; } @@ -1056,26 +1140,13 @@ if ( 'enter' === $mode && ! $this->has_processed_router_region ) { $this->has_processed_router_region = true; - // Initialize the `core/router` store. - $this->state( - 'core/router', - array( - 'navigation' => array( - 'texts' => array( - 'loading' => __( 'Loading page, please wait.' ), - 'loaded' => __( 'Page Loaded.' ), - ), - ), - ) - ); - // Enqueues as an inline style. wp_register_style( 'wp-interactivity-router-animations', false ); wp_add_inline_style( 'wp-interactivity-router-animations', $this->get_router_animation_styles() ); wp_enqueue_style( 'wp-interactivity-router-animations' ); // Adds the necessary markup to the footer. - add_action( 'wp_footer', array( $this, 'print_router_loading_and_screen_reader_markup' ) ); + add_action( 'wp_footer', array( $this, 'print_router_markup' ) ); } }