[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

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

   1  <?php
   2  /**
   3   * Custom CSS block support.
   4   *
   5   * @package WordPress
   6   */
   7  
   8  /**
   9   * Checks whether a style node has a non-empty custom CSS string.
  10   *
  11   * @since 7.2.0
  12   * @access private
  13   *
  14   * @param mixed $style_node A style node, possibly containing a `css` key.
  15   * @return bool Whether the node has a non-empty custom CSS string.
  16   */
  17  function wp_custom_css_has_value( $style_node ) {
  18      return is_array( $style_node )
  19          && isset( $style_node['css'] )
  20          && is_string( $style_node['css'] )
  21          && '' !== trim( $style_node['css'] );
  22  }
  23  
  24  /**
  25   * Collects custom CSS defined for a block instance across its default,
  26   * pseudo-selector, and viewport states.
  27   *
  28   * @since 7.2.0
  29   * @access private
  30   *
  31   * @param array  $style                    The block's `style` attribute.
  32   * @param string $block_name               Block name.
  33   * @param array  $responsive_media_queries Media queries keyed by viewport state name.
  34   * @return array[] Custom CSS entries with `css`, `pseudo`, and `media_query` keys.
  35   */
  36  function wp_get_custom_css_state_entries( $style, $block_name, $responsive_media_queries ) {
  37      $entries = array();
  38      if ( empty( $style ) || ! is_array( $style ) ) {
  39          return $entries;
  40      }
  41  
  42      $supported_pseudo_states = WP_Theme_JSON::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] ?? array();
  43  
  44      if ( wp_custom_css_has_value( $style ) ) {
  45          $entries[] = array(
  46              'css'         => $style['css'],
  47              'pseudo'      => null,
  48              'media_query' => null,
  49          );
  50      }
  51  
  52      foreach ( $supported_pseudo_states as $pseudo_state ) {
  53          $pseudo_style = $style[ $pseudo_state ] ?? null;
  54          if ( wp_custom_css_has_value( $pseudo_style ) ) {
  55              $entries[] = array(
  56                  'css'         => $pseudo_style['css'],
  57                  'pseudo'      => $pseudo_state,
  58                  'media_query' => null,
  59              );
  60          }
  61      }
  62  
  63      foreach ( $responsive_media_queries as $breakpoint => $media_query ) {
  64          $breakpoint_style = $style[ $breakpoint ] ?? null;
  65          if ( ! is_array( $breakpoint_style ) ) {
  66              continue;
  67          }
  68  
  69          if ( wp_custom_css_has_value( $breakpoint_style ) ) {
  70              $entries[] = array(
  71                  'css'         => $breakpoint_style['css'],
  72                  'pseudo'      => null,
  73                  'media_query' => $media_query,
  74              );
  75          }
  76  
  77          foreach ( $supported_pseudo_states as $pseudo_state ) {
  78              $breakpoint_pseudo_style = $breakpoint_style[ $pseudo_state ] ?? null;
  79              if ( wp_custom_css_has_value( $breakpoint_pseudo_style ) ) {
  80                  $entries[] = array(
  81                      'css'         => $breakpoint_pseudo_style['css'],
  82                      'pseudo'      => $pseudo_state,
  83                      'media_query' => $media_query,
  84                  );
  85              }
  86          }
  87      }
  88  
  89      return $entries;
  90  }
  91  
  92  /**
  93   * Validates and processes custom CSS for each block style state.
  94   *
  95   * @since 7.2.0
  96   * @access private
  97   *
  98   * @param array[] $state_entries Custom CSS entries.
  99   * @param string  $selector      Selector scoping the block instance.
 100   * @return string Processed CSS.
 101   */
 102  function wp_process_custom_css_state_entries( $state_entries, $selector ) {
 103      $processed_css = '';
 104  
 105      foreach ( $state_entries as $entry ) {
 106          // Validate CSS doesn't contain HTML markup (same validation as global styles REST API).
 107          if ( preg_match( '#</?\w+#', $entry['css'] ) ) {
 108              continue;
 109          }
 110  
 111          $entry_selector = null !== $entry['pseudo'] ? $selector . $entry['pseudo'] : $selector;
 112          $entry_css      = WP_Theme_JSON::process_blocks_custom_css( $entry['css'], $entry_selector );
 113  
 114          if ( empty( $entry_css ) ) {
 115              continue;
 116          }
 117  
 118          $processed_css .= null !== $entry['media_query']
 119              ? $entry['media_query'] . '{' . $entry_css . '}'
 120              : $entry_css;
 121      }
 122  
 123      return $processed_css;
 124  }
 125  
 126  /**
 127   * Generates the custom CSS class name for a block and registers its stylesheet.
 128   *
 129   * @since 7.2.0
 130   * @access private
 131   *
 132   * @param array $parsed_block The parsed block.
 133   * @return string|null The generated class name, or null if the block has no custom CSS to render.
 134   *
 135   * @phpstan-param array{
 136   *     blockName: string|null,
 137   *     attrs: array{
 138   *         className?: string,
 139   *         style?: array{
 140   *             css?: string,
 141   *             ...
 142   *         },
 143   *         ...
 144   *     },
 145   *     ...
 146   * } $parsed_block
 147   */
 148  function wp_generate_custom_css_class_name( $parsed_block ) {
 149      $style = $parsed_block['attrs']['style'] ?? null;
 150      if ( empty( $style ) || ! is_array( $style ) ) {
 151          return null;
 152      }
 153  
 154      $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $parsed_block['blockName'] );
 155      if ( ! block_has_support( $block_type, 'customCSS', true ) ) {
 156          return null;
 157      }
 158  
 159      $viewport_settings        = wp_get_global_settings( array( 'viewport' ) );
 160      $responsive_media_queries = WP_Theme_JSON::get_viewport_media_queries( $viewport_settings );
 161      $state_entries            = wp_get_custom_css_state_entries( $style, $parsed_block['blockName'], $responsive_media_queries );
 162      if ( empty( $state_entries ) ) {
 163          return null;
 164      }
 165  
 166      // Generate a unique class name for this block instance.
 167      $class_name    = wp_unique_id_from_values( $parsed_block, 'wp-custom-css-' );
 168      $processed_css = wp_process_custom_css_state_entries( $state_entries, '.' . $class_name );
 169      if ( '' === $processed_css ) {
 170          return null;
 171      }
 172  
 173      /**
 174       * Reuse one handle so identical custom CSS is enqueued only once via
 175       * {@see wp_unique_id_from_values()}. Explicitly declare the `wp-block-library`
 176       * dependency so `global-styles` is guaranteed to print after it, preventing
 177       * block default styles from unintentionally overriding global styles.
 178       */
 179      $handle = 'wp-block-custom-css';
 180      if ( ! wp_style_is( $handle, 'registered' ) ) {
 181          wp_register_style( $handle, false, array( 'wp-block-library', 'global-styles' ) );
 182      }
 183      $after_styles = wp_styles()->get_data( $handle, 'after' );
 184      if ( ! is_array( $after_styles ) ) {
 185          $after_styles = array();
 186      }
 187      if ( ! in_array( $processed_css, $after_styles, true ) ) {
 188          wp_add_inline_style( $handle, $processed_css );
 189      }
 190  
 191      return $class_name;
 192  }
 193  
 194  /**
 195   * Render the custom CSS stylesheet and add class name to block as required.
 196   *
 197   * @since 7.0.0
 198   *
 199   * @param array $parsed_block The parsed block.
 200   * @return array The same parsed block with custom CSS class name added if appropriate.
 201   *
 202   * @phpstan-param array{
 203   *     blockName: string|null,
 204   *     attrs: array{
 205   *         className?: string,
 206   *         style?: array{
 207   *             css?: string,
 208   *             ...
 209   *         },
 210   *         ...
 211   *     },
 212   *     ...
 213   * } $parsed_block
 214   * @phpstan-return array{
 215   *     blockName: string|null,
 216   *     attrs: array{
 217   *         className?: string,
 218   *         style?: array{
 219   *             css?: string,
 220   *             ...
 221   *         },
 222   *         ...
 223   *     },
 224   *     ...
 225   * }
 226   */
 227  function wp_render_custom_css_support_styles( $parsed_block ) {
 228      $class_name = wp_generate_custom_css_class_name( $parsed_block );
 229      if ( null === $class_name ) {
 230          return $parsed_block;
 231      }
 232  
 233      $existing_class_name = $parsed_block['attrs']['className'] ?? null;
 234      $updated_class_name  = is_string( $existing_class_name )
 235          ? "$existing_class_name $class_name"
 236          : $class_name;
 237  
 238      $parsed_block['attrs']['className'] = $updated_class_name;
 239  
 240      return $parsed_block;
 241  }
 242  
 243  /**
 244   * Enqueues the block custom CSS styles.
 245   *
 246   * @since 7.0.0
 247   */
 248  function wp_enqueue_block_custom_css() {
 249      wp_enqueue_style( 'wp-block-custom-css' );
 250  }
 251  
 252  /**
 253   * Applies the custom CSS class name to the block's rendered HTML.
 254   *
 255   * Uses the class name that {@see wp_render_custom_css_support_styles()}
 256   * stored in the block attributes. Blocks that a parent parsed and rendered
 257   * itself never reach that filter, so the class name is generated here instead
 258   * via {@see wp_generate_custom_css_class_name()}.
 259   *
 260   * @since 7.0.0
 261   * @since 7.2.0 Generates the class name when the block attributes lack one.
 262   *
 263   * @param string $block_content Rendered block content.
 264   * @param array  $block         Block object.
 265   * @return string Filtered block content.
 266   *
 267   * @phpstan-param array{
 268   *     blockName: string|null,
 269   *     attrs: array{
 270   *         className?: string,
 271   *         style?: array{
 272   *             css?: string,
 273   *             ...
 274   *         },
 275   *         ...
 276   *     },
 277   *     ...
 278   * } $block
 279   */
 280  function wp_render_custom_css_class_name( $block_content, $block ) {
 281      $class_name_attr    = $block['attrs']['className'] ?? null;
 282      $class_name_prefix  = 'wp-custom-css-';
 283      $matched_class_name = null;
 284  
 285      // Parse out the 'wp-custom-css-*' class name added by wp_render_custom_css_support_styles().
 286      if ( is_string( $class_name_attr ) && str_contains( $class_name_attr, $class_name_prefix ) ) {
 287          $token_delimiter = " \t\f\r\n";
 288          $class_token     = strtok( $class_name_attr, $token_delimiter );
 289          while ( false !== $class_token ) {
 290              if ( str_starts_with( $class_token, $class_name_prefix ) ) {
 291                  $matched_class_name = $class_token;
 292                  break;
 293              }
 294              $class_token = strtok( $token_delimiter );
 295          }
 296      }
 297  
 298      // Direct WP_Block::render() calls skip render_block_data for the block itself.
 299      if ( null === $matched_class_name ) {
 300          $matched_class_name = wp_generate_custom_css_class_name( $block );
 301      }
 302  
 303      if ( null === $matched_class_name ) {
 304          return $block_content;
 305      }
 306  
 307      $tags = new WP_HTML_Tag_Processor( $block_content );
 308      if ( $tags->next_tag() ) {
 309          $tags->add_class( 'has-custom-css' );
 310          $tags->add_class( $matched_class_name );
 311      }
 312  
 313      return $tags->get_updated_html();
 314  }
 315  
 316  add_filter( 'render_block', 'wp_render_custom_css_class_name', 10, 2 );
 317  add_filter( 'render_block_data', 'wp_render_custom_css_support_styles', 10, 1 );
 318  add_action( 'wp_enqueue_scripts', 'wp_enqueue_block_custom_css', 1 );
 319  
 320  /**
 321   * Registers the style block attribute for block types that support it.
 322   *
 323   * @since 7.0.0
 324   *
 325   * @param WP_Block_Type $block_type Block Type.
 326   */
 327  function wp_register_custom_css_support( $block_type ) {
 328      // Setup attributes and styles within that if needed.
 329      if ( ! $block_type->attributes ) {
 330          $block_type->attributes = array();
 331      }
 332  
 333      // Check for existing style attribute definition e.g. from block.json.
 334      if ( array_key_exists( 'style', $block_type->attributes ) ) {
 335          return;
 336      }
 337  
 338      $has_custom_css_support = block_has_support( $block_type, array( 'customCSS' ), true );
 339  
 340      if ( $has_custom_css_support ) {
 341          $block_type->attributes['style'] = array(
 342              'type' => 'object',
 343          );
 344      }
 345  }
 346  
 347  /**
 348   * Removes custom CSS from a style attribute and any nested style states.
 349   *
 350   * @since 7.2.0
 351   * @access private
 352   *
 353   * @param array $style The block's `style` attribute.
 354   * @return array The style attribute with all `css` keys removed.
 355   */
 356  function wp_strip_custom_css_from_style_array( $style ) {
 357      if ( ! is_array( $style ) ) {
 358          return $style;
 359      }
 360  
 361      unset( $style['css'] );
 362  
 363      foreach ( $style as $key => $value ) {
 364          if ( is_string( $key ) && is_array( $value ) && ( str_starts_with( $key, ':' ) || str_starts_with( $key, '@' ) ) ) {
 365              $nested_style = wp_strip_custom_css_from_style_array( $value );
 366              if ( empty( $nested_style ) ) {
 367                  unset( $style[ $key ] );
 368              } else {
 369                  $style[ $key ] = $nested_style;
 370              }
 371          }
 372      }
 373  
 374      return $style;
 375  }
 376  
 377  /**
 378   * Strips custom CSS (`style.css` in attributes) from all blocks in post content.
 379   *
 380   * Uses {@see WP_Block_Parser::next_token()} to scan block tokens and surgically
 381   * replace only the attribute JSON that changed — no parse_blocks() +
 382   * serialize_blocks() round-trip needed.
 383   *
 384   * @since 7.0.0
 385   * @access private
 386   *
 387   * @param string $content Post content to filter, expected to be escaped with slashes.
 388   * @return string Filtered post content with block custom CSS removed.
 389   */
 390  function wp_strip_custom_css_from_blocks( $content ) {
 391      if ( ! has_blocks( $content ) ) {
 392          return $content;
 393      }
 394  
 395      $unslashed = stripslashes( $content );
 396  
 397      $parser           = new WP_Block_Parser();
 398      $parser->document = $unslashed;
 399      $parser->offset   = 0;
 400      $end              = strlen( $unslashed );
 401      $replacements     = array();
 402  
 403      while ( $parser->offset < $end ) {
 404          $next_token = $parser->next_token();
 405  
 406          if ( 'no-more-tokens' === $next_token[0] ) {
 407              break;
 408          }
 409  
 410          list( $token_type, , $attrs, $start_offset, $token_length ) = $next_token;
 411  
 412          $parser->offset = $start_offset + $token_length;
 413  
 414          if ( 'block-opener' !== $token_type && 'void-block' !== $token_type ) {
 415              continue;
 416          }
 417  
 418          if ( ! isset( $attrs['style'] ) || ! is_array( $attrs['style'] ) ) {
 419              continue;
 420          }
 421  
 422          $stripped_style = wp_strip_custom_css_from_style_array( $attrs['style'] );
 423          if ( $stripped_style === $attrs['style'] ) {
 424              continue;
 425          }
 426  
 427          // Remove css and clean up empty style.
 428          if ( empty( $stripped_style ) ) {
 429              unset( $attrs['style'] );
 430          } else {
 431              $attrs['style'] = $stripped_style;
 432          }
 433  
 434          // Locate the JSON portion within the token.
 435          $token_string   = substr( $unslashed, $start_offset, $token_length );
 436          $json_rel_start = strcspn( $token_string, '{' );
 437          $json_rel_end   = strrpos( $token_string, '}' );
 438  
 439          $json_start  = $start_offset + $json_rel_start;
 440          $json_length = $json_rel_end - $json_rel_start + 1;
 441  
 442          // Re-encode attributes. If attrs is now empty, remove JSON and trailing space.
 443          if ( empty( $attrs ) ) {
 444              // Remove the trailing space after JSON.
 445              $replacements[] = array( $json_start, $json_length + 1, '' );
 446          } else {
 447              $replacements[] = array( $json_start, $json_length, serialize_block_attributes( $attrs ) );
 448          }
 449      }
 450  
 451      if ( empty( $replacements ) ) {
 452          return $content;
 453      }
 454  
 455      // Build the result by splicing replacements into the original string.
 456      $result = '';
 457      $was_at = 0;
 458  
 459      foreach ( $replacements as $replacement ) {
 460          list( $offset, $length, $new_json ) = $replacement;
 461          $result                            .= substr( $unslashed, $was_at, $offset - $was_at ) . $new_json;
 462          $was_at                             = $offset + $length;
 463      }
 464  
 465      if ( $was_at < $end ) {
 466          $result .= substr( $unslashed, $was_at );
 467      }
 468  
 469      return addslashes( $result );
 470  }
 471  
 472  /**
 473   * Adds the filters to strip custom CSS from block content on save.
 474   * Priority of 8 to run before wp_filter_global_styles_post (priority 9) and wp_filter_post_kses (priority 10).
 475   *
 476   * @since 7.0.0
 477   * @access private
 478   */
 479  function wp_custom_css_kses_init_filters() {
 480      add_filter( 'content_save_pre', 'wp_strip_custom_css_from_blocks', 8 );
 481      add_filter( 'content_filtered_save_pre', 'wp_strip_custom_css_from_blocks', 8 );
 482  }
 483  
 484  /**
 485   * Removes the filters that strip custom CSS from block content on save.
 486   * Priority of 8 to run before wp_filter_global_styles_post (priority 9) and wp_filter_post_kses (priority 10).
 487   *
 488   * @since 7.0.0
 489   * @access private
 490   */
 491  function wp_custom_css_remove_filters() {
 492      remove_filter( 'content_save_pre', 'wp_strip_custom_css_from_blocks', 8 );
 493      remove_filter( 'content_filtered_save_pre', 'wp_strip_custom_css_from_blocks', 8 );
 494  }
 495  
 496  /**
 497   * Registers the custom CSS content filters if the user does not have the edit_css capability.
 498   *
 499   * @since 7.0.0
 500   * @access private
 501   */
 502  function wp_custom_css_kses_init() {
 503      wp_custom_css_remove_filters();
 504      if ( ! current_user_can( 'edit_css' ) ) {
 505          wp_custom_css_kses_init_filters();
 506      }
 507  }
 508  
 509  /**
 510   * Initializes custom CSS content filters when imported data should be filtered.
 511   *
 512   * Runs at priority 999 on {@see 'force_filtered_html_on_import'} to ensure it
 513   * fires after general KSES initialization, independently of user capabilities.
 514   * If the input of the filter is true it means we are in an import situation and should
 515   * enable the custom CSS filters, independently of the user capabilities.
 516   *
 517   * @since 7.0.0
 518   * @access private
 519   *
 520   * @param mixed $arg Input argument of the filter.
 521   * @return mixed Input argument of the filter.
 522   */
 523  function wp_custom_css_force_filtered_html_on_import_filter( $arg ) {
 524      if ( $arg ) {
 525          wp_custom_css_kses_init_filters();
 526      }
 527      return $arg;
 528  }
 529  
 530  // Run before wp_filter_global_styles_post (priority 9) and wp_filter_post_kses (priority 10).
 531  add_action( 'init', 'wp_custom_css_kses_init', 20 );
 532  add_action( 'set_current_user', 'wp_custom_css_kses_init' );
 533  add_filter( 'force_filtered_html_on_import', 'wp_custom_css_force_filtered_html_on_import_filter', 999 );
 534  
 535  // Register the block support.
 536  WP_Block_Supports::get_instance()->register(
 537      'custom-css',
 538      array(
 539          'register_attribute' => 'wp_register_custom_css_support',
 540      )
 541  );


Generated : Mon Nov 2 08:20:36 2026 Cross-referenced by PHPXref