From 7940c80041db0b135b5453063f578e4ba4400d8f Mon Sep 17 00:00:00 2001 From: Ramon Date: Thu, 12 Mar 2026 18:00:26 +1100 Subject: [PATCH 1/3] Enhancement: Update WP_Theme_JSON::to_ruleset to skip non-scalar values and cast numeric values to strings. This change ensures that only plain string values are included in the generated CSS ruleset, improving the handling of declarations. --- src/wp-includes/class-wp-theme-json.php | 14 +++++++- tests/phpunit/tests/theme/wpThemeJson.php | 43 +++++++++++++++++++++-- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/src/wp-includes/class-wp-theme-json.php b/src/wp-includes/class-wp-theme-json.php index 5abd2817b8aa4..fb09da0930a93 100644 --- a/src/wp-includes/class-wp-theme-json.php +++ b/src/wp-includes/class-wp-theme-json.php @@ -1982,6 +1982,7 @@ protected function get_css_variables( $nodes, $origins ) { * creates the corresponding ruleset. * * @since 5.8.0 + * @since 7.1.0 Skip declarations whose value is not a plain string (booleans, arrays, objects, etc.). * * @param string $selector CSS selector. * @param array $declarations List of declarations. @@ -1995,7 +1996,18 @@ protected static function to_ruleset( $selector, $declarations ) { $declaration_block = array_reduce( $declarations, static function ( $carry, $element ) { - return $carry .= $element['name'] . ': ' . $element['value'] . ';'; }, + $value = $element['value']; + + if ( is_numeric( $value ) ) { + $value = (string) $value; + } + + if ( ! is_string( $value ) ) { + return $carry; + } + + return $carry .= $element['name'] . ': ' . $value . ';'; + }, '' ); diff --git a/tests/phpunit/tests/theme/wpThemeJson.php b/tests/phpunit/tests/theme/wpThemeJson.php index 95de67c69f98c..3f232ba6d3f38 100644 --- a/tests/phpunit/tests/theme/wpThemeJson.php +++ b/tests/phpunit/tests/theme/wpThemeJson.php @@ -4138,6 +4138,7 @@ public function test_get_styles_with_content_width() { * @ticket 60936 * @ticket 61165 * @ticket 61829 + * @ticket xxxx */ public function test_get_styles_with_appearance_tools() { $theme_json = new WP_Theme_JSON( @@ -4150,11 +4151,11 @@ public function test_get_styles_with_appearance_tools() { ); $metadata = array( - 'path' => array( 'settings' ), + 'path' => array( 'styles' ), 'selector' => 'body', ); - $expected = ':where(body) { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }:where(.wp-site-blocks) > * { margin-block-start: ; margin-block-end: 0; }:where(.wp-site-blocks) > :first-child { margin-block-start: 0; }:where(.wp-site-blocks) > :last-child { margin-block-end: 0; }:root { --wp--style--block-gap: ; }:root :where(.is-layout-flow) > :first-child{margin-block-start: 0;}:root :where(.is-layout-flow) > :last-child{margin-block-end: 0;}:root :where(.is-layout-flow) > *{margin-block-start: 1;margin-block-end: 0;}:root :where(.is-layout-constrained) > :first-child{margin-block-start: 0;}:root :where(.is-layout-constrained) > :last-child{margin-block-end: 0;}:root :where(.is-layout-constrained) > *{margin-block-start: 1;margin-block-end: 0;}:root :where(.is-layout-flex){gap: 1;}:root :where(.is-layout-grid){gap: 1;}.is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-constrained > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-constrained > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)){margin-left: auto !important;margin-right: auto !important;}body .is-layout-flex{display: flex;}.is-layout-flex{flex-wrap: wrap;align-items: center;}.is-layout-flex > :is(*, div){margin: 0;}body .is-layout-grid{display: grid;}.is-layout-grid > :is(*, div){margin: 0;}'; + $expected = ':where(body) { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }:where(.wp-site-blocks) > * { margin-block-start: ; margin-block-end: 0; }:where(.wp-site-blocks) > :first-child { margin-block-start: 0; }:where(.wp-site-blocks) > :last-child { margin-block-end: 0; }:root { --wp--style--block-gap: ; }.is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-constrained > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-constrained > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)){margin-left: auto !important;margin-right: auto !important;}body .is-layout-flex{display: flex;}.is-layout-flex{flex-wrap: wrap;align-items: center;}.is-layout-flex > :is(*, div){margin: 0;}body .is-layout-grid{display: grid;}.is-layout-grid > :is(*, div){margin: 0;}'; $this->assertSame( $expected, $theme_json->get_root_layout_rules( WP_Theme_JSON::ROOT_BLOCK_SELECTOR, $metadata ) ); } @@ -7054,4 +7055,42 @@ public function test_sanitize_preserves_null_schema_behavior() { $this->assertSame( 'string-value', $settings['appearanceTools'], 'Appearance tools should be string value' ); $this->assertSame( array( 'nested' => 'value' ), $settings['custom'], 'Custom should be array value' ); } + + /** + * @covers WP_Theme_JSON::to_ruleset + * + * @ticket xxxx + */ + public function test_to_ruleset_skips_non_scalar_values_and_casts_numerics() { + $reflection = new ReflectionMethod( WP_Theme_JSON::class, 'to_ruleset' ); + $reflection->setAccessible( true ); + $declarations = array( + array( + 'name' => 'color', + 'value' => 'red', + ), + array( + 'name' => 'opacity', + 'value' => true, + ), + array( + 'name' => 'margin', + 'value' => 0, + ), + array( + 'name' => 'padding', + 'value' => false, + ), + array( + 'name' => 'gap', + 'value' => array(), + ), + ); + $result = $reflection->invoke( null, '.test', $declarations ); + $this->assertStringContainsString( 'color: red;', $result, 'Color declaration should be included' ); + $this->assertStringContainsString( 'margin: 0;', $result, 'Numeric value should be cast to string' ); + $this->assertStringNotContainsString( 'opacity', $result, 'Boolean value should be skipped' ); + $this->assertStringNotContainsString( 'padding', $result, 'Boolean value should be skipped' ); + $this->assertStringNotContainsString( 'gap', $result, 'Array value should be skipped' ); + } } From c4acf904345e6d36e44d08e333866a5e654919cd Mon Sep 17 00:00:00 2001 From: Ramon Date: Thu, 12 Mar 2026 18:09:10 +1100 Subject: [PATCH 2/3] annotation for ticket --- tests/phpunit/tests/theme/wpThemeJson.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/phpunit/tests/theme/wpThemeJson.php b/tests/phpunit/tests/theme/wpThemeJson.php index 3f232ba6d3f38..d5be7dafb44e9 100644 --- a/tests/phpunit/tests/theme/wpThemeJson.php +++ b/tests/phpunit/tests/theme/wpThemeJson.php @@ -4138,7 +4138,7 @@ public function test_get_styles_with_content_width() { * @ticket 60936 * @ticket 61165 * @ticket 61829 - * @ticket xxxx + * @ticket 64848 */ public function test_get_styles_with_appearance_tools() { $theme_json = new WP_Theme_JSON( @@ -7059,7 +7059,7 @@ public function test_sanitize_preserves_null_schema_behavior() { /** * @covers WP_Theme_JSON::to_ruleset * - * @ticket xxxx + * @ticket 64848 */ public function test_to_ruleset_skips_non_scalar_values_and_casts_numerics() { $reflection = new ReflectionMethod( WP_Theme_JSON::class, 'to_ruleset' ); From 18ac1b5c7437402772595fd4e24aa7a2cff388c0 Mon Sep 17 00:00:00 2001 From: Ramon Date: Fri, 13 Mar 2026 14:41:07 +1100 Subject: [PATCH 3/3] Adjust accessibility of ReflectionMethod in test_to_ruleset for PHP version compatibility. --- tests/phpunit/tests/theme/wpThemeJson.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/phpunit/tests/theme/wpThemeJson.php b/tests/phpunit/tests/theme/wpThemeJson.php index d5be7dafb44e9..ef1da85ce76d3 100644 --- a/tests/phpunit/tests/theme/wpThemeJson.php +++ b/tests/phpunit/tests/theme/wpThemeJson.php @@ -7063,7 +7063,9 @@ public function test_sanitize_preserves_null_schema_behavior() { */ public function test_to_ruleset_skips_non_scalar_values_and_casts_numerics() { $reflection = new ReflectionMethod( WP_Theme_JSON::class, 'to_ruleset' ); - $reflection->setAccessible( true ); + if ( PHP_VERSION_ID < 80100 ) { + $reflection->setAccessible( true ); + } $declarations = array( array( 'name' => 'color',