*/ private $used_presets = []; /** * Get instance. * * @return Shortcode */ public static function get() { if ( null === self::$instance ) { self::$instance = new self(); } return self::$instance; } /** * Constructor. */ private function __construct() { // Register shortcodes. add_shortcode( 'copy', [ $this, 'render_copy_shortcode' ] ); add_shortcode( 'copy_inline', [ $this, 'render_copy_inline_shortcode' ] ); add_shortcode( 'ctc', [ $this, 'render_copy_shortcode' ] ); // Enqueue scripts/styles when shortcode is used. add_action( 'wp_footer', [ $this, 'maybe_enqueue_assets' ], 5 ); } /** * Get default attributes for [copy] shortcode. * * @return array Default attributes. */ public static function get_default_atts() { return [ // Content. 'text' => '', // Content to copy (or use content between tags). 'display' => '', // What to display (default: same as text). 'target' => '', // CSS selector to copy from (advanced). 'copy-as' => 'text', // 'text' or 'html' (legacy). 'copy_as' => '', // Copy as: text, html, text_and_html, image, json, svg. // Preset/Style. 'preset' => 'inline', // 'button', 'icon', 'inline', 'cover'. // Text. 'button-text' => '', // Button text (overrides preset default). 'success-text' => '', // Success message. 'tooltip' => '', // Tooltip text. // Icon. 'icon' => '', // Icon key: clipboard, copy, link, etc. 'icon-position' => '', // 'left' or 'right'. 'show-icon' => '', // 'yes' or 'no'. // Colors (overrides preset). 'color' => '', // Text color. 'bg' => '', // Background color. 'hover-bg' => '', // Hover background color. 'icon-color' => '', // Icon color (for icon preset). // Layout. 'class' => '', // Additional CSS class. 'id' => '', // Custom ID. // Redirect: URL to open after copy (e.g. store page). 'redirect' => '', // Preferred: URL to open after copying. 'link' => '', // Backward compatibility: same as redirect. // Legacy attributes (backward compatibility). 'copied-text' => '', // Maps to success-text. 'style' => '', // Maps to preset. 'tag' => '', // Legacy: HTML tag. 'title' => '', // Legacy: Tooltip. 'content' => '', // Legacy: Content to copy. 'hidden' => '', // Legacy: Hide display text. ]; } /** * Render [copy] shortcode. * * @param array $atts Shortcode attributes. * @param string $content Shortcode content. * @return string HTML output. */ public function render_copy_shortcode( $atts = [], $content = '' ) { $atts = shortcode_atts( self::get_default_atts(), $atts, 'copy' ); // Normalize legacy attributes. $atts = $this->normalize_legacy_atts( $atts, $content ); /** * Filter shortcode attributes before rendering. * * @since 5.0.0 * * @param array $atts Normalized shortcode attributes. * @param string $content Shortcode content. */ $atts = apply_filters( 'ctc/shortcode/atts', $atts, $content ); // Mark that we need to enqueue assets. $this->styles_enqueued = true; // Track preset for conditional CSS (only load styles for presets used). $preset = $atts['preset'] ?? 'inline'; $this->used_presets[ $preset ] = true; // Render based on preset. switch ( $preset ) { case 'button': return $this->render_button_preset( $atts ); case 'icon': return $this->render_icon_preset( $atts ); case 'cover': return $this->render_cover_preset( $atts ); case 'inline': default: return $this->render_inline_preset( $atts ); } } /** * Render [copy_inline] shortcode (backward compatible). * * Maps to [copy preset="inline"]. * * @param array $atts Shortcode attributes. * @param string $content Shortcode content. * @return string HTML output. */ public function render_copy_inline_shortcode( $atts = [], $content = '' ) { // Map copy_inline attributes to copy attributes. $mapped_atts = [ 'preset' => 'inline', 'text' => isset( $atts['text'] ) ? $atts['text'] : $content, 'display' => isset( $atts['display'] ) ? $atts['display'] : '', 'success-text' => isset( $atts['tooltip'] ) ? $atts['tooltip'] : __( 'Copied', 'ctc' ), 'class' => isset( $atts['style'] ) ? 'ctc-inline-style-' . $atts['style'] : '', 'hidden' => isset( $atts['hidden'] ) ? $atts['hidden'] : '', ]; return $this->render_copy_shortcode( $mapped_atts, $content ); } /** * Normalize legacy attributes to new format. * * @param array $atts Shortcode attributes. * @param string $content Shortcode content. * @return array Normalized attributes. */ private function normalize_legacy_atts( $atts, $content ) { // Decode HTML entities in content so [ ] work (e.g. [ ]). if ( ! empty( $atts['content'] ) ) { $atts['content'] = html_entity_decode( $atts['content'], ENT_QUOTES | ENT_HTML5, 'UTF-8' ); } // Legacy support: 'content' attribute contains what to copy. // 'text' attribute is the display text in legacy usage. // Example: [copy text="Location" content="/wp [...]"] - displays "Location", copies "/wp [...]" if ( ! empty( $atts['content'] ) ) { // Legacy mode: content = what to copy, text = what to display. $copy_text = $atts['content']; $display_text = ! empty( $atts['text'] ) ? $atts['text'] : $atts['content']; $atts['text'] = $copy_text; $atts['display'] = $display_text; } elseif ( empty( $atts['text'] ) && ! empty( $content ) ) { // Modern mode: text = both copy and display (unless display is set). $atts['text'] = $content; } // Decode HTML entities in text (supports [ for [ and ] for ] etc.). // This allows users to use HTML entities for special characters in shortcode attributes. if ( ! empty( $atts['text'] ) ) { $atts['text'] = html_entity_decode( $atts['text'], ENT_QUOTES | ENT_HTML5, 'UTF-8' ); // Fix WordPress wptexturize() converting -- to en-dash inside CSS var(). $atts['text'] = $this->fix_var_texturize( $atts['text'] ); } // Display text: prefer inner content when present, else default to copy text. $content_trimmed = isset( $content ) ? trim( (string) $content ) : ''; if ( ! empty( $content_trimmed ) && empty( $atts['display'] ) ) { $atts['display'] = html_entity_decode( $content_trimmed, ENT_QUOTES | ENT_HTML5, 'UTF-8' ); } elseif ( empty( $atts['display'] ) ) { $atts['display'] = $atts['text']; } else { $atts['display'] = html_entity_decode( $atts['display'], ENT_QUOTES | ENT_HTML5, 'UTF-8' ); } // Success text. if ( empty( $atts['success-text'] ) && ! empty( $atts['copied-text'] ) ) { $atts['success-text'] = $atts['copied-text']; } if ( empty( $atts['success-text'] ) ) { $atts['success-text'] = __( 'Copied!', 'ctc' ); } // Tooltip. if ( empty( $atts['tooltip'] ) && ! empty( $atts['title'] ) ) { $atts['tooltip'] = $atts['title']; } if ( empty( $atts['tooltip'] ) ) { $atts['tooltip'] = __( 'Copy to clipboard', 'ctc' ); } // Preset from legacy style. if ( empty( $atts['preset'] ) && ! empty( $atts['style'] ) ) { $style_map = [ 'icon' => 'icon', 'button' => 'button', 'cover' => 'cover', ]; if ( isset( $style_map[ $atts['style'] ] ) ) { $atts['preset'] = $style_map[ $atts['style'] ]; } } // Button text. if ( empty( $atts['button-text'] ) ) { $atts['button-text'] = __( 'Copy', 'ctc' ); } // Redirect URL: prefer redirect attribute; fall back to link for backward compatibility. if ( ! empty( $atts['redirect'] ) ) { $atts['link'] = $atts['redirect']; } return $atts; } /** * Fix WordPress wptexturize() inside CSS var() functions. * * WordPress converts -- to en-dash (–) and --- to em-dash (—). * This breaks CSS custom properties like var(--wp--preset--color--bg). * * This method only fixes content inside var() to avoid affecting * legitimate typography like "2020–2025" or "He said—yes". * * @since 5.0.0 * * @param string $text Text to process. * @return string Processed text with var() contents fixed. */ private function fix_var_texturize( $text ) { if ( empty( $text ) || strpos( $text, 'var(' ) === false ) { return $text; } // Replace en-dash/em-dash with hyphens only inside var(). return preg_replace_callback( '/var\s*\(([^)]+)\)/', function ( $matches ) { $inside = str_replace( [ '–', '—' ], // En-dash, Em-dash (Unicode). [ '--', '---' ], // Double, Triple hyphen. $matches[1] ); return 'var(' . $inside . ')'; }, $text ); } /** * Render inline preset. * * Displays text with a copy icon. Best for coupon codes, promo codes, etc. * * Supports: * - tag="a" : Uses native anchor tag with theme styling (no custom CSS overrides). * - show-icon="no" : Hides the copy icon. * * @param array $atts Shortcode attributes. * @return string HTML output. */ private function render_inline_preset( $atts ) { $id = ! empty( $atts['id'] ) ? $atts['id'] : 'ctc-inline-' . wp_generate_password( 8, false, false ); $hidden_class = 'yes' === $atts['hidden'] ? 'ctc-inline-hidden' : ''; $custom_class = ! empty( $atts['class'] ) ? ' ' . esc_attr( $atts['class'] ) : ''; // Determine if using native anchor tag (inherits theme styling). $use_native_tag = 'a' === strtolower( $atts['tag'] ); // Determine if icon should be shown (default: yes, unless show-icon="no"). $show_icon = ! in_array( strtolower( $atts['show-icon'] ), [ 'no', 'false', '0' ], true ); // Build CSS class based on tag type. if ( $use_native_tag ) { // Native mode: Use theme's anchor styling, minimal CTC styling. $this->used_presets['native'] = true; $css_class = 'ctc-shortcode ctc-shortcode--native' . $custom_class; } else { // Modern mode: Full CTC styling. $css_class = 'ctc-shortcode ctc-shortcode--inline' . $custom_class; } // Build inline styles (only for non-native mode). $inline_style = ''; if ( ! $use_native_tag && ! empty( $atts['color'] ) ) { $inline_style .= '--ctc-inline-color: ' . esc_attr( $atts['color'] ) . ';'; } // Determine HTML tag. $tag = $use_native_tag ? 'a' : 'span'; // Copy as: prefer copy_as. $copy_as_value = ! empty( $atts['copy_as'] ) ? $atts['copy_as'] : ''; // Build icon HTML. $icon_html = ''; if ( $show_icon ) { $icon_key = ! empty( $atts['icon'] ) ? $atts['icon'] : 'clipboard'; $icon_html = ''; } ob_start(); ?> < id="" class="" data-ctc-copy="" data-ctc-success="" data-ctc-format="" data-ctc-copy-as="" data-ctc-target="" style="" href="javascript:void(0);" title="" role="button" tabindex="0" aria-label="" > > [ 'button_text' => $atts['button-text'], 'success_text' => $atts['success-text'], ], 'icon' => [ 'enabled' => 'no' !== $atts['show-icon'], 'position' => ! empty( $atts['icon-position'] ) ? $atts['icon-position'] : 'left', 'icon_key' => ! empty( $atts['icon'] ) ? $atts['icon'] : 'clipboard', ], 'style' => [], ]; // Add custom colors if provided. if ( ! empty( $atts['color'] ) ) { $config['style']['text_color'] = $atts['color']; } if ( ! empty( $atts['bg'] ) ) { $config['style']['background_color'] = $atts['bg']; } if ( ! empty( $atts['hover-bg'] ) ) { $config['style']['hover_background_color'] = $atts['hover-bg']; } // Merge with defaults. $config = Button::merge_config( $config ); // Build inline styles. $inline_styles = Button::build_inline_styles( $config['style'] ); // Get icon SVG. $icon_svg = ''; if ( $config['icon']['enabled'] ) { $icon_svg = '' . Button::get_icon_svg( $config['icon']['icon_key'] ) . ''; } // Build content based on icon position. $button_content = ''; if ( 'left' === $config['icon']['position'] && $icon_svg ) { $button_content .= $icon_svg; } $button_content .= '' . esc_html( $config['text']['button_text'] ) . ''; if ( 'right' === $config['icon']['position'] && $icon_svg ) { $button_content .= $icon_svg; } // Copy as: prefer copy_as. $copy_as_value = ! empty( $atts['copy_as'] ) ? $atts['copy_as'] : ''; ob_start(); ?> [ 'tooltip_text' => $atts['tooltip'], 'success_text' => $atts['success-text'], ], 'icon' => [ 'icon_key' => ! empty( $atts['icon'] ) ? $atts['icon'] : 'clipboard', ], 'style' => [], ]; // Add custom colors if provided. if ( ! empty( $atts['icon-color'] ) ) { $config['style']['icon_color'] = $atts['icon-color']; } if ( ! empty( $atts['bg'] ) ) { $config['style']['bg_color'] = $atts['bg']; } // Merge with defaults. $config = Icon::merge_config( $config ); // Build inline styles. $inline_styles = Icon::build_inline_styles( $config['style'] ); // Get icon SVG. $icon_svg = '' . Icon::get_icon_svg( $config['icon']['icon_key'] ) . ''; // Copy as: prefer copy_as. $copy_as_value = ! empty( $atts['copy_as'] ) ? $atts['copy_as'] : ''; ob_start(); ?> [ 'button_text' => $atts['button-text'], 'success_text' => $atts['success-text'], ], 'icon' => [ 'enabled' => 'no' !== $atts['show-icon'], 'position' => ! empty( $atts['icon-position'] ) ? $atts['icon-position'] : 'left', 'icon_key' => ! empty( $atts['icon'] ) ? $atts['icon'] : 'clipboard', ], 'button' => [], ]; // Add custom colors if provided. if ( ! empty( $atts['color'] ) ) { $config['button']['text_color'] = $atts['color']; } if ( ! empty( $atts['bg'] ) ) { $config['button']['bg_color'] = $atts['bg']; } if ( ! empty( $atts['hover-bg'] ) ) { $config['button']['hover_bg_color'] = $atts['hover-bg']; } // Merge with defaults. $config = Cover::merge_config( $config ); // Get icon SVG. $icon_svg = ''; if ( $config['icon']['enabled'] ) { $icon_svg = ''; } // Build button content. $button_content = ''; if ( 'left' === $config['icon']['position'] && $icon_svg ) { $button_content .= $icon_svg; } $button_content .= '' . esc_html( $config['text']['button_text'] ) . ''; if ( 'right' === $config['icon']['position'] && $icon_svg ) { $button_content .= $icon_svg; } ob_start(); ?>