From 7d86181965254132857264e5a112cb5ad5b85354 Mon Sep 17 00:00:00 2001 From: Dennis Snell Date: Wed, 30 Aug 2023 13:29:37 -0500 Subject: [PATCH] Block Supports: Re-use instance of Tag Processor when adding layout classes. In #45364 (WordPress/wordpress-develop#3976) the Block Supports was extended to add layout class names using the HTML API, new in WordPress 6.2. The initial patch opened up two opportunities to refine the code, however: - There are multiple instances of the `WP_HTML_Tag_Processor` created when a single one suffices. (There is an exception in that a second processor is necessary in order to find an inner block wrapper). - The code relies on the incidental fact that searching by a whitespace-separated list of class names works if the class names in the target tag are in the same order. In this patch the use of the HTML API is refactored to address these opportunities and clean up a few places where there could be stronger consistency with other use patterns of the HTML API: - Multiple instances of the Tag Processor have been combined to remove overhead, extra variables, and code confusion. The new flow is more linear throughout the function instead of branching. - Updated HTML is returned via `get_updated_html()` instead of casting to a string. - The matching logic to find the inner block wrapper has been commented and the condition uses the null-coalescing operator now that WordPress requires PHP 7.0+. - When attempting to find the inner block wrapper at the end, a custom comparison is made against the `class` attribute instead of relying on `next_tag()` to find a tag with the given set of class names. The last refactor is important as a preliminary step to WordPress/wordpress-develop#5096 where `has_class()` and `class_list()` methods are being introduced to the Tag Processor. In that patch the implicit functionality of matching `'class_name' => 'more than one class'` is removed since that's not a single class name, but many. --- lib/block-supports/layout.php | 131 ++++++++++++++++++++++------------ 1 file changed, 87 insertions(+), 44 deletions(-) diff --git a/lib/block-supports/layout.php b/lib/block-supports/layout.php index 6eecccfdc72c5..a5ddfff54daac 100644 --- a/lib/block-supports/layout.php +++ b/lib/block-supports/layout.php @@ -533,15 +533,13 @@ function gutenberg_render_layout_support_flag( $block_content, $block ) { $support_layout = block_has_support( $block_type, array( 'layout' ), false ) || block_has_support( $block_type, array( '__experimentalLayout' ), false ); $has_child_layout = isset( $block['attrs']['style']['layout']['selfStretch'] ); - if ( ! $support_layout - && ! $has_child_layout ) { + if ( ! $support_layout && ! $has_child_layout ) { return $block_content; } $outer_class_names = array(); if ( $has_child_layout && ( 'fixed' === $block['attrs']['style']['layout']['selfStretch'] || 'fill' === $block['attrs']['style']['layout']['selfStretch'] ) ) { - $container_content_class = wp_unique_id( 'wp-container-content-' ); $child_layout_styles = array(); @@ -572,15 +570,28 @@ function gutenberg_render_layout_support_flag( $block_content, $block ) { ); $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; } - // Return early if only child layout exists. + /* + * Return early if only child layout exists. + * + * In other words, if there is nothing more complicated, add + * the wrapper class names to the first HTML tag inside the + * block's rendered HTML. + */ if ( ! $support_layout && ! empty( $outer_class_names ) ) { - $content = new WP_HTML_Tag_Processor( $block_content ); - $content->next_tag(); - $content->add_class( implode( ' ', $outer_class_names ) ); - return (string) $content; + foreach ( $outer_class_names as $class_name ) { + $processor->add_class( $class_name ); + } + return $processor->get_updated_html(); } $global_settings = gutenberg_get_global_settings(); @@ -590,7 +601,6 @@ function gutenberg_render_layout_support_flag( $block_content, $block ) { $class_names = array(); $layout_definitions = gutenberg_get_layout_definitions(); $container_class = wp_unique_id( 'wp-container-' ); - $layout_classname = ''; // 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'] ) { @@ -599,11 +609,7 @@ function gutenberg_render_layout_support_flag( $block_content, $block ) { $root_padding_aware_alignments = _wp_array_get( $global_settings, array( 'useRootPaddingAwareAlignments' ), false ); - if ( - $root_padding_aware_alignments && - isset( $used_layout['type'] ) && - 'constrained' === $used_layout['type'] - ) { + if ( $root_padding_aware_alignments && isset( $used_layout['type'] ) && 'constrained' === $used_layout['type'] ) { $class_names[] = 'has-global-padding'; } @@ -690,49 +696,86 @@ function gutenberg_render_layout_support_flag( $block_content, $block ) { $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; - $content_with_outer_classnames = ''; - + // Add classes to the outermost HTML tag if necessary. if ( ! empty( $outer_class_names ) ) { - $content_with_outer_classnames = new WP_HTML_Tag_Processor( $block_content ); - $content_with_outer_classnames->next_tag(); foreach ( $outer_class_names as $outer_class_name ) { - $content_with_outer_classnames->add_class( $outer_class_name ); + $processor->add_class( $outer_class_name ); } - - $content_with_outer_classnames = (string) $content_with_outer_classnames; } /** - * The first chunk of innerContent contains the block markup up until the inner blocks start. - * We want to target the opening tag of the inner blocks wrapper, which is the last tag in that chunk. - */ - $inner_content_classnames = ''; - - if ( isset( $block['innerContent'][0] ) && 'string' === gettype( $block['innerContent'][0] ) && count( $block['innerContent'] ) > 1 ) { - $tags = new WP_HTML_Tag_Processor( $block['innerContent'][0] ); - $last_classnames = ''; - while ( $tags->next_tag() ) { - $last_classnames = $tags->get_attribute( 'class' ); + * 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. + * + * Example: + * + * $block['innerBlocks'] = array( $list_item ); + * $block['innerContent'] = array( '' ); + * + * // After rendering, the initial contents may have been modified by other renderers or filters. + * $block_content = << + *
It's a list!
+ * + * 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. 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? + * + * @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 ); + while ( $first_chunk_processor->next_tag() ) { + $class_attribute = $first_chunk_processor->get_attribute( 'class' ); + if ( is_string( $class_attribute ) && ! empty( $class_attribute ) ) { + $inner_block_wrapper_classes = $class_attribute; + } } - - $inner_content_classnames = (string) $last_classnames; } - $content = $content_with_outer_classnames ? new WP_HTML_Tag_Processor( $content_with_outer_classnames ) : new WP_HTML_Tag_Processor( $block_content ); - - if ( $inner_content_classnames ) { - $content->next_tag( array( 'class_name' => $inner_content_classnames ) ); - foreach ( $class_names as $class_name ) { - $content->add_class( $class_name ); + /* + * Find where to add the remaining class names. If there was a last tag identified before + * the inner blocks then they belong on that tag. Otherwise, they belong on the outermost tag. + */ + do { + if ( ! $inner_block_wrapper_classes ) { + break; } - } else { - $content->next_tag(); - foreach ( $class_names as $class_name ) { - $content->add_class( $class_name ); + + if ( false !== strpos( $processor->get_attribute( 'class' ), $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 (string) $content; + return $processor->get_updated_html(); } // Register the block support. (overrides core one).