'tabs', 'tab' => 'tab', 'spoiler' => 'spoiler', 'accordion' => 'accordion', 'note' => 'note', 'box' => 'box', 'button' => 'button', 'row' => 'row', 'column' => 'column', 'youtube' => 'video', 'youtube_advanced' => 'video', 'vimeo' => 'video', 'tooltip' => 'tooltip', 'qrcode' => 'qrcode', 'table' => 'table', 'quote' => 'quote', 'highlight' => 'highlight', 'divider' => 'divider', 'spacer' => 'spacer', 'label' => 'label', 'heading' => 'heading', 'private' => 'hidden', 'scheduler' => 'hidden', 'conditional' => 'hidden', 'members' => 'members', 'guests' => 'guests', ); /** * Shortcode tags registered by us, so register() can start over. * * @var array */ private static $registered = array(); /** * Column widths of the open rows, innermost last; each column takes the next one. * * @var array> */ private static $spans = array(); /** * Hooks. Late on init, after Shortcodes Ultimate would have registered its own. */ public static function init(): void { add_action( 'init', array( __CLASS__, 'register' ), 99 ); } /** * Registers our handlers for the prefix in use, unless Shortcodes Ultimate is active, the prefix is empty * (names like [note] may belong to others) or a tag is already taken. */ public static function register(): void { global $shortcode_tags; foreach ( self::$registered as $tag ) { // Only ours: another plugin may have taken the tag over in the meantime. if ( isset( $shortcode_tags[ $tag ] ) && array( __CLASS__, 'render' ) === $shortcode_tags[ $tag ] ) { remove_shortcode( $tag ); } } self::$registered = array(); if ( ! function_exists( 'is_plugin_active' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } if ( is_plugin_active( 'shortcodes-ultimate/shortcodes-ultimate.php' ) ) { return; } $prefix = self::prefix(); if ( '' === $prefix ) { return; } foreach ( self::NAMES as $name ) { foreach ( array( '', '_', '__' ) as $nesting ) { $tag = $nesting . $prefix . $name; if ( ! shortcode_exists( $tag ) ) { add_shortcode( $tag, array( __CLASS__, 'render' ) ); self::$registered[] = $tag; } } } } /** * Prefix from the settings of Shortcodes Ultimate (default su_), only letters, digits and underscores. */ private static function prefix(): string { $prefix = get_option( 'su_option_prefix', 'su_' ); return is_string( $prefix ) ? (string) preg_replace( '#[^a-z0-9_]#', '', strtolower( $prefix ) ) : 'su_'; } /** * Shortcode name without nesting underscores and prefix. * * @param string $tag Shortcode tag. */ private static function name( $tag ): string { return substr( ltrim( $tag, '_' ), strlen( self::prefix() ) ); } /** * One shortcode of Shortcodes Ultimate. * * @param array|string $atts Attributes. * @param string|null $content Content. * @param string $tag Shortcode tag. * @return string */ public static function render( $atts, $content = null, $tag = '' ) { $atts = is_array( $atts ) ? $atts : array(); $content = (string) $content; switch ( self::HANDLERS[ self::name( (string) $tag ) ] ?? 'content' ) { case 'tabs': return self::map_tabs( $atts, $content ); case 'tab': return self::map_tab( $atts, $content ); case 'spoiler': return self::map_spoiler( $atts, $content ); case 'accordion': return self::map_accordion( $atts, $content ); case 'note': return self::map_note( $atts, $content ); case 'box': return self::map_box( $atts, $content ); case 'button': return self::map_button( $atts, $content ); case 'row': return self::map_row( $atts, $content, (string) $tag ); case 'column': return self::map_column( $atts, $content ); case 'video': return self::map_video( $atts ); case 'tooltip': return self::map_tooltip( $atts, $content ); case 'qrcode': return self::map_qrcode( $atts ); case 'table': return self::map_table( $atts, $content ); case 'quote': return self::map_quote( $atts, $content ); case 'highlight': return '' . do_shortcode( $content ) . ''; case 'divider': return '
'; case 'spacer': return self::map_spacer( $atts ); case 'label': wp_enqueue_style( 'threece-elements' ); return '' . do_shortcode( $content ) . ''; case 'heading': // Never a real heading in Shortcodes Ultimate: stays a paragraph, outline and table of contents unchanged. return '

' . do_shortcode( $content ) . '

'; case 'hidden': // Private notes, schedules and conditions are never shown. return ''; case 'members': return is_user_logged_in() ? do_shortcode( $content ) : ''; case 'guests': return is_user_logged_in() ? '' : do_shortcode( $content ); default: return do_shortcode( $content ); } } /** * Attribute as text. * * @param array $atts Attributes. * @param string $key Name. * @param string $none Fallback. */ private static function att( $atts, $key, $none = '' ): string { return isset( $atts[ $key ] ) && is_scalar( $atts[ $key ] ) ? trim( (string) $atts[ $key ] ) : $none; } /** * Extra CSS class of the old shortcode, passed on (our elements clean it). * * @param array $atts Attributes. * @return array */ private static function css_class( $atts ) { return array( 'class' => self::att( $atts, 'class' ) ); } /** * Tabs. * * @param array $atts Attributes. * @param string $content Content. */ private static function map_tabs( $atts, $content ): string { // SU stacks on phones unless told otherwise; its "desktop" is our wrapping tab list. $mobile = self::att( $atts, 'mobile', 'stack' ); return ThreeCE_Elements::tabs( array( 'active' => self::att( $atts, 'active', '1' ), 'mobile' => 'desktop' === $mobile ? 'wrap' : ( 'scroll' === $mobile ? 'scroll' : 'stack' ), 'anchor_in_url' => self::att( $atts, 'anchor_in_url', 'no' ), ) + self::css_class( $atts ), $content ); } /** * One tab. * * @param array $atts Attributes. * @param string $content Content. */ private static function map_tab( $atts, $content ): string { return ThreeCE_Elements::tab( array( 'title' => self::att( $atts, 'title' ), 'anchor' => self::att( $atts, 'anchor' ), ) + self::css_class( $atts ), $content ); } /** * Spoiler, also inside an accordion. * * @param array $atts Attributes. * @param string $content Content. */ private static function map_spoiler( $atts, $content ): string { return ThreeCE_Elements::spoiler( array( 'title' => self::att( $atts, 'title' ), 'open' => self::att( $atts, 'open', 'no' ), ) + self::css_class( $atts ), $content ); } /** * Accordion. * * @param array $atts Attributes. * @param string $content Content. */ private static function map_accordion( $atts, $content ): string { return ThreeCE_Elements::accordion( self::css_class( $atts ), $content ); } /** * Note: notice box, colors are left to the theme. * * @param array $atts Attributes. * @param string $content Content. */ private static function map_note( $atts, $content ): string { return ThreeCE_Elements::notice( array( 'type' => 'info' ) + self::css_class( $atts ), $content ); } /** * Box with title: notice box with title. * * @param array $atts Attributes. * @param string $content Content. */ private static function map_box( $atts, $content ): string { return ThreeCE_Elements::notice( array( 'type' => 'info', 'title' => self::att( $atts, 'title' ), ) + self::css_class( $atts ), $content ); } /** * Button: address, new window, nofollow and download; onclick, colors and icons are dropped. * * @param array $atts Attributes. * @param string $content Content. */ private static function map_button( $atts, $content ): string { return ThreeCE_Elements::button( array( 'url' => self::att( $atts, 'url' ), 'new_tab' => in_array( self::att( $atts, 'target' ), array( 'blank', '_blank' ), true ) ? 'yes' : 'no', 'nofollow' => false !== stripos( self::att( $atts, 'rel' ), 'nofollow' ) ? 'yes' : 'no', 'download' => '' !== self::att( $atts, 'download' ) ? 'yes' : 'no', ) + self::css_class( $atts ), $content ); } /** * Row: works out the column widths from the sizes of its own columns (1/3 and 2/3 become 1 and 2 parts). * * @param array $atts Attributes. * @param string $content Content. * @param string $tag Shortcode tag of the row. */ private static function map_row( $atts, $content, $tag ): string { $column = substr( $tag, 0, - strlen( 'row' ) ) . 'column'; preg_match_all( '#\[' . preg_quote( $column, '#' ) . '(?=[\s\]])([^\]]*)\]#', $content, $columns ); $sizes = array(); foreach ( $columns[1] as $raw ) { $sizes[] = self::fraction( self::att( (array) shortcode_parse_atts( $raw ), 'size' ) ); } self::$spans[] = self::spans( $sizes ); $html = ThreeCE_Elements::columns( self::css_class( $atts ), $content ); array_pop( self::$spans ); return $html; } /** * One column with the next width of its row. * * @param array $atts Attributes. * @param string $content Content. */ private static function map_column( $atts, $content ): string { $span = 1; $last = count( self::$spans ) - 1; if ( $last >= 0 && self::$spans[ $last ] ) { $span = (int) array_shift( self::$spans[ $last ] ); } return ThreeCE_Elements::column( array( 'span' => (string) $span ) + self::css_class( $atts ), $content ); } /** * A size like 1/3 as a number, or 0 if it is none. * * @param string $size Size. */ private static function fraction( $size ): float { if ( ! preg_match( '#^(\d{1,2})/(\d{1,2})$#', $size, $m ) || 0 === (int) $m[2] || 0 === (int) $m[1] || (int) $m[1] > (int) $m[2] ) { return 0.0; } return (int) $m[1] / (int) $m[2]; } /** * Widths in parts (1 to 3) relative to the narrowest column; unknown sizes count as one part. * * @param array $sizes Fractions. * @return array */ private static function spans( $sizes ) { $known = array_filter( $sizes ); if ( ! $known || count( $known ) !== count( $sizes ) ) { return array_fill( 0, count( $sizes ), 1 ); } $smallest = min( $known ); $spans = array(); foreach ( $sizes as $size ) { $spans[] = (int) min( 3, max( 1, round( $size / $smallest ) ) ); } return $spans; } /** * YouTube, YouTube advanced and Vimeo: video with two-click consent. * * @param array $atts Attributes. */ private static function map_video( $atts ): string { return ThreeCE_Elements::video( array( 'url' => self::att( $atts, 'url' ), 'title' => self::att( $atts, 'title' ), ) + self::css_class( $atts ) ); } /** * Tooltip: title and text become the explanation. * * @param array $atts Attributes. * @param string $content Term. */ private static function map_tooltip( $atts, $content ): string { $text = implode( ': ', array_filter( array( self::att( $atts, 'title' ), self::att( $atts, 'text' ) ) ) ); return ThreeCE_Elements::tooltip( array( 'text' => $text ) + self::css_class( $atts ), $content ); } /** * QR code: made on the server, colors are left black on white so every scanner reads it. * * @param array $atts Attributes. */ private static function map_qrcode( $atts ): string { return ThreeCE_Elements::qr( array( 'text' => self::att( $atts, 'data' ), 'size' => self::att( $atts, 'size', '200' ), 'title' => self::att( $atts, 'title' ), ) + self::css_class( $atts ) ); } /** * Table: the tables inside get our table style (striped unless alternate="no"). * * @param array $atts Attributes. * @param string $content Table HTML. */ private static function map_table( $atts, $content ): string { $style = ThreeCE_Tables::CLASS_NAME . ( 'no' === self::att( $atts, 'alternate', 'yes' ) ? '' : ' ' . ThreeCE_Tables::CLASS_NAME . '--striped' ); return (string) preg_replace_callback( '#]*)>#i', static function ( $m ) use ( $style ) { if ( preg_match( '#\sclass\s*=\s*(["\'])(.*?)\1#i', $m[1], $c ) ) { return ''; } return ''; }, do_shortcode( $content ) ); } /** * Quote with source; the source links only to http and https addresses. * * @param array $atts Attributes. * @param string $content Quote. */ private static function map_quote( $atts, $content ): string { $text = trim( do_shortcode( $content ) ); $text = false === stripos( $text, '' . $text . '

' : $text; $cite = self::att( $atts, 'cite' ); $url = esc_url( self::att( $atts, 'url' ), array( 'http', 'https' ) ); if ( '' !== $cite ) { $cite = '' !== $url ? '' . esc_html( $cite ) . '' : esc_html( $cite ); $text .= '
' . $cite . '
'; } wp_enqueue_style( 'threece-elements' ); return '
' . $text . '
'; } /** * Empty space of 0 to 200 pixels (default 20, like Shortcodes Ultimate). * * @param array $atts Attributes. */ private static function map_spacer( $atts ): string { $size = self::att( $atts, 'size', '20' ); $size = preg_match( '#^\d{1,4}$#', $size ) ? min( 200, (int) $size ) : 20; return ''; } }