: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
* The valid properties under the styles key.
* @since 5.8.0 As `ALLOWED_STYLES`.
* @since 5.9.0 Renamed from `ALLOWED_STYLES` to `VALID_STYLES`,
* added new properties for `border`, `filter`, `spacing`,
* @since 6.1.0 Added new side properties for `border`,
* added new property `shadow`,
* updated `blockGap` to be allowed at any level.
* @since 6.2.0 Added `outline`, and `minHeight` properties.
* @since 6.3.0 Added support for `typography.textColumns`.
* @since 6.5.0 Added support for `dimensions.aspectRatio`.
* @since 6.6.0 Added `background` sub properties to top-level only.
const VALID_STYLES = array(
'backgroundImage' => 'top',
'backgroundPosition' => 'top',
'backgroundRepeat' => 'top',
'backgroundSize' => 'top',
'textDecoration' => null,
* Defines which pseudo selectors are enabled for which elements.
* The order of the selectors should be: link, any-link, visited, hover, focus, active.
* This is to ensure the user action (hover, focus and active) styles have a higher
* specificity than the visited styles, which in turn have a higher specificity than
* See https://core.trac.wordpress.org/ticket/56928.
* Note: this will affect both top-level and block-level elements.
* @since 6.2.0 Added support for ':link' and ':any-link'.
const VALID_ELEMENT_PSEUDO_SELECTORS = array(
'link' => array( ':link', ':any-link', ':visited', ':hover', ':focus', ':active' ),
'button' => array( ':link', ':any-link', ':visited', ':hover', ':focus', ':active' ),
* The valid elements that can be found under styles.
* @since 6.1.0 Added `heading`, `button`, and `caption` elements.
'link' => 'a:where(:not(.wp-element-button))', // The `where` is needed to lower the specificity.
'heading' => 'h1, h2, h3, h4, h5, h6',
// We have the .wp-block-button__link class so that this will target older buttons that have been serialized.
'button' => '.wp-element-button, .wp-block-button__link',
// The block classes are necessary to target older content that won't use the new class names.
'caption' => '.wp-element-caption, .wp-block-audio figcaption, .wp-block-embed figcaption, .wp-block-gallery figcaption, .wp-block-image figcaption, .wp-block-table figcaption, .wp-block-video figcaption',
const __EXPERIMENTAL_ELEMENT_CLASS_NAMES = array(
'button' => 'wp-element-button',
'caption' => 'wp-element-caption',
* List of block support features that can have their related styles
* generated under their own feature level selector rather than the block's.
const BLOCK_SUPPORT_FEATURE_LEVEL_SELECTORS = array(
'__experimentalBorder' => 'border',
'typography' => 'typography',
* Return the input schema at the root and per origin.
* @param array $schema The base schema.
* @return array The schema at the root and per origin.
* schema_in_root_and_per_origin(
protected static function schema_in_root_and_per_origin( $schema ) {
$schema_in_root_and_per_origin = $schema;
foreach ( static::VALID_ORIGINS as $origin ) {
$schema_in_root_and_per_origin[ $origin ] = $schema;
return $schema_in_root_and_per_origin;
* Returns a class name by an element name.
* @param string $element The name of the element.
* @return string The name of the class.
public static function get_element_class_name( $element ) {
if ( isset( static::__EXPERIMENTAL_ELEMENT_CLASS_NAMES[ $element ] ) ) {
$class_name = static::__EXPERIMENTAL_ELEMENT_CLASS_NAMES[ $element ];
* Options that settings.appearanceTools enables.
* @since 6.2.0 Added `dimensions.minHeight` and `position.sticky`.
* @since 6.4.0 Added `background.backgroundImage`.
* @since 6.5.0 Added `background.backgroundSize` and `dimensions.aspectRatio`.
const APPEARANCE_TOOLS_OPT_INS = array(
array( 'background', 'backgroundImage' ),
array( 'background', 'backgroundSize' ),
array( 'border', 'color' ),
array( 'border', 'radius' ),
array( 'border', 'style' ),
array( 'border', 'width' ),
array( 'color', 'link' ),
array( 'color', 'heading' ),
array( 'color', 'button' ),
array( 'color', 'caption' ),
array( 'dimensions', 'aspectRatio' ),
array( 'dimensions', 'minHeight' ),
array( 'position', 'sticky' ),
array( 'spacing', 'blockGap' ),
array( 'spacing', 'margin' ),
array( 'spacing', 'padding' ),
array( 'typography', 'lineHeight' ),
* The latest version of the schema in use.
* @since 5.9.0 Changed value from 1 to 2.
* @since 6.6.0 Changed value from 2 to 3.
* @since 6.6.0 Key spacingScale by origin, and Pre-generate the spacingSizes from spacingScale.
* Added unwrapping of shared block style variations into block type variations if registered.
* @param array $theme_json A structure that follows the theme.json schema.
* @param string $origin Optional. What source of data this object represents.
* One of 'blocks', 'default', 'theme', or 'custom'. Default 'theme'.
public function __construct( $theme_json = array( 'version' => self::LATEST_SCHEMA ), $origin = 'theme' ) {
if ( ! in_array( $origin, static::VALID_ORIGINS, true ) ) {
$this->theme_json = WP_Theme_JSON_Schema::migrate( $theme_json, $origin );
$valid_block_names = array_keys( static::get_blocks_metadata() );
$valid_element_names = array_keys( static::ELEMENTS );
$valid_variations = static::get_valid_block_style_variations();
$this->theme_json = static::unwrap_shared_block_style_variations( $this->theme_json, $valid_variations );
$this->theme_json = static::sanitize( $this->theme_json, $valid_block_names, $valid_element_names, $valid_variations );
$this->theme_json = static::maybe_opt_in_into_settings( $this->theme_json );
// Internally, presets are keyed by origin.
$nodes = static::get_setting_nodes( $this->theme_json );
foreach ( $nodes as $node ) {
foreach ( static::PRESETS_METADATA as $preset_metadata ) {
foreach ( $preset_metadata['path'] as $subpath ) {
$preset = _wp_array_get( $this->theme_json, $path, null );
if ( null !== $preset ) {
// If the preset is not already keyed by origin.
if ( isset( $preset[0] ) || empty( $preset ) ) {
_wp_array_set( $this->theme_json, $path, array( $origin => $preset ) );
// In addition to presets, spacingScale (which generates presets) is also keyed by origin.
$scale_path = array( 'settings', 'spacing', 'spacingScale' );
$spacing_scale = _wp_array_get( $this->theme_json, $scale_path, null );
if ( null !== $spacing_scale ) {
// If the spacingScale is not already keyed by origin.
if ( empty( array_intersect( array_keys( $spacing_scale ), static::VALID_ORIGINS ) ) ) {
_wp_array_set( $this->theme_json, $scale_path, array( $origin => $spacing_scale ) );
// Pre-generate the spacingSizes from spacingScale.
$scale_path = array( 'settings', 'spacing', 'spacingScale', $origin );
$spacing_scale = _wp_array_get( $this->theme_json, $scale_path, null );
if ( isset( $spacing_scale ) ) {
$sizes_path = array( 'settings', 'spacing', 'spacingSizes', $origin );
$spacing_sizes = _wp_array_get( $this->theme_json, $sizes_path, array() );
$spacing_scale_sizes = static::compute_spacing_sizes( $spacing_scale );
$merged_spacing_sizes = static::merge_spacing_sizes( $spacing_scale_sizes, $spacing_sizes );
_wp_array_set( $this->theme_json, $sizes_path, $merged_spacing_sizes );
* Unwraps shared block style variations.
* It takes the shared variations (styles.variations.variationName) and
* applies them to all the blocks that have the given variation registered
* (styles.blocks.blockType.variations.variationName).
* For example, given the `core/paragraph` and `core/group` blocks have
* registered the `section-a` style variation, and given the following input:
* "section-a": { "color": { "background": "backgroundColor" } }
* It returns the following output:
* "section-a": { "color": { "background": "backgroundColor" } }
* "section-a": { "color": { "background": "backgroundColor" } }
* @param array $theme_json A structure that follows the theme.json schema.
* @param array $valid_variations Valid block style variations.
* @return array Theme json data with shared variation definitions unwrapped under appropriate block types.
private static function unwrap_shared_block_style_variations( $theme_json, $valid_variations ) {
if ( empty( $theme_json['styles']['variations'] ) || empty( $valid_variations ) ) {
$new_theme_json = $theme_json;
$variations = $new_theme_json['styles']['variations'];
foreach ( $valid_variations as $block_type => $registered_variations ) {
foreach ( $registered_variations as $variation_name ) {
$block_level_data = $new_theme_json['styles']['blocks'][ $block_type ]['variations'][ $variation_name ] ?? array();
$top_level_data = $variations[ $variation_name ] ?? array();
$merged_data = array_replace_recursive( $top_level_data, $block_level_data );
if ( ! empty( $merged_data ) ) {
_wp_array_set( $new_theme_json, array( 'styles', 'blocks', $block_type, 'variations', $variation_name ), $merged_data );
unset( $new_theme_json['styles']['variations'] );
* Enables some opt-in settings if theme declared support.
* @param array $theme_json A theme.json structure to modify.
* @return array The modified theme.json structure.
protected static function maybe_opt_in_into_settings( $theme_json ) {
$new_theme_json = $theme_json;
isset( $new_theme_json['settings']['appearanceTools'] ) &&
true === $new_theme_json['settings']['appearanceTools']
static::do_opt_in_into_settings( $new_theme_json['settings'] );
if ( isset( $new_theme_json['settings']['blocks'] ) && is_array( $new_theme_json['settings']['blocks'] ) ) {
foreach ( $new_theme_json['settings']['blocks'] as &$block ) {
if ( isset( $block['appearanceTools'] ) && ( true === $block['appearanceTools'] ) ) {
static::do_opt_in_into_settings( $block );
* @param array $context The context to which the settings belong.
protected static function do_opt_in_into_settings( &$context ) {
foreach ( static::APPEARANCE_TOOLS_OPT_INS as $path ) {
* Use "unset prop" as a marker instead of "null" because
* "null" can be a valid value for some props (e.g. blockGap).
if ( 'unset prop' === _wp_array_get( $context, $path, 'unset prop' ) ) {
_wp_array_set( $context, $path, true );
unset( $context['appearanceTools'] );
* Sanitizes the input according to the schemas.
* @since 5.9.0 Added the `$valid_block_names` and `$valid_element_name` parameters.
* @since 6.3.0 Added the `$valid_variations` parameter.
* @since 6.6.0 Updated schema to allow extended block style variations.
* @param array $input Structure to sanitize.
* @param array $valid_block_names List of valid block names.
* @param array $valid_element_names List of valid element names.
* @param array $valid_variations List of valid variations per block.
* @return array The sanitized output.
protected static function sanitize( $input, $valid_block_names, $valid_element_names, $valid_variations ) {
if ( ! is_array( $input ) ) {
// Preserve only the top most level keys.
$output = array_intersect_key( $input, array_flip( static::VALID_TOP_LEVEL_KEYS ) );
* Remove any rules that are annotated as "top" in VALID_STYLES constant.
* Some styles are only meant to be available at the top-level (e.g.: blockGap),
* hence, the schema for blocks & elements should not have them.
$styles_non_top_level = static::VALID_STYLES;
foreach ( array_keys( $styles_non_top_level ) as $section ) {
// array_key_exists() needs to be used instead of isset() because the value can be null.
if ( array_key_exists( $section, $styles_non_top_level ) && is_array( $styles_non_top_level[ $section ] ) ) {
foreach ( array_keys( $styles_non_top_level[ $section ] ) as $prop ) {
if ( 'top' === $styles_non_top_level[ $section ][ $prop ] ) {
unset( $styles_non_top_level[ $section ][ $prop ] );
// Build the schema based on valid block & element names.
$schema_styles_elements = array();
* Set allowed element pseudo selectors based on per element allow list.
* Target data structure in schema:
* - top level elements: `$schema['styles']['elements']['link'][':hover']`.
* - block level elements: `$schema['styles']['blocks']['core/button']['elements']['link'][':hover']`.
foreach ( $valid_element_names as $element ) {
$schema_styles_elements[ $element ] = $styles_non_top_level;
if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] ) ) {
foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) {
$schema_styles_elements[ $element ][ $pseudo_selector ] = $styles_non_top_level;
$schema_styles_blocks = array();
$schema_settings_blocks = array();
* Generate a schema for blocks.
* - Block styles can contain `elements` & `variations` definitions.
* - Variations definitions cannot be nested.
* - Variations can contain styles for inner `blocks`.
* - Variation inner `blocks` styles can contain `elements`.
* As each variation needs a `blocks` schema but further nested
* inner `blocks`, the overall schema will be generated in multiple passes.
foreach ( $valid_block_names as $block ) {
$schema_settings_blocks[ $block ] = static::VALID_SETTINGS;