diff --git a/src/wp-includes/style-engine/class-wp-style-engine-css-declarations.php b/src/wp-includes/style-engine/class-wp-style-engine-css-declarations.php index 797947d1e9e75..612a3133e2b16 100644 --- a/src/wp-includes/style-engine/class-wp-style-engine-css-declarations.php +++ b/src/wp-includes/style-engine/class-wp-style-engine-css-declarations.php @@ -188,8 +188,28 @@ public function get_declaration_options() { * @return string The filtered declaration or an empty string. */ protected static function filter_declaration( $property, $value, $spacer = '', $options = array() ) { - $filtered_value = wp_strip_all_tags( $value, true ); + $filtered_value = wp_strip_all_tags( $value ); if ( '' !== $filtered_value ) { + if ( str_contains( $filtered_value, '"' ) || str_contains( $filtered_value, "'" ) ) { + $parts = preg_split( '/("(?:[^"\\\\]|\\\\.)*"|\x27(?:[^\x27\\\\]|\\\\.)*\x27)/', $filtered_value, -1, PREG_SPLIT_DELIM_CAPTURE ); + $filtered_value = ''; + foreach ( $parts as $i => $part ) { + if ( 1 === $i % 2 ) { + // Inside quotes: keep unchanged. + $filtered_value .= $part; + } else { + // Outside quotes: collapse whitespace. + $filtered_value .= preg_replace( '/[\r\n\t ]+/', ' ', $part ); + } + } + $filtered_value = trim( $filtered_value ); + } else { + $filtered_value = preg_replace( '/[\r\n\t ]+/', ' ', $filtered_value ); + } + + if ( '' === $filtered_value ) { + return ''; + } $options = wp_parse_args( $options, array( diff --git a/tests/phpunit/tests/style-engine/wpStyleEngineCssDeclarations.php b/tests/phpunit/tests/style-engine/wpStyleEngineCssDeclarations.php index ca0f22ddd4248..a5700e286cea3 100644 --- a/tests/phpunit/tests/style-engine/wpStyleEngineCssDeclarations.php +++ b/tests/phpunit/tests/style-engine/wpStyleEngineCssDeclarations.php @@ -384,4 +384,102 @@ public function test_should_reject_non_string_values() { 'Non-string values should be rejected without causing errors.' ); } + + /** + * Tests that significant whitespace inside quoted strings is preserved. + * + * @ticket 66199 + * + * @covers ::filter_declaration + * @covers ::get_declarations_string + * + * @dataProvider data_should_preserve_whitespace_inside_quoted_strings + * + * @param array $declarations Expected declarations input. + * @param string $expected Expected compiled CSS string. + */ + public function test_should_preserve_whitespace_inside_quoted_strings( $declarations, $expected ) { + $filter = static function ( $styles ) { + $styles[] = 'font-variation-settings'; + return $styles; + }; + add_filter( 'safe_style_css', $filter ); + + $css_declarations = new WP_Style_Engine_CSS_Declarations( $declarations ); + + $this->assertSame( + $expected, + $css_declarations->get_declarations_string() + ); + + remove_filter( 'safe_style_css', $filter ); + } + + /** + * Data provider for test_should_preserve_whitespace_inside_quoted_strings(). + * + * @return array + */ + public function data_should_preserve_whitespace_inside_quoted_strings() { + return array( + 'font-family with double quotes containing consecutive spaces' => array( + 'declarations' => array( + 'font-family' => '"My Font", sans-serif', + ), + 'expected' => 'font-family:"My Font", sans-serif;', + ), + 'font-family with single quotes containing consecutive spaces' => array( + 'declarations' => array( + 'font-family' => "'Another Custom Font', serif", + ), + 'expected' => "font-family:'Another Custom Font', serif;", + ), + 'space-padded OpenType axis tags in font-variation-settings' => array( + 'declarations' => array( + 'font-variation-settings' => '"a " 12, "opsz" 36', + ), + 'expected' => 'font-variation-settings:"a " 12, "opsz" 36;', + ), + 'collapses runs of whitespace outside quotes while preserving whitespace inside quotes' => array( + 'declarations' => array( + 'font-family' => ' "My Font", sans-serif ', + ), + 'expected' => 'font-family:"My Font", sans-serif;', + ), + 'collapses newlines, tabs, and spaces outside quotes' => array( + 'declarations' => array( + 'margin' => "10px \n\t 20px", + ), + 'expected' => 'margin:10px 20px;', + ), + ); + } + + /** + * Tests that wp_style_engine_get_styles() produces matching declaration and compiled CSS for quoted font families. + * + * @ticket 66199 + * + * @covers ::filter_declaration + */ + public function test_should_preserve_whitespace_with_wp_style_engine_get_styles() { + $styles = wp_style_engine_get_styles( + array( + 'typography' => array( + 'fontFamily' => '"My Font", sans-serif', + ), + ) + ); + + $this->assertSame( + '"My Font", sans-serif', + $styles['declarations']['font-family'], + 'Style Engine declaration value should match the original input.' + ); + $this->assertSame( + 'font-family:"My Font", sans-serif;', + $styles['css'], + 'Style Engine compiled CSS should retain significant whitespace inside quotes.' + ); + } }