mailpoet
/
vendor
/
woocommerce
/
email-editor
/
src
/
Engine
/
class-site-style-sync-controller.php
Logger
11 months ago
Patterns
11 months ago
PersonalizationTags
2 days ago
Renderer
2 days ago
Templates
4 months ago
class-assets-manager.php
5 months ago
class-dependency-check.php
11 months ago
class-email-api-controller.php
6 months ago
class-email-editor.php
2 days ago
class-email-styles-schema.php
11 months ago
class-personalizer.php
2 days ago
class-send-preview-email.php
5 months ago
class-settings-controller.php
3 months ago
class-site-style-sync-controller.php
4 months ago
class-theme-controller.php
3 months ago
class-user-theme.php
11 months ago
content-editor.css
2 weeks ago
content-shared.css
11 months ago
index.php
11 months ago
theme.json
3 months ago
class-site-style-sync-controller.php
295 lines
| 1 | <?php |
| 2 | declare(strict_types = 1); |
| 3 | namespace Automattic\WooCommerce\EmailEditor\Engine; |
| 4 | if (!defined('ABSPATH')) exit; |
| 5 | use WP_Theme_JSON; |
| 6 | use WP_Theme_JSON_Resolver; |
| 7 | use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Styles_Helper; |
| 8 | class Site_Style_Sync_Controller { |
| 9 | private ?WP_Theme_JSON $site_theme = null; |
| 10 | private ?array $base_theme_data = null; |
| 11 | private $email_safe_fonts = array(); |
| 12 | public function __construct() { |
| 13 | add_action( 'init', array( $this, 'initialize' ), 20 ); |
| 14 | } |
| 15 | public function initialize(): void { |
| 16 | add_action( 'switch_theme', array( $this, 'invalidate_site_theme_cache' ) ); |
| 17 | add_action( 'customize_save_after', array( $this, 'invalidate_site_theme_cache' ) ); |
| 18 | } |
| 19 | public function sync_site_styles( ?WP_Theme_JSON $base_theme = null ): array { |
| 20 | // Store base theme data for fallback lookups. |
| 21 | $this->base_theme_data = $base_theme ? $base_theme->get_data() : null; |
| 22 | $site_theme = $this->get_site_theme(); |
| 23 | $site_data = $site_theme->get_data(); |
| 24 | $synced_data = array( |
| 25 | 'version' => 3, |
| 26 | 'settings' => $this->sync_settings_data( $site_data['settings'] ?? array() ), |
| 27 | 'styles' => $this->sync_styles_data( $site_data['styles'] ?? array() ), |
| 28 | ); |
| 29 | $synced_data = apply_filters( 'woocommerce_email_editor_synced_site_styles', $synced_data, $site_data ); |
| 30 | return $synced_data; |
| 31 | } |
| 32 | public function get_theme( ?WP_Theme_JSON $base_theme = null ): ?WP_Theme_JSON { |
| 33 | if ( ! $this->is_sync_enabled() ) { |
| 34 | return null; |
| 35 | } |
| 36 | $synced_data = $this->sync_site_styles( $base_theme ); |
| 37 | if ( empty( $synced_data ) || ! isset( $synced_data['version'] ) ) { |
| 38 | return null; |
| 39 | } |
| 40 | return new WP_Theme_JSON( $synced_data, 'theme' ); |
| 41 | } |
| 42 | public function is_sync_enabled(): bool { |
| 43 | return apply_filters( 'woocommerce_email_editor_site_style_sync_enabled', true ); |
| 44 | } |
| 45 | public function invalidate_site_theme_cache(): void { |
| 46 | if ( ! $this->is_sync_enabled() ) { |
| 47 | return; |
| 48 | } |
| 49 | $this->site_theme = null; |
| 50 | } |
| 51 | private function get_site_theme(): WP_Theme_JSON { |
| 52 | if ( null === $this->site_theme ) { |
| 53 | // Get only the theme and user customizations (e.g. from site editor). |
| 54 | $this->site_theme = new WP_Theme_JSON(); |
| 55 | $this->site_theme->merge( WP_Theme_JSON_Resolver::get_theme_data() ); |
| 56 | $this->site_theme->merge( WP_Theme_JSON_Resolver::get_user_data() ); |
| 57 | $this->site_theme = apply_filters( 'woocommerce_email_editor_site_theme', $this->site_theme ); |
| 58 | if ( isset( $this->site_theme->get_raw_data()['styles'] ) ) { |
| 59 | $this->site_theme = WP_Theme_JSON::resolve_variables( $this->site_theme ); |
| 60 | } |
| 61 | } |
| 62 | return $this->site_theme; |
| 63 | } |
| 64 | private function sync_settings_data( array $site_settings ): array { |
| 65 | $email_settings = array(); |
| 66 | // Sync color palette. |
| 67 | if ( isset( $site_settings['color']['palette'] ) ) { |
| 68 | $email_settings['color']['palette'] = $site_settings['color']['palette']; |
| 69 | } |
| 70 | return $email_settings; |
| 71 | } |
| 72 | private function sync_styles_data( array $site_styles ): array { |
| 73 | $email_styles = array(); |
| 74 | // Sync color styles. |
| 75 | if ( ! empty( $site_styles['color'] ) ) { |
| 76 | $email_styles['color'] = $this->convert_color_styles( $site_styles['color'] ); |
| 77 | } |
| 78 | // Sync typography styles. |
| 79 | if ( ! empty( $site_styles['typography'] ) ) { |
| 80 | $email_styles['typography'] = $this->convert_typography_styles( $site_styles['typography'] ); |
| 81 | } |
| 82 | // Sync spacing styles. |
| 83 | if ( ! empty( $site_styles['spacing'] ) ) { |
| 84 | $email_styles['spacing'] = $this->convert_spacing_styles( $site_styles['spacing'] ); |
| 85 | } |
| 86 | // Sync element styles. |
| 87 | if ( ! empty( $site_styles['elements'] ) ) { |
| 88 | $email_styles['elements'] = $this->convert_element_styles( $site_styles['elements'] ); |
| 89 | } |
| 90 | return $email_styles; |
| 91 | } |
| 92 | public function get_email_safe_fonts(): array { |
| 93 | if ( empty( $this->email_safe_fonts ) ) { |
| 94 | $theme_data = (array) json_decode( (string) file_get_contents( __DIR__ . '/theme.json' ), true ); |
| 95 | $font_families = $theme_data['settings']['typography']['fontFamilies'] ?? array(); |
| 96 | if ( ! empty( $font_families ) ) { |
| 97 | foreach ( $font_families as $font_family ) { |
| 98 | $this->email_safe_fonts[ strtolower( $font_family['slug'] ) ] = $font_family['fontFamily']; |
| 99 | } |
| 100 | } |
| 101 | } |
| 102 | return $this->email_safe_fonts; |
| 103 | } |
| 104 | private function convert_color_styles( array $color_styles ): array { |
| 105 | $email_colors = array(); |
| 106 | $this->resolve_and_assign( $color_styles, 'background', $email_colors ); |
| 107 | $this->resolve_and_assign( $color_styles, 'text', $email_colors ); |
| 108 | return $email_colors; |
| 109 | } |
| 110 | private function convert_typography_styles( array $typography_styles, string $element = '' ): array { |
| 111 | $email_typography = array(); |
| 112 | // Handle special cases with processors. |
| 113 | $this->resolve_and_assign( $typography_styles, 'fontFamily', $email_typography, array( $this, 'convert_to_email_safe_font' ) ); |
| 114 | $this->resolve_and_assign( |
| 115 | $typography_styles, |
| 116 | 'fontSize', |
| 117 | $email_typography, |
| 118 | function ( $value ) use ( $element ) { |
| 119 | // Try element-specific fallback first, then global fallback. |
| 120 | $fallback = null; |
| 121 | if ( $element ) { |
| 122 | $fallback = $this->get_base_theme_value( array( 'styles', 'elements', $element, 'typography', 'fontSize' ) ); |
| 123 | } |
| 124 | if ( ! $fallback ) { |
| 125 | $fallback = $this->get_base_theme_value( array( 'styles', 'typography', 'fontSize' ) ); |
| 126 | } |
| 127 | return $this->convert_to_px_size( $value, $fallback ); |
| 128 | } |
| 129 | ); |
| 130 | // Handle compatible properties without processing. |
| 131 | $compatible_props = array( 'fontWeight', 'fontStyle', 'lineHeight', 'letterSpacing', 'textTransform', 'textDecoration' ); |
| 132 | foreach ( $compatible_props as $prop ) { |
| 133 | $this->resolve_and_assign( $typography_styles, $prop, $email_typography ); |
| 134 | } |
| 135 | return $email_typography; |
| 136 | } |
| 137 | private function convert_spacing_styles( array $spacing_styles ): array { |
| 138 | $email_spacing = array(); |
| 139 | $this->resolve_and_assign( |
| 140 | $spacing_styles, |
| 141 | 'padding', |
| 142 | $email_spacing, |
| 143 | function ( $value ) { |
| 144 | return $this->convert_spacing_values( $value, array( 'styles', 'spacing', 'padding' ) ); |
| 145 | } |
| 146 | ); |
| 147 | $this->resolve_and_assign( |
| 148 | $spacing_styles, |
| 149 | 'blockGap', |
| 150 | $email_spacing, |
| 151 | function ( $value ) { |
| 152 | $fallback = $this->get_base_theme_value( array( 'styles', 'spacing', 'blockGap' ) ); |
| 153 | return $this->convert_to_px_size( $value, $fallback ); |
| 154 | } |
| 155 | ); |
| 156 | // Note: We intentionally skip margin as it's not supported in email renderer. |
| 157 | return $email_spacing; |
| 158 | } |
| 159 | private function convert_element_styles( array $element_styles ): array { |
| 160 | $email_elements = array(); |
| 161 | // Process supported elements. |
| 162 | $supported_elements = array( 'heading', 'button', 'link', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' ); |
| 163 | foreach ( $supported_elements as $element ) { |
| 164 | if ( isset( $element_styles[ $element ] ) ) { |
| 165 | $email_elements[ $element ] = $this->convert_element_style( $element_styles[ $element ], $element ); |
| 166 | } |
| 167 | } |
| 168 | return $email_elements; |
| 169 | } |
| 170 | private function convert_element_style( array $element_style, string $element_name = '' ): array { |
| 171 | $email_element = array(); |
| 172 | // Convert typography if present. |
| 173 | if ( isset( $element_style['typography'] ) ) { |
| 174 | $email_element['typography'] = $this->convert_typography_styles( $element_style['typography'], $element_name ); |
| 175 | } |
| 176 | // Convert color if present. |
| 177 | if ( isset( $element_style['color'] ) ) { |
| 178 | $email_element['color'] = $this->convert_color_styles( $element_style['color'] ); |
| 179 | } |
| 180 | // Convert spacing if present. |
| 181 | if ( isset( $element_style['spacing'] ) ) { |
| 182 | $email_element['spacing'] = $this->convert_spacing_styles( $element_style['spacing'] ); |
| 183 | } |
| 184 | return $email_element; |
| 185 | } |
| 186 | private function resolve_and_assign( array $styles, string $property, array &$target, ?callable $processor = null ): bool { |
| 187 | if ( ! isset( $styles[ $property ] ) ) { |
| 188 | return false; |
| 189 | } |
| 190 | $resolved = $this->resolve_style_value( $styles[ $property ] ); |
| 191 | if ( ! $resolved ) { |
| 192 | return false; |
| 193 | } |
| 194 | $target[ $property ] = $processor ? $processor( $resolved ) : $resolved; |
| 195 | return true; |
| 196 | } |
| 197 | private function resolve_style_value( $style_value ) { |
| 198 | // Check if this is a reference array. |
| 199 | if ( is_array( $style_value ) && isset( $style_value['ref'] ) ) { |
| 200 | $ref = $style_value['ref']; |
| 201 | if ( ! is_string( $ref ) || empty( $ref ) ) { |
| 202 | return null; |
| 203 | } |
| 204 | $path = explode( '.', $ref ); |
| 205 | return _wp_array_get( $this->get_site_theme()->get_data(), $path, null ); |
| 206 | } |
| 207 | return $style_value; |
| 208 | } |
| 209 | private function convert_to_email_safe_font( string $font_family ): string { |
| 210 | // Get email-safe fonts. |
| 211 | $email_safe_fonts = $this->get_email_safe_fonts(); |
| 212 | // Map common web fonts to email-safe alternatives. |
| 213 | $font_map = array( |
| 214 | 'helvetica' => $email_safe_fonts['arial'], // Arial fallback. |
| 215 | 'times' => $email_safe_fonts['georgia'], // Georgia fallback. |
| 216 | 'courier' => $email_safe_fonts['courier-new'], // Courier New. |
| 217 | 'trebuchet' => $email_safe_fonts['trebuchet-ms'], |
| 218 | ); |
| 219 | $email_safe_fonts = array_merge( $email_safe_fonts, $font_map ); |
| 220 | $get_font_family = function ( $font_name ) use ( $email_safe_fonts ) { |
| 221 | $font_name_lower = strtolower( $font_name ); |
| 222 | // First check for match in the email-safe slug. |
| 223 | if ( isset( $email_safe_fonts[ $font_name_lower ] ) ) { |
| 224 | return $email_safe_fonts[ $font_name_lower ]; |
| 225 | } |
| 226 | // If no match in the slug, check for match in the font family name. |
| 227 | foreach ( $email_safe_fonts as $safe_font_slug => $safe_font ) { |
| 228 | if ( stripos( $safe_font, $font_name_lower ) !== false ) { |
| 229 | return $safe_font; |
| 230 | } |
| 231 | } |
| 232 | return null; |
| 233 | }; |
| 234 | // Check if it's already an email-safe font. |
| 235 | $font_family_array = explode( ',', $font_family ); |
| 236 | $first_font = trim( $font_family_array[0] ); |
| 237 | $first_font = trim( $first_font, "\"'" ); |
| 238 | $safe_font_family = $get_font_family( $first_font ); |
| 239 | if ( $safe_font_family ) { |
| 240 | return $safe_font_family; |
| 241 | } |
| 242 | // Default to arial font if no match found. |
| 243 | return $email_safe_fonts['arial']; |
| 244 | } |
| 245 | private function convert_to_px_size( string $size, ?string $fallback = null ): string { |
| 246 | $converted = null; |
| 247 | // Replace clamp() with its minimum value. We use min because it's emails are most likely to be viewed on smaller screens. |
| 248 | if ( stripos( $size, 'clamp(' ) !== false ) { |
| 249 | $converted = Styles_Helper::clamp_to_static_px( $size, 'min' ); |
| 250 | // If clamp_to_static_px returns the original value, it failed to convert. |
| 251 | if ( $converted === $size ) { |
| 252 | $converted = null; |
| 253 | } |
| 254 | } |
| 255 | // Try standard conversion. |
| 256 | if ( is_null( $converted ) ) { |
| 257 | $converted = Styles_Helper::convert_to_px( $size, false ); |
| 258 | } |
| 259 | // If all conversions failed, use fallback if provided. |
| 260 | if ( is_null( $converted ) && $fallback ) { |
| 261 | return $fallback; |
| 262 | } |
| 263 | // Return converted value or original if conversion failed. |
| 264 | return $converted ?? $size; |
| 265 | } |
| 266 | private function convert_spacing_values( $spacing_values, array $base_path ) { |
| 267 | if ( ! is_string( $spacing_values ) && ! is_array( $spacing_values ) ) { |
| 268 | return $spacing_values; |
| 269 | } |
| 270 | if ( is_string( $spacing_values ) ) { |
| 271 | $fallback = $this->get_base_theme_value( $base_path ); |
| 272 | return $this->convert_to_px_size( $spacing_values, $fallback ); |
| 273 | } |
| 274 | $px_values = array(); |
| 275 | foreach ( $spacing_values as $side => $value ) { |
| 276 | if ( is_string( $value ) ) { |
| 277 | // Build path for side-specific fallback (e.g., ['styles', 'spacing', 'padding', 'top']). |
| 278 | $side_path = array_merge( $base_path, array( $side ) ); |
| 279 | $fallback = $this->get_base_theme_value( $side_path ); |
| 280 | $px_values[ $side ] = $this->convert_to_px_size( $value, $fallback ); |
| 281 | } else { |
| 282 | $px_values[ $side ] = $value; |
| 283 | } |
| 284 | } |
| 285 | return $px_values; |
| 286 | } |
| 287 | private function get_base_theme_value( array $path ): ?string { |
| 288 | if ( ! $this->base_theme_data ) { |
| 289 | return null; |
| 290 | } |
| 291 | $value = _wp_array_get( $this->base_theme_data, $path ); |
| 292 | return is_string( $value ) ? $value : null; |
| 293 | } |
| 294 | } |
| 295 |