wp_render_layout_support_flag()WP 5.8.0

Renders the layout config to the block wrapper.

Internal function — this function is designed to be used by the kernel itself. It is not recommended to use this function in your code.

No Hooks.

Returns

string. Filtered block content.

Usage

wp_render_layout_support_flag( $block_content, $block );
$block_content(string) (required)
Rendered block content.
$block(array) (required)
Block object.

Changelog

Since 5.8.0 Introduced.
Since 6.3.0 Adds compound class to layout wrapper for global spacing styles.
Since 6.3.0 Check for layout support via the layout key with fallback to __experimentalLayout.
Since 6.6.0 Removed duplicate container class from layout styles.

wp_render_layout_support_flag() code WP 7.1

function wp_render_layout_support_flag( $block_content, $block ) {
	static $global_styles = null;

	$block_type            = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] );
	$block_supports_layout = block_has_support( $block_type, 'layout', false ) || block_has_support( $block_type, '__experimentalLayout', false );
	$style_attr            = $block['attrs']['style'] ?? array();
	/*
	 * A block with no layout support and no style attribute at all cannot
	 * produce layout output, so return before resolving global settings.
	 *
	 * Resolving settings is not read-only: on a cold cache it queries the
	 * user's `wp_global_styles` post, which fires `the_posts`. A callback on
	 * that hook that renders blocks re-enters this filter, and the content it
	 * renders at that point is the global styles post itself, which parses to a
	 * single block with no name and no attributes. Without this return that
	 * block resolves settings again and the recursion has no base case.
	 */
	if ( ! $block_supports_layout && empty( $style_attr ) ) {
		return $block_content;
	}

	$global_settings          = wp_get_global_settings();
	$viewport_settings        = $global_settings['viewport'] ?? null;
	$responsive_media_queries = WP_Theme_JSON::get_viewport_media_queries( $viewport_settings );
	$child_layout             = $style_attr['layout'] ?? null;

	/*
	 * Collect responsive viewport child layout overrides so that a block with
	 * only responsive child layout (no base child layout) is still processed.
	 */
	$viewport_child_layouts = array();
	foreach ( $responsive_media_queries as $breakpoint => $media_query ) {
		$viewport_child = wp_get_layout_child_values( $style_attr[ $breakpoint ]['layout'] ?? null );

		if ( ! empty( $viewport_child ) ) {
			$viewport_child_layouts[ $breakpoint ] = array(
				'media_query'  => $media_query,
				'child_layout' => $viewport_child,
			);
		}
	}

	if ( ! $block_supports_layout && ! $child_layout && empty( $viewport_child_layouts ) ) {
		return $block_content;
	}

	$outer_class_names = array();

	// Child layout specific logic.
	if ( $child_layout || ! empty( $viewport_child_layouts ) ) {
		$base_child_layout = wp_get_layout_child_values( $child_layout );
		$parent_layout     = $block['parentLayout'] ?? array();
		/*
		 * Generates a unique class for child block layout styles.
		 *
		 * To ensure consistent class generation across different page renders,
		 * only properties that affect layout styling are used. These properties
		 * come from `$block['attrs']['style']['layout']`, viewport overrides in
		 * `$block['attrs']['style'][$breakpoint]['layout']`, and `$block['parentLayout']`.
		 *
		 * As long as these properties coincide, the generated class will be the same.
		 */
		$container_content_hash_input = array(
			'layout'       => $base_child_layout,
			'parentLayout' => array_intersect_key(
				$parent_layout,
				array_flip( array( 'minimumColumnWidth', 'columnCount' ) )
			),
		);

		foreach ( $viewport_child_layouts as $breakpoint => $viewport_data ) {
			$container_content_hash_input[ $breakpoint ] = $viewport_data['child_layout'];
		}

		$container_content_class = wp_unique_id_from_values(
			$container_content_hash_input,
			'wp-container-content-'
		);

		$child_layout_styles = wp_get_child_layout_style_rules( ".$container_content_class", $base_child_layout, $parent_layout );

		/*
		 * Emit responsive child layout CSS using the same container-content class
		 * so that base and responsive child layout share the exact same selector.
		 */
		foreach ( $viewport_child_layouts as $viewport_data ) {
			$viewport_child_styles = wp_get_child_layout_style_rules(
				".$container_content_class",
				$base_child_layout,
				$parent_layout,
				$viewport_data['child_layout']
			);

			foreach ( $viewport_child_styles as $index => $rule ) {
				$viewport_child_styles[ $index ]['rules_group'] = $viewport_data['media_query'];
			}

			$child_layout_styles = array_merge( $child_layout_styles, $viewport_child_styles );
		}

		/*
		 * Add to the style engine store to enqueue and render layout styles.
		 * Return styles here just to check if any exist.
		 */
		$child_css = wp_style_engine_get_stylesheet_from_css_rules(
			$child_layout_styles,
			array(
				'context'  => 'block-supports',
				'prettify' => false,
			)
		);

		if ( $child_css ) {
			$outer_class_names[] = $container_content_class;
		}
	}

	// Prep the processor for modifying the block output.
	$processor = new WP_HTML_Tag_Processor( $block_content );

	// Having no tags implies there are no tags onto which to add class names.
	if ( ! $processor->next_tag() ) {
		return $block_content;
	}

	/*
	 * A block may not support layout but still be affected by a parent block's layout.
	 *
	 * In these cases add the appropriate class names and then return early; there's
	 * no need to investigate on this block whether additional layout constraints apply.
	 */
	if ( ! $block_supports_layout && ! empty( $outer_class_names ) ) {
		foreach ( $outer_class_names as $class_name ) {
			$processor->add_class( $class_name );
		}
		return $processor->get_updated_html();
	} elseif ( ! $block_supports_layout ) {
		// Ensure layout classnames are not injected if there is no layout support.
		return $block_content;
	}

	$fallback_layout = $block_type->supports['layout']['default'] ?? array();
	if ( empty( $fallback_layout ) ) {
		$fallback_layout = $block_type->supports['__experimentalLayout']['default'] ?? array();
	}
	$used_layout = $block['attrs']['layout'] ?? $fallback_layout;

	$class_names        = array();
	$layout_definitions = wp_get_layout_definitions();

	// Set the correct layout type for blocks using legacy content width.
	if ( isset( $used_layout['inherit'] ) && $used_layout['inherit'] || isset( $used_layout['contentSize'] ) && $used_layout['contentSize'] ) {
		$used_layout['type'] = 'constrained';
	}

	$root_padding_aware_alignments = $global_settings['useRootPaddingAwareAlignments'] ?? false;

	if (
		$root_padding_aware_alignments &&
		isset( $used_layout['type'] ) &&
		'constrained' === $used_layout['type']
	) {
		$class_names[] = 'has-global-padding';
	}

	/*
	 * The following section was added to reintroduce a small set of layout classnames that were
	 * removed in the 5.9 release (https://github.com/WordPress/gutenberg/issues/38719). It is
	 * not intended to provide an extended set of classes to match all block layout attributes
	 * here.
	 */
	if ( ! empty( $block['attrs']['layout']['orientation'] ) ) {
		$class_names[] = 'is-' . sanitize_title( $block['attrs']['layout']['orientation'] );
	}

	if ( ! empty( $block['attrs']['layout']['justifyContent'] ) ) {
		$class_names[] = 'is-content-justification-' . sanitize_title( $block['attrs']['layout']['justifyContent'] );
	}

	if ( ! empty( $block['attrs']['layout']['flexWrap'] ) && 'nowrap' === $block['attrs']['layout']['flexWrap'] ) {
		$class_names[] = 'is-nowrap';
	}

	// Get classname for layout type.
	if ( isset( $used_layout['type'] ) ) {
		$layout_classname = $layout_definitions[ $used_layout['type'] ]['className'] ?? '';
	} else {
		$layout_classname = $layout_definitions['default']['className'] ?? '';
	}

	if ( $layout_classname && is_string( $layout_classname ) ) {
		$class_names[] = sanitize_title( $layout_classname );
	}

	/*
	 * Only generate Layout styles if the theme has not opted-out.
	 * Attribute-based Layout classnames are output in all cases.
	 */
	if ( ! current_theme_supports( 'disable-layout-styles' ) ) {

		$gap_value          = wp_sanitize_block_gap_value( $style_attr['spacing']['blockGap'] ?? null );
		$fallback_gap_value = $block_type->supports['spacing']['blockGap']['__experimentalDefault'] ?? '0.5em';
		$block_spacing      = $style_attr['spacing'] ?? null;

		/*
		 * If a block's block.json skips serialization for spacing or spacing.blockGap,
		 * don't apply the user-defined value to the styles.
		 */
		$should_skip_gap_serialization = wp_should_skip_block_supports_serialization( $block_type, 'spacing', 'blockGap' );

		$block_gap             = $global_settings['spacing']['blockGap'] ?? null;
		$has_block_gap_support = isset( $block_gap );

		// Get default blockGap value from global styles for use in layouts like grid.
		// Check style variation first, then block-specific styles, then fall back to root styles.
		$block_name = $block['blockName'] ?? '';
		if ( null === $global_styles ) {
			$global_styles = wp_get_global_styles();
		}

		// Check if the block has an active style variation with a blockGap value.
		// Only check the registry if the className contains a variation class to avoid unnecessary lookups.
		$variation_block_gap_value = null;
		$block_class_name          = is_string( $block['attrs']['className'] ?? null )
			? $block['attrs']['className']
			: '';
		if ( $block_class_name && str_contains( $block_class_name, 'is-style-' ) && $block_name ) {
			$styles_registry   = WP_Block_Styles_Registry::get_instance();
			$registered_styles = $styles_registry->get_registered_styles_for_block( $block_name );
			$variation_name    = wp_get_block_style_variation_name_from_registered_style( $block_class_name, $registered_styles );
			if ( $variation_name ) {
				$variation_block_gap_value = $global_styles['blocks'][ $block_name ]['variations'][ $variation_name ]['spacing']['blockGap'] ?? null;
			}
		}

		$global_block_gap_value = $variation_block_gap_value ?? $global_styles['blocks'][ $block_name ]['spacing']['blockGap'] ?? $global_styles['spacing']['blockGap'] ?? null;

		if ( null !== $global_block_gap_value ) {
			$fallback_gap_value = $global_block_gap_value;
		}

		$container_class_hash_input = array(
			$used_layout,
			$has_block_gap_support,
			$gap_value,
			$should_skip_gap_serialization,
			$fallback_gap_value,
			$block_spacing,
		);

		foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
			$viewport_style = $style_attr[ $breakpoint ] ?? null;
			if ( ! is_array( $viewport_style ) ) {
				continue;
			}

			$viewport_container_layout = wp_get_layout_container_values( $viewport_style['layout'] ?? null );
			if ( ! empty( $viewport_container_layout ) ) {
				$container_class_hash_input[] = array(
					'breakpoint' => $breakpoint,
					'layout'     => $viewport_container_layout,
				);
			}

			if ( isset( $viewport_style['spacing']['blockGap'] ) ) {
				$container_class_hash_input[] = array(
					'breakpoint' => $breakpoint,
					'blockGap'   => wp_sanitize_block_gap_value( $viewport_style['spacing']['blockGap'] ),
				);
			}
		}

		/*
		 * Generates a unique ID based on all the data required to obtain the
		 * corresponding layout style. Keeps the CSS class names the same
		 * even for different blocks on different places, as long as they have
		 * the same layout definition. Makes the CSS class names stable across
		 * paginations for features like the enhanced pagination of the Query block.
		 */
		$container_class = wp_unique_id_from_values(
			$container_class_hash_input,
			'wp-container-' . sanitize_title( $block['blockName'] ) . '-is-layout-'
		);

		$style = wp_get_layout_style(
			".$container_class",
			$used_layout,
			$has_block_gap_support,
			$gap_value,
			$should_skip_gap_serialization,
			$fallback_gap_value,
			$block_spacing
		);

		/*
		 * Emit responsive container layout styles using the same $container_class
		 * selector as the base layout so they target the inner block wrapper.
		 */
		foreach ( $responsive_media_queries as $breakpoint => $media_query ) {
			$viewport_style = $style_attr[ $breakpoint ] ?? null;
			if ( ! is_array( $viewport_style ) ) {
				continue;
			}

			$viewport_container_layout = wp_get_layout_container_values( $viewport_style['layout'] ?? null );
			$has_viewport_layout       = ! empty( $viewport_container_layout );
			$has_viewport_block_gap    = isset( $viewport_style['spacing']['blockGap'] );

			if ( ! $has_viewport_layout && ! $has_viewport_block_gap ) {
				continue;
			}

			$viewport_gap_value = $has_viewport_block_gap
				? wp_sanitize_block_gap_value( $viewport_style['spacing']['blockGap'] )
				: $gap_value;

			$viewport_block_spacing = is_array( $viewport_style['spacing'] ?? null )
				? array_replace( is_array( $block_spacing ) ? $block_spacing : array(), $viewport_style['spacing'] )
				: $block_spacing;

			$viewport_styles = wp_get_layout_style(
				".$container_class",
				$used_layout,
				$has_block_gap_support,
				$viewport_gap_value,
				$should_skip_gap_serialization,
				$fallback_gap_value,
				$viewport_block_spacing,
				array(
					'rules_group'            => $media_query,
					'viewport_overrides'     => $viewport_container_layout,
					'has_block_gap_override' => $has_viewport_block_gap,
				)
			);

			if ( ! empty( $viewport_styles ) && ! in_array( $container_class, $class_names, true ) ) {
				$class_names[] = $container_class;
			}
		}

		// Only add container class and enqueue block support styles if unique styles were generated.
		if ( ! empty( $style ) ) {
			$class_names[] = $container_class;
		}
	}

	// Add combined layout and block classname for global styles to hook onto.
	$split_block_name = explode( '/', $block['blockName'] );
	$full_block_name  = 'core' === $split_block_name[0] ? end( $split_block_name ) : implode( '-', $split_block_name );
	$class_names[]    = 'wp-block-' . $full_block_name . '-' . $layout_classname;

	// Add classes to the outermost HTML tag if necessary.
	if ( ! empty( $outer_class_names ) ) {
		foreach ( $outer_class_names as $outer_class_name ) {
			$processor->add_class( $outer_class_name );
		}
	}

	/**
	 * Attempts to refer to the inner-block wrapping element by its class attribute.
	 *
	 * When examining a block's inner content, if a block has inner blocks, then
	 * the first content item will likely be a text (HTML) chunk immediately
	 * preceding the inner blocks. The last HTML tag in that chunk would then be
	 * an opening tag for an element that wraps the inner blocks.
	 *
	 * There's no reliable way to associate this wrapper in $block_content because
	 * it may have changed during the rendering pipeline (as inner contents is
	 * provided before rendering) and through previous filters. In many cases,
	 * however, the `class` attribute will be a good-enough identifier, so this
	 * code finds the last tag in that chunk and stores the `class` attribute
	 * so that it can be used later when working through the rendered block output
	 * to identify the wrapping element and add the remaining class names to it.
	 *
	 * It's also possible that no inner block wrapper even exists. If that's the
	 * case this code could apply the class names to an invalid element.
	 *
	 * Example:
	 *
	 *     $block['innerBlocks']  = array( $list_item );
	 *     $block['innerContent'] = array( '<ul class="list-wrapper is-unordered">', null, '</ul>' );
	 *
	 *     // After rendering, the initial contents may have been modified by other renderers or filters.
	 *     $block_content = <<<HTML
	 *         <figure>
	 *             <ul class="annotated-list list-wrapper is-unordered">
	 *                 <li>Code</li>
	 *             </ul><figcaption>It's a list!</figcaption>
	 *         </figure>
	 *     HTML;
	 *
	 * Although it is possible that the original block-wrapper classes are changed in $block_content
	 * from how they appear in $block['innerContent'], it's likely that the original class attributes
	 * are still present in the wrapper as they are in this example. Frequently, additional classes
	 * will also be present; rarely should classes be removed.
	 *
	 * @todo Find a better way to match the first inner block. If it's possible to identify where the
	 *       first inner block starts, then it will be possible to find the last tag before it starts
	 *       and then that tag, if an opening tag, can be solidly identified as a wrapping element.
	 *       Can some unique value or class or ID be added to the inner blocks when they process
	 *       so that they can be extracted here safely without guessing? Can the block rendering function
	 *       return information about where the rendered inner blocks start?
	 *
	 * @var string|null
	 */
	$inner_block_wrapper_classes = null;
	$first_chunk                 = $block['innerContent'][0] ?? null;
	if ( is_string( $first_chunk ) && count( $block['innerContent'] ) > 1 ) {
		$first_chunk_processor = new WP_HTML_Tag_Processor( $first_chunk );
		/*
		 * Use a stack to track open elements as tags are visited. Void elements
		 * (those without a matching closing tag) are excluded so they don't
		 * accumulate on the stack. At the end of the chunk, every element still
		 * on the stack is unclosed — meaning its closing tag lives in a later
		 * innerContent entry alongside the inner blocks, which makes it the
		 * inner-block container. Elements that open and close within this chunk
		 * are siblings that precede the inner blocks and should be ignored.
		 * The last unclosed element with a class attribute is the best candidate
		 * for the inner-block wrapper.
		 */
		$tag_stack = array();
		while ( $first_chunk_processor->next_tag( array( 'tag_closers' => 'visit' ) ) ) {
			if ( $first_chunk_processor->is_tag_closer() ) {
				array_pop( $tag_stack );
			} elseif ( ! WP_HTML_Processor::is_void( $first_chunk_processor->get_tag() ) ) {
				$tag_stack[] = $first_chunk_processor->get_attribute( 'class' );
			}
		}
		foreach ( array_reverse( $tag_stack ) as $class_attribute ) {
			if ( is_string( $class_attribute ) && ! empty( $class_attribute ) ) {
				$inner_block_wrapper_classes = $class_attribute;
				break;
			}
		}
	}

	/*
	 * If necessary, advance to what is likely to be an inner block wrapper tag.
	 *
	 * This advances until it finds the first tag containing the original class
	 * attribute from above. If none is found it will scan to the end of the block
	 * and fail to add any class names.
	 *
	 * If there is no block wrapper it won't advance at all, in which case the
	 * class names will be added to the first and outermost tag of the block.
	 * For cases where this outermost tag is the only tag surrounding inner
	 * blocks then the outer wrapper and inner wrapper are the same.
	 */
	do {
		if ( ! $inner_block_wrapper_classes ) {
			break;
		}

		$class_attribute = $processor->get_attribute( 'class' );
		if ( is_string( $class_attribute ) && str_contains( $class_attribute, $inner_block_wrapper_classes ) ) {
			break;
		}
	} while ( $processor->next_tag() );

	// Add the remaining class names.
	foreach ( $class_names as $class_name ) {
		$processor->add_class( $class_name );
	}

	return $processor->get_updated_html();
}