[ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated : Thu May 9 08:20:02 2024 | Cross-referenced by PHPXref |