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'] );
+ }
}