[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/block-supports/ -> states.php (source)

   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 );


Generated : Sat Jun 13 09:38:55 2026 Cross-referenced by PHPXref