Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable captions on Gallery shortcodes #3659

Merged
merged 50 commits into from
Nov 26, 2019

Conversation

kienstra
Copy link
Contributor

@kienstra kienstra commented Oct 29, 2019

Summary

This allows displaying captions in Gallery shortcodes. It adds a class AMP_Carousel that uses logic in the Gallery block sanitizer from PR #3285.

Fixes #3658
Closes #3810

Checklist

  • My pull request is addressing an open issue (please create one otherwise).
  • My code is tested and passes existing tests.
  • My code follows the Engineering Guidelines (updates are often made to the guidelines, check it out periodically).

Use the existing logic in AMP_Carousel,
and add tests for this.
This was extracted from the logic
in the Gallery block sanitizer.
@googlebot googlebot added the cla: yes Signed the Google CLA label Oct 29, 2019
It doesn't only have images.
Also, change some @const tags to @var.
Instead of using destructuring,
simply use 0 and 1 indices.
This is now a method of AMP_Carousel,
so it seem unnecessary to have 'carousel'
in the function name.
It seems like the $dom property is usually
protected or private in classes in the AMP plugin.
Also, improve the DocBlock for
AMP_Carousel::create_and_get().
I forgot to add
$images_and_captions.
* }
* @return DOMElement A representation of the <amp-carousel>.
*/
public function create_and_get( $images_and_captions ) {
Copy link
Contributor Author

@kienstra kienstra Oct 29, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'd be better if $images_and_captions were an associative array of images to their respective captions.

But a DOMElement (the image) isn't allowed as a key of an associative array.

So $images_and_captions now looks like:

[
        [ DOMElement, 'Example caption here' ],
        [ DOMElement, 'Another caption for the next image' ],
]

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd actually prefer this to be a value object instead. It can be type-hinted, would enforce a given structure and make sure it cannot be used incorrectly.

Normally, this would be a combination of a value object for a single entry, and a collection for multiple entries.

I'm not sure how open @westonruter is for going deeper into objects, though, so a compromise of a single value object for a collection of entries would work as well.

As an example, here's how an object-based representation would look like and behave (comments mostly skipped for brevity):

interface Has_Caption {
	/** @return string */
	public function get_caption();
}

class AMP_Image {
	/** @var DOMElement */
	protected $image_node;

	public function __construct( DOMElement $image_node ) {
		$this->image_node = $image_node;
	}

	/** @return DOMElement */
	public function get_image_node() {
		return $this->image_node;
	}
}

final class AMP_Captioned_Image extends AMP_Image implements Has_Caption {
	/** @var string */
	private $caption;

	public function __construct( DOMElement $image_node, $caption ) {
		parent::__construct( $image_node );
		$this->caption = $caption;
	}

	/** @return string */
	public function get_caption() {
		return $this->caption;
	}
}

final class AMP_Image_List implements IteratorAggregate {
	/** @var AMP_Captioned_Image[] */
	private $elements;

	/**
	 * @param DOMElement $image_node
	 * @param string     $caption
	 * @return self
	 */
	public function add( DOMElement $image_node, $caption = '' ) {
		$elements[] = empty( $caption )
			? new AMP_Image( $image_node )
			: new AMP_Captioned_Image( $image_node, $caption );
		return $this;
	}

	// This together with the IteratorAggregate turns the object into
	// a "Traversable", so you can just foreach over it and receive its
	// elements in the correct type.
	public function getIterator() {
		return new ArrayIterator( $this->elements );
	}
}

Using this would be pretty straight-forward, and most of all, more foolproof:

$images = ( new AMP_ImageList )
    ->add( $image_node_1, 'First caption' )
	->add( $image_node_2, 'Second caption' )
	->add( $image_without_caption );

foreach ( $images as $image ) {
	// Using the node...
	$node = $image->get_image_node();

	// Using the caption...
	if ( $image instanceof Has_Caption ) {
		$caption = $image->get_caption();
	}
}

Also, any complex operations that deal with these image lists can then be encapsulated into the list itself, making the consuming code that much cleaner.

Just a suggestion, but I would love if we would slowly would into more structured code like this, instead of building everything on top of unsemantic arrays and strings.
Note: In most cases, objects will use less memory than arrays in modern versions of PHP, as it is heavily optimized towards OOP since 7.0+.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's pretty slick. I suppose my only hesitation is overly engineering an API for this which we don't necessarily want others to use externally, until we've given it it enough thought into how it relates to the other pieces. If we just use a simple array as the input, then it is more flexible in the case we decide to go a different direction. But then again, if we don't want people to be using this class yet anyway, then I don't suppose it matters. Just make sure it is marked with @internal.

If we do go this route, then it should definitely fall under the AMP namespace and the autoloader should automatically locate the filename purely based off of the name of the class/interface/trait. If we start creating a ton of little PHP files, we don't want to be having to embiggen the \AMP_Autoloader::$classmap endlessly.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I agree with marking as @internal for now. We should slowly build up our understanding and infrastructure with regards to an OOP approach of the plugin, and only really advertise an explicit set of interfaces with v2.0.

* @return DOMElement A representation of the <amp-carousel>.
*/
public function create_and_get( $images_and_captions ) {
$images = [];
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is mainly moved from AMP_Gallery_Block_Sanitizer, with few changes.

* @type int $height Height.
* }
*/
public function get_dimensions( $images ) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is mainly moved from AMP_Gallery_Block_Sanitizer::get_carousel_dimensions(), with few changes.

This was 'associate' before,
so correct it.
This should simplify the markup a lot,
as it reduces the attributes needed,
and it eliminates the <amp-image-lightbox> tag.
All that's needed here is a 'lightbox' attribute
on an image.
);
/* We need to add lightbox tag, too. @todo Could there be a better alternative for this? */
return $this->render( $args ) . $lightbox_tag;
}
Copy link
Contributor Author

@kienstra kienstra Oct 29, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is now using amp-lightbox-gallery as recommended by amphtml, so all that's needed is a lightbox attribute on the <amp-img> or <amp-carousel>. This plugin automatically adds the amp-lightbox-gallery component script.

*
* Gets the markup for an <amp-carousel>.
*
* @since 1.4.1
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this will be different.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, let's have the changes here by targeting 1.5.0

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good

@@ -12,24 +12,6 @@
*/
class AMP_Gallery_Block_Sanitizer extends AMP_Base_Sanitizer {

/**
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no intended change to the Gallery block, only a refactoring of this logic into the class AMP_Carousel.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Now it's AMP\Carousel)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now, it's Amp\AmpWp\Component\Carousel

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, it's Amp\AmpWP (capital_P_dangit? 😄)

For what it's worth, I do like AmpWP more than AmpWp.

count() will only work for arrays or objects,
so this would handle having a falsey $image
argument.
@kienstra kienstra changed the title [WIP] Enable captions on Gallery shortcodes Enable captions on Gallery shortcodes Oct 29, 2019
@kienstra
Copy link
Contributor Author

kienstra commented Oct 29, 2019

Request For Review, But No Hurry

Hi @westonruter and @schlessera, and @pierlon,
Good to see you earlier.

No hurry reviewing this, with the stable release and WCUS coming up. Just wanted to let you know that's it's ready when you have a chance.

This extracts much of the logic from the Gallery block caption PR (#3285) into a class AMP_Carousel that is also used for the Gallery shortcode.

captions-supported

There's no intended change to Gallery blocks, and I didn't see any in my testing. But I asked for regression testing for them in the QA testing instructions.

Like in the Gallery block caption PR (#3285), the Gallery shortcode now looks different if there's a mix of landscape and square images.

I think it's an improvement, but some might disagree.

Thanks!

Before

before-pr-car

After

fill-width-amp-amp-carousel

* @param array[] $images_and_captions {
* The images and their respective captions, if any.
*
* @type DOMElement $image A representation of an image, in index 0.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @type DOMElement $image A representation of an image, in index 0.
* @type DOMElement $image A representation of an image, in index 0.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good suggestion, but luckily the entire @param tag was simplified, thanks to your code below.

* }
* @return DOMElement A representation of the <amp-carousel>.
*/
public function create_and_get( $images_and_captions ) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd actually prefer this to be a value object instead. It can be type-hinted, would enforce a given structure and make sure it cannot be used incorrectly.

Normally, this would be a combination of a value object for a single entry, and a collection for multiple entries.

I'm not sure how open @westonruter is for going deeper into objects, though, so a compromise of a single value object for a collection of entries would work as well.

As an example, here's how an object-based representation would look like and behave (comments mostly skipped for brevity):

interface Has_Caption {
	/** @return string */
	public function get_caption();
}

class AMP_Image {
	/** @var DOMElement */
	protected $image_node;

	public function __construct( DOMElement $image_node ) {
		$this->image_node = $image_node;
	}

	/** @return DOMElement */
	public function get_image_node() {
		return $this->image_node;
	}
}

final class AMP_Captioned_Image extends AMP_Image implements Has_Caption {
	/** @var string */
	private $caption;

	public function __construct( DOMElement $image_node, $caption ) {
		parent::__construct( $image_node );
		$this->caption = $caption;
	}

	/** @return string */
	public function get_caption() {
		return $this->caption;
	}
}

final class AMP_Image_List implements IteratorAggregate {
	/** @var AMP_Captioned_Image[] */
	private $elements;

	/**
	 * @param DOMElement $image_node
	 * @param string     $caption
	 * @return self
	 */
	public function add( DOMElement $image_node, $caption = '' ) {
		$elements[] = empty( $caption )
			? new AMP_Image( $image_node )
			: new AMP_Captioned_Image( $image_node, $caption );
		return $this;
	}

	// This together with the IteratorAggregate turns the object into
	// a "Traversable", so you can just foreach over it and receive its
	// elements in the correct type.
	public function getIterator() {
		return new ArrayIterator( $this->elements );
	}
}

Using this would be pretty straight-forward, and most of all, more foolproof:

$images = ( new AMP_ImageList )
    ->add( $image_node_1, 'First caption' )
	->add( $image_node_2, 'Second caption' )
	->add( $image_without_caption );

foreach ( $images as $image ) {
	// Using the node...
	$node = $image->get_image_node();

	// Using the caption...
	if ( $image instanceof Has_Caption ) {
		$caption = $image->get_caption();
	}
}

Also, any complex operations that deal with these image lists can then be encapsulated into the list itself, making the consuming code that much cleaner.

Just a suggestion, but I would love if we would slowly would into more structured code like this, instead of building everything on top of unsemantic arrays and strings.
Note: In most cases, objects will use less memory than arrays in modern versions of PHP, as it is heavily optimized towards OOP since 7.0+.

$image = $image->firstChild;
}

if ( ! is_numeric( $image->getAttribute( 'width' ) ) || ! is_numeric( $image->getAttribute( 'height' ) ) ) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to take 0 values into account here? If these can pass through (not sure that might be the case), then we'll end up with a division by zero fatal in the case of the height.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, it's possible that there'd be a '0' width or height.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

93aa3ea continues if there's a 0 or 0.0 width or height.

Copy link
Contributor

@pierlon pierlon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @kienstra, your changes work as described. No errors or warnings occurred while testing 👍

westonruter and others added 4 commits November 10, 2019 22:25
…ry-shortcode-captions

* 'develop' of github.com:ampproject/amp-wp: (61 commits)
  Improve specificity of JS doc
  Fix identifying sources for validation errors coming child themes (#3708)
  Fix failing E2E tests (#3707)
  Remove amp_validate query var from Validated URL 'View' row action
  Re-factor get_html_attribute_pattern as match_element_attributes
  Quote variables added to regex pattern
  Escape instances of unescapeed output in AMP settings screen code (#3703)
  Replace incorrect usage of esc_url() with esc_url_raw()
  Remove empty alt attributes
  Add object-fit=contain to amp-youtube placeholder image
  Prevent copying empty title/alt in WP<5.2
  Extract HTML attribute parsing logic into base class, and use for YouTube embeds
  Update doc comment for `get_video_id_from_url`
  Merge separate regexes used to retrieve props into one
  Replace ambiguous `$fallback` with `$fallback_for_expected`
  Refactor `get_video_id_from_url` function
  Replace the fallback with an image placeholder for the iframe
  Remove todo
  HTML entity encoding will be handled by the DOM instead
  Bump `oembed` function deprecation version to 1.5.0
  ...
This won't make it in 1.4.1,
so target the next major release.
As Alain mentioned, this is much easier
to understand than previously
accepting an array of $images_and_captions.

Props @schlessera.
As Alain mentioned,
if there is a height of 0 or 0.0,
this could cause an error when calculating
the aspect ratio.
Like the previous commit, change other
package names here.
* @return self
*/
public function add( DOMElement $element, $caption = '' ) {
$this->elements[] = empty( $caption ) ? $element->cloneNode() : new CaptionedSlide( $element->cloneNode(), $caption );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be a deep clone? Otherwise the noscript > img children won't be included.

Suggested change
$this->elements[] = empty( $caption ) ? $element->cloneNode() : new CaptionedSlide( $element->cloneNode(), $caption );
$this->elements[] = empty( $caption ) ? $element->cloneNode( true ) : new CaptionedSlide( $element->cloneNode( true ), $caption );

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, good idea.

Copy link
Contributor Author

@kienstra kienstra Nov 22, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That helped, but the tests still failed locally, even changing all of the ->cloneNode() calls to ->cloneNode( true ). I'm working on it.

This looked to cause an error here:
https://github.com/ampproject/amp-wp/blob/7b603eaeda0d87c71815f8014c9a5ae09fd3eac4/src/Component/Carousel.php#L152

getAttribute(): Couldn't fetch DOMElement
It looks like there was no longer a reference
to the DOMElement after cloning it.
@kienstra
Copy link
Contributor Author

kienstra commented Nov 23, 2019

Hi @schlessera,

@kienstra Can you make the ImageList immutable? If I remember correctly, you'd only need to clone on add() to achieve that, and always re-assign the return value.

So far, I couldn't find a way to make ImageList (now DOMElementList) immutable, without causing an error.

This looked to cause an error in this unit test on this line.

getAttribute(): Couldn't fetch DOMElement

It looks like after running ->cloneNode( true ), there was no longer a reference to the DOMElement.

@kienstra
Copy link
Contributor Author

Though maybe I'm not clear on what immutable means in this context.

Here's a (now reverted) unit test to check if it was possible to change an attribute when iterating over the DOMElementList.

@schlessera
Copy link
Collaborator

schlessera commented Nov 23, 2019

I'm sorry, @kienstra, my comments were probably misleading.

You cannot really make the DOMNodes reliably immutable, as they are not designed that way.

What I meant was make the "container" ImageList immutable in the sense that when you have an image list, it will stick to the images it has, even if some other code adds an image later on.

So, just cloning the list itself before adding and returning the clone, and always assigning the return value of the add() operation to accept the clone as the new image list.

It should be pretty straight-forward. Anything more is overkill and will still never work reliably.

Thanks to Alain's suggestion,
this makes the DOMElementList immutable.
The $elements property is now public,
but the DOMElements in that were publicly
available through iteration,
so that isn't a huge change.
This doesn't simply return self anymore,
but a clone of it, with an element added.
@kienstra
Copy link
Contributor Author

Hi @schlessera,
Thanks, that's a good idea 😄

3cdd86f clones the list in add() and returns that clone.

For $elements, it should be array,
as it's possible for it to have CaptionedImage
instances.
@@ -25,7 +25,7 @@ final class DOMElementList implements IteratorAggregate, Countable {
*
* @var DOMElement[]
*/
private $elements = [];
public $elements = [];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you switch this to public? That totally defeats the purpose of making it immutable, if anyone can just change the internals...

Suggested change
public $elements = [];
private $elements = [];

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My mistake, 38df209 changes it back to private.

I originally changed it because of failing unit tests, but they pass now.

composer.json Outdated
@@ -33,6 +33,7 @@
"ext-zip": "Enables the use of ZipArchive to export AMP Stories."
},
"config": {
"optimize-autoloader": true,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be removed, and a corresponding change should be made to the build process instead.

If an optimized autoloader is forced, then you'll have to constantly rebuild the plugin on filesystem changes even during development.

},
"autoload": {
"psr-4": {
"Amp\\AmpWP\\": "src/"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed in #3810

src/Component/Carousel.php Outdated Show resolved Hide resolved
src/Component/Carousel.php Outdated Show resolved Hide resolved
src/Component/Carousel.php Outdated Show resolved Hide resolved
* @type int $height The height of the carousel, at index 1.
* }
*/
public function get_dimensions() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this supposed to be publicly accessible, i.e. do we want to give access to this to third-party developers as our public interface?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uf, there's probably not a need for it to be public.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

478f71e makes it private

*/
public function __construct( $dom, DOMElementList $images ) {
$this->dom = $dom;
$this->images = $images;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As the carousel can contain other elements than just images, should we rename this property and the corresponding variables into $slides instead to match the CaptionedSlide adjustment?

Copy link
Contributor Author

@kienstra kienstra Nov 25, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, good point. It's now changed in 450f9cd

I should have brought this up before: the Carousel class probably makes the most sense for images, as get_dimensions() looks for width and height attributes, and has some conditionals for <amp-img>.

Still, maybe Carousel could be used in the future for something else with width and height attributes, like amp-video.

And as you mentioned, the DOMElementList can now have any element.

kienstra and others added 9 commits November 25, 2019 12:53
I had changed this to be public,
but it looks like it doesn't need to be anymore.
Commit Alain's suggestion to make `$dom` private

Co-Authored-By: Alain Schlesser <[email protected]>
As Alain mentioned,
it can now accept any element in DOMElementList.
Following up on 450f9cd,
also change references to 'slides'.
… in build

It looks like it's not good to
have this in composer.json.
There probably isn't a need for this
to be in the public API.
This flag is recommended for production only:
"Note: You should not enable any of these optimizations in development as they all will cause various problems when adding/removing classes. The performance gains are not worth the trouble in a development setting."

@see https://getcomposer.org/doc/articles/autoloader-optimization.md#autoloader-optimization
Maybe this should be added to .travis.yml
But maybe it could cause issues with the
cached builds.
It looks like the performance improvement could be
very slight, compared to possibly debugging
in the future.
@westonruter westonruter added this to the v1.5 milestone Nov 26, 2019
Copy link
Member

@westonruter westonruter left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gallery block and gallery shortcode now render the same, with captions!

image

@westonruter westonruter merged commit 9c853ca into develop Nov 26, 2019
@westonruter westonruter deleted the add/gallery-shortcode-captions branch November 26, 2019 18:43
westonruter added a commit that referenced this pull request Dec 12, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
cla: yes Signed the Google CLA
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Namespace structure Add support for captions to the Gallery shortcode
5 participants