From 1943ac6c368b1cc170ad70e72e5331675ca0030c 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 | 150 +++++++++++++++++++++++----------- 1 file changed, 102 insertions(+), 48 deletions(-) diff --git a/lib/block-supports/layout.php b/lib/block-supports/layout.php index 6eecccfdc72c5..a948a5ba9358e 100644 --- a/lib/block-supports/layout.php +++ b/lib/block-supports/layout.php @@ -529,19 +529,17 @@ function gutenberg_get_layout_style( $selector, $layout, $has_block_gap_support * @return string Filtered block content. */ function gutenberg_render_layout_support_flag( $block_content, $block ) { - $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] ); - $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'] ); + $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] ); + $block_supports_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 ( ! $block_supports_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. - 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; + /* + * A block may not support layout, but may still have attributes which + * specify that certain class names be applied to its outermost tag. + * + * In these cases add those class names and then return early; there's + * nothing further within the block to process. + */ + 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(); } $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,97 @@ 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. 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 ); + 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 ); + /* + * 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; } - } 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).