*/ abstract class Renderer implements RenderingInterface, Iterator { /** * Web Stories stylesheet handle. */ public const STYLE_HANDLE = 'web-stories-list-styles'; /** * Web Stories stylesheet handle. */ public const LIGHTBOX_SCRIPT_HANDLE = 'web-stories-lightbox'; /** * Number of instances invoked. Kept it static to keep track. */ protected static int $instances = 0; /** * Assets instance. * * @var Assets Assets instance. */ protected Assets $assets; /** * Context instance. * * @var Context Context instance. */ protected Context $context; /** * Object ID for the Renderer class. * To enable support for multiple carousels and lightboxes * on the same page, we needed to identify each Renderer instance. * * This variable is used to add appropriate class to the Web Stories * wrapper. */ protected int $instance_id = 0; /** * Story_Query instance. * * @var Story_Query Story_Query instance. */ protected Story_Query $query; /** * Story attributes * * @var array An array of story attributes. * @phpstan-var StoryAttributes */ protected array $attributes = []; /** * Story posts. * * @var Story[] An array of story posts. */ protected array $stories = []; /** * Holds required html for the lightbox. * * @var string A string of lightbox markup. */ protected string $lightbox_html = ''; /** * Pointer to iterate over stories. */ private int $position = 0; /** * Height for displaying story. */ protected int $height = 308; /** * Width for displaying story. */ protected int $width = 185; /** * Whether content overlay is enabled for story. */ protected bool $content_overlay; /** * Constructor * * @since 1.5.0 * * @param Story_Query $query Story_Query instance. */ public function __construct( Story_Query $query ) { $this->query = $query; $this->attributes = $this->query->get_story_attributes(); $this->content_overlay = $this->attributes['show_title'] || $this->attributes['show_date'] || $this->attributes['show_author'] || $this->attributes['show_excerpt']; // TODO, find a way to inject this a cleaner way. $injector = Services::get_injector(); /** * Assets instance. * * @var Assets $assets Assets instance. */ $assets = $injector->make( Assets::class ); /** * Context instance. * * @var Context $context Context instance. */ $context = $injector->make( Context::class ); $this->assets = $assets; $this->context = $context; } /** * Output markup for amp stories. * * @since 1.5.0 * * @param array $args Array of rendering arguments. * @return string */ public function render( array $args = [] ): string { ++self::$instances; $this->instance_id = self::$instances; foreach ( $args as $key => $val ) { if ( property_exists( $this, $key ) ) { $this->{$key} = $val; } } return ''; } /** * Retrieve current story. * * @since 1.5.0 * * @return Story|null */ public function current(): ?Story { return $this->stories[ $this->position ] ?? null; } /** * Retrieve next story. * * @since 1.5.0 * * @return void */ public function next(): void { ++$this->position; } /** * Retrieve the key for current node in list. * * @since 1.5.0 * * @return bool|float|int|string|void|null */ #[\ReturnTypeWillChange] public function key() { return $this->position; } /** * Check if current position is valid. * * @since 1.5.0 * * @return bool */ public function valid(): bool { return isset( $this->stories[ $this->position ] ); } /** * Reset pointer to start of the list. * * @since 1.5.0 * * @return void */ public function rewind(): void { $this->position = 0; } /** * Perform initial setup for object. * * @since 1.5.0 * * @return void */ public function init(): void { $this->stories = array_filter( array_map( [ $this, 'prepare_stories' ], $this->query->get_stories() ) ); add_action( 'wp_footer', [ $this, 'render_stories_lightbox' ] ); add_action( 'amp_post_template_footer', [ $this, 'render_stories_lightbox' ] ); } /** * Initializes renderer functionality. * * @since 1.5.0 * * @return void */ public function load_assets(): void { if ( wp_style_is( self::STYLE_HANDLE, 'registered' ) ) { return; } // Web Stories styles for AMP and non-AMP pages. $this->assets->register_style_asset( self::STYLE_HANDLE ); // Web Stories lightbox script. $this->assets->register_script_asset( self::LIGHTBOX_SCRIPT_HANDLE, [ AMP_Story_Player_Assets::SCRIPT_HANDLE ] ); if ( \defined( 'AMPFORWP_VERSION' ) ) { add_action( 'amp_post_template_css', [ $this, 'add_amp_post_template_css' ] ); } } /** * Prints required inline CSS when using the AMP for WP plugin. * * @since 1.13.0 * * @return void */ public function add_amp_post_template_css(): void { $path = $this->assets->get_base_path( sprintf( 'assets/css/%s%s.css', self::STYLE_HANDLE, is_rtl() ? '-rtl' : '' ) ); if ( is_readable( $path ) ) { $css = file_get_contents( $path ); // phpcs:ignore WordPressVIPMinimum.Performance.FetchingRemoteData.FileGetContentsUnknown echo $css; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } } /** * Returns story item data. * * @SuppressWarnings(PHPMD.NPathComplexity) * * @since 1.5.0 * * @param object $post Array of post objects. * @return Story|null Story object or null if given post */ public function prepare_stories( $post ): ?Story { if ( ! $post instanceof WP_Post ) { return null; } $story_data = []; // TODO: get from field state instead. if ( ! $this->is_view_type( 'circles' ) ) { if ( isset( $this->attributes['show_author'] ) && true === $this->attributes['show_author'] ) { $author_id = absint( get_post_field( 'post_author', $post->ID ) ); $story_data['author'] = get_the_author_meta( 'display_name', $author_id ); } if ( isset( $this->attributes['show_date'] ) && true === $this->attributes['show_date'] ) { /* translators: Date format, see https://www.php.net/manual/en/datetime.format.php */ $story_data['date'] = get_the_date( __( 'M j, Y', 'web-stories' ), $post->ID ); } } $story_data['classes'] = $this->get_single_story_classes(); $story = new Story( $story_data ); $story->load_from_post( $post ); return $story; } /** * Render story markup. * * @since 1.5.0 * * @return void */ public function render_single_story_content(): void { /** * Story object. * * @var Story $story */ $story = $this->current(); $single_story_classes = $this->get_single_story_classes(); $lightbox_state = 'lightbox' . $story->get_id() . $this->instance_id; // No need to load these styles on admin as editor styles are being loaded by the block. if ( ! is_admin() || ( \defined( 'IFRAME_REQUEST' ) && IFRAME_REQUEST ) ) { // Web Stories Styles for AMP and non-AMP pages. $this->assets->enqueue_style_asset( self::STYLE_HANDLE ); } if ( $this->context->is_amp() ) { ?>
render_story_with_poster(); ?>
assets->enqueue_style( AMP_Story_Player_Assets::SCRIPT_HANDLE ); $this->assets->enqueue_script( AMP_Story_Player_Assets::SCRIPT_HANDLE ); $this->assets->enqueue_script_asset( self::LIGHTBOX_SCRIPT_HANDLE ); ?>
render_story_with_poster(); ?>
[ [ 'name' => 'close', 'position' => 'start', ], [ 'name' => 'skip-next', ], ], 'behavior' => [ 'autoplay' => false, ], ]; ?>
lightbox_html ); ?>
lightbox_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Generated with properly escaped data. } /** * Renders stories lightbox on 'wp_footer'. * * @return void */ public function render_stories_lightbox(): void { // Return if we don't have anything to render. if ( empty( $this->lightbox_html ) ) { return; } ?>
context->is_amp() ) { $this->render_stories_with_lightbox_amp(); } else { $this->render_stories_with_lightbox(); } ?>
attributes['view_type'] ) && $view_type === $this->attributes['view_type'] ); } /** * Get view type for stories. * * @since 1.5.0 * * @return string */ protected function get_view_type(): string { return ! empty( $this->attributes['view_type'] ) ? $this->attributes['view_type'] : 'circles'; } /** * Renders stories archive link if the 'show_archive_link' attribute is set to true. * * @since 1.5.0 * * @return void */ protected function maybe_render_archive_link(): void { if ( empty( $this->attributes['show_archive_link'] ) || true !== $this->attributes['show_archive_link'] || empty( $this->attributes['archive_link_label'] ) ) { return; } $web_stories_archive = get_post_type_archive_link( Story_Post_Type::POST_TYPE_SLUG ); if ( empty( $web_stories_archive ) ) { return; } ?> attributes['view_type'] ) ? sprintf( 'is-view-type-%1$s', $this->attributes['view_type'] ) : 'is-view-type-circles'; if ( $this->is_view_type( 'grid' ) && ! empty( $this->attributes['number_of_columns'] ) ) { $view_classes[] = sprintf( 'columns-%1$d', $this->attributes['number_of_columns'] ); } if ( ! $this->is_view_type( 'circles' ) && ! empty( $this->attributes['sharp_corners'] ) ) { $view_classes[] = 'is-style-squared'; } else { $view_classes[] = 'is-style-default'; } if ( $this->is_view_type( 'circles' ) && ! empty( $this->attributes['show_title'] ) ) { $view_classes[] = 'has-title'; } if ( $this->is_view_type( 'circles' ) || $this->is_view_type( 'carousel' ) ) { $view_classes[] = 'is-carousel'; } return implode( ' ', $view_classes ); } /** * Gets the classes for renderer container. * * @since 1.5.0 * * @return string */ protected function get_container_classes(): string { $container_classes = []; $container_classes[] = 'web-stories-list'; $container_classes[] = ! empty( $this->attributes['align'] ) ? sprintf( 'align%1$s', $this->attributes['align'] ) : 'alignnone'; $container_classes[] = ! empty( $this->attributes['class'] ) ? $this->attributes['class'] : ''; if ( ! empty( $this->attributes['show_archive_link'] ) ) { $container_classes[] = 'has-archive-link'; } $container_classes = array_filter( $container_classes ); $view_type_classes = $this->get_view_classes(); return sprintf( '%1$s %2$s', implode( ' ', $container_classes ), $view_type_classes ); } /** * Gets the single story container classes. * * @since 1.5.0 * * @return string */ protected function get_single_story_classes(): string { $single_story_classes = []; $single_story_classes[] = 'web-stories-list__story'; if ( $this->context->is_amp() ) { $single_story_classes[] = 'web-stories-list__story--amp'; } if ( ! empty( $this->attributes['image_alignment'] ) && ( 'right' === $this->attributes['image_alignment'] ) ) { $single_story_classes[] = 'image-align-right'; } $classes = implode( ' ', $single_story_classes ); /** * Filters the web stories renderer single story classes. * * @since 1.5.0 * * @param string $classes Single story classes. */ return apply_filters( 'web_stories_renderer_single_story_classes', $classes ); } /** * Gets the single story container styles. * * @since 1.5.0 * * @return string Style string. */ protected function get_container_styles(): string { $story_styles = ! empty( $this->attributes['circle_size'] ) && $this->is_view_type( 'circles' ) ? sprintf( '--ws-circle-size:%1$dpx', $this->attributes['circle_size'] ) : ''; $story_styles .= $this->is_view_type( 'carousel' ) ? sprintf( '--ws-story-max-width:%1$dpx', $this->width ) : ''; /** * Filters the web stories renderer single story classes. * * @since 1.5.0 * * @param string $story_styles Single story classes. */ return apply_filters( 'web_stories_renderer_container_styles', $story_styles ); } /** * Renders a story with story's poster image. * * @since 1.5.0 * * @return void */ protected function render_story_with_poster(): void { /** * Story object. * * @var Story $story */ $story = $this->current(); $poster_url = $story->get_poster_portrait(); $poster_srcset = $story->get_poster_srcset(); $poster_sizes = $story->get_poster_sizes(); if ( ! $poster_url ) { ?>
render_link_attributes(); ?>> get_title() ); ?>
render_link_attributes(); ?>> <?php echo esc_attr( $story->get_title() ); ?> srcset="" sizes="" loading="lazy" decoding="async" >
get_content_overlay(); if ( ! $this->context->is_amp() ) { $this->generate_lightbox_html( $story ); } else { $this->generate_amp_lightbox_html_amp( $story ); } } /** * Render additional link attributes. * * Allows customization of html attributes in the web stories widget anchor tag loop * Converts array into escaped inline html attributes. * * @since 1.17.0 * * @return void */ protected function render_link_attributes(): void { /** * The current story. * * @var Story $story */ $story = $this->current(); /** * Filters the link attributes added to a story's tag. * * @since 1.17.0 * * @param array $attributes Key value array of attribute name to attribute value. * @param Story $story The current story instance. * @param int $position The current story's position within the list. * @param string $view_type The current view type. */ $attributes = apply_filters( 'web_stories_renderer_link_attributes', [], $story, $this->position, $this->get_view_type() ); $attrs = []; if ( ! empty( $attributes ) ) { foreach ( $attributes as $attribute => $value ) { $attrs[] = wp_kses_one_attr( $attribute . '="' . esc_attr( $value ) . '"', 'a' ); } } $attrs = array_filter( $attrs ); // Filter out empty values rejected by KSES. //phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped echo implode( ' ', $attrs ); } /** * Renders the content overlay markup. * * @since 1.5.0 * * @return void */ protected function get_content_overlay(): void { /** * Story object. * * @var Story $story */ $story = $this->current(); if ( empty( $this->content_overlay ) ) { return; } ?>
attributes['show_title'] ) ) { ?>
get_title() ); ?>
attributes['show_excerpt'] ) ) { ?>
get_excerpt() ); ?>
get_author() ) ) { ?>
get_author() ) ); ?>
get_date() ) ) { ?>
render_link_attributes(); ?>>get_title() ); ?> lightbox_html .= ob_get_clean(); } /** * Markup for the lightbox used on AMP pages. * * @since 1.5.0 * * @param Story $story Current Story. * @return void */ protected function generate_amp_lightbox_html_amp( $story ): void { // Start collecting markup for the lightbox stories. This way we don't have to re-run the loop. ob_start(); $lightbox_state = 'lightbox' . $story->get_id() . $this->instance_id; $lightbox_id = 'lightbox-' . $story->get_id() . $this->instance_id; ?> lightbox_html .= ob_get_clean(); } }