registry = $registry; $this->schema = $schema; $this->store = $store; $this->assets = $assets; $this->cache = $cache; } /** * Renders a saved layout. * * @param int $id Layout post ID. * @return string */ public function render_saved( $id ) { $config = $id ? $this->store->get_config( $id ) : null; if ( ! $config ) { return $this->notice( __( 'Layout Now: this layout no longer exists.', 'layout-now' ) ); } return $this->render( $config, array( 'id' => 'lnow-' . $id, 'label' => get_the_title( $id ), ) ); } /** * Renders a config. * * @param array $config Raw or normalized config. * @param array $args { * Render arguments. * * @type string $id Base DOM ID. * @type string $label Accessible name for the layout region (defaults to "Posts"). * @type bool $preview Admin preview: skips enqueueing and current-post context. * } * @return string */ public function render( array $config, array $args = array() ) { $args = wp_parse_args( $args, array( 'id' => 'lnow', 'label' => '', 'preview' => false, ) ); $config = $this->schema->normalize( $config ); $layout = $this->registry->layout( $config['layout'] ); $source = $this->registry->source( $config['source'] ); $skin = $this->skin( $config['style']['skin'] ); if ( ! $layout || ! $source || ! $skin ) { return ''; } $layout_options = $config['layout_options'][ $layout->get_id() ]; $context = array( 'limit' => $layout->get_item_limit( $layout_options ), 'current_id' => $args['preview'] ? 0 : $this->current_id(), ); $items = $this->cache->get( $source, $config['source_options'][ $source->get_id() ], $context ); if ( ! $items ) { return $this->notice( __( 'Layout Now: no posts match this layout\'s content settings yet.', 'layout-now' ), $args['preview'] ); } if ( ! $args['preview'] ) { $this->assets->enqueue( $layout, $skin ); } $html = $layout->render( $items, new View( $config['style'], $layout_options, wp_strip_all_tags( (string) $args['label'] ) ) ); return sprintf( '%s', $this->attributes( $this->unique_id( $args['id'] ), $config['style'], $layout, $skin ), $html ); } /** * Resolves the skin, falling back to the first registered one. * * @param string $id Skin ID. * @return Skin|null */ public function skin( $id ) { $skin = $this->registry->skin( $id ); if ( ! $skin ) { $skins = $this->registry->skins(); $skin = $skins ? reset( $skins ) : null; } return $skin; } /** * Root element attributes. Stepped controls become data attributes the CSS maps to tokens; * "Skin default" values are resolved here so the CSS only deals with concrete steps. * * @param string $id DOM ID. * @param array $style Style settings. * @param LayoutInterface $layout Layout. * @param Skin $skin Skin. * @return string */ private function attributes( $id, array $style, LayoutInterface $layout, Skin $skin ) { $resolve = static function ( $key ) use ( $style, $skin ) { return 'auto' === $style[ $key ] && isset( $skin->defaults[ $key ] ) ? $skin->defaults[ $key ] : $style[ $key ]; }; $attrs = array( 'id' => $id, 'class' => trim( 'lnow lnow--' . $layout->get_id() . ( 'content' !== $style['align'] ? ' align' . $style['align'] : '' ) . ' ' . $style['css_class'] ), 'data-skin' => $skin->id, 'data-card' => $skin->card, 'data-radius' => $resolve( 'radius' ), 'data-spacing' => $resolve( 'spacing' ), 'data-hover' => $resolve( 'hover' ), 'data-ratio' => $style['ratio'], 'data-title' => $style['title_size'], 'data-font' => $style['font'], ); if ( $skin->modes ) { $attrs['data-mode'] = $style['mode']; } $vars = array(); if ( $style['accent'] ) { $vars[] = '--ln-accent:' . $style['accent']; $vars[] = '--ln-on-accent:' . self::contrast_color( $style['accent'] ); } if ( $style['max_width'] > 0 ) { $vars[] = '--ln-max:' . (int) $style['max_width'] . 'px'; } if ( $vars ) { $attrs['style'] = implode( ';', $vars ); } $out = ''; foreach ( $attrs as $name => $value ) { $out .= sprintf( ' %s="%s"', $name, esc_attr( $value ) ); } return $out; } /** * Black or white, whichever reads better on the given color (WCAG relative luminance). * * @param string $hex Hex color. * @return string */ public static function contrast_color( $hex ) { $hex = ltrim( $hex, '#' ); if ( 3 === strlen( $hex ) ) { $hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2]; } $channels = array_map( static function ( $pair ) { $c = hexdec( $pair ) / 255; return $c <= 0.03928 ? $c / 12.92 : pow( ( $c + 0.055 ) / 1.055, 2.4 ); }, str_split( $hex, 2 ) ); $luminance = 0.2126 * $channels[0] + 0.7152 * $channels[1] + 0.0722 * $channels[2]; return $luminance > 0.179 ? '#111111' : '#ffffff'; } /** * ID of the post being viewed. * * @return int */ private function current_id() { return is_singular() ? (int) get_queried_object_id() : 0; } /** * Unique DOM ID for this request. * * @param string $base Base ID. * @return string */ private function unique_id( $base ) { $base = sanitize_html_class( $base, 'lnow' ); if ( ! isset( $this->ids[ $base ] ) ) { $this->ids[ $base ] = 1; return $base; } return $base . '-' . ( ++$this->ids[ $base ] ); } /** * Message shown only to people who can manage layouts; visitors get nothing. * * @param string $message Message. * @param bool $force Show regardless of capability. * @return string */ private function notice( $message, $force = false ) { if ( ! $force && ! current_user_can( Plugin::capability() ) ) { return ''; } if ( ! $force ) { // The preview iframe loads base.css itself; on the site the notice needs it enqueued. $this->assets->enqueue_base(); } return '

' . esc_html( $message ) . '

'; } }