, then the DOM will break.
[ 'class' => 'slide' ]
);
@@ -100,19 +102,23 @@ public function get_dom_element() {
$slide_container->appendChild( $slide_node );
- // If there's a caption, wrap it and append it to the slide.
- if ( $caption ) {
- $caption_wrapper = AMP_DOM_Utils::create_node(
- $this->dom,
- 'span', // This cannot be a
because if the gallery is inside of a
, then the DOM will break.
- [ 'class' => 'amp-wp-gallery-caption' ]
- );
- $caption_span = AMP_DOM_Utils::create_node( $this->dom, 'span', [] );
- $text_node = $this->dom->createTextNode( $caption );
-
- $caption_span->appendChild( $text_node );
- $caption_wrapper->appendChild( $caption_span );
- $slide_container->appendChild( $caption_wrapper );
+ // If there's a caption, append it to the slide.
+ if ( null !== $caption_element ) {
+ // If the caption is not a , wrap it in one.
+ if ( Tag::FIGCAPTION !== $caption_element->nodeName ) {
+ $caption_content = $caption_element;
+ $caption_element = AMP_DOM_Utils::create_node( $this->dom, Tag::FIGCAPTION, [] );
+ $caption_element->appendChild( $caption_content );
+ }
+
+ $has_caption_class = AMP_DOM_Utils::has_class( $caption_element, 'amp-wp-gallery-caption' );
+
+ /** @var DOMElement $caption_element */
+ if ( ! $has_caption_class ) {
+ $caption_element->setAttribute( Attribute::CLASS_, 'amp-wp-gallery-caption' );
+ }
+
+ $slide_container->appendChild( $caption_element );
}
$amp_carousel->appendChild( $slide_container );
@@ -144,7 +150,7 @@ private function get_dimensions() {
$carousel_height = 0;
foreach ( $this->slides as $slide ) {
- $slide_node = $slide instanceof CaptionedSlide ? $slide->get_slide_node() : $slide;
+ $slide_node = $slide instanceof CaptionedSlide ? $slide->get_slide_element() : $slide;
// Account for an that's wrapped in an .
if ( ! $this->is_image_element( $slide_node ) && $slide_node->firstChild instanceof DOMElement && $this->is_image_element( $slide_node->firstChild ) ) {
$slide_node = $slide_node->firstChild;
diff --git a/src/Component/HasCaption.php b/src/Component/HasCaption.php
index dc76625c22e..9b63681fe85 100644
--- a/src/Component/HasCaption.php
+++ b/src/Component/HasCaption.php
@@ -7,6 +7,8 @@
namespace AmpProject\AmpWP\Component;
+use DOMElement;
+
/**
* Interface HasCaption
*
@@ -16,9 +18,9 @@
interface HasCaption {
/**
- * Gets the caption.
+ * Gets the caption node.
*
- * @return string The caption text.
+ * @return DOMElement
*/
- public function get_caption();
+ public function get_caption_element();
}
diff --git a/src/Dom/ElementList.php b/src/Dom/ElementList.php
index b115be2d123..252c6769a73 100644
--- a/src/Dom/ElementList.php
+++ b/src/Dom/ElementList.php
@@ -31,13 +31,13 @@ final class ElementList implements IteratorAggregate, Countable {
/**
* Adds an element to the list, possibly with a caption.
*
- * @param DOMElement $element The element to add, possibly an image.
- * @param string $caption The caption to add, if any.
+ * @param DOMElement $element The element to add, possibly an image.
+ * @param DOMElement|null $caption The caption for the element.
* @return ElementList A clone of this list, with the new element added.
*/
- public function add( DOMElement $element, $caption = '' ) {
+ public function add( DOMElement $element, DOMElement $caption = null ) {
$cloned_list = clone $this;
- $cloned_list->elements[] = empty( $caption ) ? $element : new CaptionedSlide( $element, $caption );
+ $cloned_list->elements[] = null === $caption ? $element : new CaptionedSlide( $element, $caption );
return $cloned_list;
}
diff --git a/src/Embed/HandlesGalleryEmbed.php b/src/Embed/HandlesGalleryEmbed.php
new file mode 100644
index 00000000000..206222eacc7
--- /dev/null
+++ b/src/Embed/HandlesGalleryEmbed.php
@@ -0,0 +1,100 @@
+.
+ * @param bool $is_lightbox Whether the gallery images should be shown in a lightbox.
+ * @param DOMElement $gallery_element Gallery element.
+ * @param DOMNodeList $img_elements List of image elements in gallery.
+ */
+ protected function process_gallery_embed( $is_carousel, $is_lightbox, DOMElement $gallery_element, DOMNodeList $img_elements ) {
+ // Bail if the embed does not support carousel or lightbox.
+ if ( ! $is_carousel && ! $is_lightbox ) {
+ return;
+ }
+
+ // Bail if there are no images.
+ if ( 0 === $img_elements->length ) {
+ return;
+ }
+
+ // If the carousel is not required but the lightbox is, add the `lightbox` attribute to each image and return.
+ if ( ! $is_carousel && $is_lightbox ) {
+ $this->add_lightbox_attribute_to_img_nodes( $img_elements );
+ return;
+ }
+
+ if ( $is_carousel ) {
+ $amp_carousel = $this->generate_amp_carousel( $img_elements, $is_lightbox );
+ $carousel_element = $amp_carousel->get_dom_element();
+
+ if ( $is_lightbox ) {
+ $carousel_element->setAttribute( Attribute::LIGHTBOX, '' );
+ }
+
+ $gallery_element->parentNode->replaceChild( $carousel_element, $gallery_element );
+ }
+ }
+
+ /**
+ * Create an AMP carousel component from the list of images specified.
+ *
+ * @param DOMNodeList $img_elements List of images in the gallery.
+ * @param boolean $is_amp_lightbox Whether the gallery should have a lightbox.
+ * @return Carousel An object containing markup for .
+ */
+ protected function generate_amp_carousel( DOMNodeList $img_elements, $is_amp_lightbox ) {
+ $images = new ElementList();
+
+ foreach ( $img_elements as $img_element ) {
+ $element = $img_element;
+ $parent_element_name = $img_element->parentNode->nodeName;
+
+ if ( Tag::A === $parent_element_name && ! $is_amp_lightbox ) {
+ $element = $img_element->parentNode;
+ }
+
+ $images = $images->add( $element, $this->get_caption_element( $img_element ) );
+ }
+
+ $node = $img_elements->item( 0 );
+ return new Carousel( Document::fromNode( $node ), $images );
+ }
+
+ /**
+ * Sets the `lightbox` attribute to each image in the specified list.
+ *
+ * @param DOMNodeList $img_elements List of image elements.
+ */
+ protected function add_lightbox_attribute_to_img_nodes( DOMNodeList $img_elements ) {
+ /** @var DOMElement $img_element */
+ foreach ( $img_elements as $img_element ) {
+ $img_element->setAttribute( Attribute::LIGHTBOX, '' );
+ }
+ }
+}
diff --git a/src/ObsoleteBlockAttributeRemover.php b/src/ObsoleteBlockAttributeRemover.php
new file mode 100644
index 00000000000..89a3338a117
--- /dev/null
+++ b/src/ObsoleteBlockAttributeRemover.php
@@ -0,0 +1,103 @@
+data['content']['raw'] ) ) {
+ $response->data['content']['raw'] = preg_replace_callback(
+ '#(?P\s*+)(?P<[a-z][a-z0-9_:-]*+\s[^>]*+>)#s',
+ function ( $matches ) {
+ return $matches['block_comment'] . preg_replace( $this->get_obsolete_attribute_pattern(), '', $matches['start_tag'] );
+ },
+ $response->data['content']['raw']
+ );
+ }
+ return $response;
+ }
+}
diff --git a/tests/php/src/ObsoleteBlockAttributeRemoverTest.php b/tests/php/src/ObsoleteBlockAttributeRemoverTest.php
new file mode 100644
index 00000000000..613a3695e9e
--- /dev/null
+++ b/tests/php/src/ObsoleteBlockAttributeRemoverTest.php
@@ -0,0 +1,152 @@
+ 'data-amp-carousel',
+ 'ampLayout' => 'data-amp-layout',
+ 'ampLightbox' => 'data-amp-lightbox',
+ 'ampNoLoading' => 'data-amp-noloading',
+ ];
+
+ public function setUp() {
+ parent::setUp();
+ $this->instance = new ObsoleteBlockAttributeRemover();
+ }
+
+ /** @covers ObsoleteBlockAttributeRemover::__construct() */
+ public function test_it_can_be_initialized() {
+ $this->assertInstanceOf( Service::class, $this->instance );
+ $this->assertInstanceOf( Registerable::class, $this->instance );
+ $this->assertInstanceOf( Delayed::class, $this->instance );
+ }
+
+ /** @covers ObsoleteBlockAttributeRemover::get_registration_action() */
+ public function test_get_registration_action() {
+ $this->assertEquals( 'rest_api_init', ObsoleteBlockAttributeRemover::get_registration_action() );
+ }
+
+ /** @covers ObsoleteBlockAttributeRemover::register() */
+ public function test_register() {
+ $editor_post_types = get_post_types_by_support( 'editor' );
+ $this->assertNotEmpty( $editor_post_types );
+ $this->instance->register();
+ foreach ( $editor_post_types as $editor_post_type ) {
+ $this->assertEquals( 10, has_filter( "rest_prepare_{$editor_post_type}", [ $this->instance, 'filter_rest_prepare_post' ] ) );
+ }
+ }
+
+ /** @return array */
+ public function get_block_data() {
+ return [
+ 'gallery_carousel_lightbox' => [
+ '
+
+
+
+ ',
+ 2,
+ ],
+ 'image_block_multi_attrs' => [
+ '
+
+ Noloading and lightbox and fixed layout
+
+ ',
+ 3,
+ ],
+ 'image_block' => [
+ '
+
+ Fixed layout
+
+ ',
+ 1, // Expect props.
+ ],
+ 'amp_fit_text' => [
+ '
+
+ Here is amp-fit-text!
+
+ ',
+ 0, // Expect props.
+ ],
+ ];
+ }
+
+ /**
+ * @covers ObsoleteBlockAttributeRemover::test_filter_rest_prepare_post()
+ *
+ * @dataProvider get_block_data
+ * @param string $block_content
+ * @param int $expected_prop_count
+ */
+ public function test_filter_rest_prepare_post_raw( $block_content, $expected_prop_count ) {
+ if ( ! function_exists( 'parse_blocks' ) ) {
+ $this->markTestSkipped();
+ }
+ $block_content = str_replace( "\t", '', trim( $block_content ) );
+
+ $parsed_blocks = parse_blocks( $block_content );
+ $this->assertCount( 1, $parsed_blocks );
+ $parsed_block = array_shift( $parsed_blocks );
+
+ $response = new WP_REST_Response(
+ [
+ 'content' => [
+ 'raw' => $block_content,
+ 'rendered' => preg_replace( '//s', '', $block_content ),
+ ],
+ ]
+ );
+
+ $filtered_response = $this->instance->filter_rest_prepare_post( clone $response );
+ if ( $expected_prop_count > 0 ) {
+ $this->assertNotEquals( $response->data['content']['raw'], $filtered_response->data['content']['raw'] );
+ } else {
+ $this->assertEquals( $response->data['content']['raw'], $filtered_response->data['content']['raw'] );
+ }
+ $this->assertEquals( $response->data['content']['rendered'], $filtered_response->data['content']['rendered'] );
+
+ $present_count = 0;
+ foreach ( self::PROP_ATTRIBUTE_MAPPING as $prop => $attribute ) {
+ if ( isset( $parsed_block['attrs'][ $prop ] ) ) {
+ $this->assertStringContains( "$attribute=", $response->data['content']['raw'] );
+ $this->assertStringNotContains( "$attribute=", $filtered_response->data['content']['raw'] );
+ $present_count++;
+ }
+ }
+
+ $this->assertEquals( $expected_prop_count, $present_count );
+ }
+
+ /** @covers ObsoleteBlockAttributeRemover::test_filter_rest_prepare_post() */
+ public function test_filter_rest_prepare_post_rendered_only() {
+ $response = new WP_REST_Response(
+ [
+ 'content' => [
+ 'rendered' => 'Here is amp-fit-text!
',
+ ],
+ ]
+ );
+
+ $filtered_response = $this->instance->filter_rest_prepare_post( clone $response );
+ $this->assertEquals( $response->data['content']['rendered'], $filtered_response->data['content']['rendered'] );
+ }
+}
diff --git a/tests/php/test-amp-carousel.php b/tests/php/test-amp-carousel.php
index 32857abdd27..712ee54000f 100644
--- a/tests/php/test-amp-carousel.php
+++ b/tests/php/test-amp-carousel.php
@@ -15,7 +15,7 @@
*
* @covers \AmpProject\AmpWP\Component\Carousel
*/
-class Test_Carousel extends \WP_UnitTestCase {
+class Test_Carousel extends WP_UnitTestCase {
use PrivateAccess;
@@ -25,27 +25,29 @@ class Test_Carousel extends \WP_UnitTestCase {
* @return array[] An associative array, including the slides and captions, the DOM, and the expected markup.
*/
public function get_carousel_data() {
- $dom = new Document();
- $src = 'https://example.com/img.png';
- $width = '1200';
- $height = '800';
- $image = AMP_DOM_Utils::create_node(
+ $dom = new Document();
+ $src = 'https://example.com/img.png';
+ $width = '1200';
+ $height = '800';
+ $image = AMP_DOM_Utils::create_node(
$dom,
'amp-img',
compact( 'src', 'width', 'height' )
);
- $caption = 'Example caption';
+
+ $caption_element = AMP_DOM_Utils::create_node( $dom, 'a', [ 'href' => 'example.org' ] );
+ $caption_element->appendChild( new DOMText( 'Example caption' ) );
return [
- 'image_without_caption' => [
- ( new ElementList() )->add( $image, '' ),
+ 'image_without_caption' => [
+ ( new ElementList() )->add( $image, null ),
$dom,
- '',
+ '',
],
- 'image_with_caption' => [
- ( new ElementList() )->add( $image, $caption ),
+ 'image_with_html_caption' => [
+ ( new ElementList() )->add( $image, $caption_element ),
$dom,
- '' . $caption . '',
+ 'Example caption',
],
];
}
@@ -129,15 +131,15 @@ public function get_data_carousel_dimensions() {
[ $wide_image_width, $wide_image_height ],
],
'image_with_0_height_should_not_affect_ratio' => [
- ( new ElementList() )->add( $image_with_0_height )->add( $wide_image, '' ),
+ ( new ElementList() )->add( $image_with_0_height )->add( $wide_image, null ),
[ $wide_image_width, $wide_image_height ],
],
'two_images' => [
- ( new ElementList() )->add( $narrow_image )->add( $wide_image, '' ),
+ ( new ElementList() )->add( $narrow_image )->add( $wide_image, null ),
[ $wide_image_width, $wide_image_height ],
],
'two_images_order_changed' => [
- ( new ElementList() )->add( $wide_image )->add( $narrow_image, '' ),
+ ( new ElementList() )->add( $wide_image )->add( $narrow_image, null ),
[ $wide_image_width, $wide_image_height ],
],
];
diff --git a/tests/php/test-amp-gallery-embed-handler.php b/tests/php/test-amp-gallery-embed-handler.php
index 85021a780dc..fb639cf4fbc 100644
--- a/tests/php/test-amp-gallery-embed-handler.php
+++ b/tests/php/test-amp-gallery-embed-handler.php
@@ -5,6 +5,8 @@
* @package AMP
*/
+use AmpProject\AmpWP\Admin\ReaderThemes;
+use AmpProject\AmpWP\Option;
use AmpProject\AmpWP\Tests\Helpers\WithoutBlockPreRendering;
/**
@@ -14,6 +16,16 @@ class AMP_Gallery_Embed_Handler_Test extends WP_UnitTestCase {
use WithoutBlockPreRendering;
+ private static $original_amp_options;
+
+ public static function setUpBeforeClass() {
+ self::$original_amp_options = AMP_Options_Manager::get_options();
+ }
+
+ public static function tearDownAfterClass() {
+ AMP_Options_Manager::update_options( self::$original_amp_options );
+ }
+
/**
* Tear down.
*/
@@ -44,7 +56,7 @@ public function tearDown() {
* @return array[]
*/
public function get_conversion_data() {
- $amp_carousel_caption = '' . self::CAPTION_TEXT . '';
+ $amp_carousel_caption = ' ' . self::CAPTION_TEXT . ' ';
$loading_attribute = version_compare( get_bloginfo( 'version' ), '5.5-alpha', '>' ) ? 'loading="lazy"' : '';
return [
@@ -52,88 +64,99 @@ public function get_conversion_data() {
'[gallery ids=1]',
'',
],
- 'shortcode_with_valid_id' => [
- '[gallery ids={{id1}}]',
- '' . $amp_carousel_caption . '',
- ],
- 'shortcode_with_multiple_ids' => [
+ 'shortcode_with_valid_ids_in_legacy_mode' => [
'[gallery ids={{id1}},{{id2}},{{id3}}]',
- '' .
- '' . $amp_carousel_caption . '' .
- '' .
- '' .
- '',
+ '
+
+ ' . $amp_carousel_caption . '
+
+
+ ',
+ true,
],
- 'shortcode_linking_to_file' => [
- '[gallery link="file" ids={{id1}},{{id2}},{{id3}}]',
- '' .
- '' . $amp_carousel_caption . '' .
- '' .
- '' .
- '',
+ 'shortcode_with_valid_ids' => [
+ '[gallery ids={{id1}},{{id2}},{{id3}}]',
+ '
+
+
+
+ - ' . self::CAPTION_TEXT . '
+
+
+
+
+
+
+
+
+
',
],
'shortcode_with_carousel' => [
'[gallery amp-lightbox=false amp-carousel=true ids={{id1}},{{id2}},{{id3}}]',
- '' .
- '' . $amp_carousel_caption . '' .
- '' .
- '' .
- '',
+ '
+
+ ' . $amp_carousel_caption . '
+
+
+ ',
],
'shortcode_with_carousel_linking_to_file' => [
'[gallery amp-lightbox=false amp-carousel=true link="file" ids={{id1}},{{id2}},{{id3}}]',
- '' .
- '' . $amp_carousel_caption . '' .
- '' .
- '' .
- '',
+ '
+
+ ' . $amp_carousel_caption . '
+
+
+ ',
],
'shortcode_with_lightbox' => [
'[gallery amp-lightbox=true amp-carousel=false ids={{id1}},{{id2}},{{id3}}]',
- '
-
-
-
-
-
- ' . self::CAPTION_TEXT . '
-
-
-
+ '
+
+
-
+
+
- ' . self::CAPTION_TEXT . '
+
-
+
-
-
-
+
-
+
-
+
',
],
'shortcode_with_lightbox_linking_to_file' => [
'[gallery amp-lightbox=true amp-carousel=false link="file" ids={{id1}},{{id2}},{{id3}}]',
- '
-
-
-
-
-
- ' . self::CAPTION_TEXT . '
-
-
-
+ '
+
+
-
+
+
- ' . self::CAPTION_TEXT . '
+
-
+
-
-
-
+
-
+
-
+
',
],
'shortcode_with_lightbox_and_carousel' => [
'[gallery amp-lightbox=true amp-carousel=true ids={{id1}},{{id2}},{{id3}}]',
- '' .
- '' . $amp_carousel_caption . '' .
- '' .
- '' .
+ '
+ ' .
+ '' . $amp_carousel_caption . '' .
+ '' .
+ '' .
'',
],
'shortcode_with_lightbox_and_carousel_linking_to_file' => [
'[gallery amp-lightbox=true amp-carousel=true link="file" ids={{id1}},{{id2}},{{id3}}]',
- '' .
- '' . $amp_carousel_caption . '' .
- '' .
- '' .
+ '
+ ' .
+ '' . $amp_carousel_caption . '' .
+ '' .
+ '' .
'',
],
];
@@ -145,22 +168,31 @@ public function get_conversion_data() {
* @dataProvider get_conversion_data
* @param string $source Source.
* @param string $expected Expected.
+ * @param bool $use_legacy_mode Whether to use legacy Reader mode.
*/
- public function test__conversion( $source, $expected ) {
+ public function test__conversion( $source, $expected, $use_legacy_mode = false ) {
$source_files = [
DIR_TESTDATA . '/images/test-image.jpg',
DIR_TESTDATA . '/images/canola.jpg',
DIR_TESTDATA . '/images/gradient-square.jpg',
];
+ if ( $use_legacy_mode ) {
+ AMP_Options_Manager::update_option( Option::READER_THEME, ReaderThemes::DEFAULT_READER_THEME );
+ AMP_Options_Manager::update_option( Option::THEME_SUPPORT, AMP_Theme_Support::READER_MODE_SLUG );
+ } else {
+ AMP_Options_Manager::update_option( Option::READER_THEME, 'foobar' );
+ }
+
// When we generate new attachments, we neither control the IDs being
// generated, which we need for the gallery shortcode, nor the actual
// filenames, which will have an index appended because of filename
// collisions. So we write the tests in a way that they don't rely on
// this, and str-replace the dynamic portions into the expected and
// actual output after the fact.
- $ids = [];
- $files = [];
+ $ids = [];
+ $files = [];
+ $file_urls = [];
foreach ( $source_files as $index => $source_file ) {
$ids[ $index ] = self::factory()->attachment
@@ -171,6 +203,8 @@ public function test__conversion( $source, $expected ) {
'.jpg'
);
+ $file_urls[ $index ] = get_attachment_link( $ids[ $index ] );
+
update_post_meta(
$ids[ $index ],
'_wp_attachment_image_alt',
@@ -188,7 +222,7 @@ public function test__conversion( $source, $expected ) {
}
}
- $this->initialize_replacements( $ids, $files );
+ $this->initialize_replacements( $ids, $files, $file_urls );
$embed = new AMP_Gallery_Embed_Handler();
$embed->register_embed();
@@ -198,28 +232,37 @@ public function test__conversion( $source, $expected ) {
$this->normalize( $source )
);
+ $dom = AMP_DOM_Utils::get_dom_from_content( $filtered_content );
+ $embed->sanitize_raw_embeds( $dom );
+
+ $content = AMP_DOM_Utils::get_content_from_dom( $dom );
+
$this->assertEquals(
$this->normalize( $expected ),
- $this->normalize( $filtered_content )
+ $this->normalize( $content )
);
}
/**
* Initialize the associative array of replacements to perform.
*
- * @param array $ids Array of attachment post IDs.
- * @param array $files Array of file URLs.
+ * @param array $ids Array of attachment post IDs.
+ * @param array $files Array of file URLs.
+ * @param array $file_urls Array of file permalink URLs.
*/
- private function initialize_replacements( $ids, $files ) {
+ private function initialize_replacements( $ids, $files, $file_urls ) {
$this->replacements = [
- '{{id1}}' => $ids[0],
- '{{id2}}' => $ids[1],
- '{{id3}}' => $ids[2],
- '{{file1}}' => $files[0],
- '{{file2}}' => $files[1],
- '{{file3}}' => $files[2],
- "\n" => '', // Make tests ignore new lines.
- '> <' => '><', // Remove left-over space between elements.
+ '{{id1}}' => $ids[0],
+ '{{id2}}' => $ids[1],
+ '{{id3}}' => $ids[2],
+ '{{file1}}' => $files[0],
+ '{{file2}}' => $files[1],
+ '{{file3}}' => $files[2],
+ '{{file_url1}}' => $file_urls[0],
+ '{{file_url2}}' => $file_urls[1],
+ '{{file_url3}}' => $file_urls[2],
+ "\n" => '', // Make tests ignore new lines.
+ '> <' => '><', // Remove left-over space between elements.
];
}
@@ -236,7 +279,7 @@ private function normalize( $content ) {
$content = trim( preg_replace( '/\s+/', ' ', $content ) );
// Normalize attribute quote style for 5.3-alpha.
- $content = str_replace( '