diff --git a/assets/css/amp-playlist-shortcode.css b/assets/css/amp-playlist-shortcode.css new file mode 100644 index 00000000000..3d8de1c254d --- /dev/null +++ b/assets/css/amp-playlist-shortcode.css @@ -0,0 +1,19 @@ +/** +* For the custom AMP implementation of the 'playlist' shortcode. +*/ +.wp-playlist .wp-playlist-current-item img { + margin-right: 0; +} + +.wp-playlist .wp-playlist-current-item amp-img { + float: left; + margin-right: 10px; +} + +.wp-playlist audio { + display: block; +} + +.wp-playlist .amp-carousel-button { + visibility: hidden; +} diff --git a/includes/amp-helper-functions.php b/includes/amp-helper-functions.php index d8c4690568c..d22d49cec8b 100644 --- a/includes/amp-helper-functions.php +++ b/includes/amp-helper-functions.php @@ -260,6 +260,7 @@ function amp_get_content_embed_handlers( $post = null ) { 'AMP_Vine_Embed_Handler' => array(), 'AMP_Facebook_Embed_Handler' => array(), 'AMP_Pinterest_Embed_Handler' => array(), + 'AMP_Playlist_Embed_Handler' => array(), 'AMP_Reddit_Embed_Handler' => array(), 'AMP_Tumblr_Embed_Handler' => array(), 'AMP_Gallery_Embed_Handler' => array(), diff --git a/includes/class-amp-autoloader.php b/includes/class-amp-autoloader.php index 7bf991c8d54..3ec923b7bde 100644 --- a/includes/class-amp-autoloader.php +++ b/includes/class-amp-autoloader.php @@ -42,6 +42,7 @@ class AMP_Autoloader { 'AMP_Issuu_Embed_Handler' => 'includes/embeds/class-amp-issuu-embed-handler', 'AMP_Meetup_Embed_Handler' => 'includes/embeds/class-amp-meetup-embed-handler', 'AMP_Pinterest_Embed_Handler' => 'includes/embeds/class-amp-pinterest-embed', + 'AMP_Playlist_Embed_Handler' => 'includes/embeds/class-amp-playlist-embed-handler', 'AMP_Reddit_Embed_Handler' => 'includes/embeds/class-amp-reddit-embed-handler', 'AMP_SoundCloud_Embed_Handler' => 'includes/embeds/class-amp-soundcloud-embed', 'AMP_Tumblr_Embed_Handler' => 'includes/embeds/class-amp-tumblr-embed-handler', diff --git a/includes/class-amp-theme-support.php b/includes/class-amp-theme-support.php index acda180917f..be241300086 100644 --- a/includes/class-amp-theme-support.php +++ b/includes/class-amp-theme-support.php @@ -146,9 +146,8 @@ public static function register_hooks() { // Remove core actions which are invalid AMP. remove_action( 'wp_head', 'wp_post_preview_js', 1 ); remove_action( 'wp_head', 'print_emoji_detection_script', 7 ); - remove_action( 'wp_head', 'wp_print_head_scripts', 9 ); - remove_action( 'wp_footer', 'wp_print_footer_scripts', 20 ); remove_action( 'wp_print_styles', 'print_emoji_styles' ); + remove_action( 'wp_head', 'wp_oembed_add_host_js' ); /* * Add additional markup required by AMP . @@ -183,6 +182,7 @@ public static function register_hooks() { add_filter( 'comment_reply_link', array( __CLASS__, 'filter_comment_reply_link' ), 10, 4 ); add_filter( 'cancel_comment_reply_link', array( __CLASS__, 'filter_cancel_comment_reply_link' ), 10, 3 ); add_action( 'comment_form', array( __CLASS__, 'add_amp_comment_form_templates' ), 100 ); + remove_action( 'comment_form', 'wp_comment_form_unfiltered_html_nonce' ); // @todo Add character conversion. } diff --git a/includes/embeds/class-amp-playlist-embed-handler.php b/includes/embeds/class-amp-playlist-embed-handler.php new file mode 100644 index 00000000000..32eea3d9322 --- /dev/null +++ b/includes/embeds/class-amp-playlist-embed-handler.php @@ -0,0 +1,324 @@ +(.+?):s'; + + /** + * The ID of individual playlist. + * + * @var int + */ + public static $playlist_id = 0; + + /** + * The removed shortcode callback. + * + * @var callable + */ + public $removed_shortcode_callback; + + /** + * Registers the playlist shortcode. + * + * @global array $shortcode_tags + * @return void + */ + public function register_embed() { + global $shortcode_tags; + if ( shortcode_exists( self::SHORTCODE ) ) { + $this->removed_shortcode_callback = $shortcode_tags[ self::SHORTCODE ]; + } + add_shortcode( self::SHORTCODE, array( $this, 'shortcode' ) ); + remove_action( 'wp_playlist_scripts', 'wp_playlist_scripts' ); + } + + /** + * Unregisters the playlist shortcode. + * + * @return void + */ + public function unregister_embed() { + if ( $this->removed_shortcode_callback ) { + add_shortcode( self::SHORTCODE, $this->removed_shortcode_callback ); + $this->removed_shortcode_callback = null; + } + add_action( 'wp_playlist_scripts', 'wp_playlist_scripts' ); + } + + /** + * Enqueues the playlist styling. + * + * @return void + */ + public function enqueue_styles() { + wp_enqueue_style( + 'amp-playlist-shortcode', + amp_get_asset_url( 'css/amp-playlist-shortcode.css' ), + array( 'wp-mediaelement' ), + AMP__VERSION + ); + } + + /** + * Gets AMP-compliant markup for the playlist shortcode. + * + * Uses the JSON that wp_playlist_shortcode() produces. + * Gets the markup, based on the type of playlist. + * + * @param array $attr The playlist attributes. + * @return string Playlist shortcode markup. + */ + public function shortcode( $attr ) { + $data = $this->get_data( $attr ); + if ( isset( $data['type'] ) && ( 'audio' === $data['type'] ) ) { + return $this->audio_playlist( $data ); + } elseif ( isset( $data['type'] ) && ( 'video' === $data['type'] ) ) { + return $this->video_playlist( $data ); + } + } + + /** + * Gets an AMP-compliant audio playlist. + * + * @param array $data Data. + * @return string Playlist shortcode markup, or an empty string. + */ + public function audio_playlist( $data ) { + if ( ! isset( $data['tracks'] ) ) { + return ''; + } + self::$playlist_id++; + $container_id = 'wpPlaylist' . self::$playlist_id . 'Carousel'; + $state_id = 'wpPlaylist' . self::$playlist_id; + $amp_state = array( + 'selectedIndex' => 0, + ); + + $this->enqueue_styles(); + ob_start(); + ?> +
+ + + + + get_title( $track ); + $image_url = isset( $track['thumb']['src'] ) ? $track['thumb']['src'] : ''; + $dimensions = $this->get_thumb_dimensions( $track ); + ?> +
+
+ + + +
+ +
+
+ +
+ +
+ print_tracks( $state_id, $data['tracks'] ); ?> +
+ 0, + ); + foreach ( $data['tracks'] as $index => $track ) { + $amp_state[ $index ] = array( + 'videoUrl' => $track['src'], + 'thumb' => isset( $track['thumb']['src'] ) ? $track['thumb']['src'] : '', + ); + } + + $dimensions = isset( $data['tracks'][0]['dimensions']['resized'] ) ? $data['tracks'][0]['dimensions']['resized'] : null; + $width = isset( $dimensions['width'] ) ? $dimensions['width'] : $content_width; + $height = isset( $dimensions['height'] ) ? $dimensions['height'] : null; + $src_bound = sprintf( '%s[%s.selectedIndex].videoUrl', $state_id, $state_id ); + + $this->enqueue_styles(); + ob_start(); + ?> +
+ + + + + print_tracks( $state_id, $data['tracks'] ); ?> +
+ self::THUMB_MAX_WIDTH ) { + $ratio = $original_width / self::THUMB_MAX_WIDTH; + $height = intval( $original_height / $ratio ); + } else { + $height = $original_height; + } + $width = min( self::THUMB_MAX_WIDTH, $original_width ); + return compact( 'height', 'width' ); + } + + /** + * Outputs the playlist tracks, based on the type of playlist. + * + * These typically appear below the player. + * Clicking a track triggers the player to appear with its src. + * + * @param string $state_id The ID of the container. + * @param array $tracks Tracks. + * @return void + */ + public function print_tracks( $state_id, $tracks ) { + ?> +
+ $track ) : ?> + array( 'selectedIndex' => $index ) ) ) . ')'; + $initial_class = 0 === $index ? 'wp-playlist-item wp-playlist-playing' : 'wp-playlist-item'; + $bound_class = sprintf( '%d == %s.selectedIndex ? "wp-playlist-item wp-playlist-playing" : "wp-playlist-item"', $index, $state_id ); + ?> + + +
+ instance = new AMP_Playlist_Embed_Handler(); + } + + /** + * Tear down test. + * + * @global WP_Styles $wp_styles + */ + public function tearDown() { + global $wp_styles; + $wp_styles = null; + + AMP_Playlist_Embed_Handler::$playlist_id = 0; + } + + /** + * Test register_embed. + * + * @covers AMP_Playlist_Embed_Handler::register_embed() + */ + public function test_register_embed() { + global $shortcode_tags; + $removed_shortcode = 'wp_playlist_shortcode'; + add_shortcode( 'playlist', $removed_shortcode ); + $this->instance->register_embed(); + $this->assertEquals( 'AMP_Playlist_Embed_Handler', get_class( $shortcode_tags[ AMP_Playlist_Embed_Handler::SHORTCODE ][0] ) ); + $this->assertEquals( 'shortcode', $shortcode_tags[ AMP_Playlist_Embed_Handler::SHORTCODE ][1] ); + $this->assertEquals( $removed_shortcode, $this->instance->removed_shortcode_callback ); + $this->instance->unregister_embed(); + } + + /** + * Test unregister_embed. + * + * @covers AMP_Playlist_Embed_Handler::unregister_embed() + */ + public function test_unregister_embed() { + global $shortcode_tags; + $expected_removed_shortcode = 'wp_playlist_shortcode'; + $this->instance->removed_shortcode_callback = $expected_removed_shortcode; + $this->instance->unregister_embed(); + $this->assertEquals( $expected_removed_shortcode, $shortcode_tags[ AMP_Playlist_Embed_Handler::SHORTCODE ] ); + } + + /** + * Test styling. + * + * @covers AMP_Playlist_Embed_Handler::enqueue_styles() + */ + public function test_styling() { + global $post; + $playlist_shortcode = 'amp-playlist-shortcode'; + $this->instance->register_embed(); + $this->assertFalse( in_array( 'wp-mediaelement', wp_styles()->queue, true ) ); + $this->assertFalse( in_array( $playlist_shortcode, wp_styles()->queue, true ) ); + + $post = $this->factory()->post->create_and_get(); // WPCS: global override OK. + $post->post_content = '[playlist ids="5,3"]'; + $this->instance->enqueue_styles(); + $style = wp_styles()->registered[ $playlist_shortcode ]; + $this->assertContains( $playlist_shortcode, wp_styles()->queue ); + $this->assertEquals( array( 'wp-mediaelement' ), $style->deps ); + $this->assertEquals( $playlist_shortcode, $style->handle ); + $this->assertEquals( amp_get_asset_url( 'css/amp-playlist-shortcode.css' ), $style->src ); + $this->assertEquals( AMP__VERSION, $style->ver ); + } + + /** + * Test shortcode. + * + * @covers AMP_Playlist_Embed_Handler::shortcode() + * @covers AMP_Playlist_Embed_Handler::video_playlist() + */ + public function test_shortcode() { + $attr = $this->get_attributes( 'video' ); + $playlist = $this->instance->shortcode( $attr ); + $this->assertContains( 'assertContains( 'assertContains( $this->file_1, $playlist ); + $this->assertContains( $this->file_2, $playlist ); + } + + /** + * Test video_playlist. + * + * @covers AMP_Playlist_Embed_Handler::video_playlist() + */ + public function test_video_playlist() { + $attr = $this->get_attributes( 'video' ); + $data = $this->instance->get_data( $attr ); + $playlist = $this->instance->video_playlist( $data ); + $this->assertContains( 'assertContains( 'assertContains( $this->file_1, $playlist ); + $this->assertContains( $this->file_2, $playlist ); + $this->assertContains( '[src]="wpPlaylist1[wpPlaylist1.selectedIndex].videoUrl"', $playlist ); + $this->assertContains( 'on="tap:AMP.setState({"wpPlaylist1":{"selectedIndex":0}})"', $playlist ); + } + + /** + * Test get_thumb_dimensions. + * + * @covers AMP_Playlist_Embed_Handler::get_thumb_dimensions() + */ + public function test_get_thumb_dimensions() { + $dimensions = array( + 'height' => 60, + 'width' => 60, + ); + $track = array( + 'thumb' => $dimensions, + ); + $this->assertEquals( $dimensions, $this->instance->get_thumb_dimensions( $track ) ); + + $dimensions = array( + 'height' => 68, + 'width' => 59, + ); + $track = array( + 'thumb' => $dimensions, + ); + $this->assertEquals( $dimensions, $this->instance->get_thumb_dimensions( $track ) ); + + $dimensions = array( + 'height' => 70, + 'width' => 80.5, + ); + $expected_dimensions = array( + 'height' => 52, + 'width' => 60, + ); + $track = array( + 'thumb' => $dimensions, + ); + $this->assertEquals( $expected_dimensions, $this->instance->get_thumb_dimensions( $track ) ); + + $dimensions = array( + 'width' => 80.5, + ); + $track = array( + 'thumb' => $dimensions, + ); + $expected_dimensions = array( + 'height' => 48, + 'width' => 60, + ); + $this->assertEquals( $expected_dimensions, $this->instance->get_thumb_dimensions( $track ) ); + + $track = array( + 'thumb' => array(), + ); + $expected_dimensions = array( + 'height' => AMP_Playlist_Embed_Handler::DEFAULT_THUMB_HEIGHT, + 'width' => AMP_Playlist_Embed_Handler::DEFAULT_THUMB_WIDTH, + ); + $this->assertEquals( $expected_dimensions, $this->instance->get_thumb_dimensions( $track ) ); + } + + /** + * Test audio_playlist. + * + * Logic for creating the videos copied from Tests_Media. + * + * @covers AMP_Playlist_Embed_Handler::audio_playlist() + */ + public function test_audio_playlist() { + $attr = $this->get_attributes( 'audio' ); + $playlist = $this->instance->audio_playlist( array() ); + $this->assertEquals( '', $playlist ); + + $data = $this->instance->get_data( $attr ); + $playlist = $this->instance->audio_playlist( $data ); + $this->assertContains( 'assertContains( 'assertContains( $this->file_1, $playlist ); + $this->assertContains( $this->file_2, $playlist ); + $this->assertContains( 'tap:AMP.setState({"wpPlaylist1":{"selectedIndex":0}})"', $playlist ); + } + + /** + * Test tracks. + * + * @covers AMP_Playlist_Embed_Handler::print_tracks() + */ + public function test_tracks() { + $type = 'video'; + $attr = $this->get_attributes( $type ); + $data = $this->instance->get_data( $attr ); + $container_id = 'fooContainerId1'; + $state_id = 'fooId1'; + $expected_on = 'tap:AMP.setState({"' . $state_id . '":{"selectedIndex":0}})'; + + ob_start(); + $this->instance->print_tracks( $state_id, $data['tracks'] ); + $tracks = ob_get_clean(); + $this->assertContains( '
', $tracks ); + $this->assertContains( $state_id, $tracks ); + $this->assertContains( $expected_on, $tracks ); + + $attr = $this->get_attributes( $type ); + $data = $this->instance->get_data( $attr ); + $expected_on = 'tap:AMP.setState({"' . $state_id . '":{"selectedIndex":0}})'; + + ob_start(); + $this->instance->print_tracks( $state_id, $data['tracks'] ); + $tracks = ob_get_clean(); + $this->assertContains( $expected_on, $tracks ); + } + + /** + * Test get_data. + * + * @covers AMP_Playlist_Embed_Handler::get_data() + */ + public function test_get_data() { + $type = 'audio'; + $data = $this->instance->get_data( $this->get_attributes( $type ) ); + $this->assertEquals( $type, $data['type'] ); + $this->assertContains( $this->file_1, $data['tracks'][0]['src'] ); + $this->assertContains( $this->file_2, $data['tracks'][1]['src'] ); + } + + /** + * Test get_title. + * + * @covers AMP_Playlist_Embed_Handler::get_data() + */ + public function test_get_title() { + $caption = 'Example caption'; + $title = 'Media Title'; + $track = array( + 'caption' => $caption, + ); + + $this->assertEquals( $caption, $this->instance->get_title( $track ) ); + + $track = array( + 'title' => $title, + ); + $this->assertEquals( $title, $this->instance->get_title( $track ) ); + + $track = array( + 'caption' => $caption, + 'title' => $title, + ); + $this->assertEquals( $caption, $this->instance->get_title( $track ) ); + $this->assertEquals( null, $this->instance->get_title( array() ) ); + } + + /** + * Gets the shortcode attributes. + * + * @param string $type The type of shortcode attributes: 'audio' or 'video'. + * @return array $attrs The shortcode attributes. + */ + public function get_attributes( $type ) { + if ( 'audio' === $type ) { + $this->file_1 = 'example-audio-1.mp3'; + $this->file_2 = 'example-audio-2.mp3'; + $mime_type = 'audio/mp3'; + } elseif ( 'video' === $type ) { + $this->file_1 = 'example-video-1.mp4'; + $this->file_2 = 'example-video-2.mkv'; + $mime_type = 'video/mp4'; + } else { + return; + } + + $files = array( + $this->file_1, + $this->file_2, + ); + $ids = $this->get_file_ids( $files, $mime_type ); + return array( + 'ids' => implode( ',', $ids ), + 'type' => $type, + ); + } + + /** + * Gets test file IDs. + * + * @param array $files The file names to create. + * @param string $mime_type The type of file. + * @return array $ids The IDs of the test files. + */ + public function get_file_ids( $files, $mime_type ) { + $ids = array(); + foreach ( $files as $file ) { + $ids[] = $this->factory()->attachment->create_object( + $file, + 0, + array( + 'post_mime_type' => $mime_type, + 'post_type' => 'attachment', + ) + ); + } + return $ids; + } + +}