Commit e16def7b25 for woocommerce

commit e16def7b253484497eca9fcf78f5c243d91a9494
Author: Rostislav Wolný <1082140+costasovo@users.noreply.github.com>
Date:   Fri Jan 23 11:40:07 2026 +0100

    Fix block spacing issues in emails on Assembler theme  (#62911)

    * Use base email theme values as a fallback when we are unable to convert the site value

    * Switch conversion from a clamp to min strategy

    * Add changelog

diff --git a/packages/php/email-editor/changelog/wooprd-1714-assempler-theme-no-block-spacing b/packages/php/email-editor/changelog/wooprd-1714-assempler-theme-no-block-spacing
new file mode 100644
index 0000000000..0d4a40b808
--- /dev/null
+++ b/packages/php/email-editor/changelog/wooprd-1714-assempler-theme-no-block-spacing
@@ -0,0 +1,4 @@
+Significance: patch
+Type: enhancement
+
+Improved extraction of style values from site theme
diff --git a/packages/php/email-editor/src/Engine/class-site-style-sync-controller.php b/packages/php/email-editor/src/Engine/class-site-style-sync-controller.php
index 9bb52feeae..d4be096f59 100644
--- a/packages/php/email-editor/src/Engine/class-site-style-sync-controller.php
+++ b/packages/php/email-editor/src/Engine/class-site-style-sync-controller.php
@@ -27,6 +27,13 @@ class Site_Style_Sync_Controller {
 	 */
 	private ?WP_Theme_JSON $site_theme = null;

+	/**
+	 * Base theme data for fallback lookups
+	 *
+	 * @var array|null
+	 */
+	private ?array $base_theme_data = null;
+
 	/**
 	 * Email-safe fonts
 	 *
@@ -56,9 +63,13 @@ class Site_Style_Sync_Controller {
 	/**
 	 * Sync site styles to email theme format
 	 *
+	 * @param WP_Theme_JSON|null $base_theme Base theme for fallback values. If null, no fallbacks are used.
 	 * @return array Email-compatible theme data.
 	 */
-	public function sync_site_styles(): array {
+	public function sync_site_styles( ?WP_Theme_JSON $base_theme = null ): array {
+		// Store base theme data for fallback lookups.
+		$this->base_theme_data = $base_theme ? $base_theme->get_data() : null;
+
 		$site_theme = $this->get_site_theme();
 		$site_data  = $site_theme->get_data();

@@ -82,14 +93,15 @@ class Site_Style_Sync_Controller {
 	/**
 	 * Getter for site theme.
 	 *
+	 * @param WP_Theme_JSON|null $base_theme Base theme for fallback values. If null, no fallbacks are used.
 	 * @return ?WP_Theme_JSON Synced site theme.
 	 */
-	public function get_theme(): ?WP_Theme_JSON {
+	public function get_theme( ?WP_Theme_JSON $base_theme = null ): ?WP_Theme_JSON {
 		if ( ! $this->is_sync_enabled() ) {
 			return null;
 		}

-		$synced_data = $this->sync_site_styles();
+		$synced_data = $this->sync_site_styles( $base_theme );

 		if ( empty( $synced_data ) || ! isset( $synced_data['version'] ) ) {
 			return null;
@@ -247,15 +259,31 @@ class Site_Style_Sync_Controller {
 	/**
 	 * Convert site typography styles to email format
 	 *
-	 * @param array $typography_styles Site typography styles.
+	 * @param array  $typography_styles Site typography styles.
+	 * @param string $element Optional element name for context-aware fallbacks.
 	 * @return array Email-compatible typography styles.
 	 */
-	private function convert_typography_styles( array $typography_styles ): array {
+	private function convert_typography_styles( array $typography_styles, string $element = '' ): array {
 		$email_typography = array();

 		// Handle special cases with processors.
 		$this->resolve_and_assign( $typography_styles, 'fontFamily', $email_typography, array( $this, 'convert_to_email_safe_font' ) );
-		$this->resolve_and_assign( $typography_styles, 'fontSize', $email_typography, array( $this, 'convert_to_px_size' ) );
+		$this->resolve_and_assign(
+			$typography_styles,
+			'fontSize',
+			$email_typography,
+			function ( $value ) use ( $element ) {
+				// Try element-specific fallback first, then global fallback.
+				$fallback = null;
+				if ( $element ) {
+					$fallback = $this->get_base_theme_value( array( 'styles', 'elements', $element, 'typography', 'fontSize' ) );
+				}
+				if ( ! $fallback ) {
+					$fallback = $this->get_base_theme_value( array( 'styles', 'typography', 'fontSize' ) );
+				}
+				return $this->convert_to_px_size( $value, $fallback );
+			}
+		);

 		// Handle compatible properties without processing.
 		$compatible_props = array( 'fontWeight', 'fontStyle', 'lineHeight', 'letterSpacing', 'textTransform', 'textDecoration' );
@@ -275,8 +303,23 @@ class Site_Style_Sync_Controller {
 	private function convert_spacing_styles( array $spacing_styles ): array {
 		$email_spacing = array();

-		$this->resolve_and_assign( $spacing_styles, 'padding', $email_spacing, array( $this, 'convert_spacing_values' ) );
-		$this->resolve_and_assign( $spacing_styles, 'blockGap', $email_spacing, array( $this, 'convert_to_px_size' ) );
+		$this->resolve_and_assign(
+			$spacing_styles,
+			'padding',
+			$email_spacing,
+			function ( $value ) {
+				return $this->convert_spacing_values( $value, array( 'styles', 'spacing', 'padding' ) );
+			}
+		);
+		$this->resolve_and_assign(
+			$spacing_styles,
+			'blockGap',
+			$email_spacing,
+			function ( $value ) {
+				$fallback = $this->get_base_theme_value( array( 'styles', 'spacing', 'blockGap' ) );
+				return $this->convert_to_px_size( $value, $fallback );
+			}
+		);

 		// Note: We intentionally skip margin as it's not supported in email renderer.

@@ -297,7 +340,7 @@ class Site_Style_Sync_Controller {

 		foreach ( $supported_elements as $element ) {
 			if ( isset( $element_styles[ $element ] ) ) {
-				$email_elements[ $element ] = $this->convert_element_style( $element_styles[ $element ] );
+				$email_elements[ $element ] = $this->convert_element_style( $element_styles[ $element ], $element );
 			}
 		}

@@ -307,15 +350,16 @@ class Site_Style_Sync_Controller {
 	/**
 	 * Convert individual element style to email format
 	 *
-	 * @param array $element_style Site element style.
+	 * @param array  $element_style Site element style.
+	 * @param string $element_name Element name (e.g., 'h1', 'h2', 'button').
 	 * @return array Email-compatible element style.
 	 */
-	private function convert_element_style( array $element_style ): array {
+	private function convert_element_style( array $element_style, string $element_name = '' ): array {
 		$email_element = array();

 		// Convert typography if present.
 		if ( isset( $element_style['typography'] ) ) {
-			$email_element['typography'] = $this->convert_typography_styles( $element_style['typography'] );
+			$email_element['typography'] = $this->convert_typography_styles( $element_style['typography'], $element_name );
 		}

 		// Convert color if present.
@@ -427,39 +471,62 @@ class Site_Style_Sync_Controller {
 	}

 	/**
-	 * Convert size value to px format.
+	 * Convert size value to px format with optional fallback
 	 *
-	 * @param string $size Original size value.
+	 * @param string      $size Original size value.
+	 * @param string|null $fallback Fallback value to use if conversion fails.
 	 * @return string Size in px format.
 	 */
-	private function convert_to_px_size( string $size ): string {
-		// Replace clamp() with its average value.
+	private function convert_to_px_size( string $size, ?string $fallback = null ): string {
+		$converted = null;
+		// Replace clamp() with its minimum value. We use min because it's emails are most likely to be viewed on smaller screens.
 		if ( stripos( $size, 'clamp(' ) !== false ) {
-			return Styles_Helper::clamp_to_static_px( $size, 'avg' ) ?? $size;
+			$converted = Styles_Helper::clamp_to_static_px( $size, 'min' );
+			// If clamp_to_static_px returns the original value, it failed to convert.
+			if ( $converted === $size ) {
+				$converted = null;
+			}
 		}
-		return Styles_Helper::convert_to_px( $size, false ) ?? $size; // Fallback to original value if conversion fails.
+
+		// Try standard conversion.
+		if ( is_null( $converted ) ) {
+			$converted = Styles_Helper::convert_to_px( $size, false );
+		}
+
+		// If all conversions failed, use fallback if provided.
+		if ( is_null( $converted ) && $fallback ) {
+			return $fallback;
+		}
+
+		// Return converted value or original if conversion failed.
+		return $converted ?? $size;
 	}

 	/**
 	 * Convert spacing values to px format.
 	 *
 	 * @param string|array $spacing_values Original spacing values.
+	 * @param array        $base_path Base path for fallback lookup (e.g., ['styles', 'spacing', 'padding']).
 	 * @return string|array Spacing values in px format.
 	 */
-	private function convert_spacing_values( $spacing_values ) {
+	private function convert_spacing_values( $spacing_values, array $base_path ) {
 		if ( ! is_string( $spacing_values ) && ! is_array( $spacing_values ) ) {
 			return $spacing_values;
 		}

 		if ( is_string( $spacing_values ) ) {
-			return $this->convert_to_px_size( $spacing_values );
+			$fallback = $this->get_base_theme_value( $base_path );
+			return $this->convert_to_px_size( $spacing_values, $fallback );
 		}

 		$px_values = array();

 		foreach ( $spacing_values as $side => $value ) {
 			if ( is_string( $value ) ) {
-				$px_values[ $side ] = $this->convert_to_px_size( $value );
+				// Build path for side-specific fallback (e.g., ['styles', 'spacing', 'padding', 'top']).
+				$side_path          = array_merge( $base_path, array( $side ) );
+				$fallback           = $this->get_base_theme_value( $side_path );
+				$px_values[ $side ] = $this->convert_to_px_size( $value, $fallback );
 			} else {
 				$px_values[ $side ] = $value;
 			}
@@ -467,4 +534,19 @@ class Site_Style_Sync_Controller {

 		return $px_values;
 	}
+
+	/**
+	 * Get value from base theme by path
+	 *
+	 * @param array $path Path array for _wp_array_get (e.g., ['styles', 'typography', 'fontSize']).
+	 * @return string|null Value from base theme or null if not found.
+	 */
+	private function get_base_theme_value( array $path ): ?string {
+		if ( ! $this->base_theme_data ) {
+			return null;
+		}
+
+		$value = _wp_array_get( $this->base_theme_data, $path );
+		return is_string( $value ) ? $value : null;
+	}
 }
diff --git a/packages/php/email-editor/src/Engine/class-theme-controller.php b/packages/php/email-editor/src/Engine/class-theme-controller.php
index 7367700b71..38da1a1b1a 100644
--- a/packages/php/email-editor/src/Engine/class-theme-controller.php
+++ b/packages/php/email-editor/src/Engine/class-theme-controller.php
@@ -81,7 +81,7 @@ class Theme_Controller {
 		// Merge synced styles from current active theme.
 		if ( $this->site_style_sync_controller->is_sync_enabled() ) {
 			/** @var WP_Theme_JSON $site_theme */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort
-			$site_theme = $this->site_style_sync_controller->get_theme();
+			$site_theme = $this->site_style_sync_controller->get_theme( $theme );
 			$theme->merge( $site_theme );
 		}

diff --git a/packages/php/email-editor/tests/integration/Engine/Site_Style_Sync_Controller_Test.php b/packages/php/email-editor/tests/integration/Engine/Site_Style_Sync_Controller_Test.php
index 04b9bf0f4a..4cb2052d10 100644
--- a/packages/php/email-editor/tests/integration/Engine/Site_Style_Sync_Controller_Test.php
+++ b/packages/php/email-editor/tests/integration/Engine/Site_Style_Sync_Controller_Test.php
@@ -500,8 +500,10 @@ class Site_Style_Sync_Controller_Test extends \Email_Editor_Integration_Test_Cas
 		$controller = new class() extends Site_Style_Sync_Controller {
 			/**
 			 * Mock sync_site_styles to return empty data.
+			 *
+			 * @param WP_Theme_JSON|null $base_theme Base theme for fallback values.
 			 */
-			public function sync_site_styles(): array {
+			public function sync_site_styles( ?WP_Theme_JSON $base_theme = null ): array {
 				return array();
 			}
 		};
@@ -913,4 +915,212 @@ class Site_Style_Sync_Controller_Test extends \Email_Editor_Integration_Test_Cas
 		$this->assertArrayHasKey( 'color', $synced_data['styles']['elements']['heading'] );
 		$this->assertArrayNotHasKey( 'text', $synced_data['styles']['elements']['heading']['color'] );
 	}
+
+	/**
+	 * Data provider for fallback mechanism tests.
+	 *
+	 * @return array Test cases with base theme data, site theme data, and expected assertions.
+	 */
+	public function fallback_data_provider(): array {
+		return array(
+			'global fontSize fallback'     => array(
+				'base_theme' => array(
+					'styles' => array(
+						'typography' => array( 'fontSize' => '16px' ),
+					),
+				),
+				'site_theme' => array(
+					'styles' => array(
+						'typography' => array( 'fontSize' => 'min(calc(var(--wp--custom--spacing-unit) * 2), 2vw)' ),
+					),
+				),
+				'assertions' => array(
+					'path'     => array( 'typography', 'fontSize' ),
+					'expected' => '16px',
+				),
+			),
+			'element-specific h1 fallback' => array(
+				'base_theme' => array(
+					'styles' => array(
+						'typography' => array( 'fontSize' => '16px' ),
+						'elements'   => array(
+							'h1' => array(
+								'typography' => array( 'fontSize' => '40px' ),
+							),
+						),
+					),
+				),
+				'site_theme' => array(
+					'styles' => array(
+						'elements' => array(
+							'h1' => array(
+								'typography' => array( 'fontSize' => 'clamp(var(--min), var(--preferred), var(--max))' ),
+							),
+						),
+					),
+				),
+				'assertions' => array(
+					'path'     => array( 'elements', 'h1', 'typography', 'fontSize' ),
+					'expected' => '40px',
+				),
+			),
+			'blockGap spacing fallback'    => array(
+				'base_theme' => array(
+					'styles' => array(
+						'spacing' => array( 'blockGap' => '16px' ),
+					),
+				),
+				'site_theme' => array(
+					'styles' => array(
+						'spacing' => array( 'blockGap' => 'max(var(--gap-min), var(--gap-preferred))' ),
+					),
+				),
+				'assertions' => array(
+					'path'     => array( 'spacing', 'blockGap' ),
+					'expected' => '16px',
+				),
+			),
+			'padding object with sides'    => array(
+				'base_theme' => array(
+					'styles' => array(
+						'spacing' => array(
+							'padding' => array(
+								'top'    => '20px',
+								'right'  => '20px',
+								'bottom' => '20px',
+								'left'   => '20px',
+							),
+						),
+					),
+				),
+				'site_theme' => array(
+					'styles' => array(
+						'spacing' => array(
+							'padding' => array(
+								'top'    => 'min(var(--padding-top), 5vw)',
+								'right'  => 'min(var(--padding-right), 5vw)',
+								'bottom' => 'min(var(--padding-bottom), 5vw)',
+								'left'   => 'min(var(--padding-left), 5vw)',
+							),
+						),
+					),
+				),
+				'assertions' => array(
+					'path'     => array( 'spacing', 'padding' ),
+					'expected' => array(
+						'top'    => '20px',
+						'right'  => '20px',
+						'bottom' => '20px',
+						'left'   => '20px',
+					),
+				),
+			),
+		);
+	}
+
+	/**
+	 * Test fallback mechanism uses base theme defaults.
+	 *
+	 * @dataProvider fallback_data_provider
+	 *
+	 * @param array $base_theme_styles Base theme styles configuration.
+	 * @param array $site_theme_styles Site theme styles configuration.
+	 * @param array $assertions Expected assertions with path and expected value.
+	 */
+	public function test_fallback_uses_base_theme_defaults( array $base_theme_styles, array $site_theme_styles, array $assertions ): void {
+		// Create base theme.
+		$base_theme_data = array_merge(
+			array(
+				'version'  => 3,
+				'settings' => array(),
+			),
+			$base_theme_styles
+		);
+		$base_theme      = new WP_Theme_JSON( $base_theme_data, 'default' );
+
+		// Create site theme.
+		$site_theme_data = array_merge(
+			array(
+				'version'  => 3,
+				'settings' => array(),
+			),
+			$site_theme_styles
+		);
+		$site_theme      = new WP_Theme_JSON( $site_theme_data );
+
+		// Use reflection to set the site theme.
+		$reflection          = new \ReflectionClass( $this->controller );
+		$site_theme_property = $reflection->getProperty( 'site_theme' );
+		$site_theme_property->setAccessible( true );
+		$site_theme_property->setValue( $this->controller, $site_theme );
+
+		$synced_data = $this->controller->sync_site_styles( $base_theme );
+
+		// Navigate to the expected path and verify the value.
+		$current = $synced_data['styles'];
+		foreach ( $assertions['path'] as $key ) {
+			$this->assertArrayHasKey( $key, $current );
+			$current = $current[ $key ];
+		}
+		$this->assertEquals( $assertions['expected'], $current );
+	}
+
+	/**
+	 * Test no fallback when base theme is not provided (backwards compatibility).
+	 */
+	public function test_no_fallback_when_base_theme_not_provided(): void {
+		$site_theme_data = array(
+			'version'  => 3,
+			'settings' => array(),
+			'styles'   => array(
+				'typography' => array(
+					'fontSize' => 'min(var(--size), 2vw)',
+				),
+			),
+		);
+		$site_theme      = new WP_Theme_JSON( $site_theme_data );
+
+		$reflection          = new \ReflectionClass( $this->controller );
+		$site_theme_property = $reflection->getProperty( 'site_theme' );
+		$site_theme_property->setAccessible( true );
+		$site_theme_property->setValue( $this->controller, $site_theme );
+
+		$synced_data = $this->controller->sync_site_styles();
+
+		$this->assertArrayHasKey( 'typography', $synced_data['styles'] );
+		$this->assertEquals( 'min(var(--size), 2vw)', $synced_data['styles']['typography']['fontSize'] );
+	}
+
+	/**
+	 * Test already-valid px values don't use fallback.
+	 */
+	public function test_valid_px_values_dont_use_fallback(): void {
+		$base_theme_data = array(
+			'version'  => 3,
+			'settings' => array(),
+			'styles'   => array(
+				'typography' => array( 'fontSize' => '16px' ),
+			),
+		);
+		$base_theme      = new WP_Theme_JSON( $base_theme_data, 'default' );
+
+		$site_theme_data = array(
+			'version'  => 3,
+			'settings' => array(),
+			'styles'   => array(
+				'typography' => array( 'fontSize' => '24px' ),
+			),
+		);
+		$site_theme      = new WP_Theme_JSON( $site_theme_data );
+
+		$reflection          = new \ReflectionClass( $this->controller );
+		$site_theme_property = $reflection->getProperty( 'site_theme' );
+		$site_theme_property->setAccessible( true );
+		$site_theme_property->setValue( $this->controller, $site_theme );
+
+		$synced_data = $this->controller->sync_site_styles( $base_theme );
+
+		$this->assertArrayHasKey( 'typography', $synced_data['styles'] );
+		$this->assertEquals( '24px', $synced_data['styles']['typography']['fontSize'] );
+	}
 }