: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
$schema_styles_blocks[ $block ] = $styles_non_top_level;
$schema_styles_blocks[ $block ]['elements'] = $schema_styles_elements;
$block_style_variation_styles = static::VALID_STYLES;
$block_style_variation_styles['blocks'] = $schema_styles_blocks;
$block_style_variation_styles['elements'] = $schema_styles_elements;
foreach ( $valid_block_names as $block ) {
// Build the schema for each block style variation.
$style_variation_names = array();
! empty( $input['styles']['blocks'][ $block ]['variations'] ) &&
is_array( $input['styles']['blocks'][ $block ]['variations'] ) &&
isset( $valid_variations[ $block ] )
$style_variation_names = array_intersect(
array_keys( $input['styles']['blocks'][ $block ]['variations'] ),
$valid_variations[ $block ]
$schema_styles_variations = array();
if ( ! empty( $style_variation_names ) ) {
$schema_styles_variations = array_fill_keys( $style_variation_names, $block_style_variation_styles );
$schema_styles_blocks[ $block ]['variations'] = $schema_styles_variations;
$schema['styles'] = static::VALID_STYLES;
$schema['styles']['blocks'] = $schema_styles_blocks;
$schema['styles']['elements'] = $schema_styles_elements;
$schema['settings'] = static::VALID_SETTINGS;
$schema['settings']['blocks'] = $schema_settings_blocks;
$schema['settings']['typography']['fontFamilies'] = static::schema_in_root_and_per_origin( static::FONT_FAMILY_SCHEMA );
// Remove anything that's not present in the schema.
foreach ( array( 'styles', 'settings' ) as $subtree ) {
if ( ! isset( $input[ $subtree ] ) ) {
if ( ! is_array( $input[ $subtree ] ) ) {
unset( $output[ $subtree ] );
$result = static::remove_keys_not_in_schema( $input[ $subtree ], $schema[ $subtree ] );
if ( empty( $result ) ) {
unset( $output[ $subtree ] );
$output[ $subtree ] = static::resolve_custom_css_format( $result );
* Appends a sub-selector to an existing one.
* Given the compounded $selector "h1, h2, h3"
* and the $to_append selector ".some-class" the result will be
* "h1.some-class, h2.some-class, h3.some-class".
* @since 6.1.0 Added append position.
* @since 6.3.0 Removed append position parameter.
* @param string $selector Original selector.
* @param string $to_append Selector to append.
* @return string The new selector.
protected static function append_to_selector( $selector, $to_append ) {
if ( ! str_contains( $selector, ',' ) ) {
return $selector . $to_append;
$new_selectors = array();
$selectors = explode( ',', $selector );
foreach ( $selectors as $sel ) {
$new_selectors[] = $sel . $to_append;
return implode( ',', $new_selectors );
* Prepends a sub-selector to an existing one.
* Given the compounded $selector "h1, h2, h3"
* and the $to_prepend selector ".some-class " the result will be
* ".some-class h1, .some-class h2, .some-class h3".
* @param string $selector Original selector.
* @param string $to_prepend Selector to prepend.
* @return string The new selector.
protected static function prepend_to_selector( $selector, $to_prepend ) {
if ( ! str_contains( $selector, ',' ) ) {
return $to_prepend . $selector;
$new_selectors = array();
$selectors = explode( ',', $selector );
foreach ( $selectors as $sel ) {
$new_selectors[] = $to_prepend . $sel;
return implode( ',', $new_selectors );
* Returns the metadata for each block.
* 'link' => 'link selector',
* 'etc' => 'element selector'
* 'selector': '.wp-block-image',
* @since 5.9.0 Added `duotone` key with CSS selector.
* @since 6.1.0 Added `features` key with block support feature level selectors.
* @since 6.3.0 Refactored and stabilized selectors API.
* @since 6.6.0 Updated to include block style variations from the block styles registry.
* @return array Block metadata.
protected static function get_blocks_metadata() {
$registry = WP_Block_Type_Registry::get_instance();
$blocks = $registry->get_all_registered();
$style_registry = WP_Block_Styles_Registry::get_instance();
// Is there metadata for all currently registered blocks?
$blocks = array_diff_key( $blocks, static::$blocks_metadata );
if ( empty( $blocks ) ) {
* New block styles may have been registered within WP_Block_Styles_Registry.
* Update block metadata for any new block style variations.
$registered_styles = $style_registry->get_all_registered();
foreach ( static::$blocks_metadata as $block_name => $block_metadata ) {
if ( ! empty( $registered_styles[ $block_name ] ) ) {
$style_selectors = $block_metadata['styleVariations'] ?? array();
foreach ( $registered_styles[ $block_name ] as $block_style ) {
if ( ! isset( $style_selectors[ $block_style['name'] ] ) ) {
$style_selectors[ $block_style['name'] ] = static::get_block_style_variation_selector( $block_style['name'], $block_metadata['selector'] );
static::$blocks_metadata[ $block_name ]['styleVariations'] = $style_selectors;
return static::$blocks_metadata;
foreach ( $blocks as $block_name => $block_type ) {
$root_selector = wp_get_block_css_selector( $block_type );
static::$blocks_metadata[ $block_name ]['selector'] = $root_selector;
static::$blocks_metadata[ $block_name ]['selectors'] = static::get_block_selectors( $block_type, $root_selector );
$elements = static::get_block_element_selectors( $root_selector );
if ( ! empty( $elements ) ) {
static::$blocks_metadata[ $block_name ]['elements'] = $elements;
// The block may or may not have a duotone selector.
$duotone_selector = wp_get_block_css_selector( $block_type, 'filter.duotone' );
// Keep backwards compatibility for support.color.__experimentalDuotone.
if ( null === $duotone_selector ) {
$duotone_support = isset( $block_type->supports['color']['__experimentalDuotone'] )
? $block_type->supports['color']['__experimentalDuotone']
if ( $duotone_support ) {
$root_selector = wp_get_block_css_selector( $block_type );
$duotone_selector = static::scope_selector( $root_selector, $duotone_support );
if ( null !== $duotone_selector ) {
static::$blocks_metadata[ $block_name ]['duotone'] = $duotone_selector;
// If the block has style variations, append their selectors to the block metadata.
$style_selectors = array();
if ( ! empty( $block_type->styles ) ) {
foreach ( $block_type->styles as $style ) {
$style_selectors[ $style['name'] ] = static::get_block_style_variation_selector( $style['name'], static::$blocks_metadata[ $block_name ]['selector'] );
// Block style variations can be registered through the WP_Block_Styles_Registry as well as block.json.
$registered_styles = $style_registry->get_registered_styles_for_block( $block_name );
foreach ( $registered_styles as $style ) {
$style_selectors[ $style['name'] ] = static::get_block_style_variation_selector( $style['name'], static::$blocks_metadata[ $block_name ]['selector'] );
if ( ! empty( $style_selectors ) ) {
static::$blocks_metadata[ $block_name ]['styleVariations'] = $style_selectors;
return static::$blocks_metadata;
* Given a tree, removes the keys that are not present in the schema.
* It is recursive and modifies the input in-place.
* @param array $tree Input to process.
* @param array $schema Schema to adhere to.
* @return array The modified $tree.
protected static function remove_keys_not_in_schema( $tree, $schema ) {
if ( ! is_array( $tree ) ) {
foreach ( $tree as $key => $value ) {
// Remove keys not in the schema or with null/empty values.
if ( ! array_key_exists( $key, $schema ) ) {
if ( is_array( $schema[ $key ] ) ) {
if ( ! is_array( $value ) ) {
} elseif ( wp_is_numeric_array( $value ) ) {
// If indexed, process each item in the array.
foreach ( $value as $item_key => $item_value ) {
if ( isset( $schema[ $key ][0] ) && is_array( $schema[ $key ][0] ) ) {
$tree[ $key ][ $item_key ] = self::remove_keys_not_in_schema( $item_value, $schema[ $key ][0] );
// If the schema does not define a further structure, keep the value as is.
$tree[ $key ][ $item_key ] = $item_value;
// If associative, process as a single object.
$tree[ $key ] = self::remove_keys_not_in_schema( $value, $schema[ $key ] );
if ( empty( $tree[ $key ] ) ) {
* Returns the existing settings for each block.
* @return array Settings per block.
public function get_settings() {
if ( ! isset( $this->theme_json['settings'] ) ) {
return $this->theme_json['settings'];
* Returns the stylesheet that results of processing
* the theme.json structure this object represents.
* @since 5.9.0 Removed the `$type` parameter, added the `$types` and `$origins` parameters.
* @since 6.3.0 Add fallback layout styles for Post Template when block gap support isn't available.
* @since 6.6.0 Added boolean `skip_root_layout_styles` and `include_block_style_variations` options
* to control styles output as desired.
* @param string[] $types Types of styles to load. Will load all by default. It accepts:
* - `variables`: only the CSS Custom Properties for presets & custom ones.
* - `styles`: only the styles section in theme.json.
* - `presets`: only the classes for the presets.
* @param string[] $origins A list of origins to include. By default it includes VALID_ORIGINS.
* @param array $options {
* Optional. An array of options for now used for internal purposes only (may change without notice).
* @type string $scope Makes sure all style are scoped to a given selector
* @type string $root_selector Overwrites and forces a given selector to be used on the root node
* @type bool $skip_root_layout_styles Omits root layout styles from the generated stylesheet. Default false.
* @type bool $include_block_style_variations Includes styles for block style variations in the generated stylesheet. Default false.
* @return string The resulting stylesheet.
public function get_stylesheet( $types = array( 'variables', 'styles', 'presets' ), $origins = null, $options = array() ) {
if ( null === $origins ) {
$origins = static::VALID_ORIGINS;
if ( is_string( $types ) ) {
// Dispatch error and map old arguments to new ones.
_deprecated_argument( __FUNCTION__, '5.9.0' );
if ( 'block_styles' === $types ) {
$types = array( 'styles', 'presets' );
} elseif ( 'css_variables' === $types ) {
$types = array( 'variables' );
$types = array( 'variables', 'styles', 'presets' );
$blocks_metadata = static::get_blocks_metadata();
$style_nodes = static::get_style_nodes( $this->theme_json, $blocks_metadata, $options );
$setting_nodes = static::get_setting_nodes( $this->theme_json, $blocks_metadata );
$root_style_key = array_search( static::ROOT_BLOCK_SELECTOR, array_column( $style_nodes, 'selector' ), true );
$root_settings_key = array_search( static::ROOT_BLOCK_SELECTOR, array_column( $setting_nodes, 'selector' ), true );
if ( ! empty( $options['scope'] ) ) {
foreach ( $setting_nodes as &$node ) {
$node['selector'] = static::scope_selector( $options['scope'], $node['selector'] );
foreach ( $style_nodes as &$node ) {
$node = static::scope_style_node_selectors( $options['scope'], $node );
if ( ! empty( $options['root_selector'] ) ) {
if ( false !== $root_settings_key ) {
$setting_nodes[ $root_settings_key ]['selector'] = $options['root_selector'];
if ( false !== $root_style_key ) {
$style_nodes[ $root_style_key ]['selector'] = $options['root_selector'];
if ( in_array( 'variables', $types, true ) ) {
$stylesheet .= $this->get_css_variables( $setting_nodes, $origins );
if ( in_array( 'styles', $types, true ) ) {
if ( false !== $root_style_key && empty( $options['skip_root_layout_styles'] ) ) {
$stylesheet .= $this->get_root_layout_rules( $style_nodes[ $root_style_key ]['selector'], $style_nodes[ $root_style_key ] );
$stylesheet .= $this->get_block_classes( $style_nodes );
} elseif ( in_array( 'base-layout-styles', $types, true ) ) {
$root_selector = static::ROOT_BLOCK_SELECTOR;
$columns_selector = '.wp-block-columns';
$post_template_selector = '.wp-block-post-template';
if ( ! empty( $options['scope'] ) ) {
$root_selector = static::scope_selector( $options['scope'], $root_selector );
$columns_selector = static::scope_selector( $options['scope'], $columns_selector );
$post_template_selector = static::scope_selector( $options['scope'], $post_template_selector );
if ( ! empty( $options['root_selector'] ) ) {
$root_selector = $options['root_selector'];
* Base layout styles are provided as part of `styles`, so only output separately if explicitly requested.
* For backwards compatibility, the Columns block is explicitly included, to support a different default gap value.
$base_styles_nodes = array(
'path' => array( 'styles' ),
'selector' => $root_selector,
'path' => array( 'styles', 'blocks', 'core/columns' ),
'selector' => $columns_selector,
'name' => 'core/columns',
'path' => array( 'styles', 'blocks', 'core/post-template' ),
'selector' => $post_template_selector,
'name' => 'core/post-template',
foreach ( $base_styles_nodes as $base_style_node ) {
$stylesheet .= $this->get_layout_styles( $base_style_node, $types );
if ( in_array( 'presets', $types, true ) ) {
$stylesheet .= $this->get_preset_classes( $setting_nodes, $origins );
* Processes the CSS, to apply nesting.
* @since 6.6.0 Enforced 0-1-0 specificity for block custom CSS selectors.
* @param string $css The CSS to process.
* @param string $selector The selector to nest.
* @return string The processed CSS.
protected function process_blocks_custom_css( $css, $selector ) {
// Split CSS nested rules.
$parts = explode( '&', $css );
foreach ( $parts as $part ) {
$is_root_css = ( ! str_contains( $part, '{' ) );
// If the part doesn't contain braces, it applies to the root level.
$processed_css .= ':root :where(' . trim( $selector ) . '){' . trim( $part ) . '}';
// If the part contains braces, it's a nested CSS rule.
$part = explode( '{', str_replace( '}', '', $part ) );
if ( count( $part ) !== 2 ) {
$nested_selector = $part[0];
* Handle pseudo elements such as ::before, ::after etc. Regex will also
* capture any leading combinator such as >, +, or ~, as well as spaces.
* This allows pseudo elements as descendants e.g. `.parent ::before`.
$has_pseudo_element = preg_match( '/([>+~\s]*::[a-zA-Z-]+)/', $nested_selector, $matches );
$pseudo_part = $has_pseudo_element ? $matches[1] : '';
$nested_selector = $has_pseudo_element ? str_replace( $pseudo_part, '', $nested_selector ) : $nested_selector;
// Finalize selector and re-append pseudo element if required.
$part_selector = str_starts_with( $nested_selector, ' ' )
? static::scope_selector( $selector, $nested_selector )
: static::append_to_selector( $selector, $nested_selector );
$final_selector = ":root :where($part_selector)$pseudo_part";
$processed_css .= $final_selector . '{' . trim( $css_value ) . '}';
* Returns the global styles custom CSS.
* @return string The global styles custom CSS.
public function get_custom_css() {
// Add the global styles root CSS.
$stylesheet = isset( $this->theme_json['styles']['css'] ) ? $this->theme_json['styles']['css'] : '';
// Add the global styles block CSS.
if ( isset( $this->theme_json['styles']['blocks'] ) ) {
foreach ( $this->theme_json['styles']['blocks'] as $name => $node ) {