[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/interactivity-api/ -> class-wp-interactivity-api.php (source)

   1  <?php
   2  /**
   3   * Interactivity API: WP_Interactivity_API class.
   4   *
   5   * @package WordPress
   6   * @subpackage Interactivity API
   7   * @since 6.5.0
   8   */
   9  
  10  /**
  11   * Class used to process the Interactivity API on the server.
  12   *
  13   * @since 6.5.0
  14   */
  15  final class WP_Interactivity_API {
  16      /**
  17       * Holds the mapping of directive attribute names to their processor methods.
  18       *
  19       * @since 6.5.0
  20       * @var array
  21       */
  22      private static $directive_processors = array(
  23          'data-wp-interactive'   => 'data_wp_interactive_processor',
  24          'data-wp-router-region' => 'data_wp_router_region_processor',
  25          'data-wp-context'       => 'data_wp_context_processor',
  26          'data-wp-bind'          => 'data_wp_bind_processor',
  27          'data-wp-class'         => 'data_wp_class_processor',
  28          'data-wp-style'         => 'data_wp_style_processor',
  29          'data-wp-text'          => 'data_wp_text_processor',
  30          /*
  31           * `data-wp-each` needs to be processed in the last place because it moves
  32           * the cursor to the end of the processed items to prevent them to be
  33           * processed twice.
  34           */
  35          'data-wp-each'          => 'data_wp_each_processor',
  36      );
  37  
  38      /**
  39       * Holds the initial state of the different Interactivity API stores.
  40       *
  41       * This state is used during the server directive processing. Then, it is
  42       * serialized and sent to the client as part of the interactivity data to be
  43       * recovered during the hydration of the client interactivity stores.
  44       *
  45       * @since 6.5.0
  46       * @var array
  47       */
  48      private $state_data = array();
  49  
  50      /**
  51       * Holds the configuration required by the different Interactivity API stores.
  52       *
  53       * This configuration is serialized and sent to the client as part of the
  54       * interactivity data and can be accessed by the client interactivity stores.
  55       *
  56       * @since 6.5.0
  57       * @var array
  58       */
  59      private $config_data = array();
  60  
  61      /**
  62       * Keeps track of all derived state closures accessed during server-side rendering.
  63       *
  64       * This data is serialized and sent to the client as part of the interactivity
  65       * data, and is handled later in the client to support derived state props that
  66       * are lazily hydrated.
  67       *
  68       * @since 6.9.0
  69       * @var array
  70       */
  71      private $derived_state_closures = array();
  72  
  73      /**
  74       * Flag that indicates whether the `data-wp-router-region` directive has
  75       * been found in the HTML and processed.
  76       *
  77       * The value is saved in a private property of the WP_Interactivity_API
  78       * instance instead of using a static variable inside the processor
  79       * function, which would hold the same value for all instances
  80       * independently of whether they have processed any
  81       * `data-wp-router-region` directive or not.
  82       *
  83       * @since 6.5.0
  84       * @var bool
  85       */
  86      private $has_processed_router_region = false;
  87  
  88      /**
  89       * Set of script modules that can be loaded after client-side navigation.
  90       *
  91       * @since 6.9.0
  92       * @var array<string, true>
  93       */
  94      private $script_modules_that_can_load_on_client_navigation = array();
  95  
  96      /**
  97       * Stack of namespaces defined by `data-wp-interactive` directives, in
  98       * the order they are processed.
  99       *
 100       * This is only available during directive processing, otherwise it is `null`.
 101       *
 102       * @since 6.6.0
 103       * @var array<string>|null
 104       */
 105      private $namespace_stack = null;
 106  
 107      /**
 108       * Stack of contexts defined by `data-wp-context` directives, in
 109       * the order they are processed.
 110       *
 111       * This is only available during directive processing, otherwise it is `null`.
 112       *
 113       * @since 6.6.0
 114       * @var array<array<mixed>>|null
 115       */
 116      private $context_stack = null;
 117  
 118      /**
 119       * Representation in array format of the element currently being processed.
 120       *
 121       * This is only available during directive processing, otherwise it is `null`.
 122       *
 123       * @since 6.7.0
 124       * @var array{attributes: array<string, string|bool>}|null
 125       */
 126      private $current_element = null;
 127  
 128      /**
 129       * Gets and/or sets the initial state of an Interactivity API store for a
 130       * given namespace.
 131       *
 132       * If state for that store namespace already exists, it merges the new
 133       * provided state with the existing one.
 134       *
 135       * When no namespace is specified, it returns the state defined for the
 136       * current value in the internal namespace stack during a `process_directives` call.
 137       *
 138       * @since 6.5.0
 139       * @since 6.6.0 The `$store_namespace` param is optional.
 140       *
 141       * @param string|null $store_namespace Optional. The unique store namespace identifier.
 142       * @param array|null  $state           Optional. The array that will be merged with the existing state for the specified
 143       *                                store namespace.
 144       * @return array The current state for the specified store namespace. This will be the updated state if a $state
 145       *               argument was provided.
 146       */
 147  	public function state( ?string $store_namespace = null, ?array $state = null ): array {
 148          if ( ! $store_namespace ) {
 149              if ( $state ) {
 150                  _doing_it_wrong(
 151                      __METHOD__,
 152                      __( 'The namespace is required when state data is passed.' ),
 153                      '6.6.0'
 154                  );
 155                  return array();
 156              }
 157              if ( null !== $store_namespace ) {
 158                  _doing_it_wrong(
 159                      __METHOD__,
 160                      __( 'The namespace should be a non-empty string.' ),
 161                      '6.6.0'
 162                  );
 163                  return array();
 164              }
 165              if ( null === $this->namespace_stack ) {
 166                  _doing_it_wrong(
 167                      __METHOD__,
 168                      __( 'The namespace can only be omitted during directive processing.' ),
 169                      '6.6.0'
 170                  );
 171                  return array();
 172              }
 173  
 174              $store_namespace = end( $this->namespace_stack );
 175          }
 176          if ( ! isset( $this->state_data[ $store_namespace ] ) ) {
 177              $this->state_data[ $store_namespace ] = array();
 178          }
 179          if ( is_array( $state ) ) {
 180              $this->state_data[ $store_namespace ] = array_replace_recursive(
 181                  $this->state_data[ $store_namespace ],
 182                  $state
 183              );
 184          }
 185          return $this->state_data[ $store_namespace ];
 186      }
 187  
 188      /**
 189       * Gets and/or sets the configuration of the Interactivity API for a given
 190       * store namespace.
 191       *
 192       * If configuration for that store namespace exists, it merges the new
 193       * provided configuration with the existing one.
 194       *
 195       * @since 6.5.0
 196       *
 197       * @param string $store_namespace The unique store namespace identifier.
 198       * @param array  $config          Optional. The array that will be merged with the existing configuration for the
 199       *                                specified store namespace.
 200       * @return array The configuration for the specified store namespace. This will be the updated configuration if a
 201       *               $config argument was provided.
 202       */
 203  	public function config( string $store_namespace, array $config = array() ): array {
 204          if ( ! isset( $this->config_data[ $store_namespace ] ) ) {
 205              $this->config_data[ $store_namespace ] = array();
 206          }
 207          if ( is_array( $config ) ) {
 208              $this->config_data[ $store_namespace ] = array_replace_recursive(
 209                  $this->config_data[ $store_namespace ],
 210                  $config
 211              );
 212          }
 213          return $this->config_data[ $store_namespace ];
 214      }
 215  
 216      /**
 217       * Prints the serialized client-side interactivity data.
 218       *
 219       * Encodes the config and initial state into JSON and prints them inside a
 220       * script tag of type "application/json". Once in the browser, the state will
 221       * be parsed and used to hydrate the client-side interactivity stores and the
 222       * configuration will be available using a `getConfig` utility.
 223       *
 224       * @since 6.5.0
 225       *
 226       * @deprecated 6.7.0 Client data passing is handled by the {@see "script_module_data_{$module_id}"} filter.
 227       */
 228  	public function print_client_interactivity_data() {
 229          _deprecated_function( __METHOD__, '6.7.0' );
 230      }
 231  
 232      /**
 233       * Set client-side interactivity-router data.
 234       *
 235       * Once in the browser, the state will be parsed and used to hydrate the client-side
 236       * interactivity stores and the configuration will be available using a `getConfig` utility.
 237       *
 238       * @since 6.7.0
 239       *
 240       * @param array $data Data to filter.
 241       * @return array Data for the Interactivity Router script module.
 242       */
 243      public function filter_script_module_interactivity_router_data( array $data ): array {
 244          if ( ! isset( $data['i18n'] ) ) {
 245              $data['i18n'] = array();
 246          }
 247          $data['i18n']['loading'] = __( 'Loading page, please wait.' );
 248          $data['i18n']['loaded']  = __( 'Page Loaded.' );
 249          return $data;
 250      }
 251  
 252      /**
 253       * Set client-side interactivity data.
 254       *
 255       * Once in the browser, the state will be parsed and used to hydrate the client-side
 256       * interactivity stores and the configuration will be available using a `getConfig` utility.
 257       *
 258       * @since 6.7.0
 259       * @since 6.9.0 Serializes derived state props accessed during directive processing.
 260       *
 261       * @param array $data Data to filter.
 262       * @return array Data for the Interactivity API script module.
 263       */
 264  	public function filter_script_module_interactivity_data( array $data ): array {
 265          if (
 266              empty( $this->state_data ) &&
 267              empty( $this->config_data ) &&
 268              empty( $this->derived_state_closures )
 269          ) {
 270              return $data;
 271          }
 272  
 273          $config = array();
 274          foreach ( $this->config_data as $key => $value ) {
 275              if ( ! empty( $value ) ) {
 276                  $config[ $key ] = $value;
 277              }
 278          }
 279          if ( ! empty( $config ) ) {
 280              $data['config'] = $config;
 281          }
 282  
 283          $state = array();
 284          foreach ( $this->state_data as $key => $value ) {
 285              if ( ! empty( $value ) ) {
 286                  $state[ $key ] = $value;
 287              }
 288          }
 289          if ( ! empty( $state ) ) {
 290              $data['state'] = $state;
 291          }
 292  
 293          $derived_props = array();
 294          foreach ( $this->derived_state_closures as $key => $value ) {
 295              if ( ! empty( $value ) ) {
 296                  $derived_props[ $key ] = $value;
 297              }
 298          }
 299          if ( ! empty( $derived_props ) ) {
 300              $data['derivedStateClosures'] = $derived_props;
 301          }
 302  
 303          return $data;
 304      }
 305  
 306      /**
 307       * Returns the latest value on the context stack with the passed namespace.
 308       *
 309       * When the namespace is omitted, it uses the current namespace on the
 310       * namespace stack during a `process_directives` call.
 311       *
 312       * @since 6.6.0
 313       *
 314       * @param string|null $store_namespace Optional. The unique store namespace identifier.
 315       */
 316  	public function get_context( ?string $store_namespace = null ): array {
 317          if ( null === $this->context_stack ) {
 318              _doing_it_wrong(
 319                  __METHOD__,
 320                  __( 'The context can only be read during directive processing.' ),
 321                  '6.6.0'
 322              );
 323              return array();
 324          }
 325  
 326          if ( ! $store_namespace ) {
 327              if ( null !== $store_namespace ) {
 328                  _doing_it_wrong(
 329                      __METHOD__,
 330                      __( 'The namespace should be a non-empty string.' ),
 331                      '6.6.0'
 332                  );
 333                  return array();
 334              }
 335  
 336              $store_namespace = end( $this->namespace_stack );
 337          }
 338  
 339          $context = end( $this->context_stack );
 340  
 341          return ( $store_namespace && $context && isset( $context[ $store_namespace ] ) )
 342              ? $context[ $store_namespace ]
 343              : array();
 344      }
 345  
 346      /**
 347       * Returns an array representation of the current element being processed.
 348       *
 349       * The returned array contains a copy of the element attributes.
 350       *
 351       * @since 6.7.0
 352       *
 353       * @return array{attributes: array<string, string|bool>}|null Current element.
 354       */
 355  	public function get_element(): ?array {
 356          if ( null === $this->current_element ) {
 357              _doing_it_wrong(
 358                  __METHOD__,
 359                  __( 'The element can only be read during directive processing.' ),
 360                  '6.7.0'
 361              );
 362          }
 363  
 364          return $this->current_element;
 365      }
 366  
 367      /**
 368       * Registers the `@wordpress/interactivity` script modules.
 369       *
 370       * @deprecated 6.7.0 Script Modules registration is handled by {@see wp_default_script_modules()}.
 371       *
 372       * @since 6.5.0
 373       */
 374  	public function register_script_modules() {
 375          _deprecated_function( __METHOD__, '6.7.0', 'wp_default_script_modules' );
 376      }
 377  
 378      /**
 379       * Adds the necessary hooks for the Interactivity API.
 380       *
 381       * @since 6.5.0
 382       * @since 6.9.0 Adds support for client-side navigation in script modules.
 383       */
 384  	public function add_hooks() {
 385          add_filter( 'script_module_data_@wordpress/interactivity', array( $this, 'filter_script_module_interactivity_data' ) );
 386          add_filter( 'script_module_data_@wordpress/interactivity-router', array( $this, 'filter_script_module_interactivity_router_data' ) );
 387          add_filter( 'wp_script_attributes', array( $this, 'add_load_on_client_navigation_attribute_to_script_modules' ) );
 388      }
 389  
 390      /**
 391       * Adds the `data-wp-router-options` attribute to script modules that
 392       * support client-side navigation.
 393       *
 394       * This method filters the script attributes to include loading instructions
 395       * for the Interactivity API router, indicating which modules can be loaded
 396       * during client-side navigation.
 397       *
 398       * @since 6.9.0
 399       *
 400       * @param array<string, string|true>|mixed $attributes The script tag attributes.
 401       * @return array The modified script tag attributes.
 402       */
 403      public function add_load_on_client_navigation_attribute_to_script_modules( $attributes ) {
 404          if (
 405              is_array( $attributes ) &&
 406              isset( $attributes['type'], $attributes['id'] ) &&
 407              'module' === $attributes['type'] &&
 408              array_key_exists(
 409                  preg_replace( '/-js-module$/', '', $attributes['id'] ),
 410                  $this->script_modules_that_can_load_on_client_navigation
 411              )
 412          ) {
 413              $attributes['data-wp-router-options'] = wp_json_encode( array( 'loadOnClientNavigation' => true ) );
 414          }
 415          return $attributes;
 416      }
 417  
 418      /**
 419       * Marks a script module as compatible with client-side navigation.
 420       *
 421       * This method registers a script module to be loaded during client-side
 422       * navigation in the Interactivity API router. Script modules marked with
 423       * this method will have the `loadOnClientNavigation` option enabled in the
 424       * `data-wp-router-options` directive.
 425       *
 426       * @since 6.9.0
 427       *
 428       * @param string $script_module_id The script module identifier.
 429       */
 430      public function add_client_navigation_support_to_script_module( string $script_module_id ) {
 431          $this->script_modules_that_can_load_on_client_navigation[ $script_module_id ] = true;
 432      }
 433  
 434      /**
 435       * Processes the interactivity directives contained within the HTML content
 436       * and updates the markup accordingly.
 437       *
 438       * @since 6.5.0
 439       *
 440       * @param string $html The HTML content to process.
 441       * @return string The processed HTML content. It returns the original content when the HTML contains unbalanced tags.
 442       */
 443  	public function process_directives( string $html ): string {
 444          if ( ! str_contains( $html, 'data-wp-' ) ) {
 445              return $html;
 446          }
 447  
 448          $this->namespace_stack = array();
 449          $this->context_stack   = array();
 450  
 451          $result = $this->_process_directives( $html );
 452  
 453          $this->namespace_stack = null;
 454          $this->context_stack   = null;
 455  
 456          return $result ?? $html;
 457      }
 458  
 459      /**
 460       * Processes the interactivity directives contained within the HTML content
 461       * and updates the markup accordingly.
 462       *
 463       * It uses the WP_Interactivity_API instance's context and namespace stacks,
 464       * which are shared between all calls.
 465       *
 466       * This method returns null if the HTML contains unbalanced tags.
 467       *
 468       * @since 6.6.0
 469       *
 470       * @param string $html The HTML content to process.
 471       * @return string|null The processed HTML content. It returns null when the HTML contains unbalanced tags.
 472       */
 473  	private function _process_directives( string $html ) {
 474          $p          = new WP_Interactivity_API_Directives_Processor( $html );
 475          $tag_stack  = array();
 476          $unbalanced = false;
 477  
 478          $directive_processor_prefixes          = array_keys( self::$directive_processors );
 479          $directive_processor_prefixes_reversed = array_reverse( $directive_processor_prefixes );
 480  
 481          /*
 482           * Save the current size for each stack to restore them in case
 483           * the processing finds unbalanced tags.
 484           */
 485          $namespace_stack_size = count( $this->namespace_stack );
 486          $context_stack_size   = count( $this->context_stack );
 487  
 488          while ( $p->next_tag( array( 'tag_closers' => 'visit' ) ) ) {
 489              $tag_name = $p->get_tag();
 490  
 491              /*
 492               * Directives inside SVG and MATH tags are not processed,
 493               * as they are not compatible with the Tag Processor yet.
 494               * We still process the rest of the HTML.
 495               */
 496              if ( 'SVG' === $tag_name || 'MATH' === $tag_name ) {
 497                  if ( $p->get_attribute_names_with_prefix( 'data-wp-' ) ) {
 498                      /* translators: 1: SVG or MATH HTML tag, 2: Namespace of the interactive block. */
 499                      $message = sprintf( __( 'Interactivity directives were detected on an incompatible %1$s tag when processing "%2$s". These directives will be ignored in the server side render.' ), $tag_name, end( $this->namespace_stack ) );
 500                      _doing_it_wrong( __METHOD__, $message, '6.6.0' );
 501                  }
 502                  $p->skip_to_tag_closer();
 503                  continue;
 504              }
 505  
 506              if ( $p->is_tag_closer() ) {
 507                  list( $opening_tag_name, $directives_prefixes ) = ! empty( $tag_stack ) ? end( $tag_stack ) : array( null, null );
 508  
 509                  if ( 0 === count( $tag_stack ) || $opening_tag_name !== $tag_name ) {
 510  
 511                      /*
 512                       * If the tag stack is empty or the matching opening tag is not the
 513                       * same than the closing tag, it means the HTML is unbalanced and it
 514                       * stops processing it.
 515                       */
 516                      $unbalanced = true;
 517                      break;
 518                  } else {
 519                      // Remove the last tag from the stack.
 520                      array_pop( $tag_stack );
 521                  }
 522              } else {
 523                  $each_child_attrs = $p->get_attribute_names_with_prefix( 'data-wp-each-child' );
 524                  if ( null === $each_child_attrs ) {
 525                      continue;
 526                  }
 527  
 528                  if ( 0 !== count( $each_child_attrs ) ) {
 529                      /*
 530                       * If the tag has a `data-wp-each-child` directive, jump to its closer
 531                       * tag because those tags have already been processed.
 532                       */
 533                      $p->next_balanced_tag_closer_tag();
 534                      continue;
 535                  } else {
 536                      $directives_prefixes = array();
 537  
 538                      // Checks if there is a server directive processor registered for each directive.
 539                      foreach ( $p->get_attribute_names_with_prefix( 'data-wp-' ) as $attribute_name ) {
 540                          $parsed_directive = $this->parse_directive_name( $attribute_name );
 541                          if ( empty( $parsed_directive ) ) {
 542                              continue;
 543                          }
 544                          $directive_prefix = 'data-wp-' . $parsed_directive['prefix'];
 545                          if ( array_key_exists( $directive_prefix, self::$directive_processors ) ) {
 546                              $directives_prefixes[] = $directive_prefix;
 547                          }
 548                      }
 549  
 550                      /*
 551                       * If this tag will visit its closer tag, it adds it to the tag stack
 552                       * so it can process its closing tag and check for unbalanced tags.
 553                       */
 554                      if ( $p->has_and_visits_its_closer_tag() ) {
 555                          $tag_stack[] = array( $tag_name, $directives_prefixes );
 556                      }
 557                  }
 558              }
 559              /*
 560               * If the matching opener tag didn't have any directives, it can skip the
 561               * processing.
 562               */
 563              if ( 0 === count( $directives_prefixes ) ) {
 564                  continue;
 565              }
 566  
 567              // Directive processing might be different depending on if it is entering the tag or exiting it.
 568              $modes = array(
 569                  'enter' => ! $p->is_tag_closer(),
 570                  'exit'  => $p->is_tag_closer() || ! $p->has_and_visits_its_closer_tag(),
 571              );
 572  
 573              // Get the element attributes to include them in the element representation.
 574              $element_attrs = array();
 575              $attr_names    = $p->get_attribute_names_with_prefix( '' ) ?? array();
 576  
 577              foreach ( $attr_names as $name ) {
 578                  $element_attrs[ $name ] = $p->get_attribute( $name );
 579              }
 580  
 581              // Assign the current element right before running its directive processors.
 582              $this->current_element = array(
 583                  'attributes' => $element_attrs,
 584              );
 585  
 586              foreach ( $modes as $mode => $should_run ) {
 587                  if ( ! $should_run ) {
 588                      continue;
 589                  }
 590  
 591                  /*
 592                   * Sorts the attributes by the order of the `directives_processor` array
 593                   * and checks what directives are present in this element.
 594                   */
 595                  $existing_directives_prefixes = array_intersect(
 596                      'enter' === $mode ? $directive_processor_prefixes : $directive_processor_prefixes_reversed,
 597                      $directives_prefixes
 598                  );
 599                  foreach ( $existing_directives_prefixes as $directive_prefix ) {
 600                      $func = is_array( self::$directive_processors[ $directive_prefix ] )
 601                          ? self::$directive_processors[ $directive_prefix ]
 602                          : array( $this, self::$directive_processors[ $directive_prefix ] );
 603  
 604                      call_user_func_array( $func, array( $p, $mode, &$tag_stack ) );
 605                  }
 606              }
 607  
 608              // Clear the current element.
 609              $this->current_element = null;
 610          }
 611  
 612          if ( $unbalanced ) {
 613              // Reset the namespace and context stacks to their previous values.
 614              array_splice( $this->namespace_stack, $namespace_stack_size );
 615              array_splice( $this->context_stack, $context_stack_size );
 616          }
 617  
 618          /*
 619           * It returns null if the HTML is unbalanced because unbalanced HTML is
 620           * not safe to process. In that case, the Interactivity API runtime will
 621           * update the HTML on the client side during the hydration. It will display
 622           * a notice to the developer in the console to inform them about the issue.
 623           */
 624          if ( $unbalanced || 0 < count( $tag_stack ) ) {
 625              return null;
 626          }
 627  
 628          return $p->get_updated_html();
 629      }
 630  
 631      /**
 632       * Evaluates the reference path passed to a directive based on the current
 633       * store namespace, state and context.
 634       *
 635       * @since 6.5.0
 636       * @since 6.6.0 The function now adds a warning when the namespace is null, falsy, or the directive value is empty.
 637       * @since 6.6.0 Removed `default_namespace` and `context` arguments.
 638       * @since 6.6.0 Add support for derived state.
 639       * @since 6.9.0 Receive $entry as an argument instead of the directive value string.
 640       *
 641       * @param array $entry An array containing a whole directive entry with its namespace, value, suffix, or unique ID.
 642       * @return mixed|null The result of the evaluation. Null if the reference path doesn't exist or the namespace is falsy.
 643       */
 644  	private function evaluate( $entry ) {
 645          $context                               = end( $this->context_stack );
 646          ['namespace' => $ns, 'value' => $path] = $entry;
 647  
 648          if ( ! $ns || ! $path ) {
 649              /* translators: %s: The directive value referenced. */
 650              $message = sprintf( __( 'Namespace or reference path cannot be empty. Directive value referenced: %s' ), json_encode( $entry ) );
 651              _doing_it_wrong( __METHOD__, $message, '6.6.0' );
 652              return null;
 653          }
 654  
 655          $store = array(
 656              'state'   => $this->state_data[ $ns ] ?? array(),
 657              'context' => $context[ $ns ] ?? array(),
 658          );
 659  
 660          // Checks if the reference path is preceded by a negation operator (!).
 661          $should_negate_value = '!' === $path[0];
 662          $path                = $should_negate_value ? substr( $path, 1 ) : $path;
 663  
 664          // Extracts the value from the store using the reference path.
 665          $path_segments = explode( '.', $path );
 666          $current       = $store;
 667          foreach ( $path_segments as $index => $path_segment ) {
 668              /*
 669               * Special case for numeric arrays and strings. Add length
 670               * property mimicking JavaScript behavior.
 671               *
 672               * @since 6.8.0
 673               */
 674              if ( 'length' === $path_segment ) {
 675                  if ( is_array( $current ) && array_is_list( $current ) ) {
 676                      $current = count( $current );
 677                      break;
 678                  }
 679  
 680                  if ( is_string( $current ) ) {
 681                      /*
 682                       * Differences in encoding between PHP strings and
 683                       * JavaScript mean that it's complicated to calculate
 684                       * the string length JavaScript would see from PHP.
 685                       * `strlen` is a reasonable approximation.
 686                       *
 687                       * Users that desire a more precise length likely have
 688                       * more precise needs than "bytelength" and should
 689                       * implement their own length calculation in derived
 690                       * state taking into account encoding and their desired
 691                       * output (codepoints, graphemes, bytes, etc.).
 692                       */
 693                      $current = strlen( $current );
 694                      break;
 695                  }
 696              }
 697  
 698              if ( ( is_array( $current ) || $current instanceof ArrayAccess ) && isset( $current[ $path_segment ] ) ) {
 699                  $current = $current[ $path_segment ];
 700              } elseif ( is_object( $current ) && isset( $current->$path_segment ) ) {
 701                  $current = $current->$path_segment;
 702              } else {
 703                  $current = null;
 704                  break;
 705              }
 706  
 707              if ( $current instanceof Closure ) {
 708                  /*
 709                   * This state getter's namespace is added to the stack so that
 710                   * `state()` or `get_config()` read that namespace when called
 711                   * without specifying one.
 712                   */
 713                  array_push( $this->namespace_stack, $ns );
 714                  try {
 715                      $current = $current();
 716  
 717                      /*
 718                       * Tracks derived state properties that are accessed during
 719                       * rendering.
 720                       *
 721                       * @since 6.9.0
 722                       */
 723                      $this->derived_state_closures[ $ns ] = $this->derived_state_closures[ $ns ] ?? array();
 724  
 725                      // Builds path for the current property and add it to tracking if not already present.
 726                      $current_path = implode( '.', array_slice( $path_segments, 0, $index + 1 ) );
 727                      if ( ! in_array( $current_path, $this->derived_state_closures[ $ns ], true ) ) {
 728                          $this->derived_state_closures[ $ns ][] = $current_path;
 729                      }
 730                  } catch ( Throwable $e ) {
 731                      _doing_it_wrong(
 732                          __METHOD__,
 733                          sprintf(
 734                              /* translators: 1: Path pointing to an Interactivity API state property, 2: Namespace for an Interactivity API store. */
 735                              __( 'Uncaught error executing a derived state callback with path "%1$s" and namespace "%2$s".' ),
 736                              $path,
 737                              $ns
 738                          ),
 739                          '6.6.0'
 740                      );
 741                      return null;
 742                  } finally {
 743                      // Remove the property's namespace from the stack.
 744                      array_pop( $this->namespace_stack );
 745                  }
 746              }
 747          }
 748  
 749          // Returns the opposite if it contains a negation operator (!).
 750          return $should_negate_value ? ! $current : $current;
 751      }
 752  
 753      /**
 754       * Parse the directive name to extract the following parts:
 755       * - Prefix: The main directive name without "data-wp-".
 756       * - Suffix: An optional suffix used during directive processing, extracted after the first double hyphen "--".
 757       * - Unique ID: An optional unique identifier, extracted after the first triple hyphen "---".
 758       *
 759       * This function has an equivalent version for the client side.
 760       * See `parseDirectiveName` in https://github.com/WordPress/gutenberg/blob/trunk/packages/interactivity/src/vdom.ts.:
 761       *
 762       * See examples in the function unit tests `test_parse_directive_name`.
 763       *
 764       * @since 6.9.0
 765       *
 766       * @param string $directive_name The directive attribute name.
 767       * @return array An array containing the directive prefix, optional suffix, and optional unique ID.
 768       */
 769  	private function parse_directive_name( string $directive_name ): ?array {
 770          // Remove the first 8 characters (assumes "data-wp-" prefix)
 771          $name = substr( $directive_name, 8 );
 772  
 773          // Check for invalid characters (anything not a-z, 0-9, -, or _)
 774          if ( preg_match( '/[^a-z0-9\-_]/i', $name ) ) {
 775              return null;
 776          }
 777  
 778          // Find the first occurrence of '--' to separate the prefix
 779          $suffix_index = strpos( $name, '--' );
 780  
 781          if ( false === $suffix_index ) {
 782              return array(
 783                  'prefix'    => $name,
 784                  'suffix'    => null,
 785                  'unique_id' => null,
 786              );
 787          }
 788  
 789          $prefix    = substr( $name, 0, $suffix_index );
 790          $remaining = substr( $name, $suffix_index );
 791  
 792          // If remaining starts with '---' but not '----', it's a unique_id
 793          if ( '---' === substr( $remaining, 0, 3 ) && '-' !== ( $remaining[3] ?? '' ) ) {
 794              return array(
 795                  'prefix'    => $prefix,
 796                  'suffix'    => null,
 797                  'unique_id' => '---' !== $remaining ? substr( $remaining, 3 ) : null,
 798              );
 799          }
 800  
 801          // Otherwise, remove the first two dashes for a potential suffix
 802          $suffix = substr( $remaining, 2 );
 803  
 804          // Look for '---' in the suffix for a unique_id
 805          $unique_id_index = strpos( $suffix, '---' );
 806  
 807          if ( false !== $unique_id_index && '-' !== ( $suffix[ $unique_id_index + 3 ] ?? '' ) ) {
 808              $unique_id = substr( $suffix, $unique_id_index + 3 );
 809              $suffix    = substr( $suffix, 0, $unique_id_index );
 810              return array(
 811                  'prefix'    => $prefix,
 812                  'suffix'    => empty( $suffix ) ? null : $suffix,
 813                  'unique_id' => empty( $unique_id ) ? null : $unique_id,
 814              );
 815          }
 816  
 817          return array(
 818              'prefix'    => $prefix,
 819              'suffix'    => empty( $suffix ) ? null : $suffix,
 820              'unique_id' => null,
 821          );
 822      }
 823  
 824      /**
 825       * Parses and extracts the namespace and reference path from the given
 826       * directive attribute value.
 827       *
 828       * If the value doesn't contain an explicit namespace, it returns the
 829       * default one. If the value contains a JSON object instead of a reference
 830       * path, the function tries to parse it and return the resulting array. If
 831       * the value contains strings that represent booleans ("true" and "false"),
 832       * numbers ("1" and "1.2") or "null", the function also transform them to
 833       * regular booleans, numbers and `null`.
 834       *
 835       * Example:
 836       *
 837       *     extract_directive_value( 'actions.foo', 'myPlugin' )                      => array( 'myPlugin', 'actions.foo' )
 838       *     extract_directive_value( 'otherPlugin::actions.foo', 'myPlugin' )         => array( 'otherPlugin', 'actions.foo' )
 839       *     extract_directive_value( '{ "isOpen": false }', 'myPlugin' )              => array( 'myPlugin', array( 'isOpen' => false ) )
 840       *     extract_directive_value( 'otherPlugin::{ "isOpen": false }', 'myPlugin' ) => array( 'otherPlugin', array( 'isOpen' => false ) )
 841       *
 842       * @since 6.5.0
 843       *
 844       * @param string|true $directive_value   The directive attribute value. It can be `true` when it's a boolean
 845       *                                       attribute.
 846       * @param string|null $default_namespace Optional. The default namespace if none is explicitly defined.
 847       * @return array An array containing the namespace in the first item and the JSON, the reference path, or null on the
 848       *               second item.
 849       */
 850  	private function extract_directive_value( $directive_value, $default_namespace = null ): array {
 851          if ( empty( $directive_value ) || is_bool( $directive_value ) ) {
 852              return array( $default_namespace, null );
 853          }
 854  
 855          // Replaces the value and namespace if there is a namespace in the value.
 856          if ( 1 === preg_match( '/^([\w\-_\/]+)::./', $directive_value ) ) {
 857              list($default_namespace, $directive_value) = explode( '::', $directive_value, 2 );
 858          }
 859  
 860          /*
 861           * Tries to decode the value as a JSON object. If it fails and the value
 862           * isn't `null`, it returns the value as it is. Otherwise, it returns the
 863           * decoded JSON or null for the string `null`.
 864           */
 865          $decoded_json = json_decode( $directive_value, true );
 866          if ( null !== $decoded_json || 'null' === $directive_value ) {
 867              $directive_value = $decoded_json;
 868          }
 869  
 870          return array( $default_namespace, $directive_value );
 871      }
 872  
 873      /**
 874       * Parse the HTML element and get all the valid directives with the given prefix.
 875       *
 876       * @since 6.9.0
 877       *
 878       * @param WP_Interactivity_API_Directives_Processor $p      The directives processor instance.
 879       * @param string                                    $prefix The directive prefix to filter by.
 880       * @return array An array of entries containing the directive namespace, value, suffix, and unique ID.
 881       */
 882  	private function get_directive_entries( WP_Interactivity_API_Directives_Processor $p, string $prefix ) {
 883          $directive_attributes = $p->get_attribute_names_with_prefix( 'data-wp-' . $prefix );
 884          $entries              = array();
 885          foreach ( $directive_attributes as $attribute_name ) {
 886              [ 'prefix' => $attr_prefix, 'suffix' => $suffix, 'unique_id' => $unique_id] = $this->parse_directive_name( $attribute_name );
 887              // Ensure it is the desired directive.
 888              if ( $prefix !== $attr_prefix ) {
 889                  continue;
 890              }
 891              list( $namespace, $value ) = $this->extract_directive_value( $p->get_attribute( $attribute_name ), end( $this->namespace_stack ) );
 892              $entries[]                 = array(
 893                  'namespace' => $namespace,
 894                  'value'     => $value,
 895                  'suffix'    => $suffix,
 896                  'unique_id' => $unique_id,
 897              );
 898          }
 899          // Sort directive entries to ensure stable ordering with the client.
 900          // Put nulls first, then sort by suffix and finally by uniqueIds.
 901          usort(
 902              $entries,
 903              function ( $a, $b ) {
 904                  $a_suffix = $a['suffix'] ?? '';
 905                  $b_suffix = $b['suffix'] ?? '';
 906                  if ( $a_suffix !== $b_suffix ) {
 907                      return $a_suffix <=> $b_suffix;
 908                  }
 909                  $a_id = $a['unique_id'] ?? '';
 910                  $b_id = $b['unique_id'] ?? '';
 911                  return $a_id <=> $b_id;
 912              }
 913          );
 914          return $entries;
 915      }
 916  
 917      /**
 918       * Transforms a kebab-case string to camelCase.
 919       *
 920       * @since 6.5.0
 921       *
 922       * @param string $str The kebab-case string to transform to camelCase.
 923       * @return string The transformed camelCase string.
 924       */
 925  	private function kebab_to_camel_case( string $str ): string {
 926          return lcfirst(
 927              preg_replace_callback(
 928                  '/(-)([a-z])/',
 929                  function ( $matches ) {
 930                      return strtoupper( $matches[2] );
 931                  },
 932                  strtolower( rtrim( $str, '-' ) )
 933              )
 934          );
 935      }
 936  
 937      /**
 938       * Processes the `data-wp-interactive` directive.
 939       *
 940       * It adds the default store namespace defined in the directive value to the
 941       * stack so that it's available for the nested interactivity elements.
 942       *
 943       * @since 6.5.0
 944       *
 945       * @param WP_Interactivity_API_Directives_Processor $p    The directives processor instance.
 946       * @param string                                    $mode Whether the processing is entering or exiting the tag.
 947       */
 948  	private function data_wp_interactive_processor( WP_Interactivity_API_Directives_Processor $p, string $mode ) {
 949          // When exiting tags, it removes the last namespace from the stack.
 950          if ( 'exit' === $mode ) {
 951              array_pop( $this->namespace_stack );
 952              return;
 953          }
 954  
 955          // Tries to decode the `data-wp-interactive` attribute value.
 956          $attribute_value = $p->get_attribute( 'data-wp-interactive' );
 957  
 958          /*
 959           * Pushes the newly defined namespace or the current one if the
 960           * `data-wp-interactive` definition was invalid or does not contain a
 961           * namespace. It does so because the function pops out the current namespace
 962           * from the stack whenever it finds a `data-wp-interactive`'s closing tag,
 963           * independently of whether the previous `data-wp-interactive` definition
 964           * contained a valid namespace.
 965           */
 966          $new_namespace = null;
 967          if ( is_string( $attribute_value ) && ! empty( $attribute_value ) ) {
 968              $decoded_json = json_decode( $attribute_value, true );
 969              if ( is_array( $decoded_json ) ) {
 970                  $new_namespace = $decoded_json['namespace'] ?? null;
 971              } else {
 972                  $new_namespace = $attribute_value;
 973              }
 974          }
 975          $this->namespace_stack[] = ( $new_namespace && 1 === preg_match( '/^([\w\-_\/]+)/', $new_namespace ) )
 976              ? $new_namespace
 977              : end( $this->namespace_stack );
 978      }
 979  
 980      /**
 981       * Processes the `data-wp-context` directive.
 982       *
 983       * It adds the context defined in the directive value to the stack so that
 984       * it's available for the nested interactivity elements.
 985       *
 986       * @since 6.5.0
 987       *
 988       * @param WP_Interactivity_API_Directives_Processor $p               The directives processor instance.
 989       * @param string                                    $mode            Whether the processing is entering or exiting the tag.
 990       */
 991  	private function data_wp_context_processor( WP_Interactivity_API_Directives_Processor $p, string $mode ) {
 992          // When exiting tags, it removes the last context from the stack.
 993          if ( 'exit' === $mode ) {
 994              array_pop( $this->context_stack );
 995              return;
 996          }
 997  
 998          $entries = $this->get_directive_entries( $p, 'context' );
 999          $context = end( $this->context_stack ) !== false ? end( $this->context_stack ) : array();
1000          foreach ( $entries as $entry ) {
1001              if ( null !== $entry['suffix'] ) {
1002                  continue;
1003              }
1004  
1005              $context = array_replace_recursive(
1006                  $context,
1007                  array( $entry['namespace'] => is_array( $entry['value'] ) ? $entry['value'] : array() )
1008              );
1009          }
1010          $this->context_stack[] = $context;
1011      }
1012  
1013      /**
1014       * Processes the `data-wp-bind` directive.
1015       *
1016       * It updates or removes the bound attributes based on the evaluation of its
1017       * associated reference.
1018       *
1019       * @since 6.5.0
1020       *
1021       * @param WP_Interactivity_API_Directives_Processor $p               The directives processor instance.
1022       * @param string                                    $mode            Whether the processing is entering or exiting the tag.
1023       */
1024  	private function data_wp_bind_processor( WP_Interactivity_API_Directives_Processor $p, string $mode ) {
1025          if ( 'enter' === $mode ) {
1026              $entries = $this->get_directive_entries( $p, 'bind' );
1027              foreach ( $entries as $entry ) {
1028                  if ( empty( $entry['suffix'] ) || null !== $entry['unique_id'] ) {
1029                          continue;
1030                  }
1031  
1032                  // Skip if the suffix is an event handler.
1033                  if ( str_starts_with( $entry['suffix'], 'on' ) ) {
1034                      _doing_it_wrong(
1035                          __METHOD__,
1036                          sprintf(
1037                              /* translators: %s: The directive, e.g. data-wp-on--click. */
1038                              __( 'Binding event handler attributes is not supported. Please use "%s" instead.' ),
1039                              esc_attr( 'data-wp-on--' . substr( $entry['suffix'], 2 ) )
1040                          ),
1041                          '6.9.2'
1042                      );
1043                      continue;
1044                  }
1045  
1046                  $result = $this->evaluate( $entry );
1047  
1048                  if (
1049                      null !== $result &&
1050                      (
1051                          false !== $result ||
1052                          ( strlen( $entry['suffix'] ) > 5 && '-' === $entry['suffix'][4] )
1053                      )
1054                  ) {
1055                      /*
1056                       * If the result of the evaluation is a boolean and the attribute is
1057                       * `aria-` or `data-, convert it to a string "true" or "false". It
1058                       * follows the exact same logic as Preact because it needs to
1059                       * replicate what Preact will later do in the client:
1060                       * https://github.com/preactjs/preact/blob/ea49f7a0f9d1ff2c98c0bdd66aa0cbc583055246/src/diff/props.js#L131C24-L136
1061                       */
1062                      if (
1063                          is_bool( $result ) &&
1064                          ( strlen( $entry['suffix'] ) > 5 && '-' === $entry['suffix'][4] )
1065                      ) {
1066                          $result = $result ? 'true' : 'false';
1067                      }
1068                      $p->set_attribute( $entry['suffix'], $result );
1069                  } else {
1070                      $p->remove_attribute( $entry['suffix'] );
1071                  }
1072              }
1073          }
1074      }
1075  
1076      /**
1077       * Processes the `data-wp-class` directive.
1078       *
1079       * It adds or removes CSS classes in the current HTML element based on the
1080       * evaluation of its associated references.
1081       *
1082       * @since 6.5.0
1083       *
1084       * @param WP_Interactivity_API_Directives_Processor $p               The directives processor instance.
1085       * @param string                                    $mode            Whether the processing is entering or exiting the tag.
1086       */
1087  	private function data_wp_class_processor( WP_Interactivity_API_Directives_Processor $p, string $mode ) {
1088          if ( 'enter' === $mode ) {
1089              $entries = $this->get_directive_entries( $p, 'class' );
1090              foreach ( $entries as $entry ) {
1091                  if ( empty( $entry['suffix'] ) ) {
1092                      continue;
1093                  }
1094                  $class_name = isset( $entry['unique_id'] ) && $entry['unique_id']
1095                      ? "{$entry['suffix']}---{$entry['unique_id']}"
1096                      : $entry['suffix'];
1097  
1098                  if ( empty( $class_name ) ) {
1099                      return;
1100                  }
1101  
1102                  $result = $this->evaluate( $entry );
1103  
1104                  if ( $result ) {
1105                      $p->add_class( $class_name );
1106                  } else {
1107                      $p->remove_class( $class_name );
1108                  }
1109              }
1110          }
1111      }
1112  
1113      /**
1114       * Processes the `data-wp-style` directive.
1115       *
1116       * It updates the style attribute value of the current HTML element based on
1117       * the evaluation of its associated references.
1118       *
1119       * @since 6.5.0
1120       *
1121       * @param WP_Interactivity_API_Directives_Processor $p               The directives processor instance.
1122       * @param string                                    $mode            Whether the processing is entering or exiting the tag.
1123       */
1124  	private function data_wp_style_processor( WP_Interactivity_API_Directives_Processor $p, string $mode ) {
1125          if ( 'enter' === $mode ) {
1126              $entries = $this->get_directive_entries( $p, 'style' );
1127              foreach ( $entries as $entry ) {
1128                  $style_property = $entry['suffix'];
1129                  if ( empty( $style_property ) || null !== $entry['unique_id'] ) {
1130                      continue;
1131                  }
1132  
1133                  $style_property_value  = $this->evaluate( $entry );
1134                  $style_attribute_value = $p->get_attribute( 'style' );
1135                  $style_attribute_value = ( $style_attribute_value && ! is_bool( $style_attribute_value ) ) ? $style_attribute_value : '';
1136  
1137                  /*
1138                   * Checks first if the style property is not falsy and the style
1139                   * attribute value is not empty because if it is, it doesn't need to
1140                   * update the attribute value.
1141                   */
1142                  if ( $style_property_value || $style_attribute_value ) {
1143                      $style_attribute_value = $this->merge_style_property( $style_attribute_value, $style_property, $style_property_value );
1144                      /*
1145                       * If the style attribute value is not empty, it sets it. Otherwise,
1146                       * it removes it.
1147                       */
1148                      if ( ! empty( $style_attribute_value ) ) {
1149                          $p->set_attribute( 'style', $style_attribute_value );
1150                      } else {
1151                          $p->remove_attribute( 'style' );
1152                      }
1153                  }
1154              }
1155          }
1156      }
1157  
1158      /**
1159       * Merges an individual style property in the `style` attribute of an HTML
1160       * element, updating or removing the property when necessary.
1161       *
1162       * If a property is modified, the old one is removed and the new one is added
1163       * at the end of the list.
1164       *
1165       * @since 6.5.0
1166       *
1167       * Example:
1168       *
1169       *     merge_style_property( 'color:green;', 'color', 'red' )      => 'color:red;'
1170       *     merge_style_property( 'background:green;', 'color', 'red' ) => 'background:green;color:red;'
1171       *     merge_style_property( 'color:green;', 'color', null )       => ''
1172       *
1173       * @param string            $style_attribute_value The current style attribute value.
1174       * @param string            $style_property_name   The style property name to set.
1175       * @param string|false|null $style_property_value  The value to set for the style property. With false, null or an
1176       *                                                 empty string, it removes the style property.
1177       * @return string The new style attribute value after the specified property has been added, updated or removed.
1178       */
1179  	private function merge_style_property( string $style_attribute_value, string $style_property_name, $style_property_value ): string {
1180          $style_assignments    = explode( ';', $style_attribute_value );
1181          $result               = array();
1182          $style_property_value = ! empty( $style_property_value ) ? rtrim( trim( $style_property_value ), ';' ) : null;
1183          $new_style_property   = $style_property_value ? $style_property_name . ':' . $style_property_value . ';' : '';
1184  
1185          // Generates an array with all the properties but the modified one.
1186          foreach ( $style_assignments as $style_assignment ) {
1187              if ( empty( trim( $style_assignment ) ) ) {
1188                  continue;
1189              }
1190              list( $name, $value ) = explode( ':', $style_assignment );
1191              if ( trim( $name ) !== $style_property_name ) {
1192                  $result[] = trim( $name ) . ':' . trim( $value ) . ';';
1193              }
1194          }
1195  
1196          // Adds the new/modified property at the end of the list.
1197          $result[] = $new_style_property;
1198  
1199          return implode( '', $result );
1200      }
1201  
1202      /**
1203       * Processes the `data-wp-text` directive.
1204       *
1205       * It updates the inner content of the current HTML element based on the
1206       * evaluation of its associated reference.
1207       *
1208       * @since 6.5.0
1209       *
1210       * @param WP_Interactivity_API_Directives_Processor $p               The directives processor instance.
1211       * @param string                                    $mode            Whether the processing is entering or exiting the tag.
1212       */
1213  	private function data_wp_text_processor( WP_Interactivity_API_Directives_Processor $p, string $mode ) {
1214          if ( 'enter' === $mode ) {
1215              $entries     = $this->get_directive_entries( $p, 'text' );
1216              $valid_entry = null;
1217              // Get the first valid `data-wp-text` entry without suffix or unique ID.
1218              foreach ( $entries as $entry ) {
1219                  if ( null === $entry['suffix'] && null === $entry['unique_id'] && ! empty( $entry['value'] ) ) {
1220                      $valid_entry = $entry;
1221                      break;
1222                  }
1223              }
1224              if ( null === $valid_entry ) {
1225                  return;
1226              }
1227              $result = $this->evaluate( $valid_entry );
1228  
1229              /*
1230               * Follows the same logic as Preact in the client and only changes the
1231               * content if the value is a string or a number. Otherwise, it removes the
1232               * content.
1233               */
1234              if ( is_string( $result ) || is_numeric( $result ) ) {
1235                  $p->set_content_between_balanced_tags( esc_html( $result ) );
1236              } else {
1237                  $p->set_content_between_balanced_tags( '' );
1238              }
1239          }
1240      }
1241  
1242      /**
1243       * Returns the CSS styles for animating the top loading bar in the router.
1244       *
1245       * @since 6.5.0
1246       *
1247       * @return string The CSS styles for the router's top loading bar animation.
1248       */
1249  	private function get_router_animation_styles(): string {
1250          return <<<CSS
1251              .wp-interactivity-router-loading-bar {
1252                  position: fixed;
1253                  top: 0;
1254                  left: 0;
1255                  margin: 0;
1256                  padding: 0;
1257                  width: 100vw;
1258                  max-width: 100vw !important;
1259                  height: 4px;
1260                  background-color: #000;
1261                  opacity: 0
1262              }
1263              .wp-interactivity-router-loading-bar.start-animation {
1264                  animation: wp-interactivity-router-loading-bar-start-animation 30s cubic-bezier(0.03, 0.5, 0, 1) forwards
1265              }
1266              .wp-interactivity-router-loading-bar.finish-animation {
1267                  animation: wp-interactivity-router-loading-bar-finish-animation 300ms ease-in
1268              }
1269              @keyframes wp-interactivity-router-loading-bar-start-animation {
1270                  0% { transform: scaleX(0); transform-origin: 0 0; opacity: 1 }
1271                  100% { transform: scaleX(1); transform-origin: 0 0; opacity: 1 }
1272              }
1273              @keyframes wp-interactivity-router-loading-bar-finish-animation {
1274                  0% { opacity: 1 }
1275                  50% { opacity: 1 }
1276                  100% { opacity: 0 }
1277              }
1278  CSS;
1279      }
1280  
1281      /**
1282       * Deprecated.
1283       *
1284       * @since 6.5.0
1285       * @deprecated 6.7.0 Use {@see WP_Interactivity_API::print_router_markup} instead.
1286       */
1287  	public function print_router_loading_and_screen_reader_markup() {
1288          _deprecated_function( __METHOD__, '6.7.0', 'WP_Interactivity_API::print_router_markup' );
1289  
1290          // Call the new method.
1291          $this->print_router_markup();
1292      }
1293  
1294      /**
1295       * Outputs markup for the @wordpress/interactivity-router script module.
1296       *
1297       * This method prints a div element representing a loading bar visible during
1298       * navigation.
1299       *
1300       * @since 6.7.0
1301       */
1302  	public function print_router_markup() {
1303          echo <<<HTML
1304              <div
1305                  class="wp-interactivity-router-loading-bar"
1306                  data-wp-interactive="core/router/private"
1307                  data-wp-class--start-animation="state.navigation.hasStarted"
1308                  data-wp-class--finish-animation="state.navigation.hasFinished"
1309              ></div>
1310  HTML;
1311      }
1312  
1313      /**
1314       * Processes the `data-wp-router-region` directive.
1315       *
1316       * It renders in the footer a set of HTML elements to notify users about
1317       * client-side navigations. More concretely, the elements added are 1) a
1318       * top loading bar to visually inform that a navigation is in progress
1319       * and 2) an `aria-live` region for accessible navigation announcements.
1320       *
1321       * @since 6.5.0
1322       *
1323       * @param WP_Interactivity_API_Directives_Processor $p               The directives processor instance.
1324       * @param string                                    $mode            Whether the processing is entering or exiting the tag.
1325       */
1326  	private function data_wp_router_region_processor( WP_Interactivity_API_Directives_Processor $p, string $mode ) {
1327          if ( 'enter' === $mode && ! $this->has_processed_router_region ) {
1328              $this->has_processed_router_region = true;
1329  
1330              // Initializes the `state.url` property from the server.
1331              $this->state(
1332                  'core/router',
1333                  array(
1334                      'url' => get_self_link(),
1335                  )
1336              );
1337  
1338              // Enqueues as an inline style.
1339              wp_register_style( 'wp-interactivity-router-animations', false );
1340              wp_add_inline_style( 'wp-interactivity-router-animations', $this->get_router_animation_styles() );
1341              wp_enqueue_style( 'wp-interactivity-router-animations' );
1342  
1343              // Adds the necessary markup to the footer.
1344              add_action( 'wp_footer', array( $this, 'print_router_markup' ) );
1345          }
1346      }
1347  
1348      /**
1349       * Processes the `data-wp-each` directive.
1350       *
1351       * This directive gets an array passed as reference and iterates over it
1352       * generating new content for each item based on the inner markup of the
1353       * `template` tag.
1354       *
1355       * @since 6.5.0
1356       * @since 6.9.0 Include the list path in the rendered `data-wp-each-child` directives.
1357       *
1358       * @param WP_Interactivity_API_Directives_Processor $p               The directives processor instance.
1359       * @param string                                    $mode            Whether the processing is entering or exiting the tag.
1360       * @param array                                     $tag_stack       The reference to the tag stack.
1361       */
1362  	private function data_wp_each_processor( WP_Interactivity_API_Directives_Processor $p, string $mode, array &$tag_stack ) {
1363          if ( 'enter' === $mode && 'TEMPLATE' === $p->get_tag() ) {
1364              $entries = $this->get_directive_entries( $p, 'each' );
1365              if ( count( $entries ) > 1 || empty( $entries ) ) {
1366                  // There should be only one `data-wp-each` directive per template tag.
1367                  return;
1368              }
1369              $entry = $entries[0];
1370              if ( null !== $entry['unique_id'] ) {
1371                  return;
1372              }
1373              $item_name = isset( $entry['suffix'] ) ? $this->kebab_to_camel_case( $entry['suffix'] ) : 'item';
1374              $result    = $this->evaluate( $entry );
1375  
1376              // Gets the content between the template tags and leaves the cursor in the closer tag.
1377              $inner_content = $p->get_content_between_balanced_template_tags();
1378  
1379              // Checks if there is a manual server-side directive processing.
1380              $template_end = 'data-wp-each: template end';
1381              $p->set_bookmark( $template_end );
1382              $p->next_tag();
1383              $manual_sdp = $p->get_attribute( 'data-wp-each-child' );
1384              $p->seek( $template_end ); // Rewinds to the template closer tag.
1385              $p->release_bookmark( $template_end );
1386  
1387              /*
1388               * It doesn't process in these situations:
1389               * - Manual server-side directive processing.
1390               * - Empty or non-array values.
1391               * - Associative arrays because those are deserialized as objects in JS.
1392               * - Templates that contain top-level texts because those texts can't be
1393               *   identified and removed in the client.
1394               */
1395              if (
1396                  $manual_sdp ||
1397                  empty( $result ) ||
1398                  ! is_array( $result ) ||
1399                  ! array_is_list( $result ) ||
1400                  ! str_starts_with( trim( $inner_content ), '<' ) ||
1401                  ! str_ends_with( trim( $inner_content ), '>' )
1402              ) {
1403                  array_pop( $tag_stack );
1404                  return;
1405              }
1406  
1407              // Processes the inner content for each item of the array.
1408              $processed_content = '';
1409              foreach ( $result as $item ) {
1410                  // Creates a new context that includes the current item of the array.
1411                  $this->context_stack[] = array_replace_recursive(
1412                      end( $this->context_stack ) !== false ? end( $this->context_stack ) : array(),
1413                      array( $entry['namespace'] => array( $item_name => $item ) )
1414                  );
1415  
1416                  // Processes the inner content with the new context.
1417                  $processed_item = $this->_process_directives( $inner_content );
1418  
1419                  if ( null === $processed_item ) {
1420                      // If the HTML is unbalanced, stop processing it.
1421                      array_pop( $this->context_stack );
1422                      return;
1423                  }
1424  
1425                  /*
1426                   * Adds the `data-wp-each-child` directive to each top-level tag
1427                   * rendered by this `data-wp-each` directive. The value is the
1428                   * `data-wp-each` directive's namespace and path.
1429                   *
1430                   * Nested `data-wp-each` directives could render
1431                   * `data-wp-each-child` elements at the top level as well, and
1432                   * they should be overwritten.
1433                   *
1434                   * @since 6.9.0
1435                   */
1436                  $i = new WP_Interactivity_API_Directives_Processor( $processed_item );
1437                  while ( $i->next_tag() ) {
1438                      $i->set_attribute( 'data-wp-each-child', $entry['namespace'] . '::' . $entry['value'] );
1439                      $i->next_balanced_tag_closer_tag();
1440                  }
1441                  $processed_content .= $i->get_updated_html();
1442  
1443                  // Removes the current context from the stack.
1444                  array_pop( $this->context_stack );
1445              }
1446  
1447              // Appends the processed content after the tag closer of the template.
1448              $p->append_content_after_template_tag_closer( $processed_content );
1449  
1450              // Pops the last tag because it skipped the closing tag of the template tag.
1451              array_pop( $tag_stack );
1452          }
1453      }
1454  }


Generated : Fri Jun 26 08:20:11 2026 Cross-referenced by PHPXref