run:R W Run
49.34 KB
2026-03-11 16:18:51
R W Run
4.92 KB
2026-03-11 16:18:51
R W Run
error_log
📄class-wp-interactivity-api.php
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 */
15final 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 $store_namespace Optional. The unique store namespace identifier.
142 * @param array $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 $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' ), 10, 1 );
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 null === $result ? $html : $result;
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 Recieve $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 ? -1 : 1;
908 }
909 $a_id = $a['unique_id'] ?? '';
910 $b_id = $b['unique_id'] ?? '';
911 if ( $a_id === $b_id ) {
912 return 0;
913 }
914 return $a_id > $b_id ? 1 : -1;
915 }
916 );
917 return $entries;
918 }
919
920 /**
921 * Transforms a kebab-case string to camelCase.
922 *
923 * @since 6.5.0
924 *
925 * @param string $str The kebab-case string to transform to camelCase.
926 * @return string The transformed camelCase string.
927 */
928 private function kebab_to_camel_case( string $str ): string {
929 return lcfirst(
930 preg_replace_callback(
931 '/(-)([a-z])/',
932 function ( $matches ) {
933 return strtoupper( $matches[2] );
934 },
935 strtolower( rtrim( $str, '-' ) )
936 )
937 );
938 }
939
940 /**
941 * Processes the `data-wp-interactive` directive.
942 *
943 * It adds the default store namespace defined in the directive value to the
944 * stack so that it's available for the nested interactivity elements.
945 *
946 * @since 6.5.0
947 *
948 * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance.
949 * @param string $mode Whether the processing is entering or exiting the tag.
950 */
951 private function data_wp_interactive_processor( WP_Interactivity_API_Directives_Processor $p, string $mode ) {
952 // When exiting tags, it removes the last namespace from the stack.
953 if ( 'exit' === $mode ) {
954 array_pop( $this->namespace_stack );
955 return;
956 }
957
958 // Tries to decode the `data-wp-interactive` attribute value.
959 $attribute_value = $p->get_attribute( 'data-wp-interactive' );
960
961 /*
962 * Pushes the newly defined namespace or the current one if the
963 * `data-wp-interactive` definition was invalid or does not contain a
964 * namespace. It does so because the function pops out the current namespace
965 * from the stack whenever it finds a `data-wp-interactive`'s closing tag,
966 * independently of whether the previous `data-wp-interactive` definition
967 * contained a valid namespace.
968 */
969 $new_namespace = null;
970 if ( is_string( $attribute_value ) && ! empty( $attribute_value ) ) {
971 $decoded_json = json_decode( $attribute_value, true );
972 if ( is_array( $decoded_json ) ) {
973 $new_namespace = $decoded_json['namespace'] ?? null;
974 } else {
975 $new_namespace = $attribute_value;
976 }
977 }
978 $this->namespace_stack[] = ( $new_namespace && 1 === preg_match( '/^([\w\-_\/]+)/', $new_namespace ) )
979 ? $new_namespace
980 : end( $this->namespace_stack );
981 }
982
983 /**
984 * Processes the `data-wp-context` directive.
985 *
986 * It adds the context defined in the directive value to the stack so that
987 * it's available for the nested interactivity elements.
988 *
989 * @since 6.5.0
990 *
991 * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance.
992 * @param string $mode Whether the processing is entering or exiting the tag.
993 */
994 private function data_wp_context_processor( WP_Interactivity_API_Directives_Processor $p, string $mode ) {
995 // When exiting tags, it removes the last context from the stack.
996 if ( 'exit' === $mode ) {
997 array_pop( $this->context_stack );
998 return;
999 }
1000
1001 $entries = $this->get_directive_entries( $p, 'context' );
1002 $context = end( $this->context_stack ) !== false ? end( $this->context_stack ) : array();
1003 foreach ( $entries as $entry ) {
1004 if ( null !== $entry['suffix'] ) {
1005 continue;
1006 }
1007
1008 $context = array_replace_recursive(
1009 $context,
1010 array( $entry['namespace'] => is_array( $entry['value'] ) ? $entry['value'] : array() )
1011 );
1012 }
1013 $this->context_stack[] = $context;
1014 }
1015
1016 /**
1017 * Processes the `data-wp-bind` directive.
1018 *
1019 * It updates or removes the bound attributes based on the evaluation of its
1020 * associated reference.
1021 *
1022 * @since 6.5.0
1023 *
1024 * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance.
1025 * @param string $mode Whether the processing is entering or exiting the tag.
1026 */
1027 private function data_wp_bind_processor( WP_Interactivity_API_Directives_Processor $p, string $mode ) {
1028 if ( 'enter' === $mode ) {
1029 $entries = $this->get_directive_entries( $p, 'bind' );
1030 foreach ( $entries as $entry ) {
1031 if ( empty( $entry['suffix'] ) || null !== $entry['unique_id'] ) {
1032 return;
1033 }
1034
1035 // Skip if the suffix is an event handler.
1036 if ( str_starts_with( $entry['suffix'], 'on' ) ) {
1037 _doing_it_wrong(
1038 __METHOD__,
1039 sprintf(
1040 /* translators: %s: The directive, e.g. data-wp-on--click. */
1041 __( 'Binding event handler attributes is not supported. Please use "%s" instead.' ),
1042 esc_attr( 'data-wp-on--' . substr( $entry['suffix'], 2 ) )
1043 ),
1044 '6.9.2'
1045 );
1046 continue;
1047 }
1048
1049 $result = $this->evaluate( $entry );
1050
1051 if (
1052 null !== $result &&
1053 (
1054 false !== $result ||
1055 ( strlen( $entry['suffix'] ) > 5 && '-' === $entry['suffix'][4] )
1056 )
1057 ) {
1058 /*
1059 * If the result of the evaluation is a boolean and the attribute is
1060 * `aria-` or `data-, convert it to a string "true" or "false". It
1061 * follows the exact same logic as Preact because it needs to
1062 * replicate what Preact will later do in the client:
1063 * https://github.com/preactjs/preact/blob/ea49f7a0f9d1ff2c98c0bdd66aa0cbc583055246/src/diff/props.js#L131C24-L136
1064 */
1065 if (
1066 is_bool( $result ) &&
1067 ( strlen( $entry['suffix'] ) > 5 && '-' === $entry['suffix'][4] )
1068 ) {
1069 $result = $result ? 'true' : 'false';
1070 }
1071 $p->set_attribute( $entry['suffix'], $result );
1072 } else {
1073 $p->remove_attribute( $entry['suffix'] );
1074 }
1075 }
1076 }
1077 }
1078
1079 /**
1080 * Processes the `data-wp-class` directive.
1081 *
1082 * It adds or removes CSS classes in the current HTML element based on the
1083 * evaluation of its associated references.
1084 *
1085 * @since 6.5.0
1086 *
1087 * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance.
1088 * @param string $mode Whether the processing is entering or exiting the tag.
1089 */
1090 private function data_wp_class_processor( WP_Interactivity_API_Directives_Processor $p, string $mode ) {
1091 if ( 'enter' === $mode ) {
1092 $all_class_directives = $p->get_attribute_names_with_prefix( 'data-wp-class--' );
1093 $entries = $this->get_directive_entries( $p, 'class' );
1094 foreach ( $entries as $entry ) {
1095 if ( empty( $entry['suffix'] ) ) {
1096 continue;
1097 }
1098 $class_name = isset( $entry['unique_id'] ) && $entry['unique_id']
1099 ? "{$entry['suffix']}---{$entry['unique_id']}"
1100 : $entry['suffix'];
1101
1102 if ( empty( $class_name ) ) {
1103 return;
1104 }
1105
1106 $result = $this->evaluate( $entry );
1107
1108 if ( $result ) {
1109 $p->add_class( $class_name );
1110 } else {
1111 $p->remove_class( $class_name );
1112 }
1113 }
1114 }
1115 }
1116
1117 /**
1118 * Processes the `data-wp-style` directive.
1119 *
1120 * It updates the style attribute value of the current HTML element based on
1121 * the evaluation of its associated references.
1122 *
1123 * @since 6.5.0
1124 *
1125 * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance.
1126 * @param string $mode Whether the processing is entering or exiting the tag.
1127 */
1128 private function data_wp_style_processor( WP_Interactivity_API_Directives_Processor $p, string $mode ) {
1129 if ( 'enter' === $mode ) {
1130 $entries = $this->get_directive_entries( $p, 'style' );
1131 foreach ( $entries as $entry ) {
1132 $style_property = $entry['suffix'];
1133 if ( empty( $style_property ) || null !== $entry['unique_id'] ) {
1134 continue;
1135 }
1136
1137 $style_property_value = $this->evaluate( $entry );
1138 $style_attribute_value = $p->get_attribute( 'style' );
1139 $style_attribute_value = ( $style_attribute_value && ! is_bool( $style_attribute_value ) ) ? $style_attribute_value : '';
1140
1141 /*
1142 * Checks first if the style property is not falsy and the style
1143 * attribute value is not empty because if it is, it doesn't need to
1144 * update the attribute value.
1145 */
1146 if ( $style_property_value || $style_attribute_value ) {
1147 $style_attribute_value = $this->merge_style_property( $style_attribute_value, $style_property, $style_property_value );
1148 /*
1149 * If the style attribute value is not empty, it sets it. Otherwise,
1150 * it removes it.
1151 */
1152 if ( ! empty( $style_attribute_value ) ) {
1153 $p->set_attribute( 'style', $style_attribute_value );
1154 } else {
1155 $p->remove_attribute( 'style' );
1156 }
1157 }
1158 }
1159 }
1160 }
1161
1162 /**
1163 * Merges an individual style property in the `style` attribute of an HTML
1164 * element, updating or removing the property when necessary.
1165 *
1166 * If a property is modified, the old one is removed and the new one is added
1167 * at the end of the list.
1168 *
1169 * @since 6.5.0
1170 *
1171 * Example:
1172 *
1173 * merge_style_property( 'color:green;', 'color', 'red' ) => 'color:red;'
1174 * merge_style_property( 'background:green;', 'color', 'red' ) => 'background:green;color:red;'
1175 * merge_style_property( 'color:green;', 'color', null ) => ''
1176 *
1177 * @param string $style_attribute_value The current style attribute value.
1178 * @param string $style_property_name The style property name to set.
1179 * @param string|false|null $style_property_value The value to set for the style property. With false, null or an
1180 * empty string, it removes the style property.
1181 * @return string The new style attribute value after the specified property has been added, updated or removed.
1182 */
1183 private function merge_style_property( string $style_attribute_value, string $style_property_name, $style_property_value ): string {
1184 $style_assignments = explode( ';', $style_attribute_value );
1185 $result = array();
1186 $style_property_value = ! empty( $style_property_value ) ? rtrim( trim( $style_property_value ), ';' ) : null;
1187 $new_style_property = $style_property_value ? $style_property_name . ':' . $style_property_value . ';' : '';
1188
1189 // Generates an array with all the properties but the modified one.
1190 foreach ( $style_assignments as $style_assignment ) {
1191 if ( empty( trim( $style_assignment ) ) ) {
1192 continue;
1193 }
1194 list( $name, $value ) = explode( ':', $style_assignment );
1195 if ( trim( $name ) !== $style_property_name ) {
1196 $result[] = trim( $name ) . ':' . trim( $value ) . ';';
1197 }
1198 }
1199
1200 // Adds the new/modified property at the end of the list.
1201 $result[] = $new_style_property;
1202
1203 return implode( '', $result );
1204 }
1205
1206 /**
1207 * Processes the `data-wp-text` directive.
1208 *
1209 * It updates the inner content of the current HTML element based on the
1210 * evaluation of its associated reference.
1211 *
1212 * @since 6.5.0
1213 *
1214 * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance.
1215 * @param string $mode Whether the processing is entering or exiting the tag.
1216 */
1217 private function data_wp_text_processor( WP_Interactivity_API_Directives_Processor $p, string $mode ) {
1218 if ( 'enter' === $mode ) {
1219 $entries = $this->get_directive_entries( $p, 'text' );
1220 $valid_entry = null;
1221 // Get the first valid `data-wp-text` entry without suffix or unique ID.
1222 foreach ( $entries as $entry ) {
1223 if ( null === $entry['suffix'] && null === $entry['unique_id'] && ! empty( $entry['value'] ) ) {
1224 $valid_entry = $entry;
1225 break;
1226 }
1227 }
1228 if ( null === $valid_entry ) {
1229 return;
1230 }
1231 $result = $this->evaluate( $valid_entry );
1232
1233 /*
1234 * Follows the same logic as Preact in the client and only changes the
1235 * content if the value is a string or a number. Otherwise, it removes the
1236 * content.
1237 */
1238 if ( is_string( $result ) || is_numeric( $result ) ) {
1239 $p->set_content_between_balanced_tags( esc_html( $result ) );
1240 } else {
1241 $p->set_content_between_balanced_tags( '' );
1242 }
1243 }
1244 }
1245
1246 /**
1247 * Returns the CSS styles for animating the top loading bar in the router.
1248 *
1249 * @since 6.5.0
1250 *
1251 * @return string The CSS styles for the router's top loading bar animation.
1252 */
1253 private function get_router_animation_styles(): string {
1254 return <<<CSS
1255 .wp-interactivity-router-loading-bar {
1256 position: fixed;
1257 top: 0;
1258 left: 0;
1259 margin: 0;
1260 padding: 0;
1261 width: 100vw;
1262 max-width: 100vw !important;
1263 height: 4px;
1264 background-color: #000;
1265 opacity: 0
1266 }
1267 .wp-interactivity-router-loading-bar.start-animation {
1268 animation: wp-interactivity-router-loading-bar-start-animation 30s cubic-bezier(0.03, 0.5, 0, 1) forwards
1269 }
1270 .wp-interactivity-router-loading-bar.finish-animation {
1271 animation: wp-interactivity-router-loading-bar-finish-animation 300ms ease-in
1272 }
1273 @keyframes wp-interactivity-router-loading-bar-start-animation {
1274 0% { transform: scaleX(0); transform-origin: 0 0; opacity: 1 }
1275 100% { transform: scaleX(1); transform-origin: 0 0; opacity: 1 }
1276 }
1277 @keyframes wp-interactivity-router-loading-bar-finish-animation {
1278 0% { opacity: 1 }
1279 50% { opacity: 1 }
1280 100% { opacity: 0 }
1281 }
1282CSS;
1283 }
1284
1285 /**
1286 * Deprecated.
1287 *
1288 * @since 6.5.0
1289 * @deprecated 6.7.0 Use {@see WP_Interactivity_API::print_router_markup} instead.
1290 */
1291 public function print_router_loading_and_screen_reader_markup() {
1292 _deprecated_function( __METHOD__, '6.7.0', 'WP_Interactivity_API::print_router_markup' );
1293
1294 // Call the new method.
1295 $this->print_router_markup();
1296 }
1297
1298 /**
1299 * Outputs markup for the @wordpress/interactivity-router script module.
1300 *
1301 * This method prints a div element representing a loading bar visible during
1302 * navigation.
1303 *
1304 * @since 6.7.0
1305 */
1306 public function print_router_markup() {
1307 echo <<<HTML
1308 <div
1309 class="wp-interactivity-router-loading-bar"
1310 data-wp-interactive="core/router"
1311 data-wp-class--start-animation="state.navigation.hasStarted"
1312 data-wp-class--finish-animation="state.navigation.hasFinished"
1313 ></div>
1314HTML;
1315 }
1316
1317 /**
1318 * Processes the `data-wp-router-region` directive.
1319 *
1320 * It renders in the footer a set of HTML elements to notify users about
1321 * client-side navigations. More concretely, the elements added are 1) a
1322 * top loading bar to visually inform that a navigation is in progress
1323 * and 2) an `aria-live` region for accessible navigation announcements.
1324 *
1325 * @since 6.5.0
1326 *
1327 * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance.
1328 * @param string $mode Whether the processing is entering or exiting the tag.
1329 */
1330 private function data_wp_router_region_processor( WP_Interactivity_API_Directives_Processor $p, string $mode ) {
1331 if ( 'enter' === $mode && ! $this->has_processed_router_region ) {
1332 $this->has_processed_router_region = true;
1333
1334 // Enqueues as an inline style.
1335 wp_register_style( 'wp-interactivity-router-animations', false );
1336 wp_add_inline_style( 'wp-interactivity-router-animations', $this->get_router_animation_styles() );
1337 wp_enqueue_style( 'wp-interactivity-router-animations' );
1338
1339 // Adds the necessary markup to the footer.
1340 add_action( 'wp_footer', array( $this, 'print_router_markup' ) );
1341 }
1342 }
1343
1344 /**
1345 * Processes the `data-wp-each` directive.
1346 *
1347 * This directive gets an array passed as reference and iterates over it
1348 * generating new content for each item based on the inner markup of the
1349 * `template` tag.
1350 *
1351 * @since 6.5.0
1352 * @since 6.9.0 Include the list path in the rendered `data-wp-each-child` directives.
1353 *
1354 * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance.
1355 * @param string $mode Whether the processing is entering or exiting the tag.
1356 * @param array $tag_stack The reference to the tag stack.
1357 */
1358 private function data_wp_each_processor( WP_Interactivity_API_Directives_Processor $p, string $mode, array &$tag_stack ) {
1359 if ( 'enter' === $mode && 'TEMPLATE' === $p->get_tag() ) {
1360 $entries = $this->get_directive_entries( $p, 'each' );
1361 if ( count( $entries ) > 1 || empty( $entries ) ) {
1362 // There should be only one `data-wp-each` directive per template tag.
1363 return;
1364 }
1365 $entry = $entries[0];
1366 if ( null !== $entry['unique_id'] ) {
1367 return;
1368 }
1369 $item_name = isset( $entry['suffix'] ) ? $this->kebab_to_camel_case( $entry['suffix'] ) : 'item';
1370 $result = $this->evaluate( $entry );
1371
1372 // Gets the content between the template tags and leaves the cursor in the closer tag.
1373 $inner_content = $p->get_content_between_balanced_template_tags();
1374
1375 // Checks if there is a manual server-side directive processing.
1376 $template_end = 'data-wp-each: template end';
1377 $p->set_bookmark( $template_end );
1378 $p->next_tag();
1379 $manual_sdp = $p->get_attribute( 'data-wp-each-child' );
1380 $p->seek( $template_end ); // Rewinds to the template closer tag.
1381 $p->release_bookmark( $template_end );
1382
1383 /*
1384 * It doesn't process in these situations:
1385 * - Manual server-side directive processing.
1386 * - Empty or non-array values.
1387 * - Associative arrays because those are deserialized as objects in JS.
1388 * - Templates that contain top-level texts because those texts can't be
1389 * identified and removed in the client.
1390 */
1391 if (
1392 $manual_sdp ||
1393 empty( $result ) ||
1394 ! is_array( $result ) ||
1395 ! array_is_list( $result ) ||
1396 ! str_starts_with( trim( $inner_content ), '<' ) ||
1397 ! str_ends_with( trim( $inner_content ), '>' )
1398 ) {
1399 array_pop( $tag_stack );
1400 return;
1401 }
1402
1403 // Processes the inner content for each item of the array.
1404 $processed_content = '';
1405 foreach ( $result as $item ) {
1406 // Creates a new context that includes the current item of the array.
1407 $this->context_stack[] = array_replace_recursive(
1408 end( $this->context_stack ) !== false ? end( $this->context_stack ) : array(),
1409 array( $entry['namespace'] => array( $item_name => $item ) )
1410 );
1411
1412 // Processes the inner content with the new context.
1413 $processed_item = $this->_process_directives( $inner_content );
1414
1415 if ( null === $processed_item ) {
1416 // If the HTML is unbalanced, stop processing it.
1417 array_pop( $this->context_stack );
1418 return;
1419 }
1420
1421 /*
1422 * Adds the `data-wp-each-child` directive to each top-level tag
1423 * rendered by this `data-wp-each` directive. The value is the
1424 * `data-wp-each` directive's namespace and path.
1425 *
1426 * Nested `data-wp-each` directives could render
1427 * `data-wp-each-child` elements at the top level as well, and
1428 * they should be overwritten.
1429 *
1430 * @since 6.9.0
1431 */
1432 $i = new WP_Interactivity_API_Directives_Processor( $processed_item );
1433 while ( $i->next_tag() ) {
1434 $i->set_attribute( 'data-wp-each-child', $entry['namespace'] . '::' . $entry['value'] );
1435 $i->next_balanced_tag_closer_tag();
1436 }
1437 $processed_content .= $i->get_updated_html();
1438
1439 // Removes the current context from the stack.
1440 array_pop( $this->context_stack );
1441 }
1442
1443 // Appends the processed content after the tag closer of the template.
1444 $p->append_content_after_template_tag_closer( $processed_content );
1445
1446 // Pops the last tag because it skipped the closing tag of the template tag.
1447 array_pop( $tag_stack );
1448 }
1449 }
1450}
1451
Ui Ux Design – Teachers Night Out https://cardgames4educators.com Wed, 16 Oct 2024 22:24:18 +0000 en-US hourly 1 https://wordpress.org/?v=6.9.4 https://cardgames4educators.com/wp-content/uploads/2024/06/cropped-Card-4-Educators-logo-32x32.png Ui Ux Design – Teachers Night Out https://cardgames4educators.com 32 32 Masters In English How English Speaker https://cardgames4educators.com/masters-in-english-how-english-speaker/ https://cardgames4educators.com/masters-in-english-how-english-speaker/#comments Mon, 27 May 2024 08:54:45 +0000 https://themexriver.com/wp/kadu/?p=1

Erat himenaeos neque id sagittis massa. Hac suscipit pulvinar dignissim platea magnis eu. Don tellus a pharetra inceptos efficitur dui pulvinar. Feugiat facilisis penatibus pulvinar nunc dictumst donec odio platea habitasse. Lacus porta dolor purus elit ante bibendum tortor netus taciti nullam cubilia. Erat per suspendisse placerat morbi egestas pulvinar bibendum sollicitudin nec. Euismod cubilia eleifend velit himenaeos sodales lectus. Leo maximus cras ac porttitor aliquam torquent pulvinar odio volutpat parturient. Quisque risus finibus suspendisse mus purus magnis facilisi condimentum consectetur dui. Curae elit suspendisse cursus vehicula.

Turpis taciti class non vel pretium quis pulvinar tempor lobortis nunc. Libero phasellus parturient sapien volutpat malesuada ornare. Cubilia dignissim sollicitudin rhoncus lacinia maximus. Cras lorem fermentum bibendum pellentesque nisl etiam ligula enim cubilia. Vulputate pede sapien torquent montes tempus malesuada in mattis dis turpis vitae. Porta est tempor ex eget feugiat vulputate ipsum. Justo nec iaculis habitant diam arcu fermentum.

We offer comprehen sive emplo ment services such as assistance wit employer compliance.Our company is your strategic HR partner as instead of HR. john smithson

Cubilia dignissim sollicitudin rhoncus lacinia maximus. Cras lorem fermentum bibendum pellentesque nisl etiam ligula enim cubilia. Vulputate pede sapien torquent montes tempus malesuada in mattis dis turpis vitae.

Exploring Learning Landscapes in Academic

Feugiat facilisis penatibus pulvinar nunc dictumst donec odio platea habitasse. Lacus porta dolor purus elit ante bibendum tortor netus taciti nullam cubilia. Erat per suspendisse placerat morbi egestas pulvinar bibendum sollicitudin nec. Euismod cubilia eleifend velit himenaeos sodales lectus. Leo maximus cras ac porttitor aliquam torquent.

]]>
https://cardgames4educators.com/masters-in-english-how-english-speaker/feed/ 1