Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
99.09% |
109 / 110 |
|
83.33% |
5 / 6 |
CRAP | |
0.00% |
0 / 1 |
| Playlist_Block_Email_Renderer | |
99.09% |
109 / 110 |
|
83.33% |
5 / 6 |
44 | |
0.00% |
0 / 1 |
| render | |
100.00% |
36 / 36 |
|
100.00% |
1 / 1 |
14 | |||
| render_latest_videos | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
7 | |||
| video_details | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
12 | |||
| render_first_video | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
2 | |||
| render_grid | |
100.00% |
33 / 33 |
|
100.00% |
1 / 1 |
4 | |||
| layout_width | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
5.05 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * VideoPress playlist block email renderer class. |
| 4 | * |
| 5 | * @package automattic/jetpack-videopress |
| 6 | */ |
| 7 | |
| 8 | namespace Automattic\Jetpack\VideoPress; |
| 9 | |
| 10 | /** |
| 11 | * Handles rendering VideoPress playlist blocks for email contexts. |
| 12 | * |
| 13 | * Emails cannot play a playlist, so the first video is shown as a poster with a |
| 14 | * play button and the next ones as a grid of thumbnails, all linking to the post. |
| 15 | */ |
| 16 | class Playlist_Block_Email_Renderer { |
| 17 | |
| 18 | /** |
| 19 | * Thumbnails per grid row. |
| 20 | * |
| 21 | * @var int |
| 22 | */ |
| 23 | const GRID_COLUMNS = 3; |
| 24 | |
| 25 | /** |
| 26 | * Thumbnails shown under the first video before the "more videos" link. |
| 27 | * |
| 28 | * @var int |
| 29 | */ |
| 30 | const GRID_MAX_VIDEOS = 6; |
| 31 | |
| 32 | /** |
| 33 | * Padding around each grid thumbnail, in pixels. |
| 34 | * |
| 35 | * @var int |
| 36 | */ |
| 37 | const GRID_CELL_PADDING = 4; |
| 38 | |
| 39 | /** |
| 40 | * Render the Video Playlist block for email. |
| 41 | * |
| 42 | * @param string $block_content The block's front-end HTML, unused: email builds its own markup. |
| 43 | * @param array $parsed_block The parsed block data including attributes. |
| 44 | * @param object $rendering_context Email rendering context. |
| 45 | * |
| 46 | * @return string |
| 47 | */ |
| 48 | public static function render( $block_content, $parsed_block, $rendering_context ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable |
| 49 | if ( ! isset( $parsed_block['attrs'] ) || ! is_array( $parsed_block['attrs'] ) || |
| 50 | ! class_exists( '\Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks\Video' ) || |
| 51 | ! class_exists( '\Automattic\WooCommerce\EmailEditor\Integrations\Utils\Table_Wrapper_Helper' ) ) { |
| 52 | return ''; |
| 53 | } |
| 54 | |
| 55 | $attributes = $parsed_block['attrs']; |
| 56 | $entries = Initializer::sanitize_playlist_entries( $attributes['videos'] ?? null ); |
| 57 | $post_url = get_permalink(); |
| 58 | |
| 59 | if ( ! $entries || empty( $post_url ) ) { |
| 60 | return ''; |
| 61 | } |
| 62 | |
| 63 | $videos = array(); |
| 64 | foreach ( array_slice( $entries, 0, 1 + self::GRID_MAX_VIDEOS ) as $index => $entry ) { |
| 65 | $videos[] = self::video_details( $entry['guid'], $index + 1 ); |
| 66 | } |
| 67 | |
| 68 | $width = self::layout_width( $parsed_block, $rendering_context ); |
| 69 | |
| 70 | $title = isset( $attributes['playlistTitle'] ) && is_string( $attributes['playlistTitle'] ) ? trim( $attributes['playlistTitle'] ) : ''; |
| 71 | $html = (bool) ( $attributes['showPlaylistTitle'] ?? true ) && '' !== $title |
| 72 | ? sprintf( '<h2 style="margin: 0 0 16px;">%s</h2>', esc_html( $title ) ) |
| 73 | : ''; |
| 74 | $html .= self::render_first_video( $videos[0], $post_url, $rendering_context ); |
| 75 | |
| 76 | if ( count( $videos ) > 1 ) { |
| 77 | $html .= self::render_grid( array_slice( $videos, 1 ), $post_url, $width ); |
| 78 | } |
| 79 | |
| 80 | $hidden = count( $entries ) - count( $videos ); |
| 81 | if ( $hidden > 0 ) { |
| 82 | $html .= sprintf( |
| 83 | '<p style="margin: 8px 0 0; font-size: 14px;"><a href="%s" target="_blank" rel="noopener noreferrer">%s</a></p>', |
| 84 | esc_url( $post_url ), |
| 85 | /* translators: %d: number of playlist videos not shown in the email. */ |
| 86 | esc_html( sprintf( _n( '+ %d more video', '+ %d more videos', $hidden, 'jetpack-videopress-pkg' ), $hidden ) ) |
| 87 | ); |
| 88 | } |
| 89 | |
| 90 | $email_attrs = $parsed_block['email_attrs'] ?? array(); |
| 91 | $margin_style = (string) \WP_Style_Engine::compile_css( array_intersect_key( $email_attrs, array_flip( array( 'margin', 'margin-top' ) ) ), '' ); |
| 92 | |
| 93 | // @phan-suppress-next-line PhanUndeclaredClassMethod -- Optional WooCommerce dependency, checked with class_exists() above. |
| 94 | return \Automattic\WooCommerce\EmailEditor\Integrations\Utils\Table_Wrapper_Helper::render_table_wrapper( |
| 95 | $html, |
| 96 | array( |
| 97 | 'class' => 'videopress-playlist-email', |
| 98 | 'style' => ltrim( sprintf( '%s width: 100%%; max-width: %dpx; border-collapse: collapse;', $margin_style, $width ) ), |
| 99 | ) |
| 100 | ); |
| 101 | } |
| 102 | |
| 103 | /** |
| 104 | * Render the Latest Videos Playlist block for email: the site's videos, in the block's order, as a playlist. |
| 105 | * |
| 106 | * @param string $block_content The block's front-end HTML, unused: email builds its own markup. |
| 107 | * @param array $parsed_block The parsed block data including attributes. |
| 108 | * @param object $rendering_context Email rendering context. |
| 109 | * |
| 110 | * @return string |
| 111 | */ |
| 112 | public static function render_latest_videos( $block_content, $parsed_block, $rendering_context ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable |
| 113 | $attributes = isset( $parsed_block['attrs'] ) && is_array( $parsed_block['attrs'] ) ? $parsed_block['attrs'] : array(); |
| 114 | $query = Initializer::latest_videos_playlist_query( $attributes ); |
| 115 | |
| 116 | // The same source as the front end, which has no current video to exclude on a post. |
| 117 | $is_browsed = 'newest' !== $query['order'] || $query['paginated'] || ! empty( $attributes['showBrowseControls'] ) || ! empty( $attributes['showMeta'] ); |
| 118 | |
| 119 | // Readers land on the post's first page with the block's own order, so ignore $_GET. |
| 120 | $entries = $is_browsed |
| 121 | ? Channel_Data::browse( Channel_Data::browse_request( false, false, $query['order'] ), $query['count'] )['entries'] |
| 122 | : Data::get_latest_videopress_playlist_entries( $query['count'] ); |
| 123 | |
| 124 | $parsed_block['attrs'] = $attributes; |
| 125 | $parsed_block['attrs']['videos'] = $entries; |
| 126 | |
| 127 | return self::render( '', $parsed_block, $rendering_context ); |
| 128 | } |
| 129 | |
| 130 | /** |
| 131 | * A video's title and poster, as anyone may see them. |
| 132 | * |
| 133 | * Private videos get no poster and keep their positional title, as on the |
| 134 | * front end for a viewer who is not allowed to watch them. |
| 135 | * |
| 136 | * @param string $guid Video GUID. |
| 137 | * @param int $position Position of the video in the playlist, from 1. |
| 138 | * |
| 139 | * @return array{title: string, poster: string} |
| 140 | */ |
| 141 | private static function video_details( $guid, $position ) { |
| 142 | $details = function_exists( 'videopress_get_video_details' ) ? videopress_get_video_details( $guid ) : null; |
| 143 | $details = is_object( $details ) && ! is_wp_error( $details ) && empty( $details->is_private ) ? $details : null; |
| 144 | |
| 145 | $title = $details && ! empty( $details->title ) && is_string( $details->title ) ? $details->title : ''; |
| 146 | $poster = $details && ! empty( $details->poster ) && is_string( $details->poster ) ? esc_url_raw( $details->poster ) : ''; |
| 147 | |
| 148 | return array( |
| 149 | /* translators: %d: position of the video in the playlist. */ |
| 150 | 'title' => '' !== $title ? $title : sprintf( __( 'Video %d', 'jetpack-videopress-pkg' ), $position ), |
| 151 | 'poster' => $poster, |
| 152 | ); |
| 153 | } |
| 154 | |
| 155 | /** |
| 156 | * The first video as a poster with a play button, or a link when it has no poster. |
| 157 | * |
| 158 | * @param array $video Video details. |
| 159 | * @param string $post_url Post URL. |
| 160 | * @param object $rendering_context Email rendering context. |
| 161 | * |
| 162 | * @return string |
| 163 | */ |
| 164 | private static function render_first_video( $video, $post_url, $rendering_context ) { |
| 165 | if ( '' !== $video['poster'] ) { |
| 166 | $video_block = array( |
| 167 | 'blockName' => 'core/video', |
| 168 | 'attrs' => array( |
| 169 | 'poster' => $video['poster'], |
| 170 | 'videoUrl' => $post_url, |
| 171 | ), |
| 172 | 'innerHTML' => '<figure class="wp-block-video"></figure>', |
| 173 | ); |
| 174 | |
| 175 | // @phan-suppress-next-line PhanUndeclaredClassMethod -- Optional WooCommerce dependency, checked with class_exists() in render(). |
| 176 | $video_renderer = new \Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks\Video(); |
| 177 | |
| 178 | // @phan-suppress-next-line PhanUndeclaredClassMethod -- Optional WooCommerce dependency, checked with class_exists() in render(). |
| 179 | return $video_renderer->render( $video_block['innerHTML'], $video_block, $rendering_context ); |
| 180 | } |
| 181 | |
| 182 | return sprintf( |
| 183 | '<p><a href="%s" target="_blank" rel="noopener noreferrer">%s</a></p>', |
| 184 | esc_url( $post_url ), |
| 185 | esc_html__( 'Visit the post to watch the video', 'jetpack-videopress-pkg' ) |
| 186 | ); |
| 187 | } |
| 188 | |
| 189 | /** |
| 190 | * The videos after the first as a table of linked thumbnails and titles. |
| 191 | * |
| 192 | * @param array $videos Video details. |
| 193 | * @param string $post_url Post URL. |
| 194 | * @param int $width Block width in pixels. |
| 195 | * |
| 196 | * @return string |
| 197 | */ |
| 198 | private static function render_grid( $videos, $post_url, $width ) { |
| 199 | $image_width = (int) floor( $width / self::GRID_COLUMNS ) - 2 * self::GRID_CELL_PADDING; |
| 200 | $cell_style = sprintf( |
| 201 | 'width: %.2F%%; padding: %dpx; vertical-align: top;', |
| 202 | 100 / self::GRID_COLUMNS, |
| 203 | self::GRID_CELL_PADDING |
| 204 | ); |
| 205 | |
| 206 | $rows = ''; |
| 207 | foreach ( array_chunk( $videos, self::GRID_COLUMNS ) as $row_videos ) { |
| 208 | $cells = ''; |
| 209 | foreach ( $row_videos as $video ) { |
| 210 | $thumbnail = '' !== $video['poster'] |
| 211 | ? sprintf( |
| 212 | '<img src="%1$s" alt="" width="%2$d" style="display: block; width: 100%%; max-width: %2$dpx; height: auto; margin-bottom: 4px; border: 0; border-radius: 4px;" />', |
| 213 | esc_url( $video['poster'] ), |
| 214 | $image_width |
| 215 | ) |
| 216 | : ''; |
| 217 | |
| 218 | $cells .= sprintf( |
| 219 | '<td style="%1$s"><a href="%2$s" target="_blank" rel="noopener noreferrer" style="display: block; font-size: 14px; line-height: 1.4;">%3$s%4$s</a></td>', |
| 220 | esc_attr( $cell_style ), |
| 221 | esc_url( $post_url ), |
| 222 | $thumbnail, |
| 223 | esc_html( $video['title'] ) |
| 224 | ); |
| 225 | } |
| 226 | |
| 227 | // Pad the last row so its cells keep the same width as the others. |
| 228 | $cells .= str_repeat( |
| 229 | sprintf( '<td style="%s"></td>', esc_attr( $cell_style ) ), |
| 230 | self::GRID_COLUMNS - count( $row_videos ) |
| 231 | ); |
| 232 | |
| 233 | $rows .= '<tr>' . $cells . '</tr>'; |
| 234 | } |
| 235 | |
| 236 | return sprintf( |
| 237 | '<table role="presentation" width="100%%" cellpadding="0" cellspacing="0" style="width: 100%%; margin-top: 16px; border-collapse: collapse; table-layout: fixed;">%s</table>', |
| 238 | $rows |
| 239 | ); |
| 240 | } |
| 241 | |
| 242 | /** |
| 243 | * The block's width in the email, e.g. narrower inside a column; else the content width, else 600px. |
| 244 | * |
| 245 | * @param array $parsed_block The parsed block data, with the email editor's email_attrs. |
| 246 | * @param object $rendering_context Email rendering context. |
| 247 | * |
| 248 | * @return int Width in pixels. |
| 249 | */ |
| 250 | private static function layout_width( $parsed_block, $rendering_context ) { |
| 251 | $block_width = (int) ( $parsed_block['email_attrs']['width'] ?? 0 ); |
| 252 | if ( $block_width > 0 ) { |
| 253 | return $block_width; |
| 254 | } |
| 255 | |
| 256 | if ( is_object( $rendering_context ) && method_exists( $rendering_context, 'get_layout_width_without_padding' ) ) { |
| 257 | $width = (int) $rendering_context->get_layout_width_without_padding(); |
| 258 | if ( $width > 0 ) { |
| 259 | return $width; |
| 260 | } |
| 261 | } |
| 262 | |
| 263 | return 600; |
| 264 | } |
| 265 | } |