[ 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       * Flag that indicates whether the `data-wp-router-region` directive has
  63       * been found in the HTML and processed.
  64       *
  65       * The value is saved in a private property of the WP_Interactivity_API
  66       * instance instead of using a static variable inside the processor
  67       * function, which would hold the same value for all instances
  68       * independently of whether they have processed any
  69       * `data-wp-router-region` directive or not.
  70       *
  71       * @since 6.5.0
  72       * @var bool
  73       */
  74      private $has_processed_router_region = false;
  75  
  76      /**
  77       * Gets and/or sets the initial state of an Interactivity API store for a
  78       * given namespace.
  79       *
  80       * If state for that store namespace already exists, it merges the new
  81       * provided state with the existing one.
  82       *
  83       * @since 6.5.0
  84       *
  85       * @param string $store_namespace The unique store namespace identifier.
  86       * @param array  $state           Optional. The array that will be merged with the existing state for the specified
  87       *                                store namespace.
  88       * @return array The current state for the specified store namespace. This will be the updated state if a $state
  89       *               argument was provided.
  90       */
  91  	public function state( string $store_namespace, array $state = array() ): array {
  92          if ( ! isset( $this->state_data[ $store_namespace ] ) ) {
  93              $this->state_data[ $store_namespace ] = array();
  94          }
  95          if ( is_array( $state ) ) {
  96              $this->state_data[ $store_namespace ] = array_replace_recursive(
  97                  $this->state_data[ $store_namespace ],
  98                  $state
  99              );
 100          }
 101          return $this->state_data[ $store_namespace ];
 102      }
 103  
 104      /**
 105       * Gets and/or sets the configuration of the Interactivity API for a given
 106       * store namespace.
 107       *
 108       * If configuration for that store namespace exists, it merges the new
 109       * provided configuration with the existing one.
 110       *
 111       * @since 6.5.0
 112       *
 113       * @param string $store_namespace The unique store namespace identifier.
 114       * @param array  $config          Optional. The array that will be merged with the existing configuration for the
 115       *                                specified store namespace.
 116       * @return array The configuration for the specified store namespace. This will be the updated configuration if a
 117       *               $config argument was provided.
 118       */
 119  	public function config( string $store_namespace, array $config = array() ): array {
 120          if ( ! isset( $this->config_data[ $store_namespace ] ) ) {
 121              $this->config_data[ $store_namespace ] = array();
 122          }
 123          if ( is_array( $config ) ) {
 124              $this->config_data[ $store_namespace ] = array_replace_recursive(
 125                  $this->config_data[ $store_namespace ],
 126                  $config
 127              );
 128          }
 129          return $this->config_data[ $store_namespace ];
 130      }
 131  
 132      /**
 133       * Prints the serialized client-side interactivity data.
 134       *
 135       * Encodes the config and initial state into JSON and prints them inside a
 136       * script tag of type "application/json". Once in the browser, the state will
 137       * be parsed and used to hydrate the client-side interactivity stores and the
 138       * configuration will be available using a `getConfig` utility.
 139       *
 140       * @since 6.5.0
 141       */
 142  	public function print_client_interactivity_data() {
 143          if ( empty( $this->state_data ) && empty( $this->config_data ) ) {
 144              return;
 145          }
 146  
 147          $interactivity_data = array();
 148  
 149          $config = array();
 150          foreach ( $this->config_data as $key => $value ) {
 151              if ( ! empty( $value ) ) {
 152                  $config[ $key ] = $value;
 153              }
 154          }
 155          if ( ! empty( $config ) ) {
 156              $interactivity_data['config'] = $config;
 157          }
 158  
 159          $state = array();
 160          foreach ( $this->state_data as $key => $value ) {
 161              if ( ! empty( $value ) ) {
 162                  $state[ $key ] = $value;
 163              }
 164          }
 165          if ( ! empty( $state ) ) {
 166              $interactivity_data['state'] = $state;
 167          }
 168  
 169          if ( ! empty( $interactivity_data ) ) {
 170              wp_print_inline_script_tag(
 171                  wp_json_encode(
 172                      $interactivity_data,
 173                      JSON_HEX_TAG | JSON_HEX_AMP
 174                  ),
 175                  array(
 176                      'type' => 'application/json',
 177                      'id'   => 'wp-interactivity-data',
 178                  )
 179              );
 180          }
 181      }
 182  
 183      /**
 184       * Registers the `@wordpress/interactivity` script modules.
 185       *
 186       * @since 6.5.0
 187       */
 188  	public function register_script_modules() {
 189          $suffix = wp_scripts_get_suffix();
 190  
 191          wp_register_script_module(
 192              '@wordpress/interactivity',
 193              includes_url( "js/dist/interactivity$suffix.js" )
 194          );
 195  
 196          wp_register_script_module(
 197              '@wordpress/interactivity-router',
 198              includes_url( "js/dist/interactivity-router$suffix.js" ),
 199              array( '@wordpress/interactivity' )
 200          );
 201      }
 202  
 203      /**
 204       * Adds the necessary hooks for the Interactivity API.
 205       *
 206       * @since 6.5.0
 207       */
 208  	public function add_hooks() {
 209          add_action( 'wp_enqueue_scripts', array( $this, 'register_script_modules' ) );
 210          add_action( 'wp_footer', array( $this, 'print_client_interactivity_data' ) );
 211      }
 212  
 213      /**
 214       * Processes the interactivity directives contained within the HTML content
 215       * and updates the markup accordingly.
 216       *
 217       * @since 6.5.0
 218       *
 219       * @param string $html The HTML content to process.
 220       * @return string The processed HTML content. It returns the original content when the HTML contains unbalanced tags.
 221       */
 222  	public function process_directives( string $html ): string {
 223          if ( ! str_contains( $html, 'data-wp-' ) ) {
 224              return $html;
 225          }
 226  
 227          $context_stack   = array();
 228          $namespace_stack = array();
 229          $result          = $this->process_directives_args( $html, $context_stack, $namespace_stack );
 230          return null === $result ? $html : $result;
 231      }
 232  
 233      /**
 234       * Processes the interactivity directives contained within the HTML content
 235       * and updates the markup accordingly.
 236       *
 237       * It needs the context and namespace stacks to be passed by reference, and
 238       * it returns null if the HTML contains unbalanced tags.
 239       *
 240       * @since 6.5.0
 241       *
 242       * @param string $html            The HTML content to process.
 243       * @param array  $context_stack   The reference to the array used to keep track of contexts during processing.
 244       * @param array  $namespace_stack The reference to the array used to manage namespaces during processing.
 245       * @return string|null The processed HTML content. It returns null when the HTML contains unbalanced tags.
 246       */
 247  	private function process_directives_args( string $html, array &$context_stack, array &$namespace_stack ) {
 248          $p          = new WP_Interactivity_API_Directives_Processor( $html );
 249          $tag_stack  = array();
 250          $unbalanced = false;
 251  
 252          $directive_processor_prefixes          = array_keys( self::$directive_processors );
 253          $directive_processor_prefixes_reversed = array_reverse( $directive_processor_prefixes );
 254  
 255          while ( $p->next_tag( array( 'tag_closers' => 'visit' ) ) ) {
 256              $tag_name = $p->get_tag();
 257  
 258              /*
 259               * Directives inside SVG and MATH tags are not processed,
 260               * as they are not compatible with the Tag Processor yet.
 261               * We still process the rest of the HTML.
 262               */
 263              if ( 'SVG' === $tag_name || 'MATH' === $tag_name ) {
 264                  $p->skip_to_tag_closer();
 265                  continue;
 266              }
 267  
 268              if ( $p->is_tag_closer() ) {
 269                  list( $opening_tag_name, $directives_prefixes ) = end( $tag_stack );
 270  
 271                  if ( 0 === count( $tag_stack ) || $opening_tag_name !== $tag_name ) {
 272  
 273                      /*
 274                       * If the tag stack is empty or the matching opening tag is not the
 275                       * same than the closing tag, it means the HTML is unbalanced and it
 276                       * stops processing it.
 277                       */
 278                      $unbalanced = true;
 279                      break;
 280                  } else {
 281                      // Remove the last tag from the stack.
 282                      array_pop( $tag_stack );
 283                  }
 284              } else {
 285                  if ( 0 !== count( $p->get_attribute_names_with_prefix( 'data-wp-each-child' ) ) ) {
 286                      /*
 287                       * If the tag has a `data-wp-each-child` directive, jump to its closer
 288                       * tag because those tags have already been processed.
 289                       */
 290                      $p->next_balanced_tag_closer_tag();
 291                      continue;
 292                  } else {
 293                      $directives_prefixes = array();
 294  
 295                      // Checks if there is a server directive processor registered for each directive.
 296                      foreach ( $p->get_attribute_names_with_prefix( 'data-wp-' ) as $attribute_name ) {
 297                          list( $directive_prefix ) = $this->extract_prefix_and_suffix( $attribute_name );
 298                          if ( array_key_exists( $directive_prefix, self::$directive_processors ) ) {
 299                              $directives_prefixes[] = $directive_prefix;
 300                          }
 301                      }
 302  
 303                      /*
 304                       * If this tag will visit its closer tag, it adds it to the tag stack
 305                       * so it can process its closing tag and check for unbalanced tags.
 306                       */
 307                      if ( $p->has_and_visits_its_closer_tag() ) {
 308                          $tag_stack[] = array( $tag_name, $directives_prefixes );
 309                      }
 310                  }
 311              }
 312              /*
 313               * If the matching opener tag didn't have any directives, it can skip the
 314               * processing.
 315               */
 316              if ( 0 === count( $directives_prefixes ) ) {
 317                  continue;
 318              }
 319  
 320              // Directive processing might be different depending on if it is entering the tag or exiting it.
 321              $modes = array(
 322                  'enter' => ! $p->is_tag_closer(),
 323                  'exit'  => $p->is_tag_closer() || ! $p->has_and_visits_its_closer_tag(),
 324              );
 325  
 326              foreach ( $modes as $mode => $should_run ) {
 327                  if ( ! $should_run ) {
 328                      continue;
 329                  }
 330  
 331                  /*
 332                   * Sorts the attributes by the order of the `directives_processor` array
 333                   * and checks what directives are present in this element.
 334                   */
 335                  $existing_directives_prefixes = array_intersect(
 336                      'enter' === $mode ? $directive_processor_prefixes : $directive_processor_prefixes_reversed,
 337                      $directives_prefixes
 338                  );
 339                  foreach ( $existing_directives_prefixes as $directive_prefix ) {
 340                      $func = is_array( self::$directive_processors[ $directive_prefix ] )
 341                          ? self::$directive_processors[ $directive_prefix ]
 342                          : array( $this, self::$directive_processors[ $directive_prefix ] );
 343  
 344                      call_user_func_array(
 345                          $func,
 346                          array( $p, $mode, &$context_stack, &$namespace_stack, &$tag_stack )
 347                      );
 348                  }
 349              }
 350          }
 351  
 352          /*
 353           * It returns null if the HTML is unbalanced because unbalanced HTML is
 354           * not safe to process. In that case, the Interactivity API runtime will
 355           * update the HTML on the client side during the hydration.
 356           */
 357          return $unbalanced || 0 < count( $tag_stack ) ? null : $p->get_updated_html();
 358      }
 359  
 360      /**
 361       * Evaluates the reference path passed to a directive based on the current
 362       * store namespace, state and context.
 363       *
 364       * @since 6.5.0
 365       *
 366       * @param string|true $directive_value   The directive attribute value string or `true` when it's a boolean attribute.
 367       * @param string      $default_namespace The default namespace to use if none is explicitly defined in the directive
 368       *                                       value.
 369       * @param array|false $context           The current context for evaluating the directive or false if there is no
 370       *                                       context.
 371       * @return mixed|null The result of the evaluation. Null if the reference path doesn't exist.
 372       */
 373  	private function evaluate( $directive_value, string $default_namespace, $context = false ) {
 374          list( $ns, $path ) = $this->extract_directive_value( $directive_value, $default_namespace );
 375          if ( empty( $path ) ) {
 376              return null;
 377          }
 378  
 379          $store = array(
 380              'state'   => $this->state_data[ $ns ] ?? array(),
 381              'context' => $context[ $ns ] ?? array(),
 382          );
 383  
 384          // Checks if the reference path is preceded by a negation operator (!).
 385          $should_negate_value = '!' === $path[0];
 386          $path                = $should_negate_value ? substr( $path, 1 ) : $path;
 387  
 388          // Extracts the value from the store using the reference path.
 389          $path_segments = explode( '.', $path );
 390          $current       = $store;
 391          foreach ( $path_segments as $path_segment ) {
 392              if ( isset( $current[ $path_segment ] ) ) {
 393                  $current = $current[ $path_segment ];
 394              } else {
 395                  return null;
 396              }
 397          }
 398  
 399          // Returns the opposite if it contains a negation operator (!).
 400          return $should_negate_value ? ! $current : $current;
 401      }
 402  
 403      /**
 404       * Extracts the directive attribute name to separate and return the directive
 405       * prefix and an optional suffix.
 406       *
 407       * The suffix is the string after the first double hyphen and the prefix is
 408       * everything that comes before the suffix.
 409       *
 410       * Example:
 411       *
 412       *     extract_prefix_and_suffix( 'data-wp-interactive' )   => array( 'data-wp-interactive', null )
 413       *     extract_prefix_and_suffix( 'data-wp-bind--src' )     => array( 'data-wp-bind', 'src' )
 414       *     extract_prefix_and_suffix( 'data-wp-foo--and--bar' ) => array( 'data-wp-foo', 'and--bar' )
 415       *
 416       * @since 6.5.0
 417       *
 418       * @param string $directive_name The directive attribute name.
 419       * @return array An array containing the directive prefix and optional suffix.
 420       */
 421  	private function extract_prefix_and_suffix( string $directive_name ): array {
 422          return explode( '--', $directive_name, 2 );
 423      }
 424  
 425      /**
 426       * Parses and extracts the namespace and reference path from the given
 427       * directive attribute value.
 428       *
 429       * If the value doesn't contain an explicit namespace, it returns the
 430       * default one. If the value contains a JSON object instead of a reference
 431       * path, the function tries to parse it and return the resulting array. If
 432       * the value contains strings that represent booleans ("true" and "false"),
 433       * numbers ("1" and "1.2") or "null", the function also transform them to
 434       * regular booleans, numbers and `null`.
 435       *
 436       * Example:
 437       *
 438       *     extract_directive_value( 'actions.foo', 'myPlugin' )                      => array( 'myPlugin', 'actions.foo' )
 439       *     extract_directive_value( 'otherPlugin::actions.foo', 'myPlugin' )         => array( 'otherPlugin', 'actions.foo' )
 440       *     extract_directive_value( '{ "isOpen": false }', 'myPlugin' )              => array( 'myPlugin', array( 'isOpen' => false ) )
 441       *     extract_directive_value( 'otherPlugin::{ "isOpen": false }', 'myPlugin' ) => array( 'otherPlugin', array( 'isOpen' => false ) )
 442       *
 443       * @since 6.5.0
 444       *
 445       * @param string|true $directive_value   The directive attribute value. It can be `true` when it's a boolean
 446       *                                       attribute.
 447       * @param string|null $default_namespace Optional. The default namespace if none is explicitly defined.
 448       * @return array An array containing the namespace in the first item and the JSON, the reference path, or null on the
 449       *               second item.
 450       */
 451  	private function extract_directive_value( $directive_value, $default_namespace = null ): array {
 452          if ( empty( $directive_value ) || is_bool( $directive_value ) ) {
 453              return array( $default_namespace, null );
 454          }
 455  
 456          // Replaces the value and namespace if there is a namespace in the value.
 457          if ( 1 === preg_match( '/^([\w\-_\/]+)::./', $directive_value ) ) {
 458              list($default_namespace, $directive_value) = explode( '::', $directive_value, 2 );
 459          }
 460  
 461          /*
 462           * Tries to decode the value as a JSON object. If it fails and the value
 463           * isn't `null`, it returns the value as it is. Otherwise, it returns the
 464           * decoded JSON or null for the string `null`.
 465           */
 466          $decoded_json = json_decode( $directive_value, true );
 467          if ( null !== $decoded_json || 'null' === $directive_value ) {
 468              $directive_value = $decoded_json;
 469          }
 470  
 471          return array( $default_namespace, $directive_value );
 472      }
 473  
 474      /**
 475       * Transforms a kebab-case string to camelCase.
 476       *
 477       * @param string $str The kebab-case string to transform to camelCase.
 478       * @return string The transformed camelCase string.
 479       */
 480  	private function kebab_to_camel_case( string $str ): string {
 481          return lcfirst(
 482              preg_replace_callback(
 483                  '/(-)([a-z])/',
 484                  function ( $matches ) {
 485                      return strtoupper( $matches[2] );
 486                  },
 487                  strtolower( rtrim( $str, '-' ) )
 488              )
 489          );
 490      }
 491  
 492      /**
 493       * Processes the `data-wp-interactive` directive.
 494       *
 495       * It adds the default store namespace defined in the directive value to the
 496       * stack so that it's available for the nested interactivity elements.
 497       *
 498       * @since 6.5.0
 499       *
 500       * @param WP_Interactivity_API_Directives_Processor $p               The directives processor instance.
 501       * @param string                                    $mode            Whether the processing is entering or exiting the tag.
 502       * @param array                                     $context_stack   The reference to the context stack.
 503       * @param array                                     $namespace_stack The reference to the store namespace stack.
 504       */
 505  	private function data_wp_interactive_processor( WP_Interactivity_API_Directives_Processor $p, string $mode, array &$context_stack, array &$namespace_stack ) {
 506          // When exiting tags, it removes the last namespace from the stack.
 507          if ( 'exit' === $mode ) {
 508              array_pop( $namespace_stack );
 509              return;
 510          }
 511  
 512          // Tries to decode the `data-wp-interactive` attribute value.
 513          $attribute_value = $p->get_attribute( 'data-wp-interactive' );
 514  
 515          /*
 516           * Pushes the newly defined namespace or the current one if the
 517           * `data-wp-interactive` definition was invalid or does not contain a
 518           * namespace. It does so because the function pops out the current namespace
 519           * from the stack whenever it finds a `data-wp-interactive`'s closing tag,
 520           * independently of whether the previous `data-wp-interactive` definition
 521           * contained a valid namespace.
 522           */
 523          $new_namespace = null;
 524          if ( is_string( $attribute_value ) && ! empty( $attribute_value ) ) {
 525              $decoded_json = json_decode( $attribute_value, true );
 526              if ( is_array( $decoded_json ) ) {
 527                  $new_namespace = $decoded_json['namespace'] ?? null;
 528              } else {
 529                  $new_namespace = $attribute_value;
 530              }
 531          }
 532          $namespace_stack[] = ( $new_namespace && 1 === preg_match( '/^([\w\-_\/]+)/', $new_namespace ) )
 533              ? $new_namespace
 534              : end( $namespace_stack );
 535      }
 536  
 537      /**
 538       * Processes the `data-wp-context` directive.
 539       *
 540       * It adds the context defined in the directive value to the stack so that
 541       * it's available for the nested interactivity elements.
 542       *
 543       * @since 6.5.0
 544       *
 545       * @param WP_Interactivity_API_Directives_Processor $p               The directives processor instance.
 546       * @param string                                    $mode            Whether the processing is entering or exiting the tag.
 547       * @param array                                     $context_stack   The reference to the context stack.
 548       * @param array                                     $namespace_stack The reference to the store namespace stack.
 549       */
 550  	private function data_wp_context_processor( WP_Interactivity_API_Directives_Processor $p, string $mode, array &$context_stack, array &$namespace_stack ) {
 551          // When exiting tags, it removes the last context from the stack.
 552          if ( 'exit' === $mode ) {
 553              array_pop( $context_stack );
 554              return;
 555          }
 556  
 557          $attribute_value = $p->get_attribute( 'data-wp-context' );
 558          $namespace_value = end( $namespace_stack );
 559  
 560          // Separates the namespace from the context JSON object.
 561          list( $namespace_value, $decoded_json ) = is_string( $attribute_value ) && ! empty( $attribute_value )
 562              ? $this->extract_directive_value( $attribute_value, $namespace_value )
 563              : array( $namespace_value, null );
 564  
 565          /*
 566           * If there is a namespace, it adds a new context to the stack merging the
 567           * previous context with the new one.
 568           */
 569          if ( is_string( $namespace_value ) ) {
 570              $context_stack[] = array_replace_recursive(
 571                  end( $context_stack ) !== false ? end( $context_stack ) : array(),
 572                  array( $namespace_value => is_array( $decoded_json ) ? $decoded_json : array() )
 573              );
 574          } else {
 575              /*
 576               * If there is no namespace, it pushes the current context to the stack.
 577               * It needs to do so because the function pops out the current context
 578               * from the stack whenever it finds a `data-wp-context`'s closing tag.
 579               */
 580              $context_stack[] = end( $context_stack );
 581          }
 582      }
 583  
 584      /**
 585       * Processes the `data-wp-bind` directive.
 586       *
 587       * It updates or removes the bound attributes based on the evaluation of its
 588       * associated reference.
 589       *
 590       * @since 6.5.0
 591       *
 592       * @param WP_Interactivity_API_Directives_Processor $p               The directives processor instance.
 593       * @param string                                    $mode            Whether the processing is entering or exiting the tag.
 594       * @param array                                     $context_stack   The reference to the context stack.
 595       * @param array                                     $namespace_stack The reference to the store namespace stack.
 596       */
 597  	private function data_wp_bind_processor( WP_Interactivity_API_Directives_Processor $p, string $mode, array &$context_stack, array &$namespace_stack ) {
 598          if ( 'enter' === $mode ) {
 599              $all_bind_directives = $p->get_attribute_names_with_prefix( 'data-wp-bind--' );
 600  
 601              foreach ( $all_bind_directives as $attribute_name ) {
 602                  list( , $bound_attribute ) = $this->extract_prefix_and_suffix( $attribute_name );
 603                  if ( empty( $bound_attribute ) ) {
 604                      return;
 605                  }
 606  
 607                  $attribute_value = $p->get_attribute( $attribute_name );
 608                  $result          = $this->evaluate( $attribute_value, end( $namespace_stack ), end( $context_stack ) );
 609  
 610                  if (
 611                      null !== $result &&
 612                      (
 613                          false !== $result ||
 614                          ( strlen( $bound_attribute ) > 5 && '-' === $bound_attribute[4] )
 615                      )
 616                  ) {
 617                      /*
 618                       * If the result of the evaluation is a boolean and the attribute is
 619                       * `aria-` or `data-, convert it to a string "true" or "false". It
 620                       * follows the exact same logic as Preact because it needs to
 621                       * replicate what Preact will later do in the client:
 622                       * https://github.com/preactjs/preact/blob/ea49f7a0f9d1ff2c98c0bdd66aa0cbc583055246/src/diff/props.js#L131C24-L136
 623                       */
 624                      if (
 625                          is_bool( $result ) &&
 626                          ( strlen( $bound_attribute ) > 5 && '-' === $bound_attribute[4] )
 627                      ) {
 628                          $result = $result ? 'true' : 'false';
 629                      }
 630                      $p->set_attribute( $bound_attribute, $result );
 631                  } else {
 632                      $p->remove_attribute( $bound_attribute );
 633                  }
 634              }
 635          }
 636      }
 637  
 638      /**
 639       * Processes the `data-wp-class` directive.
 640       *
 641       * It adds or removes CSS classes in the current HTML element based on the
 642       * evaluation of its associated references.
 643       *
 644       * @since 6.5.0
 645       *
 646       * @param WP_Interactivity_API_Directives_Processor $p               The directives processor instance.
 647       * @param string                                    $mode            Whether the processing is entering or exiting the tag.
 648       * @param array                                     $context_stack   The reference to the context stack.
 649       * @param array                                     $namespace_stack The reference to the store namespace stack.
 650       */
 651  	private function data_wp_class_processor( WP_Interactivity_API_Directives_Processor $p, string $mode, array &$context_stack, array &$namespace_stack ) {
 652          if ( 'enter' === $mode ) {
 653              $all_class_directives = $p->get_attribute_names_with_prefix( 'data-wp-class--' );
 654  
 655              foreach ( $all_class_directives as $attribute_name ) {
 656                  list( , $class_name ) = $this->extract_prefix_and_suffix( $attribute_name );
 657                  if ( empty( $class_name ) ) {
 658                      return;
 659                  }
 660  
 661                  $attribute_value = $p->get_attribute( $attribute_name );
 662                  $result          = $this->evaluate( $attribute_value, end( $namespace_stack ), end( $context_stack ) );
 663  
 664                  if ( $result ) {
 665                      $p->add_class( $class_name );
 666                  } else {
 667                      $p->remove_class( $class_name );
 668                  }
 669              }
 670          }
 671      }
 672  
 673      /**
 674       * Processes the `data-wp-style` directive.
 675       *
 676       * It updates the style attribute value of the current HTML element based on
 677       * the evaluation of its associated references.
 678       *
 679       * @since 6.5.0
 680       *
 681       * @param WP_Interactivity_API_Directives_Processor $p               The directives processor instance.
 682       * @param string                                    $mode            Whether the processing is entering or exiting the tag.
 683       * @param array                                     $context_stack   The reference to the context stack.
 684       * @param array                                     $namespace_stack The reference to the store namespace stack.
 685       */
 686  	private function data_wp_style_processor( WP_Interactivity_API_Directives_Processor $p, string $mode, array &$context_stack, array &$namespace_stack ) {
 687          if ( 'enter' === $mode ) {
 688              $all_style_attributes = $p->get_attribute_names_with_prefix( 'data-wp-style--' );
 689  
 690              foreach ( $all_style_attributes as $attribute_name ) {
 691                  list( , $style_property ) = $this->extract_prefix_and_suffix( $attribute_name );
 692                  if ( empty( $style_property ) ) {
 693                      continue;
 694                  }
 695  
 696                  $directive_attribute_value = $p->get_attribute( $attribute_name );
 697                  $style_property_value      = $this->evaluate( $directive_attribute_value, end( $namespace_stack ), end( $context_stack ) );
 698                  $style_attribute_value     = $p->get_attribute( 'style' );
 699                  $style_attribute_value     = ( $style_attribute_value && ! is_bool( $style_attribute_value ) ) ? $style_attribute_value : '';
 700  
 701                  /*
 702                   * Checks first if the style property is not falsy and the style
 703                   * attribute value is not empty because if it is, it doesn't need to
 704                   * update the attribute value.
 705                   */
 706                  if ( $style_property_value || $style_attribute_value ) {
 707                      $style_attribute_value = $this->merge_style_property( $style_attribute_value, $style_property, $style_property_value );
 708                      /*
 709                       * If the style attribute value is not empty, it sets it. Otherwise,
 710                       * it removes it.
 711                       */
 712                      if ( ! empty( $style_attribute_value ) ) {
 713                          $p->set_attribute( 'style', $style_attribute_value );
 714                      } else {
 715                          $p->remove_attribute( 'style' );
 716                      }
 717                  }
 718              }
 719          }
 720      }
 721  
 722      /**
 723       * Merges an individual style property in the `style` attribute of an HTML
 724       * element, updating or removing the property when necessary.
 725       *
 726       * If a property is modified, the old one is removed and the new one is added
 727       * at the end of the list.
 728       *
 729       * @since 6.5.0
 730       *
 731       * Example:
 732       *
 733       *     merge_style_property( 'color:green;', 'color', 'red' )      => 'color:red;'
 734       *     merge_style_property( 'background:green;', 'color', 'red' ) => 'background:green;color:red;'
 735       *     merge_style_property( 'color:green;', 'color', null )       => ''
 736       *
 737       * @param string            $style_attribute_value The current style attribute value.
 738       * @param string            $style_property_name   The style property name to set.
 739       * @param string|false|null $style_property_value  The value to set for the style property. With false, null or an
 740       *                                                 empty string, it removes the style property.
 741       * @return string The new style attribute value after the specified property has been added, updated or removed.
 742       */
 743  	private function merge_style_property( string $style_attribute_value, string $style_property_name, $style_property_value ): string {
 744          $style_assignments    = explode( ';', $style_attribute_value );
 745          $result               = array();
 746          $style_property_value = ! empty( $style_property_value ) ? rtrim( trim( $style_property_value ), ';' ) : null;
 747          $new_style_property   = $style_property_value ? $style_property_name . ':' . $style_property_value . ';' : '';
 748  
 749          // Generates an array with all the properties but the modified one.
 750          foreach ( $style_assignments as $style_assignment ) {
 751              if ( empty( trim( $style_assignment ) ) ) {
 752                  continue;
 753              }
 754              list( $name, $value ) = explode( ':', $style_assignment );
 755              if ( trim( $name ) !== $style_property_name ) {
 756                  $result[] = trim( $name ) . ':' . trim( $value ) . ';';
 757              }
 758          }
 759  
 760          // Adds the new/modified property at the end of the list.
 761          $result[] = $new_style_property;
 762  
 763          return implode( '', $result );
 764      }
 765  
 766      /**
 767       * Processes the `data-wp-text` directive.
 768       *
 769       * It updates the inner content of the current HTML element based on the
 770       * evaluation of its associated reference.
 771       *
 772       * @since 6.5.0
 773       *
 774       * @param WP_Interactivity_API_Directives_Processor $p               The directives processor instance.
 775       * @param string                                    $mode            Whether the processing is entering or exiting the tag.
 776       * @param array                                     $context_stack   The reference to the context stack.
 777       * @param array                                     $namespace_stack The reference to the store namespace stack.
 778       */
 779  	private function data_wp_text_processor( WP_Interactivity_API_Directives_Processor $p, string $mode, array &$context_stack, array &$namespace_stack ) {
 780          if ( 'enter' === $mode ) {
 781              $attribute_value = $p->get_attribute( 'data-wp-text' );
 782              $result          = $this->evaluate( $attribute_value, end( $namespace_stack ), end( $context_stack ) );
 783  
 784              /*
 785               * Follows the same logic as Preact in the client and only changes the
 786               * content if the value is a string or a number. Otherwise, it removes the
 787               * content.
 788               */
 789              if ( is_string( $result ) || is_numeric( $result ) ) {
 790                  $p->set_content_between_balanced_tags( esc_html( $result ) );
 791              } else {
 792                  $p->set_content_between_balanced_tags( '' );
 793              }
 794          }
 795      }
 796  
 797      /**
 798       * Returns the CSS styles for animating the top loading bar in the router.
 799       *
 800       * @since 6.5.0
 801       *
 802       * @return string The CSS styles for the router's top loading bar animation.
 803       */
 804  	private function get_router_animation_styles(): string {
 805          return <<<CSS
 806              .wp-interactivity-router-loading-bar {
 807                  position: fixed;
 808                  top: 0;
 809                  left: 0;
 810                  margin: 0;
 811                  padding: 0;
 812                  width: 100vw;
 813                  max-width: 100vw !important;
 814                  height: 4px;
 815                  background-color: #000;
 816                  opacity: 0
 817              }
 818              .wp-interactivity-router-loading-bar.start-animation {
 819                  animation: wp-interactivity-router-loading-bar-start-animation 30s cubic-bezier(0.03, 0.5, 0, 1) forwards
 820              }
 821              .wp-interactivity-router-loading-bar.finish-animation {
 822                  animation: wp-interactivity-router-loading-bar-finish-animation 300ms ease-in
 823              }
 824              @keyframes wp-interactivity-router-loading-bar-start-animation {
 825                  0% { transform: scaleX(0); transform-origin: 0 0; opacity: 1 }
 826                  100% { transform: scaleX(1); transform-origin: 0 0; opacity: 1 }
 827              }
 828              @keyframes wp-interactivity-router-loading-bar-finish-animation {
 829                  0% { opacity: 1 }
 830                  50% { opacity: 1 }
 831                  100% { opacity: 0 }
 832              }
 833  CSS;
 834      }
 835  
 836      /**
 837       * Outputs the markup for the top loading indicator and the screen reader
 838       * notifications during client-side navigations.
 839       *
 840       * This method prints a div element representing a loading bar visible during
 841       * navigation, as well as an aria-live region that can be read by screen
 842       * readers to announce navigation status.
 843       *
 844       * @since 6.5.0
 845       */
 846  	public function print_router_loading_and_screen_reader_markup() {
 847          echo <<<HTML
 848              <div
 849                  class="wp-interactivity-router-loading-bar"
 850                  data-wp-interactive="core/router"
 851                  data-wp-class--start-animation="state.navigation.hasStarted"
 852                  data-wp-class--finish-animation="state.navigation.hasFinished"
 853              ></div>
 854              <div
 855                  class="screen-reader-text"
 856                  aria-live="polite"
 857                  data-wp-interactive="core/router"
 858                  data-wp-text="state.navigation.message"
 859              ></div>
 860  HTML;
 861      }
 862  
 863      /**
 864       * Processes the `data-wp-router-region` directive.
 865       *
 866       * It renders in the footer a set of HTML elements to notify users about
 867       * client-side navigations. More concretely, the elements added are 1) a
 868       * top loading bar to visually inform that a navigation is in progress
 869       * and 2) an `aria-live` region for accessible navigation announcements.
 870       *
 871       * @since 6.5.0
 872       *
 873       * @param WP_Interactivity_API_Directives_Processor $p               The directives processor instance.
 874       * @param string                                    $mode            Whether the processing is entering or exiting the tag.
 875       */
 876  	private function data_wp_router_region_processor( WP_Interactivity_API_Directives_Processor $p, string $mode ) {
 877          if ( 'enter' === $mode && ! $this->has_processed_router_region ) {
 878              $this->has_processed_router_region = true;
 879  
 880              // Initialize the `core/router` store.
 881              $this->state(
 882                  'core/router',
 883                  array(
 884                      'navigation' => array(
 885                          'texts' => array(
 886                              'loading' => __( 'Loading page, please wait.' ),
 887                              'loaded'  => __( 'Page Loaded.' ),
 888                          ),
 889                      ),
 890                  )
 891              );
 892  
 893              // Enqueues as an inline style.
 894              wp_register_style( 'wp-interactivity-router-animations', false );
 895              wp_add_inline_style( 'wp-interactivity-router-animations', $this->get_router_animation_styles() );
 896              wp_enqueue_style( 'wp-interactivity-router-animations' );
 897  
 898              // Adds the necessary markup to the footer.
 899              add_action( 'wp_footer', array( $this, 'print_router_loading_and_screen_reader_markup' ) );
 900          }
 901      }
 902  
 903      /**
 904       * Processes the `data-wp-each` directive.
 905       *
 906       * This directive gets an array passed as reference and iterates over it
 907       * generating new content for each item based on the inner markup of the
 908       * `template` tag.
 909       *
 910       * @since 6.5.0
 911       *
 912       * @param WP_Interactivity_API_Directives_Processor $p               The directives processor instance.
 913       * @param string                                    $mode            Whether the processing is entering or exiting the tag.
 914       * @param array                                     $context_stack   The reference to the context stack.
 915       * @param array                                     $namespace_stack The reference to the store namespace stack.
 916       * @param array                                     $tag_stack       The reference to the tag stack.
 917       */
 918  	private function data_wp_each_processor( WP_Interactivity_API_Directives_Processor $p, string $mode, array &$context_stack, array &$namespace_stack, array &$tag_stack ) {
 919          if ( 'enter' === $mode && 'TEMPLATE' === $p->get_tag() ) {
 920              $attribute_name   = $p->get_attribute_names_with_prefix( 'data-wp-each' )[0];
 921              $extracted_suffix = $this->extract_prefix_and_suffix( $attribute_name );
 922              $item_name        = isset( $extracted_suffix[1] ) ? $this->kebab_to_camel_case( $extracted_suffix[1] ) : 'item';
 923              $attribute_value  = $p->get_attribute( $attribute_name );
 924              $result           = $this->evaluate( $attribute_value, end( $namespace_stack ), end( $context_stack ) );
 925  
 926              // Gets the content between the template tags and leaves the cursor in the closer tag.
 927              $inner_content = $p->get_content_between_balanced_template_tags();
 928  
 929              // Checks if there is a manual server-side directive processing.
 930              $template_end = 'data-wp-each: template end';
 931              $p->set_bookmark( $template_end );
 932              $p->next_tag();
 933              $manual_sdp = $p->get_attribute( 'data-wp-each-child' );
 934              $p->seek( $template_end ); // Rewinds to the template closer tag.
 935              $p->release_bookmark( $template_end );
 936  
 937              /*
 938               * It doesn't process in these situations:
 939               * - Manual server-side directive processing.
 940               * - Empty or non-array values.
 941               * - Associative arrays because those are deserialized as objects in JS.
 942               * - Templates that contain top-level texts because those texts can't be
 943               *   identified and removed in the client.
 944               */
 945              if (
 946                  $manual_sdp ||
 947                  empty( $result ) ||
 948                  ! is_array( $result ) ||
 949                  ! array_is_list( $result ) ||
 950                  ! str_starts_with( trim( $inner_content ), '<' ) ||
 951                  ! str_ends_with( trim( $inner_content ), '>' )
 952              ) {
 953                  array_pop( $tag_stack );
 954                  return;
 955              }
 956  
 957              // Extracts the namespace from the directive attribute value.
 958              $namespace_value         = end( $namespace_stack );
 959              list( $namespace_value ) = is_string( $attribute_value ) && ! empty( $attribute_value )
 960                  ? $this->extract_directive_value( $attribute_value, $namespace_value )
 961                  : array( $namespace_value, null );
 962  
 963              // Processes the inner content for each item of the array.
 964              $processed_content = '';
 965              foreach ( $result as $item ) {
 966                  // Creates a new context that includes the current item of the array.
 967                  $context_stack[] = array_replace_recursive(
 968                      end( $context_stack ) !== false ? end( $context_stack ) : array(),
 969                      array( $namespace_value => array( $item_name => $item ) )
 970                  );
 971  
 972                  // Processes the inner content with the new context.
 973                  $processed_item = $this->process_directives_args( $inner_content, $context_stack, $namespace_stack );
 974  
 975                  if ( null === $processed_item ) {
 976                      // If the HTML is unbalanced, stop processing it.
 977                      array_pop( $context_stack );
 978                      return;
 979                  }
 980  
 981                  // Adds the `data-wp-each-child` to each top-level tag.
 982                  $i = new WP_Interactivity_API_Directives_Processor( $processed_item );
 983                  while ( $i->next_tag() ) {
 984                      $i->set_attribute( 'data-wp-each-child', true );
 985                      $i->next_balanced_tag_closer_tag();
 986                  }
 987                  $processed_content .= $i->get_updated_html();
 988  
 989                  // Removes the current context from the stack.
 990                  array_pop( $context_stack );
 991              }
 992  
 993              // Appends the processed content after the tag closer of the template.
 994              $p->append_content_after_template_tag_closer( $processed_content );
 995  
 996              // Pops the last tag because it skipped the closing tag of the template tag.
 997              array_pop( $tag_stack );
 998          }
 999      }
1000  }


Generated : Thu May 9 08:20:02 2024 Cross-referenced by PHPXref