> */ private $card_stack = array(); /** * How many chatboxes without a key, by the key they'd share, this request * printed. * * @var array */ private $chatbox_keys = array(); /** * Constructor. * * @param Frontend $frontend Front-end printer. */ public function __construct( Frontend $frontend ) { $this->frontend = $frontend; } /** * Hooks. */ public function register(): void { add_shortcode( 'stand_button', array( $this, 'button' ) ); add_shortcode( 'stand_card', array( $this, 'card' ) ); add_shortcode( 'stand_chatbox', array( $this, 'chatbox' ) ); add_shortcode( 'stand_action', array( $this, 'card_child' ) ); add_shortcode( 'stand_visitor_message', array( $this, 'card_child' ) ); add_shortcode( 'stand_dismiss', array( $this, 'card_child' ) ); add_filter( 'the_content', array( self::class, 'unwrap_paragraphs' ), 12 ); add_filter( 'widget_text_content', array( self::class, 'unwrap_paragraphs' ), 12 ); } /** * [stand_button]. * * @param array|string $atts Attributes. * @param string|null $content Label. */ public function button( $atts, $content = null ): string { $atts = shortcode_atts( array( 'label' => '', 'show_when' => 'either', 'avatar' => 'yes', 'greeting' => '', 'prompt' => '', 'analytics_id' => '', 'class' => '', ), is_array( $atts ) ? $atts : array(), 'stand_button' ); $this->frontend->content_needs_widget(); $label = '' !== trim( (string) $content ) ? wp_strip_all_tags( (string) $content ) : $atts['label']; return Markup::button( array( 'label' => $label, 'showWhen' => $atts['show_when'], 'avatar' => self::flag( $atts['avatar'] ), 'greeting' => $atts['greeting'], 'prompt' => $atts['prompt'], 'analyticsId' => $atts['analytics_id'], ), self::wrapper( 'wp-block-stand-chat-stand-button', $atts['class'] ) ); } /** * [stand_card]. * * @param array|string $atts Attributes. * @param string|null $content Body with optional child shortcodes. */ public function card( $atts, $content = null ): string { $atts = shortcode_atts( array( 'show_when' => 'either', 'greeting' => '', 'prompt' => '', 'analytics_id' => '', 'identity' => 'avatar name', 'theme' => 'outlined', 'spotlight' => 'no', 'action' => '', 'action_type' => 'button', 'message' => '', 'message_field' => 'no', 'placeholder' => '', 'footer' => '', 'image' => '', 'image_alt' => '', 'class' => '', ), is_array( $atts ) ? $atts : array(), 'stand_card' ); $this->frontend->content_needs_widget(); $this->card_stack[] = array(); $body = do_shortcode( (string) $content ); $children = array_pop( $this->card_stack ); $body = wp_kses_post( self::balance( $body ) ); $message_mode = 'none'; $message = '' !== ( $children['stand_visitor_message'] ?? '' ) ? $children['stand_visitor_message'] : $atts['message']; if ( self::flag( $atts['message_field'] ) ) { $message_mode = 'field'; } elseif ( '' !== $message ) { $message_mode = 'fixed'; } return Markup::card( array( 'showWhen' => $atts['show_when'], 'greeting' => $atts['greeting'], 'prompt' => $atts['prompt'], 'analyticsId' => $atts['analytics_id'], 'identity' => $atts['identity'], 'theme' => $atts['theme'], 'spotlight' => self::flag( $atts['spotlight'] ), 'actionType' => $atts['action_type'], 'actionLabel' => '' !== ( $children['stand_action'] ?? '' ) ? $children['stand_action'] : $atts['action'], 'messageMode' => $message_mode, 'visitorMessage' => $message, 'fieldPlaceholder' => $atts['placeholder'], 'dismissText' => $children['stand_dismiss'] ?? '', 'footer' => $atts['footer'], 'mediaUrl' => $atts['image'], 'mediaAlt' => $atts['image_alt'], ), $body, self::wrapper( 'wp-block-stand-chat-stand-card', $atts['class'] ) ); } /** * [stand_action], [stand_visitor_message] and [stand_dismiss] inside a card. * * @param array|string $atts Unused. * @param string|null $content Text. * @param string $tag Shortcode name. */ public function card_child( $atts, $content = null, $tag = '' ): string { unset( $atts ); if ( empty( $this->card_stack ) ) { return ''; } $last = count( $this->card_stack ) - 1; $this->card_stack[ $last ][ $tag ] = trim( wp_strip_all_tags( (string) $content ) ); return ''; } /** * [stand_chatbox]. * * @param array|string $atts Attributes. * @param string|null $content Intro HTML. */ public function chatbox( $atts, $content = null ): string { $atts = shortcode_atts( array( 'greeting' => '', 'placeholder' => '', 'send_label' => '', 'floating' => 'no', 'show_header' => 'no', 'height' => '', 'show_when' => 'either', 'shared_session' => 'no', 'prompt' => '', 'analytics_id' => '', 'key' => '', 'label' => '', 'class' => '', ), is_array( $atts ) ? $atts : array(), 'stand_chatbox' ); $this->frontend->content_needs_widget(); $intro = wp_kses_post( self::balance( do_shortcode( (string) $content ) ) ); return Markup::chatbox( array( 'greeting' => $atts['greeting'], 'placeholder' => $atts['placeholder'], 'sendLabel' => $atts['send_label'], 'floating' => self::flag( $atts['floating'] ), 'showHeader' => self::flag( $atts['show_header'] ), 'heightMode' => 'grow' === $atts['height'] ? 'grow' : 'capped', 'maxHeight' => is_numeric( $atts['height'] ) ? (int) $atts['height'] : null, 'showWhen' => $atts['show_when'], 'sharedSession' => self::flag( $atts['shared_session'] ), 'prompt' => $atts['prompt'], 'analyticsId' => $atts['analytics_id'], 'sessionKey' => '' !== $atts['key'] ? $atts['key'] : $this->chatbox_key( (string) $content ), 'label' => $atts['label'], ), $intro, self::wrapper( 'wp-block-stand-chat-stand-chatbox', $atts['class'] ) ); } /** * The conversation key of a [stand_chatbox] without one: from the page and * the chatbox's intro, and, for another with the same intro on the page, * its place among them, so the two don't share a visitor's conversation. * * @param string $content Intro HTML. */ private function chatbox_key( string $content ): string { $key = 'wp-' . get_the_ID() . '-' . substr( md5( $content ), 0, 6 ); $this->chatbox_keys[ $key ] = ( $this->chatbox_keys[ $key ] ?? 0 ) + 1; return 1 === $this->chatbox_keys[ $key ] ? $key : $key . '-' . $this->chatbox_keys[ $key ]; } /** * Removes the paragraph WordPress wraps around a card or chatbox. * * Automatic paragraphs run before shortcodes, so an enclosing shortcode on * its own lines ends up as

…

. Browsers close * that paragraph at the card's first block element, which would end the * card early. * * @param string $html Content after shortcodes ran. */ public static function unwrap_paragraphs( $html ) { if ( ! is_string( $html ) || false === strpos( $html, '\s*()\s*(?:\s*)?

#', '$1', (string) $html ); } /** * Tidies enclosed content: automatic paragraphs leave a dangling

at the * start and

at the end when the shortcode spans several paragraphs. * * @param string $html Enclosed content. */ private static function balance( string $html ): string { $html = trim( $html ); $html = preg_replace( '#^(?:

||\s)+#i', '', $html ); $html = preg_replace( '#(?:

||\s)+$#i', '', (string) $html ); $html = preg_replace( '#

\s*

#i', '', (string) $html ); return force_balance_tags( (string) $html ); } /** * Truthy shortcode values: yes, true, 1, on. * * @param mixed $value Raw value. */ private static function flag( $value ): bool { return in_array( strtolower( trim( (string) $value ) ), array( 'yes', 'true', '1', 'on' ), true ); } /** * Wrapper attributes with the block class and the author's classes. * * @param string $base Block class. * @param string $extra Classes from the shortcode. * @return array */ private static function wrapper( string $base, string $extra ): array { $classes = array_filter( array_map( 'sanitize_html_class', preg_split( '/\s+/', $base . ' ' . $extra ) ) ); return array( 'class' => implode( ' ', $classes ) ); } }