| [ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * Block state support for frontend CSS generation. 4 * 5 * Generates scoped CSS for per-instance state styles declared in block attributes, 6 * including pseudo-states (e.g., `style[':hover']`) and responsive states 7 * (e.g., `style['mobile']` and `style['mobile'][':hover']`). 8 * 9 * @package WordPress 10 * @since 7.1.0 11 */ 12 13 /** 14 * Converts internal preset references to CSS custom property references. 15 * 16 * State styles are emitted as CSS rules and cannot rely on preset classnames. 17 * Converting `var:preset|color|contrast` to 18 * `var(--wp--preset--color--contrast)` ensures preset values are emitted as 19 * declarations by the style engine. 20 * 21 * @since 7.1.0 22 * 23 * @param mixed $value Style value to normalize. 24 * @return mixed Normalized style value. 25 */ 26 function wp_normalize_state_preset_vars( $value ) { 27 if ( is_array( $value ) ) { 28 foreach ( $value as $key => $nested_value ) { 29 $value[ $key ] = wp_normalize_state_preset_vars( $nested_value ); 30 } 31 return $value; 32 } 33 34 if ( ! is_string( $value ) || ! str_starts_with( $value, 'var:preset|' ) ) { 35 return $value; 36 } 37 38 $unwrapped_name = str_replace( '|', '--', substr( $value, strlen( 'var:' ) ) ); 39 return "var(--wp--$unwrapped_name)"; 40 } 41 42 /** 43 * Normalizes a state style object before generating CSS declarations. 44 * 45 * @since 7.1.0 46 * 47 * @param array $style State style object. 48 * @return array Normalized state style object. 49 */ 50 function wp_normalize_state_style_for_css_output( $style ) { 51 // Layout is processed separately by wp_render_layout_support_flag(), so we remove it before declaration generation. 52 unset( $style['layout'] ); 53 $style = wp_normalize_state_preset_vars( $style ); 54 return $style; 55 } 56 57 /** 58 * Adds fallback border-style declarations for visible border declarations. 59 * 60 * CSS does not render border color or width unless a border style is also set. 61 * State styles are emitted as stylesheet rules rather than inline styles, so 62 * they cannot rely on the block-library inline-style attribute fallback rules. 63 * 64 * @since 7.1.0 65 * 66 * @param array $declarations CSS declarations generated by the style engine. 67 * @return array CSS declarations with fallback border styles applied where needed. 68 */ 69 function wp_get_state_declarations_with_fallback_border_styles( $declarations ) { 70 if ( ! is_array( $declarations ) ) { 71 return $declarations; 72 } 73 74 $has_border_style = isset( $declarations['border-style'] ) && '' !== $declarations['border-style']; 75 $has_border_color = isset( $declarations['border-color'] ) && '' !== $declarations['border-color']; 76 $has_border_width = isset( $declarations['border-width'] ) && '' !== $declarations['border-width']; 77 78 if ( ! $has_border_style && ( $has_border_color || $has_border_width ) ) { 79 $declarations['border-style'] = 'solid'; 80 } 81 82 $sides = array( 'top', 'right', 'bottom', 'left' ); 83 foreach ( $sides as $side ) { 84 $side_style_property = "border-$side-style"; 85 $side_color_property = "border-$side-color"; 86 $side_width_property = "border-$side-width"; 87 88 $has_side_style = isset( $declarations[ $side_style_property ] ) && '' !== $declarations[ $side_style_property ]; 89 $has_side_color = isset( $declarations[ $side_color_property ] ) && '' !== $declarations[ $side_color_property ]; 90 $has_side_width = isset( $declarations[ $side_width_property ] ) && '' !== $declarations[ $side_width_property ]; 91 92 if ( ! $has_border_style && ! $has_side_style && ( $has_side_color || $has_side_width ) ) { 93 $declarations[ $side_style_property ] = 'solid'; 94 } 95 } 96 97 return $declarations; 98 } 99 100 /** 101 * Adds a style fragment to a selector-keyed state style group. 102 * 103 * @since 7.1.0 104 * 105 * @param array $groups Selector-keyed style groups. 106 * @param string|null $selector Block or feature selector. 107 * @param array $style Style fragment. 108 */ 109 function wp_add_state_style_group( &$groups, $selector, $style ) { 110 $key = is_string( $selector ) ? $selector : ''; 111 112 if ( ! isset( $groups[ $key ] ) ) { 113 $groups[ $key ] = array( 114 'selector' => $selector, 115 'style' => array(), 116 ); 117 } 118 119 $groups[ $key ]['style'] = array_replace_recursive( $groups[ $key ]['style'], $style ); 120 } 121 122 /** 123 * Splits a state style object into groups based on block feature selectors. 124 * 125 * @since 7.1.0 126 * 127 * @param array $state_style State style object. 128 * @param array $block_selectors Block selectors metadata. 129 * @return array[] Selector/style groups. 130 */ 131 function wp_get_state_style_groups( $state_style, $block_selectors ) { 132 $groups = array(); 133 134 foreach ( $state_style as $feature => $feature_styles ) { 135 $feature_selectors = $block_selectors[ $feature ] ?? null; 136 137 if ( is_string( $feature_selectors ) ) { 138 wp_add_state_style_group( 139 $groups, 140 $feature_selectors, 141 array( $feature => $feature_styles ) 142 ); 143 continue; 144 } 145 146 if ( is_array( $feature_selectors ) && is_array( $feature_styles ) ) { 147 $remaining_styles = $feature_styles; 148 149 foreach ( $feature_selectors as $subfeature => $subfeature_selector ) { 150 if ( 151 'root' === $subfeature || 152 ! is_string( $subfeature_selector ) || 153 ! array_key_exists( $subfeature, $feature_styles ) 154 ) { 155 continue; 156 } 157 158 wp_add_state_style_group( 159 $groups, 160 $subfeature_selector, 161 array( 162 $feature => array( 163 $subfeature => $feature_styles[ $subfeature ], 164 ), 165 ) 166 ); 167 unset( $remaining_styles[ $subfeature ] ); 168 } 169 170 if ( array() !== $remaining_styles ) { 171 wp_add_state_style_group( 172 $groups, 173 $feature_selectors['root'] ?? ( $block_selectors['root'] ?? null ), 174 array( $feature => $remaining_styles ) 175 ); 176 } 177 continue; 178 } 179 180 wp_add_state_style_group( 181 $groups, 182 $block_selectors['root'] ?? null, 183 array( $feature => $feature_styles ) 184 ); 185 } 186 187 return array_values( $groups ); 188 } 189 190 /** 191 * Returns a style object with nested state keys removed. 192 * 193 * @since 7.1.0 194 * 195 * @param array $state_style State style object. 196 * @param array $nested_keys Keys to remove from the root style object. 197 * @return array Root-only style object. 198 */ 199 function wp_get_root_state_style( $state_style, $nested_keys ) { 200 if ( ! is_array( $state_style ) ) { 201 return $state_style; 202 } 203 204 $root_style = $state_style; 205 foreach ( $nested_keys as $key ) { 206 unset( $root_style[ $key ] ); 207 } 208 209 return $root_style; 210 } 211 212 /** 213 * Builds compiled state style rules, preserving the selector each rule targets. 214 * 215 * @since 7.1.0 216 * 217 * @param array $state_styles Map of state to style array. 218 * @param WP_Block_Type $block_type Block type. 219 * @param string|null $rules_group Optional CSS grouping rule, e.g. a media query. 220 * @return array[] State style rules. 221 */ 222 function wp_get_block_state_style_rules( $state_styles, $block_type, $rules_group = null ) { 223 $css_rules = array(); 224 $block_selectors = isset( $block_type->selectors ) && is_array( $block_type->selectors ) 225 ? $block_type->selectors 226 : array(); 227 228 foreach ( $state_styles as $state => $state_style ) { 229 if ( empty( $state_style ) || ! is_array( $state_style ) ) { 230 continue; 231 } 232 233 foreach ( wp_get_state_style_groups( $state_style, $block_selectors ) as $group ) { 234 $compiled = wp_style_engine_get_styles( 235 wp_normalize_state_style_for_css_output( $group['style'] ) 236 ); 237 238 if ( ! empty( $compiled['declarations'] ) ) { 239 $css_rules[] = array( 240 'state' => $state, 241 'selector' => $group['selector'], 242 'declarations' => $compiled['declarations'], 243 ); 244 if ( ! empty( $rules_group ) ) { 245 $css_rules[ count( $css_rules ) - 1 ]['rules_group'] = $rules_group; 246 } 247 } 248 } 249 } 250 251 return $css_rules; 252 } 253 254 /** 255 * Returns a unique class for a set of state style rules. 256 * 257 * @since 7.1.0 258 * 259 * @param string $block_name Block name. 260 * @param array $css_rules State style rules. 261 * @return string Unique class name. 262 */ 263 function wp_get_block_state_unique_class( $block_name, $css_rules ) { 264 return 'wp-states-' . substr( 265 md5( 266 wp_json_encode( 267 array( 268 'blockName' => $block_name, 269 'rules' => $css_rules, 270 ) 271 ) 272 ), 273 0, 274 8 275 ); 276 } 277 278 /** 279 * Splits a selector list by top-level commas. 280 * 281 * @since 7.1.0 282 * 283 * @param string $selector CSS selector list. 284 * @return string[] Selectors. 285 */ 286 function wp_split_selector_list( $selector ) { 287 if ( ! str_contains( $selector, ',' ) ) { 288 return array( $selector ); 289 } 290 291 $selectors = array(); 292 $current_selector = ''; 293 $parentheses_depth = 0; 294 $selector_length = strlen( $selector ); 295 296 for ( $i = 0; $i < $selector_length; $i++ ) { 297 $char = $selector[ $i ]; 298 299 if ( '(' === $char ) { 300 ++$parentheses_depth; 301 } elseif ( ')' === $char && $parentheses_depth > 0 ) { 302 --$parentheses_depth; 303 } elseif ( ',' === $char && 0 === $parentheses_depth ) { 304 $selectors[] = $current_selector; 305 $current_selector = ''; 306 continue; 307 } 308 309 $current_selector .= $char; 310 } 311 312 $selectors[] = $current_selector; 313 314 return $selectors; 315 } 316 317 /** 318 * Builds a scoped selector from a block selector and optional pseudo-state. 319 * 320 * @since 7.1.0 321 * 322 * @param string $base_selector Block-instance scoping selector. 323 * @param string|null $block_selector Block or feature selector from metadata. 324 * @param string $state Pseudo-state selector. 325 * @return string Scoped selector. 326 */ 327 function wp_build_state_selector( $base_selector, $block_selector, $state ) { 328 if ( ! is_string( $block_selector ) || '' === trim( $block_selector ) ) { 329 return $base_selector . $state; 330 } 331 332 $selectors = wp_split_selector_list( $block_selector ); 333 $scoped_selectors = array(); 334 335 foreach ( $selectors as $selector ) { 336 $selector = trim( $selector ); 337 if ( '' === $selector ) { 338 continue; 339 } 340 341 /* 342 * Replace only the leading block selector part (e.g. class name, 343 * attribute selector, ID, or tag name) with the block instance selector. 344 * Preserve anything after that prefix, including modifier classes on the 345 * same element and combinators without spaces. 346 */ 347 if ( preg_match( '/^([.#]?[-_a-zA-Z0-9]+|\[[^\]]+\])/', $selector, $matches ) ) { 348 $scoped_selectors[] = $base_selector . substr( $selector, strlen( $matches[0] ) ) . $state; 349 continue; 350 } 351 352 $scoped_selectors[] = $base_selector . $state; 353 } 354 355 return empty( $scoped_selectors ) 356 ? $base_selector . $state 357 : implode( ', ', $scoped_selectors ); 358 } 359 360 /** 361 * Renders per-instance state styles on the frontend. 362 * 363 * @since 7.1.0 364 * 365 * @param string $block_content The block's rendered HTML. 366 * @param array $block The block data including blockName and attrs. 367 * @return string Modified block content with injected state styles. 368 */ 369 function wp_render_block_states_support( $block_content, $block ) { 370 if ( empty( $block['blockName'] ) || empty( $block_content ) ) { 371 return $block_content; 372 } 373 374 $block_name = $block['blockName']; 375 $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block_name ); 376 if ( ! $block_type ) { 377 return $block_content; 378 } 379 380 $supported_pseudo_states = WP_Theme_JSON::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] ?? array(); 381 $style = $block['attrs']['style'] ?? array(); 382 $css_rules = array(); 383 384 foreach ( $supported_pseudo_states as $pseudo_state ) { 385 if ( empty( $style[ $pseudo_state ] ) || ! is_array( $style[ $pseudo_state ] ) ) { 386 continue; 387 } 388 389 $css_rules = array_merge( 390 $css_rules, 391 wp_get_block_state_style_rules( 392 array( $pseudo_state => $style[ $pseudo_state ] ), 393 $block_type 394 ) 395 ); 396 } 397 398 foreach ( WP_Theme_JSON::RESPONSIVE_BREAKPOINTS as $breakpoint => $media_query ) { 399 if ( empty( $style[ $breakpoint ] ) || ! is_array( $style[ $breakpoint ] ) ) { 400 continue; 401 } 402 403 $root_state_style = wp_get_root_state_style( 404 $style[ $breakpoint ], 405 array_merge( array( 'elements' ), $supported_pseudo_states ) 406 ); 407 408 if ( ! empty( $root_state_style ) ) { 409 $css_rules = array_merge( 410 $css_rules, 411 wp_get_block_state_style_rules( 412 array( '' => $root_state_style ), 413 $block_type, 414 $media_query 415 ) 416 ); 417 } 418 419 foreach ( $supported_pseudo_states as $pseudo_state ) { 420 if ( empty( $style[ $breakpoint ][ $pseudo_state ] ) || ! is_array( $style[ $breakpoint ][ $pseudo_state ] ) ) { 421 continue; 422 } 423 424 $css_rules = array_merge( 425 $css_rules, 426 wp_get_block_state_style_rules( 427 array( $pseudo_state => $style[ $breakpoint ][ $pseudo_state ] ), 428 $block_type, 429 $media_query 430 ) 431 ); 432 } 433 } 434 435 if ( empty( $css_rules ) ) { 436 return $block_content; 437 } 438 439 $unique_class = wp_get_block_state_unique_class( $block_name, $css_rules ); 440 441 /* 442 * Register each state's CSS rules with the block-supports style engine store. 443 * The store deduplicates rules by selector — two block instances with identical 444 * state styles share the same hash class and therefore the same selector, 445 * so only one CSS rule is emitted. The store is flushed to the page by 446 * wp_enqueue_stored_styles() rather than injected inline here. 447 * 448 * State declarations need !important to apply reliably over inline styles and 449 * preset utility classes such as .has-accent-3-background-color. 450 * 451 * Layout-driven state styles (responsive layout, blockGap, child layout) are 452 * handled by wp_render_layout_support_flag() so they share a selector with 453 * the base layout and target the correct (inner) wrapper element. 454 */ 455 $style_rules = array(); 456 foreach ( $css_rules as $rule ) { 457 $declarations = array(); 458 foreach ( $rule['declarations'] as $property => $value ) { 459 $declarations[ $property ] = is_string( $value ) && str_contains( $value, '!important' ) 460 ? $value 461 : $value . ' !important'; 462 } 463 $declarations = wp_get_state_declarations_with_fallback_border_styles( $declarations ); 464 $style_rule = array( 465 'selector' => wp_build_state_selector( 466 ".$unique_class", 467 $rule['selector'], 468 $rule['state'] 469 ), 470 'declarations' => $declarations, 471 ); 472 if ( ! empty( $rule['rules_group'] ) ) { 473 $style_rule['rules_group'] = $rule['rules_group']; 474 } 475 $style_rules[] = $style_rule; 476 } 477 478 wp_style_engine_get_stylesheet_from_css_rules( 479 $style_rules, 480 array( 481 'context' => 'block-supports', 482 'prettify' => false, 483 ) 484 ); 485 486 $processor = new WP_HTML_Tag_Processor( $block_content ); 487 if ( $processor->next_tag() ) { 488 $processor->add_class( $unique_class ); 489 } 490 return $processor->get_updated_html(); 491 } 492 add_filter( 'render_block', 'wp_render_block_states_support', 10, 2 );
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Sat Jun 13 09:38:55 2026 | Cross-referenced by PHPXref |