Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions includes/create-theme/resolver_additions.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,16 @@ public static function export_theme_data( $content, $extra_theme_data = null ) {

$data = $theme->get_data();

if ( 'current' === $content && $current_theme->parent() ) {
$parent_file = static::get_file_path_from_theme( 'theme.json', true );
if ( $parent_file ) {
$parent_data = static::read_json_file( $parent_file );
if ( is_array( $parent_data ) ) {
$data = static::merge_parent_preset_arrays( $data, $parent_data );
}
}
}

// Add the schema.
if ( empty( $schema ) ) {
global $wp_version;
Expand All @@ -102,6 +112,119 @@ public static function export_theme_data( $content, $extra_theme_data = null ) {
return static::stringify( $data );
}

/**
* Merge parent theme preset arrays into child theme data.
*
* For child themes, WordPress Core replaces indexed preset arrays (like color palettes,
* font sizes, gradients) entirely rather than merging them. This method traverses
* child data and backfills parent preset entries for any preset array present in child data,
* with child entries overriding parent entries that share the same slug.
*
* @param array $child_data Child theme data.
* @param array $parent_data Parent theme data.
* @return array Merged theme data.
*/
public static function merge_parent_preset_arrays( array $child_data, array $parent_data ) {
if ( empty( $child_data ) || empty( $parent_data ) ) {
return $child_data;
}

$result = $child_data;

foreach ( $child_data as $key => $child_value ) {
// Non-arrays (scalars, booleans, null) are already preserved in $result.
if ( ! is_array( $child_value ) ) {
continue;
}

// Associative objects (settings, color, blocks, etc.): recurse.
if ( ! static::is_list( $child_value ) ) {
$parent_subdata = ( isset( $parent_data[ $key ] ) && is_array( $parent_data[ $key ] ) ) ? $parent_data[ $key ] : array();
$result[ $key ] = static::merge_parent_preset_arrays( $child_value, $parent_subdata );
continue;
}

// Slug-keyed preset lists: backfill parent entries if parent defines this preset.
if ( static::is_preset_list( $child_value ) && isset( $parent_data[ $key ] ) && static::is_preset_list( $parent_data[ $key ] ) ) {
$result[ $key ] = static::merge_preset_list( $child_value, $parent_data[ $key ] );
}
}

return $result;
}

/**
* Check whether a value is a slug-keyed preset list (e.g. palette, fontSizes).
*
* @param mixed $value Value to check.
* @return bool Whether the value is a preset list.
*/
private static function is_preset_list( $value ) {
return is_array( $value )
&& static::is_list( $value )
&& ! empty( $value )
&& is_array( $value[0] )
&& isset( $value[0]['slug'] );
}

/**
* Merge parent and child preset lists keyed by slug.
*
* Preserves parent item order with child overrides replacing parent items
* at their original positions, and new child entries appended.
*
* @param array $child_list Child preset list.
* @param array $parent_list Parent preset list.
* @return array Merged preset list.
*/
private static function merge_preset_list( array $child_list, array $parent_list ) {
$merged = array();
$slug_to_index = array();

foreach ( $parent_list as $item ) {
if ( is_array( $item ) && isset( $item['slug'] ) ) {
$slug_to_index[ $item['slug'] ] = count( $merged );
}
$merged[] = $item;
}

foreach ( $child_list as $item ) {
if ( is_array( $item ) && isset( $item['slug'] ) && isset( $slug_to_index[ $item['slug'] ] ) ) {
$merged[ $slug_to_index[ $item['slug'] ] ] = $item;
} else {
if ( is_array( $item ) && isset( $item['slug'] ) ) {
$slug_to_index[ $item['slug'] ] = count( $merged );
}
$merged[] = $item;
}
}

return $merged;
}

/**
* Detect whether an array is a list (sequential integer keys starting at 0).
* Mirrors PHP 8.1+ `array_is_list()`.
*
* @param array $arr Array to check.
* @return bool Whether the array is a list.
*/
private static function is_list( array $arr ) {
if ( function_exists( 'array_is_list' ) ) {
return array_is_list( $arr );
}
if ( array() === $arr ) {
return true;
}
$expected = 0;
foreach ( $arr as $key => $_v ) {
if ( $key !== $expected++ ) {
return false;
}
}
return true;
}

/**
* Get the user data.
*
Expand Down
120 changes: 120 additions & 0 deletions tests/test-resolver-preset-merge.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<?php
/**
* @package Create_Block_Theme
*/
class Test_Resolver_Preset_Merge extends WP_UnitTestCase {

public function set_up() {
parent::set_up();
if ( ! class_exists( 'CBT_Theme_JSON_Resolver' ) && function_exists( 'cbt_augment_resolver_with_utilities' ) ) {
cbt_augment_resolver_with_utilities();
}
}

public function test_merge_appends_child_presets_and_overrides() {
$parent = array(
'settings' => array(
'color' => array(
'palette' => array(
array(
'slug' => 'base',
'color' => '#ffffff',
'name' => 'Base',
),
array(
'slug' => 'contrast',
'color' => '#000000',
'name' => 'Contrast',
),
),
),
),
);

$child = array(
'settings' => array(
'color' => array(
'palette' => array(
array(
'slug' => 'base',
'color' => '#f0f0f0',
'name' => 'Custom Base',
),
array(
'slug' => 'accent',
'color' => '#0073aa',
'name' => 'Accent',
),
),
),
),
);

$result = CBT_Theme_JSON_Resolver::merge_parent_preset_arrays( $child, $parent );
$palette = $result['settings']['color']['palette'];

$this->assertCount( 3, $palette );
$this->assertSame( array( 'base', 'contrast', 'accent' ), array_column( $palette, 'slug' ) );
$this->assertSame( '#f0f0f0', $palette[0]['color'] );
$this->assertSame( 'Custom Base', $palette[0]['name'] );
$this->assertSame( '#000000', $palette[1]['color'] );
$this->assertSame( '#0073aa', $palette[2]['color'] );
}

public function test_merge_empty_child_preset_clears() {
$parent = array(
'settings' => array(
'color' => array(
'palette' => array(
array(
'slug' => 'base',
'color' => '#ffffff',
'name' => 'Base',
),
),
),
),
);

$child = array(
'settings' => array(
'color' => array(
'palette' => array(),
),
),
);

$result = CBT_Theme_JSON_Resolver::merge_parent_preset_arrays( $child, $parent );
$this->assertSame( array(), $result['settings']['color']['palette'] );
}

public function test_merge_preserves_non_preset_deltas() {
$parent = array(
'settings' => array(
'layout' => array(
'contentSize' => '645px',
'wideSize' => '1200px',
),
'spacing' => array(
'units' => array( '%', 'px', 'em' ),
),
),
);

$child = array(
'settings' => array(
'layout' => array(
'contentSize' => '800px',
),
'spacing' => array(
'units' => array( '%', 'rem' ),
),
),
);

$result = CBT_Theme_JSON_Resolver::merge_parent_preset_arrays( $child, $parent );

$this->assertSame( array( 'contentSize' => '800px' ), $result['settings']['layout'] );
$this->assertSame( array( '%', 'rem' ), $result['settings']['spacing']['units'] );
}
}
Loading