Commit dba25a6e0c9 for woocommerce
commit dba25a6e0c90457fb6b2dd713b6f32c50193f87d
Author: Pavel Dohnal <pavel.dohnal@automattic.com>
Date: Thu Nov 12 15:59:44 2026 +0100
Leave site colors that email clients can’t render out of emails (#70869)
* Leave unrenderable site colors out of email themes
The site style sync copied the site’s color palette into the email
theme as is. A theme can define a palette color with a CSS function:
Twenty Twenty-Five’s accent-6 is color-mix(). Email clients drop such
a declaration, and the value was sent on every block that used the
color, by slug or by its has-* class.
Palette colors that email clients can’t render are now left out when
the palette is synced. The email editor no longer offers them, and a
block that names one keeps the surrounding color, as it does for any
color the palette doesn’t define.
Site text and background colors get the same check. Such a value used
to replace the email theme’s own color and then vanish at render,
which left the email without a text or background color. A pair is
now synced only when clients can render both, so text never lands on
a background it wasn’t chosen for.
* Leave undefined preset variables out of sent emails
A block can point at a palette color through its preset variable, as
a link color does. When the email theme doesn’t define that preset,
the variable stayed in the style attribute, and no email client can
resolve it. That now includes the palette colors the previous commit
stops syncing.
The declaration is removed after the known variables are replaced, so
the sent HTML only carries values a client can use. The client would
have dropped the declaration anyway.
* Add changelog entry for unrenderable site colors in emails
* Use typographic apostrophes in the new email color comments
* Keep site colors that point at a palette color
The check for colors email clients can’t render also rejected every
var(), so a color that only points at a palette color was dropped.
Twenty Twenty-Four’s Fossil and Ice styles set their button text to
core’s white this way, and their email buttons lost the site colors.
A palette color that names another palette color was dropped too.
Such a reference is not for an email client to read: the renderer
swaps it for the color before sending. The sync can’t judge it either,
because it doesn’t see the whole email palette. References are now
synced as before, and the renderer resolves them or leaves the
declaration out.
* Test that only palette references pass the color sync
The sync keeps a site color that points at a palette color. No test
checked that other variables, or a function wrapped around such a
reference, are still left out, so the check could be loosened to let
them through with every test green.
* Resolve preset variables that point at another
A palette color can name another palette color. The postprocessor
replaced the variables in one pass, in palette order, so such a color
only resolved when it was listed before its target. Listed after it,
the reference stayed behind and its declaration was then removed as an
undefined preset: a link colored with it was sent without a color.
The known variables are now replaced in up to three passes before the
undefined ones are removed, so the order no longer matters.
diff --git a/packages/php/email-editor/changelog/fix-nl-859-unrenderable-site-colors b/packages/php/email-editor/changelog/fix-nl-859-unrenderable-site-colors
new file mode 100644
index 00000000000..89160ab9811
--- /dev/null
+++ b/packages/php/email-editor/changelog/fix-nl-859-unrenderable-site-colors
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Use the email’s own colors when a site theme color is one email clients can’t show, such as Twenty Twenty-Five’s Accent 6.
diff --git a/packages/php/email-editor/src/Engine/Renderer/ContentRenderer/Postprocessors/class-variables-postprocessor.php b/packages/php/email-editor/src/Engine/Renderer/ContentRenderer/Postprocessors/class-variables-postprocessor.php
index b54dbd3b1f5..2b95d972e88 100644
--- a/packages/php/email-editor/src/Engine/Renderer/ContentRenderer/Postprocessors/class-variables-postprocessor.php
+++ b/packages/php/email-editor/src/Engine/Renderer/ContentRenderer/Postprocessors/class-variables-postprocessor.php
@@ -56,8 +56,17 @@ class Variables_Postprocessor implements Postprocessor {
$style = $processor->get_attribute( 'style' );
if ( null !== $style && true !== $style ) {
- // Replace CSS variables with their values.
- $processed_style = preg_replace( array_keys( $replacements ), array_values( $replacements ), $style );
+ // Replace CSS variables with their values. A value can point at another variable,
+ // and one pass only resolves it when that variable comes later in the map.
+ $processed_style = $style;
+ for ( $pass = 0; $pass < 3 && null !== $processed_style; $pass++ ) {
+ $processed_style = preg_replace( array_keys( $replacements ), array_values( $replacements ), $processed_style );
+ }
+
+ // No email client can resolve a preset the theme does not define, so its declaration goes.
+ if ( null !== $processed_style ) {
+ $processed_style = preg_replace( '/[^;]*var\(--wp--preset--[^;]*;?/i', '', $processed_style );
+ }
if ( null !== $processed_style ) {
$processor->set_attribute( 'style', $processed_style );
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 20a0f2ddbdf..b50b62eb440 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
@@ -177,9 +177,16 @@ class Site_Style_Sync_Controller {
private function sync_settings_data( array $site_settings ): array {
$email_settings = array();
- // Sync color palette.
+ // Sync color palette, without the colors email clients can’t render.
if ( isset( $site_settings['color']['palette'] ) ) {
- $email_settings['color']['palette'] = $site_settings['color']['palette'];
+ $email_settings['color']['palette'] = array_values(
+ array_filter(
+ $site_settings['color']['palette'],
+ function ( $preset ) {
+ return $this->is_usable_color( $preset['color'] ?? null );
+ }
+ )
+ );
}
return $email_settings;
@@ -244,6 +251,9 @@ class Site_Style_Sync_Controller {
/**
* Convert site color styles to email format
*
+ * Returns nothing when email clients can’t render one of the colors, so the email theme’s own
+ * text and background apply together. One site color on the other default can be unreadable.
+ *
* @param array $color_styles Site color styles.
* @return array Email-compatible color styles.
*/
@@ -253,9 +263,32 @@ class Site_Style_Sync_Controller {
$this->resolve_and_assign( $color_styles, 'background', $email_colors );
$this->resolve_and_assign( $color_styles, 'text', $email_colors );
+ foreach ( $email_colors as $color ) {
+ if ( ! $this->is_usable_color( $color ) ) {
+ return array();
+ }
+ }
+
return $email_colors;
}
+ /**
+ * Whether the email can use a site color.
+ *
+ * Either email clients can render it, or it points at a palette color. Only the renderer sees
+ * the whole email palette, so it swaps such a reference for its color or leaves it out.
+ *
+ * @param mixed $color Color value from the site theme.
+ * @return bool
+ */
+ private function is_usable_color( $color ): bool {
+ if ( is_string( $color ) && 1 === preg_match( '/^var\(--wp--preset--color--[\w-]+\)$/', trim( $color ) ) ) {
+ return true;
+ }
+
+ return Styles_Helper::is_email_safe_color( $color );
+ }
+
/**
* Convert site typography styles to email format
*
diff --git a/packages/php/email-editor/src/Integrations/Utils/class-styles-helper.php b/packages/php/email-editor/src/Integrations/Utils/class-styles-helper.php
index 50e66d8cb9c..ce1e4b56fc1 100644
--- a/packages/php/email-editor/src/Integrations/Utils/class-styles-helper.php
+++ b/packages/php/email-editor/src/Integrations/Utils/class-styles-helper.php
@@ -89,6 +89,31 @@ class Styles_Helper {
return 1 === preg_match( '/^[a-z][a-z0-9-]*\(.*\)$/', $value );
}
+ /**
+ * Whether email clients can render a color value.
+ *
+ * Hex, a color keyword, and rgb() or hsl() with plain arguments are safe. Any other CSS function,
+ * such as color-mix(), light-dark() or var(), is not: clients drop the whole declaration.
+ *
+ * @param mixed $color CSS color value. Untyped, because theme data arrives through filters.
+ * @return bool
+ */
+ public static function is_email_safe_color( $color ): bool {
+ if ( ! is_string( $color ) ) {
+ return false;
+ }
+
+ $color = strtolower( trim( $color ) );
+
+ if ( false !== strpos( $color, '(' ) ) {
+ // Nothing nested: rgb(var(--red), 0, 0) is as unusable as var() alone.
+ return 1 === preg_match( '/^(rgb|hsl)a?\([^()]+\)$/', $color );
+ }
+
+ // Hex, or a keyword such as `white`. Keywords are not checked against the CSS color names.
+ return self::is_color_literal( $color ) || 1 === preg_match( '/^[a-z]+$/', $color );
+ }
+
/**
* Every color the palette defines, one entry per slug, highest priority origin first.
*
diff --git a/packages/php/email-editor/tests/integration/Engine/Renderer/ContentRenderer/Postprocessors/Variables_Postprocessor_Test.php b/packages/php/email-editor/tests/integration/Engine/Renderer/ContentRenderer/Postprocessors/Variables_Postprocessor_Test.php
index 16950b071d3..d24d4c50848 100644
--- a/packages/php/email-editor/tests/integration/Engine/Renderer/ContentRenderer/Postprocessors/Variables_Postprocessor_Test.php
+++ b/packages/php/email-editor/tests/integration/Engine/Renderer/ContentRenderer/Postprocessors/Variables_Postprocessor_Test.php
@@ -54,4 +54,37 @@ class Variables_Postprocessor_Test extends \Email_Editor_Unit_Test {
$result = $this->postprocessor->postprocess( $html );
$this->assertEquals( '<div style="padding:10px;margin:20px"><p style="color:white;padding-left:10px;">Helloo I have padding var(--wp--preset--spacing--10); </p></div>', $result );
}
+
+ /**
+ * Test it leaves out a declaration whose preset the theme does not define.
+ *
+ * No email client can resolve such a variable, so the declaration is one the client would drop.
+ */
+ public function testItLeavesOutDeclarationsWithUndefinedPresets(): void {
+ $this->theme_controller_mock->method( 'get_variables_values_map' )->willReturn(
+ array( '--wp--preset--spacing--10' => '10px' )
+ );
+ $html = '<a style="text-decoration: none; color: var(--wp--preset--color--accent-6); padding: var(--wp--preset--spacing--10)">Link</a><p style="color:var(--wp--preset--color--accent-6)">I mention var(--wp--preset--color--accent-6);</p>';
+ $result = $this->postprocessor->postprocess( $html );
+ $this->assertSame( '<a style="text-decoration: none; padding: 10px">Link</a><p style="">I mention var(--wp--preset--color--accent-6);</p>', $result );
+ }
+
+ /**
+ * Test it resolves a preset whose value points at another preset, whatever their order.
+ *
+ * A palette color can name another palette color. The target is listed first here, so one
+ * pass over the variables leaves the reference behind.
+ */
+ public function testItResolvesAPresetThatPointsAtAnotherPreset(): void {
+ $this->theme_controller_mock->method( 'get_variables_values_map' )->willReturn(
+ array(
+ '--wp--preset--color--contrast' => '#111111',
+ '--wp--preset--color--primary' => 'var(--wp--preset--color--contrast)',
+ '--wp--preset--color--brand' => 'var(--wp--preset--color--accent-6)',
+ )
+ );
+ $html = '<a style="color: var(--wp--preset--color--primary)">Resolved</a><a style="margin: 0; color: var(--wp--preset--color--brand)">Points at nothing</a>';
+ $result = $this->postprocessor->postprocess( $html );
+ $this->assertSame( '<a style="color: #111111">Resolved</a><a style="margin: 0;">Points at nothing</a>', $result );
+ }
}
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 fe7edc72bc5..208586ba821 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
@@ -162,6 +162,153 @@ class Site_Style_Sync_Controller_Test extends \Email_Editor_Integration_Test_Cas
$this->assertEquals( 'Primary Color', $synced_data['settings']['color']['palette'][0]['name'] );
}
+ /**
+ * Test a palette color email clients can’t render is not synced.
+ *
+ * Twenty Twenty-Five defines `accent-6` as color-mix(). It comes first here because the synced
+ * palette has to stay a list: WP_Theme_JSON reads one without a first entry as a set of origins.
+ */
+ public function test_sync_site_styles_leaves_out_palette_colors_email_clients_cannot_render(): void {
+ $this->use_site_theme(
+ array(
+ 'version' => 3,
+ 'settings' => array(
+ 'color' => array(
+ 'palette' => array(
+ array(
+ 'slug' => 'accent-6',
+ 'color' => 'color-mix(in srgb, currentColor 20%, transparent)',
+ 'name' => 'Accent 6',
+ ),
+ array(
+ 'slug' => 'accent-1',
+ 'color' => '#ffee58',
+ 'name' => 'Accent 1',
+ ),
+ array(
+ 'slug' => 'primary',
+ 'color' => 'var(--wp--preset--color--accent-1)',
+ 'name' => 'Primary',
+ ),
+ // Only a plain reference to a palette color is kept. The email has no other variables.
+ array(
+ 'slug' => 'brand',
+ 'color' => 'var(--wp--custom--brand)',
+ 'name' => 'Brand',
+ ),
+ array(
+ 'slug' => 'tint',
+ 'color' => 'color-mix(in srgb, var(--wp--preset--color--accent-1) 20%, transparent)',
+ 'name' => 'Tint',
+ ),
+ ),
+ ),
+ ),
+ )
+ );
+
+ $synced_data = $this->controller->sync_site_styles();
+
+ $this->assertEquals(
+ array(
+ array(
+ 'slug' => 'accent-1',
+ 'color' => '#ffee58',
+ 'name' => 'Accent 1',
+ ),
+ array(
+ 'slug' => 'primary',
+ 'color' => 'var(--wp--preset--color--accent-1)',
+ 'name' => 'Primary',
+ ),
+ ),
+ $synced_data['settings']['color']['palette'],
+ 'A color that points at another palette color is kept: the renderer resolves it.'
+ );
+
+ $synced_theme = $this->controller->get_theme();
+ $this->assertInstanceOf( WP_Theme_JSON::class, $synced_theme );
+ $this->assertSame( array( 'accent-1', 'primary' ), array_column( $synced_theme->get_settings()['color']['palette']['theme'], 'slug' ) );
+ }
+
+ /**
+ * Test site colors email clients can’t render are not synced, so the email theme’s own apply.
+ *
+ * Text and background go together: a site text color on the email theme’s background, or the
+ * other way round, can be unreadable.
+ */
+ public function test_sync_site_styles_leaves_out_style_colors_email_clients_cannot_render(): void {
+ $this->use_site_theme(
+ array(
+ 'version' => 3,
+ 'styles' => array(
+ 'color' => array(
+ 'text' => '#111111',
+ 'background' => 'light-dark(#ffffff, #000000)',
+ ),
+ 'elements' => array(
+ 'heading' => array( 'color' => array( 'text' => 'color-mix(in srgb, #ff0000 85%, #000000)' ) ),
+ 'button' => array( 'color' => array( 'text' => 'var(--wp--custom--brand)' ) ),
+ 'link' => array( 'color' => array( 'text' => '#0000ff' ) ),
+ ),
+ ),
+ )
+ );
+
+ $synced_styles = $this->controller->sync_site_styles()['styles'];
+
+ $this->assertSame( array(), $synced_styles['color'], 'A text color is not synced without its background.' );
+ $this->assertSame( array(), $synced_styles['elements']['heading']['color'] );
+ $this->assertSame( array(), $synced_styles['elements']['button']['color'], 'A variable that is not a palette color is not synced.' );
+ $this->assertSame( array( 'text' => '#0000ff' ), $synced_styles['elements']['link']['color'], 'A color email clients can render is still synced.' );
+ }
+
+ /**
+ * Test site colors that point at a palette color are synced as they are.
+ *
+ * The site theme can’t resolve a reference to one of core’s colors, and the renderer does it
+ * later. Twenty Twenty-Four’s Fossil style sets its button text this way.
+ */
+ public function test_sync_site_styles_keeps_style_colors_that_point_at_a_palette_color(): void {
+ $this->use_site_theme(
+ array(
+ 'version' => 3,
+ 'styles' => array(
+ 'color' => array(
+ 'text' => 'var:preset|color|black',
+ 'background' => 'var:preset|color|pale-pink',
+ ),
+ 'elements' => array(
+ 'button' => array(
+ 'color' => array(
+ 'text' => 'var(--wp--preset--color--white)',
+ 'background' => '#65574e',
+ ),
+ ),
+ ),
+ ),
+ )
+ );
+
+ $synced_styles = $this->controller->sync_site_styles()['styles'];
+
+ // WP_Theme_JSON rewrites the `var:preset|` form as a CSS variable.
+ $this->assertSame(
+ array(
+ 'background' => 'var(--wp--preset--color--pale-pink)',
+ 'text' => 'var(--wp--preset--color--black)',
+ ),
+ $synced_styles['color']
+ );
+ $this->assertSame(
+ array(
+ 'background' => '#65574e',
+ 'text' => 'var(--wp--preset--color--white)',
+ ),
+ $synced_styles['elements']['button']['color']
+ );
+ }
+
/**
* Test sync_site_styles with typography styles.
*/
@@ -1336,4 +1483,18 @@ class Site_Style_Sync_Controller_Test extends \Email_Editor_Integration_Test_Cas
}
}
}
+
+ /**
+ * Make the controller sync the given theme data as the site theme.
+ *
+ * @param array $theme_data Theme JSON data.
+ */
+ private function use_site_theme( array $theme_data ): void {
+ add_filter(
+ 'woocommerce_email_editor_site_theme',
+ static function () use ( $theme_data ) {
+ return new WP_Theme_JSON( $theme_data, 'theme' );
+ }
+ );
+ }
}
diff --git a/packages/php/email-editor/tests/integration/Integrations/Core/Renderer/Renderer_Test.php b/packages/php/email-editor/tests/integration/Integrations/Core/Renderer/Renderer_Test.php
index 03bd27ed664..9fb0015a59b 100644
--- a/packages/php/email-editor/tests/integration/Integrations/Core/Renderer/Renderer_Test.php
+++ b/packages/php/email-editor/tests/integration/Integrations/Core/Renderer/Renderer_Test.php
@@ -287,6 +287,126 @@ class Renderer_Test extends \Email_Editor_Integration_Test_Case {
$this->assertStringContainsString( 'background-color: #000', $style ); // black is #000.
}
+ /**
+ * Test a site palette color email clients can’t render is left out of the sent email.
+ *
+ * Twenty Twenty-Five defines `accent-6` as color-mix(). A block reaches a palette color by its
+ * slug, by its `has-*` class and by its preset variable, so the test checks the final HTML
+ * rather than one lookup.
+ */
+ public function testItLeavesOutSitePaletteColorsEmailClientsCannotRender(): void {
+ add_filter(
+ 'woocommerce_email_editor_site_theme',
+ static function () {
+ return new \WP_Theme_JSON(
+ array(
+ 'version' => 3,
+ 'settings' => array(
+ 'color' => array(
+ 'palette' => array(
+ array(
+ 'slug' => 'accent-6',
+ 'name' => 'Accent 6',
+ 'color' => 'color-mix(in srgb, currentColor 20%, transparent)',
+ ),
+ array(
+ 'slug' => 'accent-1',
+ 'name' => 'Accent 1',
+ 'color' => '#ffee58',
+ ),
+ ),
+ ),
+ ),
+ ),
+ 'theme'
+ );
+ }
+ );
+ $email_post_id = $this->factory->post->create(
+ array(
+ 'post_content' => '<!-- wp:paragraph {"textColor":"accent-6"} --><p class="text-test has-accent-6-color has-text-color">Text</p><!-- /wp:paragraph -->
+ <!-- wp:paragraph {"backgroundColor":"accent-6"} --><p class="background-test has-accent-6-background-color has-background">Background</p><!-- /wp:paragraph -->
+ <!-- wp:paragraph {"style":{"elements":{"link":{"color":{"text":"var:preset|color|accent-6"}}}}} --><p class="has-link-color"><a class="link-test" href="https://example.com">Link</a></p><!-- /wp:paragraph -->
+ <!-- wp:paragraph {"textColor":"accent-1"} --><p class="control-test has-accent-1-color has-text-color">Control</p><!-- /wp:paragraph -->',
+ )
+ );
+ $this->assertIsInt( $email_post_id );
+ $email_post = get_post( $email_post_id );
+ $this->assertInstanceOf( WP_Post::class, $email_post );
+
+ $rendered = $this->renderer->render( $email_post, 'Subject', '', 'en' );
+
+ $this->assertStringNotContainsString( 'color-mix', $rendered['html'], 'A color email clients cannot render should not be sent.' );
+ $this->assertStringNotContainsString( 'color', $this->extractBlockStyle( $rendered['html'], 'text-test', 'p' ), 'The text should keep the surrounding color.' );
+ $this->assertStringNotContainsString( 'background', $this->extractBlockStyle( $rendered['html'], 'background-test', 'td' ), 'The block should keep the surrounding background.' );
+ $this->assertStringNotContainsString( 'color', $this->extractBlockStyle( $rendered['html'], 'link-test', 'a' ), 'The link should not point at a color the email does not have.' );
+ $this->assertStringContainsString( 'color: #ffee58', $this->extractBlockStyle( $rendered['html'], 'control-test', 'p' ), 'A hex palette color should still be inlined.' );
+ }
+
+ /**
+ * Test site colors that point at a palette color still reach the sent email.
+ *
+ * A palette color can name another one, and a site style can name one of core’s colors, as the
+ * button text of Twenty Twenty-Four’s Fossil style does.
+ */
+ public function testItKeepsSiteColorsThatPointAtAPaletteColor(): void {
+ add_filter(
+ 'woocommerce_email_editor_site_theme',
+ static function () {
+ return new \WP_Theme_JSON(
+ array(
+ 'version' => 3,
+ 'settings' => array(
+ 'color' => array(
+ 'palette' => array(
+ array(
+ 'slug' => 'contrast',
+ 'name' => 'Contrast',
+ 'color' => '#111111',
+ ),
+ array(
+ 'slug' => 'primary',
+ 'name' => 'Primary',
+ 'color' => 'var(--wp--preset--color--contrast)',
+ ),
+ ),
+ ),
+ ),
+ 'styles' => array(
+ 'elements' => array(
+ 'button' => array(
+ 'color' => array(
+ 'text' => 'var(--wp--preset--color--white)',
+ 'background' => '#65574e',
+ ),
+ ),
+ ),
+ ),
+ ),
+ 'theme'
+ );
+ }
+ );
+ $email_post_id = $this->factory->post->create(
+ array(
+ 'post_content' => '<!-- wp:paragraph {"textColor":"primary"} --><p class="alias-test has-primary-color has-text-color">Text</p><!-- /wp:paragraph -->
+ <!-- wp:paragraph {"style":{"elements":{"link":{"color":{"text":"var:preset|color|primary"}}}}} --><p class="has-link-color"><a class="alias-link-test" href="https://example.com">Link</a></p><!-- /wp:paragraph -->
+ <!-- wp:button --><div class="wp-block-button button-test"><a class="wp-block-button__link wp-element-button">Button</a></div><!-- /wp:button -->',
+ )
+ );
+ $this->assertIsInt( $email_post_id );
+ $email_post = get_post( $email_post_id );
+ $this->assertInstanceOf( WP_Post::class, $email_post );
+
+ $rendered = $this->renderer->render( $email_post, 'Subject', '', 'en' );
+
+ $this->assertStringContainsString( 'color: #111111', $this->extractBlockStyle( $rendered['html'], 'alias-test', 'p' ) );
+ $this->assertStringContainsString( 'color: #111111', $this->extractBlockStyle( $rendered['html'], 'alias-link-test', 'a' ), 'A link color that points at the alias should resolve too.' );
+ $button_style = $this->extractBlockStyle( $rendered['html'], 'button-test', 'td' );
+ $this->assertStringContainsString( 'background-color: #65574e', $button_style );
+ $this->assertStringContainsString( 'color: #ffffff', $button_style );
+ }
+
/**
* Extracts the HTML of a block
*
diff --git a/packages/php/email-editor/tests/unit/Integrations/Utils/Styles_Helper_Test.php b/packages/php/email-editor/tests/unit/Integrations/Utils/Styles_Helper_Test.php
index 74fb817033b..dbbf85a65f4 100644
--- a/packages/php/email-editor/tests/unit/Integrations/Utils/Styles_Helper_Test.php
+++ b/packages/php/email-editor/tests/unit/Integrations/Utils/Styles_Helper_Test.php
@@ -162,6 +162,51 @@ class Styles_Helper_Test extends \Email_Editor_Unit_Test {
$this->assertFalse( Styles_Helper::is_color_literal( '' ) );
}
+ /**
+ * Color values, and whether email clients can render them.
+ *
+ * @return array<string, array{mixed, bool}>
+ */
+ public function emailSafeColorProvider(): array {
+ return array(
+ 'hex' => array( '#abcdef', true ),
+ 'short hex' => array( '#abc', true ),
+ 'hex with alpha' => array( '#aabbccdd', true ),
+ 'uppercase hex in spaces' => array( ' #ABCDEF ', true ),
+ 'rgb' => array( 'rgb(1, 2, 3)', true ),
+ 'rgba' => array( 'rgba(1, 2, 3, 0.5)', true ),
+ 'hsl, space separated' => array( 'hsl(120deg 50% 50%)', true ),
+ 'hsla, uppercase' => array( 'HSLA(120, 50%, 50%, 0.3)', true ),
+ 'keyword' => array( 'white', true ),
+ 'keyword, mixed case' => array( 'currentColor', true ),
+
+ // Twenty Twenty-Five’s accent-6.
+ 'color-mix' => array( 'color-mix(in srgb, currentColor 20%, transparent)', false ),
+ 'light-dark' => array( 'light-dark(#ffffff, #000000)', false ),
+ 'preset variable' => array( 'var(--wp--preset--color--base)', false ),
+ 'oklch' => array( 'oklch(0.7 0.1 200)', false ),
+ 'rgb with a nested variable' => array( 'rgb(var(--red), 0, 0)', false ),
+ 'rgba with calc' => array( 'rgba(0, 0, 0, calc(1 - 0.5))', false ),
+ 'unclosed function' => array( 'rgb(1, 2, 3', false ),
+ 'incomplete hex' => array( '#zz', false ),
+ 'not a color value' => array( 'dark-gray', false ),
+ 'empty' => array( '', false ),
+ 'not a string' => array( array( '#abcdef' ), false ),
+ );
+ }
+
+ /**
+ * Test it tells a color email clients can render from one they drop.
+ *
+ * @dataProvider emailSafeColorProvider
+ *
+ * @param mixed $color Color value.
+ * @param bool $expected Whether email clients can render it.
+ */
+ public function testItIdentifiesEmailSafeColors( $color, bool $expected ): void {
+ $this->assertSame( $expected, Styles_Helper::is_email_safe_color( $color ) );
+ }
+
/**
* Test it resolves a slug against every palette origin, highest priority first.
*/