From e47a3d07ec1c139d1f698b53aab3eae67053bf02 Mon Sep 17 00:00:00 2001 From: Dennis Snell Date: Sat, 7 Oct 2023 16:56:40 -0700 Subject: [PATCH] Lightbox: Re-use existing Tag Processor instance. Resolves WordPress/Gutenberg#55123 Replaces existing single-letter-named Tag Processor instances with a single processor that's reuse throughout the function. As a work-in-progress (WIP) patch, this is a preliminary exploration and not ready for review or merging. --- src/wp-includes/blocks/image.php | 151 +++++++++++++++++++------------ 1 file changed, 94 insertions(+), 57 deletions(-) diff --git a/src/wp-includes/blocks/image.php b/src/wp-includes/blocks/image.php index e1f71964622c0..dd4617ad9731f 100644 --- a/src/wp-includes/blocks/image.php +++ b/src/wp-includes/blocks/image.php @@ -124,21 +124,31 @@ function block_core_image_get_lightbox_settings( $block ) { */ function block_core_image_render_lightbox( $block_content, $block ) { $processor = new WP_HTML_Tag_Processor( $block_content ); + if ( ! $processor->next_tag() ) { + return $block_content; + } + + /* + * This code jumps around a fair amount, so bookmarking the first + * tag provides the ability to "reset" to and start scanning + * again without recreating a new Tag Processor instance. + */ + $processor->set_bookmark( 'first tag' ); - $aria_label = __( 'Enlarge image' ); + // The first tag that was already found could be an image, but it may not be. + if ( 'IMG' !== $processor->get_tag() && ! $processor->next_tag( 'IMG' ) ) { + return $block_content; + } $processor->next_tag( 'img' ); $alt_attribute = $processor->get_attribute( 'alt' ); + $alt_attribute = is_string( $alt_attribute ) ? trim( $alt_attribute ) : null; - // An empty alt attribute `alt=""` is valid for decorative images. - if ( is_string( $alt_attribute ) ) { - $alt_attribute = trim( $alt_attribute ); - } - - // It only makes sense to append the alt text to the button aria-label when the alt text is non-empty. - if ( $alt_attribute ) { + if ( ! empty( $alt_attribute ) ) { /* translators: %s: Image alt text. */ $aria_label = sprintf( __( 'Enlarge image: %s' ), $alt_attribute ); + } else { + $aria_label = __( 'Enlarge image' ); } // Currently, we are only enabling the zoom animation. @@ -163,12 +173,16 @@ function block_core_image_render_lightbox( $block_content, $block ) { $scale_attr = false; } - $w = new WP_HTML_Tag_Processor( $block_content ); - $w->next_tag( 'figure' ); - $w->add_class( 'wp-lightbox-container' ); - $w->set_attribute( 'data-wp-interactive', true ); + // Jump back to the first tag and find the first figure. + $processor->seek( 'first tag' ); + if ( 'FIGURE' !== $processor->get_tag() ) { + $processor->next_tag( 'FIGURE' ); + } - $w->set_attribute( + $processor->set_bookmark( 'first figure' ); + $processor->add_class( 'wp-lightbox-container' ); + $processor->set_attribute( 'data-wp-interactive', true ); + $processor->set_attribute( 'data-wp-context', sprintf( '{ "core": @@ -196,14 +210,16 @@ function block_core_image_render_lightbox( $block_content, $block ) { __( 'Enlarged image' ) ) ); - $w->next_tag( 'img' ); - $w->set_attribute( 'data-wp-init', 'effects.core.image.setCurrentSrc' ); - $w->set_attribute( 'data-wp-on--load', 'actions.core.image.handleLoad' ); - $w->set_attribute( 'data-wp-effect', 'effects.core.image.setButtonStyles' ); - $w->set_attribute( 'data-wp-effect--setStylesOnResize', 'effects.core.image.setStylesOnResize' ); - $body_content = $w->get_updated_html(); - - // Wrap the image in the body content with a button. + + // Assume that an image appears within the currently-matched FIGURE element. It may not. + $processor->next_tag( 'img' ); + $processor->set_attribute( 'data-wp-init', 'effects.core.image.setCurrentSrc' ); + $processor->set_attribute( 'data-wp-on--load', 'actions.core.image.handleLoad' ); + $processor->set_attribute( 'data-wp-effect', 'effects.core.image.setButtonStyles' ); + $processor->set_attribute( 'data-wp-effect--setStylesOnResize', 'effects.core.image.setStylesOnResize' ); + $body_content = $processor->get_updated_html(); + + // Insert a button in the body content before the image. $img = null; preg_match( '/]+>/', $body_content, $img ); @@ -222,42 +238,63 @@ function block_core_image_render_lightbox( $block_content, $block ) { $body_content = preg_replace( '/]+>/', $button, $body_content ); - // We need both a responsive image and an enlarged image to animate - // the zoom seamlessly on slow internet connections; the responsive - // image is a copy of the one in the body, which animates immediately - // as the lightbox is opened, while the enlarged one is a full-sized - // version that will likely still be loading as the animation begins. - $m = new WP_HTML_Tag_Processor( $block_content ); - $m->next_tag( 'figure' ); - $m->add_class( 'responsive-image' ); - $m->next_tag( 'img' ); - // We want to set the 'src' attribute to an empty string in the responsive image - // because otherwise, as of this writing, the wp_filter_content_tags() function in - // WordPress will automatically add a 'srcset' attribute to the image, which will at - // times cause the incorrectly sized image to be loaded in the lightbox on Firefox. - // Because of this, we bind the 'src' attribute explicitly the current src to reliably - // use the exact same image as in the content when the lightbox is first opened while - // we wait for the larger image to load. - $m->set_attribute( 'src', '' ); - $m->set_attribute( 'data-wp-bind--src', 'context.core.image.imageCurrentSrc' ); - $m->set_attribute( 'data-wp-style--object-fit', 'selectors.core.image.lightboxObjectFit' ); - $initial_image_content = $m->get_updated_html(); - - $q = new WP_HTML_Tag_Processor( $block_content ); - $q->next_tag( 'figure' ); - $q->add_class( 'enlarged-image' ); - $q->next_tag( 'img' ); - - // We set the 'src' attribute to an empty string to prevent the browser from loading the image - // on initial page load, then bind the attribute to a selector that returns the full-sized image src when - // the lightbox is opened. We could use 'loading=lazy' in combination with the 'hidden' attribute to - // accomplish the same behavior, but that approach breaks progressive loading of the image in Safari - // and Chrome (see https://github.com/WordPress/gutenberg/pull/52765#issuecomment-1674008151). Until that - // is resolved, manually setting the 'src' seems to be the best solution to load the large image on demand. - $q->set_attribute( 'src', '' ); - $q->set_attribute( 'data-wp-bind--src', 'selectors.core.image.enlargedImgSrc' ); - $q->set_attribute( 'data-wp-style--object-fit', 'selectors.core.image.lightboxObjectFit' ); - $enlarged_image_content = $q->get_updated_html(); + /* + * This code needs to generate a responsive image and an enlarged image + * to animate zoom seamlessly on slow internet connections; the responsive + * image is a copy of the one in the body, which animates immediately + * as the lightbox is opened, while the enlarged one is a full-sized + * version that will likely still be loading as the animation begins. + * + * In order to reuse the existing Tag Processor, changes made before + * need to be undone before setting the new changes here. + */ + $processor->seek( 'first figure' ); + $processor->remove_class( 'wp-lightbox-container' ); + $processor->remove_attribute( 'data-wp-context' ); + $processor->remove_attribute( 'data-wp-interactive' ); + $processor->add_class( 'responsive-image' ); + + $processor->seek( 'first figure' ); + $processor->next_tag( 'img' ); + $processor->remove_attribute( 'data-wp-init' ); + $processor->remove_attribute( 'data-wp-on--load' ); + $processor->remove_attribute( 'data-wp-effect' ); + $processor->remove_attribute( 'data-wp-effect--setStylesOnResize' ); + + /* + * The 'src' attribute needs to be an empty string in the responsive image because + * otherwise, as of this writing, the wp_filter_content_tags() function in WordPress + * will automatically add a 'srcset' attribute to the image, which will at times + * cause the incorrectly sized image to be loaded in the lightbox on Firefox. + * + * Because of this, the 'src' attribute needs to be explicitly bound to the current + * src to reliably use the exact same image as in the content when the lightbox is + * first opened while waiting for the larger image to load. + */ + $processor->set_attribute( 'src', '' ); + $processor->set_attribute( 'data-wp-bind--src', 'context.core.image.imageCurrentSrc' ); + $processor->set_attribute( 'data-wp-style--object-fit', 'selectors.core.image.lightboxObjectFit' ); + $initial_image_content = $processor->get_updated_html(); + + /* + * Reusing the existing Tag Processor again requires resetting state. + */ + $processor->seek( 'first figure' ); + $processor->remove_class( 'responsive-image' ); + $processor->add_class( 'enlarged-image' ); + + /* + * It's necessary to set the 'src' attribute to an empty string to prevent the browser from loading the image + * on initial page load, then to bind the attribute to a selector that returns the full-sized image src when + * the lightbox is opened. The combination of 'loading=lazy' with the 'hidden' attribute could be used to + * accomplish the same behavior, but that approach breaks progressive loading of the image in Safari + * and Chrome (see https://github.com/WordPress/gutenberg/pull/52765#issuecomment-1674008151). Until that + * is resolved, manually setting the 'src' loads the large image on demand without causing renderering issues. + */ + $processor->next_tag( 'img' ); + $processor->set_attribute( 'data-wp-bind--src', 'selectors.core.image.enlargedImgSrc' ); + $processor->set_attribute( 'data-wp-style--object-fit', 'selectors.core.image.lightboxObjectFit' ); + $enlarged_image_content = $processor->get_updated_html(); // If the current theme does NOT have a `theme.json`, or the colors are not defined, // we need to set the background color & close button color to some default values