WP_Interactivity_API::_process_directives()privateWP 6.6.0

Processes the interactivity directives contained within the HTML content and updates the markup accordingly.

It uses the WP_Interactivity_API instance's context and namespace stacks, which are shared between all calls.

This method returns null if the HTML contains unbalanced tags.

Method of the class: WP_Interactivity_API{}

No Hooks.

Returns

String|null. The processed HTML content. It returns null when the HTML contains unbalanced tags.

Usage

// private - for code of main (parent) class only
$result = $this->_process_directives( $html );
$html(string) (required)
The HTML content to process.

Changelog

Since 6.6.0 Introduced.

WP_Interactivity_API::_process_directives() code WP 6.8.1

private function _process_directives( string $html ) {
	$p          = new WP_Interactivity_API_Directives_Processor( $html );
	$tag_stack  = array();
	$unbalanced = false;

	$directive_processor_prefixes          = array_keys( self::$directive_processors );
	$directive_processor_prefixes_reversed = array_reverse( $directive_processor_prefixes );

	/*
	 * Save the current size for each stack to restore them in case
	 * the processing finds unbalanced tags.
	 */
	$namespace_stack_size = count( $this->namespace_stack );
	$context_stack_size   = count( $this->context_stack );

	while ( $p->next_tag( array( 'tag_closers' => 'visit' ) ) ) {
		$tag_name = $p->get_tag();

		/*
		 * Directives inside SVG and MATH tags are not processed,
		 * as they are not compatible with the Tag Processor yet.
		 * We still process the rest of the HTML.
		 */
		if ( 'SVG' === $tag_name || 'MATH' === $tag_name ) {
			if ( $p->get_attribute_names_with_prefix( 'data-wp-' ) ) {
				/* translators: 1: SVG or MATH HTML tag, 2: Namespace of the interactive block. */
				$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 ) );
				_doing_it_wrong( __METHOD__, $message, '6.6.0' );
			}
			$p->skip_to_tag_closer();
			continue;
		}

		if ( $p->is_tag_closer() ) {
			list( $opening_tag_name, $directives_prefixes ) = end( $tag_stack );

			if ( 0 === count( $tag_stack ) || $opening_tag_name !== $tag_name ) {

				/*
				 * If the tag stack is empty or the matching opening tag is not the
				 * same than the closing tag, it means the HTML is unbalanced and it
				 * stops processing it.
				 */
				$unbalanced = true;
				break;
			} else {
				// Remove the last tag from the stack.
				array_pop( $tag_stack );
			}
		} else {
			if ( 0 !== count( $p->get_attribute_names_with_prefix( 'data-wp-each-child' ) ) ) {
				/*
				 * If the tag has a `data-wp-each-child` directive, jump to its closer
				 * tag because those tags have already been processed.
				 */
				$p->next_balanced_tag_closer_tag();
				continue;
			} else {
				$directives_prefixes = array();

				// Checks if there is a server directive processor registered for each directive.
				foreach ( $p->get_attribute_names_with_prefix( 'data-wp-' ) as $attribute_name ) {
					if ( ! preg_match(
						/*
						 * This must align with the client-side regex used by the interactivity API.
						 * @see https://github.com/WordPress/gutenberg/blob/ca616014255efbb61f34c10917d52a2d86c1c660/packages/interactivity/src/vdom.ts#L20-L32
						 */
						'/' .
						'^data-wp-' .
						// Match alphanumeric characters including hyphen-separated
						// segments. It excludes underscore intentionally to prevent confusion.
						// E.g., "custom-directive".
						'([a-z0-9]+(?:-[a-z0-9]+)*)' .
						// (Optional) Match '--' followed by any alphanumeric charachters. It
						// excludes underscore intentionally to prevent confusion, but it can
						// contain multiple hyphens. E.g., "--custom-prefix--with-more-info".
						'(?:--([a-z0-9_-]+))?$' .
						'/i',
						$attribute_name
					) ) {
						continue;
					}
					list( $directive_prefix ) = $this->extract_prefix_and_suffix( $attribute_name );
					if ( array_key_exists( $directive_prefix, self::$directive_processors ) ) {
						$directives_prefixes[] = $directive_prefix;
					}
				}

				/*
				 * If this tag will visit its closer tag, it adds it to the tag stack
				 * so it can process its closing tag and check for unbalanced tags.
				 */
				if ( $p->has_and_visits_its_closer_tag() ) {
					$tag_stack[] = array( $tag_name, $directives_prefixes );
				}
			}
		}
		/*
		 * If the matching opener tag didn't have any directives, it can skip the
		 * processing.
		 */
		if ( 0 === count( $directives_prefixes ) ) {
			continue;
		}

		// Directive processing might be different depending on if it is entering the tag or exiting it.
		$modes = array(
			'enter' => ! $p->is_tag_closer(),
			'exit'  => $p->is_tag_closer() || ! $p->has_and_visits_its_closer_tag(),
		);

		// Get the element attributes to include them in the element representation.
		$element_attrs = array();
		$attr_names    = $p->get_attribute_names_with_prefix( '' ) ?? array();

		foreach ( $attr_names as $name ) {
			$element_attrs[ $name ] = $p->get_attribute( $name );
		}

		// Assign the current element right before running its directive processors.
		$this->current_element = array(
			'attributes' => $element_attrs,
		);

		foreach ( $modes as $mode => $should_run ) {
			if ( ! $should_run ) {
				continue;
			}

			/*
			 * Sorts the attributes by the order of the `directives_processor` array
			 * and checks what directives are present in this element.
			 */
			$existing_directives_prefixes = array_intersect(
				'enter' === $mode ? $directive_processor_prefixes : $directive_processor_prefixes_reversed,
				$directives_prefixes
			);
			foreach ( $existing_directives_prefixes as $directive_prefix ) {
				$func = is_array( self::$directive_processors[ $directive_prefix ] )
					? self::$directive_processors[ $directive_prefix ]
					: array( $this, self::$directive_processors[ $directive_prefix ] );

				call_user_func_array( $func, array( $p, $mode, &$tag_stack ) );
			}
		}

		// Clear the current element.
		$this->current_element = null;
	}

	if ( $unbalanced ) {
		// Reset the namespace and context stacks to their previous values.
		array_splice( $this->namespace_stack, $namespace_stack_size );
		array_splice( $this->context_stack, $context_stack_size );
	}

	/*
	 * It returns null if the HTML is unbalanced because unbalanced HTML is
	 * not safe to process. In that case, the Interactivity API runtime will
	 * update the HTML on the client side during the hydration. It will also
	 * display a notice to the developer to inform them about the issue.
	 */
	if ( $unbalanced || 0 < count( $tag_stack ) ) {
		$tag_errored = 0 < count( $tag_stack ) ? end( $tag_stack )[0] : $tag_name;
		/* translators: %1s: Namespace processed, %2s: The tag that caused the error; could be any HTML tag.  */
		$message = sprintf( __( 'Interactivity directives failed to process in "%1$s" due to a missing "%2$s" end tag.' ), end( $this->namespace_stack ), $tag_errored );
		_doing_it_wrong( __METHOD__, $message, '6.6.0' );
		return null;
	}

	return $p->get_updated_html();
}